目录
1、变量名回顾
- 变量是一段实际连续存储空间的别名
- 程序中通过变量来申请并命名存储空间
- 通过变量的名字可以使用存储空间
如图:
思考:
对于一段连续的存储空间只能有一个别名吗?
肯定不是,因此我们可以给同一个连续的存储空间取多个别名,这就诞生了C++中的引用机制
2、引用的概念
- 引用可以看作一个已定义变量的别名
- 引用的语法:Type& name = var;
示例:
exp-1.cpp
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 4;
int& b = a;
b = 5;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
return 0;
}
运行结果:
可见,a和b指向了同一内存块!
Tip:
普通引用在声明时必须用其它的变量进行初始化。
3、引用的意义
- 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
- 引用相对于指针来说具有更好的可读性和实用性
示例:
exp-2.cpp
#include <stdio.h>
void swap_q(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void swap_p(int *pa, int *pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
int main(int argc, char *argv[])
{
int a = 4;
int b = 5;
swap_q(a, b);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("******************************\n");
swap_p(&a, &b);
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
Tip:
引用作为函数参数声明时可不进行初始化。
4、const引用
- 在C++中可以声明const引用
- const Type& name = var;
- const引用让变量拥有只读属性
示例:
exp-3.cpp
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 4;
const int& b = a;
int* p = (int*)&b;
//b = 5; //这里编译器会报错,不能修改只读变量的值
*p = 5; //这里成功修改了变量a的值
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}
运行结果:
Tip:
从运行结果来看,指向变量a的指针还是能成功的改变a的值,所以,此时只是无法通过常量引用b来修改a的值!
- 当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
示例:
exp-4.cpp
#include <stdio.h>
int main(int argc, char *argv[])
{
const int& b = 1; //编译器会为其分配存储空间
int* p = (int*)&b;
//b = 5; //这里会报错,因为常量引用为只读
*p = 5; //还是可以通过指针来更改其值
printf("b = %d\n", b);
return 0;
}
Tip:
使用常量对const引用初始化后将生成一个只读变量。
经典问题:
#include <stdio.h>
int main()
{
int y = 10;
const int z = y; //试图用一个变量给一个常量赋值,来定义一个常量,但是这样做,z真的是一个常量么?
//验证方法:
//取z的地址,让编译器为它分配空间,然后把它的地址强制去掉底层const属性,通过指针向其地址空间赋值,最后看z的输出值是否改变,如果改变说名不是常量,是只读变量!
int *p = const_cast<int*>(&z);
*p = 7;
printf("z = %d\n", z);
printf("*p = %d\n", *p);
return 0;
}
运行结果:
从运行结果来看,上面的z是一个只读变量,而非常量!
#include <stdio.h>
int main()
{
char c = 'c';
char& rc = c;
const int& trc = c; //用一个变量给一个常引用赋值
//已经知道,如果 const int& i = 1; 编译器会为它分配存储空间,从而生成一个只读变量
//而这里也是一样的,trc最终会是一个只读变量
rc = 'a';
printf("c = %c\n", c);
printf("rc = %c\n", rc);
printf("trc = %c\n", trc);
return 0;
}
运行结果:
5、引用的本质
思考:
引用有自己的存储空间吗?
测试:
exp-5.cpp
#include <stdio.h>
struct TRef
{
int& a;
int& b;
};
int main(int argc, char *argv[])
{
printf("sizeof(TRef) = %ld\n", sizeof(TRef));
return 0;
}
运行结果:
Tip:
从上面的结果可知,引用是有存储空间的,那么这又是为什么呢?
- 引用在C++中的内部实现是一个指针常量
- Type& name <==> Type* const name
Improtance:
- C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。
- 从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间,这是C++为了实用性而做出的细节隐藏。
思考:
既然引用变量有自己的存储空间,如何获得它的该空间的地址?
解答:
无法通过“&”取地址符,获得“引用变量”自身的存储空间的地址,通过“&”取地址符得到的是它的目标变量的地址,引用机制在一开始设计时,就没打算让我们操作它本身,对它的所有操作都反应在目标变量上;而且,引用机制很容易被编译器优化,优化后,原来访问引用变量的代码编译成的机器代码,实际上是直接访问原变量,这样的话,编译器甚至可以不为引用变量分配空间,全部直接访问它所引用的原变量。
举例:
exp-6.cpp
#include <stdio.h>
struct TRef
{
int& a;
int& b;
int& c;
};
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
int c = 3;
TRef rA = {a, b, c};
printf("&a = %p\n", &a);
printf("&rA.a = %p\n", &rA.a);
printf("&b = %p\n", &b);
printf("&rA.b = %p\n", &rA.b);
printf("&c = %p\n", &c);
printf("&rA.c = %p\n", &rA.c);
printf("&rA = %p\n", &rA);
printf("sizeof(TRef) = %ld\n", sizeof(rA));
return 0;
}
运行结果:
Tip:
可见,对引用变量取地址,实际上得到的是其目标变量的地址!
6、函数返回引用
- 若返回栈变量
- 不能成为其它引用的初始值
- 不能作为左值使用
- 若返回静态变量或全局变量
- 可以成为其他引用的初始值
- 即可作为右值使用,也可作为左值使用
示例:
exp-7.cpp
#include <stdio.h>
int& f()
{
static int a = 0; //静态局部变量,函数退出后不释放
return a;
}
int& g()
{
int a = 0; //局部变量,函数退出后释放
return a;
}
int main()
{
int a = g();
int& b = g();
f() = 10; //C中编译不过,
//C++中f函数返回的引用可作为左值使用,这一点类似于三目运算符
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("f() = %d\n", f());
return 0;
}
运行结果:
Tip:
首先,g函数执行后,将局部变量的引用返回并赋值给了变量a,所以a的值为0;然后,g函数执行后,将局部变量赋值给一个引用变量b,然后由于g函数执行完毕,系统释放了调用g函数时的栈空间,导致b此时成为了一个未知内存块的引用,所以访问它得到的是随机值;最后,f函数执行后将它的静态局部变量的引用返回,然后作为左值给它赋值,所以值为10。
7、C++对三目运算符做了什么?
- 当三目运算符的可能返回都是变量时,返回的是变量的引用
- 当三目运算符的可能返回中有常量时,返回的是常量值
8、小结
- C++中的引用可以看作变量的别名来使用
- C++中的常引用可以使得一个变量拥有只读属性
- C++中的常引用可以用常量初始化而得到一个只读变量
- C++中引用的本质是一个指针常量