C++
笔记来源网课教程:C++教程
10月·8号发布
C++是怎么工作的
项目中的源文件传输给编译器,编译器将其转化成二进制的东西,可能转化成某种库,也可能是可执行的程序
opp编译成obj文件,然后通过linker将obj整合起来
预处理
编译前处理include后面的文件,该文件通常叫做“头文件”,我们之所以要包括iostream这个头文件,是因为我们需要一个被调用的函数的声明,例如std::cout
main函数
程序的入口,它不一定需要返回值,默认返回0
<<重载语句
相当于一个函数而已,相当于print()std::cout << "Hello World"<< std::endl;
相当于std::cout .print( "Hello World").pint(std::endl);
链接项目中的函数代码
Log.cpp
1 |
|
Main.cpp
1 |
|
通过linker将main.cpp中声明并使用的函数链接到某个项目文件中唯一的log函数中
声明只包含了函数,定义包含了函数和函数体
C++编译器的工作
首先预处理,将所有代码转化成常量数据或指令。
将我们项目所有的cpp生成opj文件
cpp叫做翻译单元
#include工作原理
在编译前将指定的文件粘贴并复制到当前cpp当中
EndBrace.h
1 | } |
Main.cpp
1 |
|
编译器的工作就是,将EndBrace中的所有代码copy进去当前cpp
查看预处理器实际上生成的文件
编译后会生成一个.i文件
里面含有预处理后的结果
查看obj文件
将obj中的二进制文件转化为汇编代码(ASM文件)
若在优化将速度调最大会忽略掉一些无用的操作
C++链接
编译后的操作,链接的焦点是找到每个符号和函数在哪里然后连接在一起。
编译可能不需要main函数,但是链接过程一定需要main函数
编译错误 错误类型是C开头,链接错误是LIN开头
不能存在两个相同的函数带着相同的参数,这样链接器不知道链接哪一个,从而产生错误。
易错处
Log.cpp
1 |
|
Main.cpp
1 |
|
链接没有错误
Main.cpp
1 |
|
链接错误
为什么?
虽然在这个文件中可能用不上Multiply函数,但在其他文件可能用得上,所以链接器确实需要链接它
限制链接,函数只在当前翻译单元使用的方法
用static
加在函数前
产生链接错误的例子及修正
错误例子
log.h
1 |
|
log.cpp
1 |
|
main.cpp
1 |
|
链接失败,log重复了
原因分析:
两个头文件同时引入了两个log函数所以产生错误.
修改方法
- log.h
1
2
3
4
5
6
7
static void log(const char* message)
//将log函数修改为静态函数
{
std::cout << message << std::endl;
} - log.h
1
2
3
4
5
6
inline void log(const char* message)
{
std::cout << message << std::endl;
}
inlind的作用是获得我们实际的函数体并将函数调用替换为函数体。
1 | log("Initialized log"); |
- 将定义移到一个翻译单元
log.h
1 |
|
log.cpp
1 |
|
C++变量
变量允许我们命名存储在内存中的数据并继续使用
当我们创造变量时,他被存储在内存中。
不同变量类型的区别是内存大小
变量类型
- char : 1 byte (经常存储字符)
- short : 2 byte
- int : 4 byte
- long : 4 byte
- long long : 8 byte
- float : 4 byte
- double : 8 byte
- bool : 1 byte
float和double的区别可以是在数字后面加f(float)
1 是 Ture,0 是 False.
查看数字大小
sizeof(bool)
C++函数
最主要的是提高维护效率
避免复制重复
我们通常在头文件中写声明,在翻译单元或cpp文件中编写定义,原因就是链接错误中的修改方法3
例子
Main.cpp
1 |
|
io_mul的作用就是避免重复
C++头文件
当我们跨文件调用函数的时候免不了声明,如果某个函数很常用那就要一直复制粘贴,很繁琐。
而头文件就是塞入一堆声明,然后在其他cpp中include后,让预处理器帮忙复制粘贴。
pragma once
当我们创建了一个头文件,vs会自动帮我们填写#pragma once
这个的作用是防止include多个头文件时,里面有重复的声明,导致编译失败。他只会复制一次声明
ifndef
log.h
1 |
|
检查是否有个_LOG_H被定义了,如果没有就编译中包括以下代码,如果被定义了,那么这些都不会被包括进来
如果通过了这初次检查,我们定义_LOG_H,如果下次用到的时候,就不会重复声明了
include两个不同形式
< >形式
告诉编译器去搜索包含路径的文件夹
“ “形式
告诉编译器就在当前文件夹,我们也可以用”../log.h”去返回到当前文件的上级目录
区别c++标准库和c标准库
关键在于有无.h扩展,c++文件通常没有。
如何在vs中调试代码
调试
断点
在断点处暂停程序
读取内存
程序中断后,内存数据实际上还在,查看内存对诊断问题.
如何调试
- 确保模式是debug模式
- 设置断点
- 读内存
按键介绍
- 逐语句(F11)(step into):进入到当前这行代码上的函数里面
- 逐过程(F10)(step over):从当前函数跳到下一行代码
- 跳出(shift+F11)(step out):跳出当前函数,回到调用这个函数的位置
窗口介绍
- 自动、局部窗口 : 向你展示可能重要的全局或局部
- 监视1:观察变量(输入要观察的变量然后回车)
右键可以修改成查看16进制
内存视图
- 最左侧为内存地址
- 中间是以16进制存储的实际值
- 最右边是以ACCII对值的解释
内存视图的使用
在地址一栏输入&+变量名即可
VS的最佳设置
项目的设置
这只是虚拟组织的文件夹,在文件资源管理器中并不存在。
我们可以点击显示所有文件这个按钮,这样子新添加文件夹的时候就实际的添加了文件夹,而非虚拟文件夹。
文件夹的设置
$(SolutionDir)bin\$(Platform)\$(Configuration)\
C++条件与分值(if语句)
检查条件,然后跳转到内存的不同的地方,并从这里开始执行指令。
内在指令
设置断点调试时,右键进入反汇编模式
- mov : move
- jne :jump not equal
- je :jump equel
mov dword ptr [a], 5
:这条指令将立即数5移动到名为a
的整数变量。它将5存储到a
的内存位置。
cmp dword ptr [a], 5
:这是一个比较指令,用于比较a
的值与5的值。它将a
的值与5进行比较,但不会更改任何寄存器的值。
jne main+34h (07FF6F0B823B4h)
:这是一个条件跳转指令。它检查前面的比较结果是否不等于(jne表示”jump if not
equal”)零,如果不等于零,则跳转到指定的地址,这里是main+34h
。
mov dword ptr [rbp+0F4h], 1
:如果比较结果是相等的(即a
等于5),则将立即数1移动到内存中的某个位置,该位置可能是一个标志变量,用于表示条件满足。
jmp main+3Eh (07FF6F0B823BEh)
:这是一个无条件跳转指令,它将程序跳转到指定的地址,这里是main+3Eh
。
mov dword ptr [rbp+0F4h], 0
:这个指令是前面条件跳转的目标(如果比较结果不等于零),它将立即数0移动到内存中的某个位置,表示条件不满足。
movzx eax, byte ptr [rbp+0F4h]
:这条指令将内存中的一个字节(8位)加载到32位寄存器eax
中,并将其零扩展(即高位填充0)。这可能是为了将条件满足与否的标志位加载到寄存器中。
mov byte ptr [comparisonResult], al
:这个指令将寄存器al
中的字节值写入名为comparisonResult
的布尔变量。这是将条件判断的结果保存到布尔变量中的操作。
movzx eax, byte ptr [comparisonResult]
:这是将布尔变量comparisonResult
的值加载到寄存器eax
中,以便进行进一步的条件判断。
test eax, eax
:这个指令将寄存器eax
与自身进行按位与操作。它的目的是检查eax
中的值是否为零。
je main+5Ch (07FF6F0B823DCh)
:这是一个条件跳转指令,如果前面的按位与操作结果等于零(即eax
中的值为零),则跳转到指定的地址,这里是main+5Ch
。
lea rcx, [string "hello" (07FF6F0B8BCA4h)]
:这个指令将字符串”hello”的地址加载到寄存器rcx
中,准备调用一个名为Log
的函数。
call Log (07FF6F0B8135Ch)
:这是一个函数调用指令,它调用名为Log
的函数,并将rcx
中的地址作为参数传递给该函数,用于记录”hello”。
C++循环
for and while 循环
1 | for (int i = 0; a < 5 ; i++) |
变量的声明,循环的条件(评估后的bool值),一次循环后的操作
1 | int i = 0; |
1 | do |
C++控制流语句
控制流一般和循环一起使用
- continue :只能在循环使用
- break : 能在循环和switch语句使用
- return
continue
跳到循环的下一个迭代
break
跳出循环
return
返回值,终止语句
C++指针
指针是整数,一种存储内存地址的数字。对管理和操纵内存有很大用处.
指针的引用和逆引用
类型的意义在于逆引用指针时可以访问和修改变量
1 | int main() |
申请内存和二次指针
1 | int main() |
pte地址的内存实际上是buffer的内存地址,只不过反了过来
比如如果pte的内存是b8 f1 02 00
那么buffer的内存地址就是00 02 f1 b8
C++引用
(指针的扩展)
引用不用占用内存,但是指针是变量,会占用内存。
1 | int main() |
引用的作用
运用指针的例子
1 | void Increment(int* value) |
运用引用的例子
1 | void Increment(int& value) |
在C++中,函数参数的默认传递方式是按值传递(pass by value)。这意味着当你调用一个函数时,传递给函数的是原始数据的副本,而不是原始数据本身。这是因为按值传递会创建原始数据的副本,以便函数可以在副本上执行操作,而不会影响原始数据。
当你调用Increment(a)时,a的值被复制到Increment函数的局部变量value中,然后在函数内部对value进行递增操作。这个递增操作只会影响value的副本,而不会影响a的原始值。这就是为什么在main函数中a 的值仍然是5。
那如果我用return a呢?
如果你在 Increment 函数中返回 value,那么你需要在 main 函数中捕获这个返回值并将其分配给 a,才能使 a 的值增加。这是因为在 C++ 中,函数的返回值不会自动修改传递给它的参数。
a = increment(a)
简而言之,引用可以节约内存开销,避免重复复制。
引用的注意
你不能改变它引用的东西
例如
1 | int main() |
应该改为
1 | int main() |
注意,错误例子中会运行成功,但是他并不是更改引用,ref这个引用还是引用的a
C++类
1 | class Player |
花括号后面需要有分号
由类类型构成的变量称为对象
新的对象变量称为实例
类中的函数称为方法
类中的属性是私有的,如果需要访问修改需要public类中的属性。
C++类与结构体对比
类默认是私有的,类外部调用时无法调用。
技术上说,没什么区别,但是使用情境不同。
struct and class
弹幕:用结构体当数据容器,用类来写具备逻辑的功能对象.
区别:
- 默认的继承访问权 : class默认的是private,strcut默认的是public。
- 默认访问权限 : struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
- “class”这个关键字还用于定义模板参数,就像“typename”。