在我们使用C语言的时候,如果我们想用一个函数实现交换两个变量,我们会选择使用指针,但是指针用着是比较麻烦的。特别是在用C语言写二叉树啊、单链表啥的,被指针折磨的够呛。所以引用应运而生。
// 用C语言实现两个数交换
void swap(int* a, int* b){
int tmp = *a;
*a = *b;
*b = tmp;
}
// 使用C++中的引用将两个数进行交换
void swap(int& a, int& b){
int tmp = a;
a = b;
b = tmp;
}
使用引用的时候不需要在变量前面加星号,用起来非常顺畅。
引用的概念
引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间。所以当我们改变引用的值时,那块空间的值也会被改变。
类型& 引用变量名(对象名) = 引用实体;
void Test(){
int a = 10;
int& b = a; // 定义引用类型
printf("%p\n", &a);
printf("%p\n", &b); // 这两条输出语句输出的是一样的地址值
}
注意:引用类型必须和引用实体是同种类型
引用的特性
-
引用在定义时必须初始化
-
一个变量可以有多个引用
-
引用一旦引用一个实体,就不能引用其他实体
-
void Test2(){ int a = 10; // int& aa; 这样的写法是错误的 int& b = a; int& c = a; printf("%p %p %p", &a, &b, &c); // 输出的这三个变量的地址是完全一样的。 }
常引用
故名思意,常引用就是对带有常属性的变量进行引用。在举例子之前有几个注意的点。如果实体是被const
修饰的变量,那么在对它使用引用的时候也需要加上const
,否则就会报错。如果实体不具有常属性,在引用时可以不加const
,也可以加上,这需要看实际情况而定。如果实体类型和引用类型之间存在隐式类型转换,也需要加上const
,因为隐式类型转换的过程会产生一个临时变量,再将这个临时变量赋给左值。而临时变量是一个常量,所以如果引用和实体之间存在隐式类型转换就需要加上const
。
int main(){
const int a = 10; // 变量a具有常性
// int& aa = 0; // 这种情况是权限放大,错误
const int& b = a; // 对const修饰的变量进行引用时也需要加上const
// 权限缩小
int num = 10;
const int& n = num; // 权限缩小,可以这么写是对的
// 如果引用类型和实体之间存在隐式类型转换,也需要加上const
float f = 1.2;
const int& ff = f; // 这样也是可以的
return 0;
}
引用的使用场景
讲了这么多引用的知识点,什么时候才需要我们去使用引用呢?一般来说,引用是用来做参数的,就像文章开头所编写的将两个数交换这样的函数,它的形参就是使用的引用类型。另一个用处就是做函数的返回值,做函数返回值时有一个有趣的用处。
// 引用做参数
void swap(int& num1, int& num2){
int tmp = num1;
num1 = num2;
num2 = tmp;
}
如果是将引用作为返回值的话就需要注意引用的实体在使用完函数之后是否还存在,我们知道一个变量是有声明周期的。例如,在函数内部定义一个变量,在函数使用完之后就会被销毁。因为,在函数内部声明的变量都是在栈上开辟空间的,而在栈上开辟的空间在使用结束后都会被销毁。那么有哪些变量是在函数使用结束后不会被销毁的呢?比如我们定义的静态变量、以及我们使用malloc
函数向堆空间申请的变量就是不会被销毁的,除非我们使用free
函数对其进行释放。但是如果是地址的可以就返回指针就可以。
// 因为n是静态变量,即使函数使用结束了也不会销毁,所以可以返回引用
int& count(){
static int n = 0;
n++;
return n;
}
// 因为这个函数返回的是ret的引用,而引用是别名,当函数执行结束之后,ret就销毁了,所以这里不能使用引用类型的返回
int& add(int a, int b){
int ret = a + b;
return ret;
}
引用在做返回值的时候还有一个妙用,前面说了,引用是对变量取的一个别名,改变引用的值,实体的值也同样会被改变。那么改变函数的返回值,实体也会被改变。
int& count(){
static int num = 1;
return num;
}
// 在main函数中调用这个函数
int main(){
std::cout << count() << std::endl;
count() = 2;
std::cout << count() << std::endl;
return 0;
}
总结:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
传值、传引用效率的比较
以值作为参数或者返回值类型,在传值返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值做参数或者返回值类型,效率时非常低下的,尤其时当参数或者返回值类型非常大时,效率更低。
#include<time.h>
using namespace std;
struct A { // 定义了一个类
int arr[1000] = { 0 };
};
void function1(A a) {}
void function2(A& a) {}
int main()
A a;
// 计算传值传参函数花费的时间
size_t begin1 = clock();
for (int i = 0; i < 100000; i++) {
function1(a);
}
size_t end1 = clock();
// 计算引用传参函数的时间
size_t begin2 = clock();
for (int i = 0; i < 100000; i++) {
function2(a);
}
size_t end2 = clock();
cout << "function1-time:" << (end1 - begin1) << endl;
cout << "function2-time:" << (end2 - begin2) << endl;
return 0;
}
同样的,函数在返回值时,也是将数据拷贝到临时变量中,然后再返回。所以,传值和引用在作为传参以及返回值类型上效率相差很大。
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用一块空间。但是,在底层实现上实际是有空间的,因为引用是按照指针的方式来实现的。
int main(){
int a = 10;
int& b = a;
cout << "&a =" << &a <<endl;
cout << "&b =" << &b <<endl; // 两个输出一模一样
return 0;
}
引用和指针的不同点
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以再任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在
sizeof
中含义不同:引用结果为引用类型的大小,但指针始终时地址空间所占的字节个数- 引用自加即引用的实体增加1,指针自加是指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显示解引用,引用时编译器自己处理
- 引用相对指针使用起来安全一些
—end