前言

C++是在C语言基础上发展而来的编程语言,兼容C语言绝大多数的语法。本文将从一个经典的C++程序开始,系统讲解C++入门阶段的核心语法知识,包括命名空间、输入输出、缺省参数、函数重载、引用、内联函数以及nullptr等关键特性。


1. C++的第一个程序

C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行。C++中需要把定义文件代码后缀改为.cpp,VS编译器看到是.cpp就会调用C++编译器编译,Linux下要用g++编译,不再是gcc

// test.cpp
#include <stdio.h>

int main()
{
    printf("hello world\n");
    return 0;
}

当然C++有自己的一套输入输出,严格来说C++版本的hello world应该是这样写的:

// test.cpp
#include <iostream>
using namespace std;

int main()
{
    cout << "hello world\n" << endl;
    return 0;
}

2. 命名空间(namespace)

2.1 namespace的价值

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

C语言项目类似下面程序这样的命名冲突是普遍存在的问题,C++引入namespace就是为了更好地解决这样的问题:

#include <stdio.h>
#include <stdlib.h>
int rand = 10;

int main()
{
    // 编译报错:error C2365: "rand": 重定义;以前的定义是"函数"
    printf("%d\n", rand);
    return 0;
}

2.2 namespace的定义

  • 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
  • namespace本质是定义出一个,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不再冲突了。
  • C++中域有函数局部域、全局域、命名空间域、类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
  • namespace只能定义在全局,当然它还可以嵌套定义。
  • 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
  • C++标准库都放在一个叫std(standard)的命名空间中。
  • C++17开始支持嵌套命名空间的简洁写法:namespace A::B::C { } 等价于 namespace A { namespace B { namespace C { } } }
  • 无名命名空间(namespace { ... })提供了内部链接性的替代方案——比起用static修饰全局变量,更推荐用无名命名空间来限制符号仅在本文件可见。

📖 参考:《C++ Primer Plus》第9章 —— 名称空间与内存模型

示例1:正常的命名空间定义

#include <stdio.h>
#include <stdlib.h>

// kira是命名空间的名字,一般开发中是用项目名字做命名空间名。
namespace kira
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;

    int Add(int left, int right)
    {
        return left + right;
    }

    struct Node
    {
        struct Node* next;
        int val;
    };
}

int main()
{
    // 这里默认访问的是全局的rand函数指针
    printf("%p\n", rand);
    // 这里指定kira命名空间中的rand
    printf("%d\n", bit::rand);

    return 0;
}

示例2:命名空间可以嵌套

namespace kiracrimson
{
    // kiracrimson
    namespace kira
    {
        int rand = 1;

        int Add(int left, int right)
        {
            return left + right;
        }
    }

    // crimson
    namespace crimson
    {
        int rand = 2;

        int Add(int left, int right)
        {
            return (left + right) * 10;
        }
    }
}

int main()
{
    printf("%d\n", kiracrimson::kira::rand);
    printf("%d\n", kiracrimson::crimson::rand);

    printf("%d\n", kiracrimson::kira::Add(1, 2));
    printf("%d\n", kiracrimson::kira::Add(1, 2));

    return 0;
}

示例3:多文件中同名namespace自动合并

// Stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

namespace kiracrimson
{
    typedef int STDataType;
    typedef struct Stack
    {
        STDataType* a;
        int top;
        int capacity;
    } ST;

    void STInit(ST* ps, int n);
    void STDestroy(ST* ps);
    void STPush(ST* ps, STDataType x);
    void STPop(ST* ps);
    STDataType STTop(ST* ps);
    int STSize(ST* ps);
    bool STEmpty(ST* ps);
}

// Stack.cpp
#include "Stack.h"
namespace kiracrimson
{
    void STInit(ST* ps, int n)
    {
        assert(ps);
        ps->a = (STDataType*)malloc(n * sizeof(STDataType));
        ps->top = 0;
        ps->capacity = n;
    }

    void STPush(ST* ps, STDataType x)
    {
        assert(ps);
        // 满了,扩容
        if (ps->top == ps->capacity)
        {
            printf("扩容\n");
            int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
            STDataType* tmp = (STDataType*)realloc(ps->a,
                newcapacity * sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            ps->a = tmp;
            ps->capacity = newcapacity;
        }
        ps->a[ps->top] = x;
        ps->top++;
    }
    // ...
}

2.3 命名空间的使用

编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错:

#include <stdio.h>

namespace N
{
    int a = 0;
    int b = 1;
}

int main()
{
    // 编译报错:error C2065: "a": 未声明的标识符
    printf("%d\n", a);
    return 0;
}

要使用命名空间中定义的变量/函数,有三种方式

方式一:指定命名空间访问(项目中推荐)

int main()
{
    printf("%d\n", N::a);
    return 0;
}

方式二:using将命名空间中某个成员展开(推荐用于经常访问且不冲突的成员)

using N::b;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    return 0;
}

方式三:展开命名空间中全部成员(项目不推荐,冲突风险大,日常小练习方便)

using namespace N;
int main()
{
    printf("%d\n", a);
    printf("%d\n", b);
    return 0;
}

⚠️ 特别提醒using namespace std; 绝对不要在头文件中使用。头文件会被多个源文件 #include,一旦展开 std 命名空间,所有包含该头文件的源文件都被强制污染,命名冲突风险无限放大。同样,用无名命名空间 namespace { ... } 代替 static 来限制全局变量的可见性,是现代 C++ 推荐的做法。

📖 参考:《C++ Primer Plus》第9章


3. C++输入 & 输出

  • <iostream> 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
  • std::cinistream 类的对象,它主要面向窄字符的标准输入流。
  • std::coutostream 类的对象,它主要面向窄字符的标准输出流。
  • std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
  • << 是流插入运算符,>> 是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
  • 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的),其实最重要的是C++的流能更好地支持自定义类型对象的输入输出。
  • cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用它们。
  • 一般日常练习中可以using namespace std,实际项目开发中不建议using namespace std
#include <iostream>
using namespace std;

int main()
{
    int a = 0;
    double b = 0.1;
    char c = 'x';

    cout << a << " " << b << " " << c << endl;
    std::cout << a << " " << b << " " << c << std::endl;

    scanf("%d%lf", &a, &b);
    printf("%d %lf\n", a, b);

    // 可以自动识别变量的类型
    cin >> a;
    cin >> b >> c;

    cout << a << endl;
    cout << b << " " << c << endl;

    return 0;
}

提高C++ IO效率的技巧:

在IO需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码可以提高C++ IO效率:

#include <iostream>
using namespace std;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // ...

    return 0;
}

4. 缺省参数

  • 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。缺省参数分为全缺省半缺省参数。(有些地方把缺省参数也叫默认参数)
  • 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
  • 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
  • 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须在函数声明给缺省值
#include <iostream>
using namespace std;

void Func(int a = 0)
{
    cout << a << endl;
}

int main()
{
    Func();     // 没有传参时,使用参数的默认值
    Func(10);   // 传参时,使用指定的实参
    return 0;
}

全缺省与半缺省示例:

#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;
}

// 半缺省(从右向左依次连续)
void Func2(int a, int b = 10, int c = 20)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl << endl;
}

int main()
{
    Func1();
    Func1(1);
    Func1(1, 2);
    Func1(1, 2, 3);

    Func2(100);
    Func2(100, 200);
    Func2(100, 200, 300);

    return 0;
}

实际应用:Stack初始化

// Stack.h
#include <iostream>
#include <assert.h>
using namespace std;

typedef int STDataType;
typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

void STInit(ST* ps, int n = 4);  // 声明中给缺省值

// Stack.cpp
#include "Stack.h"
// 缺省参数不能声明和定义同时给
void STInit(ST* ps, int n)
{
    assert(ps && n > 0);
    ps->a = (STDataType*)malloc(n * sizeof(STDataType));
    ps->top = 0;
    ps->capacity = n;
}

// test.cpp
#include "Stack.h"
int main()
{
    ST s1;
    STInit(&s1);       // 使用默认大小4

    // 确定知道要插入1000个数据,初始化时一把开好,避免扩容
    ST s2;
    STInit(&s2, 1000); // 指定大小1000

    return 0;
}

5. 函数重载

C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。

重载的三种情况:

#include <iostream>
using namespace std;

// 1、参数类型不同
int Add(int left, int right)
{
    cout << "int Add(int left, int right)" << endl;
    return left + right;
}

double Add(double left, double right)
{
    cout << "double Add(double left, double right)" << endl;
    return left + right;
}

// 2、参数个数不同
void f()
{
    cout << "f()" << endl;
}

void f(int a)
{
    cout << "f(int a)" << endl;
}

// 3、参数类型顺序不同
void f(int a, char b)
{
    cout << "f(int a, char b)" << endl;
}

void f(char b, int a)
{
    cout << "f(char b, int a)" << endl;
}

int main()
{
    Add(10, 20);
    Add(10.1, 20.2);

    f();
    f(10);

    f(10, 'a');
    f('a', 10);

    return 0;
}

注意:

  • 返回值不同不能作为重载条件,因为调用时也无法区分
  • 下面两个函数构成重载,但调用 f() 时会报错,存在歧义,编译器不知道调用谁:
void f1() {}
void f1(int a = 10) {}  // 与f1()构成重载,但调用f1()时产生歧义

6. 引用

6.1 引用的概念和定义

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头。

语法格式:类型& 引用别名 = 引用对象;

#include <iostream>
using namespace std;

int main()
{
    int a = 0;

    // 引用:b和c是a的别名
    int& b = a;
    int& c = a;
    // 也可以给别名b取别名,d相当于还是a的别名
    int& d = b;

    ++d;

    // 这里取地址我们看到是一样的
    cout << &a << endl;
    cout << &b << endl;
    cout << &c << endl;
    cout << &d << endl;

    return 0;
}

6.2 引用的特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;

int main()
{
    int a = 10;

    // 编译报错:"ra": 必须初始化引用
    // int& ra;

    int& b = a;

    int c = 20;
    // 这里并非让b引用c,因为C++引用不能改变指向,
    // 这里是一个赋值
    b = c;

    cout << &a << endl;
    cout << &b << endl;
    cout << &c << endl;

    return 0;
}

6.3 引用的使用

引用在实践中主要用于引用传参引用做返回值中减少拷贝提高效率,以及在改变引用对象时同时改变被引用对象。

  • 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大区别的,除了用法,最大的点是C++引用定义后不能改变指向,而Java的引用可以改变指向。

为什么传引用优于传值?

Effective C++ 条款20把这个道理讲得很透彻:

  1. 效率:传值需要调用拷贝构造函数和析构函数,对于包含大量数据的对象来说开销很大。传引用只是传递一个别名,零拷贝。
  2. 避免对象切割(slicing):如果派生类对象以传值方式传入一个基类形参,派生类特有的部分会被"切掉",函数内部只看到一个基类对象。传引用则保留完整类型。
  3. 例外:内置类型(int、double等)、STL迭代器和函数对象仍建议传值——它们体积小,拷贝开销可忽略。
// 传值:发生切割
void display(Window w) { w.show(); }      // 传入 SpecialWindow 时被切为 Window

// 传引用:保持完整
void display(const Window& w) { w.show(); } // 传入什么类型就是什么类型

📖 参考:《Effective C++》条款20 —— 以pass-by-reference-to-const替换pass-by-value

引用传参示例:

// 交换两个数
void Swap(int& rx, int& ry)
{
    int tmp = rx;
    rx = ry;
    ry = tmp;
}

int main()
{
    int x = 0, y = 1;
    cout << x << " " << y << endl;

    Swap(x, y);
    cout << x << " " << y << endl;

    return 0;
}

引用做返回值示例:

#include <iostream>
using namespace std;

typedef int STDataType;
typedef struct Stack
{
    STDataType* a;
    int top;
    int capacity;
} ST;

void STInit(ST& rs, int n = 4)
{
    rs.a = (STDataType*)malloc(n * sizeof(STDataType));
    rs.top = 0;
    rs.capacity = n;
}

void STPush(ST& rs, STDataType x)
{
    // 满了,扩容
    if (rs.top == rs.capacity)
    {
        printf("扩容\n");
        int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
        STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *
            sizeof(STDataType));
        if (tmp == NULL)
        {
            perror("realloc fail");
            return;
        }
        rs.a = tmp;
        rs.capacity = newcapacity;
    }

    rs.a[rs.top] = x;
    rs.top++;
}

// 引用做返回值
int& STTop(ST& rs)
{
    assert(rs.top > 0);
    return rs.a[rs.top - 1];
}

int main()
{
    ST st1;
    STInit(st1);
    STPush(st1, 1);
    STPush(st1, 2);

    cout << STTop(st1) << endl;

    STTop(st1) += 10;   // 通过引用返回值修改栈顶元素

    cout << STTop(st1) << endl;

    return 0;
}

指针变量取别名(替代二级指针):

typedef struct ListNode
{
    int val;
    struct ListNode* next;
} LTNode, *PNode;

// 指针变量也可以取别名,这里LTNode*& phead就是给指针变量取别名
// 这样就不需要用二级指针了,相对而言简化了程序
// void ListPushBack(LTNode** phead, int x)
// void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
    PNode newnode = (PNode)malloc(sizeof(LTNode));
    newnode->val = x;
    newnode->next = NULL;
    if (phead == NULL)
    {
        phead = newnode;
    }
    else
    {
        // ...
    }
}

int main()
{
    PNode plist = NULL;
    ListPushBack(plist, 1);
    return 0;
}

6.4 const引用

  • 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
  • 类似 int& rb = a * 3;double d = 12.34; int& rd = d; 这样的场景下,a*3的结果保存在一个临时对象中,int& rd = d 在类型转换中也会产生临时对象存储中间值,也就是 rbrd 引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
  • 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
  • 一个很有用的特性:const引用可以延长临时对象的生命周期。当你写 const int& ref = 30; 时,临时创建的int对象不会在语句结束后销毁,而是活到ref离开作用域为止。

📖 参考:《C++ Primer Plus》第8章 —— const引用与临时对象

权限放大与缩小:

int main()
{
    const int a = 10;
    // 编译报错:error C2440: "初始化": 无法从"const int"转换为"int &"
    // 这里的引用是对a访问权限的放大
    // int& ra = a;

    // 这样才可以
    const int& ra = a;
    // 编译报错:error C3892: "ra": 不能给常量赋值
    // ra++;

    // 这里的引用是对b访问权限的缩小
    int b = 20;
    const int& rb = b;
    // 编译报错:error C3892: "rb": 不能给常量赋值
    // rb++;

    return 0;
}

临时对象与const引用:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;

    const int& ra = 30;         // OK

    // 编译报错: "初始化": 无法从"int"转换为"int &"
    // int& rb = a * 3;
    const int& rb = a * 3;      // OK

    double d = 12.34;
    // 编译报错:"初始化": 无法从"double"转换为"int &"
    // int& rd = d;
    const int& rd = d;          // OK

    return 0;
}

6.5 指针和引用的关系

C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中它们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。

对比维度 引用 指针
语法概念 变量的别名,不开空间 存储变量地址,要开空间
初始化 定义时必须初始化 建议初始化,语法上非必须
能否改变指向 不能改变 可以不断改变指向对象
访问方式 直接访问指向对象 需要解引用才能访问对象
sizeof 结果为引用类型的大小 始终是地址空间所占字节(32位4字节,64位8字节)
安全性 很少出现空/野引用问题 容易出现空指针和野指针

7. 内联函数(inline)

  • inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,可以提高效率。
  • inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数、代码相对多一些的函数,加上inline也会被编译器忽略。
  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,且不方便调试,C++设计inline的目的就是替代C的宏函数
  • VS编译器 debug版本下默认是不展开inline的,这样方便调试,debug版本想展开需要进行一些设置。
  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

为什么宏函数那么坑?三个括号的真相:

Effective C++ 条款02把宏函数的三大缺陷讲得很清楚:

#define ADD(a, b) ((a) + (b))
// 为什么需要三个保护层?

// 1. 外层括号 — 防止运算符优先级劫持
//    ADD(1, 2) * 5  →  ((1) + (2)) * 5 = 15  ✓
//    如果写成 (a + b) →  1 + 2 * 5 = 11      ✗

// 2. 内层括号 — 防止表达式参数被拆分
//    ADD(x & y, x | y)  →  ((x & y) + (x | y))  ✓
//    如果写成 a + b     →  x & y + x | y        ✗ (位运算优先级低于+)

// 3. 不能加分号 — 加分号会截断if/else语句
//    if (cond) ADD(1, 2); else ...  → if (cond) ((1)+(2));; else ...  ✗

宏函数的三个致命缺陷:不检查类型、不能调试(预处理阶段就被替换了)、行为诡异(参数被多次求值——ADD(++a, b) 会让a加两次)。inline 函数完美避免所有这些问题——类型安全、可断点调试、参数只求值一次。

📖 参考:《Effective C++》条款02 —— 以const, enum, inline替换 #define

inline 不是万能药:

