Primer c++ 笔记汇总(三)

第十二章 动态内存

  • 静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存用来保存定义在函数内的非static对象。
    • 分配在静态或栈内存中的对象由编译器自动创建和销毁
    • 栈对象仅在其定义的程序块运行时才存在
    • static对象在使用之前分配,在程序结束时销毁
  • 动态内存
    • C++中,动态内存的管理是通过一对运算符来完成的
      • new 在动态内存中为对象分配空间并返回一个指向对象的指针
      • delete 接受一个动态对象的指针,销毁对象,并释放与之关联的内存
  • 智能指针:定义在memory头文件中
    • shared_ptr
      • 允许多个指针指向同一个对象
    • unique_ptr
      • 独占所指向的对象
    • weak_ptr
      • 是一种弱引用,指向shared_ptr所管理的对象
  • shared_ptr 与 unique_ptr 都支持的操作
    • shared_ptr sp
      • 空智能指针,可以指向类型为T的对象
    • unique_ptr up
    • p
      • 将p作用一个条件判断,若p指向一个对象,则为true
    • *p
      • 解引用p,获得它指向的对象
    • p->mem
      • 等价于(*p).mem
    • p.get()
      • 返回p中保持的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象就会消失了
    • swap(p, q)
      • 交换p和q中的指针
    • p.swap(q)
  • shared_ptr 独有的操作
    • make_shared(args)
      • 返回一个shared_ptr, 指向一个动态分配的类型为T的对象。使用args初始化此对象
    • shared_ptr p(q)
      • p是shared_ptr q的拷贝
        • 此操作会递增q中的计数器
        • q中的指针必须能转换为T*
    • p = q
      • p和q都是shared_ptr,所保存的指针必须能互相转换
      • 此操作会递减p的引用计数,递增q的引用计数
      • 若p的引用计数变为0,则将其管理的原内存释放
    • p.unique()
      • 若p.use_count()为1,返回true;否则返回false
    • p.use_count()
      • 返回与p共享对象的智能指针数量;
      • 可能很慢,主要用于调试
  • make_shared函数
    • 最安全的分配和使用动态内存的方法
  • 使用动态内存的原因
    • 程序不知道自己需要使用多少对象
    • 程序不知道所需对象的准确类型
    • 程序需要在多个对象间共享数据
  • 使用动态内存的一个常见原因是允许多个对象共享相同的状态
  • 动态分配的const对象
    • cont int *pci = new const int(1024);
  • 内存耗尽
    • int *p1 = new int; 如果分配失败,new抛出std::bad_alloc
    • int *p2 = new (nothrow) int; 如果分配失败,new返回一个空指针
  • 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
  • 使用new 和 delete 管理动态内存三个常见问题
    • 忘记delete内存
    • 使用已经释放掉的对象
    • 同一块内存释放两次
  • 如果需要保留指针,可以在delete之后将nullptr赋予指针
  • 定义和改变shared_ptr的其他方法
    • shared_ptr p(q)
      • p管理内置指针q所指向的对象
      • q必须指向new分配的内存,且能转换为T*类型
    • shared_ptr p(u)
      • p从unique_ptr u那里接管了对象的所有权
      • 将u置为空
    • shared_ptr p(q, d)
      • p接管了内置指针q所指向的对象的所有权
      • q必须能转换为T*类型
      • p将使用可调用对象d来代替delete
    • shared_ptr p(p2 , d)
      • p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete
    • p.reset() / p.reset(q) / p.reset(q, d)
      • 若p是唯一指向其对象的shared_ptr,reset会释放此对象
      • 若传递了可选的参数内置指针q,会令p指向q
      • 否则会将p置为空
      • 若还传递了参数d,将会调用d而不是delete来释放q
  • 使用一个内置指针来访问一个智能指针所负责对象是很危险的,因为我们无法知道对象何时会被销毁
  • get 用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。
    • 永远不要用get初始化另一个智能指针或者为另一个智能指针赋值
  • 其他shared_ptr操作
    • p.reset(new int(1024));
  • 如果在new和delete之间发生异常,且异常未在函数中被捕获,则内存永远不会被释放
    • 因为在函数之外没有指针指向这块内存,也就无法释放它
  • 使用智能指针的规范
    • 不使用相同的内置指针初始化(或reset)多个智能指针
    • 不delete get() 返回的指针
    • 不使用get()初始化或reset另一个智能指针
    • 如果你使用get()返回指针,记住当最后一个对应的智能指针销毁后,该指针就变为无效了
    • 如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
  • unique_ptr
    • unique_ptr<int> p2(new int(42));
    • unique_ptr不支持普通的拷贝或赋值操作
    • 操作
      • unique_ptr u1
      • unique_ptr<T, D> u2
        • 空unique_ptr,可以指向类型为T的对象
        • u1会使用delete来释放它的指针;
        • u2会使用一个类型为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)
        • 如果提供了内置指针q,令u指向这个对象;否则将u置为空
      • u.reset(nullptr)
      • 虽然不能拷贝和赋值unique_ptr, 但是可以通过调用release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr
    • 虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr
  • weak_ptr
    • 是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。
    • 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
    • 即使由weak_ptr指向对象,shared_ptr被销毁,对象还是会被释放
    • 操作
      • weak_ptr w
        • 空 weak_ptr可以指向类型为T的对象
      • weak_ptr w(sp)
        • 与shared_ptr sp指向相同对象的 weak_ptr。T必须能转换成sp指向的类型
      • w = p
        • p可以是一个shared_ptr或一个 weak_ptr
        • 赋值后w与p共享对象
      • w.reset()
        • 将w置为空
      • w.use_count()
        • 与w共享对象的shared_ptr的数量
      • w.expored()
        • 若w.use_count()为0, 返回true,否则返回false
      • w.lock()
        • 如果expired为true, 返回一个空shared_ptr
        • 否则返回一个指向w的对象的shared_ptr
    • 由于对象可能不存在,我们不能使用 weak_ptr直接访问对象,而必须使用lock
      • 此函数检查 weak_ptr指向的对象是否存在
      • 如果存在,lock返回一个指向共享对象的shared_ptr
  • 动态数组
    • C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组
    • 标准库中包含一个名为allocator的类,允许我们将分配和初始化分离
      • 使用allocator通常会提供更好的性能和灵活的内存管理能力
    • 大多数应用应该使用标准库容器而不是动态分配的数组
      • 使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能
    • 虽然通常我们常称new T[] 分配的内存为“动态数组”, 但实际上用new分配一个数组时,我们并未得到一个数组类型的对象
      • 我们所说的动态数组并不是数组类型
    • 初始化动态分配对象的数组
      • 默认情况下,new分配的对象,都是默认初始化的。
      • 可以对数组中元素进行值初始化,方法是在大小之后跟一对空括号
        • int *pia = new int[10]; 10个未初始化的int
        • int *pia2 = new int[10](); 10个值初始化为0的int
      • 新标准中,可以提供一个元素初始化器的花括号列表
        • int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    • 释放动态数组
      • delete p;
        • p必须指向一个动态分配的对象或为空
      • delete [] pa;
        • pa必须指向一个动态分配的数组或为空
        • 销毁顺序是逆序
          • 最后一个元素先被销毁,然后倒数第二个,以此类推
      • 如果delete一个数组指针忘记了方括号,或者delete一个单一对象的指针使用了方括号,编译器可能不会给出警告
  • 智能指针和动态数组
    • 标准库提供了一个可以管理new分配的数组的unique_ptr版本
      • 为了用一个unique_ptr管理动态数组,必须在对象类型后面跟一对空方括号
        • unique_ptr<int []> up(new int[10]);
        • up.release(); 自动用delete[]销毁其指针
      • 当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符
      • 当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素
    • 指向数组的unique_ptr
      • 不支持成员访问运算符(点和箭头运算符)
      • unique_ptr<T[ ]> u
        • u可以指向一个动态分配的数组,数组元素类型为T
      • unique_ptr<T[ ]> u( p)
        • u指向内置指针p所指向的动态分配的数组
        • p必须能转换为类型T*
      • u[i]
        • 返回u拥有的数组中位置i处的对象
        • u必须指向一个数组
  • 与unique_ptr不同, shared_ptr不直接支持管理动态数组
    • 如果要支持,必须提供自己定义的删除器
      • shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});
      • sp.reset();
    • shared_ptr未定义下标运算符,而智能指针类型不支持指针算术运算符
      • 因此,为了访问数组中的元素,必须使用get获取一个内置指针,然后用它来访问数组元素
  • allocator
    • new有一些灵活性上的局限性
      • 它将内存分配和对象构造组合在一起
      • delete 将对象析构和内存释放组合在一起
    • 当分配一大块内存时,我们通常计划在这块内存上按需构造对象
      • 希望内存分配和对象构造分离
        • 这样可以分配大块内存,但只在真正需要时才真正执行对象创建操作
    • allocator类定义在头文件memory中
    • allocator是一个模板
      • 必须指明这个allocator可以分配的对象类型
      • 当一个allocator对象分配内存时,它会根据给定对象类型来确定恰当的内存大小和对齐位置
      • allocator<string> alloc; 可以分配string的allocator对象
      • auto const p = alloc.allocate(n); 分配n个未初始化的string,为n个string分配内存
      • allocator<T> a
        • 定义了一个名为a的allocator对象,它可以为类型为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)
        • p为T*类型的指针, 此算法对p指向的对象执行析构函数
      • 为了使用allocate返回的内存,我们必须用construct构造对象
        • 使用未构造的内存,其行为是未定义的
      • 我们只能对真正构造了的元素进行destroy操作
      • 一旦元素被销毁后,就可以重新使用这部分内存来保存其他T类型的对象,也可以将其归还给系统。
        • 释放内存通过调用deallocate来完成
    • 算法
      • 这些函数在给定目的位置创建元素,而不是由系统分配内存给他们
      • 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必须指向足够大的未构造的原始内存。能够容纳给定数量的对象

