【C++杂货铺】引用(二)

简介: 【C++杂货铺】引用(二)

尽管函数中的a是一个静态变量,没有存储在当前函数调用的栈帧中,但是在返回a的时候,还是创建了一个临时的中间变量来存储a。因此可以得出结论:

只要是传值返回,编译器都会生成一个临时的中间变量。

临时的中间变量具有常性。

传引用返回:

 和传值返回不同,传引用返回不需要创建临时的中间变量,但前提是,在函数调用结束,函数栈帧销毁后,返回的变量任然存在。换句话说就是,返回的变量不能存储在函数调用所创建的栈帧中,即返回的变量,不能是普通的局部变量,而是存储在静态区的静态变量,或是在堆上动态申请得到的变量。

局部变量传引用返回存在的问题:

 引用即别名,传引用返回,就是给一块空间取了一个别名,再把这个别名返回。一个局部变量的空间,是函数栈帧的一部分,这块空间会随着函数调用结束,函数栈帧的销毁而销毁,因此给这块空间取一个别名,再把这个别名返回给调用它的地方,这显然是有问题的,因为这块空间已经被释放了,归还给了操作系统。

int& add(int x, int y)
{
  int sum = x + y;
  return sum;
}
int main()
{
  int a = 5;
  int b = 4;
  int ret = add(a, b);
  cout << ret << endl;
  return 0;
}

9c7171c34f01493c9b8773b5bfe915cf.png

 还是上面这个求和代码,sum是一个局部变量,但是传引用返回,结果貌似没有什么问题,这是为什么呢?其实,sum标识的这块空间在函数调用结束,确确实实是归还给了操作系统,但是操作系统并没有将里面存储的内容清理,这就导致打印出来的结果貌似是正确的。可以对上面的代码稍作修改,继续验证:

int& add(int x, int y)
{
  int sum = x + y;
  return sum;
}
int main()
{
  int a = 5;
  int b = 4;
  int& ret = add(a, b);
  cout << ret << endl;
  printf("hello\n");
  cout << ret << endl;
  return 0;
}

c7eacc11ebed4ae98540ec8e681345f3.png

这一次验证,最重要的变化是从int ret = add(a, b);变成了int& ret = add(a, b);,可不要小瞧了这一个&,他让ret变成了引用,即ret从一个独立的变量,变成了一块空间的别名。原本调用add函数,返回sum所标识空间的一个别名,在把这块空间里的内容赋值给ret,而现在,ret也变成了sum所标识空间的别名,为什么要这样做?先看结果,两次打印ret的结果并不相同,第一次侥幸是正确的,因为sum标识的空间在归还给操作系统后,操作系统并没有对这块空间进行清理,接着调用了printf函数,由于函数调用会创建栈帧,sum标识的空间在此次创建的函数栈帧中被重新使用,这就导致里面存储的内容一定会发生改变,此时再去打印ret,结果就是错误的。假如这里的ret不是引用,是无法验证出这个错误的,因为此时ret有自己单独的空间,int ret = add(a, b);就是一次赋值操作,在第一次赋值后,ret就不会再变化,因此两次打印的结果可能侥幸都是正确的,所以需要让ret变成引用。

 上面说了这么多就是想告诉大家,局部变量传引用返回,你的结果可能侥幸是正确的。所以对于局部变量,大家还是老老实实的用传值返回。

引用做返回值的优势:

减少拷贝,提高效率。

可以同时读取和修改返回值(重载[ ]就是利用这个优势)

五、传值、传引用效率比较

 以值作为参数或返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常地下的,尤其是当参数或者返回值类型非常大时,效率就更低。

参数对比:

struct A
{
  int a[100000];
};
void TestFunc1(A a)
{
  ;
}
void TestFunc2(A& a)
{
  ;
}
void TestFunc3(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();
  //以指针作为函数参数
  size_t begin3 = clock();
  for (int i = 0; i < 10000; i++)
  {
    TestFunc3(&a);
  }
  size_t end3 = clock();
  // 分别计算两个函数运行结束后的时间
  cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
  cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
  cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;
}

476babf0ba244ac3809ab95cedb7cfcd.png

其中,A类型里面有一个四十万字节的数组,TestFunc1是值传递,TestFunc2是传引用,TestFunc3是传地址,分别把这三个函数调用一万次,通过结果可以看出,值传递花费的时间最长,并且也是最占用空间的,每次调用TestFunc1函数,都会重新创建一个四十万字节的A类型的变量,来存储实参,而传引用,形参只是给实参所标识的内存空间取了一个别名,并没有创建新的空间,传地址,只会创建一块空间来存储实参的地址,这块空间在32位机下是4字节,在64位机下是8字节。

返回值对比:

struct A
{
  int a[100000];
};
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;
}

6f6c7a2abd6f4662b016feb1e5dd2695.png

 值返回每次都要创建临时的中间变量,这就导致效率下降和空间上的浪费。

六、常引用

6.1 权限放大——不被允许

int main()
{
  const int a = 10;
  int& b = a;//权限放大
  return 0;
}

595408972d8d412c9e5053b30dd7e95b.png


上面代码中,用const定义了一个常变量a,接着想给a取一个别名b,但是编译的过程中报错了:无法从const int 转换为int &。为什么会这样呢?原因是:权限可以平移、缩小,但是不能放大。

 a最初是一个常变量,意味着a一旦定义就不能再修改,而此时引用b出现了,它是a的一个别名,但是它没有加const修饰,意味着可以对b进行修改,这时就相当于权限的放大,这种情况是不允许的。正确的做法是,给引用b加上const进行修饰,即:cconst int& b = a;,此时属于权限的平移。

