首页 > 编程语言 >C++语法

C++语法

时间:2022-11-03 08:59:01浏览次数:63  
标签:int mov C++ 语法 eax ebp dword ptr

C++

常量和变量

变量的定义方式:

const 类型 名字 {};

直接使用值

#define 名字 值

常量定义方式:

类型 名字 {};

类型 名字 = 初始值;

不管是常量还是变量,本质都是在内存中申请一块内存,用来存放数据,只不过常量申请的内存区域不允许修改,而变量申请的内存区域,允许修改。以上说的可以修改和不可以修改,只是站在编译器的角度,不管是变量还是常量,甚至是已经编译好的代码,都是可以修改的。

类型

整数类型

image-20221013203715422

image-20221013203814389

布尔类型

浮点数类型

image-20221014110904162

浮点数也可以用来表达整数

下面进行举例表示

int main()
{
	float a{100000001.0};
	std::cout << a;
}

1e+08代表1*10的8次方,通过运行程序后发现初始化浮点数a的个位数被舍掉了这就是float精度带来的问题

img

image-20221014104449650

下面放着例子截图

image-20221014105115978

基础运算

image-20221014105631010

image-20221014105701432

将a++分解开就相对好理解先是a=a;a=a+1;

而++a则是a=a+1;再进行赋值。

类型转换

下面是C++中的隐性转换遵循下面的顺序表,类型越在上面就以此类型转换

image-20221014112334045

发现结果还是1e+08但实际我们想要的结果不该是这个,按类型转换表float在int类型之上那么就要将b转换为浮点数,但出现这样的结果不仅仅是类型转换这么简单,还与浮点数的精度计算有关,我通过测试发现

img

但出现这样的结果不仅仅是类型转换这么简单,还与浮点数的精度计算有关,我通过测试发现

img

img

到这里就明确了,就是浮点数精度影响了计算的结果,我的判断是浮点数计算同意了科学计数法从而让变量b变成了0.

还有一种类型转换

image-20221014115239174

img

两种转换方式都可以进行类型转换,但也存在区别,通过C的方式,是无论什么类型都可以转换为指定类型,但C++的方式则相对更加安全,因为它会去验证是否可以转换。总得来所C的更加粗暴。

补充

当无法分清那种类型占用多少字节可以通过

image-20221014120336315

字符

在计算机的世界中万物皆数字,那么在使用的过程中人类交流的语言文字,又是以什么形式表现呢

这里就引出字符的概念,无论是符号,字母,汉字等都可以叫做字符,字符在计算机眼中依旧是以数字的形式存在。但人为得规定了一套对应的规则,例64=A

char chara{64};

最初只有char类型,char占用一个字节也就是,也就是0-253,最初计算机是由美国人发明的,他们最先制定的编码规范,美国使用英语,他们制定的编码规范里只要包括符号,数字字母就都用了,但其他国家也需要使用呀,美国人用掉了前0-121,也就是ascll码,其他国家的人在后面122-253添加自己需要的也就够用了,也就有了自己的编码规范。

但到中国就行不通了,中国的汉字有上万个这254很显然不够看,那么好我用两个字节来表示汉字,65536个完全够用了,所有汉字占两字节。也就是GBK编码

但各个国家的的编码规范都不相同,那么就会出现一个问题,在中国可以正常使用的文件到美国可能就不能正常使用了,这也就有了Unicode编码,也叫万国码。

image-20221014144357602

image-20221014145213114

注:wchar可以占用2\4字节

char charA{ 'A'};
char charB{ 65 };//在ascll码表中64对应的就是A。
wchar_t wcharA{ L'A' };//wchar_t初始化时值的前面带个L,宽字节字符
char16_t char16A{ u'A' };//char16_t初始化值前要带u,为utf-16字符
char32_t char32A{ U'我' };//char32_t初始化值前要带U,为utf-32字符
char8_t char8A{};
std::cout << charA<<charB<<(char)10;//std::cout使用的编码方式不支持显示汉字,换行符的ascll码为10
std::wcout << wcharA;

虽然char和wchar_t都是A但他们本质上其实是不同的。

字符的故事

image-20221014162055093

ascll码是0-121都为ascll有美国制定。

ANSI是微软自己制定的编码规范,它先确认你在哪个国家,加载指定的内码页

GB2312是中国最开始提出的支持汉字的编码规范,后来演化出来的gbk,GB18030

UNICODE编码是国际编码组织为了解决各国编码的不统一,在中国能正常使用,出国就无法编码的硬伤,所以也叫万国码。在实际使用中推出了UTF-16(字母和汉字都是两字节),UTF-8(字母为一字节,节省体积),UTF-32

判断类型

auto a{100};//int类型
auto b{100L};//long类型
auto c{100LL};//long long类型
auto d{L'10'};//wchar_t类型
auto e{U'10'};//char32_t类型
auto f{u'10'};//char16_t类型
auto g{100u;};//unsigned int类型后面加u/U为无符号型
auto h{100.0f};//float类型不加f的浮点数默认为double类型
auto r{100.0L};//long double类型

image-20221014152824102

auto a{100};//int类型
typeid(a).name();

image-20221014152846241

格式化输出流及转义

C++

image-20221014154828828

image-20221015193518237

输出

image-20221016205711611

输入

image-20221016205818732

image-20221017111527826

运算优先级

image-20221014160027563

c=a---b;//根据符号的优先级关系,c=a-- -b

注:可以通过括号强制改变运算顺序

生命周期

枚举类型

image-20221014191152670

枚举类型需要明白的几个点

1.枚举类型可以提高代码的可读性和安全性
2.枚举类型默认是int类型
3.枚举类型成员只能是整数类型
4.枚举类型和其他类型需要强制转换
5.默认情况下,枚举类型的下一个项的初始值是上一个项的初始值+1

enum class Tres//enum class 类型名称:基本类型(不写默认为int)
	{
		a = 100, 
		b,//b=101
		c,//c=102
		d=10,//d=10
		e=a,//e=100
		f,//f==101
		g,//g=102
		h//h=103
	};

自定义变量名称

image-20221014192816424

#define cout std::cout

int main()
{
	cout << "修改成功";
	typedef long long int64;
	using eint = int;
}

命名空间

如何写命名空间

namespace game
{
	int HP = 1000;
	int MP = 100;
	int lv = 10;
}

int main()
{
	int iHP = game::HP;
}
namespace game//嵌套命名空间
{
	int HP = 1000;
	int MP = 100;
	int lv = 10;
	namespace igame
	{
		int iHP = 666;
		int iMP = game::HP;
		int lv = 1;//特意嵌套时定义了相同命名的变量
	}
}

int main()
{
	using std::cout;
	int iHP = game::HP;
	cout << game::lv << (char)10 << game::igame::lv << "\n";//这里的两个lv分别是两个嵌套的命名空间中的变量,需要一层一层的取
}

修改命名空间

using namespace std;//后续在使用例如std::cout时直接使用cout就可以了。这种是std所以都不用在带std
using std::cout;//对指定使用。

image-20221014202148906

变量的生命周期

image-20221014205010755

全局变量的生命周期从程序运行开始知道结束。

局部变量的生命周期存在于它所在的代码块中。

#include <isotream>
int a{ 100 };//定义全局变量

int main()
{
	int a{ 150 };//代码块中声明的变量都可以叫做局部变量
	using std::cout;
	{
		int a = 200;//当变量名存在冲突时,程序会访问最近的,
		{
			cout << a << "\n" << ::a << (char)10;//访问变量名冲突的全局变量,可以通过限定符::来访问
		}
	}
}

image-20221014210056224

位运算

image-20221015142305131

求反运算

求反运算说起来其实挺简单就是将被求反的对象,0变1,1变0.下图呈现了求反的过程

image-20221015151833996

#include <iostream>
#include <bitset>

int main()
{
	int a{ 0x3085 };
	std::cout << a << (char)10;//a=00000000000000000011000010000101
	a = ~a;
	std::cout <<std::bitset<32>(a);//11111111111111111100111101111010
}

从前后的二进制来开很明显就能看出变化

int a{ 12345 };//此时a为int类型,为有符号整数
std::cout << a << (char)10;
a = ~a;//求反
std::cout <<a;//此时a=-12346

image-20221015154621162

从上面的例子就可以看出有符号数如何求它的相反数

int a{ 12345 };
	std::cout << a << (char)10;
	a = ~a;
	a += 1;//此时再对求反的值加1就是这个值的相反数
	std::cout <<a;

位移运算

位移运算从名字上就很直白得能听出它的作用,就是把整个数据向指定方向移动几位。

但从计算机的角度理解,就没有这么简单。

当程序声明一个变量其实就是内存中申请了一个一块地址,大小呢根据它的类型来决定,对这个变量赋值的过程其实也就是将数据放到这个内存中的过程,对计算机而言这块内存就是这个变量,那么对它进行位移运算的操作,出去的数据将不再是这个变量的值,也就会被舍弃,空下的位则用0填充

image-20221015142424200

向左移位运算的本质:移动一位相当于对值乘以2

int a{ 0b01101101101 };
a <<= 6;
std::cout << std::bitset<32>(a);

image-20221015161325352

image-20221015142444676

向右移位运算的本质:移动一位相当于对值除以2

int a{ 0b01101101101 };
a >>= 6;
std::cout << std::bitset<32>(a);

那么如果我对一个数先进行位数的左移运算再右移运算呢

int a{ -100 };
int b{1234};
std::cout << std::bitset<32>(a) ;
a <<= 5;
std::cout << (char)10 << std::bitset<32>(a);
a >>= 5;
std::cout <<(char)10 << std::bitset<32>(a);
std::cout <<(char)10 << std::bitset<32>(b);
b <<= 20;
std::cout << (char)10 << std::bitset<32>(b);
b >>= 20;
std::cout <<(char)10 << std::bitset<32>(b);

image-20221015164122935

unsigned c{100000000};
std::cout << std::bitset<32>(c) ;
c <<= 15;
std::cout << (char)10 << std::bitset<32>(c);
c >>= 15;
std::cout << (char)10 << std::bitset<32>(c);

image-20221015190216326

通过两端程序,以及结果很快就能发现区别,有符号时进行右移位运算很不确定,会以为cpu不同改变,可能会根据符号位进行填充。

无符号则只会用0填充

与运算

image-20221015155232516

将两位的二进制的各位进行运算,有0则为0,都为1才为1

int main()
{
	int a{ 0b01101101101 };
	int b{ 0b01111100000 };
	a = b&a;
	std::cout << std::bitset<32>(a);
}

image-20221015155739267

int main()
{
	int a{ 0xFF00 };
	int b{0x1234};
	std::cout <<std::hex<< (b&a);
}

或运算

image-20221015155940897

有1则为1,都为0才为0,这一点跟与运算有点反过来了

int main()
{
	int a{ 0b01101101101 };
	int b{ 0b01111100000 };
	a = b|a;
	std::cout << std::bitset<32>(a);
}//结果为a=00000000000000000000001111101101

异或运算

image-20221014210607112

所谓位运算就是将两个数的二进制各位进行计算,相同则为0,不同则为1。

从异或运算的特性很快就能得出:

int a;
int b;
int c;
c=a^b;
b=a^c;
a=b^c;

用途:通常在加密解密中使用

using std::cout;
	unsigned int diamond{ 100 };
	unsigned int vip_exp{ 90 };
	unsigned realdiamond{};
	realdiamond = diamond ^ vip_exp;
	cout << "修改钻石数量为:"<<realdiamond;
	std::cin >> diamond;
	cout << "修改消费钻石数量为:";
	std::cin >> vip_exp;
	while (((vip_exp == (realdiamond ^ diamond))||(diamond == (realdiamond ^ vip_exp))))
	{
		diamond = realdiamond ^ vip_exp;
		vip_exp = realdiamond ^ diamond;
		cout << "钻石数量为:" << diamond;
		cout << "消费钻石数量为:" << vip_exp;
		goto Loop;
	}
	cout << "你修改了所有数据!\n";
Loop:;

自定义数据类型---结构

image-20221015104711343

image-20221015110048883

image-20221015110153056

struct PEOPLE//创建结构
	{
		wchar_t name{ L'小树' };
		unsigned height{ 185 };
		unsigned weight{};//可以不进行初始化
	};
	PEOPLE peopleA;//声明变量
	std::cout << "输入您的体重:";
	std::cin >> peopleA.weight;//通过变量.参数的方式访问
	std::cout << "您的身高是" << peopleA.height<<"\n您的体重是"<<peopleA.weight<<(char)10;
	std::wcout << "您的名字是" << peopleA.name;

在创建结构体中的变量类型可以自由设置,而之前学习的枚举类型,只能是统一类型,并且只能是整数类型

选择结构式if

关系运算符

image-20221015193143808

image-20221015193232258

if

image-20221015194049460

int a{ 1 };
int b{ 2 };
if (a = b)//这个if执行与否取决于b的值,当b=0为就不执行
{
	std::cout << "1234";
}
else
    std::cout<<"4321";
if(1)
{
    std::cout<<char(10);
}

逻辑运算符

image-20221015225407634

bool a=0xFFFF0000&0xFFFF;//这里是先进行位运算得到结果为0,所有a=0;
bool b=0xFFFF0000&&0xFFFF;//这里是逻辑运算,(bool)0xFFFF0000&&(bool)0xFFFF,布尔类型非0为真,b=1.

image-20221016153334855

条件运算符

image-20221016154324609

int cs{};
std::cout << "请输入一个数:";
std::cin >> cs;
int x = cs ? 1000 / cs : 0;
std::cout << "结果为:" << x;

大神技巧

image-20221016193001186

异或运算是什么,相同为0,不同为1,那么if(a^b), a^b的结果会转化成布尔类型,非0即真所以a ^b不为0,就要a不等于b。

