C++ 继承详解:从入门到深入

继承是面向对象中实现类层次代码复用的机制,在保留父类特性的基础上扩展出新功能,由此产生的新类称为派生类。

一、为什么需要继承?

1.1 问题引入

假设我们要设计学生和老师两个类,你会发现它们有很多共同的属性:姓名、年龄、地址、电话等。如果没有继承,代码会大量重复

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 没有继承的写法 - 代码冗余严重
class Student {
public:
    void identity() {
        cout << "身份认证:" << _name << endl;
    }
    void study() {
        cout << _name << "正在学习" << endl;
    }

protected:
    string _name = "张三";
    int _age = 18;
    string _address;
    string _tel;
    int _stuId;      // 学号(学生特有)
};

class Teacher {
public:
    void identity() {
        cout << "身份认证:" << _name << endl;
    }
    void teach() {
        cout << _name << "正在授课" << endl;
    }

protected:
    string _name = "李老师";
    int _age = 35;
    string _address;
    string _tel;
    string _title;   // 职称(老师特有)
};

int main() {
    Student s;
    Teacher t;
    s.identity();
    t.identity();
    return 0;
}

1.2 继承解决方案

使用继承,将公共部分提取到基类(Person)中

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 基类(父类)- 存放公共部分
class Person {
public:
    Person(const string& name = "无名", int age = 0)
        : _name(name), _age(age) {
       //cout << "Person构造函数: " << _name << endl;
    }

    void identity() {
        cout << "身份认证:" << _name << endl;
    }

    void showInfo() {
        cout << "姓名:" << _name << ",年龄:" << _age << endl;
    }

protected:
    string _name;
    int _age;
    string _address;
    string _tel;
};

// 派生类(子类)- 继承Person
class Student : public Person {
public:
    Student(const string& name, int age, int stuId)
      : Person(name, age), _stuId(stuId)
    {
        cout << "Student构造函数: " << _name << endl;
    }

    void study() {
        cout << _name << "(学号:" << _stuId << ")正在学习" << endl;
    }

protected:
    int _stuId;   // 学号(学生特有)
};

class Teacher : public Person {
public:
    Teacher(const string& name, int age, const string& title)
        : Person(name, age),  // 直接一步到位
        _title(title)
    {
        cout<< "Teacher构造函数: " << _name << endl;
    }

    void teach() {
        cout << _name << "(职称:" << _title << ")正在授课" << endl;
    }

protected:
    string _title;  // 职称(老师特有)
};

int main() {
    cout << "=== 创建学生 ===" << endl;
    Student s("小明", 18, 2024001);
    s.identity();    // 继承自Person
    s.study();       // Student自己的函数
    s.showInfo();    // 继承自Person

    cout << "\n=== 创建老师 ===" << endl;
    Teacher t("王老师", 35, "教授");
    t.identity();    // 继承自Person
    t.teach();       // Teacher自己的函数
    t.showInfo();    // 继承自Person

    return 0;
}

注意:

必须调用父类构造函数,让父类自己初始化自己的成员

cpp 复制代码
// ✅ 正确!唯一写法!
Student(const string& name, int age, int stuId)
    : Person(name, age),  // 调用父类构造,让父类初始化 _name _age
      _stuId(stuId)       // 子类初始化自己的成员
{}

我自己写的错误示例:

cpp 复制代码
// ❌ 错误!绝对不能这么写!
Student(const string& name, int age, int stuId)
   :_name(name), _age(age), _stuId(stuId)  

子类 不能在初始化列表里直接给父类的成员变量赋值!

  • _name_age 是父类 Person 的成员
  • 父类成员,必须由父类的构造函数来初始化
  • 子类无权在初始化列表里直接初始化父类成员!

二、继承的三种方式

访问权限和继承权限是不同的概念:

  1. 访问权限(成员本身的权限)

作用:控制 "外面能不能直接用"

  • public:谁都能访问
  • protected:自己 + 子类能访问
  • private:只有自己能访问

这是成员自己的属性,跟继承没关系。

  1. 继承权限(继承方式)

