十九、函数(二)

1、函数参数之接受不定量参数

1)普通函数不定量传参用法

//接受不定量参数的函数
#include <cstdarg>       //引入头文件cstdarg
int Add(unsigned count, ...)  //第一个参数为参数的个数,第二个参数为三个.
{
	int rt{};
	char* c_arg; //声明一个指针变量
	va_start(c_arg, count); //将参数数据指针赋值给c_arg
	for (int i = 0; i < count; i++) rt += va_arg(c_arg, int);
	va_end(c_arg);  //释放指针
	return rt;
}

std::cout << Add(5, 1, 2, 3, 4, 5); //函数参赛数,需要依次传入各个参数

2)示例:计算多个数的平均值

//通过不定量参数函数,求多个数的平均数
#include <iostream>
#include <cstdarg>

int Average(unsigned count, ...)
{
	va_list  arg;          //va_list 是一个char类型的指针,相当于char* arg;
	va_start(arg, count);  //第一个参数为接受数据的指针,第二个参数为参数的个数,目的是为了将参数的地址放到arg中。此处做了一个内存分配给arg
	int sum{};
	
	for (int i{}; i < count; i++)
	{
		//注:每调用一次va_arg()函数,都会将参数切换至下一个;va_arg()第一个参数为指针,第二个参数为参数的类型         
		sum += va_arg(arg, int);   //相当于把arg当成int类型的值解读,每解读一个,则切换为下一个
		std::cout << "arg地址:" << (int)arg << std::endl;  //本质还是利用了连续的内存空间
	}
	va_end(arg);  //释放arg内存
	sum = sum / count;

	return sum;
}

int main()
{
	int x = Average(5, 221, 331, 202, 555, 776);
	std::cout << "平均数为:" << x << std::endl;
}


3)自己设计一个函数,计算多个数的平均值

//自己设计一个函数,计算多个数的平均值
#include <iostream>

struct Sarg
{
	int count; //统计参数的个数
	char* cMem; //参数的地址
};
int Avg(Sarg& y)
{
	int sum{};
	int* arg = (int*)y.cMem;
	for (int i = 0; i < y.count; i++)
	{
		sum += arg[i];
	}
	return sum / y.count;
}

void main()
{
	Sarg y;
	y.count = 5;
	y.cMem = (char*)new int[5]{ 221, 331, 202, 555, 776 };
	int x = Avg(y);
	std::cout << "平均数为:" << x << std::endl;

}

2、函数返回之返回指针和引用

1)项目设计:设计一个函数,能够让我们直接为c字符串赋值

//设计一个函数,能够让我们直接为c字符串赋值,如
char* str;
str = cstr("你好");
std::cout<<str;
//输出你好
//直接使用强制类型转化,将一个字符串进行赋值
#include <iostream>

int  main()
{
	char* str;
	str = (char*)"你好";  //强制类型转化,"你好"是一个常量;强行的使得str指向了"你好"的地址
	//str没有自己的内存空间,str只是"你好"字符串的一个副本
	std::cout << str << std::endl;
	//str[0]=0;  不允许修改值,因为指向的是一个常量的内存地址
}
//通过函数输出字符串
#include <iostream>

//求字符串占用多少内存的函数
int clen(const char* str)   
{
	int i;
	for (i = 0; str[i]; i++);   //当字符串最后一位为0,表示字符串结束
	return ++i;
}
char* cstr(const char* str)
{
	//将字符串传递出去
	int len = clen(str);   //求出字符串长度
	//char strRt[0x20];  //错误,因为此处strRt为局部变量,没有自己的内存空间,必须返回一个指针
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目标,源,长度)
	return strRt;
}

int  main()
{
	char* str;
	str = cstr("你好");  //强制类型转化,"你好"是一个常量;强行的使得str指向了"你好"的地址
	std::cout << str << std::endl;
}

注:返回指针时,一定不能返回一个局部变量

2)项目设计:游戏麟江湖新手村有6中怪物,要求设计一个函数来创建怪物,怪物结构如下:

typedef struct Role
{
    char* Name;
    int Hp;
    int maxHp;
    int Mp;
    int maxMp;
}*PROLE;
//性能损耗较大
#include <iostream>

