CS106L 笔记

news/2025/2/22 20:33:41/文章来源:https://www.cnblogs.com/PHarr/p/18731340

Design philosophy of C++

  • Allow the programmer full control, responsibility, and choice if they want it.
    • 解释: C++ 强调程序员对代码的完全控制权,允许程序员在需要时直接管理内存、硬件资源等底层细节。
    • 体现:
      • 手动内存管理: 通过 newdelete 直接控制内存分配和释放。
      • 指针和引用: 提供指针和引用,允许直接操作内存地址。
      • 底层操作: 支持位操作、内联汇编等底层特性。
      • 零开销抽象: C++ 的高级特性(如模板、RAII)在运行时几乎没有额外开销。
  • Express ideas and intent directly in code.
    • 解释: C++ 鼓励代码清晰地表达设计意图,使代码易于理解和维护。
    • 体现:
      • 强类型系统: 通过类型系统明确表达数据的含义。
      • 面向对象编程: 使用类、继承和多态来表达抽象和层次结构。
      • 模板和泛型编程: 通过模板实现通用的、类型安全的代码。
      • RAII: 通过构造函数和析构函数明确资源管理的生命周期。
  • Enforce safety at compile time whenever possible.
    • 解释: C++ 强调在编译时尽可能捕获错误,而不是在运行时。
    • 体现:
      • 类型检查: 编译器在编译时检查类型错误。
      • 模板实例化: 模板在编译时实例化,确保类型安全。
      • constexpr: 在编译时计算常量表达式,避免运行时错误。
      • static_assert: 在编译时断言条件是否满足。
  • Do not waste time or space.
    • 解释: C++ 追求高效,避免不必要的运行时开销和内存浪费。
    • 体现:
      • 零开销抽象: 高级特性(如模板、RAII)在运行时几乎没有额外开销。
      • 内联函数: 通过 inline 关键字减少函数调用开销。
      • 移动语义: 通过移动构造函数和移动赋值运算符避免不必要的拷贝。
      • 内存对齐: 支持手动控制内存对齐,优化性能。
  • Compartmentalize messy constructs.
    • 解释: C++ 鼓励将复杂或容易出错的代码隔离到特定的模块或类中,以提高代码的可维护性和安全性。
    • 体现:
      • 命名空间: 使用命名空间隔离代码,避免命名冲突。
      • 类封装: 将数据和操作封装在类中,隐藏实现细节。
      • RAII: 将资源管理封装在类的构造函数和析构函数中。
      • 模板特化: 将特定类型的特殊处理隔离到模板特化中。

总结

  1. 控制与选择: 程序员可以完全控制代码的行为,同时享受高级抽象的便利。
  2. 表达意图: 代码清晰地表达设计意图,易于理解和维护。
  3. 编译时安全: 尽可能在编译时捕获错误,减少运行时问题。
  4. 高效性: 避免不必要的开销,追求时间和空间的最优利用。
  5. 隔离复杂性: 将复杂或容易出错的代码隔离到特定模块中,提高代码的可维护性。

Streams

流可以认为是一个缓冲区,我们可以像先缓冲区中写入数据<<,也可以从缓冲区中读取数据>>

String Streams

# include <bits/stdc++.h>using namespace std;int main(){ostringstream oss("Ito-En Green Tes");cout << oss.str() << endl; // 对于一个流,我们可以用 str() 获得缓存区中的内容oss << 16.9 << " Ounces"; // 流中所有的所有数据都是以字符串形式存储,并且会在写入是把元素强制类型转换为字符串cout << oss.str() << endl;
}

上述代码中,我们发现两次输出的结果是一样的。这是因为流默认在每次写入后会移动指针,而默认的指针一开始是在开头。

# include <bits/stdc++.h>using namespace std;int main(){
//    ostringstream oss("Ito-En Green Tes", ostringstream::bin);ostringstream oss("Ito-En Green Tes", ostringstream::ate);cout << oss.str() << endl;oss << 16.9 << " Ounces";cout << oss.str() << endl;
}

这样写构造函数,流的初始指针就会是末尾。

下面我们考虑输入字符串流

# include <bits/stdc++.h>using namespace std;int main(){istringstream iss("16.9 Ounces");double amount;string unit;iss >> amount >> unit;cout << amount / 2 << " " << unit << endl;
}

刚才我们说到了,流内全部是用字符串存储的,但是在读入的时候,流会跟读入变量的类型来进行自动的截断,并进行强制类型转换。

# include <bits/stdc++.h>using namespace std;int main(){istringstream iss("16.9 Ounces");int amount;string unit;iss >> amount >> unit;cout << amount << " " << unit << endl;
}

同样的,对于这个代码,我们在读入amount时,读入16后就没有下一个是.,但是.是不会出现在 int 中的,因此就会截断,并且unit会从.开始继续向后读入。

int stringToInter(const string &s) {istringstream iss(s);int result;iss >> result;return result;
}

通过字符串流实现了一个字符串转换为整形的函数。

State Bits (状态位)

state bits 用来表示流的状态。主要包含以下几种:

  1. good bit,表示流处于正常状态。
  2. eof bit,表示流已经达到文件末尾。
  3. fail bit,表示流操作失败,通常是由格式错误或类型不匹配引起。
  4. bad bit,表示流处于严重的错误,通常是不可恢复的,如设备故障。

如果出现类型不匹配的情况,fail bit会被激活,同时指针会回退到操作之前。

我们可以用以下代码输出一个流的状态位。

void printStateBits(const istream & iss) {cout << "State bits: ";cout << (iss.good() ? "G" : "-");cout << (iss.fail() ? "F" : "-");cout << (iss.eof() ? "E" : "-");cout << (iss.bad() ? "B" : "-");cout << "\n";
}

Stream Buffer

在 C++ 中,流缓冲区(Stream Buffer)是输入输出(I/O)系统的重要组成部分,用于管理数据的缓存和传输。流缓冲区是 std::streambuf 类的实例,它充当流(如 std::istreamstd::ostream)与底层数据源(如文件、内存或设备)之间的桥梁。

流缓冲区的作用:

  1. 数据缓存

    • 流缓冲区用于临时存储数据,以减少频繁访问底层数据源(如文件或设备)的开销。
    • 例如,写入文件时,数据会先存储在缓冲区中,当缓冲区满或显式刷新时,数据才会写入文件。
  2. 提高性能

    • 通过减少直接 I/O 操作的次数,流缓冲区可以显著提高程序的性能。
  3. 数据格式化

    • 流缓冲区与流对象(如 std::cinstd::cout)配合,支持数据的格式化输入输出。

流缓冲区的类型:

  1. 输入缓冲区

    • 用于从数据源(如文件或键盘)读取数据。
    • 例如,std::cin 使用输入缓冲区来存储从键盘输入的数据。
  2. 输出缓冲区

    • 用于向数据目标(如文件或屏幕)写入数据。
    • 例如,std::cout 使用输出缓冲区来存储要显示的数据。
  3. 双向缓冲区

    • 同时支持输入和输出操作。

