C++ 入门基础:从命名空间到引用、inline 与 nullptr

C++ 对很多 C 语言学习者来说并不陌生,因为它兼容 C 的大部分语法。但真正开始写 C++ 后,会发现它并不是“C 加几个新关键字”这么简单。它在 C 的基础上引入了命名空间、函数重载、引用、默认参数、内联函数、面向对象、泛型和标准库等能力,让大型程序更容易组织,也让代码表达能力更强。

这篇文章面向已经有一点 C 基础、准备正式进入 C++ 的读者。我们不追求一次讲完所有语法,而是抓住入门阶段最容易遇到、也最容易混淆的几个点:namespace、C++ 输入输出、缺省参数、函数重载、引用、const 引用、inlinenullptr

一、为什么要学 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::coutstd::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 用更安全的方式替代宏函数的部分场景;
  • nullptrNULL 更适合表达空指针。

这些内容看起来分散,本质上都在增强 C++ 的表达能力和工程组织能力。把这些基础打稳,再继续学习类和对象、STL、模板、智能指针等内容时,很多语法设计就会顺起来。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