typedef struct Role
{
    char* Name;
    int Hp;
    int maxHp;
    int Mp;
    int maxMp;
    int lv;
}*PROLE,ROLE;

int clen(const char* str)   
{
	int i;
	for (i = 0; str[i]; i++);   //当字符串最后一位为0,表示字符串结束
	return ++i;
}
char* cstr(const char* str)
{
	//将字符串传递出去
	int len = clen(str);   //求出字符串长度
	//char strRt[0x20];  //错误,因为此处strRt为局部变量,没有自己的内存空间,必须返回一个指针
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目标,源,长度)
	return strRt;
}

ROLE CreateMonster(const char* str, int Hp, int Mp)
{
	Role rt{ cstr(str),Hp,Hp,Mp,Mp,1 };
	return rt;  //将整个结构体的成员进行了返回,性能损耗较大
}
int main()
{
	ROLE role = CreateMonster("aoteman", 1500, 1500);  //实际项目中,不会使用结构体实体创建对象,因为性能损耗非常大
	std::cout << role.Name << std::endl;
	std::cout << role.Hp << "/" << role.maxHp << std::endl;
}
//上述代码优化,函数返回指针
#include <iostream>

typedef struct Role
{
	char* Name;
	int Hp;
	int maxHp;
	int Mp;
	int maxMp;
	int lv;
}*PROLE, ROLE;

int clen(const char* str)
{
	int i;
	for (i = 0; str[i]; i++);   //当字符串最后一位为0,表示字符串结束
	return ++i;
}
char* cstr(const char* str)
{
	//将字符串传递出去
	int len = clen(str);   //求出字符串长度
	//char strRt[0x20];  //错误,因为此处strRt为局部变量,没有自己的内存空间,必须返回一个指针
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目标,源,长度)
	return strRt;
}

PROLE CreateMonster(const char* str, int Hp, int Mp)  
{
	PROLE rt = new Role{ cstr(str),Hp,Hp,Mp,Mp,1 };  //申请一个结构体rt类型大小的内存空间
	return rt;  //返回值是一个指针
}
int main()
{
	PROLE role = CreateMonster("aoteman", 1500, 1500);  //实际项目中,不会使用结构体实体创建对象,因为性能损耗非常大
	std::cout << role->Name << std::endl;
	std::cout << role->Hp << "/" << role->maxHp << std::endl;
}

//函数返回一个引用
#include <iostream>

typedef struct Role
{
	char* Name;
	int Hp;
	int maxHp;
	int Mp;
	int maxMp;
	int lv;
}*PROLE, ROLE;

int clen(const char* str)
{
	int i;
	for (i = 0; str[i]; i++);   //当字符串最后一位为0,表示字符串结束
	return ++i;
}
char* cstr(const char* str)
{
	//将字符串传递出去
	int len = clen(str);   //求出字符串长度
	//char strRt[0x20];  //错误,因为此处strRt为局部变量,没有自己的内存空间,必须返回一个指针
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目标,源,长度)
	return strRt;
}

ROLE& CreateMonster(const char* str, int Hp, int Mp)   //返回一个引用
{
	PROLE rt = new Role{ cstr(str),Hp,Hp,Mp,Mp,1 };  //申请一个结构体rt类型大小的内存空间
	return *rt;  //  rt表示指针,*rt标志指针的值。若此处是个控制在程序会报错,因为引用必须初始化
}
int main()
{
	Role& role = CreateMonster("aoteman", 1500, 1500);  
	std::cout << role.Name << std::endl;           //引用需要使用实体调用结构体成员变量
	std::cout << role.Hp << "/" << role.maxHp << std::endl;
}

3)传递引用参数时的类型转化

//传递引用参数时存在一个隐士的类型转化
#include <iostream>

int Add1(int a, int b)
{
    return a + b;
}
int Add2(int& a, int& b)
{
    return a + b;
}
int main()
{
    float a = 200.0f;
    float b = 125.53f;
    std::cout << Add1(a, b) << std::endl;  //如果函数的参数不是引用,可以直接传入其他类型的值
    std::cout << Add2(a,b) << std::endl;  //错误。如果函数的参数是引用,必须传入对于引用类型的值,否则报错
}