作用:**控制 "继承过来后,成员变成什么权限"**三种继承方式:

  • public 继承
  • protected 继承
  • private 继承

它只改变成员在子类里的最终权限,不改变基类本身。

在类的继承中,有以下几点特性:

1 基类的构造函数与析构函数不能被继承

2 派生类对基类成员的继承没有选择权,不能选则继承或不继承某些成员

3 派生类中可以添加新成员,用于实现新功能,让派生类的功能在基类上有所扩展。

2.1 访问限定符与继承方式的关系

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Base {
public:
    int pub = 1;      // 公有成员
protected:
    int pro = 2;      // 保护成员
private:
    int pri = 3;      // 私有成员
};

// 1. public继承
class PubDerived : public Base {
public:
    void test() {
        cout << pub << endl;   // ✅ 可以访问:pub → public
        cout << pro << endl;   // ✅ 可以访问:pro → protected
        // cout << pri << endl;   // ❌ 错误:基类private成员在派生类不可见
    }
};

// 2. protected继承
class ProDerived : protected Base {
public:
    void test() {
        cout << pub << endl;   // ✅ 可以访问:pub → protected   
        cout << pro << endl;   // ✅ 可以访问:pro → protected
        // cout << pri << endl;   // ❌ 错误:不可见
    }
};

// 3. private继承
class PriDerived : private Base {
public:
    void test() {
        cout << pub << endl;   // ✅ 可以访问:pub → private
        cout << pro << endl;   // ✅ 可以访问:pro → private
        // cout << pri << endl;   // ❌ 错误:不可见   
    }
};

int main() {
    PubDerived pubObj;
    pubObj.pub = 10;     // ✅ public成员在类外可访问
    // pubObj.pro = 20;  // ❌ protected成员类外不可访问

    ProDerived proObj;
    // proObj.pub = 10;  // ❌ 变成protected后,类外不可访问

    PriDerived priObj;
    // priObj.pub = 10;  // ❌ 变成private后,类外不可访问

    cout << "=== 结论 ===" << endl;
    cout << "实际开发中,99%的情况使用public继承" << endl;
 
    return 0;
}

总结:

  1. private 成员

子类永远无法直接访问,不管什么继承方式。

  1. public 继承

父类的访问权限原样保留:

  • public → 依然 public
  • protected → 依然 protected
  • private → 依然不能直接访问

你这句话说得特别到位:"公有继承就是基类的成员在子类也有一份, 子类的访问权限和基类一样。**"**只是父类 private 永远碰不到,构造析构不继承。

  1. protected 继承
  • 父类 public、protected → 在子类里都变成 protected正确,父类 private子类永远无法直接访问

4. private 继承

  • 父类所有可继承成员 → 在子类都变成 private 正确,父类 private子类永远无法直接访问

虽然父类 private子类永远无法直接访问,但子类的成员函数可以调用父类的public,protected权限的 成员函数 通过父类间接访问

例子:

cpp 复制代码
class A {
private:
    int c = 100;  // 父类私有
public:
    void show_c() {
        // 父类自己的函数,可以访问 c
        cout << c << endl;
    }
};

class B : public A {
public:
    void func() {
        // c = 200; ❌ 不能直接访问父类 private

        show_c();   // ✅ 可以调用父类 public 函数
        // 间接访问 c
    }
};
int main() {
    B a;
    a.show_c();   
    a.func();
}

三、基类和派生类之间的转换(切片)

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    Person(const string& name = "") : _name(name) {}
    string _name;
    int _age = 0;
};

class Student : public Person {
public:
    Student(const string& name, int stuId) 
        : Person(name), _stuId(stuId) {}
    int _stuId;
};

