
🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 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章 模板与泛型编程
数据抽象、继承、动态绑定。继承
通过继承,联系在一起的类构成一种层次关系
派生列表
首先是一个冒号,然后是以逗号分隔的基类列表,每个基类前面可以有访问说明符。class Student : public Class {}; //Class:基类,Student:派生类
必须在内部对所有重新定义的虚函数进行声明,声明时可以在前面加上 virtual,也可以不加。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是重写,需要修改基类的信息
}
动态绑定
当使用基类的引用或指针来调用一个虚函数时将发生动态绑定。动态绑定根据传入的参数类型来选择函数版本(可能是基类中的该函数或派生类中的该函数),它发生在运行时,又称运行时绑定。
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。关键字virtual使得该函数执行动态绑定(解析过程发生在运行时),而成员函数没有被声明为虚函数。则解析过程发生在编译时而非运行时。覆盖的函数,为虚函数。构造函数与静态函数都不能定义成虚函数。任何构造函数之外的非静态函数都可以定义为虚函数。注意:
使用指针或引用调用虚函数时,该调用将被动态绑定。该函数在派生类中隐式地也是虚函数(c++11派生类的中不用写virtual,而是直接写override)。virtual只能出现在类内部声明语句之前,而不能用于类外部函数定义。访问控制与继承
派生类能访问基类的公有成员和受保护成员,不能访问私有成员。
public, protected, private。
派生类中的虚函数
派生类经常覆盖它继承的虚函数。如果没有覆盖,派生类会直接继承其在基类中的版本。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))按照声明的顺序依次初始化派生类的成员。派生类使用基类的成员

继承与静态成员
基类的静态成员只存在唯一一个实例,不论有多少派生类。(类似太阳)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
当使用基类的引用或指针时,实际上我们并不清楚它所绑定对象的真实类型,可能是基类对象也可能是派生类对象。静态类型与动态类型

不存在从基类向派生类的隐式类型转换(但是可以强制转换)
派生类可以向基类转换是因为派生类对象中包含基类部分,而基类的引用或指针可以绑定到该基类部分上,反过来是不行的。·Bulk_quote bulk; //派生类
Quote *p = &bulk; //将基类的指针绑定到派生类
Bulk_quote *bulkp = p //错误:不能将基类绑定到派生类上
在对象之间不存在类型转换
此时是可以将派生类对象赋值给基类对象的,实际运行的是以引用作为参数的赋值运算符。
当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉
虚函数必须提供定义,即使没有被用到。因为编译器不知道哪个虚函数在运行时会被使用到。只有当通过指针或引用调用虚函数时才会发生动态绑定,正常调用虚函数时不会发生动态绑定,只是简单的赋值。'通过指针或引用调用虚函数会发生动态绑定'
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
派生类中的虚函数
虚函数在所有的派生类中都是虚函数。(一般我们会加上override来说明该函数是虚函数,且是基类继承过来的。)它的形参类型必须与被覆盖的基类函数完全一致。返回类型也必须相匹配。final 和 override 说明符
此时派生类的函数并没有覆盖掉基类中的版本。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 的虚函数版本。
成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的机制。或者当一个派生类的虚函数需要调用它的基类版本时。纯虚函数
可以将一个没有实际意义的虚函数定义为纯虚函数,只需当在类内对它进行声明时最后加一个 =0 即可,无需额外定义。(抽象类是半成品,只能用作基类,来派生其他的类。但是本身不能够创建对象,只能由它的派生类创建对象)Student{
virtual Gorad()=0; //这是一个纯虚函数
}
含有纯虚函数的类是抽象类
含有纯虚函数的类是抽象基类。不能直接创建一个抽象基类的对象。可以创建派生类的对象,前提是派生类覆盖了原本的纯虚函数,否则该派生类也是抽象基类。派生类构造函数只初始化它的直接基类

总结
纯虚函数就是一个中间层,只提供接口,不能进行创建对象,只有它派生的类才可以创建对象
派生类的成员和友元可以通过派生类对象访问基类的受保护成员,而不能直接通过基类对象访问
公有、私有和受保护继承
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的
派生类向基类转换的可访问性
总结
1.普通用户:普通用户的代码使用类的对象,只能访问类的公有成员。2.类的实现者:实现者负责编写类的成员和友元。成员和友元可以访问类的所有部分。1. 派生类:类的公有成员和受保护成员可以对派生类可见。注意:
友元与继承(看书P545)
友元关系不能传递也不能继承。如果类 A 是基类 B 的友元,那么 A 可以访问 B 对象的成员和 B 的派生类对象中属于 B 部分的成员。改变个别成员的可访问性
通过using声明可以改变派生类继承的某个名字的访问级别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 继承
每个类定义自己的作用域,在这个作用域内定义类的成员。派生类的作用域嵌套在基类的作用域内Bulk_quote bulk;
cout << bulk.isbn();