unsigned age{};
unsigned grades{};
std::cout << "请输入你的年龄:";
std::cin >> age;
std::cout << "请输入你的成绩:";
std::cin >> grades;
if ((age<18)^(grades<90))//要使用a^b就要年龄达标,成绩却不行
{
	std::cout << "恭喜你获得奖励!";
}
else
	std::cout << "很遗憾!";

字符处理

image-20221016200736623

isupper

char a{};
std::cout << "请输入一个字符:";
std::cin >> a;
if (a >= 65 && a <= 90)//大写字母的ascll码表值是65到90
{
	std::cout << "输入的字符是大写字母\n";
}
else
	std::cout << "输入的字符不是大写\n";

islower

char a{};
std::cout << "请输入一个字符:";
std::cin >> a;
if (a >= 97 && a <= 122)//小写字母的ascll码表值是97到122
{
	std::cout << "输入的字符是小写字母\n";
}
else
	std::cout << "输入的字符不是小写\n";

isalpha

char a{};
std::cout << "请输入一个字符:";
std::cin >> a;
if ((a >= 97 && a <= 122)||(a>=65&&a<=90))
{
	std::cout << "输入的字符是字母\n";
}
else
	std::cout << "输入的字符不是字母\n";

isdigit

int a{};
std::cout << "请输入一个字符:";
std::cin >> a;
if (a>=48&&a<=57)
{
	std::cout << "输入的字符是数字\n";
}
else
	std::cout << "输入的字符不是数字\n";

就不全部都写了大体上基本没有差别。

选择结构switch

image-20221016203305734

int main()

{
	int a;
	std::cin >> a;
	switch (a)
	{
	case 1:
		break;
	case 2:
		break;
	case 3:
		break;
	default:
		printf("执行到最后!");
		break;
	}
}

image-20221016204140984

int main()

{
	int a;
	std::cin >> a;
	switch (a)
	{
	case 1:
		break;
	case 2:
		break;
	case 3:
		[[fallthrough]];//防止编译器报错,程序接着贯穿执行到case 4
	case 4:
		break;
	default:
		printf("执行到最后!");
		break;
	}
}

语句块中的变量

image-20221016204625125

image-20221016205045498

goto

image-20221017180906184

for

while循环

数组的概念

image-20221016101120128

创建变量的过程本质上是想计算机申请一块内存,内存大小由类型决定

数组其实就相当于批量的创建变量,同样也是申请内存

image-20221016104002034

int sudentId[10];//声明数组不对其进行初始化
int tres;//声明了int类型的变量但未进行初始化
std::cout<<tres;//访问未初始化的变量编译时会报错
for (int i{0}; i < 10; i++)
{
	std::cin >> sudentId[i];
}
for (int i{0}; i < 10; i++)
{
	std::cout << sudentId[i];
}

image-20221016105841760

image-20221016105937763

不对数组进行初始化不会报错,可以正常运行。还是由于数组其实就是 一块内存不对他进行初始化,里面的数据就是之前遗留的数据。

image-20221016132230472

image-20221016110312207

int sudentId[10]{};
unsigned number{};
int a{};
int b{};
a=sizeof(sudentId[0]);//sudentId[0]是数组sudentId的一个元素单拎出来这里更像是一个int类型的变量,所有a就会得到数组的类型占几个字节
b = sizeof(sudentId);
number = b / a;
printf("数组中共有%d个元素",number);

数组的应用

int main()

{
	int studentId[100]{};
	int indexIN{0};
	while (indexIN < 100)
	{
		std::cout << "输入学号(输入0查看已登记的学生信息)";
		std::cin >> studentId[indexIN];
		for (int i{ 0 }; i < indexIN; i++)
		{
			if (studentId[i] == studentId[indexIN])
			{
				std::cout << "这位学生你已经登记,给爷爬\n";
				indexIN -= 1;//
				break;
			}
		}
		if (studentId[indexIN] == 0)
		{
			system("cls");
			for (int i{ 0 }; i < indexIN; i++)
			{
				std::cout << i + 1 << "号 学生学号" << studentId[i] << (char)10;
			}
		}
		else
			indexIN++;
	}
}
int main()

{
	int studentId[100]{};
	int indexIN{0};
	int studentID{};
	while (indexIN < 100)
	{
		std::cout << "输入学号(输入0查看已登记的学生信息)";
		std::cin >> studentID;
		if (studentID == 0)//判断输入是否为0
		{
			system("cls");
			for (int i{ 0 }; i < indexIN; i++)
			{
				std::cout << i + 1 << "号 学生学号"<<studentId[i]<<std::endl;
			}
		}
		for (int i{ 0 }; i < indexIN; i++)//判断是否重复
		{
			if (studentID == studentId[i])
			{
				std::cout << "已经登记过来,给爷爬!" << (char)10;
				goto Loop;//重复跳不将输入加入数组
			}
		}
		studentId[indexIN] = studentID;
		indexIN++;
	Loop:;
	}
}

基于数组的循环

image-20221016141106142

这种通过for循环遍历数组的方式是c++11引入的

int number[]{ 1000,1001,1002,1003 };//直接初始化数组不对进行元素数进行初始化
for (char i:number)
{
	std::cout << i<<(char)10;
}
for (auto i : number)
{
	std::cout << i << (char)10;
}

image-20221016141740555

这种方式很简洁,还可以自己选择类型;

auto则是系统根据原有的类型

多维数组

image-20221016142342485

int number[]{ 1000,1001,1002,1003 };//一维数组可以不指定元素个数
int studentId[2][3]//多维数组必须指定
{
	{1,2,3},
	{1,2,3}
};

image-20221016144320233

int number[]{ 1000,1001,1002,1003 };
int studentId[2][3]//二维数组
{
	{1,2,3},
	{1,2,3}
};
int studentID[2][3][5]//三维数组
{
	{
		{1,2,3,4,5},
		{1,2,3,4,5},
		{1,2,3,4,5}
	},
	{
		{1,2,3,4,5},
		{1,2,3,4,5},
		{1,2,3,4,5}
	}
};
for (int i{0}; i < 2; i++)//遍历三维数组
{
	for(int a{0};a<3;a++)
	{
		for (int b : studentID[i][a])
		{
			std::cout << b << std::endl;//直接通过b输出
		}
	}
}

image-20221016144326914

数据安全:无论是变量,数组,多维数组本质都还是向内存申请内存,数组,多维数组是一块连续的内存。

int studentId[2][3]//二维数组
{
	{1,2,3},
	{1,2,3}
};
std::cout<<studentId[9][9];

那么此时又会出现什么情况呢,依旧可以访问到数据,但这是超出数组之外内存中的数据。所有这种原生数组是不安全的。

std::array

image-20221016150932425

std::vector

image-20221026193207938

指针和引用

指针

image-20221017201111230

指针还得从计算机万物皆数据,创建变量进行赋值的本质还是申请一块内存,内存大小由类型决定,赋值就是向这块内存写入数据,指针本质就是一块内存地址,大小同样也由声明是的类型来决定。

image-20221017201139952

image-20221017201202000

image-20221017201227316

int* pa{ };
int a;
pa = &a;//通过&取址符获取变量a的内存地址
*pa = 500;//通过*间接运算符来修改变量a的值
printf("%d", a);

但这里的*pa和a是完全不同的,虽然都能实现修改变量a值的效果

a=500是先找a所在的地址再往其中写入

*pa则是直接对内存写入

指针数组

image-20221017203456571

image-20221017203504164

指针补充

在声明变量的时候,需要告诉计算机变量的类型,数据类型就告诉计算机申请内存的大小,但这里其实就会有一个问题,就是都知道计算机只能存储0和1,那么计算机用如何知道我该以无符号数处理还是有符号整数还是字符的方式处理呢,数据类型就是告诉计算机你要用这种方式处理数据

指针本质上也就是一个特殊的变量类型,那么声明指针就同样也要向计算机申请一块用于存放数据的内存。但指针又与正常的变量类型存在很明显的区别,因为它里面放的是一个内存地址,接下来就要考虑内存地址的大小由什么决定呢,但我可以知道的是所占内存大小一定不受类型影响了。

它根据操作系统有关,32位操作系统232 也就是最大4G的内存那么在操作系统的设计之初,必然考虑极端情况,换句话说就是我就是4G的32位操作系统那么我默认所有的内存地址都为32位也就是4个字节。同理64位的地址就都为64位也就是16个字节

image-20221017210703020

image-20221017220833743

对指针进行类型转换

占用同等内存

int a{1000};
unsigned c{1000};
unsigned* pc{&c};
int* pa{&a};
char* pb{};
pa = (int*)pc;//将unsigned*类型变量pc类型转换位int*,那么*pa就是变量c的指针了
*pa = -1;//pa为int*类型赋值-1自然会成立
std::cout << c<<std::endl<<*pa;//c是unsigned类型那么系统就会以unsigned类型该有的方式处理内存中的数据,*pa则是int*类型所以为-1

从占用内存小向大

unsigned sa{10001};//sa=00002711
char* pb{};
unsigned* pa{ &sa };
pb = (char*)pa;
*pb = 'A';//sa=00002741 字符A的16进制就是41
std::cout << sa<<std::endl<<pa;

从占用内存大到小

int a{1000};
unsigned c{1000};
unsigned* pc{&c};
int* pa{&a};
char* pb{};
pa = (int*)pc;
*pa = -1;//pa为int*类型赋值-1自然会成立
std::cout << c << std::endl << *pa << std::endl;
pb = (char*)pc;
*pb = 'A';
std::cout << c << std::endl <<*pc<< std::endl<<*((int*)pc) << std::endl << *pb;//FFFF FF41

指针实验1----指针的加减

image-20221018160603656

int a[]{10001,10002,20001,30001};
int* ptr{ &a[0] };
std::cout << ptr << std::endl;
std::cout << (*ptr)++ << std::endl;//(*ptr)++这里用括号了,那么就不用管运算符的优先级,就是先输出显示*ptr也就是a[0]然后对a[0]加1;
std::cout << ptr << std::endl;
std::cout << *ptr++ << std::endl;//这里就会受到运算符的优先级影响了,++的优先级大于*,但ptr是先输出在加,那么先输出*ptr,然后对指针加1
std::cout << ptr << std::endl;

image-20221018161005131

指针的加减法

拿上面的ptr++举例对指针ptr加1实际上相当于+1*数据类型所占内存的大小

指针实验2------多级指针

上面已经很多次说明,其实指针就是特殊的变量,那既然是变量,不管里面放着内存地址,那就需要内存空间来存储,那么如果想操作这指针存放内存地址的内存呢

image-20221018192143035

int a[]{ 10001,10002,10003,40001 };
int* prt{ &a[0]};//prt为变量a的指针
int** pprt{&prt};//pprt为指针prt的指针
int*** ppprt{ &pprt };//ppprt为指针prt的指针的指针
std::cout << *prt<<std::endl<<*pprt << std::endl <<**pprt<<std::endl<<*ppprt << std::endl <<**ppprt << std::endl <<***ppprt << std::endl;
**ppprt = &a[1];//**ppprt就是指针prt
std::cout << *prt << std::endl << *pprt << std::endl << **pprt << std::endl << *ppprt << std::endl << **ppprt << std::endl << ***ppprt << std::endl;

image-20221018193447512

image-20221018192116372

常量指针

image-20221018165352536

常量指针不能对其指向的内存地址进行改变,但可以改变指向的地址

const int a{};
int b{};
const int* pa{&b};
//*pa = 9000;//通过指针pa修改指向的变量b的内存数据会报错
b=9999;
pa=&b;

image-20221018165330295

指针常量则是不可以修改指向的内存地址,但却可以修改内存地址中的数据

image-20221018165306699

常量的变量指针可谓集万家于一体,既不能修改指向的内存地址,也不能修改内存地址中的数据。

指针和数组

	int a[]{1001,1002,1003,1004};
00007FF6D276184D  mov         dword ptr [a],3E9h  
00007FF6D2761854  mov         dword ptr [rbp+0Ch],3EAh  
00007FF6D276185B  mov         dword ptr [rbp+10h],3EBh  
00007FF6D2761862  mov         dword ptr [rbp+14h],3ECh  
    int* prta{ &a[0] };
00007FF6D2761869  mov         eax,4  
00007FF6D276186E  imul        rax,rax,0  
00007FF6D2761872  lea         rax,a[rax]  
00007FF6D2761877  mov         qword ptr [prta],rax  
    *prta = 10;
00007FF6D276187B  mov         rax,qword ptr [prta]  
00007FF6D276187F  mov         dword ptr [rax],0Ah  
    a[0] = 1;
00007FF6D2761885  mov         eax,4  
00007FF6D276188A  imul        rax,rax,0  
00007FF6D276188E  mov         dword ptr a[rax],1  
    a[1] = 2;
00007FF6D2761896  mov         eax,4  
00007FF6D276189B  imul        rax,rax,1  
00007FF6D276189F  mov         dword ptr a[rax],2  
    a[2] = 3;
00007FF6D27618A7  mov         eax,4  
00007FF6D27618AC  imul        rax,rax,2  
00007FF6D27618B0  mov         dword ptr a[rax],3  
    a[3] = 4;
00007FF6D27618B8  mov         eax,4  
00007FF6D27618BD  imul        rax,rax,3  
00007FF6D27618C1  mov         dword ptr a[rax],4 

通过反汇编将来探究指针与数组的关系

	int a[]{1001,1002,1003,1004};
00007FF6D276184D  mov         dword ptr [a],3E9h  
00007FF6D2761854  mov         dword ptr [rbp+0Ch],3EAh  
00007FF6D276185B  mov         dword ptr [rbp+0Ch],3EBh  
00007FF6D2761862  mov         dword ptr [rbp+14h],3ECh  

这里汇编的意识是创建数组并进行初始化向其内存空间中写入数据的过程,[rbp+0Ch] [rbp+0Ch]这里也佐证了数组在内存中是一连续的,它们的间隔为四个字节,这也与它的数据类型恰好符合

后面几句汇编指令很好理解,但第一句汇编mov dword ptr [a],3E9h

