C++ 实践笔记
随缘更 (~ ̄▽ ̄)~
C++11 新特性
C++的发展
- 早期的 C++ 被称为 “C with Classes”,引入了类(Class)概念,以及构造函数、析构函数和基本的面向对象编程支持。
- 1983 年,“C++” 的名字首次出现,代表着比 C 更进一步,同年,第一个正式的 C++ 编译器 Cfront 诞生,这是基于 C 编译器的一个预处理器。
- 1998 年,C++98,成为第一个 ISO 标准版本。
- 2003 年,C++03,是对 C++98 的小幅修订,主要修复了细节问题。
- 2011 年,C++11,C++的崛起,引入了许多革命性特性。如:auto 关键字、Lambda 表达式等。
- 2014 年,C++14,小幅改进 C++11,增强了 Lambda 表达式和标准库。
- 2017 年,C++17,新增特性包括结构化绑定(Structured Bindings)、std::optional 和 文件系统支持。更关注现代开发需求,尤其是代码可读性和性能优化。
- 2020 年,C++20,大规模升级,包括概念(Concepts)、协程(Coroutines)和模块化(Modules),C++20 被认为是现代 C++ 的一个里程碑。
- 2023 年,C++23,继续改进协程和标准库。
auto 关键字
见文章Lambda表达式decltype 关键字
decltype
可用于获取表达式的类型1
2int x = 42;
decltype(x) y = 10; // y 的类型为 intlambda 表达式
见文章Lambda表达式右值引用
- 左值:指可以出现在赋值操作符左边的表达式,表示一个内存位置,可以取地址。
- 右值:指不能出现在赋值操作符左边的表达式,通常是临时对象或字面值,表示一个值而不是一个内存位置。
- 左值引用:左值引用是对左值的引用,可以通过
&
符号来定义。左值引用允许我们通过引用来访问和修改对象。1
2
3int a = 10;
int &ref = a; // ref 是 a 的左值引用
ref = 20; // 修改 ref 也会修改 a - 右值引用:通过
&&
符号来定义。右值引用允许我们捕获和修改临时对象,主要用于实现移动语义和完美转发。1
2int &&rref = 10; // rref 是一个右值引用,引用了临时右值 10
rref = 20; // 修改 rref - 移动语义:右值引用的核心意义在于引入移动语义,优化对象的拷贝行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyString {
std::string data;
public:
MyString(const std::string& str) : data(str) {} // 拷贝构造函数
MyString(std::string&& str) : data(std::move(str)) {} // 移动构造函数
};
int main() {
std::string str = "Hello";
MyString obj1(str); // 调用拷贝构造函数
MyString obj2(std::move(str)); // 调用移动构造函数
return 0;
} std::unique_ptr
: 独占所有权。std::shared_ptr
: 共享所有权。
1 |
|
constexpr
用于在编译时计算常量表达式。它可以用于变量、函数和构造函数,以提高程序的性能和安全性。1
2constexpr int square(int x) { return x * x; }
constexpr int result = square(5); // result 在编译时计算
范围for循环
用于简化容器和数组的遍历1
2
3
4std::vector<int> vec = {1, 2, 3};
for (auto x : vec) {
std::cout << x << " ";
}
默认与删除的函数
显式声明函数为默认或禁止:1
2
3
4
5class MyClass {
public:
MyClass() = default; // 使用默认构造函数
MyClass(const MyClass&) = delete; // 禁止拷贝构造函数
};
function
模板类
- 是 C++11 中引入的一个模板类,用来包装任何可调用对象,如普通函数、Lambda 表达式、函数指针、成员函数、仿函数等。它使得函数和函数指针可以像对象一样传递和使用,提供了更大的灵活性。
- 用法示例:
1
2
3
4
5
6
7
8
9
10
11
12
void printMessage(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::function<void(const std::string&)> func = printMessage; //定义了一个变量,用来存储输入类型为const std::string&,输出类型为void的函数。并给其赋值,像使用变量一样。
func("Hello, <functional>!");
return 0;
}
auto关键字
auto(C++11新特性)
C++11赋予了auto新的定义,让其做自动类型推导,也就是说,编译器可以在编译期间自动推算出变量的类型,这样就可以更加方便的编写代码了。
使用:
- auto 用法最常见的场景是声明变量时,让编译器根据初始化表达式自动推导出变量的类型。
- 在使用 STL 容器(如 std::vector、std::map 等)时,auto 能大大简化代码,尤其是在使用迭代器时。
1
2
3
4
5
6
7
8
9
10
11
12
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 auto 推导类型,避免手动指定迭代器类型
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
return 0;
} - C++11 引入了范围-based for 循环(range-based for loop),auto 关键字与之结合可以使代码更简洁,尤其是在处理容器元素时。
1
2
3
4
5std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 auto 自动推导类型来遍历容器元素
for (auto element : vec) {
std::cout << element << " ";
} - 与lambda配合使用。
注意:
- auto 不能在函数的参数中使用。
- auto 关键字不能定义数组
auto(C++14新特性)
C++14中auto可以作为返回值了
Lambda表达式(C++11新特性)
Lambda表达式:是 C++11引入的一种函数对象,可以方便地创建匿名函数。与传统的函数不同,Lambda表达式可以在定义时直接嵌入代码,无需单独定义函数名称、参数和返回类型等信息。Lambda表达式通常用于需要定义一些简单的回调函数或者函数对象。
什么是 Lambda表达式
Lambda表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。Lambda表达式的基本语法如下:1
[capture list] (parameter list) -> return type { function body }
其中:
- capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。捕获列表可以为空,表示不访问任何外部变量,也可以使用默认捕获模式 & 或 = 来表示按引用或按值捕获所有外部变量,还可以混合使用具体的变量名和默认捕获模式来指定不同的捕获方式。
- parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称,还可以在 c++14 中使用 auto 关键字来实现泛型参数。
- return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体推导,也可以使用 -> 符号显式指定,还可以在 c++14 中使用 auto 关键字来实现泛型返回值。
function body 是函数体,用于表示 Lambda表达式的具体逻辑,可以是一条语句,也可以是多条语句,还可以在 c++14 中使用 constexpr 来实现编译期计算。
return type一般省略,所以lambda表达式一般式这个形式
1
[capture list] (parameter list) { function body }
理解
首先例如1
2
3
4[]
{
cout << "hello lambda" << endl;
}这是一个最简单的lambda表达式,在它后面加上()就可以调用它。
但是,不会这么用,给它一个名字,由于不知道什么类型,所以用auto1
2
3
4auto L= []
{
cout << "hello lambda" << endl;
};使用L()就可以调用它了。
lambda表达式的捕获方法
- 值捕获(capture by value):在捕获列表中使用变量名,表示将该变量的值拷贝到 Lambda 表达式中,作为一个数据成员。值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化。值捕获的变量默认不能在 Lambda 表达式中修改,除非使用
mutable
关键字。
例如1
2
3
4int x = 10;
auto f = [x] (int y) { return x + y; }; // 值捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 15,不受外部 x 的影响 - 引用捕获(capture by reference):在捕获列表中使用
&
加变量名,表示将该变量的引用传递到 Lambda 表达式中,作为一个数据成员。引用捕获的变量在 Lambda 表达式调用时才确定,会随着外部变量的变化而变化。引用捕获的变量可以在 Lambda 表达式中修改,但要注意生命周期的问题,避免悬空引用的出现。1
2
3
4int x = 10;
auto f = [x] (int y) { return x + y; }; // 值捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 25,受外部 x 的影响 - 隐式捕获(implicit capture):在捕获列表中使用 = 或 &,表示按值或按引用捕获 Lambda 表达式中使用的所有外部变量。这种方式可以简化捕获列表的书写,避免过长或遗漏。隐式捕获可以和显式捕获混合使用,但不能和同类型的显式捕获一起使用。
1
2
3
4
5
6int x = 10;
int y = 20;
auto f = [=, &y] (int z) { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y
x = 30; // 修改外部的 x
y = 40; // 修改外部的 y
cout << f(5) << endl; // 输出 55,不受外部 x 的影响,受外部 y 的影响
- 值捕获(capture by value):在捕获列表中使用变量名,表示将该变量的值拷贝到 Lambda 表达式中,作为一个数据成员。值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化。值捕获的变量默认不能在 Lambda 表达式中修改,除非使用
- lambda表达式的使用
- 定义简单的匿名函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
int main()
{
// 定义一个 Lambda表达式,计算两个数的和
auto plus = [] (int a, int b) -> int { return a + b; };
// 调用 Lambda表达式
cout << plus(3, 4) << endl; // 输出 7
// 定义一个 Lambda表达式,判断一个数是否为奇数
auto is_odd = [] (int n) { return n % 2 == 1; };
// 调用 Lambda表达式
cout << is_odd(5) << endl; // 输出 1
cout << is_odd(6) << endl; // 输出 0
return 0;
} - 捕获外部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using namespace std;
int main()
{
int x = 10;
int y = 20;
// 定义一个 Lambda表达式,按值捕获 x 和 y
auto add = [x, y] () -> int { return x + y; };
// 调用 Lambda表达式
cout << add() << endl; // 输出 30
// 修改 x 和 y 的值
x = 100;
y = 200;
// 再次调用 Lambda表达式
cout << add() << endl; // 输出 30,捕获的是 x 和 y 的副本,不受外部变化的影响
// 定义一个 Lambda表达式,按引用捕获 x 和 y
auto mul = [&x, &y] () -> int { return x * y; };
// 调用 Lambda表达式
cout << mul() << endl; // 输出 20000
// 修改 x 和 y 的值
x = 1000;
y = 2000;
// 再次调用 Lambda表达式
cout << mul() << endl; // 输出 2000000,捕获的是 x 和 y 的引用,会反映外部变化的影响
return 0;
} - Lambda表达式作为函数参数
- Lambda表达式作为函数返回值
- 定义简单的匿名函数
- lambda表达式的实质
Lambda表达式虽然是一种语法糖,但它本质上也是一种函数对象,也就是重载了 operator() 的类的对象。每一个 Lambda表达式都对应一个唯一的匿名类,这个类的名称由编译器自动生成,因此我们无法直接获取或使用。Lambda表达式的捕获列表实际上是匿名类的数据成员,Lambda表达式的参数列表和返回值类型实际上是匿名类的 operator() 的参数列表和返回值类型,Lambda表达式的函数体实际上是匿名类的 operator() 的函数体。
例如1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int x = 10;
auto f = [x] (int y) -> int { return x + y; };
//相当于定义了一个匿名类,类似于:
int x = 10;
class __lambda_1
{
public:
__lambda_1(int x) : __x(x) {} // 构造函数,用于初始化捕获的变量
int operator() (int y) const // 重载的 operator(),用于调用 Lambda表达式
{
return __x + y; // 函数体,与 Lambda表达式的函数体相同
}
private:
int __x; // 数据成员,用于存储捕获的变量
};
auto f = __lambda_1(x); // 创建一个匿名类的对象,相当于 Lambda表达式lambda(C++14)
- 初始化捕获(init capture):C++14 引入的一种新的捕获方式,它允许在捕获列表中使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。这种方式可以使用 auto 关键字来推导类型,也可以显式指定类型。这种方式可以用来捕获只移动的变量,或者捕获 this 指针的值。
1
2
3
4int x = 10;
auto f = [z = x + 5] (int y) -> int { return z + y; }; // 初始化捕获 z,相当于值捕获 x + 5
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 20,不受外部 x 的影响 - 泛型 Lambda:C++14 允许在 Lambda表达式的参数列表和返回值类型中使用 auto 关键字,从而实现泛型 Lambda,即可以接受任意类型的参数和返回任意类型的值的 Lambda表达式。
lambda(C++17)
- 捕获 this 指针:C++17 允许在 Lambda表达式的捕获列表中使用 *this,从而实现捕获 this 指针,即可以在 Lambda表达式中访问当前对象的成员变量和成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using namespace std;
// 定义一个类
class Test
{
public:
Test(int n) : num(n) {} // 构造函数,初始化 num
void show() // 成员函数,显示 num
{
cout << num << endl;
}
void add(int x) // 成员函数,增加 num
{
// 定义一个 Lambda表达式,捕获 this 指针
auto f = [*this] () { return num + x; };
// 调用 Lambda表达式
cout << f() << endl;
}
private:
int num; // 成员变量,存储一个整数
};
int main()
{
Test t(10); // 创建一个 Test 对象
t.show(); // 调用成员函数,输出 10
t.add(5); // 调用成员函数,输出 15
return 0;
}
explicit 关键字
explicit 关键字用于修饰类的构造函数。它的作用是防止隐式的类型转换。
当一个类的构造函数只有一个参数时,如果没有 explicit 关键字修饰,C++ 编译器可能会允许将该参数类型的对象隐式地转换为该类类型的对象。这通常被称为隐式转换构造函数。虽然这在某些情况下很方便,但它也可能导致意料之外的行为和错误。
explicit 关键字就是为了避免这种隐式转换,强制你进行显式转换。
override 关键字
override 是 C++11 及更高版本中的一个上下文关键字。它主要用来明确表示派生类中的成员函数旨在覆盖基类中的虚函数。
编译时检查: 如果没有 override,你不小心打错了虚函数的名称,或者它的签名(参数或 const 属性)与基类的虚函数不完全匹配,编译器是不会报错的。相反,你会在派生类中无意中创建了一个新函数,它只是隐藏了基类的函数。这可能导致难以调试的微妙的运行时错误,尤其是在多态性中。override 让编译器强制执行规则:“如果你说你要覆盖,那么你必须覆盖一个现有的、签名完全匹配的虚函数。” 如果你不匹配,你就会得到一个编译时错误,这比运行时出现意外要好得多。
提高可读性: 它清楚地向其他开发者(以及未来的你自己)表明,这个函数是多态接口的一部分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40class Base {
public:
virtual void doSomething() {
// 基类实现
}
virtual void anotherFunc(int x) const {
// 基类实现
}
virtual void onlyInBase() {
// 只有基类有的虚函数
}
};
class Derived : public Base {
public:
// 正确使用 override
void doSomething() override {
// 派生类实现
}
// 错误示例:签名不匹配,如果加上 override 会编译报错
// void anotherFunc(int x) { // 缺少 const
// // ...
// }
// 正确使用 override
void anotherFunc(int x) const override {
// 派生类实现
}
// 错误示例:基类中没有这个虚函数,如果加上 override 会编译报错
// void notAVirtualFunc() override {
// // ...
// }
// 正确示例:没有 override,因为 doSomethingElse 不是虚函数,或者不是覆盖基类的虚函数
void doSomethingElse() {
// ...
}
};
enum枚举
C enum
enum是C语言中的一个关键字,enum叫枚举数据类型,枚举类型可以让我们的程序使用一些固定长度和固定数值的变量值范围。枚举型是预处理指令#define的替代,枚举和宏其实非常类似,宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。
格式
1
2
3
4
5
6enum typename{
valuaname1,
valuename2,
valuename3,
...
}typeName是枚举类型的名字,花括号里面的元素(枚举成员)是常量而不是变量,因为枚举成员的是常量,所以不能对它们赋值,只能将它们的值赋给其他的变量。
- 注意
- 枚举型是一个集合,集合中的元素(枚举成员)是一些命名的整型常量,元素之间用逗号,隔开。
- 第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员上加1。在当前值没有赋值的情况下,枚举类型的当前值总是前一个值+1。
- 可以人为设定枚举成员的值,从而自定义某个范围内的整数。
- 类型定义以分号;结束。
- 枚举变量的定义
- 先定义枚举类型,再定义枚举变量
1
2
3
4
5enum DAY
{
...
};
enum DAY day1; - 定义枚举类型的同时定义枚举变量
1
2
3
4enum DAY
{
...
}day1; - 省略枚举名称,直接定义枚举变量
1
2
3
4enum
{
...
}day1; - 使用typedef
1
2
3
4
5typedef enum
{
...
}DAY;
DAY day1;
- 先定义枚举类型,再定义枚举变量
- 使用枚举类型的变量
- 枚举成员是常量,不能对它们赋值,只能将它们的值赋给其他的变量。他们只能在等号左边。
- 允许非枚举值赋值给枚举类型的变量(不是枚举成员), 允许其他枚举类型的值赋值给当前枚举类型。
- 不同枚举 类型的枚举值可以直接比较。
- 枚举值具有外层作用域,容易造成名字冲突,即不能定义与枚举成员同名的变量(无论什么类型)。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
enum Week { Mon, Tue, Wed, Thi, Fri, Sat, Sun };
enum Other { One, Two, Three };
enum Week week = Mon;
// 1. 允许非枚举值赋值给枚举类型, 允许其他枚举类型的值赋值给当前枚举类型
week = 100; //ok
week = One; //ok
// 2. 枚举值具有外层作用域,容易造成名字冲突
//int One = 100; //error //错误 C2365 “One” : 重定义;以前的定义是“枚举数”
// 3. 不同类型的枚举值可以直接比较
if (week == One)
{
printf("equal\n"); //equal
}
return 0;
}C++ enum
基本与C enum一致
但是在使用时有区别:
- C++ 只能允许赋值枚举值(枚举成员),且不允许其他枚举类型的值赋值给当前枚举类型
不同的两个枚举类型,若含有相同枚举元素,则会冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using namespace std;
int main()
{
enum Week { Mon, Tue, Wed, Thi, Fri, Sat, Sun };
enum Other { One, Two, Three };
enum Week week = Mon; //警告 C26812 枚举类型“main::__l2::Week”未设定范围。相比于 "enum",首选 "enum class" (Enum.3)。
// 1. C++ 只能允许赋值枚举值
// week = 100; //error
// week = One; //error
// 2. 枚举元素会暴露在外部作用域,不同的两个枚举类型,若含有相同枚举元素,则会冲突
//enum OtherWeek { Mon }; //error,重定义,以前的定义是枚举数
// 3. C++ 只允许同枚举类型值之间比较(作者说得貌似不对,能够比较!)
enum E1 { A, B };
enum E2 { C, D };
cout << (E1::B == E2::D ? "相等" : "不相等") << endl; //相等
return 0;
}enum class(强枚举类型)(C++11新特性)
强枚举类型禁止不同枚举类型之间进行比较。
强枚举类型不会将枚举元素暴露在外部作用域1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
int main()
{
enum class E1 { A = 1, B = 2 };
enum class E2 { A = 1, C = 2 };
// 1. 强枚举类型不会将枚举元素暴露在外部作用域
cout << (int)(E1::A) << endl; //1
int A = 100;
// 2. 不相关的两个枚举类型不能直接比较,编译报错
//cout << (E1::B == E2::C ? "相等" : "不相等") << endl; //error //E0349 没有与这些操作数匹配的 "==" 运算符
//cout << (E1::B == 2 ? "相等" : "不相等") << endl; //error E0349 没有与这些操作数匹配的 "==" 运算符
return 0;
}
struct
- 先定义结构体,再定义结构体变量
1
2
3
4
5struct 结构体名
{
...
};
struct 结构体名 变量名; - 定义结构体时同时定义结构体变量
1
2
3
4struct 结构体名
{
...
}变量名; - 第2种,如果后续不再使用结构体定义变量,也可以省略结构体名。
- 使用typedef(推荐)
1
2
3
4
5
6
7
8
9
10
11typedef struct
{
...
} 结构体别名;
结构体名 变量名;
// 或者
typedef struct 结构体名
{
...
} 结构体别名;
结构体别名 变量名; - 结构体变量初始化
1
2
3
4
5struct student
{
char name[50];
int id;
} charon = {"charon", 666};