int main() {
    Student s("小明", 2024001);
    
    // 1. 子类对象赋值给父类对象(切片)
    Person p1 = s;           // 只拷贝Person部分
    cout << "p1._name: " << p1._name << endl;  // 小明
    
    // 2. 子类对象赋值给父类指针(指向子类中的父类部分)
    Person* p2 = &s;
    cout << "p2->_name: " << p2->_name << endl;  // 小明
    // p2->_stuId;  // ❌ 错误:父类指针不能访问子类成员
    
    // 3. 子类对象赋值给父类引用
    Person& p3 = s;
    cout << "p3._name: " << p3._name << endl;    // 小明
    
    // 4. ❌ 父类对象不能赋值给子类对象
    // Student s2 = p1;  // 编译错误
    
    // 5. 父类指针指向子类时,可以强制转换回子类指针(需要确保确实指向子类)
    Person* p4 = &s;
    Student* s2 = (Student*)p4;  // C风格强制转换
    cout << "s2->_stuId: " << s2->_stuId << endl;  // 2024001
    
    cout << "\n=== 切片示意图 ===" << endl;
    cout << "子类对象: [Person部分][Student部分]" << endl;
    cout << "父类指针: 指向 [Person部分] ← 切片" << endl;
    
    return 0;
}

派生类对象可以直接赋值 / 初始化给基类对象 / 引用 / 指针,派生类中独有的成员会被切掉,只保留基类部分,这就是对象切片。

派生类切片 → 切掉独有部分,只留基类成员,安全向上转换。

四、继承中的作用域(隐藏规则)

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    Person() : _num(111) {}
    
    void print() {
        cout << "Person::print()" << endl;
    }
    
    void func(int x) {
        cout << "Person::func(int) = " << x << endl;
    }
    
protected:
    string _name = "Person";
    int _num;        // 身份证号
};

class Student : public Person {
public:
    Student() : _num(999) {}
    
    void print() {
        cout << "Student::print()" << endl;
    }
    
    void func() {    // 函数名相同,参数不同也构成隐藏
        cout << "Student::func()" << endl;
    }
    
    void show() {
        // 同名成员:子类会隐藏父类的
        cout << "子类_num: " << _num << endl;           // 999
        cout << "父类_num: " << Person::_num << endl;   // 111(显式指定)
        
        // 同名函数:子类隐藏父类
        print();               // 调用子类的
        Person::print();       // 调用父类的
        
        func();                // 调用子类的(无参)
        // func(10);           // ❌ 错误:被隐藏了,找不到
        Person::func(10);      // ✅ 显式指定可以调用
    }
    
protected:
    int _num;        // 学号(与父类同名)
};

int main() {
    Student s;
    s.show();
    
    cout << "\n=== 隐藏规则总结 ===" << endl;
    cout << "1. 子类和父类有同名成员(变量/函数),子类会隐藏父类" << endl;
    cout << "2. 函数同名即构成隐藏(不看参数)" << endl;
    cout << "3. 可以用 父类::成员 显式访问被隐藏的成员" << endl;
    cout << "4. 建议:尽量不要在子类定义同名成员" << endl;
    
    return 0;
}

子类和基类的作用域不一样,所以子类有和基类一样的成员变量或者成员函数时,构不成重载(重载要作用域一样),根据就近原则,用子类的,除非显示调用父类的

作用域不同 → 不能重载 → 同名就隐藏 → 就近用子类 → 父类加::

五、派生类的默认成员函数(完整示例)

这是面试中的高频考点!

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    // 构造函数
    Person(const string& name = "无名") 
        : _name(name) {
        cout << "Person构造函数: " << _name << endl;
    }
    
    // 拷贝构造函数
    Person(const Person& p) 
        : _name(p._name) {
        cout << "Person拷贝构造函数: " << _name << endl;
    }
    
    // 赋值运算符重载
    Person& operator=(const Person& p) {
        cout << "Person赋值运算符: " << p._name << " -> " << _name << endl;
        if (this != &p) {
            _name = p._name;
        }
        return *this;
    }
    
    // 析构函数
    ~Person() {
        cout << "Person析构函数: " << _name << endl;
    }
    
protected:
    string _name;
};

class Student : public Person {
public:
    // 构造函数:必须调用基类构造函数初始化基类部分
    Student(const string& name, int stuId)
        : Person(name)      // 显式调用基类构造函数
        , _stuId(stuId) {
        cout << "Student构造函数: " << name << ", 学号:" << _stuId << endl;
    }
    