他直接向a赋值,那a是什么a不就是我们创建的数组名吗,但汇编中却是以[a]这样的方式出现那我是不是可以理解为a也是一个内存地址,那它不也就是某种意义上的指针吗。这也只是一种猜想,因为我其实也没看到过指针在汇编中的展现,产生这种判断也会带有俯视的视角看问题了。接着看

	int* prta{ &a[0] };
00007FF6D2761869  mov         eax,4  
00007FF6D276186E  imul        rax,rax,0  //rax置零
00007FF6D2761872  lea         rax,a[rax]  
00007FF6D2761877  mov         qword ptr [prta],rax  
    *prta = 10;
00007FF6D276187B  mov         rax,qword ptr [prta]  
00007FF6D276187F  mov         dword ptr [rax],0Ah   

那么现在就来看指针

lea rax,a[rax] a[rax]又出现了更加确信a很可能也是指针但[rax]又是什么呢,rax为0,那么a[rax]=a[0],而lea这句指令的意思就是将a[0]的内存地址给到寄存器rax中,那么我大胆推断这句话就是&a[0]的汇编指令。

mov qword ptr [prta],rax 这里的[prta]但我很明确的知道它是指针,那么到这里就很明确了,那么也就可以大胆推测了a就是指针但a[0]又是什么

mov rax,qword ptr [prta] mov dword ptr [rax],0Ah

这两句汇编其实就能佐证指针就是特殊的变量,它同样需要内存来存储数据

	a[0] = 1;
00007FF6D2761885  mov         eax,4  
00007FF6D276188A  imul        rax,rax,0  
00007FF6D276188E  mov         dword ptr a[rax],1  
    a[1] = 2;
00007FF6D2761896  mov         eax,4  
00007FF6D276189B  imul        rax,rax,1  
00007FF6D276189F  mov         dword ptr a[rax],2  
    a[2] = 3;
00007FF6D27618A7  mov         eax,4  
00007FF6D27618AC  imul        rax,rax,2  
00007FF6D27618B0  mov         dword ptr a[rax],3  
    a[3] = 4;
00007FF6D27618B8  mov         eax,4  
00007FF6D27618BD  imul        rax,rax,3  
00007FF6D27618C1  mov         dword ptr a[rax],4 
//单用一种类型的数组很难看出什么这里加了一个short类型的数组作为对比项
	b[0] = 10;
00007FF68B1259DD  mov         eax,2  
00007FF68B1259E2  imul        rax,rax,0  
00007FF68B1259E6  mov         ecx,0Ah  //ecx为32位寄存器而short为占用2字节的数据类型
00007FF68B1259EB  mov         word ptr b[rax],cx  
    b[1] = 20;
00007FF68B1259F0  mov         eax,2  
00007FF68B1259F5  imul        rax,rax,1  
00007FF68B1259F9  mov         ecx,14h  
00007FF68B1259FE  mov         word ptr b[rax],cx  
    b[2] = 30;
00007FF68B125A03  mov         eax,2  
00007FF68B125A08  imul        rax,rax,2  
00007FF68B125A0C  mov         ecx,1Eh  
00007FF68B125A11  mov         word ptr b[rax],cx

想要对数组的某个元素赋值不用想都知道肯定会用到mov向它的内存地址写入数据,可是问题来了数组是连续的内存,那怎么知道什么它某个元素的内存地址呢。

通过汇编指令很容易发现,向数据的某个元素赋值,按理只需要一条mov指令就完事,可是这里却不同,存在肯定就有它的意义,首先看mov dword ptr a[rax],1 向a[rax]写入数据不用看就知道它是一个内存地址那rax里是什么呢

mov eax,4 eax=4,但eax为啥等于4暂时不知道
imul rax,rax,0 然后进行有符号乘法rax乘以0再看另一个的

imul rax,rax,1 这里是rax*1这里就能确定了rax乘以几就是数组中的第几个元素

那么eax=4是为什么,通过和short类型的数组来看,就可以判断出这是根据数据类型来的

那么rax中放着的其实就偏移量了,那么a[rax]=[a+rax];

那么大胆猜测首先之前说了a是一个指针里面放着内存地址,那既然a[rax]是不是同样适用于指针

大胆猜测小心求证

int a[]{1001,1002,1003,1004};
short b[]{ 1,2,3,4 };
int* prta{ a };//既然a是指针那么就直接用不用&a[0]
*prta = 10;
std::cout << a << std::endl << prta[2];//prta[2]=a[2]

image-20221019104848603

小结:数组跟指针真的存在关系,数组的a就是指针,a[rax]=[a+rax]既然a是指针那就适用于指针

多维数组

一维数组已经没问题了那么来看看多维数组怎么搞

int a[]{1001,1002,1003,1004};
short b[]{ 1,2,3,4 };
int c[2][5]
{
	{1,2,3,4,5},
	{1,2,3,4,5}
};
int d[2][3][5]
{
	{
		{1,2,3,4,5},
		{1,2,3,4,5},
		{1,2,3,4,5}
	},
	{
		{1,2,3,4,5},
		{1,2,3,4,5},
		{1,2,3,4,5}
    },
};
int(*prtc)[3][5]{d};//声明数组指针
int* prta{ a };
*prta = 10;
int(*prtb)[5] {c};
std::cout << a << std::endl << prta[2] << prtb[0][1]<< std::endl<< prtc[0][2][1];

创建数组指针通过使用[]的方式,本质就是告诉计算机它的偏移量是多少,从而找到指定元素的内存地址

那就看一下如何计算多维数组的偏移量

    int(*prtc)[3][5]{d};
00007FF779BE5DEB  lea         rax,[d]  
00007FF779BE5DEF  mov         qword ptr [prtc],rax  
    int(*prtb)[5] {c};
00007FF779BE5DF6  lea         rax,[c]  
00007FF779BE5DFA  mov         qword ptr [prtb],rax  
    prtb[1][3] = 10;
00007FF779BE5E01  mov         eax,14h  
00007FF779BE5E06  imul        rax,rax,1  
00007FF779BE5E0A  mov         rcx,qword ptr [prtb]  
00007FF779BE5E11  add         rcx,rax  
00007FF779BE5E14  mov         rax,rcx  
00007FF779BE5E17  mov         ecx,4  
00007FF779BE5E1C  imul        rcx,rcx,3  
00007FF779BE5E20  mov         dword ptr [rax+rcx],0Ah  
    prtc[1][2][4] = 10;
00007FF779BE5E27  mov         eax,3Ch  
00007FF779BE5E2C  imul        rax,rax,1  
00007FF779BE5E30  mov         rcx,qword ptr [prtc]  
00007FF779BE5E37  add         rcx,rax  
00007FF779BE5E3A  mov         rax,rcx  
00007FF779BE5E3D  mov         ecx,14h  
00007FF779BE5E42  imul        rcx,rcx,2  
00007FF779BE5E46  add         rax,rcx  
00007FF779BE5E49  mov         ecx,4  
00007FF779BE5E4E  imul        rcx,rcx,4  
00007FF779BE5E52  mov         dword ptr [rax+rcx],0Ah  

首先很明显能看到数组指针和普通指针创建从汇编指令上并没有什么区别

从之前的分析有经验了mov eax,14h 14h转换成十进制为20,20是因为什么呢之前创建的时候的[5],5*4=20,那么后面甚至可以不用看了大胆推测prtb[1][3]=[prtb+(1*5*数据类型大小)+(3*数据类型大小)]

由此得出结论创建数组指针就是定一个量,换句话说多维数组也是一段连续的内存,按上面的例子来说[5]就是一次跨过了一列的数据

prtb[1][3]这里的[1][3]就是用来计算偏移量的

动态分配内存

image-20221019152228981

image-20221019152307874

image-20221019152329638

image-20221019152433593

image-20221019152505139

image-20221019194022965

int* prta=(int*)malloc(100);
if (prta == 0)
{
    printf("内存生成失败!");
}
else std::cout << prta<<std::endl;
free(prta);
prta = (int*)calloc(100, sizeof(char));
if (prta == 0) printf("内存生成失败!");
else std::cout << prta << std::endl;
prta = (int*)realloc(prta,100);
if (prta == 0) printf("内存生成失败!");
else std::cout << prta << std::endl;
free(prta);
prta = new int[5];
if (prta == 0) printf("内存生成失败!");
else std::cout << prta << std::endl;
delete[] prta;

动态分配内存的分险

image-20221019152627448

image-20221019195219221

image-20221019162944184

image-20221019195415678

引用

image-20221019200154015

image-20221021130443584

堆栈

image-20221021130527908

智能指针

std::unique_ptr-----唯一智能指针

image-20221021095201453

image-20221021095305591

image-20221021095350516

image-20221021095433990

image-20221021095421449

image-20221021094610551

int* a{ new int[5] };
int* b = a;
delete[] a;
std::unique_ptr<int[]> ptra{new int[5] {1,2,3,4,5}};//<int>就不能通过ptra[0]来访问数据
std::unique_ptr<int[]> ptrB{std::make_unique<int[]>(10)};
std::unique_ptr<int[]> prtC{};
prtC = std::move(ptra);//类型相同才能转移
//ptra=ptrb;
std::cout << prtC[4] << std::endl;
ptra.reset();
std::cout << ptra<<std::endl;
b = ptrB.get();
std::cout << b << std::endl;
ptrB.release();
std::cout << ptrB << std::endl<<b[5];

所谓唯一智能指针,唯一何解。唯一智能指针不能直接复制,也就是不能将指针所指向的内存赋值给其他指针

std::shared_ptr----共享智能指针

image-20221021104123615

image-20221021105252061

image-20221021105310791

image-20221021105221592

共享智能指针可以多个指针指向同一个内存空间,也就有了共享之意

字符处理

image-20221021130957115

image-20221021131021670

char stra[]{ "Hello" };
char strb[]{ 'H','o','l','l','0','\0' };
std::cout << stra << std::endl << strb << std::endl;
char* ptrstr = new char[10] {"Hollo"};
std::cout << ptrstr<<std::endl;
char* strc{ (char*)"Hello" };//"Hello"为常量需要强制转换为char*
std::cout << strc;

image-20221021131855969

image-20221021131914134

char类型占用1个字节,wchar_t占用2个字节,这里就会出现一个问题,同样一个字符,char类型和wchar_t类型在内存中的表现可能完全不同。这里不得不提如何实现从数字到各种字符的转换,首先建立一个表,告诉计算机为1等于多少为2等于多少,那么计算器只要拿着数据去对比这个表就能完成。就拿支撑中文的GBK编码举例,它也只是这个表的一种。

image-20221021152308136

image-20221021152902280

通过strlen()获得字符串长度

下面是自己实现strlen的功能

int main()
{
    char strA[0xff]{};
    std::cin>>strA;
    unsigned strnumber{0};
    for (int i{};; i++)
    {
        if (strA[i] == '\0')
        {
            break;
        }
        else if (strA[i] <= (unsigned)0x7F)
        {
            strnumber++;
        }
        if(strA[i]> (unsigned)0x7f)
        {
            i += 1;
            strnumber+=1;
        }
    }
    printf("这个字符串长度为%u", strnumber);
}

指针和结构体

image-20221021163858026

typedef struct Role
	{
		int Hp;
		char str;
		int Mp;
	}*ptrRole;

这是一种复合写法分开就是;

struct Role
	{
		int Hp;
		char str;
		int Mp;
	};
typedef Role* ptrRole;

也就是自定义结构体,将Role*也就是这个定义的结构体的指针改名为ptrRole

看结构体跟汇编的关系自然也离不开汇编,那看汇编

	typedef struct Role
	{
		int Hp;
		char str;
		int Mp;
	}*ptrRole;
	Role user;
	ptrRole ptruser = &user;
00007FF7B439210D  lea         rax,[user]  
00007FF7B4392111  mov         qword ptr [ptruser],rax  
	ptruser->Hp = 1000;
00007FF7B4392115  mov         rax,qword ptr [ptruser]  
00007FF7B4392119  mov         dword ptr [rax],3E8h  
	ptruser->Mp = 1000;
00007FF7B439211F  mov         rax,qword ptr [ptruser]  
00007FF7B4392123  mov         dword ptr [rax+8],3E8h  
	user.Mp = 2000;
00007FF7B439212A  mov         dword ptr [rbp+10h],7D0h  

那要理解这些东西,自然要站在编译器的角度看问题嘛。那为什么呢,首先写的代码通过编译器编译成机器码,毕竟汇编其实也就是助记符。那么其实写代码本质上还是跟编译器做沟通,让它理解。

从汇编上来看构造结构体的过程并没有在汇编中展现,那也就是说这告诉编译器的。让编译器知道我构造的结构体几只嘴巴几个手,就相当于一个模版。

	ptrRole ptruser = &user;
00007FF7B439210D  lea         rax,[user]  
00007FF7B4392111  mov         qword ptr [ptruser],rax 

在之前指针和数组那就知道了,lea rax,[user]其实就是&user的汇编指令上的展现。mov指令则是典型的指针行为,将结构体的内存地址放到[ptruser]

	ptruser->Mp = 1000;
00007FF7B439211F  mov         rax,qword ptr [ptruser]  
00007FF7B4392123  mov         dword ptr [rax+8],3E8h  
	user.Mp = 2000;
00007FF7B439212A  mov         dword ptr [rbp+10h],7D0h  

来看这种新的通过结构体指针赋值的方式和之前的区别

从汇编指令很直观看到除了都有的mov指令赋值外,ptruser->Mp = 1000; 还多了一步从指针中获取内存地址放到rax,但这好像是废话,通过指针赋值当然要先去获取内存地址。然后向[rax+8]写入数据。8应该不出意外就是它的偏移量,编译器通过给的模版,计算出它的偏移量,在汇编指令上就能很直观的感受到

这种方式跟之前数组指针存在区别,但本质上是一样的。

内存对齐

结构体本质上在内存对齐

image-20221021203127312

自定义数据类型-----联合体

