C++面试总结

本文深入解析C与C++在内存管理、虚函数、多态、动态内存分配、引用和模板等方面的差异,以及lambda表达式、内存对齐和构造析构函数的特性和最佳实践。

C语言和C++的区别:
区别1:C语言是面向过程的语言; 主要模式 输入 + 处理过程 + 输出。
C++是面向对象的语言,主要特征:封装、继承和多态,封装实现的细节,代码模块化;继承:派生类继承父类的数据和方法,扩展已经存在的模块,实现代码重用;多态:一个接口有多种实现方式,派生类重写父类虚函数,实现接口重用(多态)。

区别2:C /C++动态内存管理方法不一样,C是malloc/free,C++除此之外还有new/delete。
区别3:C++有引用,C中不存在这个概念。C++新版本一直在迭代新特性。
区别4:C++是C的超集,兼容C 包含C的内容,并且提供大量C++特有的新特性。

NULL和nullptr:
C语言中,NULL是 (void*)0; 整型字面量 0(即 #define NULL 0)。 为了兼容海量的C语言代码,因此C++支持了NULL。
C++中,NULL是0; C++不能将void*类型隐式转换为其它指针类型。 用0来表示空指针。C++强烈不建议使用NULL,建议使用nullptr。

C语言NULL的缺点:
所有缺点来源:本质只是一个整数0.不是一个真正的指数类型
1.类型模糊缺乏安全性(核心缺点)
C++需要类型很明确,NULL无法和整数0区分开来,函数重载、模板推导中会调用错误的函数版本。
2.误导人,一般NULL给人直觉是个空指针,编译器得到的是个整数。
3.C++强类型,数据类型必须清晰明确。

C++ nullptr的优点:
nullptr的设计,就是为了解决NULL的所有缺陷。
1 .nullptr类型安全 ,是 std::nullptr_t 类型的纯右值,可以隐式转换到任何原始指针类型绝不能转换到非指针类型。
2. 命名上,意图明确, null + ptr,就是空指针意思。
3. 在模版和自动推导中表现完美,泛型编程更加健壮。可以被正确推导到所需的指针类型。
4.nullptr是C++的一个关键字,并非宏定义,没有那么容易被替换。

nullptr:(1)可以标识空指针,(2)可以转换为任何指针类型和布尔类型。

二进制程序文件启动过程,如何分配内存?
1.会将二进制依赖的所有dll(二进制本身自带的dll,或者依赖的系统dll),都加载到进程空间中,这些dll本身自己也是二进制可执行文件,都加载到进程的代码段内存区。
2.等待所有依赖的dll加载完毕到进程空间后,才会将二进制主程序加载到进程空间,
3.然后启动运行时库
4.给全局变量,并执行初始化操作,分配全局内存区。
5.这时候才进入main函数,程序才算真正启动起来。
6.进入函数时,从所在线程的栈内存中,给函数局部变量分配栈内存; 遇到malloc/new,在堆上分配堆内存。

lambda: (可以看做未命名的内联函数)
C++ 11 引入的一个重要特性,(思想来源于函数式编程
是一种lambda表达式,用于定义并创建匿名函数对象(必包),简化编程工作。
lambda表达式 比起普通函数 的 区别: 没有函数名,(匿名函数和具名函数(普通函数)的区别)
lambda表达式的基本语法结构如下:
[capture list] (parameter list) specifiers exception -> type (function body)

[capture list]: 捕获列表,[]这个方括号必须要有;lambda函数最开始的位置,编译器根据[]来判断接下来的代码是否是lambda函数,去捕获上下文变量,给lambda使用;
捕获列表的作用: (1)定义了lambda表达式可以访问的外部作用域中的变量及其方式。(2)代码的安全性,明确性,用哪些外部变量,怎么用,权限边界在哪。

捕获方式[]有下面5种:
(0)[]: 空的,不捕获任何外部变量。
(1) 值捕获[=]: 里面是个等号=,以值的方式捕获所有外部变量,默认不可修改; 外部变量以const常量引用方式传递到lambda表达式中,表达式中可以访问变量、但不能修改变量。 使用=等号可以将函数作用域中的所有变量以值捕获的方式传入到表达式中。
(2)引用捕获[&]:里面是个引用符号,将外部变量以引用的方式传递到lambda表达式中,表达式中可以访问变量和修改变量;用&引用符号。
(3)混合捕获[a, &b]:捕获列表捕获函数作用域内的多个变量,既有值捕获也有引用捕获
(4) [this]: 捕获当前类的 this 指针,从而可以访问类的成员变量和函数。
*(5) [this] (C++17): 以值的方式捕获当前对象(生成一个副本)。

参数列表: ()
是可选的,不需要传参的话()都不需要写;但是建议还是保留括号。
调用这个表达式函数要传的参数。

返回值类型
->return_type:
可写可不写,编译器会自动推导;

{}: 函数体函数体内包含return之外的其它语句,则编译器推断返回类型为void。 这时候就需要自己设置返回类型。

最简单的lambda表达式:[] {};
lambda把一个函数 变成了 匿名可调用的代码块,
参数列表 和 返回类型 可以省略。
捕获列表 可以 捕获值,也可以捕获引用。
返回类型 可以自己推断(根据返回值)

lambda底层原理:
编译的时候,编译器把这个表达式翻译成一个未命名类的对象(闭包类型),该对象包含(重载)一个operator操作。从而可以像函数一样被调用。 参数都包含在这个类成员里面。对lambda的调用,就是对这个类对象的调用。

  1. 捕获的变量:变成了类的成员变量;
  2. lambda表达式尽量使用auto类型,让编译器自动推导;
  3. lambda是一个对象,有类型,有大小。

lambda 优势和适用场景:
在调用的地方直接定义。使用时候去定义,不用跳出当前函数,定义时立即执行,(适用于小逻辑的函数)
1. 对于一次性使用、简短的函数逻辑, 适合lambda;不需要单独写在外部;(就地性)
2. 捕获列表的定义,简化代码写法,不需要显式传参,可以直接用上下文的变量。(形成一个闭包)

STL的一些应用和回调时,适合用lambda。

auto: 在程序编译时期就确定了数据类型。 数据类型自动推导。 运行时类型就是确定的,没有任何性能开销。
原因:C++是静态类型的语言,编译时就要确定好所有变量类型的情况,内存分配需要类型匹配,函数传参调用需要类型匹配。
运行时再确定类型,效率比较低;编译时就确定时,方便性能优化;出现类型错误也方便修改。
过程:根据右值确定数据类型,将auto替换成推导出来的数据类型。

C++模板
泛型编程的一种实现方式。
实现功能基本相同,仅仅是数据类型不同时,可以用模板。

函数模板:
定义一个通用函数,所用到的类型(参数类型、返回值类型、局部变量类型都可以作为参数),用一个虚拟类型代替。这个通用函数就叫做函数模板。

一般放在全局, template关键字告诉编译器开始泛型编程。
模版本身不是代码,只是一个类似框架的东西。编译器为特定模版参数(如int、double等)生成真正的函数或类代码。 这个过程叫做模版实例化。

隐式实例化,自动推导; 最常见常用的方式。自动由编译器触发,
会有冗余的问题: 同一特化在多个单元被使用,都会独立实例化一份相同代码。

显示实例化,告诉用哪个。手动控制。减少冗余。但是不灵活。需要新的类型还要重新修改代码编译。

make-shared 和 new的比较:

new:
  1. 是一个c++ 运算符,在堆上分配原始内存,并返回一个指向该内存的原始指针
  2. new只负责分配内存,不负责释放内存。(需要delete配合释放内存)
  3. 在使用new的过程中 ,内存分配需要2次, 往往需要2个步骤:(1)先new(分配对象内存),(2)再赋值给shared_ptr(在堆上创建一个智能指针控制块,为控制块分配内存);
    4. 内存释放: 对象内存和shared_ptr控制块内存分开释放。(1)shared_ptr析构后,对象内存立即被释放;(2)weak_ptr析构后,控制块内存被释放。
    new的优缺点:
    1.分配2次,不连续,会导致内存碎片化
make_shared:

直接用make_shared创建指针。
1.是一个函数模版,
2.一次分配获得单块内存,同时容纳对象和控制块。 (优点:减少内存分配的开销,提高了空间局部性–都挨在一起,对缓存友好,也提高了性能)。
3.内存释放: 对象和控制块内存一起释放(可能会有延迟,等weak_ptr)
4.不支持指定自定义删除器。

适用场景:
1.大多数情况下,make_shared都是首选的;

2.需要自定义删除器的时候,或其他自定义高级特性,用new;

c++ 11 加了move语义,(move太复杂了。。。)

虚函数

C++的多态主要通过虚函数实现

什么是虚函数:
virtual: 带有关键字virtual声明的成员函数就是虚函数。
不能加static,虚函数动态绑定,static是静态绑定。

虚函数的作用:
允许在派生类中,重写这个虚函数,从而实现运行时多态。

运行时多态:
(1)没有虚函数时,函数调用在编译时就根据指针类型确定了(静态绑定,早绑定)
(2)有了虚函数,函数调用在运行期根据对象的实际类型确定(动态绑定,晚绑定)。

纯虚函数
1.没有函数体,定义的时候函数名后面加上=0.
2.包含纯虚函数的类,叫做抽象类
3.在基类中,只有声明、没有定义。
4.抽象类无法实例化对象;抽象类的子类,必须把纯虚函数全部实现,才能实例化对象。
作用:
1.强制派生类必须重写纯虚函数。体现了接口继承

虚函数和重载的区别:
重载函数实现(函数名相同,参数返回值不同); 运行多态性: 虚函数实现(都相同。。)。。

虚函数的实现:
主要依靠虚函数表(virtual table, vt),和虚函数表指针(virtual ptr, vptr)

  1. 有虚函数的类,都会在编译时生成一个虚函数表(存放在内存的代码段、常量区)。虚函数也存放在代码段中。
  2. 虚函数表:(1)是存放全部虚函数地址的数组,以NULL结尾。(2)每个虚函数地址都相当于一个虚函数指针 并且,(3)排在对象的地址首部(保证能正确取到虚函数的偏移量)。(4)父类、子类顺序保存,表状结构。
  3. 虚函数指针,存放在对象中。

具体对象调用时,通过对象实例的地址得到虚函数表,然后就可以遍历其中的函数指针,进行相应的函数调用

虚函数的继承:
1.父类,一般都是用来让子类覆盖的,
2.覆盖情况下的子类继承(覆盖的函数放在表中最前面,调用覆盖函数时,实际调用的子类函数。。)
3.多重继承: 地址首部有多个虚函数地址(按照声明顺序)
覆盖的情况下,覆盖函数在每个父类地址中都有。。
原因: 一个派生类有两个或多个基类,从两个或多个基类中继承所需的属性。
缺点: 结构太复杂了,优先顺序模糊,功能冲突。
二义性: 多重继承会产生的问题(两个基类中数据成员名称相同)
二义性的解决方法:
(1)用类名做前缀,来调用成员。(2)好好设计类。
4.多级继承(级联继承): B继承A, C继承B, 这种不会有啥问题。很正常的继承方式,单向继承。
5.菱形继承:。多重继承时,一个基类在派生类中被多次继承,导致数据成员存在多个副本。
虚继承: 使用virtual来修饰继承关系,。虚基类在继承中无论出现多少次,最终派生类中都只包含子对象。。(保证不会出现重复的副本)

虚函数的构造函数和析构函数
1.构造函数不能是虚函数!!! 虚函数表指针是在构造函数初始化列表阶段才初始化的;需要先有构造函数,才能有虚函数,互相矛盾了!。 初始化时默认调用的,不可能通过父子指针去引用。
2.析构函数可以是虚函数!!!一般都必须是虚函数 不影响,所以可以。。(通过虚函数表地址,可以指引正确delete析构对象。) 为了避免内存泄漏,析构时必须调用子类自己实现的析构函数用来释放。(不存在继承关系的话,单独一个类本身不需要虚函数)不用虚函数的话,继承关系中,可能只调用基类的析构函数,派生类对象不会释放内存。
类析构 顺序:( 1)派生类本身的析构函数;(2)对象成员析构函数;(3)基类析构 函数。
3.内联函数 不能是虚函数!!不能,内联函数没有地址(只是在调用点对代码展开。。)
对象访问普通函数,和虚函数哪个快?? (1)普通对象的话一样快,(2)指针对象或者引用对象的话,普通函数更快,虚函数要去列表中查询地址,查表。。

有虚函数的类对象不能使用memset来初始化!!!
memset初始化类对象会报错:
初始化obj的时候,将obj包含的指向虚函数表VTBL的指针也清除了。包含虚函数的类对象都有一个指向虚函数表的指针,此指针被用于解决运行时和动态类型强制转换时虚函数的调用问题。
解决方法:有虚拟函数的类对象,决不能使用memset来进行初始化操作。而是要用缺省的构造函数或其它的init例程来初始化成员变量。

override和final:
override: 明确表示意图重写基类的虚函数,函数标记为override,强烈建议使用;如果子类没有任何重写,编译器会报错。
final:表示不能被继承。 用于类,表示该类不能被继承;用于虚函数,表示该虚函数不能在派生类中被重写。
1.每个类都有虚指针和虚表;

C语言字节对齐

字节对齐(Byte Alignment), 也叫做数据对齐(Data Alignment)
是一种提高程序性能,尤其是内存访问速度,编译器和计算机硬件强行加的内存数据存储规则。

核心思想: 类型为T的变量的起始地址,必须是sizeof(T)的整数倍。

需要要对齐的原因:
1.现代操作系统CPU访问内存,并非以字节为单位来读取内存,而是以字长(word size)为块来访问。如:32位系统是4字节,64位系统是8字节。
2.如果数据没有对齐,CPU可能需要访问两次内存数据,才能读到完整的数据。显著降低性能。
3.某些架构,不对齐的话,还会导致异常和崩溃。

如何做到字节对齐: 以64位操作系统为例。
(1)基本数据类型,在特定平台上固定长度。
(2)结构体或类对齐成员中值最大的那个值

对于类和结构体,对齐比较浪费存储空间。需要优化
将大小相近、对齐要求相同的成员放在一起,或者从大到小排列成员,通常可以有效地减少填充字节。

#pragma pack
编译器特定的预处理指令,控制结构体(struct)、联合体(union)、类(class)的成员在内存中的对齐方式和填充(padding)。
主要作用:修改编译器中默认的对齐规则,允许指定一个更小或更大的打包对齐边界。

有些情况下,需要紧凑的内存布局。如:CPU通信协议,硬件驱动程序,寄存器。。
建议用法:修改一种对齐方式后,可以完美恢复。
#pragma pack(push, n)
#pragma pack(pop)

新的对齐规则:
(1) 对于结构体成员,min(n, 该成员的自然对齐要求) 的整数倍
(2) 整个结构体对齐,min(n, 结构体中最宽成员的自然对齐要求) 的整数倍

大多数64位的linux、unix系统都是LP64模型;32位Linux系统是ILP32模型;64位windows使用LLP64模型。
基本数据类型的地址必须是某个值K(2 4 8)的倍数。

初始化参数列表 和 构造函数体 的 区别:
构造函数,通过初始化列表和函数体内赋值,都可以实现对成员变量赋值。
构造函数体:
1.指:构造函数中位于参数列表和初始化列表之后,由大括号 {} 包围的代码块
2.开始执行构造函数体时,内存已分配;所有基类都已通过对应的构造函数初始化完毕;对象的基础构造已经完成。
3.构造函数体内,做的大多数都是赋值操作。
4.性能较低。需要先调用默认构造函数,再执行赋值操作。
5.必要性:所有初始化操作,都可以用初始化参数列表来完成,没必要必须放到函数体内。

初始化参数列表:
1.执行时机:对象正在创建/构造过程中。
2.是真正的初始化操作。
3.有些场景必须使用初始化参数列表:
4.性能:更高;直接调用了构造函数初始化,不需要多一个赋值操作。
5.必要性:有些类型的成员必须使用参数化列表初始化,如:const类型,引用成员,没有默认构造函数的类成员,
例子写法如下:
Example(const std::string& name, int value): m_name(name), m_value(value)

1.初始化列表: 所有类的非静态成员,都可以在这里初始化;普通成员变量,const,没有默认构造函数的。
2.构造函数体: 普通成员变量,需要复杂运算的初始化变量。
3.类的静态成员static不能在这里初始化,静态成员属于类,类的所有对象共享,专门在外部初始化。

形参 和 实参(函数传参):
形参:是函数声明和定义中的参数值,是空值,没有实际数据;
实参:是调用函数时,把实际有数据的值传到函数里。
函数参数传递的3种方式:
值传递:实参传递给形参的是实际的值,形参和实参 在内存上是两个独立的变量(函数中是在临时分配的栈上操作,函数执行完形参就消失了),对形参做任何修改,不会影响实参。(不希望改变影响调用者时,使用值传递)
引用传递:实参传给形参的是被引用的变量的地址,实参和形参在内存上指向了同一块区域,(形参添加一个&符号,表示是引用,只有C++支持,形参相当于实参的别名,对形参的操作都相当于实参的操作)
指针传递:形参为指向实参地址的指针,对形参的指向操作时,相当于对实参本身进行操作,外部实参也会被修改。
在这里插入图片描述
引用: 只是个别名,本身不占用内存
编译器通过符号表,记录引用关系,实际会直接替换成被引用变量的地址。

函数参数的压栈顺序从右至左( 栈底是最右边参数,栈顶是最左边参数),对于变长参数也好处理,栈顶看到的都是首参数

如何理解指向指针的指针:
指针本质是保存地址的寄存器,保存一个地址;
指向指针的指针,就是指向地址。为了获取地址

C++ 中的链接库
很多程序都需要依赖基础的底层库,库是可执行代码的二进制形式,被操作系统载入内存执行。
静态库(linux是 .a格式, windows是 .lib格式)
动态库(linux是.so格式, windows是.dll格式)

静态库编译链接时载入,链接时,库完整的拷贝到可执行文件中,多次使用就会有多份冗余的拷贝;生成可执行程序后,静态库就不需要了。 ,每次都要全量更新。
特点:编译链接时执行,移植方便(直接拷贝, 运行时不再依赖); 浪费资源空间( 所有相关目标文件和牵涉的函数库都会被链接合成一个可执行文件);静态库更新时,会很麻烦,所有依赖该库的程序都要再重新编译(全量更新。)

动态库(又叫做共享库):程序运行时 才被载入,供程序调用;系统加载一次,便可以被多个程序共同使用,共享库实例;增量更新

数据类型:
计算机底层存储,本质是二进制,
位运算符总结:
&:与运算符, 2者都为1,则结果为1;
| :或运算符,有1位为1,则结果为1;
~: 非运算符, ~0 = 1, ~1 = 0
^: 异或,2者不相同,则结果为1.
<< : 左移符号,各二进位 全部左移 若干位, 高位丢弃, 低位补0;
–>>: 右移符号,各二进位 全部右移若干位,高位补0,
。。。。
常用位操作:
判断奇偶:
x&1 == 1, 等价于 x%2 == 1, (看最低位是奇数还是偶数)
x&1 == 0, 等价于 x%2 == 0,
x>>1, (右移1位), 等价于x/2,
获取x第n位的值,(x>>n)&1(右移n位,最低位正好是第n位,和1与操作,得到第n位是1或0)

位掩码(Bit Mask):类似子网掩码,屏蔽输入段,

位运算练习题目:
5亿个int数字,找到中位数,内存有限。
(1) 分治思想:从高位到低位,先比较最高位,0和1分治,0的数字大于1的数字,通过个数统计便可以判断中位数位于哪一堆。。
以此类推,继续比较次高位,对小规模数据进行分治。(根据每一堆的个数。。)

数据量小时,维护2个堆;大顶堆的最大值,小于 小顶堆的最小值,
偶数个数时,结果为两数之和; 奇数个数时,结果为多数堆的堆顶元素。

C++ 不支持二进制的表示,支持8进制,10进制,16进制。
(以0开头的数字,就是8进制。0x或者0X开头,代表16进制。其它整型数字,默认都是10进制。。)
int类型:默认是signed int,有符号类型,最高位是符号位,数据仅仅31位。
UINT32:是unsigned int的别名,无符号整数,最高位也是数据位,占32位。typedef unsigned int UINT32
INT32: 表示有符号的整数,32位有符号。

int32_t, 根据不同机器占据不同的位数。。 还有uint32_t…

atoi函数:将字符串转换成整型数,(ASCII to Integer), 只转换整型数字的字符串。

to_string(), 数字转字符串。

string.c_str(), string转const char*类型。

字符转数字: 直接进行计算,每个字符有对应的ASCII数值。。 比如:‘A’ + 1 = ‘B’

C/C++中的内存

操作系统中,会先把高地址的部分内存空间分配给内核(内核空间给内核线程执行特权操作),剩余的作为内存空间。
内存分为5个区:(C++堆栈内存空间,程序运行时系统分配使用。)
内存从高到低: 栈 -> 堆 -> 全局/静态/常量存储区 -> 代码区

(1)堆:(堆空间一般比栈大很多)
堆(heap)是C语言和操作系统里的术语概念,操作系统维护的一块动态分配内存,比如malloc和free,就是对堆内存的动态申请和释放。(生存周期由程序控制。。主动创建。。属于动态内存分配。。一般发生在程序调入和执行时)(会造成内存碎片)向内存地址变大的方向分配生长。
(–动态内存分配:按需分配,充分利用内存空间,及时释放,
在程序运行时完成,分配释放要占用cpu资源,要用到指针和引用,)
-----操作不当,会造成内存泄漏(memory leak, 程序未能释放掉不再使用的内存。失去对该段内存的控制,造成内存浪费。)
----内存泄漏的3种情况:1.堆内存泄漏(heap leak), 申请的内存没有及时释放掉;2.系统资源泄漏(resource leak), 程序使用系统分配的资源,没有使用相应函数释放掉(比如socket, handle, bitmap),系统资源浪费,系统性能降低不稳定;3. 基类析构函数没有定义为虚函数,子类资源没有被正确释放掉。

内存泄漏问题:最好使用智能指针(C++)
智能指针:
智能指针是一个类(类都是栈上的对象,所以函数或程序结束时会自动释放),类的构造函数 传入一个普通指针,析构函数释放传入的指针。
C++11引入的,memory头文件

unique_ptr,独占式指针,不支持复制和赋值,赋值使用move。(具有排他性,指针指向的对象只能一个指针去进行操作使用)
unique_ptr具有排他性,所以出现了shared_ptr.
特点:禁止拷贝赋值;

shared_ptr,强智能指针,共享型的智能指针,维护一个引用计数(在堆上内存控制快),多个指针拷贝指向同一个堆内存,可以随意赋值,引用计数为0时才会释放堆内存。引用计数器是线程安全的,指向的对象访问会加锁。
特点:允许拷贝;拷贝一个shared_ptr,则增加一个引用计数。
通过原子操作的机制, 保证引用计数器的线程安全。

引用计数器的线程安全性如何保证? 每个都是原子操作!!
shared_ptr的所有对象通过指针共享引用计数器(实质是一个指针),属于临界资源,
比较简单的一些操作,可以设置为原子操作来保证线程安全。
原子操作: 不会被线程调度机制打断的操作从开始运行到结束。原子操作是无锁的,通过CPU指令实现的,加上LOCK。
**原子操作底层:**也是对缓存加锁、总线加锁来实现多CPU之间的原子操作,CPU指令层面。 原子操作效率更高,底层硬件支持的
缺点:消耗资源;会导致循环引用问题,互相引用,资源永远释放不掉(引用计数不会为0)(a指向b,b指向a,引用计数无法为0,内存一直没法释放)

最佳用法:定义对象时,用强智能指针shared_ptr; 引用对象时用弱智能指针(weak_ptr)

weak_ptr,弱引用指针
weak_ptr 是为了解决 shared_ptr 的循环引用问题而设计的
方法:(1)只引用,不计数(不计数的话,就不会循环引用了)。 (2)指向shared管理的智能指针,weak-ptr绑定到一个shared对象,资源观测权。 (3)不共享指针,不操作资源,不改变shared的引用计数。

接口: expired(判断指向对象是否被销毁),lock(绑定shared,返回智能指针,可以判断指针是否为空),use_count(可以得到shared引用个数)
weak_ptr不能保证引用内存有效,使用前要检查为空。
把普通指针封装成栈对象,生命周期结束后,析构函数中释放掉申请的内存。管理堆上分配内存。最常用的是shared_ptr,引用计数法,是一个类,make_shared传入指针, 当引用计数为0时才自动释放引用内存资源。
智能指针删除器:不自定义的话,会有默认删除器。 引用计数为0时,调用删除器。(资源不是常规途径来的,要自定义删除器)unique自定义删除器时,要指明删除器类型;shared指定删除器对象即可,不需要指明类型。
unique_str类里面,删除器是其中一部分(默认的删除器是无状态的)不能推断删除器类型;shared总是具有删除器的状态容量。

(2)栈:(一般就多少M,很小,操作系统维护,)(栈在C++内存最高位,从高地址往低地址写入。)往内存地址变小的方向分配生长。
存放的内容: 1.程序运行时的局部变量, (2)函数调用时,栈用来传递参数和返回值,栈先进先出,放在保存和恢复调用现场。
用来保存函数内定义的非static对象,如局部变量(或者函数参数),仅在对象定义的程序块内运行才存在。。(被动的创建。。)

静态内存分配(由系统释放,相对于堆,大部分都是静态内存分配): 可能很多在栈空间中,或者常量存储区、全局静态存储区。。比如数组。。在整个程序中固定不变,。事先已知大小,由编译器负责,(大多发生在编译和链接过程中就申请好的,全程固定不变)

(3)程序代码区:存放二进制代码。。
(4)全局静态存储区: 存放全局变量、static静态变量(系统释放)
static: 静态全局变量,在全局数据存储区上分配内存;未经初始化的静态全局变量会被初始化为0。 变量声明在整个文件都是可见的,文件之外不可见。
全局变量。(可以被外部文件使用)
(5)常量存储区。 存放常量。(结束后系统释放)

(6)C++的自由存储区。
是C++中new和delete动态分配和释放对象的抽象概念,new申请到的内存区域叫做自由存储区,C++编译器默认使用堆来实现自由存储,new和delete底层使用malloc和free来实现,说在堆上也对,在自由存储区也对。

new和malloc的区别:
1.new是关键字,需要编译器支持;malloc是库函数,需要头文件支持。
2.new申请内存时,无需指定内存大小;编译器根据类型信息自动计算;new还会调用构造函数初始化内存;malloc需要指定内存大小;malloc只管申请分配内存,不会初始化。
3.new的效率高于malloc。malloc/free没有构造、析构函数。

new分配的内存用delete释放;new[] 分配的内存用delete[]释放。 (配套的!!)
不能混用,new和delete分别调用了构造函数和析构函数, malloc和free只是分配内存和释放内存操作。
混用的话,比如用new申请内存,free的话,就少了个析构函数操作。 操作不配对!!
malloc和free配套,free传递前面malloc的 指针对象就行了,不用传内存长度。

delete和delete[]区别:
delete[]会调用每一个数组元素的析构函数,释放每一个元素空间;delete只会调用数组头元素的析构释放空间。
1.对于基本数据类型,没有析构函数,用delete和delete[]没有什么区别。
2.对于类对象的数据类型,申请数组,一定要用delete[]来释放每个对象空间。

malloc的原理:????(本质是内存管理)
malloc函数的功能:是在内存的动态存储区中分配一个长度为size的连续空间。返回值是指向所分配连续存储区域的起始地址。
将可用的内存块连接成长长的链表,沿着链表找到用户申请的大小空间,一分为二,一份给用户,一份空暇;free释放时,把用户内存块又链接到空暇内存上。
操作系统层面原理:
(1)开辟的空间小于128k时,调用brk函数,移动指针enddata(空间堆段的末尾地址)
(2)开辟的空间大于128k时,调用mmap函数,在虚拟地址空间(堆和栈空间,文件映射区域)找一块空间来开辟。(mmap可以单独释放。brk造成内存碎片。。)
这两种方法分配的是虚拟内存,操作系统分配物理内存,然后建立虚拟和物理的映射。(去使用这个内存时,才能进行物理内存的映射。。)

进程分配内存有两种方式,分别是由2个系统调用完成的:brk和mmap


段错误:访问的内存超出了系统所给这个程序的内存空间。存储器访问权限冲突。
原因:使用了野指针,指向一个已删除的对象或未申请访问受限内存区域的指针)


内存溢出(Out of Memory, OOM):程序在申请内存时,没有足够的内存供申请者使用。
栈溢出(Stack Overflow): 递归或循环嵌套层次太多造成的,解决方法:该用堆。增大栈空间,该用动态分配
堆溢出(heap …):只有一种可能,不停的new,但是没有销毁。。。

计算机负数、浮点数:
原码、反码、补码主要针对二进制而言。
对于正数没啥区别,引入反码、补码,主要是为了减法方便,(减号转换为负数,负数化为补码求加法)
负数存储:一个标志位1和补码表示(补码是反码+1)
浮点数存储:单精度类型(float)占32位和双精度类型(double)占64位,遵从ieee固定的标准存储。
单精度32: 符号位1+指数位8+尾数部分23
双精度64:符号位1+指数位11+尾数部分52

C++ 各数据类型取值范围:
char, 1个字节,占8位,
unsigned char, 无符号字符,都是正值,0 ~ 2^8-1 0 ~ 255 (全用来表示数值)
signed char, 有符号字符,分正负,-2^7~ 2^7-1 -128 ~ 127 (一个符号位)

整型,
int, 4个字节, 占32位,
unsigned int, 无符号整型,都是正值,0~2^32-1
signed int, 有符号整型,分正负,-2^31 ~ 2^31-1
short,2个字节,占16位。
unsigned short, 0~2^16-1
signed short, -215~215-1
long, 4个字节,占32位。和int一样。
float,4个字节;double,8个字节。

2. C++程序编译过程
-预处理(.c/.cpp变为.i),源程序文本变成被修改的源程序文本。(处理#include,拷贝到源程序中,#define宏替换,没用的注释删除掉。。)
#开头的都是预处理指令,#include,#define, #if #else(条件编译,,)
预处理时,头文件 和 宏定义,直接内容替换。。。
条件编译(程序规模很大时,很有用)的话,就按实际条件,来进行编译。。

-编译(.i变成.s),源程序文本变成汇编程序文本。(词法分析,语法分析,语义分析,生成汇编
-汇编器(.s变成.o),汇编程序变成可重定位的目标程序(机器语言,二进制
链接器(.o变成二进制),变成可执行的目标程序二进制文件。(把需要用到的库链接到一起,比如printf在libc.so.6里面。)

3.函数调用的原理
大体过程:参数入栈、函数跳转、保护现场、恢复现场。
(1)函数调用中的3个寄存器,
(ebp,esp, eip,3个系统寄存器,和栈相关;栈顶-低地址区,栈底-高地址区,栈填充从高地址往低地址增长。)
eip:存储下一条指令地址(每执行一条指令,该寄存器变化一次)
ebp:存储当前函数栈底的地址,栈底是基址,可以通过栈底和偏移计算来获取变量地址。
esp:始终指向栈顶
(2)栈中存储的顺序和内容:
参数拷贝(压栈),参数顺序从右向左(push指令,参数入栈) -> 保存函数调用的下一条指令,(指令跳转,函数跳转,跳转函数体内,EBP也是该函数基地址)

(3)保存现场,(保存返回地址)
EIP压栈(下一条执行指令地址), 函数返回时,从栈中弹出该值,继续执行。
会把EBX, ESI, EDI压入栈中,保护现场的一部分(分别存储基地址,源地址寄存器,目标地址寄存器)

(4) 执行函数, 局部变量。
局部变量也是存到栈上,基于EBP基地址计算来存放。

(5) 恢复现场
函数执行完毕,做后处理操作,从栈顶读取EBX, ESI, EDI,恢复现场; 内存释放, 恢复断点,栈的指针和寄存器都恢复到函数调用前的状态,

移动ebp,esp,形成新的栈帧结构,
压栈,
return值
出栈,
恢复main的栈帧结构,
返回main函数。

C++ 左值和右值

左值:
指的是:可以放在赋值符号左边的变量
特点: 1. 指向特定内存位置的表达式,2.可以获取它的地址;3.通常是有名字的;
具有相应用户可访问的存储单元,也可以改变其值。存储在计算机中的对象,也代表一个地址,通过地址可以对内存中这个左值对象进行读写操作。
右值:
指的是:赋值符号右边; 通常是一个短暂的值。
特点: 1.通常是一个临时的、短暂的、一次性的值,代表的只是一个纯粹的值,2.不是一个持久的内存位置。3.无法获取它的地址。
右值相当于数据值,可以是个常量没有地址(符号常量放在操作符右边)(临时的,通常生命周期在某个表达式内)
(所有左值也可以认为是右值,右值不能是左值)

左值引用和右值引用
1.左值引用:只能绑定到左值,(这个就是我们常见的引用的使用场景
核心是起一个别名,为已经存在的对象,避免拷贝。
2.常量左值引用:可以绑定左值,也可以绑定右值。

3.右值引用:
作用:
1.解决拷贝开销问题
。C++中对象的拷贝有时候非常昂贵(比如深拷贝),尤其在函数传参时。
2. 为移动语义std::move 提供语法基础。

std::move

1.不做任何移动操作,只是一个强类型转换器; 将一个左值无条件的转换成右值引用。
2.作用:将资源从一个对象转移到另一个对象。而不是拷贝复制。

语言语法相关

  1. C++和C,
    const:在c和c++中差距很大。
    c语言的const:修饰变量之后还是变量,只读。

c++的const
const修饰过后,变量就变成常量了。当常量用,固定不可变。(const常量会被加入到符号表,会查表,所以不可变。)值在编译时就已经定了
尽量使用const,可以帮助我们避免很多错误,提高程序正确性。

const和宏定义区别:
宏是在预处理阶段替换的,const是在编译阶段固定的。
(1)const变量: const变量必须初始化, 变量值是只读的,不能修改。
(2)const类对象:此类对象不应该被改变。 const修饰类对象,类对象任何成员值都不能被改变。
(3)指向const变量的指针: 指针指向内容不能改变,但是指针指向可以改变。
(4)const指针:一个指针经过const修饰,指针指向不能改变,指针指向的内容可以改变。
(5)const变量作为函数参数: 限制函数体内,不能改变这个参数值。传参时,不要求必须是const,但是在函数体内会转换为const。
(6)const返回值: 函数返回const引用。 返回值不能在外面被修改。
(7)类中成员变量为constconst成员变量必须在构造函数中被初始化。类中成员变量为只读,不能被修改(包括在类内部和外部)
(8)类中const成员函数: 此函数不应该修改任何成员变量。传给const成员函数的this指针,是指向const对象的const指针。

符号表:语义分析。。hash表。。
————
宏和内联(inline)
宏是C语言的一种预处理功能
内联(inline):是C++引入的一个关键字,C++中推荐使用内联函数inline来代替宏
内联发生在编译阶段(会有参数检查、返回值检查),会更安全。宏只是在预处理,
内联函数,一般定义在头文件中。(只在当前文件中生效,可以被其它文件include去调用)
调用普通函数(比求解等价表达式)效率会低很多,
内联函数:为了提高函数执行效率,
内联函数原理
1.编译时,编译器将内联函数在调用点上源代码展开,inline放在函数定义前,内联函数将它在程序中的每个调用点上“内联的”展开,
2. 代码直接和主代码合并到一起了,不需要函数调用的过程了(参数压栈、跳转、返回值,寄存器处理等); 减少了函数调用的流程,提高了代码执行效率。

普通函数调用开销很大,参数压栈、跳转、返回值,寄存器处理等过程),频繁调用函数效率很低。
内联函数缺点: 调用点过多的话,代码冗余
内联函数使用场景:
1.对于小而精、并且需要频繁调用的函数,尽量使用inline内联处理,可以提高效率。
2.注意点:函数少于10行时使用内联;递归函数、虚函数不能内联。。

—————
有了malloc和free,为啥还要new/delete??
malloc和free:是C语言的标准库函数,需要相应库支持,某些平台可能不支持;
new/delete是C++语言自带的运算符,不是库函数,对于对象来说需要,malloc和free不能满足需求,new可以初始化。

——————
C++提供了4种类型强制转换操作
意图明确,满足各种数据转换的需求。

1.const_cast: 常量转换,强制去掉不能被修改的常量特性,一般用于常量指针或者引用。

2.static_cast(类似C的强制隐式转换,静态类型转换
最普通的,最常用的转换。编译器已知的、相对安全的转换。
(1)继承之间的父类和子类之间的互相转换,都不会报错;派生类转基类是安全的,基类转派生类,由于没有动态类型检查,是不安全的。
(2)基本数据类型之间的转换,需要开发人员自己保证安全性。int、char等等。

3.dynamic_cast(动态类型转换,有条件转换,会检查类型安全,类指针间的转换,需要虚函数支持)(最安全的转换!!!)
主要用于继承中基类和派生类之间的互相转换,基类转派生类时不安全会报错。

4.reinterpret_cast(重新解释的意思):指针或引用 和 整型之间的互相转换,转换时,字节逐个拷贝,提供了最大的灵活性,但是安全性很差。
————————-
static:(作用于静态变量,静态函数)
1.面向过程中使用,加上static,定义全局静态变量、局部静态变量。
**静态全局变量:**在全局数据存储区,分配内存;未初始化的会默认初始化为0;静态全局变量在整个文件里都是可见的,文件之外不可见。
函数退出时,全局数据区的数据并不会释放空间。
静态全局变量和普通的全局变量的区别:都能实现文件内共享,静态全局变量不能被其他文件所使用。
普通全局变量,会被其他修改使用,不受控制。

静态局部变量:
普通局部变量:每次运行到变量时,都会在栈上分配内存;函数体退出时回收栈内存,局部变量就失效。

静态局部变量:保存在**全局数据存储区,**局部变量值也可以一直保存,下次函数调用时还能读到,继续赋新值。
第一次调用时被初始化,后面就不再初始化。始终在全局数据存储区,直到程序结束。作用域为局部作用域,

静态函数:
在函数的返回类型加上static关键字,函数就被定义为静态函数。只能在声明的这个文件中可见,其它文件不可见。优点:1.其它文件不可用;2.其它文件也可以定义同名函数,不冲突。

2.面向对象中的static:类中的static
(1) 类中的静态数据成员:比如,static int sum; 静态数据成员属于类的成员,只有一份拷贝 只分配一次内存,类的所有对象共享该静态数据成员。 (也保存在全局数据存储区)
在没有产生类的实例时,就可以操作它。
静态数据成员定义时要分配空间,所以不能在类声明中定义。
使用场景:(1)类的所有对象都有相同某个属性,可以使用;在全局区共享,节省存储空间;(2)需要改变时,改变一次就可以了。

(2)类中的静态成员函数:与类的静态数据成员一样,都属于类的一部分。
静态成员函数没有this指针,不与任何对象相联系,通过类名可以直接调用。
静态成员之间,可以互相访问,静态变量和静态成员函数。
非静态成员函数,可以访问静态的;
静态成员函数,不能访问非静态的。
优点:方便,不需要创建对象就可以使用。 没有this指针开销,静态成员函数速度会快点。

static作用:
1.隐藏作用,文件内共享,对其他文件隐藏;普通的全局变量,通过extern,就可以被外部文件随意调用。
2.名字冲突问题:静态数据成员,不会进入程序全局名字空间,不存在与程序其它全局名字冲突的可能性。

1.隐藏的作用, 未加static的全局变量和函数是全局可见的(可以通过extern), 加入static,就会对其它源文件隐藏,只在当前文件用 其它文件用不了。
2.保持变量内容持久:静态存储区的内容,在程序运行时就完成初始化,唯一一次初始化,全局变量和static变量在静态存储区,生存周期为整个源程序,但是作用域和普通变量相同。存在但是不能用。
static作用于类,类的静态数据成员,类的静态成员函数。
类的静态成员变量:类内声明static,类外单独分配存储空间,不依赖类的某个对象,所有对象共享静态成员变量,通过类名直接调用静态成员变量;
静态成员函数:类共享,可以访问静态成员变量,不能直接访问普通成员变量,类名直接调用静态成员函数

——————
extern:变量或函数声明前,说明在别处定义的,在此处引用。
作用:1.引用同一工程下的其它文件里的变量;2.引用后面作用域的变量,通过extern扩展作用域。
弊端:被引用后,可以修改该变量,会影响其他地方使用,所以不太安全,慎用(所以就有了static)
引用其它文件的变量和函数有2种方法:1.#include(引用整个文件里的接口调用),2.extern(只引用某个变量)

extern C
作用:调用C库时使用,因为C语言和C++的编译差异。
extern C 放到函数或变量前面,告诉编译器:(1)这个函数或变量是extern类型的;可以在本模块或其它模块使用;
(2) extern C: 表示该函数编译生成的符号表是C语言风格,_foo(不包含参数类型和返回值类型)。
C++编译器:编译链接的函数符号表,_foo_int_float。
C++编译的时候把函数和参数联合生成中间函数名称,C语言不会(会造成链接找不到对应函数),
C++调C库,告诉编译器不要修改C库函数名。 会导致链接差异,找不到符号。

————-
头文件,
C++条件编译, (作用:1.防止头文件过多重复包含,2.调试使用,3.不同角色或场景判断使用。)
#ifndef (if not define…的缩写,如果没有定义),一种宏定义判断,防止头文件被过多重复包含。
#define
#else
#endif,

和program once
都是为了防止头文件被重复包含。作用于包含起来的某一段代码。
#ifndef和#program once的作用都是一样的,都为了防止头文件被重复包含。。细微区别如下:
(1)ifndef是编程语言支持的,可以针对部分代码片段,编译速度特别慢,效率低,要不停的检查头文件引用情况。(更灵活,兼容性好,移植性好)
(2)program once是编译器支持的,目前不同版本编译器支持的不够好。 作用于包含该语句的整个文件(无法对某一段代码做program once声明),操作简单,效率高。

—————-
指针,引用

  1. 指针:是一个变量,这个变量存储的是一个地址,可以通过访问这个地址进行内容的查询和修改;
    引用:只是一个别名,还是被引用的变量本身,对引用的任何操作,都是对变量本身的操作,可以达到修改变量的目的。 (本质上是一个指针常量!!const指针 不会单独申请空间,而是和被引用变量指向同一个地址。)
    共同点:都是地址概念,都是指向一块内存,
  2. 指针:可以不初始化,引用必须初始化。定义引用时,必须把该引用和初始值绑定在一起。(引用不能为空)
    引用的内部实现是借助指针来实现的,一些场合下引用可以替代指针,比如函数形参。

3.引用只有一级,指针可以多级指针。
4.传参:指针传参时,指针本身的值不可以修改,通过指针指向才能对对象进行操作。
引用传参时,传的就是变量本身,通过引用,可以随意修改这个变量。

引用:就是基于指针,加一些限制比如必须初始化、不能改变引用指向。
引用是别名,在汇编层面 和指针并没有区别,引用会被C++编译器当作const指针。
引用占用的内存空间 和 指针一样,因此,引用内部是通过指针完成。


struct和union:
struct:结构体,将不同类型数据组合起来,成为一个整体,属于一种自定义数据类型。
struct sizeof计算,数据对齐问题。struct的内存布局:依赖CPU,操作系统,编译器编译时的选项。
struct中,各成员都占用自己的内存空间,都同时存在,struct变量总内存长度 = 各成员内存长度(对齐后的)之和。
对齐规则:
1.第一个成员在结构体变量偏移量为0的位置,(起始位置为0)
2.其他成员要对齐到某个数字(对齐数)整数倍的住址处。
对齐数:min(编译器默认对齐数 ,成员大小),比如VS默认对齐数为8。 每个成员计算出自己的对齐数。
3.结构体总内存大小为最大对齐数(每个成员都有一个对齐数)的整数倍。 (找到最大对齐数,作为整个结构体的对齐数) 要做最后的检查!! 不是整数倍的话,要补齐。
(改变成员前后顺序,因为每个成员对齐数不同,摆放住处不同,可能导致整个结构体大小不一样。)

大端(Big-Endian)和小端(Little-Endian)
只是数据在地址中的不同存储顺序。
char长度为1,没有高低字节之分。大于1的字节的类型,才有高低之分。
小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;
大端模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
大端:左边都是有数据的,右边可能为空;小端:右边都是有数据的,左边可能为空。
读取时,大端依次读出,小端倒叙读出。
大端模式:更符合人理解的模式,符号位在内存的第一个字节,可以快速判断数据正负和大小;
小端模式:更符合机器性能模式,低地址存放低字节,高地址存放高字节,强制转换时不需要调整字节内容。
union(联合体):采用小端模式,从低地址开始存放。
网络中,TCP/IP,规定必须采用大端模式,网络字节顺序(Network Byte Order,NBO)。
CPU中,都是采用小端模式。
芯片中都是采用大端模式,符合阅读习惯(这个不确定??)

为什么要内存对齐?
1.硬件平台要求,比如int,有些只找4倍数位置的数字。不对齐的话,可能找不到。
2.经过对齐后,CPU访问数据的速度大大提升。按照对齐数一个一个去读的,不对齐的话,要访问2次才能拿到一个数据。 空间换时间,提高访问速度。
缺点:空间资源浪费。

union: 共用体,又叫联合体,将不同类型的数据变量存储在同一段内存单元。union所有数据成员从同一个地址开始存储,union的内存大小 = union中占用内存单元最多的数据成员大小。
优点:使用频率没有struct高,节省存储空间。每次赋值一个成员变量,会被重写覆盖。

优化struct结构体的空间资源浪费:
__packed:进行一字节对齐。完全紧凑起来。
#pragma pack(4): 修改系统对齐字节数,
会降低系统运行性能,CPU访问数据的速度变慢。

class内存空间
1.函数放在代码区,数据主要放在堆和栈上,全局静态区或者常量区;
2.默认空类为1,
3.也要内存对齐,和结构体一样。

——————-
#面向对象
封装:
数据和函数集合在一个个单元中,称为类。保护或防止代码数据被破坏。
哪种属性,类内都可以访问。
public:暴露手段,暴露接口 类的对象都可以访问
private:隐藏手段,类的对象不能访问。
protected:和public一样可以被子类继承,和private一样不能在类外被直接调用。

继承:
主要实现重用代码,节省开发时间。子类继承父类的一些东西。
公有继承:派生类成员状态和基类完全一致
私有继承:派生类成员状态都是私有(私有不能继承)
保护继承:派生类成员状态都是保护(私有不能继承)

多态
多态是设计模式基础,是框架基础。多种状态。接口的多种不同实现方式即为多态。
多态三要素:同名函数、依据上下文、实现却不同。 运行时动态调用实际绑定对象函数的行为。

对象的静态类型:编译时确定的,程序中声明采用的类型(字面类型),静态绑定,前期绑定,(非虚函数是静态绑定,缺省函数是静态绑定)
对象的动态类型:运行时确定的,动态绑定,实际类型,后期绑定,(virtual函数是动态绑定的,编译时生成虚函数表,运行时才知道调用哪一个函数。。)
两个概念一般发生在基类和派生类之间。
引用可以实现动态绑定。 引用使用时必须初始化,引用既可以指向基类对象,也可以指向派生类对象,这是实现动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。
———-
构造函数和析构函数
(顺序:最先创建构造函数,最后析构。跟栈一样。)
默认构造函数:没有任何构造函数时,编译器自动生成默认构造函数,即无参构造函数。
当类没有拷贝构造函数时,会生成默认拷贝构造函数,(默认是浅拷贝)
???
构造函数:处理对对象的初始化。是一个特殊的成员函数,不需要用户调用,创建对象时自动调用。
拷贝构造函数:对未初始化的对象存储区进行初始化;每创建一个对象时,都会开辟一个新的地址空间;相同类型的类对象通过拷贝构造函数完成复制过程。
默认拷贝构造函数(浅拷贝):只是简单的复制成员变量值。静态成员处理不到位(static引用计数次数不对)。还有动态申请内存的成员,两个指针指向同一个内存地址空间(析构时销毁2次)(浅拷贝。)
没有静态成员和动态申请内存的成员,可以用浅拷贝。
深拷贝:对象成员都重新申请内存空间,指向不同的地址,只是内容相同。动态成员会重新分配内存空间。(指向的内容是相同的。尽量少用默认拷贝。。)

析构函数:特殊的成员函数,编译器自动调用,对象不再使用时,清理资源的时候调用。

构造函数和析构函数,可以虚函数吗? 顺序? 问题!!
对于同一个类的不同对象而言,先构造后析构,后构造的先析构。


封装:可以隐藏实现细节,保护代码数据,使得代码模块化;
继承:可以拓展已存在的代码模块(类);
封装和继承的目的,都是为了代码重用。

多态的真正作用:派生类的功能可以被基类的方法或引用变量所调用,向后兼容,提高可扩展性和可维护性。 (多态是设计模式的基础。。)
多态(poly - morphism)
1.什么是多态?
从字面理解,不同对象,对同一行为(具有相同的函数),有不同的状态(函数内部实现不同)。

和重写(覆盖)相似的概念:overload、override、overwrite
1.重载(函数):overload,函数名称相同、参数不同,不考虑返回值,**在同一个作用域内,**减少函数名数量。 意义:提高代码可读性、代码复用、减少函数名冲突。 (不同作用域内不叫重载)
2.覆盖重写(override):派生类函数覆盖重写基类函数,基类必须是virtual类型。(这就是多态) 用来修饰派生类函数,表明我这个是要重写基类函数。
3.重定义(又叫隐藏)overwrite: 前提普通函数,不是虚函数。。继承关系中,子类实现了和父类名称完全一样的函数,子类就把父类函数隐藏了。

2.多态如何实现?
(1)虚函数重写(也叫做覆盖),重写就是设置不同状态。(基类和派生类之间,函数名、参数和返回值都完全相同,都是虚函数)
(C++提供override 和 final来帮助用户检测是否重写,防止疏忽写错,override在编译时会在子类函数检查是否重写正确(使用override是好习惯)。
final: 不希望某个类被继承或不希望某个虚函数被重写,使用final, 后面被继承或被重写时会报错。。)
(2)对象调用虚函数时,必须是指针或者引用。
代码上体现多态:当父类指针指向子类对象时,通过父类指针能调用到子类的成员函数。


虚函数是C++实现 动态(dynamic)、单分派(single-dispatch)、子类型多态(subtype poly morphism)的方式。
动态: 运行时决定的。(静态:编译时决定)
单分派:(基于一个类型,去选择调用哪个函数。)(多分派,是基于多个类型去选择调用)

move:移动语义
将对象的状态/所有权,从一个对象转移到另一个对象(只是转移,没有内存的搬迁或者拷贝)
RValue b = std::move(a); 移动构造后,b指向a的资源,a不再拥有资源,a被清空。
避免不必要的内存拷贝操作,提高效率,左值引用转换为右值引用,减少临时内存拷贝。
move原理:本质上是将传入的参数强制转换为右值引用类型,然后返回该引用。move本身不会移动数据,只是告诉编译器该对象可以进行移动操作。(改变了引用,本来A引用那个地址,换成了B引用)
(右值引用可以绑定到右值,右值引用可以修改)
move应用场景:(1)内存很大的变量A,换成变量B。(2)要将一个对象传参,然后该对象传完就不需要了。

volatile:(翻译过来,就是不稳定的意思)
和const正好相反。
告诉编译器,这个变量具有易变性,不可信,每次使用变量时都要重新从内存中读取一次。
不可优化性,不需要编译器对该变量优化;
顺序性,保证变量之间的顺序性。
volatile经常用于多线程场景。

程序调试和问题分析相关

google-perftools
针对C/C++程序的性能分析工具,可以对CPU时间片、内存等系统资源的分配和使用进行分析。
使用方法:(1)引用so的动态链接库,编译进去。
(2)在需要调试的代码前后,引用库函数的剖析模块的启动和终止函数,就能实现对这段代码的性能分析。
(3)落下了一个分析文件。(运行时间、内存大小)

gdb调试core
程序崩溃。对堆栈信息分析,了解程序运行状态,分析判断程序崩溃的原因。
(1)堆栈完整且稳定(指稳定出错)
(1.1)可以快速看出错误原因,堆栈行本身代码有错误,
(1.2)上下文代码有错误,缩小范围后前后多看几行就知道了;
(1.3)常规函数调用方式不对,传递野指针啥的。

(2)堆栈完整且不稳定(不稳定是指:程序时好时坏)
排查较为困难。往往和多线程有关,
先将并发数改为1,看看能否解决问题。
观察core的堆栈行的代码,是否存在多线程冲突。
加锁、加日志打印。
单线程后,还存在随机core,问题就转换为野指针类似的定位,

(3)堆栈被破坏。

gdb原理:
基于ptrace这个系统调用。

被调试程序和gdb之间建立跟踪关系,截获所有信号(hook),根据截获信号,查看被调试程序的内存地址,并控制被调试程序继续运行。

gdb常见的使用方法:
(1)断点设置

(2)单步调试

建立调试关系,2种模式:
(1)fork:利用fork+execve执行调试程序, 子进程在执行execve之前调用ptrace,建立与父进程之间的跟踪关系;
(2)attach:调试器调用ptrace,建立自己与进程之间的跟踪关系。

断点原理:
(1)指定位置插入断点指令,调试程序运行到断点位置时,产生SIGTRAP信号。信号被gdb捕获判断是否断点命中,等待用户输入进行下一步处理。(类似软中断)

C++程序性能分析工具

CPU
cpu问题的典型症状:cpu使用率过高、cpu响应缓慢、单线程瓶颈等。
1.perf(linux)
linux官方提供的性能分析工具,首选和必会的工具。
原理: 利用CPU内部的硬件性能监控单元(PMU)和内核注入的软件探针,以极低的开销采集系统运行时的各种事件,通过一系列工具和接口呈现给开发者,定位性能瓶颈。

收集数据:
(1) 计数模式(Counting). perf stat
PMU在特定上下文中,累加特定事件发生的次数。程序结束后,从寄存器中读取最终数值。
特点:开销极低,几乎不影响程序性能,常用于获取宏观的性能指标。
(2) 采样模式(Sampling)
定期中断CPU,收集当时正在执行的指令、堆栈等信息,分析大量样本来推断程序热点。
perf record: 指定采样事件。
perf report:分析采样文件,生成可视化报告,显示哪些函数或代码路径消耗了最多的资源。
特点:开销比计数模式高,能提供代码行级别的热点定位。

(3)火焰图:与perf结合,生成直观的火焰图,可视化的显示函数调用栈和CPU时间消耗。

实时监控某个进程的CPU使用率和热点函数
perf top -p

内存
内存使用量不断增长(内存泄漏),非法访问(段错误),频繁换页导致性能下降。
1.valgrind --tool=memcheck
C++开发者必备工具,内存调试常用。
功能: 程序结束时,可以告知哪些内存没有释放。检测非法内存操作:未初始化的值、访问已释放的内存、数组越界、重复释放。
优点:精确定位到代码行。
缺点:只能在开发、测试阶段使用。

2.pmap 、 proc //smaps
linux自带,查看进程的内存映射。
查看进程地址空间的详细布局,包括堆、栈、共享库、匿名映射等各自占用了多少内存。

网络分析
网络:使用 tcpdump 抓包,用 Wireshark 分析;使用 strace 查看应用层网络调用。

分析定位流程:
1. 宏观定位:先快速定位是CPU、内存、还是网络问题。然后看是哪个进程。
top, htop, vmstat, netstat
2.微观分析:
依次使用cpu、内存、网络中分别常用的深入分析的工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值