    // 拷贝构造函数:必须调用基类拷贝构造
    Student(const Student& s)
        : Person(s)         // 切片:Student对象赋值给Person引用
        , _stuId(s._stuId) {
        cout << "Student拷贝构造函数: " << _stuId << endl;
    }
    
    // 赋值运算符:必须显式调用基类的赋值运算符
    Student& operator=(const Student& s) {
        cout << "Student赋值运算符" << endl;
        if (this != &s) {
            Person::operator=(s);  // 显式调用基类赋值运算符
            _stuId = s._stuId;
        }
        return *this;
    }
    
    // 析构函数:会自动调用基类析构(无需显式调用)
    ~Student() {
        cout << "Student析构函数: 学号" << _stuId << endl;
        // 会自动调用 ~Person()
    }
    
protected:
    int _stuId;
};

int main() {
    cout << "=== 1. 创建对象(构造) ===" << endl;
    Student s1("小明", 1001);
    
    cout << "\n=== 2. 拷贝构造 ===" << endl;
    Student s2(s1);
    
    cout << "\n=== 3. 赋值运算 ===" << endl;
    Student s3("小红", 1002);
    s1 = s3;
    
    cout << "\n=== 4. 销毁对象(析构) ===" << endl;
    // 对象会按创建顺序逆序析构
    
    return 0;
}
1 子类对象 = 父类部分 + 子类自己部分

子类构造函数初始化列表必须先调用父类构造函数(用匿名对象的方法:父类名(值)),不然父类的成员变量无法初始化(上面说的的子类不能直接初始化父类成员),除非你不用父类的成员变量。

cpp 复制代码
   // 构造函数:必须调用基类构造函数初始化基类部分
   Student(const string& name, int stuId)
       : Person(name)      // 显式调用基类构造函数
       , _stuId(stuId) {
       cout << "Student构造函数: " << name << ", 学号:" << _stuId << endl;
   }

name要初始化成学生自己的名字

2 拷贝构造时,s2 里面也包含父类的 name 变量,父类部分必须由父类的拷贝构造来初始化,所以子类必须调用父类的拷贝构造!
cpp 复制代码
// 拷贝构造函数
Person(const Person& p)
    : _name(p._name) {
    cout << "Person拷贝构造函数: " << _name << endl;
}
cpp 复制代码
// 拷贝构造函数:必须调用基类拷贝构造
Student(const Student& s)
    : Person(s)         // 切片:Student对象赋值给Person引用
    , _stuId(s._stuId) {
    cout << "Student拷贝构造函数: " << _stuId << endl;
}
cout << "\n=== 2. 拷贝构造 ===" << endl;
Student s2(s1);

这里的 Person(s) 还顺便复习了 切片: sStudent的引用,参数匹配父类的拷贝构造, 这里的 p 看到的只是 s 里面的 Person 部分 ,切掉子类部分,只保留父类部分

如果不写 Person(s) 会发生什么?编译器会**自动调用父类的【默认构造】**Person(),而不是拷贝构造!

cpp 复制代码
Student(const Student& s)
    : Person()   // 编译器自动插入
    , _stuId(s._stuId)
{}

结果:

  • s2 的名字 = 无名
  • s2 的学号 = 1001这就错了!

3

  1. 构造:子类必须在初始化列表调用父类构造
  2. 拷贝构造:子类必须调用父类拷贝构造
  3. 赋值重载:子类必须显式调用父类赋值重载 Person::operator=(s)
  4. 析构:不用调用,编译器自动调用父类析构
3赋值重载:编译器不会自动帮你调用父类赋值!必须手动写!
cpp 复制代码
  // 赋值运算符重载
  Person& operator=(const Person& p) {
      cout << "Person赋值运算符: " << p._name << " -> " << _name << endl;
      if (this != &p) {
          _name = p._name;
      }
      return *this;
  }

 // 赋值运算符:必须显式调用基类的赋值运算符
 Student& operator=(const Student& s) {
     cout << "Student赋值运算符" << endl;
     if (this != &s) {
         Person::operator=(s);  // 显式调用基类赋值运算符
         _stuId = s._stuId;
     }
     return *this;
 }

