
| 🍕阿i索 | 个人主页 |
|---|---|
| 《C语言专栏》 | 《C++专栏》 |
| 《数据结构专栏》 | 待更新... |
前言.
在熟悉了 C 语言的底层逻辑与高效特性后,我们不妨迈出下一步 ------ 走进 C++ 的世界。
从最经典的 hello world 就能直观感受到两者的差异:

了解C++的产生与发展
C++ 诞生于 1983 年,由 Bjarne Stroustrup 在 C 语言基础上扩展而来,核心是引入面向对象编程,解决了 C 在大型项目中的可维护性短板。1998 年 C++98 标准发布标志着语言成熟,而 C++11 则是现代 C++ 的分水岭 ------ 智能指针、Lambda 表达式等特性让开发更简洁安全;后续 C++14/17/20 的迭代,进一步强化了模块化、协程等能力,让它在系统开发、游戏引擎、高性能计算等领域始终占据核心地位。
C++ 在保留 C 语言大部分语法的基础上,针对 C 语言存在的缺陷提供了改进方案,是学习高性能编程的必经之路
📚 学习资源推荐
接下来,我们就从基础语法开始,一步步探索 C++ 的强大之处。
一. 命名冲突------命名空间
C++通过引入命名空间机制有效解决了全局作用域中的命名冲突问题。该特性允许将变量、函数和类等标识符限定在特定的命名空间内,从而实现名称的本地化管理。

如何定义命名空间?
- 使用
namespace关键字声明,后跟命名空间名称 - 在大括号
{}内定义命名空间的成员,可以包含变量、函数、类型等 - 命名空间必须定义在全局作用域中,支持嵌套定义
注意:
- 同一命名空间可以在多个文件中分别定义,系统会自动合并
- 不同文件中定义的同名命名空间不会产生冲突

命名空间的本质是创建一个独立的作用域,该作用域与全局作用域相互隔离。通过这种方式,不同作用域中可以定义同名的变量,从而有效避免了命名冲突问题。需要注意的是,命名空间内定义的变量仍属于全局变量范畴,只有函数内部或代码块(例如for循环)中定义的变量才是局部变量。
为什么不同的域可以定义同名变量?
C++中域有函数局部域,全局域,命名空间域,类域。域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
如何访问变量?
变量访问遵循"局部优先"原则,即优先查找局部变量。要访问其他作用域的变量,需使用域作用限定符 " :: ":

- 单独使用 " :: " 表示访问全局变量
- 前加命名空间名称可访问指定命名空间的变量
调用其他作用域的函数时,需在函数名前添加命名空间名称。例如调用C++标准库函数(位于std命名空间)的格式为:"std::函数名"。
访问嵌套命名空间时,需多次使用域作用限定符。
编译器查找变量的默认顺序:
- 局部作用域
- 全局作用域
- 未找到则报错"未声明的标识符"
若指定了作用域,则直接在该作用域中查找,未找到同样报错。

**命名空间的使用场景:**当一个项目包含大量文件时,函数命名容易产生冲突。通过在头文件中定义命名空间,可以为函数提供区分标识。值得注意的是,不同文件中可以定义同名命名空间,它们会被视为同一个命名空间而不会产生冲突。
如何使用命名空间?
-
使用域作用限定符"::":通过指定命名空间来访问成员,这是项目开发中的推荐方式。
-
使用using展开整个命名空间:将命名空间中的所有成员都引入当前作用域,适合练习和刷题时使用。

-
使用using引入特定成员:只展开命名空间中的某个成员,适合项目中需要频繁访问且不会造成命名冲突的成员。

二. C++的输入输出
C++的输入输出流库是<iostream>(Input Output Stream的缩写)。
在标准库中:
- std::cin是istream类的对象,负责处理窄字符的标准输入
- std::cout是ostream类的对象,负责处理窄字符的标准输出
- std::endl是一个函数,在流操作中相当于插入换行符并刷新缓冲区
主要运算符:
<< 是流插入运算符
>> 是流插入运算符
这些组件都定义在std命名空间中,使用时需要通过命名空间访问。
相比C语言,C++的输入输出更加便捷,能够自动识别变量类型而无需手动指定格式。


一些编译器想要使用printf、scanf需要包含头文件<cstdio>。

