C++ 实践笔记
随缘更 (~ ̄▽ ̄)~
1.auto关键字
1.1.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 关键字不能定义数组
1.2.auto(C++14新特性)
C++14中auto可以作为返回值了
2.Lambda表达式
2.1.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表达式2.2.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表达式。
2.3.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;
}
3.explicit 关键字
explicit 关键字用于修饰类的构造函数。它的作用是防止隐式的类型转换。
当一个类的构造函数只有一个参数时,如果没有 explicit 关键字修饰,C++ 编译器可能会允许将该参数类型的对象隐式地转换为该类类型的对象。这通常被称为隐式转换构造函数。虽然这在某些情况下很方便,但它也可能导致意料之外的行为和错误。
explicit 关键字就是为了避免这种隐式转换,强制你进行显式转换。
4.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() {
// ...
}
};
5.C++11 新特性
5.1.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,继续改进协程和标准库。
5.2.auto 关键字
auto
关键字用于自动类型推导,编译器会根据初始化表达式的类型来推导变量的类型。
5.3.decltype 关键字
decltype
可用于获取表达式的类型1
2int x = 42;
decltype(x) y = 10; // y 的类型为 int
5.4.右值引用
- 左值:指可以出现在赋值操作符左边的表达式,表示一个内存位置,可以取地址。
- 右值:指不能出现在赋值操作符左边的表达式,通常是临时对象或字面值,表示一个值而不是一个内存位置。
- 左值引用:左值引用是对左值的引用,可以通过
&
符号来定义。左值引用允许我们通过引用来访问和修改对象。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 |
|
5.7.constexpr
用于在编译时计算常量表达式。它可以用于变量、函数和构造函数,以提高程序的性能和安全性。1
2constexpr int square(int x) { return x * x; }
constexpr int result = square(5); // result 在编译时计算
5.8.范围for循环
用于简化容器和数组的遍历1
2
3
4std::vector<int> vec = {1, 2, 3};
for (auto x : vec) {
std::cout << x << " ";
}
5.9.默认与删除的函数
显式声明函数为默认或禁止:1
2
3
4
5class MyClass {
public:
MyClass() = default; // 使用默认构造函数
MyClass(const MyClass&) = delete; // 禁止拷贝构造函数
};
5.10.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;
}
6.enum枚举
6.1.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;
}6.2.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;
}6.3.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;
}
7.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};
8.动态内存
动态内存是指程序在运行时通过堆(heap)分配的内存,用于在程序执行过程中动态创建和管理数据。在 C++ 中,动态内存使用 new
和 delete
操作符进行分配和释放。
8.1.分配单个对象
1 | int* ptr = new int; // 分配一个整数 |
在 C++ 中,还可以使用动态内存分配为类的对象分配内存。通过 new 操作符可以创建对象,同时调用构造函数进行初始化。使用完对象后,需要用 delete 释放内存以调用析构函数并回收资源。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
using namespace std;
// 基类
class Animal {
public:
Animal() {
cout << "Animal's constructor called!" << endl;
}
virtual ~Animal() { // 虚析构函数
cout << "Animal's destructor called!" << endl;
}
};
// 派生类
class Dog : public Animal {
public:
Dog() {
cout << "Dog's constructor called!" << endl;
}
~Dog() { // 重载析构函数
cout << "Dog's destructor called!" << endl;
}
};
int main() {
Animal* animal = new Dog(); // 基类指针指向派生类对象
delete animal; // 删除时会调用派生类和基类的析构函数
return 0;
}
/*输出
Animal's constructor called!
Dog's constructor called!
Dog's destructor called!
Animal's destructor called!
*/
8.2.分配对象数组
如果需要分配一个类对象的数组,可以使用 new[]
,并在释放时使用 delete[]
。1
2
3
4
5
6
7
8int* arr = new int[5]; // 分配一个包含 5 个整数的数组
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
delete[] arr; // 释放数组内存
8.3.注意事项
内存泄漏
忘记调用 delete 或 delete[] 会导致内存泄漏,程序占用的内存无法被操作系统回收。悬空指针
如果在释放内存后继续使用指针,会导致未定义行为。
9.多态
多态 (Polymorphism) 是面向对象编程中的一个重要特性,它允许同一个操作或函数在不同的情况下有不同的表现方式。C++中的多态分为静态多态和动态多态。
9.1.静态多态
静态多态是在编译时决定调用哪个函数或操作,它主要通过函数重载和运算符重载实现。
9.1.1.函数重载
函数重载是指在同一作用域内,可以定义多个同名函数,但它们的参数列表(参数类型、参数个数、参数顺序)必须不同。编译器在编译时根据函数调用时的参数列表来确定调用哪个函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using namespace std;
class Math {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
};
int main() {
Math math;
cout << math.add(1, 2) << endl; // 调用int add(int, int)
cout << math.add(1.5, 2.5) << endl; // 调用double add(double, double)
return 0;
}
9.1.2.运算符重载
C++可以重定义或重载大部分 C++ 内置的运算符。这样,就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator
和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
可重载的运算符列表:
不可重载的运算符列表:
.
成员访问运算符.*
,->*
成员指针访问运算符::
域运算符sizeof
长度运算符? :
条件运算符#
预处理符号
运算符重载是通过在类中定义一个与运算符相对应的成员函数或友元函数实现的。
成员函数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
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
// 重载加法运算符
Complex operator+(const Complex& c) const {
return Complex(real + c.real, imag + c.imag);
}
void display() const {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
Complex c1(1.2, 3.4), c2(5.6, 7.8);
Complex c3 = c1 + c2; // 使用重载的加法运算符
c3.display(); // 输出 6.8 + 11.2i
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
using namespace std;
class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 声明友元函数
friend Complex operator+(const Complex& c1, const Complex& c2);
void display() const {
cout << real << " + " << imag << "i" << endl;
}
};
// 友元函数实现运算符重载
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
int main() {
Complex c1(1.2, 3.4), c2(5.6, 7.8);
Complex c3 = c1 + c2; // 调用友元函数 operator+
c3.display(); // 输出 6.8 + 11.2i
return 0;
}
9.1.3.函数模板
模板允许定义通用的函数或类,编译器在编译时会根据实际传递的类型生成对应的函数或类实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
using namespace std;
// 函数模板
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(10, 20) << endl; // 调用 int 类型的 add
cout << add(1.5, 2.5) << endl; // 调用 double 类型的 add
return 0;
}
9.2.动态多态
动态多态 (Dynamic Polymorphism) 是面向对象编程中的一种重要特性,它允许程序在运行时根据对象的实际类型调用相应的函数版本,而不是在编译时确定。这种行为通过虚函数 (virtual function) 和继承 (inheritance) 机制实现。
动态多态的核心
- 继承 (Inheritance):基类和派生类之间的关系是实现多态的基础。
- 虚函数 (Virtual Function):在基类中声明为virtual的成员函数允许派生类重写,并在运行时动态绑定到对象的实际类型。
- 基类指针或引用:通过基类的指针或引用调用虚函数,动态决定调用基类或派生类的实现。
1 |
|
如果基类中的虚函数没有具体实现,可以将其声明为纯虚函数,派生类必须重写该函数。
10.数据封装
所有的 C++ 程序都有以下两个基本要素:
- 程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
- 程序数据:数据是程序的信息,会受到程序函数的影响。
数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。
实例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
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
公有成员 addNum
和 getTotal
是对外的接口,用户需要知道它们以便使用类。私有成员 total
是对外隐藏的,用户不需要了解它,但它又是类能正常工作所必需的。数据封装通过类和访问修饰符(public
, private
, protected
)来实现.
通常情况下,我们都会设置类成员状态为私有(private
),除非我们真的需要将其暴露,这样才能保证良好的封装性。这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
11.数据抽象
数据抽象是指只向外界暴露对象的必要接口,隐藏其内部实现细节,强调“做什么”而非“怎么做”。抽象的目标是简化复杂系统,提供一种更高级别的理解方式。
数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
关键点:
- 关注接口而非实现:用户只需要知道如何使用对象,而不需要了解其内部运作。
- 减少复杂性:将实现细节隐藏起来,减少用户处理信息的负担。
- 依赖抽象层:通过定义抽象类或接口实现更灵活的代码。数据封装和数据抽象概念的区别
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
using namespace std;
// 抽象类:定义一个接口
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() {}
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing a Circle" << endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
cout << "Drawing a Rectangle" << endl;
}
};
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
shape1->draw(); // 只需要调用接口,不关心内部实现
shape2->draw();
delete shape1;
delete shape2;
return 0;
}
| 特性 | 数据封装 | 数据抽象 |
| ———— | ——————————————————————— | —————————————————— |
| 核心概念 | 绑定数据和操作,并隐藏数据的实现细节。 | 仅暴露接口,隐藏实现的复杂性。 |
| 目标 | 保护数据,避免被外部非法访问或修改。 | 简化接口,强调“做什么”而非“怎么做”。 |
| 实现手段 | 通过访问修饰符(private, protected, public)。 | 通过抽象类、接口和多态。 |
| 使用范围 | 主要在类的成员变量和成员函数中实现。 | 主要在类和对象的设计层次。 |
12.模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
12.1.函数模板
所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
模板函数定义的一般形式如下所示:1
2
3
4
5template <typename T >
返回类型 函数名 (形式参数表)
{
//函数体
}
T
是一个占位符类型,可以用任何合法的标识符替代。T
在函数体中表示实际类型,会在调用时被推导或显式指定。- 返回类型和参数列表可以使用模板类型
T
。 - 模板可以有多个参数,用逗号分隔。
template<typename T1,typename T2>
, 但是定义了就要用,不用会报错。
实例: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
using namespace std;
template <typename T>
T Max (T a, T b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max<double>(f1, f2) << endl; // 显示指定类型调用模板函数
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
12.2.类模板
C++ 类模板是一种用于创建通用类的机制,它可以让程序员编写一次类,然后让它适用于多种类型,在实际编程中非常实用。
声明的形式1
2
3
4template <typename T>
class 类名 {
//类的定义
}
T
是占位符类型名称,可以在类被实例化的时候进行指定。- 可以使用一个逗号分隔的列表来定义多个泛型数据类型。
T
可以用于定义类的成员变量、成员函数的参数和返回值等。
实例: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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
13.命名空间
命名空间是一个封装机制,用于将标识符(如变量、函数、类等)组织在一起。这样同名的标识符可以出现在不同的命名空间中,避免名称冲突。本质上,命名空间就是定义了一个范围。
13.1.定义命名空间
命名空间的定义使用关键字 namespace
,后跟命名空间的名称,如下所示:1
2
3namespace namespace_name {
// 代码声明
}
13.2.访问命名空间
命名空间中的代码可以通过以下两种方式访问:
13.2.1.使用作用域解析运算符::
如下所示:1
name::code; // code 可以是变量或函数
实例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
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();
return 0;
}
13.2.2.using指令
如果不想每次都使用::
来访问命名空间的成员,可以使用using
声明来引入整个命名空间或命名空间中的特定成员:
使用using指令引入整个命名空间1
using namespace namespace_name;
实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
namespace MyNamespace {
int x = 10;
void print() {
cout << "Inside MyNamespace" << endl;
}
}
int main() {
using namespace MyNamespace;
cout << x << endl; // 输出 10
print(); // 输出 Inside MyNamespace
return 0;
}
使用using指令引入命名空间中的特定成员1
using namespace_name::code;
实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
namespace MyNamespace {
int x = 10;
void print() {
cout << "Inside MyNamespace" << endl;
}
}
int main() {
using MyNamespace::x;
cout << x << endl; // 输出 10
// print(); // 编译错误,因为 print() 没有被引入
return 0;
}
13.3.嵌套命名空间
命名空间可以嵌套,允许将多个命名空间放在一个命名空间内。1
2
3
4
5
6namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}
实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using namespace std;
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
first_space::second_space::func();
return 0;
}
13.4.标准库命名空间 std
C++的标准库中,所有的标准函数、类和对象都包含在std
命名空间中。例如,cout
、cin
、vector
等都属于std
命名空间。
14.多线程
多线程的两个主要用途(1)保护共享数(2)同步并行操作
14.1.概念
进程:进程之间相互独立,每个进程拥有独立的地址空间、代码、数据和系统资源。
线程:是进程中的一个执行路径,同一个进程可以有多个线程。
并发:多个任务在同一时间段内交替执行。
并行:多个任务在同一时刻,在多个处理器/核心上同时执行。
多线程并发:多个线程在同一时间段交替执行,一个时间片运行一个线程的代码,并不是真正意义的并行计算。硬件要求单核多核都可。
多线程并行:多个线程在真正的同一时刻同时运行,可以做到真正的并行计算。硬件要求必须是多核。
线程状态:(1)新建,创建线程对象后,尚未启动执行。
(2)就绪(Ready) 准备好运行,等待被调度器分配 CPU
(3)运行中(Running) 正在使用 CPU 执行任务
(4)阻塞(Blocked) 等待某资源(如锁、I/O、信号量)不能继续执行
(5)等待/睡眠(Waiting) 主动等待某事件(如 sleep()、condition_variable::wait())
(6)终止(Terminated) 执行完毕或被强制退出,生命周期结束
14.2.std::thread
在 C++ 中,main() 函数运行的线程就是主线程(main thread),这是程序启动时操作系统自动创建的第一个线程。
main() 中运行的线程就是“主线程”。
调用 std::thread 创建的新线程是“子线程”。
主线程和子线程是并发执行的,你必须用 .join() 或 .detach() 来管理子线程。
程序不会在 main() 返回(或 return 0)之后自动等待子线程,未处理的线程会导致异常。
- 引入
#include <thread>
- 创建和启用线程
每个 std::thread 对象表示一个线程,当一个 std::thread 对象被创建后,一个新线程会被启动,用来执行在 std::thread 构造函数中传入的函数。
1 |
|
值传递
1
2
3
4
5
6void print(int a, std::string b) {
std::cout << "a=" << a << ", b=" << b << "\n";
}
std::thread t(print, 5, "thread"); // 传值
t.join();线程操作函数
- join()主线程等待子线程执行完毕
- detach() 让线程在后台运行,主线程不会等待它
- joinable() 判断线程是否可以 join(未 join/detach)
- get_id() 获取线程 ID
- hardware_concurrency() 获取系统的并发线程数量(CPU核心数)
- sleep_for(std::chrono::seconds(1)) 休眠1秒
14.3.锁
14.3.1.std::mutex(互斥量)
互斥量是一种同步原语,用于防止多个线程同时访问共享资源,确保同一时刻只有一个线程访问该资源。当一个线程需要访问共享资源时,它首先需要锁定(lock)互斥量。如果互斥量已经被其他线程锁定,那么请求锁定的线程将被阻塞,直到互斥量被解锁(unlock)。1
2
3
4
5
6
7
8
9
10
std::mutex mtx;
void safe_increase() {
mtx.lock(); // 加锁
// 临界区(critical section)
++counter;
mtx.unlock(); // 解锁
}
14.3.2.std::lock_guard
作用域锁,当构造时自动锁定互斥量,当析构时自动解锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int counter = 0;
std::mutex mtx;
void increase() {
for (int i = 0; i < 10000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::thread t1(increase);
std::thread t2(increase);
t1.join();
t2.join();
std::cout << "Counter: " << counter << "\n"; // 应该是 20000
return 0;
}
std::lock_guard
→ 进入作用域,构造函数自动调用 mtx.lock()
函数执行完,lock 离开作用域(printSomething() 结束)
→ 析构函数自动调用 mtx.unlock(),释放锁
14.3.3.std::unique_lock
与std::lock_guard类似,但提供了更多的灵活性,例如可以转移所有权和手动解锁。
14.4.std::condition_variable(条件变量)
允许线程在某个条件为真之前一直处于等待状态,当其他线程修改了条件并且通知了条件变量后,等待的线程可以继续执行。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
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "Worker waiting...\n";
cv.wait(lock, [] { return ready; }); // 解锁等待,收到通知后再加锁
std::cout << "Worker starts work!\n";
}
void notifier() {
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
std::cout << "Notifier set ready = true\n";
}
cv.notify_one(); // 唤醒一个等待线程
}
int main() {
std::thread t1(worker);
std::thread t2(notifier);
t1.join();
t2.join();
return 0;
}