CPP学习
2025年7月2日
2025年4月22日
-
迭代器扮演着一种桥梁的角色,它们允许程序员以统一和抽象的方式访问容器中的元素
- 标准模板库(STL)中的 std::vector 容器有一个 begin() 方法,该方法返回一个指向容器第一个元素的迭代器
-
迭代器的几种类型
输入迭代器 (Input Iterators): 只允许向前移动并读取元素。
输出迭代器 (Output Iterators): 只允许向前移动并写入元素。
前向迭代器 (Forward Iterators): 同时具备输入和输出迭代器的特性。
双向迭代器 (Bidirectional Iterators): 可以向前和向后移动。
随机访问迭代器 (Random Access Iterators): 提供对元素的随机访问 -
在 C++ 的世界里,迭代器扮演着桥梁的角色,它们连接了算法和容器,为我们提供了一种优雅、高效的方式来操作数据
-
cbegin()和cend() 返回的是不可修改的const_iterator类型的迭代器
- const_iterator 是 C++ 中的一种迭代器类型,用于遍历容器中的元素。与普通的迭代器不同,const_iterator 不允许修改所指向的元素的值。它通常用于只读访问容器的元素。
-
对于常量容器,begin() 和 end() 也会返回 const_iterator 类型。这是因为在常量容器上,我们不能修改其元素的值,这与 cbegin() 和 cend() 的行为一致。
?什么是常量容器- 使用 const 关键字
- 使用 const_iterator
- 使用 std::array std::array<int, 5> numbers = {1, 2, 3, 4, 5};
-
然而,在非常量容器上,使用 const auto& 在范围基于的 for 循环中不会改变 begin() 和 end() 的返回类型。它们仍然返回 iterator 类型,但是元素的引用是常量。
学习一下 try catch
try {
std::cout << "Element at index 2: " << numbers.at(2) << std::endl;//.at()访问时会带边界检查,如果越界会抛出out_of_range异常
} catch (const std::out_of_range& e) {
std::cerr << "Index out of range: " << e.what() << std::endl;
}
- 当使用 try-catch 结构时,了解 try 块中可能抛出的异常类型是非常重要的
- 在 C++ 中,异常是通过类来表示的,所有异常类都继承自标准库中的 std::exception 类。
- 多重捕获
try {
// 可能抛出多种异常的代码
} catch (const std::out_of_range& e) {
// 处理 std::out_of_range 异常
} catch (const std::invalid_argument& e) {
// 处理 std::invalid_argument 异常
}
- 自定义容器和迭代器
模板类和模板函数
在 C++ 中,模板是一种强大的特性,允许你编写通用的代码。模板可以用于类和函数,使它们能够处理不同的数据类型,而无需重复编写相同的代码
- 模板类 模板类允许你定义一个类,该类可以处理多种数据类型。你可以定义一个类模板,然后在实例化时指定具体的数据类型。
#include <iostream>
// 定义一个模板类,用于存储一对值
template <typename T>
class Pair {
private:
T first, second;
public:
Pair(T a, T b) : first(a), second(b) {}
T getFirst() const { return first; }
T getSecond() const { return second; }
};
int main() {
Pair<int> intPair(1, 2);
std::cout << "First: " << intPair.getFirst() << ", Second: " << intPair.getSecond() << std::endl;
Pair<double> doublePair(3.14, 2.71);
std::cout << "First: " << doublePair.getFirst() << ", Second: " << doublePair.getSecond() << std::endl;
return 0;
}
经典lambda表达式
- lambda 表达式是一种轻量级的匿名函数,允许你在代码中定义和使用函数,而无需显式地声明它们。lambda 表达式通常用于简化代码,特别是在需要传递函数作为参数的场合
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 3};
// 使用 lambda 表达式对向量进行排序
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b; // 升序排序
});
// 输出排序后的结果
std::cout << "Sorted numbers: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
-
在实践中,通常建议优先使用标准容器,因为它们经过广泛测试和优化。但在需要特定功能或性能优化时,自定义容器成为了一个可行的选择。
-
一个对比
常量迭代器
const std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
std::cout << num << " ";
}
非常量迭代器
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (const auto& num : numbers) {
std::cout << num << " ";
}
都不可以修改,后者是因为const auto&
关于const
- 指针常量:
const int* ptr = &a;
值不可以改变 - 常量指针:
int* const ptr = &a;
指针指向不可以改变 - 常量成员函数
void display() const;
表示该函数不会修改类的成员变量
2025年4月17日
- RAII 解决内存问题,不影响效率
- 应该先重点攻克cpp三大件 容器、算法和迭代器
RAII(Resource Acquisition Is Initialization)设计模式详解
1. 什么是 RAII?
RAII(资源获取即初始化)是 C++ 中的核心设计模式,核心思想是:
- 资源获取(如内存、文件句柄、锁)与对象的初始化绑定。
- 资源释放与对象的析构绑定。
- 通过对象的生命周期自动管理资源,避免手动管理导致的泄露或错误。
2. RAII 的核心机制
(1) 资源获取在构造函数
- 对象创建时,在构造函数中获取资源(如分配内存、打开文件)。
- 如果资源获取失败,抛出异常,确保对象未完全构造时不会泄露资源。
(2) 资源释放在析构函数
- 对象销毁时,在析构函数中自动释放资源。
- 无论对象是因正常退出作用域还是异常而销毁,析构函数都会被调用。
3. RAII 的优势
优势 | 说明 |
---|---|
自动释放 | 无需手动释放资源,避免忘记释放或重复释放。 |
异常安全 | 即使代码抛出异常,析构函数仍会调用,确保资源释放。 |
代码简洁 | 资源管理逻辑封装在类中,业务代码更清晰。 |
减少错误 | 避免悬空指针、内存泄漏等问题。 |
- 悬空指针:指针指向的内存已被释放或失效,但指针本身未被置空,仍保留原地址。
- 野指针:指针未初始化(随机指向非法内存),或指向已释放的内存(与悬空指针部分重叠)。
- 内存泄漏:动态分配的内存未被释放,导致程序持续占用内存,最终可能耗尽系统资源。
4. RAII 的经典示例
示例 1:动态内存管理(智能指针)
C++ 的 std::unique_ptr
和 std::shared_ptr
是 RAII 的典型实现:
#include <memory>
void example_memory() {
// 使用 unique_ptr 管理动态内存
std::unique_ptr<int> ptr(new int(42));
// 无需手动 delete,离开作用域时自动释放
}
示例 2:文件操作
封装文件句柄的打开和关闭:
#include <fstream>
class File {
public:
File(const std::string& filename) {
file.open(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
~File() {
if (file.is_open()) {
file.close(); // 析构时自动关闭文件
}
}
void write(const std::string& data) {
file << data;
}
private:
std::fstream file;
};
void example_file() {
try {
File file("test.txt");
file.write("Hello RAII!");
// 离开作用域时,自动调用 ~File() 关闭文件
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
}
示例 3:互斥锁管理
确保锁在作用域结束时自动释放:
#include <mutex>
class LockGuard {
public:
explicit LockGuard(std::mutex& mtx) : mutex(mtx) {
mutex.lock(); // 构造函数中加锁
}
~LockGuard() {
mutex.unlock(); // 析构时解锁
}
private:
std::mutex& mutex;
};
void example_lock() {
std::mutex mtx;
{
LockGuard lock(mtx); // 加锁
// 临界区操作...
} // 离开作用域,自动解锁
}
5. RAII 的对比分析
不使用 RAII 的问题
// 手动管理文件,容易忘记关闭或异常导致泄露
void bad_file_usage() {
std::fstream file;
file.open("test.txt");
file << "Data";
// 如果此处抛出异常,file.close() 不会被调用!
file.close(); // 可能被忘记
}
使用 RAII 的改进
- 无论是否发生异常,资源都会在析构函数中释放。
- 代码逻辑更清晰,资源管理由类封装。
6. RAII 的关键设计原则
-
单一职责原则
每个 RAII 类只管理一种资源。 -
禁止拷贝(或使用移动语义)
若资源不可复制(如文件句柄),需禁用拷贝构造函数和赋值操作符,或实现移动语义。 -
异常安全
构造函数中若资源获取失败,应抛出异常,避免返回部分构造的对象。
7. 常见 RAII 应用场景
资源类型 | RAII 封装示例 |
---|---|
动态内存 | std::unique_ptr , std::shared_ptr |
文件句柄 | std::fstream , 自定义 File 类 |
网络连接 | 封装 Socket 连接类 |
图形资源(如 GPU) | OpenGL 纹理管理类 |
数据库连接 | 封装连接池类 |
8. RAII 的进阶技巧
(1) 移动语义支持
允许资源所有权的转移,避免拷贝:
class Resource {
public:
Resource() { /* 获取资源 */ }
~Resource() { /* 释放资源 */ }
// 移动构造函数
Resource(Resource&& other) noexcept {
// 转移资源所有权
}
// 移动赋值操作符
Resource& operator=(Resource&& other) noexcept {
// 转移资源所有权
return *this;
}
// 禁用拷贝
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
};
(2) 延迟初始化
某些资源可能需要在首次使用时初始化:
class LazyResource {
public:
void use() {
if (!initialized) {
acquireResource(); // 首次使用时初始化
initialized = true;
}
// 使用资源
}
~LazyResource() {
if (initialized) {
releaseResource();
}
}
private:
bool initialized = false;
};
9. 总结
- RAII 的本质:通过对象的生命周期管理资源,确保安全释放。
- 核心实现:构造函数获取资源,析构函数释放资源。
- 适用场景:所有需要手动管理的资源(内存、文件、锁等)。
- 优势:代码简洁、异常安全、避免资源泄露。
通过结合智能指针、自定义 RAII 类,可以大幅提升 C++ 程序的健壮性和可维护性。