侯捷 C++ 课程学习笔记:C++ 中引用与指针的深度剖析
目录
一、引言
二、引用与指针的基本概念
(一)引用
(二)指针
三、引用与指针的区别
(一)定义与初始化
(二)内存空间与 NULL 值
(三)自增操作
(四)多级情况
(五)访问实体方式
(六)安全性
(七)sizeof 操作
四、引用与指针在函数中的应用
(一)作为函数参数
(二)作为函数返回值
五、 auto 关键字的应用
(一)类型自动推导
(二)简化迭代器声明
(三)在范围 for 循环中的应用
1. 遍历数组:
2. 遍历容器:
六、代码示例分析
(一)类型转换与引用
(二)函数性能测试
(三)返回值问题
七、总结
一、引言
在 C++ 编程中,引用和指针是两个极为重要且容易混淆的概念。它们在很多场景下都能实现相似的功能,但在底层原理、使用方式和特性上又存在诸多不同。深入理解它们的区别,对于写出高效、安全且正确的 C++ 代码至关重要。本文将结合代码示例,从多个维度对引用和指针进行剖析。同时,也会引入 auto 关键字相关内容,进一步丰富对 C++ 语言特性的理解。
二、引用与指针的基本概念
(一)引用
引用在概念上是为一个已存在的变量取的别名,它和被引用的变量共用同一块内存空间。例如:
cpp
int a = 10;
int& ra = a;
这里 ra 就是 a 的引用,对 ra 的操作等同于对 a 的操作 。从语法层面看,引用不开辟新的空间,只是给 a 起了个别名。但在底层实现上,引用其实是按照指针的方式来实现的,这一点从汇编指令可以看出:
asm
// 假设 int a = 10; int& ra = a;
// 以下是相关汇编指令
lea eax,[a]
mov dword ptr [ra],eax
(二)指针
指针用于存储变量的地址,通过该地址可以间接访问所指向的变量。例如:
cpp
int a = 10;
int* pa = &a;
这里 pa 存储了 a 的地址,要访问 a 的值,需要通过解引用操作 *pa 。从语法层面看,指针会开辟空间来存储地址。
三、引用与指针的区别
(一)定义与初始化
- 引用:定义时必须初始化,因为它是变量的别名,初始化后就不能再引用其他实体。例如 int& ra; 这样的定义是错误的,必须写成 int a = 10; int& ra = a; 。
- 指针:定义时不要求必须初始化,可以先定义 int* pa; ,后续再让它指向合适的变量,如 pa = &a; ,并且指针可以在不同时刻指向不同的同类型实体 。
(二)内存空间与 NULL 值
- 引用:没有独立的内存空间(语法层面),和被引用变量共用空间,并且不存在 NULL 引用 。
- 指针:有自己独立的内存空间用于存储地址,存在 NULL 指针,如 int* p = NULL; ,表示指针不指向任何有效的内存地址。
(三)自增操作
- 引用:自增操作等同于对被引用的实体进行自增。例如 int a = 1; int& ra = a; ra++; ,执行后 a 的值变为 2 。
- 指针:自增操作是让指针向后偏移一个其所指向类型大小的字节数。比如 int* p = &a; p++; ,假设 a 地址为 0x1000 , int 类型占 4 个字节,那么 p 自增后地址变为 0x1004 。
(四)多级情况
- 引用:不存在多级引用的概念,只有一级引用。
- 指针:有多级指针,例如 int** pp; ,表示指向指针的指针。
(五)访问实体方式
- 引用:编译器自动处理对引用实体的访问,直接使用引用变量名就相当于访问被引用的实体,如 int& ra = a; ra = 5; ,就直接修改了 a 的值。
- 指针:需要显式使用解引用操作符 * 来访问所指向的实体,如 int* pa = &a; *pa = 5; 。
(六)安全性
- 引用:由于不能为 NULL 且一旦初始化就固定指向一个实体,所以使用起来相对更安全,减少了一些因空指针等问题导致的错误。
- 指针:如果使用不当,比如访问了 NULL 指针或者野指针,容易导致程序崩溃等严重问题。
(七)sizeof 操作
- 引用: sizeof 一个引用,得到的是被引用类型的大小。例如 int& ra = a; sizeof(ra) 结果为 4 (假设 int 占 4 字节)。
- 指针: sizeof 一个指针,得到的是指针所在地址空间占用的字节数,在 32 位平台下通常为 4 字节,64 位平台下通常为 8 字节。
四、引用与指针在函数中的应用
(一)作为函数参数
- 引用作为参数:
可以避免对实参的拷贝,提高效率,尤其是对于大对象。例如:
cpp
struct A { int a[10000]; };
void TestFunc2(A& a){}
这里使用引用传递 A 类型对象,不会产生对象的拷贝。而且通过引用参数可以在函数内部修改实参的值,常用于实现交换函数等,如:
cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
- 指针作为参数:
也能实现避免拷贝和修改实参的功能。例如:
cpp
void TestFunc(A* a){}
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
不过使用指针作为参数时,需要注意指针是否为 NULL ,要进行必要的判空检查,否则容易引发程序错误。
(二)作为函数返回值
- 引用作为返回值:
如果返回的是局部变量的引用,会导致未定义行为,因为局部变量在函数结束后生命周期结束。但如果返回的是静态变量或者成员变量的引用,则是合法的。例如:
cpp
int& Count()
{
static int n = 0;
n++;
return n;
}
引用返回的优势在于避免了返回值的拷贝,提高性能。特别是对于大对象的返回,性能提升明显。比如:
cpp
struct A { int a[10000]; };
A& TestFunc2() { return a;}
- 指针作为返回值:
同样不能返回指向局部变量的指针,因为局部变量生命周期结束后指针会变成野指针。指针返回常用于动态内存分配的场景,比如函数返回一个新分配内存的指针:
cpp
int* AllocateMemory()
{
return new int(10);
}
使用指针返回值时,调用者需要注意内存的释放,防止内存泄漏。
五、 auto 关键字的应用
(一)类型自动推导
auto 关键字可以根据右边表达式自动推导变量的类型,使代码更加简洁。例如:
cpp
int a = 0;
int b = a;
auto c = a; // 根据右边的表达式自动推导c的类型
auto d = 1 + 1.11; // 根据右边的表达式自动推导d的类型
std::cout << typeid(c).name() << std::endl;
std::cout << typeid(d).name() << std::endl;
在上述代码中, c 的类型会被推导为 int , d 的类型会被推导为 double ,因为 1 + 1.11 的运算结果是 double 类型。
(二)简化迭代器声明
当处理容器类型时,使用 auto 可以简化迭代器的声明,尤其是对于类型很长的迭代器声明。比如:
cpp
std::vector<int> v;
// 类型很长
// std::vector<int>::iterator it = v.begin();
// 等价于
auto it = v.begin();
std::map<std::string, std::string> dict;
// std::map<std::string, std::string>::iterator dit = dict.begin();
// 等价于
auto dit = dict.begin();
这样可以让代码更简洁易读,减少出错的可能性。
(三)在范围 for 循环中的应用
范围 for 循环是 C++ 提供的一种语法糖,结合 auto 可以方便地遍历数组或容器。
1. 遍历数组:
cpp
int arr[] = { 1, 2, 3, 4, 5 };
// 传统方式通过下标遍历并修改数组元素
for (int i = 0; i < sizeof(arr) / sizeof(int); ++i)
arr[i] *= 2;
// 使用指针遍历并输出数组元素
for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); ++p)
std::cout << *p << " ";
std::cout << std::endl;
// 使用范围for循环结合auto遍历并输出数组元素
for (auto e : arr)
{
std::cout << e << " ";
}
std::cout << std::endl;
// 使用范围for循环结合auto&修改数组元素
for (auto& e : arr)
{
e *= 2;
}
在上述代码中, for (auto e : arr) 会依次将数组 arr 中的元素赋值给 e 进行遍历,而 for (auto& e : arr) 中 e 是引用,通过它可以修改数组中的元素。
2. 遍历容器:
对于容器(如 vector 、 map 等),同样可以使用范围 for 循环结合 auto 进行遍历。例如:
cpp
std::vector<int> vec = { 10, 20, 30 };
for (auto num : vec)
{
std::cout << num << " ";
}
std::cout << std::endl;
std::map<std::string, int> m = { {"one", 1}, {"two", 2} };
for (const auto& pair : m)
{
std::cout << pair.first << ": " << pair.second << " ";
}
std::cout << std::endl;
在遍历 map 时,使用 const auto& 可以避免不必要的拷贝,并且防止在遍历过程中意外修改 map 中的键值对。
六、代码示例分析
(一)类型转换与引用
cpp
double dd = 1.11;
int ii = dd; // 隐式类型转换,截断小数部分
const int& rii = dd;
这里 int ii = dd 是普通的类型转换,将 double 类型的 dd 转换为 int 类型。而 const int& rii = dd ,编译器会创建一个临时的 int 变量,将 dd 的值转换后存入临时变量,然后 rii 引用这个临时变量。
(二)函数性能测试
在这个示例中, TestFunc1 以值传递方式接收参数,每次调用函数都会对 A 类型的对象进行拷贝,开销较大;而 TestFunc2 以引用传递方式接收参数,避免了拷贝,运行效率更高。通过计时测试可以明显看出两者在性能上的差异。
(三)返回值问题
cpp
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;
}
此代码中 Add 函数返回局部变量 c 的引用,这是错误的。因为 c 在函数结束后生命周期结束, ret 引用的是一块已经无效的内存,后续的输出结果是未定义的,可能会导致程序崩溃等问题。
七、总结
引用和指针在 C++ 中各有特点和用途。引用语法简洁、使用安全,常用于避免拷贝和作为函数参数、返回值来提高性能;指针功能强大、灵活,适用于动态内存管理等场景,但使用时需要更加谨慎,注意空指针、野指针等问题。而 auto 关键字则为 C++ 代码带来了类型推导的便利,简化了代码书写,特别是在处理复杂类型和容器遍历方面。在实际编程中,根据具体的需求和场景,合理选择使用引用、指针和 auto ,能够编写出更高效、健壮且简洁的 C++ 程序。