C++的引用
前言
C++的引用是别名,它为已存在的对象提供了另一个名称。一旦引用被初始化指向一个对象,它就不能再指向其他对象。引用必须在声明时初始化,并且必须初始化为有效的对象或字面量。引用通常用于函数参数和返回值,以实现按引用传递和返回。此外,它们也常用于大型对象和数组,以避免复制的开销。C++11引入了右值引用和移动语义,允许更高效的资源管理和性能优化。总的来说,C++的引用是一种强大的工具,能够增强代码的可读性和性能。
一、C++引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
在C++中,引用是一个别名,用于已经存在的变量或对象。引用提供了对变量的间接访问,通过引用,可以通过不同的名称来访问同一变量。
类型&
引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注意:引用类型必须和引用实体是同种类型的
引用的定义使用&符号,如下所示:
int x = 10;
int &ref = x;
在这个例子中,ref
是x
的引用,它是x
的别名。现在,ref
和x
可以互换使用,任何对ref
的更改将反映在x
上,反之亦然。
二、引用特性
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
引用有以下几个特点:
- 引用必须在定义时进行初始化,一旦初始化完成后,就无法更改引用的绑定。
- 引用必须与其所引用的对象具有相同的类型。
- 引用可以作为函数的参数和返回值,通过引用参数传递参数可以避免复制大型对象的开销。
- 一个变量可以有多个引用
引用与指针不同,指针是一个对象,可以指向任何其他对象,而引用始终指向同一个对象。另外,引用在使用时不需要解引用操作符(*
),因为它本身就是对象的别名。
引用的使用可以简化代码并提高可读性,它常用于函数参数传递、函数返回值、以及在循环中使用。
void increment(int& i) {
i++;
}
int main() {
int x = 10;
increment(x);
cout << x; // 输出11
return 0;
}
在上面的例子中,increment
函数接受一个引用参数i
,对该引用进行递增操作。在main
函数中,将变量x
传递给increment
函数后,x
的值被递增为11。因为参数是引用类型,所以对i
的修改会直接影响到x
。
交换
指针
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main{
int x = 0, y = 10;
cout << x << " " << y << endl;
Swap(&x, &y);
cout << x << " " << y << endl;
return 0;
}
引用
void _Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main{
int x = 0, y = 10;
cout << x << " " << y << endl;
_Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
三、常引用
保证值不变
C++中的常引用是指使用const
关键字修饰的引用,即被引用的对象不能被修改。常引用和普通引用的主要区别在于,常引用所引用的对象在引用过程中不能被修改。
常引用的语法形式如下:
const T& ref;
其中,T
是被引用对象的类型。常引用可以指向任何类型的对象,包括基本类型、自定义类型、指针等。
常引用在函数参数传递中很常用,可以用于避免拷贝大对象,同时又不希望对对象进行修改。在函数定义时,使用常引用作为参数,可以防止函数对参数进行修改。
需要注意的是,引用作为函数参数时,函数内部对引用的修改也会反映到函数外部的变量上。
void print(const int& i) {
cout << i;
}
int main() {
int x = 10;
print(x);
return 0;
}
在上述例子中,print
函数接受一个const
引用参数i
,这意味着i
不可修改。在main
函数中,将变量x
传递给print
函数后,print
函数无法修改x
的值。这样做可以确保函数不会意外地修改传递给它的参数。
权限的方法
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
权限的放大
const int a = 10;
int& ra = a; // 该语句编译时会出错,a为常量
int p = a;
这样是可取的,因为这是赋值语句
如上const int a = 10;
按照语法来理解是可读但不可写,我们使用一个可读可写的int
类型来引用就会报错,所以这种方法是不可取的
const int* a = &m;
int* ra = a;
这也是权限的放大,不可以
权限的缩小
int a = 10;
const int& ra = a;
如上 int a = 10;
按照语法来理解是可读可写,我们使用一个可读的const int
类型来引用是没有问题的,所以这种方法是可取的
int p = a;//p = ra;
这样是可取的,因为这是赋值语句
注意:a++
,ra
也会++
,因为ra
是a
的别名,但是ra
不能++
,因为ra
是常量,总结来说就是不能通过ra
来修改
int* a = &m;
const int* ra = a;
这也是权限的缩小,可以
权限的平移
int a = 10;
int& ra = a;
int& rra = a;
const int b = 10;
const int& rb = b;
如上 int a = 10;
按照语法来理解是可读可写,我们使用一个可读可写的int
类型来引用是没有问题的,所以这种方法是可取的,同理const
的修饰也是一样的
类型转换
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
如上为什么int& rd = d;
不行,而 const int& rd = d;
确可以,是因为类型转换会生成临时变量,类型转换是将一个数据类型的值转换为另一个数据类型的值,而不是直接修改原始值。因此,在执行类型转换时,会创建一个新的变量来存储转换后的值,并且可以在需要的地方使用。这时的临时变量是具有常性的,使用int& rd = d;
就相当于权限的放大,会报错,需要使用const
修饰
临时变量
int x = 0,y = 1;
//int& p = x + y;是不可以的
const int& p = x + y;//是可以的,和上面一样是临时变量的原因
除了类型转换之外,还有以下几种情况会生成临时变量:
- 函数返回值:当一个函数返回一个临时变量时,编译器会在函数结束时生成一个临时变量,并将其复制到函数返回的地方。
- 表达式计算:在进行表达式计算时,如果表达式中包含临时变量的创建和销毁,编译器会在需要的地方生成临时变量。
- 函数调用:当调用函数时,会将实参传递给形参。如果实参的类型与形参的类型不匹配,编译器可能会生成临时变量来进行类型转换。
- 对象初始化:当创建对象时,如果使用了拷贝构造函数,编译器会生成一个临时变量来初始化新对象。
- 运算符重载:当重载一个运算符时,可能会生成临时变量来进行操作。
需要注意的是,编译器为了优化性能可能会对临时变量进行优化,比如使用编译器自动生成的构造函数、析构函数等。因此,生成临时变量并不一定会带来显著的性能损耗。
四、引用的使用场景
1. 做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
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 << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
五、传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++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;
}
值和引用的作为返回值类型的性能比较
#include <time.h>
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()
{
//TestRefAndValue();
TestReturnByRefOrValue();
return 0;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
六、引用和指针的区别
引用和指针的注意点
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有
NULL
引用,但有NULL
指针 - 在
sizeof
中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节) - 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
注意:
int* ptr = NULL;
int& r = ptr;
这样编译器是不会报错的
int main()
{
int* ptr = NULL;
int& r = *ptr;
cout << r << endl;
return 0;
}
这样才会报错,因为指针的解引用是在使用的阶段,在底层是r
存放的是ptr
的地址
七、测试代码展示
&.cpp
#include <iostream>
using namespace std;
//void TestRef()
//{
// int a = 10;
// int& ra = a;//<====定义引用类型
// printf("%p\n", &a);
// printf("%p\n", &ra);
//}
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
void increment(int& i) {
i++;
}
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void _Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
//int main()
//{
// //TestRef();
// //int x = 10;
// //increment(x);
// //cout << x; // 输出11
// int x = 0, y = 100;
// cout << x << " " << y << endl;
// //Swap(&x, &y);
// _Swap(x, y);
// cout << x << " " << y << endl;
// return 0;
//}
//void print(const int& i) {
// cout << i;
//}
//
//int main() {
// int x = 10;
// /*int a = 10;
// const int& ra = a;
// int p = a;*/
// const int a = 10;
// //int& ra = a; // 该语句编译时会出错,a为常量
// int p = a;
// print(p);
// return 0;
//}
//int& Add(int a, int b)
//{
// int c = a + b;
// return c;
//}
//int main()
//{
// int& ret = Add(1, 2);
// Add(3, 4);
// cout << "Add(1, 2) is :" << ret << endl;
// return 0;
//}
#include <time.h>
//struct A { int a[10000]; };
//void TestFunc1(A a) {}
//void TestFunc2(A& a) {}
//void TestRefAndValue()
//{
// A a;
// // 以值作为函数参数
// size_t begin1 = clock();
// for (size_t i = 0; i < 10000; ++i)
// TestFunc1(a);
// size_t end1 = clock();
// // 以引用作为函数参数
// size_t begin2 = clock();
// for (size_t i = 0; i < 10000; ++i)
// TestFunc2(a);
// size_t end2 = clock();
// // 分别计算两个函数运行结束后的时间
// cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
// cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
//}
//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()
//{
// //TestRefAndValue();
// TestReturnByRefOrValue();
// return 0;
//}
int main()
{
int* ptr = NULL;
int& r = *ptr;
cout << r << endl;
return 0;
}