联合体跟结构体直观来看很类似,而且在功能上也很相似

image-20221022131730089

image-20221022131845153

image-20221022131851251

结构体在内存中是连续的内存,它会受到内存对齐影响。但联合体则会不同,联合体所占用的内存大小由它占用内存最大的数据类型决定。

union User
{
    short Hp;
    int Mp;
};
int main()
{
    User user{};
    std::cout << sizeof(user);
}

image-20221022133524234

image-20221022133528440

对比来看就能很直观了结构体受内存对齐影响,联合体最大占用的数据类型为int所以为4

union User
{
    short Hp;
    int Mp;
};
int main()
{
    User user{};
    user.Hp = 0xffff;//此时内存中的数据=0xffff0000
    std::cout << user.Mp;//user.Mp为int类型要对0xffff0000转换为有符号数为65535
}

image-20221022133904263

image-20221022134156708

字符串

std::string

image-20221022184304360

image-20221022184544487

image-20221022184752285

image-20221022214059803

image-20221022214205480

#include <iostream>
#include<string>
using std::string;

int main()
{
	string str(6, 'A');//str="AAAAAA"
	str = str + "123";
}

string进阶

![image-20221023082757768](C++语法.assets/image-20221023082757768.png

image-20221023083020599

image-20221023083133144

image-20221023083407239

image-20221023083712871

字符串补充知识点

image-20221023083945354

image-20221023084228146

image-20221023085035331

image-20221023085232207

image-20221023085416067

image-20221023091138348

image-20221023090458245

字符串应用

image-20221023094021126

#include <iostream>
#include<string>
using std::string;

int main()
{
	string str{ "id=user;pass=632105;role=郝英俊" };
	string user;
	int Fstart{};//查询属性的起始位位置
	int Fover;
	string strOut;
	while (true)
	{
		std::cout << "请输入要查询的属性:";
		std::cin >> user;
		Fstart = str.find(user + "=");
		if (Fstart == std::string::npos)
		{
			printf("您输入的%s属性无法查询!", user);
		}
		else
		{
			Fover = str.find(";", Fstart);
			strOut=str.substr(Fstart+user.length() + 1, Fover-Fstart-user.length()-1);//Fstart+user.length() + 1截取字符串的位置  Fover-Fstart-user.length()-1截取长度
			std::cout << strOut<<std::endl;
		}
	}
}

image-20221023094602945

image-20221023094740219

image-20221023101047194

image-20221023101037152

指针数组字符串

image-20221023100558666

声明了一个string类型的字符串,这里就能看到这个string类型的字符串str其实是const char字符常量

image-20221023152213880

	string str{ "123456" };
	char strc[]{ "123456" };
	std::cout << std::endl << &strc<<strc;

image-20221023153053389

其实这里也就体现了字符和数组之间的区别,如果是数组的话,此时输出strc应该为一个内存地址,这也不奇怪,因为strc本身其实就是指针。那么这里的strc是指针吗。那么我如何才能证明strc为指针呢。

那我就要先研究编译器对char类型的指针的处理方式

char a{'1'};
const char* strptr{(char*) & a};
std::cout << strptr;

image-20221023154751116

从上面看一切就很直观了首先如果是其他类型的指针那么输出出来的strptr应该是它所指向的内存地址,但这里呢一串乱码为什么呢,先看到了1这可以理解因为1是char类型的变量a的字符,可后面的一连串乱码又该如何解释呢。

大胆猜测处理char类型指针和其他数据类型的指针存在区别,如果是其他指针想通过指针读取内存数据需要间接运算符,或者通过strptr[0]这种方式,但char却可以直接通过指针读取,就会当字符处理,可还没解释通为啥会出现上面的结果呀。

这是因为char类型字符串需要\0结尾来告诉编译器好了读完了,如果没有就会一直读下去很显然,这里就是这个结果。

综上也就间接证明了其实strc会被作为*strc。

string str{ "123456" };
char strc[]{ "123456" };
std::cout << (int)&str << std::endl << (int)&str[0] << std::endl << (int)&str[1]<<std::endl;
std::cout << (int) & strc << std::endl << (int)&strc[0]<<std::endl<<(int)&strc[1];

image-20221023160620874

对比string类型字符串和char类型,有一点是肯定的在内存上都是连续的可是为什么从数据上看好像不太能说得通。

这很大概率是string是类

大神必修----字符串

image-20221023101226076

image-20221023102047776

字符串流

image-20221023102051927

#include <iostream>
#include<string>
#include<sstream>
using std::string;

int main()
{
	std::stringstream str;
	str << "你好" << 1234;
	string stra = str.str();
	std::cout << stra;
}

函数基础

函数概念

image-20221023135417308

image-20221023135846949

自定义一个函数首先要有一个返回值类型,各种数据类型都可以,void很特殊它代表任意类型,甚至返回一个空值都可以。

当然也需要给函数起名还需要提供需要的参数,需要参数那就还要说明参数的数据类型。

int Add(int a,int b)//提供了两个int类型的参数
{
	c=a+b;
	return c;//返回c
}
int main()
{
    int a{10};
    int b{20};
    int c{Add(a,b)};
}

在没学习函数之前代码基本都放在main函数中,这时候回头看main函数也有返回值类型,那也就可以判断main其实也就是某种意义上的函数,只是程序从main函数开始到结束

函数参数:指针参数

int Add(int a,int b)
{
	int c{a+b};
    return c;
}
int main()
{
    int a{10};
    int b{20};
    int c{Add(a,b)};
}
int e;
int d{ e };

image-20221023161852583

这里不对e进行初始化,然后又让e的值给d它就报错了,首先有一点要先确认的是,声明变量是否已经分配了内存

img

通过取e的地址也就能得出一个结论声明变量即使不进行初始化也已经分配了内存。但如何解释不对a,b进行初始化对其内存空间进行操作却能成功编译不报错呢。但好像用函数时需要对其进行传参,那也就是说其实对参数a,b进行了赋值,只是隐藏掉了编译器自己偷偷干了这样的操作。

image-20221023163105552

image-20221023163223504

函数参数:数组

image-20221024082402948

image-20221024082430017

image-20221024082533302

函数参数:引用

image-20221024082839866

image-20221024083051886

#include <iostream>
#include<string>
using std::string;

struct Role
{
	string name;
	int Hp;
	int Mp;
	int damage;
};
bool Act(const Role& Acter, Role*& beAct)
{
	beAct->Hp -= Acter.damage;
	bool bEnd = beAct->Hp < 0;
	beAct = (Role*)&Acter;
	return bEnd;
}
int main()
{
	Role user{ "奥特曼",200,300,850 };
	Role monstor{ "小怪兽",800,300,50 };
	Role* pRole = &monstor;
	if (Act(user, pRole)) std::cout << pRole->name << " 怪兽死亡!";
}
bool Act(const Role& Acter, Role* beAct)
{
	beAct->Hp -= Acter.damage;
	bool bEnd = beAct->Hp < 0;
	beAct = (Role*)&Acter;
	return bEnd;
}

这里参数用指针,但在函数中修改了指针指向,这就不得不提变量的生命周期的问题了,用上面这个函数来说beAct是修改成功了,但它在执行完这个函数,它的寿命就结束了。还是没能实现改变指向。函数中的指针参数终归还是赋值来的,而不是直接对给的参数进行操作。

bool Act(const Role& Acter, Role*& beAct)
{
	beAct->Hp -= Acter.damage;
	bool bEnd = beAct->Hp < 0;
	beAct = (Role*)&Acter;
	return bEnd;
}

用引用就能实现上面所要的效果,引用的本质其实也就是指针,引用中放着的就是它所引用的内存地址,所以指针的引用其实本质是什么呢。就是指针的内存地址,那引用作为函数参数不就是把那个指针的内存地址给到引用吗,那这逻辑就通了。那它其实就直接到指针的内存地址修改值。

函数参数:默认实参

函数参数:不定量参数

image-20221024095621443

#include <iostream>
#include<string>
using std::string;
int main(unsigned cound,char* arm[])
{
	string str = arm[0];
	int Fstart;
	int Flend{};
	string inStr;
	while (true)
	{
		if (str.find("\\", Flend)==std::string::npos)
		{
			break;
		}
		Flend = str.find("\\", Flend);
	};
	inStr=str.substr(Flend,str.length()-Flend+4 );
	std::cout << inStr;
}

麟江湖游戏设计

image-20221024142752818

#include <iostream>
int GetStrLength(const char str[])//计算字符串长度
{
    int length{};
    for (int i{}; str[i]; i++)
    {
        length++;
    }
    return length;
}
char* GetStrFind(const char str[],const char strA[])//在目标字符串中查找返回指针,查找不到返回空指针
{
    for(int x{};str[x];x++)
    {
        if (str[x] == strA[0])
        {
            bool bfind = true;
            int i{};
            for (i; strA[i]; i++)
            {
                if (str[i + x] != strA[i])
                {
                    bfind = false;
                    break;
                }
            }
            if (bfind) return (char*)&str[i + x];
        }
    }
    return nullptr;
}
void OutStr(const char* stra,const char* strb,const char* strc)//输出结果
{
    std::cout << "账号:" << stra;
    std::cout << "密码:" << strb;
    std::cout << "国家:" << strc;
}
int main(unsigned round,char* arm[])
{
    char* id{}, * pass{}, * country{};
    std::cout << arm[1];
    for (int i{}; arm[i]; i++)
    {
        if (id == nullptr)
        {
            id = GetStrFind(arm[i], "id=");
            if (id != nullptr) continue;
        }
        if (pass == nullptr)
        {
            pass = GetStrFind(arm[i], "pass=");
            if (pass != nullptr) continue;
        }
        if (country == nullptr)
        {
            country = GetStrFind(arm[i], "country=");
            if (country != nullptr) continue;
        }
    }
    if ((int)id * (int)pass * (int)country)
    {
        OutStr(id, pass, country);
    }
    else
    {
        std::cout << "请使用命令的方式调用本程序!";
    }
}

函数的底层知识

函数参数:不定量参数

image-20221024152459985

#include <iostream>
#include<cstdarg>
int Add(unsigned count, ...)
{
    int rt{};
    char* c_arg;
    va_start(c_arg, count);//讲参数数据指针赋值给c_arg
    for (int i{}; i < count;i++) rt += va_arg(c_arg, int);
    va_end(c_arg);
    return rt;
}
int main()
{
    std::cout << Add(5, 1, 2, 3, 4, 5);
}

之前的不定量参数是main函数是通过数组实现的,但值个不定量参数就不再是通过数组实现了,因为它的每一个参数都可以是任意的数据类型,那就看看它在内存中的表现

int Add(unsigned count, ...)
{
    int rt{};
    char* c_arg;
    va_start(c_arg, count);
    for (int i{}; i < count; i++)
    {
        std::cout << (int)c_arg<<std::endl;
        rt += va_arg(c_arg, int);
    }

    va_end(c_arg);
    return rt;
}
int main()
{
    std::cout << Add(5, 1, 2, 3, 4, 5);
}

image-20221024195118137

很直观就能从数据中发现写什么首先有一点是可以肯定的那就是这些数据在内存中依旧是连续的,而且之间的间隔为8那也就是说其实是为每个数据准备了8个字节的位置。从这里我的一切疑虑其实就都迎刃而解了

首先这里的不定量参数跟main函数通过数组实现不同是肯定的。那么假设我来设计,在提前确定数据的数据类型的情况下会怎么做呢。肯定要先将数据放到一个生成的内存空间中,但生成内存空间该生成多大合适呢,很明显给每个参数8个字节的空间,这样既做到了不大可能出现空间不够的情况,因为大多数数据类型都为8个字节及以内,只有long long类型超出,还兼顾省内存。

可是那读取数据又要如何呢,是通过va_start()函数来让数据以想要的数据类型输出出来

函数返回--返回指针和引用

函数参数:右值引用

image-20221025140334104

image-20221025140340901

函数的本质

学习到现在已经知道了结构体变量参数数组指针等,它们本质都是放在内存中的一串数据,那么函数呢,无论是main函数,还是C/C++自己封装的函数,以及自己写的函数,它们都是什么呢,在计算机中究竟是以何种方式存在呢。

image-20221025144503977

int Add(int a, int b)
{
00671002  in          al,dx  
    return a + b;
00671003  mov         eax,dword ptr [ebp+8]  
00671006  add         eax,dword ptr [ebp+0Ch]  
}
00671009  pop         ebp  
0067100A  ret  
int main()
{
00671010  push        ebp  
00671011  mov         ebp,esp  
00671013  push        ecx  
    int c;
    c=Add(1, 2);
00671014  push        2  
00671016  push        1  
00671018  call        00671000  
0067101D  add         esp,8  
00671020  mov         dword ptr [ebp-4],eax  

}
00671023  xor         eax,eax  
00671025  mov         esp,ebp  
00671027  pop         ebp  
00671028  ret  

遇事不决反汇编那就先从main函数看起程序也不都从这里开始执行的

    c=Add(1, 2);
00671014  push        2  
00671016  push        1  
00671018  call        00671000  
0067101D  add         esp,8  
00671020  mov         dword ptr [ebp-4],eax  

这里其实展示了调用函数在汇编上的体现,push执行是将数据推入栈中。

那么push 1 push 2这不就是将给所调用函数的参数放入栈中嘛,然后call 00671000 call指令有点像jmp指令意思就是跳到内存地址00671000来执行,那00671000这个位置是哪呢,很明显就能发现这个位置就是Add函数的位置,那我就可以理解为call让程序跳到函数的位置从而开始执行函数

int Add(int a, int b)
{
00671002  in          al,dx  
    return a + b;
00671003  mov         eax,dword ptr [ebp+8]  
00671006  add         eax,dword ptr [ebp+0Ch]  
}
00671009  pop         ebp  
0067100A  ret  

mov eax,dword ptr [ebp+8]

add eax,dword ptr [ebp+0Ch]

这两句是将[ebp+8][ebp+C]两个值相加放到eax中那也就说明这两个地址中放着给的两个参数,计算结果在eax中保存。

ret执行完函数返回到main函数接着执行

0067101D  add         esp,8  
00671020  mov         dword ptr [ebp-4],eax

mov dword ptr [ebp-4],eax把计算的结果放到[ebp-4]中

00671002 EC                   in          al,dx  
    return a + b;
00671003 8B 45 08             mov         eax,dword ptr [a]  
00671006 03 45 0C             add         eax,dword ptr [b]  
}
00671009 5D                   pop         ebp  
0067100A C3                   ret  

