构造器
基类的构造器在子类的构造器之前调用
1 | class Animal |
- ;类的属性和方法默认为私有属性。
- 如是局部对象,在函数返回即销毁局部对象的时候调用析构器。
- 构造器可以有参数和无参数,析构器没有参数。


方法的重写和方法的重载的区别
- 方法的重写:子类和父类拥有同样名字和参数返回值类型的函数,叫做子类重写了基类中的方法。
- 方法的重载:名字相同但参数个数或类型不同的多个函数,在调用函数式会根据传入参数的类型和个数自动确定调用哪个函数,叫做方法的重载。
- 重载并不是面向对象的特征,但可以简化编程。 简化编程是C++的全部追求。
- 类中的方法可以重写也可以被重载。
- 最好少用重载,重载的方法越多程序就越难看懂。
- 子类继承了基类的方法,然后不能再去重载这个方法。

子类可以直接调用基类的类方法,因为类没实例化之前都没有分配空间,没空间的调用没空间的。
静态属性和方法
- 静态属性属于这个类,不属于某一个对象。
- 能过在该类的所有对象之间共享这个类属性。


- 为类属性分配内存并初始化为0,类属性必须要在类外面定义一下(和类方法一样)目的是为了分配空间

- 类属性对类和类的所有对象可见,所以可以作为类实例化为对象个数计数,在对象的构造函数对类属性count++,析构函数对类属性count–。
- 类的静态方法可以直接用:类名字::类静态方法()、对象.类静态方法() 两种方式调用,推荐使用第一种,更能清楚的知道这是个类静态方法。
- 静态方法只能访问静态成员变量,无法访问非静态成员变量。

C++的this指针

虚方法
1 | class Pet |

new Cat(参数): 参数是传递给构造器的。

- Pet是Cat和Dog的基类,可以用Pet接收Cat和Dog的new出来的空间。
- 但是new是在运行时才确定的,把这个空间解释为Cat和Dog。
- 而在编译时Cat和Dog都是Pet类型的指针,所以编译器会把Dog和Cat解释为Pet类型。
- 所以在Dog和Cat类里面重写了Pet类型的Play方法没有起作用,调用Dog.play时任然是调用的Pet.play。
- 要想让Dog.play解释为重写后的Dog.play就必须在Pet.playe声明为虚方法,在方法名前面添加Virtual关键字。
- 虚方法会使程序速度慢一点,但如果不确定是否应该将其声明为虚方法,就最好声明为虚方法。防止程序没能按预期运行。
- 虚方法是继承的,基类中的虚方法,子类也必须声明为虚方法。
- 在实现一个多层次的继承关系的时候,最顶级的基类应该只有虚方法。(类似java的接口)
- 其实析构器就是虚方法,析构对象的时候都是先调用的最底层子类的析构函数,而不是直接调用的基类的析构函数。
小结:1.虚方法是在运行时根据new的类型才被确定具体该调用哪个子类的方法,而非虚方法是在编译时根据对象的类型就被确定了。对象指针是Pet类型就调用Pet类型的方法。
2.基类的虚方法可以用来实现多态,使用基类的指针接收不同new出来的子类(子类中已对基类的虚方法进行了重写),当调用不同的基类对象的虚方法可以实现运行不同的方法而得到不同结果。
3.就像上面的Pet类型的两个对象(Dog和Cat),同样是Pet类型Dog和Cat对象在调用自己的Play方法时运行的结果确是不一样的。
抽象方法
抽象方法也可以称为纯虚函数。基类中只声明不定义,交给子类去实现定义的方法。

1 | class Pet |
多态的概念

- 重载与覆盖

- 编译时的多态性:通过重载实现。
- 运行时的多态性:通过虚函数实现。
- 当一个类被作为基类时,它的析构函数应该写成虚函数

虚函数是可以被继承的,基类中的虚函数,子类里面也必须写成虚函数。
运算符的重载
运算符的重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该方法,以实现相应的运算。
也就是说,运算符重载是通过定义函数实现的。运算符重载实质上是函数的重载。
- 运算符函数格式


以上运算符重载函数,在使用 + 号时,实际会执行 - 号操作。
1 | class car |
- C++不运行用户自己定义新的运算符,所以要想让固有的运算符显示不用的运算就只能对运算符进行重载。
- C++不允许被重载的五个运算符: . * :: sizeof ?:
- 重载运算符的一些规则


多继承
多继承场景:但一个类不能用是来表示的时候需要多继承。
学生是人、老师是人,学生和老师都单继承人类。
多继承示例:
- 假如有个助教,他必须多继承学和老师两个类。因为助教一方面是学生的角色一方面又是老师的角色。
- 此时不能说助教是老师或者助教是学生,助教应该同时拥有学生和老师类的方法、属性。
虚继承
- 上术例子,助教多继承了学生和老师类。而学生继承了人类,老师也继承了人类。导致一个助教对象拥有两份人类对象实例。
- 导致助教使用人类属性时出现不确定性,因为助教有两份人类实例。
- 虚继承就是为了解决这个问题,虚继承可以保证一个对象最多只拥有一个类的一份实例。



