Starguard
开发笔记
Toggle navigation
Starguard
全部笔记
Unity
大话存储笔记
C语言
MongoDB
About Me
归档
标签
现代C++的高级概念
2022-02-23 14:16:24
37
0
0
admin
# 现代C++的高级概念 [TOC] ## 资源管理 * 资源包括: * 内存(栈或堆) * 访问硬盘或其他介质(如网络)上的文件所需的文件句柄 * 网络连接 * 线程、锁、定时器和事务 * 其他操作系统资源,如Windows的GDI(图形设备接)句柄 * 处理资源时,需要在正常和异常分支都合理地进行资源释放(例如delete释放new创建的对象),但异常分支变多时,很可能忘记,任何一个分支只要有一个忘记释放,就会导致资源泄漏,对于生命周期长或快速分配很多资源的进程,资源泄漏是很严重的问题,还可能导致存在拒绝服务(DOS)攻击漏洞。 * 一个解决方法就是在栈上创建对象,即不要创建对象的指针。 ### 资源申请即初始化 * 资源申请即初始化(Resource Acquisition is Initialization,RAII)是帮助我们安全处理资源的术语,该术语也被称为“构造时获得,析构时释放”,或者“基于范围的资源管理”。 * RAII利用类的构造函数和析构函数的对称性,我们可以在类的构造函数中分配资源,在析构函数中释放资源。下面的示例是一个适用于不同资源的简单模板类,不需要用户去new创建对象或delete释放对象。 被管理资源: ```C++ using namespace std; class Foo { public: void foo(); private: string name; }; void Foo::foo() { name = "foo"; cout << name << endl; } ``` 模板类实现的资源管理类: ```C++ template <typename RESTYPE> class ScopedResource final { public: ScopedResource() { managedResource = new RESTYPE(); } ~ScopedResource() { delete managedResource; } RESTYPE* operator->() const { return managedResource; } private: RESTYPE* managedResource; }; ``` 使用方法: ```C++ int main() { ScopedResource<Foo> res = ScopedResource<Foo>(); res->foo(); return 0; } ``` ### 智能指针 * 从C++11开始提供了智能指针,提供类似上面示例的功能,我们就不必重复造轮子了。智能指针减少了内存泄漏的可能性,而且是线程安全的。 #### 具有独占所有权的`std::unique_ptr<T>` * `std::unique_ptr<T>`定义在`<memory>`头文件中,一个对象一次只能由`std::unique_ptr<T>`的一个实例拥有,这是和`std::shared_ptr<T>`的主要区别。 * 超出`std::unique_ptr<T>`作用域时,能够安全地释放其所持有的T类型的实例,还支持move语义,可以很容易地放入容器中(不发生深拷贝)。 * 使用示例: ```C++ using FooPtr = std::unique_ptr<Foo>; using FooVector = std::vector<FooPtr>; int main() { FooPtr fooPtr { std::make_unique<Foo>() }; fooPtr->foo(); FooVector vec; vec.push_back(std::move(fooPtr)); vec[0]->foo(); // fooPtr->foo(); // 这里fooPtr不再管理资源,为空,即持有一个nullptr return 0; } ``` * 不要再使用`std::auto_ptr<T>`,C++11中已被标记为弃用,C++17中已删除,因为这个智能指针不支持rvalue引用和move语义,也不能存储到STL库的容器中,`std::unique_ptr<T>`是`std::auto_ptr<T>`的完美替代者。 * 不允许调用`std::unique_ptr<T>`的拷贝构造函数,但使用move语义可以将一个`std::unique_ptr<T>`持有的资源转移另一个`std::unique_ptr<T>`。 #### 具有共享所有权的`std::shared_ptr<T>` * 多个`std::shared_ptr<T>`可以管理同一个T类型对象,内部实现有一个引用计数器,监视当前有多少个`std::shared_ptr<T>`的实例,如果最后一个`std::shared_ptr<T>`实例被销毁,就会释放它持有的资源。 * 使用move语义也可以将一个对象从一个`std::shared_ptr<T>`实例“移动”到另一个`std::shared_ptr<T>`实例。同时相对`std::unique_ptr<T>`,`std::shared_ptr<T>`是可以拷贝的。 #### 无所有权但是能够安全访问的`std::weak_ptr<T>` * 使用`std::shared_ptr<T>`的get()成员函数可以获取原始指针(智能指针管理的指针),但要小心,因为如果`std::shared_ptr<T>`的最后一个实例在程序的某个地方被释放,这个原始指针仍然某个地方被使用,就成了野指针,没办法确定原始指针指向的地址是否有效。 * 可以通过`std::weak_ptr<T>`来包装`std::shared_ptr<T>`解决上面的问题,因为垃圾回收进程需要评估`std::shared_ptr<T>`的引用计数,`std::weak_ptr<T>`包装`std::shared_ptr<T>`导致`std::shared_ptr<T>`引用数不为0,生命周期被延长了。 * `std::weak_ptr<T>`不提供指针解引用操作符,仅用来“观察”它指向的资源,并检查资源是否有效,通常只在一段时间里将弱指针`std::weak_ptr<T>`提升为强指针(如`std::shared_ptr<T>`)。 * 使用示例: ```C++ void doSomething(const std::weak_ptr<Foo>& wp) { if (!wp.expired()) { // wp指向的对象是有效的 std::shared_ptr<Foo> sp = wp.lock(); sp->foo(); } else { cout << "wp is expired" << endl; } } void testWeakPtr() { auto sharedResource(std::make_shared<Foo>()); std::weak_ptr<Foo> weakPtr(sharedResource); doSomething(weakPtr); sharedResource.reset(); // 删除sharedResource指向的Foo实例 doSomething(weakPtr); } int main() { testWeakPtr(); return 0; } 打印结果: foo wp is expired ``` * `std::weak_ptr<T>`还可以用来解决循环依赖问题,例如类A包含类B的智能指针成员,类B包含类A的智能指针成员,导致即使类A、类B超出了作用域范,它们的智能指针成员引用计数也永远不会为0,对象不会释放内存,导致内存泄漏: ```C++ // 类型前置声明,防止类A中类B没有定义 class B; class A { public: void setB(std::shared_ptr<B>& b) { myB = b; } ~A() { cout << "release A" << endl; } private: std::shared_ptr<B> myB; // 替换为std::weak_ptr<B> myB;解决循环依赖问题 }; class B { public: void setA(std::shared_ptr<A>& a) { myA = a; } ~B() { cout << "release B" << endl; } private: std::shared_ptr<A> myA; // 替换为std::weak_ptr<A> myA;解决循环依赖问题 }; void testAB() { // 花括号建立作用域范围 { auto pa = std::make_shared<A>(); auto pb = std::make_shared<B>(); pa->setB(pb); pb->setA(pa); } // pa、pb已无法使用了,但A、B又没有被释放 } int main() { testAB(); return 0; } ``` ### 避免显式的new和delete * 使用new、delete会增加代码的复杂度,必须处理异常情形、非默认情形或需要特别处理的情形。 * 可以通过以下方式避免显式的new和delete: * 尽可能使用栈内存。 * 用`make_functions`在堆上分配资源。用`std::make_unique<T>`或`std::make_shared<T>`实例化资源,然后将它包装成一个资源管理对象去管理资源及智能指针。 * 尽量使用容器(标准库、Boost)。容器会对其元素进行存储空间的管理,自己开发数据结构或序列化容器必须自己实现所有的内存管理细节。 * 如果有特殊的内存管理,利用特有的第三方库封装资源。 ### 管理特有资源 * C++中不允许定义`std::unique_ptr<void>`类型,因为`std::shared_ptr<T>`实现了类型删除,但`std::unique_ptr<T>`没有。如果一个类支持类型删除,就意味着它可以存储任意类型的对象,而且会正确地释放对象占用的内存。 * 除了指针,也可以用智能指针管理特有资源,例如代表windows中的进程的HANDLE句柄`typedef void *HANDLE`,不用智能指针,手动管理时: ```C++ #include <windows.h> const DWORD processId = 4711; HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId); // 使用句柄... // 用完后释放句柄 BOOL success = CloseHandle(processHandle); ``` * 使用智能指针管理: ```C++ #include <windows.h> class Win32HandleCloser { public: void operator()(HANDLE handle) const { if (handle != INVALID_HANDLE_VALUE) { CloseHandle(handle); } } }; using Win32SharedHandle = std::shared_ptr<void>; using Win32WeakHandle = std::weak_ptr<void>; int main() { const DWORD processId = 4711; Win32SharedHandle processHandle { OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId), Win32HandlerCloser() }; } ``` ## Move语义 * C++11之前,还没有出现Move语义,很多时候必须使用拷贝,例如: * 局部变量作为函数或方法的返回值会发生拷贝构造,C++11之前经常利用指针解决这类问题 * 向`std::vector`或者其他容器插入一个对象时 * `std::swap<T>`模板函数的实现 * 相对于拷贝运算,通常move操作符的效率要高,原对象的数据只是传递给了目标对象,而原对象被置于一种“空”或者原始的状态。 * 要遵循小心优化原则,不要滥用move语义,示例: ```C++ using StringVector = std::vector<std::string>; StringVector createVectorOfStrings() { StringVector result; // ... return std::move(result); // 没有必要,return result即可 } ``` * 上面的例子返回值没有必要显式地使用move语义,编译器会自行判断变量返回值,并将其move返回(从C++11开始所有标准库容器都支持move语义,很多其他标准库的类也支持,如std::string),更糟糕的是这样可能会干扰RVO(Return Value Optimization),使编译器无法正确执行其优化策略,而且大篇幅的`std::move<T>()`还会影响代码的可读性。 ### 零原则 * 三大规则是说一个类需显式定义析构函数、拷贝构造函数和赋值构造函数,随着C++11的出现,因为move构造器和move赋值运算符被加入C++语言,三大规则被扩展成五大规则。如果违反了五大规则,例如没有定义copy/move构造器,编译器会生成默认的copy/move构造器,默认的实现只会创建一个源对象的浅拷贝,这就意味着调用默认的拷贝构造函数后,两个实例指向同一个内存,如果对象被销毁,会导致内存的数据被双重删除。 * 一个非默认的析构函数通常需要释放资源,例如堆上的内存,这样导致的结果是,为了让资源能正确地拷贝或者move,就需要同时显式地定义copy/move构造器和copy/move操作运算符,这就是五大规则的含义。 * C++11之后也可以通过删除这些特殊的成员函数,但这样会使这个类的使用范围受限制,例如`std::vector`需要T元素类型是可赋值拷贝的和可构造拷贝的。 * 如果我们可以摆脱释放资源的析构函数,就没有必要去显式地提供其他的特殊成员函数,编译器默认生成的特殊成员函数可以正确地执行,就代表我们遵循了KISS原则(Keep It Simple and Stupid),也是我们所说的零原则。 * 零原则:在实现你的类的时候,应该不需要声明/定义析构函数,也不需要声明/定义copy/move构造器和copy/move赋值运算符,用C++智能指针和标准库类来管理资源。 ## 编译器是你的搭档 ### 自动类型推导 #### auto关键字 * 当类型很长时,使用auto关键字可以提高代码的可读性: ```C++ // 不使用auto std::shared_ptr<controller::CreateMonthlyInvoicesController> createMonthlyInvoicesController = std::make_shared<controller::CreateMonthlyInvoicesController>(); // 使用auto auto createMonthlyInvoicesController = std::make_shared<controller::CreateMonthlyInvoicesController>(); ``` * C++14之后,支持函数的返回值自动推导: ```C++ auto function() { std::vector<std::map<std::pair<int, double>, int>> returnValue; // ... return returnValue; } ``` #### 初始化表达式列表 * C++11以前,初始化一个STL容器: ```bash std::vector<int> integerSequence; integerSequence.push_back(14); integerSequence.push_back(33); integerSequence.push_back(69); // 不断地添加 ``` * 使用C++11时,只需要这样写,原因是`std::vector<T>`重载了它的构造函数,能够接收初始化表达式列表作为参数,初始化列表的类型是`std::initializer_list<T>`: ```bash std::vector<int> integerSequence { 14, 33, 69 }; ``` * 写一个构造函数接受初始化表达式列表的类: ```C++ #include <string> #include <vector> using WordList = std::vector<std::string>; class LexicalRepository { public: explicit LexicalRepository(const std::initializer_list<const char*>& words) { wordList.insert(begin(wordList), begin(words), end(words)); } private: WordList wordList; }; int main() { LexicalRepository repo { "hello", "world", "programmer" }; // ... return 0; } ``` ### 编译时计算 * 在编译时进行运算是提高程序运行效率最简单的手段。 ### 模板变量
上一篇:
2023年9月23日
下一篇:
C++代码整洁的基本规范
0
赞
37 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
文档导航