搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第8章 引用与const限定

第8章 引用与const限定

入门 👤 已掌握指针基础,希望写出更安全高效代码的开发者 🔧 函数参数传递、避免拷贝、定义不可修改的常量
本章你将学到
  • 引用的声明、初始化和基本用法
  • 引用与指针的核心区别与选用原则
  • const关键字修饰变量、指针和引用的不同效果
核心概念引用const限定符

指针给了你直接操作内存地址的能力,但随之而来的是空指针、野指针和复杂的语法。有没有一种方式,既能像指针一样高效地“指向”某个对象,又更安全、更直观?这就是引用(Reference)的用武之地。而const,则是你代码中的“安全锁”,它能明确告诉编译器和你自己:哪些数据是只读的,不能被修改。

8.1 引用:变量的“别名”

生活类比:你的本名是“张三”,在家里小名叫“三儿”。无论别人叫“张三”还是“三儿”,指的都是你这个人。技术定义:引用是为一个已存在的变量起的另一个名字,它和原变量共享同一块内存地址,对引用的操作直接影响原变量。

8.1.1 引用的声明与使用

声明引用时,使用&符号,并且必须在声明时进行初始化,绑定到一个已存在的对象。一旦绑定,终身不变。

int value = 10;
int& ref = value; // ref是value的引用,即别名

ref = 20; // 通过引用修改值
cout << value; // 输出 20,原变量也被修改
cout << &value << " " << &ref; // 输出相同的地址

关键特性

  • 必须初始化:引用在诞生时就必须指明它是谁的别名,不能先声明后赋值。
  • 绑定不可变:一个引用一旦绑定到某个变量,就不能再绑定到其他变量。
  • 无空引用:不存在“空引用”的概念,这比指针安全。
  • 无引用的引用:C++不支持引用的引用(但可以通过类型别名实现类似效果)。

8.1.2 引用 vs 指针:一张表看清本质

理解了引用的基础,我们立刻把它和上一章学的指针放在一起对比。这是C++面试的经典问题,也是实际开发中选择的依据。

特性引用 (Reference)指针 (Pointer)
初始化要求必须在声明时初始化可以声明时不初始化(危险!)
能否为空不能为空,总代表某个对象可以为nullptr或NULL
能否重新绑定不能,一旦绑定终身不变可以,指向不同地址
访问语法像普通变量一样使用(ref = 5)需要通过解引用(*ptr = 5)
内存占用通常不占用额外存储(由编译器实现)占用独立内存存储地址(通常4或8字节)
多级间接不支持支持多级指针(int**)
主要用途函数参数传递、返回值,创建别名动态内存管理、数组遍历、复杂数据结构

我在项目中踩过这个坑:早期为了“灵活”,大量使用指针作为函数参数,结果团队里新手经常忘记检查空指针,导致程序崩溃。后来我们制定规范:如果函数参数目的只是读取或修改传入的对象,且不允许为空,优先使用引用。这从语法层面就杜绝了空值问题。

常见误区

  • 误区一:引用是“安全版的指针”。不完全对。引用有更严格的语义(必须初始化、不可重绑定),这带来了安全性,但也失去了指针的灵活性(如链表遍历)。
  • 误区二:引用不占内存。这是实现细节。在语法层面,你不需要关心它是否占内存;在底层,编译器可能会用指针实现引用,但这不影响你的代码逻辑。
  • 误区三:可以用引用实现链表。不行!因为引用不能重绑定,而链表节点的next需要指向不同的节点。

8.2 const限定符:定义“只读”规则

const关键字用于定义一个值不可被修改的对象。它就像给你的变量贴上一个“只读”标签,编译器会帮你检查任何试图修改它的操作。

const int MAX_SIZE = 1024; // 定义一个常量
// MAX_SIZE = 2048; // 错误!不能修改const变量

const double PI = 3.1415926;
int radius = 5;
double area = PI * radius * radius; // 正确,只是读取

实际开发中,我推荐用const替换C语言风格的#define宏定义常量。因为const变量有明确的类型,会参与类型检查,并且会进入符号表,调试时可以看到它的值。而宏只是简单的文本替换。

编者提示:关于#define宏的详细弊端以及const的优势,我们会在第23章《命名空间与作用域》中深入讨论,包括宏可能引发的命名污染和调试困难问题。

8.2.1 const与指针的组合:四种情况

当const遇到指针,位置不同,含义天差地别。这是C++语法的一个难点,但只要你记住“const修饰它左边的东西,如果左边没东西,就修饰右边的东西”这个口诀,就能轻松破解。

const int* p
→
指针指向的内容是常量
int const* p
→
同上(const在*左边)
int* const p
→
指针本身是常量
const int* const p
→
指针和指向的内容都是常量

我们用具体代码来验证:

int a = 10, b = 20;

// 情况1:指向常量的指针(指针可以变,指向的值不能变)
const int* p1 = &a;
// *p1 = 30; // 错误!不能通过p1修改a的值
p1 = &b; // 正确,可以让p1指向b

// 情况2:常量指针(指针不能变,指向的值可以变)
int* const p2 = &a;
*p2 = 30; // 正确,a现在等于30
// p2 = &b; // 错误!p2不能再指向别的地址

