🎯 本节目标
🔑 C++关键字 📦 命名空间 📥📤 C++输入 & 输出 ⚙️ 缺省参数 🔄 函数重载 🏷️ 引用 ⚡ 内联函数 🤖 auto关键字 (C++11) 🔄 基于范围的 for循环 (C++11) 🎯 指针空值 — nullptr(C++11)
🧭 本节知识点安排目的
C++ 是在 C 语言的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉 C 语言之后,对 C++ 学习有一定的帮助,本章节主要目标:
补充 C 语言语法的不足 ,以及 C++ 是如何对 C 语言设计不合理的地方进行优化的,比如:作用域方面、IO 方面、函数方面、指针方面、宏方面等。为后续类和对象学习打基础 。
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; } 复制代码 命名空间的使用有三种方式:
加命名空间名称及作用域限定符 使用 using 将命名空间中某个成员引入 使用 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; } 复制代码 说明:
使用 cout 标准输出对象 (控制台) 和 cin 标准输入对象 (键盘) 时,必须 包含 头文件 以及按命名空间使用方法使用 std。 cout 和 cin 是全局的流对象,endl 是特殊的 C++ 符号,表示换行输出,他们都包含在包含 头文件中。 << 是流插入运算符,>> 是流提取运算符。 使用 C++ 输入输出更方便,不需要像 printf/scanf 输入输出时那样,需要手动控制格式。C++ 的输入输出可以自动识别变量类型。 实际上 cout 和 cin 分别是 ostream 和 istream 类型的对象,>> 和 << 也涉及运算符重载等知识,这些知识我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习 IO 流用法及原理。
注意: 早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h;旧编译器 (vc 6.0) 中还支持 格式,后续编译器已不支持,因此 推荐 使用 +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 使用更合理呢?
在日常练习中,建议直接 using namespace std 即可,这样就很方便。 using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像 std::cout 这样使用时指定命名空间 + using std::cout 展开常用的库对象/类型等方式。
补充说明: 关于 cout 和 cin 还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为 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; } 复制代码 注意:
半缺省参数必须 从右往左依次 来给出,不能间隔着给。 缺省参数不能在函数声明和定义中同时出现。 缺省值必须是常量或者全局变量。 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++ 中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接 。
实际项目通常是由多个头文件和多个源文件构成,而通过 C 语言阶段学习的编译链接,我们可以知道,【当前 a.cpp 中调用了 b.cpp 中定义的 Add 函数时】,编译后链接前,a.o 的目标文件中没有 Add 的函数地址,因为 Add 是在 b.cpp 中定义的,所以 Add 的地址在 b.o 中。那么怎么办呢? 所以链接阶段就是专门处理这种问题,链接器看到 a.o 调用 Add,但是没有 Add 的地址,就会到 b.o 的符号表中找 Add 的地址,然后链接到一起 。 那么链接时,面对 Add 函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。 由于 Windows 下 vs 的修饰规则过于复杂,而 Linux 下 g++ 的修饰规则简单易懂,下面我们使用了 g++ 演示了这个修饰后的名字。 通过下面我们可以看出 gcc 的函数修饰后名字不变。而 g++ 的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
采用 C 语言编译器编译后结果
结论:
在 linux 下,采用 gcc 编译完成后,函数名字的修饰没有发生改变。
采用 C++ 编译器编译后结果
结论:在 linux 下,采用 g++ 编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
Windows 下名字修饰规则
对比 Linux 会发现,windows 下 vs 编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。
扩展学习: C/C++ 函数调用约定和名字修饰规则 – 有兴趣好奇的朋友可以看看,里面有对 vs 下函数名修饰规则讲解。
通过这里就理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,
6. 🏷️ 引用
6.1 引用概念
引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间 。
比如:李逵,在家称为 “铁牛” ,江湖上人称 “黑旋风” 。
注意: 引用类型必须和引用实体是同种类型的。
void TestRef() { int a = 10; int& ra = a; //<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); } 复制代码 6.2 引用特性
引用在 定义时必须初始化 。 一个变量可以有多个引用。 引用一旦引用一个实体,再不能引用其他实体。 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; } 复制代码 常引用的注意事项:
常量引用 :对常量的引用必须是常量引用,不能是普通引用。字面值引用 :字面值(如10)只能被常量引用绑定。类型转换 :当发生隐式类型转换时,会产生临时变量,临时变量具有常性,所以只能用常量引用接收。// 示例:类型转换时的临时变量 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 使用场景
做参数 void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } 复制代码 做返回值 // 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 内联函数的特性
inline 是一种以空间换时间的做法 :如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。这可能会使目标文件变大,但减少了函数调用的开销,提高程序运行效率。
inline 对于编译器而言只是一个建议 :不同编译器关于 inline 实现机制可能不同。一般建议将函数规模较小、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 特性。
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 内联函数的使用场景
小函数 :函数体代码较少(通常不超过10行)频繁调用的函数 :在循环中或频繁调用的地方性能关键路径 :对性能要求较高的代码段// 适合内联的小函数 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 内联函数的注意事项
递归函数不能作为内联函数 :递归函数调用自身,展开会导致代码无限膨胀。包含循环、switch 等复杂控制语句的函数 :通常不适合内联。虚函数不能是内联函数 :虚函数需要在运行时确定调用哪个函数。构造函数和析构函数 :即使没有显式声明 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 内联函数的优缺点总结
优点:
提高效率 :减少函数调用开销(压栈、跳转、返回)类型安全 :相比宏函数,有严格的类型检查可调试 :可以像普通函数一样调试作用域 :遵循 C++ 的作用域规则
缺点:
代码膨胀 :如果函数体较大,多次展开会使代码体积增大增加编译时间 :需要更多时间编译可能降低缓存命中率 :代码膨胀可能影响 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 的注意事项
不要滥用 auto :在明确知道类型且类型简单时,直接写出类型更清晰。auto 不能推导出模板参数类型 :如 vector 是错误的。 auto 会忽略引用和顶层 const :需要时显式使用 auto& 或 const auto。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 中,我们通常使用 NULL 或 0 来表示空指针。但 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 总结与建议
优先使用 nullptr :在 C++11 及以后版本中,始终使用 nullptr 表示空指针。避免使用 NULL :NULL 存在类型安全问题,可能导致函数重载解析错误。避免使用 0 表示空指针 :虽然 0 可以隐式转换为指针,但语义不够清晰。nullptr 与条件判断 :nullptr 可以隐式转换为 bool,false 表示空。// 推荐写法 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 是类型安全的空指针常量,可以明确表示空指针。
实践建议
命名空间 :项目开发中使用 std::cout 或 using std::cout,避免 using namespace std。引用传参 :对于自定义类型,优先使用 const T& 传参,避免拷贝。内联函数 :只对频繁调用的小函数使用 inline,让编译器决定是否真正内联。auto 使用 :在类型复杂或不确定时使用 auto,简单类型直接写出更清晰。范围 for :优先使用范围 for 遍历容器,需要索引时使用传统 for。nullptr :始终使用 nullptr 表示空指针,避免使用 NULL 或 0。