子类赋值 = 父类赋值 + 子类赋值,缺一不可!

切片发生了!

  • sStudent
  • 传给父类的 operator=(const Person& p)
  • 编译器自动切片 → 只把父类部分传给父类赋值函数

调用父类的赋值重载

_name 正确赋值过去。

如果不写:

  • 子类学号赋值成功
  • 父类名字根本没赋值!还是原来的值!

注意:

Person::operator=(s); // 显式调用基类赋值运算符

不加Person::子类和父类有同名函数 → 父类被直接隐藏!,发生无限递归,栈溢出

总结:

  1. 构造顺序:基类 → 派生类

  2. 析构顺序:派生类 → 基类

  3. 拷贝构造必须调用基类拷贝构造

  4. 赋值运算符必须显式调用基类赋值运算符

六、菱形继承与虚继承(重点难点)

6.1 菱形继承的问题

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    string _name = "Person";
    int _age = 0;
};

// Student和Teacher都继承Person
class Student : public Person {
public:
    int _stuId = 0;
};

class Teacher : public Person {
public:
    int _teacherId = 0;
};

// Assistant同时继承Student和Teacher
class Assistant : public Student, public Teacher {
public:
    string _major = "计算机";
};

int main() {
    Assistant a;
    
    // ❌ 二义性:_name有两个副本(一份来自Student,一份来自Teacher)
    // a._name = "张三";  // 编译错误:对_name的访问不明确
    
    // ✅ 需要显式指定从哪个路径访问
    a.Student::_name = "张三(学生身份)";
    a.Teacher::_name = "张三(老师身份)";
    
    // 数据冗余:同一个Person对象有两份
    cout << "Student::_name地址: " << &a.Student::_name << endl;
    cout << "Teacher::_name地址: " << &a.Teacher::_name << endl;
    cout << "两个地址不同,说明有两份数据" << endl;
    
    // 内存大小:包含两份Person的成员
    cout << "\n对象大小: " << sizeof(Assistant) << " 字节" << endl;
    // string(32字节) * 2份 = 64字节 + 其他成员
    
    return 0;
}

对象内存布局:

分析:

cpp 复制代码
        Person
        /    \
       /      \
   Student  Teacher
       \      /
        \    /
     Assistant

编译器看见:class Assistant : public Student, public Teacher

它就严格按从左到右执行:

1. 先构造 左边第一个:Student

  • 构造 Student 必须先构造它的父类 Person
  • 输出:Person 构造
  • 然后构造 Student 自己
  • 输出:Student 构造

2. 再构造 右边第二个:Teacher

  • 构造 Teacher 必须先构造它的父类 Person
  • 输出:Person 构造又来一次!
  • 然后构造 Teacher 自己
  • 输出:Teacher 构造

3. 最后构造 自己:Assistant

  • 输出:Assistant 构造

所以:

  • _name 存在 两个不同的地址
  • 编译器不知道你要哪一个 → 二义性错误
  • 数据冗余、浪费空间、容易出错

6.2 虚继承解决菱形继承

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 使用虚继承(virtual)
class Person {
public:
    Person() : _name("Person") {
        cout << "Person构造函数" << endl;
    }
    
    string _name;
    int _age = 0;
};

// 虚继承Person
class Student : virtual public Person {
public:
    Student() : _stuId(0) {
        cout << "Student构造函数" << endl;
    }
    int _stuId;
};

// 虚继承Person
class Teacher : virtual public Person {
public:
    Teacher() : _teacherId(0) {
        cout << "Teacher构造函数" << endl;
    }
    int _teacherId;
};

class Assistant : public Student, public Teacher {
public:
    Assistant() : _major("计算机") {
        cout << "Assistant构造函数" << endl;
    }
    string _major;
};