第十三章 拷贝控制

  • 五种特殊成员函数来控制对象拷贝、移动、赋值、和销毁
    • 拷贝构造函数
    • 拷贝赋值运算符
    • 移动构造函数
    • 移动赋值运算符
    • 析构函数
  • 上述操作称为拷贝控制操作
  • 拷贝构造函数
    • 拷贝构造函数的第一个参数必须是引用类型
      • Foo(const Foo&);
    • 如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝
  • 直接初始化和拷贝初始化之间的差异
    • string dots(10, '.'); 直接初始化
    • string s(dots); 直接初始化
    • string s2 = dots; 拷贝初始化
    • string null_book = "9-999-9999-9"; 拷贝初始化
    • string nines = string(100, '9'); 拷贝初始化
  • 如果一个类有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成
  • 拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生
    • 将一个对象作为实参传递给一个非引用类型的形参
    • 从一个返回类型为非引用类型的函数返回一个对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 拷贝构造函数被用来初始化非引用类类型参数的原因
    • 调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需调用拷贝构造函数,如此无限循环
  • 拷贝初始化的限制
    • 如果使用的初始值要求通过一个explicit的构造函数类进行类型转换,那么使用拷贝初始化还是直接初始化就无关紧要了
    • 如果希望使用一个explicit构造函数,就必须显式的使用
      • vector<int> v1(10); 显式
      • vector<int> v2 = 10; 错误,隐式
  • 编译绕过拷贝构造函数
    • string null_book = "9-999-9999-9"; 拷贝初始化
    • string null_book("9-999-9999-9"); 绕过拷贝构造函数
    • 绕过拷贝构造函数,拷贝/移动构造函数必须存在且可访问的
  • 拷贝赋值运算符
    • 与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个
    • operator关键字后接表示要定义的运算符
    • 赋值运算符通常返回一个指向左侧运算对象的引用
  • 析构函数
    • 构造函数初始化对象的非static数据成员
    • 析构函数释放对象使用的资源,并销毁对象的非static数据成员
      • 它没有返回值,也不接受参数
      • 因此不能被重载,对于一个给定类,只有唯一一个析构函数
    • 销毁成员,成员按初始化顺序的逆序销毁
    • 销毁类类型的成员需要执行成员自己的析构函数
    • 内置类型没有析构函数,因此销毁时什么也不需要做
    • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
      • 智能指针成员在析构阶段会被自动销毁
    • 调用析构函数
      • 无论何时,一个对象被销毁,就会自动调用析构函数
        • 变量在离开其作用域时被销毁
        • 当一个对象被销毁时,其成员被销毁
        • 容器被销毁时,其元素被销毁
        • 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
        • 对于临时对象,当创建它的完整表达式结束时被销毁
    • 当一个对象的引用或指针离开作用域时,析构函数不会执行
  • 三/五法则
    • 有三个基本操作可以控制类的拷贝操作
      • 拷贝构造函数
      • 拷贝赋值运算符
      • 析构函数
      • 新标准下还可以定义一个移动构造函数和一个移动赋值运算符
    • 法则
      • 需要析构函数的类也需要拷贝和赋值操作
      • 需要拷贝操作的类也需要赋值操作,反之亦然
  • 使用=default
    • 我们可以通过将拷贝控制成员定义为=default来显示的要求编译器生成合成的版本
    • 我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)
  • 阻止拷贝
    • 大多数类应用定义默认构造函数,拷贝构造函数和拷贝赋值运算符,无论是显式的还是隐式的
  • 定义删除函数
    • 新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除函数来阻止拷贝
      • 在函数参数列表后加上=delete来指出我们希望将它定义为删除的
      • 与=default不同,=delete必须出现在函数第一次声明的时候
      • 我们可以对任何函数指定=delete
      • 析构函数不能删除
        • 因为删除析构函数就不能释放指向动态分配对象的指针
    • 当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
    • 希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将他们声明为private的
  • 编写赋值运算符时,注意
    • 如果将一个对象赋予它自身,赋值运算符必须能正确工作
    • 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
    • 当编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中
      • 当拷贝完成后,销毁左侧运算对象的现有成员就是安全的
      • 一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中
  • 定义直接管理资源的类
    • 使用引用计数
    • 引用计数的工作方式
      • 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。
        • 当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1
      • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。
        • 拷贝构造函数递增共享计数器,指出给定对象的状态又被一个新用户共享
      • 析构函数递减计数器,指出共享状态的用户少了一个
        • 如果计数器变为0,则析构函数释放状态
      • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器
        • 如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态
  • 交换操作
    • 管理资源的类通常还定义一个名为swap的函数
      • 如果一个类定义了自己的swap,则算法将使用类自定义版本,否则算法将使用标准库定义的swap
    • 与拷贝控制成员不同,swap并不是必要的,但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段
    • swap函数应该调用swap,而不是std::swap
  • 拷贝并交换技术
    • 将左侧运算对象与右侧运算对象的一个副本进行交换
      • 它自动处理了自赋值情况且天然就是异常安全的
      • 如果真发生了异常,它也会在我们改变左侧运算对象之前发生
    • 使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值
  • 如果想自己设计动态内存管理类可以研读13.5小结内容
  • 对象移动
    • 新标准的一个主要的特性是可以移动而非拷贝对象的能力
      • 移动而非拷贝对象会大幅提升性能
      • 标准库容器,string和shared_ptr类即支持移动也支持拷贝
      • IO类和unique_ptr类可以移动但不能拷贝
    • 右值引用
      • 必须绑定到右值的引用
        • 通过&&而不是&来获取右值引用
        • 我们可以将一个右值引用绑定到表达式、字面常量或返回右值表达式上,但不能将一个右值引用直接绑定到一个左值上
          • int i = 42;
          • int &r = i; 正确,r应用i
          • int &&rr = i; 错误,不能将一个右值引用绑定到一个左值上
          • int &r2 = i * 42; 错误, i*42是一个右值
          • const int &r3 = i * 42; 正确, 我们可以将一个const的引用绑定到一个右值上
          • int &&rr2 = i * 42; 正确,将rr2绑定到乘法结果上
      • 左值持久,右值短暂
        • 左值有持久的状态
        • 而右值要么是字面常量,要么是表达式求值过程中创建的临时对象
      • 由于右值引用只能绑定到临时对象,可知
        • 所引用的对象将要被销毁
        • 该对象没有其他用户
        • 这两个特性意味着:
          • 使用右值引用的代码可以自由的接管所引用的对象资源
      • 右值引用指向将要销毁的对象,因此,我们可以从绑定到右值引用的对象“窃取”状态
    • 变量的左值
      • 变量可以看作只有一个运算对象而没有运算符的表达式
      • 变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行
    • move函数
      • 虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。
        • 使用新标准库函数move获得绑定到左值上的右值引用
        • 定义在头文件utility中
      • 定义move就意味着
        • 除了对左值赋值或销毁外,我们将不再使用它
        • 在调用move之后,我们不能对以后源对象的值做任何假设
        • 可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值
      • 对move不提供using声明
        • 直接调用std::move而不是move
          • 为了避免潜在的名字冲突
      • 与拷贝构造函数不同,移动构造函数不分配任何新内存
        • 它接管给定的对象中的内存
        • 接管内存之后,它将给定对象中的指针置为nullptr,这样就完成了从给定对象的移动操作
        • 最终,移动源对象会被销毁,意味着将在其上运行析构函数
      • 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
        • 指出一个移动操作不抛出异常原因
          • 虽然移动操作通常不抛出异常,但抛出异常也是允许的
          • 标准库容器能对异常发生时自身的行为提供保障
      • 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每一个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符
      • 定义一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员默认地被定义为删除
  • 移动右值,拷贝左值
    • 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。
      • 拷贝赋值运算符和移动赋值运算符的情况类型
  • 更新三/五原则
    • 所有五个拷贝控制成员应该看作一个整体
      • 如果一个类定义了任何一个拷贝操作,它就应该定义所有5个操作
  • 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&
  • 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符

