目录
[1. 继承的概念及定义](#1. 继承的概念及定义)
[1.1 继承的概念](#1.1 继承的概念)
[1.2 继承定义](#1.2 继承定义)
[1.2.1 定义格式](#1.2.1 定义格式)
[1.2.2 继承基类成员访问方式的变化](#1.2.2 继承基类成员访问方式的变化)
[1.3 继承类模板](#1.3 继承类模板)
[2. 基类和派生类间的转换](#2. 基类和派生类间的转换)
[2.1 public继承的派生类对象赋值给基类指针或引用](#2.1 public继承的派生类对象赋值给基类指针或引用)
[2.2 基类对象不能赋值给派生类对象](#2.2 基类对象不能赋值给派生类对象)
[2.3 基类指针或引用强制转换为派生类指针或引用](#2.3 基类指针或引用强制转换为派生类指针或引用)
[2.4 使用dynamic_cast进行运行时类型检查](#2.4 使用dynamic_cast进行运行时类型检查)
[3.1 成员变量隐藏示例](#3.1 成员变量隐藏示例)
[3.2 成员函数隐藏示例](#3.2 成员函数隐藏示例)
[4 . 派生类默认成员函数的生成规则](#4 . 派生类默认成员函数的生成规则)
[4.2. 拷贝构造函数](#4.2. 拷贝构造函数)
[4.3. 赋值操作符 (operator=)](#4.3. 赋值操作符 (operator=))
[4.4. 析构函数](#4.4. 析构函数)
[4.5. 对象初始化顺序](#4.5. 对象初始化顺序)
[4.6. 对象析构顺序](#4.6. 对象析构顺序)
[4.7. 多态与析构函数](#4.7. 多态与析构函数)
[5. 继承与友元](#5. 继承与友元)
[6. 继承与静态成员](#6. 继承与静态成员)
[7. 多继承及其菱形继承问题](#7. 多继承及其菱形继承问题)
[7.1 继承模型](#7.1 继承模型)
[1. 单继承](#1. 单继承)
[2. 多继承](#2. 多继承)
[3. 菱形继承(钻石继承)](#3. 菱形继承(钻石继承))
[4. 菱形继承的两大问题](#4. 菱形继承的两大问题)
[① 数据冗余](#① 数据冗余)
[② 二义性](#② 二义性)
[5. 菱形继承的解决方案:虚继承(virtual)](#5. 菱形继承的解决方案:虚继承(virtual))
[6. 重要结论](#6. 重要结论)
1. 继承的概念及定义
1.1 继承的概念
继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法 (成员函数) 和属性 (成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类 Student 和Teacher ,Student 和Teacher都有姓名 / 地址 /电话 / 年龄等成员变量,都有 identity 身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
cpp
#include<iostream>
using namespace std;
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
我们可以将公共成员统一放在Person类中,让Student和Teacher类继承Person。这样既能实现代码复用,又能避免重复定义成员变量,大大简化了开发工作。
代码示例分析
以下是合并后的代码示例:
cpp
class Person
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "身份验证:" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
代码功能说明
代码定义了一个基类Person和两个派生类Student和Teacher,展示了面向对象编程中的继承特性。
Person类包含身份验证方法和基本个人信息。Student和Teacher类继承Person,并添加了各自特有的方法和属性。
在main函数中创建了Student和Teacher对象,并调用了继承自Person的identity方法。
1.2 继承定义
1.2.1 定义格式



1.2.2 继承基类成员访问方式的变化

-
使用关键字
class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式写出继承方式。 -
基类
private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。 -
基类
private成员在派生类中是不能被访问的;如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。 -
基类的私有成员在派生类中都是不可见。基类的其他成员在派生类的访问方式 == Min (成员在基类的访问限定符,继承方式),权限大小:public > protected > private。
-
在实际运用中一般使用都是
public继承,几乎很少使用protected/private继承,也不提倡使用,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
1.3 继承类模板
cpp
#include<iostream>
#include<vector>
using namespace std;
namespace bit
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template < class T>
class stack : public std::vector < T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: "push_back": 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
int main()
{
bit::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
//3 2 1
return 0;
}
2. 基类和派生类间的转换
2.1 public继承的派生类对象赋值给基类指针或引用
在C++中,通过public继承的派生类对象可以赋值给基类的指针或引用。这种行为通常被称为"切片"(slicing),因为派生类对象中只有基类部分被保留,派生类特有的部分会被丢弃。基类指针或引用仅能访问派生类对象中的基类部分成员。
2.2 基类对象不能赋值给派生类对象
基类对象无法直接赋值给派生类对象,因为派生类可能包含基类中没有的成员。如果尝试直接赋值,编译器会报错。
cpp
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;
// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷贝构造完成的
Person pobj = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
sobj = pobj;
return 0;
}

补充:
2.3 基类指针或引用强制转换为派生类指针或引用
基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但这种转换仅在基类指针实际指向派生类对象时才是安全的。如果基类指针指向的是真正的基类对象,强制转换会导致未定义行为。
cpp
Base* b_ptr = new Derived; // 基类指针实际指向派生类对象
Derived* d_ptr = static_cast<Derived*>(b_ptr); // 安全转换
2.4 使用dynamic_cast进行运行时类型检查
如果基类是多态类型(至少包含一个虚函数),可以使用dynamic_cast进行安全的向下转换。dynamic_cast会在运行时检查类型是否匹配,如果不匹配则返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。
cpp
Base* b_ptr = new Derived;
Derived* d_ptr = dynamic_cast<Derived*>(b_ptr); // 安全转换
if (d_ptr) {
// 转换成功
} else {
// 转换失败
}
总结
- public继承的派生类对象可以安全赋值给基类指针或引用(切片)。
- 基类对象不能直接赋值给派生类对象。
- 基类指针或引用可以通过
static_cast强制转换为派生类指针或引用,但需确保实际指向派生类对象。 - 对于多态类型,优先使用
dynamic_cast进行安全的运行时类型检查。
3.继承中的作用域隐藏规则
在C++继承体系中,基类和派生类拥有独立的作用域。当派生类与基类存在同名成员时,派生类成员会屏蔽基类对同名成员的直接访问,这种现象称为隐藏。
派生类可以通过基类::基类成员语法显式访问被隐藏的基类成员。对于成员函数,只需函数名相同即可构成隐藏,与参数列表无关。
3.1 成员变量隐藏示例
cpp
class Person {
public:
int age = 30;
};
class Student : public Person {
public:
int age = 20; // 隐藏基类的age成员
void printAge() {
cout << age << endl; // 输出20(派生类成员)
cout << Person::age << endl; // 输出30(显式访问基类成员)
}
};
3.2 成员函数隐藏示例
cpp
class Base {
public:
void func() { cout << "Base::func()" << endl; }
};
class Derived : public Base {
public:
void func(int) { cout << "Derived::func(int)" << endl; } // 隐藏基类func()
void test() {
func(1); // 正确:调用派生类函数
Base::func(); // 正确:显式调用基类函数
// func(); // 错误:基类无参版本被隐藏
}
};
最佳实践建议
在实际开发中应尽量避免在继承体系内定义同名成员,这会显著降低代码可读性并增加维护难度。若必须使用同名成员,建议通过显式作用域解析运算符::明确指定访问路径。
4 . 派生类默认成员函数的生成规则


4.1构造函数
在C++中,派生类(如Student继承自Person)的默认成员函数由编译器自动生成,但需遵循特定规则以确保基类和派生类成员的正确初始化、拷贝、赋值和销毁。以下是基于C++标准的行为逐步解释:
-
派生类构造函数必须显式调用基类构造函数(通过初始化列表),以初始化基类部分成员。
-
若基类没有默认构造函数(如
Person定义了带参构造),派生类必须在初始化列表中显式调用基类构造函数。 -
示例:
cppclass Person { public: Person(int id) : _id(id) {} // 基类无默认构造 private: int _id; }; class Student : public Person { public: Student(int id, int score) : Person(id) { // 显式调用基类构造 // 派生类成员初始化... } private: int _score; };
4.2. 拷贝构造函数
-
派生类拷贝构造函数必须调用基类拷贝构造函数(通过初始化列表),完成基类部分的深拷贝。
-
编译器生成的默认拷贝构造会自动调用基类拷贝构造。
-
示例:
cppclass Student : public Person { public: Student(const Student& other) : Person(other) { // 调用基类拷贝构造 // 派生类成员拷贝... } };
4.3. 赋值操作符 (operator=)
-
派生类
operator=必须显式调用基类operator=(通过作用域解析符::),因为派生类版本会隐藏基类版本。 -
调用格式:
BaseClass::operator=(other)。 -
示例:
cppclass Student : public Person { public: Student& operator=(const Student& other) { if (this != &other) { Person::operator=(other); // 显式调用基类赋值 // 派生类成员赋值... } return *this; } };
4.4. 析构函数
-
派生类析构函数执行完成后,编译器自动调用基类析构函数(无需显式调用)。
-
销毁顺序:派生类成员先销毁,然后基类成员销毁(逆序于构造顺序)。
-
示例:
cppclass Student : public Person { public: ~Student() { // 派生类资源清理... } // 此处自动调用 ~Person() };
4.5. 对象初始化顺序
- 构造顺序:先调用基类构造函数 ,再调用派生类构造函数。
- 示例:
Student s;先执行Person(),再执行Student()。
4.6. 对象析构顺序
- 析构顺序:先调用派生类析构函数 ,再调用基类析构函数。
- 示例:
Student对象销毁时,先执行~Student(),再执行~Person()。
4.7. 多态与析构函数
- 基类析构函数应声明为
virtual以支持多态(如virtual ~Person())。 - 若基类析构非虚函数:
- 派生类析构函数与基类析构函数构成隐藏关系(非重写)。
- 编译器将析构函数名统一处理为
destructor(),避免名称冲突。
- 重写条件:基类析构为虚函数时,派生类析构通过
override实现重写。
cpp
// 1. 基类:人类 Person
class Person
{
public:
// 2. 基类 带缺省参数的构造函数
// 功能:初始化姓名 _name
Person(const char* name = "peter")
: _name(name) // 3. 初始化列表:初始化 string 成员 _name
{
cout<<"Person()" <<endl; // 4. 打印:调用基类构造
}
// 5. 基类 拷贝构造函数
// 功能:用另一个 Person 对象拷贝初始化
Person(const Person& p)
: _name(p._name) // 6. 初始化列表:拷贝 _name
{
cout<<"Person(const Person& p)" <<endl; // 7. 打印:调用基类拷贝构造
}
// 8. 基类 赋值运算符重载
// 功能:把一个 Person 对象赋值给当前对象
Person& operator=(const Person& p )
{
cout<<"Person operator=(const Person& p)"<< endl; // 9. 打印:调用基类赋值重载
if (this != &p) // 10. 防止自己给自己赋值
_name = p ._name; // 11. 赋值姓名
return *this ; // 12. 返回自身,支持连续赋值
}
// 13. 基类 析构函数
~Person()
{
cout<<"~Person()" <<endl; // 14. 打印:调用基类析构
}
protected: // 15. 保护成员:子类可以访问,外部不能访问
string _name ; // 16. 成员变量:姓名
};
// 17. 派生类:学生 Student,公有继承 Person
class Student : public Person
{
public:
// 18. 派生类 构造函数
// 功能:初始化姓名 + 学号
Student(const char* name, int num)
: Person(name) // 19. 必须显式调用基类构造,初始化继承的 _name
, _num(num ) // 20. 初始化派生类自己的成员 _num
{
cout<<"Student()" <<endl; // 21. 打印:调用派生类构造
}
// 22. 派生类 拷贝构造函数
// 功能:用另一个 Student 对象拷贝初始化
Student(const Student& s)
: Person(s) // 23. 切片:把 s 当作 Person,调用基类拷贝构造
, _num(s ._num) // 24. 拷贝派生类自己的成员
{
cout<<"Student(const Student& s)" <<endl ; // 25. 打印:调用派生类拷贝构造
}
// 26. 派生类 赋值运算符重载
// 功能:把一个 Student 对象赋值给当前对象
Student& operator = (const Student& s )
{
cout<<"Student& operator= (const Student& s)"<< endl; // 27. 打印:调用派生类赋值重载
if (this != &s) // 28. 防止自赋值
{
// 29. 子类和基类的 operator= 构成隐藏,必须显式调用基类赋值
Person::operator =(s);
_num = s ._num; // 30. 赋值派生类自己的成员
}
return *this ; // 31. 返回自身
}
// 32. 派生类 析构函数
~Student()
{
cout<<"~Student()" <<endl; // 33. 打印:调用派生类析构
// 34. 编译器会自动调用基类析构 ~Person(),不需要手动写
}
protected:
int _num ; // 35. 派生类自己的成员变量:学号
};
int main()
{
// 36. 调用 Student 构造 → 先调用 Person 构造 → 再执行 Student 构造
Student s1 ("jack", 18);
// 37. 调用 Student 拷贝构造 → 先调用 Person 拷贝构造 → 再执行 Student 拷贝构造
Student s2 (s1);
// 38. 调用 Student 构造 → 先调用 Person 构造 → 再执行 Student 构造
Student s3 ("rose", 17);
// 39. 调用 Student 赋值重载 → 显式调用 Person 赋值重载 → 赋值 _num
s1 = s3 ;
// 40. main 结束,局部对象析构:先析构派生类 ~Student → 再自动析构基类 ~Person
return 0;
}
cpp
Person()
Student()
Person(const Person& p)
Student(const Student& s)
Person()
Student()
Student& operator= (const Student& s)
Person operator=(const Person& p)
~Student()
~Person()
~Student()
~Person()
~Student()
~Person()
4.8实现不能被继承的类办法
方法1:基类构造函数私有化
将基类的构造函数设为私有,派生类无法访问基类的私有成员,包括构造函数。派生类在实例化时必须调用基类的构造函数,但由于构造函数不可见,导致编译错误。
cpp
class Base {
private:
Base() {} // 私有构造函数
};
// 编译错误:无法访问私有构造函数
class Derived : public Base {
public:
Derived() {}
};
方法2:使用C++11的final关键字
C++11引入final关键字,可直接修饰类,禁止其他类继承。语法简洁,直接明确类的不可继承性。
cpp
class Base final { // 使用final禁止继承
public:
Base() {}
};
// 编译错误:Base被声明为final,无法继承
class Derived : public Base {
public:
Derived() {}
};
注意事项
- 方法1需确保基类提供静态工厂方法或其他方式实例化自身对象。
- 方法2需编译器支持C++11或更高标准,现代编译环境普遍兼容。
5. 继承与友元
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员 。
cpp
// 前向声明:告诉编译器 "Student" 是一个类,后面会定义
// 因为 Person 的友元函数里用到了 Student,必须先声明它的存在
class Student;
// 定义基类 Person
class Person
{
public:
// 声明 Display 函数为 Person 的友元
// 友元的作用:Display 可以直接访问 Person 的 protected/private 成员
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名(保护成员:子类可以直接访问,外部类/普通函数不行)
};
// 定义派生类 Student,公有继承 Person
class Student : public Person
{
protected:
int _stuNum; // 学号(保护成员:子类可以直接访问,外部类/普通函数不行)
};
// 定义友元函数 Display
void Display(const Person& p, const Student& s)
{
// 可以访问 Person 的 protected 成员,因为 Display 是 Person 的友元
cout << p._name << endl;
// ❌ 编译报错!这里不能访问 Student 的 _stuNum
// 原因:友元关系**不能继承**,Display 只是 Person 的友元,不是 Student 的友元
// 即使 Student 继承了 Person,Person 的友元也不能访问 Student 的 protected/private 成员
cout << s._stuNum << endl;
}
int main()
{
Person p; // 创建基类 Person 对象
Student s; // 创建派生类 Student 对象
// 调用 Display 函数,此时编译会报错
Display(p, s);
return 0;
}
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有⼀个static成员实例。
cpp
// 基类 Person
class Person
{
public:
string _name; // 非静态成员变量:每个对象都有自己独立的一份
static int _count; // 静态成员变量:整个类体系中只有一份,所有对象共享
};
// 静态成员变量必须在类外初始化(类内只是声明)
// 作用:为整个继承体系中唯一的那个 _count 分配内存并赋初值
int Person::_count = 0;
// 派生类 Student,公有继承 Person
class Student : public Person
{
protected:
int _stuNum; // 派生类自己的非静态成员变量:每个 Student 对象各有一份
};
int main()
{
// 创建基类对象
Person p;
// 创建派生类对象
Student s;
// 非静态成员 _name:p 和 s 各有一份,地址不同
cout << &p._name << endl; // 输出 p 的 _name 地址
cout << &s._name << endl; // 输出 s 的 _name 地址
// 结论:非静态成员属于具体对象,每个对象都有独立的副本
// 静态成员 _count:整个继承体系只有一份,p 和 s 访问的是同一个地址
cout << &p._count << endl; // 输出 p 访问的 _count 地址
cout << &s._count << endl; // 输出 s 访问的 _count 地址
// 结论:静态成员属于整个类(包括派生类),所有对象共享同一份
// 静态成员的访问方式1:通过类名::静态成员访问
// 基类方式访问:Person::_count
cout << Person::_count << endl;
// 派生类方式访问:Student::_count(继承了基类的静态成员)
cout << Student::_count << endl;
// 两种方式访问的是同一个变量,值完全相同
return 0;
}
| 特性 | 非静态成员(如 _name) | 静态成员(如 _count) |
|---|---|---|
| 副本数量 | 每个对象都有独立的副本 | 整个类体系中只有一个副本 |
| 内存分配位置 | 随对象分配在栈 / 堆上 | 分配在全局静态存储区 |
| 访问方式 | 只能通过对象访问 | 可通过对象或类名::访问 |
| 继承后的表现 | 派生类对象会继承一份独立副本 | 派生类和基类共享同一份静态成员 |
7. 多继承及其菱形继承问题
7.1 继承模型
1. 单继承
定义 :一个派生类只有一个直接基类 ,这种继承关系称为单继承。
- 语法:
class 派生类 : 继承方式 基类 - 特点:结构简单,无歧义、无数据冗余问题。
2. 多继承
定义 :一个派生类有两个或两个以上直接基类 ,这种继承关系称为多继承。
多继承语法
cpp
// 派生类同时继承 基类1、基类2 ...
class 派生类 : 继承方式 基类1, 继承方式 基类2, ...
{
派生类新增成员;
};
多继承对象内存模型
核心规则:
- 按照继承顺序,先继承的基类成员放在内存前面;
- 后继承的基类成员放在中间;
- 派生类自己的成员放在内存最后面。
示例:
cpp
class A { int a; };
class B { int b; };
// 多继承:先继承A,再继承B
class C : public A, public B { int c; };
内存布局:A的成员(a) → B的成员(b) → C自己的成员(c)


3. 菱形继承(钻石继承)
定义 :菱形继承是多继承的特殊情况。
- 结构:
- 有一个公共基类(Person);
- 两个派生类(Student、Teacher)同时继承它;
- 最后一个类(Assistant)同时继承 Student 和 Teacher。
结构示意图:

4. 菱形继承的两大问题
① 数据冗余
Assistant 对象中,会包含两份 Person 类的成员(一份来自 Student,一份来自 Teacher),造成内存浪费。
② 二义性
当访问 Person 的成员时,编译器不知道使用Student 继承的那份 还是Teacher 继承的那份,直接编译报错。
示例代码(问题演示):
cpp
// 公共基类
class Person {
public:
int _id; // 身份证号
};
// 两个中间类
class Student : public Person { int _stuId; };
class Teacher : public Person { int _teaId; };
// 菱形继承:同时继承 Student + Teacher
class Assistant : public Student, public Teacher {};
int main() {
Assistant a;
// a._id = 1; // ❌ 编译报错:二义性,不知道是 Student 的 _id 还是 Teacher 的 _id
return 0;
}
5. 菱形继承的解决方案:虚继承(virtual)
虚继承 :让最终派生类只保留一份公共基类成员,解决冗余和二义性。
语法:
cpp
class 派生类 : virtual 继承方式 基类 {};
修正代码:
cpp
class Person { int _id; };
// 虚继承
class Student : virtual public Person { int _stuId; };
class Teacher : virtual public Person { int _teaId; };
// 现在 Assistant 中只有一份 Person 成员
class Assistant : public Student, public Teacher {};
6. 重要结论
- 支持多继承 → 必然会产生菱形继承;
- Java 等语言直接禁止多继承,从根源规避问题;
- 工程实践中:不建议设计菱形继承,复杂、易出错、可读性差;
- 必须使用时:用虚继承解决数据冗余和二义性。
总结
- 单继承:一个子类一个父类,简单安全;
- 多继承:一个子类多个父类,内存按继承顺序排列;
- 菱形继承 :多继承的特殊情况,有数据冗余 + 二义性缺陷;
- 解决方法 :使用虚继承(virtual);
- 工程建议:尽量避免使用菱形继承。