目录
1.缺省参数
1.1缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用这个函数时,如果没有传入实参,则采用缺省值作为形参。
void print(int a = 0)
{
cout << a << endl;
}
int main()
{
print();//没有传参时,使用参数的默认值
print(10);//传参时,使用指定的实参。
return 0;
}
输出结果: 0
10
1.2缺省参数的分类及使用
我们可以将缺省参数分为两类
- 全缺省参数
void print(int a = 0,int b=3,int c=5)
{
cout << a <<' ';
cout << b <<' ';
cout << c <<endl;
}
在使用上述代码中的全缺省参数的函数时,我们可以全部使用其给出的缺省参数,也可以从左到右给出实参。
int main()
{
print();
print(7);
return 0;
}
结果:0 3 5
7 3 5
- 半缺省参数
半缺省参数必须从右向左依次给出,不可从左向右给,也不可间隔着给。
void print(int a,int b=3,int c=5)//a没有给出缺省值
{
cout << a <<' ';
cout << b <<' ';
cout << c <<endl;
}
1.3注意点
在使用缺省参数时,我们不可在声明和定义中同时给出缺省参数,这是因为如果你同时给出了缺省值,编译器无法分辨你将缺省值给了谁。
因此,我们建议大家在声明函数时给出缺省值。
另外,大家可能还有一个问题,为什么在使用采用了缺省值的函数时,实参必须从左往右给?为什么在给形参缺省值时,必须从右往左给呢?
问题1:在使用缺省参数时,我们的脑袋可以跳过第一个参数给第二个参数实参,但是编译器不是我们的脑袋,他没办法分辨,而且,再说了,换一个脑袋也不知道你想给第几个参数实参。
问题2:如果我们给形参缺省值时从左向右给,那么你在使用时给出实参,你给的是谁实参呢?是给了赋予了缺省值的参数实参,还是给了没有赋予缺省值的参数实参数呢?编译器又无法分辨了。
2.函数重载
在我们使用语言时,同一个词可能有很多个意思。
譬如背单词时一个单词有好多个意思......
我们希望也可以这样使用函数,因此,函数重载诞生了。
2.1函数重载的定义
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明同名函数,这些同名函数的形参列表(形参个数或形参类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
2.2函数重载的情况分类
- 形参个数不同
void print(int a,int b=3,int c=5)//3个参数
{
cout << a <<' ';
cout << b <<' ';
cout << c <<endl;
}
void print(int a, int b = 3)//两个参数
{
cout << a << ' ';
cout << b << ' ';
}
- 形参类型不同
void print(int a, int b,)
{
cout << a << ' ';
cout << b << ' ';
}
void print(char a, char b,)
{
cout << a << ' ';
cout << b << ' ';
}
- 形参类型顺序不同
void print(int a, char b,)
{
cout << a << ' ';
cout << b << ' ';
}
void print(char a, int b,)
{
cout << a << ' ';
cout << b << ' ';
}
2.3注意
这里需要大家注意的是,缺省值不同不会构成重载,返回类型不同也不会构成重载。
//返回类型不同
int Add(int a, int b)
{
return a + b;
}
double Add(int a,int b)//error
{
return a + b;
}
//缺省值不同
int Add(int a=1, int b=20)
{
return a + b;
}
int Add(int a=1, int b=2)//error
{
return a + b;
}
缺省值不同不会构成重载的原因一者是因为编译器调用时不知道调用哪个函数,二者需要大家了解函数名修饰规则。
返回类型不同不会构成重载便需要大家了解函数名修饰规则了。
2.4函数名修饰规则
我们首先要知道编译与链接时就知道C/C++程序运行起来要经历的四个阶段:
预处理:头文件展开、宏替换、条件编译、去掉注释,生成 .i 的文件。.h的文件直接被展开。
编译: 语法检查(语法分析、语义分析、词法分析)、符号汇总、生成汇编代码,生成.s文件。
汇编: 把汇编代码转换为二进制机器码,形成符号表,生成.o文件。符号表里存放定义函数的地址信息。
链接: 合并目标文件、段表,符号表的合并和符号表的重定位,.o格式的目标文件合并到一起,生成.out/.exe文件。
实际项目通常是由多个头文件和多个源文件构成,在上图中,main.c中调用了 sum.c中定义的sum函数,编译后链接前,main.c的目标文件中并没有Add的函数地址,因此sum是在sum.c中定义的,所以sum的地址在b.o中。那么应该怎么找到这个地址呢?
链接阶段就是专门处理这种问题的,链接器在看到main.o调用了sum后,会到sumo的符号表中找sum的地址,然后链接到一起。
在链接时,面对sum函数,链接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
下面我们可以看看在g++环境下
编译的函数修饰规则:
g++的函数修饰规则相对较简单:_Z+函数名长度+每个参数类型
因此,缺省值和返回值不同不会构成函数重载。
3.引用
3.1引用的概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
打个比方:我本名叫小明,但我在CSDN上叫裤裤。小王和裤裤都是我。
其语法为:
int xiaoming = 3;
类型& 引用变量的名字 = 引用实际名;
int& kuku = xiaoming;
这里,我们先创建了一个叫xiaoming的变量,然后给他取了一个叫kuku的别名。而xiaoming和kuku实际上都是3.
通过打印它们的地址,我们发现它们的地址也是一样的,因此他们共用同一块内存空间。
3.2注意事项
- 由于引用变量和被引用的变量指向同一块空间,所以改变引用变量,被引用的变量也会改变。
这里将引用变量自增1,被引用的实体也自增了1.
- 引用在定义时必须初始化
- 一个变量可以有多个引用
int a = 3;
int& ra=a;
int& raa = a;
- 一个引用一旦初始化一个实体,不可再引用别的实体。
int a = 3;
int b = 5;
int& ra=a;
ra=b;//这时是赋值,而不是引用b
- 无法引用类型不同的实体
3.3常引用
可能大家对无法引用类型不同的实体有所疑惑,为什么要常量限定呢?
现在我们学习一下常引用。
先补充一个知识点:
如果权限是可读可写,那么我们可以在使用时将权限缩小为只读;
如果权限是只可读,那么我们在使用时就不可将权限扩大为可读可写。
简而言之,权限可以平移和缩小,但不可扩大。
使用const修饰的变量为常变量,常变量只可读,不可写。
我们可以通过const修饰引用让其变为常引用。这时引用变量就无法被修改了,根据我们刚刚说的有关权限的知识点,我们可以推导出这个结论:我们只能将常变量赋值给常引用(权限的平移),不能将常变量赋值给引用(权限的扩大)。
const int a = 10;//只可读
int& ra = a;//报错 权限不可扩大为可读可写
const int& ra = a;//正确,权限的平移 可读->可读
int& b = 10;//报错,权限不可扩大 10为可读不可写,b为可读可写
const int& b = 10;//正确,权限的平移
现在我们补充一个知识点,类型不同的赋值会发生类型转换,而类型转换会产生临时变量,而临时变量具有常性,即临时变量只可读。
因此,我们可以写出如下代码:
double d = 12.34;
int& rd = d;//报错,d类型转化产生只可读的临时变量,可读的权限不可扩大为可读可写。
const int& rd = d;//不报错,权限的平移
在学习常引用时,我需要大家再清晰一点:
之所以在使用引用时需要注意权限的大小,是因为它操纵的是一块空间,我们可以在一个可读可写的空间中进行读写操作,但是我们无法在只可读的空间中进行写的操作。但是,我们却可以将只可读的空间中的内容拿出来赋值给一个可读可写的空间。
4.4引用的使用场景
4.4.1作为函数的参数
由于引用操作的是同一块空间,因此我们可以作为函数的参数进行传递,这样可以达到传指针一样的效果。
int swap(int& a, int& b)
{
int tmp = 0;
tmp = a;
a = b;
b = tmp;
}
4.4.2做函数返回值
做函数返回值一定要注意变量的生命周期,要保证返回的变量在函数销毁时生命周期还没有结束,否则可能出现野指针的问题。
int& Count()
{
static int n = 0;
n++;
return n;
}
在这里可以再给大家拓展一个知识点:静态变量只能初始化一次,若出现多次初始化语句,则会在编译时跳过除第一句之外的初始化语句。
现在给大家给出一个问题:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << ret << endl;
return 0;
}//输出什么
答案是:7
这是因为我们采用了引用返回的方式,而c在函数栈帧销毁时也跟着被销毁了。
而我们在第二次调用Add()函数时,会在原来第一次调用Add()函数时调用的空间上重新建立栈帧。
所以我们返回值的c就被覆盖掉了,而ret指向的是c的空间,因此ret的值也发生改变了。
因此这样的情况下我们传值返回即可,传值返回的返回值会在函数栈帧被销毁前放入寄存器。
4.5传值和传引用
以值作为参数或者返回值类型,在传参或者返回期间,函数不会直接传递实参或者将变量本身直接返回,而是拷贝实参形成形参或者返回变量的一份临时拷贝(放在寄存器)。因此用值作为参数或者返回值类型的效率会比较低。
4.6引用和指针的联系与区别
引用的底层实现和指针其实没有区别。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
现在我们观察一下这段代码的汇编:
但引用和指针还是有一定区别的
1.引用是变量的别名,指针是变量的地址
2.引用必须初始化,指针建议初始化
3.引用初始化后不可修改对象,但指针可以
4.引用的大小为引用类型的大小,指针要看平台。
5.没有多级引用,但是有多级指针
6.引用更安全
码字不易,给裤裤一个三连关注评论叭~~