总结:如果函数的参数不是引用,可以直接传入其他类型的值;如果函数的参数是引用,必须传入对于引用类型的值,否则报错

4)数组的引用

//int类型的引用定义
int a;
int & b=a;

//数组的引用定义
int c[100];
//int & d[100]=c;  //此写法错误
int (&e)[100]=c;  //创建c的引用e,且e的数组长度必须和c的一致。e首先要是个引用,且e中有100个元素
//数组的引用用法
//缺点:若数组的元素不固定,则无法进行定义
#include <iostream>

void ave(int (&art)[5])   //传入数组引用参数
{
	std::cout << sizeof(art) << std::endl;
	for (auto x : art)std::cout << x << std::endl;
}
int main()
{
	int a[5]{1,2,3,4,5};
	ave(a);
}

3、函数参数之右值引用

左值:有着明确的内存空间,可以往里面写入值,就叫做左值。如int c = 320,则c就是一个左值

右值:临时空间存放的值,无法往里面写入值,就叫做右值。如上面的230+250。

//右值引用语法
int&& a = 320+230;   //右值引用指向的是临时的值
//a = 1500; //错误,无法给右值引用进行传值

右值引用可以解决上述问题,并且可以节省变量

#include <iostream>

void Add(int&& a)   //右值引用
{
	std::cout << a << std::endl;
}
int main()
{
	Add(320 + 250);  //如果函数的参数是一个引用,服务直接进行计算传值
}

//右值引用示例
#include <iostream>
struct Role
{
	int Hp;
	int Mp;
};

Role CreateMonster()
{
	Role rt{ 100,200 };
	return rt;
}

void show(Role&& r1)   //使用右值引用,没有再创建变量,而是直接接受CreateMonster()传递过来的rt
{
	std::cout << r1.Hp << std::endl;
	std::cout << r1.Mp << std::endl;
}

int main()
{
	show(CreateMonster());
}

4、函数的本质

1)分析函数汇编代码时,先将调试方式设置为release,再打开项目属性页,将C/C++优化功能关闭

2)汇编代码指令说明:

//部分汇编代码指令说明:
push   x        //将x的内容方放到临时变量的内存区域(栈)
call   x        //让CPU去执行内存地址X处的代码
ret           //让CPU返回跳转前的位置
//C++函数
#include <iostream>

int Add(int a, int b)
{
	return a + b;
}

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

//汇编代码
int Add(int a, int b)
{
00F71000  push        ebp            //将ebp放如是临时变量区,即栈区    
00F71001  mov         ebp,esp  //esp表示栈的位置,ebp=esp
	return a + b;
00F71003  mov         eax,dword ptr [ebp+8]  //将[ebp+8]内存地址中的值放入到eax寄存器
00F71006  add         eax,dword ptr [ebp+0Ch]  //eax=eax+[ebp+0Ch]内存地址中的值,即a和b的加法操作
}
00F71009  pop         ebp  
00F7100A  ret               //ret表示返回值跳转前的位置
int main()
{
00F71010  push        ebp  
00F71011  mov         ebp,esp  
00F71013  push        ecx  
	int c = Add(1, 2);
00F71014  push        2    //push用户给函数传递参数,即将2推送至栈区
00F71016  push        1    //先push最后一个参数
00F71018  call        00F71000  //call表示CPU跳转到目标地址去指向,即此处CPU跳转到00F71000地址
00F7101D  add         esp,8  
00F71020  mov         dword ptr [ebp-4],eax  
	std::cout << c;
00F71023  mov         eax,dword ptr [ebp-4]  
00F71026  push        eax  
00F71027  mov         ecx,dword ptr ds:[00F72038h]  
00F7102D  call        dword ptr ds:[00F72034h]  
}
00F71033  xor         eax,eax  
00F71035  mov         esp,ebp  
00F71037  pop         ebp  
00F71038  ret    //函数尾,有几个ret就有几个函数

3)函数的本质

​ 经过上面的分析,可知函数的本质是一段内存里的二进制数据,我们写下的C++代码会翻译成对应的二进制数据,程序运行的时候会通过某种规则来加载到我们的内存里,一个程序一旦编译(生成),这个程序的二进制数据就不会再发生变化

