目录
前言
C++的主要优点
C++作为一门已有40多年历史的编程语言,能够在快速变化的计算机领域长期保持重要地位,主要归功于以下几个核心优势:1. 高性能与高效性
- 接近硬件的性能:C++代码编译后效率极高,几乎可以达到手写汇编的性能
- 零成本抽象:高级特性(如模板、RAII)几乎不引入运行时开销
- 精细的内存控制:允许手动内存管理,避免垃圾回收的不可预测停顿
- 内联和优化:编译器可以进行深度优化,生成极其高效的机器码
2. 多范式支持
- 面向对象编程:支持类、继承、多态等OOP特性
- 泛型编程:通过模板实现高度灵活的通用代码
- 过程式编程:保留C风格的过程化编程能力
- 函数式编程:C++11后引入lambda、函数对象等函数式特性
- 元编程:通过模板元编程在编译期执行复杂计算
3. 广泛的应用领域
- 系统/操作系统开发:Windows/Linux内核、驱动等
- 游戏开发:Unreal、Unity等主流游戏引擎
- 高频交易:金融行业的低延迟系统
- 嵌入式系统:资源受限的物联网设备
- 科学计算:高性能数值计算和模拟
- 图形处理:3D渲染、计算机视觉等
4. 精细的资源控制
- 确定性的资源管理:通过RAII模式保证资源及时释放
- 直接内存访问:指针和引用提供底层内存操作能力
- 自定义内存管理:可重载new/delete,实现特殊内存分配策略
- 位级操作:可直接操作硬件寄存器和特定内存位置
5. 丰富的标准库和生态系统
- STL(标准模板库):提供强大的容器、算法和迭代器
- Boost等高质量库:扩展了标准库的功能
- 跨平台支持:可在几乎所有硬件平台和操作系统上运行
- 成熟的工具链:强大的编译器(如GCC、Clang、MSVC)和调试工具
6. 向后兼容与稳定性
- 长期兼容性:几十年积累的代码仍可编译运行
- 标准化发展:由ISO委员会规范,发展稳健可控
- 渐进式改进:每三年发布新标准,平衡创新与稳定
7. 社区与行业支持
- 庞大的开发者社区:全球数百万C++开发者
- 行业标准地位:许多关键系统依赖C++实现
- 持续现代化:C++11/14/17/20/23不断引入现代特性
- 教育价值:学习C++能深入理解计算机原理
C++值得吐槽的痛点
尽管C++功能强大,但它确实存在不少让人抓狂的设计缺陷和历史包袱。以下是被开发者广泛吐槽的几个方面:1. 语法复杂且不一致
- 操作符重载滥用:<< 既表示位移又用于输出,+ 可能执行任意操作
- 最恼人的解析(Most Vexing Parse):TimeKeeper time_keeper(Timer()); 实际声明函数而非对象
- 初始化方式过多:C风格、构造函数、列表初始化、统一初始化等至少5种方式
- 模板错误信息灾难:简单的模板错误能产生上百行难以理解的编译器输出
2. 历史包袱沉重
- 兼容C带来的问题:必须保留C的原始指针、宏、隐式转换等危险特性
- 头文件系统过时:#include 是简单的文本替换,导致编译缓慢
- 预处理器滥用:宏污染命名空间,难以调试(C++20模块才开始改善)
3. 内存安全问题
- 悬垂指针/引用:访问已释放内存是常见错误
- 手动内存管理:忘记delete导致内存泄漏,或重复delete导致崩溃
- 未定义行为(UB)陷阱:数组越界、空指针解引用等行为完全不可预测
4. 编译与构建问题
- 编译速度慢:模板实例化使编译时间随项目规模指数增长
- 二进制兼容性问题:不同编译器甚至同一编译器的不同版本可能不兼容
- 构建系统复杂:CMake语法反人类,但又是事实标准
5. 学习曲线陡峭
- 特性膨胀:C++11/14/17/20/23不断增加新特性,语言规范已超2000页
- 多重范式混乱:同一项目可能混杂面向对象、泛型、函数式等多种风格
- 专家级陷阱:即使经验丰富的开发者也会踩中UB或模板元编程的坑
6. 现代化进程的尴尬
- 新特性采用缓慢:许多项目仍停留在C++98/03,因旧代码迁移成本高
- 半吊子改进:比如std::string_view解决了一些问题,但依然不是默认字符串类型
- 模块化姗姗来迟:C++20才引入模块,生态迁移需要多年
7. 工具链痛点
- 调试困难:模板展开后的代码、优化后的二进制难以调试
- 包管理落后:没有官方包管理器(Conan/vcpkg等第三方方案各有缺陷)
- IDE支持有限:由于语言复杂性,代码补全和重构不如Java/C#流畅
正如Bjarne Stroustrup所说:"C++的设计目标就是让你能轻松射击自己脚部,同时让高手能写出优雅高效的代码。"
1、域
域有类域、命名空间域、局部域、全局域等。
// 全局域
int a = 0;
// 命名空间域
namespace yu
{
int a = 1;
}
int main()
{
// 局部域
int a = 2;
return 0;
}
main 函数中如果要访问 a 变量,默认先在局部域访问,如果局部域没有定义,就在全局域访问,如果全局域没有定义,并且没有展开命名空间域或指定访问命名空间域,编译器不会在命名空间域访问,而是会报错。
:: 限定域访问符
:: 的作用是告诉编译器优先在哪里访问,使用的基本形式:
域 :: 变量名
如果 :: 前面什么都没有或只有空格,表示在全局域访问,如果是命名空间名,表示在命名空间内访问。
2、命名空间
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d", rand);
return 0;
}
以上程序会报错:“rand”: 重定义;以前的定义是“函数”,原因是 stdlib.h 文件中定义了 rand 函数,我们又定义了一个全局变量也叫 rand,这就产生了命名冲突的问题。C++ 采用“命名空间”来解决此类问题。
举个例子来说明命名空间如何使用:
#include <stdio.h>
#include <stdlib.h>
namespace variable
{
int a = 10;
}
namespace bit
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
// 命名空间可以嵌套
// test.cpp
namespace N1
{
int a;
int b;
int a1;
int Add(int left, int right)
{
return left + right;
}
// N1 中嵌套 N2
namespace N2
{
int c;
int d;
int a1;//与命名空间 N1 的 a1 是可以同时存在的
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
//全部展开命名空间
using namespace variable;
//部分展开命名空间
using N1::b;
int main()
{
printf("%d", a);
//访问嵌套的命名空间里的变量
printf("%d",N1::N2::a);
//访问命名空间里的函数
printf("%d",N1::Add(1,1));
return 0;
}
定义了命名空间 variable ,并展开该命名空间。using namespace 的作用是将命名空间包含在全局域。如果全局域也有一个变量叫 a,编译器会报错说 a 不明确,所以不能随便轻易使用 using 关键字。但可以使用 using 关键字将命名空间部分展开,比如说 using std::cout;
运用命名空间和限定域访问符就可以解决命名冲突的问题:
#include <stdio.h>
#include <stdlib.h>
namespace variable
{
int rand = 10;
}
int main()
{
// 访问自己定义的 rand
printf("%d\n", variable::rand);
// 访问库定义的 rand
printf("%p\n", rand);
return 0;
}
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用像std :: cout这样展开常用的库对象/类型等方式。
3、输入输出
需要包含头文件:include <iostream>,并且要展开 cout 、 endl、cin 等。
<< 流插入运算符,>> 流提取运算符
输出
输出字符串:
cout << "Hello world" << endl;
在屏幕上就打印了 ”Hello world“,cout 可以理解为终端(黑框框), << 可以理解为”流向“,即 ”Hello world“ 流向了终端(黑框框),endl 是 end line,等价于 ‘\n' 。
输出变量:
int x = 1;
double y = 1.1;
cout << x << y << endl;
相对于 printf 来说这种方式可以自动识别变量的类型,用相应的方式来输出变量。如果想控制输出变量的小数位数,可以使用 c 语言的 printf ,而不使用 c++ 的方式,因为 c 语言的方式更方便。
输入
int a = 0;
cin >> a;
一次输入多个变量:
int a , b , c;
cin >> a >> b >> c;
在终端中输入 1 2 3 再按回车,a,b,c就被赋值为 1,2,3.
c 语言的 printf 和 scanf 比 c++ 的效率要更高。
getline 函数
有时候向string对象输入字符时,不想以空格为分割(仍以‘\n' 为输入结束),就可以使用 getline 函数:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string a;
getline(cin, a);
return 0;
}
在终端输入 “a b c d ", a 的内容就是 a b c d。
istream 的 get 成员函数
istream 类有一个成员函数叫 get ,它可以获取包括空格和换行符的输入:
#include<iostream>
#include<string>
using namespace std;
int main()
{
char ch;
ch = cin.get();
return 0;
}
但这样就无法结束输入了,通常结合循环条件来结束输入:
#include<iostream>
#include<string>
using namespace std;
int main()
{
char ch;
string s;
ch = cin.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = cin.get();
}
return 0;
}
如果循环条件是 while(ch != '\n') ,那就是 getline 的模拟实现了。
4、缺省参数
缺省参数(默认参数)是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
// 没有传参时,使用参数的默认值
Func();
// 传参时,使用指定的实参
Func(10);
return 0;
}
缺省参数的分类:
全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " <<<<< endl;
}
int main()
{
Func();
Func(1); // 1 传给了 a
Func(1,2); // 1 传给了 a,2 传给了 b
Func(1,2,3);
//Func(1,,3) error
return 0;
}
半缺省参数
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << engl << endl;
}
注意:
1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现,防止函数声明和定义中的缺省参数不一致。缺省参数只能在函数声明中出现,因为预处理阶段包含头文件时将函数声明拷贝替换的是函数声明,如果缺省参数在函数定义出现,那么编译器在检查函数调用时的参数是否合法时会报错
缺省参数的应用
在初始化栈的时候,有时候我们已知数据元素的个数,那么我们在初始化时候能不能开辟相应的大小呢?是可以的,只需要给初始化函数多设置一个形参就行了,但这个形参又不是必须接收实参的,比如不知道数据元素的个数,这时我们不可以随便开辟一定空间,因为可能开小了要扩容,开大了要浪费空间,这时就可以使用缺省参数:
void StackInit(struct Stack* pst, int defaultCapacity = 4)
{
pst->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (pst->a == NULL)
{
perror("malloc fail");
return;
}
pst->top = 0;
pst->capacity = defaultCapacity;
}
int main()
{
struct Stack st1;
// 插入100个数据
StackInit(&st1, 100);
// 不知道要插入多少数据
struct Stack st2;
StackInit(&st2);
}
defaultCapacity 就是一个缺省参数
5、函数重载
函数重载:是函数的一种特殊情况,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;
}
// 4、参数的权限不同
void f(int a)
{
cout << "f(int a)" << endl;
}
void f(const int a)
{
cout << "f(int a)" << endl;
}
// 5、函数的权限不同
void f(int a)
{
cout << "f(int a)" << endl;
}
void f(int a) const
{
cout << "f(int a)" << endl;
}
// 6、参数的引用类型不同
void f(int& a)
{
cout << "f(int a)" << endl;
}
void f(int&& a) const
{
cout << "f(int a)" << endl;
}
会发生歧义的函数重载:
void f()
{
cout << "f()" << endl;
}
void f(int a = 0)
{
cout << "f(int a)" << endl;
}
按照函数重载的定义,以上函数构成重载,但如果无参调用会有歧义。
c++ 是如何支持函数重载的呢?
// Stack.h
typedf StackType int;
struct Stack
{
StackType* a;
int top;
int capacity;
};
void StackInit(struct Stack* pst, int defaultCapacity = 4)
{
pst->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (pst->a == NULL)
{
perror("malloc fail");
return;
}
pst->top = 0;
pst->capacity = defaultCapacity;
}
void StackPush(struct Stack* pst,int a);
void StackPush(struct Stack* pst,double a);
// Stack.cpp
#include "Stack.h"
void StackPush(struct Stack* pst,int a)
{
printf("StackPush(struct Stack* pst,int a)");
}
void StackPush(struct Stack* pst,double a)
{
printf("StackPush(struct Stack* pst,double a)");
}
// Test.c
#include "Stack.h"
int main
{
Stack s;
StackInit(&s);
StackPUsh(&s,1);
StackPUsh(&s,1.1);
return 0;
}
编译链接过程回顾:


