摘要:本文主要介绍了 C++的一些基础知识:命名空间、输入输出流、缺省参数、函数重载、引用、内联函数与
nullptr。文章从第一个 C++程序"Hello World"开始逐步展开讲解,每个机制都配有可编译的示例代码,并说明它在实际工程中的适用场景与常见坑,帮助读者把 C 的写法平滑迁移到 C++17。
文章目录
1. 第一个 C++ 程序
先把最小的程序跑起来,再看它和 C 的写法差在哪。
新建一个 hello.cpp:
// hello.cpp
#include <iostream>
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
和 C 版相比只有三处变化:<stdio.h> 换成 <iostream>,printf 换成 std::cout <<,换行用 std::endl。第一次写会卡在几个地方:std:: 是什么,为什么不能直接写 cout?<< 为什么能连着写?std::endl 和 '\n' 有什么区别?
先把程序跑起来:
g++ hello.cpp # 默认生成 a.out
g++ hello.cpp -o hello # 指定输出文件名
g++ -std=c++17 hello.cpp -o hello
Windows 下生成 hello.exe,运行时写 .\hello.exe。在 Visual Studio(VS)里新建 .cpp 文件即可,它会调用 C++ 编译器。
VS 的两个细节
VS 的<iostream>会间接包含<stdio.h>,所以只包含<iostream>也能用printf。标准不保证这一点,换编译器可能报「未声明」;需要 C 风格输入输出时,显式包含<stdio.h>。另外 VS 会对scanf、strcpy报 C4996 警告,在文件首行写#define _CRT_SECURE_NO_WARNINGS 1可以关闭。
std:: 和 std::cout 分别对应命名空间和输入输出流。再加上后面五处常用写法,这篇要讲的就是这七个机制:
| 写法 | 解决什么问题 | 对应机制 |
|---|---|---|
std::string 前的 std:: | 标准库名字不污染全局 | 命名空间(namespace) |
std::cout << x | 不用手写格式串,类型由变量决定 | 输入输出流 |
void f(int n = 4); | 大多数调用共用默认值 | 缺省参数(default argument) |
void f(int) / void f(double) | 同一件事不用起多个名字 | 函数重载(function overloading) |
void f(int& x) | 传参不拷贝,又能修改实参 | 引用(reference) |
inline int Add(int, int); | 小函数免去调用开销 | 内联函数(inline function) |
nullptr | 空指针有独立类型 | 空指针字面量 |
这篇按这张表往下讲:先看每个机制怎么写,再看它的边界和代价。
2. C++ 与 C 的关系
C++ 保留了 C 的绝大多数语法,又加了类、模板、异常和标准库。把 .c 文件改成 .cpp,多数 C 程序仍能编译;但 C++ 的常用写法、标准库和编译选项与 C 不同。
C++ 由本贾尼·斯特劳斯特卢普(Bjarne Stroustrup)在 1979 年开始设计,1983 年正式命名为 C++。1998 年发布第一个国际标准化组织(ISO)标准 C++98;2011 年的 C++11 加入 lambda、范围 for、右值引用、智能指针等,之后的版本大致按三年一版推进。
| 标准 | 年份 | 主要变化 |
|---|---|---|
| C++98 | 1998 | 第一个 ISO 标准,纳入 STL(Standard Template Library,标准模板库) |
| C++03 | 2003 | 修错与兼容性修订 |
| C++11 | 2011 | lambda、范围 for、右值引用与移动语义、智能指针、线程库 |
| C++14 | 2014 | 泛型 lambda、二进制字面量等 |
| C++17 | 2017 | if constexpr、折叠表达式、filesystem |
| C++20 | 2020 | 概念(Concepts)、模块(Modules)、协程(Coroutines) |
| C++23 | 2023 | if consteval、flat_map、import std 等 |
入门阶段用 C++11 及以上即可,本文代码按 C++17 编译。版本号不是越新越好:公司项目常被编译器或第三方库卡在某个标准上,写代码前先确认工程用的是哪一版。
3. 命名空间:解决全局重名
3.1 全局名字为什么会撞
先看一段 C 代码:
#include <stdio.h>
#include <stdlib.h>
int rand = 10; // 与 stdlib.h 中的 rand 函数重名
int main(void)
{
printf("%d\n", rand);
return 0;
}
GCC(GNU Compiler Collection)的报错是:
error: 'rand' redeclared as different kind of symbol
note: previous declaration of 'rand' with type 'int(void)'
C 的变量、函数、类型都放在全局作用域里,名字只有一份。stdlib.h 已经声明了 int rand(void),这里再定义一个 int rand,编译器不会把它们当成两个不同的东西。
命名空间(namespace)的做法是给名字加一层前缀:同名实体放进不同的命名空间,就可以共存。
// namespace.cpp
#include <cstdio>
int a = 1; // 全局域
namespace bit
{
int a = 2; // 命名空间域
int Add(int left, int right) { return left + right; }
}
int main()
{
int a = 3; // 局部域
std::printf("%d\n", a); // 3
std::printf("%d\n", ::a); // 1
std::printf("%d\n", bit::a); // 2
std::printf("%d\n", bit::Add(1, 2)); // 3
return 0;
}
:: 是作用域限定符(scope resolution operator)。::a 前面不写名字,表示全局域;bit::a 表示 bit 命名空间里的 a。
C++ 有四类域:函数局部域、全局域、命名空间域、类域。局部域和全局域还决定变量的生命周期;命名空间域和类域只影响编译期查找名字,不影响变量活多久。
3.2 查找顺序:编译器不会主动进命名空间
上面的 a 之所以打印 3,是因为编译器找名字的顺序是「当前局部域 → 外层局部域 → 全局域」。命名空间不在自动查找路径上:不写 bit::,编译器不会进去找。

