目录
缺省参数
缺省参数的分类
全缺省参数
半缺省参数
小应用
函数重载
名字修饰
预处理阶段
编译阶段
汇编阶段
链接阶段
“承诺”与“兑现”的依赖关系
小思考
C++函数名修饰规则
Linux中的引入方式
Windows中的引入方式
小拓展
缺省参数
基本概念:是声明或定义函数时为函数的参数指定一个缺省值(默认值)。在调用该函数时,如果
没有指定实参则采用该形参的缺省值(默认值),否则使用指定的实参
#include <iostream>
using std::cout;
using std::endl;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(10);
Func();
return 0;
}
注意事项:不允许跳跃式的传递参数
#include <iostream>
using std::cout;
using std::endl;
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func(1,2,3);
Func(1,2);
Func(1);
Func();
Func(, 1, ); //wrong
Func(, , 2); //wrong
Func(, 1, 2); //wrong
return 0;
}
缺省参数的分类
全缺省参数
特点:所有缺省参数都被赋值
void Func(int a = 10, int b = 20, int c = 30)
半缺省参数
特点:不是所有缺省参数都被赋值
void Func(int a, int b = 20, int c = 30)
void Func(int a, int b = 20, int c = 30); //right
void Func(int a = 20, int b = 30, int c); //wrong
若半缺省参数不能按照从右至左的顺序依次进行Func(int a = 20,int b = 30,int c),那么对于传参时只能从左至右依次传递的情况Func(1,2),函数的最后一个参数c是未被赋值的。
2、缺省参数不能在函数声明和定义中同时出现,否则会起冲突,且缺省值要在函数声明时提供(不做过多解释)
//a.h文件
void Func(int a = 10);
// a.cpp文件
void Func(int a = 20)
{
......
}
//wrong!!
3、缺省值必须是常量或全局变量(全局变量一般不用)
小应用
在之前学习顺序表的时,当顺序表的空间不够用时我们就会申请整数倍的新空间,但是这样可能会造成空间的浪费,我们更希望的是一个萝卜一个坑,我们可以使用缺省参数实现这一效果,设置缺省的默认值为4,当有确定需要的空间大小时就按确定的空间大小来,如果没有就用默认值
#include <stdio.h>
#include <stdlib.h>
//顺序表的扩容
struct Stack
{
int* a;
int size;
int capacity;
//...
};
//顺序表的初始化
void StackInit(struct Stack* ps, int n = 4)//设置缺省参数n,并设置默认值为4
{
ps->a = (int*)malloc(sizeof(int) * n); //扩容
}
int main()
{
struct Stack st1;
//1、确定要插入一百个数据
StackInit(&st1, 100);
//2、确定只插入十个数据
StackInit(&st1, 10);
//3、不知道要插入多少数据,那就直接用默认的缺省参数的4
StackInit(&st1);
return 0;
}
函数重载
基本概念:在同一个作用域内,可以定义多个具有相同名称但参数列表(参数的类型、参数的个数、参数类型的顺序)不同的函数
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
注意熟悉:C语言不允许同名函数
名字修饰
问题:为什么C++支持函数重载,而C语言不支持函数重载
对于C语言而言,在代码执行的过程中会经过预处理、编译、汇编、链接四个阶段,下面我们
通过一个函数声明与定义分离的例子看一看代码在经过这四个阶段都会发生什么:
Stack.h文件
#pragma once
#include<stdlib.h>
struct Stack
{
int* a;
int size;
int capacity;
//...
};
void StackInit(struct Stack* ps, int n = 4);
void StackPush(struct Stack* ps, int x);
Stack.cpp文件(.cpp也行懒得改成.c了😘)
#include "Stack.h"
void StackInit(struct Stack* ps, int n)
{
ps->a = (int*)malloc(sizeof(int) * n);
}
void StackPush(struct Stack* ps, int x)
{}
Test.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"Stack.h"
using namespace std;
int main()
{
struct Stack st1;
// 1、确定要插入100个数据
StackInit(&st1, 100); // call StackInit(?)
// 2、只插入10个数据
struct Stack st2;
StackInit(&st2, 10); // call StackInit(?)
// 3、不知道要插入多少个
struct Stack st3;
StackInit(&st3);
return 0;
}
预处理阶段
Stack.h文件会被展开并与Stcak.cpp的文件合并成新文件Stack.i,Test.cpp文件经预处理后
会变为Test.i文件
编译阶段
检查完Stack.i和Test.i文件的语法无误后,会将它们生成由汇编指令组成的文件Stack.s和
Test.s,其中StackInit(&st1,100)对应的汇编指令是 :
我们注意到其中一条汇编指令是“call + 地址”,表示调用StackInit函数这段代码转变为汇编
指令后就是call这个函数的地址(从底层的角度来讲函数调用的本质就是call+函数的地址,从语法
的角度来讲函数名就是其对应的地址)而函数是一系列功能代码的集合,故从底层的角度来讲函数
就是这些功能代码转变成的汇编指令的集合,而函数的地址就是这些指令的第一句指令的地址(有
点像数组的地址,数组的地址就是它的第一个元素的地址),最后我们可以得到的结论是:调用函
数的本质就是call一个函数的地址,然后通过该地址找到函数的指令集中的第一句指令的地址,然
后将这些指令依次取出交给CPU去依次执行,依次执行完后该函数的功能就完成了(也跟数组相
似,各个元素之间地址的距离相似)。
但是实际上在调试反汇编时,call的下一步会跳转到地址为0A001357的另一条汇编指令jmp处(相当于套了一个壳子,调用函数之前还要先把盒子jmp打开才行)关于这点作者能力有限就不过多解释,为了方便理解你也可以忽略这一步。
结论1:本质上讲一个函数的地址就是函数中第一条汇编指令的地址
结论2:函数声明不能得到函数的地址(空壳无内部指令),函数定义可以得到函数的地址(有内部指令)
所以,有函数定义的Stack.s文件有StackInit函数的地址,而有函数声明的Test.s文件无该函
数的地址,此外,只有函数声明的程序依然可以执行的原因是:针对只有函数声明没有定义的情
况,编译阶段的语法检查只负责检查声明是否存在以及是否正确(有并且还正确就行有多少不管)
汇编阶段
将Stack.s和Test.s文件中的汇编指令转换为两份二进制的机器码文件Stcak.o和Test.o文件
(汇编指令是符号式的,为了方便我们理解而存在的,但是CPU看不懂这些只能看懂二进制的
机器码,比如汇编指令push可能就就会变为111代替之类的)
链接阶段
链接器尝试将源文件Stack.o和Test.o连接成一个可执行文件,但Test.o文件只有StackInit函
数的声明而没它的定义,此时系统就会用StackInit这个函数名去其它源文件中(Stack.o)找函数
地址(实际上就是在找该函数的定义)(编译阶段会有一个统计整个项目中各种内容比如变量、函
数以及其它标志符号的符号表),即在链接阶段会利用函数名在符号表中找到函数的地址
如果此时将StackInit函数的定义内容取消,那么链接器就无法找到对应的 StackInit 函数定
义,会提示链接错误(出现链接问题就是函数只给了声明但是没给定义):
综合上述内容我们可以得到结论3:对编译器而言函数声明相当于承诺,函数定义相当于兑现承诺
“承诺”与“兑现”的依赖关系
函数声明:可以被看作是对编译器做出的一种承诺。通过函数声明,你向编译器保证说“这个函数我会在程序中实现”,它告诉编译器有关该函数名称、参数类型和返回类型等信息。这使得在代码中可以使用该函数而不需要知道具体实现细节。
函数定义:相当于兑现了之前所做出的承诺。在函数定义中,你提供了完整的代码实现,让编译器知道如何执行这个功能。只有当给定了完整定义并且提供了可执行代码时,该功能就真正变得可用,并且能够被其他部分调用。
在链接阶段,在已经生成目标文件后(包含已经编写好但未被实际执行过程所需的信息),链接程序会去查找对应所有符号地址以建立最终可执行文件或库文件。
小思考
让我们再回头看看对C++种的函数重载的定义:“在同一个作用域内,可以定义多个具有相同
名称但参数列表(参数的类型、参数的个数、参数类型的顺序)不同的函数”,这说明在C++中可
以存在多个名字相同的函数,而C语言链接阶段寻找函数地址的方法是在符号表中根据函数名找函
数地址,而如果出现多个重名函数那么就无法判断函数的地址谁是谁的。
那么C++是如何处理这一问题的呢?
C++函数名修饰规则
基本概念:一种编译器将函数和变量名转换成特定格式的技术,以便支持函数重载和链接器能够区
分不同作用域下的同名函数
实现方法:为每个函数名引入它们各自的参数类型
注意事项:各个编译器有自己不同的引入方式
Linux中的引入方式
格式:_Z + 函数名字的长度 + 函数名 + 参数类型首字母
void f(int a,char b):call f(?)——> call _Z1fic(?)
void f(int a,char b):call f(?)——> call _Z1fci(?)
int Add(int left,int right):call Add(?)——> call _Z3Addii(?)
double Add(double left,double right):call Add(?)——>call _Z3Addii(?)
- _Z是前缀
- 1和3表示函数名所占的字节数(在字符表中查找时是字符匹配,如果要找的函数名所占字节数为1就不会再花时间去找所占字节数为3的)
- f和Add就是原来的函数名
- ii、ic、ci就是函数中的参数类型的首字母
Windows中的引入方式
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂
小拓展
在Linux中用gcc和g++分别编译两个函数并查看编译后的结果:
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中
~over~