继承

1.继承的概念及定义

1.1继承的概念:

继承是一种我们可以使得代码可以复用的一种手段,可以保证我们在编写代码的时候,在原来的类上,我们新增的类,可以继承父(基)类的特性,并扩展更多的功能,这样的新增的类,称为派生类(我一般喜欢称为子类),继承是类设计层次的复用。

1.2继承的定义:

​
class 派生类 : 访问限定符(例如:public等) 基类
{
};

​例如:
class Student : public Person
{
};

例如:

#include <iostream>
using namespace std;

// 基类
class Animal {
public:
    void eat() {
        cout << "I can eat!" << endl;
    }
};

// 派生类
class Dog : public Animal {
public:
    void bark() {
        cout << "I can bark! Woof woof!" << endl;
    }
};

int main() {
    // 创建派生类对象
    Dog myDog;
    
    // 调用基类方法
    myDog.eat();
    
    // 调用派生类方法
    myDog.bark();
    
    return 0;
}

1.3继承的访问限定符的影响:

注意:

1.这里说明基类的private域,派生类无论是说明限定符继承是怎么也访问不了的

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在
派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

3.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

4.总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

2.基类和派生类对象赋值转换

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象(但是指针或引用可以)。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类
    的指针是指向派生类对象时才是安全的。

例如:

class Person
{
protected :
    string _name; // 姓名
    string _sex; // 性别
    int _age; // 年龄
};
class Student : public Person
{
public :
    int _No ; // 学号
};
void Test ()
{
    Student sobj ;
    // 1.子类对象可以赋值给父类对象/指针/引用
    Person pobj = sobj ;
    Person* pp = &sobj;
    Person& rp = sobj;
    //2.基类对象不能赋值给派生类对象
    sobj = pobj;
    // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj
    Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;
    pp = &pobj;
    Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
    ps2->_No = 10;
}

3.继承中的作用域

1. 在继承体系中基类和派生类都有独立的作用域。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
4. 注意在实际中在继承体系里面最好不要定义同名的成员。

例如:

#include <iostream>
using namespace std;

class Base {
public:
    void func() {
        cout << "Base func()" << endl;
    }
    
    void func(int x) {
        cout << "Base func(int): " << x << endl;
    }
};

class Derived : public Base {
public:
    // 重写基类的func(),这会隐藏基类中所有同名的func重载
    void func() {
        cout << "Derived func()" << endl;
    }
    
};

int main() {
    Derived d;
    d.func();        // 调用Derived::func()
    d.func(5);       // 错误:如果没有using声明,这个重载会被隐藏
    
    // 使用作用域解析运算符直接调用基类方法
    d.Base::func(5); // 调用Base::func(int)
    
    return 0;
}

4.继承后派生类的默认成员函数与基类的关系

4.1构造函数

  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
#include <iostream>
#include <string>
using namespace std;

// 基类 Person
class Person {
public:
    // 基类没有默认构造函数
    // 因为它有一个需要参数的构造函数
    Person(const string& name) : m_name(name) {
        cout << "Person Constructor Called. Name: " << m_name << endl;
    }

private:
    string m_name;
};

// 派生类 Student
class Student : public Person {
public:
    // 派生类构造函数必须在初始化列表中显式调用基类构造函数
    Student(const string& name, int id)
        : Person(name), // <--- 显式调用基类Person的构造函数,传递name参数
          m_id(id)     //    初始化派生类自己的成员
    {
        cout << "Student Constructor Called. ID: " << m_id << endl;
    }

    // 错误示例!如果像下面这样写,编译器会报错:
    /*
    Student(const string& name, int id) {
        m_id = id;
        // 这里试图隐式调用Person的默认构造函数,但Person类根本没有默认构造函数,所以会编译失败。
    }
    */

private:
    int m_id;
};

int main() {
    // 创建一个Student对象
    Student stu("Alice", 12345);

    return 0;
}
  •  派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的复制。
  • 派生类对象析构清理先调用派生类析构再调基类的析构。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。我们要指定作用域在派生类里调用基类的析构函数

5.继承与友元、继承与静态成员关系

友元关系:

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

静态成员关系:

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

