C++ 入门基础:从命名空间到引用、inline 与 nullptr
C++ 入门基础:从命名空间到引用、inline 与 nullptr
C++ 对很多 C 语言学习者来说并不陌生,因为它兼容 C 的大部分语法。但真正开始写 C++ 后,会发现它并不是“C 加几个新关键字”这么简单。它在 C 的基础上引入了命名空间、函数重载、引用、默认参数、内联函数、面向对象、泛型和标准库等能力,让大型程序更容易组织,也让代码表达能力更强。
这篇文章面向已经有一点 C 基础、准备正式进入 C++ 的读者。我们不追求一次讲完所有语法,而是抓住入门阶段最容易遇到、也最容易混淆的几个点:namespace、C++ 输入输出、缺省参数、函数重载、引用、const 引用、inline 和 nullptr。
一、为什么要学 C++
C++ 的起源可以追溯到 C 语言之后的系统级开发需求。C 语言高效、接近底层,但在大型软件开发中,单纯依靠全局函数、结构体和手动约定来组织代码,维护成本会逐渐变高。C++ 在保留 C 高性能和底层能力的同时,增强了抽象能力。
常见应用方向包括:
| 方向 | 典型场景 |
|---|---|
| 系统软件 | 编译器、数据库、浏览器、操作系统组件 |
| 服务端 | 高性能网络服务、游戏服务、流媒体服务 |
| 游戏开发 | 游戏引擎、客户端核心模块 |
| 音视频处理 | FFmpeg、WebRTC 等相关方向 |
| 嵌入式 | 车载系统、智能硬件、设备控制 |
| 基础库和工具 | 算法库、框架、底层工具链 |
| 机器学习系统 | 底层算子、推理引擎、性能敏感模块 |
C++ 难点不只在语法数量多,更在于它同时服务于“接近底层”和“高级抽象”两类需求。入门时不要急着追求所有特性,先把基础语法的设计动机和使用边界弄清楚,会比死记语法更稳。
二、第一个 C++ 程序
C 语言版本的 hello world 在 C++ 中依然可以运行:
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
但更符合 C++ 风格的写法是使用标准输入输出流:
#include <iostream>
int main() {
std::cout << "hello world" << std::endl;
return 0;
}
编译运行:
g++ hello.cpp -std=c++11 -o hello
./hello
预期输出:
hello world
这里有几个符号第一次看到会有点陌生:
std是标准库所在的命名空间;cout是标准输出流对象;<<在这里是流插入运算符,不是位运算左移;endl表示输出换行并刷新缓冲区。
很多入门代码会写 using namespace std;,这样就可以直接写 cout。不过在真实工程中,更推荐写 std::cout、std::endl,避免把整个标准库命名空间展开后造成名字冲突。
三、namespace:解决命名冲突
C/C++ 程序中会出现大量变量、函数、类型名。如果都放在全局作用域,很容易冲突。比如自己定义了一个 rand,就可能和库函数 rand 撞名。
命名空间的作用是:把一组名字放进一个独立的作用域,降低命名冲突风险。
#include <iostream>
namespace project_a {
int value = 10;
int Add(int left, int right) {
return left + right;
}
}
namespace project_b {
int value = 20;
int Add(int left, int right) {
return (left + right) * 10;
}
}
int main() {
std::cout << project_a::value << std::endl;
std::cout << project_b::value << std::endl;
std::cout << project_a::Add(1, 2) << std::endl;
std::cout << project_b::Add(1, 2) << std::endl;
return 0;
}
命名空间有几个常见特点:
namespace只能定义在全局作用域;- 命名空间可以嵌套;
- 多个文件中同名命名空间会被视为同一个命名空间;
- C++ 标准库放在
std命名空间中; - 命名空间影响名字查找,不直接改变变量生命周期。
使用命名空间常见方式有三种:
// 方式一:指定命名空间访问,工程中推荐
std::cout << "hello" << std::endl;
// 方式二:只展开某个成员
using std::cout;
using std::endl;
cout << "hello" << endl;
// 方式三:展开整个命名空间,小练习可以用,工程中慎用
using namespace std;
很多初学者会觉得 std:: 麻烦,但它让名字归属非常清楚。项目一大,清楚比省几个字符更重要。
四、C++ 输入输出流
C++ 使用 <iostream> 提供标准输入输出:
| 对象或符号 | 含义 |
|---|---|
std::cin |
标准输入流 |
std::cout |
标准输出流 |
std::endl |
换行并刷新缓冲区 |
<< |
流插入,用于输出 |
>> |
流提取,用于输入 |
一个简单例子:
#include <iostream>
int main() {
int a = 0;
double b = 0.0;
char c = 'x';
std::cin >> a >> b >> c;
std::cout << a << " " << b << " " << c << std::endl;
return 0;
}
和 scanf / printf 相比,C++ 流不需要手动写 %d、%lf 这类格式控制符。它可以根据变量类型选择合适的输入输出逻辑,这背后和函数重载、运算符重载有关。
如果遇到大量输入输出场景,可以加上下面几行提升 C++ 流的效率:
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
需要注意,一旦关闭同步,就不要在同一段程序里随意混用 C 风格 IO 和 C++ 风格 IO,否则可能出现输出顺序不符合预期的问题。
五、缺省参数:给函数参数设置默认值
缺省参数也常叫默认参数。它允许我们在声明或定义函数时给参数指定默认值,调用时如果没有传实参,就使用默认值。
#include <iostream>
void Print(int value = 0) {
std::cout << value << std::endl;
}
int main() {
Print(); // 使用默认值 0
Print(10); // 使用传入值 10
return 0;
}
缺省参数分两类:
// 全缺省:所有参数都有默认值
void Func1(int a = 10, int b = 20, int c = 30);
// 半缺省:部分参数有默认值
void Func2(int a, int b = 10, int c = 20);
这里有两条规则必须记住:
- 半缺省参数必须从右往左连续给默认值;
- 函数调用传参必须从左往右依次传,不能跳过中间参数。
下面这种写法是错误的:
// 错误:默认参数不能中间断开
void Func(int a = 10, int b, int c = 30);
声明和定义分离时,默认值通常放在声明处,定义处不要重复写:
// Stack.h
void Init(int capacity = 4);
// Stack.cpp
void Init(int capacity) {
// ...
}
如果声明和定义都写默认值,编译器会报重复指定默认参数的错误。
六、函数重载:同名函数也可以共存
C++ 支持在同一作用域中定义多个同名函数,只要它们的参数列表不同。参数列表不同可以体现在:
- 参数类型不同;
- 参数个数不同;
- 参数类型顺序不同。
#include <iostream>
int Add(int left, int right) {
std::cout << "int Add" << std::endl;
return left + right;
}
double Add(double left, double right) {
std::cout << "double Add" << std::endl;
return left + right;
}
void Print() {
std::cout << "Print()" << std::endl;
}
void Print(int value) {
std::cout << "Print(int)" << std::endl;
}
int main() {
Add(1, 2);
Add(1.1, 2.2);
Print();
Print(10);
return 0;
}
返回值不同不能构成重载:
// 错误:仅返回值不同,无法重载
// int Func();
// double Func();
原因很简单:调用 Func() 时,编译器无法仅根据返回值判断你想调用哪个版本。
缺省参数和重载放在一起时也容易出问题:
void f();
void f(int value = 10);
// f(); // 存在歧义,编译器不知道调用哪个
所以默认参数不要滥用,尤其不要和重载组合出让人难以判断的调用形式。
七、引用:给已有对象取别名
引用不是新定义一个对象,而是给已有对象起别名。
#include <iostream>
int main() {
int a = 10;
int& ra = a;
int& rb = ra;
rb += 5;
std::cout << a << std::endl; // 15
std::cout << &a << std::endl;
std::cout << &ra << std::endl;
std::cout << &rb << std::endl;
return 0;
}
引用有三个重要特性:
- 定义时必须初始化;
- 一个对象可以有多个引用;
- 引用一旦绑定到某个对象,就不能再改为引用其他对象。
下面这点特别容易误解:
int a = 10;
int b = 20;
int& r = a;
r = b; // 不是让 r 改为引用 b,而是把 b 的值赋给 a
引用最常见的用途是传参和返回值。
引用传参
引用传参可以避免拷贝,也可以让函数内部修改外部对象。
void Swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
和指针传参相比,引用写起来更自然:
int a = 1;
int b = 2;
Swap(a, b);
引用返回
引用返回可以让函数调用结果作为左值使用,但要非常注意生命周期。
int& Top(int* array, int size) {
return array[size - 1];
}
int main() {
int arr[3] = {1, 2, 3};
Top(arr, 3) += 10;
std::cout << arr[2] << std::endl; // 13
return 0;
}
补充说明:返回引用时,必须保证返回对象在函数结束后仍然存在。不要返回局部变量的引用,因为局部变量离开函数后生命周期结束。
八、const 引用:权限只能缩小,不能放大
普通引用不能绑定到 const 对象:
const int a = 10;
// int& ra = a; // 错误:试图通过普通引用修改 const 对象
const int& ra = a;
const 引用可以绑定普通对象,因为这相当于缩小访问权限:
int b = 20;
const int& rb = b;
// rb++; // 错误:不能通过 const 引用修改对象
const 引用还能绑定临时对象:
int a = 10;
// int& r1 = a * 3; // 错误
const int& r2 = a * 3;
double d = 12.34;
// int& r3 = d; // 错误,类型转换会产生临时对象
const int& r4 = d;
这里的关键是:表达式结果、类型转换结果经常会产生临时对象,临时对象具有常性,不能用普通引用绑定,只能用 const 引用绑定。
九、指针和引用的区别
引用和指针有重叠场景,但不是同一个东西。
| 对比点 | 引用 | 指针 |
|---|---|---|
| 本质 | 已有对象的别名 | 保存对象地址的变量 |
| 初始化 | 定义时必须初始化 | 语法上不是必须,但建议初始化 |
| 是否能改变指向 | 不能 | 可以 |
| 访问方式 | 直接访问 | 需要解引用 |
| 空值问题 | 一般不表示空 | 可能为空指针 |
sizeof |
引用类型大小 | 指针本身大小,常见 64 位下 8 字节 |
简单理解:
- 如果参数一定有效,并且只是想简化调用,优先考虑引用;
- 如果参数可能为空,或者需要改变指向,指针更合适;
- 引用不是为了完全替代指针,而是让常见场景写得更安全、更自然。
十、inline:替代宏函数的一种更安全方式
用 inline 修饰的函数称为内联函数。它的设计目标是减少短小函数调用时建立栈帧的成本。
inline int Add(int x, int y) {
return x + y;
}
inline 更适合短小、频繁调用的函数。它只是给编译器一个建议,最终是否展开由编译器决定。递归函数、代码较长的函数,即使写了 inline,也可能不会被展开。
C 语言里常用宏函数减少调用成本:
#define ADD(a, b) ((a) + (b))
宏函数有不少坑,比如括号少了会导致优先级错误,也不方便调试。inline 保留函数的类型检查和调试友好性,因此比宏函数更适合大多数简单函数封装。
还有一个细节:inline 函数不建议只声明在头文件、定义放到单独源文件中。因为内联展开需要在调用处看到函数定义,否则可能引发链接问题。常见做法是把 inline 函数定义直接放在头文件里。
十一、nullptr:更安全的空指针
传统 C/C++ 代码中经常使用 NULL 表示空指针。但在 C++ 中,NULL 可能被定义为字面量 0,这会在函数重载场景下带来问题。
#include <iostream>
#include <cstddef>
void f(int x) {
std::cout << "f(int)" << std::endl;
}
void f(int* ptr) {
std::cout << "f(int*)" << std::endl;
}
int main() {
f(0);
f(NULL); // 容易出现非预期匹配或歧义,不推荐
f((int*)NULL); // 明确调用指针版本
f(nullptr); // C++11 后推荐
return 0;
}
常见实现中,NULL 可能被当作 0,从而匹配到整数版本。nullptr 是 C++11 引入的关键字,它可以转换为任意指针类型,但不会被当作整数使用。因此表达空指针时,推荐使用 nullptr。
十二、常见问题与易错点
1. 为什么工程中不建议直接 using namespace std
因为它会把整个 std 命名空间展开,项目变大后容易造成命名冲突。小练习可以图方便,工程代码更建议写 std::cout 或只展开少量常用成员。
2. 缺省参数为什么必须从右往左给
函数调用传参是从左往右匹配的。如果中间参数缺省,后面的实参就无法判断应该匹配哪个形参,因此 C++ 要求默认值从右侧连续给出。
3. 函数重载为什么不能只看返回值
调用函数时,编译器主要根据函数名和实参列表匹配目标函数。仅返回值不同,调用表达式本身无法区分要调用哪一个。
4. 引用是不是一定比指针好
不是。引用适合“对象一定存在”的场景,指针适合“可能为空”或“需要改变指向”的场景。二者是互补关系。
5. const int& r = a * 3 为什么可以
a * 3 的结果是临时对象,临时对象不能被普通引用绑定,但可以被 const 引用绑定。const 引用会延长该临时对象的生命周期到引用的生命周期。
6. inline 一定会展开吗
不一定。inline 是建议,不是命令。编译器会根据函数复杂度、优化级别、调试模式等因素决定是否展开。
十三、总结
C++ 入门阶段不要把所有语法点孤立记忆,可以按“解决什么问题”来理解:
namespace解决命名冲突;iostream提供类型更友好的输入输出方式;- 缺省参数让函数调用更灵活;
- 函数重载让同一操作可以适配不同参数;
- 引用让传参和返回更自然,但要关注生命周期;
const引用解决权限控制和临时对象绑定问题;inline用更安全的方式替代宏函数的部分场景;nullptr比NULL更适合表达空指针。
这些内容看起来分散,本质上都在增强 C++ 的表达能力和工程组织能力。把这些基础打稳,再继续学习类和对象、STL、模板、智能指针等内容时,很多语法设计就会顺起来。
更多推荐


所有评论(0)