- 继承类的构造方法定义

如果一个类继承了其它类,构造方法不是默认无参的情况下,需要在自己的构造函数调用基类的构造方法。
如果一个类是虚继承一个类,需要在其子类的构造方法调用虚继承的类的构造方法。
虚继承的东西不能被再继承出去:Teacher和Student类都是虚继承Person类,Teacher和Student类都不能拥有Person类的备份(不能继承给TeachingStudent类只有一份Person类实例)。所以需要在TeachingStudent类的构造函数调用Person类的构造方法,使TeachingStudent类自己去继承一份Person类,而不是通过Teacher或者Student类。
可以把虚继承理解成一个单例继承:只会在子类申请一份空间。别人不能通过这个类再去继承这个类的虚继承来的东西。
继承的本质就是在子类中申请一份和基类同样大小的空间,再把基类拷贝进来。而虚继承就是不让自己虚继承来的东西被别人继承(拷贝)走,别人需要自己去继承(拷贝)我虚继承来的东西。
- 什么时候用虚继承
- 当一个类太抽象(底层)了,可能被多个同级别的类继承,这时候就需要用虚继承来继承这个类。
- 当一个类需要多继承的时候,首先考虑是否应该用虚继承。
同级别的类:会被一个子类同时继承(包含)的基类。
C++的错误机制
- 错误的分类
- 编译时错误。
- 运行时错误。
- 防止运行时错误方法
- 在一个类里面编写类专门的测试方法。
- 类方法多返回错误码。
- windows的蓝屏错误代码,就是运行时的错误码。
- try catch




- 示例


- try语句块中函数抛出异常后语句块中函数后面的代码就不会被执行,而是去执行catch中的代码了。
- try catch 可以使用参数指针(即异常类型参数)加goto实现。
- 使用异常的基本原则是:应该只用它们来处理确实可能不正常的情况。
- 开发测试阶段,应多用assert()来捕获运行是错误。
- 在构造器和析构器中不应该使用异常。

动态内存管理
new出来的内存块:里面充满了垃圾数据。
delete后的指针最好将他指向NULL,防止程序在后面错误使用delete后的指针指向空间的内容。
副本构造器
- 逐位复制

- 当一个对象包含指针,将这个对象复制一份。就有两个对象里面的指针都指向同一个内存块。而当一个对象删除时将内存块释放,另一个对象还通过指针访问这个内存块就会出错。并且删除第二个对象时也会去释放这块内存出现二次释放的错误。
- 解决的版本就是两个对象的指针分别指向两个不同的内存块。
- 在复制对象的时候把对象指向指针的内存块也复制一份。

- 重载 = 号的方法解决

以上情况可以在类中对 = 运算符进行重载,在运算符重载函数中处理类中的指针。

= 号重载函数,接收一个const同类型的引用并且返回一个同类型的引用。
返回一个同类型的引用可以保证连续赋值:a = b = c

- 创建副本构造器的方法解决


在对象声明的时候同时初始化对象,编译器会在类中寻找副本构造器,找不到编译器就会自己创建。


C++的对象安全的类型强制转换
- C语言方式的类型强制转换
C语言的方式强制转换不同类型的对象,如果被转换的类型占用空间小于接收对象的类型,在访问成员时可能访问越界的空间。
- C++安全的强制类型转换

- 也就是只有子类(占用空间大)可以强制转换为基类(占用空间小)。
- 以上的C++语法也是通过函数实现的,会把类型的检测放在函数中,不符号规则的强制类型转换类型将返回NULL。


命名空间


不要把using指令用在全局作用域,将失去命名空间的意义。
- 单独从命名空间提取需要的部分

链接器和作用域

- 如果一个函数没有被声明就调用,编译的时候就会报错。
- 如果一个文件中有这个函数的声明就可以使用这个函数,编译的时候就不会报错。
- 如果一个函数声明了,并且调用了。但没有在任何一个源文件定义这个函数,链接的时候就会报错。


没有被初始化的static变量储存在.rss段,被初始化的static变量储存在.data段。
- 内链接和外链接

外链接:变量函数定义在其他文件,需使用extren关键字。头文件中函数如果会被其他文件调用都需要加extren,只是编译器默认给我们添加了。
内链接:本文件调用本文件定义的函数、变量。static用来说明这个变量是一个内链接变量、函数。
无链接:局部定义的变量。
模板
- 范型编程


- 模板

STL库是范型编程的经典之作,它包含了许多非常有用的数据类型和算法。