流缓冲区的关键操作:

  1. 刷新缓冲区(Flush)

    • 将缓冲区中的数据强制写入底层数据源。
    • 例如,std::flushstd::endl 可以刷新输出缓冲区。
  2. 同步缓冲区(Sync)

    • 将输入缓冲区中的数据与底层数据源同步。
    • 例如,std::cin.sync() 可以清空输入缓冲区。
  3. 设置缓冲区

    • 可以使用 std::streambuf::pubsetbuf() 自定义缓冲区的大小和位置。
  4. 获取和设置缓冲区指针

    • 使用 std::streambuf::pbase()std::streambuf::pptr()std::streambuf::epptr() 可以访问输出缓冲区的指针。
    • 使用 std::streambuf::eback()std::streambuf::gptr()std::streambuf::egptr() 可以访问输入缓冲区的指针。

Initialization(初始化)

C++ 中初始化的方法有很多,课程中说有不少于26种。

Uniform Initialization

C++11 引入了统一初始化(Uniform Initialization),也称为列表初始化(List Initialization),旨在提供一种更一致、更简洁的初始化语法。它可以用于初始化各种类型的对象,包括基本类型、数组、结构体、类对象等。

特点

  1. 一致性:无论是基本类型、数组、结构体还是类对象,都可以使用 {} 进行初始化。
  2. 防止窄化转换:使用 {} 初始化时,编译器会检查是否存在窄化转换(如将 double 转换为 int),如果存在则会报错。
  3. 避免歧义:可以避免 C++ 中最令人头疼的解析问题(Most Vexing Parse),即编译器将某些语法解析为函数声明而非对象初始化。

窄化转换(Narrowing Conversion)是指在 C++ 中将一个值从一种类型转换为另一种类型时,如果目标类型的范围无法完全容纳源类型的值,导致数据丢失或精度降低的情况。窄化转换可能会导致意外的行为或错误,因此在某些情况下需要避免。

常见例子

  1. 从浮点数到整数
  2. 从大范围整数到小范围整数
  3. 从无符号整数到有符号整数
  4. 从高精度浮点数到低精度浮点数

窄化转换可能会导致以下问题:

  1. 数据丢失:例如,将浮点数转换为整数时,小数部分会被截断。
  2. 未定义行为:如果目标类型无法容纳源类型的值,结果可能是未定义的。
  3. 隐藏的错误:窄化转换可能不会导致编译错误,但会在运行时引发问题。

在 C++11 中,统一初始化(使用 {})会禁止窄化转换。如果检测到窄化转换,编译器会报错。

int x{3.14};  // 错误:从 double 到 int 的窄化转换
int y = 3.14; // 合法:传统初始化允许窄化转换

Sequence Containers

C++中有五种Sequence Containers,分别是std::vector<T>, std::deque<T>, std::list<T>, std::array<T>, std::forward_list<T>

std::vector<T>

int main(){std::vector<int> a;a.push_back(0);a.push_back(1);std::cout << a.at(1) << "\n";
//    std::cout << a.at(2) << "\n"; // 会检查越界std::cout << a[2] << "\n";return 0;
}

这里如果atoperator[]都可以用来访问元素,但是at会检查是否越界,并且如果越界会抛出异常,但是operator[]并不会。

为什么 operator[] 不检查越界?

  • 性能考虑operator[] 的设计目标是高效,避免额外的检查开销。
  • 灵活性:C++ 强调“零开销抽象”,允许程序员在需要时手动管理边界检查。
  • 历史原因operator[] 的行为与 C 风格数组一致,保持了语言的连贯性。

Container Adaptors

在 C++ 标准库中,容器适配器(Container Adaptors) 是一种特殊的容器,它们基于现有的标准容器(如 std::vectorstd::dequestd::list)实现,但提供了不同的接口和行为。容器适配器并不是独立的容器,而是对现有容器的封装,用于实现特定的数据结构(如栈、队列和优先队列)。

C++ 提供了三种主要的容器适配器:


1. std::stack

  • 功能: 实现栈(后进先出,LIFO)的数据结构。

  • 默认底层容器: std::deque

  • 支持的操作:

    • push(): 将元素压入栈顶。
    • pop(): 弹出栈顶元素。
    • top(): 访问栈顶元素。
    • empty(): 检查栈是否为空。
    • size(): 返回栈中元素的数量。
  • 示例:

    #include <iostream>
    #include <stack>int main() {std::stack<int> s;s.push(1);s.push(2);s.push(3);while (!s.empty()) {std::cout << s.top() << " ";  // 输出: 3 2 1s.pop();}
    }
    

2. std::queue

  • 功能: 实现队列(先进先出,FIFO)的数据结构。

  • 默认底层容器: std::deque

  • 支持的操作:

    • push(): 将元素添加到队尾。
    • pop(): 移除队首元素。
    • front(): 访问队首元素。
    • back(): 访问队尾元素。
    • empty(): 检查队列是否为空。
    • size(): 返回队列中元素的数量。
  • 示例:

    #include <iostream>
    #include <queue>int main() {std::queue<int> q;q.push(1);q.push(2);q.push(3);while (!q.empty()) {std::cout << q.front() << " ";  // 输出: 1 2 3q.pop();}
    }
    

3. std::priority_queue

  • 功能: 实现优先队列(元素按优先级排序,默认最大堆)。

  • 默认底层容器: std::vector

  • 支持的操作:

    • push(): 将元素添加到优先队列。
    • pop(): 移除优先级最高的元素。
    • top(): 访问优先级最高的元素。
    • empty(): 检查优先队列是否为空。
    • size(): 返回优先队列中元素的数量。
  • 自定义优先级:
    可以通过提供自定义的比较函数来改变优先级规则。

  • 示例:

    #include <iostream>
    #include <queue>int main() {std::priority_queue<int> pq;pq.push(3);pq.push(1);pq.push(4);while (!pq.empty()) {std::cout << pq.top() << " ";  // 输出: 4 3 1pq.pop();}
    }
    

    如果需要最小堆,可以这样定义:

    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;
    

容器适配器的特点

  1. 基于现有容器:

    • 容器适配器并不是独立的容器,而是基于现有的标准容器(如 std::dequestd::vector 等)实现的。
    • 例如,std::stack 默认使用 std::deque 作为底层容器,但也可以指定其他容器(如 std::vectorstd::list)。
  2. 简化接口:

    • 容器适配器提供了特定数据结构的接口(如栈的 pushpop),隐藏了底层容器的复杂性。
  3. 灵活性:

    • 可以通过模板参数指定底层容器类型。例如:

      std::stack<int, std::vector<int>> s;  // 使用 std::vector 作为底层容器
      

容器适配器的底层容器

  • std::stack:
    • 默认使用 std::deque,但也可以使用 std::vectorstd::list
  • std::queue:
    • 默认使用 std::deque,但也可以使用 std::list
  • std::priority_queue:
    • 默认使用 std::vector,但也可以使用 std::deque

总结

容器适配器是对现有标准容器的封装,提供了特定数据结构(如栈、队列和优先队列)的接口。它们的优点是:

  • 简化了特定数据结构的实现。
  • 基于现有的高效容器,性能有保障。
  • 灵活支持自定义底层容器。

如果需要实现栈、队列或优先队列,优先考虑使用容器适配器,而不是手动实现。

Associative Containers

