1. 空间命名
1.1 namespace的价值
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
1.2 namespace的定义
- 定义命名空间,需要使⽤到 namespace 关键字,后⾯跟命名空间的名字,然后接⼀对 {} 即可,{} 中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace 本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量。
- C++中域有函数局部域、全局域、命名空间域、类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
- namespace 只能定义在全局,当然它还可以嵌套定义。
- 项⽬⼯程中多⽂件中定义的同名 namespace 会认为是⼀个 namespace,不会冲突。
- C++标准库都放在⼀个叫 std(standard) 的命名空间中。
示例一:基础命名空间定义与访问
#include <stdio.h>
// 1. 正常的命名空间定义 —— 只能定义在全局作用域
namespace hai
{
// 在命名空间内可以定义变量
int rand = 9;
// 定义函数
int Add(int x, int y)
{
return x + y;
}
// 定义结构体类型
struct Node
{
struct Node* next;
int val;
};
}
// 全局变量
int a = 10;
int main()
{
// 局部变量 a,就近原则:优先使用局部变量
int a = 20;
// 打印局部变量 a → 输出 20
printf("%d\\n", a);
// ::a 表示访问全局作用域的变量 a → 输出 10
printf("%d\\n", ::a);
// hai::rand 表示访问命名空间 hai 中的 rand → 输出 9
printf("%d\\n", hai::rand);
// 调用命名空间 hai 中的函数
int sum = hai::Add(3, 5);
printf("%d\\n", sum); // → 输出 8
return 0;
}
关键解释:
- :: 是作用域限定符。::a 表示访问全局变量,hai::rand 表示访问命名空间 hai 中的成员。
- 局部变量会遮蔽同名的全局变量,遵循"就近原则"。
- 命名空间中的成员需要通过 命名空间::成员名 的方式访问,编译器默认不会自动在命名空间中查找。
示例二:命名空间嵌套
#include <stdio.h>
// 2. 命名空间可以嵌套定义
namespace ha
{
namespace hb // 嵌套子命名空间 hb
{
int a = 10;
int Add(int x, int y)
{
return x + y;
}
}
namespace hc // 嵌套子命名空间 hc
{
int a = 100; // 与 hb::a 同名,但属于不同作用域,不冲突
int Add(int x, int y)
{
return (x + y) * 10; // 不同的实现逻辑
}
}
}
int main()
{
// 逐层使用 :: 访问嵌套命名空间中的变量
printf("%d\\n", ha::hb::a); // → 输出 10
printf("%d\\n", ha::hc::a); // → 输出 100
// 调用不同嵌套命名空间中的同名函数,实现完全不同的功能
printf("%d\\n", ha::hb::Add(1, 2)); // → 输出 3
printf("%d\\n", ha::hc::Add(3, 4)); // → 输出 70
return 0;
}
关键解释:
- 命名空间嵌套时,访问路径为 外层::内层::成员。
- 不同嵌套命名空间中可以有同名变量或函数,互不冲突,各自独立。
- 这在实际项目中非常有用——比如不同模块可以有同名的工具函数,放在各自的命名空间中即可隔离。
1.3 命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
- 指定命名空间访问(推荐):命名空间::成员名,冲突风险最低,项目中推荐使用。
- using 展开单个成员:using 命名空间::成员名;,经常访问且不冲突的成员推荐这种方式。
- using namespace 展开全部成员:using namespace 命名空间;,项目不推荐(冲突风险大),日常小练习为了方便可以使用。
#include <iostream>
// 定义一个演示用的命名空间
namespace demo
{
int value = 42;
void show()
{
std::cout << "demo::show() called" << std::endl;
}
}
int main()
{
// 方式一:指定命名空间访问(项目中推荐)
std::cout << demo::value << std::endl; // → 42
demo::show();
// 方式二:using 展开单个成员
using demo::value;
std::cout << value << std::endl; // 现在可以直接使用 value
// 方式三:展开整个命名空间(小练习可用,项目慎用)
using namespace demo;
show(); // 直接调用,无需前缀
return 0;
}
关键解释:
- 项目开发中优先使用方式一,明确来源,避免命名冲突。
- using namespace std; 虽然方便,但会将标准库中大量名称引入全局,增加冲突风险,工程中应慎重使用。
2. 输入输出
- <iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
- std::cin 是 istream 类的对象,它主要⾯向窄字符的标准输⼊流。
- std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
- std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
- << 是流插⼊运算符,>> 是流提取运算符。(C 语⾔还⽤这两个运算符做位运算左移/右移)
- 使⽤ C++ 输⼊输出更⽅便,不需要像 printf/scanf 那样需要⼿动指定格式,C++ 的输⼊输出可以⾃动识别变量类型,最重要的是 C++ 的流能更好的⽀持⾃定义类型对象的输⼊输出。
- cout/cin/endl 等都属于 C++ 标准库,C++ 标准库都放在⼀个叫 std(standard) 的命名空间中,所以要
通过命名空间的使⽤⽅式去⽤它们。 - ⼀般⽇常练习中我们可以 using namespace std;,实际项⽬开发中不建议 using namespace std;。
- 这⾥我们没有包含 <stdio.h>,也可以使⽤ printf 和 scanf,因为包含 <iostream> 间接包含了。vs 系列编译器是这样的,其他编译器可能会报错。
示例一:cout 自动识别类型 + cin 连续输入
#include <iostream>
#include <algorithm> // for sort
#include <list> // for list
using namespace std;
int main()
{
// cout 自动识别类型 —— 无需像 printf 那样写 %d / %lf / %c
int a[10] = { 1, 2, 4, 6, 7, 9, 3, 5, 8, 10 };
list<int> lt;
sort(a, a + 10); // 对数组排序
for (int i = 0; i < 10; i++)
{
cout << a[i] << " "; // 自动识别 int 类型输出
}
cout << "\\n"; // 换行
// cin 自动识别类型,连续输入多个变量
int i = 0;
double d = 0;
cin >> i >> d; // 从标准输入读取 int 和 double,自动跳过空白字符
cout << i << '\\n'; // 输出 int
cout << d << "\\n"; // 输出 double
cout << i << '\\n' << d << "\\n"; // 链式输出
cout << i << endl << d << endl; // endl 换行 + 刷新缓冲区
return 0;
}
关键解释:
- cout 通过运算符重载自动识别类型,不需要 %d、%lf、%s 等格式控制符。
- cin >> i >> d; 可以连续读取多个变量,自动根据变量类型解析输入,比 scanf 更简洁安全。
- endl 不仅换行,还会刷新缓冲区(flush),确保内容立即输出到终端。如果不需要强制刷新,'\\n' 效率更高。
示例二:提高 IO 效率(竞赛常用)
#include <iostream>
using namespace std;
int main()
{
// 在 IO 需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下 3 ⾏代码
// 可以提⾼ C++ IO 效率
ios_base::sync_with_stdio(false); // 解除 C++ 流与 C 标准 IO 的同步
cin.tie(nullptr); // 解除 cin 与 cout 的绑定
cout.tie(nullptr); // 解除 cout 与其它流的绑定
// 之后使用 cin / cout 时不再与 scanf / printf 同步,速度大幅提升
int n;
cin >> n;
cout << n << endl;
return 0;
}
关键解释:
- 默认情况下,C++ 的 cin/cout 与 C 的 scanf/printf 同步,保证混用时顺序正确,但性能较低。
- sync_with_stdio(false) 解除同步后,cin/cout 速度接近 scanf/printf。
- cin.tie(nullptr) 解除绑定:默认每次 cin 前都会刷新 cout,解除后不再自动刷新,进一步提升吞吐量。
- 注意:解除同步后不要混用 C 和 C++ 的 IO(如同时用 cout 和 printf),否则输出顺序可能错乱。
3. 缺省参数
- 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。缺省参数分为全缺省和半缺省参数(也叫默认参数)。
- 全缺省就是全部形参都给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调用时,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在声明和定义中同时出现,规定必须在函数声明处给缺省值。
示例一:全缺省与半缺省的基本用法
#include <iostream>
using namespace std;
// 全缺省:三个参数都有默认值
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
// 半缺省:必须从右往左连续缺省,不能跳跃(如下 a 不能缺省)
// ❌ 错误示例:void Func2(int a = 1, int b, int c = 3); 不允许 b 在前
void Func2(int a, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
// 不传参 → 全部使用默认值
Func1(); // a=10, b=20, c=30
// 传参时必须从左向右连续给实参,不能跳跃
Func1(1); // a=1, b=20, c=30
Func1(1, 2); // a=1, b=2, c=30
Func1(1, 2, 3); // a=1, b=2, c=3
// 半缺省调用:至少需要提供第一个参数
Func2(100); // a=100, b=20, c=30
Func2(100, 200); // a=100, b=200, c=30
Func2(100, 200, 300); // a=100, b=200, c=300
// Func2(); // ❌ 错误:至少需要给 a 传值
return 0;
}
关键解释:
- 半缺省的顺序:缺省值必须从右向左连续给出,不能左边缺省而右边没给缺省值。例如 void func(int a = 1, int b); 是错误的,因为 a 有默认值而 b 没有,调用 func() 时不知道该给 b 什么值。
- 调用实参顺序:实参从左向右依次赋值给形参,不能跳过。Func1(,2,) 这种写法 C++ 不支持。
- 声明与定义:缺省值应写在函数声明处,定义处不再重复写缺省值,否则可能导致重复定义/冲突。
示例二:缺省参数在实际场景中的运用(栈初始化)
#include <iostream>
#include <cassert>
#include <cstdlib>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a; // 指向动态数组的指针
int top; // 栈顶索引(元素个数)
int capacity; // 当前栈的容量
} ST;
// 函数声明:初始容量 n 带有缺省值 4
// 声明处给出缺省值,定义处不能再写
void StackInit(ST* pa, int n = 4);
// 函数定义:不要再写缺省值,否则可能导致重复定义错误
void StackInit(ST* pa, int n)
{
assert(pa); // 防御性编程:确保 pa 非空
// 根据传入的容量 n 分配内存(缺省值为 4)
pa->a = (STDataType*)malloc(sizeof(STDataType) * n);
if (pa->a == NULL)
{
cout << "malloc fail" << endl;
exit(–1);
}
pa->top = 0;
pa->capacity = n;
}
void StackPush(ST* pa, int x)
{
// 简化:假设容量足够,直接压栈
pa->a[pa->top++] = x;
}
int main()
{
// 确定要插入 100 个值 —— 显式指定容量 100
ST st1;
StackInit(&st1, 100); // n = 100(指定实参,不使用缺省值)
for (int i = 0; i < 100; i++)
{
StackPush(&st1, i);
}
cout << "st1 capacity = " << st1.capacity << endl; // → 100
// 不知道要插入多少 —— 不传参数,使用缺省容量 4
ST st2;
StackInit(&st2); // n = 4(使用声明处的缺省值)
cout << "st2 capacity = " << st2.capacity << endl; // → 4
free(st1.a);
free(st2.a);
return 0;
}
关键解释:
- 缺省参数让接口更灵活:常见情况(容量 4)无需每次都填写参数,特殊需求(容量 100)仍可自定义。
- 声明处写缺省值,定义处不写,这是 C++ 的规定。如果两处都写则可能产生二义性冲突。
- 缺省值的本质是编译器在调用点自动补全缺失的实参,因此缺省值必须是常量表达式(全局变量、字面量等),不能是局部变量。
4. 函数重载
C++ 支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者参数类型不同。这样 C++ 函数调用就表现出了多态行为,使用更灵活。C 语言是不支持同一作用域中出现同名函数的。
示例一:参数类型不同的函数重载(指针版 swap)
#include <iostream>
using namespace std;
// 重载 Swap:参数类型不同 —— int*
int Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
return tmp; // 返回交换前的 *x
}
// 重载 Swap:double*
double Swap(double* x, double* y)
{
double tmp = *x;
*x = *y;
*y = tmp;
return tmp;
}
// 重载 Swap:char*
char Swap(char* x, char* y)
{
char tmp = *x;
*x = *y;
*y = tmp;
return tmp;
}
int main()
{
int x = 10, y = 20;
double d1 = 2.90, d2 = 9.99;
char ch1 = 'A', ch2 = 'B';
// 编译器根据实参类型自动选择对应版本的 Swap
Swap(&x, &y); // 调用 int Swap(int*, int*)
Swap(&d1, &d2); // 调用 double Swap(double*, double*)
Swap(&ch1, &ch2); // 调用 char Swap(char*, char*)
cout << x << ' ' << y << endl; // → 20 10
cout << d1 << ' ' << d2 << endl; // → 9.99 2.9
cout << ch1 << ' ' << ch2 << endl; // → B A
return 0;
}
示例二:参数个数不同的函数重载
#include <iostream>
using namespace std;
// 重载 test:参数个数不同 —— 1 个参数
int test(int* x)
{
return *x;
}
// 重载 test:2 个参数
int test(int* x, int* y)
{
return *x + *y; // 返回两数之和
}
int main()
{
int a = 10, b = 999;
cout << test(&a) << endl; // 调用 test(int*) → 10
cout << test(&a, &b) << endl; // 调用 test(int*,int*) → 1009
return 0;
}
关键解释:
- 重载的判定标准:只看参数列表(参数个数、类型、顺序),与返回值类型无关。仅返回值不同不能构成重载。
- 调用匹配规则:编译器根据实参数量和类型找到最佳匹配的版本。若有多个版本都能匹配,会产生歧义导致编译错误。
- C++ 为何能重载:C++ 采用**名字修饰(Name Mangling)**技术,编译时将函数名与参数类型编码成一个唯一的符号。例如 int Swap(int*, int*) 可能被修饰为 _Z4SwapPiS_,double Swap(double*, double*) 修饰为 _Z4SwapPdS_,从而在链接时区分不同版本。这是 C 语言不支持的。
- 实际应用:重载让同一功能的不同数据类型版本共用函数名,提高代码可读性和复用性,例如 cout << 就是通过重载 operator<< 实现对各种类型的输出。
5. 引用
5.1 引用的概念和定义
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用别名 = 引用对象
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& ref = a; // ref 是 a 的引用(别名)
cout << "a = " << a << ", ref = " << ref << endl; // → 10, 10
cout << "&a = " << &a << ", &ref = " << &ref << endl; // 地址完全相同
ref = 20; // 修改 ref 即修改 a
cout << "a = " << a << endl; // → 20
return 0;
}
关键解释:
- 引用的定义格式为 类型& 引用名 = 变量名;,& 与类型结合时表示引用类型,而非取地址。
- 引用和被引用的变量指向同一块内存,修改引用等价于修改原变量。
- 引用必须在定义时初始化,且不能为 NULL,这使其比指针更安全。
5.2 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;
int main()
{
int a = 100;
int& ref1 = a; // 必须初始化,否则编译错误
int& ref2 = a; // 一个变量可以有多个引用
cout << &ref1 << " " << &ref2 << endl; // 相同地址
int b = 200;
ref1 = b; // ⚠️ 这不是让 ref1 引用 b,而是把 b 的值赋给 ref1(即 a)
cout << "a = " << a << endl; // → 200,a 被修改了
cout << "&ref1 = " << &ref1 << " &a = " << &a << endl; // 地址仍然与 a 相同
// 引用不能中途改变绑定的对象,没有语法能让 ref1 变成 b 的别名
return 0;
}
关键解释:
- 引用不允许有空引用,必须在定义时绑定一个合法的变量。
- ref1 = b; 是赋值操作,将 b 的值复制到 ref1 指向的 a 上,并不会改变 ref1 的绑定关系。
- 正因为引用不可重新绑定,使用时更安全——不会出现指针悬挂问题。
5.3 引用的使用
- 引用在实践中主要用于引用传参和引用做返回值中减少拷贝提高效率,以及通过引用形参修改实参。
- 引用传参跟指针传参功能类似,引用传参相对更方便一些,语法更简洁。
- 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。
示例:引用传参与引用做返回值
#include <iostream>
using namespace std;
// 引用传参:直接操作原变量,避免拷贝
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
// 引用做返回值:返回原变量的别名,可支持链式赋值
int& getElem(int* arr, int idx)
{
return arr[idx]; // 返回 arr[idx] 的引用
}
int main()
{
int a = 10, b = 20;
Swap(a, b); // 无需取地址,直接传变量
cout << "a = " << a << ", b = " << b << endl; // → 20, 10
int arr[5] = {1, 2, 3, 4, 5};
getElem(arr, 2) = 100; // 通过引用直接修改数组元素
cout << arr[2] << endl; // → 100
return 0;
}
关键解释:
- 引用传参:函数参数直接是原变量的别名,在函数内部修改形参会直接改变实参,且无需拷贝大对象,效率高。调用时无需额外取地址。
- 引用做返回值:返回的是某个变量的引用,调用者可以像操作普通变量一样赋值给它。但要注意不能返回局部变量的引用(局部变量在函数结束后被销毁,引用将变成悬空引用)。
- 相比指针传参,引用传参语法更简洁,且天然非空,减少了空指针检查的负担。
5.4 const 引用
- 可以引用一个 const 对象,但是必须用 const 引用。const 引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
- 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++ 中把这个未命名对象叫做临时对象。
#include <iostream>
using namespace std;
int main()
{
const int a = 10;
// int& ref = a; // ❌ 错误:不能将非 const 引用绑定到 const 对象
const int& ref1 = a; // ✅ 正确:const 引用绑定 const 对象
cout << ref1 << endl; // → 10
int b = 20;
const int& ref2 = b; // ✅ 正确:const 引用可以绑定到普通对象(权限缩小)
b = 30;
cout << ref2 << endl; // → 30,通过非 const 修改,ref2 只读
// const 引用可以绑定临时对象
const double& ref3 = 3.14; // 临时 double 对象
const int& ref4 = int(3.14); // 显式类型转换产生的临时对象
cout << ref3 << ' ' << ref4 << endl; // → 3.14 3
// 函数返回的临时对象也可以用 const 引用延长生命周期
const string& ref5 = string("hello");
cout << ref5 << endl; // → hello
return 0;
}
关键解释:
- 权限不能放大:不能用一个非 const 引用来引用一个 const 对象,那样就能通过引用修改 const 对象,破坏了 const 语义。
- 权限可以缩小:允许 const 引用绑定到非 const 对象,此时通过该引用只能读,不能写,起到了保护作用。
- const 引用延长临时对象生命周期:当 const 引用绑定到临时对象时,临时对象的生命周期会延长至该引用的作用域结束。这是 C++ 中很有用的特性,常用于函数参数以避免不必要的拷贝。
5.5 指针和引用的关系
- 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
- 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
- sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节,64 位下是 8 byte)。
- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
// 引用:必须初始化,直接当别名用
int& ref = a;
ref = 100; // 直接修改 a
cout << "a = " << a << endl; // → 100
// 指针:可以单独声明,可以修改指向
int* ptr = &a;
*ptr = 200; // 需要通过解引用修改
cout << "a = " << a << endl; // → 200
ptr = &b; // 指针可以重新指向 b
*ptr = 300;
cout << "b = " << b << endl; // → 300
// sizeof 区别
cout << "sizeof(ref) = " << sizeof(ref) << endl; // → 4 (int 的大小)
cout << "sizeof(ptr) = " << sizeof(ptr) << endl; // → 8 (64 位指针大小)
// 空指针问题
int* p = nullptr;
// *p = 42; // 运行时错误:解引用空指针
// 引用不存在 null reference
return 0;
}
关键解释:
- 底层的相似性:在编译器实现中,引用通常就是通过指针来实现的,但其语法上隐藏了指针的解引用细节,让代码更简洁。
- 安全性:引用必须初始化且不能为空,有效避免了空指针和野指针引发的问题。因此能使用引用的场景尽量用引用。
- 灵活性:指针可以改变指向、参与算术运算、与 nullptr 比较,在需要动态分配、链表等场景下更合适。两者各有适用场景。
6. inline
- 用 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,可以提高效率。
- inline 对于编译器而言只是一个建议,也就是说,你加了 inline 编译器也可以选择在调用的地方不展开。不同编译器关于 inline 什么情况展开各不相同,因为 C++ 标准没有规定这个。inline 适用于频繁调用的短小函数,对于递归函数、代码相对多一些的函数,加上 inline 也会被编译器忽略。
- C 语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,且不方便调试,C++ 设计了 inline 目的就是替代 C 的宏函数。
- inline 不建议声明和定义分离到两个文件,分离会导致链接错误。因为 inline 被展开,就没有函数地址,链接时会出现报错。
示例:inline 函数 vs 宏函数
#include <iostream>
using namespace std;
// 内联函数:类型安全,可调试
inline int add(int a, int b)
{
return a + b;
}
// 对比:C 风格宏函数(存在隐患)
#define ADD(a, b) ((a) + (b))
int main()
{
int x = 10, y = 20;
// inline 函数调用 —— 编译后可能展开为 x + y
cout << "inline add: " << add(x, y) << endl; // → 30
// 宏函数调用 —— 预处理阶段替换文本
cout << "macro ADD: " << ADD(x, y) << endl; // → 30
// 宏函数的隐患示例
int a = 5, b = 3;
cout << "ADD(a++, b): " << ADD(a++, b) << endl;
// 展开后变成 ((a++) + (b)),a 被加了两次!
// 预期结果可能是 5+3=8,但实际 a 被递增两次,输出可能与预期不符
cout << "a after macro: " << a << endl; // → 7(而不是 6)
// inline 函数不会有此问题
a = 5;
cout << "inline add(a++, b): " << add(a++, b) << endl; // → 8
cout << "a after inline: " << a << endl; // → 6(符合预期)
return 0;
}
关键解释:
- 内联函数 vs 宏函数:
- 内联函数是真正的函数,有类型检查、作用域规则,可调试;宏只是文本替换,无类型检查,可能产生副作用(如上述 a++ 多次求值)。
- 内联函数可以访问类的私有成员,宏不能。
- 展开的代价:展开会增大目标代码体积(代码膨胀),如果函数体较大且调用频繁,可能导致指令缓存 miss,反而降低性能。因此编译器会自行判断是否真正展开。
- inline 的定义位置:因为内联函数可能被展开,调用点需要看到函数体,因此通常将 inline 函数定义在头文件中,或者直接在类定义内实现(这也会隐含 inline 建议)。
7. nullptr
- C++11 引入 nullptr 作为空指针字面量,它是指针类型的 std::nullptr_t 类型,可以隐式转换为任意指针类型,但不能转换为整数类型。
- 在 C 语言中,NULL 通常被定义为 (void*)0 或 0,在某些重载场景下会引发歧义。nullptr 的引入就是为了解决这一历史遗留问题。
示例:nullptr vs NULL 的重载调用对比
#include <iostream>
using namespace std;
// 重载函数:一个接收 int,一个接收 int*
void func(int x)
{
cout << "func(int) called, x = " << x << endl;
}
void func(int* p)
{
if (p == nullptr)
cout << "func(int*) called with nullptr" << endl;
else
cout << "func(int*) called, *p = " << *p << endl;
}
int main()
{
int a = 42;
func(0); // 调用 func(int),0 被当成整数
func(NULL); // ⚠️ 可能调用 func(int) 或产生歧义,取决于 NULL 的定义
func(nullptr); // ✅ 明确调用 func(int*),传入空指针
func(&a); // 正常传指针
// 与指针比较
int* p = nullptr;
if (p == nullptr)
cout << "p is nullptr" << endl;
// nullptr 的类型特性
// int n = nullptr; // ❌ 错误:nullptr 不能转换为 int
// 需要通过显式转换:int n = (int)nullptr; // 不推荐
// 模板类型推导中也能正确识别为指针类型
auto ptr = nullptr; // ptr 类型为 nullptr_t
cout << "sizeof(nullptr_t) = " << sizeof(nullptr_t) << endl; // 通常等于指针大小
return 0;
}
关键解释:
- 消除重载歧义:NULL 在 C++ 中通常定义为 0,调用 func(NULL) 时会匹配到 func(int),而不是预期的 func(int*)。使用 nullptr 则明确匹配指针版本。
- 类型安全:nullptr 只能赋值给指针或转换为指针类型,不能隐式转为整数,避免了 0 的二义性。
- 统一表示空指针:在现代 C++ 中,应始终使用 nullptr 表示空指针,不再使用 NULL 或 0。这是最佳实践。
- 注意:sizeof(nullptr_t) 通常是平台指针大小,说明 nullptr 底层可能存储与指针相同的信息。
网硕互联帮助中心





评论前必须登录!
注册