1. 继承
1.1 概念与基础使用
继承就是在一个已经存在的类的基础上建立一个新的类,并拥有其特性,体现了代码复用的思想。
已经存在类被称为“基类”或“父类”;
新建立的类被称为“派生类”或“子类”。
下面是一个最基础的继承案例:
#include <iostream>using namespace std;/** * @brief The Father class 基类 */class Father{public: string first_name = "王"; void work() { cout << "我是个厨师" << endl; }};/** * @brief The Son class 派生类 */class Son:public Father{};int main(){ Son s; cout << s.first_name << endl; // 王 s.work(); // 我是个厨师 return 0;}
上面的代码中Son类与Father基本一致,在实际开发中,无需保持一致,通常派生类会在继承的基础上增加或修改一些基类的内容。
#include <iostream>using namespace std;/** * @brief The Father class 基类 */class Father{public: string first_name = "王"; void work() { cout << "我是个厨师" << endl; }};/** * @brief The Son class 派生类 */class Son:public Father{public: Son() { // 如果派生类对基类的属性值不满意 // 可以修改,前提是能修改 first_name = "刘"; } // 函数隐藏:对基类的函数不满意 void work() { cout << "我是个律师" << endl; } // 增加内容 void play() { cout << "我喜欢打游戏" << endl; }};int main(){ Son s; cout << s.first_name << endl; // 刘 s.work(); // 我是个律师 s.play(); // 我喜欢打游戏 // 调用被隐藏的基类函数 s.Father::work(); // 我是个厨师 return 0;}
需要注意的是,基类和派生类是相对的,某个类可能既是类A的基类,又是类B的派生类;派生类往往比基类更具体,基类往往比派生类更抽象。
1.2 构造函数
类的构造函数和析构函数不能被继承。
#include <iostream>using namespace std;class Father{private: string name;public: Father(string name):name(name){} string get_name() const { return name; } void set_name(string name) { this->name = name; }};class Son:public Father{public:};int main(){// Son s("张三"); 错误 return 0;}
上面的例子可以看出,派生类并没有继承基类的带参数的构造函数。实际上1.1节中的派生类Son也没有继承Father的构造函数,这是因为当程序员不手写构造函数时,编译器会增加以下代码:
● 给基类Father增加一个无参构造函数
● 给派生类Son增加一个无参构造函数
● 在派生类Son的无参构造函数中,调用基类的无参构造函数
因为派生类中省略了基类的代码,因此在创建派生类对象时,需要调用基类的代码完成派生类中基类代码部分内存的开辟,即派生类的任意一个构造函数都必须直接或间接调用基类的任意一个的构造函数。
派生类调用基类构造函数的写法:
● 透传构造
● 委托构造
● 继承构造(只是叫这个名字,并没有继承构造函数)
1.2.1 透传构造
透传构造指的是在派生类的构造函数中直接调用基类的构造函数。
#include <iostream>using namespace std;class Father{private: string name = "无名氏"; int age = 1;public: Father(string name) :name(name) { cout << "Father的一参构造函数" << endl; } Father(string name,int age) :name(name),age(age) { cout << "Father的二参构造函数" << endl; } void show() { cout << name << " " << age << endl; }};class Son:public Father{public: Son(string name) :Father(name) { } Son(string name,int age) :Father(name,age) { }};int main(){ Son s("张三",12); s.show(); Son s2("李四"); s2.show(); return 0;}
1.2.2 委托构造
委托构造指的是,某个类的构造函数可以调用这个类的另一个重载的构造函数。
#include <iostream>using namespace std;class Father{private: string name = "无名氏"; int age = 1;public: Father(string name) :name(name) { cout << "Father的一参构造函数" << endl; } Father(string name,int age) :name(name),age(age) { cout << "Father的二参构造函数" << endl; } void show() { cout << name << " " << age << endl; }};class Son:public Father{public: Son(string name) :Son(name,10) { } Son(string name,int age) :Father(name,age) { }};int main(){ Son s("张三",12); s.show(); Son s2("李四"); s2.show(); return 0;}
委托构造需要注意以下几点:
● 如果是派生类,最终委托的构造函数要透传调用基类的构造函数。
● 不要形成委托闭环。
1.2.3 继承构造
是C++11的新写法,在派生类中使用继承构造后,编译器会按照基类的构造函数格式,创建出派生类的构造函数,并且分别使用透传构造调用基类的同参数构造函数。
#include <iostream>using namespace std;class Father{private: string name = "无名氏"; int age = 1;public: Father(string name) :name(name) { cout << "Father的一参构造函数" << endl; } Father(string name,int age) :name(name),age(age) { cout << "Father的二参构造函数" << endl; } void show() { cout << name << " " << age << endl; }};class Son:public Father{public: using Father::Father; // 继承构造};int main(){ Son s("张三",12); s.show(); Son s2("李四"); s2.show(); return 0;}
实际开发不建议使用继承构造。
1.3 对象的创建与销毁
#include <iostream>using namespace std;/** * @brief The Value class * 作为其它类的变量 */class Value{private: string name;public: Value(string name):name(name) { cout << name << "创建了" << endl; } ~Value() { cout << name << "销毁了" << endl; }};class Father{public: static Value s_value; Value value = Value("Father类的成员变量"); Father() { cout << "Father的构造函数" << endl; } ~Father() { cout << "Father的析构函数" << endl; }};Value Father::s_value = Value("Father类的静态成员变量");class Son:public Father{public: static Value s_value; Value value = Value("Son类的成员变量"); Son() { cout << "Son的构造函数" << endl; } ~Son() { cout << "Son的析构函数" << endl; }};Value Son::s_value = Value("Son类的静态成员变量");int main(){ cout << "主函数开始执行" << endl; { // 局部代码块 Son s; cout << "Son类对象使用中......" << endl; } cout << "主函数结束执行" << endl; return 0;}
上述运行结果可以发现遵循如下规律:
● 创建流程与销毁流程对称。
● 相同部分的创建,基类先派生类后;相同部分的销毁,派生类先基类后。
● 静态成员先创建,后销毁。
一个派生类对象的创建与销毁需要逐层调用到最上层的基类,因此体现了面向对象编程的特点:编写效率高,运行效率低。
可以使用继承,但是不要滥用继承。
1.4 多重继承
1.4.1 基础使用
之前的代码都是单继承,即一个派生类只有一个基类,实际上C++是支持多继承的,即一个派生类可以有多个基类,派生类在多重继承中与每一个基类的关系,仍然可以看做是一个单继承。
#include <iostream>using namespace std;class Sofa{public: void sit() { cout << "能坐着" << endl; }};class Bed{public: void lay() { cout << "能躺着" << endl; }};class SofaBed:public Sofa,public Bed{};int main(){ SofaBed sb; sb.lay(); sb.sit(); return 0;}
1.4.2 二义性问题
多重继承容易出现二义性问题,主要有两种情况:
● 在多继承中,不同的基类拥有同名成员,此时派生类的调用会出现二义性问题。
解决方法:可以通过类名::的方式区分重名成员
#include <iostream>using namespace std;class Sofa{public: void sit() { cout << "能坐着" << endl; } void position() { cout << "放在客厅" << endl; }};class Bed{public: void lay() { cout << "能躺着" << endl; } void position() { cout << "放在卧室" << endl; }};class SofaBed:public Sofa,public Bed{};int main(){ SofaBed sb; sb.lay(); sb.sit();// sb.position(); 错误 sb.Bed::position(); sb.Sofa::position(); return 0;}
● 菱形(钻石)继承:如果一个基类有两个派生类,这两个派生类又作为基类拥有一个派生类,此时在最终的派生类中访问间接基类的成员会出现二义性。
解决方法1:可以通过类名::的方式区分重名成员
#include <iostream>using namespace std;class Furniture // 家具{public: void func() { cout << "家里要有家具" << endl; }};class Sofa:public Furniture{};class Bed:public Furniture{};class SofaBed:public Sofa,public Bed{};int main(){ SofaBed sb;// sb.func(); 错误// sb.Furniture::func(); 错误 sb.Bed::func(); sb.Sofa::func(); return 0;}
解决方法2:使用虚继承。
虚继承的实现通过虚基类指针与虚基类表完成,当Sofa和Bed使用虚继承继承Furniture类时,会在Sofa和Bed类中增加一个虚基类表,虚基类表中记录的是Furniture的成员,Sofa类(Bed类)的所有对象共用这一张虚基类表,通过每个对象创建时新增一个隐藏成员虚基类表指针调用虚基类表。SofaBed继承了Sofa和Bed类,SofaBed对象也会有继承来的虚基类指针,虚基类指针可以查询Sofa和Bed的虚基类表,在调用时通过查表来区分二义性问题。
#include <iostream>using namespace std;class Furniture // 家具{public: void func() { cout << "家里要有家具" << endl; }};class Sofa:virtual public Furniture{};class Bed:virtual public Furniture{};class SofaBed:public Sofa,public Bed{};int main(){ SofaBed sb; sb.func(); return 0;}
虚继承在代码形式上完美解决了菱形继承的二义性问题,但是在调用这些原本二义性的代码时,增加了一些开销(虚基类表与虚基类指针内存占用和查询等),因此虚继承代码执行效率比普通继承要低。
2. 权限
2.1 权限修饰符
在C++中有三种权限修饰符:private(私有)、protected(保护)和public(公有),这三种权限的访问呢能力如下表所示。
本类中 | 派生类中 | 全局 | |
private | √ | X | X |
protected | √ | √ | X |
public | √ | √ | √ |
#include <iostream>using namespace std;class Father{private: string str1 = "private成员";protected: string str2 = "protected成员";public: string str3 = "public成员"; void test() { cout << str1 << endl; cout << str2 << endl; cout << str3 << endl; }};class Son:public Father{public: void test() {// cout << str1 << endl; 错误 cout << str2 << endl; cout << str3 << endl; }};int main(){ Father f; f.test();// cout << f.str1 << endl; 错误// cout << f.str2 << endl; 错误 cout << f.str3 << endl; Son s; s.test(); return 0;}
2.2 不同权限的继承
在C++有三种权限的继承:
● 公有继承
● 保护继承
● 私有继承
之前使用的继承都是公有继承,实际上公有继承也是使用最多的继承。
2.2.1 公有继承
公有继承的特点如下:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会继续成为派生类的protected成员 |
基类的public成员 | 会继续成为派生类的public成员 |
#include <iostream>using namespace std;class Father{private: string str1 = "private成员";protected: string str2 = "protected成员";public: string str3 = "public成员";};class Son:public Father{public: void test() {// cout << str1 << endl; 错误:Father的private cout << str2 << endl; // Son类的protected cout << str3 << endl; // Son类的public }};class Grandson:public Son{public: void test() {// cout << str1 << endl; 错误:Father的private cout << str2 << endl; cout << str3 << endl; }};int main(){ Son s;// cout << s.str1 << endl; 错误:Father的private// cout << s.str2 << endl; 错误:Son的protected cout << s.str3 << endl; // Son的public return 0;}
2.2.2 保护继承
保护继承的特点是:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会继续成为派生类的protected成员 |
基类的public成员 | 会成为派生类的protected成员 |
#include <iostream>using namespace std;class Father{private: string str1 = "private成员";protected: string str2 = "protected成员";public: string str3 = "public成员";};class Son:protected Father{public: void test() {// cout << str1 << endl; 错误:Father的private cout << str2 << endl; // Son类的protected cout << str3 << endl; // Son类的protected }};class Grandson:public Son{public: void test() {// cout << str1 << endl; 错误:Father的private cout << str2 << endl; cout << str3 << endl; }};int main(){ Son s;// cout << s.str1 << endl; 错误:Father的private// cout << s.str2 << endl; 错误:Son的protected// cout << s.str3 << endl; 错误:Son的protected return 0;}
2.2.3 私有继承
私有继承的特点是:
基类的private成员 | 无法在派生类中直接访问 |
基类的protected成员 | 会成为派生类的private成员 |
基类的public成员 | 会成为派生类的private成员 |
#include <iostream>using namespace std;class Father{private: string str1 = "private成员";protected: string str2 = "protected成员";public: string str3 = "public成员";};class Son:private Father{public: void test() {// cout << str1 << endl; 错误:Father的private cout << str2 << endl; // Son类的private cout << str3 << endl; // Son类的private }};class Grandson:public Son{public: void test() {// cout << str1 << endl; 错误:Father的private// cout << str2 << endl; 错误:Son的private// cout << str3 << endl; 错误:Son的private }};int main(){ Son s;// cout << s.str1 << endl; 错误:Father的private// cout << s.str2 << endl; 错误:Son的protected// cout << s.str3 << endl; 错误:Son的protected return 0;}
继承时不写权限默认为私有继承。
3. 多态
3.1 概念
多态按照字面的意思可以认为是“多种状态”,可以简单概括为“一个接口,多种状态”,即程序在运行时动态决定调用的代码。
多态与模板的区别在于,模板针对不同的数据类型采用同样的策略,而多态针对不同的数据类型采用不同的策略。
多态的实现需要以下几个条件:
1. 要有公有继承
2. 要有函数覆盖:派生类中覆盖基类的成员函数
3. 基类引用/指针指向派生类对象
3.2 函数覆盖
函数覆盖与函数隐藏类似,但是函数覆盖可以通过虚函数来支持多态,一个成员函数使用virtual关键字修饰,这个函数就是虚函数。
在派生类中,使用之前函数隐藏的方式重新实现一个基类中的虚函数,此时就是函数覆盖,函数覆盖与虚函数具有以下特点:
● 当函数覆盖成功时,虚函数具有传递性
● C++11中可以在派生类的新覆盖的函数后增加override关键字进行覆盖是否成功的验证
● 成员函数与析构函数可以定义为虚函数,静态成员函数与构造函数不可以定义为虚函数。
● 如果成员函数的声明与定义分离,virtual关键字只需要在声明时使用
#include <iostream>using namespace std;class Animal{public: virtual void eat() { cout << "吃东西" << endl; }};class Dog:public Animal{public: void eat() override { cout << "吃骨头" << endl; }};int main(){ Animal a; a.eat(); // 吃东西 Dog d; d.eat(); // 吃骨头 return 0;}
3.3 使用方式
多态既可以引用的方式,也可以使用指针的方式实现。
#include <iostream>using namespace std;class Animal{public: virtual void eat() { cout << "吃东西" << endl; }};class Dog:public Animal{public: void eat() override { cout << "吃骨头" << endl; }};class Cat:public Animal{public: void eat() { cout << "吃鱼" << endl; }};int main(){ Dog d; Cat c; // 基类引用派生类对象 Animal& a1 = d; Animal& a2 = c; // 多态 a1.eat(); // 吃骨头 a2.eat(); // 吃鱼 Dog* d2 = new Dog; Cat* c2 = new Cat; // 基类指针指向派生类对象 Animal* a3 = d2; Animal* a4 = c2; // 多条 a3->eat(); // 吃骨头 a4->eat(); // 吃鱼 // 先别管delete问题 return 0;}
3.4 应用
多态的主要应用是函数的参数传递,从而实现“一个接口,多种状态”的效果。
#include <iostream>using namespace std;class Animal{public: virtual void eat() { cout << "吃东西" << endl; }};class Dog:public Animal{public: void eat() override { cout << "吃骨头" << endl; }};class Cat:public Animal{public: void eat() { cout << "吃鱼" << endl; }};// 基于引用的多态参数传递void test_eat1(Animal& a){ a.eat();}// 基于指针的多态参数传递void test_eat2(Animal* a){ a->eat();}int main(){ Dog d1; Cat c1; Animal a1; // 测试基于引用的多态参数传递 test_eat1(d1); // 吃骨头 test_eat1(c1); // 吃鱼 test_eat1(a1); // 吃东西 Dog* d2 = new Dog; Cat* c2 = new Cat; Animal* a2 = new Animal; // 测试基于指针的多态参数传递 test_eat2(d2); // 吃骨头 test_eat2(c2); // 吃鱼 test_eat2(a2); // 吃东西 // 先别管delete问题 return 0;}
3.5 原理
当一个类中有虚函数时,编译器会为这个类创建一个虚函数表,这个类的对象拥有隐藏的成员变量虚函数表指针指向虚函数表。此类被继承时,虚函数表也会被继承,但是如果当前继承的派生类中出现函数覆盖,则会在派生类中更新这张表。
注:重写=覆盖
在实际多态的运行中是一个动态类型绑定的过程,当使用基类引用或指针指向派生类对象时,编译器会产生一段代码,用来检查当前内存中的对象的真正类型,在运行时通过对象的虚函数表指针找到真正的调用函数,因此多态也是一个查表的过程。
使用多态会产生一些额外的代码调用开销,不要滥用多态。
3.6 缺陷
当形成多态时,有可能会出现内存泄露的问题。
#include <iostream>using namespace std;class Animal{public: virtual void eat() { cout << "吃东西" << endl; } ~Animal() { cout << "Animal析构函数" << endl; }};class Dog:public Animal{public: void eat() override { cout << "吃骨头" << endl; } ~Dog() { cout << "Dog析构函数" << endl; }};int main(){ Dog* d = new Dog; delete d; // 先调用Dog的析构,再调用Animal的析构 Animal* a = new Dog; delete a; // 只调用Animal的析构函数 return 0;}
上面问题的出现是由于析构函数没有函数覆盖,实际上析构函数也无法覆盖,但是析构可以设置为虚函数,且无需覆盖就能解决上述问题。
#include <iostream>using namespace std;class Animal{public: virtual void eat() { cout << "吃东西" << endl; } virtual ~Animal() { cout << "Animal析构函数" << endl; }};class Dog:public Animal{public: void eat() override { cout << "吃骨头" << endl; } ~Dog() // 仍然具有虚函数的传递性 { cout << "Dog析构函数" << endl; }};int main(){ Dog* d = new Dog; delete d; // 先调用Dog的析构,再调用Animal的析构 Animal* a = new Dog; delete a; // 先调用Dog的析构,再调用Animal的析构 return 0;}
如果一个类可能作为基类,建议都手写为虚析构函数,以防未来可能出现的内存泄露问题。
4. 抽象类
4.1 概念
抽象类只表达一个抽象的概念,并不与具体的对象相联系,即无法创建对象,通常为其派生类提供一个算法框架。抽象类不光无法创建实体对象,也无法作为声明类型,因此不能作为参数类型、返回值类型等。
如果一个类有纯虚函数,那么这类就是抽象类;
如果一个类是抽象类,那么一定有纯虚函数。
纯虚函数是一种的虚函数,这种函数只有声明,没有定义。
#include <iostream>using namespace std;/** * @brief The Shape class 形状 */class Shape{public: // 纯虚函数 virtual void area() = 0; // 面积 virtual void perimeter() = 0; // 周长};int main(){// Shape s; 错误 return 0;}
4.2 使用
抽象类的使用有两种情况。
● 抽象类作为基类,其派生类继承抽象类并实现其所有纯虚函数,实现指的是函数覆盖并定义函数,这样的派生类可以作为普通类使用。
#include <iostream>using namespace std;/** * @brief The Shape class 形状 */class Shape{public: // 纯虚函数 virtual void area() = 0; // 面积 virtual void perimeter() = 0; // 周长};class Circle:public Shape{public: // 实现基类的所有纯虚函数 void area() { cout << "πR^2" << endl; } void perimeter() { cout << "2πR" << endl; }};int main(){ Circle c; c.area(); c.perimeter(); return 0;}
● 抽象类的派生类没有实现抽象基类的所有纯虚函数,此时派生类也会变成抽象类,需要继续继承直到所有的纯虚函数都被实现。
#include <iostream>using namespace std;/** * @brief The Shape class 形状 */class Shape{public: // 纯虚函数 virtual void area() = 0; // 面积 virtual void perimeter() = 0; // 周长};/** * @brief The Polygon class 多边形 */class Polygon:public Shape{public: void perimeter() { cout << "∑边长" << endl; }};/** * @brief The Rectangle class 矩形 */class Rectangle:public Polygon{public: void area() { cout << "h*w" << endl; }};int main(){// Polygon p; 错误 Rectangle r; r.area(); r.perimeter(); return 0;}
抽象类也支持多态,因此抽象类的析构函数应该写为虚析构函数。