这里的class不是类的意思,这只是一种通俗的写法。

模板相比于函数的重载:
- 重载需要自己手动定义需要的不同参数功能相同的函数。
- 模板把自己手动定义函数的事情交给了编译器做,解放了程序员。
- 函数重载就算不使用的函数也会被定义,而模板是根据需求来定义函数,不需要的就不会被定义。
- 模板的另一种声明写法

- 类模板


为明确说明swap是一个函数模板,调用时可以使用swap
(i1, i2); C++的 array、vector模板就是此写法:
vector
vd; vector<double, 4> vd;
array<int, 5> ai;




即vector<int, 5> vd;
迭代器

每一个容器都会提供一个迭代器,迭代器的定义如上代码。
容器:用来处理数据的方法集合和储存数据的集合,类模板的实例就是容器。它可以处理一类数据和保存这类数据。
运算符隐式转换
- 如果两个整形运算,如果是char short比int小的,全部提升为int类型运算,再按照需要赋值的类型转换给赋值变量的类型。因为int类型硬件在计算时最快。
- 如果两个数分别是有符号和无符号,先看哪个数类型的优先级高,怎转换成哪个符号类型进行运算。
- 如果两个数分别是有符号和无符号相同类型,则先看有符号是否可以容纳无符号转换为有符号的类型。可以则将无符号类型转换为有符号类型进行运算,否则全部使用有符号类型的无符号版本。
C++扩展字符串
1 | wchar_t title[] = L"cherio frv"; // w_char string |
枚举
- 枚举变量不能进行算数运算,自增、自减。因为枚举变量算术运算后就可能不在枚举声明的范围了。
- 枚举类型的变量只能赋值为枚举声明范围内的,不能直接赋值为int类型的字面量或变量等(但可以使用强制类型转换)。
- 枚举量在和int类型变量做算术运算时,首先会被转换为int类型。
1 | // 一个枚举声明中可以有多个枚举量的值相同 |
- 枚举类型值的取值范围
取值范围定义如下:
- 首先,要找到上线,找到枚举量的最大值,找到大于这个最大值的,最小的2的幂,将它减去1,便是取值范围的上限
- 例:enum bigstep 最大枚举值是101,在2的幂中,比这个值大的最小的值为128,因此取值范围上限为127.要知道下限,需要知道枚举量的最小值.如果它不小于0,则取值范围的下限为0.否则,采取与寻找上限方式同样的方式,但加上负号,例如,如果最小的枚举量为-6,则比它小的,2的幂最大的值为-8,加1之后为-7.于是,上限与下限便能算出来.
指针
使用常规方法定义变量时,值是指定量,地址是派生量。
面向对象编程的与面向过程处理数据的策略:
- 面向对象在程序运行时动态分配内存(动态联编),面向过程在编译时(静态联编)确定并分配固定大小内存。
- 通过动态创建变量、结构、对象的方式:地址是指定量,地址中的内容(变量)是派生量。
- 通过编译时通过类型的方式定义变量、结构、对象的方式:内容是指定量,地址是派生量。
- 动态创建的好处可以方便的根据程序运行时,每次需要的大小申请不同大小的空间,C++使用 :new 类型。
当地址为指定量时,*指定量:运算符结果被称为间接值。
不要重复delete一个new出来的指针,对NULL进行delete是安全的。
- 动态数组的申请释放
1 | /* 变量、结构、类 */ |
- 指针的算数运算
- 指针进行乘除运算是没意义的。可以用两个指针做加减运算将得到一个int类型的整数(仅当两个指针指向同一类型时才有意义,将得到两个元素的间隔)。
- 指针加1,相当于加:指针指向类型的字节数。减1同理。
- 指针加n,相当于加: n * 指针指向类型的 字节数。减n同理。
- 指针和数组区别
- 数组名是常量(是第一个元素的地址类型为数组的类型),指针是变量。
- 数组名是数组第一个元素的地址,而对数组名取地址是整个数组的地址。数组(类型是数组类型)和对数组名(类型是一维数组)取地址:都是指针值是一样的但类型不一样所以它们表示的步进长度也不一样。
- sizeof(数组名)返回整个数组的长度字节。
- arry[7]和*(arry + 7)等效。下标便表示法内部就是使用指针实现的(所以指针表示法的效率应略高于下标表示法)。
1 | int arry[10] = { 0 , 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
- 结构数组
1 | struct tyu ar[6]; |
模板类
- 模板类vector
- verctor类类似于string类,是一种动态数组。内部通过new和delete实现。
- 可以在程序运行时动态创建不同(变量或字面量)大小的相同元素的空间。
- 空间存储于堆上。
vector是c++的一个类可以看成是一个单链表及其函数集,可以动态创建,动态插入删除vector里面的元素。
1 |
|
- 模板类array
- array类和数组类似,只能在编译时分配字面量大小的固定空间。
- 空间存储在栈上。
array相比数组更加安全,array类有运行是对数组边间检查。array类有方法设置array对象的内存范围等安全手段。
1 |
|
数组:a1[-2] = 20,等效于*(a1 - 2) = 20。此方法是采用数组下标,(语法没错)但可能会出现越界。而array类通过类方法的形式:
array模板类:a1.at(-2),at方法就是索引元素的意思,还有a1.begin(s)、a1.end(e)。如果-2不在s和e之间(非法索引)程序就会捕捉到。
打印字符串地址:
1 | // cout对象的智能类型判断功能 |
表达式
- 表达式
1 | 22 + 27 // 表达式的值为49 |
都是表达式,表达式的值定义为左侧成员的值。
- 语句
1 | 22 + 27; // 在表达式后面加上;就变成了语句 |
- 只要加上;所有的表达式都可以变成语句。
- 赋值语句的值为左操作数的值。
- – ++ 表达式
C++用户类定义这些运算符存在差别:
–前缀函数:将值加1,然后返回结果。
–后缀函数:将值先复制一份,将其加1,然后将复制的值返回。
因此,–前缀版本的效率更高。
C++内置的的版本前缀后缀无差别。
指针变量的自增自减:增加其指向类型占用字节数。
逗号运算符
1 | i = 20, j = 2 * i; // 先计算i为20 j为40,表达式的值为40 |
- 逗号运算符从左边开始计算,逗号表达式的值是最后一个逗号后的值(最右边)。
- 而函数参数是从最右边开始往左边的顺序计算。
- 逗号运算符是所有运算符中优先级最低的。
比较字符串
string类不使用字符’\0’来标记字符串的末尾。
cin对象
cin.get()不用传参数的引用即可改变参数变量的值。
文件末尾EOF,cin.eof()或者cin.fail()检测是否读到文件末尾。EOF值为-1。
- c语言的getchar()、putchar(int)为什么返回的是int类型,因为需要返回EOF(-1),但char类型根据编译器有些是有符号有的是无符号,所以不能全部用char。
- EOF可以使用Ctrl+Z或者Ctrl+D来模拟,以终止输入。
- cin.get(ch)返回一个cin对象,所以可以cin.get(ch).get(cha1)。
- cin.get(ch)有参方法到达文件末尾是不会把EOF(-1)的值赋值给ch。而cin.get()无参方法(类似c语言getchar)会返回EOF。
- cin.get()无参方法(类似c语言getchar)返回的是int类型而不是cin对象,cin.get(ch)返回一个cin对象。
- cin << ch; 这种方法读取字符串将忽略空格、换行符、制表符,需要这样使用 ch = cin.get();
关系运算符
|| && ! 先对关系运算符先修改左侧的值在对右侧的值进行判定。|| 如果左侧的值为true将不会再去判定右侧的值。
关系运算符和逗号分号运算符都是一个顺序点(以运算符为中心分为左侧和右侧),顺序点会先判断左侧。
&& 运算符优先级高于 ||。
字符函数库
cctype :isalpha(ch)测试是否是字母、ispunct(ch)测试是否为标点符、isdigits(ch)测试是否是数字、isapace(ch)测试字符是否是换行符、空格、制表符。
更多库函数见库文件。
switch效率高于else if
全局错误码
1 | int *_fpErrNo(void) |
非局部跳转
goto只能在函数内跳转,非局部跳转是在函数之间。
1 | jmp_buf gJmpBuf; |
setjmp()函数返回longjmp()函数所设置的参数val值,程序将继续执行setjmp调用后的下一条语句(仿佛从未离开setjmp)。参数val为非0值,返回val。若设置为0,则setjmp()函数返回1。
非局部跳转模拟C++的异常处理机制
1 | jmp_buf gJmpBuf; |
longjmp()函数必须在setjmp()函数的作用域之内。在调用setjmp()函数时,它保存的程序执行点环境只在当前主调函数作用域以内(或以后)有效。若主调函数返回或退出到上层(或更上层)的函数环境中,则setjmp()函数所保存的程序环境也随之失效(函数返回时堆栈内存失效)。这就要求setjmp()不可该封装在一个函数中,若要封装则必须使用宏(详见《C语言接口与实现》“第4章 异常与断言”)。
断言
1 |
注意,expr1||expr2表达式作为单独语句出现时,等效于条件语句if(!(expr1))expr2。这样,assert宏就可扩展为一个表达式,而不是一条语句。逗号表达式expr2返回最后一个表达式的值(即0),以符合||操作符的要求。
各种字节对齐
1 |
|
项目地址:
https://github.com/smartmx/TFDB/tree/main
- 本文作者: 龙兄嵌入式
- 本文链接: https://hexo.880755.xyz/2022/05/11/C++ Primer Plus/

