笔记来源网课教程: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
2
3
4
5
#include <iostream>
void log(const char* message)
{
std::cout << message << std::endl;
}

Main.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>

void log(const char* message);
// 声明语句

int main()
{
log("hello");
std::cin.get();
}

通过linker将main.cpp中声明并使用的函数链接到某个项目文件中唯一的log函数中

声明只包含了函数,定义包含了函数和函数体

C++编译器的工作

首先预处理,将所有代码转化成常量数据或指令。
将我们项目所有的cpp生成opj文件
cpp叫做翻译单元

#include工作原理

在编译前将指定的文件粘贴并复制到当前cpp当中

EndBrace.h

1
}

Main.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>

void log(const char* message);
// 声明语句

int main()
{
log("hello");
std::cin.get();
#include 'EndBrace.h'

编译器的工作就是,将EndBrace中的所有代码copy进去当前cpp

查看预处理器实际上生成的文件

预处理器文件
编译后会生成一个.i文件
里面含有预处理后的结果

查看obj文件

程序集

将obj中的二进制文件转化为汇编代码(ASM文件)

若在优化将速度调最大会忽略掉一些无用的操作

C++链接

编译后的操作,链接的焦点是找到每个符号和函数在哪里然后连接在一起。
编译可能不需要main函数,但是链接过程一定需要main函数

编译错误 错误类型是C开头,链接错误是LIN开头

不能存在两个相同的函数带着相同的参数,这样链接器不知道链接哪一个,从而产生错误。

易错处

Log.cpp

1
2
3
4
5
6
#include <iostream>
void logr(const char* message)
// 注意log函数名字已经被修改了
{
std::cout << message << std::endl;
}

Main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void log(const char* message);
//声明语句

int Multiply(int a,int b)
{
//log("multiply");
return a*b;
}
int main()
{
std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}

链接没有错误

Main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void log(const char* message);
//声明语句

int Multiply(int a,int b)
{
log("multiply");
return a*b;
}
int main()
{
//std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}

链接错误

为什么?
虽然在这个文件中可能用不上Multiply函数,但在其他文件可能用得上,所以链接器确实需要链接它

限制链接,函数只在当前翻译单元使用的方法

static加在函数前

产生链接错误的例子及修正

错误例子

log.h

1
2
3
4
5
6
#pragma once

void log(const char* message)
{
std::cout << message << std::endl;
}

log.cpp

1
2
3
4
5
6
#include <iostream>
#include "log.h"
void Initlog()
{
log("Initialized log");
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "log.h"

int Multiply(int a,int b)
{
log("multiply");
return a*b;
}
int main()
{
//std::cout << Multiply(5, 8) << std::endl;
std::cin.get();
}

链接失败,log重复了

原因分析:
两个头文件同时引入了两个log函数所以产生错误.

修改方法

  1. log.h
    1
    2
    3
    4
    5
    6
    7
    #pragma once

    static void log(const char* message)
    //将log函数修改为静态函数
    {
    std::cout << message << std::endl;
    }
  2. log.h
    1
    2
    3
    4
    5
    6
    #pragma once

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

inlind的作用是获得我们实际的函数体并将函数调用替换为函数体。

1
2
3
log("Initialized log");
//相当于
std::cout << "Initialized log" << std::endl;
  1. 将定义移到一个翻译单元

log.h

1
2
3
4
#pragma once

void log(const char* message);

log.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include "log.h"

void Initlog()
{
log("Initialized log");
}

void log(const char* message)
{
std::cout << message << std::endl;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int multiply(int a, int b)
{
return a*b;
}

void io_mul(int a, int b)
{
int result = multiply(a, b);
std::cout << result << std::endl;
}

int main()
{
io_mul(3,4);
io_mul(3,8);
io_mul(2,4);
std::cin.get();
}

io_mul的作用就是避免重复

C++头文件

当我们跨文件调用函数的时候免不了声明,如果某个函数很常用那就要一直复制粘贴,很繁琐。
而头文件就是塞入一堆声明,然后在其他cpp中include后,让预处理器帮忙复制粘贴。

pragma once

当我们创建了一个头文件,vs会自动帮我们填写#pragma once
这个的作用是防止include多个头文件时,里面有重复的声明,导致编译失败。他只会复制一次声明

ifndef

log.h

1
2
3
4
5
6
7
8
9
#ifndef _LOG_H
#define _LOG_H

void nitLog();
void log(const char* message);

struct Player {};

#endif

检查是否有个_LOG_H被定义了,如果没有就编译中包括以下代码,如果被定义了,那么这些都不会被包括进来

如果通过了这初次检查,我们定义_LOG_H,如果下次用到的时候,就不会重复声明了

include两个不同形式

< >形式

告诉编译器去搜索包含路径的文件夹

“ “形式

告诉编译器就在当前文件夹,我们也可以用”../log.h”去返回到当前文件的上级目录

区别c++标准库和c标准库

关键在于有无.h扩展,c++文件通常没有。

如何在vs中调试代码

调试

断点

在断点处暂停程序

读取内存

程序中断后,内存数据实际上还在,查看内存对诊断问题.

如何调试

  1. 确保模式是debug模式
  2. 设置断点
  3. 读内存

按键介绍

  • 逐语句(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
  1. mov dword ptr [a], 5:这条指令将立即数5移动到名为a的整数变量。它将5存储到a的内存位置。

  2. cmp dword ptr [a], 5:这是一个比较指令,用于比较a的值与5的值。它将a的值与5进行比较,但不会更改任何寄存器的值。

  3. jne main+34h (07FF6F0B823B4h):这是一个条件跳转指令。它检查前面的比较结果是否不等于(jne表示”jump if not
    equal”)零,如果不等于零,则跳转到指定的地址,这里是main+34h

  4. mov dword ptr [rbp+0F4h], 1:如果比较结果是相等的(即a等于5),则将立即数1移动到内存中的某个位置,该位置可能是一个标志变量,用于表示条件满足。

  5. jmp main+3Eh (07FF6F0B823BEh):这是一个无条件跳转指令,它将程序跳转到指定的地址,这里是main+3Eh

  6. mov dword ptr [rbp+0F4h], 0:这个指令是前面条件跳转的目标(如果比较结果不等于零),它将立即数0移动到内存中的某个位置,表示条件不满足。

  7. movzx eax, byte ptr [rbp+0F4h]:这条指令将内存中的一个字节(8位)加载到32位寄存器eax中,并将其零扩展(即高位填充0)。这可能是为了将条件满足与否的标志位加载到寄存器中。

  8. mov byte ptr [comparisonResult], al:这个指令将寄存器al中的字节值写入名为comparisonResult的布尔变量。这是将条件判断的结果保存到布尔变量中的操作。

  9. movzx eax, byte ptr [comparisonResult]:这是将布尔变量comparisonResult的值加载到寄存器eax中,以便进行进一步的条件判断。

  10. test eax, eax:这个指令将寄存器eax与自身进行按位与操作。它的目的是检查eax中的值是否为零。

  11. je main+5Ch (07FF6F0B823DCh):这是一个条件跳转指令,如果前面的按位与操作结果等于零(即eax中的值为零),则跳转到指定的地址,这里是main+5Ch

  12. lea rcx, [string "hello" (07FF6F0B8BCA4h)]:这个指令将字符串”hello”的地址加载到寄存器rcx中,准备调用一个名为Log的函数。

  13. call Log (07FF6F0B8135Ch):这是一个函数调用指令,它调用名为Log的函数,并将rcx中的地址作为参数传递给该函数,用于记录”hello”。

C++循环

for and while 循环

1
2
3
4
for (int i = 0; a < 5 ; i++)
{
pass
}

变量的声明,循环的条件(评估后的bool值),一次循环后的操作

1
2
3
4
5
6
int i = 0;
while (i < 0)
{
.....
i ++;
}
1
2
3
4
do
{

} while (i < 5)

C++控制流语句

控制流一般和循环一起使用

  • continue :只能在循环使用
  • break : 能在循环和switch语句使用
  • return

continue

跳到循环的下一个迭代

break

跳出循环

return

返回值,终止语句

C++指针

指针是整数,一种存储内存地址的数字。对管理和操纵内存有很大用处.

指针的引用和逆引用

类型的意义在于逆引用指针时可以访问和修改变量

1
2
3
4
5
6
int main()
{
int var = 8;
int* ptr = &var;
*ptr = 10;
}

申请内存和二次指针

1
2
3
4
5
6
7
8
9
10
int main()
{
char* buffer = new char[8];
memset(buffer, 0, 8);

char** ptr = &buffer;

delete[] buffer;
std::cin.get();
}

pte地址的内存实际上是buffer的内存地址,只不过反了过来
比如如果pte的内存是b8 f1 02 00
那么buffer的内存地址就是00 02 f1 b8

C++引用

(指针的扩展)
引用不用占用内存,但是指针是变量,会占用内存。

1
2
3
4
5
6
7
8
9
int main()
{
int a = 5;
int& ref = a;
//ref 实际上不存在,编译后只会有变量a
//相当于a的别名

std::cin.get();
}

引用的作用

运用指针的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
void Increment(int* value)
{
(*value)++;
}

int main()
{
int a = 5;
Increment(&a);
Log(a);
//结果为 : 6
std::cin.get();
}

运用引用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
void Increment(int& value)
{
value++;
}

int main()
{
int a = 5;
Increment(a);
Log(a);
//结果为 : 6
std::cin.get();
}

在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
2
3
4
5
6
7
int main()
{
int a = 5;
int b = 8;
int& ref = a;
ref = b;
}

应该改为

1
2
3
4
5
6
7
8
9
10
11
int main()
{
int a = 5;
int b = 8;
int* ref = &a;
*ref = 2;
ref = &b;
*ref = 1;
Log(a);
Log(b);
}

注意,错误例子中会运行成功,但是他并不是更改引用,ref这个引用还是引用的a

C++类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Player
{
public:
int x,y;
int speed;

void Move(int xa, int ya)
{
x += xa * speed;
y += ya * speed;
}
};

//void Move(Player& player, int xa, int ya)
//{
// player.x += xa * player.speed;
// player.y += xa * player.speed;
//}

int main()
{
Player player;
player.x = 5;
}

花括号后面需要有分号
由类类型构成的变量称为对象
新的对象变量称为实例
类中的函数称为方法

类中的属性是私有的,如果需要访问修改需要public类中的属性。

C++类与结构体对比

类默认是私有的,类外部调用时无法调用。
技术上说,没什么区别,但是使用情境不同。
struct and class

弹幕:用结构体当数据容器,用类来写具备逻辑的功能对象.

区别:

  1. 默认的继承访问权 : class默认的是private,strcut默认的是public。
  2. 默认访问权限 : struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
  3. “class”这个关键字还用于定义模板参数,就像“typename”。