6.继承的种类以及菱形继承

6.1单继承

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

6.2多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承

6.3菱形继承

菱形继承是其继承构成一个类似菱形的形状

例如:

类D通过B、C继承了A,因此,D中包含了两份A的副本(B中的A和C中的A),当直接访问A的成员时,编译器无法确定应该使用哪条路径。所以菱形继承会造成二义性问题。

例如:

#include <iostream>
using namespace std;
// 基类A
class A {
public:
    void show() {
        cout << "A::show() called" << endl;
    }
    int value;
};

// 类B继承自A
class B : public A {
public:
    void displayB() {
        cout << "B::displayB() called" << endl;
    }
};

// 类C也继承自A
class C : public A {
public:
    void displayC() {
        cout << "C::displayC() called" << endl;
    }
};

// 类D同时继承B和C,形成菱形继承
class D : public B, public C {
public:
    void displayD() {
        cout << "D::displayD() called" << endl;
    }
};

int main() {
    D d;
    
    // 以下调用没有问题,因为它们在各自的类中是唯一的
    d.displayB(); // 来自B
    d.displayC(); // 来自C
    d.displayD(); // 来自D
    
    // 问题1: 访问show()方法会产生二义性
     d.show(); // 错误: 对'show'的请求不明确
    
    // 解决方案: 使用作用域解析运算符指定路径
    d.B::show(); // 通过B路径调用
    d.C::show(); // 通过C路径调用
    
    // 问题2: 访问value成员也会产生二义性
    // d.value = 10; // 错误: 对'value'的请求不明确
    
    // 解决方案: 同样使用作用域解析运算符
    d.B::value = 10;
    d.C::value = 20;
    
    cout << "B路径的value: " << d.B::value << endl;
    cout << "C路径的value: " << d.C::value << endl;
    
    return 0;
}

由此可见菱形继承会带来二义性问题。

那么如何解决这个问题呢?

1.使用作用域解析运算符(::)明确指定访问路径

2.使用虚继承可以解决这个问题

6.4虚继承

虚继承要使用virtual关键字,放在继承的访问限定符前面

例如:下面的学生类使用了虚继承,继承人这个类

class Person
{
public :
    string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
    int _num ; //学号
};

为什么说虚继承可以解决二义性的问题呢?

虚继承使得在D中只生成一个A,且B和C没有A的副本,只有指向A的指针,A由派生类D直接管理,也就是D负责构造和初始化这个共享的A实例。

在没有使用虚继承的时候,对象模型是这样的:

没有虚继承
没有使用虚继承模型

使用了虚继承后:

使用了虚继承

我们来看看代码的例子:

没有虚继承前:

#include <iostream>
using namespace std;

class A {
public:
    A() { 
        cout << "A构造函数 @" << this << endl; 
        value = 0;
    }
    void show() { 
        cout << "A::show() @" << this << " value = " << value << endl; 
    }
    int value;
};

class B : public A {
public:
    B() { 
        cout << "B构造函数 @" << this << endl; 
    }
};

class C : public A {
public:
    C() { 
        cout << "C构造函数 @" << this << endl; 
    }
};

// 菱形继承,没有使用虚继承
class D : public B, public C {
public:
    D() { 
        cout << "D构造函数 @" << this << endl; 
    }
};

