C++
C++基础
编译过程
预处理
在这一步,编译器会对所有的#define宏定义,#include文件包含,#ifdef条件编译等预处理指令进行处理。预处理器会将所有#define定义的符号替换为对应的内容,这样在接下来的编译阶段,代码中原来出现的宏符号就会被替换成具体的值或表达式。
编译
预处理后的代码会被翻译成汇编代码。在这个阶段,编译器将处理变量类型、表达式计算、语法结构等内容。
汇编
编译生成的汇编代码会被转为目标机器的二进制代码,即生成目标文件(通常是.obj或.0文件)。
链接
最后编译器会把多个目标文件以及所需的库文件链接在一起,生成最终的可执行文件。
内存区块
代码区
代码区存储程序的机器指令,即源代码编译后生成的可执行代码。这个区域通常是只读的,以防止程序在运行期间修改自身的指令,避免潜在的错误和安全问题。
数据区
数据区分为静态数据区和全局数据区,用来存储程序中定义的全局变量和静态变量。数据区通常在程序开始时被分配,并在程序结束时被释放。
BSS段(未初始化数据段):用来存储未初始化的全局变量和静态变量,系统会自动将这些变量初始化为0。
Data段(已初始化数据段):用来存储已初始化的全局变量和静态变量,系统会按定义的值进行初始化。
堆区
堆区用于动态分配内存。程序在运行时可以通过new、malloc等动态分配堆内存,通过delete、free等释放。堆区的内存分配和释放由程序员手动管理,如果不及时释放内存,会导致内存泄漏。
栈区
栈区用于存储局部变量、函数参数和返回地址等信息。每当一个函数被调用时,系统会在栈上为这个函数分配一块内存,函数调用结束后自动释放。栈区的内存分配和释放由系统自动管理,但栈区的大小有限,递归调用过深或局部变量过大可能导致栈溢出。
常量区
常量区存储常量字符串和其他常量数据。常量区的内容通常为只读,且在程序的整个生命周期内保持不变,防止对常量的修改。
关键字
#define
将标识符定义为宏,也就是常说的宏定义,告诉编译器将之后出现的所有标识符替换为替换列表。一般分为两种,对象式宏和函数式宏。
对象式宏
对象式宏以替换列表替换每次出现的被定义标识符。函数式宏
函数式宏以替换列表替换每次出现的被定义标识符,可选地接受一定量的实参,它们随即替换掉替换列表中出现的任何对应的形参。
函数式宏语法类似函数调用语法:每个宏名实例后随一个 ( 作为下个预处理记号,所引入的记号序列将被替换为替换列表。该序列以匹配的 ) 记号终止,跳过中间的匹配左右括号对。
#override 和 final
override:用于标记派生类中的重写函数。如果函数没有成功重写基类的虚函数,编译器会给出错误提示。这样可以避免因函数签名不匹配而导致的意外错误。
final:用于标记不允许派生类再重写的虚函数。这样可以锁定该函数,防止进一步的重写。
继承
继承是一种让一个类(派生类)获得另一个类(基类)属性和方法的机制。通过继承,可以在不重复编写代码的情况下复用已有的类。继承的类型主要有三种:
公共继承(public inheritance):基类的公有成员和保护成员在派生类中保持公有和保护状态,派生类可以访问这些成员,其他外部类也可以访问公有成员。
保护继承(protected inheritance):基类的公有和保护成员在派生类中都变为保护成员,派生类可以访问这些成员,但外部类无法访问。
私有继承(private inheritance):基类的公有和保护成员在派生类中都变为私有成员,派生类可以访问这些成员,但外部类无法访问。
多态
多态允许一个指针或引用在不同上下文中表现出不同的行为。C++中最常用的多态机制是通过 虚函数 实现的 运行时多态,即动态多态。多态主要依赖以下两个特性:
函数重载(Overloading):同一个作用域中多个函数可以具有相同名称,但参数列表不同,这就是重载。
虚函数和重写(Virtual Functions and Overriding):通过基类指针或引用调用派生类的实现。定义一个基类函数为虚函数(virtual),在派生类中重新定义它。
静态多态(Static Polymorphism)指的是在 编译时 决定函数的调用方式,而不是在运行时。静态多态通常通过 模板 和 函数重载 实现,而不像动态多态那样依赖虚函数。由于静态多态是在编译时确定的,代码执行速度通常比动态多态更快,但灵活性较低。
虚函数和虚函数表
在C++中,将基类中的一个函数声明为virtual,使得该函数成为虚函数。派生类可以重写这个虚函数,从而定义自己独特的行为。访问虚函数的常用方式是通过基类指针或引用,这样可以在运行时动态绑定到正确的派生类函数实现上。
虚函数表(Virtual Table):
- 虚函数的实现依赖于 虚函数表(vtable)和 虚函数表指针(vptr)。每个含有虚函数的类通常会创建一张虚函数表,其中记录了该类的虚函数地址。每个包含虚函数的对象实例都有一个vptr,指向这个虚函数表。
虚函数调用的具体流程是:
- 编译器生成一个虚函数表,记录所有虚函数的地址。
每个包含虚函数的类对象都持有一个vptr指针,指向该对象的虚函数表。
当通过基类指针调用虚函数时,程序会通过vptr查找虚函数表中的地址,并调用相应的函数。
因此,通过虚函数实现的多态性是 运行时多态,即函数的实际调用在运行时被动态决定。
虚函数表存储在常量区,虚函数存储在代码区
当一个虚函数在基类中没有实现时,可以将其声明为纯虚函数,即在函数声明后加上= 0。含有纯虚函数的类称为 抽象类,不能直接实例化。抽象类通常用作接口,以便派生类实现不同的具体行为。
STL容器库
新特性
编程技巧
CRTP (Curiously Recurring Template Pattern)
最近在学习一个项目的时候,用到了这个叫奇异递归模板模式的技巧,经过查阅学习,这是一个实现静态多态的C++模板编程技巧。它的基本做法是将派生类作为模板参数传递给它的基类:
1 | template <class Derived> |
此处引用cppreference中的示例片段,其中基类Base是一个模板类,派生类Derived继承自基类,继承的同时派生类把自己作为模板参数传递给了基类。基类通过公开接口,派生类内实现接口来实现静态多态。可以看到基类中使用static_cast将基类Base转换为传递过来的派生类Derived,然后调用该派生类中的方法。
至于c++23中引入的推导this语法,我暂时还没有了解过,以后了解后再来补充。
那么为什么要使用这个技巧呢,由于它是静态多态,那么在编译期绑定行为,避免了虚函数表的开销。其次基类定义通用逻辑,派生类专注于具体实现,使得代码的复用性提高。
