1、继承的概念与定义
1.1继承的概念
继承:是c++代码复用的手段,允许在原有的基础上扩展,在此之前都是函数层次的复用,继承是类设计层次的复用。
下面有两个类Student和Teacher都有姓名/地址/电话/年龄等成员变量。都有identity身份,设计到两个类⾥⾯就是冗余的。当然他们 也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣ 的独有成员函数是学习,⽼师的独有成员函数是授课。
#include<iostream>
#include<string>
using namespace std;
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;
int _stuid;
};
int main()
{
return 0;
}
下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就 不需要重复定义了,省去了很多⿇烦。
class Person
{
public:
void identity() { }
protected:
string _name="王五";
string _address;
string _tel;
int _age;
};
class Student:public Person
{
public:
void print()
{
cout << _name << endl;
}
void study() { }
protected:
int _stuid;
};
class Teacher:public Person
{
public:
void teaching() { }
protected:
string title;
};
int main()
{
Student s;
s.print();
return 0;
}
1.2 继承定义
1.2.1 定义下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以 既叫基类/派⽣类,也叫⽗类/⼦类)
1.2.2 继承基类成员访问⽅式的变化
如何记忆:记得权限大小 public>protected>private 起中private是最小的 所以它和谁配都小,成员变量存在但不能访问。由此可见谁小听谁的。
有一种最有趣,基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。 例子在第二个代码段。
1.3 继承类模板
如何实现用继承写一段栈呢
namespace bit
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public 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();
}
return 0;
}
注意当我们想写一个通用的迭代器打印的时候,容易发生编译器困惑的错误❌。
在这段 C++ 模板代码里,typename
不加会报错,
是因为编译器无法确定 Container::const_iterator
究竟是类型还是成员变量 。
在 C++ 模板中,当模板未实例化时,编译器不知道模板参数 Container
具体是什么类型。Container::const_iterator
这种嵌套依赖类型(其具体类型依赖于 Container
),它有两种可能身份:
-
类型:在类似
std::vector
或std::list
这些标准容器中,const_iterator
是一种类型,用于定义常量迭代器,能遍历容器元素。 -
成员变量:假如
Container
是个自定义类,它里面恰好有个叫const_iterator
的成员变量,编译器在模板实例化前无法知晓。
编译器没办法在编译阶段,模板还没实例化、不知道 Container
确切类型的情况下,自动判断出 Container::const_iterator
到底是类型还是成员变量 。如果不明确告诉编译器它是类型,编译器就会困惑,不知道该怎么处理这行代码,进而报错 。
解决方法
1 typename:关键字就是专门用来告知编译器,它后面跟着的是一个类型 。
2 写auto 倒数第二个有讲解
2.基类和派⽣类间的转换
在C++中,公有继承的派生类对象可以赋值给基类的指针或引用,这种现象被称为切片(Slicing)/切割。
2.1 什么是切片?
想象你有一个完整的三明治(派生类对象),它包含面包(基类部分)、火腿、蔬菜和奶酪(派生类特有部分)。
当你将整个三明治递给一个只吃面包的人(基类指针/引用),他只会拿走面包部分,而火腿和奶酪会被“切掉”并丢弃。
这就是切片:基类指针或引用只能访问派生类对象中属于基类的部分,派生类特有的成员被“切无法通过基类接口访问。
#include <iostream>
using namespace std;
// 基类:Animal
class Animal {
public:
string name;
int age;
Animal(string n, int a) : name(n), age(a) {}
};
// 派生类:Dog(公有继承Animal)
class Dog : public Animal {
public:
string breed; // 派生类特有成员(品种)
Dog(string n, int a, string b) : Animal(n, a), breed(b) {}
};
int main() {
Dog dog("Buddy", 3, "Golden Retriever");
// 基类引用绑定到派生类对象(切片发生)
Animal& animalRef = dog;
Animal* animalPtr = &dog;
Animal animal= dog;//在后面的基类拷贝构造有
// 可以访问基类成员
cout << "Name: " << animalRef.name << endl; // 输出:Buddy
cout << "Age: " << animalPtr->age << endl; // 输出:3
// 无法访问派生类特有成员(编译错误)
// cout << "Breed: " << animalRef.breed << endl; // 错误!
// cout << "Breed: " << animalPtr->breed << endl; // 错误!
return 0;
}
2.2为什么需要切片?
-
统一接口:将不同派生类对象统一视为基类对象处理,提高代码通用性。
例如:所有动物(基类)都可以调用eat()
,而具体实现由派生类(狗、猫)决定。 -
避免内存错误:强制类型安全,防止通过基类接口误操作派生类特有数据,没有临时变量参与。
2.3常见实例
函数参数传递
void printAnimal(const Animal& animal) {
cout << "Name: " << animal.name << ", Age: " << animal.age << endl;
}
int main() {
Dog dog("Buddy", 3, "Golden Retriever");
printAnimal(dog); // 传递派生类对象给基类引用(切片发生)
}
容器存储
vector<Animal> animals;
Dog dog("Buddy", 3, "Golden Retriever");
animals.push_back(dog); // 切片:容器中只存储Animal部分数据
3.继承中的作⽤域
3.1 独立作用域:基类和派生类各占山头
基类和派生类就像两个独立房间,各自定义的成员(变量/函数)默认只在各自房间可见。
class Base {
public:
int a = 10;
void print() { cout << "Base: " << a << endl; }
};
class Derived : public Base {
public:
int a = 20; // 隐藏基类的a
void print() { cout << "Derived: " << a << endl; } // 隐藏基类的print()
};
3.2 同名成员:派生类“盖住”基类
当基类和派生类有同名成员时,派生类成员会直接覆盖基类成员,如同用纸盖住桌上的字。
访问规则:
-
默认访问派生类成员。
-
要访问基类成员,需用
基类名::成员名
显式指明。Derived d; cout << d.a << endl; // 输出20(派生类的a) cout << d.Base::a << endl; // 输出10(显式访问基类的a) d.print(); // 调用派生类的print() d.Base::print(); // 显式调用基类的print()
3.3 函数隐藏:只看函数名,不管参数
- 只要函数名相同,无论参数是否相同,派生类函数都会隐藏基类所有同名函数。
class Base {
public:
void func(int x) { cout << "Base::func(int)" << endl; }
};
class Derived : public Base {
public:
void func() { cout << "Derived::func()" << endl; }
// 隐藏了Base::func(int)
};
Derived d;
d.func(); // 正确:调用Derived::func()
// d.func(5); // 错误!基类的func(int)被隐藏
d.Base::func(5); // 正确:显式调用基类函数
3.4继承作⽤域相关选择题
3.2.1 A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系
3.2.2 下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
3.2.2当子类(
B
)定义了一个与父类(A
)同名的函数(无论参数是否相同),子类会隐藏父类的所有同名函数。因此:b.fun(10);走的是B,b.fun();走的也是B但是没有提供所以编译报错,运行报错是代码本身有语法问题。答案B. 隐藏 A. 编译报错
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();
return 0;
};
4.继承中派生类的常见成员函数
4.1构造函数
1. 派生类构造函数必须调用基类构造函数
如果基类没有默认构造函数(即无参构造函数)派生类无需显示调用,但若没有默认构造有带参构造函数,派生类必须在初始化列表中显式调用基类的带参构造函数。
class Base {
public:
Base(int value) : data(value) {} // 基类只有带参构造函数
private:
int data;
};
class Derived : public Base {
public:
Derived(int baseValue, int derivedValue)
: Base(baseValue), derivedData(derivedValue) {} // 显式调用基类构造
private:
int derivedData;
};
2.派生类对象初始化顺序:基类构造→派生类构造
创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
class Base {
public:
Base() { cout << "Base Constructor" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived Constructor" << endl; }
};
// 使用示例
int main() {
Derived d;
return 0;
}
- 基类部分必须在派生类部分初始化之前完成构造,因为派生类可能依赖基类的状态。
4.2拷贝构造
1.派生类拷贝构造函数必须调用基类拷贝构造函数
派生类的拷贝构造函数需要显式调用基类的拷贝构造函数,以复制基类部分的成员。
class Base {
public:
Base(int value) : data(value) {}
Base(const Base& other) : data(other.data) {} // 基类拷贝构造函数
private:
int data;
};
class Derived : public Base {
public:
Derived(int value) : Base(value), derivedData(0) {}
// 派生类拷贝构造函数
Derived(const Derived& other)
: Base(other), // 显式调用基类拷贝构造
derivedData(other.derivedData) {}
private:
int derivedData;
};
-
当创建
Derived
对象的拷贝时,Derived
的拷贝构造函数必须先调用Base
的拷贝构造函数,确保基类部分被正确复制。 -
若不显式调用
Base(other)
,基类部分将使用默认构造函数初始化(而非复制),导致数据丢失。 -
如果基类没有定义拷贝构造函数,编译器会自动生成一个浅拷贝的拷贝构造函数。
-
浅拷贝足够时:无需自定义拷贝构造函数(如基类只有值类型成员)。
-
深拷贝需求:若基类包含动态资源(如指针),必须自定义拷贝构造函数,否则会导致多个对象共享同一资源,析构时重复释放。
4.3赋值运算符
派生类的operator=
必须调用基类的operator=
派生类的赋值运算符需要显式调用基类的赋值运算符,以复制基类部分的成员。
class Base {
public:
Base& operator=(const Base& other) {
if (this != &other) {
data = other.data;
}
return *this;
}
private:
int data;
};
class Derived : public Base {
public:
Derived& operator=(const Derived& other) {
if (this != &other) {
Base::operator=(other); // 显式调用基类赋值运算符
derivedData = other.derivedData;
}
return *this;
}
private:
int derivedData;
};
-
派生类的
operator=
会隐藏基类的operator=
,因此必须通过Base::operator=(other)
显式调用基类的赋值逻辑。 -
若不调用,基类部分将不会被赋值,导致对象状态不一致。
-
如果基类没有定义赋值运算符,编译器会自动生成一个浅拷贝的赋值运算
4.4析构函数
1.派生类析构函数自动调用基类析构函数
派生类析构函数执行完毕后,会自动调用基类的析构函数,确保资源按 “派生类→基类” 的顺序释放。
class Base {
public:
~Base() { cout << "Base Destructor" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived Destructor" << endl; }
};
// 使用示例
int main() {
Derived d; // 离开作用域时自动析构
return 0;
}
-
派生类对象销毁时,先执行自身的析构函数(清理派生类资源),再自动调用基类的析构函数(清理基类资源)。
-
这种顺序确保资源释放的完整性,避免基类资源在派生类资源之前被释放。
2.派生类对象析构顺序:派生类析构→基类析构
class Base {
public:
~Base() { cout << "Base Destructor" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived Destructor" << endl; }
};
// 使用示例
int main() {
Derived* d = new Derived();
delete d; // 显式释放对象
return 0;
}
操作 | 顺序 / 规则 | 关键原因 |
---|---|---|
构造函数 | 基类构造 → 派生类构造 | 派生类可能依赖基类初始化 |
拷贝构造函数 | 显式调用基类拷贝构造 | 确保基类部分被正确复制 |
赋值运算符 (operator= ) | 显式调用基类operator= | 避免基类部分未被赋值 |
析构函数 | 派生类析构 → 基类析构(自动调用) | 确保资源按正确顺序释放 |
4.5实现⼀个不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
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;
}
5.继承和友元
#include <iostream>
using namespace std;
class Base {
private:
int baseSecret = 10; // 基类的私有成员
public:
// 声明友元函数,允许访问基类的私有成员
friend void visitBase(Base& b);
};
class Derived : public Base {
private:
int derivedSecret = 20; // 派生类的私有成员
};
// 基类的友元函数
void visitBase(Base& b) {
cout << "访问基类的秘密: " << b.baseSecret << endl; // 允许访问
// cout << "尝试访问派生类的秘密: " << b.derivedSecret << endl; // 错误!无法访问
}
int main() {
Derived d;
visitBase(d); // 合法,但只能访问基类的成员
return 0;
}
如果想让友元访问派生类对象,就需要将派生类中加入友元。
6.继承与静态成员
派生类和基类共用一份静态成员变量。
#include<iostream>
using namespace std;
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;
}
7. 多继承及其菱形继承问题
7.1继承模型




产生问题
-
数据冗余:公共基类成员在派生类中存在多份副本。
-
二义性:访问公共成员时,编译器无法确定路径。
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;
}
7.2虚继承
解决菱形继承(钻石继承)中的两个核心问题:数据冗余和二义性。
-
Person
成员只保留一份,通过虚基表指针访问。
#include <iostream>
using namespace std;
class Person {
public:
string _name;
Person(const string& name) : _name(name) {}
};
class Student : virtual public Person { // 虚继承
public:
Student(const string& name) : Person(name) {}
};
class Teacher : virtual public Person { // 虚继承
public:
Teacher(const string& name) : Person(name) {}
};
class TeachingAssistant : public Student, public Teacher {
public:
// 最终派生类必须调用虚基类构造函数
TeachingAssistant(const string& name)
: Person(name), Student(name), Teacher(name) {}
};
int main() {
TeachingAssistant ta("Alice");
cout << ta._name << endl; // 直接访问,输出:Alice
return 0;
}
不规则菱形在哪写virtual
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;
return 0;
}
其中 P1是继承的第一个基类,P2是继承的第二个基类,p1与p3的位置相同P2将往后移。