汇编指令是方便人来读的,可是计算机是读不懂这些的,它还需要翻译成机器能明白的0和1的数据

call 00671000 跳转到一个内存地址中,这个内存中放着已经编译的数据。

00671018 E8 E3 FF FF FF       call        Add (0671000h)

再看这句汇编指令,在想想为什么调用函数后能跳转到函数所在的内存位置,能指向内存的好像指针就能做到,那我可不可以吧函数名理解为一个指针

image-20221025204826211

事实证明没错就是指针

函数指针

image-20221025155112452

从函数的角度彻底认识栈

image-20221026083056160

栈本质上也是一段连续的内存空间,用来存放一些临时变量,但问题来了栈是如何做到在变量的生命周期接受后被它也同步的呢

首先先要知道栈是怎么运行的,在汇编中都有看到esp,ebp等这其实就是CPU的寄存器

EAX:(针对操作数和结果数据的)累加器 ,返回函数结果

EBX:(DS段中的数据指针)基址寄存器

ECX:(字符串和循环操作数)计数器

EDX:(I/O指针)数据寄存器

EBP:(SS段中栈内数据指针)扩展基址指针寄存器

ESI:(字符串操作源指针)源变址寄存器

EDI:(字符串操作目标指针)目的变址寄存器

ESP:(SS段中栈指针)栈指针寄存器,其内存放着一个指针,该指针永远指向系统堆栈最上面一个栈帧的栈顶

EIP:指令存储器,用来存储CPU要读取指令的地址,CPU通过指令寄存器读取即将要执行的指令。简单点说就是程序下一步将执行到指令的地址

eax用来返回函数结果这点其实已经见识过了,接下来就来看汇编,因为是在看完整篇函数后产出的内容,因此对函数的认识会更加深刻

首先计算机是不懂C\C++等这些高级编程语言的,但为什么写的程序可以被计算机执行呢,这中间就需要编译器在中间传达,把想要的操作以计算机能听懂的方式转达给计算机,从而被执行。

也就是说代码其实本质上还是给编译器看的,编译器读我们写的代码是从main函数开始,中间程序中调用了函数,那么就会跳到函数所在位置接着执行。但如果定义了函数但并没有调用,其实编译器就不会管,这一点从反汇编来看就很直观。

说完这些就开始进入今天的主题 栈

int A(int a)
{
007B1002 EC                   in          al,dx  
007B1003 83 EC 24             sub         esp,24h  
    int b[]{1,2,3,4,5,6,7,8,9};
007B1006 C7 45 DC 01 00 00 00 mov         dword ptr [ebp-24h],1  
007B100D C7 45 E0 02 00 00 00 mov         dword ptr [ebp-20h],2  
007B1014 C7 45 E4 03 00 00 00 mov         dword ptr [ebp-1Ch],3  
007B101B C7 45 E8 04 00 00 00 mov         dword ptr [ebp-18h],4  
007B1022 C7 45 EC 05 00 00 00 mov         dword ptr [ebp-14h],5  
007B1029 C7 45 F0 06 00 00 00 mov         dword ptr [ebp-10h],6  
007B1030 C7 45 F4 07 00 00 00 mov         dword ptr [ebp-0Ch],7  
007B1037 C7 45 F8 08 00 00 00 mov         dword ptr [ebp-8],8  
007B103E C7 45 FC 09 00 00 00 mov         dword ptr [ebp-4],9  
    return b[a];
007B1045 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B1048 8B 44 85 DC          mov         eax,dword ptr [ebp+eax*4-24h]  
}
007B104C 8B E5                mov         esp,ebp  
007B104E 5D                   pop         ebp  
007B104F C3                   ret  
int Ave(int a, int b)
{
007B1050 55                   push        ebp  
007B1051 8B EC                mov         ebp,esp  
    a += 50;
007B1053 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B1056 83 C0 32             add         eax,32h  
007B1059 89 45 08             mov         dword ptr [ebp+8],eax  
    return a + b;
007B105C 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B105F 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
}
007B1062 5D                   pop         ebp  
007B1063 C3                   ret  
int Add(int a, int b)
{
007B1070 55                   push        ebp  
007B1071 8B EC                mov         ebp,esp  
007B1073 83 EC 08             sub         esp,8  
    int c = 250;
007B1076 C7 45 FC FA 00 00 00 mov         dword ptr [ebp-4],0FAh  
    int d = Ave(a, b);
007B107D 8B 45 0C             mov         eax,dword ptr [ebp+0Ch]  
007B1080 50                   push        eax  
007B1081 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
007B1084 51                   push        ecx  
007B1085 E8 C6 FF FF FF       call        007B1050  
007B108A 83 C4 08             add         esp,8  
007B108D 89 45 F8             mov         dword ptr [ebp-8],eax  
    c += b;
007B1090 8B 55 FC             mov         edx,dword ptr [ebp-4]  
007B1093 03 55 0C             add         edx,dword ptr [ebp+0Ch]  
007B1096 89 55 FC             mov         dword ptr [ebp-4],edx  
    return c;
007B1099 8B 45 FC             mov         eax,dword ptr [ebp-4]  
}
007B109C 8B E5                mov         esp,ebp  
007B109E 5D                   pop         ebp  
007B109F C3                   ret   
int main()
{
007B10A0 55                   push        ebp  
007B10A1 8B EC                mov         ebp,esp  
007B10A3 83 EC 08             sub         esp,8  
    std::cout << Add;
007B10A6 68 70 10 7B 00       push        7B1070h  
007B10AB 8B 0D 38 20 7B 00    mov         ecx,dword ptr ds:[007B2038h]  
007B10B1 FF 15 34 20 7B 00    call        dword ptr ds:[007B2034h]  
    system("pause");
007B10B7 68 08 21 7B 00       push        7B2108h  
007B10BC FF 15 A8 20 7B 00    call        dword ptr ds:[007B20A8h]  
007B10C2 83 C4 04             add         esp,4  
    int x = Add(250, 50);
007B10C5 6A 32                push        32h  
007B10C7 68 FA 00 00 00       push        0FAh  
007B10CC E8 9F FF FF FF       call        007B1070  
007B10D1 83 C4 08             add         esp,8  
007B10D4 89 45 FC             mov         dword ptr [ebp-4],eax  
    int e = { A(5) };
007B10D7 6A 05                push        5  
007B10D9 E8 22 FF FF FF       call        007B1000  
007B10DE 83 C4 04             add         esp,4  
007B10E1 89 45 F8             mov         dword ptr [ebp-8],eax  
}
007B10E4 33 C0                xor         eax,eax  
007B10E6 8B E5                mov         esp,ebp  
007B10E8 5D                   pop         ebp  
007B10E9 C3                   ret  
int x = Add(250, 50);
007B10C5 6A 32                push        32h  
007B10C7 68 FA 00 00 00       push        0FAh  
007B10CC E8 9F FF FF FF       call        007B1070  
007B10D1 83 C4 08             add         esp,8  
007B10D4 89 45 FC             mov         dword ptr [ebp-4],eax  

从这里来看函数调用的过程在汇编上是如何体现的

007B10C5 6A 32                push        32h  
007B10C7 68 FA 00 00 00       push        0FAh  

通过两个push将参数打入到堆栈中,至于在堆栈中的位置其实是由当时esp决定的,因为esp是堆栈顶指针,esp减少多少其实也就意味着这些内存已经被使用了

007B10CC E8 9F FF FF FF       call        007B1070 

int Add(int a, int b)
{
007B1070 55                   push        ebp  
007B1071 8B EC                mov         ebp,esp  
007B1073 83 EC 08             sub         esp,8  
    int c = 250;
007B1076 C7 45 FC FA 00 00 00 mov         dword ptr [ebp-4],0FAh  
    int d = Ave(a, b);
007B107D 8B 45 0C             mov         eax,dword ptr [ebp+0Ch]  
007B1080 50                   push        eax  
007B1081 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
007B1084 51                   push        ecx  
007B1085 E8 C6 FF FF FF       call        007B1050  
007B108A 83 C4 08             add         esp,8  
007B108D 89 45 F8             mov         dword ptr [ebp-8],eax  
    c += b;
007B1090 8B 55 FC             mov         edx,dword ptr [ebp-4]  
007B1093 03 55 0C             add         edx,dword ptr [ebp+0Ch]  
007B1096 89 55 FC             mov         dword ptr [ebp-4],edx  
    return c;
007B1099 8B 45 FC             mov         eax,dword ptr [ebp-4]  
}
007B109C 8B E5                mov         esp,ebp  
007B109E 5D                   pop         ebp  
007B109F C3                   ret  

call指令可以拆分理解为push 下一行指令的位置,jmp 007B1070

007B1070这是哪呢,可以看到其实就是Add函数头的位置,也就是计算机跳到此处执行了,push ebp,将esp值赋值给ebp,再对esp-8,这其实也意味着这8个字节已经被使用了,给谁用呢,这其实可以理解,这个函数就是两个int类型的参数。

007B1070 55                   push        ebp  
007B1071 8B EC                mov         ebp,esp  
007B1073 83 EC 08             sub         esp,8  
    int c = 250;
007B1076 C7 45 FC FA 00 00 00 mov         dword ptr [ebp-4],0FAh  

看到赋值了还记得ebp中是什么吗,是没对这个函数临时变量分配内容前的地址,也就可以通过ebp来控制向那块内存写数据,这也只是猜想接着看

此时堆栈中有多少数据了呢

012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  
    int d = Ave(a, b);
007B107D 8B 45 0C             mov         eax,dword ptr [ebp+0Ch]	//此时[ebp+0Ch]其实就是a
007B1080 50                   push        eax  
007B1081 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
007B1084 51                   push        ecx  
007B1085 E8 C6 FF FF FF       call        007B1050  
007B108A 83 C4 08             add         esp,8  
007B108D 89 45 F8             mov         dword ptr [ebp-8],eax  

mov eax,dword ptr [ebp+0Ch] //此时[ebp+0Ch]其实就是a
push eax
mov ecx,dword ptr [ebp+8]
push ecx

这几句指令也就向栈中读了数据,到这那我就有问题了,ebp加减之间读取的数据之间是否存在区别呢,万事不经推敲,想想还真存在区别,减呢代表其实是这个函数内的临时变量,可向下这是超出函数外的变量。

call 007B1050 进入Ave函数下面是到此栈中存在的数据

012FFAB4  00E9108A  返回到 20.4.Add+1A 自 20.4.int __cdecl Ave(int, int)
012FFAB8  000000FA  
012FFABC  00000032  
012FFAC0  00E92108  20.4.GS_ExceptionPointers+8
012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  
int Ave(int a, int b)
{
007B1050 55                   push        ebp  
007B1051 8B EC                mov         ebp,esp  
    a += 50;
007B1053 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B1056 83 C0 32             add         eax,32h  
007B1059 89 45 08             mov         dword ptr [ebp+8],eax  
    return a + b;
007B105C 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B105F 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
}
007B1062 5D                   pop         ebp  
007B1063 C3                   ret  

又出现了先保存ebp的内容,将esp赋值给ebp,那么这是跳转到函数头后都会做的准备工作吗

但有一点就是为什么没有为Ave函数的参数准备内存,其实它的两个参数都是main函数中的a,b编译器很智能发现已经给你保存了,那告诉计算机你直接到这来拿吧

push        ebp  
mov         ebp,esp  
012FFAB0  012FFAC8	  
012FFAB4  00E9108A  返回到 20.4.Add+1A 自 20.4.int __cdecl Ave(int, int)
012FFAB8  0000012C  
012FFABC  00000032  
012FFAC0  00E92108  20.4.GS_ExceptionPointers+8
012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  
	return a + b;
007B105C 8B 45 08             mov         eax,dword ptr [ebp+8]  
007B105F 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
}
007B1062 5D                   pop         ebp  
007B1063 C3                   ret  

执行完后return 发现结果比没有想象中的通过栈传递,而是结果放在了eax中传递

接下来关键的来了

pop ebp

pop指令将栈顶的内容弹出赋值给ebp

012FFAB4  00E9108A  返回到 20.4.Add+1A 自 20.4.int __cdecl Ave(int, int)
012FFAB8  0000012C  
012FFABC  00000032  
012FFAC0  00E92108  20.4.GS_ExceptionPointers+8
012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  

ret

ret指令相当于pop eip,eip为cup将执行的地方

012FFAB8  0000012C  
012FFABC  00000032  
012FFAC0  00E92108  20.4.GS_ExceptionPointers+8
012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  
007B108A 83 C4 08             add         esp,8  
007B108D 89 45 F8             mov         dword ptr [ebp-8],eax  
    c += b;
007B1090 8B 55 FC             mov         edx,dword ptr [ebp-4]  
007B1093 03 55 0C             add         edx,dword ptr [ebp+0Ch]  
007B1096 89 55 FC             mov         dword ptr [ebp-4],edx  
    return c;
007B1099 8B 45 FC             mov         eax,dword ptr [ebp-4]  
}
007B109C 8B E5                mov         esp,ebp  
007B109E 5D                   pop         ebp  
007B109F C3                   ret  

add esp,8 对esp+8也就抹去了之前为Ave函数传递参数时的空间

012FFAC0  00E92108  20.4.GS_ExceptionPointers+8
012FFAC4  000000FA  
012FFAC8  012FFAE0  
012FFACC  00E910D1  返回到 20.4.main+31 自 20.4.int __cdecl Add(int, int)
012FFAD0  000000FA  
012FFAD4  00000032  

