C++ 运行时

 

执行

在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第一个被找到的参数就是字符指针。函数通过判断字符串控制参数的个数来判断参数个数与数据类型,进而算出需要的堆栈指针偏移量