C++继承全揭秘:原来编程也能“拼爹”
前言:从这个博客开始,我们就正式就进入C++进阶篇了,这篇文章与类和对象紧密关联,忘了的可以翻一下这篇博客“C++ 类与对象”。
目录
一、继承的概念与基础语法
1. 继承的引入
我们现实生活中有很多东西相似但不相同,比如老师与学生都有姓名/地址/ 电话/年龄等属性,都需要⾝份认证这一行为,但老师还有学生没有的职称,还要授课,学生有老师没有的学号,要学习,现要设计两个类,分别是Student和Teacher,要求他们包含以上属性及行为,那我们怎么写,应该是:
class Student {
public:
void identity() { /* 身份验证实现 */ }
void study() { /* 学习功能实现 */ }
protected:
string _name;
string _address;
string _tel;
int _age;
int _stud; // 学号
};
class Teacher {
public:
void identity() { /* 身份验证实现 */ }
void teaching() { /* 授课功能实现 */ }
protected:
string _name;
int _age;
string _address;
string _tel;
string _title; // 职称
};
这样一看,重复的东西太多了,但我们还要写两次,有没有办法只写一次呢?这就要用到C++继承的概念了。
2. 继承的定义与作用
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类(称基类/父类)特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类/子类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触了函数层次的复⽤,继承是类设计层次的复⽤。
3. 继承的语法
class father {};
class son : 继承方式 father {};
//这样son就继承了father类的全部特征,但继承了不代表就能使用!
虽然3个单词一样,但它们在不同位置的叫法和用处都是不一样的
所以,我们上面的Student和Teacher就可以这样写:
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 _stud; // 学号
};
class Teacher : public Person {
public:
void teaching() { /* 授课功能实现 */ }
protected:
string _title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
4. 访问权限变化规则
| 基类成员访问权限 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
知识要点:
(1)基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。但是间接访问还是行,比如父类提供get***函数等。
class father { public: int geta() { return a; } private: int a = 1; }; class son: public father { public: int b; };
(2)基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
(这次明白为什么刚才写的都是protected了吧)
(3)实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public > protected > private。
(4)使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显示的写出继承⽅式。
(5)在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
5. 继承类模板
类模板也可以参与继承关系,既可以作为基类,也可以作为派生类。这使得我们能够创建更加灵活和可复用的代码结构。比如我们以前栈的实现就能变成:
#define Con vector<T>
namespace DaYuanTongXue
{
template<class T>
class Stack : public Con {
public:
void push(const T& x) {
Con::push_back(x);
}
void pop() {
if (!this->empty()) {
Con::pop_back();
}
}
const T& top() {
return Con::back();
}
bool empty() {
return Con::empty();
}
size_t size() {
return Con::size();
}
};
}
注意点:
- 调用模板基类的函数时一定要加上类域名(Con::),因为不写就不会实例化,不实例化就不会到模板里找这个函数。调用成员变量是也一样。(模板按需实例化的知识,忘了的看一下“链表奇遇记:从零揭秘C++ list的魔法世界”这篇博客的五.2)
- 派生类可以增加或减少模板参数,但要保证它给的模板参数可以成功实例化基类。同样,基类被继承时模板参数必须明确,class Stack : public vector; 这就是错的,应该写成vector<T>或vector<int>等。
二、继承语法的注意点及易错点
1. 基类和派生类间的转换
public继承的派⽣类对象可以赋值给基类的对象/基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。基类对象不能赋值给派⽣类对象。
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. 隐藏
- 在继承体系中基类和派⽣类都有独⽴的作⽤域。
- 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤ “基类::基类成员” 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
class Person
{
public:
void Print()
{
cout << " 父类" << endl;
}
protected:
string _name = "小李子"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};
/*
姓名:小李子
身份证号:111
学号:999
*/
3. 派生类的默认成员函数
(1)构造函数
其实规则很简单,如果基类有默认构造函数,那你的派生类可以不用管他,也可以显示调用;如果没有,那一定要显示调用。显示调用的意思是和正常使用构造函数一样写出来(在初始化列表或者函数体中)。
- 基类有默认构造函数
class Person
{
public:
string _name;
int _age;
Person(const string& name = "abc",int age = 18)
: _name(name)
,_age(age)
{
cout << "Person构造函数: " << endl;
}
};
//不管父类
class Student1 : public Person {
public:
int _id;
Student1(int id = 20)
: _id(id)
{
cout << "Student1构造函数: " << endl;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
//显示调用
class Student2 : public Person {
public:
int _id;
Student2(const string& name, int age, int id = 20)
:Person(name, age)
, _id(id)
{
cout << "Student2构造函数: " << endl;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
Student1 s1;
s1.Print();
cout << endl;
Student2 s2("cba", 19);
s2.Print();
return 0;
}

- 基类没有默认构造函数
class Person
{
public:
string _name;
int _age;
Person(const string& name ,int age = 18)
: _name(name)
,_age(age)
{
cout << "Person构造函数: " << endl;
}
};
class Student3 : public Person
{
public:
int _id;
Student3(const string& name = "lnm", int age = 56, int id = 20)
:Person(name, age) //必须显示调用
, _id(id)
{
cout << "Student3构造函数: " << endl;
}
void Print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
Student3 s3;
s3.Print();
return 0;
}

不难看出,派⽣类对象初始化是先调⽤基类构造再调派⽣类构造,不然为什么s3._age=56呢
(2)拷贝构造函数
规则是,派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。写派生类的拷贝构造函数用到了我们上一个切割的知识点“派⽣类对象可以赋值给基类的对象”,所以我们要这样写:
class Person
{
public:
string _name;
int _age;
Person(const Person& p)
: _name(p._name)
, _age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
};
class Student3 : public Person {
public:
int _id;
Student3(const Student3& s)
: Person(s)
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
};
派⽣类对象赋值给基类的对象时,只会调用基类的拷贝构造函数!
(3)赋值重载函数
规则是,派生类的operator=必须要调用基类的operator=完成基类的复制。因为派生类的 operator=隐藏了基类的operator=,所以显示调⽤基类的operator=,需要指定基类作用域。
class Person {
public:
string _name;
Person& operator=(const Person& p) {
if (this != &p) {
_name = p._name;
}
cout << "Person赋值操作" << endl;
return *this;
}
};
class Student : public Person {
public:
int _id;
Student& operator=(const Student& s) {
if (this != &s) {
// 必须显式调用基类赋值运算符
Person::operator=(s); // 处理基类部分
_id = s._id; // 处理派生类部分
}
cout << "Student赋值操作" << endl;
return *this;
}
Student(const string& name, int id) : Person(name), _id(id) {}
};
(4)析构函数
规则是,派生类千万不能显示调用基类的析构。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

4.继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有一个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个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;
// 这里的运行结果可以看到非静态成员_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;
}
5. 继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员(派生类继承的时候就没有那句 friend ......)
这一趴就不演示了哈🐶🐶🐶
6. 不能被继承的类
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
/*Base()
{}*/
};
三、多继承及其菱形继承问题
1. 继承模型
(1)单继承
⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
(2)多继承
⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
(3)菱形继承
菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。(不是只有构成菱形图案的才叫菱形继承哦)
2. 菱形继承的坏处
记住一句,别用这玩意儿!
class Person1
{
public:
string _name; // 姓名
};
class Student1 : public Person1 //单继承
{
protected:
int _num; //学号
};
class Teacher1 : public Person1 //单继承
{
protected:
int _id; // 职工编号
};
class Assistant1 : public Student1, public Teacher1 //多继承
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant1 a;
//a._name = "peter"; 编译报错:error C2385: 对“_name”的访问不明确
// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student1::_name = "xxx";
a.Teacher1::_name = "yyy";
return 0;
}
3. 虚继承
数据冗余问题用虚继承解决,用法是
class 派生类 : virtual 继承方式 基类 {
// 类定义
};
//virtual放在继承相同基类前
class Person {
public:
string _name;
};
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"; // ✅ 正确:没有二义性
cout << "Name: " << a._name << endl; // 输出: peter
return 0;
}
(底层原理如果有机会以后会给大家讲,反正就是别设计菱形继承就行)
IO库中的菱形虚拟继承
4. 多继承中指针偏移问题
题目:下面正确的是:( )
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;
}
解析:
按声明顺序初始化,先继承Base1,后继承Base2,最后声明_d1,地址顺序亦如此。若派生类这样继承
class Derive : public Base2, public Base1
就是p2=p3!=p1
答案:C
四、继承和组合
1. 组合的概念
public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。说白了组合本质是就是一个类的成员变量里声明了自定义类对象。只是在面向对象设计中,我们给这种特定的成员变量关系起了一个专门的名字——"组合",用来强调:整体与部分的关系;生命周期的绑定;设计的"has-a"语义。
// 继承用于is-a关系
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() = default;
};
class Dog : public Animal { // Dog is an Animal
public:
void speak() override {
cout << "Woof!" << endl;
}
};
// 组合用于has-a关系
class Collar {
private:
string color;
public:
Collar(const string& c) : color(c) {}
void display() {
cout << "Collar color: " << color << endl;
}
};
class PetDog : public Dog { // 继承:PetDog is a Dog
private:
Collar collar; // 组合:PetDog has a Collar
string owner;
public:
PetDog(const string& c, const string& o)
: collar(c), owner(o) {}
void showInfo() {
speak(); // 继承的方法
collar.display(); // 组合的方法
cout << "Owner: " << owner << endl;
}
};
2. 扩展知识
我们写程序讲究“低耦合,高内聚”
so,实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的 关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
后记:说C++难其实继承也是个表现,就多继承那块,设计的确实有缺陷,不过C++作为开创性的语言,已经相当厉害了,我们谁又能在第一次把一个新的东西做完美呢😊😊。点个赞再走吧,小伙伴
更多推荐

所有评论(0)