三. 缺省参数
缺省参数是指在函数声明或定义时为参数预设默认值。当调用函数时,若未传入对应实参,则自动使用该默认值;若传入实参,则使用传入值。缺省参数分为两种类型:全缺省(所有参数都设默认值)和半缺省(部分参数设默认值)。需要注意的是,C语言不支持缺省参数功能,这是C++特有的特性。
如何使用缺省参数?
- 声明与定义分离规则 :函数声明和定义分离时,缺省参数不可重复指定,需在声明中指定缺省值(无声明时可在定义中指定)。
- 半缺省参数规则 :半缺省参数必须从右往左依次连续指定,禁止间隔跳跃给缺省值。
- 函数调用规则 :调用带缺省参数的函数时,实参需从左到右依次传递,不能跳过前面的参数给后面传值。

使用缺省参数可以去解决的场景:
当Stack需要存储n个元素时,如果预先分配足够的空间,就能避免频繁扩容操作。这种情况下,可以在初始化函数的声明或定义中添加缺省参数来实现。


四. 函数重载
C++允许在同一作用域内定义多个同名函数,但要求它们的形参列表必须有所区别(参数数量或类型不同)。这种函数重载机制使得C++的函数调用展现出多态特性,使用更加灵活。相比之下,C语言严格禁止同一作用域内出现同名函数,因此不支持函数重载功能。


五. 引用
C++引入引用(reference)的概念,本质上是为变量创建别名。例如:"及时雨"和"宋江"都是指同一个人。引用并非定义新变量,而是为现有变量赋予另一个名称。编译器不会为引用分配独立的内存空间,引用与其指向的变量共享相同的内存地址。
引用格式:类型& 引用别名=引用对象
引用的特性:
- 引用在定义时必须初始化;
- 一个变量可以有多个引用;
- 引用一旦引用一个实体,再不能引用其他实体。

引用可以解决大部分指针问题,但在特定场景仍需使用指针(如链表、树等数据结构中的节点定义)。这是因为C++引用一旦初始化就无法改变其指向对象,而某些场景需要动态调整节点指向关系,此时只能使用指针来实现。
引用主要用于传参和返回值场景,能有效减少数据拷贝提升效率。修改引用对象时,原对象也会同步更新。
1. 引用传参


引用传参和指针传参在功能上相似,但引用传参使用起来更为便捷。下面以SeqList为例,分别展示指针传参和引用传参的实现方式:


也有的地方先对结构体地址进行引用:


2. 传引用返回