在 call 指令后有函数的地址 07B1492h ,执行 call 指令后会跳转到 jmp 指令,jmp 指令会跳转到函数。那么 StackPush 函数的地址是在什么时候生成的呢?
在 Test.i 经过编译后生成 Test.s 后,函数的地址并没有在 Test.s 中生成,而是在 Stack.s 中生成。因为 Test.s 包含的头文件里是函数的声明而不是定义,在 Stack.s 里才有函数的定义。Test.i 经过编译后生成 Test.s 的过程中,编译器只是检查调用函数时的实参与函数声明(函数声明是预处理阶段拷贝过来的,这也说明了函数中的缺省参数要在声明中定义)中的形参是否匹配,函数的地址编译器暂时不管,而函数的地址是在链接时确定。如果函数的定义也在 Test.i ,那么在编译时直接生成该函数地址。
在 c 语言中,直接靠函数名来区分函数,但在 c++ 中,生成的两个函数名不同:

C++ 函数名修饰(Name Mangling)规则
函数名修饰(Name Mangling)是 C++ 编译器在编译过程中对函数名进行编码的一种机制,目的是为了支持 函数重载、命名空间、模板 等 C++ 特性。由于 C++ 允许函数重载(相同函数名,不同参数列表),编译器需要生成唯一的符号名,以便链接器正确识别和链接。
1. 为什么需要 Name Mangling?
在 C 语言中,函数名在符号表中就是原始名称(如 printf),但 C++ 由于支持:
-
函数重载(
void foo(int)和void foo(double)) -
命名空间(
ns::func) -
类成员函数(
MyClass::method) -
模板函数(
std::vector<int>::push_back)
编译器必须对函数名进行编码,使其唯一,例如:
-
void foo(int)→_Z3fooi -
void foo(double)→_Z3food
2. 不同编译器的修饰规则
C++ 标准没有规定统一的 Name Mangling 规则,不同编译器采用不同的方案:
(1) GCC/Clang (Itanium ABI)
采用 Itanium C++ ABI 规则(Linux/macOS 默认):
-
格式:
_Z + 函数名长度 + 函数名 + 参数类型编码 -
示例:
int foo(int); // _Z3fooi float bar(double, int); // _Z3bardf namespace ns { void func(); // _ZN2ns4funcEv } class MyClass { void method(); // _ZN7MyClass6methodEv };
(2) MSVC (Microsoft ABI)
Windows 平台使用不同的规则:
-
格式:
? + 函数名 + 参数类型 + 返回类型 + 其他修饰 -
示例:
int foo(int); // ?foo@@YAHH@Z float bar(double, int); // ?bar@@YAMHN@Z namespace ns { void func(); // ?func@ns@@YAXXZ } class MyClass { void method(); // ?method@MyClass@@QEAAXXZ };
6、引用
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
int a = 10;
int& ra = a;// <==== 定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注意:引用类型必须和引用实体是同种类型的

