C++ 入门核心知识点全解析 | 面试复习必备指南
🎯 本节目标
- 🔑 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标准输入对象 (键盘) 时,必须 包含<iostream>头文件 以及按命名空间使用方法使用std。 cout和cin是全局的流对象,endl是特殊的 C++ 符号,表示换行输出,他们都包含在包含<iostream>头文件中。<<是流插入运算符,>>是流提取运算符。- 使用 C++ 输入输出更方便,不需要像
printf/scanf输入输出时那样,需要手动控制格式。C++ 的输入输出可以自动识别变量类型。 - 实际上
cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习 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 使用更合理呢?
- 在日常练习中,建议直接
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>是错误的。 - 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。
更多推荐
所有评论(0)