​ ①程序的生成:C++代码=>二进制数据=>程序文件(硬盘)

​ ②程序的运行:程序文件(硬盘)=>加载到内存中

注:函数名的本质就是一个内存地址

#include <iostream>
#include <bitset>
int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int c = Add(1, 2);
	std::cout <<"函数名的地址为:"<< Add << std::endl;;
	char* str = (char*)Add ;
	for (int i = 0; i < 30; i++)     //将函数的内容显示出来
	{
		std::cout << std::bitset<8>(str[i]) << std::endl;  //函数的内容2进制表示
		//std::cout << std::hex<<(unsigned)str[i] << std::endl;   //函数的内容16进制表示
		//printf("%X\n", (unsigned char)str[i]);
	}
}

5、函数指针

1)函数指针声明

//函数指针声明语法
函数返回类型 (*函数指针变量名)(参数类型 参数名称,......参数类型 参数名称);

//示例
int (*pAdd)(int a,int b)
//函数指针简单用法
#include <iostream>

int Add(int a, int b)
{
	return a + b;
}
int Add_X(int a, int b)
{
	return (a + b)/2;
}

int main()
{
	int (*pAdd)(int c, int d) {Add};  //申明一个函数指针,并将其初始化为函数Add的地址

	std::cout << pAdd(100, 200) << std::endl;
	std::cout << "函数指针大小为:"<<sizeof(pAdd(100, 200)) << std::endl;

	char (*pAdd_X)(int ,int ) { (char (*)(int,int))Add_X };  //如果函数的返回值类型和函数指针的返回值类型不同,需要进行强制类型转化
	std::cout << pAdd_X(110, 20) << std::endl;
}

2)函数指针的类型的自定义

//通过typedef自定义函数指针的类型
#include <iostream>

//把(char (*)(int, int)类型定义为新的类型pFadd
typedef char(*pFadd)(int, int);  //声明函数指针类型


int Add_X(int a, int b)
{
	return (a + b) / 2;
}

int main()
{
	pFadd pAdd_X = (pFadd)Add_X;   //pFadd就相当于(char (*)(int, int)
	std::cout << pAdd_X(110, 20) << std::endl;
}
//通过using自定义函数指针的类型
#include <iostream>

//把(char (*)(int, int)类型定义为新的类型pFadd
using pFadd =  char(*)(int, int);  //声明函数指针类型

int Add_X(int a, int b)
{
	return (a + b) / 2;
}

int main()
{
	pFadd pAdd_X = (pFadd)Add_X;   //pFadd就相当于(char (*)(int, int)
	std::cout << pAdd_X(110, 20) << std::endl;
}

3)函数指针和指针函数

①函数指针本事是个指针,即一个可以指向特定类型函数的指针,如int (*pAdd)(int a,int b);

②指针函数是指一个返回指针的函数,如int* xAdd(int a,int b);

//函数指针类型也可以被当作函数参数
#include <iostream>

using pRole = int(*)(int hp, int mp);   //自定义一个函数指针类型

int Test(int a,int b,pRole x)   //传入一个函数指针类型
{
	return x(a, b);
}

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	pRole pAdd{ Add };  //声明一个函数指针
	std::cout << Test(100, 200, pAdd);  //将100,200传入函数指针
}

6、从函数的角度认识栈

​ 1)我们都知道,变量的本质是对应的内存空间,因此每个变量都需要独立的内存空间,问题是,在实际开发过程中,一个函数可能会被反复调用,如果每次都分配内存空间,那么系统开销将非常大,如果为这样的变量都分配固定的内存空间,又非常的浪费内存资源,所以才有了栈的感念,栈的本质是一段提前分配好的内存空间,主要就是用来存放临时变量!这样我们只需要管理好栈的读写就可以避免频繁的内存分配和不必要的内存浪费!

2)栈是预先分配好的,连续的内存空间,局部变量就放在栈空间上,通过控制esp来实现局部变量的创建和释放