引用这种语法优化了指针的复杂性:
1、Swap 函数不再使用指针:Swap 函数现在可以直接传变量而不是变量的地址。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
// 对比:使用指针
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
2、交换一级指针的指向不再使用二级指针:
void Swap(int*& a, int*& b)
{
int* tmp = a;
a = b;
b = tmp;
}
// 对比:使用二级指针
void Swap(int** a, int** b)
{
int* tmp = *a;
*a = *b;
*b = tmp;
}
引用的使用场景
1. 做输出型参数(形参的改变影响到实参的值)
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
2. 做返回值(传值返回与传引用返回)
int& Count ()
{
static int n = 0;
n++;
// ...
return n;
}
函数返回值时,通常将返回的值先存储在寄存器中,等退出函数时,函数栈帧被销毁,此时待返回的值在寄存器中。在返回到的函数栈帧中,寄存器的值赋值给调用函数的表达式,作为该表达式的值。寄存器的大小通常只有 4 字节。
如果返回的值是静态的变量,那么该返回值仍会被存储到寄存器里去吗?
答案是会,因为这样可以简化编译器的设计,编译器不想考虑多种情况,它想作统一处理。
那怎么不使用寄存器(临时变量)呢?
方法当然是使用引用作为返回值。
不使用寄存器(临时变量)有什么价值?
如果函数的参数是大对象(占用空间很多),这样可以减少拷贝,提升效率。
如果以上代码的 n 不是静态的:
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
这里打印 ret 的值是不确定的
如果 Count 函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的。
如果 Count 函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值。
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;
Count();
cout << ret << endl;
return 0;
}
总结:
1、基本任何场景都可以用引用传参。2、谨慎用引用做返回值。出了函数作用域,对象不在,就不能用引用返回,还在就可以用引用返回。
引用作返回值的两大功能:
1、 减少拷贝,提升效率
2、 获取、修改返回值
获取、修改返回值:
int& SLAt(SeqList& ps, int pos)
{
assert(pos < 100 && pos >= 0);
return s.a[pos];
}
以上代码可以获取顺序表第 pos 位置的值,并且可以对 pos 位置的值在函数外直接修改
//对下标为 0 的位置赋值为 1
SLAt(s, 0) = 1;
//对下标为 0 的位置增加 5
SLAt(s, 0) += 5;
如果不使用引用,则要先获取第 pos 位置的值(SLGet 函数),再对 pos 位置进行修改 SLModify 函数)
常引用
在引用过程中,权限可以平移或缩小,但不能放大。
const int a = 0;
int& b = a;
// 不可以
// 引用过程中,权限不能放大
int x = 0;
int& y = x;
// 可以
// 引用过程中,权限可以平移或者缩小
const int& z = x; //缩小 z 作为引用的权限
++x;//可以 ++x,此时 z 的值增加 1,但不能 ++z
const int& a = 10;//给常量取别名
double a = 1.1
int b = a; // 可以,隐式类型转换
// 隐式类型转换会产生临时变量,临时变量具有常性,要用常引用
int& b = a; // 不可以
const int& b = a; // 可以
不管是隐式类型转换还是显式类型转换都会产生临时变量,临时变量具有常性,要传递给常引用的函数参数(const 类型名&)

