C++基础2:引用 · inline · nullptr · 类和对象初探
目录
第一章 引用(Reference)
引入
C 语言里让函数修改外部变量只能靠指针,繁琐且易错。C++ 提供了更简洁安全的方式——引用。
提示: “引用在实践中主要用于两个地方:传参和做返回值。其余理解都是为这两个场景服务的。”
1.1 引用是什么——别名哲学
引用(Reference):给已存在变量取别名,两个名字指向同一块内存,语法层面不开辟新空间。
int a = 10;
int& b = a; // b 是 a 的别名,&b == &a
int& c = b; // 对别名再取别名,c 也是 a 的别名
注意: 定义时
int&中的&是引用声明符,不是取地址运算符。
1.2 引用的三大语法特性
1.2.1 必须初始化
int& ra; // × 编译报错:引用必须初始化
int& ra = a; // √ 定义即绑定
1.2.2 不能改变指向(一旦绑定,终身不变)
这是 C++ 引用与 Java 引用最本质的区别:
int a = 10, d = 20;
int& b = a; // b 是 a 的别名
b = d; // **注意:** 这是赋值,不是改变指向!
// 等价于 a = d,a 值变为 20
// &b == &a 依然成立,&b != &d
提示: 口诀:"改变引用对象,同时改变被引用对象。"改变的是值,不是指向。
1.2.3 语法层面不开新空间
| 引用 | 指针 | |
|---|---|---|
| 语法层面开空间 | × 不开 | √ 开(4/8字节) |
sizeof 结果 | 被引用类型大小 | 指针本身大小 |
本节小结
三大特性(必须初始化、不能改指向、不开空间)是理解引用一切用法的基石。
1.3 引用的价值:两个核心场景
1.3.1 引用传参
优势: ①避免拷贝,提高效率 ②允许函数内部修改外部实参
// 传值:修改无效
void swap_val(int x, int y) { int t=x; x=y; y=t; }
// 引用:正确修改实参
void swap_ref(int& x, int& y) { int t=x; x=y; y=t; }
注意: 调试陷阱:在被调函数的监视窗口里只能看当前帧,
main中的变量显示为旧值不代表没有修改,函数返回后再看才是真实结果。
1.3.2 引用作返回值
传值返回的问题: 生成临时对象(有拷贝开销),临时对象有常性,不能作左值。
引用返回的优势: 直接返回对象别名,无拷贝,可作左值直接赋值。
// × 传值返回:返回副本,不能左值赋值
int STTop_val(ST& rs) { return rs.a[rs.top-1]; }
// STTop_val(st) = 4; // 编译报错!
// √ 引用返回:返回别名,可左值赋值
int& STTop_ref(ST& rs) { return rs.a[rs.top-1]; }
STTop_ref(st) = 4; // 直接修改栈顶元素!

提示: “这比单独写个
ModifyTop函数爽多了!以后结合运算符重载,v[i] = 10就是这个原理。”
注意: 不能返回局部变量的引用!
int& bad_func { int a = 10; return a; } // × 函数结束 a 销毁 → 类似野指针判断原则:出了作用域,被返回的对象是否还存在?堆上/全局 √;局部栈变量 ×。

本节小结
引用传参 = 无拷贝 + 可修改实参;引用返回 = 无拷贝 + 返回值可作左值。两者核心都是"我就是它"。
1.4 const 引用
1.4.1 权限只能平移或缩小,不能放大
权限放大/缩小只存在于引用和指针之间,普通变量赋值(值拷贝)没有此概念。
| 场景 | 合法性 | 说明 |
|---|---|---|
const int a; const int& ra = a; | √ | 权限平移(都只读) |
int b; const int& rb = b; | √ | 权限缩小(b 可写,rb 只读) |
const int a; int& ra = a; | × | 权限放大(a 只读,ra 可写,矛盾!) |

提示: 比喻:“鲁迅(原变量,可读可写)以’周树人’(const引用,只读)身份出现时,需要谨言慎行。但这不影响在家(用 b 本身)依然随意——
b++仍然有效。**权限缩小的是别名,不是原变量。”
1.4.2 const 引用的特殊能力——引用临时对象
临时对象(Temporary Object):运算、类型转换、传值返回时,编译器产生的未命名中间对象,具有常性(只读)。只有 const 引用才能绑定临时对象。
int a = 1, b = 2;
double d = 3.14;
const int& rc = 30; // √ 字面常量 → 临时对象,const 可引用
// int& rc = 30; // × 普通引用不能绑定临时对象
const int& rd = (a + b); // √ 表达式结果 → 临时对象
// int& rd = (a + b); // ×
const int& ri = d; // √ double→int 类型转换产生临时 int 对象
// int& ri = d; // ×