3)栈平衡如果被破环,函数就可能不能返回到预期的位置,同理,利用这个原理,我们也可以控制目标程序进入指定的位置,来获取目标操作系统的控制权限,这也就是栈攻击的技术原理,同时编写代码时也要积极预防栈攻击

4)当我们逆向的时候,可以通过函数头部的sub esp,x 来判断这个函数有多少个局部变量

5)关于CPU寄存器的说明:eax、ecx、edx、ebp、esp、edi、eip

①eax:函数的指向结果会通过eax来传递

②esp:代表栈顶,栈顶一下的值代表着可以访问的局部变量,即表示已经用了的内存空间

③ebp:代表栈底

④eip:CPU执行的位置
//C++代码
#include <iostream>

int Ave(int a, int b)
{
	a = a + 250;
	return a + b;
}

int Add(int a, int b)
{
	int c = 250;
	int d = Ave(a, b);  //调用Ave()函数
	c = c + d;
	return c;
}

int main()
{
	std::cout << Add;  
	system("pause");
	int x = Add(250, 50);  //调用Add()函数
}

//汇编
#include <iostream>

int Ave(int a, int b)
{
00231002 EC                   in          al,dx  
	a = a + 250;
00231003 8B 45 08             mov         eax,dword ptr [ebp+8]  
00231006 05 FA 00 00 00       add         eax,0FAh  
0023100B 89 45 08             mov         dword ptr [ebp+8],eax  
	return a + b;
0023100E 8B 45 08             mov         eax,dword ptr [ebp+8]  
00231011 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
}
00231014 5D                   pop         ebp  
00231015 C3                   ret  

int Add(int a, int b)
{
00231020 55                   push        ebp  
00231021 8B EC                mov         ebp,esp  
00231023 83 EC 08             sub         esp,8  
	int c = 250;
00231026 C7 45 FC FA 00 00 00 mov         dword ptr [ebp-4],0FAh  
	int d = Ave(a, b);  //调用Ave()函数
0023102D 8B 45 0C             mov         eax,dword ptr [ebp+0Ch]  
00231030 50                   push        eax  
00231031 8B 4D 08             mov         ecx,dword ptr [ebp+8]  
00231034 51                   push        ecx  
00231035 E8 C6 FF FF FF       call        00231000  
0023103A 83 C4 08             add         esp,8  
0023103D 89 45 F8             mov         dword ptr [ebp-8],eax  
	c = c + d;
00231040 8B 55 FC             mov         edx,dword ptr [ebp-4]  
00231043 03 55 F8             add         edx,dword ptr [ebp-8]  
00231046 89 55 FC             mov         dword ptr [ebp-4],edx  
	return c;
00231049 8B 45 FC             mov         eax,dword ptr [ebp-4]  
}
0023104C 8B E5                mov         esp,ebp  
0023104E 5D                   pop         ebp  
0023104F C3                   ret  


int main()
{
00231050 55                   push        ebp  
00231051 8B EC                mov         ebp,esp  
00231053 51                   push        ecx  
	std::cout << Add;  
00231054 68 20 10 23 00       push        231020h  
00231059 8B 0D 38 20 23 00    mov         ecx,dword ptr ds:[00232038h]  
0023105F FF 15 34 20 23 00    call        dword ptr ds:[00232034h]  
	system("pause");
00231065 68 08 21 23 00       push        232108h  
0023106A FF 15 A8 20 23 00    call        dword ptr ds:[002320A8h]  
00231070 83 C4 04             add         esp,4  
	int x = Add(250, 50);  //调用Add()函数
00231073 6A 32                push        32h  
00231075 68 FA 00 00 00       push        0FAh  
0023107A E8 A1 FF FF FF       call        00231020  
0023107F 83 C4 08             add         esp,8  
00231082 89 45 FC             mov         dword ptr [ebp-4],eax  
}
00231085 33 C0                xor         eax,eax  
00231087 8B E5                mov         esp,ebp  
00231089 5D                   pop         ebp  
0023108A C3                   ret  

//push 32表示,先让栈顶往上走(即esp=esp-4),再将值32放入栈中,
//casll 00C1020表示,先让栈顶再往上走(即esp=esp-4),再将此指令下一个指令的地址放入栈中