当前位置: 首页 > news >正文

侯捷 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++ 程序。

 

相关文章:

  • CS2 DEMO导入blender(慢慢更新咯)
  • Mayo Clinic Platform在人工智能医疗领域的现状及启示意义研究
  • 深度学习——图像余弦相似度
  • 基于华为设备技术的端口类型详解
  • 嵌入式八股RTOS与Linux--中断篇
  • vue如何实现前端控制动态路由
  • 基于pycatia的CATIA零部件激活状态管理技术解析
  • Centos7,tar包方式部署rabbitmq-3.7.6
  • C++ 初阶总复习 (16~30)
  • 液压式精密矫平机——以稳定压力,成就工业级平整
  • CVPR-2025 | 南洋理工基于图表示的具身导航统一框架!UniGoal:通用零样本目标导航方法
  • WordPress essential-addons-for-elementor xss漏洞
  • 全排列 II:去重的技巧与实现
  • 深入理解:阻塞IO、非阻塞IO、水平触发与边缘触发
  • 使用FastExcel时的单个和批量插入的问题
  • constant(safe-area-inset-bottom)和env(safe-area-inset-bottom)在uniapp中的使用方法解析
  • 网络安全(一):常见的网络威胁及防范
  • 【动态规划篇】- 路径问题
  • Java算法模板
  • Linux Mem -- 通过reserved-memory缩减内存
  • 启程回家!神十九轨道舱与返回舱成功分离
  • 中国科学院院士张泽民已任重庆医科大学校长
  • 新开发银行如何开启第二个“金色十年”?
  • 贵州茅台一季度净利268亿元增长11.56%,系列酒营收增近两成
  • 探索演艺产业新路径,2万观众走进音乐科技融创节
  • 打造全域消费场景,上海大世界百个演艺娱乐新物种待孵化