C++笔记---继承(下)

大筒木老辈子 2024-10-08 17:35:01 阅读 58

1. 无法被继承的类

要实现无法被继承的类有两种方式:

C++98及其之前:将父类的构造函数设置为private成员。

C++11及其之后:使用final关键字修饰父类。

将构造函数设置为private是因为:子类的构成必须调用父类的构造函数,但是父类的构成函数私有化以后,子类看不见就不能调用了,那么子类就无法实例化出对象。 

<code>// C++11的⽅法

class Base final

{

public:

void func5() { cout << "Base::func5" << endl; }

protected:

int a = 1;

private:

// C++98的⽅法

/*Base()

{}*/

};

class Derive :public Base

{

void func4() { cout << "Derive::func4" << endl; }

protected:

int b = 2;

};

int main()

{

Base b;

Derive d;

return 0;

}

2. 友元关系与继承

父类的友元关系不能被继承。也就是说,也就是说父类友元不能访问子类私有和保护成员。

这一点很好记忆:

父类进行友元声明  --->  这个函数/类是我的好友。

子类  --->  我父亲的好友是我的叔叔而不是好友。

<code>class Student;

class Person

{

public:

friend void Display(const Person& p, const Student& s);

protected:

string _name; // 姓名

};

class Student : public Person

{

protected:

int _stuNum; // 学号

};

void Display(const Person& p, const Student& s)

{

cout << p._name << endl;

cout << s._stuNum << endl;

}

int main()

{

Person p;

Student s;

// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员

// 解决⽅案:Display也变成Student 的友元即可

Display(p, s);

return 0;

}

 3. 静态成员与继承

父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

<code>class Person

{

public:

string _name;

static int _count;

};

int Person::_count = 0;

class Student : public Person

{

protected:

int _stuNum;

};

int main()

{

Person p;

Student s;

// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的

// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份

cout << &p._name << endl;

cout << &s._name << endl;

// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的

// 说明派⽣类和基类共⽤同⼀份静态成员

cout << &p._count << endl;

cout << &s._count << endl;

// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员

cout << Person::_count << endl;

cout << Student::_count << endl;

return 0;

}

4. 多继承以及菱形继承问题

4.1 多继承

单继承:一个子类类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员在放到最后面。

class Assistant : public Student, public Teacher

 4.2 菱形继承

菱形继承是多继承的一种特殊情况。

菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。

支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

<code>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; // 主修课程

};

int main()

{

// 编译报错:error C2385: 对“_name”的访问不明确

Assistant a;

a._name = "peter";

// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决

a.Student::_name = "xxx";

a.Teacher::_name = "yyy";

return 0;

}

4.3 虚继承

虚继承是为了解决菱形继承而提出的,像Person这样被重复继承的类可以用virtual关键字来进行继承:

// 使用虚继承Person类

class Student : virtual public Person

{

protected:

int _num; //学号

};

// 使用虚继承Person类

class Teacher : virtual public Person

{

protected:

int _id; // 职⼯编号

};

// 教授助理

class Assistant : public Student, public Teacher

{

protected:

string _majorCourse; // 主修课程

};

当两个虚继承了同一个爷爷类(菱形的上顶点)的类被孙子类(菱形的下顶点)继承时,这两个子类的共同父类(爷爷类)就只会被孙子类继承一次。

此时,Person由于只被继承了一次,所以不再适合放到Student内部或Teacher内部,而是被放到整个对象的最下面。

在调用构造函数时,孙子类的初始化列表中会优先调用爷爷类的构造函数,然后是先继承的类的构造函数,后继承的类的构造函数。

而爸爸类初始化列表中对爷爷类构造函数的调用会失效。

很多人说C++语法复杂,其实多继承就是一个体现。

有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承

多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。

我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。

5. 指针偏移问题

上一篇提到,子类对象可以给父类指针赋值。

在上面,我们已经见识过多继承对象内部成员的分布,当我们用Assistant对象给Teacher对象的指针赋值时,得到的地址实际上是Teacher成员开始的位置。

在下面的这个例子中,我们可以看到p1 == p3 != p2的结果。

<code>class Base1 { public: int _b1; };

class Base2 { public: int _b2; };

class Derive : public Base1, public Base2

{ public: int _d; };

int main()

{

Derive d;

Base1* p1 = &d;

Base2* p2 = &d;

Derive* p3 = &d;

return 0;

}

6. IO库中的菱形虚拟继承

<code>template<class CharT, class Traits = std::char_traits<CharT>>

class basic_ostream : virtual public std::basic_ios<CharT, Traits>

{};

template<class CharT, class Traits = std::char_traits<CharT>>

class basic_istream : virtual public std::basic_ios<CharT, Traits>

{};

 7. 继承和组合

• public继承是一种is - a的关系。也就是说每个派生类对象都是一个基类对象。

• 组合是一种has - a的关系。假设B组合了A,每个B对象中都有一个A对象。

• 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white - box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

• 对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is - a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is - a)也适合组合(has - a),就用组合。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。