7.4继承与组合
继承
-
继承表示派生类是基类的一种特殊类型,即“是一个(is-a)”的关系。
例如:狗是动物 →Dog
继承Animal
。
-
白箱复用:派生类可以直接访问基类的成员(包括保护成员),了解内部实现细节。
-
高耦合:基类改动可能影响所有派生类,维护成本高。
-
问题:继承破坏了基类的封装,鸡肋的 改变对派生类影响大,两者依赖性强,耦合度高。
class Animal {
public:
void breathe() { cout << "呼吸中..." << endl; }
};
class Dog : public Animal { // Dog is-a Animal
public:
void bark() { cout << "汪汪!" << endl; }
};
int main() {
Dog dog;
dog.breathe(); // 继承自Animal
dog.bark();
}
组合
-
组合表示一个类包含另一个类的对象,即“有一个(has-a)”的关系。
例如:汽车有发动机 →Car
包含Engine
。 -
黑箱复用:通过接口交互,无需了解内部实现。
-
低耦合:被组合类的修改不会直接影响组合类。
class Engine {
public:
void start() { cout << "引擎启动!" << endl; }
};
class Car {
private:
Engine engine; // Car has-a Engine
public:
void drive() {
engine.start();
cout << "汽车行驶中..." << endl;
}
};
int main() {
Car car;
car.drive();
}