这也解释了一个常见现象:using namespace std; 之后,cout 能直接用;不写这句话,cout 就必须写成 std::cout。using 的作用是把名字引入当前作用域,不是让编译器「优先去命名空间里找」。
3.3 嵌套与跨文件合并
命名空间可以嵌套,使用时逐层限定:
#include <cstdio>
namespace bit
{
namespace pg { int rand = 1; }
namespace hg { int rand = 2; }
}
int main()
{
std::printf("%d %d\n", bit::pg::rand, bit::hg::rand); // 1 2
return 0;
}
不同文件里的同名命名空间会被编译器合并成一个。因此可以把声明和实现分开写:
// Stack.h
#pragma once
namespace bit
{
struct Stack { int* a; int top; int capacity; };
void STInit(Stack* ps, int n);
}
// Stack.cpp
#include "Stack.h"
#include <cstdlib>
namespace bit
{
void STInit(Stack* ps, int n)
{
ps->a = (int*)std::malloc(n * sizeof(int));
ps->top = 0;
ps->capacity = n;
}
}
Stack.cpp 里的 namespace bit 和 Stack.h 里的同名命名空间是同一个,不需要额外的声明。test.cpp 里写 bit::Stack、bit::STInit 才能用到它们;全局再定义一个 Stack 也不会冲突。
3.4 三种使用方式
| 写法 | 作用 | 适用场景 |
|---|---|---|
bit::a | 指定命名空间访问 | 项目代码,推荐 |
using bit::a; | 只把 a 引入当前作用域 | 频繁使用且不冲突的单个名字 |
using namespace bit; | 把 bit 里所有名字引入当前作用域 | 小练习图方便,项目不推荐 |
using 只改变名字的可见性,不改变优先级。局部变量仍然最优先:
#include <cstdio>
namespace bit { int b = 1; }
using bit::b;
int main()
{
std::printf("%d\n", b); // 1,来自 bit
int b = 2; // 局部变量遮蔽外层
std::printf("%d\n", b); // 2
return 0;
}
头文件里不要写
using namespace
头文件会被多个源文件包含,展开后污染会扩散到所有包含者。头文件内始终使用完整限定名,比如std::string。
匿名命名空间
namespace { int x; }不给名字,里面的名字只在本文件可见,效果与 C 里给全局变量加static相同(内部链接)。想让某个全局名字「出不了这个文件」时用它。
4. cin / cout:不再手写格式串
4.1 从 scanf 的格式串说起
C 的输入输出把类型写在格式串里:
int a = 0;
double b = 0.1;
scanf("%d %lf", &a, &b);
printf("%d %.2f\n", a, b);
格式串和实参类型必须手动对齐。%d 配 double、%lf 配 int,编译器不把它当类型错误;开了格式检查可能给警告,但程序仍会编译,运行时按错误的布局读写内存,结果不可预测。
C++ 的 <iostream> 是输入输出流(input/output stream),std::cin 和 std::cout 直接按变量类型选择读写方式:
// io.cpp
#include <iostream>
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
std::cout << a << " " << b << " " << c << std::endl;
std::cin >> a >> b >> c;
std::cout << a << " " << b << " " << c << std::endl;
return 0;
}
<< 是流插入运算符,>> 是流提取运算符。它们和 C 的位移运算符长得一样,但在这里由重载决定行为:cout << a 调用的版本由 a 的类型决定,所以不需要格式串。第 6 节会解释这种「同名不同参」的机制。
输入 10 3.14 z,程序先输出初始值 0 0.1 x,读入后再输出 10 3.14 z。
4.2 endl 与 ‘\n’
std::endl 做两件事:输出换行符,并刷新缓冲区。刷新意味着把缓冲区里的内容真正交给操作系统,频繁刷新会增加底层 IO 次数。
循环里输出几十万行时,用 '\n' 通常比 std::endl 快;只有确实需要立刻看到输出(交互式提示、调试定位)时,才用 std::endl。
for (int i = 0; i < 100000; ++i)
{
std::cout << i << '\n'; // 不强制刷新
}
4.3 读取规则与失败状态
std::cin >> 以空白字符(空格、制表符、换行)为分隔符,并且会跳过前导空白。所以下面两种输入读到的 c 都是 'x':
10 x
10
x
要读入空白字符本身,用 std::cin.get(c) 或 std::getline(std::cin, s)。
读取失败时,流会置上失败位,之后的所有读取都会失败。常见写法是把读取表达式直接放进条件:
int x = 0;
while (std::cin >> x)
{
std::cout << x << '\n';
}
std::cin >> x 返回流对象,流对象能隐式转换为 bool,转换结果表示读取是否成功。输入结束或遇到非数字时,循环自然退出。
>>和getline混用
std::cin >> n读完数字后,换行符还留在缓冲区里;紧接着std::getline(std::cin, s)会读到这个换行,得到一个空字符串。需要先std::cin.ignore()把换行丢掉。
4.4 大量输入时的提速
默认情况下,C++ 流和 C 的 stdio 保持同步,std::cin 也比 scanf 慢一些。IO 密集的场景可以在 main 开头加两行:
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
第一行关闭 C 与 C++ 流的同步,第二行解除 std::cin 与 std::cout 的绑定。两行都必须在任何输入输出之前调用。
using namespace std;该不该写
小练习里写一句using namespace std;省事;项目代码和头文件里不要写,原因见第 3.4 节。本文的示例统一写std::,读者可以对照看出哪些名字来自标准库。
5. 缺省参数:省掉重复的实参
5.1 全缺省与半缺省
还是栈的初始化。多数时候容量取 4 就够,只有已知要插入很多元素时才需要一次开够:
void STInit(ST* ps, int n = 4);
调用 STInit(&s1) 时 n 取 4,调用 STInit(&s2, 1000) 时 n 取 1000。n 的默认值就是缺省参数(default argument)。
缺省参数分两种:
- 全缺省:所有形参都有默认值;
- 半缺省:只有部分形参有默认值。
半缺省必须从右往左连续给,不能跳着给。下面是一个完整程序:
// default.cpp
#include <iostream>
void Func1(int a = 10, int b = 20, int c = 30)
{
std::cout << a << ' ' << b << ' ' << c << '\n';
}
void Func2(int a, int b = 10, int c = 20)
{
std::cout << a << ' ' << b << ' ' << c << '\n';
}
// void Func3(int a = 1, int b); // 非法:b 没有默认值
int main()
{
Func1(); // 10 20 30
Func1(1, 2); // 1 2 30
Func2(100); // 100 10 20
Func2(100, 200, 300); // 100 200 300
return 0;
}
调用时实参从左到右依次匹配。Func2(100, 200) 是 a=100、b=200、c=20;想只给 c 传值、跳过 b,C++ 没有这种写法。
5.2 声明与定义分离
缺省值只能写一次。声明和定义分开时,通常写在声明处:
// Stack.h
#pragma once
struct ST
{
int* a;
int top;
int capacity;
};
void STInit(ST* ps, int n = 4);
// Stack.cpp
#include "Stack.h"
#include <cstdlib>
void STInit(ST* ps, int n) // 这里不能再写 = 4
{
ps->a = (int*)std::malloc(n * sizeof(int));
ps->top = 0;
ps->capacity = n;
}
两处都写会报「重定义默认参数」。缺省值也不属于函数类型:void f(int a = 10); 和 void f(int a = 20); 只差默认值,不是重载,编译器会当成同一个函数重复声明。
缺省值在调用点求值,可以写常量、全局变量、sizeof 或函数调用,但不能引用形参或局部变量。下面两行只示意规则,函数体省略:
int g = 10;
void f(int a, int b = g); // 可以
void h(int a, int b = a); // 报错:默认值不能引用形参
5.3 两个代价
- 调用点看不出哪些参数用了默认值。
STInit(&s)只看这一行,不知道容量是 4;参数一多,读代码要回查声明。 - 缺省值写在头文件里,修改它会让所有包含该头文件的源文件重新编译。
缺省参数适合「大多数调用用同一个值」的接口,不适合把多个可选行为塞进一个函数。可选行为多时,重载或配置结构体更清楚。
6. 函数重载:同名函数靠形参区分
6.1 什么算重载
C 里同一件事要起多个名字:
void STInit(ST* ps);
void QInit(Queue* pq);
void LTInit(ListNode** pphead);
C++ 允许同一作用域内有同名函数,只要形参列表不同,这叫函数重载(function overloading):
void Init(ST* ps);
void Init(Queue* pq);
void Init(ListNode** pphead);
编译器按实参的类型、个数和顺序挑选版本。常见情况如下:
| 差异 | 是否构成重载 | 说明 |
|---|---|---|
| 参数个数不同 | 是 | f() 与 f(int) |
| 参数类型不同 | 是 | Add(int, int) 与 Add(double, double) |
| 参数类型顺序不同 | 是 | f(int, char) 与 f(char, int) |
| 仅返回值类型不同 | 否 | 调用时无法区分 |
| 仅形参名不同 | 否 | 形参名不参与函数类型 |
仅值传递的顶层 const 不同 | 否 | f(int) 与 f(const int) 是同一个函数 |
引用加不加 const | 是 | f(int&) 与 f(const int&) 可重载 |
// overload.cpp
#include <iostream>
int Add(int left, int right) { return left + right; }
double Add(double left, double right) { return left + right; }
void f() { std::cout << "f()\n"; }
void f(int a) { std::cout << "f(int)\n"; }
void f(int a, char b) { std::cout << "f(int, char)\n"; }
void f(char b, int a) { std::cout << "f(char, int)\n"; }
int main()
{
std::cout << Add(10, 20) << '\n'; // 30
std::cout << Add(10.1, 20.2) << '\n'; // 30.3
f(); // f()
f(10); // f(int)
f(10, 'a'); // f(int, char)
f('a', 10); // f(char, int)
return 0;
}
6.2 编译器怎么挑版本
候选函数不止一个时,编译器按下面的优先级匹配:
- 精确匹配;
- 类型提升(整型提升、浮点提升);
- 标准转换。
同一优先级有多个候选,就报「调用有歧义」。
// resolution.cpp
#include <iostream>
void h(int x) { std::cout << "int\n"; }
void h(double x) { std::cout << "double\n"; }
int main()
{
char c = 'a';
float f = 1.0f;
h(c); // int:char → int 是整型提升
h(f); // double:float → double 是浮点提升
// h(1L); // 报错:long → int 和 long → double 都是转换,同级,歧义
return 0;
}
缺省参数也会制造歧义。void f1() 和 void f1(int a = 10) 构成重载,但 f1() 既匹配无参版本,也能用默认值匹配带参版本,编译器无法选择:
void f1();
void f1(int a = 10);
// f1(); // 报错:call of overloaded 'f1()' is ambiguous
6.3 名字修饰与 extern “C”
C++ 编译器会对函数名做名字修饰(name mangling),把参数类型编码进符号名。int Add(int, int) 在 GCC 下修饰成 _Z3Addii,double Add(double, double) 修饰成 _Z3Adddd。两个 Add 在目标文件里是不同的符号,链接器不会把它们当成重复定义。
C 编译器不做名字修饰,同名函数在链接时必然冲突。这也是 C++ 调用 C 库时需要 extern "C" 的原因:它告诉编译器「这个名字按 C 的规则生成,不要修饰」。
extern "C" int CAdd(int left, int right);
包含 C 头文件时,常见写法是:
extern "C"
{
#include "clib.h"
}
7. 引用:给变量取别名
7.1 引用不是指针
C 的交换函数要传地址:
void Swap(int* px, int* py);
int x = 0, y = 1;
Swap(&x, &y);
C++ 的引用(reference)是给已存在的变量取别名,调用处不用取地址:
void Swap(int& rx, int& ry);
int x = 0, y = 1;
Swap(x, y);
int& rx = x; 之后,rx 就是 x 的另一个名字。对 rx 的操作就是对 x 的操作;&rx 得到的也是 x 的地址。
引用有三条性质:
- 定义时必须初始化,
int& ra;直接报错; - 一个变量可以有多个引用;
- 引用一旦绑定对象,不能再改绑到别的对象。
#include <iostream>
int main()
{
int a = 0;
int& b = a;
int& c = a;
int& d = b;
++d; // a、b、c、d 都变成 1
int e = 20;
b = e; // 赋值:把 e 的值写进 a,b 仍然绑定 a
std::cout << a << ' ' << b << ' ' << c << '\n'; // 20 20 20
return 0;
}
最后一行是关键:b = e 不是让 b 改引 e,而是把 e 的值赋给 b 所绑定的 a。C++ 引用不能改变指向,这一点和 Java 的引用不同。
7.2 引用传参
引用最常用的场景是传参。交换函数:
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
栈的接口改成引用后,调用处不用写 &,函数内部不用写 ->:
void STInit(ST& rs, int n = 4); // 对比第 5 节的 void STInit(ST* ps, int n = 4);
函数体里把 ps->a 改成 rs.a,ps->top 改成 rs.top 即可。
指针变量本身也可以取引用。链表头插要修改头指针时,C 里传二级指针 ListNode**,C++ 可以写 ListNode*&:
void ListPushFront(ListNode*& phead, int x);
ListNode*& 读作「对 ListNode* 的引用」。函数内给 phead 赋值,改的就是调用方的头指针,不需要再解引用一层。
| 对比项 | 传值 | 传引用 |
|---|---|---|
| 是否拷贝 | 会,自定义类型还要调用拷贝构造 | 不会 |
| 能否修改实参 | 不能 | 能 |
| 是否可能为空 | 不可能 | 不可能(引用必须绑定对象) |
| 大对象效率 | 低 | 高 |
7.3 引用做返回值
引用也可以作为返回值。下面这个程序把 ST、引用传参和引用返回放在一起:
// reference.cpp
#include <cassert>
#include <cstdlib>
#include <iostream>
struct ST
{
int* a;
int top;
int capacity;
};
void STInit(ST& rs, int n = 4)
{
rs.a = (int*)std::malloc(n * sizeof(int));
rs.top = 0;
rs.capacity = n;
}
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top - 1];
}
int main()
{
ST st;
STInit(st, 4);
st.a[st.top++] = 1;
st.a[st.top++] = 2;
std::cout << STTop(st) << '\n'; // 2
STTop(st) += 10; // 通过引用修改栈顶
std::cout << STTop(st) << '\n'; // 12
std::free(st.a);
return 0;
}
STTop 返回的是 rs.a[rs.top - 1] 的别名,所以 STTop(st) += 10 能编译通过;如果返回 int 值,这句会报错,因为右值不能被赋值。
返回引用有一个前提:被引用的对象在函数返回后必须仍然存在。只能返回全局变量、静态变量、堆上分配的对象,或者传入的引用/指针指向的对象。返回局部变量的引用,函数返回后栈帧销毁,引用悬空:
int& Bad()
{
int x = 10;
return x; // GCC 只给 -Wreturn-local-addr 警告,运行结果是未定义行为
}
7.4 const 引用
const 引用是「只读 + 不拷贝」的传参方式:
void Print(const ST& st);
函数内部不能修改 st,调用方也不用付出整份拷贝的代价。大对象(字符串、容器、结构体)传参时,const T& 通常是默认选择。
const 引用还有几条规则:
- 引用
const对象必须用const引用,权限不能放大; const引用可以引用普通对象,权限可以缩小;- 表达式结果和类型转换的中间值存放在临时对象里,临时对象具有常性,必须用
const引用绑定。
const int a = 10;
// int& ra = a; // 报错:权限放大
const int& ra = a; // 正确
int b = 20;
const int& rb = b; // 正确:权限缩小
// rb++; // 报错:rb 是 const 引用
const int& r1 = a * 3; // 正确:结果在临时对象中
// int& r2 = a * 3; // 报错:临时对象具有常性
double d = 12.34;
const int& r3 = d; // 正确:类型转换产生临时 int 对象
// int& r4 = d; // 报错:同上
const 引用绑定临时对象时,会延长临时对象的生命周期到引用的生命周期结束。所以 const int& x = 42; 合法。
7.5 指针与引用怎么选

