本笔记按照cherno的C++视频进行学习,可能会存在部分错误。

一、预处理

"# " 预处理本质上是寻找对应的单元然后复制粘贴到当前文件,比如#include 本质是找到iostream这个文件然后将这个文件的内容复制粘贴到当前文件,# MAX_SIZE 20 本质是在这个文件中找到MAX_SIZE并将所有MAX_SIZE替换成20。
这里我们可以举一个很简单的例子,来自cherno大佬。
assistant.h

}

main.cpp

#include<iostream>
int main()
{
	std::cout<<"hellow word"<<std::endl;
	std ::cin.get();
	return 0;
#include"assistant.h"

这里我们可以看到编译器vscode甚至已经提前将对应的内容显示了

Pasted image 20260314222337.png

我们可以看到,它能够十分正常的进行运行
Pasted image 20260314222456.png

这是因为#include"assistant.h"预处理将assistant.h中的内容复制拷贝到了main.cpp ,也就是将’}‘拷贝到了main .cpp #include"assistant.h"这一行,补齐了原先缺少的右花括号,我们可以通过*.ii预处理文件直观看到预处理做了什么,在vscode中我们可以在tasks.json文件中进行配置,将obj这些临时文件进行保存,在"tasks""[{"args":[]}]中的输出文件设置"-o",path下方添加"-save-temps"保留临时文件

*.ii 预处理文件
*.s 汇编代码文件
*.o object文件

打开预处理文件可以看到其预处理结果
Pasted image 20260314223321.png

可以看到他将assistant.h中的'}'复制粘贴到了main.cpp中。

二、编译

编译器对源文件.cpp生成抽象语法树,之后转化为机器码二进制文件.obj,后续工作交由Linker链接器将.obj链接为一个.exe,在这个阶段会完成所有常数的计算,也就是说return 2* 5;实际上在obj中是return 10;

三、链接

Linker将各个.obj 链接到一起合成一个可执行文件.exe,其间链接器会对各个函数进行链接,所以只需要你本身在当前文件有进行函数声明,链接器会自动在所有obj文件中找到对应的函数定义。举个例子

main.cpp

#include<iostream>
void hellow();
int main()
{
	hellow();
}

hellow.cpp

#include<iostream>
void hellow()
{
	std::cout<<"hellow"<<std::endl;
}

hellow()在main.cpp只进行了声明,但是在编译器进行编译的时候会给声明的函数提供函数签名,只要对应的返回值、函数名、参数数量及类型相同,对应的函数签名就相同,两个cpp文件编译成obj文件之后linker会在这两个obj文件中找到对应的函数签名,将对应的函数内容链接合成到exe中,所以exe可以正常运行而不会出现因为hellow()没有定义而报错。
这就带来了预处理和链接的一些问题
由于预处理实际上只是复制粘贴到本文件,所以如果我们在hellow.h中对hellow()进行定义,然后在hellow().cpp中再对hellow()进行同样的定义,那么hellow.obj和main.obj中都有具有相同函数签名的hellow()函数,此时linker将不知道要链接向哪一次,便会出现LNK链接错误报错。
解决方法有三种
第一种是限定作用域
使用static 将hellow()函数的作用域限定在对应的.obj中,这样链接器只会在对应的obj内部进行链接,此时main.obj有自己的hellow(),hellow.obj也有自己的hellow()。
第二种方法是使用内联inline

inline void hellow(const char* message)
{
	std::cout<<message<<std::endl;
}
void OutHellow()
{
	hellow("hellow");
}

此时程序相当于

void OutHellow()
{
	std::cout<<"hellow"<<std::endl;
}

内联直接将函数进行展开,那么预处理的时候直接复制粘贴过来,在编译的时候变不会作为函数处理,提供对应的函数签名,那么链接的时候也就不会因为不同obj文件中有拥有着相同的函数签名的函数定义而出现LNK链接错误。
第三种就是最常见的,只在cpp中定义。
将.h中的函数定义移植到.cpp中,那么预处理的时候只会复制粘贴对应的声明语句,那么就不会出现有不同obj文件有拥有者相同的函数签名的函数定义而出现的LNK链接错误。

四、变量

变量是用存储数据的一种结构,不同类型的变量之间的本质差别只是其单个变量在内存(堆、栈)中占用的字节大小。
C++中有一些基础的原始数据类型,不同数据类型有各自不同的作用。

short 一般占用两个字节
int 一般占用四个字节(取决于系统或编译器)
long 一般占用四个字节(取决于编译器) 
long long  一般占用八个字节
float 一般占用四个字节
double  一般占用八个字节
bool 一般占用一字节

取int为例子,在他占用四个字节的情况下,一个字节8个bit位,总共能表示$2^{32}$种情况,C++中的int默认是有符号int,意味着它的最开始的bit位被用来表示正负,则只剩下31个bit位用来表示大小,所以其存储范围为 [-$2{31},2-1$]

int a = 0;//-2^{31}<=a<=2^{31}-1

如果要将最开始的符号位去掉,只需要使用无符号int即可

unsigned int  a =  0;//0<=a<=2^{32}-1}

数据类型本身有自己的对应用途,但C++本身很灵活,在一些情况下不必局限于类型本身。

#include<iostream>
int main()
{
   char a = 65;
   int b = 65;
   std::cout<<a<<std::endl<<b<<std::endl;
   a = 'A';
   std::cout<<a<<std::endl<<(int) a<<std::endl;
}

在这里我们可以看到我们创建了两个变量,分别是char类型的a和int类型的b,但是在输出他们的时候同样都是 65的十进制数值为什么输出不同?
原因是对于char类型的变量我们去翻阅ascii表可以看到他对应的十进制65实际上是字符'A',那么在输出这个char类型的时候被作为字符处理,输出到终端自然就是字符'A',而int类型表示整数,b则自然被当作整数处理,输出其对应的十进制数值65,后续我们对a的再赋值(a = 'A'😉 将字符'A'赋值给他a,这个初始化时的赋值65没有任何差别,所以后续仍旧输出的'A'。
可以看到cout中还有一个(int)的类型转化,将char类型的a转化为了int类型,所以他按照int类型处理便和b一样输出了65。
这大概可以说明变量和数据类型的用处,便于对不同类型的数据进行管理 。
在这里存在类型转化的问题,以cherno大佬视频中的例子为例,我们定义一个浮点变量a的时候右值输入的小数系统默认是double,在将他赋值给a的时候会触发隐式转化,将double向下转化为float再赋予a,如下面

int main(){
	float a = 3.14;
}

鼠标放置到3.14上我们可以看到显示的是double而不是我们期望的float
Pasted image 20260314223822.png

这会导致精度丢失还有性能下降,我们可以在后面添加一个f或者F表示其为float。

int main(){
	float a = 3.14f;
	//或者
	//float a = 3.14F;
}

下面是不添加f可能会导致的四种问题

  1. 编译器由于double向下转化float触发精度丢失警告
  2. auto 关键字和template模板的自我推导会受到影响
auto v1 = 3.13;//推导为double
auto v2 = 3.13f;//推导为float
  1. 函数重载调用
#include<iostream>
void print(float a)
{
	std::cout<<"float";
}
void pirnt(double a)
{
	std::cout<<"double";
}
int main()
{
	print(3.13);//输出double
	print(3.13f);//输出float
}
  1. 运算时性能下降
float a = 3.3f;
float b = a*3.13;
/*
**在执行a*3.13的时候需要先将float a转化为double之后再和3.13进行相乘,之后再将double结果向下转化为float塞回到b中,部分编译器可能会将这个过程优化,但没有优化的话将会多出两步转化,在矩阵等场景会浪费很大的算力
*/

五、函数

函数是代码块,用于反复调用,主要是减少重复代码的编写,类似模板。
主要格式是
返回类型 函数名(参数类型)
{
//代码块
return 返回类型对应对象;
}
如下面

#include<iostream>
void print(const char* string)
{
	std::cout<<string<<std::endl;
	return;//void 类型没有返回对象,所以return后面不跟东西,也可以直接省略return。
}
int add(const int& a, const int& b)//const 只读,表示不会修改a和b,int& a,引用a,引用传递,在这里使用const常量和&引用目的是避免值传递导致的性能开销和避免引用传递改变不应该改变的变量值。
{
	return a+b;//返回int类型
}
int main()
{
	print("TEST");
	int a = 1,b =2;
	std::cout<<add(a,b);
	std::cout<<add(b,3);
	//main()主函数是程序的主入口,是个唯一的特殊例子,主入口不需要写return 0;可写可不写
}

六、头文件

头文件*.h,用于存放声明,方便预处理、编译和链接
在预处理处我们知道#inclue"file"会将file文件里的内容进行复制拷贝到本文件,为了减少反复编写声明代码,我们将声明写到头文件中,这样只需要#include"file"一行就能直接将file里的声明全部预处理到本文件中,之后编译器便会编译然后根据声明去其他obj文件中寻找并链接,很省时间,比如下面(下面的代码会很糟糕,但只是暂时作为一个例子进行学习)。
assistant.h

void log(const char* message)
{
	std::cout<<message<<std::endl;
}
int add(const int& a,const int& b)
{
	return a+b;
}

math.cpp

#include"assistant.h"

void logMath()
{
	log("logMath used.");
}

main.cpp

#include"assistant.h"
int main()
{
	log("logMain here.");
	std::cout<<add(1,2)<<std::endl;
}

在这里我们只需要#include"assistant"就能将assistant.h下的两个函数复制粘贴到main和math中,省去多余代码编写,但这就会带来三、链接那一部分提到的问题。
同时还有代码重复的问题,比如说更改main.cpp为下面

#include"assistant.h"
#include"math"
int main()
{
//todo
}

在预处理的时候main.cpp中的#include"assistant.h"会把assistant.h的内容复制粘贴到main.cpp中,而math.cpp中再次包含了一次#include"assistant.h",于是又会再粘贴一次,重复进行了声明和定义,编译器会发生编译错误,为了防止这种情况出现,我们有两种方法提供你使用,在大体上没太大的差别
第一种是

#pragram once 

绝大多数编译器都支持,原理是标记物理文件是否已经被读取过,理论上性能稍微快点

#pragram once
void log()
{
//...
}

第二种是

#ifndef \_filename\_ 
#def  \_filename\_ 
声明
#endif

官方标准,所有编译器都支持,通过宏定义检查,性能更低,同时由于宏定义检查,如果两个头文件的宏定义名相同,会导致靠后读取到的头文件失效,存在一定风险

#ifndef _ASSISTANT_H_
#def _ASSISTANT_H_
void log()
{
...
}
#endif

七、条件分支

条件分支语句

if(expr1)
{
//如果expr1为真,也就是非零就执行该语句块
}
else if (expr2) 
{
//如果expr1为假,且expr2为真则执行该语句块
}
else 
{
//expr1和expr2均假,执行该语句块
}

在判定expr真假的时候,如果是两个数值进行比对,做法是读取这两个数值在内存中各自对应位置的值进行一字节一字节的比对。
比如,
int a和int b 进行比对,CPU会读取a和b在内存中对应的存储值进行一字节一字节的比对,四个字节完全相同才会判断相等。
如果是double a和int b进行比对呢?double占用八字节,int占用四字节,而且他们在内存中的存储机制还完全不同,int是使用头位做符号,剩下31位做值, 而double对应的存储标准是IEEE 754,一个符号位十一个指数位五十二个尾数位。
假设这两者还是按照一字节一字节进行比对的话,结果可以预想得到的糟糕,光是总字节数就对不上了,更何况他们存储的机制也不同,
所以在这种情况下C++的机制是低级的向上转化,转化之后再进行比对。
下面是用汇编语言表示的a和b比对时CPU会执行的指令。

int b = 4;
double a = 4.0;
bool result = (a==b);
mov eax, 4 
; 将4存储到整数寄存器eax中,对应int b = 4
cvtsi2sd xmm(),eax
; 检测到a和b类型不同,将b用cvtsi2sd指令转化为double存储到xmm()中
ucomisd xmm(),xmm1
; 比较寄存器xmm()((double)b对应寄存器)和xmm1((double) a对应寄存器)
sete al
; 如果相同,结果设为1或true

可以看到不同类型数值之间的比较会多一步向上转化,导致多出部分CPU指令,降低系统性能,所以最好在进行比对的时候只进行同类型数据之间的比对。