执行
在C++程序的执行阶段,程序将按照main()函数开始执行,然后根据程序中的逻辑顺序执行相应的语句和函数调用。
内存管理
容易发生内存问题的包括栈溢出和内存泄漏,分别对应内存模型中的栈和堆,但需要注意以下问题
- 不要返回局部变量的引用或指针
- 不要对已经释放的内存进行操作
异常处理
线程安全
资源管理
代码优化
边界条件
错误处理
测试与调试
动态联编和静态联编
- 指计算机程序不同部分彼此关联,分为静态和动态
- 静态联编是指联编⼯作在编译阶段完成的,这种联编过程是在程序运⾏之前完成的,⼜称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调⽤(如函数调⽤)与执⾏该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引⽤的类型。其优点是效率⾼,但灵活性差
- 动态联编指联编在程序运⾏时动态地进⾏,根据当时的情况来确定调⽤哪个同名函数,实际上是在运⾏时虚函数的实现。这种联编⼜称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果
- C++中⼀般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使⽤动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引⽤来调⽤虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引⽤名.虚函数名(实参表)
实现的条件
- 必须把动态联编的⾏为定义为类的虚函数;
- 类之间应满⾜⼦类型关系,通常表现为⼀个类从另⼀个类公有派⽣⽽来;
- 必须先使⽤基类指针指向⼦类型的对象,然后直接或间接使⽤基类指针调⽤虚函数;
何时需要初始化
- 引用成员变量
- const
- 基类构造有一组参数
- 成员构造有一组参数
- 初始化顺序:按照成员声明顺序决定
C++初始化
类型
- 编译初始化
- 动态初始化
编译初始化
- 静态初始化在程序加载的过程中完成
- 包括全局变量初始化和constexpr类型的初始化
- zero initialization 的变量会被保存在 bss 段
- constexpr initialization 的变量则放在 data 段内
- 其次全局类对象也是在编译器初始化。
动态初始化
出现时机:出现在编译期和运行期的局部位置初始化
- 动态初始化也叫运行时初始化
- 需要经过函数调用才能完成的初始化、类初始化
- 局部静态类对象的初始化
- 局部静态变量的初始化
- 动态初始化一般出现在
动态初始化中静态局部变量2个问题
线程安全问题
实现方法
- 一个线程在初始化 m 的时候,其他线程执行到 m 的初始化这一行的时候,就会挂起而不是跳过
- 局部静态变量在编译时,编译器的实现是和全局变量类似的,均存储在bss段中。
- 然后编译器会生成一个保证线程安全和一次性初始化的整型变量,是编译器生成的,存储在 bss 段。
- 它的最低的一个字节被用作相应静态变量是否已被初始化的标志
- 若为 0 表示还未被初始化,否则表示已被初始化(if ((guard_for_bar & 0xff) == 0)判断)。
- __cxa_guard_acquire 实际上是一个加锁的过程,
- 相应的 __cxa_guard_abort 和__cxa_guard_release 释放锁。
- 它的最低的一个字节被用作相应静态变量是否已被初始化的标志
内存泄漏问题
原因
- 在局部作用域消失时,data区仍然保存其内存空间
- 执行路径不明
- 对于局部静态变量,构造和析构都取决于程序的执行顺序。程序的实际执行路径不可预知的
- 关系不明
- 局部静态变量分布在程序代码各处,彼此直接没有明显的关联,很容易让开发者忽略它们之间的这种关系
建议
- 减少使用局部静态变量
实例化
过程
- 分配内存空间
- 执行构造
不可实例化的类
- 带有一个或以上virtual的函数的类
- 工具类(直接通过static调用函数)
如何阻止实例化
- 包含纯虚函数
- 构造函数私有
实例化中的变量初始化时机与顺序
时机
- 类中const初始化必须在构造函数初始化列表中初始化
- 类中static初始化必须在类外初始化
- 成员变量初始化顺序按照类中声明顺序,而构造函数初始化顺序按照成员变量在构造函数中位置决定
顺序
- 初始化base类中的static部分(按程序出现顺序初始化)
- 初始化派生类中的static部分(按程序出现顺序初始化)
- 初始化base类的普通成员变量和代码块,再执行父类的构造方法;
- 初始化派生的普通成员变量和代码块,在执行子类的构造方法;
问题
把异常完全封装在析构函数内部,决不让异常抛出函数之外
栈上的指针什么时候析构
.h .cpp .hpp 关系
函数指针
- 指向函数的指针变量。函数指针本身是一个指针变量,变量指向一个函数。
- 有了这个指针,可以用这个指针变量调用函数
- 除了调用外还可以做函数的参数
char * fun(char * p) {…} // 指向char的指针
char * (*pf)(char * p); // pf函数指针
pf = fun; // 函数指针指向函数
pf(p); // 调用
bool、int、float、指针类型变量a与0的比较语句
if(!a) or if(a)
if (a ==0)
if(a <= 0.000001 && a >=-0.000001)
if(a != NULL ) or if(a == NULL)
helloworld程序开始到打印到屏幕的过程
- ⽤户告诉操作系统执⾏ HelloWorld 程序(通过键盘输⼊等);
- 操作系统:找到 HelloWorld 程序的相关信息,检查其类型是否是可执⾏⽂件;并通过程序⾸部信息,确定代码和数据在可执⾏⽂件中的位置并计算出对应的磁盘块地址;
- 操作系统:创建⼀个新进程,将 HelloWorld 可执⾏⽂件映射到该进程结构,表示由该进程执⾏ HelloWorld程序;
- 操作系统:为 HelloWorld 程序设置 cpu 上下⽂环境,并跳到程序开始处;
- 执⾏ HelloWorld 程序的第⼀条指令,发⽣缺⻚异常;
- 操作系统:分配⼀⻚物理内存,并将代码从磁盘读⼊内存,然后继续执⾏ HelloWorld 程序;
- HelloWorld 程序执⾏ puts 函数(系统调⽤),在显示器上写⼀字符串;
- 操作系统:找到要将字符串送往的显示设备,通常设备是由⼀个进程控制的,所以,操作
- 系统将要写的字符串送给该进程;
- 操作系统:控制设备的进程告诉设备的窗⼝系统,它要显示该字符串,窗⼝系统确定这是⼀个合法的操作,然后将字符串转换成像素,将像素写⼊设备的存储映像区;
- 视频硬件将像素转换成显示器可接收和⼀组控制数据信号;
- 显示器解释信号,激发液晶屏;
printf实现原理
函数参数通过压入堆栈的方式来传递参数 而栈是从内存高地址向低地址生长,因此最后压栈的在堆栈指针的上方,printf第一个被找到的参数就是字符指针。函数通过判断字符串控制参数的个数来判断参数个数与数据类型,进而算出需要的堆栈指针偏移量