第十四章 重载运算与类型转换

  • 当一个重载的运算符是成员函数时,this绑定到左侧运算符对象

    • 成员运算符函数(显式)的参数数量比运算对象的数量少一个
  • 只能重载已有的运算符,而无权发明新的运算方法

  • 既是一元运算符也是二元运算符

    • + - * &
  • 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符

  • 适用于定义成重载的运算符

    • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致
    • 如果类的某个操作是检查相等性,则定义operator==
      • 如果类有了operator==,它也应该有operator!=
    • 如果类包含一个内在的单序比较操作,则定义operator<
      • 如果类有了operator<,则它也应该有其他关系操作
  • 如果类含有算术运算符,或者位运算符,则最好也提供对应的复合赋值运算符

    • +=,即先执行+,再执行=
  • 如何定义重载的运算符

    • 赋值=,下标[],调用()和成员访问箭头->运算符必须是成员
    • 复合赋值运算符一般来说应该是成员,但并非必须
    • 改变对象状态的运算符或者给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应是成员
    • 具有对称性的运算符可能转换任意一端的运算符,如算术、相等性、关系和位运算符等,通常应该是普通的非成员函数
  • 输入输出运算符

    • IO标准库分别使用>>和<<执行输入和输出
    • 输出运算符尽量减少格式化操作
      • 用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符
    • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要
    • 执行输入运算符时可能发生的错误
      • 当流含有错误类型的数据时读取操作可能失败
      • 当读取操作达到文件末尾或者遇到输入流的其他错误时也会失败
    • 当读取操作发生错误时,输入运算符应该负责从错误中恢复
  • 算术和关系运算符

    • 算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换
      • 因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
    • 如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符
  • 相等运算符设计准则

    • 如果一个类含有判断两个对象相等的操作,则它应该把函数定义成operator==而非一个普通的命名函数
    • 如果类定义了operator==,则该运算符应该能判断一组给定的对象是否含有重复数据
    • 相等运算符应该具有传递性
    • 如果定义了operator==,则这个类也应该定义operator!=
    • 相等运算符和不相等运算符中的一个应该把工作委托给另一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符只是调用那个真正工作的运算符
  • 关系运算符

    • 设计准则
      • 定义顺序关系,令其与关联容器中对关键字的要求一致
      • 如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致,特别是,如果两个对象是!=的,那么一个对象应该<另外一个
        • 如果存在唯一一种逻辑可靠的< 定义,则应考虑这个类定义<运算符。如果类同时还含有==,则当且仅当<的定义和==产生的结果一致时才定义<运算符
  • 赋值运算符

    • 我们可以重载赋值运算符,无论形参的类型是什么,赋值运算符都必须定义为成员函数
    • 赋值运算符必须定义为类的成员,复合赋值运算符通常情况下也应该这样做,这两个运算符都应该返回左侧运算对象的引用
  • 下标运算符

    • 下标运算符必须是成员函数
    • 如果一个类包含下标运算符,则它通常会定义两个版本
      • 一个返回普通引用
        • std::string& operator[](std::size_t n) {return elements[n];}
      • 一个是类的常量成员,并且返回常量引用
        • const std::string& operator[](std::size_t n) const {return elements[n];}
  • 递增和递减运算符

    • 定义递增和递减运算符的类应该同时定义前置版本和后置版本
      • 通常应该被定义成类的成员
    • 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用
    • 区分前置和后置运算符
      • 普通的重载形式无法区分这两种情况
      • 解决方式
        • 后置版本接受一个额外而不使用的int类型形参
        • 使用后置运算符时,编译器为这个形参提供一个值为0的实参
      • p.operator++(0) 调用后置版本
      • p.operator++() 调用前置版本
    • 为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用
  • 成员访问运算符

    • 箭头运算符必须是类的成员,解引用运算符通常也是类的成员
    • 重载的箭头运算符必须返回类的指针或者定义了箭头运算符的某个类的对象
  • 函数调用运算符

    • int operator() (int val) const {return val < 0 ? -val : val;}
    • 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别
  • 在默认情况下,排序算法使用operator<将序列按照升序排列。

    • 如果要执行降序排列的话,可以传入一个greater类型的对象
    • sort(svec.begin(), svec.end(), greater<string>());
  • 可调用对象与function

    • 可调用对象有
      • 函数
      • 函数指针
      • lambda表达式
      • bind创建的对象
      • 重载了函数调用运算符的类
    • c++种函数表通过map来实现
      • string作为关键字
      • 实现运算符的函数作为值
    • 标准库function类型
      • function操作
        • function f;
          • f是一个用来存储可调用对象的空function,这些调用对象的调用形式应该与函数类型T相同
        • function f(nullptr);
          • 显式构造一个空function
        • function f(obj);
          • 在f中存储可调用对象obj的副本
        • f
          • 将 f 作为条件
            • 当f含有一个可调用的对象时为真
            • 否则为假
        • f(args)
          • 调用f中的对象,参数是args
      • 定义为function的成员的类型
        • result_type
          • 该function类型的可调用对象返回的类型
        • argument_type
          • 当T有一个或两个实参时定义的类型
            • 如果T只有一个实参,则argument_type是该类型的同义词
            • 如果T有两个实参
              • first_argument_type
              • second_argument_type
      • 新标准库中的function类与旧版本中的unary_function和binary_function没有关联,后两个类已经被更通用的bind函数代替了
  • 重载、类型转换与运算符

  • 类型转换运算符

    • 是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。
      • operator type() const;
        • type表示某种类型
        • 类型转换运算符可以面向任何类型进行定义(除了void)
      • 不允许转换成数组或者函数类型
        • 但允许转换成指针(包含数组指针及函数指针) 或者引用类型
      • 这话前后矛盾、能理解其说明的内容就行,不要太较真#109
    • 一个类型转换函数必须是类的成员函数
      • 它不能声明返回类型
      • 形参列表也必须为空
      • 类型转换函数通常应该是const
  • 显式的类型转换运算符

    • explicit operator int() const {return val;}
      • 编译器不会自动执行这一类型转换
      • 需要显式转换类型
        • static_cast<int>(si) + 3;
    • 当表达式出现在下列位置时,显式类型转换被隐式的执行
      • if、while及do语句的条件部分
      • for 语句头的条件表达式
      • 逻辑非运算符、逻辑或运算符、逻辑与运算符的运算对象
      • 条件运算符(? :)的条件表达式
    • 向bool的类型转换通常用在条件部分,因此 operator bool 一般定义成explicit的
  • 避免二义性的类型转换

    • 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标的算术类型的转换
    • 当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个
  • 类型转换与运算符的经验规则

    • 不要令两个类执行相同的类型转换
      • 如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是Foo类的类型转换运算符
      • 避免转换目标是内置算术类型的类型转换,特别是当自己已经定义了一个转换成算法类型的类型转换时,接下来
        • 不要再定义接受算法类型的重载运算符,如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,然后使用内置的运算符
        • 不要定义转换到多种算法类型的类型转换。让标准类型转换完成向其他算术类型转换工作
      • 除了显式的向bool类型的转换之外,我们应该尽量避免定义类型转换函数并尽可能的限制那些“显然正确”的非显式构造函数
  • 如果在调用重载函数时,我们需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序设计存在不足

  • 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性

  • 函数匹配与重载运算符

    • 如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题

