? ? ? ? 各位看官,大家好!今天我們將探討C++中的三大特性之一:繼承。繼承是一種面向對象編程的重要概念,它允許我們通過創建新的類,從而復用和擴展現有類的功能。通過繼承,我們不僅能夠提高代碼的可重用性和可維護性,還能更好地體現現實世界中事物的層次結構。希望大家通過今天的學習,能夠深入理解繼承的核心原理,并能在實際編程中靈活應用這一強大的工具。
目錄
一、繼承的概念及定義
1.1繼承的概念
1.2 繼承定義
1.2.1定義格式
1.2.2繼承關系和訪問限定符
1.2.3繼承基類成員訪問方式的變化
二、基類和派生類對象賦值轉換
2.1? 子類可以賦值給父類
2.2 父類不可以賦值給子類?
2.3??父類賦值給子類的特殊情況
三、繼承中的作用域
3.1 隱藏的概念
3.2 如何解決呢?
3.3? 注意事項
四、派生類的默認成員函數
4.1 構造函數
?4.2 拷貝構造函數
4.3 賦值重載?
4.4 析構函數
4.5 總結
4.6 練習
五、繼承與友元
六、繼承與靜態成員
七、復雜的菱形繼承及菱形虛擬繼承
7.1 繼承的分類及概念
7.2 菱形繼承存在的問題
7.3?虛擬繼承
7.4?虛擬繼承解決數據冗余和二義性的原理
八、繼承的總結和反思
8.1理解
8.2. 繼承和組合
九、筆試面試題
9.1 C++的缺陷是什么
9.2??什么是菱形繼承?菱形繼承的問題是什么?
一、繼承的概念及定義
1.1繼承的概念
? ? ? ? 繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象 程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,繼 承是類設計層次的復用。
class Person
{public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter"; // 姓名int _age = 18; // 年齡
};class Student : public Person //學生類繼承自Person類
{protected:int _stuid; // 學號 (新增的屬于自己類成員變量)
};class Teacher : public Person //老師類繼承自Person類
{protected:int _jobid; // 工號 (新增的屬于自己類成員變量)
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
繼承帶來的作用:
? ? ? ?子類/派生類會具有父類/基類的成員變量和成員函數,當然也可以有屬于自己類的成員變量和函數。
1.2 繼承定義
1.2.1定義格式
? ? ? ?下面我們看到Person是父類,也稱作基類。Student是子類,也稱作派生類
1.2.2繼承關系和訪問限定符
1.2.3繼承基類成員訪問方式的變化
總結:
- 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私 有成員還是被繼承到了派生類對象中(內存空間會有這個成員變量),但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在 派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。protected訪問限定符和private訪問限定符在當前類中沒有區別,他們是一樣的,類外都不能訪問,區別在于繼承的派生類,private成員無論什么繼承方式,在派生類中都不能訪問,但是protected就不一樣了!
- 實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- 使用關鍵字class聲明的類(指的是派生類)時默認的繼承方式(派生類不寫繼承方式)是private,使用struct聲明的類(指的是派生類)默認的繼承方式(派生類不寫繼承方式)是public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡 使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
// 實例演示三種繼承關系下基類成員的各類型成員訪問關系的變化
class Person
{public :void Print (){cout<<_name <<endl;}protected :string _name ; // 姓名private :int _age ; // 年齡
};//class Student : protected Person
//class Student : private Person
class Student : public Person
{protected :int _stunum ; // 學號
};
二、基類和派生類對象賦值轉換
2.1? 子類可以賦值給父類
?1、派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片 或者切割。寓意把派生類中父類那部分切來賦值過去。
2.2 父類不可以賦值給子類?
2、基類對象不能賦值給派生類對象。
2.3??父類賦值給子類的特殊情況
3、基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態類型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 來進行識別后進行安全轉換。(ps:這個我們后 面再講解,這里先了解一下)
class Person
{protected :string _name; // 姓名string _sex; // 性別int _age; // 年齡
};class Student : public Person
{public :int _No ; // 學號
};void Test ()
{Person p;Student s ;子類和父類之間的賦值兼容規則:// 1.子類對象可以賦值給父類的對象/的指針/的引用,叫做切片Person p = s ;Person* ptr = &s;Person& rp = s;//2.父類對象不能賦值給子類對象(父給子是不可以的!反過來是不可以的!)s = p; 堅決不可以!// 3.基類的指針可以通過強制類型轉換賦值給派生類的指針ptr = &sStudent* ps1 = (Student*)ptr;//這種情況轉換時可以的,因為這個父類的指針有時是指向子類對象的ps1->_No = 10;ptr = &p;Student* ps2 = (Student*)ptr; //這種情況轉換時雖然可以,但是會存在越界訪問的問
題ps2->_No = 10;
}
三、繼承中的作用域
3.1 隱藏的概念
? ? ? ?在繼承體系中基類和派生類都有獨立的作用域。當父類和子類同時有同名成員變量或者成員函數時,子類就會隱藏父類的同名成員變量或者成員函數。子類和父類中有同名成員,子類成員將屏蔽對父類同名成員(成員變量或者成員函數)的直接訪問,這種情況叫隱藏, 也叫重定義。
3.2 如何解決呢?
? ? (如何解決呢?在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
3.3? 注意事項
注意在實際中在繼承體系里面最好不要定義同名的成員變量或者同名的成員函數。
// Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆
class Person
{protected :string _name = "小李子"; // 姓名int _num = 111; // 身份證號
};class Student : public Person
{public:void Print(){cout<<" 姓名:"<<_name<< endl;cout<<" 身份證號:"<<Person::_num<< endl; //必須顯示的指定基類才可使用,否則它用的是派生類自己的成員變量cout<<" 學號:"<<_num<<endl;}protected:int _num = 999; // 學號
};void Test()
{Student s1;s1.Print();
};
B中的fun和A中的fun不是構成重載,因為不是在同一作用域!!!
B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。class A
{public:void fun(){cout << "func()" << endl;}
};class B : public A
{public:void fun(int i){A::fun();cout << "func(int i)->" <<i<<endl;}
};void Test()
{B b;b.fun(10);b.A::fun(); //必須要顯示的指定基類,才可以調用父類的這個同名的隱藏函數
};
四、派生類的默認成員函數
? ? ? 6個默認成員函數,“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數是如何生成的呢?
4.1 構造函數
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :protected :int _num ; //學號
};int main()
{Student s;return 0;
}
基本原則:
? ? ? ? 派生類的初始化和析構會分別自動調用基類的構造函數初始化基類的那一部分成員和自動調用析構函數,然后還會調用自己的構造函數和析構函數。也就是說他把父類和基類分的很清楚!
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): _name(name) 這里編譯器不允許這樣初始化!不寫這個,他會編譯通過,因為他會自動調用父類的構造函數初始化, _num(num){cout<<"Student()" <<endl;}protected :int _num ; //學號
};int main()
{Student s("peter",1);return 0;
}
? ? ? ?派生類繼承父類,對于父類那一部分,調用父類的構造函數進行初始化!?不可以在初始化列表中以初始化自己的成員變量的方式進行初始化!如果我們不對父類的成員變量進行初始化,他會自動的調用父類的構造函數進行初始化,如果想要在派生類中顯示的初始化這個父類的成員變量,就必須以父類的構造函數的方式顯示初始化,如下所示:
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name) 顯示調用父類構造函數初始化!, _num(num){cout<<"Student()" <<endl;}protected :int _num ; //學號
};int main()
{Student s("peter",1);return 0;
}
? 總結1:?
? ? ? ? 派生類的構造函數包含兩個部分:第一部分是父類繼承的,不能自己去初始化父類繼承的那一部分,必須要調用父類的構造函數進行初始化(或者你不調,他會去調用父類默認的那個構造函數:編譯器默認生成的構造函數、全缺省的構造函數、無參的構造函數,進行初始化)。第二部分是派生類自己的成員變量和之前普通的類沒有什么區別。
?4.2 拷貝構造函數
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}protected :int _num ; //學號
};int main()
{Student s1("jack", 18);Student s2(s1); //拷貝構造}
? ? ? 派生類不實現自己的拷貝構造函數,編譯器會自動生成一個拷貝構造函數,進行拷貝,對于父類的那一部分,他會自動的調用父類的拷貝構造函數。
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s)//: _name(s._name) 這里是不可以的!: Person(s) 這樣做!把子類對象給父類的引用,切片!!!, _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}protected :int _num ; //學號
};int main()
{Student s1("jack", 18);Student s2(s1); //拷貝構造}
? 總結2:?
? ? ? ? 派生類的拷貝構造函數也同樣包含兩個部分:第一部分是父類繼承的,不能自己去拷貝父類繼承的那一部分,必須要調用父類的拷貝構造函數進行拷貝,第二部分是派生類自己的成員變量和之前普通的類沒有什么區別,這一部分也會調用自己的拷貝構造函數進行拷貝。
4.3 賦值重載?
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s): Person(s) , _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}Student& operator = (const Student& s ){cout<<"Student& operator= (const Student& s)"<< endl;if (this != &s){operator =(s); 這樣顯示調用賦值重載(this->operator=(s);),這里也有切片_num = s ._num;cout<<"Student& operator= (const Student& s)"<< endl;}return *this ;} protected :int _num ; //學號
};int main()
{Student s1("jack", 18);Student s2(s1); //拷貝構造Student s3("rose", 20);s1 = s3;}
為什么會發生棧溢出??
? ? ? ? 因為派生類調用operator=與基類的operator=構成隱藏了(同名函數),子類和父類中有同名成員函數,那么子類成員函數將屏蔽對父類同名成員函數的直接訪問!!!也就是說,沒辦法調用基類的operator=。解決辦法:指定基類!
class Person
{public :Person(const char* name = "peter"): _name(name ){cout<<"Person()" <<endl;}Person(const Person& p): _name(p._name){cout<<"Person(const Person& p)" <<endl;}Person& operator=(const Person& p ){cout<<"Person& operator=(const Person& p)"<< endl;if (this != &p)_name = p ._name;return *this ;}~Person(){cout<<"~Person()" <<endl;}protected :string _name ; // 姓名
};class Student : public Person
{public :Student(const char* name, int num): Person(name ), _num(num ){cout<<"Student()" <<endl;}Student(const Student& s): Person(s) , _num(s ._num){cout<<"Student(const Student& s)" <<endl ;}Student& operator = (const Student& s ){if (this != &s){Person::operator =(s); 指定基類的,并顯示的調用基類的賦值重載_num = s ._num;cout<<"Student& operator= (const Student& s)"<< endl;}return *this ;} protected :int _num ; //學號
};int main()
{Student s1("jack", 18);Student s2(s1); //拷貝構造Student s3("rose", 20);s1 = s3;}
4.4 析構函數
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person& operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){if (this != &s){Person::operator =(s); //指定基類的,并顯示的調用基類的賦值重載_num = s._num;cout << "Student& operator= (const Student& s)" << endl;}return *this;}~Student() //子類的析構函數和父類的析構函數構成隱藏!!!因為他們的名字會被編譯器統一處理成: destructor(跟多態相關){//~Person(); //不能這樣直接調用基類的析構函數,因為它們構成隱藏,基類無法訪問父類的析構函數,解決辦法:指定基類Person::~Person();cout << "~Student()" << endl;}protected:int _num; //學號
};int main()
{Student s1("jack", 18);return 0;}
第一個Person析構應該去掉!基類的析構函數不需要我們顯示的去調用,他會在派生類析構函數調用后,自動的去調用基類的析構函數!!修改如下:
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person& operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){if (this != &s){Person::operator =(s); //指定基類的,并顯示的調用基類的賦值重載_num = s._num;cout << "Student& operator= (const Student& s)" << endl;}return *this;}~Student() //子類的析構函數和父類的析構函數構成隱藏!!!因為他們的名字會被編譯器統一處理成: destructor(跟多態相關){//Person::~Person();cout << "~Student()" << endl;}protected:int _num; //學號
};int main()
{Student s1("jack", 18);return 0;}
4.5 總結
4.6 練習
?請設計一個類,不能被繼承
? ? ? ? 只需要將父類的構造函數的訪問限定符設置成私有的,這樣子類無論以什么方式繼承,父類的構造函數在子類中都不可見,那么我們在創建子類對象時,它必須首先去調用父類的構造函數進行初始化,但是,發現父類的構造函數此時不可見,那他就不能調用了,那么子類對象就創建失敗了!也就是說,這個父類不能被繼承。
class A
{
private:A(){}
};class B:public A
{B(){}
};
五、繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員
class Person
{public:friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}class Student : public Person
{protected:int _stuNum; // 學號
};int main()
{Person p;Student s;Display(p, s);return 0;
}
六、繼承與靜態成員
? ? ? 基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子 類,都只有一個static成員實例 。
class Person
{public:Person () {++ _count;}protected :string _name ; // 姓名public :static int _count; // 統計人的個數。
};int Person :: _count = 0;class Student : public Person
{protected :int _stuNum ; // 學號
};class Graduate : public Student
{protected :string _seminarCourse ; // 研究科目
};void main()
{Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人數 :"<< Person ::_count << endl;Student ::_count = 0;cout <<" 人數 :"<< Person ::_count << endl;
}
七、復雜的菱形繼承及菱形虛擬繼承
7.1 繼承的分類及概念
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況。
7.2 菱形繼承存在的問題
? ? ? ?菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。 在Assistant的對象中Person成員會有兩份。
class Person
{public :string _name ; // 姓名
};class Student : public Person
{protected :int _num ; //學號
};class Teacher : public Person
{protected :int _id ; // 職工編號
};class Assistant : public Student, public Teacher
{protected :string _majorCourse ; // 主修課程
};void Test ()
{// 這樣會有二義性無法明確知道訪問的是哪一個Assistant a ;a._name = "peter";
// 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數據冗余問題無法解決a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
7.3?虛擬繼承
? ? ? 虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和 Teacher的繼承Person時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
class Person
{public :string _name ; // 姓名
};class Student : virtual public Person
{protected :int _num ; //學號
};class Teacher : virtual public Person
{protected :int _id ; // 職工編號
};class Assistant : public Student, public Teacher
{protected :string _majorCourse ; // 主修課程
};void Test ()
{Assistant a ;a._name = "peter";
}
7.4?虛擬繼承解決數據冗余和二義性的原理
正常的菱形繼承:
class A
{public:int _a;
};class B : public A
{public:int _b;
};class C : public A
{public:int _c;
};class D : public B, public C
{public:int _d;
};int main()
{D d;cout<<sizeof(d)<<endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
?
下圖是菱形繼承的內存對象成員模型:這里可以看到數據冗余
虛擬菱形繼承:
class A
{public:int _a;
};class B : virtual public A
{public:int _b;
};class C : virtual public A
{public:int _c;
};class D : public B, public C
{public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
? ? ? ?為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成 員的模型。
下圖是菱形虛擬繼承的內存對象成員模型:這里可以分析出D對象中將A放到的了對象組成的最下 面,這個A同時屬于B和C,那么B和C如何去找到公共的A呢?這里是通過了B和C的兩個指針,指 向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量 可以找到下面的A。
有童鞋會有疑問為什么D中B和C部分要去找屬于自己的A?那么大家看看當下面的賦值發生時,d是
不是要去找出B/C成員中的A才能賦值過去?D d;
B b = d;
C c = d;
下面是上面的Person關系菱形虛擬繼承的原理解釋:
八、繼承的總結和反思
8.1理解
? ? ? ? 很多人說C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱 形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設 計出菱形繼承。否則在復雜度及性能上都有問題。 多繼承可以認為是C++的缺陷之一,很多后來的OO語言都沒有多繼承,如Java。
8.2. 繼承和組合
九、筆試面試題
9.1 C++的缺陷是什么
? ? ? ?多繼承就是C++的一個問題,多繼承中的菱形繼承存在數據冗余和二義性的問題,解決它的方法是虛擬繼承,它的底層結構的對象模型非常復雜,且有一定的效率損失。
9.2??什么是菱形繼承?菱形繼承的問題是什么?
? ? ? ? ?菱形繼承(diamond inheritance)是C++中多重繼承的一種特殊情況,其繼承結構形成一個菱形,因此得名。這種繼承方式通常涉及一個基類、兩個從這個基類繼承的中間類以及一個從這兩個中間類繼承的派生類。
菱形繼承的問題
重復繼承(重復基類): 當
D
繼承自B
和C
時,由于B
和C
都繼承自A
,導致D
將包含兩份A
的成員,這會造成數據冗余和不一致性問題。二義性(Ambiguity): 如果在類
D
中調用基類A
的成員,例如函數或變量,由于D
包含兩份A
的成員,編譯器無法確定應該調用哪一份,會導致二義性錯誤。
至此,這一講內容介紹完畢,內容簡單,星光不問趕路人,加油吧,感謝閱讀,如果對此專欄感興趣,點贊加關注!