上图解释了为什么 double 类型可以取 const int& 类型的别名,因为临时变量具有常性。
int func1()
{
static int x = 0;
return x;
}
int& func2()
{
static int x = 0;
return x;
}
func1 函数的返回值不能用 int& 类型接收,因为函数的返回值具有常性。要用 const int& 类型接收

func2 函数的返回值可以用 int& 类型接收(权限平移),也可以用 const int& 类型接收(权限缩小)
指针的引用
在某个类中有以下变量的声明:
float* &c;
c 是 一个指向 float 类型的指针的引用。
引用与指针的区别

从底层汇编指令实现的角度看,引用是类似指针的方式实现的
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
7、auto 关键字

auto 可以根据右边的的表达式自动推导要创建的变量的类型。当变量的类型很长时,可以用 auto 简化。

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用细则
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main ()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x:
return 0;
}
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto ()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto 不能推导的场景
1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
2. auto不能直接用来声明数组
void TestAuto
{
int a[] = {1,2,3};
auto b[] = {4, 5, 6};
}
8、基于范围的 for 循环
基本用法:
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
依次取 arr 的元素赋值给 e,自动判断结束。
由于 e 是 arr 元素的拷贝,所以 e 的改变不影响 arr,如果要用范围 for 来修改 arr 数组,e 必须是arr 元素的引用:
// 修改数据
for (auto& e : arr)
{
e *= 2;
}
如果 arr 中的每个元素所占空间比较大,为了提升效率,也可以使用引用。
范围for的使用条件
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor (int array[])
{
for (auto& e : array)
cout << e << endl;
}
范围 for 的底层实现原理是迭代器
9、内联函数
有些函数需要被频繁的调用,调用函数时建立函数栈帧的时间开销很大。
#include<iostream>
using namespace std;
int Add(int x, int y)
{
return (x + y) * 10;
}
int main()
{
for (int i = 0; i < 10000; i++)
{
cout << Add(i, i + 1) << endl;
}
return 0;
}
上面的 Add 函数要被调用 10000 次,就要建立 10000 次栈帧。
解决方法1:宏函数
#define Add(X,Y) (((X) + (Y)) * 10)
解决方法2:内联函数
inline int Add(int x, int y)
{
return (x + y) * 10;
}
inline 关键字使得函数在调用的地方不会生成 call 指令,而是被替换为函数的指令。假如有一个函数叫 func ,该函数经过编译后有 50 行指令,调用函数时只有一句 call 指令,如果该函数不是内联函数,在一个项目中被调用 10000 次,那么总计有 10000 + 50 行指令,如果该函数是内联函数,那么总计有 10000 * 50 行指令。指令变多会使软件的安装包变大。
inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定
像类似这些函数就算加了 inline 也会被编译器否决:
1、比较长的函数 2、递归函数
inline 关键字在默认 debug 模式下不会生效,否则不方便调试。可以设置:

inline不建议声明和定义分离,分离会导致链接错误。因为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()
{
f(10);
return 0;
}
// 链接错误: main.obj : error LNK2019: 无法解析的外部符号
//"void_cdecl f(int)" (? f@@YAXH@Z),该符号在函数 _main 中被引用
解决方法是将内联函数的声明和定义都放在头文件中。
10、nullptr
NULL 和 nullptr 都用于表示空指针,但它们在C++中有重要区别:
NULL
传统C/C++中的空指针常量
通常是定义为整数0的宏:
#define NULL 0类型不明确,可以是整数或指针类型
可能导致函数重载的不明确调用问题
nullptr
C++11引入的关键字
明确的空指针常量,类型为
std::nullptr_t不会与整数类型混淆
提供更好的类型安全
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
以上代码中,按我们的理解 f(0) 应该调用 f(int) ,f(NULL) 应该调用 f(int*) ,但实际上 f(0) 和 f(NULL) 都是调用的 f(int)。
在C++98中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr)与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在表示指针空值时建议最好使用nullptr。
4万+

被折叠的 条评论
为什么被折叠?



