本章文章的重点
1.什么是引用
2.引用不为人知的细节
3.引用做参数和引用做返回值
4.引用和指针的对比
5.什么是内联函数(inline)
回顾C语言:
学过C语言的同学应该都知道在C语言里面有一个叫做指针的东西,它的功能非常强大,几乎什么事情都可以完成。那么C++不仅兼容了C语言,还创造出了一个叫做引用的全新语法。它的行为和指针很像,但是在语法定义来说引用和指针是完全不同。
1.引用的基本概念
首先,在介绍引用这个概念之前,我们先来想一下这样一个场景:一个人可能在日常生活里有多个称谓.比如你身份证上的名字是你的正式的名字,你的家里人也会给你起一个小名,那么在C++中给一个变量取小名的方式就叫做引用。
引用的基本语法格式如下:
类型 & 别名=变量名;
例:
int a=10;
int& ra=a;
int& b=a;
//这里的ra,b都是a的别名
那么怎么理解引用只是一个别名呢?我们知道,变量其实就是一块空间的标识符,也就是如果变量存在我们就可以找到对应变量的地址.假如说这里的ra和b是独立的变量,那么a,ra,b的地址是各不相同的.下面我们通过调试窗口来观察这个三个变量的地址:
可以看到,这里的a,ra,b的地址都是一样的,也证明了这个的ra,b和a共用一块空间,所以说这也印证了引用是别名
我们接下来尝试对ra进行赋值操作,看看会发生什么.
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
int& b = a;
ra = 20;
cout << "a=" << a << endl;
cout << "ra= " << ra << endl;
cout << "b=" << b << endl;
return 0;
}
程序运行结果如下:
这里我们只改变了ra,但是a和b的值都随着一起变了!因为ra是你的别名,本质上ra还是你这个人,所以对ra的修改就是对a的修改。
那么引用还有如下几个注意点:
1.引用必须初始化!
2.引用只能绑定一个实体,一旦绑定了以后,不能再去绑定其他的实体。
int& ra;//错误,引用必须初始化
int& b=a;
int c=20;
b=c;//这里是赋值操作,而不是让b成为c的别名。
到这里相信大家已经对引用有一定的了解了,那么接下来我们接下来介绍引用的一些细节。
二.引用不为人知的细节
1.const对象的引用
接下来我们来看这样一段代码:
int& ra = 20;
//错误,代码通过不了编译
因为这里的10是一个常量,我们不能对一个常量进行写操作!而你去了一个int&的别名,有可能通过这个别名对数据进行写操作,则是不允许的!
改成const int& 就可以通过了.
const int& ra=10;
//对于const变量也是相同
const int b=20;
int& rb=b;//错误
const int& rb=b;//正确
那么相对来说,上面这两个可能对于还算比较好理解,那么我们来看这么一个例子
int a=10;
int& ra=a;//正确
const int& b=a;//是否正确?
我们编译一下代码:
发现编译通过,这是为什么呢?这里就要提到引用的一个规则:引用的权限只能缩小而不能放大,这里的缩小和放大指的是读写权限。
那么接下来我们来看这样一段代码
double d=2.0;
//int& b=d;错误
const int& rd=d;//是否正确?
对于第一个,肯定很容易知道这个是错误的。但是下面哪个是正确的吗?程序编译结果如下:
编译居然通过了!这是为什么呢?在分析这个之前我们来看这样一个例子:
double d=2.0;
int a=d;
在这个例子里,double类型发生了隐式类型转换,这个转换产生了一个临时变量,而最终a拷贝的就是这个临时变量的值,而是这个临时变量具有常量属性。那么理解了这个机制,我们再来回看这段代码:
double d = 2.0;
const int& rd = d;
那么这里的rd实际绑定的就是d发生类型转换的时候产生的临时变量而这个临时变量具有常量属性,所以我们只能用const int&来接收。
三.引用做参数和引用做返回值
1.引用做参数
那么接下来我们来讲一讲引用函数的参数和返回值里面的应用。
假设现在我们需要一个交换两个整型的函数,那么如果使用C语言的话,我们只能够这么写:
void Swap(int* ra, int* rb)
{
int tmp = *ra;
*ra = *rb;
*rb = tmp;
}
这是C语言中交换两个整数的方式。那么在C++里面我们就可以使用引用来代替指针
void Swap(int& ra, int& rb)
{
int tmp = ra;
ra = rb;
rb = tmp;
}
这段代码同样可以起到交换两个数字的作用,而且传参的时候只要传交换对象自身即可另外引用在传参的效率上也有质量的飞跃:
const int N = 100000;
struct Demo
{
int a[N];
};
void A(Demo x)
{
}
void B(Demo& x)
{
}
//比较拷贝传参和引用传参的效率
void Test()
{
Demo x;
memset(x.a, 0, sizeof(int) * N);
long begin1 = clock();
for (int i = 0; i < N; ++i)
A(x);
long end1 = clock();
cout << "A: " << end1 - begin1 << endl;
long begin2 = clock();
for (int i = 0; i < N; ++i)
B(x);
long end2 = clock();
cout << "B: " << end2 - begin2 << endl;
}
测试结果如下:
不难看出,相对于拷贝传参,引用传参的效率非常高!这就是C++里面推荐使用引用传参的原因.另外有的类型不支持拷贝传参,只能使用引用传参,所以引用传参在C++里面是非常常见的。
2.引用做返回值
那么同样我们可以返回一个引用,我们来看这么一段代码
对文件进行编译:
这里给了一个警告,说不要返回局部变量的引用。因为引用本身是变量的别名,而一旦这个变量销毁了,即使记住别名返回也没有什么意义!而这里能够打印出1只是因为当前函数的栈帧还没有被覆盖!而如果后面调用了其他的函数以后,打印出来的就是随机值了。而如果加上static的话,那么这个变量在函数调用后不会销毁,返回引用就是合法的.
所以尽量不要返回局部变量的引用!如果对象没有被销毁,那么你可以使用引用返回。
我们知道函数调用要建立栈帧,那么当函数调用结束的时候对应的要释放栈帧,那么定义在函数作用域的局部变量就会被回收,里面的内容就会变成随机值.但是很奇怪的是,返回值的函数却能够正确的返回值!和前面的类型转换类似,在返回值的函数里面也是进行了待返回值的拷贝,这个拷贝也具有常量属性!
我们可以用引用来证明这一个过程:
int count()
{
int n = 0;
++n;
cout<<&n<<endl;
return n;
}
int main()
{ //因为返回值的常属性,所以这里用const int&
const int& ret = count();
cout << &ret << endl;
return 0;
}
可以看得出来,这里的n,ret地址是不一样的,那么说明返回的并不是n这个变量,而是具有常量性的n的拷贝。而返回引用的函数也可以认为产生了一个临时变量tmp,只不过这个tmp恰好是n的别名(等价于n)
这里的返回通常是有两种方式:
1.如果变量自身不大,拷贝的临时变量放到寄存器。
2.如果比较大,那么函数在开辟栈帧的时候就会预留空间来保存返回值
四.引用和指针的对比
那么到这里,我们对引用这个语法有了更深一点的了解,那么我们对比一下引用和指针
1. 引用只是别名没有开辟空间,指针是变量
2. 引用必须初始化,指针可以不需要初始化
3. 引用只能绑定一个实体,指针不一定
4. 求引用的大小就是求对象的大小,指针的大小固定
5. 没有多级引用,但是又多级指针。
那么既然引用和指针如此相似,那么二者的底层是实现是怎么样的呢?我们来看对应的汇编代码:
那么我们可以看到指针和引用产生的汇编代码是一模一样,底层实现是相同,但是二者的语义不一样。一个是地址,另外一个是别名
五.什么是内联函数(inline)
我们知道函数调用要创建栈帧,那么创建栈帧是要花费时间和空间,那么如果一个函数很短小并且频繁调用,那么显然写函数的成本太大了。所以我们要改进。
在C语言中里面,使用宏来起到这样的作用:
#define ADD(x,y) ((x)+(y))
那么宏有很多的缺陷:
1.宏是类型不安全的
2.宏在预处理阶段被替代,不支持调试
3.书写繁琐---->最主要的原因
那么C++为了能够简化,引入了内联函数的机制。
用inline修饰的函数,编译器在调用的地方默认展开,不会创建栈帧。
可以看到,这里没有call指令,所以说这个函数直接在调用点展开了。那么在Debug模式下看到这个效果需要尽心两个设置
1.
注意:
1.内联对于编译器来说只是一个建议,具体是否展开由编译器决定
2.类内部的函数默认都是内联的
3.内联不建议定义和声明分离,否则容易造成链接错误。
总之:内联是一种空间换时间的方法,.相对于宏来说安全可以调试而且简洁。最后是否真实在调用点展开由编译器自行决定。
以上就是本文的主要内容,如有不足之处,还望即使指出。