C/C++入门-函数起始
- 函数
- 引用与指针
- 函数参数 指针写法 和 数组写法
- 数组的引用
- 右值引用
- 概念:
- **反汇编:**
- 总结
- 用结构体的示例再理解一遍
- 函数的本质
- 栈分析
- 栈溢出攻击
- 函数重载
- 函数重载 进阶 思考
- 函数重载补充
- 函数模板
- (1)
- (2)函数模板的大坑
- (3)显示的为函数模板指定一个类型
- 函数模板 例外情况
函数
引用与指针
在函数中的应用:
如果直接传递内容,一些比较大的内容(如自定义的结构体)传递会非常耗费系统资源。
于是可以用引用和指针传递内容的地址,如此函数便可以通过地址获取到内容,避免了传递整个内容带来的开销。
用做函数参数时,没有太多区别,凭个人习惯
重点:
例如void test(int a)
①需要在函数中修改变量的值:
void test(int& a)
或者
void test(int* a)
②不需要在函数中修改变量的值:
void test(const int& a)
void test(const int&& a)//右值引用
函数参数 指针写法 和 数组写法
示例:
void test(int a[])
{
cout << *a << endl;
}
void test(int* a)
{
cout << *a << endl;
}
这两种方式并无区别,本质都是传递指针,使用方法也一样:
int a{ 100 };
test(&a);
非要说区别,那可能是传递数组时用数组写法更直观;
数组的引用
int a[100]
int (&b)[100] = a;
示例:
int getSum(int arr[], int count)
{
int sum{};
for (int i = 0; i < count; i++)
sum += arr[i];
return sum;
}
int getSum(int(&arr)[10])
{//数组的引用 的方式
//cout << "大小:" << sizeof(arr);
int sum{};
for (auto x : arr)
sum += x;
return sum;
}
int main()
{
int arr[10]{ 11,12,13,14,15,16,17,18,19,20 };
cout << "求和:" << getSum(arr, 10)<<endl;
cout << "求和:" << getSum(arr) << endl;
}
输出
求和:155
求和:155
优点:不知道
缺点:大小得固定,像这里函数形参大小就得写死
右值引用
概念:
左值:有明确内存空间,可以改值的
右值:没有明确的内存空间,临时拿来用的
声明语法:
int&& q{ 100 + 200 };
q = 300 + 400;
应用右值引用的示例:
void printInt(int&& a)
{
cout << a;
}
int main()
{
int a{ 99 };
printInt(a + 100);
}
输出
199
上述代码实现的结果 等同于:
void printInt(int a)//注意这里传的时int类型值,不是引用
{
cout << a;
}
int main()
{
int a{ 99 };
printInt(a + 100);
}
那么区别在哪里呢
反汇编:
printInt(a + 100);//void printInt(int a)
00F1236D mov eax,dword ptr [a]
00F12370 add eax,64h
00F12373 push eax
00F12374 call printInt (0F11492h)
00F12379 add esp,4
//对比
printInt(a + 100);//void printInt(int&& a)
00A5236D mov eax,dword ptr [a]
00A52370 add eax,64h
00A52373 mov dword ptr [ebp-0D4h],eax
00A52379 lea ecx,[ebp-0D4h]
00A5237F push ecx
00A52380 call printInt (0A51497h)
00A52385 add esp,4
可见右值引用会给值找一个内存先存起来
那么如果用左值引用呢?
反例:
总结
函数 用引用传递参数是为了避免传输过大的内容导致时空的浪费,
但普通的左值引用无法解决上述问题,若采用先运算再参,如下:
int ad{ a + 100 };
printInt(ad);
此时变量ad又多占了内存,
于是,便可以采用右值应用解决问题;
用结构体的示例再理解一遍
struct Role
{
int HP;
unsigned MP;
};
Role createRole()
{
Role role{ 100,100 };
return role;
}
需求:通过打印查看createRole的效果,无需保存创建的结构体;
方案1:
void showRole(Role role)
{
cout << role.HP << endl;
cout << role.MP << endl;
}
int main()
{
showRole(createRole());
}
传递了整个结构体,开销太大
方案2:
void showRole(Role& role)
{
cout << role.HP << endl;
cout << role.MP << endl;
}
int main()
{
Role role = createRole();
showRole(role);
}
方案3:
void showRole(Role&& role)
{
cout << role.HP << endl;
cout << role.MP << endl;
}
int main()
{
showRole(createRole());
}
函数的本质
函数调用过程:
1.参数压栈
2.调用函数,汇编指令call
执行函数之前,会将当前执行的位置压栈push,以便执行完函数再回到这里
3.函数执行完毕"}",pop退栈,回到之前执行的位置
栈分析
栈的本质是一段提前分配好的内存空间,主要用来存放临时变量
esp 用来保存栈顶指针
ebp 保存当前栈帧的基址指针,以便在函数执行完毕后能够正确的恢复调用者的栈帧
push 32 指令的操作
①esp =esp-4//先移动栈顶指针
②esp的内存地址里写入32
call CD1020的操作
esp=esp-4
esp的内存地址里写入call的下一条指令的地址CD107F
CPU执行跳转到CD1020
pop ebp 操作
①esp里的内存地址里的值写入ebp
②esp = esp+4
ret 操作
CPU执行跳转到esp的内存地址里的值
esp = esp+4
20minute
关键点:
1、栈是一个连续的内存空间,可以通过int类型(因为指针是4字节/8字节)的数组访问栈的内容
2、跳转之前,会保存下一条指令的地址,以便返回来继续执行
栈溢出攻击
利用栈是一块连续地址的特性,一旦破坏掉栈中用于返回的函数地址信息,就会引发程序崩溃,甚至系统崩溃;或者将这个地址换成植入的病毒的运行地址,就可以进一步控制对方。
函数重载
概念:不同类型返回值、不同类型参数、参数个数不同的函数,函数名可以重复。
作用:使用时方便,比如:int add(int a,int b),float add1(int a,int b),…;都是求和函数,用的时候却要区分,就不太方便,而C++支持函数重载,可以写成:
int add(const int& a, const int& b)
{
return (a + b);
}
float add(const float& a, const float& b, const float& c)
{
return (a + b + c);
}
调用时:
注意:返回类型可以不同,但函数重载不能只通过返回类型区分
函数重载 进阶 思考
(情况1)
void showType(int a)
{
cout <<"传递了一个int" << endl;
}
void showType(float a)
{
cout << "传递了一个float" << endl;
}
int main()
{
char a{ 100 };
showType(a);
}
(情况2)
void showType(int& a)
{
cout <<"传递了一个int" << endl;
}
void showType(float a)
{
cout << "传递了一个float" << endl;
}
int main()
{
char a{ 100 };
showType(a);
}
(情况3)
void showType(int& a)
{
cout <<"传递了一个int" << endl;
}
void showType(float a)
{
cout << "传递了一个float" << endl;
}
int main()
{
char a{ 100 };
showType((int)a);
}
提示:临时变量没有引用
可以自己多练几个示例,看看不同类型参数 传递时的情况
在补充两个
(情况4)
void showType(int a)
{
cout <<"传递了一个int" << endl;
}
void showType(float a)
{
cout << "传递了一个float" << endl;
}
int main()
{
double a{ 100 };
showType(a);
}
(情况5)
void showType(int a)
{
cout <<"传递了一个int" << endl;
}
int main()
{
double a{ 100 };
showType(a);
}
函数重载补充
(1)常量引用 和 变量引用是可以重载的;如:void test(int& a);和void test(const int& a);
(2)重载函数的参数如果全有默认值,那么调用时就不能完全不传入值,因为会产生歧义
函数模板
(1)
示例:
首先,我们有如下两个函数,这两个函数只有类型不一样。
int add(const int& a, const int& b)
{
return (a + b);
}
float add(const float& a, const float& b)
{
return (a + b);
}
如这样的求和功能,如果再加上char,short,long long等类型,那么就需要写很多个函数。
为了方便,C++引入函数模板的概念
将上述求和函数重写为函数模板:
template <typename type1>type1 add(type1 a,type1 b)
{
return (a+b);
}
使用:
int a{ 6 }, b{ 3 };
cout << add(a, b);
结果打印:9
(2)函数模板的大坑
(2.1)函数模板的typename可以是任何类型,取决于用的时候给什么类型,可以是引用、指针等,另外也可以在模板上加上*,&,&&,** 等
(2.2)还有const、static等关键字,直接让函数模板变得极其复杂。
(2.3)并且,在函数的实现里,诸如运算、赋值等操作,面对传入进来的是指针、常量等情况,在使用时需要非常注意。
(示例1)函数模板 结合 函数重载
template <typename type1>type1 add(type1&& a, type1&& b)
{
return (a + b);
}
template <typename type1>type1 add(type1& a, type1& b)
{
return (a + b);
}
使用:
int a{ 6 }, b{ 3 };
cout << add(a,b) << endl;
cout << add(100.1 + 200.3, 1.1 + 2.2) << endl;
输出
9
303.7
(示例2)
略
(3)显示的为函数模板指定一个类型
(示例:当输入参数类型不统一时,编译器无法判断)
解决方法:
函数模板 例外情况
当一个模板满足不了需求时,例如:
template <typename type1>type1 bigger(type1 a,type1 b)
{
return (a > b) ? a : b;
}
int main()
{
int a{ 999 }, b{ 300 };
cout << bigger(a, b) << endl;
cout << bigger(&a, &b) << endl;
}
运行结果:
999
00B5FE08
显然这里的00B5FE08不是我们想要的结果
为了解决这种问题,我们可以再写一个模板:
template <typename type1>type1 bigger(type1 a,type1 b)
{
return (a > b) ? a : b;
}
template <>int* bigger(int* a, int* b)
{
return (*a > *b) ? a : b;
}
int main()
{
int a{ 999 }, b{ 300 };
cout << bigger(a, b) << endl;
cout << *bigger(&a, &b) << endl;
}
输出结果:
999
999
注意:这里的模板
template <>int* bigger(int* a, int* b)
{
return (*a > *b) ?a : b;
}
模板上的类型必须有已知模板与其对应,如下面这种就不行:
但类似的需求我们依旧可以结合函数重载来解决
总之 函数模板+例外处理+函数重载 就是非常灵活
处理调用的优先级:更为具体的函数重载 > 模板例外 > 模板