一、引用
1.1 什么是引用?
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。简单来说就是,比如张三,在学校的外号可能叫小张,但是本质上张三和小张都是同一个人,小张只不过是张三的别名而已。
如何定义引用:
类型& 引用变量名(对象名) = 引用实体
例如:int a=10; int& b=a;
那么b就是a的引用。b和a都是指向同一块空间,也就是说,改变a的同时也会改变b,相反,改变b也会改变a。
注意:引用类型必须和引用实体是同种类型的。
1.2 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,不能再引用其他实体
int main() { int a = 10; //int& b;//这种写法是错误的,引用在定义的时候必须初始化 int& b = a;//这种写法才是正确的 int x = 20; int& y = x; int& z = x; printf("%p\n", &x); printf("%p\n", &y); printf("%p\n", &z); return 0; }
1.3 常引用
1.4 引用的使用场景
- 引用可以做参数
- 引用也可以做返回值
1、引用做参数:
相信大家对C语言的指针都已经相当的熟悉了,指针传参也是用得非常多了,但是应该在指针的使用上也吃过不少亏了,指针传参确实可以较少拷贝,提高效率,但是指针使用起来还是非常的繁琐的,传参的时候要取地址,访问的时候要解引用等等。正是基于这样的一些原因,C++才增加了引用,引用做参数既能做到减少拷贝,使用起来又比较简单,不容易出错。用引用做参数可以说是既拥有了指针的优点,又弥补了指针使用复杂的缺点,堪称完美。
下面我们就来看看如何用引用做参数:
//用指针做参数 void swap1(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } //用引用做参数 //引用传参的x是a的引用,y是b的引用 //也就是说x就是a,y就是b,所以在函数 //里面直接交换即可,改变x和y就是改变 //a和b,能达到交换a和b的结果 void swap2(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; cout << "a=" << a << " b=" << b << endl; //用指针做参数 swap1(&a, &b); cout << "a=" << a << " b=" << b << endl; //用引用做参数 swap2(a, b); cout << "a=" << a << " b=" << b << endl; return 0; }
引用可以作为输出型参数。
引用做参数,可以减少拷贝,提高效率。
2、引用做返回值(重点)
以下这段代码的输出结果是多少?
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(10, 20); Add(5, 5); cout << "Add(10, 20) is :" << ret << endl; return 0; }
有人说:“这还不简单吗?10+20当然等于30啊!”确实10+20=30是没错的,但是这里的运行结果真的是30吗?我们来看一下。
这是个什么情况?10+20=10?看来数学是体育老师教的实锤了哈哈哈。我们看一下到底是什么原因导致这个10+20会等于10。
特别需要注意:函数返回时,出了函数作用域,如果返回对象还在(还没还给操作系统),则可以使用引用返回,如果已经还给操作系统了,则必须使用传值返回。(切记)
1.5 传值和传引用效率比较
1.5.1 传值和传引用做参数的性能对比
以值作为参数或者返回值类型,在传参和传返回值期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。比如说返回值是一个很大的对象的时候,拷贝的消耗就太大了。
struct A { int a[10000] = { 0 }; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) { TestFunc1(a); } size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) { TestFunc2(a); } size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
1.5.2 传值和传引用做返回值的性能对比
struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) { TestFunc1(); } size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) { TestFunc2(); } size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); return 0; }
通过上述代码的比较,明显看到传值和传引用做参数或者返回值的时候效率相差很大,显然传引用是更高效的,因为传引用可以减少拷贝。
1.6 引用和指针之间的区别
在语法层面上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。实际在底层实现上是有空间的,因为引用是按照指针方式来实现的。
引用和指针的不同点:
- 引用在概念上是变量的一个别名,指针变量是存储了这个变量的地址。
- 引用在定义是必须初始化,指针不一定要初始化。
- 引用一旦有了它引用的实体之后就不能修改,即不能再引用别的实体,但是指针可以先指针一个变量,后指向另一个变量。
- 引用不能有空引用,但是可以有空指针。
- 引用在sizeof下的大小是这个引用的实体的大小,但是指针是固定大小的,在32位平台下是4个字节,在64位下是8个字节。
- 引用的自加是引用的实体的值增加1,而指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体的方式不同,指针访问实体需要显示地解引用,引用访问实体由编译器处理。
- 引用比指针使用起来更安全,例如引用不会存在越界引用等等的问题。