注意:
const int& ri = d中ri引用的不是 d 本身,而是类型转换后的临时int对象。const引用会延长临时对象的生命周期,随引用一起销毁。
哪些情况会产生临时对象:
- 算术/位运算表达式(
a+b、a|b) - 隐式类型转换(
double→int) - 函数传值返回
1.4.3 const 引用传参的真正价值
提示: “讲这么半天
const引用,就是为了一个场景:**函数传参。”
const T& val 作为参数,能接受一切类型的参数:
// × 普通引用:限制太多
void func(int& val);
// func(30); // 传常量失败
// func(a+b); // 传表达式失败
// √ const 引用:无所不能,且无拷贝
void func(const int& val);
func(a); // √ 普通变量(权限缩小)
func(a_const); // √ const 变量(权限平移)
func(30); // √ 字面常量(临时对象)
func(a + b); // √ 表达式(临时对象)
func(d); // √ 类型转换(临时对象)
提示: “以后你们看
push_back(const T&)这类接口,就是这个道理——既减少拷贝,又接受一切。如果函数内需要修改(如swap),才不加const。”
本节小结
const引用 = 权限缩小的别名。核心价值:绑定临时对象 + 通用传参。
1.5 引用与指针的深度对比
1.5.1 底层窥探
从汇编指令层看,引用和指针生成的代码完全相同:都是取地址、存地址、解引用访问。引用的底层实现就是指针。
注意: 面试时不要**上来就说"引用底层是指针,两者一样"——要先从语法层面讲区别,底层只是辅助理解。

