0%

一元运算符运算顺序从右向左。

练习14.3:string和vector都定义了重载的==以比较各自的对象,假设svec1和svec2是存放string的vector,确定在下面的表达式中分别使用了哪个版本的==?(a) 应用了C++语言内置版本的==,比较两个指针。

阅读全文 »

第十二章 动态内存

  • 对象的生命周期:
    • 全局对象在程序启动时分配,结束时销毁。
    • 局部对象在进入程序块时创建,离开块时销毁。
    • 局部static对象在第一次使用前分配,在程序结束时销毁。
    • 动态分配对象:只能显式地被释放。
  • 对象的内存位置:
    • 静态内存用来保存局部static对象、类static对象、定义在任何函数之外的变量。
    • 栈内存用来保存定义在函数内的非static对象。
    • 堆内存,又称自由空间,用来存储动态分配的对象。

动态内存与智能指针

  • 动态内存管理:
    • new:在动态内存中为对象分配空间并返回一个指向该对象的指针。
    • delete:接受一个动态对象的指针(new分配或空指针),销毁该对象,并释放与之关联的内存。
  • 智能指针:
    • 管理动态对象。
    • 行为类似常规指针。
    • 负责自动释放所指向的对象。
    • 智能指针也是模板。

shared_ptr类

shared_ptr和unique_ptr都支持的操作

操作解释
shared_ptr<T> sp unique_ptr<T> up空智能指针,可以指向类型是T的对象
p.get()返回p中保存的指针,要小心使用,若智能指针释放了对象,返回的指针所指向的对象也就消失了。

shared_ptr独有的操作

操作解释
make_shared<T>(args)返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象。
shared_ptr<T>p(q)pshared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = qpq都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
p.unique()p.use_count()是1,返回true;否则返回false
p.use_count()返回与p共享对象的智能指针数量;可能很慢,主要用于调试。
  • 使用动态内存的三种原因
    • 程序不知道自己需要使用多少对象(比如容器类)。
    • 程序不知道所需要对象的准确类型。
    • 程序需要在多个对象间共享数据。

直接管理内存

  • new动态分配和初始化对象。
    • new无法为分配的对象命名(因为自由空间分配的内存是无名的),因此是返回一个指向该对象的指针。
    • int *pi = new int([123]); 不写123默认值初始化。
    • vector<int> *pv = new vector<int>{1, 2, 3};
    • 一旦内存耗尽,会抛出类型是bad_alloc的异常。
  • delete将动态内存归还给系统。
    • 接受一个指针,指向要释放的对象。
    • delete后的指针称为空悬指针(dangling pointer)。
  • 使用newdelete管理动态内存存在三个常见问题:
    • 1.忘记delete内存。
    • 2.使用已经释放掉的对象。
    • 3.同一块内存释放两次。
  • 坚持只使用智能指针可以避免上述所有问题。

shared_ptr和new结合使用

定义和改变shared_ptr的其他方法

操作解释
shared_ptr<T> p(q)p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)punique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d)p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d)pshared_ptr p2的拷贝,唯一的区别是p将可调用对象d来代替delete
p.reset()p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
p.reset(q)同上
p.reset(q, d)同上

智能指针和异常

  • 如果使用智能指针,即使程序块由于异常过早结束,智能指针类也能确保在内存不需要的时候将其释放。
  • 智能指针陷阱
    • 不用相同的内置指针初始化(或reset)多个智能指针
    • delete get()返回的指针。
    • 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

  • 某一个时刻只能有一个unique_ptr指向一个给定的对象。
  • 不支持拷贝或者赋值操作。但可以:p1.reset(p2.release();
  • 向后兼容:auto_ptr:老版本,具有unique_ptr的部分特性。特别是,不能在容器中保存auto_ptr,也不能从函数返回auto_ptr

unique_ptr操作:

操作解释
unique_ptr<T> u1unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T, D> u2u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T, D> u(d)unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr释放u指向的对象,将u置为空。
u.release()u放弃对指针的控制权,返回指针,并将u置空。
u.reset()释放u指向的对象
u.reset(q)u指向q指向的对象
u.reset(nullptr)u置空

weak_ptr

  • weak_ptr是一种不控制所指向对象生存期的智能指针。
  • 指向一个由shared_ptr管理的对象,不改变shared_ptr的引用计数。
  • 一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,不管有没有weak_ptr指向该对象。

weak_ptr操作:

操作解释
weak_ptr<T> wweak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp)shared_ptr指向相同对象的weak_ptrT必须能转换为sp指向的类型。
w = pp可以是shared_ptr或一个weak_ptr。赋值后wp共享对象。
w.reset()w置为空。
w.use_count()w共享对象的shared_ptr的数量。
w.expired()w.use_count()为0,返回true,否则返回false
w.lock()如果expiredtrue,则返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

动态数组

new和数组

  • new一个动态数组:
    • 类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量)。
    • 返回指向第一个对象的指针
    • int *p = new int[size];
  • delete一个动态数组:
    • delete [] p;
  • unique_ptr和数组:
    • 指向数组的unique_ptr不支持成员访问运算符(点和箭头)。
操作解释
unique_ptr<T[]> uu可以指向一个动态分配的数组,整数元素类型为T
unique_ptr<T[]> u(p)u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i]返回u拥有的数组中位置i处的对象。u必须指向一个数组。

allocator类

  • 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开。
  • 分配的是原始的、未构造的内存。
  • allocator是一个模板。
  • allocator<string> alloc;

标准库allocator类及其算法

操作解释
allocator<T> a定义了一个名为aallocator对象,它可以为类型为T的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为T的对象。
a.deallocate(p, n)释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针。且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args)p必须是一个类型是T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p)pT*类型的指针,此算法对p指向的对象执行析构函数。

