【C++】继承详解:从入门到理解底层原理

🔥铅笔小新z:个人主页
🎬博客专栏:C++学习
💫滴水不绝,可穿石;步履不休,能至渊。

引言
大家好,这篇文章我们来聊聊C++面向对象编程非常重要的一个概念-----继承。
很多初学者会觉得继承很难理解,其实它就像现实生活中的"子承父业"。
比如,儿子继承了父亲的基因,既有父亲的特点,又发展出自己的独特之处。在C++中,继承也是这个道理。
一、继承的概念及定义
1.1 为什么要用继承?
想象一下,我们要在学校系统设置两个类:Student(学生)和Teacher(老师)。
没有继承时,代码是这样的:
class Student
{
public:
void identity() { /* 身份认证 */ }
void study() { /* 学习 */ }
protected:
string _name; // 姓名
string _address; // 地址
string _tel; // 电话
int _age; // 年龄
int _stuid; // 学号(学生独有)
};
class Teacher
{
public:
void identity() { /* 身份认证 */ }
void teaching() { /* 授课 */ }
protected:
string _name; // 姓名
string _address; // 地址
string _tel; // 电话
int _age; // 年龄
string _title; // 职称(老师独有)
};
发现没有?学生和老师都有姓名、地址、电话、年龄这些共同的属性和身份认证这个行为,代码重复了!这就是冗余。
继承就能解决这个问题:把公共的部分提取出来,作为一个“基类”(也叫父类),然后让学生和老师去“继承”它,这样公共代码就能服用了。
1.2 继承的定义格式
// 先定义一个基类 Person
class Person
{
public:
void identity()
{
cout << "身份认证" << endl;
}
protected:
string _name = "张三";
string _address;
string _tel;
int _age = 18;
};
// Student 继承 Person
class Student : public Person
{
public:
void study() { /* ... */ }
protected:
int _stuid; // 学号
};
// Teacher 继承 Person
class Teacher : public Person
{
public:
void teaching() { /* ... */ }
protected:
string _title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity(); // 调用从Person继承来的成员函数
t.identity();
return 0;
}
格式说明:
class 子类(派生类)名 :继承方式 父类(基类)名
- 继承的方式有三种:public(公有继承)、protected(保护继承)、private(私有继承)。
- 如果不写,class 默认是 private 继承,struct 默认是 public 继承。实际开发中最常用的是 public 继承。
1.3 继承后成员访问权限的变化
基类成员在派生类中的访问权限,是由基类中的访问限定符和继承方式共同决定的,规则如下表:
| 基类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
简单记忆:
- 基类的 private 成员,无论什么继承,在派生类中都不能直接访问(但确实被继承下来了,只是语法限制不能访问)。
- 其他成员的访问权限 = min(成员在基类的权限,继承方式),其中 public > protected > private。
- 如果想在派生类中访问基类的某个成员,又不希望类外直接访问,就把它定义为 protected 。保护成员限定符就是为继承而生的。
小提示:实际工程中几乎只用 public 继承,因为 protected\private 继承会限制派生类的使用,维护性差。
1.4 继承类模板
C++ 允许我们继承一个模板类。比如STL中的 vector ,我们可以继承它来实现一个栈:
#include <iostream>
#include <vector>
using namespace std;
namespace xx
{
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 注意:必须用 vector<T>::push_back 指定类域,否则编译报错
vector<T>::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();
}
return 0;
}
注意:因为模板是按需实例化,如果直接写 push_back(x),编译器可能找不到,所以要加上 vector<T>:: 来指定类域。
二、基类和派生类间的转换(切片)
派生类对象可以赋值给基类的对象/指针/引用,这叫“切片”或“切割”,就是把派生类中从基类继承来的那部分切出来。
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; // 引用派生类中的基类部分
// 2. 派生类对象 赋值给 基类对象(调用基类的拷贝构造)
Person pobj = sobj;
// 3. 基类对象 不能 赋值给派生类对象
// sobj = pobj; // 编译错误!
return 0;
}
为什么基类对象不能赋值给派生类对象?
因为基类对象缺少派生类的特有成员,强行赋值会导致信息丢失。
基类指针/引用 强制转换为 派生类的指针/引用:
如果确定基类指针指向的是派生类对象,可以用强制类型转换。但存在风险,后续学了多态和 dynamic_cast 会更安全。
三、继承中的作用域(隐藏规则)
- 每个类都有自己的独立作用域。
- 如果派生类和基类有同名成员,派生类成员会隐藏基类同名成员(及直接访问时访问到的时派生类的)。要访问基类的隐藏成员,必须加上基类的作用域:基类::成员。
- 特别注意:成员函数的隐藏只要求函数名相同,不要求参数相同!哪怕参数列表不同,也是隐藏,不是重载(重载必须在同一个作用域)。
示例1:同名成员变量
class Person
{
protected:
string _name = "小李子";
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl; // 必须加作用域
cout << "学号:" << _num << endl; // 这里的_num是派生类的
}
protected:
int _num = 999; // 学号
};
示例2:成员函数隐藏
看下面这个选择题:
class A
{
public:
void fun() { cout << "func()" << endl; }
};
class B : public A
{
public:
void fun(int i) { cout << "func(int i)" << i << endl; }
};
int main()
{
B b;
b.fun(10); // 调用 B::fun(int)
// b.fun(); // 编译错误!因为B中的fun隐藏了A中的fun,无参版本不可见
return 0;
}
问题:A 和 B 中的两个 fun 构成什么关系?
A. 重载 B. 隐藏 C. 没关系
答案:B(隐藏)。因为函数名相同,即使参数不同,也构成隐藏。重载必须在同一个作用域,这里 A 和 B 是两个作用域。
建议:在实际开发中,尽量避免在继承体系中定义同名成员,以免混淆。
四、派生类的默认成员函数
我们知道,一个类有6个默认成员函数(构造、拷贝构造、赋值、析构、取地址、const 取地址)。在派生类中,这些函数的生成规则则有特别之处。
4.1 构造函数
- 派生类的构造函数必须调用基类的构造函数来初始化基类部分。
- 如果基类没有默认构造函数(即需要传参的构造函数),那么派生类必须在初始化列表中显式调用基类的构造函数。
4.2 拷贝构造函数
- 派生类的拷贝构造必须调用基类的拷贝构造来完成基类部分的拷贝。
4.3 赋值运算符重载
- 派生类的 operator= 必须调用基类的 operator= 来完成基类部分的赋值。
- 注意:派生类的 operator= 会隐藏基类的 operator= ,所以要显式调用,加上基类作用域。
4.4 析构函数
- 派生类的析构函数在执行完自己的清理后,会自动调用基类的析构函数,保证先派生后基类的析构顺序。
- 编译器会对析构函数名做特殊处理,统一处理成 destructor() ,所以基类和派生类的析构函数也会构成隐藏(如果基类析构没有加 virtual,后面多态会讲)。
4.5 构造和析构顺序
- 构造顺序:先基类构造,再派生类构造。
- 析构顺序:先派生类析构,再基类析构(与构造相反)。
看一个完整例子:
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name) // 调用基类构造
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s) // 调用基类拷贝构造
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s); // 必须显式调用基类赋值
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
// 派生类析构结束后会自动调用基类析构
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
运行结果:
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.6 实现一个不能被继承的类
有时候我们希望某个类不能被其他类继承,C++98 和 C++11 有两种方法:
方法1(C++98):将基类的构造函数设为私有。这样派生类无法调用基类构造,就不能实例化派生类对象。
class Base
{
private:
Base() {} // 私有构造
public:
static Base getInstance() { return Base(); } // 可以提供一个静态方法创建对象
};
// class Derive : public Base { }; // 编译错误,无法调用Base的私有构造
方法2(C++11):使用 final 关键字,直接禁止继承。
class Base final
{
// ...
};
// class Derive : public Base { }; // 编译错误:无法从final类型继承
五、继承与友元
友元关系不能继承。意思是:如果基类有一个友元函数,这个友元函数不能访问派生类的私有和保护成员。
class Student; // 前向声明
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuNum;
};
// 这个友元函数只能访问Person的成员,不能访问Student的成员
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; // 正确
// cout << s._stuNum << endl; // 错误!无法访问
}
解决方案:如果想让 Display 也能访问 Student 的私有成员,就需要在 Student 中也将其声明为友元。
六、继承与静态成员
如果基类定义了一个 static 成员,那么在整个继承体系中,只有一个这样的成员实例,无论派生出多少个子类,所有的对象都共享这一个静态成员。
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;
cout << &p._name << endl; // 不同对象的_name地址不同
cout << &s._name << endl;
cout << &p._count << endl; // 静态成员地址相同
cout << &s._count << endl;
// 可以通过类名访问
cout << Person::_count << endl;
cout << Student::_count << endl; // Student::_count 实际上就是 Person::_count
return 0;
}
输出显示,_count 的地址在 p 和 s 中是一样的,说明所有对象共享同一个静态成员。
七、多继承及其菱形继承问题
7.1 继承模型
- 单继承:一个派生类只有一个直接基类。
- 多继承:一个派生类有两个或以上直接基类。
- 菱形继承:多继承的一种特殊情况,形如:
Person
/ \
Student Teacher
\ /
Assistant
菱形继承的问题:
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()
{
Assistant a;
// a._name = "peter"; // 编译错误:对_name的访问不明确(二义性)
// 解决方法:指定访问哪个基类的成员
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
// 但数据冗余仍然存在:a对象中有两份Person的成员(_name)
return 0;
}
问题总结:
- 二义性:访问 _name 时不知道是从 Student 来的还是从 Teacher 来的。
- 数据冗余:Assistant 对象中包含两份 Person 的成员,浪费内存。
7.2 虚继承解决菱形继承
C++ 提供了虚继承来解决菱形继承问题。通过在中间层(Student 和 Teacher)继承 Person 时加上 virtual 关键字,使得最终派生类 Assistant 中只保留一份 Person 的成员。
class Person
{
public:
string _name;
};
// 虚继承 Person
class Student : virtual public Person
{
protected:
int _num;
};
class Teacher : virtual public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;
};
int main()
{
Assistant a;
a._name = "peter"; // 现在可以直接访问,没有二义性,且只有一份_name
return 0;
}
虚继承的底层原理:编译器会在对象中增加一些额外信息(如虚基类指针),用来定位虚基类子对象的位置,从而实现共享。这会使对象模型变复杂,性能略有损失,所以不要轻易设计菱形继承。
7.3 多继承中指针偏移问题
看下面这道选择题:
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;
// p1, p2, p3 的值有什么关系?
return 0;
}
答案:p1 == p3 != p2 (选项C)。
解释:因为先继承 Base1,后继承 Base2,在内存布局中,Base1 的子对象在低地址,Base2 的子对象在高地址。p1 指向 d 中 Base1 部分,p3 指向整个 Derive 对象,它们的起始地址相同(因为 Base1 在最最前面)。而 p2 指向 Base2 部分,地址偏移了 sizeof(Base1),所以 p2 与 p1 、p3 不同。
八、继承和组合
8.1 两种复用方式
- 继承(is-a关系):“派生类是一个基类”。比如 Student 是一个人(Person)。
- 组合(has-a关系):“类中包含另一个类的对象”。比如 Car 包含 Tire(轮胎)。
继承示例:
class Car {
public:
string _colour = "白色";
string _num = "陕A12345";
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
组合示例:
class Tire {
protected:
string _brand = "Michelin";
size_t _size = 17;
};
class Car {
protected:
string _colour;
string _num;
Tire _t1, _t2, _t3, _t4; // 汽车包含轮胎
};
8.2 继承 vs 组合的选择
- 继承是“白箱复用”:基类的内部细节对派生类可见,因此破坏封装性,耦合度高。基类的改变会影响所有派上类。
- 组合是“黑箱复用”:对象之间通过接口交互,内部细节不可见,耦合度底,维护性好。
优先使用对象组合,而不是继承----这是面向对象设计的一个重要原则。但也不是绝对的:
- 如果类之间确实存在明确的 is-a 关系,或者需要利用多态(后续会讲),就用继承。
- 如果即可以用继承也可以用组合,优先选择组合。
一个有趣的例子:栈(stack)和向量(vector)的关系。栈可以继承 vector(is-a?栈是一个向量?不太合理),也可以组合 vector(has-a:栈内部包含一个 vector)。实际 STL 中的 stack 是一个容器适配器,它通过组合实现,而不是继承。这体现了组合的灵活性。
总结
今天我们详细学习了C++继承的方方面面:
-
继承的概念:代码复用,is-a关系。
-
继承的语法:继承方式,访问权限变化。
-
切片:派生类对象可以赋值给基类指针/引用。
-
隐藏规则:同名成员,基类成员被隐藏,需加作用域访问。
-
派生类的默认成员函数:构造、拷贝、赋值、析构的调用规则。
-
友元与静态成员:友元不继承,静态成员共享一份。
-
多继承与菱形继承:菱形继承的问题及虚继承的解决方案。
-
继承与组合:两种复用方式的对比,优先使用组合。

更多推荐


所有评论(0)