
文章目录
- 一、继承开篇
- 二、继承的概念及定义
-
- [1. 继承是什么](#1. 继承是什么)
- [2. 继承定义格式](#2. 继承定义格式)
- [3. 继承后成员访问权限变化(超级重要)](#3. 继承后成员访问权限变化(超级重要))
- 三、基类和派生类的赋值转换(切片/切割)
- [四、继承中的作用域(隐藏 / 重定义)](#四、继承中的作用域(隐藏 / 重定义))
-
-
- [1. 成员变量隐藏](#1. 成员变量隐藏)
- [2. 成员函数隐藏](#2. 成员函数隐藏)
-
- 五、派生类的默认成员函数(超级重点)
-
- [1. 构造函数](#1. 构造函数)
- [2. 拷贝构造](#2. 拷贝构造)
- [3. 赋值重载](#3. 赋值重载)
- [4. 析构函数](#4. 析构函数)
- [5. 写一个不能被继承的类](#5. 写一个不能被继承的类)
- 六、继承与友元
- 七、继承与静态成员
- 八、多继承与菱形继承(C++最复杂的点)
-
- [1. 概念](#1. 概念)
- [2. 菱形继承的两大问题](#2. 菱形继承的两大问题)
- [3. 虚继承(解决菱形继承问题)](#3. 虚继承(解决菱形继承问题))
- 九、继承与组合(面试高频)
-
-
- [1. 继承(白箱复用)](#1. 继承(白箱复用))
- [2. 组合(黑箱复用)](#2. 组合(黑箱复用))
-
- 十、继承总结
一、继承开篇
前面我们学了类和对象、模板,今天我们进入面向对象最核心的特性之一:继承
继承是类层次的复用,可以让我们在不改动原有类的基础上扩展新功能,是C++面试、工程开发必学知识点。如果还没学过类与对象、封装,建议先看前面的文章打牢基础,再来学继承会轻松很多
本篇内容非常细,我会一一把继承概念、定义、赋值转换、作用域隐藏、默认成员函数、友元、静态、多继承、菱形继承、虚继承、继承与组合全部讲透
二、继承的概念及定义
1. 继承是什么
继承就是:
在已有类的基础上,扩展属性和函数,生成新的类。
- 原来的类:基类 / 父类
- 新的类:派生类 / 子类
没有继承之前,我们写Student和Teacher会出现大量重复代码:姓名、地址、电话、身份认证函数全都要写两遍,非常冗余
而有了继承,我们就可以把公共部分抽到Person,让Student和Teacher继承它,就能直接复用,不用重复写,定义如下:
cpp
class Person
{
public:
void identity()
{
cout << "身份认证: " << _name << 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; // 职称
};
然后我们就可以直接使用:
cpp
int main()
{
Student s;
Teacher t;
s.identity(); // 直接用父类的函数
t.identity();
return 0;
}
这就是继承最直观的作用:复用代码,减少冗余。
2. 继承定义格式
最常用的是公有继承:
cpp
class 派生类 : 继承方式 基类
继承方式有三种:
public(最常用)protectedprivate
3. 继承后成员访问权限变化(超级重要)
这张表一定要背下来:
| 基类成员 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可见 | 不可见 | 不可见 |
我给你总结成最简单的口诀:
- 基类 private 成员,无论怎么继承,在子类都不可见
- 访问权限 = 取更小的那个
public > protected > private - 实际开发大部分用public 继承,另外两种几乎不用
示例如下:
cpp
class Person
{
public:
void Print()
{
cout << _name << endl;
}
protected:
string _name;
private:
int _age;
};
// 公有继承
class Student : public Person
{
protected:
int _stunum;
};
Print()在子类里是public_name在子类里是protected_age在子类里不可见
三、基类和派生类的赋值转换(切片/切割)
public 继承下,派生类对象可以赋值给基类对象/指针/引用
这叫切片 / 切割:把派生类里"属于基类"的那部分切出来用,示例类如下:
cpp
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _No;
};
切片的使用如下:
cpp
int main()
{
Student sobj;
// 子类对象 → 父类指针/引用/对象 都可以
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj;
// 父类 → 子类 不行!
// sobj = pobj; 报错
return 0;
}
记住:子类可以自动转父类,父类不能转子类,这在后面的多态很重要,我们先暂时把切片介绍到这里,到多态我们再详细讲解切片的作用
四、继承中的作用域(隐藏 / 重定义)
继承里有一个超级重要的规则:同名隐藏。
规则:
- 基类和子类是两个独立作用域
- 同名成员(变量/函数)会构成隐藏
- 子类会屏蔽父类的同名成员,直接访问只会访问到子类的成员
- 想访问父类同名成员必须加 类名::
1. 成员变量隐藏
cpp
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;
}
protected:
int _num = 999; // 学号,和父类同名,构成隐藏
};
这里父类和子类都有_num成员,那么就会构成隐藏,默认访问子类的_num,而不能直接访问到父类的_num,除非使用访问时加::
2. 成员函数隐藏
只要函数名相同,就构成隐藏,不管参数!,这也是最容易踩的坑:
cpp
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int i)
{
cout << "func(int)" << i << endl;
}
};
这里父类和子类都有func函数,虽然参数不同,但是还是构成了隐藏,默认调用子类,如果你的本意不是这样,就注意不要让父类和子类的成员函数名相同,上述代码的调用结果:
cpp
B b;
b.func(10); // 正确
// b.func(); // 报错!父类func被隐藏了
b.A::func(); // 必须加类域才能访问
结论:继承体系里尽量不要写同名成员, 除非你本意就是要隐藏父类成员函数
五、派生类的默认成员函数(超级重点)
子类的6个默认成员函数,必须调用父类对应的函数。
我给你总结最核心的7条:
- 子类构造 必须先调用父类构造
- 子类拷贝构造 必须先调用父类拷贝构造
- 子类赋值重载 必须先调用父类赋值重载
- 子类析构 自动调用父类析构,不用我们写
- 构造顺序:先父后子
- 析构顺序:先子后父
- 析构函数名会被编译器统一处理,不加virtual就构成隐藏
1. 构造函数
如果父类没有默认构造,子类必须在初始化列表显式调用。
cpp
class Person
{
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
// 必须初始化列表调用父类构造
Student(const char* name, int num)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
protected:
int _num;
};
2. 拷贝构造
cpp
Student(const Student& s)
: Person(s) // 切片调用父类拷贝构造
, _num(s._num)
{
cout << "Student拷贝构造" << endl;
}
3. 赋值重载
cpp
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); // 必须指定类域,否则隐藏
_num = s._num;
}
return *this;
}
4. 析构函数
子类析构结束后,自动调用父类析构,我们不用写。
cpp
~Student()
{
cout << "~Student()" << endl;
// 自动调用 ~Person()
}
5. 写一个不能被继承的类
两种方法:
- C++98:把父类构造私有
子类构造无法调用,就不能实例化 - C++11:final 关键字
cpp
// C++11 最简单
class Base final
{
};
// 报错:无法继承final类
// class Derive : public Base
// {};
六、继承与友元
友元关系不能继承!
父类的友元函数,不能访问子类的私有/保护成员
cpp
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuNum;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
// cout << s._stuNum << endl; 报错!访问不到
}
想访问必须再把Display声明为Student的友元。
七、继承与静态成员
基类的 static 成员,整个继承体系只有一份!
所有子类和父类共享同一个静态变量
cpp
class Person
{
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{};
使用:
cpp
int main()
{
Person p;
Student s;
// 地址相同,是同一个变量
cout << &p._count << endl;
cout << &s._count << endl;
// 都可以访问
Person::_count++;
Student::_count++;
return 0;
}
八、多继承与菱形继承(C++最复杂的点)
1. 概念
- 单继承:一个子类只有一个父类
- 多继承:一个子类有两个或以上父类
- 菱形继承:多继承的特殊情况,有共同祖先
cpp
class Person {};
class Student : public Person {};
class Teacher : public Person {};
// 菱形继承
class Assistant : public Student, public Teacher {};
2. 菱形继承的两大问题
- 数据冗余 :
Person成员存两份 - 二义性 :访问
_name不知道是哪一个
cpp
Assistant a;
// a._name; 报错:不明确
可以指定类域解决二义性,但解决不了冗余:
cpp
a.Student::_name;
a.Teacher::_name;
3. 虚继承(解决菱形继承问题)
在中间层 加 virtual 继承:
cpp
class Student : virtual public Person {};
class Teacher : virtual public Person {};
这样:
Person成员只存一份- 无冗余、无二义性
cpp
Assistant a;
a._name = "peter"; // 正确
注意:虚继承只用来解决菱形继承,平时不要乱用。
九、继承与组合(面试高频)
最后我们讲一个设计思想:优先使用组合,少用继承。
- 继承:is-a 关系
Student is a Person - 组合:has-a 关系
Car has a Tire
1. 继承(白箱复用)
- 父类对子类透明
- 耦合度高
- 父类改,子类全受影响
2. 组合(黑箱复用)
- 内部不可见
- 耦合度低
- 维护性极强
- 优先使用
示例:
cpp
class Tire {};
class Car
{
protected:
Tire _t1;
Tire _t2;
Tire _t3;
Tire _t4;
};
stack 和 vector 更适合组合,不适合继承。
十、继承总结
- 继承是类层次复用,减少冗余
- 继承方式大部分只用 public
- 基类private成员在子类不可见
- 子类可以赋值给父类(切片),父类不能转子类
- 同名成员构成隐藏,必须加类域访问
- 子类默认函数必须调用父类对应函数
- 构造先父后子,析构先子后父
- 友元不能继承,static全类族共享
- 菱形继承用虚继承解决
- 设计优先组合,少用继承
那么今天关于C++ 继承 就全部讲完了,内容非常多,大家多敲代码多体会。
有什么不懂欢迎私信问我,我会及时做出解答!
下一篇我们开始学习多态,面向对象最后的核心知识点,敬请期待吧!
bye~