6.2 权限平移

int main()
{
  const int a = 10;
  const int& b = a;//权限平移
  return 0;
}

2f85abac7ba44d5f89187e9f2dff4995.png

 上面代码中的,给常变量a取一个别名b,这里的b其实就是一个常引用。

6.3 权限缩小

int main()
{
  int a = 10;
  const int& b = a;//错误:权限缩小
  int& c = 10//错误:权限缩小
  return 0;
}

eb123c4590144e0ea4ab39108583340b.png

上面代码中,给一个普通的变量a取了一个别名b,这个b是一个常引用。这意味着,可以通过a变量去对内存中存储的数据进行修改,但是不能通过引用b去修改内存中存储的数据。这就有点类似于:李逵在梁山泊的时候可以喝酒,下了梁山泊以后就叫黑旋风,不能喝酒一样。但是李逵在了梁山泊喝了酒,黑旋风的肚子里一定也是有酒的(黑旋风不能喝酒,管我李逵什么事dog)。这就意味着:通过a去对内存中存储的数据进行修改,b也会跟着变,虽然不能通过b去修改,但是它会跟着变。

int main()
{
  int a = 10;
  const int& b = a;
  cout << "修改前:" << endl;
  cout << "a:" << a << endl;
  cout << "b:" << b << endl;
  a++;
  //b++;//b是一个常引用,不能通过b去修改
  cout << "修改后:" << endl;
  cout << "a:" << a << endl;
  cout << "b:" << b << endl;
  return 0;
}

c4837a073bec4148b9cc76f4d2138181.png

6.4 赋值拷贝不涉及权限问

 上面说到的权限放大,缩小问题,都是在取别名的时候发生的,即在定义一个引用变量的时候。权限的放大、缩小等,针对的必定是同一块空间,不同的空间有自己的权限,不存在权限变更的问题。而引用恰恰就是对同一块空间取了一个别名而已,赋值则是重新创建了一块空间。

int main()
{
  const int a = 10;
  const int b = a;
  int c = a;
  return 0;
}

 如上面的代码,可以把常变量a重新赋值给常变量b或者普通变量c,都是可以的,因为这三个变量标识了三个独立的内存空间,它们之间互不影响。

七、临时变量

在4.2小节中提到,函数的传值返回,会创建一个临时变量,在最后的总结中还写到:临时的中间变量具有常性。这条性质适用于所有的临时变量,不只是传值返回产生的临时变量具有常性,在类型转换(包括但不限于:强制类型转换、隐式类型转换、整型提升、截断)过程中,也会产生临时变量,并且这个临时变量也具有常性。为什么要提这个?

 因为引用是针对同一块空间的操作,引用就是给同一块空间取别名,既然是同一块空间,就逃不了会涉及到权限变化问题,又因为临时变量不经过调试,我们是很难发现的它的存在,并且临时变量很特殊,具有常性,所以,我们需要特别注意哪些可能会产生临时变量的操作。下面举一些可能会产生临时变量的例子:

7.1 传值返回

int Text()
{
  int a = 99;
  return a;
}
int main()
{
  //int& ret = Text();//函数返回会创建临时变量
  const int& ret = Text();//用引用接收必须要加const修饰
  return 0;
}

7.2 类型转换

int main()
{
  double a = 3.14;
  //int& b = a;//错误的类型转换,产生临时变量
  const int& b = a;//正确
  return 0;
}

7.3 传参

void Text1(int& y)
{
  cout << y << endl;
}
void Text(const int& y)
{
  cout << y << endl;
}
int main()
{
  //Text1(1 + 3);//错误
  Text(1 + 3);//正确
  return 0;
}

上面代码中的函数调用Text1(1 + 3);是错误的,因为1 + 3的结果会保存在一个临时变量里面,同时形参是一个引用,相当于要给这个临时变量取一个别名,但这个临时变量具有常性,而这里的引用只是一个普通引用,不是常引用,所以就会涉及权限的放大,导致函数调用出错。Text函数的形参是一个常引用,在调用的时候就不会出错。

八、引用与指针的区别

引用在概念上定义一个变量的别名,指针存储一个变量的地址。

引用在定义时必须初化,指针没有要求。

引用在初始化时引用一个一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。

没有NULL引用,但有NULL空指针。

在sizeof中的含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位机下占四个字节,64位机下占八个字节)。

引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

有多级指针,但是没多级引用。

访问实体方式不同。指针显式解引用,引用编译器自己做处理。

引用比指针使用起来相对更安全。

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!


目录
相关文章
|
5月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
38 3
|
5月前
|
存储 自然语言处理 编译器
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
|
6月前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
70 5
|
6月前
|
C++
C++引用
C++引用
|
5月前
|
C++
C++基础知识(二:引用和new delete)
引用是C++中的一种复合类型,它是某个已存在变量的别名,也就是说引用不是独立的实体,它只是为已存在的变量取了一个新名字。一旦引用被初始化为某个变量,就不能改变引用到另一个变量。引用的主要用途包括函数参数传递、操作符重载等,它可以避免复制大对象的开销,并且使得代码更加直观易读。
|
5月前
|
存储 自然语言处理 编译器
|
5月前
|
安全 C++
|
6月前
|
C++
C++对C的改进和拓展\引用
C++对C的改进和拓展\引用
28 0