即使静态类型和动态类型可能不一致。
名字冲突与继承
定义在派生类的函数不会重载基类中的同名成员,而是直接隐藏掉。
虚函数的作用域
基类与派生类中的虚函数必须有相同的形参列表,如果形参列表不相同,仅仅只有函数名相同,那么派生类会隐藏基类的函数(如果不同,派生类定义的将是一个新函数,该新函数不是虚函数,并且会隐藏掉从基类继承的同名虚函数。 )例子(P550)
因为继承的影响,通常基类应该定义一个虚析构当类指针的静态类型与被删除对象的动态类型相符时,不会出现问题,但是当不符时,会出想内存泄露
类指针的静态类型是基类指针,却动态绑定到了一个派生类,这时要确保delete执行的是派生类的析构函数,所以要将析构函数定义为虚析构,确保动态绑定到正确版本class Quote{
public:
virtual ~Quote = default; //动态绑定析构函数
};
Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote析构
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote的析构函数
虚析构函数阻止合成移动操作(移动拷贝、移动赋值),而移动操作又阻止合成拷贝控制、拷贝赋值。所以尽量这五个控制操作全部都定义(如果定义了拷贝构造、拷贝赋值、析构,则编译器不会为我们合成移动操作)在对其中的基类部分进行相关拷贝、销毁等操作时都是通过调用基类的对应成员完成的。
派生类中删除的拷贝控制与基类的关系
如果基类的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是删除的,那么派生类中对应的成员也会是删除的(因为派生类中的拷贝控制成员需要调用基类的对应成员来完成操作)。基类有一个不可访问或删除的析构函数,则派生类中合成的默认和拷贝构造函数也会是删除的,因为编译器无法销毁派生类对象的基类部分。
移动操作与继承
class Quote{
public:
Quote() = default; //合成的默认构造函数
Quote(const Quote&) = default; //合成的拷贝构造函数
Quote(Quote&&) = default; //合成的移动构造函数
Quote& operator=(const Quote&) = default; //合成的拷贝赋值运算符
Quote& operator=(Quote&&) = default; //合成的移动赋值运算符
virtual ~Quote() = defalut; //合成的虚析构函数
}
派生类构造函数在初始化时不但需要初始化派生类自己的成员,还需要初始化派生类对象的基类部分(它是由基类构造函数初始化的)。因此派生类的拷贝、移动构造函数和赋值都需要把自己的成员处理完后,在处理派生类当中基类的部分。定义派生类的拷贝或移动构造函数

派生类赋值运算符

派生类析构函数

在构造函数和析构函数中调用虚函数
解释
能够重用其直接基类定义的构造函数。这些构造函数并非以常规的方式继承而来。一个类只初始化它的直接基类,一个类也只继承其直接基类的构造函数(类不能继承默认、拷贝、移动构造函数,这三种构造函数如果没有定义类会自己合成)派生类通过一个 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访问级别)如果基类含有几个构造函数,大多数时候派生类会继承所有这些构造函数。除了两个例外情况:
使用容器存放继承体系中的对象时,通常采用间接存储的方式,如存储对象的指针。
在容器中放(智能)指针而非对象

俩个智能指针类型都是静态Quote类型(都是基类的指针),但是指向不一样,一个是动态类型是Quote另一个动态类型是Bulk_quote为什么这里用make_ptr而不是shared_ptr,这俩个区别是什么
因为shared_ptr创建一个指针时,里面会写new一个类型,但这是在堆上创建一个动态对象,而堆上的内存和智能指针无关系(仅仅new对象成功了,但是这个智能指针没有成功,他需要另外的内存存放引用计数,生成计数内存和堆上的内存是分开的,当生成计数内存出错了,那上面就会内存泄露,因为此时还没哟产生智能指针)make_ptr是对象和计数这俩个内存放在一起,要不都成功,要不都失败,不会产生内存泄露