// 情况3:指向常量的常量指针(都不能变)
const int* const p3 = &a;
// *p3 = 40; // 错误!
// p3 = &b; // 错误!

记忆技巧

从右向左读声明:int* const p读作“p是一个常量,这个常量是指针,指向int”。const int* p读作“p是一个指针,指向int常量”。

8.3 引用与const的强强联合

引用本身不能重绑定,但如果加上const,我们主要约束的是不能通过这个引用来修改目标对象。这产生了两种主要用途:常量引用和函数参数传递。

常量引用

  • 绑定到常量或非常量
  • 承诺“只读不写”
  • 可延长临时对象生命周期

函数参数传递

  • 避免大对象拷贝开销
  • 防止函数内部意外修改
  • 可接受常量和非常量实参

8.3.1 常量引用

声明为const T&的引用,不能用来修改它绑定的对象。但它可以绑定到常量、非常量,甚至临时对象(右值)。

int x = 100;
const int& cref1 = x; // 正确,绑定到非常量
// cref1 = 200; // 错误!不能通过常量引用修改

const int y = 200;
const int& cref2 = y; // 正确,绑定到常量

// 绑定到临时对象(右值)
const int& cref3 = 300; // 正确!字面量300是临时对象
// int& bad_ref = 300; // 错误!非常量引用不能绑定右值

这里有个隐藏的坑:常量引用绑定临时对象时,会延长该临时对象的生命周期,使其生命周期与引用相同。这是C++语言的特殊规则,非常有用。

8.3.2 函数参数中的常量引用

这是const引用最实用、最高频的场景。当函数只需要读取参数值,而不需要修改它时,使用常量引用作为参数。

// 不好的设计:传值,大对象会有拷贝开销
void printStudentBad(Student stu) {
    cout << stu.name << ", " << stu.age;
}

// 好的设计:传常量引用,零拷贝,且安全
void printStudentGood(const Student& stu) {
    // stu.age = 25; // 错误!编译器阻止你意外修改
    cout << stu.name << ", " << stu.age;
}

// 调用
Student s {"Alice", 20};
printStudentGood(s); // 传递对象本身,没有拷贝
printStudentGood(Student{"Bob", 22}); // 甚至可以传递临时对象!

我在项目中强制要求:所有自定义类型(结构体、类)作为函数参数时,如果不需修改,必须使用const T&。对于内置类型(int, double等),由于拷贝开销极小,传值或传引用均可,但为了代码一致性,也推荐使用const T&。

定义函数参数
→
是否需要修改参数?
→
参数类型是否为内置小类型?
是
→
使用非常量引用 T&
否
→
是
→
传值 或 const T&
否
→
使用常量引用 const T&

8.4 函数返回引用

函数可以返回引用,但必须确保返回的引用绑定的是一个在函数返回后依然有效的对象。最常见的是返回参数引用、类成员引用,或者全局/静态变量引用。

// 正确:返回参数引用
int& getLarger(int& a, int& b) {
    return (a > b) ? a : b;
}

// 使用
int x = 5, y = 10;
getLarger(x, y) = 100; // 现在y等于100
cout << y; // 输出100

// 错误:返回局部变量的引用(悬空引用)
int& badFunction() {
    int local = 42;
    return local; // 警告!local在函数结束时销毁
} // 返回的引用指向已销毁的内存,行为未定义

如果你看到编译器警告“returning reference to local variable”,100%是犯了上面的错误。实际开发中,返回引用常用于实现链式调用,比如cout << a << b;中的<<运算符就返回流对象的引用。

动手试一试

1. 编写一个函数swap,使用引用参数交换两个整数的值。比较一下,如果用指针实现,代码有什么区别?
2. 定义一个表示二维点的结构体Point,包含x和y坐标。编写一个函数printPoint,使用常量引用接收Point参数并打印。
3. 尝试写出以下声明的含义,并验证你的理解:

int a = 1, b = 2;
const int* ptr1 = &a;
int* const ptr2 = &a;
const int* const ptr3 = &a;
分别尝试修改*ptr1、ptr1、*ptr2、ptr2,观察编译器报错信息。

检验你的理解

  1. 为什么引用必须在声明时初始化,而指针不需要?
  2. 下面代码错在哪里?
    int& ref;
    ref = someVariable;
  3. 给定int data = 10;,const int& r = data;之后,能否通过data = 20;来修改值?为什么?
  4. 函数参数使用常量引用(const T&)相比传值(T)的主要优势是什么?
  5. 判断:int* const p表示p指向的整数是常量,p本身可以指向其他地址。

本章小结

  • 引用是变量的别名:必须初始化、不可重绑定、无空引用,使用起来比指针更安全直观。
  • const定义只读变量:保护数据不被意外修改,应用常量引用可以避免函数参数拷贝并保证安全。
  • const与指针组合有四种情况:牢记“const修饰左边”的口诀来区分指向常量的指针、常量指针等。
  • 函数参数传递优先使用常量引用:对于非内置类型,这能避免拷贝开销;对于所有类型,它能明确表达“只读”意图。
  • 返回引用需谨慎:确保返回的引用在函数调用后依然有效,绝不能返回局部变量的引用。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注