C++学习:六个月从基础到就业——内存管理:RAII原则
C++学习:六个月从基础到就业——内存管理:RAII原则
本文是我C++学习之旅系列的第十九篇技术文章,也是第二阶段"C++进阶特性"的第四篇,主要介绍C++中的RAII原则及其在资源管理中的应用。查看完整系列目录了解更多内容。
引言
在前几篇文章中,我们讨论了堆与栈、new
/delete
操作符以及内存泄漏问题。本文将深入探讨C++中一个核心的资源管理原则:RAII(Resource Acquisition Is Initialization)。这个原则是C++区别于许多其他编程语言的重要特性之一,它提供了一种优雅而安全的方式来管理资源。
RAII原则看似简单,但蕴含深意:将资源的生命周期与对象的生命周期绑定在一起,在构造函数中获取资源,在析构函数中释放资源。这个简单而强大的概念为C++提供了一种不依赖垃圾回收就能安全管理资源的方式,成为现代C++编程不可或缺的核心原则。
本文将带你深入理解RAII的概念、实现方式、应用场景以及最佳实践,帮助你写出更加安全、可靠的C++代码。
RAII原则概述
什么是RAII?
RAII(Resource Acquisition Is Initialization)是一种C++编程范式,字面意思是"资源获取即初始化"。这个名字来源于它的核心思想:将资源的获取与对象的初始化(构造)绑定,将资源的释放与对象的销毁(析构)绑定。
在RAII模式下,资源(如内存、文件句柄、锁等)由对象的构造函数获取,并由析构函数自动释放。由于C++保证对象离开作用域时会调用其析构函数,这就确保了资源的正确释放,无论函数如何返回(正常返回或异常返回)。
RAII的基本原理
RAII的工作原理可概括为以下几个步骤:
- 创建一个类,其构造函数获取资源
- 类的析构函数负责释放资源
- 使用该类的对象来管理资源
- 当对象离开作用域时,自动调用析构函数释放资源
这种机制利用了C++栈展开(stack unwinding)的特性,即使在异常情况下,也能确保资源被正确释放。
一个简单的RAII示例
以下是一个简单的RAII示例,展示如何管理动态分配的内存:
#include <iostream>class IntResource {
private:int* data;public:// 构造函数获取资源IntResource(int value) : data(new int(value)) {std::cout << "Resource acquired: " << *data << std::endl;}// 析构函数释放资源~IntResource() {std::cout << "Resource released: " << *data << std::endl;delete data;}// 访问资源int getValue() const {return *data;}// 修改资源void setValue(int value) {*data = value;}
};void useResource() {IntResource resource(42); // 资源获取std::cout << "Using resource: " << resource.getValue() << std::endl;resource.setValue(100);std::cout << "Modified resource: " << resource.getValue() << std::endl;// 无需手动释放资源,当resource离开作用域时自动释放
}int main() {std::cout << "Before calling useResource()" << std::endl;useResource();std::cout << "After calling useResource()" << std::endl;return 0;
}
输出:
Before calling useResource()
Resource acquired: 42
Using resource: 42
Modified resource: 100
Resource released: 100
After calling useResource()
在这个例子中,IntResource
类管理一个动态分配的整数。当resource
对象创建时,构造函数分配内存;当对象离开作用域时,析构函数自动释放内存。这就是RAII的核心思想。
RAII的应用场景
内存资源管理
RAII最常见的应用之一是管理动态分配的内存,这也是标准库智能指针的基本原理:
#include <memory>
#include <iostream>void smartPointerExample() {// 使用unique_ptr管理动态分配的整数std::unique_ptr<int> ptr = std::make_unique<int>(42);std::cout << "Value: " << *ptr << std::endl;// 无需手动delete,ptr离开作用域时自动释放内存
}
文件句柄管理
RAII可用于确保文件正确关闭:
#include <fstream>
#include <iostream>
#include <stdexcept>class FileHandler {
private:std::fstream file;public:FileHandler(const std::string& filename, std::ios_base::openmode mode) {file.open(filename, mode);if (!file.is_open()) {throw std::runtime_error("Failed to open file: " + filename);}std::cout << "File opened successfully" << std::endl;}~FileHandler() {if (file.is_open()) {file.close();std::cout << "File closed" << std::endl;}}std::fstream& getFile() {return file;}
};void processFile(const std::string& filename) {try {FileHandler handler("example.txt", std::ios::in | std::ios::out);// 使用文件...auto& file = handler.getFile();file << "Hello, RAII!" << std::endl;// 即使这里抛出异常,文件也会在handler销毁时关闭if (someErrorCondition) {throw std::runtime_error("Processing error");}} catch (const std::exception& e) {std::cerr << "Error: " << e.what() << std::endl;// 文件已经在这里被关闭了}// 无论是正常退出还是异常退出,文件都会关闭
}
互斥锁管理
在多线程编程中,RAII可用于确保互斥锁的正确释放:
#include <mutex>
#include <iostream>
#include <thread>std::mutex mtx;class ScopedLock {
private:std::mutex& mutex;public:explicit ScopedLock(std::mutex& m) : mutex(m) {mutex.lock();std::cout << "Mutex locked" << std::endl;}~ScopedLock() {mutex.unlock();std::cout << "Mutex unlocked" << std::endl;}// 禁止复制ScopedLock(const ScopedLock&) = delete;ScopedLock& operator=(const ScopedLock&) = delete;
};void criticalSection() {// 进入作用域时锁定互斥锁ScopedLock lock(mtx);// 临界区代码...std::cout << "Critical section executed by thread " << std::this_thread::get_id() << std::endl;// 可能抛出异常的代码...// 离开作用域时自动解锁互斥锁
}
注意:C++标准库已经提供了std::lock_guard
、std::unique_lock
等RAII包装器来管理互斥锁。
数据库连接管理
RAII可用于管理数据库连接:
class DatabaseConnection {
private:DB_Connection* connection;public:DatabaseConnection(const std::string& connectionString) {connection = DB_Connect(connectionString.c_str());if (!connection) {throw std::runtime_error("Failed to connect to database");}std::cout << "Database connected" << std::endl;}~DatabaseConnection() {if (connection) {DB_Disconnect(connection);std::cout << "Database disconnected" << std::endl;}}// 提供访问connection的方法DB_Connection* getConnection() {return connection;}// 禁止复制DatabaseConnection(const DatabaseConnection&) = delete;DatabaseConnection& operator=(const DatabaseConnection&) = delete;
};void queryDatabase() {DatabaseConnection db("server=localhost;user=root;password=1234");// 使用数据库...DB_ExecuteQuery(db.getConnection(), "SELECT * FROM users");// 数据库会在函数退出时自动断开连接
}
网络连接管理
类似地,RAII可用于管理网络连接:
class NetworkConnection {
private:int socketFd;public:NetworkConnection(const std::string& address, int port) {socketFd = socket(AF_INET, SOCK_STREAM, 0);if (socketFd < 0) {throw std::runtime_error("Failed to create socket");}// 连接到服务器...if (connect(socketFd, /*服务器地址*/, /*地址长度*/) < 0) {close(socketFd);throw std::runtime_error("Failed to connect to server");}std::cout << "Connected to server" << std::endl;}~NetworkConnection() {if (socketFd >= 0) {close(socketFd);std::cout << "Disconnected from server" << std::endl;}}// 提供socket访问方法...int getSocket() const {return socketFd;}// 禁止复制NetworkConnection(const NetworkConnection&) = delete;NetworkConnection& operator=(const NetworkConnection&) = delete;
};
RAII与异常安全
RAII是实现异常安全代码的基础,它确保即使在异常发生时资源也能正确释放。
异常安全与资源管理
让我们看看不使用RAII时可能发生的问题:
void nonRaiiFunction() {int* array = new int[1000];// 如果process()抛出异常,array将泄漏process(array);delete[] array; // 如果发生异常,这行不会执行
}
而使用RAII则可以避免这个问题:
void raiiFunction() {std::unique_ptr<int[]> array(new int[1000]);// 即使process()抛出异常,array也会被释放process(array.get());// 不需要手动delete,unique_ptr会自动处理
}
栈展开和RAII
当异常被抛出时,C++会执行"栈展开"(stack unwinding)过程,即沿着调用栈逐层回溯,销毁每个作用域中的局部对象。这确保了所有RAII对象的析构函数都会被调用,从而释放它们管理的资源。
#include <iostream>
#include <stdexcept>class Resource {
public:Resource(int id) : id_(id) {std::cout << "Resource " << id_ << " acquired" << std::endl;}~Resource() {std::cout << "Resource " << id_ << " released" << std::endl;}private:int id_;
};void function3() {Resource res3(3);std::cout << "In function3, throwing exception..." << std::endl;throw std::runtime_error("Exception from function3");
}void function2() {Resource res2(2);std::cout << "In function2, calling function3..." << std::endl;function3();std::cout << "This line will not be executed" << std::endl;
}void function1() {Resource res1(1);std::cout << "In function1, calling function2..." << std::endl;try {function2();} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;}std::cout << "Back in function1" << std::endl;
}int main() {std::cout << "In main, calling function1..." << std::endl;function1();std::cout << "Back in main" << std::endl;return 0;
}
输出:
In main, calling function1...
Resource 1 acquired
In function1, calling function2...
Resource 2 acquired
In function2, calling function3...
Resource 3 acquired
In function3, throwing exception...
Resource 3 released
Resource 2 released
Caught exception: Exception from function3
Back in function1
Resource 1 released
Back in main
从输出可以看出,当异常从function3
抛出时,栈展开过程逐一释放了资源3、资源2和资源1,确保所有资源都被正确释放。
强异常保证与RAII
RAII有助于实现"强异常保证",即操作要么完全成功,要么在失败时不产生任何影响(不改变程序状态):
class DataHolder {
private:int* data;size_t size;public:DataHolder(size_t s) : data(nullptr), size(0) {// 采用"先分配后赋值"策略以实现强异常保证int* temp = new int[s]; // 可能抛出异常// 到这里,内存分配已成功data = temp;size = s;}~DataHolder() {delete[] data;}void resize(size_t newSize) {// 采用"copy-and-swap"策略DataHolder temp(newSize); // 创建新对象(可能抛出异常)// 复制数据for (size_t i = 0; i < std::min(size, newSize); ++i) {temp.data[i] = data[i];}// 交换资源(不会抛出异常)std::swap(data, temp.data);std::swap(size, temp.size);// temp销毁时释放原始资源}// 禁止复制DataHolder(const DataHolder&) = delete;DataHolder& operator=(const DataHolder&) = delete;
};
在上面的例子中,resize
方法使用RAII和"copy-and-swap"策略实现了强异常保证:如果resize
过程中发生异常,原对象保持不变。
设计良好的RAII类
基本原则
设计良好的RAII类应遵循以下原则:
- 在构造函数中获取资源,构造失败时抛出异常
- 在析构函数中释放资源,且析构函数不应抛出异常
- 提供清晰的资源访问接口
- 考虑资源所有权语义:复制、移动或禁止复制
- 避免资源被意外释放,例如通过禁止某些操作
复制与移动语义
一个RAII类需要明确定义其资源的复制和移动行为:
禁止复制
如果资源不应被共享或复制成本高昂,应禁止复制:
class UniqueResource {
private:Resource* resource;public:UniqueResource(const std::string& name) : resource(acquireResource(name)) {}~UniqueResource() { releaseResource(resource); }// 禁止复制UniqueResource(const UniqueResource&) = delete;UniqueResource& operator=(const UniqueResource&) = delete;// 允许移动UniqueResource(UniqueResource&& other) noexcept : resource(other.resource) {other.resource = nullptr;}UniqueResource& operator=(UniqueResource&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}
};
深复制
如果资源可以被复制,实现深复制:
class CopyableResource {
private:Resource* resource;public:CopyableResource(const std::string& name) : resource(acquireResource(name)) {}~CopyableResource() { releaseResource(resource); }// 深复制CopyableResource(const CopyableResource& other) : resource(cloneResource(other.resource)) {}CopyableResource& operator=(const CopyableResource& other) {if (this != &other) {Resource* newResource = cloneResource(other.resource);releaseResource(resource);resource = newResource;}return *this;}// 移动语义CopyableResource(CopyableResource&& other) noexcept : resource(other.resource) {other.resource = nullptr;}CopyableResource& operator=(CopyableResource&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}
};
引用计数
如果资源需要共享且支持引用计数:
class SharedResource {
private:struct ControlBlock {Resource* resource;int refCount;ControlBlock(Resource* r) : resource(r), refCount(1) {}~ControlBlock() { releaseResource(resource); }};ControlBlock* controlBlock;void incrementRefCount() {if (controlBlock) {++controlBlock->refCount;}}void decrementRefCount() {if (controlBlock && --controlBlock->refCount == 0) {delete controlBlock;controlBlock = nullptr;}}public:SharedResource(const std::string& name) : controlBlock(new ControlBlock(acquireResource(name))) {}~SharedResource() {decrementRefCount();}// 复制增加引用计数SharedResource(const SharedResource& other) : controlBlock(other.controlBlock) {incrementRefCount();}SharedResource& operator=(const SharedResource& other) {if (this != &other) {decrementRefCount();controlBlock = other.controlBlock;incrementRefCount();}return *this;}// 移动不改变引用计数SharedResource(SharedResource&& other) noexcept : controlBlock(other.controlBlock) {other.controlBlock = nullptr;}SharedResource& operator=(SharedResource&& other) noexcept {if (this != &other) {decrementRefCount();controlBlock = other.controlBlock;other.controlBlock = nullptr;}return *this;}
};
这类似于std::shared_ptr
的实现原理。
“Rule of Three/Five/Zero”
在C++中,资源管理类通常遵循以下规则之一:
-
Rule of Three:如果一个类需要自定义析构函数、复制构造函数或复制赋值运算符中的任何一个,那么通常它需要三个全部。
-
Rule of Five(C++11后):如果一个类需要自定义析构函数、复制构造函数、复制赋值运算符、移动构造函数或移动赋值运算符中的任何一个,那么通常它需要五个全部。
-
Rule of Zero:如果一个类不直接管理资源,那么它不应该自定义任何这些函数,而应该依赖编译器生成的默认版本。
示例 - Rule of Five:
class ResourceManager {
private:Resource* resource;public:// 构造函数ResourceManager(const std::string& name) : resource(acquireResource(name)) {}// 析构函数~ResourceManager() { releaseResource(resource); }// 复制构造函数ResourceManager(const ResourceManager& other) : resource(cloneResource(other.resource)) {}// 复制赋值运算符ResourceManager& operator=(const ResourceManager& other) {if (this != &other) {Resource* newResource = cloneResource(other.resource);releaseResource(resource);resource = newResource;}return *this;}// 移动构造函数ResourceManager(ResourceManager&& other) noexcept : resource(other.resource) {other.resource = nullptr;}// 移动赋值运算符ResourceManager& operator=(ResourceManager&& other) noexcept {if (this != &other) {releaseResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}
};
示例 - Rule of Zero:
class NoResourceManagement {
private:std::unique_ptr<Resource> resource; // 使用RAII包装器管理资源std::string name;public:NoResourceManagement(const std::string& n) : resource(std::make_unique<Resource>(n)), name(n) {}// 不需要自定义任何特殊函数,编译器会生成合适的版本
};
防止资源泄漏的技巧
在设计RAII类时,应考虑以下防止资源泄漏的技巧:
- 构造函数保证:确保构造完成后对象处于有效状态,否则抛出异常
- 析构函数安全:确保析构函数不会抛出异常
- 防止双重释放:释放资源后将指针设为nullptr
- 考虑自赋值:在赋值运算符中处理自赋值情况
- 使用智能指针:尽可能利用标准库的智能指针管理资源
示例 - 防止双重释放:
class SafeResource {
private:Resource* resource;public:SafeResource(const std::string& name) : resource(acquireResource(name)) {}~SafeResource() {if (resource) { // 检查资源是否有效releaseResource(resource);resource = nullptr; // 防止double-free}}// 确保移动后原对象处于安全状态SafeResource(SafeResource&& other) noexcept : resource(other.resource) {other.resource = nullptr; // 防止原对象释放资源}SafeResource& operator=(SafeResource&& other) noexcept {if (this != &other) {if (resource) {releaseResource(resource);}resource = other.resource;other.resource = nullptr;}return *this;}// 禁止复制SafeResource(const SafeResource&) = delete;SafeResource& operator=(const SafeResource&) = delete;
};
标准库中的RAII实现
智能指针
标准库提供了几种智能指针,它们都是RAII的典型实现:
std::unique_ptr
std::unique_ptr
实现了独占所有权语义的RAII,管理的资源不能共享:
#include <memory>void uniquePtrExample() {// 创建管理单个对象的unique_ptrstd::unique_ptr<int> p1 = std::make_unique<int>(42);// 创建管理数组的unique_ptrstd::unique_ptr<int[]> p2 = std::make_unique<int[]>(10);// 使用自定义删除器auto deleter = [](FILE* f) { fclose(f); };std::unique_ptr<FILE, decltype(deleter)> file(fopen("example.txt", "r"), deleter);// unique_ptr不能复制,但可以移动// std::unique_ptr<int> p3 = p1; // 错误:不能复制std::unique_ptr<int> p4 = std::move(p1); // 正确:转移所有权// 离开作用域时,p2、p4和file会自动释放其资源
}
std::shared_ptr
std::shared_ptr
实现了共享所有权语义的RAII,多个指针可以共享同一资源:
#include <memory>void sharedPtrExample() {// 创建一个shared_ptrstd::shared_ptr<int> p1 = std::make_shared<int>(42);std::cout << "Reference count: " << p1.use_count() << std::endl; // 输出1// 共享所有权{std::shared_ptr<int> p2 = p1;std::cout << "Reference count: " << p1.use_count() << std::endl; // 输出2// 修改共享对象*p2 = 100;std::cout << "Value through p1: " << *p1 << std::endl; // 输出100} // p2销毁,引用计数减1std::cout << "Reference count: " << p1.use_count() << std::endl; // 输出1// 使用自定义删除器auto deleter = [](int* p) { std::cout << "Custom deleter called" << std::endl;delete p;};std::shared_ptr<int> p3(new int(99), deleter);// p1和p3离开作用域时,会释放它们管理的资源
}
std::weak_ptr
std::weak_ptr
是std::shared_ptr
的伴随类,它不拥有所指对象,不影响引用计数,用于解决循环引用问题:
#include <memory>class Node {
public:std::shared_ptr<Node> next; // 强引用std::weak_ptr<Node> previous; // 弱引用,防止循环引用Node(int val) : value(val) {std::cout << "Node " << value << " created" << std::endl;}~Node() {std::cout << "Node " << value << " destroyed" << std::endl;}int value;
};void weakPtrExample() {// 创建节点auto node1 = std::make_shared<Node>(1);auto node2 = std::make_shared<Node>(2);// 建立双向链接node1->next = node2;node2->previous = node1; // 弱引用,不增加node1的引用计数// 检查引用std::cout << "node1 reference count: " << node1.use_count() << std::endl; // 应为1std::cout << "node2 reference count: " << node2.use_count() << std::endl; // 应为2// 使用weak_ptrif (auto shared = node2->previous.lock()) {std::cout << "Previous node value: " << shared->value << std::endl;} else {std::cout << "Previous node is gone" << std::endl;}// 节点离开作用域时会被正确销毁
}
标准库的其他RAII类
除了智能指针,标准库还有许多其他基于RAII的类:
std::lock_guard和std::unique_lock
用于互斥量管理的RAII类:
#include <mutex>
#include <thread>std::mutex mtx;void lockGuardExample() {// 在构造时锁定互斥量,析构时解锁std::lock_guard<std::mutex> lock(mtx);// 临界区代码...std::cout << "Critical section with lock_guard" << std::endl;// lock离开作用域时自动解锁,即使有异常抛出也是如此
}void uniqueLockExample() {// unique_lock比lock_guard更灵活std::unique_lock<std::mutex> lock(mtx);// 临界区代码...std::cout << "Critical section with unique_lock" << std::endl;// 可以提前解锁lock.unlock();std::cout << "Lock released" << std::endl;// 可以重新锁定lock.lock();std::cout << "Lock acquired again" << std::endl;// lock离开作用域时自动解锁
}
std::scoped_lock (C++17)
用于同时锁定多个互斥量,避免死锁:
#include <mutex>
#include <thread>std::mutex mtx1, mtx2;void scopedLockExample() {// 原子地锁定多个互斥量,避免死锁std::scoped_lock lock(mtx1, mtx2);// 临界区代码...std::cout << "Critical section with scoped_lock" << std::endl;// lock离开作用域时自动解锁所有互斥量
}
std::ifstream和std::ofstream
文件流类也遵循RAII原则:
#include <fstream>
#include <iostream>void fileStreamExample() {// 打开文件std::ofstream outFile("example.txt");if (!outFile) {std::cerr << "Failed to open file for writing" << std::endl;return;}// 写入文件outFile << "Hello, RAII!" << std::endl;// 读取文件std::ifstream inFile("example.txt");if (inFile) {std::string line;while (std::getline(inFile, line)) {std::cout << "Read from file: " << line << std::endl;}}// 文件流在离开作用域时自动关闭
}
设计自己的RAII包装器
有时我们需要为没有现成RAII包装器的资源创建自己的包装器:
#include <iostream>// 假设这是一个C风格的API
extern "C" {struct Resource;Resource* createResource();void destroyResource(Resource* res);void useResource(Resource* res);
}// RAII包装器
class ResourceWrapper {
private:Resource* resource;public:ResourceWrapper() : resource(createResource()) {if (!resource) {throw std::runtime_error("Failed to create resource");}}~ResourceWrapper() {destroyResource(resource);}// 禁止复制ResourceWrapper(const ResourceWrapper&) = delete;ResourceWrapper& operator=(const ResourceWrapper&) = delete;// 允许移动ResourceWrapper(ResourceWrapper&& other) noexcept : resource(other.resource) {other.resource = nullptr;}ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {if (this != &other) {destroyResource(resource);resource = other.resource;other.resource = nullptr;}return *this;}// 访问底层资源Resource* get() const {return resource;}// 如果API经常被使用,可以提供便捷方法void use() {useResource(resource);}
};void raiiWrapperExample() {ResourceWrapper res; // 获取资源res.use(); // 使用资源// 资源在res离开作用域时自动释放
}
实际应用案例
RAII与线程同步
在多线程编程中,RAII可用于确保线程安全的资源管理:
#include <mutex>
#include <thread>
#include <vector>
#include <iostream>class ThreadSafeCounter {
private:mutable std::mutex mtx;int value;public:ThreadSafeCounter() : value(0) {}void increment() {std::lock_guard<std::mutex> lock(mtx); // RAII锁管理++value;}bool compare_exchange(int expected, int desired) {std::lock_guard<std::mutex> lock(mtx); // RAII锁管理if (value == expected) {value = desired;return true;}return false;}int get() const {std::lock_guard<std::mutex> lock(mtx); // RAII锁管理return value;}
};void threadSafeCounterExample() {ThreadSafeCounter counter;std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back([&counter]() {for (int j = 0; j < 1000; ++j) {counter.increment();}});}for (auto& t : threads) {t.join();}std::cout << "Final counter value: " << counter.get() << std::endl; // 应为10000
}
自定义内存池与RAII
结合RAII和自定义内存分配策略可以优化性能:
#include <iostream>
#include <vector>
#include <memory>class MemoryPool {
private:std::vector<char*> chunks;size_t chunkSize;char* currentChunk;size_t remainingBytes;public:explicit MemoryPool(size_t initialChunkSize = 4096) : chunkSize(initialChunkSize), currentChunk(nullptr), remainingBytes(0) {allocateChunk();}~MemoryPool() {for (auto chunk : chunks) {delete[] chunk;}}// 禁止复制MemoryPool(const MemoryPool&) = delete;MemoryPool& operator=(const MemoryPool&) = delete;// 分配内存void* allocate(size_t bytes) {// 对齐到8字节边界bytes = (bytes + 7) & ~7;if (bytes > remainingBytes) {if (bytes > chunkSize) {// 分配特大块char* bigChunk = new char[bytes];chunks.push_back(bigChunk);return bigChunk;} else {allocateChunk();}}char* result = currentChunk;currentChunk += bytes;remainingBytes -= bytes;return result;}// 释放单个对象不做任何事情,内存池管理整个块void deallocate(void*, size_t) {}private:void allocateChunk() {char* newChunk = new char[chunkSize];chunks.push_back(newChunk);currentChunk = newChunk;remainingBytes = chunkSize;}
};// 使用内存池的分配器
template<typename T>
class PoolAllocator {
public:using value_type = T;PoolAllocator(MemoryPool& pool) : pool_(pool) {}template<typename U>PoolAllocator(const PoolAllocator<U>& other) : pool_(other.pool_) {}T* allocate(size_t n) {return static_cast<T*>(pool_.allocate(n * sizeof(T)));}void deallocate(T* p, size_t n) {pool_.deallocate(p, n * sizeof(T));}MemoryPool& pool_;
};// RAII包装器,管理整个内存池生命周期
class PoolManager {
private:MemoryPool pool;public:explicit PoolManager(size_t chunkSize = 4096) : pool(chunkSize) {}// 创建使用此池的分配器template<typename T>PoolAllocator<T> makeAllocator() {return PoolAllocator<T>(pool);}
};struct MyObject {int data[25]; // 100字节MyObject() {for (int i = 0; i < 25; ++i) {data[i] = i;}}
};void memoryPoolExample() {PoolManager manager;// 创建使用内存池的vectorstd::vector<MyObject, PoolAllocator<MyObject>> objects(manager.makeAllocator<MyObject>());// 添加10000个对象for (int i = 0; i < 10000; ++i) {objects.emplace_back();}std::cout << "Created 10000 objects using memory pool" << std::endl;// 处理对象...// 离开作用域时,先销毁vector,然后PoolManager销毁内存池
}
资源获取与配置:游戏引擎示例
在游戏引擎中,RAII可用于管理资源加载和释放:
#include <string>
#include <unordered_map>
#include <memory>
#include <stdexcept>// 游戏资源基类
class Resource {
public:virtual ~Resource() = default;virtual void reload() = 0;
};// 纹理资源
class Texture : public Resource {
private:unsigned int textureId;std::string filename;public:Texture(const std::string& file) : filename(file) {// 加载纹理...std::cout << "Loading texture: " << filename << std::endl;textureId = loadTextureFromFile(filename);}~Texture() override {// 释放纹理...std::cout << "Releasing texture: " << filename << std::endl;unloadTexture(textureId);}void reload() override {// 重新加载纹理...unloadTexture(textureId);textureId = loadTextureFromFile(filename);}unsigned int getId() const {return textureId;}private:// 模拟纹理加载和卸载unsigned int loadTextureFromFile(const std::string& file) {// 实际中会读取文件并创建纹理static unsigned int nextId = 1;return nextId++;}void unloadTexture(unsigned int id) {// 实际中会释放纹理资源}
};// 声音资源
class Sound : public Resource {
private:unsigned int soundId;std::string filename;public:Sound(const std::string& file) : filename(file) {// 加载声音...std::cout << "Loading sound: " << filename << std::endl;soundId = loadSoundFromFile(filename);}~Sound() override {// 释放声音...std::cout << "Releasing sound: " << filename << std::endl;unloadSound(soundId);}void reload() override {// 重新加载声音...unloadSound(soundId);soundId = loadSoundFromFile(filename);}unsigned int getId() const {return soundId;}private:// 模拟声音加载和卸载unsigned int loadSoundFromFile(const std::string& file) {// 实际中会读取文件并创建声音static unsigned int nextId = 1000;return nextId++;}void unloadSound(unsigned int id) {// 实际中会释放声音资源}
};// 资源管理器
class ResourceManager {
private:std::unordered_map<std::string, std::shared_ptr<Resource>> resources;public:// 获取资源(如果不存在则加载)template<typename T>std::shared_ptr<T> getResource(const std::string& name) {auto it = resources.find(name);if (it != resources.end()) {// 资源已存在,尝试转换为请求的类型auto resource = std::dynamic_pointer_cast<T>(it->second);if (!resource) {throw std::runtime_error("Resource type mismatch: " + name);}return resource;} else {// 创建新资源auto resource = std::make_shared<T>(name);resources[name] = resource;return resource;}}// 重新加载所有资源void reloadAll() {for (auto& pair : resources) {pair.second->reload();}}
};// 游戏级别类
class Level {
private:ResourceManager& resourceManager;std::vector<std::shared_ptr<Texture>> textures;std::vector<std::shared_ptr<Sound>> sounds;public:Level(ResourceManager& manager, const std::string& levelFile) : resourceManager(manager) {// 加载关卡配置...std::cout << "Loading level: " << levelFile << std::endl;// 加载所需资源textures.push_back(resourceManager.getResource<Texture>("grass.png"));textures.push_back(resourceManager.getResource<Texture>("water.png"));sounds.push_back(resourceManager.getResource<Sound>("background.wav"));sounds.push_back(resourceManager.getResource<Sound>("effect.wav"));}void render() {// 渲染关卡...std::cout << "Rendering level with " << textures.size() << " textures" << std::endl;for (const auto& texture : textures) {std::cout << " Using texture ID: " << texture->getId() << std::endl;}}void playSound(size_t index) {if (index < sounds.size()) {std::cout << "Playing sound ID: " << sounds[index]->getId() << std::endl;}}
};// 游戏应用类
class GameApplication {
private:ResourceManager resourceManager;std::unique_ptr<Level> currentLevel;public:void loadLevel(const std::string& levelName) {// 创建新关卡(自动加载所需资源)currentLevel = std::make_unique<Level>(resourceManager, levelName);}void run() {std::cout << "Game running..." << std::endl;// 渲染当前关卡if (currentLevel) {currentLevel->render();currentLevel->playSound(0); // 播放背景音乐}}// 游戏结束时,所有资源会自动释放
};void gameEngineExample() {GameApplication game;// 加载关卡game.loadLevel("level1.map");// 运行游戏game.run();// 当game离开作用域时,所有资源(纹理、声音等)都会自动释放
}
RAII的最佳实践
尽早建立所有权语义
在设计资源管理类时,应尽早明确所有权语义:
- 独占所有权:一个对象独占资源,不允许复制,但可以转移所有权
- 共享所有权:多个对象共享资源,通常通过引用计数实现
- 非拥有引用:引用资源但不参与其生命周期管理
// 独占所有权
class UniqueOwner {
private:Resource* resource;public:UniqueOwner(Resource* r) : resource(r) {}~UniqueOwner() { delete resource; }// 禁止复制UniqueOwner(const UniqueOwner&) = delete;UniqueOwner& operator=(const UniqueOwner&) = delete;// 允许移动UniqueOwner(UniqueOwner&& other) noexcept : resource(other.resource) {other.resource = nullptr;}UniqueOwner& operator=(UniqueOwner&& other) noexcept {if (this != &other) {delete resource;resource = other.resource;other.resource = nullptr;}return *this;}
};// 共享所有权
class SharedOwner {
private:Resource* resource;int* refCount;void increment() {if (refCount) ++(*refCount);}void decrement() {if (refCount && --(*refCount) == 0) {delete resource;delete refCount;resource = nullptr;refCount = nullptr;}}public:SharedOwner(Resource* r) : resource(r), refCount(new int(1)) {}SharedOwner(const SharedOwner& other) : resource(other.resource), refCount(other.refCount) {increment();}SharedOwner& operator=(const SharedOwner& other) {if (this != &other) {decrement();resource = other.resource;refCount = other.refCount;increment();}return *this;}~SharedOwner() {decrement();}
};// 非拥有引用
class NonOwner {
private:Resource* resource; // 指向资源但不拥有public:NonOwner(Resource* r) : resource(r) {}// 可以自由复制NonOwner(const NonOwner&) = default;NonOwner& operator=(const NonOwner&) = default;// 析构函数不释放资源~NonOwner() {}
};
优先使用标准库组件
尽可能使用标准库提供的RAII组件,而不是自己实现:
// 不推荐:自定义资源管理
class MyFileHandler {
private:FILE* file;public:MyFileHandler(const char* filename, const char* mode) {file = fopen(filename, mode);if (!file) throw std::runtime_error("Failed to open file");}~MyFileHandler() {if (file) fclose(file);}// 禁止复制...
};// 推荐:使用标准库
void betterFileHandling() {std::ifstream file("example.txt");if (!file) throw std::runtime_error("Failed to open file");// 使用文件...
}
小心避免循环引用
使用智能指针时,特别是std::shared_ptr
,要小心避免循环引用:
class Node {
public:std::shared_ptr<Node> parent; // 问题:可能导致循环引用std::vector<std::shared_ptr<Node>> children;~Node() {std::cout << "Node destroyed" << std::endl;}
};void circularReferenceProblem() {auto node1 = std::make_shared<Node>();auto node2 = std::make_shared<Node>();node1->children.push_back(node2);node2->parent = node1; // 创建循环引用// 函数返回后,node1和node2的引用计数都不会归零,导致内存泄漏
}// 解决方案:使用weak_ptr
class BetterNode {
public:std::weak_ptr<BetterNode> parent; // 使用weak_ptr避免循环引用std::vector<std::shared_ptr<BetterNode>> children;~BetterNode() {std::cout << "BetterNode destroyed" << std::endl;}
};void circularReferenceFixed() {auto node1 = std::make_shared<BetterNode>();auto node2 = std::make_shared<BetterNode>();node1->children.push_back(node2);node2->parent = node1; // weak_ptr不增加引用计数// 函数返回后,两个节点都会被正确销毁
}
确保异常安全
RAII类应该确保在异常情况下也能正确释放资源:
class ExceptionSafeResource {
private:Resource* resource;bool initialized;void cleanup() {if (initialized && resource) {releaseResource(resource);resource = nullptr;initialized = false;}}public:ExceptionSafeResource(const std::string& name) : resource(nullptr), initialized(false) {try {resource = acquireResource(name);initialized = true;} catch (const std::exception& e) {cleanup(); // 确保失败时资源被释放throw; // 重新抛出异常}}~ExceptionSafeResource() {try {cleanup(); // 确保资源总是被释放} catch (...) {// 析构函数不应抛出异常,所以在这里捕获并静默处理std::cerr << "Error during resource cleanup" << std::endl;}}// 移动语义实现...
};
遵循"Rule of Zero"
尽可能使用标准库组件管理资源,让你的类满足"Rule of Zero":
// 遵循Rule of Zero的类
class ZeroClass {
private:std::string name; // 管理自己的内存std::unique_ptr<Resource> resource; // 自动管理资源生命周期std::vector<int> data; // 自动管理内存public:ZeroClass(const std::string& n) : name(n), resource(std::make_unique<Resource>(n)) {}// 无需自定义析构函数、复制函数或移动函数// 编译器会生成正确的行为
};
总结
RAII是C++中最重要的设计原则之一,它通过将资源获取与对象初始化绑定、将资源释放与对象销毁绑定,提供了一种简单而强大的资源管理机制。正确使用RAII可以有效避免资源泄漏,简化代码,提高程序的可靠性和安全性。
本文详细介绍了RAII的概念、实现方式和应用场景。我们探讨了如何设计良好的RAII类,包括所有权语义、复制/移动行为和异常安全性。我们还展示了标准库中的RAII组件,以及在实际应用中如何利用RAII解决资源管理问题。
记住,在C++中编写安全可靠的代码,RAII是你最强大的武器之一。无论是管理内存、文件句柄、锁还是其他资源,RAII都能帮助你以简洁、优雅的方式确保资源的正确使用和释放。
在下一篇文章中,我们将深入探讨智能指针的细节,这是C++标准库提供的最重要的RAII工具之一。
这是我C++学习之旅系列的第十九篇技术文章。查看完整系列目录了解更多内容。
相关文章:
C++学习:六个月从基础到就业——内存管理:RAII原则
C学习:六个月从基础到就业——内存管理:RAII原则 本文是我C学习之旅系列的第十九篇技术文章,也是第二阶段"C进阶特性"的第四篇,主要介绍C中的RAII原则及其在资源管理中的应用。查看完整系列目录了解更多内容。 引言 在…...
量子计算与经典计算融合:开启计算新时代
一、引言 随着科技的飞速发展,计算技术正迎来一场前所未有的变革。量子计算作为前沿技术,以其强大的并行计算能力和对复杂问题的高效处理能力,吸引了全球科技界的关注。然而,量子计算并非要完全取代经典计算,而是与经典…...
RV1126网络环境TFTPNFS搭建(二)
二、RV1126 开发板TFTP环境搭建 2.1、Ubuntu下安装和配置 xinetd 执行以下指令,安装 xinetd sudo apt-get install xinetd 执行以下指令创建一个 xinetd.conf 文件 sudo vi /etc/xinetd.conf 修改 xinetd.conf 文件内容如下: # Simple configurat…...
计算机视觉7——齐次坐标与相机内外参
一、透视投影 透视投影(Perspective Projection)是计算机视觉和图形学中描述三维物体在二维平面成像的基础模型,其核心思想是模拟人类视觉系统的成像原理——中心投影。具体而言,三维空间中的点通过一个固定的投影中心࿰…...
学习笔记—C++—string(一)
目录 string 为什么学习string的类 string类的常用接口 string类对象的常见构造 string类对象的访问及遍历操作 operator[] 迭代器 范围for auto 迭代器(二) string类对象的容量操作 size,length,max_size,capacity,clear基本用法 reserve 提…...
Linux命令-Shell编程
Shell是一个命令行解释器,它接收应用程序/用户命令,然后调用操作系统内核。 写一个hello.sh脚本: 1.mkdir scripts 2.cd scripts 3.touch hello.sh 4.vim hello.sh #!/bin/bash echo "hello,world" 5.bash hello.sh(…...
基于Django的AI客服租车分析系统
基于Django的AI客服租车分析系统 【包含内容】 【一】项目提供完整源代码及详细注释 【二】系统设计思路与实现说明 【三】AI智能客服与用户交互指导手册 【技术栈】 ①:系统环境:Python 3.8,Django 4.2框架 ②:开发环境&a…...
计算机组成与体系结构:计算机结构的分类(classifications of computer architecture)
目录 Von Neumann Architecture(冯诺依曼结构) Harvard Architecture(哈佛结构) Modified Harvard Architecture(改进哈佛结构) 三种结构对比总结表 💡 从“内存访问结构”角度分类&#x…...
在阿里云和树莓派上编写一个守护进程程序
目录 一、阿里云邮件守护进程 1. 安装必要库 2. 创建邮件发送脚本 mail_daemon.py 3. 设置后台运行 二、树莓派串口守护进程 1. 启用树莓派串口 2. 安装依赖库 3. 创建串口输出脚本 serial_daemon.py 4. 设置开机自启 5. 使用串口助手接收 一、阿里云邮件守护进程 1.…...
Redis 的几种数据类型
Redis 提供了多种数据类型,以支持不同的应用场景。每种数据类型都有其特定的操作方式,并且在内部实现上也有所优化,能够满足不同的业务需求。以下是 Redis 支持的几种常见数据类型: 1. 字符串(String) 描…...
Spring之我见 - Spring Boot Starter 自动装配原理
欢迎光临小站:致橡树 Spring Boot Starter 的核心设计理念是 约定优于配置,其核心实现基于 自动配置(Auto-Configuration) 和 条件化注册(Conditional Registration)。以下是其生效原理: 约定…...
LeRobot 项目部署运行逻辑(二)—— Mobile Aloha 真机部署
LeRobot 在开源项目中详细说明了部署流程,所以首先看一下开源的内容,然后再逐步拆解 首先,LeRobot 开源的硬件是配全部在 examples 文件夹中 包括了 Stretch 3、Aloha and Aloha 2 stationary、SO-100、LeKiwi、Moss v1 等机器人 恰好实验…...
大模型面经 | 介绍一下CLIP和BLIP
大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…...
Java发生OOM是否必然导致JVM退出
Java发生OOM是否必然导致JVM退出? 核心结论 不一定。OOM是否导致JVM退出取决于以下因素: OOM发生的区域JVM启动参数配置是否捕获了OOM异常 详细分析 1. 不同内存区域的OOM影响 内存区域错误类型默认是否导致JVM退出可恢复性Java堆OutOfMemoryError…...
Docker Compose 外部网络(`external: true`)与内部网络的区别
Docker Compose 外部网络(external: true)与内部网络的区别 在 Docker Compose 中,external: true 声明的外部网络与普通(内部)网络有重要区别,以下是它们的详细对比: 1. 定义与创建方式 特性外部网络 (external: true)内部网络 (默认)创建…...
【Android】Wallpaper学习
从wallpaper的设置来了解相关内容: 一,静态壁纸 静态壁纸设置的原理是在WallpaperManagerService里监听/data/system/users/0/wallpaper_orig相关文件的变化来触发设置,通过相应的组件程序去进行绘制, 相应的组件如:…...
Java基础-第一章、基本数据类型
运算符: 1.算术运算符:加减乘除%等 2.逻辑运算符:与或非等 3.关系运算符:大于、小于... 4.赋值运算符: 这里牵扯运算符的运算先后顺序了。 赋值运算的返回值:就是赋值的变量本身...
《Operating System Concepts》阅读笔记:p748-p748
《Operating System Concepts》学习第 64 天,p748-p748 总结,总计 1 页。 一、技术总结 1.Transmission Control Protocol(TCP) 重点是要自己能画出其过程,这里就不赘述了。 二、英语总结(生词:3) transfer, transport, tran…...
Arduino示例代码讲解:Project 08 - Digital Hourglass 数字沙漏
Arduino示例代码讲解:Project 08 - Digital Hourglass 数字沙漏 Project 08 - Digital Hourglass 数字沙漏程序功能概述功能:硬件要求:输出:代码结构全局变量`setup()` 函数`loop()` 函数计时和点亮LED:读取倾斜开关状态:重置LED和计时器:运行过程注意事项Project 08 - …...
报告总结笔记 | Jeff Dean ETH AI趋势 笔记:AI 的重要趋势:我们是如何走到今天的,我们现在能做什么,以及我们如何塑造 AI 的未来?
报告总结笔记 | Jeff Dean ETH AI趋势 笔记:AI 的重要趋势:我们是如何走到今天的,我们现在能做什么,以及我们如何塑造 AI 的未来? 2025年 4 月 14 日,Google Research 及 Google DeepMind 的首席科学家、A…...
RocketMQ实现基于可靠消息的最终一致性
RocketMQ实现基于可靠消息的最终一致性 文章目录 RocketMQ实现基于可靠消息的最终一致性一、RocketMQ应用场景**应用解耦****流量削峰****数据分发** 二、RocketMQ 基础概念1. 核心组件2. 消费模式3. 消息可靠性 三、消息类型按发送方式分同步发送异步发送单向发送 按使用功能特…...
【题解-Acwing】790. 数的三次方根
题目:790. 数的三次方根 题目描述 给定一个浮点数 n,求它的三次方根。 输入 共一行,包含一个浮点数 n 。 输出 共一行,包含一个浮点数,表示问题的解。 注意,结果保留 6 位小数。 数据范围 −10000 ≤ n ≤ 10000 时空限制 1s / 64MB 输入样例 1000.00输出样…...
一键升级OpenSSH/OpenSSL修复安全漏洞
在服务器安全运维过程中,我们经常面临这样的问题:收到高危漏洞通报(如最近的OpenSSH多个CVE漏洞),但Ubuntu系统无法通过apt直接升级到修复版本。这种情况下,传统方法需要手动编译源码,处理依赖关…...
Pycharm 如何删除某个 Python Interpreter
在PyCharm中,点击右下角的“Interpreter Settings”按钮,或者通过菜单栏选择“File” > “Settings”(macOS用户选择“PyCharm” > “Preferences”)。在设置窗口中,导航到“Project: [Your Project Name]” >…...
【C++】深入浅出之多态
目录 多态的概念多态的定义和实现多态的构造条件虚函数虚函数的重写虚函数重写的两个例外协变析构函数作为虚函数重写 C11的override和final重载、重写(覆盖)、隐藏(重定义)的对比相关面试题⭐ 抽象类概念接口继承和实现继承 多态的原理虚函数表多态的原理动态绑定和静态绑定 e…...
精益数据分析(9/126):如何筛选创业路上的关键数据指标
精益数据分析(9/126):如何筛选创业路上的关键数据指标 大家好!在创业的漫漫长路中,数据就像一盏明灯,指引着我们前行的方向。但要让这盏灯发挥作用,关键在于找到那些真正有价值的数据指标。今天…...
【Python爬虫详解】第二篇:HTML结构的基本分析
在上一篇文章中,我们介绍了网络爬虫的基本概念、发展历程和工作原理。要进行有效的网页内容爬取,首先需要理解我们要爬取的对象 —— 网页的基本结构和语法。网页本质上是由HTML代码构成的,爬虫程序需要从HTML中提取我们需要的信息。因此&…...
【C++】 —— 笔试刷题day_21
一、爱丽丝的人偶 题目解析 现在存在n个玩偶,每个玩偶的身高是1、2、3......n; 现在我们要对这些玩偶进行排序(如果x人偶,它左右两边的玩偶一个比x高、一个比x矮,那这个玩偶就会爆炸)。 我们不想要任何一个…...
云点数据读写
一、常见点云数据格式 LAS/LAZ格式 LAS是点云数据的行业标准格式 LAZ是LAS的压缩版本 支持地理参考信息、颜色、强度等属性 PCD格式(Point Cloud Data) PCL(Point Cloud Library)开发的格式 支持ASCII和二进制存储 包含头部信息和数据部分 PLY格式(Polygon File Format…...
Matlab 汽车行驶速度PID控制系统仿真
1、内容简介 Matlab 213-汽车行驶速度PID控制系统仿真 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
STM32嵌入式
一、创建工程项目 1、进入软件首页 2、新建项目,【file】->【new project】 3、选择需要的芯片 4、系统内核部分设置 ① 选择晶振(使用外部的高速晶振) ② 选择debug形式(SW类型) 5、时钟设置 6、选择自己需要的引脚设置&a…...
机器学习(神经网络基础篇)——个人理解篇6(概念+代码)
1 在声明一个类中,构建一个属于类的函数,前面为什要加上“self”? 就像下面这一串代码: class TwoLayerNet:def __init__(self, input_size, hidden_size, output_size,weight_init_std0.01):# 初始化权重self.params {}self.p…...
Java学习手册:Filter 和 Listener
在 JavaWeb 开发中,Filter(过滤器)和 Listener(监听器)是两个重要的技术组件,它们在处理客户端请求、管理应用状态和资源以及实现全局逻辑控制等方面发挥着关键作用。 一、Filter(过滤器&#…...
深度学习总结(25)
抽样偏倚问题 非代表性数据有一个特别隐蔽又特别常见的例子,那就是抽样偏倚(sampling bias)。如果你的数据收集过程与你尝试预测的目标之间存在相互影响,就会出现抽样偏倚,从而导致有偏差的结果。 理解数据 将数据…...
探索 Model Context Protocol (MCP):它如何影响 AI 的表现?
Anthropic 公司 Anthropic 是一家技术实力雄厚的公司,也是大模型领域的重要参与者之一。其开发的 **Claude 模型** 是全球首个以编程能力见长并广受欢迎的大语言模型。这款模型凭借卓越的代码生成和理解能力,迅速成为许多开发者工具的核心组件。例如&am…...
Three.js + React 实战系列-3D 个人主页 :完成 Navbar 导航栏组件
在上一节中,我们搭建了项目的基础结构,搭建好了项目框架。 本节我们将继续完善页面结构,完成第一个视觉组件 —— Navbar 导航栏 ✅ 前置准备: ✅下载静态资源在根目录下 (src 同级)谷歌云盘地址 🎥 02 完成 Navba…...
游戏引擎学习第238天:让 OpenGL 使用我们的屏幕坐标
回顾并为今天的内容做准备 我们已经完成了硬件显示的实现,现在通过GPU来显示游戏。原本以为这会花费很长时间,但结果实际所需的时间并不多。因此,我们现在有了进展,但接下来应该做什么还不确定。虽然有很多事情可以做,…...
go+mysql+cocos实现游戏搭建
盲目的学了一段时间了,刚开始从Box2d开始学习,明白了很多,Box2d是物理模型的基础,是我们在游戏中模拟现实的很重要的一个开源工具。后来在朋友的建议下学习了cocos,也是小程序开发的利器,而golang是一款高效…...
Linux 网络基础(二) (传输协议层:UDP、TCP)
目录 一、传输层的意义 二、端口号 1、五元组标识一个通信 2、端口号范围划分 3、知名端口号(Well-Know Port Number) (1)查看端口号 4、绑定端口号数目问题 5、pidof & netstat 命令 (1)ne…...
Vue常用指令入门
1. v-for 作用:用于遍历对象或数组 注意:需要提供key属性,可以提高性能和避免渲染错误,值通常为index或item.id <li v-for"(item, index) in items" :key"index">{{ item }} </li>2. v-if,v-el…...
【文献阅读】EndoNet A Deep Architecture for Recognition Tasks on Laparoscopic Videos
关于数据集的整理 Cholec80 胆囊切除手术视频数据集介绍 https://zhuanlan.zhihu.com/p/700024359 数据集信息 Cholec80 数据集 是一个针对内窥镜引导 下的胆囊切除手术视频流程识别数据集。数据集提供了每段视频中总共7种手术动作及总共7种手术工具的标注,标…...
UML统一建模
UML UML(统一建模语言)介绍 UML(统一建模语言)介绍 面向对象软件开发需要经过OOA面向对象分析、OOD面向对象设计和OOP面向对象编程三个阶段。OOA对目标系统进行分析并寄哪里分析模型,并将之文档化,OOD用面向…...
Ubuntu下安装和卸载MySQL
Ubuntu下安装和卸载MySQL 下面的演示系统版本:Ubuntu 24.04 更新系统软件包 在开始安装之前,建议先更新系统的软件包列表,以确保所有依赖项是最新的。 sudo apt update && sudo apt upgrade -y安装MySQL服务器 Ubuntu的官方软件…...
物联网技术赋能:复杂环境下的能源数据零丢失
安科瑞顾强 在全球能源挑战日益严峻的背景下,高效节能已成为各行业的核心诉求。无论是商业综合体、工业厂房还是公共设施,如何实现能源的精细化管理成为关键课题。安科瑞能耗云平台凭借其创新技术与多功能服务,为企业提供了一站式能源管理解…...
卷积神经网络综述
摘要 本文对卷积神经网络(Convolutional Neural Network,CNN)进行了全面综述。首先介绍了卷积神经网络的发展历程,包括早期的理论基础和关键突破。接着详细阐述了卷积神经网络的结构组成,包括卷积层、池化层、全连接层…...
SpringBoot3设置maven package直接打包成二进制可执行文件
注意事项 SpringBoot普通native打包顺序clean compile spring-boot:process-aot native:compile 使用以下配置只会的打包顺序clean package(注意:使用此配置以后打包会有编译后的class文件、jar包、original源文件、二进制可执行文件【Linux是无后缀的包…...
在 Anaconda 上安装多版本 Python 解释器并在 PyCharm 中配置
默认已安装好 Anaconda 和 PyCharm ,想在 Anaconda 上安装最新版本的 Python 解释器。 一、在 Anaconda 上创建虚拟环境 在连网状态下进入系统的命令提示符(快捷键:win r ,输入 cmd 即可),输入如下命令&a…...
AES (高级加密标准)
原理详解 AES是一种对称加密算法,使用相同的密钥进行加密和解密。它采用替代-置换网络(SPN)结构,主要步骤包括: 密钥扩展:从初始密钥派生多轮密钥 初始轮:AddRoundKey(轮密钥加) 主轮ÿ…...
Git拉分支技巧:从零开始创建并推送分支
Git拉分支技巧:从零开始创建并推送分支 在团队协作开发中,Git 分支管理是不可或缺的技能。合理地创建、同步和推送分支,不仅能提高开发效率,还能避免代码冲突。本文将基于以下技巧,详细讲解如何从零开始创建并推送一个…...
线性回归之归一化(normalization)
文章目录 归一化与梯度下降归一化的必要性:从特征量纲到梯度下降问题背景矛盾与低效归一化的作用 归一化提高模型精度的原因归一化的本质常见归一化方法最大值最小值归一化示例说明优缺点分析 标准归一化具体机制示例说明 强调 归一化与梯度下降 归一化与梯度下降 &…...