十九、函数(二)

十九、函数(二)

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)栈和栈的意义

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

​ 栈是连续的内存空间。

热门相关:全系灵师:魔帝嗜宠兽神妃   甜妻动人,霸道总裁好情深   试婚100天:夜少,宠上瘾   傲娇驾到,总裁别闹   全系灵师:魔帝嗜宠兽神妃