🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 c + + , g o , p y t h o n , 目前熟悉 c + + , g o 语言,数据库,网络编程,了解分布式等相关内容 \textcolor{orange}{博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容} 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容
📃 个人主页: \textcolor{gray}{个人主页:} 个人主页: 小呆鸟_coding
🔎 支持 : \textcolor{gray}{支持:} 支持: 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦 \textcolor{green}{如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦} 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦👍 就是给予我最大的支持! \textcolor{green}{就是给予我最大的支持!} 就是给予我最大的支持!🎁
💛本文摘要💛
本专栏主要是对c++ primer这本圣经的总结,以及每章的相关笔记。目前正在复习这本书。同时希望能够帮助大家一起,学完这本书。 本文主要讲解第15章 面向对象程序设计 文章目录
c++ primer 第五版 系列文章:可面试可复习
第2章 变量和基本类型
第3章 字符串、向量和数组
第4章 表达式
第5章 语句
第6章 函数
第8章 IO库
第9章 顺序容器
第10章 泛型算法
第11章 关联容器
第12章 动态内存
第13章 拷贝控制
第 14章 重载运算符
第15章 面向对象程序设计
第 16章 模板与泛型编程
❄️面向对象程序设计
- 面向对象程序设计的核心思想:
数据抽象、继承、动态绑定。
- 继承和动态绑定对程序编写有俩方面影响
- 定义相似的类并对其相似关系建模
- 忽略相似类型的区别,而以统一的方式使用他们的对象
☔️15.1 OOP:概述
继承
通过继承
,联系在一起的类构成一种层次关系
- 基类:定义共同拥有的成员
- 派生类:定义特有的成员(是从基类直接或间接的继承过来的)
- 虚函数:基类希望派生类各自自定义自己合适的版本
派生列表
首先是一个冒号,然后是以逗号分隔的基类列表,每个基类前面可以有访问说明符。
- 派生类必须通过使用类派生列表明确指出基类,因为一个类可以有多个基类
class Student : public Class {}; //Class:基类,Student:派生类
- 派生类
必须在内部对所有重新定义的虚函数进行声明
,声明时可以在前面加上virtual
,也可以不加。 - C++11 允许使用
override
关键字显式地指明重新定义的虚函数,把 override 放到形参列表后面。(当派生类写错时,编译器会报错如果不写override
的话,编译器以为派生类自己定义了一个新函数,所以不会报错)
class Quote{
public:
std::string isbn() const; //派生类完全继承基类,和基类的行为一样
virtual double net_price(std::size_t n) const; //定义了虚函数,则派生类可以有自己的版本
};
class Bulk_quuote:public Quote{
public:
double net_price(std::size_t) const override; //override是重写,需要修改基类的信息
}
动态绑定
当使用基类的引用或指针来调用一个虚函数时将发生动态绑定。
动态绑定根据传入的参数类型来选择函数版本(可能是基类中的该函数或派生类中的该函数),它发生在运行时,又称运行时绑定。
☔️15.2 定义基类和派生类
⛄️15.2.1 定义基类
基类通常都应该定义一个虚析构函数
,即使该函数不执行任何实际操作也是如此。- 基类通过在其成员函数的声明语句前加上
关键字virtual
使得该函数执行动态绑定(解析过程发生在运行时),而成员函数没有被声明为虚函数。则解析过程发生在编译时而非运行时
。 - 基类通过虚函数区分两种成员函数:
- 基类希望派生类进行
覆盖
的函数,为虚函数。构造函数与静态函数都不能定义成虚函数
。任何构造函数之外的非静态函数都可以定义为虚函数。
- 基类希望派生类直接继承不要改变的函数。
- 基类希望派生类进行
注意:
使用指针或引用调用虚函数时
,该调用将被动态绑定。- 如果基类把一个函数声明为虚函数,
该函数在派生类中隐式地也是虚函数(c++11派生类的中不用写virtual,而是直接写override)
。 virtual
只能出现在类内部声明语句之前
,而不能用于类外部函数定义。
访问控制与继承
- 派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
派生类能访问基类的公有成员和受保护成员,不能访问私有成员。
⛄️15.2.2 定义派生类
- 派生类需要派生列表明确指出它是从哪个基类继承来的
- 类派生列表:首先是一个冒号,然后是以逗号分隔的基类列表,每个基类前面可以有访问说明符。
- 访问说明符包括:
public, protected, private
。
- 访问说明符包括:
派生类中的虚函数
派生类经常覆盖它继承的虚函数。如果没有覆盖,派生类会直接继承其在基类中的版本。
- C++11 允许使用
override 关键字显式
地指明重新定义的虚函数,把 override 放到形参列表后面、或 const 成员函数的 const 关键字后面、或引用成员函数的引用限定符后面。
派生类对象及派生类向基类的类型转换
-
一个派生类对象有多个组成部分:
- 一个含有
派生类自己定义
的成员的子对象, - 一个与
该派生类继承的基类
对应的子对象。
- 一个含有
-
因为派生类对象中含有与基类对应的组成部分,
所以可以把派生类的对象当成基类对象来使用,也能把派生类的指针或引用用在需要基类指针的地方。
理解:
- 派生类是大队长覆盖面大,而基类是小队长覆盖面小,覆盖面大的可以转换为覆盖面小的(
类似非const成员覆盖面大为大队长,而const覆盖面小为小队长,那么可以从非const转换为const,反之不行
)
Quote item; //基类对象
Bulk_quote bulk; //派生类对象
Quote *p = &item; //定义基类指针
p = &bulk; //指向派生类的基类指针(隐式转换)
Quote &r = bulk; //绑定到Quote部分
派生类构造函数
- 派生类
不能直接初始化
从基类继承来的成员,而是使用基类的构造函数来初始化它的基类部分。
每个类控制自己的成员初始化过程。派生类构造函数通过构造函数初始化列表将实参传递给基类构造函数。
Bulk_quote(const std::string &book, double p, std::size_t qty,double disc)
:Quote(book, p), min_qty(qty), discount(disc)( )
//Quote就是从基类继承过来的,必须使用基类的构造函数进行初始化,剩下俩个是派生类独有的,直接调用派生类的构造函数
- 默认情况下,派生类对象的基类部分会像数据成员一样
默认初始化
。如果要使用其他的基类构造函数,需要以类名加圆括号内的实参列表的形式为构造函数提供初始值。(Quote(book, p))
- 首先初始化基类的部分,然后
按照声明的顺序依次初始化派生类的成员。
派生类使用基类的成员
继承与静态成员
基类的静态成员只存在唯一一个实例,不论有多少派生类。(类似太阳)
- 如果静态成员是可访问的,派生类也能使用它,如果是private的,则派生类无权访问
Class Base{
public:
static void statment();
};
Class Derived:public Base{
void f(const Derived&);
}
void Derived::f(const Derived &derived_obj)
{
Base::statment(); //直接通过基类访问,无须创建对象
Derived::statment(); //直接通过派生类访问,无须创建对象
derived_obj.statment(); //使用派生类对象
statment(); //使用this指针
}
派生类的声明
- 派生类的声明不包含派生列表(定义包含)。
class Bulk_quote:public Quote; //错误
class Bulk_quote; //正确
被用作基类的类
- 如果想要某个类用作基类,`则该类必须已经定义而非仅仅声明
继承可以多重继承
,最终的派生类将包含它的直接基类的子对象和每一个间接基类的子对象。一个类的基类,同时也可以是一个派生类
class Base; //错误声明但没有定义,不能作为基类
class Bulk_quote:public Qupte{...}; //正确
'多重继承'
class Base{ };
class D:public Base{ };
class D2:public D1 { };
防止继承
- 如果定义了一个类并不希望它被其他类继承,
可以在类名后跟一个关键字 final
⛄️15.2.3 类型转换与继承
当使用基类的引用或指针时,实际上我们并不清楚它所绑定对象的真实类型,可能是基类对象也可能是派生类对象。
静态类型与动态类型
- 形参的类型是静态的,实参的类型是动态的。只有当跑起来之后,在内存中调用相应的函数,此时内存中才会有一个对象,这个对象可能是基类可能是派生类
不存在从基类向派生类的隐式类型转换(但是可以强制转换)
派生类可以向基类转换是因为派生类对象中包含基类部分,而基类的引用或指针可以绑定到该基类部分上,反过来是不行的。
·
Bulk_quote bulk; //派生类
Quote *p = &bulk; //将基类的指针绑定到派生类
Bulk_quote *bulkp = p //错误:不能将基类绑定到派生类上
在对象之间不存在类型转换
- 在对象间不存在类型转换的,只是派生类的指针和引用可以隐式转换为基类
- 当初始化或赋值一个类类型的对象时,实际是调用构造函数或赋值运算符。他们通常包含一个参数:该参数类型是类类型的 const 引用。
此时是可以将派生类对象赋值给基类对象的,实际运行的是以引用作为参数的赋值运算符。
当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉
☔️15.3 虚函数
虚函数必须提供定义,即使没有被用到。因为编译器不知道哪个虚函数在运行时会被使用到。
只有当通过指针或引用调用虚函数时才会发生动态绑定
,正常调用虚函数时不会发生动态绑定,只是简单的赋值。
'通过指针或引用调用虚函数会发生动态绑定'
Quote base("2022", 50);
print_total(cout, base, 10); //调用Quote::net_price
Bulk_quote derived("2021", 50);
print_total(cout, derived, 10); //调用Bulk_quote::net_price
'正常调用虚函数不会发生动态绑定'
base = derived; //把derived的Quote部分拷贝到base
base.net_price(20); //调用Quote::net_price
派生类中的虚函数
- 当在派生类中覆盖了某个虚函数时,可以用 virtual 指明也可以不用。
虚函数在所有的派生类中都是虚函数。(一般我们会加上override来说明该函数是虚函数,且是基类继承过来的。)
- 如果派生类的函数覆盖了继承而来的虚函数,
它的形参类型必须与被覆盖的基类函数完全一致。返回类型也必须相匹配。
final 和 override 说明符
- 问题:
- 如果派生类定义了一个函数与基类中虚函数名字相同但形参列表不同是合法的,不会报错。编译器会认为这个新定义的函数与基类中原有的函数时相互独立的,
此时派生类的函数并没有覆盖掉基类中的版本。
- 如果派生类定义了一个函数与基类中虚函数名字相同但形参列表不同是合法的,不会报错。编译器会认为这个新定义的函数与基类中原有的函数时相互独立的,
- c++ 11中可以使用
override
来指明派生类中的虚函数。这时如果该函数没有覆盖基类中的虚函数,编译器就会报错。
- 可以把某个函数指定为
final
,这样该函数就不能被派生类所覆盖 final 和 override 都写到形参列表和尾置返回类型之后。
虚函数与默认实参
- 虚函数也可以有默认实参,实参值由调用的静态类型决定。
即如果通过基类的指针或引用调用虚函数,则使用基类中定义的默认实参。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
B(1,2) // B为基类,默认实参为1,2
A:public B(1,2,3,4) //A为派生类,默认实参为1,2,3,4
B b; //b为B的对象
//b在进行初始化时,前面俩个参数是由基类的构造函数进行初始化的,因此如果实参不一样的话,例如(a,b,2,3),则通过基类初始化后为(1,2,3,4)发生错误
回避虚函数机制
- 有时希望对虚函数的调用不进行动态绑定,
而是强迫执行虚函数的某个特定版本,可以通过作用域运算符来实现。
double price = baseP->Quote::net_price(42);//net_price 是虚函数,这里指定调用基类 Quote 的虚函数版本。
- 通常只有在
成员函数或友元中
的代码才需要使用作用域运算符来回避虚函数的机制。或者当一个派生类的虚函数需要调用它的基类版本时。
☔️15.4 抽象类
纯虚函数
可以将一个没有实际意义的虚函数定义为纯虚函数,只需当在类内对它进行声明时最后加一个 =0 即可,无需额外定义。(抽象类是半成品,只能用作基类,来派生其他的类。但是本身不能够创建对象,只能由它的派生类创建对象)
Student{
virtual Gorad()=0; //这是一个纯虚函数
}
- 可以为纯虚函数定义,函数体必须定义在类的外部,而不能在类的内部为一个=0的函数提供函数体
含有纯虚函数的类是抽象类
含有纯虚函数的类是抽象基类。不能直接创建一个抽象基类的对象。
可以创建派生类的对象,前提是派生类覆盖了原本的纯虚函数,否则该派生类也是抽象基类。
派生类构造函数只初始化它的直接基类
总结
纯虚函数就是一个中间层,只提供接口,不能进行创建对象,只有它派生的类才可以创建对象
☔️15.5 访问控制与继承
- 一个类使用 protected 关键字来声明希望对派生类可见但对其他用户不可见的成员。
派生类的成员和友元可以通过派生类对象访问基类的受保护成员,而不能直接通过基类对象访问
公有、私有和受保护继承
- 一个类对继承的基类成员的访问权限受两方面影响:
1. 基类中该成员的访问说明符
2. 派生类的派生列表中的访问说明符
理解:
派生列表中的访问说明符不会影响派生类自身的成员和友元对基类的访问权限,对直接基类的访问权限只与基类中的访问说明符有关。它影响的是派生类的用户(包括派生类的对象、派生类的派生类)对基类成员的访问权限。
class B{
public:
void pub_mem();
protected:
int prot_mem();
private:
char priv_mem();
};
struct Pub_Derv:public B{
int f(){return prot_mem;} //正确:派生类可以访问protected成员
char g(){return priv_mem();} //错误:private 成员对于派生类是不可访问的
};
struct Priv_Derv:private B{
int f1() const {return prot_mem;} //正确:privatea不影响派生类的访问权限,只影响派生对象的访问权限
};
Pub_Derv d1; //继承来自B的成员是public
Priv_Derv d2; //继承来自B的成员是private
d1.pub_mem(); //正确:pub_mem在派生类中是public的
d2.pub_mem(); //错误:pub_mem在派生类中是private的
派生类向基类转换的可访问性
- 只有当 D 公有地继承 B 时,用户代码才能使用派生类向基类的转换。
- 无论 D 以什么方式继承 B,D 的成员函数和友元都能使用派生类向基类的转换。
- 如果 D 以受保护的方式继承 B,则 D 的派生类的成员和友元也可以使用 D 向 B 的类型转换。
总结
- 不考虑继承时,可以认为类有两种用户:
1.普通用户
:普通用户的代码使用类的对象,只能访问类的公有成员。
2.类的实现者
:实现者负责编写类的成员和友元。成员和友元可以访问类的所有部分。 - 考虑继承时出现了第三种用户
1. 派生类
:类的公有成员和受保护成员可以对派生类可见。
注意:
- 其实private成员是可以继承的,但是只能通过内存地址等非常规方式进行访问
友元与继承(看书P545)
友元关系不能传递也不能继承。
如果类 A 是基类 B 的友元,那么 A 可以访问 B 对象的成员和 B 的派生类对象中属于 B 部分的成员。
改变个别成员的可访问性
通过using声明可以改变派生类继承的某个名字的访问级别
- using 声明位于 public 部分就是公有成员,位于 private 部分就是私有成员,位于 protected 部分就是受保护成员。
class B{
public:
char name;
private:
int age;
};
class D:private B{
public:
using B::name; //公有成员
private:
using B::age; //私有成员
};
默认继承保护级别
当继承时不使用访问说明符,使用 class 关键字定义的派生类默认是私有继承的,使用 struct 关键字定义的派生类默认是公有继承的。
struct 和 class
两个关键字唯一的区别就是默认成员访问说明符和默认派生访问说明符,没有其他任何区别。
class B{};
class D : B{}; //默认 private 继承
struct D : B{}; //默认 public 继承
☔️15.6 继承中的类作用域
每个类定义自己的作用域,在这个作用域内定义类的成员。
派生类的作用域嵌套在基类的作用域内
Bulk_quote bulk;
cout << bulk.isbn();
- 先从派生类内部找,如果没找到,就依次向外找,直到找到基类
在编译时进行名字查找 - 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,
即使静态类型和动态类型可能不一致
。
名字冲突与继承
- 派生类也能重用定义在其基类中的名字,重用后派生类的成员将隐藏同名的基类成员。
定义在派生类的函数不会重载基类中的同名成员,而是直接隐藏掉。
- 除了覆盖虚函数外,派生类最好不要重用基类成员的名字。
- 通过作用域运算符来使用隐藏的基类成员。
虚函数的作用域
基类与派生类中的虚函数必须有相同的形参列表,如果形参列表不相同,仅仅只有函数名相同,那么派生类会隐藏基类的函数(如果不同,派生类定义的将是一个新函数,该新函数不是虚函数,并且会隐藏掉从基类继承的同名虚函数。 )
例子(P550)
☔️15.7 构造函数与拷贝控制
- 如果一个类(基类或派生类)没有定义拷贝控制操作(包括拷贝构造、拷贝赋值、析构、移动构造、移动赋值),则编译器将会合成一个版本。这个版本也可以定义为删除
⛄️15.7.1 虚析构函数
因为继承的影响,通常基类应该定义一个虚析构
- 当我们动态分配生成类的对象时,使用完后要delete掉相应的指针,此时需要调用析构函数来销毁对象,
当类指针的静态类型与被删除对象的动态类型相符时,不会出现问题,但是当不符时,会出想内存泄露
类指针的静态类型是基类指针,却动态绑定到了一个派生类,这时要确保delete执行的是派生类的析构函数
,所以要将析构函数定义为虚析构,确保动态绑定到正确版本
class Quote{
public:
virtual ~Quote = default; //动态绑定析构函数
};
Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote析构
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote的析构函数
虚析构函数阻止合成移动操作(移动拷贝、移动赋值),而移动操作又阻止合成拷贝控制、拷贝赋值。所以尽量这五个控制操作全部都定义(如果定义了拷贝构造、拷贝赋值、析构,则编译器不会为我们合成移动操作)
⛄️15.7.2 合成拷贝控制与继承
- 继承类的合成拷贝控制成员(构造函数、析构函数、赋值运算符等)
在对其中的基类部分进行相关拷贝、销毁等操作时都是通过调用基类的对应成员完成的。
派生类中删除的拷贝控制与基类的关系
如果基类的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是删除的,那么派生类中对应的成员也会是删除的
(因为派生类中的拷贝控制成员需要调用基类的对应成员来完成操作)。- 如果
基类有一个不可访问或删除的析构函数,则派生类中合成的默认和拷贝构造函数也会是删除的,因为编译器无法销毁派生类对象的基类部分。
- 如果基类的移动操作是删除的,那么派生类中的对应函数也是删除的,因为派生类的基类部分不可移动。
移动操作与继承
- 大多数基类都会定义一个虚析构函数,所以默认情况下基类通常没有合成的移动操作,同时也会阻止它的派生类合成自己的移动操作。
- 如果需要执行移动操作,首先要在基类中定义。之后派生类会自动合成移动操作。
class Quote{
public:
Quote() = default; //合成的默认构造函数
Quote(const Quote&) = default; //合成的拷贝构造函数
Quote(Quote&&) = default; //合成的移动构造函数
Quote& operator=(const Quote&) = default; //合成的拷贝赋值运算符
Quote& operator=(Quote&&) = default; //合成的移动赋值运算符
virtual ~Quote() = defalut; //合成的虚析构函数
}
⛄️15.7.3 派生类的拷贝控制成员
派生类构造函数在初始化时不但需要初始化派生类自己的成员,还需要初始化派生类对象的基类部分(它是由基类构造函数初始化的)。因此派生类的拷贝、移动构造函数和赋值都需要把自己的成员处理完后,在处理派生类当中基类的部分。
- 析构函数不同,因为析构部分是隐式销毁的,基类部分也是自动销毁的,不需要派生类来负责。
定义派生类的拷贝或移动构造函数
派生类赋值运算符
派生类析构函数
- 各自析构自己的(派生类析构函数只负责销毁由派生类自己分配的资源,基类会隐式被销毁)
在构造函数和析构函数中调用虚函数
- 如果在构造函数或析构函数中调用了某个虚函数,则会执行与构造函数或析构函数所属类型相对应的虚函数版本(即不会执行派生类的虚函数版本)。
解释
- 当派生类构造函数会先调用基类构造函数完成基类部分的初始化,如果基类的构造函数中有虚函数,此时虚函数需要执行基类版本,而不能执行派生类覆盖的版本,因为此时派生类特有的版本,还未构造成功,它是要等基类完成后,才构造自己特有的。
⛄️15.7.4 继承的构造函数
- 派生类
能够重用其直接基类定义的构造函数
。这些构造函数并非以常规的方式继承而来。 一个类只初始化它的直接基类,一个类也只继承其直接基类的构造函数(类不能继承默认、拷贝、移动构造函数,这三种构造函数如果没有定义类会自己合成)
派生类通过一个 using 声明语句来继承基类的构造函数
class Bulk_quote : public Disc_quote{
public:
using Disc_quote::Disc_quote; //继承基类的构造函数
}
'等价于'
Bulk_quote(const std::string book, double price, std::sie_t qty, double disc)
:Disc_quote(book,price,qty,disc){} //直接调用的基类的构造函数
理解:继承的构造函数就是相当于派生类采用基类的构造函数初始化自己的基类部分,而派生类中定义的成员采用默认初始化。也可以直接定义具有相同功能的构造函数。
继承的构造函数的特点
构造函数的 using 声明不会改变该构造函数的访问级别(不改变public、private、protected访问级别)
- 如果基类的构造函数是 explicit 或 constexpr 的,则继承的构造函数也有相同的属性。
- 如果基类的构造函数是 explicit 或 constexpr 的,则继承的构造函数也有相同的属性。
- 当一个基类构造函数含有默认实参,这些实参并不会被直接继承,派生类会获得多个继承的构造函数,每个构造函数分别省略掉一个含有默认实参的形参。
如果基类含有几个构造函数,大多数时候派生类会继承所有这些构造函数。除了两个例外情况:
- 如果基类的某个构造函数和派生类自己定义的构造函数具有相同的参数列 ,则该构造函数不会被继承。
- 默认、拷贝、移动构造函数不会被继承。
☔️15.8 容器与继承
使用容器存放继承体系中的对象时,通常采用间接存储的方式,如存储对象的指针。
- 不能把基类和派生类同时放到一个容器中
在容器中放(智能)指针而非对象
- 当要在容器中存放继承体系中的对象时,使用基类指针作为容器的元素类型。
- 此时就会有静态类型与动态类型区分,指针类型是基类的但是指向的对象可以是派生类的
俩个智能指针类型都是静态Quote类型(都是基类的指针),但是指向不一样,一个是动态类型是Quote另一个动态类型是Bulk_quote
为什么这里用make_ptr而不是shared_ptr,这俩个区别是什么
因为shared_ptr创建一个指针时,里面会写new一个类型,但这是在堆上创建一个动态对象,而堆上的内存和智能指针无关系(仅仅new对象成功了,但是这个智能指针没有成功,他需要另外的内存存放引用计数,生成计数内存和堆上的内存是分开的,当生成计数内存出错了,那上面就会内存泄露,因为此时还没哟产生智能指针)
make_ptr是对象和计数这俩个内存放在一起,要不都成功,要不都失败,不会产生内存泄露
- 缺点:
- 无法delete,只能自定义
- 还有一个弱计数,当shared_pte计数到0,但是还有弱计数,他们是在一起的,无法删除
- 缺点: