🎯 本节目标

  1. 🔑 C++关键字
  2. 📦 命名空间
  3. 📥📤 C++输入 & 输出
  4. ⚙️ 缺省参数
  5. 🔄 函数重载
  6. 🏷️ 引用
  7. ⚡ 内联函数
  8. 🤖 auto关键字 (C++11)
  9. 🔄 基于范围的 for循环 (C++11)
  10. 🎯 指针空值 — nullptr(C++11)

🧭 本节知识点安排目的

C++ 是在 C 语言的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉 C 语言之后,对 C++ 学习有一定的帮助,本章节主要目标:

  1. 补充 C 语言语法的不足,以及 C++ 是如何对 C 语言设计不合理的地方进行优化的,比如:作用域方面、IO 方面、函数方面、指针方面、宏方面等。
  2. 为后续类和对象学习打基础

1. 🔑 C++关键字 (C++98)

C++ 总计 63 个关键字,C 语言 32 个关键字。

提示:下面我们只是看一下 C++ 有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲。

asm         do          if          return      try         continue
auto        double      inline      short       typedef     for
bool        dynamic_cast int        signed      typename    throw
break       else        long        sizeof      union       wchar_t
case        enum        mutable     static      unsigned    default
catch       explicit    namespace   static_cast using       friend
char        export      new         struct      virtual     register
const       false       private     template    void        true
const_cast  float       protected   this        volatile    while
delete      goto        reinterpret_cast

2. 📦 命名空间

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

2.1 命名空间定义

定义命名空间,需要使用到 namespace 关键字,后面跟 命名空间的名字,然后接一对 {} 即可,{} 中即为命名空间的成员。

#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
    printf("%d\n", rand);
    return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
// bit是命名空间的名字,一般开发中是用项目名字做命名空间名。
// 我们上课用的是bit,大家下去以后自己练习用自己名字缩写即可,如张三:zs
// 1. 正常的命名空间定义
namespace bit
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;
    int Add(int left, int right)
    {
        return left + right;
    }
    struct Node
    {
        struct Node* next;
        int val;
    };
}
// 注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
//2. 命名空间可以嵌套
// test.cpp
namespace N1
{
    int a;
    int b;
    int Add(int left, int right)
    {
        return left + right;
    }
    namespace N2
    {
        int c;
        int d;
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

2.2 命名空间使用

命名空间中成员该如何使用呢?比如:

namespace bit
{
    int a = 0;
    int b = 1;
    int Add(int left, int right)
    {
        return left + right;
    }
}
int main()
{
    // 编译报错:error C2065: “a”: 未声明的标识符
    printf("%d\n", a);
    return 0;
}

命名空间的使用有三种方式:

  1. 加命名空间名称及作用域限定符
  2. 使用 using 将命名空间中某个成员引入
  3. 使用 using namespace 命名空间名称引入
// 方式1:加命名空间名称及作用域限定符
int main()
{
    printf("%d\n", N::a);
    return 0;
}
// 方式2:使用using将命名空间中某个成员引入
using N::b;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    return 0;
}
// 方式3:使用using namespace 命名空间名称引入
using namespace N;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    Add(10, 20);
    return 0;
}

3. 📥📤 C++输入 & 输出

新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++ 刚出来后,也算是一个新事物,那 C++ 是否也应该向这个美好的世界来声问候呢?我们来看下 C++ 是如何来实现问候的。

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
    cout << "Hello world!!!" << endl;
    return 0;
}

说明:

  1. 使用 cout 标准输出对象 (控制台) 和 cin 标准输入对象 (键盘) 时,必须 包含 <iostream> 头文件 以及按命名空间使用方法使用 std
  2. coutcin 是全局的流对象,endl 是特殊的 C++ 符号,表示换行输出,他们都包含在包含 <iostream> 头文件中。
  3. << 是流插入运算符,>> 是流提取运算符。
  4. 使用 C++ 输入输出更方便,不需要像 printf/scanf 输入输出时那样,需要手动控制格式。C++ 的输入输出可以自动识别变量类型。
  5. 实际上 coutcin 分别是 ostreamistream 类型的对象,>><< 也涉及运算符重载等知识,这些知识我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习 IO 流用法及原理。

注意: 早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此 推荐 使用 <iostream>+std 的方式。

#include <iostream>
using namespace std;
int main()
{
    int a;
    double b;
    char c;
    // 可以自动识别变量的类型
    cin >> a;
    cin >> b >> c;
    cout << a << endl;
    cout << b << "  " << c << endl;
    return 0;
}

std 命名空间的使用惯例:
std 是 C++ 标准库的命名空间,如何展开 std 使用更合理呢?

  1. 在日常练习中,建议直接 using namespace std 即可,这样就很方便。
  2. using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像 std::cout 这样使用时指定命名空间 + using std::cout 展开常用的库对象/类型等方式。

补充说明: 关于 coutcin 还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为 C++ 兼容 C 语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,我们再配合文档学习。

4. ⚙️ 缺省参数

4.1 缺省参数概念

缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

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

4.2 缺省参数分类

  • 全缺省参数
// 全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}
  • 半缺省参数
// 半缺省参数
void Func(int a, int b = 10, int c = 20)
{
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
}

注意:

  1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给。
  2. 缺省参数不能在函数声明和定义中同时出现。
  3. 缺省值必须是常量或者全局变量。
  4. C 语言不支持(编译器不支持)。
// a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

5. 🔄 函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是 “谁也赢不了!”,后者是 “谁也赢不了!”

5.1 函数重载概念

函数重载: 是函数的一种特殊情况,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;
}

5.2 C++支持函数重载的原理 – 名字修饰 (name Mangling)

为什么 C++ 支持函数重载,而 C 语言不支持函数重载呢?

在 C/C++ 中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接
在这里插入图片描述
在这里插入图片描述

  1. 实际项目通常是由多个头文件和多个源文件构成,而通过 C 语言阶段学习的编译链接,我们可以知道,【当前 a.cpp 中调用了 b.cpp 中定义的 Add 函数时】,编译后链接前,a.o 的目标文件中没有 Add 的函数地址,因为 Add 是在 b.cpp 中定义的,所以 Add 的地址在 b.o 中。那么怎么办呢?
  2. 所以链接阶段就是专门处理这种问题,链接器看到 a.o 调用 Add,但是没有 Add 的地址,就会到 b.o 的符号表中找 Add 的地址,然后链接到一起
  3. 那么链接时,面对 Add 函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
  4. 由于 Windows 下 vs 的修饰规则过于复杂,而 Linux 下 g++ 的修饰规则简单易懂,下面我们使用了 g++ 演示了这个修饰后的名字。
  5. 通过下面我们可以看出 gcc 的函数修饰后名字不变。而 g++ 的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。

采用 C 语言编译器编译后结果
在这里插入图片描述
结论:在 linux 下,采用 gcc 编译完成后,函数名字的修饰没有发生改变。

采用 C++ 编译器编译后结果
在这里插入图片描述

结论:在 linux 下,采用 g++ 编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

Windows 下名字修饰规则
对比 Linux 会发现,windows 下 vs 编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。
在这里插入图片描述

扩展学习: C/C++ 函数调用约定和名字修饰规则 – 有兴趣好奇的朋友可以看看,里面有对 vs 下函数名修饰规则讲解。

  1. 通过这里就理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
  2. 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,

6. 🏷️ 引用

6.1 引用概念

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

比如:李逵,在家称为 “铁牛”,江湖上人称 “黑旋风”

类型& 引用变量名(对象名) = 引用实体;

注意: 引用类型必须和引用实体是同种类型的。

void TestRef()
{
    int a = 10;
    int& ra = a; //<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

6.2 引用特性

  1. 引用在 定义时必须初始化
  2. 一个变量可以有多个引用。
  3. 引用一旦引用一个实体,再不能引用其他实体。
void TestRef()
{
    int a = 10;
    // int& ra;   // 该条语句编译时会出错
    int& ra = a;
    int& rra = a;
    printf("%p  %p  %p\n", &a, &ra, &rra);
}

引用特性详解:

1. 定义时必须初始化
引用必须在定义时初始化,不能先声明后赋值。这是因为引用本质上是一个别名,必须在创建时就明确它代表哪个变量。

int a = 10;
int& ra = a;  // ✅ 正确:定义时初始化
// int& rb;    // ❌ 错误:引用必须初始化
// rb = a;     // ❌ 错误:不能先声明后赋值

2. 一个变量可以有多个引用
就像一个人可以有多个外号一样,一个变量也可以有多个引用。

int value = 100;
int& ref1 = value;  // 第一个引用
int& ref2 = value;  // 第二个引用
int& ref3 = ref1;   // 通过引用再创建引用

ref1 = 200;
cout << value << endl;   // 输出:200
cout << ref2 << endl;    // 输出:200
cout << ref3 << endl;    // 输出:200

3. 引用一旦引用一个实体,再不能引用其他实体
引用一旦绑定到一个变量,就不能再绑定到其他变量。这与指针不同,指针可以改变指向。

int a = 10;
int b = 20;
int& ra = a;  // ra 引用 a
// ra = b;     // 这是赋值操作,不是重新引用
// ra 仍然是 a 的引用,只是把 b 的值赋给了 a

cout << a << endl;  // 输出:20
cout << ra << endl; // 输出:20

// 不能这样写:
// int& ra = b;  // ❌ 错误:ra 已经引用 a,不能重新引用 b

引用与指针的区别:

  • 引用在定义时必须初始化,指针可以不初始化(但不安全)
  • 引用初始化后不能改变绑定,指针可以改变指向
  • 引用没有空值(null),指针可以为空
  • 引用更安全,指针更灵活
  • 引用在语法上更简洁,不需要解引用操作符

6.3 常引用

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10;  // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d;  // 该语句编译时会出错,类型不同
    const int& rd = d;
}

常引用的注意事项:

  1. 常量引用:对常量的引用必须是常量引用,不能是普通引用。
  2. 字面值引用:字面值(如10)只能被常量引用绑定。
  3. 类型转换:当发生隐式类型转换时,会产生临时变量,临时变量具有常性,所以只能用常量引用接收。
// 示例:类型转换时的临时变量
double d = 3.14;
// int& ri = d;  // 错误:类型不匹配
const int& ri = d;  // 正确:发生类型转换,产生临时int变量

// 等价于:
// const int temp = d;  // 产生临时变量
// const int& ri = temp;
// 示例:函数参数中的常引用
void PrintValue(const int& val)
{
    cout << val << endl;
}

int main()
{
    int a = 10;
    const int b = 20;
    PrintValue(a);    // 正确:普通变量可以传给常引用
    PrintValue(b);    // 正确:常量可以传给常引用
    PrintValue(30);   // 正确:字面值可以传给常引用
    return 0;
}

6.4 使用场景

  1. 做参数
void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}
  1. 做返回值
// 2. 做返回值
int& Count()
{
    static int n = 0;
    n++;
    // ...
    return n;
}

在这里插入图片描述

注意:

  • 引用做参数时,函数内部对形参的修改会直接影响实参,可以实现类似指针的效果,但语法更简洁。
  • 引用做返回值时,不能返回局部变量的引用,因为局部变量在函数结束后会被销毁,返回的引用会成为"野引用"。
  • 如果函数返回时,离开函数作用域后,其返回对象还未还给系统,则可以使用引用返回,否则必须使用传值返回。
// 错误示例:返回局部变量的引用
int& Add(int a, int b)
{
    int c = a + b;
    return c;  // 错误!c是局部变量,函数结束会被销毁
}

// 正确示例:返回静态变量或全局变量的引用
int& GetStaticValue()
{
    static int value = 0;
    value++;
    return value;  // 正确,静态变量生命周期持续到程序结束
}

7. ⚡ 内联函数

7.1 内联函数概念

内联函数 是 C++ 中一种特殊的函数,它通过 inline 关键字来声明。内联函数的目的是 减少函数调用的开销,提高程序运行效率。

当函数被声明为内联函数时,编译器会尝试在调用处直接展开函数体,而不是进行常规的函数调用(压栈、跳转、返回等操作)。这类似于 C 语言中的宏函数,但比宏更安全、更智能。

// 普通函数调用
int Add(int left, int right)
{
    return left + right;
}

int main()
{
    int ret = Add(1, 2);  // 函数调用:压栈、跳转、返回
    return 0;
}
// 内联函数
inline int Add(int left, int right)
{
    return left + right;
}

int main()
{
    int ret = Add(1, 2);  // 编译器可能会展开为:int ret = 1 + 2;
    return 0;
}

7.2 内联函数的特性

  1. inline 是一种以空间换时间的做法:如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。这可能会使目标文件变大,但减少了函数调用的开销,提高程序运行效率。

  2. inline 对于编译器而言只是一个建议:不同编译器关于 inline 实现机制可能不同。一般建议将函数规模较小、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 特性。

  3. inline 不建议声明和定义分离:内联函数不建议将声明和定义分离到不同的文件,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。

// 正确用法:内联函数的声明和定义在一起
inline int Add(int left, int right)
{
    return left + right;
}

// 错误示例:声明和定义分离
// test.h
inline int Add(int left, int right);

// test.cpp
inline int Add(int left, int right)  // 链接时会出错
{
    return left + right;
}

7.3 内联函数 vs 宏函数

C++ 中引入内联函数的一个重要原因是为了替代 C 语言中的宏函数,因为宏函数存在一些缺陷:

特性 宏函数 内联函数
类型检查 无类型检查,不安全 有严格的类型检查
调试 难以调试,展开后看不到源码 可以调试,保留函数特性
作用域 无作用域限制 遵循作用域规则
副作用 容易产生副作用 避免副作用
// 宏函数的缺陷示例
#define ADD(x, y) ((x) + (y))

int main()
{
    int a = 1, b = 2;
    int ret1 = ADD(a, b);      // 正确:((a) + (b))
    int ret2 = ADD(a++, b++);  // 危险:((a++) + (b++)),a和b被加了两次!
    return 0;
}
// 内联函数更安全
inline int Add(int left, int right)
{
    return left + right;
}

int main()
{
    int a = 1, b = 2;
    int ret1 = Add(a, b);      // 安全
    int ret2 = Add(a++, b++);  // 安全:参数先计算,再传递
    return 0;
}

7.4 内联函数的使用场景

  1. 小函数:函数体代码较少(通常不超过10行)
  2. 频繁调用的函数:在循环中或频繁调用的地方
  3. 性能关键路径:对性能要求较高的代码段
// 适合内联的小函数
inline int Max(int a, int b)
{
    return a > b ? a : b;
}

inline int Min(int a, int b)
{
    return a < b ? a : b;
}

inline bool IsEven(int num)
{
    return num % 2 == 0;
}

int main()
{
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int maxVal = arr[0];
    int minVal = arr[0];
    int evenCount = 0;
    
    for (int i = 0; i < 10; i++)
    {
        maxVal = Max(maxVal, arr[i]);  // 频繁调用,适合内联
        minVal = Min(minVal, arr[i]);  // 频繁调用,适合内联
        if (IsEven(arr[i]))            // 频繁调用,适合内联
            evenCount++;
    }
    
    cout << "最大值: " << maxVal << endl;
    cout << "最小值: " << minVal << endl;
    cout << "偶数个数: " << evenCount << endl;
    
    return 0;
}

7.5 内联函数的注意事项

  1. 递归函数不能作为内联函数:递归函数调用自身,展开会导致代码无限膨胀。
  2. 包含循环、switch 等复杂控制语句的函数:通常不适合内联。
  3. 虚函数不能是内联函数:虚函数需要在运行时确定调用哪个函数。
  4. 构造函数和析构函数:即使没有显式声明 inline,编译器也可能将其作为内联函数处理。
// 不适合内联的函数示例

// 1. 递归函数 - 不适合内联
int Fibonacci(int n)
{
    if (n <= 1) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);  // 递归调用
}

// 2. 包含循环的函数 - 通常不适合内联
void PrintArray(int arr[], int size)
{
    for (int i = 0; i < size; i++)  // 包含循环
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

// 3. 虚函数 - 不能是内联
class Base
{
public:
    virtual void Show()  // 虚函数
    {
        cout << "Base::Show()" << endl;
    }
};

7.6 内联函数在类中的使用

在类定义内部直接实现的成员函数,默认就是内联函数(隐式 inline)。

class Calculator
{
public:
    // 在类内定义,默认为内联函数
    int Add(int a, int b)
    {
        return a + b;
    }
    
    int Subtract(int a, int b)
    {
        return a - b;
    }
    
    // 也可以在类外定义,但需要显式声明为inline
    int Multiply(int a, int b);
    
    int Divide(int a, int b);
};

// 类外定义,需要显式声明为inline
inline int Calculator::Multiply(int a, int b)
{
    return a * b;
}

inline int Calculator::Divide(int a, int b)
{
    if (b != 0)
        return a / b;
    return 0;
}

int main()
{
    Calculator calc;
    cout << "10 + 5 = " << calc.Add(10, 5) << endl;
    cout << "10 - 5 = " << calc.Subtract(10, 5) << endl;
    cout << "10 * 5 = " << calc.Multiply(10, 5) << endl;
    cout << "10 / 5 = " << calc.Divide(10, 5) << endl;
}

7.7 内联函数的优缺点总结

优点:

  1. 提高效率:减少函数调用开销(压栈、跳转、返回)
  2. 类型安全:相比宏函数,有严格的类型检查
  3. 可调试:可以像普通函数一样调试
  4. 作用域:遵循 C++ 的作用域规则

缺点:

  1. 代码膨胀:如果函数体较大,多次展开会使代码体积增大
  2. 增加编译时间:需要更多时间编译
  3. 可能降低缓存命中率:代码膨胀可能影响 CPU 缓存效率

使用建议:

  • 对于简单的 getter/setter 函数,使用内联
  • 对于频繁调用的小函数,使用内联
  • 避免对复杂函数使用内联
  • 让编译器决定:现代编译器很智能,即使没有 inline 关键字,也可能将小函数内联
// 实际开发中的建议
class Point
{
private:
    int x;
    int y;
    
public:
    // 简单的getter/setter适合内联
    inline int GetX() const { return x; }
    inline int GetY() const { return y; }
    
    void SetX(int newX) { x = newX; }  // 类内定义,默认内联
    void SetY(int newY) { y = newY; }  // 类内定义,默认内联
    
    // 复杂函数不适合内联
    void ComplexOperation();  // 声明,在类外定义
};

// 复杂函数在类外定义,不内联
void Point::ComplexOperation()
{
    // 复杂的计算和逻辑
    // ...
}

8. 🤖 auto关键字 (C++11)

8.1 auto 的概念

在 C++11 中,auto 关键字被赋予了全新的含义:自动类型推导。使用 auto 声明变量时,编译器会根据变量的初始化表达式自动推导出变量的类型。

注意:C++98 中的 auto 表示"自动存储期",但在实际中几乎不使用,C++11 将其废弃并赋予了新的含义。

// auto 的基本用法
int main()
{
    auto a = 10;           // a 被推导为 int
    auto b = 3.14;         // b 被推导为 double
    auto c = 'A';          // c 被推导为 char
    auto d = "hello";      // d 被推导为 const char*
    
    // auto 与表达式
    auto e = a + b;        // e 被推导为 double(int + double)
    
    // auto 与引用
    int x = 10;
    int& ref = x;
    auto y = ref;          // y 被推导为 int(引用会被忽略)
    auto& z = ref;         // z 被推导为 int&
    
    return 0;
}

8.2 auto 的使用规则

1. auto 必须初始化

auto a;           // ❌ 错误:auto 必须初始化
auto b = 10;      // ✅ 正确

2. auto 可以同时声明多个变量,但类型必须一致

auto a = 10, b = 20;      // ✅ 正确:都是 int
auto c = 10, d = 3.14;    // ❌ 错误:类型不一致

3. auto 与 const 结合

const int ci = 10;
auto a = ci;              // a 是 int(顶层 const 被忽略)
const auto b = ci;        // b 是 const int
auto& c = ci;             // c 是 const int&(底层 const 保留)

4. auto 不能用于函数参数

void Func(auto a)         // ❌ 错误:auto 不能用于函数参数
{
    // ...
}

5. auto 不能用于数组类型

int arr[10];
auto a = arr;             // a 被推导为 int*,不是 int[10]
auto b[10] = arr;         // ❌ 错误:auto 不能用于数组声明

8.3 auto 的实际应用场景

场景一:简化迭代器声明

#include <iostream>
#include <vector>
#include <map>
using namespace std;

int main()
{
    vector<int> v = {1, 2, 3, 4, 5};
    map<string, int> m = {{"Alice", 90}, {"Bob", 85}};
    
    // 不使用 auto(繁琐)
    for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
    
    // 使用 auto(简洁)
    for (auto it = v.begin(); it != v.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
    
    // 遍历 map
    for (auto it = m.begin(); it != m.end(); ++it)
    {
        cout << it->first << ": " << it->second << endl;
    }
    
    return 0;
}

场景二:处理复杂类型

#include <iostream>
#include <vector>
using namespace std;

// 一个返回复杂类型的函数
vector<pair<int, string>> GetData()
{
    return {{1, "one"}, {2, "two"}, {3, "three"}};
}

int main()
{
    // 不使用 auto
    vector<pair<int, string>> data1 = GetData();
    
    // 使用 auto(推荐)
    auto data2 = GetData();
    
    // 遍历时使用 auto& 避免拷贝
    for (auto& item : data2)
    {
        cout << item.first << ": " << item.second << endl;
    }
    
    return 0;
}

场景三:lambda 表达式

#include <iostream>
using namespace std;

int main()
{
    // lambda 表达式的类型只有编译器知道,必须用 auto
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    
    auto multiply = [](int a, int b) {
        return a * b;
    };
    
    cout << "3 + 5 = " << add(3, 5) << endl;
    cout << "3 * 5 = " << multiply(3, 5) << endl;
    
    return 0;
}

8.4 auto 的注意事项

  1. 不要滥用 auto:在明确知道类型且类型简单时,直接写出类型更清晰。
  2. auto 不能推导出模板参数类型:如 vector<auto> 是错误的。
  3. auto 会忽略引用和顶层 const:需要时显式使用 auto&const auto
  4. auto 不能用于类的非静态成员变量
class Test
{
    auto a = 10;          // ❌ 错误:非静态成员不能使用 auto
    static const auto b = 20;  // ✅ 正确:静态常量成员可以使用 auto
};

9. 🔄 基于范围的 for循环 (C++11)

9.1 基本语法

C++11 引入了 基于范围的 for 循环(Range-based for loop),它提供了一种更简洁、更安全的遍历容器或数组的方式。

// 基本语法
for (元素类型 变量名 : 容器/数组)
{
    // 循环体
}
#include <iostream>
using namespace std;

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    
    // 传统 for 循环
    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
    
    // 基于范围的 for 循环
    for (int val : arr)
    {
        cout << val << " ";
    }
    cout << endl;
    
    return 0;
}

9.2 使用 auto 简化

基于范围的 for 循环通常与 auto 配合使用,让代码更加简洁:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> v = {10, 20, 30, 40, 50};
    
    // 1. 只读遍历:使用 const auto&
    for (const auto& val : v)
    {
        cout << val << " ";
    }
    cout << endl;
    
    // 2. 修改元素:使用 auto&
    for (auto& val : v)
    {
        val *= 2;  // 每个元素乘以2
    }
    
    // 3. 按值遍历(拷贝):使用 auto
    for (auto val : v)
    {
        cout << val << " ";
    }
    cout << endl;
    
    return 0;
}

9.3 遍历各种容器

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <string>
using namespace std;

int main()
{
    // 1. 遍历 vector
    vector<int> vec = {1, 2, 3, 4, 5};
    for (auto val : vec)
    {
        cout << val << " ";
    }
    cout << endl;
    
    // 2. 遍历 list
    list<string> lst = {"C++", "Java", "Python"};
    for (const auto& lang : lst)
    {
        cout << lang << " ";
    }
    cout << endl;
    
    // 3. 遍历 map
    map<string, int> scores = {{"Alice", 95}, {"Bob", 88}, {"Charlie", 92}};
    for (const auto& pair : scores)
    {
        cout << pair.first << ": " << pair.second << endl;
    }
    
    // 4. 遍历字符串
    string str = "Hello C++";
    for (char ch : str)
    {
        cout << ch << "-";
    }
    cout << endl;
    
    return 0;
}

9.4 注意事项

1. 不能在遍历时修改容器大小

vector<int> v = {1, 2, 3, 4, 5};
for (auto val : v)
{
    // v.push_back(6);  // ❌ 错误:遍历时修改容器大小会导致未定义行为
}

2. 遍历数组时,数组名不能退化为指针

void PrintArray(int arr[])  // arr 已经退化为指针
{
    // for (int val : arr)  // ❌ 错误:arr 是指针,不是数组
    // {
    //     cout << val << " ";
    // }
}

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    for (int val : arr)  // ✅ 正确:arr 是数组
    {
        cout << val << " ";
    }
    return 0;
}

3. 使用引用避免拷贝

vector<string> words = {"hello", "world", "C++"};
for (auto word : words)      // 每次循环都会拷贝一个 string,效率低
{
    cout << word << " ";
}

for (const auto& word : words)  // 使用引用,避免拷贝,效率高
{
    cout << word << " ";
}

10. 🎯 指针空值 — nullptr (C++11)

10.1 为什么需要 nullptr

在 C++98/03 中,我们通常使用 NULL0 来表示空指针。但 NULL 实际上是一个宏,通常被定义为 0((void*)0),这会导致一些微妙的问题:

#include <iostream>
using namespace std;

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

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

int main()
{
    f(0);       // 调用 f(int)
    f(NULL);    // 可能调用 f(int) 而不是 f(int*),取决于 NULL 的定义
    // f(nullptr);  // C++11:明确调用 f(int*)
    return 0;
}

问题分析:

  • NULL 在 C++ 中通常被定义为 0,所以 f(NULL) 会调用 f(int) 而不是 f(int*)
  • 这会导致函数重载解析出现不符合预期的结果。

10.2 nullptr 的引入

C++11 引入了 nullptr 关键字,它是一个 空指针常量,类型为 std::nullptr_t,可以隐式转换为任何指针类型,但不能转换为整型。

#include <iostream>
using namespace std;

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

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

int main()
{
    f(0);           // 调用 f(int)
    // f(NULL);     // 可能调用 f(int),有歧义
    f(nullptr);     // 明确调用 f(int*)
    
    // nullptr 的类型
    cout << typeid(nullptr).name() << endl;  // 输出:nullptr_t
    
    // nullptr 可以赋值给任何指针类型
    int* p1 = nullptr;
    char* p2 = nullptr;
    double* p3 = nullptr;
    void* p4 = nullptr;
    
    // nullptr 不能赋值给整型
    // int n = nullptr;  // ❌ 错误:不能隐式转换
    
    return 0;
}

10.3 nullptr 的使用场景

场景一:指针初始化

int* ptr = nullptr;  // 推荐:明确表示空指针

if (ptr == nullptr)
{
    cout << "指针为空" << endl;
}

// 检查指针是否为空
if (ptr)
{
    // 指针非空
}
else
{
    // 指针为空
}

场景二:函数返回值

#include <iostream>
using namespace std;

int* FindValue(int arr[], int size, int target)
{
    for (int i = 0; i < size; i++)
    {
        if (arr[i] == target)
        {
            return &arr[i];
        }
    }
    return nullptr;  // 未找到,返回空指针
}

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    int* result = FindValue(arr, 5, 3);
    
    if (result != nullptr)
    {
        cout << "找到值: " << *result << endl;
    }
    else
    {
        cout << "未找到" << endl;
    }
    
    return 0;
}

场景三:智能指针初始化

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 智能指针初始化
    unique_ptr<int> up1(nullptr);
    shared_ptr<int> sp1(nullptr);
    
    // 智能指针判空
    if (up1 == nullptr)
    {
        cout << "unique_ptr 为空" << endl;
    }
    
    // 重置智能指针
    shared_ptr<int> sp2 = make_shared<int>(42);
    sp2 = nullptr;  // 释放资源并置空
    
    return 0;
}

10.4 nullptr_t 类型

nullptr 的类型是 std::nullptr_t,它可以用于函数重载和模板编程:

#include <iostream>
using namespace std;

void Process(int)
{
    cout << "处理整型" << endl;
}

void Process(int*)
{
    cout << "处理指针" << endl;
}

void Process(nullptr_t)
{
    cout << "处理空指针类型" << endl;
}

int main()
{
    Process(0);         // 处理整型
    Process(nullptr);   // 处理空指针类型(优先匹配 nullptr_t)
    
    nullptr_t myNull = nullptr;
    Process(myNull);    // 处理空指针类型
    
    return 0;
}

10.5 总结与建议

  1. 优先使用 nullptr:在 C++11 及以后版本中,始终使用 nullptr 表示空指针。
  2. 避免使用 NULLNULL 存在类型安全问题,可能导致函数重载解析错误。
  3. 避免使用 0 表示空指针:虽然 0 可以隐式转换为指针,但语义不够清晰。
  4. nullptr 与条件判断nullptr 可以隐式转换为 boolfalse 表示空。
// 推荐写法
int* ptr1 = nullptr;
if (ptr1 == nullptr) { /* ... */ }
if (ptr1) { /* 非空 */ }

// 不推荐写法
int* ptr2 = NULL;   // 不推荐
int* ptr3 = 0;      // 不推荐

📝 本章总结

核心知识点回顾

知识点 关键要点 使用场景
命名空间 避免命名冲突,使用 namespace 定义 大型项目、库开发
输入输出 cin/cout,自动识别类型 控制台交互
缺省参数 从右向左依次给出,声明定义不能同时出现 函数参数有默认值
函数重载 参数类型/个数/顺序不同,与返回值无关 同名函数处理不同类型
引用 别名,必须初始化,不能改变绑定 参数传递、返回值优化
内联函数 空间换时间,适合小函数 频繁调用的简单函数
auto 自动类型推导,必须初始化 简化复杂类型声明
范围 for 简洁遍历容器/数组 遍历操作
nullptr 类型安全的空指针 指针初始化、判空

常见面试题

Q1:引用和指针的区别是什么?
A:引用必须初始化且不能改变绑定,指针可以不初始化且可以改变指向;引用没有空值,指针可以为空;引用更安全,指针更灵活;引用语法更简洁,不需要解引用操作符。

Q2:函数重载的底层原理是什么?
A:C++ 通过名字修饰(Name Mangling)技术,将函数名和参数类型信息组合成唯一的修饰名,使得同名函数可以共存。C 语言不支持函数重载,因为它的函数名修饰不包含参数信息。

Q3:内联函数和宏函数的区别?
A:内联函数有类型检查、可调试、遵循作用域规则;宏函数只是简单的文本替换,没有类型检查,容易产生副作用。C++ 推荐使用内联函数替代宏函数。

Q4:auto 关键字的使用注意事项?
A:auto 必须初始化;不能用于函数参数和数组声明;会忽略引用和顶层 const;不能用于类的非静态成员变量。

Q5:为什么 C++11 要引入 nullptr?
A:因为 NULL 在 C++ 中通常被定义为 0,会导致函数重载解析错误(如 f(NULL) 可能调用 f(int) 而不是 f(int*))。nullptr 是类型安全的空指针常量,可以明确表示空指针。

实践建议

  1. 命名空间:项目开发中使用 std::coutusing std::cout,避免 using namespace std
  2. 引用传参:对于自定义类型,优先使用 const T& 传参,避免拷贝。
  3. 内联函数:只对频繁调用的小函数使用 inline,让编译器决定是否真正内联。
  4. auto 使用:在类型复杂或不确定时使用 auto,简单类型直接写出更清晰。
  5. 范围 for:优先使用范围 for 遍历容器,需要索引时使用传统 for。
  6. nullptr:始终使用 nullptr 表示空指针,避免使用 NULL 或 0。

更多推荐