int main() {
    cout << "=== 菱形虚继承 ===" << endl;
    Assistant a;
    
    // ✅ 可以直接访问,没有二义性
    a._name = "张三";
    cout << "姓名: " << a._name << endl;
    
    // ✅ 只有一份Person数据
    cout << "&a._name: " << &a._name << endl;
    cout << "&a.Student::_name: " << &a.Student::_name << endl;
    cout << "&a.Teacher::_name: " << &a.Teacher::_name << endl;
    cout << "三个地址相同,说明只有一份数据" << endl;
    
    cout << "\n=== 构造顺序(虚继承) ===" << endl;
    // 输出会显示构造顺序:最远的基类先构造
    
    return 0;
}

虚继承写在 StudentTeacher 上,它的作用只有一个:告诉编译器:

Student 和 Teacher 不要各自复制一份 Person,而是共享同一份 Person,所以

cpp 复制代码
 cout << "&a._name: " << &a._name << endl;
 cout << "&a.Student::_name: " << &a.Student::_name << endl;
 cout << "&a.Teacher::_name: " << &a.Teacher::_name << endl;

他们地址完全一样。a.Student::_name意思是:我要访问的是从 Student 继承下来的 _name

作用:虚继承 = 只有一份公共的 Person 基类

让最终子类 Assistant 里,Person 只保留一份! 不再有两份 _name,不再有二义性!

6.3 虚继承的构造顺序(重要)

cpp 复制代码
#include <iostream>
using namespace std;

class Grand {
public:
    Grand() { cout << "Grand构造" << endl; }
};

class Base1 : virtual public Grand {
public:
    Base1() { cout << "Base1构造" << endl; }
};

class Base2 : virtual public Grand {
public:
    Base2() { cout << "Base2构造" << endl; }
};

class Derived : public Base1, public Base2 {
public:
    Derived() { cout << "Derived构造" << endl; }
};

int main() {
    cout << "虚继承构造顺序:最远的虚基类最先构造" << endl;
    Derived d;
    // 输出顺序:
    // Grand构造  ← 最远的基类
    // Base1构造
    // Base2构造
    // Derived构造
    
    return 0;
}

因为虚继承保证 最高层基类只构造一份

所以必须让 **最终孙子类(Derived)直接负责构造爷爷(Grand)**不能让 Base1、Base2 各自去构造,否则又会重复!

  • 最远的基类由最终派生类直接构造
  • 只构造一次,不会重复构造

6.4 虚继承底层原理

cpp 复制代码
class A { int _a; };

// 虚继承
class B : virtual public A { int _b; };
class C : virtual public A { int _c; };

class D : public B, public C { int _d; };

不是虚继承监视:

虚继承监视:

  • 虚继承:只保留一份间接基类

    • B、C 虚继承 A
    • 每个虚继承的类,编译器生成一张 vbtable(虚基类表)( 全局只有一张!所有 B对象共用这一张表**)**
    • 表中存放:偏移量 ------ 用来找到唯一的那份 A
    • 编译器会在 B、C 对象 里加一个 vbptr(虚基类指针)( 存的是虚基类表的地址**)**

**访问原理:**通过 vbptr → 查虚基类表 → 得到偏移 → 找到公共的基类成员

普通继承(非虚):A 的成员 直接放在 B 内部

cpp 复制代码
B 对象:
[A _a] + [B _b]

虚继承:A 的成员 _a 依然存在! 只是不放在 B 主体内部而是放在对象的最末尾,变成共享区域。

cpp 复制代码
B 对象:
[ vbptr ]  ---------->  B 的虚基类表 vbtable
[  _b   ]                [ 偏移量:到 A 的距离 ]
[  _a   ]  <----------- 用偏移量找到这里

虚继承不是去掉基类成员,而是把基类成员挪到对象末尾,变成共享的一份!

创建一个 C 对象:

cpp 复制代码
C 对象:
┌─────────────┐
│ vbptr       │
├─────────────┤
│ _c          │
├─────────────┤
│ _a          │  ✅ 同样有 A!
└─────────────┘

创建 D 对象(B + C 虚继承 A)