Effective C++ 条款30 补充了几个容易被忽略的现实问题:

  • 构造函数和析构函数看起来短小精悍,但编译器会默默插入基类构造、成员对象构造等大量代码——所以不适合声明为 inline。
  • 虚函数不能 inline(虚函数靠运行时 vtable 查找,inline 要求编译期展开,这两者逻辑冲突)。
  • 库中的 inline 函数一旦修改,所有用到它的客户端代码都需要重新编译——而非 inline 函数只需要重新链接。
class Base {
public:
    inline virtual void f() {}  // virtual + inline?这个inline不会被展开
};

📖 参考:《Effective C++》条款30 —— 透彻了解inlining

inline示例:

#include <iostream>
using namespace std;

inline int Add(int x, int y)
{
    int ret = x + y;
    ret += 1;
    ret += 1;
    ret += 1;
    return ret;
}

int main()
{
    // 可以通过汇编观察程序是否展开
    // 有call Add语句就是没有展开,没有就是展开了
    int ret = Add(1, 2);
    cout << Add(1, 2) * 5 << endl;
    return 0;
}

宏函数的坑(inline要解决的问题):

#include <iostream>
using namespace std;

// 实现一个ADD宏函数的常见问题
// #define ADD(int a, int b) return a + b;      // 错误
// #define ADD(a, b) a + b;                     // 错误
// #define ADD(a, b) (a + b)                    // 不够

// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外面的括号?
// 为什么要加里面的括号?

int main()
{
    int ret = ADD(1, 2);

    cout << ADD(1, 2) << endl;
    cout << ADD(1, 2) * 5 << endl;  // 不加外层括号会出错

    int x = 1, y = 2;
    ADD(x & y, x | y); // 不加内层括号 -> (x&y + x|y),运算优先级错误

    return 0;
}

inline声明与定义分离导致链接错误:

// F.h
#include <iostream>
using namespace std;
inline void f(int i);

// F.cpp
#include "F.h"
void f(int i)
{
    cout << i << endl;
}

// main.cpp
#include "F.h"
int main()
{
    // 链接错误:无法解析的外部符号 "void __cdecl f(int)"
    f(10);
    return 0;
}

8. nullptr

NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
    #ifdef __cplusplus
        #define NULL    0
    #else
        #define NULL    ((void *)0)
    #endif
#endif
  • C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免地会遇到一些麻烦。
  • C++11中引入nullptrnullptr是一个特殊的关键字,是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
  • 从 Effective C++ 条款04 的视角来看,选择 nullptr 而不用 NULL 本质上是"确保对象使用前被正确初始化"原则在指针上的体现。NULL(即0)作为空指针标记,类型本身就是模糊的——它既是整数又是空指针。nullptr 从语言层面消灭了这种歧义。在现代 C++ 中,裸写 NULL0 表示空指针已经是过时做法。

📖 参考:《Effective C++》条款04 —— 确定对象使用前已被初始化

#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()
{
    f(0);            // 调用 f(int x)

    // 本想通过f(NULL)调用指针版本的f(int*)函数,
    // 但是由于NULL被定义成0,调用了f(int x),与程序初衷相悖
    f(NULL);         // 调用 f(int x) —— 不是我们想要的!
    f((int*)NULL);   // 调用 f(int* ptr) —— 需要强制转换

    // 编译报错:error C2665: "f": 2个重载中没有一个可以转换所有参数类型
    // f((void*)NULL);

    f(nullptr);      // 调用 f(int* ptr) —— 正确!

    return 0;
}

总结: 在C++中,推荐统一使用nullptr来表示空指针,避免使用NULL0,以防止函数重载时的歧义问题。


总结

本文从C++的第一个程序出发,系统地介绍了C++入门阶段的核心语法:

  1. 命名空间(namespace):解决命名冲突,实现标识符本地化
  2. 输入输出(cin/cout):更便捷的IO方式,支持自动类型识别
  3. 缺省参数:函数参数的默认值机制,分为全缺省和半缺省
  4. 函数重载:同一作用域内同名函数的不同形参形式
  5. 引用:变量的别名,用于传参和返回值减少拷贝
  6. 内联函数(inline):替代宏函数,在调用处展开以提高效率
  7. nullptr:C++11引入的空指针关键字,解决NULL的歧义问题

这些特性构成了C++区别于C语言的重要基础,掌握它们是深入学习C++面向对象编程、STL等内容的前提。

更多推荐