当前位置: 首页 > news >正文

广州网站建设知名 乐云践新网页界面制作

广州网站建设知名 乐云践新,网页界面制作,wordpress主体怎么用,深圳百度推广电话1、到目前为止#xff0c;我们编写的程序中 所使用的对象 都有着严格定义的生存期。全局对象 在程序启动时分配#xff0c;在程序结束时 销毁。对于 局部自动对象#xff0c;当我们进入 其定义所在的程序块时被创建#xff0c;在 离开块时销毁。局部static对象 在第一次使用…1、到目前为止我们编写的程序中 所使用的对象 都有着严格定义的生存期。全局对象 在程序启动时分配在程序结束时 销毁。对于 局部自动对象当我们进入 其定义所在的程序块时被创建在 离开块时销毁。局部static对象 在第一次使用前分配在程序结束时销毁 2、局部static变量 是指 在函数内部声明的静态变量。这种变量 具有局部作用域但其生命周期 从声明开始 直到程序结束。这意味着即使函数 执行完毕局部static变量的值 也不会消失而是 保持上次函数调用结束时的值。在下一次 调用同一函数时局部static变量 将使用上次留下的值而不是 重新初始化 使用局部static变量的好处包括 1保持 函数内变量的状态无需 使用外部变量 2避免 变量的频繁创建 和 销毁提高性能尤其是对于复杂的对象 3实现单例模式等 设计模式时局部static变量非常有用 #include iostreamvoid counter() {static int count 0; // 局部static变量count;std::cout 当前计数: count std::endl; }int main() {counter(); // 输出: 当前计数: 1counter(); // 输出: 当前计数: 2counter(); // 输出: 当前计数: 3return 0; }在这个例子中每次调用 counter函数时count变量 都会递增。由于count是 局部static变量它不会在 每次调用counter函数时重新初始化而是 保留上一次调用结束时的值 如果局部static变量 没有显式初始化它将 被自动初始化为零对于基本数据类型 在多线程环境中局部static变量的初始化 可能需要特别注意以确保 线程安全。C11及以后的标准中局部static变量的初始化 是线程安全的但在此之前的老版本中可能不是 3、除了 自动和static对象外C 还支持 动态分配对象。动态分配的对象的生存期 与 它们在哪里创建是无关的只有 当显式地被释放时这些对象才会销毁 动态对象的正确释放 被证明是 编程中极其容易出错的地方。为了 更安全地使用动态对象标准库 定义了 两个智能指针类型来管理动态分配的对象。当一个对象 应该被释放时指向它的智能指针 可以确保自动地释放它 4、我们的程序 到目前为止 只使用过 静态内存 或 栈内存 静态内存 用来保存 局部static对象、类static数据成员 以及 定义在任何函数之外的变量以及 常量 栈内存 用来保存 定义在函数内的非static对象用于存储函数调用时的局部变量和函数的参数。当函数被调用时系统会为函数的局部变量分配内存空间当函数执行结束时这些内存空间会被自动释放 分配在 静态 或 栈内存中的对象 由编译器自动创建和销毁。对于 栈对象仅在其定义的程序块运行时 才存在static对象 在使用之前分配在程序结束时 销毁 静态内存具有以下特点 1生命周期长静态内存的生命周期从程序启动到程序结束变量的值在整个程序运行期间保持不变 2作用域广静态内存中的数据 可以在程序的任何地方访问具有全局性 3一次分配多次使用静态内存 只需分配一次之后可以 多次读取和写入 栈内存的特点包括 1后进先出栈内存采用后进先出LIFO的存储方式最后进入的数据首先被取出 2生命周期短栈内存中的数据的生命周期 随着函数的调用和结束 而动态变化局部变量的生命周期 仅限于函数的执行期间 除了 静态内存和栈内存每个程序 还拥有一个内存池。这部分内存 被称作自由空间 或 堆。程序 用堆来 存储动态分配的对象即那些在程序运行时 分配的对象。动态对象的生存期 由程序来控制也就是说当动态对象 不再使用时我们的代码 必须显式地销毁它们 1、动态内存 和 智能指针 1、动态内存的管理 是通过 一对运算符来完成的 new在动态内存中 为对象分配空间 并返回一个指向该对象的指针我们可以选择 对对象进行初始化delete, 接受 一个动态对象的指针销毁该对象并释放 与之关联的内存 2、动态内存的使用很容易出问题因为确保 在正确的时间 释放内存是极其困难的。有时 会忘记释放内存在这种情况下 就会产生内存泄漏有时在 尚有指针引用内存的情况下 我们就释放了它在这种情况下 就会产生 引用非法内存的指针 3、为了更容易同时也更安全地使用动态内存新的标准库 提供了两种智能指针类型 来管理动态对象。智能指针的行为 类似常规指针重要的区别是 它负责自动释放 所指向的对象。新标准库 提供的这两种智能指针的区别 在于管理底层指针的方式shared_ptr 允许多个指针指向同一个对象unique_ptr 则 “独占” 所指向的对象 标准库还定义了 一个名为 weak_ptr 的伴随类它是一种弱引用指向 shared_ptr 所管理的对象 这三种类型 都定义在memory头文件中 1.1 shared_ptr类 1、智能指针也是模板。因此当我们 创建一个智能指针时必须提供额外的信息——指针可以指向的类型。与vector一样我们在尖括号内 给出类型之后是 所定义的这种智能指针的名字 shared_ptrstring p1; // shared_ptr可以指向string shared_ptrlistint p2; // shared_ptr可以指向int的list默认初始化的智能指针 中保存着 一个空指针 智能指针的使用方式 与普通指针类似。解引用一个智能指针 返回它指向的对象。如果 在一个条件判断中 使用智能指针效果就是 检测它是否为空 // 如果p1不为空检查 它是否指向一个空string if (p1 pl-empty())*p1 hi; // 如果p1指向一个空string解引用p1将一个新值 赋予string2、shared_ptr 和 unique_ptr 都支持的操作 操作解释shared_ptrT sp, unique_ptrT up空智能指针可以指向类型为T的对象p将p 用作一个条件判断若p 指向一个对象则为true*p解引用p获得它指向的对象p-mem等价于(*p).memp.get()返回 p中保存的 原始指针。要小心使用若智能指针 释放了其对象返回的指针 所指向的对象 也就消失了swap(p, q), p.swap(q)交换p和q中的指针 p.get() 用法 #include iostream #include memoryint main() {// 创建一个 shared_ptr指向动态分配的整型对象std::shared_ptrint ptr(new int(42));// 使用 .get() 方法获取指向被 shared_ptr 管理对象的原始指针int *rawPtr ptr.get();// 打印原始指针的值std::cout 原始指针的值: rawPtr std::endl;// 注意不要使用原始指针释放内存// 不要手动 delete rawPtr;return 0; }shared_ptr 独有的操作 操作解释make_sharedT (args)返回一个shared_ptr指向 一个动态分配的 类型为T的对象。使用args 初始化此对象shared_ptrT p(q)p是 shared_ptr q 的拷贝此操作 会递增q中的计数器。q中的指针 必须能转换为Tpqp和q 都是shared_ptr所保存的指针 必须能相互转换。此操作会递减p的引用计数递增q的引用计数若p的引用计数变为0则将其管理的原内存释放p.unique()若p.use_count() 为1返回true否则返回falsep.use_count()返回与p共享对象的智能指针数量可能很慢主要用于调试 3、make_shared函数最安全的分配 和 使用动态内存的方法 是调用一个名为 make_shared 的标准库函数 此函数 在动态内存中 分配一个对象 并初始化它返回 指向此对象的shared_ptr。与智能指针一样make_shared 也定义在头文件memory中 当要用 make_shared 时必须指定 想要创建的对象的类型。定义方式 与 模板类相同在函数名之后 跟一个尖括号在其中给出类型 // 指向一个值为42的int的shared_ptr shared_ptrint p3 make_sharedint(42); // p4指向一个值为9999999999的string shared_ptrstring p4 make_sharedstring(10, 9); // p5指向一个值初始化的int即值为0 shared_ptrint p5 make_sharedint();类似顺序容器的emplace成员make_shared 用其参数 来构造给定类型的对象 如果我们不传递任何参数对象就会进行 值初始化 通常用auto 定义一个对象 来保存 make_shared 的结果 // p6指向 一个动态分配的空vectorstring auto p6 make_sharedvectorstring();4、当进行 拷贝或赋值操作 时每个 shared_ptr 都会记录有多少个 其他 shared_ptr 指向相同的对象 auto p make_sharedint(42); // p指向的对象 只有p一个引用者 auto q(p); // p和q指向相同对象此对象有两个引用者可以认为 每个 shared_ptr 都有一个关联的计数器通常称其为引用计数 无论何时 我们拷贝一个 shared_ptr计数器都会递增。例如当用一个 shared_ptr 初始化另一个shared_ptr或 将它作为参数传递给一个函数 以及 作为函数的返回值 时它所关联的计数器 就会递增 当 给shared_ptr 赋予一个新值 或是 shared_ptr 被销毁例如 局部的 shared_ptr 离开其作用域时计数器就会递减 下面是一个示例演示了当 shared_ptr 作为函数的返回值时关联的计数器是如何递增的 #include iostream #include memorystd::shared_ptrint createSharedPtr() {std::shared_ptrint ptr(new int(42));std::cout 创建 shared_ptr计数器 ptr.use_count() std::endl;return ptr; }int main() {// 调用函数创建 shared_ptr并接收返回值std::shared_ptrint returnedPtr createSharedPtr();// 打印返回的 shared_ptr 的计数器值std::cout 返回的 shared_ptr计数器 returnedPtr.use_count() std::endl;return 0; }当运行这个程序时你会看到如下输出 创建 shared_ptr计数器1 返回的 shared_ptr计数器1 可以看到创建 shared_ptr 的函数中计数器值为 1而返回的 shared_ptr 的计数器值为 1。这说明了当 shared_ptr 作为函数返回值时关联的计数器会递增本来 ptr 都销毁了应该为0了以确保内存中的对象在 至少有一个 shared_ptr 指向它时不会被销毁 就算函数 换成 std::shared_ptrint createSharedPtr() {return std::make_sharedint(42); }结果还是返回的 shared_ptr计数器1 一旦 一个shared_ptr的计数器变为0它就会 自动释放自己所管理的对象 auto r make_sharedint(42); // r指向的int只有一个引用者 r q; // 给r赋值令它指向另一个地址// 递增q指向的对象的引用计数// 递减r原来指向的对象的引用计数// r原来指向的对象 已没有引用者会自动释放分配了一个int将其指针保存在r中 到底是 用一个计数器 还是 其他数据结构 来记录有多少指针共享对象完全由标准库的具体实现 来决定。关键是 智能指针类 能记录有多少个 shared_ptr 指向相同的对象并 能在恰当的时候 自动释放对象 5、shared_ptr 自动销毁 所管理的对象它是 通过另一个特殊的成员函数 —— 析构函数 完成销毁工作的。类似于 构造函数每个类 都有一个 析构函数。就像 构造函数控制初始化一样析构函数 控制此类型的对象销毁时 做什么操作 析构函数 一般用来 释放对象所分配的资源。例如string的构造函数 会分配内存 来保存构成string的字符。string的析构函数 就负责释放 这些内存 shared_ptr 的析构函数 会递减它所指向的对象的引用计数。如果 引用计数变为0shared_ptr 的析构函数 就会销毁对象并释放 它占用的内存 6、shared_ptr 还会自动释放相关联的内存当动态对象 不再被使用时shared_ptr 类 会自动释放动态对象这一特性使得 动态内存的使用 变得非常容易 例如我们可能有一个函数它返回一个shared ptr指向 一个Foo类型的动态分配的对象对象 是通过一个类型为T的参数进行初始化的 // factory返回一个shared_ptr指向 一个动态分配的对象 shared_ptrFoo factory(T arg) {// 恰当地处理arg// shared_ptr负责释放内存return make_sharedFoo(arg); }由于factory 返回一个shared_ptr所以 我们可以确保 它分配的对象会 在恰当的时刻被释放。例如下面的函数 将factory返回的shared_ptr 保存在局部变量中 void use_factory(T arg) {shared_ptrFoo p factory(arg);// 使用p } // p离开了作用域它指向的内存 会被自动释放掉当p 被销毁时将递减 其引用计数 并检查它是否为0。在此例中p是 唯一引用factory返回的 内存的对象。由于p将要销毁p指向的 这个对象也会被销毁所占用的内存 会被释放 void use_factory(T arg) {shared_ptrFoo p factory(arg);// 使用preturn p; // 当我们返回p时引用计数进行了递增操作 } // p离开了作用域但它指向的内存不会被释放掉拷贝一个 shared_ptr 会增加 所管理对象的引用计数值。现在 当p被销毁时它所指向的内存 还有其他使用者。对于一块内存shared_ptr 类 保证只要有任何 shared_ptr对象 引用它它就不会 被释放掉 由于在 最后一个shared_ptr销毁前 内存都不会释放保证shared_ptr在无用之后 不再保留就非常重要了。如果 你忘记了销毁程序 不再需要的 shared_ptr程序仍会正确执行但会浪费内存。share_ptr 在无用之后 仍然保留的一种可能情况是你将shared_ ptr 存放在一个容器中随后 重排了容器从而 不再需要某些元素。在这种情况下你应该 确保用erase删除那些不再需要的shared_ptr元素 #include iostream #include vector #include memory #include algorithmint main() {// 创建一个存放 std::shared_ptr 的容器std::vectorstd::shared_ptrint ptrContainer;// 向容器中添加一些 std::shared_ptrptrContainer.push_back(std::make_sharedint(1));ptrContainer.push_back(std::make_sharedint(2));ptrContainer.push_back(std::make_sharedint(3));// 在这里对容器进行重排或者其他操作// 假设在这之后不再需要第一个指针ptrContainer.erase(ptrContainer.begin()); // 删除第一个元素// 假设在这之后不再需要第三个指针auto it std::find(ptrContainer.begin(), ptrContainer.end(), nullptr); // 找到需要删除的指针if (it ! ptrContainer.end()) {ptrContainer.erase(it); // 删除指定元素}// 在这之后确保容器中存放的都是仍然需要的指针return 0; }如果你将 shared_ptr 存放于一个容器中而后 不再需要全部元素而只使用 其中一部分要记得 用erase删除不再需要的那些元素 7、使用了动态生存期的资源的类程序使用动态内存 出于以下三种原因之一 1程序不知道 自己需要使用多少对象 2程序不知道 所需对象的准确类型 3程序需要 在多个对象间 共享数据 容器类是出于第一种原因而使用动态内存的典型例子 8、到目前为止我们使用过的类中分配的资源 都与对应对象生存期一致。例如每个vector “拥有” 其自己的元素。当我们拷贝 一个vector时原vector 和 副本vector中的元素 是相互分离的 vectorstring v1; // 空vector { // 新作用城vectorstring v2 {a, an, the};v1 v2; // 从v2拷贝元素到v1中 } // v2被销毁其中的元素也被销毁 // v1有三个元素是原来v2中元素的拷贝由一个vector分配的元素 只有当这个vector存在时 才存在。当一个vector 被销毁时这个vector中的元素 也都被销毁 但某些类分配的资源 具有 与原对象 相独立的生存期。例如假定我们希望定义一个名为Blob的类保存一组元素。与容器不同我们希望Blob对象的不同拷贝之间 共享相同的元素。即当我们 拷贝一个Blob时原Blob对象 及其拷贝 应该引用相同的底层元素 一般而言如果 两个对象 共享底层的数据当某个对象 被销毁时我们 不能单方面地销毁底层数据 Blobstring b1; // 空Blob { // 新作用城Blobstring b2 {a, an, the};b1 b2; // b1和b2共享相同的元素 } // b2被销毁了但b2中的元素不能销毁// b1指向最初由b2创建的元素在此例中b1和b2 共享相同的元素。当b2离开作用域时这些元素必须保留因为b1仍然 在使用它们 9、定义 StrBlob 类先定义一个管理string的类此版本命名为 StrBlob 实现 一个新的集合类型的 最简单方法是 使用某个标准库容器来管理元素。采用这种方法我们 可以借助标准库类型 来管理元素所使用的内存空间。在本例中我们 将使用vector来保存元素 不能 在一个Blob对象内 直接保存vector因为 一个对象的成员 在对象销毁时 也会被销毁。例如假定b1和b2是 两个Blob对象共享相同的vector。如果此 vector保存在 其中一个Blob中——例如b2中那么 当b2离开作用域时此vector也将被销毁也就是说 其中的元素都将不复存在。为了保证vector中的元素继续存在我们 将vector保存在动态内存中 为了实现 我们所希望的数据共享我们为每个 StrBlob 设置一个 shared_ptr 来管理动态分配的vector。此 shared_ptr 的成员将记录 有多少个StrBlob共享相同的vector并在 vector的最后一个使用者被销毁时 释放vector 将实现一个vector操作的小的子集。我们 会修改访问元素的操作如front 和 back如果用户试图 访问不存在的元素这些操作会抛出一个异常 类有一个默认构造函数和一个构造函数接受单一的 initializer_liststring 类型参数此构造函数可以接受一个初始化器的花括号列表 StrBlob构造函数两个构造函数 都使用初始化列表 来初始化其data成员令它 指向一个动态分配的vector。默认构造函数 分配一个空vector StrBlob::StrBlob(): data(make_sharedvectorstring()) StrBlob::StrBlob(initializer_liststring il):data(make_sharedvectorstring(il)) { }接受 一个initializer_list的构造函数 将其参数传递给对应的vector构造函数。此构造函数 通过拷贝列表中的值 来初始化vector的元素 std::initializer_list 是 C11 引入的一种新特性它允许你用花括号 {} 初始化列表的方式来初始化对象。std::initializer_list 是一个模板类用于表示一个特定类型 T 的值的数组 std::initializer_list 常用于构造函数和函数参数使得函数可以接受任意数量的参数只要这些参数是同一类型的。这对于初始化容器类如 std::vector、std::array、std::map 等非常有用 #include iostream #include string #include initializer_listclass StringList { public:StringList(std::initializer_liststd::string initList) {for (const auto str : initList) {std::cout str std::endl;}} };int main() {// 使用 initializer_list 来初始化 StringList 对象StringList list{Hello, World, Initializer, List};return 0; } 特点 1自动推导类型你不需要指定列表中元素的数量编译器会自动根据初始化列表中的元素数量来推导 2只读访问通过 std::initializer_list 提供的元素只能进行只读访问。它提供的迭代器是常量迭代器这意味着你不能修改列表中的元素 3生命周期std::initializer_list 对象的生命周期通常很短它只是临时存储和传递初始化值的一种手段。因此它适合用于初始化但不适合用作存储容器 元素访问成员函数定义了一个名为check的 private工具函数它检查 一个给定索引是否在合法范围内。除了索引check还接受一个string参数它会 将此参数传递给异常处理程序这个string 描述了错误内容 pop.back 和 元素访问成员函数 首先调用check。如果check成功这些成员函数 继续利用底层vector的操作 来完成自己的工作 front和back应该对const进行重载 在C中使用const关键字 可以实现 对成员函数的重载这允许你 根据对象是否为常量 来调用不同的函数实现。这种技术常用于提供对常量和非常量对象的不同操作确保对常量对象的访问不会修改其状态 下面是一个使用const重载成员函数的例子 class MyClass { public:void display() const {// 对于const对象调用的版本std::cout Display const std::endl;}void display() {// 对于非const对象调用的版本std::cout Display non-const std::endl;} };在这个例子中display函数 被重载了两次一次是 带有const修饰的一次是 不带const的。当你尝试 在常量对象上调用display时编译器 会选择带有const修饰的版本而在 非常量对象上调用时会选择 不带const的版本 int main() {MyClass obj;const MyClass cObj;obj.display(); // 调用非const版本cObj.display(); // 调用const版本return 0; }通过合理使用const重载你可以提高代码的安全性和灵活性确保在适当的上下文中以正确的方式访问对象 StrBlob的拷贝、赋值和销毁拷贝一个 shared_ptr 会递增 其引用计数将一个 shared_ptr 赋予另一个 shared_ptr 会递增赋值号右侧 shared_ptr 的引用计数而递减左侧 shared_ptr 的引用计数。如果一个 shared_ptr 的引用计数变为0它所指向的对象 会被自动销毁。因此对于 由StrBlob构造函数分配的vector当最后一个指向它的 StrBlob对象 被销毁时它会 随之被自动销毁 实现 StrBlob.h #pragma once #ifndef STRBLOB_H #define STRBLOB_H#include string #include vector #include iostream #include memory #include initializer_list #include stdexceptclass StrBlob { public:typedef std::vectorstd::string::size_type size_type;StrBlob() :data(std::make_sharedstd::vectorstd::string()) {};StrBlob(std::initializer_liststd::string il);size_type size() const { return data-size(); } // data是指针bool empty() const { return data-empty(); }// 添加删除元素void push_back(const std::string t) { data-push_back(t); }void pop_back(); // 需要检查了// 元素访问std::string front();std::string front() const;std::string back();std::string back() const;private:std::shared_ptrstd::vectorstd::string data;void check(size_type i, const std::string msg) const; };StrBlob::StrBlob(std::initializer_liststd::string il):data(std::make_sharedstd::vectorstd::string(il)) {} // 构造函数也要StrBlob::void StrBlob::check(size_type i, const std::string msg) const{ // 实现的时候也需要加constif (i data-size())throw std::out_of_range(msg); }void StrBlob::pop_back() {check(0, pop_back on empty StrBlob);data-pop_back(); }std::string StrBlob::front() {check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::front() const{ // 对const进行重载check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::back() {check(0, back on empty StrBlob);return data-back(); }std::string StrBlob::back() const {check(0, back on empty StrBlob);return data-back(); }#endif12.2.cpp #include StrBlob.h #include iostreamint main() {StrBlob b1({ a, an, the });const StrBlob b2 { a, b, c };std::cout b1.back() std::endl;std::cout b2.back() std::endl;return 0; }为什么在initial_list的参数里不能直接加引用需要加const 引用 StrBlob(const std::initializer_liststd::string il); StrBlob::StrBlob(const std::initializer_liststd::string il):data(std::make_sharedstd::vectorstd::string(il)) {}在C中std::initializer_list 通常被设计为一种轻量级容器用于初始化列表的传递和访问。它的设计初衷是为了实现轻量级的、不可变的列表即元素都是 const 类型 这就导致了在初始化列表的参数中只添加引用是无效的 10、在此代码的结尾b1 和 b2 各包含多少个元素注意第二行 不是两个 shared_ptrvectorstring 赋值所以b1减b2增不成立 StrBlob b1; {StrBlob b2 {a, an, the};b1 b2;b2.push_back(about); }代码第 3 行创建 b2 时提供了 3 个 string 的列表因此会创建一个包含 3 个 string 的 vector 对象并创建一个 shared_ptr 指向此对象引用计数为 1 第 4 行将 b2 赋予 b1 时创建一个 shared_ptr 也指向刚才创建的 vector 对象引用计数变为 2 因此第 4 行向 b2 添加一个 string 时会向两个 StrBlob 共享的 vector 中添加此 string。最终在代码结尾b1 和 b2 均包含 4 个 string 右花括号结束b2 销毁b1 仍有效包含 4 个 string 11、StrBlob 需要const 版本的push_back 和 pop_back吗 通常情况下push_back 和 pop_back 这样的函数用于修改对象的状态因此它们通常不应该是 const 成员函数。const 成员函数声明了不修改对象状态的保证因此在设计上不应该将修改对象状态的操作放在 const 成员函数中 如果你的设计中希望在对象是 const 的情况下也能够修改对象的状态那么可以提供 const 版本的 push_back 和 pop_back。这种情况通常较少见而且需要特别小心因为它违反了常规的对象语义 12、在我们的 check 函数中没有检查 i 是否大于0。为什么可以忽略这个检查 因为 vectorstring::size_type 是一个unsigned任何小于0的数 传进来 就会转成大于0的数 13、我们未编写接受一个 initializer_list explicit 参数的构造函数。讨论这个设计策略的优点和缺点 使用explicit之后 优点我们可以清楚地知道使用的是哪种类型 缺点不易使用需要显式地初始化 未编写接受一个初始化列表参数的显式构造函数意味着可以进行列表向 StrBlob 的隐式类型转换亦即在需要 StrBlob 的地方如函数的参数可以使用列表进行代替。而且可以进行拷贝形式的初始化如赋值。这令程序编写更为简单方便 但这种隐式转换并不总是好的。例如列表中可能并非都是合法的值。再如对于接受 StrBlob 的函数传递给它一个列表会创建一个临时的 StrBlob 对象用列表对其初始化然后将其传递给函数当函数完成后此对象将被丢弃再也无法访问了。对于这些情况我们可以定义显式的构造函数禁止隐式类类型转换 隐式初始化 class MyClass { public:std::vectorint values;MyClass(std::initializer_listint initList) : values(initList) {std::cout MyClass initialized with a list of values.size() elements.\n;} };void func(MyClass obj) {// 函数内容... }int main() {MyClass obj1 {5, 6, 7, 8}; // 隐式MyClass obj2 {{5, 6, 7, 8}}; // 隐式MyClass obj3 {1, 2, 3, 4}; // 复制列表初始化隐式func({9, 10, 11}); // 隐式地构造 MyClass 对象并传递给 func }显式初始化 加了explict class MyClass { public:std::vectorint values;explicit MyClass(std::initializer_listint initList) : values(initList) {std::cout MyClass initialized with a list of values.size() elements.\n;} };void func(MyClass obj) {// 函数内容... }int main() {MyClass obj1({1, 2, 3, 4}); // 显式初始化合法// func({9, 10, 11}); // 编译错误不能隐式地使用初始化列表构造 MyClass 对象func(MyClass({9, 10, 11})); // 显式构造 MyClass 对象 }1.2 直接管理内存 1、C语言定义了两个运算符 来分配和释放动态内存。运算符new 分配内存delete 释放new分配的内存 2、相对于智能指针使用这两个运算符管理内存非常容易出错。而且自己直接管理内存的类 与使用智能指针的类不同它们 不能依赖类对象拷贝、赋值和销毁操作的任何默认定义 3、使用new动态分配 和初始化对象在自由空间分配的内存 是无名的因此 new无法为其分配的对象命名而是返回 一个指向该对象的指针 int *pi new int; // pi指向一个动态分配的、未初始化的无名对象默认情况下动态分配的对象 是默认初始化的这意味着 内置类型 或 组合类型的对象的值 将是未定义的而类类型对象 将用默认构造函数进行 初始化 string *ps new string; // 初始化为空string int *pi new int; // pi指向一个未初始化的int可以使用直接初始化方式 来初始化一个动态分配的对象可以使用传统的构造方式使用圆括号在新标准下也可以使用列表初始化 int *pi new int(1024); // pi指向的对象的值为1024 // *ps 为9999999999 string *ps new string(10, 9); // vector有10个元素值依次从0到9vectorint *pv new vectorint{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};对动态分配的对象进行值初始化只需在类型名之后 跟一对空括号即可 // 默认初始化*pi1 的值未定义 int *pi1 new int; // 值初始化为0*pi2 为0 int *pi2 new int();对于定义了 自己的构造函数 的类类型例如string来说要求 值初始化 是没有意义的不管 采用什么形式对象 都会通过默认构造函数来初始化 但对于 内置类型两种形式的差别 就很大了值初始化的内置类型对象 有着良好定义的值而默认初始化的对象的值 则是未定义的。类似的对于类中 那些依赖于 编译器合成的默认构造函数的 内置类型成员如果它们 未在类内被初始化那么它们的值 也是未定义的 4、提供了 一个括号包围的 初始化器就可以 使用auto 从此初始化器来推断 我们想要分配的对象的类型。但是由于编译器要用初始化器的类型 来推断要分配的类型只有 当括号中 仅有单一初始化器时 才可以使用auto // p指向一个 与obj类型相同的对象 auto p1 new auto(obj); // 该对象用obj进行初始化// 错误括号中只能有单个初始化器 auto p2 new auto{a, b, c};p1的类型 是一个指针指向 从obj自动推断出的类型 5、动态分配的const对象用 new分配 const对象是合法的 // 分配并初始化 一个const int const int *pci new const int(1024); // 分配并默认初始化 一个const的空string const string *pcs new const string;对于一个定义了 默认构造函数的类 类型其const动态对象 可以隐式初始化而 其他类型的对象 就必须显式初始化。由于分配的对象是 const的new返回的指针 是一个指向const的指针 6、内存耗尽默认情况下如果new 不能分配 所要求的内存空间它 会抛出一个类型为 bad_alloc 的异常。我们可以改变 使用new的方式 来阻止它抛出异常 // 如果分配失败new 返回一个空指针 int *pl new int; // 如果分配失败new抛出std::bad_alloc int *p2 new (nothrow) int; // 如果分配失败new返回一个空指针称这种形式的new 为定位new定位new表达式 允许我们 向new传递额外的参数。我们传递给它 一个由标准库定义的 名为no throw的对象。如果 将 nothrow 传递给 new我们的意图是 告诉它不能抛出异常。如果这种形式的new 不能分配所需内存它会返回 一个空指针。bad_alloc 和 nothrow 都定义在 头文件new中 7、释放动态内存为了 防止内存耗尽在动态内存使用完毕后必须 将其归还给系统。我们通过 delete表达式 来将动态内存归还给系统。delete表达式 接受一个指针指向 我们想要释放的对象 delete p; // p必须 指向一个动态分配的对象 或是一个空指针与new类型 类似delete表达式 也执行两个动作销毁给定的指针 指向的对象释放对应的内存 8、指针值和delete传递给delete的指针 必须指向动态分配的内存或者是 一个空指针 释放一块 并非new分配的内存或者 将相同的指针值 释放多次其行为是未定义的 int i, *pi1 i, *pi2 nullptr; double *pd new double(33), *pd2 pd; delete i; // 错误i不是一个指针delete pi1; // 未定义pi1指向一个局部变量delete pd; // 正确 delete pd2; // 未定义pd2指向的内存 已经被释放了delete pi2; // 正确释放一个空指针总是没有错误的delete pi1 和 pd2 所产生的错误 则更具潜在危害通常情况下编译器不能分辨一个指针指向的是 静态 还是动态分配的对象。类似的编译器 也不能分辨 一个指针所指向的内存 是否已经被释放了。对于 这些delete表达式大多数编译器 会编译通过尽管它们是错误的 虽然一个const对象的值 不能被改变但它本身 是可以被销毁的。如同 任何其他动态对象一样想要 释放一个const动态对象只要 delete指向它的指针即可 const int *pci new const int(1024); delete pci; // 正确释放一个const对象9、动态对象的生存期 直到被释放时为止由 shared_ptr 管理的内存 在最后一个 shared_ptr 销毁时会被自动释放。但对于通过内置指针类型 来管理的内存就不是这样了。对于一个 由内置指针管理的动态对象直到 被显式释放之前 它都是存在的 返回指向动态内存的指针而不是智能指针的函数 给其调用者 增加了一个额外负担——调用者必须记得释放内存 Foo* factory(T arg) {// 视情况处理argreturn new Foo(arg); // 调用者负责释放此内存 }factory的调用者 负责在不需要此对象时 释放它 void use_factory(T arg) {Foo *p factory(arg);// 使用p但不delete它 } // p离开了它的作用域但它所指向的内存 没有被释放当 use_factory 返回时局部变量p 被销毁。此变量 是一个内置指针而不是一个智能指针 与类类型不同内置类型的对象 被销毁时什么也不会发生。特别是当一个指针离开 其作用域时它所指向的对象 什么也不会发生。如果这个指针 指向的是动态内存那么内存 将不会被自动释放 p是 指向factory分配的内存的唯一指针。一旦use_factory返回程序 就没有办法 释放这块内存了。根据 整个程序的逻辑修正这个错误的正确方法是 在 use_factory 中记得释放内存 void use_factory(T arg) {Foo *p factory(arg);// 使用pdelete p; //现在记得释放内存我们已经不需要它了 }还有一种可能我们的系统中的其他代码要使用 use_factory 所分配的对象我们就应该修改此函数让它返回一个指针指向它分配的内存调用者必须释放内存 Foo* use_factory(T arg) {Foo *p factory(arg);// 使用preturn p; // 调用者必须释放内存 }10、动态内存的管理非常容易出错 1忘记delete内存 2使用已经释放掉的对象 3同一块内存释放两次 坚持只使用智能指针就可以避免所有这些问题 11、delete之后重置指针值delete一个指针后指针值就变为 无效了。虽然指针 已经无效但在很多机器上 指针仍然保存着已经释放了的动态内存的地址。在delete之后指针就变成了 空悬指针即指向 一块曾经保存数据对象 但现在已经无效的内存的指针 未初始化指针 的所有缺点 空悬指针也都有。有一种方法可以 避免空悬指针的问题在指针 即将要离开其作用域之前 释放掉它所关联的内存。这样 在指针关联的内存 被释放掉之后就没有机会 继续使用指针了。如果 需要保留指针可以 在delete之后将nullptr 赋予指针这样就清楚地指出指针 不指向任何对象 12、这只是提供了有限的保护动态内存的一个基本问题是 可能有多个指针 指向相同的内存。在delete内存之后 重置指针的方法 只对这个指针有效对其他任何 仍指向已释放的内存的指针 是没有作用的。例如 int *p(new int(42)); // p指向动态内存 auto q p; // p和q指向相同的内存 delete p; // p和q均变为无效 p nullptr; // 指出p不再绑定到任何对象重置p对q 没有任何作用在我们释放p所指向的同时也是q所指向的内存时q也变为无效了。在实际系统中查找 指向相同内存的所有指针 是异常困难的 13、返回一个动态分配的 int 的vector。将此vector 传递给另一个函数这个函数 读取标准输入将读入的值 保存在 vector 元素中。再将 vector传递给另一个函数打印读入的值。记得在恰当的时刻delete vector 第二次 使用 shared_ptr 而不是内置指针 不使用智能指针在前需要 手动delete使用智能指针操作 注意注释 在标准输入流 std::cin 遇到文件结束 (EOF) 之后它会处于错误状态此时再次尝试从其读取输入会失败 这是因为 C 标准库 将文件结束看作是一个不可恢复的输入状态因此 在此之后对输入流的进一步读取操作 都会失败 为了 使程序能够重复 从标准输入中读取数据你需要 清除错误标志 并重置输入流的状态 可以 使用 std::cin.clear() 来清除错误标志然后 使用 std::cin.ignore() 来清除输入缓冲区 std::numeric_limitsstd::streamsize::max() 返回了 std::streamsize 类型的最大值 std::streamsize 是一种用于表示输入/输出操作的字节数或字符数的整数类型 std::numeric_limits 是 C 标准库中的一个模板类用于获取数值类型的特性信息比如最大值、最小值等 因此std::numeric_limitsstd::streamsize::max() 返回了 std::streamsize 类型的最大值表示输入缓冲区的最大尺寸 std::cin.ignore() 函数用于从输入缓冲区中提取字符并丢弃它们。其第一个参数 表示最大读取的字符数第二个参数 表示要忽略的特定字符 在这里我们使用 std::numeric_limitsstd::streamsize::max() 作为第一个参数以确保 尽可能地读取输入缓冲区中的所有字符而 ‘\n’ 表示忽略换行符 std::streamsize 通常用于 表示 I/O 操作中的缓冲区大小、读取或写入的字节数等。它是一个平台无关的类型因此 可以在不同平台上 保证一致的行为 这个操作的目的是 清空输入缓冲区中的所有剩余字符以便 在下一次从标准输入中读取输入时不会受到 之前输入操作的影响 #include iostream #include vector #include string #include memory // 使用智能指针using namespace std;vectorint* create() {return new vectorint; // 注意返回值是一个指针 }void read(vectorint *vecp) // 操作都是基于指针 {int i;while (cin i){vecp-push_back(i);} }void print(vectorint* vecp) {for (auto i : (*vecp))cout i ; }// 使用智能指针参数 和 返回值都需要改 shared_ptrvectorint create_smartp() {return make_sharedvectorint(); }void read_smartp(shared_ptrvectorint sp) {int i;while (cin i) // 这里读不进去了因为已经遇到EOF了{sp-push_back(i);} }void print_smartp(shared_ptrvectorint sp) {for (auto i : (*sp))cout i ; }int main() {vectorint* p create();read(p);print(p);delete p;// 注意cin.clear();cin.ignore(numeric_limitsstreamsize::max(), \n);shared_ptrvectorint sp create_smartp();read_smartp(sp);print_smartp(sp);return 0; }运行结果 14、下面的函数是否有错误 bool b() {int* p new int;// ...return p; }意图是通过 new 返回的指针值来区分内存分配成功或失败 —— 成功返回一个合法指针转换为整型是一个非零值可转换为 bool 值 true分配失败p 得到 nullptr其整型值是 0可转换为 bool 值 false 但普通 new 调用在分配失败时抛出一个异常 bad_alloc而不是返回 nullptr因此程序不能达到预想的目的 可将 new int 改为 new (nothrow) int 来令 new 在分配失败时不抛出异常而是返回 nullptr 15、rq后r所指的内存 没有释放应该先delete r再rq第二段代码内存会自动释放 int *q new int(42), *r new int(100); r q; auto q2 make_sharedint(42), r2 make_sharedint(100); r2 q2;第一段代码 带来了两个非常严重的内存管理问题 首先是一个直接的内存泄漏问题r 和 q 一样都指向 42 的内存地址而 r 中原来保存的地址 —— 100 的内存再无指针管理变成 “孤儿内存”从而造成内存泄漏。 其次是一个 “空悬指针” 问题。由于 r 和 q 指向同一个动态对象如果程序编写不当很容易产生释放了其中一个指针而继续使用另一个指针的问题。继续使用的指针指向的是一块已经释放的内存是一个空悬指针继续读写它指向的内存可能导致程序崩溃甚至系统崩溃的严重问题 1.3 shared_ptr和new结合使用 1、如果 我们不初始化一个智能指针它就会 被初始化为一个空指针。我们 还可以用new返回的指针 来初始化智能指针 shared_ptrdouble p1; // shared_ptr 可以指向一个double shared_ptrint p2(new int(42)); // p2指向一个值为42的int接受指针参数的智能指针 构造函数是explicit的不能 将一个内置指针 隐式转换为 一个智能指针必须使用 直接初始化即 显式初始化 形式 来初始化一个智能指针 shared_ptrint p1 new int(1024); // 错误必须使用直接初始化形式 shared_ptrint p2(new int(1024)); // 正确使用了直接初始化形式p1的初始化 隐式地要求 编译器用一个new返回的 int* 来创建一个 shared_ptr。由于 不能进行内置指针 到智能指针间的隐式转换因此 这条初始化语句 是错误的。出于相同的原因一个返回 shared_ptr 的函数 不能 在其返回语句中 隐式转换一个普通指针 shared_ptrint clone(int p) {return new int(p); // 错误隐式转换为 shared_ptrint }必须将 shared_ptr 显式绑定到 一个想要返回的指针上 shared_ptrint clone(int p) {//正确显式地用int* 创建 shared_ptrintreturn shared_ptrint(new int(p)); }默认情况下一个 用来初始化智能指针的普通指针 必须指向 动态内存因为 智能指针 默认使用 delete 释放 它所关联的对象。可以 将智能指针 绑定到 一个指向其他类型的资源的指针上但是为了这样做必须提供自己的操作 来替代 delete 2、定义和改变 shared_ptr 的其他方法 方法解释shared_ptrT p(q)p管理 内置指针q所指向的对象q必须指向 new分配的内存且 能够转换为T*类型shared_ptrT p(u)p 从unique_ptr u 那里接管了 对象的所有权将u置为空shared_ptrT p(q, d)p接管了 内置指针q所指向的对象的所有权。q必须能转换为 T*类型。p将 使用可调用对象d 来代替deleteshared_ptrT p(p2, d)p是 shared_ptr p2 的拷贝唯一的区别是 p将用可调用对象d 来代替deletep.reset(), p.reset(q), p.reset(q, d)若p 是唯一指向其对象的shared_ptr, reset会释放此对象。若传递了 可选的参数内置指针q会令p指向q否则会将p 置为空。若还传递了参数d将会 调用d而不是delete来释放q 3、不要混合使用普通指针 和 智能指针shared_ptr 可以 协调对象的析构但 这仅限于 其自身的拷贝也是shared_ptr之间。这是 推荐使用make_shared 而不是new 的原因。这样我们就能 在分配对象的同时 就将shared_ptr与之绑定从而避免了无意中将同一块内存绑定到 多个独立创建的shared_ptr上 4、 // 在函数被调用时 ptr被创建并初始化 void process(shared_ptrint ptr) {// 使用ptr } // ptr离开作用域被销毁process的参数是 传值方式传递的因此实参 会被拷贝到ptr中。拷贝一个shared_ptr 会递增 其引用计数因此在process运行过程中引用计数值 至少为2。当process结束时ptr的引用计数 会递减但不会变为0。因此当局部变量ptr 被销毁时ptr指向的内存 不会被释放 使用此函数的正确方法 是传递给它一个shared_ptr shared_ptrint p(new int(42)); // 引用计数为1 process(p); // 拷贝p会递增 它的引用计数在process中 引用计数值为2出来就是1了 int i *p; // 正确引用计数值为1虽然 不能传递给 process 一个内置指针但可以 传递给它一个临时的shared_ptr这个 shared_ptr 是 用一个内置指针显式构造的 但是这样做很可能 会导致错误 int *x(new int(1024)); // 危险x是一个普通指针不是一个智能指针 process(x); // 错误不能将int* 转换为 一个shared_ptrint process(shared_ptrint(x)); // 合法的但内存会被释放 int j *x; // 未定义的x是一个空悬指针将一个 临时shared_ptr 传递给 process。当这个调用 所在的表达式结束 时这个 临时对象就被销毁了。销毁这个临时变量 会递减引用计数此时引用计数就变为0了。因此当临时对象 被销毁时它所指向的内存 会被释放 但x继续指向已经释放的内存从而变成一个 空悬指针。如果 试图使用x的值其行为 是未定义的 当将一个shared_ptr 绑定到 一个普通指针时我们就 将内存的管理责任交给了这个 shared_ptr。一旦这样做了我们就 不应该再使用内置指针 来访问 shared_ptr 所指向的内存了 使用一个内置指针 来访问 一个智能指针所负责的对象 是很危险的因为我们无法知道 对象何时会被销毁 5、不要使用 get初始化 另一个智能指针或为智能指针赋值智能指针类型 定义了一个名为get的函数它返回 一个内置指针 指向智能指针管理的对象。此函数 是为了这样一种情况而设计的我们需要 向不能使用智能指针的代码 传递一个内置指针。使用get返回的指针的代码 不能delete此指针 虽然编译器 不会给出错误信息但将另一个智能指针 也绑定到get返回的指针上是 错误的 shared_ptrint p(new int(42)); // 引用计数为1 int *q p.get(); // 正确但使用q时要注意不要让它管理的指针被释放 { // 新程序块// 未定义两个独立的 shared_ptr 指向相同的内存shared_ptrint (q); } // 程序块结束q被销毁它指向的内存被释放 int foo *p; // 未定义:p指向的内存 已经被释放了p和q 指向相同的内存。由于 它们是 相互独立创建的因此 各自的引用计数 都是1。当q所在的程序块 结束时q被销毁这会导致q指向的内存 被释放。从而p变成 一个空悬指针意味着 当我们试图使用p时将发生 未定义的行为。而且当p被销毁时 这块内存 会被第二次delete get 用来 将指针的访问权限 传递给代码你 只有 在确定代码不会delete指针的情况下才能使用get。特别是永远不要用get初始化 另一个智能指针 或者 为另一个智能指针赋值 6、其他shared_ptr 操作可以用reset来 将一个新的指针 赋予一个shared_ptr p new int(1024); // 错误不能 将一个指针 赋予shared_ptr p.reset(new int(1024)); // 正确P指向一个新对象与赋值类似reset 会 更新引用计数如果需要的话会释放p指向的对象。reset成员 经常与unique一起使用来控制多个shared_ptr 共享的对象 if (!p.unique())p.reset(new string(*p)); // 我们不是唯一用户分配新的拷贝 *p new Val; // 现在我们知道自己是唯一的用户可以改变对象的值在多用户或者说多个指针共享同一资源的情况下确保在修改资源之前 每个用户都有自己的私有拷贝是很重要的。这个概念在共享资源管理中 非常重要尤其是 在使用智能指针如 std::shared_ptr 管理共享资源时。实现这样的机制 可以避免意外地修改了其他用户 正依赖的资源从而引入难以发现的bug 演示 如何在不是唯一用户的情况下为修改操作 分配新的资源拷贝。我们将使用 std::shared_ptr 来管理 一个简单的资源例如一个 std::vectorint #include iostream #include vector #include memory// 模拟资源类 class Resource { public:std::vectorint data;// 添加数据的方法为了简化就直接在这里实现void add(int value) {data.push_back(value);}// 打印数据的方法void print() const {for (auto val : data) {std::cout val ;}std::cout \n;} };// 确保在修改前拥有自己的副本 void ensureOwnCopy(std::shared_ptrResource resourcePtr) {if (!resourcePtr.unique()) { // 如果不是唯一的用户resourcePtr std::make_sharedResource(*resourcePtr); // 创建一个新的资源拷贝}// 现在可以安全地修改 resourcePtr 指向的资源不影响其他用户 }int main() {auto ptr1 std::make_sharedResource(); // 创建一个资源ptr1-add(1); // 添加一些数据ptr1-add(2);auto ptr2 ptr1; // ptr2 现在共享 ptr1 指向的相同资源// 在修改前确保 ptr1 有自己的副本ensureOwnCopy(ptr1);ptr1-add(3); // 现在这个修改不会影响 ptr2std::cout ptr1: ;ptr1-print();std::cout ptr2: ;ptr2-print();return 0; }在这个例子中ensureOwnCopy 函数检查 std::shared_ptr 是否是指向其资源的唯一指针即没有其他共享指针指向同一个资源。如果不是唯一的函数 创建一个新的资源拷贝并更新 std::shared_ptr 以指向这个新资源这样 就可以在不影响其他共享指针的情况下 修改资源了。在 main 函数中我们演示了 如何使用 ensureOwnCopy 在修改资源前 确保我们有自己的资源拷贝 7、智能指针和普通指针使用上的问题 shared_ptrint p(new int(42)); process(shared_ptrint(p));此调用是正确的利用 p 创建一个临时的 shared_ptr 赋予 process 的参数 ptrp 和 ptr 都指向相同的 int 对象引用计数被正确地置为 2。process 执行完毕后ptr 被销毁int 对象 42 引用计数减 1这是正确的 —— 只有 p 指向它 process(shared_ptrint(p.get()));此调用是错误的。p.get() 获得一个普通指针指向 p 所共享的 int 对象。利用此指针普通指针创建一个 shared_ptr 赋予 process 的参数 ptr而不是利用 p 创建一个 shared_ptr 赋予 process 的参数 ptr这样的话将不会形成正确的动态对象共享。编译器会认为 p 和 ptr 是使用两个地址虽然它们相等创建的两个不相干的 shared_ptr而非共享同一个动态对象。这样两者的引用计数均为 1。当 process 执行完毕后ptr 的引用计数减为 0所管理的内存地址被释放而此内存就是 p 所管理的。p 成为一个管理空悬指针的 shared_ptr auto p new int(); auto sp make_sharedint(); void process(shared_ptrint ptr);(a) process(sp); (b) process(new int()); (c) process(p); (d) process(shared_ptrint(p));a合法。sp 是一个共享指针指向一个 int 对象。对 process 的调用会拷贝 sp传递给 process 的参数 ptr两者都指向相同的 int 对象引用计数变为 2。当 process 执行完毕时ptr 被销毁引用计数变回 1 b不合法。普通指针不能隐式转换为智能指针 c不合法。原因同b d合法但是是错误的程序。p 是一个指向 int 对象的普通指针被用来创建一个临时 shared_ptr传递给 process 的参数 ptr引用计数为 1。当 process 执行完毕ptr 被销毁引用计数变为 0int 对象被销毁。p 变为空悬指针 auto sp make_sharedint(); auto p sp.get(); delete p;第二行用 get 获取了 sp 指向的 int 对象的地址第三行用 delete 释放这个地址。这意味着 sp 的引用计数仍为 1但其指向的 int 对象已经被释放了。sp 成为类似空悬指针的 shared_ptr 1.4 智能指针和异常 1、以前介绍了 使用异常处理的程序 能在异常发生后令程序流程继续这种程序 需要确保 在异常发生后 资源能被正确地释放。一个简单的确保资源被释放的方法是 使用智能指针 使用智能指针即使 程序块过早结束智能指针类 也能确保 在内存不再需要时将其释放 void f() {shared_ptrint sp(new int(42)); // 分配一个新对象// 这段代码抛出一个异常且在f中未被捕获 } // 在函数结束时shared_ptr自动释放内存函数的退出 有两种可能正常处理结束 或者发生了异常无论哪种情况局部对象都会被销毁 与之相对的当发生异常时我们直接管理的内存 是不会自动释放的。如果使用内置指针 管理内存且在new之后 在对应的delete之前发生了异常则内存不会被释放 void f() {int *ip new int(42); // 动态分配一个新对象// 这段代码抛出一个异常且在f中未被捕获delete ip; // 在退出之前释放内存 }如果 在new和delete之间发生异常且异常 未在f中被捕获则内存就永远不会被释放了。在函数f之外 没有指针指向这块内存因此 就无法释放它了 2、智能指针和哑类包括所有标准库类在内的 很多C类都定义了析构函数负责清理对象使用的资源。但是不是所有的类都是这样良好定义的。特别是那些为C和C两种语言 设计的类通常都要求 用户显式地释放 所使用的任何资源 与管理动态内存类似我们通常可以 使用类似的技术来管理 不具有良好定义的析构函数的类。例如假定我们 正在使用一个C和C都使用的网络库使用这个库的代码可能是这样的 struct destination; //表示我们正在连接什么 struct connection; //使用连接所需的信息 connection connect(destination*);//打开连接 void disconnect(connection); //关闭给定的连接 void f(destination d /* 其他参数 */) {//获得一个连接记住使用完后要关闭它connection c connect(d);//使用连接//如果我们在f退出前 忘记调用disconnect就无法关闭c了 }如果connection 有一个析构函数就可以 在f结束时 由析构函数自动关闭连接。但是 connection没有析构函数。这个问题与我们上一个程序中使用 shared_ptr 避免内存泄漏 几乎是等价的。使用 shared_ptr 来保证 connection 被正确关闭已被证明是一种有效的方法 3、使用我们自己的释放操作shared_ptr 假定它们指向的是 动态内存。因此当一个 shared _ptr 被销毁时它默认地 对它管理的指针 进行delete操作。为了用 shared_ptr 来管理一个 connection我们必须 首先定义一个函数 来代替 delete 这个删除器函数 必须能够完成 对 shared_ptr 中保存的指针 进行释放的操作。在本例中我们的删除器 必须接受单个类型为connection* 的参数 void end_connection(connection *p) { disconnect(*p); }当我们创建一个 shared_ptr 时可以传递一个可选的指向删除器函数的参数 void f(destination d /* 其他参数 */) {connection c connect(d);shared_ptrconnection p(c, end_connection);//使用连接//当f退出时即使是由于异常而退出connection会被正确关闭 }当p被销毁时它不会对自己保存的指针 执行delete而是调用 end_connection传入这个函数的参数 为指针 接下来end_connection 会调用 disconnect从而确保 连接被关闭。如果f正常退出那么p的销毁 会作为结束处理的一部分。如果发生了异常p同样 会被销毁从而连接被关闭 4、智能指针陷阱为了 正确使用智能指针我们必须坚持一些基本规范 不使用 相同的内置指针值初始化或reset多个智能指针不 delete get() 返回的指针不使用 get() 初始化或reset另一个智能指针如果你使用 get() 返回的指针记住 当最后一个对应的智能指针销毁后你的指针就变为无效了如果你使用 智能指针管理的资源 不是new分配的内存记住 传递给它一个删除器 5、编写 自己版本的用 shared_ptr 管理 connection 的函数使用智能指针释放 #include iostream #include memoryusing namespace std;struct destination {}; struct connection {};connection connect(destination*) {cout 连接建立 endl;return connection(); }void disconnect(connection) {cout 断开连接 endl; }// 直接管理指针未使用 shared_ptr忘记调用disconnect void f(destination d) {connection c connect(d);// 在退出前 忘记调用disconnect就无法关闭连接了cout f结束 endl; }// 使用 shared_ptr void close_connect(connection* cp) {return disconnect(*cp); }void f1(destination d) {connection c connect(d);shared_ptrconnection p(c, close_connect);// 别忘了指出 智能指针 名字p// 退出函数前调用 close_connect注意 close_connect参数是指针shared_ptr第一个参数 是指针 不是引用cout f1结束 endl; }// 使用 shared_ptr同时使用 lambda void f2(destination d) {connection c connect(d);shared_ptrconnection p(c, [](connection* cp) { return disconnect(*cp); });cout f2结束 endl; }int main() {destination d;f(d);cout endl;f1(d);cout endl;f2(d);return 0; }运行结果 1.5 unique_ptr 1、一个 unique_ptr “拥有” 它所指向的对象。与 shared_ptr 不同某个时刻只能 有一个 unique_ptr 指向一个给定对象。当unique_ptr 被销毁时它所指向的对象也被销毁 2、与 shared_ptr 不同没有类似 make_shared 的标准库函数 返回一个 unique_ptr。当 定义一个 unique_ptr 时需要 将其绑定到 一个new返回的指针上。类似 shared_ptr初始化 unique_ptr 必须采用直接初始化形式 unique_ptrdouble p1; // 可以指向一个double的 unique_ptr unique_ptrint p2(new int(42)); // p2指向一个值为42的int由于一个 unique_ptr 拥有它指向的对象因此 unique_ptr 不支持普通的拷贝 或 赋值操作 unique_ptrstring p1(new string(Stegosaurus)); unique_ptrstring p2(p1); //错误unique_ptr不支持拷贝 unique_ptrstring p3; p3 p2; //错误unique_ptr不支持赋值#include memory #include iostreamint main() {std::unique_ptrint up1(new int(1));// 报错无法引用std::unique_ptrint up2(up1);// 报错无法引用std::unique_ptrint up2 up1;// 正确std::unique_ptrint up2;up2.reset(up1.release());std::cout *up2 std::endl;return 0; }3、unique_ptr 操作 操作解释unique_ptrT u1, unique_ptrT, D u2空unique_ptr可以指向 类型为T的对象。u1会使用delete 来释放它的指针u2会使用 一个类型为D的可调用对象 来释放它的指针unique_ptrT, D u(d)空unique_ptr指向类型为T的对象用类型为D的对象d 代替deleteu nullptr释放 u指向的对象将u置为空u.release()u放弃 对指针的控制权返回指针并将u置为空u.reset(), u.reset(q), u.reset(nullptr)释放u指向的对象如果提供了内置指针q令u指向这个对象否则将u置为空 虽然我们 不能拷贝或赋值 unique_ptr但可以 通过调用 release 或 reset 将指针的所有权 从一个非constunique_ptr 转移给另一个unique //将所有权从p1指向 string Stegosaurus转移给p2 unique_ptrstring p2(p1.release()); // release将p1置为空unique_ptrstring p3(new string(T rex)); //将所有权从p3转移给p2 p2.reset(p3.release()); // reset释放了p2原来指向的内存将p3置为空release成员 返回 unique_ptr 当前保存的指针 并将其置为空。因此p2被初始化为 p1 原来保存的指针而p1 被置为空 reset成员 接受一个可选的指针参数令 unique_ptr 重新指向给定的指针。如果 unique_ptr 不为空它原来指向的对象 被释放 调用 release会切断 unique_ptr 和 它原来管理的对象间的联系。release 返回的指针 通常 被用来初始化 另一个智能指针 或 给另一个智能指针赋值 如果 用另一个智能指针 来保存release返回的指针程序就要 负责资源的释放 p2.release(); //错误p2不会释放内存 (只是切断联系)而且我们丢失了指针 auto p p2.release(); //正确但我们必须记得delete(p)4、传递 unique_ptr 参数 和 返回 unique_ptr不能拷贝 unique_ptr 的规则 有一个例外我们可以拷贝 或 赋值一个将要被销毁的 unique_ptr 最常见的例子是 从函数返回一个 unique_ptr unique_ptrint clone(int p) {//正确:从int*创建一个 unique_ptrintreturn unique_ptrint(new int(p)); }还可以 返回一个局部对象的拷贝 unique_ptrint clone(int p) { unique_ptrint ret(new int(p));// ...return ret; }对于 两段代码编译器 都知道 要返回的对象将要被销毁。在此情况下编译器执行一种特殊的“拷贝” 向后兼容auto_ptr 标准库的较早版本 包含了一个名为auto_ptr的类不能在容器中保存 auto_ptr也不能从函数中返回 auto_ptr 虽然 auto_ptr 仍是标准库的一部分但编写程序时 应该使用 unique_ptr 5、向 unique_ptr 传递删除器类似 shared_ptrunique_ptr 默认情况下 用delete释放它指向的对象。与 shared_ptr 一样我们可以 重载一个 unique_ptr 中默认的删除器。但是unique_ptr 管理删除器的方式 与 shared_ptr 不同 重载一个 unique_ptr 中的删除器 会影响到 unique_ptr类型以及如何构造或 reset)该类型的对象 与重载关联容器的比较操作 类似我们必须在 尖括号中 unique_ptr 指向类型之后 提供删除器类型。在创建 或 reset一个这种 unique_ptr 类型的对象时必须 提供一个指定类型的可调用对象删除器 //p指向一个类型为objT的对象并使用一个类型为delT的对象释放objT对象 //它会调用一个名为fcn的delT类型对象 unique_ptrobjT, delT p(new objT, fcn);将重写连接程序用 unique_ptr来代替shared_ptr void f(destination d /*其他需要的参数*/) {connection c connect(d); //打开连接//当p被销毁时连接将会关闭unique_ptrconnection, decltype(end_connection)*p(c, end_connection);//使用连接//当f退出时即使是由于异常而退出connection会被正确关闭 }由于 decltypeend_connection返回一个函数类型所以我们 必须添加一个* 来指出我们 正在使用该类型的一个指针 同时 初始化p的第一个参数 c 也是一个地址 6、下面的 unique_ptr 声明中哪些是合法的哪些可能导致后续的程序错误解释每个错误的问题在哪里 int ix 1024, *pi ix, *pi2 new int(2048); typedef unique_ptrint IntP; (a) IntP p0(ix); (b) IntP p1(pi); (c) IntP p2(pi2); (d) IntP p3(ix); (e) IntP p4(new int(2048)); (f) IntP p5(p2.get());a不合法。unique_ptr 需要用一个指针初始化无法将 int 转换为指针 b编译时合法运行时会报错因为pi不是new出来的销毁时使用默认的delete会出错 c编译时合法但是运行时会导致空悬指针unique_ptr释放空间时使用pi2指针会出错 在将原始指针 赋给 unique_ptr 之后不再使用 原始指针如果 需要再次访问 该内存应该再次通过 unique_ptr 来访问 pi2 是一个原始指针指向一个动态分配的 int。当 使用 pi2 初始化 p2一个 std::unique_ptrint 类型的对象时p2 接管了 pi2 指向的内存的所有权 int* pi2 new int(2048); // 动态分配一个 int std::unique_ptrint p2(pi2); // p2 现在拥有这个 int 的所有权这之后应该避免 直接使用 pi2因为当 p2 被销毁例如当它的作用域结束时或者 显式地调用 p2.reset() 时p2 所拥有的内存将被释放。此时如果 尝试通过 pi2 访问该内存位置将遇到未定义行为因为 pi2 现在是一个悬挂指针。这可能导致程序崩溃或其他不可预测的行为 d编译时合法运行时会报错因为指针不是new出来的销毁时使用默认的delete会出错 e合法 f编译时合法但是会导致两次delete或者一个delete后另一个变为空悬指针 7、shared_ptr 为什么没有 release 成员unique_ptr “独占” 对象的所有权不能拷贝和赋值。release 操作是用来将对象的所有权 转移给另一个 unique_ptr 的 而多个 shared_ptr 可以 “共享” 对象的所有权。需要共享时可以简单拷贝和赋值。因此并不需要 release 这样的操作来转移所有权 1.6 weak_ptr 1、weak_ptr 是一种 不控制所指向对象生存期的智能指针它指向由一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个shared_ptr 不会改变 shared_ptr 的引用计数。一旦 最后一个指向对象的 shared_ptr 被销毁对象 就会被释放。即使有 weak_ptr 指向对象对象 也还是会被释放 weak_ptr解释weak_ptrT w空 weak_ptr 可以 指向类型为T的对象weak_ptrT w(sp)与shared_ptr sp 指向相同对象的 weak_ptr。T必须 能转换为 sp指向的类型w pp可以是一个 shared_ptr 或 一个weak_ptr。赋值后 w与p共享对象w.reset()将w置为空w.use_count()与w共享对象的 shared_ptr 的数量w.expired()若 w.use_count() 为0返回true否则返回falsew.lock()如果expired 为true返回一个 空shared_ptr否则 返回一个指向w的对象的 shared_ptr 当我们创建一个 weak_ptr 时要用一个 shared_ptr 来初始化它 auto p make_sharedint(42); weak_ptrint wp(p); // wp弱共享pp的引用计数未改变wp和p 指向相同的对象。由于 是弱共享创建wp 不会改变p的引用计数wp指向的对象 可能被释放掉 由于对象 可能不存在我们不能使用 weak_ptr 直接访问对象而必须调用 lock。此函数 检查 weak_ptr 指向的对象 是否仍存在。如果存在lock 返回一个指向共享对象的 shared_ptr。与 任何其他 shared_ptr 类似只要此 shared_ptr 存在它所指向的底层对象 也就会一直存在 if (shared_ptrint np wp.lock()) { // 如果np不为空则条件成立// 在if中np与p共享对象 }只有当 lock调用 返回true时 我们才会进入if语句体。在if中使用 np访问共享对象 是安全的 2、作为weak_ptr用途的一个展示我们 将为StrBlob类 定义一个伴随指针类。定义的指针类 将命名为 StrBlobPtr会保存一个weak_ptr指向StrBlob的data成员 初始化时提供给它的我们通过使用 weak_ptr不会影响 一个给定的StrBlob所指向 的vector的生存期。但是可以 阻止用户访问一个 不再存在的vector的企图 // 对于访问一个不存在StrBlobStrBlobPtr抛出异常 class StrBlobPtr { public:StrBlobPtr() :curr(0) {} // 构造函数后面不需要加StrBlobPtr(StrBlob a, size_t sz 0) :wptr(a.data), curr(sz) {}std::string deref() const; // 解引用StrBlobPtrStrBlobPtr incr(); // 前缀递增private:// 若检查确实存在check返回一个 指向vector的shared_ptrstd::shared_ptrstd::vectorstd::string check(std::size_t, const std::string) const;// 保存一个weak_ptr意味着 底层vector可能被销毁直接用vector初始化即可std::weak_ptrstd::vectorstd::string wptr;std::size_t curr; // 在数组中的当前位置 };不能将 StrBlobPtr 绑定到一个 const StrBlob 对象。这个限制是 由于构造函数 接受一个非 const StrBlob 对象的引用 而导致的。它还要 检查指针指向的vector是否还存在 StrBlobPtr的check成员 与strBlob中的同名成员不同它还要检查指针指向的vector 是否还存在 std::shared_ptrstd::vectorstd::string StrBlobPtr::check(std::size_t i, const std::string msg) const// msg不同错误不同信息 {auto ret wptr.lock(); // vector存在与否if (!ret)throw std::runtime_error(unbound StrBlobPtr);if (i ret-size())throw std::out_of_range(msg);return ret; // 返回指向vector的shared_ptr }3、指针操作将定义名为 deref 和 incr的函数分别用来 解引用和递增StrBlobPtr deref成员 调用check检查使用 vector是否安全 以及curr是否在合法范围内 std::string StrBlobPtr::deref() const {auto p check(curr, dereference past end);return (*p) [curr]; //(*p)是对象所指向的vector }如果check成功p就是一个shared_ptr指向StrBlobPtr所指向的vector incr成员 也调用check StrBlobPtr StrBlobPtr::incr() {// 如果curr已经指向容器的尾后位置就不能递增它check(curr, increment past end of StrBlobPtr);curr; // 推进当前位置return *this; }为了访问data成员我们的指针类必须声明为StrBlob的friend 4、完整定义 StrBlobPtr更新 StrBlob 类加入恰当的 friend 声明以及 begin 和 end 成员并逐行读入一个输入文件将内容存入一个 StrBlob 中用一个 StrBlobPtr 打印出 StrBlob 中的每个元素 StrBlob_20.h #pragma once #ifndef STRBLOB_20_H #define STRBLOB_20_H#include string #include vector #include iostream #include memory #include initializer_list #include stdexceptclass StrBlobPtr;class StrBlob { public:friend class StrBlobPtr; // 声明友元这样可以使用StrBlob中的数据typedef std::vectorstd::string::size_type size_type;StrBlob() :data(std::make_sharedstd::vectorstd::string()) {};StrBlob(const std::initializer_liststd::string il);size_type size() const { return data-size(); } // data是指针bool empty() const { return data-empty(); }// 添加删除元素void push_back(const std::string t) { data-push_back(t); }void pop_back(); // 需要检查了// 元素访问std::string front();std::string front() const;std::string back();std::string back() const;// StrBlobPtr还没定义只是声明所以直接使用构造函数是不对的/*StrBlobPtr begin() { return StrBlobPtr(*this); }StrBlobPtr end(){auto ret StrBlobPtr(*this, data-size());return ret;}*/StrBlobPtr begin();StrBlobPtr end();private:std::shared_ptrstd::vectorstd::string data;void check(size_type i, const std::string msg) const; };// 对于访问一个不存在StrBlobStrBlobPtr抛出异常 // 除了检查之外还负责随机取出 StrBlob中存的信息 class StrBlobPtr { public:StrBlobPtr() :curr(0) {} // 构造函数后面不需要加StrBlobPtr(StrBlob a, size_t sz 0) :wptr(a.data), curr(sz) {}std::string deref() const; // 解引用StrBlobPtrStrBlobPtr incr(); // 前缀递增bool operator!(const StrBlobPtr p) { return p.curr ! curr; } // 重新定义运算符private:// 若检查确实存在check返回一个 指向vector的shared_ptrstd::shared_ptrstd::vectorstd::string check(std::size_t, const std::string) const;// 保存一个weak_ptr意味着 底层vector可能被销毁直接用vector初始化即可std::weak_ptrstd::vectorstd::string wptr;std::size_t curr; // 在数组中的当前位置 };StrBlob::StrBlob(const std::initializer_liststd::string il) :data(std::make_sharedstd::vectorstd::string(il)) {} // 构造函数也要StrBlob::void StrBlob::check(size_type i, const std::string msg) const { // 实现的时候也需要加constif (i data-size())throw std::out_of_range(msg); }void StrBlob::pop_back() {check(0, pop_back on empty StrBlob);data-pop_back(); }std::string StrBlob::front() {check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::front() const { // 对const进行重载check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::back() {check(0, back on empty StrBlob);return data-back(); }std::string StrBlob::back() const {check(0, back on empty StrBlob);return data-back(); }std::shared_ptrstd::vectorstd::string StrBlobPtr::check(std::size_t i, const std::string msg) const // msg不同错误不同信息 {auto ret wptr.lock(); // vector存在与否if (!ret)throw std::runtime_error(unbound StrBlobPtr);if (i ret-size())throw std::out_of_range(msg);return ret; // 返回指向vector的shared_ptr }std::string StrBlobPtr::deref() const {auto p check(curr, dereference past end);return (*p)[curr]; // *p是对象所指向的vector }StrBlobPtr StrBlobPtr::incr() {// 如果curr已经指向容器的尾后位置就不能递增它check(curr, increment past end of StrBlobPtr);curr; // 推进当前位置return *this; }StrBlobPtr StrBlob::begin() {return StrBlobPtr(*this); }StrBlobPtr StrBlob::end() {auto ret StrBlobPtr(*this, data-size());return ret; }#endif 12.20.cpp #include iostream #include fstream #include StrBlob_20.husing namespace std;int main() {ifstream ifs(data_20.txt);string s;StrBlob sb;while (getline(ifs, s)){sb.push_back(s);}for (StrBlobPtr beg StrBlobPtr(sb, 0), end StrBlobPtr(sb, sb.size()); beg ! end; beg.incr()) // 注意begend以及重载的运算符 !cout beg.deref() endl;// 等价for (StrBlobPtr beg sb.begin(), ed sb.end(); beg ! ed; beg.incr())// sb.begin()和sb.end()本来就构造好了当然包括currstd::cout beg.deref() std::endl;return 0; }data_20.txt c primer 5th C Primer 5th运行结果 2、动态数组 1、new和delete运算符 一次分配 / 释放一个对象但某些应用 需要一次 为很多对象分配内存的功能。例如vector 和 string都是 在连续内存中保存它们的元素因此当容器 需要重新分配内存时必须一次性 为很多元素分配内存 C语言 定义了 另一种new表达式语法可以分配 并 初始化一个对象数组。标准库中 包含一个名为 allocator的类允许我们将分配 和 初始化分离 2、使用容器的类 可以使用 默认版本的拷贝、赋值和析构操作。分配动态数组的类 则必须定义 自己版本的操作在拷贝、复制以及销毁对象时 管理所关联的内存 2.1 new和数组 1、要在类型名之后 跟一对方括号在其中指明 要分配的对象的数目。new分配要求数量的对象 并假定分配成功后返回指向 第一个对象的指针 //调用get_size确定分配多少个int int *pia new int[get_size()]; //pia指向第一个int方括号中的大小必须是整型但不必是常量 也可以 用一个表示数组类型的类型别名 来分配一个数组 这样new表达式中就不需要方括号了 typedef int arrT[42]; //arrT表示42个int的数组类型 int *p new arrT; //分配一个42个int的数组p指向第一个int2、分配一个数组 会得到一个元素类型的指针当 用new分配一个数组时我们 并未得到一个数组类型的对象而是得到一个数组元素类型的指针。即使 我们使用类型别名 定义了一个数组类型new 也不会分配一个数组类型的对象。在上例中我们正在分配一个数组的事实 甚至都是不可见的——连[num] 都没有。new 返回的是 一个元素类型的指针 由于分配的内存 并不是一个数组类型因此 不能对动态数组调用 begin或end。这些函数 使用数组维度 来返回 指向首元素和尾后元素的指针。出于相同的原因也不能用范围for语句 来处理 动态数组中的元素 动态数组并不是数组类型这是很重要的 3、初始化动态分配对象的数组默认情况下new分配的对象不管是 单个分配的还是数组中的都是默认初始化的。可以对数组中的元素 进行值初始化方法是 在大小之后 跟一对空括号 int *pia new int[10]; // 10个未初始化的int int *pia2 new int[10]();// 10个值初始化为0的int string *psa new string[10]; // 10个空string string *psa2 new string[10]();//10个空string在新标准中我们 还可以提供一个元素初始化器的花括号列表 //10个int分别 用列表中对应的初始化器初始化 int *pia3 new int[10](0,1,2,3,4,5,6,7,8,9}; //10个string前4个 用给定的初始化器初始化剩余的 进行值初始化 string *psa3 new string[10]{a, an, the, string(3, x)};与内置数组对象的列表初始化 一样初始化器 会用来初始化动态数组中 开始部分的元素。如果 初始化器数目小于元素数目剩余元素 将进行值初始化 如果初始化器数目 大于 元素数目则new表达式失败不会分配 任何内存。在本例中new 会抛出一个类型为bad_array_new_length的异常。类似 bad_alloc此类型定义在 头文件new中 虽然我们用空括号 对数组中元素进行值初始化但不能 在括号中 给出初始化器这意味着 不能用auto分配数组 虽然我们 不能创建 一个大小为0的静态数组对象但当n等于0时调用new[n]是合法的 char arr[0]; // 错误不能定义长度为0的数组 char *cp new char[0];//正确:但cp不能解引用当我们用new分配一个大小为0的数组时new返回 一个合法的非空指针。对于零长度的数组 来说此指针 就像尾后指针一 样可以像使用 尾后迭代器一样 使用这个指针。可以用此 指针进行比较操作。可以 向此指针加上或从此指针减去0 也可以 从此指针 减去自身从而得到0。但此指针 不能解引用——毕竟它不指向任何元素 4、释放动态数组使用一种特殊形式的delete——在指针前加上 一个空方括号对 delete p; //p必须指向一个动态分配的对象或为空 delete[]pa; //pa必须指向一个动态分配的数组或为空第二条语句 销毁pa指向的数组中的元素并释放对应的内存。数组中的元素按逆序销毁即最后一个元素首先被销毁然后是倒数第二个依此类推 如果我们在delete一个指向数组的指针时 忽略了方括号或者在delete一个 指向单一对象的指针时使用了方括号其行为是未定义的 当我们使用 一个类型别名 来定义 一个数组类型时在new表达式中 不使用[ ]。即使是这样在释放一个数组指针时 也必须使用方括号 typedef int arrT[42]; //arrT是42个int的数组的类型别名 int *p new arrT; //分配一个42个int的数组p指向第一个元素 delete [] p; //方括号是必需的因为我们当初分配的是一个数组5、智能指针和动态数组标准库提供了 一个可以管理new分配的数组的unique_ptr版本。为了用一个 unique_ptr 管理动态数组我们 必须在对象类型后面 跟一对空方括号 //up指向一个包含10个未初始化int的数组 unique_ptrint[] up(new int[10]); up.release(); //自动用delete[]销毁其指针类型说明符中的方括号int[ ]指出up指向一个int数组 而不是一个int。由于 up指向一个数组当up销毁它管理的指针时会自动使用delete[ ] 当一个unique_ptr指向一个数组时我们 不能使用点和箭头成员运算符。毕竟unique_ptr指向的是 一个数组而不是单个对象因此 这些运算符是无意义的。另一方面当一个unique_ptr指向一个数组时我们可以 使用下标运算符 来访问数组中的元素 6、指向数组的unique_ptr指向数组的 unique_ptr 不支持成员访问运算符点和箭头运算符 其他unique ptr操作不变 操作解释unique_ptrT[ ] uu可以指向 一个动态分配的数组数组元素类型为 Tunique_ptrT[ ] u§u指向 内置指针p所指向的 动态分配的数组。p必须能转换为类型T*u[i]返回u拥有的数组中 位置i处的对象u必须指向一个数组 与 unique_ptr 不同shared_ptr 不直接 支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组必须提供自己定义的删除器 //为了使用shared_ptr必须提供一个删除器 shared_ptrint sp(new int[10], [](int *p) { delete[] p; }); sp.reset(); //使用我们提供的lambda释放数组它使用delete[]如果 未提供删除器这段代码 将是未定义的。默认情况下shared_ptr 使用 delete 销毁它指向的对象 //shared_ptr未定义下标运算符并且不支持指针的算术运算 for (size_t i 0; i ! 10; i)*(sp.get() i) i; // 使用get获取一个内置指针shared_ptr 未定义 下标运算符而且智能指针类型 不支持 指针算术运算。因此为了访问数组中的元素必须用get 获取一个内置指针然后 用它来访问数组元素 7、连接两个字符串字面常量将结果保存在一个动态分配的char数组中。重写这个程序连接两个标准库string对象 用 new char[xx] 即可分配用来保存结果的 char 数组其中 xx 应该足以保存结果字符串。由于 C 风格字符串以 \0 结尾因此 xx 应不小于字符数加 1 #define _CRT_SECURE_NO_WARNINGS #include iostream #include string #include cstringusing namespace std;int main() {const char a[] aaa;const char b[] bbb;char *c new char[strlen(a) strlen(b) 1]; // new一个char数组返回指针strcpy(c, a);strcat(c, b); // C风格字符数组的拷贝和拼接cout string(c) endl;delete[]c;string sa aaa;string sb bbb;char* c2 new char[sa.size() sb.size() 1];// 使用 std::string 的 c_str() 函数获取 C 风格的字符串strcpy(c2, (sa sb).c_str());cout c2 endl;delete[]c2;std::string* c3 new std::string;*c3 sa sb;std::cout *c3 std::endl;delete c3;return 0; }从标准输入读取一个字符串存入一个动态分配的字符数组中。输入一个超出你分配的数组长度的字符串只会读入 指定大小的字符数组即采取了截断的方式。还可以采取其他处理方式如抛出异常 在动态分配字符数组的空间大小时记得加上字符数组结束符 \0 所占的空间 #include iostream #include cstringusing namespace std;int main() {int size 0;cin size;char* s new char[size 1];cin.ignore();cin.get(s, size 1);cout s endl;delete[]s;return 0; }2.2 allocator类 1、new 有一些灵活性上的局限其中一方面 表现在它将内存分配 和 对象构造组合在了一起。类似的delete 将对象析构 和 内存释放组合在了一起。我们分配单个对象时通常 希望将内存分配 和 对象初始化 组合在一起。因为 在这种情况下我们几乎肯定知道对象 应有什么值 当分配 一大块内存时我们通常 计划在这块内存上 按需构造对象。在此情况下我们希望 将内存分配 和 对象构造分离。这意味着 我们可以分配大块内存但只 在真正需要时才真正执行 对象创建操作同时付出一定开销 将内存分配和对象构造组合在一起 可能会导致不必要的浪费 string *const p new string[n]; //构造n个空string string s; string *q p; //指向第一个string while (cin s q ! p n)*q s; //赋予*q一个新值 const size_t size q - p; //记住我们读取了多少个string //使用数组 delete[] p; //p指向一个数组记得用delete[]来释放可能不需要 n个string少量 string 可能就足够了。这样我们就可能 创建了 一些永远也用不到的对象 而且对于 那些确实要使用的对象我们也在 初始化之后 立即赋予了它们新值。每个使用到的元素 都被赋值了两次第一次 是在默认初始化时随后 是在赋值时 更重要的是那些没有默认构造函数的类 就不能动态分配数组了 2、allocator类标准库 allocator类 定义在 头文件memory中它帮助我们 将内存分配 和 对象构造 分离开来。它提供一种类型感知的内存分配方法它分配的内存是 原始的、未构造的 allocator 是一个模板。为了定义一个 allocator对象我们必须指明 这个 allocator 可以分配的 对象类型。当一个 allocator对象 分配内存时它会根据 给定的对象类型 来确定恰当的内存大小 和 对齐位置 allocatorstring alloc; //可以分配string的allocator对象 auto const p alloc.allocate(n); //分配n个未初始化的string这个allocate调用 为n个string分配了内存 标准库allocator类 及其算法 操作解释allocatorT a定义了 一个名为a的allocator对象它可以为类型为T的对象 分配内存a.allocate(n)分配一段原始的、未构造的内存保存 n个类型为T的对象a.deallocate(p, n)释放从T*指针p中地址开始的内存这块内存 保存了n个类型为T的对象p必须是 一个先前由allocate返回的指针且n 必须是p创建时 所要求的大小。在调用deallocate之前用户必须对 每个在这块内存中创建的对象 调用destroya.construct(p, args)p必须是 一个类型为T*的指针指向一块原始内存arg被传递给 类型为T的构造函数用来在p指向的内存中 构造一个对象a.destroy§p为T*类型的指针此算法 对p指向的对象 执行析构函数 3、allocator 分配未构造的内存allocator 分配的内存 是未构造的。我们按需要 在此内存中构造对象。在新标准库中construct成员函数 接受一个指针 和 零个或多个额外参数在给定位置 构造一个元素。额外参数用来 初始化构造的对象。类似make_shared的参数这些额外参数 必须是与构造的对象的类型 相匹配的 合法的初始化器 auto q p; // q指向 最后构造的元素之后的位置 alloc.construct(q); //*q为空字符串 alloc.construct(q, 10, c); //*q为 cccccccccc alloc.construct(q, hi); //*q为hi在早期版本的标准库中construct 只接受 两个参数指向创建对象位置的指针 和 一个元素类型的值。因此我们 只能将一个元素 拷贝到 未构造空间中而不能 用元素类型的任何其他构造函数 来构造一个元素 还未构造对象的情况下 就使用原始内存是错误的 cout *p endl; //正确使用string的输出运算符 cout *q endl; //灾难q指向未构造的内存为了使用allocate返回的内存我们必须 用construct构造对象。使用未构造的内存其行为是未定义的 当我们用完对象后必须 对每个构造的元素 调用destroy来销毁它们。函数destroy接受一个指针对指向的对象 执行析构函数 while(q ! p)alloc.destroy(--q); //释放我们真正构造的string第一次 调用destroy时q指向 最后一个构造的元素。最后一步循环中 我们destroy了 第一个构造的元素随后 q将与p相等循环结束 只能对真正构造了的元素进行destroy操作 一旦元素被销毁后就可以 重新使用这部分内存 来保存其他string也可以将其归还给系统。释放内存 通过调用 deallocate来完成 alloc.deallocate(p, n);传递给 deallocate的指针 不能为空它必须指向 由allocate分配的内存。而且传递给 deallocate的大小参数 必须与调用allocate 分配内存时 提供的大小参数 具有一样的值 4、拷贝和填充 未初始化内存的算法标准库 还为allocator类 定义了 两个伴随算法可以在 未初始化内存中 创建对象 allocator算法都定义在头文件memory中 这些函数 在给定目的位置 创建元素而不是 由系统分配内存给它们 操作解释uninitialized_copy(b, e, b2)从迭代器b和e指出的输入范围中 拷贝元素到迭代器b2指定的 未构造的原始内存中。b2指向的内存 必须足够大能容纳输入序列中 元素的拷贝uninitialized_copy_n(b, n, b2)从迭代器b指向的元素开始拷贝n个元素 到b2开始的内存中uninitialized_fill(b, e, t)在迭代器b和e指定的原始内存范围中 创建对象对象的值均为t的拷贝uninitialized_fill_n(b, n, t)从迭代器b指向的内存地址 开始创建n个对象。b必须指向足够大的未构造的原始内存能够容纳给定数量的对象 将分配一块 比vector中元素所占用空间 大一倍的动态内存然后将 原vector中的元素 拷贝到 前一半空间对后一半空间 用一个给定值进行填充 //分配比vi中元素所占用空间大一倍的动态内存 auto p alloc.allocate(vi.size() * 2); //通过拷贝vi中的元素来构造从p开始的元素 auto q uninitialized_copy(vi.begin(), vi.end(), p); //将剩余元素初始化为42 uninitialized_fill_n(q, vi.size(), 42);uninitialized_copy 接受三个迭代器参数。前两个 表示输入序列第三个 表示这些元素 将要拷贝到的目的空间。传递给uninitialized_copy 的目的位置迭代器 必须指向 未构造的内存 uninitialized_copy 返回递增后的目的位置迭代器。因此一次 uninitialized_copy 调用 会返回一个指针指向 最后一个构造的元素之后的位置 在本例中我们将 此指针保存在q中然后将q传递给 uninitialized_fill_n 此函数类似 fill_n接受 一个指向目的位置的指针、一个计数 和 一个值。它会在 目的位置指针 指向的内存中 创建给定数目个对象用给定值 对它们进行初始化 #include iostream #include string #include memoryusing namespace std;int main() {allocatorstring alloc;auto const p alloc.allocate(3);auto q p;alloc.construct(q);alloc.construct(q, 10, c);alloc.construct(q, hi);while (q ! p) // 注意输出所有string写法{cout *(--q) endl;alloc.destroy(q); // 同时删除}alloc.deallocate(p, 3);return 0; }3、使用标准库文本查询程序 允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表 3.1 文本查询程序设计 1、开始一个程序的设计的一种好方法是列出程序的操作 当程序读取输入文件时它必须记住单词出现的每一行。因此程序需要逐行读取输入文件并将每一行分解为独立的单词当程序读取输入文件时 – 它必须能提取每个单词所关联的行号 – 行号必须按升序出现且无重复 –它必须能打印给定行号中的文本 实现这些要求 将使用一个vectorstring来保存整个输入文件的一份拷贝。输入文件中的 每行保存为vector中的一个元素。当需要打印一行时可以 用行号作为下标来 提取行文本使用一个 istringstream 来 将每行分解为单词使用一个set 来保存 每个单词在输入文本中出现的行号。这保证了 每行只出现一次 且 行号按升序保存使用一个map来将每个单词 与它出现的行号set关联起来。这样我们就可以方便地提取 任意单词的set 2、数据结构 从定义一个保存输入文件的类 开始将这个类命名为TextQuery它包含 一个vector和 一个map。vector用来保存 输入文件的文本map用来关联 每个单词和它出现的行号的set。这个类 将会有一个用来读取给定输入文件的构造函数 和一个执行查询的操作 查询操作 要完成的任务非常简单查找map成员检查给定单词是否出现。设计这个函数的难点 是确定应该返回什么内容。一旦找到了一个单词我们需要知道 它出现了多少次、它出现的行号以及每行的文本 返回所有这些内容的最简单的方法 是定义另一个类可以命名为 QueryResult来保存 查询结果。这个类会有 一个print函数完成 结果打印工作 3、在类之间共享数据由于 QueryResult 所需要的数据 都保存在 一个TextQuery对象中我们就必须 确定如何访问它们。我们可以 拷贝行号的set但这样做可能很耗时。而且我们当然不希望 拷贝vector因为这可能会 引起整个文件的拷贝而目标只不过是 为了打印文件的一小部分而已 通过返回指向 TextQuery对象内部的迭代器或指针我们可以避免 拷贝操作。如果 TextQuery对象 在对应的 QueryResult对象之前被销毁QueryResult 就将 引用一个不再存在的对象中的数据 对于 QueryResult对象 和对应的 TextQuery对象的生存期 应该同步这一观察结果考虑到这两个类概念上“共享”了数据可以使用 shared_ptr 来反映 数据结构中的这种共享关系 4、使用TextQuery类当我们设计一个类时在真正实现成员之前 先编写程序使用这个类是一种非常有用的方法。通过这种方法可以看到 类是否具有我们所需要的操作 #include Query.husing namespace std;void runQueries(ifstream ifs) {TextQuery tq(ifs);while (true) {cout enter word to look for, or q to quit: ;string s;if (!(cin s) || s q)break;QueryResult qr tq.query(s);qr.print();} }int main() {ifstream ifs(data_27.txt);runQueries(ifs);return 0; }3.2 文本查询程序类的定义 1、设计类的数据成员时需要考虑 与QueryResult对象共享数据的需求。QueryResult类 需要共享 保存输入文件的vector 和 保存单词关联的行号的set 因此这个类 应该有两个数据成员一个指向 动态分配的vector保存输入文件的 shared_ptr 和 一个string 到 shared_ptrset的map。map将文件中 每个单词关联到 一个动态分配的set上而此set 保存了 该单词所出现的行号 class QueryResult;class TextQuery { public:friend class QueryResult;TextQuery(std::ifstream ifs);QueryResult query(std::string word); private:std::shared_ptrstd::vectorstd::string vec; // 文件std::mapstd::string, std::shared_ptrstd::setint m; // value也需要std::shared_ptr };2、TextQuery构造函数由于vec是一个shared_ptr我们用-运算符解引用vec 来提取vec指向的vector对象的push_back成员 若str_after不在map中下标运算符 会将str_after添加到m中与str_after关联的值 进行值初始化。如果m[str_after] 为空我们分配 一个新的set并调用reset 更新m[str_after]的 shared_ptr使其 指向这个新分配的set 不管 是否创建了一个新的set我们都调用insert 将当前行号添加到set中 TextQuery::TextQuery(std::ifstream ifs):vec(new std::vectorstd::string) // new的那个vector被shared_ptr管理了 {std::string s;while (getline(ifs, s)) {std::istringstream iss(s);std::string str;vec-push_back(s);int lineNo vec-size();while (iss str) {std::string str_after;std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);if (m.find(str_after) m.end()) {m[str_after].reset(new std::setint); // shared_ptr用法}m[str_after]-insert(lineNo);}} }3、QueryResult类 class QueryResult { public:QueryResult(std::string s, std::shared_ptrstd::setint l, std::shared_ptrstd::vectorstd::string v) :word(s), lines(l), vec(v) {}void print(); private:std::shared_ptrstd::vectorstd::string vec; // 输入文件std::string word; // 查询单词std::shared_ptrstd::setint lines; // 出现的行号把map拆开来了 };query函数接受 一个string参数即 查询单词query用它来 在map中定位 对应的行号set。如果找到了 这个stringquery函数构造一个QueryResult保存给定 string、TextQuery的vec成员 以及 从m中提取的set 如果给定string未找到我们应该返回什么定义一个局部static对象自己实现中没有整成staticemp它是一个指向空的行号set 的shared_ptr。当未找到给定单词时我们返回此对象的一个拷贝 QueryResult TextQuery::query(std::string word) {std::shared_ptrstd::setint emp(new std::setint); // new的那个set被shared_ptr管理了if (m.find(word) m.end())return QueryResult(word, emp, vec);elsereturn QueryResult(word, m[word], vec); }4、打印结果 void QueryResult::print() {std::cout word occurs lines-size() times std::endl;int t lines-size();for (int i : *lines){std::cout (line i ) *(vec-begin() i - 1) std::endl;} }vec-begin() i - 1 即为vec指向的vector中的第i个位置的元素 此函数能正确处理 未找到单词的情况。在此情况下set为空。第一条输出语句 会注意到单词出现了0次。由于*res.lines为空for循环 一次也不会执行 5、完整程序 Query.h #pragma once #ifndef QUERY_H #define QUERY_H#include string #include iostream #include fstream #include vector #include sstream #include set #include map #include algorithm #include memoryclass QueryResult;class TextQuery { public:friend class QueryResult;TextQuery(std::ifstream ifs);QueryResult query(std::string word); private:std::shared_ptrstd::vectorstd::string vec;std::mapstd::string, std::shared_ptrstd::setint m; // value也需要std::shared_ptr };class QueryResult { public:QueryResult(std::string s, std::shared_ptrstd::setint l, std::shared_ptrstd::vectorstd::string v) :word(s), lines(l), vec(v) {}void print(); private:std::shared_ptrstd::vectorstd::string vec;std::string word;std::shared_ptrstd::setint lines; };TextQuery::TextQuery(std::ifstream ifs):vec(new std::vectorstd::string) // new的那个vector被shared_ptr管理了 {std::string s;while (getline(ifs, s)) {std::istringstream iss(s);std::string str;vec-push_back(s);int lineNo vec-size();while (iss str) {std::string str_after;std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);if (m.find(str_after) m.end()) {m[str_after].reset(new std::setint); // shared_ptr用法}m[str_after]-insert(lineNo);}} }QueryResult TextQuery::query(std::string word) {std::shared_ptrstd::setint emp(new std::setint); // new的那个set被shared_ptr管理了if (m.find(word) m.end())return QueryResult(word, emp, vec);elsereturn QueryResult(word, m[word], vec); }void QueryResult::print() {std::cout word occurs lines-size() times std::endl;int t lines-size();for (int i : *lines){std::cout (line i ) *(vec-begin() i - 1) std::endl;} }#endif12.27.cpp #include Query.husing namespace std;void runQueries(ifstream ifs) {TextQuery tq(ifs);while (true) {cout enter word to look for, or q to quit: ;string s;if (!(cin s) || s q)break;QueryResult qr tq.query(s);qr.print();} }int main() {ifstream ifs(data_27.txt);runQueries(ifs);return 0; }data_27.txt c primer 5th c primer 3th example. Cplusplus primer example, example primer运行结果 6、如果用vector 代替 set 保存行号会有什么差别哪个方法更好 vector 更好。因为虽然 vector 不会维护元素值的序set 会维护关键字的序但注意到我们是逐行读取输入文本的因此每个单词出现的行号是自然按升序加入到容器中的不必特意用关联容器来保证行号的升序。而从性能角度set 是基于红黑树实现的插入操作时间复杂性为 O(logn)n 为容器中元素数目而 vector 的 push_back 可达到常量时间 另外一个单词在同一行中可能出现多次。set 自然可保证关键字不重复但对 vector 这也不成为障碍 —— 每次添加行号前与最后一个行号比较一下即可。总体性能仍然是 vector 更优 7、重写 TextQuery 和 QueryResult类用StrBlob 代替 vector 保存输入文件 Query_32.h #pragma once #ifndef QUERY_32_H #define QUERY_32_H#include string #include iostream #include fstream #include vector #include sstream #include set #include map #include algorithm #include memory #include StrBlob_20.hclass QueryResult;class TextQuery { public:friend class QueryResult;TextQuery(std::ifstream ifs);QueryResult query(std::string word); private:StrBlob file;std::mapstd::string, std::shared_ptrstd::setint m; // value也需要std::shared_ptr };class QueryResult { public:QueryResult(std::string s, std::shared_ptrstd::setint l, StrBlob sb) :word(s), lines(l), file(sb) {}void print(); private:StrBlob file;std::string word;std::shared_ptrstd::setint lines; };TextQuery::TextQuery(std::ifstream ifs) :file(StrBlob()) // new的那个vector被shared_ptr管理了 {std::string s;while (getline(ifs, s)) {std::istringstream iss(s);std::string str;file.push_back(s);int lineNo file.size();while (iss str) {std::string str_after;std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);if (m.find(str_after) m.end()) {m[str_after].reset(new std::setint); // shared_ptr用法}m[str_after]-insert(lineNo);}} }QueryResult TextQuery::query(std::string word) {std::shared_ptrstd::setint emp(new std::setint); // new的那个set被shared_ptr管理了if (m.find(word) m.end())return QueryResult(word, emp, file);elsereturn QueryResult(word, m[word], file); }void QueryResult::print() {std::cout word occurs lines-size() times std::endl;int t lines-size();for (int i : *lines)// 利用StrBlobPtr类 取 指定行的数据file只是为了记录对应行的数据{StrBlobPtr sbp(file, i - 1);std::cout (line i ) sbp.deref() std::endl;} }#endifStrBlob_20.h #pragma once #ifndef STRBLOB_20_H #define STRBLOB_20_H#include string #include vector #include iostream #include memory #include initializer_list #include stdexceptclass StrBlobPtr;class StrBlob { public:friend class StrBlobPtr; // 声明友元这样可以使用StrBlob中的数据typedef std::vectorstd::string::size_type size_type;StrBlob() :data(std::make_sharedstd::vectorstd::string()) {};StrBlob(const std::initializer_liststd::string il);size_type size() const { return data-size(); } // data是指针bool empty() const { return data-empty(); }// 添加删除元素void push_back(const std::string t) { data-push_back(t); }void pop_back(); // 需要检查了// 元素访问std::string front();std::string front() const;std::string back();std::string back() const;// StrBlobPtr还没定义只是声明所以直接使用构造函数是不对的/*StrBlobPtr begin() { return StrBlobPtr(*this); }StrBlobPtr end(){auto ret StrBlobPtr(*this, data-size());return ret;}*/StrBlobPtr begin();StrBlobPtr end();private:std::shared_ptrstd::vectorstd::string data;void check(size_type i, const std::string msg) const; };// 对于访问一个不存在StrBlobStrBlobPtr抛出异常 // 除了检查之外还负责随机取出 StrBlob中存的信息 class StrBlobPtr { public:StrBlobPtr() :curr(0) {} // 构造函数后面不需要加StrBlobPtr(StrBlob a, size_t sz 0) :wptr(a.data), curr(sz) {}std::string deref() const; // 解引用StrBlobPtrStrBlobPtr incr(); // 前缀递增bool operator!(const StrBlobPtr p) { return p.curr ! curr; } // 重新定义运算符private:// 若检查确实存在check返回一个 指向vector的shared_ptrstd::shared_ptrstd::vectorstd::string check(std::size_t, const std::string) const;// 保存一个weak_ptr意味着 底层vector可能被销毁直接用vector初始化即可std::weak_ptrstd::vectorstd::string wptr;std::size_t curr; // 在数组中的当前位置 };StrBlob::StrBlob(const std::initializer_liststd::string il) :data(std::make_sharedstd::vectorstd::string(il)) {} // 构造函数也要StrBlob::void StrBlob::check(size_type i, const std::string msg) const { // 实现的时候也需要加constif (i data-size())throw std::out_of_range(msg); }void StrBlob::pop_back() {check(0, pop_back on empty StrBlob);data-pop_back(); }std::string StrBlob::front() {check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::front() const { // 对const进行重载check(0, front on empty StrBlob);return data-front(); }std::string StrBlob::back() {check(0, back on empty StrBlob);return data-back(); }std::string StrBlob::back() const {check(0, back on empty StrBlob);return data-back(); }std::shared_ptrstd::vectorstd::string StrBlobPtr::check(std::size_t i, const std::string msg) const // msg不同错误不同信息 {auto ret wptr.lock(); // vector存在与否if (!ret)throw std::runtime_error(unbound StrBlobPtr);if (i ret-size())throw std::out_of_range(msg);return ret; // 返回指向vector的shared_ptr }std::string StrBlobPtr::deref() const {auto p check(curr, dereference past end);return (*p)[curr]; // *p是对象所指向的vector }StrBlobPtr StrBlobPtr::incr() {// 如果curr已经指向容器的尾后位置就不能递增它check(curr, increment past end of StrBlobPtr);curr; // 推进当前位置return *this; }StrBlobPtr StrBlob::begin() {return StrBlobPtr(*this); }StrBlobPtr StrBlob::end() {auto ret StrBlobPtr(*this, data-size());return ret; }#endif 12.32.cpp跟12.27.cpp一致数据结构的变化 不影响使用 #include Query_32.husing namespace std;void runQueries(ifstream ifs) {TextQuery tq(ifs);while (true) {cout enter word to look for, or q to quit: ;string s;if (!(cin s) || s q)break;QueryResult qr tq.query(s);qr.print();} }int main() {ifstream ifs(data_27.txt);runQueries(ifs);return 0; }术语表 1、空悬指针指向 曾经保存一个对象 但现在已释放的内存。空悬指针 引起的程序错误 非常难以调试 2、析构函数特殊的成员函数负责 在对象离开作用域 或 被释放时完成 清理工作 3、动态分配在自由空间中 分配的对象。在自由空间中分配的对象 直到被显式释放 或 被释放时 完成清理工作 4、自由空间程序可用的内存池保存 动态分配的对象 5、定位new一种new表达式形式接受一些额外参数在new关键字 后面的括号中给出。例如new(nothrow) int 告诉new不要抛出异常
http://www.zqtcl.cn/news/603005/