cpp 复制代码
D 对象:
┌─────────────┐
│ B部分(vbptr+_b)
├─────────────┤
│ C部分(vbptr+_c)
├─────────────┤
│ _a          ← 【唯一一份!共享!】
├─────────────┤
│ _d
└─────────────┘
cpp 复制代码
D 对象(物理内存地址从低到高):
+-------------------------+  ← 低地址(对象开头)
| B 部分(vbptr + _b)    |
+-------------------------+
| C 部分(vbptr + _c)    |
+-------------------------+
| D 自己的成员 _d         |
+-------------------------+
| 虚基类 A(_a = 100)    |  ← 高地址(对象末尾)
+-------------------------+

只有 D 这种多继承派生类,才会把 A 合并成唯一一份

  • 最终对象布局

    • D 对象中:B 部分 (vbptr) + C 部分 (vbptr) + 唯一一份 A + D 自身成员
    • A 不再重复,无冗余、无二义性

**访问原理:**通过 vbptr → 查虚基类表 → 得到偏移 → 找到公共的基类成员

七、继承与友元、静态成员

cpp 复制代码
#include <iostream>
using namespace std;

class Person {
public:
    friend void show(const Person& p);
    static int cnt;

    Person() { cnt++; }

protected:
    string name = "Person";
};

int Person::cnt = 0;

class Student : public Person {};

void show(const Person& p) {
    cout << "访问:" << p.name << endl;
}

int main() {
    // 静态:整个继承体系共用一个
    Person p1, p2;
    Student s1, s2;

    cout << "总数:" << Person::cnt << endl;    // 4
    cout << Student::cnt << endl;;
    cout<<s2.cnt<<endl;
    cout << "地址一样:" << &Person::cnt << " " << &Student::cnt << endl;

    // 友元只作用于 Person,不继承给 Student
    Person p;
    show(p);   // 可以

    // Student s;
    // show(s); // 不行!友元不继承
}

1 静态成员属于类,不属于对象,存贮在静态变量/全局区,独一份

所有对象共享:父类、子类全都共用这一个

cpp 复制代码
static int cnt;
  • 不是每个 Person 自带一个 cnt
  • 整个 Person 类、整个继承体系共用这一个
  • 所以创建 p1、p2、s1、s2 时每构造一次,同一份 cnt++
  • 结果自然就是 4

2 友元关系不能被继承,父类友元函数不是子类的友元函数不能访问子类的私有,保护变量

八、继承 vs 组合(设计原则)

cpp 复制代码
#include <iostream>
#include <string>
#include<vector>
using namespace std;

// ========== 组合示例(has-a关系) ==========
// 轮胎类
class Tire {
public:
    Tire(const string& brand = "米其林", int size = 17)
        : _brand(brand), _size(size) {
        cout << "Tire构造: " << _brand << " " << _size << "寸" << endl;
    }

    void show() {
        cout << "轮胎品牌: " << _brand << ", 尺寸: " << _size << "寸" << endl;
    }

private:
    string _brand;
    int _size;
};

// 发动机类
class Engine {
public:
    Engine(int power = 200) : _power(power) {
        cout << "Engine构造: " << _power << "马力" << endl;
    }
    void show()
    {
        cout << "Engine构造: " << _power << "马力" << endl;
    }
private:
    int _power;
};

class Car {
public:
    // 给 tire 和 engine 也写上初始化!
    Car(const string& brand) : _brand(brand), _tire(), _engine() {
        cout << "Car构造: " << _brand << endl;
    }

    void show() {
        cout << "汽车品牌: " << _brand << endl;
        _tire.show();
        _engine.show();
    }

private:
    string _brand;
    Tire _tire;
    Engine _engine;
};

// ========== 继承示例(is-a关系) ==========
// 交通工具类
class Vehicle {
public:
    virtual void run() {
        cout << "交通工具在运行" << endl;
    }
};

// 汽车也是一种交通工具 - is-a关系
class Benz : public Vehicle {
public:
    void run() override {
        cout << "奔驰汽车在飞驰" << endl;
    }

    void luxury() {
        cout << "提供豪华配置" << endl;
    }
};