再来看函数return返回

	return c;
007B1099 8B 45 FC             mov         eax,dword ptr [ebp-4]  
}
007B109C 8B E5                mov         esp,ebp  
007B109E 5D                   pop         ebp  
007B109F C3                   ret  

依旧时通过eax存放函数的返回值

mov esp,ebp 执行完esp=012FFAC8,其实也就是栈回到什么时候的状态呢,这里我思考了很久,发现这里的门道让我觉得设计栈的工程师真是天才。

首先我们来捋一遍每次调用一个函数都通过一个call指令,保存下一条指令的地址,跳转到目标函数头的地址去执行,push ebp 如果函数内参数需要内存放临时变量,则通过sub对esp进行减法运算,执行完,通过mov sep,ebp首先如果函数内不调用其他函数的情况下ebp是不会发生改变的,就算调用了其他函数,也可以通过

push ebp保存进入函数前的状态

mov esp,ebp退回到进入函数之前栈的状态,同时函数中临时变量在栈中也就消失了

pop ebp轮回就开始了通过mov此时栈顶的数据就是之前push上来的ebp,至此函数结束ebp恢复到原来的状态,临时变量也消失。

小结:通过esp和ebp实现执行函数后栈也就恢复进call之前的样子,函数返回值通过eax传递

屠驴

#include <iostream>
#include <iomanip>

void Hack()
{
	unsigned long long x = 0;
	for (int i = 0; true; i++)
	{
		if (i % 100000000 == 0)
		{
			system("cls");
			std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";
			std::cout << "\n 你的系统已经被我们拿下! hacked by 黑兔档案局:[ID:000001 ]\n";
			std::cout << "\n              加群:868267304 解除\n";
			std::cout << "\n\\>正在传输硬盘数据....已经传输" << x++ << "个文件......\n\n";

			std::cout << std::setfill('>')<< std::setw(x % 60) << "\n";

			std::cout << "\n\\>摄像头已启动!<==============\n\n";

			std::cout << std::setfill('#') << std::setw(x % 60) << "\n";

			std::cout << "\n\\>数据传输完成后将启动自毁程序!CPU将会温度提升到200摄氏度\n";

			std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";
		}
	}
}

int GetAge()
{
	int rt;
	std::cout << "请输入学员的年龄:";
	std::cin >> rt;
	return rt;
}

int count()
{
	int i{};
	int total{};
	int age[10]{};
	do
	{
		age[i] = GetAge();
		total += age[i];
		//将AGE[I]保存到数据库中
	} while (age[i++]);
	return total;
}

int main()
{
	std::cout << "======= 驴百万学院 学员总年龄统计计算系统 =====\n";
	std::cout << "\n                API:"<<Hack<<std::endl;
	std::cout << "\n[说明:最多输入10个学员的信息,当输入0时代表输入结束]\n\n";
	std::cout << "\n驴百万学院的学员总年龄为:" << count();
}

带着问题找答案先从代码看起吧,从main函数作为切入点main函数调用了count()函数

int GetAge()
{
	int rt;
	std::cout << "请输入学员的年龄:";
	std::cin >> rt;
	return rt;
}

int count()
{
	int i{};
	int total{};
	int age[10]{};
	do
	{
		age[i] = GetAge();
		total += age[i];
		//将AGE[I]保存到数据库中
	} while (age[i++]);
	return total;
}

这里就已经发现问题了,count函数大概就是通过do while向age数组写入数据,但它使用的是原生数组,虽然初始化时定义了10个元素,原生数组比如age[]其实还是通过age指针在加通过数据类型算出来的偏移量,从而确定要找的或要往哪里写入。但它这里判断的条件是age[i]==0这肯定是存在问题的,也就是说原则上只要不输入0我可以一直往里面写入数据

接下来看汇编

int GetAge()
{
00C011D0  push        ebp  
00C011D1  mov         ebp,esp  
00C011D3  push        ecx  
	int rt;
	std::cout << "请输入学员的年龄:";
00C011D4  push        0C032E8h  
00C011D9  mov         eax,dword ptr [__imp_std::cout (0C0308Ch)]  
00C011DE  push        eax  
00C011DF  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C011E4  add         esp,8  
	std::cin >> rt;
00C011E7  lea         ecx,[rt]  
00C011EA  push        ecx  
00C011EB  mov         ecx,dword ptr [__imp_std::cin (0C03084h)]  
00C011F1  call        dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0C03050h)]  
	return rt;
00C011F7  mov         eax,dword ptr [rt]  
}
00C011FA  mov         esp,ebp  
00C011FC  pop         ebp  
00C011FD  ret  
int count()
{
00C01200  push        ebp  
00C01201  mov         ebp,esp  
00C01203  sub         esp,34h  
	int i{};
00C01206  mov         dword ptr [i],0  
	int total{};
00C0120D  mov         dword ptr [total],0  
	int age[10]{};
00C01214  xor         eax,eax  
00C01216  mov         dword ptr [age],eax  
00C01219  mov         dword ptr [ebp-30h],eax  
00C0121C  mov         dword ptr [ebp-2Ch],eax  
00C0121F  mov         dword ptr [ebp-28h],eax  
00C01222  mov         dword ptr [ebp-24h],eax  
00C01225  mov         dword ptr [ebp-20h],eax  
00C01228  mov         dword ptr [ebp-1Ch],eax  
00C0122B  mov         dword ptr [ebp-18h],eax  
00C0122E  mov         dword ptr [ebp-14h],eax  
00C01231  mov         dword ptr [ebp-10h],eax  
	do
	{
		age[i] = GetAge();
00C01234  call        GetAge (0C011D0h)  
00C01239  mov         ecx,dword ptr [i]  
00C0123C  mov         dword ptr age[ecx*4],eax  
		total += age[i];
00C01240  mov         edx,dword ptr [i]  
00C01243  mov         eax,dword ptr [total]  
00C01246  add         eax,dword ptr age[edx*4]  
00C0124A  mov         dword ptr [total],eax  
		//将AGE[I]保存到数据库中
	} while (age[i++]);
00C0124D  mov         ecx,dword ptr [i]  
00C01250  mov         edx,dword ptr age[ecx*4]  
00C01254  mov         dword ptr [ebp-0Ch],edx  
00C01257  mov         eax,dword ptr [i]  
00C0125A  add         eax,1  
00C0125D  mov         dword ptr [i],eax  
00C01260  cmp         dword ptr [ebp-0Ch],0  
00C01264  jne         count+34h (0C01234h)  
	return total;
00C01266  mov         eax,dword ptr [total]  
}
00C01269  mov         esp,ebp  
00C0126B  pop         ebp  
00C0126C  ret  
int main()
{
00C01270  push        ebp  
00C01271  mov         ebp,esp  
	std::cout << "======= 驴百万学院 学员总年龄统计计算系统 =====\n";
00C01273  push        0C032FCh  
00C01278  mov         eax,dword ptr [__imp_std::cout (0C0308Ch)]  
00C0127D  push        eax  
00C0127E  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C01283  add         esp,8  
	std::cout << "\n                API:"<<Hack<<std::endl;
00C01286  push        offset std::endl<char,std::char_traits<char> > (0C01900h)  
00C0128B  push        offset Hack (0C01000h)  
00C01290  push        0C03330h  
00C01295  mov         ecx,dword ptr [__imp_std::cout (0C0308Ch)]  
00C0129B  push        ecx  
00C0129C  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C012A1  add         esp,8  
00C012A4  mov         ecx,eax  
00C012A6  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C0304Ch)]  
00C012AC  mov         ecx,eax  
00C012AE  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C03040h)]  
	std::cout << "\n[说明:最多输入10个学员的信息,当输入0时代表输入结束]\n\n";
00C012B4  push        0C03348h  
00C012B9  mov         edx,dword ptr [__imp_std::cout (0C0308Ch)]  
00C012BF  push        edx  
00C012C0  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C012C5  add         esp,8  
	std::cout << "\n驴百万学院的学员总年龄为:" << count();
00C012C8  call        count (0C01200h)  
00C012CD  push        eax  
00C012CE  push        0C03380h  
00C012D3  mov         eax,dword ptr [__imp_std::cout (0C0308Ch)]  
00C012D8  push        eax  
00C012D9  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C012DE  add         esp,8  
00C012E1  mov         ecx,eax  
00C012E3  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C03044h)]  
}
00C012E9  xor         eax,eax  
00C012EB  pop         ebp  
00C012EC  ret  
int GetAge()
{
00C011D0  push        ebp  
00C011D1  mov         ebp,esp  
00C011D3  push        ecx  
	int rt;
	std::cout << "请输入学员的年龄:";
00C011D4  push        0C032E8h  
00C011D9  mov         eax,dword ptr [__imp_std::cout (0C0308Ch)]  
00C011DE  push        eax  
00C011DF  call        std::operator<<<std::char_traits<char> > (0C01570h)  
00C011E4  add         esp,8  
	std::cin >> rt;
00C011E7  lea         ecx,[rt]  
00C011EA  push        ecx  
00C011EB  mov         ecx,dword ptr [__imp_std::cin (0C03084h)]  
00C011F1  call        dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0C03050h)]  
	return rt;
00C011F7  mov         eax,dword ptr [rt]  
}
00C011FA  mov         esp,ebp  
00C011FC  pop         ebp  
00C011FD  ret  
int count()
{
00C01200  push        ebp  
00C01201  mov         ebp,esp  
00C01203  sub         esp,34h  
	int i{};
00C01206  mov         dword ptr [i],0  
	int total{};
00C0120D  mov         dword ptr [total],0  
	int age[10]{};
00C01214  xor         eax,eax  
00C01216  mov         dword ptr [age],eax  
00C01219  mov         dword ptr [ebp-30h],eax  
00C0121C  mov         dword ptr [ebp-2Ch],eax  
00C0121F  mov         dword ptr [ebp-28h],eax  
00C01222  mov         dword ptr [ebp-24h],eax  
00C01225  mov         dword ptr [ebp-20h],eax  
00C01228  mov         dword ptr [ebp-1Ch],eax  
00C0122B  mov         dword ptr [ebp-18h],eax  
00C0122E  mov         dword ptr [ebp-14h],eax  
00C01231  mov         dword ptr [ebp-10h],eax  
	do
	{
		age[i] = GetAge();
00C01234  call        GetAge (0C011D0h)  
00C01239  mov         ecx,dword ptr [i]  
00C0123C  mov         dword ptr age[ecx*4],eax  
		total += age[i];
00C01240  mov         edx,dword ptr [i]  
00C01243  mov         eax,dword ptr [total]  
00C01246  add         eax,dword ptr age[edx*4]  
00C0124A  mov         dword ptr [total],eax  
		//将AGE[I]保存到数据库中
	} while (age[i++]);
00C0124D  mov         ecx,dword ptr [i]  
00C01250  mov         edx,dword ptr age[ecx*4]  
00C01254  mov         dword ptr [ebp-0Ch],edx  
00C01257  mov         eax,dword ptr [i]  
00C0125A  add         eax,1  
00C0125D  mov         dword ptr [i],eax  
00C01260  cmp         dword ptr [ebp-0Ch],0  
00C01264  jne         count+34h (0C01234h)  
	return total;
00C01266  mov         eax,dword ptr [total]  
}
00C01269  mov         esp,ebp  
00C0126B  pop         ebp  
00C0126C  ret  

首先要明确的是要想达成栈溢出实现执行Hack函数,需要知道修改哪里

00C012E3  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C03044h)]  

call指令会将下一行的内存地址打入[esp]也就是栈中然后跳转去执行目标地址,再通过ret跳转回来,那么如果我想让程序去调用Hack函数,就需要通过数组向栈中写入数据,达到栈溢出从而修改call指令打入栈中的值,要实现这样的效果就需要先知道数组的首元素距离这个值有多远。

00C012E3  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C03044h)]  
push        ebp
sub         esp,34h

从这些值就可以知道距离它28个字节,确定了距离接下来就要知道数组在哪里了

	int age[10]{};
00C01214  xor         eax,eax  
00C01216  mov         dword ptr [age],eax  
00C01219  mov         dword ptr [ebp-30h],eax  
00C0121C  mov         dword ptr [ebp-2Ch],eax  
00C0121F  mov         dword ptr [ebp-28h],eax  
00C01222  mov         dword ptr [ebp-24h],eax  
00C01225  mov         dword ptr [ebp-20h],eax  
00C01228  mov         dword ptr [ebp-1Ch],eax  
00C0122B  mov         dword ptr [ebp-18h],eax  
00C0122E  mov         dword ptr [ebp-14h],eax  
00C01231  mov         dword ptr [ebp-10h],eax  

可以看出数组首元素好像就在[esp]也就是栈顶指针所在位置

首先如果用数组表达要修改的返回地址就是age[14]

10	i=0		
10	i=1
10	i=2
10	i=3
10	i=4
10	i=5
10	i=6
10	i=7
10	i=8	
10	i=9
//下面就是溢出的数值
10	i=10
13	i=11	total	
13	i=12	i	i++	14
ebp	i=13	
4001792	i=14	i++	16
0	i=15

因为是通过i来控制偏移量,那么就会产生一个问题,修改之前的值都不会有问题,但我修改到i在栈中的位置了,那会产生什么情况

这里就需要谨慎了,因为退出循环的条件是age[i]==0然后对i++,那么如果修改到i的值时改为13,age[13]为ebp肯定不为0不退出循环,i++后i=14那么就可以修改目标的值了修改为12587008也就是Hack函数的函数头的位置,i++,i=15此时赋值为0结束循环,去执行Hack函数

image-20221026105350073

成功执行

函数重载与函数模版

函数重载

image-20221026131344312

函数模版

image-20221026133607436

image-20221026133822855

image-20221026134443780

image-20221026134938774

template<typename type1>
type1 ave(type1 a, type1 b)
{
    return a + b;
}

