ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++继承详解(下):默认成员函数、多继承、菱形继承、虚继承与组合

C++继承详解(下):默认成员函数、多继承、菱形继承、虚继承与组合 前言先看代码class Person { public: string _name; }; class Student : public Person { public: int _num; };以后看到这种继承牢记一个 Student 对象 一个 Person 基类子对象 Student 自己新增的部分。父类构造拷贝与赋值重载默认class Person { public: Person(const char* name) : _name(name) {} protected: string _name; }; class Student : public Person { public: Student(const char* name, int num) : Person(name), _num(num) {} protected: int _num; };如上父类的构造无论是拷贝还是默认构造都需要子类显示调用出来如果子类不显示调用默认会在构造子类时优先构造父类但是如果父类的默认构造不存在此时就会报错所以最好显示调用。拷贝Student s1(张三, 100); Student s2(s1);这种s2拷贝s1也是分工合作各司其职所以在Student中也必须显示调用Student(const Student s) : Person(s), _num(s._num) {}这里的Person(s),实际上就是“拿s中的 Person 那部分调用 Person 的拷贝构造。”完整的例子:#include iostream #include string using namespace std; class Person { public: Person(const string name ) : _name(name) {} Person(const Person p) : _name(p._name) { cout Person拷贝构造 endl; } protected: string _name; }; class Student : public Person { public: Student(const string name , int num 0) : Person(name) , _num(num) {} Student(const Student s) : Person(s) , _num(s._num) { cout Student拷贝构造 endl; } private: int _num; }; int main() { Student s1(张三, 100); Student s2(s1); return 0; }Student中第二个拷贝构造里面有Person(s),这个默认会将s中关于Person的内容拷贝给Person,而这里的Person指的是子类中的地基person。赋值operator同理Student s1(张三, 100); Student s2(李四, 200); s1 s2; Student operator(const Student s) { if (this ! s) { Person::operator(s); // 父类部分 _num s._num; // 子类部分 } return *this; }之所以写成Person::operator(s);是因为如果不指明类域的话由于Student自己也有一个所以会让编译器无法识别是哪一个就会报错和之前的隐藏比较类似。析构顺序一句话先子后父先拆房子再拆地基。Student 可能使用 Person 提供的资源如果先析构父类会导致子类在析构时找不到父类。继承禁止final继承class Base final { };在类后面加个final,就可以禁止别人继承自己。友元继承友元不能继承class Person { friend void func(); protected: string _name; }; class Student : public Person { protected: int _num; };func()不能继承给Student。例如void func() { // 可以访问 Person 私有/保护成员 // 不能因为 Person 是 Student 父类 // 就顺便访问 Student::_num }static继承class Person { public: static int count; }; class Student : public Person { }; Person p; Student s1; Student s2; Student s3; Person::count ↑ │ ┌─────┼─────┐ Person Student Teacher 全部访问同一个这里的static int count只会实例化一份所以是所有无论是父类还是子类共用的其内存也是共享的。所以打印p._count s._count地址是相同的。多继承(重点)class Base1 { }; class Base2 { }; class Derived : public Base1, public Base2 { };以上是最简单的多继承模型模型同样是先打地基谁先写在前面就先打哪个。但是这样会引来一个问题也就是指针漂移。Derive* p3 d; Base1* p1 d; Base2* p2 d;based2指针的实际地址会在编译期间强行加上偏移量所以打印出来结果也不同。虚继承菱形继承如图所示该继承方式Assistant 从 Student 获得一份 PersonAssistant 又从 Teacher 获得另一份 Person所以Assistant ┌─────────────────────┐ │ Student │ │ └─ Person │ │ └─ _name │ ├─────────────────────┤ │ Teacher │ │ └─ Person │ │ └─ _name │ └─────────────────────┘但是这会引来2个问题————数据冗余 二义性。二义性Assistant a; a._name 张三;如果这么调用的话编译器无法识别这个_name是哪个父类继承的你说的是 Student → Person → _name 还是 Teacher → Person → _name所以解决办法还是和以前一样加上类域a.Student::_name xxx; a.Teacher::_name yyy;但是这里有一个坑能不能用前面说的static修饰_name解决二义性呢确实可以解决二义性但是不管 Student、Teacher 继承多少次 Person全部共享同一个静态的Person::_name对象内部不会生成两份副本。这就会导致所有 Assistant、Student、Teacher 对象共用同一个名字实例本该各自拥有自己的_name现在变成全局唯一业务逻辑直接坏掉。数据冗余前面加上类域虽然解决了二义性但是为啥保存了2份所以这就造成了数据冗余。于是就有了虚继承class Student : virtual public Person { }; class Teacher : virtual public Person { }; class Assistant : public Student, public Teacher { };此时Person ↑ 只有一份 / \ Student Teacher \ / AssistantPerson只保存一份这样就可以解决数据冗余与二义性。以下也没歧义了Assistant a; a._name peter;虚继承的构造函数class Assistant : public Student, public Teacher { public: Assistant(const char* name1, const char* name2, const char* name3) : Person(name3) , Student(name1, 1) , Teacher(name2, 2) {} };Assistant 明明不是直接继承 Person为什么却自己调用Person(name3)因为虚继承以后Person是最终派生类 Assistant 共享的那个虚基类。所以最终要由最底层的 Assistant负责初始化这一份 Person。假设Assistant a(张三, 李四, 王五);真正那份Person::_name使用的是Person(name3)也就是王五虚基类由最底层、最具体的派生类负责初始化继承和组合一句话优先组合而非继承因为组合的耦合性较低。什么是继承什么是组合很简单继承用英语就是is-a例如Student is a Person 学生是一种人所以class Student : public Person { };public 就是is-a。组合has-a例如Car has a Tire 汽车拥有轮胎所以class Car { Tire tire; };而不是class Car : public Tire因为汽车是一种轮胎显然不合理。组合就是has-a并指出组合耦合度通常更低。就像你愿意选择继承你爸的一套房还是自己本身就有一套房舒服呢
返回列表