class BMW : public Vehicle {
public:
    void run() override {
        cout << "宝马汽车在狂飙" << endl;
    }

    void sport() {
        cout << "提供运动模式" << endl;
    }
};

// ========== 实际开发建议 ==========
// 栈的实现:既可以用继承,也可以用组合
template<typename T>
class StackByInheritance : public vector<T> {  // 继承方式
public:
    void push(const T& val) {
        vector<T>::push_back(val);
    }
    void pop() {
        vector<T>::pop_back();
    }
    T& top() {
        return vector<T>::back();
    }
};

template<typename T>
class StackByComposition {  // 组合方式(推荐)
public:
    void push(const T& val) {
        _v.push_back(val);
    }
    void pop() {
        _v.pop_back();
    }
    T& top() {
        return _v.back();
    }

private:
    vector<T> _v;  // 组合一个vector
};

int main() {
    cout << "=== 组合示例(has-a) ===" << endl;
    Car myCar("特斯拉");

    cout << "\n=== 继承示例(is-a) ===" << endl;
    Vehicle* v1 = new Benz();
    Vehicle* v2 = new BMW();
    v1->run();  // 多态
    v2->run();

    cout << "\n=== 设计原则 ===" << endl;
    cout << "1. 优先使用组合(耦合度低,更灵活)" << endl;
    cout << "2. 只有当确实是is-a关系时才使用继承" << endl;
    cout << "3. 需要多态时必须使用继承" << endl;
    cout << "4. 类之间的关系既适合继承也适合组合时,优先用组合" << endl;

    delete v1;
    delete v2;

    return 0;
}

注意:为什么继承 vector 时,必须写 vector<T>::push_back(val)

模板本身不是真正的类 / 函数,只是一张图纸。 因为编译时父类还没生成,所以调用父类函数必须写 父类<T>时,编译器才会根据图纸生成真正的代码(也就是按需实例化)

  1. 两种关系(最核心)

① 组合:has-a (有一个)

  • 汽车 有一个 轮胎
  • 汽车 有一个 发动机
  • 写法:类里面包含另一个类对象

构造顺序(组合):先构造成员对象 → 再构造自己

cpp 复制代码
1. 两种关系(最核心)
① 组合:has-a (有一个)
汽车 有一个 轮胎
汽车 有一个 发动机
写法:类里面包含另一个类对象

② 继承:is-a (是一个)

  • 奔驰 是一种 交通工具
  • 宝马 是一种 交通工具
  • 写法:class Benz : public Vehicle
  1. 设计原则(最重要!)

优先使用组合,少用继承!

  • 组合:耦合低、安全、灵活
  • 继承:耦合高、会继承所有接口,容易被乱用

例子:栈(Stack)

  • 用组合:只开放 push/pop/top
  • 用继承:能调用 vector 所有函数(不安全)

终极口诀

  1. is-a 用继承
  2. has-a 用组合
  3. 能组合绝不继承

九、练习

相关推荐
minji...2 小时前
Linux 网络基础(一)认识协议,网络协议,网络协议分层框架搭建,网络传输基本流程,跨网络的数据传输
linux·运维·服务器·网络·c++·网络协议
草木红2 小时前
Python 中使用 Docker Compose
开发语言·python·docker·flask
吃着火锅x唱着歌2 小时前
深度探索C++对象模型 学习笔记 第四章 Function语意学(1)
c++·笔记·学习
lsx2024062 小时前
PostgreSQL WITH 子句详解
开发语言
ID_180079054732 小时前
京东商品详情 API 数据分析业务场景 + JSON 返回参考
java·开发语言
周杰伦fans2 小时前
C# CAD二次开发:RotatedDimension 旋转标注完全指南
开发语言·c#
郝学胜-神的一滴2 小时前
Python魔法函数深度探索|从工具实操到核心应用,解锁语言底层的优雅密码
开发语言·数据库·人工智能·python·pycharm
她说彩礼65万2 小时前
C语言 函数指针
c语言·开发语言·算法
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【排序贪心】:纪念品分组
c++·算法·贪心·csp·信奥赛·排序贪心·纪念品分组