C++ 标准库提供了几种关联容器(Associative Containers),这些容器基于键(key)来存储和访问元素。关联容器的主要特点是它们能够高效地根据键来查找、插入和删除元素。以下是 C++ 中常见的关联容器:

1. std::set

  • 特点: std::set 是一个有序的集合,其中的元素是唯一的(即不允许重复)。

  • 底层实现: 通常基于红黑树(一种平衡二叉搜索树)。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(log n)。

  • 示例:

    #include <iostream>
    #include <set>int main() {std::set<int> s;s.insert(3);s.insert(1);s.insert(4);for (int x : s) {std::cout << x << " ";  // 输出: 1 3 4}
    }
    

2. std::multiset

  • 特点: std::multisetstd::set 类似,但它允许存储重复的元素。

  • 底层实现: 同样基于红黑树。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(log n)。

  • 示例:

    #include <iostream>
    #include <set>int main() {std::multiset<int> ms;ms.insert(3);ms.insert(1);ms.insert(4);ms.insert(1);for (int x : ms) {std::cout << x << " ";  // 输出: 1 1 3 4}
    }
    

3. std::map

  • 特点: std::map 是一个有序的键值对容器,键是唯一的。

  • 底层实现: 通常基于红黑树。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(log n)。

  • 示例:

    #include <iostream>
    #include <map>int main() {std::map<std::string, int> m;m["apple"] = 5;m["banana"] = 3;m["orange"] = 8;for (const auto& pair : m) {std::cout << pair.first << ": " << pair.second << std::endl;}// 输出:// apple: 5// banana: 3// orange: 8
    }
    

4. std::multimap

  • 特点: std::multimapstd::map 类似,但它允许键重复。

  • 底层实现: 同样基于红黑树。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(log n)。

  • 示例:

    #include <iostream>
    #include <map>int main() {std::multimap<std::string, int> mm;mm.insert({"apple", 5});mm.insert({"banana", 3});mm.insert({"apple", 2});for (const auto& pair : mm) {std::cout << pair.first << ": " << pair.second << std::endl;}// 输出:// apple: 5// apple: 2// banana: 3
    }
    

5. std::unordered_set (C++11)

  • 特点: std::unordered_set 是一个无序的集合,其中的元素是唯一的。

  • 底层实现: 基于哈希表。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(1),最坏情况下为 O(n)。

  • 示例:

    #include <iostream>
    #include <unordered_set>int main() {std::unordered_set<int> us;us.insert(3);us.insert(1);us.insert(4);for (int x : us) {std::cout << x << " ";  // 输出顺序不确定}
    }
    

6. std::unordered_multiset (C++11)

  • 特点: std::unordered_multisetstd::unordered_set 类似,但它允许存储重复的元素。

  • 底层实现: 基于哈希表。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(1),最坏情况下为 O(n)。

  • 示例:

    #include <iostream>
    #include <unordered_set>int main() {std::unordered_multiset<int> ums;ums.insert(3);ums.insert(1);ums.insert(4);ums.insert(1);for (int x : ums) {std::cout << x << " ";  // 输出顺序不确定}
    }
    

7. std::unordered_map (C++11)

  • 特点: std::unordered_map 是一个无序的键值对容器,键是唯一的。

  • 底层实现: 基于哈希表。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(1),最坏情况下为 O(n)。

  • 示例:

    #include <iostream>
    #include <unordered_map>int main() {std::unordered_map<std::string, int> um;um["apple"] = 5;um["banana"] = 3;um["orange"] = 8;for (const auto& pair : um) {std::cout << pair.first << ": " << pair.second << std::endl;}// 输出顺序不确定
    }
    

8. std::unordered_multimap (C++11)

  • 特点: std::unordered_multimapstd::unordered_map 类似,但它允许键重复。

  • 底层实现: 基于哈希表。

  • 操作复杂度: 插入、删除和查找操作的平均时间复杂度为 O(1),最坏情况下为 O(n)。

  • 示例:

    #include <iostream>
    #include <unordered_map>int main() {std::unordered_multimap<std::string, int> umm;umm.insert({"apple", 5});umm.insert({"banana", 3});umm.insert({"apple", 2});for (const auto& pair : umm) {std::cout << pair.first << ": " << pair.second << std::endl;}// 输出顺序不确定
    }
    

总结

  • 有序关联容器 (std::set, std::multiset, std::map, std::multimap) 基于红黑树实现,元素按键排序,操作复杂度为 O(log n)。
  • 无序关联容器 (std::unordered_set, std::unordered_multiset, std::unordered_map, std::unordered_multimap) 基于哈希表实现,元素无序,操作复杂度为 O(1) 平均情况下。

选择哪种关联容器取决于具体的应用场景和性能需求。如果需要有序性,可以选择有序关联容器;如果需要更高的性能并且不关心顺序,可以选择无序关联容器。

Iterators

基本操作

    std::vector<int> vec;std::vector<int>::iterator it = vec.begin(); // 创建迭代器std::cout << *it << std::endl; // 通过解引用获得迭代器原本的值++ it; // 移动迭代器if(it == vec.end()) return; // 比较迭代器

Iterators Type

  1. Input
  2. Output
  3. Forward
  4. Bidirectional
  5. Random access

对于每种 Container 它的 Iterator 的类型是固定的。

对于所有的迭代器都可以

  1. 从已经存在的迭代器创建
  2. ++向后移动一位
  3. 使用==!=进行比较

对于 Input 迭代器,它是只能读取值,也就是说他只能在表达式的右侧,并且它只能单次遍历。

对于 Output 迭代器,它是只能写入值,也就是说他只能出现在的表达式的左侧,并且它只能单次遍历。

对于 Forward 迭代器,它可以实现 Input 迭代器和 Output 迭代器的所有功能,并且可以实现多次遍历。

多次遍历指的是能够对同一个序列(如容器或数据集合)进行多次从头到尾的访问。具体来说,如果一个迭代器支持多次遍历,意味着你可以通过它多次访问序列中的元素,而不需要重新创建或重置迭代器。

对于 Bidirectional 迭代器可以实现 Forward 迭代器的所有功能,并且有--操作

对于 Random access 迭代器可以 Bidirectional 的所有功能,并且实现任何值的加减,比如iter = iter + 3这样。

对于迭代器,如果用继承来看就是

  • Random access
    • Bidirectional
      • Forward
        • Input
        • Output

并且,对于任何一种迭代器,我们都可以直接使用任何一种比他更高级的迭代器。

如何理解指针和迭代器的区别?

可以把指针理解为一种 class,而迭代器理解为一种 interface。这样指针就是 Random access Iterator 的一种实现。

Templates

Template Function

我们要实现一个函数minmax传入两个值,并返回最小值和最大值。

这个还是很好实现的

#include <iostream>using std::cout;std::pair<int, int> minmax(int a, int b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";return 0;
}

然后我们考虑其他类型是否可以执行?

#include <iostream>using std::cout;std::pair<int, int> minmax(int a, int b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";auto [min2, max2] = minmax(2.5, 3.1);cout << min2 << " " << max2 << "\n";auto [min3, max3] = minmax('a', 'l');cout << min3 << " " << max3 << "\n";return 0;
}

