C++
常量和变量
变量的定义方式:
const 类型 名字 {};
直接使用值
#define 名字 值
常量定义方式:
类型 名字 {};
类型 名字 = 初始值;
不管是常量还是变量,本质都是在内存中申请一块内存,用来存放数据,只不过常量申请的内存区域不允许修改,而变量申请的内存区域,允许修改。以上说的可以修改和不可以修改,只是站在编译器的角度,不管是变量还是常量,甚至是已经编译好的代码,都是可以修改的。
类型
整数类型
布尔类型
浮点数类型
浮点数也可以用来表达整数
下面进行举例表示
int main()
{
float a{100000001.0};
std::cout << a;
}
1e+08代表1*10的8次方,通过运行程序后发现初始化浮点数a的个位数被舍掉了这就是float精度带来的问题
下面放着例子截图
基础运算
将a++分解开就相对好理解先是a=a;a=a+1;
而++a则是a=a+1;再进行赋值。
类型转换
下面是C++中的隐性转换遵循下面的顺序表,类型越在上面就以此类型转换
发现结果还是1e+08但实际我们想要的结果不该是这个,按类型转换表float在int类型之上那么就要将b转换为浮点数,但出现这样的结果不仅仅是类型转换这么简单,还与浮点数的精度计算有关,我通过测试发现
但出现这样的结果不仅仅是类型转换这么简单,还与浮点数的精度计算有关,我通过测试发现
到这里就明确了,就是浮点数精度影响了计算的结果,我的判断是浮点数计算同意了科学计数法从而让变量b变成了0.
还有一种类型转换
两种转换方式都可以进行类型转换,但也存在区别,通过C的方式,是无论什么类型都可以转换为指定类型,但C++的方式则相对更加安全,因为它会去验证是否可以转换。总得来所C的更加粗暴。
补充
当无法分清那种类型占用多少字节可以通过
字符
在计算机的世界中万物皆数字,那么在使用的过程中人类交流的语言文字,又是以什么形式表现呢
这里就引出字符的概念,无论是符号,字母,汉字等都可以叫做字符,字符在计算机眼中依旧是以数字的形式存在。但人为得规定了一套对应的规则,例64=A
char chara{64};
最初只有char类型,char占用一个字节也就是,也就是0-253,最初计算机是由美国人发明的,他们最先制定的编码规范,美国使用英语,他们制定的编码规范里只要包括符号,数字字母就都用了,但其他国家也需要使用呀,美国人用掉了前0-121,也就是ascll码,其他国家的人在后面122-253添加自己需要的也就够用了,也就有了自己的编码规范。
但到中国就行不通了,中国的汉字有上万个这254很显然不够看,那么好我用两个字节来表示汉字,65536个完全够用了,所有汉字占两字节。也就是GBK编码
但各个国家的的编码规范都不相同,那么就会出现一个问题,在中国可以正常使用的文件到美国可能就不能正常使用了,这也就有了Unicode编码,也叫万国码。
注: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但他们本质上其实是不同的。
字符的故事
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类型
auto a{100};//int类型
typeid(a).name();
格式化输出流及转义
C++
输出
输入
运算优先级
c=a---b;//根据符号的优先级关系,c=a-- -b
注:可以通过括号强制改变运算顺序
生命周期
枚举类型
枚举类型需要明白的几个点
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
};
自定义变量名称
#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;//对指定使用。
变量的生命周期
全局变量的生命周期从程序运行开始知道结束。
局部变量的生命周期存在于它所在的代码块中。
#include <isotream>
int a{ 100 };//定义全局变量
int main()
{
int a{ 150 };//代码块中声明的变量都可以叫做局部变量
using std::cout;
{
int a = 200;//当变量名存在冲突时,程序会访问最近的,
{
cout << a << "\n" << ::a << (char)10;//访问变量名冲突的全局变量,可以通过限定符::来访问
}
}
}
位运算
求反运算
求反运算说起来其实挺简单就是将被求反的对象,0变1,1变0.下图呈现了求反的过程
#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
从上面的例子就可以看出有符号数如何求它的相反数
int a{ 12345 };
std::cout << a << (char)10;
a = ~a;
a += 1;//此时再对求反的值加1就是这个值的相反数
std::cout <<a;
位移运算
位移运算从名字上就很直白得能听出它的作用,就是把整个数据向指定方向移动几位。
但从计算机的角度理解,就没有这么简单。
当程序声明一个变量其实就是内存中申请了一个一块地址,大小呢根据它的类型来决定,对这个变量赋值的过程其实也就是将数据放到这个内存中的过程,对计算机而言这块内存就是这个变量,那么对它进行位移运算的操作,出去的数据将不再是这个变量的值,也就会被舍弃,空下的位则用0填充
向左移位运算的本质:移动一位相当于对值乘以2
int a{ 0b01101101101 };
a <<= 6;
std::cout << std::bitset<32>(a);
向右移位运算的本质:移动一位相当于对值除以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);
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);
通过两端程序,以及结果很快就能发现区别,有符号时进行右移位运算很不确定,会以为cpu不同改变,可能会根据符号位进行填充。
无符号则只会用0填充
与运算
将两位的二进制的各位进行运算,有0则为0,都为1才为1
int main()
{
int a{ 0b01101101101 };
int b{ 0b01111100000 };
a = b&a;
std::cout << std::bitset<32>(a);
}
int main()
{
int a{ 0xFF00 };
int b{0x1234};
std::cout <<std::hex<< (b&a);
}
或运算
有1则为1,都为0才为0,这一点跟与运算有点反过来了
int main()
{
int a{ 0b01101101101 };
int b{ 0b01111100000 };
a = b|a;
std::cout << std::bitset<32>(a);
}//结果为a=00000000000000000000001111101101
异或运算
所谓位运算就是将两个数的二进制各位进行计算,相同则为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:;
自定义数据类型---结构
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
关系运算符
if
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);
}
逻辑运算符
bool a=0xFFFF0000&0xFFFF;//这里是先进行位运算得到结果为0,所有a=0;
bool b=0xFFFF0000&&0xFFFF;//这里是逻辑运算,(bool)0xFFFF0000&&(bool)0xFFFF,布尔类型非0为真,b=1.
条件运算符
int cs{};
std::cout << "请输入一个数:";
std::cin >> cs;
int x = cs ? 1000 / cs : 0;
std::cout << "结果为:" << x;
大神技巧
异或运算是什么,相同为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 << "很遗憾!";
字符处理
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
int main()
{
int a;
std::cin >> a;
switch (a)
{
case 1:
break;
case 2:
break;
case 3:
break;
default:
printf("执行到最后!");
break;
}
}
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;
}
}
语句块中的变量
goto
for
while循环
数组的概念
创建变量的过程本质上是想计算机申请一块内存,内存大小由类型决定
数组其实就相当于批量的创建变量,同样也是申请内存
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];
}
不对数组进行初始化不会报错,可以正常运行。还是由于数组其实就是 一块内存不对他进行初始化,里面的数据就是之前遗留的数据。
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:;
}
}
基于数组的循环
这种通过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;
}
这种方式很简洁,还可以自己选择类型;
auto则是系统根据原有的类型
多维数组
int number[]{ 1000,1001,1002,1003 };//一维数组可以不指定元素个数
int studentId[2][3]//多维数组必须指定
{
{1,2,3},
{1,2,3}
};
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输出
}
}
}
数据安全:无论是变量,数组,多维数组本质都还是向内存申请内存,数组,多维数组是一块连续的内存。
int studentId[2][3]//二维数组
{
{1,2,3},
{1,2,3}
};
std::cout<<studentId[9][9];
那么此时又会出现什么情况呢,依旧可以访问到数据,但这是超出数组之外内存中的数据。所有这种原生数组是不安全的。
std::array
std::vector
指针和引用
指针
指针还得从计算机万物皆数据,创建变量进行赋值的本质还是申请一块内存,内存大小由类型决定,赋值就是向这块内存写入数据,指针本质就是一块内存地址,大小同样也由声明是的类型来决定。
int* pa{ };
int a;
pa = &a;//通过&取址符获取变量a的内存地址
*pa = 500;//通过*间接运算符来修改变量a的值
printf("%d", a);
但这里的*pa和a是完全不同的,虽然都能实现修改变量a值的效果
a=500是先找a所在的地址再往其中写入
*pa则是直接对内存写入
指针数组
指针补充
在声明变量的时候,需要告诉计算机变量的类型,数据类型就告诉计算机申请内存的大小,但这里其实就会有一个问题,就是都知道计算机只能存储0和1,那么计算机用如何知道我该以无符号数处理还是有符号整数还是字符的方式处理呢,数据类型就是告诉计算机你要用这种方式处理数据
指针本质上也就是一个特殊的变量类型,那么声明指针就同样也要向计算机申请一块用于存放数据的内存。但指针又与正常的变量类型存在很明显的区别,因为它里面放的是一个内存地址,接下来就要考虑内存地址的大小由什么决定呢,但我可以知道的是所占内存大小一定不受类型影响了。
它根据操作系统有关,32位操作系统232 也就是最大4G的内存那么在操作系统的设计之初,必然考虑极端情况,换句话说就是我就是4G的32位操作系统那么我默认所有的内存地址都为32位也就是4个字节。同理64位的地址就都为64位也就是16个字节
对指针进行类型转换
占用同等内存
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----指针的加减
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;
指针的加减法
拿上面的ptr++举例对指针ptr加1实际上相当于+1*数据类型所占内存的大小
指针实验2------多级指针
上面已经很多次说明,其实指针就是特殊的变量,那既然是变量,不管里面放着内存地址,那就需要内存空间来存储,那么如果想操作这指针存放内存地址的内存呢
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;
常量指针
常量指针不能对其指向的内存地址进行改变,但可以改变指向的地址
const int a{};
int b{};
const int* pa{&b};
//*pa = 9000;//通过指针pa修改指向的变量b的内存数据会报错
b=9999;
pa=&b;
指针常量则是不可以修改指向的内存地址,但却可以修改内存地址中的数据
常量的变量指针可谓集万家于一体,既不能修改指向的内存地址,也不能修改内存地址中的数据。
指针和数组
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]
小结:数组跟指针真的存在关系,数组的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]就是用来计算偏移量的
动态分配内存
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;
动态分配内存的分险
引用
堆栈
智能指针
std::unique_ptr-----唯一智能指针
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----共享智能指针
共享智能指针可以多个指针指向同一个内存空间,也就有了共享之意
字符处理
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;
char类型占用1个字节,wchar_t占用2个字节,这里就会出现一个问题,同样一个字符,char类型和wchar_t类型在内存中的表现可能完全不同。这里不得不提如何实现从数字到各种字符的转换,首先建立一个表,告诉计算机为1等于多少为2等于多少,那么计算器只要拿着数据去对比这个表就能完成。就拿支撑中文的GBK编码举例,它也只是这个表的一种。
通过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);
}
指针和结构体
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应该不出意外就是它的偏移量,编译器通过给的模版,计算出它的偏移量,在汇编指令上就能很直观的感受到
这种方式跟之前数组指针存在区别,但本质上是一样的。
内存对齐
结构体本质上在内存对齐
自定义数据类型-----联合体
联合体跟结构体直观来看很类似,而且在功能上也很相似
结构体在内存中是连续的内存,它会受到内存对齐影响。但联合体则会不同,联合体所占用的内存大小由它占用内存最大的数据类型决定。
union User
{
short Hp;
int Mp;
};
int main()
{
User user{};
std::cout << sizeof(user);
}
对比来看就能很直观了结构体受内存对齐影响,联合体最大占用的数据类型为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
}
字符串
std::string
#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
字符串补充知识点
字符串应用
#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;
}
}
}
指针数组字符串
声明了一个string类型的字符串,这里就能看到这个string类型的字符串str其实是const char字符常量
string str{ "123456" };
char strc[]{ "123456" };
std::cout << std::endl << &strc<<strc;
其实这里也就体现了字符和数组之间的区别,如果是数组的话,此时输出strc应该为一个内存地址,这也不奇怪,因为strc本身其实就是指针。那么这里的strc是指针吗。那么我如何才能证明strc为指针呢。
那我就要先研究编译器对char类型的指针的处理方式
char a{'1'};
const char* strptr{(char*) & a};
std::cout << strptr;
从上面看一切就很直观了首先如果是其他类型的指针那么输出出来的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];
对比string类型字符串和char类型,有一点是肯定的在内存上都是连续的可是为什么从数据上看好像不太能说得通。
这很大概率是string是类
大神必修----字符串
字符串流
#include <iostream>
#include<string>
#include<sstream>
using std::string;
int main()
{
std::stringstream str;
str << "你好" << 1234;
string stra = str.str();
std::cout << stra;
}
函数基础
函数概念
自定义一个函数首先要有一个返回值类型,各种数据类型都可以,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 };
这里不对e进行初始化,然后又让e的值给d它就报错了,首先有一点要先确认的是,声明变量是否已经分配了内存
通过取e的地址也就能得出一个结论声明变量即使不进行初始化也已经分配了内存。但如何解释不对a,b进行初始化对其内存空间进行操作却能成功编译不报错呢。但好像用函数时需要对其进行传参,那也就是说其实对参数a,b进行了赋值,只是隐藏掉了编译器自己偷偷干了这样的操作。
函数参数:数组
函数参数:引用
#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;
}
用引用就能实现上面所要的效果,引用的本质其实也就是指针,引用中放着的就是它所引用的内存地址,所以指针的引用其实本质是什么呢。就是指针的内存地址,那引用作为函数参数不就是把那个指针的内存地址给到引用吗,那这逻辑就通了。那它其实就直接到指针的内存地址修改值。
函数参数:默认实参
函数参数:不定量参数
#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;
}
麟江湖游戏设计
#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 << "请使用命令的方式调用本程序!";
}
}
函数的底层知识
函数参数:不定量参数
#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);
}
很直观就能从数据中发现写什么首先有一点是可以肯定的那就是这些数据在内存中依旧是连续的,而且之间的间隔为8那也就是说其实是为每个数据准备了8个字节的位置。从这里我的一切疑虑其实就都迎刃而解了
首先这里的不定量参数跟main函数通过数组实现不同是肯定的。那么假设我来设计,在提前确定数据的数据类型的情况下会怎么做呢。肯定要先将数据放到一个生成的内存空间中,但生成内存空间该生成多大合适呢,很明显给每个参数8个字节的空间,这样既做到了不大可能出现空间不够的情况,因为大多数数据类型都为8个字节及以内,只有long long类型超出,还兼顾省内存。
可是那读取数据又要如何呢,是通过va_start()函数来让数据以想要的数据类型输出出来
函数返回--返回指针和引用
函数参数:右值引用
函数的本质
学习到现在已经知道了结构体变量参数数组指针等,它们本质都是放在内存中的一串数据,那么函数呢,无论是main函数,还是C/C++自己封装的函数,以及自己写的函数,它们都是什么呢,在计算机中究竟是以何种方式存在呢。
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)
再看这句汇编指令,在想想为什么调用函数后能跳转到函数所在的内存位置,能指向内存的好像指针就能做到,那我可不可以吧函数名理解为一个指针
事实证明没错就是指针
函数指针
从函数的角度彻底认识栈
栈本质上也是一段连续的内存空间,用来存放一些临时变量,但问题来了栈是如何做到在变量的生命周期接受后被它也同步的呢
首先先要知道栈是怎么运行的,在汇编中都有看到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函数
成功执行
函数重载与函数模版
函数重载
函数模版
template<typename type1>
type1 ave(type1 a, type1 b)
{
return a + b;
}
函数模版和重载
auto->decltype
auto
decltype
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类型引用
}
auto->decltype
decltype(auto) Add(int& a, int& b)
{
std::cout << "123";
return a > b ? a : b;
}
auto和decltype存在明显的区别
推断函数模版返回类型
函数模版参数
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
静态变量的生命周期跟全局变量的生命周期,当在函数中声明,临时变量会随着函数结束而结束,但静态变量不会,但它也同时只允许这函数去访问,其实这也就是说静态变量在内存中有固定的内存,因此也就不会像在栈中的临时变量一样。
通过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]
}
当然现在的编译器很智能能自己对代码进行优化
从编译器的角度理解定义和声明
首先要知道的是计算机是只懂0和1组成的机器码的,通过它来知道自己做什么样的操作,但问题来了写的C\C++都不是它能懂的,那么就需要编译器,把写出来的代码就像文章一样告诉编译器,我要做什么你帮我翻译给计算机让它去做。那么写代码其实本质上是跟编译器去沟通,那说的话就要符合的的规则,毕竟这样才能准确的表达要做什么
那么声明其实也就是告诉编译器的,比如说我跟小明说小红明天要来你准备一下,那小明问小红是谁啊
告诉小明就是声明,那么谁是小红则是定义的过程,这里定义当然不仅仅是函数,也可以是变量
编译器在读代码的过程中很可能会出现多个代码嵌套的情况,但如果这个代码中出现编译器还没见过的那它如何认识又谈何编译呢,所以就告诉编译器,这里的函数其实返回值参数类型等信息,后面会把详细信息给你
毕竟其实对于编译器来说最主要的其实是它的返回值,函数参数,通过之前的学习其实结合起来也就明白了
在调用一个函数首先要将给函数的参数打入栈中,这是就涉及到参数的个数是多少,类型分别是什么,然后才是call指令到函数的地址执行,执行完后ret指令回来接着执行,函数结果在eax中,那么如果处理这eax的值
这样看其实对编译器来说函数的内容就没这么重要了,先告诉我它的返回值,参数情况,至于具体情况我通过call过去就好了
源文件和头文件
C++的源文件文件后缀为cpp,C的源文件后缀为c
那么当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函数。
extern
创建自己的sdk
创建项目类型
递归函数
递归函数是
从编译器角度理解函数
One Definition Rulr----单定义规则
结合之前的知识来讲转换单元,首先编译器需要对源文件进行编译,每一个源文件都会编译成一个obj文件,也就是对象文件,里面的内容是将头文件和源文件和并就叫做转换单元,翻译成计算机能懂的机器码和转换单元的引用信息(不在转换单元中定义的对象)
然后会通过链接器将各个转换单元的对象文件链接起来,从而生成最终的程序
说到这里结合起来看,声明的意义就负责起对多个转换单元之间的对象文件串联的作用,所谓的引用信息多半是通过声明建立,在生成完obj文件后,在链接器开始串连整个程序中,在其他转换单元的对象文件中寻找之前声明的对象的定义
未定义的行为其实还算挺容易理解的,不在C++标准中规定的行为,就是未定义的行为
内部连接属性:只在本转换单元有效
外部链接属性:在其他的转换单元同样有效
无连接属性:只在该名称的作用域内访问
从我个人的知识体系来表达
1.内部连接属性更像是局部变量 静态函数,静态变量等都是局部变量
2.外部链接属性则妥妥的全局 通过关键字extern来声明全局,函数也具备外部链接属性
3.无连接属性则是临时变量存放在栈中,具有生命周期
注:以上为个人此刻的间解,不排除以后随着学习的深入理解加深的可能性
#define
通过#define设置别名本质上来说其实就是替换,将代码中所有A都替换成B
通过#define定义常量对类型不做限制
namespace---命名空间
//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;
}
如果不在头文件建立一个声明告诉编译器建立一个链接,从而正常将b.cpp源文件编译成obj文件,通过执行的结果看在另一个源文件中定义的变量也放到了同一个命名空间中,这也就说明命名空间具备外部链接属性
已命名的命名空间可在多个转换单元中进行扩展,说明已命名的命名空间具备外部链接属性
未命名的命名空间为内部链接属性,只能在当前转换单元有效。
预处理指令逻辑
预处理指令跟正常的代码区别在于,预处理指令是告诉编译器的而不会体现在汇编中
#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
预定义宏
assert
int a{};
std::cin>>a;
assert(a);
std::cout << 1000 / a;
面向对象编程
OOP
面向对象编程本质上是一种编程思想,C语言更多的是面向过程编程,但这也不意味着不能面向对象,只是会更加复杂。
定义的类中公有成员可以外部属性,可以被外部访问
私有成员只能在类内使用
成员函数
之前学习过的结构体,可以知道如何判断它在内存中所占空间,在结构体中定义变量,当然C++中也可以定义函数。但这是很自然就会想到一个问题。
结构体中定义变量很容易计算,可是定义函数又是如何一种情况呢。
通过之前学习其实就能知道函数确实也在内存中有属于它的内存中间,调用它就直接通过call跳转过去执行,执行结束回来。那么问题来了,定义在结构体中的函数需要开辟一个开辟一块空间放这个函数吗,很显然不需要,它只需要跟正常的函数一样编译,调用跳转过去就可以实现我的目的了。
类跟结构体很相似,它也是这样
class ROLE
{
private:
int hpRcover;
void Init();
public:
int hp;
int damage;
};
int main()
{
std::cout << sizeof(ROLE);
//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
const对象不能改变
对象的const变量不能发生改变
const成员不量不能修改成员变量的值
const对象只能调用const函数
普通对象可以调用const函数
const成员函数函数类型需要与函数返回值一致
const对象只能调用const函数,普通对象却可以都可以调用
就可以利用函数重载来解决const对象和普通对象调用函数的问题
构造函数
#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(),那么对编译器来说就会迷茫,到底该是执行哪一个函数,自然就会报错,如果调用时带参还会触发函数重载,但别忘了构造函数是创建时就自动调用了
#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关键字不让被标记的构造函数进行类型转换
深入理解构造函数
相同类的对象可以访问其私有变量,函数
#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类设定了三个构造函数
这里发现Hstring strA{str}没有执行预设的构造函数,那它执行的是哪个构造函数呢,在加入一个构造函数测试看看
Hstring(const Hstring& strA) :str{strA.str}
{
std::cout << "构造函数2"<<std::endl;
}
这样就能很明显得看出
Hstring strA{ str };//调用的是下面这个构造函数
Hstring(const Hstring& strA) :str{strA.str}
{
std::cout << "构造函数2"<<std::endl;
}
那么现在就可以来解答之前那个调用的是哪个构造函数了
可以看到Hstring这个类有五个构造函数可是事实是我们只构造了三个,那问题来了其他两个呢,是编译器帮我们默认的构造函数,通过函数重载实现调用对应的构造函数。
析构函数
类与对象的本质
静态成员变量
从本质上理解,静态成员变量在类实例化时脱离类,也就是说静态成员函数不占用类的内存空间,而具备了全局变量的属性,从而实现多个实例的类共享一个静态成员
静态成员函数
友元类
嵌套类
访问权限从逻辑关系上理解:嵌套类终归还是外层类的成员,类的成员访问其他成员本身就是可行的,那么嵌套类就可以访问外层类的所有成员
虽然嵌套类是属于外层类的成员但,外层类终归在嵌套类外,自然也就无法访问嵌套类封装的私有成员
malloc和new的本质区别
当用malloc为类分配内存空间,malloc只是单纯的类所需的内存大小分配内存空间,毕竟类是C++才有的东西,malloc是C语言的东西
new为类分配内存空间,会调用类的构造函数
free同样只是释放内存空间
delecte则会调用类的析构函数
从底层学习类
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
还可以发现类的成员函数执行完返回时直接进行了栈恢复,但普通函数执行完后返回后进行栈平衡恢复
类的成员函数默认遵循_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
运算符重载
运算符重载的概念
计算符重载的原则和时机
重新赋值运算符
重载位移运算符
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;
}