我们上节讲了 C++ 中的引用,那么我们就来看下引用的本质。引用作为变量别名而存在,因此在一些场合可以代替指针。引用相对于指针来说具有更好的可读性和实用性。注意:函数中的引用参数不需要进行初始化!
下来我们来看看 swap 函数的实现对比,如下
void swap(int* a, int* b) // 指针形式的
{
int t = *a;
*a = *b;
*b = t;
}
void swap(int& a, int& b) // 引用形式的
{
int t = a;
a = b;
b = t;
}
那么这块就有个特殊的引用,便是 const 引用了。在 C++ 中可以声明 const 引用,它的格式为 const Type& name = var;const 引用让变量拥有只读属性。当使用常量对 const 引用进行初始化时,C++ 编译器会为常量值分配空间并将引用作为这段空间的别名。使用常量对 const 引用初始化后将生成一个只读变量!
下来我们以代码为例进行分析,看看引用的特殊意义,代码如下
#include <stdio.h>
void Example()
{
printf("Example:\n");
int a = 3;
const int& b = a;
int* p = (int*)&b;
// b = 5;
*p = 5;
printf("a = %d\n", a);
printf("b = %d\n", b);
}
void Demo()
{
printf("Demo:\n");
const int& c = 1;
int* p = (int*)&c;
// c = 5;
*p = 5;
printf("c = %d\n", c);
}
int main(int argc, char *argv[])
{
Example();
printf("\n");
Demo();
return 0;
}
我们在 Example 函数中定义了变量 a,用 b const 引用 a,然后用指针 p 指向 b。然后通过指针 p 改变 b 的值,但是这块 b 是 const 引用,所以不能直接改变 b。我们看看 a 和 b 会是多少。在 Demo 函数中,我们通过 const 引用 c 为 1,并且定义指针 p 指向它。同样不能直接改变 c,但是可以通过指针 p 来改变它的值。我们先来看看通过指针 p 改变后的值是否为 5 呢?看看编译结果
我们看到值已经都改变了,我们再来去掉第 11 和 26 行的注释,看看直接改变 const 引用会怎样?
我们看到报的都是它们是只读变量。那么我们思考下:引用有自己的存储空间吗?我们通过程序来看看
#include <stdio.h>
struct test
{
char& c;
};
int main(int argc, char *argv[])
{
char c = 'c';
char& rc = c;
test r = { c };
printf("sizeof(char&) = %d\n", sizeof(char&));
printf("sizeof(rc) = %d\n", sizeof(rc));
printf("sizeof(test) = %d\n", sizeof(test));
printf("sizeof(r.c) = %d\n", sizeof(r.c));
return 0;
}
我们在第 3 行定义了一个结构体变量 test,但它里面只有一个 char 类型的引用 c。我们来看看这个结构体占用内存吗?编译如下
我们看到引用本身只占用了一个字节,但是结构体 test 占用了 4 个字节的内存。我们猜想它是不是跟指针有某种联系呢?其实引用在 C++ 中的内部实现是一个指针常量。关系如下
注意:a> C++ 编译器在编译过程中用 指针常量 作为引用的内部实现,因此引用所占的空间大小与指针相同;b> 从使用的角度,引用只是一个别名,C++ 为了实用性而隐藏了引用的存储空间这一细节。下来我们通过一个示例代码进行说明
#include <stdio.h>
struct TRef
{
char* before;
char& ref;
char* after;
};
int main(int argc, char* argv[])
{
char a = 'a';
char& b = a;
char c = 'c';
TRef r = {&a, b, &c};
printf("sizeof(r) = %d\n", sizeof(r));
printf("sizeof(r.before) = %d\n", sizeof(r.before));
printf("sizeof(r.after) = %d\n", sizeof(r.after));
printf("&r.before = %p\n", &r.before);
printf("&r.after = %p\n", &r.after);
return 0;
}
我们看到在结构体 TRef 内部只有 3 个成员,两个指针,一个引用。我们通过打印结构体的大小和它的 before 指针和 after 指针的大小和地址来分别看看中间的引用究竟是什么
我们看到结构体总共占 12 个字节的内存,指针 before 和 after 各占 4 个字节,并且他们的地址相差 8,从而双重说明了中间的引用占 4 个字节的内存空间,引用便是指向一个地址的。那么它的本质便是指针了。
那么为什么还要弄个引用来代替指针呢?我们知道在 C 语言中,凡是涉及到指针的操作都是容易出 bug 的地方,因此 C++ 设计了引用来在大部分情况下代替指针。从功能性来说,可以满足大多数的需要使用指针的场合;从安全性来说,可以避免由于操作指针不当而带来的内存错误;从操作性来说,简单易用,又不失功能强大。下面我们来看看函数返回引用的一个示例
#include <stdio.h>
int& demo()
{
int d = 0;
printf("demo: d = %d\n", d);
return d;
}
int& func()
{
static int s = 0;
printf("func: s = %d\n", s);
return s;
}
int main(int argc, char* argv[])
{
int& rd = demo();
int& rs = func();
printf("\n");
printf("main: rd = %d\n", rd);
printf("main: rs = %d\n", rs);
printf("\n");
rd = 10;
rs = 11;
demo();
func();
printf("\n");
printf("main: rd = %d\n", rd);
printf("main: rs = %d\n", rs);
printf("\n");
return 0;
}
我们在 demo 函数里返回了局部变量 d,因此这个肯定会出问题。在 func 函数里返回的加 static 修饰的变量,因此它是会放在全局数据区,不会出错。我们在第 23 和 24 行用 demo 和 func 函数进行初始化,因此这会打印出 d = 0 和 s = 0;在第 27 和 28 行打印 rd 和 rs 的值,因为 demo 函数返回之后 d 会丢失,这时 rd 便是一个野指针了。所以 rd 指向的是一个随机数,但是 rs 还是为 0;第 31 和 32 行分别对 rd 和 rs 进行重新赋值,再次调用 demo 和 func 函数时,d 还是为 0,s 就为 11 了;最后第 38 和 39 行会打印出 rd 为随机数,rs 为 11。我们来看看编译结果和我们分析的是否一致
我们看到它在编译的时候都已经报警告了,打印的结果和我们所分析的是一致的。通过对引用本质的学习,总结如下:1、引用作为变量别名而存在旨在代替指针;2、const 引用可以使得变量具有只读属性;3、引用在编译器内部使用指针常量实现,它的最终本质为指针;4、引用可以尽可能的避开内存错误。
欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。