这依旧是可以执行的,因为浮点型和字符型都可以隐式类型转换位整型,但是结果是不对。

这样之所以可以编译,还是因为 "Allow the programmer full control, responsibility, and choice if they want it." 所以编译器认为程序员知道自己在做什么。

但是如果继续新增

#include <iostream>using std::cout;std::pair<int, int> minmax(int a, int b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";auto [min2, max2] = minmax(2.5, 3.1);cout << min2 << " " << max2 << "\n";auto [min3, max3] = minmax('a', 'l');cout << min3 << " " << max3 << "\n";auto [min4, max4] = minmax("Alice", "Bob");cout << min4 << " " << max4 << "\n";return 0;
}

此时就无法进行编译了,因为字符串不能隐式类型转换为整型。为了解决这个问题我们可以用到 C 语言中的函数重载解决。

#include <iostream>using std::cout;std::pair<int, int> minmax(int a, int b) {if (a < b) return {a, b};return {b, a};
}std::pair<double, double> minmax(double a, double b) {if (a < b) return {a, b};return {b, a};
}std::pair<char, char> minmax(char a, char b) {if (a < b) return {a, b};return {b, a};
}std::pair<std::string, std::string> minmax(std::string a, std::string b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";auto [min2, max2] = minmax(2.5, 3.1);cout << min2 << " " << max2 << "\n";auto [min3, max3] = minmax('a', 'l');cout << min3 << " " << max3 << "\n";auto [min4, max4] = minmax("Alice", "Bob");cout << min4 << " " << max4 << "\n";return 0;
}

这样做既消除了编译的警告也可以正确的返回结果,但是我们注意到其实四个函数是非常冗余的。这里我们就可以引出模板类型这一概念。

#include <iostream>using std::cout;template<typename T>
std::pair<T, T> minmax(T a, T b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";auto [min2, max2] = minmax(2.5, 3.1);cout << min2 << " " << max2 << "\n";auto [min3, max3] = minmax('a', 'l');cout << min3 << " " << max3 << "\n";auto [min4, max4] = minmax("Alice", "Bob");cout << min4 << " " << max4 << "\n";return 0;
}

这样做,也可以消除警告,并且大多数时候也可以正常的运行,唯独第四个的计算可能出现问题,我这里的结果是

-3 2
2.5 3.1
a l
Bob Alice

这是为什么?因为模板在识别"Alice"时,把他识别为const char *类型了。此次就会比较两个字符串常量的地址,而不是字典序?

对于这个问题我们可以在调用类型时指明参数的类型来实现。

#include <iostream>
#include <array>using std::cout;template<typename T>
std::pair<T, T> minmax(T a, T b) {if (a < b) return {a, b};return {b, a};
}int main() {auto [min1, max1] = minmax(2, -3);cout << min1 << " " << max1 << "\n";auto [min2, max2] = minmax(2.5, 3.1);cout << min2 << " " << max2 << "\n";auto [min3, max3] = minmax('a', 'l');cout << min3 << " " << max3 << "\n";auto [min4, max4] = minmax<std::string>("Alice", "Bob");cout << min4 << " " << max4 << "\n";auto [min5, max5] = minmax<std::array<int, 2>>({2, 4}, {1, 3});cout << min5[0] << " " << min5[1] << " , " << max5[0] << " " << max5[1] << "\n";return 0;
}

可以注意到,当你指明类型后,编译器就知道了如何进行类型转换,因此你甚至可以传入一些比较奇怪的东西。

关于模板函数,我们在再看下一个例子,这个例子是统计一个集合中某种元素出现的次数。

#include <bits/stdc++.h>using std::cout;template<typename T>
int count(std::vector<T> set, T target) {int cnt = 0;for (size_t i = 0; i < set.size(); i += 1)if (set[i] == target) cnt++;return cnt;
}int main() {cout << count({1, 1, 4, 5, 1, 4}, 1);return 0;
}

对于这个函数,我们默认了arr必须是一个std::vector才行,我们可以做一个优化。

template<typename containerType, typename dataType>
int count(const containerType &set, dataType target) {int cnt = 0;for (size_t i = 0; i < arr.size(); i += 1)if (set[i] == target) cnt++;return cnt;
}

这样的话,理论上我们可以接受任何一种容器,但是并不是所有的容器都可以用[]进行下标访问。因此我们可以用迭代器实现。

template<typename ContainerType, typename DataType>
int count(const ContainerType &set, DataType target) {int cnt = 0;for(auto iter = set.begin; iter != set.end; iter += 1)if(*iter == target) cnt ++;return cnt;
}

但是如果我们不需要整个集合的情况,我们改怎么办呢?可以不传入容器,而是传入两个类型的迭代器。

template<typename InputIterator, typename DataType>
int count(const InputIterator &begin, const InputIterator end , DataType target) {int cnt = 0;for(auto iter = begin; iter != end; iter += 1)if(*iter == target) cnt ++;return cnt;
}

但是这个又对迭代器提出了要求,必须是Random access类型才行。因此我们可以在做使用++

#include <iostream>
#include <set>
#include <vector>
#include <list>using std::cout;template<typename InputIterator, typename DataType>
int count(const InputIterator &begin, const InputIterator end, DataType target) {int cnt = 0;for (auto iter = begin; iter != end; iter++)if (*iter == target) cnt++;return cnt;
}int main() {std::set<int> a{1, 1, 4, 5, 1, 4};std::vector<int> b{1, 1, 4, 5, 1, 4};std::list<int> c{1, 1, 4, 5, 1, 4};cout << count(a.begin(), a.end(), 1) << "\n";cout << count(b.begin(), b.end(), 1) << "\n";cout << count(c.begin(), c.end(), 1) << "\n";return 0;
}

这也是采用模板的一个重要作用就是可以实现泛型编程,对于最终的版本我们对于模板参数要求已经降低到了只需要保证DataType可以用==进行比较即可。

我们再来观察这个函数,这个函数目前实现的功能是查询集合中等于target的元素个数,我能否实现一些其他的功能?当然可以,模板参数不仅可以传入变量,也可以把函数传入进去。

#include <iostream>
#include <set>
#include <vector>
#include <list>using std::cout;template<typename InputIterator, typename Predicate>
int count(const InputIterator &begin, const InputIterator end, Predicate pred) {int cnt = 0;for (auto iter = begin; iter != end; iter++)if (pred(*iter)) cnt++;return cnt;
}bool lessThan3(int val) {return val < 3;
}bool lessThan4(int val) {return val < 4;
}bool lessThan5(int val) {return val < 5;
}int main() {std::vector<int> a{1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 1, 0};cout << count(a.begin(), a.end(), lessThan3) << "\n";cout << count(a.begin(), a.end(), lessThan4) << "\n";cout << count(a.begin(), a.end(), lessThan5) << "\n";return 0;
}

这样就可以在不修改count函数的情况下修改count的功能,但是我们看lessThan3,lessThan4,lessThan5三个函数其实是大同小异的,有没有什么方法增加其通用性?当然我们可以把lessThan函数设计为两个参数,但是这样的话对于count函数就失去了通用性。

对于这个问题我们当然可以这样实现