allocator伴随算法

操作解释
uninitialized_copy(b, e, b2)从迭代器be给定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2)从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t)在迭代器be执行的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t)从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
  • 定义在头文件memory中。
  • 在给定目的位置创建元素,而不是由系统分配内存给他们。

第十三章 拷贝控制

拷贝控制操作(copy control):

  • 拷贝构造函数(copy constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动构造函数(move constructor)
  • 移动赋值函数(move-assignement operator)
  • 析构函数(destructor)

拷贝、赋值和销毁

拷贝构造函数

  • 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
  • 引用的原因:传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。
  • class Foo{ public: Foo(const Foo&); }
  • 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
  • 拷贝初始化
    • 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
    • 通常使用拷贝构造函数完成。
    • string book = "9-99";
    • 出现场景:
      • = 定义变量时。
      • 将一个对象作为实参传递给一个非引用类型的形参。
      • 从一个返回类型为非引用类型的函数返回一个对象。
      • 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。花括号内元素个数是拷贝构造次数。

拷贝赋值运算符

  • 重载赋值运算符
    • 重写一个名为operator=的函数.
    • 通常返回一个指向其左侧运算对象的引用。
    • Foo& operator=(const Foo&);
  • 合成拷贝赋值运算符
    • 一个类未定义自己的拷贝赋值运算符,默认生成。
    • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。

析构函数

  • 释放对象所使用的资源,并销毁对象的非static数据成员。
  • 名字由波浪号接类名构成。没有返回值,也不接受参数。
  • ~Foo();
  • 调用时机:
    • 变量在离开其作用域时。
    • 当一个对象被销毁时,其成员被销毁。
    • 容器被销毁时,其元素被销毁。
    • 动态分配的对象,当对指向它的指针应用delete运算符时。
    • 对于临时对象,当创建它的完整表达式结束时。
  • 合成析构函数
    • 空函数体执行完后,成员会被自动销毁。
    • 注意:析构函数体本身并不直接销毁成员。
    • 合成析构函数并不删除指针成员所指向的对象,它需要程序员显式编写析构函数去处理。

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作。

    • 一个对象拥有额外的资源(指针指向的内存),但另一个对象使用默认的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源。
  • 需要拷贝操作的类也需要赋值操作,反之亦然。

  • 13.1.4练习

    • 合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。1 1 1

    • 定义变量和函数传参时,两次调用拷贝构造函数。4 5 6

    • 引用,不拷贝。1 2 3

    • ```cpp
      #include
      int unique;

      class numbered {
      public:
      numbered() {mysn = ++unique;}
      // numbered(numbered &d): mysn(++unique) {}

        int mysn;

      };

      void f(numbered s) {
      std::cout << s.mysn << std::endl;
      }

      int main() {
      numbered a, b = a, c = b;
      f(a);
      f(b);
      f(c);
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60

      ### 使用=default

      - 可以通过将拷贝控制成员定义为`=default`来显式地要求编译器生成合成的版本。
      - 合成的函数将隐式地声明为内联的。

      ### 阻止拷贝

      - 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
      - 定义删除的函数:`=delete`。
      - 虽然声明了它们,但是不能以任何方式使用它们。
      - 析构函数不能是删除的成员。
      - 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
      - 老版本使用`private`声明来阻止拷贝。

      ## 拷贝控制和资源管理

      - 类的行为可以像一个值,也可以像一个指针。
      - 行为像值:对象有自己的状态,副本和原对象是完全独立的。
      - 编写赋值运算符:如果将一个对象赋予它自身,赋值运算符必须能正确工作。
      - 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。

      ## 交换操作

      - 管理资源的类通常还定义一个名为`swap`的函数。
      - 经常用于重排元素顺序的算法。
      - 用`swap`而不是`std::swap`。

      ## 对象移动

      - 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
      - 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
      - 标准库容器、`string`和`shared_ptr`类既可以支持移动也支持拷贝。`IO`类和`unique_ptr`类可以移动但不能拷贝。

      ### 右值引用

      - 新标准引入右值引用以支持移动操作。
      - 通过`&&`获得右值引用。
      - 只能绑定到一个将要销毁的对象,右值引用变量不能直接绑定到右值引用上。
      - 常规引用可以称之为左值引用。
      - 左值持久,右值短暂。

      **move函数**:

      - `int &&rr2 = std::move(rr1);`
      - `qmove`告诉编译器,我们有一个左值,但我希望像右值一样处理它。
      - 调用`move`意味着:除了对`rr1`赋值或者销毁它外,我们将不再使用它。

      ### 移动构造函数和移动赋值运算符

      - **移动构造函数**:

      - 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个**右值引用**。

      - ```cpp
      StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements), first_free(s.first_free), cap(s.cap) {
      // 置于可析构状态
      s.elements = s.first_free = s.cap = nullptr;
      // 然后析构
      }

    • 不分配任何新内存,只是接管给定的内存。

  • 移动赋值运算符

    • StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
    • 检测自赋值的情况
  • 移动右值(移动构造函数),拷贝左值(拷贝构造函数)。

  • 如果没有移动构造函数,右值也被拷贝。

  • 拷贝并交换赋值运算符:根据实参类型,既可拷贝又可移动。

  • 更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

  • 移动迭代器:

    • make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。
  • 建议:小心地使用移动操作,以获得性能提升。

右值引用和成员函数

  • 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&
  • 引用限定符:
    • 在参数列表后面加一个&,限定只能向可修改的左值赋值而不能向右值赋值。加&&同理。