🔥🔥本章重内容
C++入门
- 1. 函数重载
- C++是怎么支持函数名重载的呢?
- 2.引用
- 2.1引用特性
- 2.2常引用
- 2.3使用场景
- 1. 做参数
- 2. 做返回值
- 2.4引用和指针的区别
- 3.内联函数
1. 函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表==(参数个数 或 类型 或 类型顺序)==不同,常用来处理实现功能类似数据类型不同的问题。
那函数重载是上面样呢?
列如:
#include <iostream>
using namespace std;
void Add(int a, int b)
{
cout << a + b << endl;
}
void Add(double a, double b)
{
cout << a + b << endl;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
return 0;
}
上面这段代码在C语言中肯定是编不过去的,C语言会报错说,函数重命名
但是C++支持这样写,只要函数的**(参数个数 或 类型 或 类型顺序)**不同就可以。
//参数类型不同
void Add(int a, int b)
{
cout << a + b << endl;
}
void Add(double a, double b)
{
cout << a + b << endl;
}
//参数个数不同
void fun()
{
cout << "fun()" << endl;
}
void fun(int a)
{
cout << "fun(int a)" << endl;
}
//参数顺序不同
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;
}
C++是怎么支持函数名重载的呢?
C++中有函数名修饰规则,但是这个规则不是C++创始人给的,是由写编译器的人来给的,所以Linux与Windows的函数名修饰规则是不同的。
函数名修饰规则会给函数另起一个名字,简单的了解一下汇编代码。
图中箭头是main函数中对应的指令,call的意思是跳转到被调用的函数那里,可以看到两次call他们的地址是不同的,所以C++允许函数名相同,但(参数个数 或 类型 或 类型顺序)不能相同。
看一下输出结果:
注意:如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
2.引用
引用就相当于是起别名,相当于你的大名和小名,它都指的是你。
//类型& 引用变量名(对象名) = 引用实体;
void fun()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
代码中的int& ra = a;就是对a的引用
既然是对a起别名那他们的地址相同吗?
如下图所示:
所以如果我们改变别名,原本的值也会发生改变。
注意:引用类型必须和引用实体是同种类型的
2.1引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
int main()
{
//这样的代码是不行的
int& b;
//一个变量可以有多个引用
int a = 0;
int& c = a;
int& d = a;
//引用一旦引用一个实体,再不能引用其他实体
int x = 5;
int y = 6;
int& z = x;
int& z = y;//错误写法
return 0;
}
前面两种好理解,讲一下后面这种为什么不行。
解释为什么引用一旦引用一个实体,再不能引用其他实体
当我们写了两个
int& z = ;
int& z = ;
编译器会认为 z 是重定义,进行了多次初始化。
就像我们写了两个 int a = ; int a = ;这样肯定是不行的。
如果我们写成
int& z = x;
z = y;
这相当于我们把y的值赋给了z
所以说:引用一旦引用一个实体,再不能引用其他实体
2.2常引用
什么是常引用呢,就是我们引用的是常量。
先来说结论,大家来判断下面的代码是否正确。
结论:引用过程中权限可以平移或缩小,但不能扩大。
int main()
{
//问题一
const int a = 10;
int& ra = a;
//问题二
int b = 10;
const int& rb = b;
//问题三
double d = 1.1;
int& rd = d;
return 0;
}
前两个问题大家都可以根据结论判断它是否正确。
问题一是错误的,因为它将a的权限扩大了,原本a是const的常量,我们在起别名的时候只能是常量别名。
问题二是正确的,我们说权限可以缩小或平移,所以别名可以加上const。
问题三呢?它是给一个double类型的变量d起别名,但别名的类型是int类型。
有的同学可能会说,给double类型的数据起别名只能用double类型。
那我们先来看结果。
为什么会有警告说是从“double”转换到“const int”呢?
所以会有上面那样的警告。
我们说权限可以缩小和平移,如果我们给int前加上const那么程序是不是就可以正常运行了?
答案是正确的。
2.3使用场景
1. 做参数
用引用参数,可以代替我们之前使用的指针。
列如我们要交换两个数的值
//指针写法
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
//引用写法
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
我们发现引用使用起来更方便,使用指针有时候我们可能还会传值错误,忘记传地址过去。
但我们使用引用的话,根本不需要去考虑传地址的问题。
引用直接给变量起别名,使用起来更方便。
2. 做返回值
那我们先来回忆一下之前C语言用类型做返回值。
如果返回值的内存占用大,我们直接返回引用值。
如果是返回引用的话,可以直接跳过拷贝,赋值给a。减少时间和空间的使用。
那我们以后是所有带返回值的函数都要用引用返回吗?
当然不是。
就比如上面那段代码如果用引用值返回的话,会带来后果呢?
图片中的输出结果看似没有问题,但实际是有问题的。
我们返回c的别名,但函数调用结束后,栈帧会被销毁,空间会被释放掉。
这里打印ret的值是不确定的
如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的
如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值
当我们再调用依次其他函数,或再多写几行代码,之前的函数的数据会被覆盖。
那怎么证明呢?
我们用引用来接收返回值。相当于是返回值的别名。
因为a是返回值的别名,所以后面再调函数,会改变a的值。
当我们调用其它函数后,它会覆盖掉当前的函数。所以a会变为随机值。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。
2.4引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。(他们的地址相同我们有证明过)。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
可以看到引用的汇编代码与指针的汇编代码是相同的,所以引用也会开辟一个指针的空间
32位平台下开4个字节,64位平台下开8个字节
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
3.内联函数
内联函数是怎么来的呢?
我们先看一段代码。
void Print(int i)
{
cout << i << endl;
}
int main()
{
for (int i = 0; i < 100; i++)
{
Print(i);
}
return 0;
}
这个函数会被调用100次,函数调用建立栈帧的开销会比较大。
而内联函数是在调用函数时直接在被调用的位置上展开的,不会开辟栈帧。
只需要在函数前加一个inline。这个函数就可能会成为内联函数。
//内联函数写法
inline void Print(int i)
{
cout << i << endl;
}
在我们看汇编代码时,它为什么还是会call(调用函数)呢?
因为我们在debug版本下,debug模式下,编译器默认不会对代码进行优化。
但我们也可以对其进行修改,使inline可以在debug版本下进行优化。
修改完成之后便可以观察汇编代码,看其是否以优化。
所以这就是内联函数的优点,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
既然内联函数没有函数调用建立栈帧的开销,又可以提升运行效率那我们是不是应该全去使用内联函数呢?
如果我们都去使用内敛函数的话,上图会产生1000 * 50行代码,
不用内联函数的话就只会有1000行调用代码和20行Add的代码。
所以如果函数内容代码量大的话,我们就不能使用内联函数。
如果函数内代码行数大于10,我们就不能使用内联函数。
使用内联函数还需要注意一个点,就是内联函数的声明和定义必须放在一起。
如果像我们这样写程序是过不去的。
因为内联函数是没有地址的,当我们链接时发现找不到fun函数,内联函数不会函数名修饰。
所以我们要把内联函数的定义与声明放在一起写。
当我们把声明和定义都写在头文件里时,程序就可以链接上了。
特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到