int limit;
bool lessThanLimit(int val) {return val < limit;
}

limit作为一个全局变量,然后在每次调用count前修改limit的值即可。但是这样又会牵扯到一个问题,limit只在lessThanlimit中使用,作为全局变量不合适。

对于这个问题,我们可以用Lambda解决。

int main() {std::vector<int> a{1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 1, 0};int limit = 0;auto lessThanLimit = [&limit](int val) {return val < limit;};limit = 3;cout << count(a.begin(), a.end(), lessThanLimit) << "\n";limit = 4;cout << count(a.begin(), a.end(), lessThanLimit) << "\n";limit = 5;cout << count(a.begin(), a.end(), lessThanLimit) << "\n";return 0;
}

当然了,其实这个问题我们可以用非类型模板参数解决。

template<int limit>
bool lessThanLimit(int val) {return val < limit;
}int main() {std::vector<int> a{1, 1, 4, 5, 1, 4, 1, 9, 1, 9, 8, 1, 0};cout << count(a.begin(), a.end(), lessThanLimit<3>) << "\n";cout << count(a.begin(), a.end(), lessThanLimit<4>) << "\n";cout << count(a.begin(), a.end(), lessThanLimit<5>) << "\n";return 0;
}

要注意的是,非类型模板参数只能接受常量,也就是必须在编译时就已知值才行。

OOP

Const函数

  • 不会修改对象状态const 成员函数承诺不会修改对象的非静态成员变量。如果函数尝试修改非静态成员变量,编译器会报错。
  • 可以被 const 对象调用:只有 const 成员函数可以被 const 对象调用。非 const 对象也可以调用 const 函数,但 const 对象不能调用非 const 函数。
  • 不会调用非 const 成员函数const 函数内部不可以调用非 const 成员函数,除非非 const 函数指定了 mutable 成员变量。

关于const函数有两种

void f(const int &x) {cout << x << "\n";
}

这里的 const 表示参数 x 是一个 const 引用,即函数 f 不能修改 x 的值。

class myClass {
public:int x;void f() const {cout << x << "\n";}
};

这个const 限定符表示该成员函数不会修改对象的状态(即不会修改类的非静态成员变量)。如果函数尝试修改成员变量(如 x),编译器会报错。

Const Pointer

int x;
const int y = 1;
// constant pointer to a non-constant int
int *const p1 = &x;
int *const p2 = &y;
// non-constant pointer to a constant int
const int *p2 = &y;
int const *p3 = &x;
// constant pointer to a constant int
const int *const p4 = &x;
int const *const p5 = &y;
  • 数据是否可变p1 指向的数据可以被修改,而其他指针(p2p3p4p5)指向的数据不能被修改。
  • 指针是否可变
    • p1 是常量指针,不能重新指向。
    • p2p3 是非常量指针,可以重新指向。
    • p4p5 是常量指针,不能重新指向。
  • 语法上的差异
    • const int *pint const *p 是等价的。
    • int *const p 表示指针本身是常量。

值得注意的是int *const p6 = &y;是无法编译的,因为yconst int 但是p6只能指向非常量变量。

分析以下代码中const的作用

const int * const myClassmethod(const int * const & pararm) const;
  • 参数部分const int * const &pararm

    • const int *表示指针指向了一个const int
    • const表示指针是const类型,不能被修改
  • 返回值部分const int * const

    • const int * 返回值指针指向const int
    • const表示指针是const类型
  • 成员函数const

    • 成员函数声明末尾的 const 表示该函数不会修改类的任何非静态成员变量

    参数中的&,表示的是参数是传引用。

Const Iterators

vector<int> a{1,23};
const vector<int>::iterator iter1 = a.begin();
iter1 ++;// doesnt compile
*iter1 = 4;// compilevector<int>::const_iterator iter2 = a.begin();
iter2 ++;// compile
*iter2 = 4;// doesnt compile

Operators

vector<string> a{"Hello", "World"};
cout << a[0];
a[1] += "1";

我看先来看这个简单的例子,这个例子中用到了运算符,运算符实际上可以理解为成员函数,因此这里的写法实际上是简写。

vector<string> a{"Hello", "World"};
cout.operator<<(a.operator[](0));
a.operator[](1) += "1";

上面说到了,运算符实际上就是函数,因此也可以这样写

vector<string> a{"Hello", "World"};
operator<<(cout, operator[](a, 1));
operator+=(operator[](a, 1), "!");

我们再来看一个下面的例子

#include <bits/stdc++.h>using std::cout;
using std::vector;
using std::string;using VecStr = vector<string>;VecStr &operator+=(VecStr &vs, const string &element) {vs.push_back(element);return vs;
}int main() {VecStr s;(((s += "Hello") += " ") += "World") += "!";for (auto i: s)cout << i;cout << "\n";return 0;
}

Special Member Function

这里学习了两个特殊的函数 copy constructor 和 copy assignment。

class String {
private:char* data;
public:// 拷贝构造函数(初始化新对象)String(const String& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);}// 拷贝赋值操作符(修改已存在对象)String& operator=(const String& other) {if (this != &other) { // 处理自赋值delete[] data;    // 释放旧资源data = new char[strlen(other.data) + 1];strcpy(data, other.data);}return *this;}~String() { delete[] data; } // 析构函数
};
  • 拷贝构造函数:创建新对象时初始化其状态。
  • 拷贝赋值操作符:修改已存在对象的状态,需处理自赋值和资源释放。

Move

l-value和r-value只是两个简化的模型

  • l-value 是一个具有名称(身份)的表达式。
    • 可以使用取地址运算符(&var)找到其地址。
    • 可以出现在赋值语句左边或右边的表达式。
  • r-value 是一个没有名称(身份)的表达式。
    • 临时值。
    • 不能使用取地址运算符(&var)找到其地址。
    • 只能出现在赋值语句右边的表达式。

下面再看一些简单的例子

int val = 2; // val is l-value, 2 is r-value
int *ptr = 0x2248837;// *ptr is l-value, 0x2248837 is r-valueauto v3 = v1 + v2; // v3, v1, v2 is l-value, but v1 + v2 is r-value
size_t size = v3.size(); // size is l-value, v3.size() is r-value
v1[1] = 4 * i; // v1[1] is l-value, 4 * i is r-value
ptr = &val; // &val is r-value
v1[2] = *ptr; // *ptr is l-value

为什么v3.size()是r-value?因为size()的返回值是size_t不是size_t&。同理v1[1]是l-value是因为operator[]的返回值是int&

然后再看两个例子

auto& v2 = v1; // v2 is l-value reference
auto&& v3 = v1 + v2; // v3 is r-value reference

v2是左值引用,因为v1是左值,所以访问v2实际上就是访问v1

v3是右值引用,v1 + v2是右值,因此访问v3是访问计算v1 + v2产生的临时变量,这样做可以延长临时变量的生存周期,减小内存消耗。

正常情况左值引用是不能引用到右值的。但是常量左值引用除外。

const auto& v4 = v1 + v2;

因为左值引用对于这如果修改了v1v2的值,v4的值也会被修改。但是因为是常量所以v4的值是不能被改变的。

r-value只是一个临时值,用完后就会被立即销毁,我们是否可以通过利用r-value 来提高效率?

