C++复习
Part I 编译内存相关
编译过程
编译分为四个过程:预处理(#)、编译、汇编、链接。
静态链接:在链接阶段将库的内容导入到可执行程序中。
动态链接:在程序运行时,由操作系统的装载程序加载库。
内存管理
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放
堆:存放动态申请的空间,由程序员控制分配和释放,如果程序执行结束还没释放,操作系统会自动回收。
**全局区/静态存储区 (.bss .data)**:存放常量,不允许修改,程序运行结束自动释放。
**代码区 (.text)**:存放代码,不允许修改,但可以执行。
栈和堆的区别
- 申请方式:操作系统、程序员
- 内存空间:连续、不连续
- 申请效率:高,低
- 存放内容:局部变量、由程序员控制
变量的区别
- 全局变量:全局作用域。只需要在一个源文件中定义,就可以作用于所有源文件,但是,在其他源文件中使用时,需要使用
extern
关键字声明 - 静态全局变量:文件作用域。即被
static
修饰的变量,只在该源文件内起作用。 - 局部变量:局部作用域。像循环内部声明的变量,代码块执行完成后撤销,内存被收回。
- 静态局部变量:局部作用域。只被初始化一次,从初始化到程序运行结束一直存在。只被定义自己的函数体可见。
字和字节
字节: 8个二进制位。
字:由机器的寻址长度决定,16位机器1个字就是2个字节。
内存对齐
有效对齐值:字的大小(操作系统数据处理的运算单位)
是什么:编译器把程序中的“数据单元”安排在字的整数倍的地址指向的内存之中。
为什么:
- 平台原因(便于移植)
- 性能原因
原则:
- 基本类型的对齐值就是其sizeof的值。
- 结构体第一个成员变量的偏移量为0, 以后每个成员相对于结构体首地址的offset都是该成员大小与有效对齐值中较小那个的整数倍。
- 结构体的总大小为有效对齐值的整数倍,如果不够,则在最末一个成员之后填充字节。
内存泄露
由于疏忽或者错误导致的程序未能释放已经不再使用的内存。
- 常指堆内存泄露
- 指针重新赋值,导致空间无法找到。
防止内存泄露
- 内部封装:将内存的分配和释放封装到类中,构造的时候申请内存,析构的时候释放内存。
- 智能指针:我们虽然可以在每次 new 完一个对象后,写出对应的 delete ,但我们不能保证在调用 delete 之前,程序不会发生错误或者提前返回。————智能指针是一个类,当超出类的作用域的时候,类会自动调用析构函数,析构函数会自动释放资源。
浅拷贝与深拷贝
浅拷贝:位拷贝,把对象里的值完全复制给另一个对象,如A=B。如果B中有一个成员变量指针已经申请了内存,那A中的成员变量也指向同一块内存。这就会出问题:假如B把内存释放了,这时A内的指针就是野指针了,出现运行错误。
深拷贝:资源重新分配。
类初始化
类变量初始化:
- 引用
- 常量
- 静态变量
- 静态整型常量
- 静态非整型常量
对于引用和常量:必须通过构造函数的初始化列表初始化。
对于 静态变量:类内定义,类外初始化,因为 static 独立该类的任意对象存在,它是类关联的对象,不与类对象关联。
对与 静态整型常量 和 静态非整型常量:可类内定义初始化,也可类内定义,类外初始化。
类的编译顺序:
- 类名
- 成员名称
- 成员函数的返回值和形参
- 成员函数的函数体
编译时函数名
类或命名空间中的变量或函数:
各个空间和类的名字,每个名字前是名的字符长度,然后是变量/函数名的长度和变量/函数名,后面紧跟”E”,然后如果是函数则跟参数别名,如果是变量则什么都不用加。
如:
mangling::C1::C2::func(int)
_ZN8mangling2C12C24funcEi
Part II 语言对比
C++ 11 新特性
auto
类型推导。编译器会在 编译期间 通过初始值推到出变量的类型。decltype
类型推导。是“declare type”的缩写,译为“声明类型”。decltype
作用是选择并返回操作数的数据类型。1
2auto var = var1 + var2;
decltype(var1 + var2) var = 0;lambda
表达式范围
for
右值引用
格式:类型 && 引用名 = 右值表达式;
作用:
充分利用右值的构造来减少对象构造和析构操作以达到提高效率的目的。
绑定到右值的引用,用
&&
来获得右值引用,右值引用智能绑定到要销毁的对象
左值:可以取地址的,有名字的,非临时的
右值:不能取地址,没有名字,临时的。立即数 -> 右值。左值和右值引用本质区别:
创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值;而由用户创建的,通过作用域规则可知其生存期的,就是左值。
移动语义:
为了解决进行大数据复制的时候,进行大量的数据拷贝问题
转移资源所有权,类似于转让或者资源窃取,对于那个资源,转为自己所有,别人不再拥有也不会再使用。移动构造函数:
std::move的本质,就是一个转换函数,将给定的类型转化为右值引用,而并不是真正地“移动”了资源
完美转发:
可以写一个接受任意实参的函数模板,并转发到其他函数,目标函数会受到与转发函数完全相同的实参
delete
:= delete
表示该函数不能被调用。default
:= default
表示编译器生成默认的函数。列表初始化
C++11中可以直接在变量名后加上初始化列表进行对象的初始化。智能指针
引入了智能指针的概念,方便管理堆内存,使得自动、异常安全的对象生存期管理可行。解决内存泄露问题,构造函数不能被隐式调用。
shared_ptrweak_ptr
unique_ptr
constexpr:编译时的常量和常量函数
与const区别:
两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。1
2
3
4
5
6
7
8
9
10#include<iostream>
using namespace std;
constexpr int func(int i) {
return i + 1;
}
int main() {
int i = 2;
func(i);// 普通函数
func(2);// 编译期间就会被计算出来
}
C 和 C++ 的区别
- 面向过程;面向对象。
- 嵌入式、驱动开发等与硬件直接打交道的领域;可以用于应用层开发等与操作系统打交道的领域。
- C++ 增强方面:类型检查更为严格,增加了面向对象机制,泛型编程的机制,异常处理,运算符重载,标准模板库,命名空间。
Java 和 C++ 的区别
- java 是完全面向对象语言。
- C++能够操作指针。
- C++能多重继承。
- java 支持垃圾回收,以线程的方式在后台运行,利用空闲时间。
- 场景:
java主要用来开发Web应用。
C++主要用在嵌入式开发、网络、并发编程的方面。
Python 和 C++ 的区别
- Python 是脚本语言,不经过编译。
- 面向更上层的开发者。
Part III 面向对象
定义
对象是指具体的某个事物,事物即类,类中包括数据和动作。
三大特性
- 封装:将具体的实现过程和数据封装成一个函数,智能通过接口进行访问,降低耦合性。
- 继承:子类继承父类的特征和行为,子类有父类的非
private
方法或成员变量,子类可以对父类的方法进行重写,增强类之间的耦合性,不能继承final
关键字修饰的变量和函数。 - 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使基类指针呈现出不同的表现方式。 shape、rectangle、triangle。->area();
重载、重写、隐藏的区别
- 重载:对同一可访问区内被声明的几个具有不同参数的同名函数,根据参数列表确定调用哪个函数,重载不关心返回类型,即返回类型必须相同。
- 隐藏:派生类的函数屏蔽与其同名的基类函数。
- 重写:派生类中存在重新定义的函数,且只有函数体不一样,且基类函数要被
virtual
修饰。
多态的实现
- 在类中使用
virtual
关键字声明的函数叫做虚函数。 - 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针。
- 当基类指针指向派生类对象,基类调用虚函数,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。
Part IV 关键字库函数
sizeof 和 strlen 区别
strlen :测量的是字符串的实际长度,以\0结束。
sizeof :测量的是空间分配大小,单位为字节。
lambda 表达式的具体应用和使用场景
1 |
|
- 捕获列表: & 为引用捕获, = 为值捕获。
- 通常和排序结合,或者使用
auto
关键字在函数中定义。
explicit 作用
用来声明类构造函数是显示调用的,可以阻止调用构造函数时进行隐式转换。
隐式转换:先创建一个值为 10 的 A 对象,在赋值给 ex1 。
1 |
|
static 作用 (need to learn further)
定义静态变量和函数。
作用:
1. 保持变量内容持久,
2. 隐藏: static 修饰全局变量,则可对其他源文件不可见。
3. 可以不实例化对象通过类访问静态成员。
4. 类的静态成员函数中只能访问静态成员变量或静态成员函数,不能将静态成员函数定义成虚函数。
在类中使用注意事项
1. 静态成员变量在类内进行声明,在类外进行定义和初始化。
2. 静态成员变量可以相当于类域中的全局变量,被类的所有对象共享,包括派生类的对象。
3. 静态成员变量可以作为成员函数的参数,普通变量不行。
4. 静态成员变量可以是所属类的类型,而普通的只能是该类类型的指针或引用。
5. 静态成员函数不能调用非静态成员变量或非静态成员函数,因为静态成员函数没有this
指针。
1 |
|
const 作用及用法
作用:
- const 修饰成员变量,可进行类型检查,节省内存空间,提高效率。
- 修饰函数参数,使函数参数的值不可改变。
- 修饰成员函数,使成员函数不能修改成员变量,也不能调用非 const 成员函数。
用法
- const 成员变量只能在类内声明,在构造函数初始化列表中初始化
define 和 const 区别
- define 在编译预处理阶段进行替换,const在编译阶段确定其值。
- 安全性:define 定义的宏常量没有数据类型,只进行简单的替换,不会进行类型安全检查; const定义的常量是有类型的,需要进行判断。
- 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间; const定义的常量只有一份,在静态存储区的空间。
- 调试: define 定义的不能调试,因为在预编译阶段已经进行替换;
define 和 typedef 的区别
- 原理
#define 预编译阶段、简单替换、不做正确性检查。
typedef 是关键字,在编译时处理,有类型检查,用来给一个已经存在的类型一个别名 - 功能:
#define 可以为类型取别名、定义常量、变量、编译开关。
- 作用域;
#define 没有作用域; typedef 有。
inline 作用
内联函数的作用:
消除函数调用的开销。
在内联函数出现之前,程序员通常用 #define 定义一些“函数”来消除调用这些函数的开销。内联函数设计的目的之一,就是取代 #define 的这项功能去除函数只能定义一次的限制。
内联函数可以在头文件中被定义,并被多个 .cpp 文件 include,而不会有重定义错误。这也是设计内联函数的主要目的之一。
工作原理
- 不发生状态转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中。
- 普通函数是将程序执行到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场和恢复现场,需要较大的资源开销。
delete 实现原理
- 首先调用该对象所属类的析构函数
- 进而调用 operator delete 的标准库函数来释放所占的内存空间。
delete[] 用来释放数组空间。
new 和 malloc、 delete 和 free 区别。
- malloc 和 free 是库函数; new 和 delete 是关键字。
- new 无需指定空间大小, malloc 需要确定申请空间大小。
- new 返回的是对象的指针,malloc 返回的是
void *
,需要强制类型转换。 - new 在自由存储区上分配内存, malloc 在堆上分配。
C 和 C++ struct 区别
C++中的 struct 和类一样。
volatile 关键字
volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
使用场景:
- 多线程都会用到某已变量,需要修饰。
- 中断服务程序、并行设备
memcpy 和 memmove
memcpy 不保证拷贝的正确性。
memmove 保证拷贝的正确性。
内存重叠,判断从左往右还是从右往左拷贝。
strcpy 有什么缺陷
不检查目标缓冲区的大小边界,可能导致溢出。
auto 变量
“做函数模板需要做的事情”
基本类型和值是一样的,但是第二属性(const volatile) 不一定相同。
Part V 类相关
虚函数、纯虚函数
虚函数:
被 virtual 关键字修饰的成员函数
纯虚函数:
在类中声明,加上
= 0;
只要含有纯虚函数的类称为抽象类,类中只有接口,没有具体的实现方法。
继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化。
1 |
|
区别
- 虚函数和纯虚函数可以同时出现在一个类中。
- 使用方式不同:虚函数可以直接使用,纯虚函数需要在派生类实现后才能使用。
- 定义形式不同:virtual 和 = 0;
- 虚函数必须实现
- 如果一个类需要被继承,为了避免内存泄露,析构函数应设置为虚函数;反之,不要设置成虚函数。
虚函数的实现机制
实现机制
虚函数通过虚函数表实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(虚表指针),通过虚表指针可以找到类对应的虚函数表,虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题。当用基类的指针来操作一个派生类的时候,虚函数表就指明了实际应该调用的函数。
虚函数表
内容:类的虚函数的地址。
建立时间:编译阶段。
虚表指针位置:对象的内存空间中最前面的位置。
- 只要有虚函数(无论是继承来的还是本类自己定义的)就一定有虚表指针和虚函数表
- 同一个类的不同对象,虚表指针和虚函数表一样
- 不同类的对象,虚表指针不一样;至于虚函数表中的函数指针是否一样,主要看是否重写了虚函数
- 多继承,需要有多个虚表指针。
- 两个虚函数指针分别指两个虚函数表。每个虚函数表保存每个父类的虚函数地址。
- 内存布局与继承的父类的顺序有关,子类的虚函数插入到第一个虚指针所指的虚函数表中。
- 特别关注子类的虚析构函数。第二个虚指针调用虚析构函数时,会跳转到第一个虚函数表调用子类虚析构函数。
- 子类的虚函数表中虚函数的顺序与父类一样,若子类重写父类虚函数,即在虚函数表中原位置覆盖即可。
构造函数一般不定义为虚函数原因
- 存储空间考虑:构造函数是在实例化对象的时候调用,如果构造函数是虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(虽然编译的时候就有虚函数表了,但是没有虚函数的指针,续表指针只有创建了对象才有)
- 使用角度:虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数调用。
如何避免拷贝
使用delete关键字。
如何减少构造函数开销
使用类初始化列表。
因为C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。如果在构造函数中初始化,则会先调用默认的构造函数为成员变量设初值。
如何解决多重继承命名冲突问题
使用虚继承,保证存在命名冲突的成员变量或函数在派生类中只保留一份。
空类占多少字节,编译器会生成哪些函数?
1字节
编译器会生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符
为什么拷贝构造函数必须为引用?
因为形参需要调用拷贝构造函数,构造函数无限制递归,导致栈溢出
类对象的初始化顺序
构造函数顺序:
- 按照派生类继承基类的顺序,即派生列表声明的顺序,依次调用基类的构造函数
- 按照派生类中成员变量的声明顺序,依次调用派生类中成员变量所属类的构造函数
- 执行派生类自身的构造函数。
析构顺序和构造顺序相反。
1 |
|
实例化一个对象的过程
- 分配空间
- (可选,如有虚函数,先给虚表指针赋值)
- 初始化
- 赋值
ps: 初始化在赋值之前,可在初始化列表那里了解。
友元函数作用及使用场景。
作用:通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。
使用 friend 关键字。
静态绑定和动态绑定
静态绑定是指程序在编译阶段确定类型
动态绑定是指程序在运行阶段确定类型。
动态绑定只发生在基类指针指向派生类对象。
Part VI 语言特性相关
重载运算符
- ()为函数调用运算符
指针常量 和 常量指针。
int 和 const 位置没有关系
由 const 和 * 的位置决定
const 在 * 左边:表示常量指针(不能修改指向的内容)
const 在 * 右边:表示指针常量(不能修改指针)
函数指针 和 指针函数
指针函数:正常的函数,返回的是指针。
函数指针:指向函数的指针
指针 和 引用
- 指针可以多级,引用只有一级
- 指针可以为空,引用不可为空
- 指针占空间,引用不占空间
- 指针指向的内存空间可以变,引用一旦绑定不可更改。
C++11 nullptr 和 NULL
- nullptr 有类型,可以转换成任意指针类型; NULL 是预处理变量,通过宏定义,值为 0
- 函数重载 nullptr 能够绑定到参数上, NULL 不能
野指针 和 悬空指针。
类型转换
static_cast
const_cast<int *>():用于去掉 const/volatile。
reinterpret_cast<int *>():static_cast 的补充,完成指针之间的转换
dynamic_cast<B *>():通过RTTI动态向上向下转型。向上转型不做检查(有可能指针本身问题导致错误),向下转型需要检查,只有基类指向派生类才可以向下转型。
结构体如何判断相等?
需要重载操作符 == 判断。
不能使用 memcmp 函数,因为结构体保存时会发生内存对齐,随机填充的值不保证相同。