我们上节讲了 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。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。