因此对于刚才的String我可以写出这样两个函数,如果构造函数的值或赋值的值是右值我就可以直接移动指针。

String::String(String &&other) : data(other.data) { // Move Constuctorother.data = nullptr;
}String &String::operator=(String &&other) { // Move Assignmentif (this != &other) {delete[] data;ata = other.data;other.data = nullptr;}return *this;
}

这样写会减少很多内存的浪费。当然了这里用到的是指针可以直接赋值。但如果是类,我们可以使用std::move函数来实现。

class VecStr {
private:std::vector<std::string> data;
public:VecStr(VecStr &&other);VecStr &operator=(VecStr &&other);
};VecStr::VecStr(VecStr &&other) : data(std::move(other)) {}VecStr &VecStr::operator=(VecStr &&other) {if (this != &other) {data = std::move(other.data);}return *this;
}

因此我也可以写出一个不增加额外内存消耗的swap函数

template<typename T>
void swap(T &a, T &b) {T temp = std::move(a);a = std::move(b);b = std::move(temp);
}

Interface

class Drink {
public:virtual void make() = 0;
};class Tea : public Drink {
public:void make() override {return;}
};

由于C++中没有接口这个概念,因此如果要实现类似Java的接口可以采用这种方法实现。

首先定义了一个基类Drink,这个类包含了一个纯虚函数make(),因此Dirnk不能直接实例化,并且所有派生类都必须实现make()

什么是纯虚函数?

  • 纯虚函数是基类中声明但不提供具体实现的虚函数。
  • 语法:在虚函数声明末尾添加 = 0
  • 抽象类:包含纯虚函数的类称为抽象类,不能直接实例化。
  • 派生类责任:派生类必须实现所有基类的纯虚函数,否则自身也会成为抽象类。
  • 可选的基类实现:纯虚函数可以有默认实现(但派生类仍需显式重写)。

与普通虚函数的区别

特性 纯虚函数 普通虚函数
实现要求 基类无实现,派生类必须重写 基类有默认实现,派生类可选重写
类性质 使类成为抽象类 普通类(可直接实例化)
语法 virtual void func() = 0; virtual void func();
用途 定义强制接口规范 提供可扩展的默认行为

override 的作用

  • 明确意图 :它明确告知编译器,当前函数的目标是覆盖基类的虚函数。如果基类中没有对应的虚函数(例如拼写错误或参数列表不匹配),编译器会报错。
  • 防止拼写错误 :避免因为函数名称拼写错误或参数列表不匹配而未覆盖基类函数的问题。例如:
    • 如果基类的虚函数是 virtual void print();,而派生类中写成 void prin() override;,编译器会报错,因为基类中没有 prin() 虚函数。

class Tea : public Drink 中的 public 是继承访问修饰符,用于指定基类 Drink 中成员在派生类 Tea 中的可见性。

  • public 继承

    • 基类 Drink 中的 public 成员 在派生类 Tea 中仍然是 public
    • 基类 Drink 中的 protected 成员 在派生类 Tea 中仍然是 protected
  • protected 继承

    • 基类 Drink 中的 public 成员 在派生类 Tea 中变为 protected
    • 基类 Drink 中的 protected 成员 在派生类 Tea 中仍然是 protected
    • 外部无法通过 Tea 的对象或指针访问 make() 方法。
  • private 继承

    • 基类 Drink 中的 publicprotected 成员 在派生类 Tea 中变为 private
    • 外部无法通过 Tea 的对象或指针访问 make() 方法。

为什么要有虚函数,C++是可以对非虚函数进行重写的。

class Drink {
public:void make() {cout << "Drink" << "\n";return;};
};class Tea : public Drink {
public:void make() {cout << "Tea\n";return;}
};int main() {Drink drink;Tea tea;drink.make();tea.make();Drink *ptr1 = &drink;Drink *ptr2 = &tea;ptr1->make();ptr2->make();return 0;
}

ptr2指向tea,但是这样做只能调用基类的make()函数。

场景 非虚函数 虚函数
调用方式 静态绑定(编译时决定) 动态绑定(运行时决定)
多态支持 ❌ 无法通过基类指针调用派生类实现 ✅ 支持基类接口调用不同派生类的实现
性能开销 无额外开销 虚函数表查找的微小开销
设计意图 提供固定实现,不希望被派生类修改 定义可扩展的接口,强制/允许派生类自定义行为

下面这个例子综合了目前学过的所有有关继承的知识。