1.5.2 语法层面的主要区别(面试高频)
| 对比维度 | 引用 | 指针 |
|---|---|---|
| 语法层面开空间 | × 不开 | √ 开(4/8字节) |
| 初始化要求 | 必须初始化 | 建议但非强制 |
| 改变"指向" | × 不能 | √ 可以 |
| 访问对象 | 直接访问 | 需 * 解引用 |
sizeof 含义 | 引用类型大小 | 指针大小(4/8字节) |
| 安全性 | 相对较高 | 容易野指针/空指针 |
| 相互替代 | × 不能完全替代 | × 不能完全替代 |
为什么引用不能替代指针:链表中 node->next = newNode 需要修改指针的指向,引用一旦绑定就无法改变,所以动态改变"连接关系"的数据结构必须用指针。
提示: “引用和指针是 C++ 的’打虎亲兄弟’——功能有重叠,但各有独特价值,相辅相成。”
本节小结
语法层面区别清晰,底层一致。二者互补,共同解决 C++ 中的各类问题。
第二章 内联函数(inline)
引入
C 语言宏(#define)通过预处理阶段文本替换避免函数调用开销,但充满陷阱。C++ 引入 inline 关键字,作为宏函数的优雅替代者。
2.1 宏函数的缺陷
宏的本质: 预处理阶段的纯文本替换**,不是函数调用。
实现两数相加宏的常见错误(面试高频考点):
// × 各种错误写法
# define ADD(a, b) a + b // 无括号
# define ADD(a, b) (a + b) // 只有外括号
# define ADD(a, b) a + b; // 加了分号(在 if/else 中爆炸)
// √ 正确写法:内外都要加括号
# define ADD(a, b) ((a) + (b))
为什么内外都要加括号:
-
外括号:控制整体优先级
ADD(1,2)*3展开为1+2*3 = 7(期望 9)→ 加外括号后(1+2)*3 = 9√
-
内括号(参数括号):防止参数是表达式时优先级错乱
ADD(a|b, c&d)→ 位运算优先级低,不括住会先和加法混算 → 加内括号 √
-
不加分号:宏会被展开到
if、三目运算符等表达式中,多余分号破坏语法
提示: “就算能写对宏,你还要能解释清楚每个括号的存在意义。面试官考’精通C语言’,第一关就是考宏。”
2.2 inline 的本质与优势
内联函数(inline function):在函数前加 inline,建议编译器在调用处直接展开函数体(不建立栈帧),消除函数调用开销。
// F.h 中定义(注意:直接在头文件定义,不分离到 .cpp)
inline void f(int i) {
cout << i << endl;
}
inline int Add(int x, int y) {
return x + y;
}
inline vs 宏:
| 对比维度 | 宏(#define) | inline 函数 |
|---|---|---|
| 处理时机 | 预处理(文本替换) | 编译期(编译器决定展开) |
| 类型检查 | × 无 | √ 有 |
| 可调试性 | × 替换后无法步入 | √ Debug 下可 F11 进入 |
| 括号陷阱 | × 容易中招 | √ 函数语义,无此问题 |
2.3 inline 的注意事项
① inline 只是建议,编译器有最终决定权
提示: “编译器是个’有独立思想的大孩子’——你的建议合理我就听,不合理我就忽略。”
- Debug 模式 :默认不展开(方便调试)
- Release 模式: 会展开(可通过 VS 属性 → C/C++ → 内联函数扩展 配置)**。
- 适合 inline :函数体短小(约 5~10 行以内)、频繁调用(如
swap) - 不适合 inline: 递归函数、代码行数多的函数**。
为什么编译器会拒绝:一个 100 条指令的函数被 1 万处调用:
- 不展开:
~1万 + 100 条指令(共享函数体) - 展开:
1万 × 100 = 100万条指令(**可执行文件急剧膨胀!)

② inline 不建议声明和定义分离到不同文件
// × 错误:声明在 F.h,定义在 F.cpp → 链接错误
// F.h: inline void f(int i);
// F.cpp: void f(int i) { cout << i << endl; }
原因:编译时只有声明(inline 属性),编译器认为不需要地址,不生成完整指令;链接时找不到地址 → 链接错误,且 inline 属性失效。
√ 正确做法: 把
inline函数的定义直接写在.h头文件中,#include后即可在调用处展开。
③ 类内直接定义的成员函数默认是 inline
class Stack {
void push(int x) { _a[_top++] = x; } // 默认 inline
};
本节小结
inline是宏函数的优雅替代:有类型检查、可调试、无括号陷阱。适合短小+频繁调用函数。只是建议,编译器有权拒绝。定义必须写在头文件中。
第三章 nullptr——空指针的正确姿势
3.1 NULL 的历史包袱与缺陷
NULL 是宏,在 C++ 中被定义为整数 0(而非 (void*)0,因为 C++ 对 void* 转换更严格):
// C++ 中 NULL 的定义
# define NULL 0 // 就是整数字面量
这导致函数重载歧义:
void f(int x); // 版本1
void f(int* ptr); // 版本2
f(NULL); // 期望调用 f(int*),实际调用 f(int)!
// 因为 NULL 就是整数 0,优先匹配 int
注意: 这是 C++ 的历史遗留问题:
NULL的类型是int,在重载中会匹配到错误的版本。
3.2 nullptr 的设计哲学
C++11 引入 nullptr 关键字(不是宏,是编译器直接支持的字面常量),彻底解决问题:
- √ 可隐式转换为任意类型的指针
- × 不能隐式转换为整型
f(nullptr); // √ 明确调用 f(int*),类型安全
// int x = nullptr; // × 编译报错:不能转整型
int* p = nullptr; // √ 替代 NULL/0 初始化指针
| 对比 | 0 / NULL | nullptr |
|---|---|---|
| 本质 | 整数字面量(宏) | C++11 关键字 |
| 可转指针 | √ | √ |
| 可转整型 | √ | ×(安全!) |
| 重载中匹配 | 整数版本(**注意:有歧义) | 指针版本(√ 准确) |
提示: “从此以后,我们不再用
NULL。初始化指针一律用nullptr。”
本节小结
nullptr是 C++11 对NULL的替代,类型安全,在函数重载中正确匹配指针参数。
第四章 类和对象·上篇
引入:C++ 对 C 结构体的升级
C 语言结构体只能存数据(属性),行为(方法)分离在外;而且 Stack 的函数和 Queue 的函数容易名字冲突。C++ 把数据和方法装进同一个"箱子",并通过访问控制实现封装——这就是类(class)**。
提示: “以前 C 语言:数据是数据,函数是函数,各干各的。现在 C++ 把它们捏在一起,这个’捏在一起的东西’就叫类。”
4.1 类的定义格式
class 类名 {
// 成员变量(属性)+ 成员函数(方法)
}; // ← 末尾分号不能省略!
成员变量(Member Variable):类中的数据,描述对象的属性。
成员函数(Member Function):类中的函数,描述对象的行为。
类内定义 vs 类外定义(声明与定义分离):
class Date {
public:
// 类内定义:默认 inline
void Print {
cout << _year << "/" << _month << "/" << _day;
}
// 类内声明,类外定义
void Init(int year, int month, int day);
private:
int _year, _month, _day;
};
// 类外定义:必须用 类名:: 指定类域
void Date::Init(int year, int month, int day) {
_year = year; _month = month; _day = day;
}
注意: 类外定义成员函数时,必须加
类名::**,否则编译器把它当作全局函数,找不到成员变量(_year等不在全局域里)。
4.2 访问限定符
C++ 通过访问限定符控制类成员对外的可见性,这是实现封装的核心手段:
| 访问限定符 | 类内访问 | 类外访问 | 说明 |
|---|---|---|---|
public(公有) | √ | √ | 对外开放的接口 |
private(私有) | √ | × | 仅类内使用 |
protected(保护) | √ | × | 类内可用(继承时与 private 有区别) |
注意: 现阶段把
protected和private视为等同,区别留到**继承章节再讲。
作用范围规则:从该限定符开始,到下一个限定符(或类的结束 })为止。
class Stack {
public: // 以下成员为 public
void Init;
void Push(int x);
int Top;
private: // 以下成员为 private
int* _a;
int _top;
int _capacity;
};
封装设计惯例:
- 成员变量 →
private/protected(不希望外部随意修改数据) - 对外使用的成员函数 →
public(提供安全接口) - 内部辅助函数 →
private(不对外暴露)
4.3 类域
C++ 有四种域:局部域、全局域、命名空间域、类域(Class Scope)。
类定义后形成独立的类域:
- **名字隔离:**不同类中可有同名函数(
Stack::Push和Queue::Push),不会冲突 - 只影响名字查找,不影响生命周期
- 类外访问类域中的名字需要
类名::限定
注意: 与命名空间的区别:命名空间可用
using namespace xxx;展开,类域不能展开,必须通过对象或类名::访问。
4.4 成员变量命名规范
成员函数参数与成员变量同名时,代码可读性极差:
// × 可读性差:year 到底是参数还是成员变量?
void Init(int year, int month, int day) {
year = year; // 自赋值!根本没有效果!
}
C++ 没有强制规范,惯例上给成员变量加特殊标记:
| 风格 | 示例 | 说明 |
|---|---|---|
| 前加下划线(推荐) | _year | 本课程采用 |
| 后加下划线 | year_ | 部分公司使用 |
前加 m_ | m_year | m = member |
// √ 清晰易读
class Date {
void Init(int year, int month, int day) {
_year = year; _month = month; _day = day;
}
int _year, _month, _day; // 一眼识别为成员变量
};
4.5 class 与 struct 的异同
C++ 中 struct 也升级成了类(可定义成员函数),与 class 的唯一区别是默认访问权限:
class | struct | |
|---|---|---|
| 默认访问权限 | private | public |
| 其余功能 | 完全相同 | 完全相同 |
C++ 对 C 结构体的兼容:
// C 语言风格(C++ 也支持)
typedef struct ListNodeC {
int val;
struct ListNodeC* next; // 必须写 struct 前缀
} ListNodeC;
// C++ 风格(更简洁,无需 typedef,无需 struct 前缀)
struct ListNodeCPP {
int val;
ListNodeCPP* next; // 类名直接代表类型
};
提示: “以后写链表节点,用 C++ 风格,省去
typedef和struct前缀,直接ListNode* next就搞定。”
什么时候用 struct: 所有成员都是公开的、不需要访问控制时(如简单数据聚合)。
4.6 类与对象的关系——图纸与房子
类(Class):是蓝图/设计图,描述"这类事物有什么属性和方法",本身是声明,不存储运行时数据。
对象(Object):用类实例化出来的具体个体,有独立内存,可存储实际数据。
提示: “类就像图纸——图纸不能住人;对象就像按图纸盖的房子——可以住人(存数据)。一张图纸可以盖出 N 栋房子,一个类可以实例化出 N 个对象。”
// 类定义(声明,不开空间)
class Date {
public:
void Init(int y, int m, int d) { _year=y; _month=m; _day=d; }
private:
int _year, _month, _day;
};
// 实例化(定义对象,开辟内存)
Date d1; // 实例化出对象 d1
d1.Init(2024, 7, 9); // 通过对象调用成员函数
Date d2; // 另一个独立对象
d2.Init(2025, 1, 1);
本节小结
类是封装单元(数据+方法),对象是类的具体实例。类定义是声明(不占内存),实例化才开辟内存。


被折叠的 条评论
为什么被折叠?