六. const引用
在 C++ 中,若要引用const对象,必须使用const引用;而const引用也可引用普通(非 const)对象 。这是因为引用在绑定对象时,遵循 权限只能缩小、不能放大 的规则:
- 权限缩小(允许) :普通对象 →
const引用(限制修改权限) - 权限放大(禁止) :
const对象 / 临时对象 → 普通引用(试图突破只读限制)
需要特别注意以下两种非法场景,其核心问题均为引用了临时对象:
int a = 10;
int& rb = a*3; // 错误:引用临时对象,且未使用const引用
double d = 12.34;
int& rd = d; // 错误:类型转换产生临时对象,引用权限放大
临时对象的产生原因
a*3是表达式求值,其计算结果会被编译器存储在一个临时对象中(临时对象无名称,仅暂存结果);int& rd = d中,double类型的d转换为int类型时,会生成临时对象存储转换后的中间值
C++ 标准规定:所有临时对象都具有 "常性"(const 属性) 。上述代码中,rb和rd是普通非 const 引用,试图引用具有常性的临时对象,本质是 "权限放大"(普通引用试图修改只读的临时对象),因此编译报错。
正确的写法是使用const引用绑定临时对象:
int a = 10;
const int& rb = a*3; // 正确:const引用可绑定临时对象
double d = 12.34;
const int& rd = d; // 正确:const引用接受类型转换产生的临时对象
补充定义:临时对象(Temporary Object)是编译器在求值表达式(如算术运算、类型转换)时,为暂存中间结果临时创建的未命名对象,其生命周期通常仅限于当前表达式,且默认具有 const 属性。
const 引用的三大用法
1. 绑定 const 对象
必须使用 const 引用,否则会因权限放大报错:
const int x = 10;
const int& rx = x; // ✅ 正确
// int& rx = x; // ❌ 错误:const对象 → 普通引用(权限放大)
2. 绑定普通对象
const 引用可以安全绑定普通对象,只是限制了修改权限:
int y = 0;
const int& ry = y; // ✅ 正确
3. 绑定临时对象(唯一合法方式)
临时对象(字面量、表达式、类型转换产生)默认具有常性 ,只能被 const 引用绑定:
const int& r1 = 10; // ✅ 绑定字面量临时对象
const int& r2 = y * 2; // ✅ 绑定表达式临时对象
double d = 12.34;
const int& r3 = d; // ✅ 绑定类型转换临时对象
普通引用的限制
普通(非const)引用 只能绑定可修改的左值(普通变量),不能绑定:
-
const对象(权限放大) -
临时对象(字面量、表达式、类型转换产生)
int y = 0;
int& ry = y; // ✅ 正确const int x = 10;
// int& rx = x; // ❌ 错误:const对象 → 普通引用(权限放大)// int& r = 10; // ❌ 错误:绑定临时对象(权限放大)
// int& r2 = y * 2; // ❌ 错误:绑定临时对象(权限放大)
类型转换与临时对象
1. 隐式类型转换
当不同类型绑定引用时,会生成临时对象:
int i = 1;
// double& rd = i; // ❌ 错误:普通引用不能绑定临时对象
const double& rd = i; // ✅ 正确:const引用绑定转换产生的临时对象
int→double隐式转换生成临时对象,具有常性。- 普通引用绑定临时对象属于权限放大,编译报错;
const引用则合法。
2. 强制类型转换
强制类型转换同样会生成临时对象:
int i = 1;
// int& rp = (int)&i; // ❌ 错误:普通引用不能绑定临时对象
const int& rp = (int)&i; // ✅ 正确:const引用绑定转换产生的临时对象
(int)&i将int*强制转为int,生成临时对象,具有常性。- 只有
const引用能安全绑定这个临时对象。
函数传参中的引用
1. 不修改实参的情况:推荐用 const 引用
-
兼容普通对象、
const对象、临时对象,保证安全。void func(const int& x) { /* 只读访问 */ }
int main() {
const int a = 10;
int y = 0;
func(a); // ✅ const对象 → const引用
func(y); // ✅ 普通对象 → const引用
func(2); // ✅ 临时对象 → const引用
return 0;
}
2. 修改实参的情况:用普通引用
-
只能接收普通对象,不能接收
const对象或临时对象。void func2(int& x) { x = 100; /* 修改实参 */ }
int main() {
int y = 0;
func2(y); // ✅ 普通对象 → 普通引用const int a = 10; // func2(a); // ❌ 错误:const对象 → 普通引用(权限放大) // func2(2); // ❌ 错误:临时对象 → 普通引用(权限放大) return 0;}
指针与引用
| 特性 | 引用(Reference) | 指针(Pointer) |
|---|---|---|
| 内存空间 | 是变量的 "别名",不开辟空间 | 存储变量地址,需要开辟空间 |
| 初始化要求 | 定义时必须初始化,不能空引用 | 建议初始化,允许空指针 |
| 指向性修改 | 初始化绑定对象后,不能更换绑定目标 | 可随时修改指向,指向不同对象 / 地址 |
| 访问目标方式 | 直接访问( 等价于原变量) | 需解引用才能访问指向的对象 |
| sizeof 结果 | 等于引用类型的大小 (如 int& 是 4 字节) |
固定为地址空间大小 |
| 安全性 | 几乎无空 / 野引用问题,使用更安全 | 易出现空指针、野指针,风险更高 |
1. 内存与初始化
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 引用:必须初始化,绑定后不能改
int& ref = a; // ✅ 正确:引用a
// int& ref2; // ❌ 错误:引用未初始化
// 指针:可先定义后初始化,可改指向
int* ptr; // ✅ 语法允许:未初始化(不推荐)
ptr = &a; // ✅ 指向a
int b = 20;
ptr = &b; // ✅ 改为指向b
return 0;
}
2. 访问方式与 sizeof
#include <iostream>
using namespace std;
int main() {
int a = 10;
int& ref = a;
int* ptr = &a;
// 访问目标
ref = 20; // ✅ 直接修改a的值
*ptr = 30; // ✅ 解引用修改a的值
// sizeof 结果
cout << "sizeof(ref) = " << sizeof(ref) << endl; // 4(int类型大小)
cout << "sizeof(ptr) = " << sizeof(ptr) << endl; // 8(64位平台地址大小)
return 0;
}
3. 安全性对比
#include <iostream>
using namespace std;
int main() {
// 指针风险:空指针/野指针
int* null_ptr = nullptr;
// *null_ptr = 10; // ❌ 空指针解引用,程序崩溃
int* wild_ptr; // 野指针(未初始化)
// *wild_ptr = 20; // ❌ 野指针访问,内存越界
// 引用安全:无空/野引用
int a = 10;
int& ref = a;
ref = 30; // ✅ 安全访问
return 0;
}
使用场景建议:
1. 优先用引用的场景
- 函数参数传递(避免拷贝,且无需检查空值):
void func(int& x)- 函数返回值(返回类成员 / 全局变量,避免拷贝):
int& getVal()- 追求代码简洁、安全的场景
2. 必须用指针的场景
- 需要动态分配内存(
new/delete):int* arr = new int[10]- 需要修改指向目标的场景:
ptr = &b
- 实现多态(基类指针指向派生类对象):
Base* p = new Derived()
七、宏函数与内联函数
回顾:C 语言宏函数
1. 宏函数的本质
宏是预处理阶段的纯文本替换机制,不是真正的函数调用,无栈帧创建、无类型检查。
2. 宏函数的特点
| 优点 | 缺点 |
|---|---|
| 1.高效:高频调用小函数时,宏替换避免了函数调用开销,执行效率更高。 2.无栈帧:直接替换代码,不占用栈空间。 | 1. 极易出错(运算符优先级、重复求值) 2. 无法调试(预处理后代码已展开) 3. 类型不安全(不检查参数类型) |
3. 宏函数的安全写法(以 ADD 为例)
// 错误版本:多分号+无括号
#define ADD1(a, b) a+b; // 展开后多余分号,语法错误
// 基础版本:无整体括号,优先级错误
#define ADD2(a, b) a+b // ADD2(1,2)*3 → 1+2*3=7(错误)
// 安全版本:参数+整体都加括号(推荐)
#define ADD4(a, b) ((a)+(b)) // ADD4(1,2)*3 → ((1)+(2))*3=9(正确)
4. 宏函数建议
- 每个参数加括号 + 整个表达式加括号,避免优先级问题;
- 不加分号结尾,由调用者自行添加;
- 避免带副作用的参数(如
ADD(i++, j++)会导致多次自增)。
C++ 内联函数(inline):替代宏函数的更优方案
1. 内联函数的本质
用inline修饰的函数,编译阶段 编译器会在调用处直接展开函数体,无需建立栈帧,既保留函数特性,又兼具宏的高效。
2. 内联函数的特点
| 内联函数 | 宏函数 |
|---|---|
| 编译阶段展开,保留函数特性 | 预处理阶段文本替换,无函数特性 |
| 类型安全(编译检查参数类型) | 类型不安全(任意文本均可替换) |
| 可调试(debug 模式默认不展开) | 无法调试(预处理后无宏痕迹) |
| 语法简单(无需手动加括号) | 语法复杂(易因括号 / 分号出错) |
| 支持函数重载、作用域限制 | 无重载,全局文本替换 |
注意
inline是编译器建议:编译器可拒绝展开(如递归函数、代码量过大的函数);- 适用场景:频繁调用的短小函数(如几行代码的加减、判断);
- 不适用场景:递归函数、代码量大的函数(加
inline也会被编译器忽略); - 声明与定义不可分离:分离到.h 和.cpp 会导致链接错误(展开后无函数地址);
- VS 调试说明:debug 版本默认不展开(方便调试),release 版本默认展开(追求效率)。
3. 内联函数的代码示例
#include <iostream>
using namespace std;
// 内联函数:替代ADD宏,简洁且安全
inline int Add(int a, int b) {
return a + b;
}
int main() {
// 编译时展开为:int ret = 1 + 2; 无栈帧开销
int ret = Add(1, 2);
// 无需担心优先级:Add(1,2)*3 → (1+2)*3=9
int ret2 = Add(1, 2) * 3;
cout << ret << " " << ret2 << endl; // 输出:3 9
return 0;
}
4. 内联函数的使用注意
// ❌ 错误:声明与定义分离(链接错误)
// test.h 中声明:inline int Add(int a, int b);
// test.cpp 中定义:int Add(int a, int b) { return a+b; }
// ✅ 正确:声明+定义写在.h中(或直接写在调用处)
inline int Add(int a, int b) {
return a + b;
}
5. 宏函数 vs 内联函数
| 特性 | 宏函数(C) | 内联函数(C++) |
|---|---|---|
| 处理阶段 | 预处理阶段(文本替换) | 编译阶段(函数体展开) |
| 类型检查 | 无(类型不安全) | 有(编译检查,类型安全) |
| 调试性 | 无法调试 | 可调试(debug 默认不展开) |
| 语法复杂度 | 高(需手动加括号) | 低(普通函数写法) |
| 适用场景 | C 语言短小高频函数 | C++ 短小高频函数(替代宏) |
| 重载 / 作用域 | 不支持 | 支持 |
| 递归 / 大函数 | 可替换(但易出错) | 编译器自动忽略 inline |
八、NULL 与 nullptr
回顾NULL
NULL 是一个宏,在标准头文件 stddef.h 中定义:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 // C++ 中通常定义为字面量 0
#else
#define NULL ((void *)0) // C 中定义为无类型指针 (void*)
#endif
#endif
NULL容易遇见的情况:
(1)重载匹配错误
当函数存在 int 和指针版本的重载时,NULL 会被错误地匹配到 int 版本:
void f(int x) { cout << "f(int x)" << endl; }
void f(int* ptr) { cout << "f(int* ptr)" << endl; }
int main() {
f(0); // ✅ 匹配 f(int x)
f(NULL); // ❌ 仍匹配 f(int x),因为 NULL 被定义为 0
return 0;
}
(2)类型转换歧义
f((void*)NULL):在 C++ 中会直接编译报错,因为void*无法隐式转换为int*。f((int*)NULL):虽然可以强制转换调用指针版本,但写法繁琐。
C++11 新关键字:nullptr
1. nullptr 的本质
nullptr 是 C++11 引入的特殊关键字 ,也是一种特殊类型的字面量。它的类型是std::nullptr_t ,可以隐式转换为任意指针类型 ,但不能转换为整数类型。
2. nullptr 解决的问题
(1)正确匹配指针重载
void f(int x) { cout << "f(int x)" << endl; }
void f(int* ptr) { cout << "f(int* ptr)" << endl; }
int main() {
f(nullptr); // ✅ 正确匹配 f(int* ptr)
return 0;
}
nullptr只能被当作指针,不会被误判为整数,彻底解决了重载匹配错误。
(2)统一空指针表示
int* p1 = nullptr; // ✅ 任意指针类型均可接收
char* p2 = nullptr;
double* p3 = nullptr;
- 无需强制类型转换,代码更简洁、安全。
NULL对比nullptr
| 特性 | NULL (C++) |
nullptr (C++11+) |
|---|---|---|
| 本质 | 宏,通常被定义为 0 |
关键字,类型为 std::nullptr_t |
| 类型安全 | 可隐式转换为整数,易引发重载匹配错误 | 只能转换为指针类型,类型安全 |
| 重载匹配 | 易匹配到 int 版本,而非指针版本 |
精确匹配指针版本 |
| 代码可读性 | 语义模糊(是 0 还是空指针?) | 语义明确:代表空指针 |
| 兼容性 | 兼容 C/C++ 旧代码 | C++11 及以后支持 |
代码示例:
#include <iostream>
using namespace std;
void f(int x) { cout << "f(int x)" << endl; }
void f(int* ptr) { cout << "f(int* ptr)" << endl; }
int main() {
// 1. 字面量 0
f(0); // 输出: f(int x)
// 2. NULL 的问题
f(NULL); // 输出: f(int x) ❌ 错误匹配
// f((void*)NULL); // 编译错误: 无法从 void* 转换为 int*
f((int*)NULL); // 输出: f(int* ptr) ✅ 强制转换可行,但繁琐
// 3. nullptr 的正确用法
f(nullptr); // 输出: f(int* ptr) ✅ 正确匹配
// 4. 定义空指针
int* p1 = NULL; // 旧写法
int* p2 = nullptr; // 新写法(推荐)
return 0;
}
建议在C++11 及以后的代码中,一律使用 nullptr 表示空指针。
下一篇再见🫡