#include <iostream>using std::cin;
using std::cout;class Drink {
public:Drink() = default; // 显式声明默认构造函数Drink(const Drink &drink) = delete; // 禁用拷贝构造函数Drink(std::string flavor) : flavor(flavor) {}virtual void make() = 0; // 纯虚函数virtual void foo() { // 虚函数cout << "Drink " << flavor << "\n";}~Drink() = default; // 显式声明默认析构函数private:std::string flavor;
};class Tea : public Drink {
public:Tea() = default;Tea(std::string flavor) : Drink(flavor) {}virtual void make() override { // 对父类的make函数重写,并且允许之类继续重写但不强制要求cout << "made from Tea class\n";}void foo() override {Drink::foo(); // 显式调用父类版本cout << "Tea\n";}~Tea() = default;
};int main() {Tea t("red");t.foo();t.make();t.Drink::foo(); // 直接访问父类的foo函数
}

Template Class

基本语法

template <typename T> // 或者 template <class T>
class Box {// 类成员和方法的定义,可以使用 T 作为类型占位符
private:T content;
public:Box(T value) : content(value) {}T getContent() { return content; }
};

模板实例化

Box<int> intBox(42);        // T 被替换为 int
Box<std::string> strBox("Hello"); // T 被替换为 string

多类型参数

template <typename T, typename U>
class Pair {
public:T first;U second;Pair(T a, U b) : first(a), second(b) {}
};Pair<int, double> p1(10, 3.14);
Pair<std::string, bool> p2("key", true);

非类型模板参数

template <typename T, int N>
class Array {
private:T data[N];
public:T& operator[](int index) { return data[index]; }
};Array<double, 5> arr; // 创建一个大小为 5 的 double 数组

模板特化

// 通用模板
template <typename T>
class Printer {
public:void print(T value) { std::cout << value << std::endl; }
};// 特化为 bool 类型
template <>
class Printer<bool> {
public:void print(bool value) { std::cout << (value ? "true" : "false") << std::endl; }
};

typename 和 class 的区别

在C++17 之前的版本:有当声明模板模板参数必须使用class

// 定义一个接受模板模板参数的类
template <template <class T> class Container  // 正确:使用 class(兼容所有标准)
>
class MyClass {Container<int> data;  // 使用模板模板参数实例化
};// C++17 起也可以用 typename(但部分编译器可能不完全支持)
template <template <typename T> typename Container  // C++17 允许
>
class MyClassV2 { /* ... */ };

C++20 concept

在C++20 中引入了concept概念以在编译期检查模板实参是否满足指定的约束。

先看这个例子

template<typename T>
T foo(T a) {return ++a;
}

这个例子其实对a是限制的,即必须要求a必须有operator++。但这个实际上是隐式要求,只有当编译的时候才能体现出来。为了方便介绍,我们先强制要求为整型。

我们可以先用concept 定义约束

template<typename T>
concept integral = std::is_integral<T>::value;

std::is_integral是一个类型特征,可以判断一个值是否是整型。如果要知道结果就要调用value,其返回值是一个bool类型。

特征名称 功能描述 示例
std::is_integral 检查类型是否为整数类型(如 int, long 等) std::is_integral<int>::value == true
std::is_floating_point 检查类型是否为浮点类型(如 float, double std::is_floating_point<float>::value == true
std::is_arithmetic 检查类型是否为算术类型(整数或浮点) std::is_arithmetic<int>::value == true
std::is_same 检查两个类型是否相同 std::is_same<int, int>::value == true
std::is_const 检查类型是否为常量类型 std::is_const<const int>::value == true
std::is_volatile 检查类型是否为易失性类型 std::is_volatile<volatile int>::value == true
std::is_pointer 检查类型是否为指针类型 std::is_pointer<int*>::value == true
std::is_reference 检查类型是否为引用类型 std::is_reference<int&>::value == true
std::is_lvalue_reference 检查类型是否为左值引用类型 std::is_lvalue_reference<int&>::value == true
std::is_rvalue_reference 检查类型是否为右值引用类型 std::is_rvalue_reference<int&&>::value == true
std::remove_const 去除类型的常量限定符 typename std::remove_const<const int>::type
std::remove_volatile 去除类型的易失性限定符 typename std::remove_volatile<volatile int>::type
std::remove_reference 去除类型的引用限定符 typename std::remove_reference<int&>::type
std::remove_cv 去除类型的常量和易失性限定符 typename std::remove_cv<const volatile int>::type
  • _v 后缀:C++17 引入的模板变量,用于直接获取类型特征的布尔值。
    例如:std::is_integral_v<T> 等价于 std::is_integral<T>::value
  • _t 后缀:用于获取类型特征的别名。
    例如:std::remove_const_t<const int> 等价于 int

在定义好约束后,我们可以用以下方法要求T满足这个约束,这四种写法是等价的。

template<integral T>
T foo1(T a) {return ++a;
}integral auto foo2(integral auto a) {return ++a;
}template<typename T>
T foo3(T a) requires integral<T> {return ++a;
}template<typename T>
requires integral<T>
T foo4(T a) {return ++a;
}

当然对于约束,我们也可以进行嵌套

template<typename T>
concept signed_integral = integral<T> and std::is_signed<T>::value;template<typename T>
concept unsigned_integral = integral<T> && std::is_unsigned_v<T>;

当然了对于上述的要求我们也可自己实现一些要求,比如

template<typename T>
concept AddAble = requires(T a, T b) { a + b; }; // T 支持加法template<typename T, typename U>
concept check1 = requires(T a, U b) {{ a + b }; { b + a }; }; // T U 支持相加template<typename T>
concept check2 = requires(T a){{ *a }; // *a 有意义{ a + 1 } -> std::same_as<T>; // a + 1 有意义,且相加后类型不变。{ std::same_as<decltype((a * a)), T> }; //
};

RAII

内存泄漏的主要原因

  1. 手动管理失误
    • 使用 new 分配内存后,未调用 delete 释放。
    • 数组未用 delete[] 释放,或错误使用 free() 释放 new 分配的内存。
  2. 异常或中断
    • delete 前发生异常、return 或分支跳转,导致释放代码未执行。
  3. 循环引用(智能指针)
    • shared_ptr 互相引用时,引用计数无法归零,需结合 weak_ptr 解决。
  4. 未正确释放资源
    • 基类析构函数未声明为 virtual,导致派生类资源未释放。

异常(Exception)

异常是 C++ 中用于处理程序运行时错误的一种机制,允许程序在检测到错误时跳出当前执行流程,并通过特定代码块(catch)处理错误。其核心是 分离正常逻辑与错误处理,避免代码被大量错误检查污染。

  1. throw

    • 抛出异常:当检测到错误时,用 throw 抛出一个异常对象(可以是任意类型,但建议用标准异常类或自定义类)。
    • 示例:throw std::runtime_error("File not found");
  2. try

    • 监控代码块:将可能抛出异常的代码包裹在 try 块中。

    • 示例:

      try {// 可能抛出异常的代码openFile("data.txt");
      }
      
  3. catch

    • 捕获并处理异常:根据异常类型匹配对应的 catch 块,执行错误处理逻辑。

    • 示例:

      catch (const std::runtime_error& e) {std::cerr << "Error: " << e.what() << std::endl;
      }
      

完整例子

#include <iostream>
#include <stdexcept>double divide(int a, int b) {if (b == 0) {throw std::invalid_argument("除数不能为0"); // 抛出异常}return static_cast<double>(a) / b;
}int main() {try {double result = divide(10, 0); // 可能抛出异常的调用std::cout << "结果: " << result << std::endl;} catch (const std::invalid_argument &e) { // 捕获特定异常std::cerr << "捕获异常: " << e.what() << std::endl;} catch (...) { // 捕获所有其他异常std::cerr << "未知错误" << std::endl;}return 0;
}
  • 异常类型匹配时catch按照顺序匹配异常类型,并支持继承关系,如捕获基类异常std::exception可处理所有派生类异常。

  • 抛出异常后,程序会逐层退出函数调用栈,直到找到匹配的 catch 块,同时自动调用局部对象的析构函数(需确保析构函数不抛异常)。

  • C++ 在<stdexpect>提供了标准异常类,可以用what()获取错误信息

    • logic_error 程序逻辑错误

    • invalid_argument 逻辑错误:无效参数

    • domain_error 逻辑错误:参数对应的结果值不存在

    • length_error 逻辑错误:试图创建一个超出该类型最大长度的对象

    • out_of_range 逻辑错误:使用一个超出有效范围的值

    • runtime_error 运行时错误

    • range_error 运行时错误:生成的结果超出了有效值的范围

    • overflow_error 运行时错误:计算上溢

    • underflow_error 运行时错误:计算下溢

异常安全

函数可以具有四个级别的异常安全:

  • 不抛异常保证(Nothrow exception guarantee)
    • 绝对不会抛出异常:析构函数、交换操作、移动构造函数等。
  • 强异常安全保证(Strong exception guarantee)
    • 回滚到函数调用之前的状态。
  • 基本异常安全保证(Basic exception guarantee)
    • 异常后程序处于有效状态。
  • 无异常保证(No exception guarantee)
    • 资源泄漏、内存损坏、严重错误等。

RAII

RAII 的核心思想是将资源的获取与对象的构造绑定在一起,而资源的释放则与对象的析构绑定在一起。当对象超出作用域时,析构函数会自动被调用,从而释放资源。这种机制可以确保即使在发生异常的情况下,资源也能被正确释放。

void printFile() {ifstream input;input.open("hamlet.txt");string line;while (getline(input, line))std::cout << line << std::endl;input.close();
}

这样的一段代码就不符合RAII,因为获取资源和释放资源都是通过条用函数实现的,如果在中间出现了异常,则input.close()将无法正常执行。

void printFile() {ifstream input("hamlet.txt");string line;while (getline(input, line))std::cout << line << std::endl;
}

其实只要这样修改就可以满足RAII了

智能指针

void foo() {Node *n = new Node();// do somthingdelete n;
}

我们看这个函数,实际上这里的指针是不符合RAII的。如何解决这个问题?我们可以用智能指针实现,智能指针实际上就是一个类,我们用构造函数获取资源,在用析构函数释放资源。

C++ 的标准库中已经准备了三种智能指针

std::unique_ptr;
std::shared_ptr;
std::weak_ptr;

std::unique_ptr 不允许被复制,因为复制会导致一个资源不被唯一占用,还可能会导致资源被重复释放产生安全漏洞。

对于刚才的例子,用智能指针实现也很简单。

void foo1() {std::unique_ptr<Node> n(new Node);// do somthing// free!
}

如果确实需要复制指针,或者说确实需要多个指针指向一个对象,我们可以用std::shared_prt

{std::shared_ptr<int> p1(new int);// use p1{std::shared_ptr<int> p2(p1);std::shared_ptr<int> p3 = p2;// use p1, p2, p3}// use p1
}
// free

只有当指向一个对象所有的共享指针都离开了作用域时,这个对象才会被释放。

共享指针是如何实现的?对于同一个对象,共享指针维护了一个引用计数器,每拷贝一次计数器就会加一,每一个指针离开作用域计数器就会减一。当计数器为0时就会释放资源。

对于std::week_ptr他和std::shared_ptr比较类似,但是当他增加时,并不会使得引用计数器加一。

相同点

特性 说明
自动释放内存 离开作用域时自动释放资源,避免内存泄漏。
支持自定义删除器 可通过模板参数指定删除器(如 unique_ptr<T, Deleter>)。
操作符重载 支持 operator*operator->,行为类似裸指针。
RAII原则 资源生命周期与对象绑定,确保异常安全。

不同点

特性 unique_ptr shared_ptr weak_ptr
所有权 独占资源 共享资源 不拥有资源
拷贝语义 不可拷贝,只能移动 可拷贝,引用计数递增 可拷贝,但引用计数不变
引用计数 有(use_count() 无(但依赖 shared_ptr 的计数)
性能开销 有(原子操作维护引用计数) 低(仅观察)
循环引用问题 可能导致循环引用(需配合 weak_ptr 用于解决循环引用
典型使用场景 独占动态对象、工厂模式返回值 共享资源(如缓存、多线程数据) 观察者模式、打破循环引用

选择指南

  • 优先使用 unique_ptr:资源无需共享时,优先选择(性能最优)。
  • 共享资源用 shared_ptr:需要多个所有者时使用,但需注意循环引用风险。
  • 观察资源用 weak_ptr:配合 shared_ptr 使用,避免循环引用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/888147.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 中declare命令详解

Linux 中declare命令001、普通测试[root@PC1 dir1]# ls [root@PC1 dir1]# echo $var1[root@PC1 dir1]# var1="hello world" [root@PC1 dir1]# echo $var1 hello world [root@PC1 dir1]# var1=100.55 [root@PC1 dir1]# echo $var1 100.55 [root@PC1 dir1]# var1=100 […

《软件开发与创新课程设计》第一次课后作业——对学生选课系统的改进

(1)博客介绍 本文的学生选课系统的源码来自于csdn的一篇博客当中。该系统的实现语言以C++为主,本文的主要内容围绕该系统进行分析,并针对系统的主要问题进行一些修改或重构。 本篇如有问题存在,请各位读者多多指正! (2)学生选课系统分析 源代码如下: 点击查看代码 #de…

pikachu unsafe Fileupload

在上传点上传非法文件,提示上传文件不符合要求,且BP没有新的数据包产生,判断为客户端检查禁用浏览器JavaScript后刷新网页,再次上传文件,提示上传成功,文件路径为uploads/test.phpedge: 设置->Cookie和网站权限->所有权限->Javascript->禁用 Chorme:设置-&g…

rust学习十九.1、模式匹配(match patterns)

本章节大概是书本上比较特殊一个,因为它没有什么复杂的内容,通篇主要讨论模式匹配的语法。 一、两个名词a.可反驳 - refutable 对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable) let Some(x) = some_option_value;如果 some_option_value 的值是…

大对数电缆打线顺序

5种线缆主色:白色、红色、黑色、黄色、紫色 5种线缆配色:蓝色、橙色、绿色、棕色、灰色 25对电话电缆色谱线序表30对电话电缆色谱线序 这里要特别说明下:30对的电话电缆要注意了,30对通信电缆里有2种白色的主色,大于25对了就一定要看标识线了!!有一小把是用“白蓝"…

01-springsecurity数据库登录

01 - SpringSecurity实现数据库登录 环境: springboot 3.4.2, springsecurity 6.4.2, mybatis 3.0.4springsecurity中的UserDetails接口用于表示用户信息, 包含用户名、密码等信息。UserDetailsService接口用于加载用户信息, 里边就这一个方法 public interface UserDetailsSer…

【喜与悲】- 2025.2.22 晚

下图为《Balatro》中的一张小丑牌:【喜与悲】喜与悲可以重新触发所有打出的人头牌,是重新触发家族中的一员。但其特性也决定了其强度方差极大,有配合则强度很高,没有配合则纯浪费小丑位。但很少有小丑能与其配合,而能与其配合的小丑大多单独拎出来又不强。更多时候其几乎只…

莫队算法学习笔记

莫队算法的发明者是一个叫做 莫涛 的人,所以被称为莫队算法,简称莫队。 英语 Mos algorithm。 使用场景 莫队算法常常被用来处理多次区间询问的问题,离线处理询问(必须满足!!!)。 插叙:离线是一种得到所有询问再进行计算的方法,是很重要的思想。 对于这种“区间询问”…

参数-返回值-局部变量-数组

参数和局部变量没有本质区别,都是栈中的数据 参数时在函数调用前分配的值,局部变量是在函数调用时分配的值 参数 ebp+* 局部变量 ebp-* 赋值的本质是把运算结果放到某个内存里数组: 一堆连续存储的等宽数据

详细介绍java的线程池状态

一、详细介绍java的线程池状态 Java 中的线程池状态是 ThreadPoolExecutor 类内部管理的一个重要概念。线程池的状态决定了线程池的行为,例如是否接受新任务、是否处理队列中的任务、是否中断正在执行的任务等。 线程池的状态通过一个 AtomicInteger 变量(ctl)来表示,该变量…

[Java SE] Java静态代码块与静态属性的执行顺序

序 重要结论先说结论,再去观察实验现象,印证结论。静态变量初始化和静态代码块的执行顺序是:按照它们在类中出现的顺序进行的。代码实验 实验1import org.slf4j.Logger; import org.slf4j.LoggerFactory;public class JavaStaticTest {private final static String VAR = &qu…

PyTorch TensorBoard 使用

这篇文章介绍如何在 PyTorch 中使用 TensorBoard 记录训练数据。 记录数据 初始化 在程序启动时创建 SummaryWriter 对象用于写入日志数据。 from torch.utils.tensorboard import SummaryWriter import datetime# 获取当前时间戳,一般以时间戳作为记录文件夹名称的一部分 tim…