第十五章 面向对象程序设计

  • 派生类必须在其内部对所有重新定义的虚函数进行声明
    • 派生类可以在这样的函数之前加上virtual关键字,但并不是必须的
    • C++11新标准允许派生类显式的注明它将使用哪个成员函数改写基类的虚函数
      • 在该函数的形参列表之后增加一个override关键字
  • 动态绑定又称运行时绑定
    • 使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
  • 定义基类
    • 基类通常都定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
    • 基类通常将其定义为虚函数,当我们使用指针或引用调用虚函数时,该调用将被动态绑定
    • 基类通过在其成员函数的声明语句之前加上关键字 virtual 使得该函数执行动态绑定
      • 任何构造函数之外的非静态函数都可以是虚函数
      • 关键字virtual 只能出现在类内部的声明语句之前,而不能用于类外部的函数定义
    • 派生类能访问基类共有成员、而不能访问私有成员。
      • 基类希望它的派生类有权访问该成员,同时禁止其他用户访问则使用protected 访问运算符说明
  • 定义派生类
    • 派生类必须通过使用类派生列表明确指出它是从哪个基类继承而来
    • 大多数都只继承自一个类,称为单继承
    • 派生类经常(但不总是)覆盖它继承的虚函数
      • c++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数, 使用关键字override
    • 派生类到基类的类型转换
      • 因为在派生类对象中含有与基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用。而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分
    • 在派生类对象中含有与基类对应的组成部分,这一事实是继承的关键所在
    • 每个类控制它自己的成员初始化过程
      • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
  • 派生类使用基类成员
    • 遵循基类的接口
      • 派生类对象不能直接初始化基类成员,尽管从语法上来说可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但最好不要这么做
      • 派生类应该遵循基类的接口,并通过调用基类的构造函数来初始化那些从基类中继承而来的成员
  • 继承与静态成员
    • 不论从基类中派生出来多少个派生类,对于每个静态成员来说只存在唯一的实例
  • 派生类的声明
    • 如果我们想将某个类作为基类,该类必须已经定义而非仅仅声明
    • 直接基类和间接基类的概念有个印象就行
  • 防止继承的发生
    • c++新标准提供了一种防止继承发生的方法
      • 在类名后跟一个关键字final
  • 类型转换与继承
    • 理解基类和派生类之间的类型转换是理解C++语言面向对象编程的关键所在
    • 可以将基类的指针或引用绑定到派生类对象上的意义
      • 当使用基类的引用或指针时,实际上我们不清楚该引用或指针所绑定对象的真实类型。
      • 该对象可能是基类的对象,也可能是派生类的对象
    • 和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内
  • 静态类型与动态类型
    • 当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式对象的动态类型区分开来
    • 基类的指针或引用的静态类型可能与动态类型不一致
    • 不存在从基类向派生类的隐式类型转换
    • 如果在基类中含有一个或多个虚函数,可以使用dynamic_cast请求一个类型转换,该转换的安全检查将在运行时执行
    • 如果已知某个基类向派生类的转换是安全的,则可以使用static_cast来强制覆盖掉编译器的检查工作
    • 对象之间不存在类型转换
      • 派生类向基类的自动转换只对指针或引用类型有效
    • 当用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中基类部分会被拷贝,移动或赋值,它的派生类部分将被忽略掉
  • 存在继承关系的类型之间的转换规则
    • 从派生类向基类的类型转换只对指针或引用类型有效
    • 基类向派生类不存在隐式类型转换
    • 和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行
  • 虚函数
    • 对虚函数的调用可能在运行时才被解析
    • 多态性
      • 对非虚函数的调用在编译时进行绑定
      • 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
    • 派生类中的虚函数
      • 可以再一次使用virtual关键字指出该函数的性质,然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数
      • 一个派生类的函数如果覆盖某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致
      • 派生类中虚函数的返回类型也必须与基类函数匹配
    • 基类中的虚函数在派生类中隐含的也是一个虚函数,当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配
    • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
    • 回避虚函数机制
      • double undiscounted = baseP->Quote::net_price(42);
      • 通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制
      • 如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归
  • 含有纯虚函数的类是抽象基类
    • 抽象基类负责定义接口
      • 后续的其他类可以覆盖该接口
      • 我们不能直接创建一个抽象基类的对象
  • 重构
    • 重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。
  • 受保护的成员
    • 一个类使用protected关键字声明那些它希望与派生类分享但不想被其他公共访问使用的成员。
    • protected 说明符可以看作是public和private中和后的产物
      • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
      • 和公有成员类似,受保护的成员对于派生类成员和友元来说是可以访问的
      • 派生类的成员或友元只能通过派生类对象来访问基类受保护的成员
        • 派生类对于一个基类对象中的受保护成员没有任何访问权限
  • 派生类向基类转换的可访问性
    • 派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定D继承自B:
      • 只有当D公有继承B时,用户代码才能使用派生类向基类的转换
        • 如果D继承B的方式是受保护或私有的,则用户代码不能使用该转换
      • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换
        • 派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
      • 如果D继承B的方式是共有或者保护,则D的派生类成员和友元可以使用D向B的类型转换
        • 如果D继承B的方式是私有的,则不能使用
    • 对于代码中的某个给定节点来说,如果基类的共有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行
  • 友元关系不能继承,基类的友元访问派生类成员时不具有特殊性
  • 改变个别成员的可访问性
    • 使用using声明
      • public:
      • using Base::size;
    • 派生类只能为那些它可以访问的名字提供using声明
  • class 与 struct的区别
    • class定义派生类是私有继承
    • struct关键字定义的派生类是公有继承
    • 默认成员访问说明符以及派生访问说明符不同,其他都相同
  • 一个私有派生的类最好显式的将private声明出来,而不要仅仅依赖默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误解
  • 名字冲突与继承
    • 和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字
    • 派生类的成员将隐藏同名的基类成员
    • 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
  • 理解函数调用的解析过程对于理解C++的继承至关重要,假定我们调用p->mem(),依次执行以下四个步骤
    • 首先确定p(或obj)的静态类型。
      • 因为我们调用的是一个成员,所以该类型必然是类类型
    • 在p(或obj)的静态类型对应的类中查找mem。
      • 如果找不到,则依次在直接基类中不断查找直到到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将会报错
    • 一旦找到了mem,就进行常规的类型检查,以确认对应当前找到的mem,本次调用是否合法
    • 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码
      • 如果mem是虚函数,且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型
      • 反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用
  • 覆盖重载的函数
    • 和其他函数一样,成员函数无论是否是虚函数都能被重载
    • 当一个类仅需覆盖重载集合中的一些而非全部函数时
      • 为重载的成员提供一条using声明语句
      • 类内using声明的一般规则适用于重载函数的名字
  • 虚析构函数
    • 如果删除的是一个指向派生类对象的基类指针,则需要虚析构函数
      • 和其他虚函数一样,虚析构的虚属性也会被继承
    • 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为
    • 某些定义基类的方式也能导致有的派生类成员为被删除的函数
      • 如果基类中的默认构造函数,拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的
        • 原因是编译器不能使用基类成员来执行派生类对象的基类部分的构造、赋值或销毁操作
      • 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的。
        • 因为编译器无法销毁派生类对象的基类部分
      • 编译器将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的
        • 原因是派生类对象的基类部分不可移动,
      • 如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的(这句话感觉是错误的,暂不理会)
  • 移动操作与继承
    • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时,应该首先在基类中进行定义
    • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象
    • 在默认情况下,基类默认构造函数初始化派生类对象的基类部分,如果我们想拷贝或移动基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝或移动构造函数
    • 与拷贝移动构造一样,派生类的赋值运算符也必须显式的为其基类部分赋值
  • 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本
  • 继承的构造函数
    • 派生类能重用其直接基类定义的构造函数
      • public:
      • using Disc_quote::Disc_quote; //继承Disc_quote的构造函数
      • 通常情况下,using 声明语句只是令某个名字在当前作用域内可见
    • 特点
      • 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别
      • 一个using声明语句不能指定explicit 或constexpr
    • 当一个基类构造函数含有默认实参,这些实参不会被继承。
    • 相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参
  • 容器与继承
    • 当我们使用容器存放继承体系中的对象时,通常必须采取间接存储的方式
      • 因为不允许在容器中保存不同类型的元素,所以我们不能把具有继承关系的多种类型的对象直接存放在容器当中
    • 当派生类对象被赋值给基类对象时,其中的派生类部分被切掉,因此容器和存在继承关系的类型无法兼容
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值