相关文章:

  • 沈阳网站哪家公司做的好招标信息发布
  • 兰州企业网站h5页面用什么软件
  • 东莞自助建站软件ppt怎么做 pc下载网站
  • 兴化网站建设价格怎样用自己的电脑,做网站
  • 东莞网站建设企慕网站名称 注册
  • 佛山网站建设服务商百度推广客户端手机版下载
  • 做网站找个人还是找公司wordpress jiathis
  • 淘宝客推广网站建设百度云wordpress转服务器
  • 网站构建代码模板怎么在云服务器上建设网站
  • 国内产品网站建设游戏创造器
  • 北京南站到北京站怎么走南宁美丽南方官方网站建设意见
  • 网站建设捌金手指专业5电商运营怎么推广一个新品
  • 医院网站建设企业走廊文化建设图片网站
  • 学网站建设培训机构公司网站建立费用
  • 阜宁网站制作服务商自学网站开发设计
  • 湖南建设监理工程网站设计类招聘网站
  • 门户网站建设的平台搭建长春专业网站建设推广
  • 网站建设宗旨怎么写网站建设公司外链怎么做
  • 绍兴市交通建设检测中心网站seo专业培训网络班
  • 设计国外网站有哪些玉环在哪里做网站
  • 设计网站思路如何写wordpress 修改登录
  • 网站开发php国外设计网站app吗
  • 智能响应式网站淳安县住房和城乡建设局网站
  • 招投标网站开发企业网站建设之后
  • 如何做好集团网站建设怎么做门淘宝网站
  • 医疗协助平台网站建设方案学生个人网页制作html报告
  • 专注于网站营销服务新浪云搭建wordpress
  • 免费自助建站代理鞍山招聘网最新招聘
  • 营销型类型网站有哪些类型php网站后台制作
  • 安全的网站制作公司百度app最新版本