函数模版和重载

image-20221026142412242

image-20221026153410828

image-20221026153435742

auto->decltype

auto

image-20221026143751022

image-20221026144223817

decltype

image-20221026151913631

image-20221026151938835

image-20221026152016606

image-20221026152039392

image-20221026152110184

int& Add(int& a, int& b)
{
    std::cout << "123";
    return a > b ? a : b;
}
int main()
{
    int a{}, b{};
    auto c=Add(a,b);
    decltype(Add(a, b)) x=a;//x为int类型引用
}

image-20221026152839604

auto->decltype

image-20221026152130858

image-20221026151724914

decltype(auto) Add(int& a, int& b)
{
    std::cout << "123";
    return a > b ? a : b;
}

auto和decltype存在明显的区别

推断函数模版返回类型

image-20221026155224532

函数模版参数

image-20221026180450916

image-20221026183310979

image-20221026184601958

image-20221026185626711

template<typename T,int count>
T ave(const T(&ary)[count])//这种方式定义的函数模版能自己根据提供的数组确定元素
{
	T all{};
	for (int i{}; i < count; i++)
		all += ary[i];
	return all / count;
}
int main()
{
	int ary[]{1,2,3,4,5};
	std::cout << ave(ary);
}

函数模版的本质

C\C++联合编程

static和inline

image-20221027101803187

静态变量的生命周期跟全局变量的生命周期,当在函数中声明,临时变量会随着函数结束而结束,但静态变量不会,但它也同时只允许这函数去访问,其实这也就是说静态变量在内存中有固定的内存,因此也就不会像在栈中的临时变量一样。

image-20221027103538704

通过inline声明一个内联函数,建议编译器将函数处理成内联代码提升性能,但建议终归是建议听不听还是编译器考量决定。

int a{ Add(1,2) };
	std::cout << a;
00601000 8B 0D 38 20 60 00    mov         ecx,dword ptr ds:[00602038h]  
00601006 6A 03                push        3  
00601008 FF 15 34 20 60 00    call        dword ptr ds:[00602034h]  
}

当然现在的编译器很智能能自己对代码进行优化

从编译器的角度理解定义和声明

image-20221027195038820

image-20221027204507169

image-20221027212409166

首先要知道的是计算机是只懂0和1组成的机器码的,通过它来知道自己做什么样的操作,但问题来了写的C\C++都不是它能懂的,那么就需要编译器,把写出来的代码就像文章一样告诉编译器,我要做什么你帮我翻译给计算机让它去做。那么写代码其实本质上是跟编译器去沟通,那说的话就要符合的的规则,毕竟这样才能准确的表达要做什么

那么声明其实也就是告诉编译器的,比如说我跟小明说小红明天要来你准备一下,那小明问小红是谁啊

告诉小明就是声明,那么谁是小红则是定义的过程,这里定义当然不仅仅是函数,也可以是变量

编译器在读代码的过程中很可能会出现多个代码嵌套的情况,但如果这个代码中出现编译器还没见过的那它如何认识又谈何编译呢,所以就告诉编译器,这里的函数其实返回值参数类型等信息,后面会把详细信息给你

毕竟其实对于编译器来说最主要的其实是它的返回值,函数参数,通过之前的学习其实结合起来也就明白了

image-20221027212451748

在调用一个函数首先要将给函数的参数打入栈中,这是就涉及到参数的个数是多少,类型分别是什么,然后才是call指令到函数的地址执行,执行完后ret指令回来接着执行,函数结果在eax中,那么如果处理这eax的值

这样看其实对编译器来说函数的内容就没这么重要了,先告诉我它的返回值,参数情况,至于具体情况我通过call过去就好了

image-20221027212507923

源文件和头文件

C++的源文件文件后缀为cpp,C的源文件后缀为c

image-20221028094146943

image-20221028094214474

那么当main函数调用一个函数,但此函数并不在本源文件而是在另一个文件

//a.cpp
int Add(int a, int b)
{
	//std::cout<<a;
	return a + b;
}
//class.cpp
#include <iostream>
int Add(int a, int b);//需要先有一个声明

int main()
{
    std::cout << Add(1,2);
}

如此看来编译器在读代码时是先到存在main函数的看起,如果不先建立声明,编译器不认识谁是Add那么又谈何编译呢,因此先要有个声明告诉编译器一些关键信息,让它做好准备,然后最后还是要到其他源文件中找Add函数。

image-20221027233734969

image-20221028094242668

image-20221028094313183

extern

image-20221028000915017

image-20221028001626528

image-20221028001802966

创建自己的sdk

image-20221028101135366

image-20221028101021865

image-20221028101047942

image-20221028101108972

创建项目类型

递归函数

递归函数是

从编译器角度理解函数

One Definition Rulr----单定义规则

image-20221028142755614

结合之前的知识来讲转换单元,首先编译器需要对源文件进行编译,每一个源文件都会编译成一个obj文件,也就是对象文件,里面的内容是将头文件和源文件和并就叫做转换单元,翻译成计算机能懂的机器码和转换单元的引用信息(不在转换单元中定义的对象)

然后会通过链接器将各个转换单元的对象文件链接起来,从而生成最终的程序

说到这里结合起来看,声明的意义就负责起对多个转换单元之间的对象文件串联的作用,所谓的引用信息多半是通过声明建立,在生成完obj文件后,在链接器开始串连整个程序中,在其他转换单元的对象文件中寻找之前声明的对象的定义

image-20221028143911688

未定义的行为其实还算挺容易理解的,不在C++标准中规定的行为,就是未定义的行为

image-20221028143905752

image-20221028163555238

内部连接属性:只在本转换单元有效

外部链接属性:在其他的转换单元同样有效

无连接属性:只在该名称的作用域内访问

从我个人的知识体系来表达

1.内部连接属性更像是局部变量 静态函数,静态变量等都是局部变量

2.外部链接属性则妥妥的全局 通过关键字extern来声明全局,函数也具备外部链接属性

3.无连接属性则是临时变量存放在栈中,具有生命周期

注:以上为个人此刻的间解,不排除以后随着学习的深入理解加深的可能性

#define

image-20221028172159539

通过#define设置别名本质上来说其实就是替换,将代码中所有A都替换成B

image-20221028165713971

通过#define定义常量对类型不做限制

image-20221028170142768

image-20221028172544313

image-20221028172518956


image-20221028223418559

namespace---命名空间

image-20221028223531018

image-20221028224958513

image-20221028225348752

//a.cpp
namespace T
{
	int b {10 };
}
//a.h
#pragma once
namespace T
{
	extern int b;//在命名空间里通过extern关键字声明一个变量
}
int Add(int a, int b);
//b.cpp
#include <iostream>
#include"标头.h"
namespace T
{
    int a;
}
int main()
{
    std::cout << T::a;
    std::cout << T::b;
}

image-20221028233432561

如果不在头文件建立一个声明告诉编译器建立一个链接,从而正常将b.cpp源文件编译成obj文件,通过执行的结果看在另一个源文件中定义的变量也放到了同一个命名空间中,这也就说明命名空间具备外部链接属性

已命名的命名空间可在多个转换单元中进行扩展,说明已命名的命名空间具备外部链接属性

image-20221028230147784

未命名的命名空间为内部链接属性,只能在当前转换单元有效。

预处理指令逻辑

预处理指令跟正常的代码区别在于,预处理指令是告诉编译器的而不会体现在汇编中

image-20221028233834258

#ifdef 如果定义了宏则执行

#ifndef 如果没有定义宏则执行

#else

#endif 跟

#elif 否则

#if如果

#define He 100
#ifdef He//如果定义了He则执行
void Test()
{
    return;
}
#else
#endif
#ifndef He
int a;
#elif 1

#endif
#if(He<1000)
void Add()
{
    return;
}
#else
int b{};
#endif 

预定义宏

image-20221029083601194

image-20221029083819235

image-20221029085538303

assert

image-20221029095043083

int a{};
std::cin>>a;
assert(a);
std::cout << 1000 / a;

image-20221029093257865

面向对象编程

OOP

image-20221029095447449

面向对象编程本质上是一种编程思想,C语言更多的是面向过程编程,但这也不意味着不能面向对象,只是会更加复杂。

image-20221029102111624

image-20221029103144391

定义的类中公有成员可以外部属性,可以被外部访问

私有成员只能在类内使用

成员函数

之前学习过的结构体,可以知道如何判断它在内存中所占空间,在结构体中定义变量,当然C++中也可以定义函数。但这是很自然就会想到一个问题。

结构体中定义变量很容易计算,可是定义函数又是如何一种情况呢。

通过之前学习其实就能知道函数确实也在内存中有属于它的内存中间,调用它就直接通过call跳转过去执行,执行结束回来。那么问题来了,定义在结构体中的函数需要开辟一个开辟一块空间放这个函数吗,很显然不需要,它只需要跟正常的函数一样编译,调用跳转过去就可以实现我的目的了。

类跟结构体很相似,它也是这样

class ROLE
{
private:
    int hpRcover;
    void Init();
public:
    int hp;
    int damage;
};
int main()
{
    std::cout << sizeof(ROLE);

image-20221029140452612

image-20221029131328332

image-20221029133921230

image-20221029134020029

//Role.cpp
#include "Role.h"
void Role::Act(Role& role)
{
    role.hp -= damage;
}
void Role::Init()
{
    hpRecover = 3;
}
Role* Role::bigger(Role* role)
{
   return role->lv > lv ? role : this;
}

Role& Role::SetLv(int newLv)
{
    lv = newLv;
    return *this;
}
Role& Role::SetHp(int newHp)
{
    hp = newHp;
    return *this;
}
Role& Role::SetDamage(int newDamage)
{
    damage = newDamage;
    return *this;
}
//Role.h
#pragma once
class Role
{
private:
    int hpRecover;
    void Init();
public:
    int hp;
    int damage;
    int lv{};
    void Act(Role& role);
    Role* bigger(Role* role);
    Role& SetLv(int newLv);
    Role& SetHp(int newHp);
    Role& SetDamage(int newDamage);
};
//main.cpp
#include <iostream>
#include"Role.h"
int main()
{
    std::cout << sizeof(Role);
    Role user;
    Role monster;
    user.SetLv(100).SetDamage(50).SetHp(500).bigger(&monster)->bigger(&user);
}

const

image-20221029145619573

image-20221029150122478

image-20221029150127219

image-20221029151815114

const对象不能改变

对象的const变量不能发生改变

const成员不量不能修改成员变量的值

const对象只能调用const函数

普通对象可以调用const函数

const成员函数函数类型需要与函数返回值一致

const对象只能调用const函数,普通对象却可以都可以调用

就可以利用函数重载来解决const对象和普通对象调用函数的问题

image-20221029154119791

构造函数

image-20221029155831087

image-20221029155836871

image-20221029160942219

image-20221029160959914

#include <iostream>
class Role
{
public:
	int Hp;
	int Lv;
	Role() = default;
	//Role() {};
	/*Role(int lv = 100) 
    {
		Lv = lv;        
    };*/	//默认参数的构造函数
	Role(int hp,int lv)
	{
		Hp = hp;
		Lv = lv;
	};//默认的构造函数
	Role& GetHp(int hp)
	{
		Role::Hp = hp;
		return *this;
	}
	Role& GetLv(int lv)
	{
		Lv = lv;
		return *this;
	}
private:
};

int main()
{
	Role role;
	Role user{1000,100};//通过自定义的构造函数完成初始化
	std::cout << user.Hp;
}

带默认参数的构造函数会出现错误为什么呢?

首先构造函数在类创建对象时会被自动调用,那么此时默认构造函数是Role()带默认参数的构造函数也是Role(),那么对编译器来说就会迷茫,到底该是执行哪一个函数,自然就会报错,如果调用时带参还会触发函数重载,但别忘了构造函数是创建时就自动调用了

image-20221029161010225

#include <iostream>
class T
{
public:
	int Hp;
	int Lv;
	T() = default;
	//T() {};
	T(int lv = 100)
	{
		Lv = lv;
	};
	T(int hp, int lv)
	{
		Hp = hp;
		Lv = lv;
	};
	T& GetHp(int hp)
	{
		T::Hp = hp;
		return *this;
	}
	T& GetLv(int lv)
	{
		Lv = lv;
		return *this;
	}
	bool GetBig(T role)
	{
		return Lv > role.Lv;
	}
private:
};

int main()
{
    T t;//如果这样进行初始化就会报错还是因为编译器无法分辨你调用的是哪个调用函数
	T user{1000,100};//通过自定义的构造函数完成初始化
	std::cout << user.Hp<<std::endl;
	std::cout << user.GetBig(50);

GeiBig是T类的成员函数它的参数是role给一个常量会发生什么样的事呢

换成参数是常见的数据类型给5的话就相当于将这个常量5赋值给这个变量

可这里会发生一件事就是对新的对象进行初始化,再配合构造函数就完成了初始化。

为了防止这种事的发生可以通过explicit关键字不让被标记的构造函数进行类型转换

深入理解构造函数

image-20221029193332444

image-20221030094628192

image-20221030094708079

image-20221030100502706

相同类的对象可以访问其私有变量,函数

#include<iostream>
class Hstring
{
public:
	Hstring()//无值初始化构造函数
	{
		str = new char[1] {};
	}
	Hstring(const unsigned strlength)
	{
		str = new char[strlength];
	}
	Hstring(const char* strA)//赋值构造
	{
		std::cout << "构造函数1"<<std::endl;
		length = Length(strA);
		delete[] str;
		str = new char[length] {};
		memcpy(str, strA, length);
	}
	/*Hstring(const Hstring& strA) :str{strA.str}
	{
		std::cout << "构造函数2"<<std::endl;
	}*/
	char* StrShow() const//输出字符串
	{
		return str;
	}
	void ChangeStr(const char* strA)
	{
		length = Length(strA);
		delete[] str;
		str = new char[length] {};
		memcpy(str, strA, length);
	}
private:
	unsigned short length{};
	char* str;
	unsigned short Length(const char* str)//计算字符串长度
	{
		unsigned short len{};
		while (str[len++]);
			return len;
	}
	void Memoryfree(char* str)
	{
		delete[] str;
	}
};
int main()
{
	Hstring str{"你好"};
	Hstring strA{ str };
	Hstring strB{ 5 };
	strB = strA;
	//strB.ChangeStr("发生改变!");
	std::cout << str.StrShow()<<std::endl;
	std::cout << strA.StrShow() <<std::endl;
	std::cout << strB.StrShow()<<std::endl;
	std::cout << strB.StrShow();
}

对Hstring类设定了三个构造函数

image-20221030131736244

这里发现Hstring strA{str}没有执行预设的构造函数,那它执行的是哪个构造函数呢,在加入一个构造函数测试看看

Hstring(const Hstring& strA) :str{strA.str}
	{
		std::cout << "构造函数2"<<std::endl;
	}

image-20221030132014891

这样就能很明显得看出

Hstring strA{ str };//调用的是下面这个构造函数
Hstring(const Hstring& strA) :str{strA.str}
	{
		std::cout << "构造函数2"<<std::endl;
	}

那么现在就可以来解答之前那个调用的是哪个构造函数了

image-20221030132401152

可以看到Hstring这个类有五个构造函数可是事实是我们只构造了三个,那问题来了其他两个呢,是编译器帮我们默认的构造函数,通过函数重载实现调用对应的构造函数。

析构函数

image-20221030102345133

类与对象的本质

静态成员变量

image-20221030135203465

image-20221030135705278

从本质上理解,静态成员变量在类实例化时脱离类,也就是说静态成员函数不占用类的内存空间,而具备了全局变量的属性,从而实现多个实例的类共享一个静态成员

image-20221030140031054

image-20221030140035768

静态成员函数

image-20221030141117303

友元类

image-20221030145420642

image-20221030150625073

嵌套类

image-20221030151546180

image-20221030151557215

image-20221030152152545

image-20221030152204866

访问权限从逻辑关系上理解:嵌套类终归还是外层类的成员,类的成员访问其他成员本身就是可行的,那么嵌套类就可以访问外层类的所有成员

虽然嵌套类是属于外层类的成员但,外层类终归在嵌套类外,自然也就无法访问嵌套类封装的私有成员

image-20221030153255203

image-20221030153646295

malloc和new的本质区别

image-20221030161342570

当用malloc为类分配内存空间,malloc只是单纯的类所需的内存大小分配内存空间,毕竟类是C++才有的东西,malloc是C语言的东西

new为类分配内存空间,会调用类的构造函数

image-20221030162041686

free同样只是释放内存空间

delecte则会调用类的析构函数

image-20221030162330418

从底层学习类

image-20221030164101447

image-20221030191857736image-20221030191911600

int main()
{
008C1010 55                   push        ebp  
008C1011 8B EC                mov         ebp,esp  
008C1013 83 EC 0C             sub         esp,0Ch  
008C1016 A1 00 30 8C 00       mov         eax,dword ptr ds:[008C3000h]  
008C101B 33 C5                xor         eax,ebp  
008C101D 89 45 FC             mov         dword ptr [ebp-4],eax  
    T t{199,100};
008C1020 C7 45 F4 C7 00 00 00 mov         dword ptr [ebp-0Ch],0C7h  
008C1027 C7 45 F8 64 00 00 00 mov         dword ptr [ebp-8],64h  
    t.Add(100,200);
0088103E 68 C8 00 00 00       push        0C8h  
00881043 6A 64                push        64h  
00881045 8D 4D F4             lea         ecx,[t]  
00881048 E8 B3 FF FF FF       call        T::Add (0881000h)  
    std::cout << &t;
0088104D 8D 45 F4             lea         eax,[t]  
00881050 50                   push        eax  
00881051 8B 0D 38 20 88 00    mov         ecx,dword ptr [__imp_std::cout (0882038h)]  
00881057 FF 15 34 20 88 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0882034h)]  
}
0088105D 33 C0                xor         eax,eax  
0088105F 8B 4D FC             mov         ecx,dword ptr [ebp-4]  
00881062 33 CD                xor         ecx,ebp  
00881064 E8 04 00 00 00       call        __security_check_cookie (088106Dh)  
00881069 8B E5                mov         esp,ebp  
0088106B 5D                   pop         ebp  
0088106C C3                   ret   