| 维度 | 指针 | 引用 |
|---|---|---|
| 语法概念 | 存储变量地址,本身占空间 | 变量别名,语言层面不占独立空间 |
| 初始化 | 语法上不强制 | 必须初始化 |
| 改变指向 | 可以随时改指 | 绑定后不能改绑 |
| 访问方式 | 需要解引用 *p | 直接访问 |
sizeof | 地址大小(32 位 4 字节,64 位 8 字节) | 被引用类型的大小 |
| 空值风险 | 可能为空或悬空 | 必须绑定对象,风险更低 |
两者功能有重叠,但适用场景不同:需要「可能为空」或「中途改指向」时用指针;需要「稳定别名」或「避免拷贝」时用引用。函数参数如果只是读取大对象,const T& 最直接;如果要表达「这个参数可以没有」,用指针。
8. inline:替掉宏函数
8.1 宏的括号问题
C 里想省掉小函数的调用开销,常用宏:
#define ADD(a, b) a + b
ADD(1, 2) * 5 // 展开成 1 + 2 * 5 = 11,不是 15
外层加括号能修好这一处:
#define ADD(a, b) ((a) + (b))
但宏还有别的问题:
- 参数可能被求值多次:
ADD(x++, y++)里x++、y++各执行两次; - 没有类型检查,参数传错也能通过预处理;
- 调试时看到的是展开后的代码,断点不好下;
- 宏没有作用域,名字可能污染全局。
8.2 inline 只是建议
内联函数(inline function)用 inline 修饰,编译时把函数体展开到调用处,省去建立栈帧和跳转的开销:
// inline.cpp
#include <iostream>
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
std::cout << Add(1, 2) << '\n';
return 0;
}
inline 只是给编译器的建议,编译器可以选择不展开。标准没有规定必须展开,递归函数、代码量大的函数即使写了 inline 也可能被忽略。
它适合短小且频繁调用的函数。用 GCC 可以观察展开结果:
g++ -O2 -S inline.cpp -o inline.s
grep "call.*Add" inline.s
在 -O2 下,上面的 grep 没有输出,说明 Add 被展开;换 -O0 再编译,就能看到 call _Z3Addii。VS 的 Debug 版本默认不展开,方便调试;想在 Debug 下观察,需要在项目属性里把「调试信息格式」改为「程序数据库 (/Zi)」,把「内联函数扩展」改为「只适用于 __inline (/Ob1)」。
8.3 定义放头文件
inline 函数必须在每个使用它的源文件里都能看到定义,否则编译器没有函数体可以展开。因此通常直接把定义写在头文件里:
// Add.h
#pragma once
inline int Add(int x, int y)
{
return x + y;
}
如果声明放头文件、定义放 .cpp,其他源文件只看到声明,编译器无法展开,链接时还可能找不到符号。类内定义的成员函数默认就是 inline,原因相同——定义在类体里,每个包含该头文件的源文件都能看到。
9. nullptr:让空指针有类型
9.1 NULL 的重载问题
C++11 之前用 NULL 表示空指针。但 NULL 是宏,不同实现给的值不同:
- C:通常是
((void*)0); - 微软的 Microsoft Visual C++(MSVC):
0; - GCC 的 libstdc++:
__null。
看一个重载例子:
void f(int x);
void f(int* ptr);
f(0) 匹配 f(int),f(nullptr) 匹配 f(int*)。如果写 f(NULL):
- MSVC 下
NULL是0,调用f(int),不是想要的指针版本; - GCC 的 libstdc++ 下
__null同时能转成int和int*,两个候选都「差不多合适」,直接报调用歧义。
两种情况都调不到指针版本。这不是写法问题,而是 NULL 本身没有指针类型。
9.2 nullptr 的规则
C++11 引入关键字 nullptr,类型是 std::nullptr_t。它可以隐式转换为任意指针类型,但不能转换为整数类型,因此不会误入 f(int)。
| 对比项 | NULL | nullptr |
|---|---|---|
| 本质 | 宏,通常是 0 或 ((void*)0) | 关键字,std::nullptr_t 字面量 |
| 能否转整数 | 可以 | 不可以 |
| 重载匹配 | 容易匹配到整型版本 | 准确匹配指针版本 |
| 建议 | 只用于遗留代码 | C++11 起统一使用 |
// nullptr.cpp
#include <iostream>
void f(int x) { std::cout << "f(int)\n"; }
void f(int* ptr) { std::cout << "f(int*)\n"; }
int main()
{
f(0); // f(int)
f(nullptr); // f(int*)
std::cout << sizeof(nullptr) << '\n'; // 64 位下 8,32 位下 4
return 0;
}
sizeof(nullptr) 等于指针大小;if (p) 这类需要布尔值的语境里,nullptr 按 false 处理。新代码里不再用 NULL,也不再写 (int*)0。
10. 小结
- C++ 保留 C 的绝大多数语法,源文件用
.cpp、Linux 下用g++;入门用 C++11 及以上,先确认工程用的标准版本。 - 命名空间解决全局重名,查找顺序是「局部 → 全局」,不会主动进命名空间;项目写
bit::x,头文件不展开。 std::cin/std::cout按变量类型读写,不用格式串;std::endl带刷新,循环里用'\n';读取失败用while (std::cin >> x)判断。- 缺省参数从右往左连续给,声明与定义分离时只写在声明处;调用点看不出用了哪些默认值,改缺省值会触发重新编译。
- 函数重载看形参的个数、类型和顺序,不看返回值;匹配优先级是「精确 > 提升 > 转换」,同级即歧义。
- 引用必须初始化、不能改绑;传参省拷贝,返回引用要保证对象活着,只读大对象用
const T&。 inline替代宏函数,只是建议,定义放头文件;nullptr只转指针不转整数,避免NULL的重载歧义。
本文系统讲解C++17标准下的七大核心机制:命名空间解决全局重名、cin/cout流式I/O替代格式化输入输出、缺省参数简化函数调用、函数重载支持多态接口、引用实现零拷贝传参与返回、inline内联优化性能、nullptr类型安全空指针。内容涵盖语法、编译原理(如名字修饰)、常见陷阱及工程实践建议,助力C程序员平滑过渡到现代C++开发。

688

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