int main() {
    cout << "创建D对象(非虚继承)..." << endl;
    D d;
    
    cout << "\n对象地址:" << endl;
    cout << "整个D对象: " << &d << endl;
    cout << "B部分: " << static_cast<B*>(&d) << endl;
    cout << "C部分: " << static_cast<C*>(&d) << endl;
    
    // 注意:以下转换会产生二义性,不能直接转换
    // cout << "A部分: " << static_cast<A*>(&d) << endl; // 错误: 对'A'的转换不明确
    
    cout << "\n通过不同路径访问A:" << endl;
    cout << "通过B访问的A: " << static_cast<A*>(static_cast<B*>(&d)) << endl;
    cout << "通过C访问的A: " << static_cast<A*>(static_cast<C*>(&d)) << endl;
    
    cout << "\n验证两个A实例是独立的:" << endl;
    // 设置不同值验证它们是不同的实例
    d.B::value = 100;
    d.C::value = 200;
    
    cout << "通过B访问的value: " << d.B::value << endl;
    cout << "通过C访问的value: " << d.C::value << endl;
    
    cout << "\n调用show方法:" << endl;
    d.B::show();        // 通过B路径
    d.C::show();        // 通过C路径
    
    // 以下调用会产生二义性错误
    // d.show();        // 错误: 对'show'的请求不明确
    // d.value = 10;    // 错误: 对'value'的请求不明确
    
    cout << "\n计算A实例之间的距离:" << endl;
    A* aViaB = static_cast<A*>(static_cast<B*>(&d));
    A* aViaC = static_cast<A*>(static_cast<C*>(&d));
    cout << "两个A实例的地址差: " << (aViaC - aViaB) << " 个A对象大小" << endl;
    cout << "实际字节偏移: " << (reinterpret_cast<char*>(aViaC) - reinterpret_cast<char*>(aViaB)) << " 字节" << endl;
    
    return 0;
}

代码结果:

创建D对象(非虚继承)...
A构造函数 @0x7ffc2f1d2a8e
B构造函数 @0x7ffc2f1d2a8e
A构造函数 @0x7ffc2f1d2a92
C构造函数 @0x7ffc2f1d2a92
D构造函数 @0x7ffc2f1d2a8e

对象地址:
整个D对象: 0x7ffc2f1d2a8e
B部分: 0x7ffc2f1d2a8e
C部分: 0x7ffc2f1d2a92

通过不同路径访问A:
通过B访问的A: 0x7ffc2f1d2a8e
通过C访问的A: 0x7ffc2f1d2a92

验证两个A实例是独立的:
通过B访问的value: 100
通过C访问的value: 200

调用show方法:
A::show() @0x7ffc2f1d2a8e value = 100
A::show() @0x7ffc2f1d2a92 value = 200

计算A实例之间的距离:
两个A实例的地址差: 1 个A对象大小
实际字节偏移: 4 字节

虚继承的代码:

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "A构造函数 @" << this << endl; }
    void show() { cout << "A::show() @" << this << endl; }
};

class B : virtual public A {
public:
    B() { cout << "B构造函数 @" << this << endl; }
};

class C : virtual public A {
public:
    C() { cout << "C构造函数 @" << this << endl; }
};

class D : public B, public C {
public:
    D() { cout << "D构造函数 @" << this << endl; }
};

int main() {
    cout << "创建D对象..." << endl;
    D d;
    
    cout << "\n对象地址:" << endl;
    cout << "整个D对象: " << &d << endl;
    cout << "B部分: " << (B*)&d << endl;
    cout << "C部分: " << (C*)&d << endl;
    cout << "A部分: " << (A*)&d << endl;
    
    cout << "\n调用show方法:" << endl;
    d.show();           // 直接调用,无二义性
    d.B::show();        // 通过B路径
    d.C::show();        // 通过C路径
    
    cout << "\n地址比较:" << endl;
    cout << "通过B访问的A: " << static_cast<A*>(static_cast<B*>(&d)) << endl;
    cout << "通过C访问的A: " << static_cast<A*>(static_cast<C*>(&d)) << endl;
    
    return 0;
}

代码结果:

创建D对象...
A构造函数 @0x7ffd2f1d2a8e
B构造函数 @0x7ffd2f1d2a8e
C构造函数 @0x7ffd2f1d2a8f
D构造函数 @0x7ffd2f1d2a8e

对象地址:
整个D对象: 0x7ffd2f1d2a8e
B部分: 0x7ffd2f1d2a8e
C部分: 0x7ffd2f1d2a8f
A部分: 0x7ffd2f1d2a90

调用show方法:
A::show() @0x7ffd2f1d2a90
A::show() @0x7ffd2f1d2a90
A::show() @0x7ffd2f1d2a90

地址比较:
通过B访问的A: 0x7ffd2f1d2a90
通过C访问的A: 0x7ffd2f1d2a90

所以虚继承也可以解决二义性问题,B、C存放着A的指针,解引用指针是其B距离和C距离A的偏移量。

继承与组合

  1. 继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  3. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