二话不说直接上汇编看

008C1010 55                   push        ebp  
008C1011 8B EC                mov         ebp,esp  
008C1013 83 EC 0C             sub         esp,0Ch  

正常的进函数后的流程push ebp mov ebp esp 对esp进行加法运算准备好空间

008C1016 A1 00 30 8C 00       mov         eax,dword ptr ds:[008C3000h]  
008C101B 33 C5                xor         eax,ebp  
008C101D 89 45 FC             mov         dword ptr [ebp-4],eax  
    T t{199,100};
008C1020 C7 45 F4 C7 00 00 00 mov         dword ptr [ebp-0Ch],0C7h  
008C1027 C7 45 F8 64 00 00 00 mov         dword ptr [ebp-8],64h 

从这里大致判断应该就是类的汇编

先通过mov将一个地址放到eax中,再将未知地址放入栈中,然后再分别将两个参数100,199放入栈中

其实这里的位置地址就类在内存中的地址

入栈的顺序是先将t的地址入栈,参数是由右往左入栈

0088103E 68 C8 00 00 00       push        0C8h  
00881043 6A 64                push        64h  
00881045 8D 4D F4             lea         ecx,[ebp-0Ch]  
00881048 E8 B3 FF FF FF       call        00881000  

调用成员函数先是将参数入栈通过lea将之前入栈的实例化类的内存地址放到ecx中,然后就是进call

    int Add(int a,int b)
    {
00881000 55                   push        ebp  
00881001 8B EC                mov         ebp,esp  
00881003 51                   push        ecx  
00881004 89 4D FC             mov         dword ptr [ebp-4],ecx  
        return a+b;
00881007 8B 45 08             mov         eax,dword ptr [ebp+8]  
0088100A 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
    }
0088100D 8B E5                mov         esp,ebp  
0088100F 5D                   pop         ebp  
00881010 C2 08 00             ret         8  

push ecx
mov dword ptr [ebp-4],ecx

将ecx入栈ecx放着t的内存地址,然后再放回到原来的位置,绕一大圈又回来了,ecx入栈,这样看好像这汇编感觉有点多此一举了,绕来绕去最后绕一大圈回到原地多少有点呆

int Add(int a,int b)
    {
00881000 55                   push        ebp  
00881001 8B EC                mov         ebp,esp  
00881002 EC                   in          al,dx  
00881003 51                   push        ecx  
00881004 89 4D FC             mov         dword ptr [this],ecx  
        return a+b;
00881007 8B 45 08             mov         eax,dword ptr [a]  
0088100A 03 45 0C             add         eax,dword ptr [b]  
    }
0088100D 8B E5                mov         esp,ebp  
0088100F 5D                   pop         ebp  
00881010 C2 08 00             ret         8  

现在带上符号来看汇编,就发现其中原来还是有门道的,不是单纯的在转圈

现在再分析一遍做完函数正常的准备后,push了ecx前面也都知道了ecx里放的其实就是类的内存地址,将eax给了this,this就是类的指针

mov dword ptr [this],ecx

所以这一步其实是向this指针指向这个类,这里的例子能分析的东西有限,但也能知道了无论有没有用到类的this指针,其实都会有this指针指向的这个过程,还是通过ecx进行传递

执行完将结果放在eax

还可以发现类的成员函数执行完返回时直接进行了栈恢复,但普通函数执行完后返回后进行栈平衡恢复

image-20221030164101447

类的成员函数默认遵循_thiscall函数调用约定

所谓的栈由被调用者恢复,这里的被调用者和调用者其实就是被调用函数,和调用函数的主体,所以被调用者也就是在函数执行完返回执行前进行栈恢复,由调用者恢复则是函数执行完返回后进行栈恢复

类的自定义函数调用约定

函数调用约定

函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。

类型__stdcall__cdecl__fastcall,__thiscall,__nakedcall,__pascal,__vectorcall

参数传递顺序

1.从右到左依次入栈:__stdcall__cdecl,__thiscall,__fastcall

2.从左到右依次入栈:__pascal

调用堆栈清理

1.调用者清除栈。

2.被调用函数返回后清除栈。

__cdecl

1、参数是从右向左传递的,也是放在堆栈中。

2、堆栈平衡是由调用函数来执行的(在call B,之后会有add esp x,x表示参数的字节数)。

3、函数的前面会加一个前缀_(_sumExample)

对角色吃药逆向分析

#include <iostream>
class Medicine
{
    unsigned Addhp;
    friend class Role;
public:
    Medicine(unsigned ahp)
    {
        Addhp = ahp;
    }
};
class Role
{
    int Hp;
    int MaxHp;
    unsigned Damage;
public:
    void Eat(Medicine& med)
    {
        Hp += med.Addhp;
        Hp = Hp > MaxHp ? MaxHp : Hp;
    }
    Role(int hp,int maxhp,unsigned damage)
    {
        Hp = hp;
        MaxHp = maxhp;
        Damage = damage;
    }
    Role() :Hp{ 1000 }, MaxHp{ 2000 },Damage{100}
    {

    }
    int ShowHp() const
    {
        return  Hp;
    }
    void attack(Role& role)
    {
        Hp -= role.Damage;
    }
};
int main()
{
    Role user;
    Role boss{ 2000,2000,100 };
    Medicine med{100};
    int a{ 1 };
    while (a)
    {
        std::cin >> a;
        if(a==1) user.Eat(med);
        std::cout << user.ShowHp()<<std::endl<<boss.ShowHp() << std::endl;
        if (a == 2)
        {
            user.attack(boss);
            boss.attack(user);
        }
    }
}

下面是在对类进行实例化调用来设置的构造函数

{
008C10A0 55                   push        ebp  
008C10A1 8B EC                mov         ebp,esp  
008C10A3 51                   push        ecx  
008C10A4 89 4D FC             mov         dword ptr [this],ecx  
    Role() :Hp{ 1000 }, MaxHp{ 2000 },Damage{100}
008C10A7 8B 45 FC             mov         eax,dword ptr [this]  
008C10AA C7 00 E8 03 00 00    mov         dword ptr [eax],3E8h  
008C10B0 8B 4D FC             mov         ecx,dword ptr [this]  
008C10B3 C7 41 04 D0 07 00 00 mov         dword ptr [ecx+4],7D0h  
008C10BA 8B 55 FC             mov         edx,dword ptr [this]  
008C10BD C7 42 08 64 00 00 00 mov         dword ptr [edx+8],64h  

    }
008C10C4 8B 45 FC             mov         eax,dword ptr [this]  
008C10C7 8B E5                mov         esp,ebp  
008C10C9 5D                   pop         ebp  
008C10CA C3                   ret  

来看这个构造函数的汇编指令,首先是常规的push ebp mov ebp,esp

接着将类的指针放到ecx中,通过ecx传递给this指针,这也就再次证实上面的结论,类的指针通过ecx传递。

传递完成后通过this指针一顿操作向成员变量赋值

user.attack(boss);
008C11AF 8D 55 DC             lea         edx,[boss]  
008C11B2 52                   push        edx  
008C11B3 8D 4D E8             lea         ecx,[user]  
008C11B6 E8 25 FF FF FF       call        Role::attack (08C10E0h)
    void attack(Role& role)
    {
008C10E0 55                   push        ebp  
008C10E1 8B EC                mov         ebp,esp  
008C10E3 51                   push        ecx  
008C10E4 89 4D FC             mov         dword ptr [this],ecx  
        Hp -= role.Damage;
008C10E7 8B 45 FC             mov         eax,dword ptr [this]  
008C10EA 8B 4D 08             mov         ecx,dword ptr [role]  
008C10ED 8B 10                mov         edx,dword ptr [eax]  
008C10EF 2B 51 08             sub         edx,dword ptr [ecx+8]  
008C10F2 8B 45 FC             mov         eax,dword ptr [this]  
008C10F5 89 10                mov         dword ptr [eax],edx  
    }
008C10F7 8B E5                mov         esp,ebp  
008C10F9 5D                   pop         ebp  
008C10FA C2 04 00             ret         4  

lea edx,[boss]
push edx

这两句是引用传参先获取实例boss的内存地址给edx,将其打入栈,很典型的传参行为,lea获取地址是很典型的指针行为,这里也就说明引用其实本质上也是指针

lea ecx,[user]
call Role::attack (08C10E0h)

又在获取地址给ecx但这不一样它的用ecx传递user实例的地址。忙猜少不了的this指针指向

push        ebp  
mov         ebp,esp  
push        ecx  
mov         dword ptr [this],ecx

果然没猜错成员函数少不了的this指针传递,执行完直接进行栈平衡

这都符合_thiscall

运算符重载

运算符重载的概念

image-20221031084142098

image-20221031090521867

image-20221031090534891

计算符重载的原则和时机

image-20221031092038263

image-20221031092848375

image-20221031092853583

image-20221031093315703

image-20221031094743127

重新赋值运算符

image-20221031094843489

image-20221031095434091

image-20221031095445011

image-20221031100107053

重载位移运算符

image-20221031144207515

image-20221031233238957

std::ostream& operator<<(std::ostream& _cout,char* strA)
{
	_cout << strA;
	return _cout;
}
std::istream& operator>>(std::istream& _cin, Hstring& strA)
{
	char cptr[0xff]{};
	_cin>>cptr;
	strA.ChangeStr(cptr);
	return _cin;
}

重载下标运算符

image-20221101091639969

重载函数运算符

image-20221101150124641

重载二元算术运算符

image-20221101213009407

标签:int,mov,C++,语法,eax,ebp,dword,ptr
From: https://www.cnblogs.com/tres/p/16853209.html

相关文章