《C++ Primer》笔记 第7章 类

  1. 成员函数的声明必须在类的内部,它的定义则既能够在类的内部也能够在类的外部。做为接口组成部分的非成员函数,它们的定义和声明都在类的外部。数组

  2. 定义在类内部的函数是隐式的inline函数。app

  3. 成员函数经过一个名为this的额外的隐式参数来访问调用它的那个对象。当咱们调用一个成员函数时,用请求该函数的对象地址初始化this(至关于Python中的self形参?)。伪代码示意:Sales_data::isbn(&total),任何对类成员的直接访问都被看作this的隐式引用,也就是说,当isbn使用bookNo时,它隐式地使用this指向的成员,就像咱们书写了this->bookNo同样。函数

  4. 对于咱们来讲,this形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。咱们能够在成员函数体内部使用this。例:std::string isbn() const { return this->bookNo; }this

  5. 由于this的目的老是指向“这个”对象,因此this是一个常量指针,咱们不容许改变this中保存的地址。指针

  6. 默认状况下,this的类型是指向类类型很是量版本的常量指针。尽管this是隐式的,但它仍然须要遵循初始化规则,意味着(在默认状况下)咱们不能把this绑定到一个常量对象上。这一状况也就使得咱们不能在一个常量对象上调用普通的成员函数。调试

  7. 紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称做常量成员函数rest

  8. 在常量成员函数中,由于this是指向常量的指针,因此常量成员函数不能改变调用它的对象的内容。code

  9. 常量对象,以及常量对象的引用或指针都只能调用常量成员函数。对象

  10. 编译器分两步处理类:首先编译成员的声明,而后才轮到成员函数体(若是有的话)。所以,成员函数体能够随意使用类中的其余成员而无须在乎这些成员出现的次序。接口

  11. 当咱们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。例:double Sales_data::avg_price() const {...}。使用做用域运算符(::),告知编译器剩余的代码是位于类的做用域内的。(参数列表和函数体内不用再加做用域运算符)

  12. 内置的赋值运算符把它的左侧运算对象当成左值返回。示例:返回对象的引用

    Sales_data& Sales_data::combine(const Sales_data &rhs)
      {
            units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
            revenue += rhs.revenue;
            return *this; // 返回调用该函数的对象
      }
  13. 通常来讲,若是非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中。

  14. 默认状况下,拷贝类的对象其实拷贝的是对象的数据成员。

  15. 构造函数的名字和类名相同。构造函数没有返回类型。类能够包含多个构造函数,和其余重载函数差很少,不一样的构造函数之间必须在参数数量或参数类型上有所区别。构造函数不能被声明成const的。当咱们建立类的一个const对象时,直到构造函数完成初始化过程后,对象才能真正取得其“常量”属性。所以,构造函数在const对象的构造过程当中能够向其写值。

  16. 类经过一个特殊的构造函数来控制默认初始化过程,这个函数叫作默认构造函数,默认构造函数无须任何实参。若是咱们的类没有显式地定义构造函数,那么编译器就会为咱们隐式地定义一个默认构造函数。

  17. 编译器建立的构造函数又被称为合成的默认构造函数。对于大多数类来讲,这个合成的默认构造函数将按照以下规则初始化类的数据成员:

    • 若是存在类内的初始值,用它来初始化成员。
    • 不然,默认初始化该成员。
  18. 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

  19. 若是类包含有内置类型或者复合类型(好比数组和指针)的成员,则只有当这些成员全都被赋予了类内的初始值时(不然它们的值将是未定义的),这个类才适合于使用合成的默认构造函数。

  20. 有的时候编译器不能为某些类合成默认的构造函数。例如:若是类中包含一个其余类类型的成员且这个成员的类型没有默认构造函数,那么编译器将没法初始化该成员。对于这样的类来讲,咱们必须自定义默认构造函数,不然该类将没有可用的默认构造函数。

  21. 若是咱们须要默认的行为,那么能够经过在参数列表后面写上= default来要求编译器生成构造函数。其中,= default既能够和声明一块儿出如今类的内部,也能够做为定义出如今类的外部。和其余函数同样,若是= default在类的内部,则默认构造函数是内联的;若是它在类的外部,则该成员默认状况下不是内联的。例:Sales_data() = default;

  22. 构造函数初始值是成员名字的一个列表,每一个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不一样成员的初始化经过逗号分隔开来。例:Sales_data(const std::string &s): bookNo(s) {}

  23. 当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。若是你的编译器不支持类内初始值,则全部构造函数都应该显式地初始化每一个内置类型的成员。

  24. 没有出如今构造函数初始值列表中的成员将经过相应的类内初始值(若是存在的话)初始化,或者执行默认初始化。

  25. 对象在几种状况下会被拷贝,如咱们初始化变量以及以值的方式传递或返回一个对象等。当咱们使用了赋值运算符时会发生对象的赋值操做。当对象再也不存在时执行销毁的操做。若是咱们不主动定义这些操做,则编译器将替咱们合成它们。通常来讲,编译器生成的版本将对对象的每一个成员执行拷贝、赋值和销毁操做。

    total = trans; // 处理下一本书的信息
      
      // 它的行为与下面的代码相同
      total.bookNo = trans.bookNo;
      total.units_sold = trans.units_sold;
      total.revenue = trans.revenue;
  26. 使用class和struct定义类惟一的区别就是默认的访问权限。若是咱们使用struct关键字,则定义在第一个访问说明符以前的成员是public的;相反,若是过咱们使用class关键字,则这些成员是private的。

  27. 友元声明只能出如今类定义的内部,可是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。通常来讲,最好在类定义开始或结束前的位置集中声明友元。

  28. 尽管当类的定义发生改变时无需更改用户代码,可是使用了该类的源文件必须从新编译。

  29. 友元的声明仅仅指定了访问的权限,而非一个一般意义上的函数声明。若是咱们但愿类的用户可以调用某个友元函数,那么咱们就必须在友元声明以外再专门对函数进行一次声明。为了使友元对类的用户可见,咱们一般把友元的声明与类自己放置在同一个头文件中(类的外部)。

  30. 除了定义数据和函数成员以外,类还能够自定义某种类型在类中的别名。由类定义的类型名字和其余成员同样存在访问权限,能够是public或者private中的一种。用来定义类型的成员必须先定义后使用,这一点与普通成员有所区别。所以,类型成员一般出如今类开始的地方。

    class Screen
      {
      public:
            typedef std::string::size_type pos; // using pos = std::string::size_type;
      private:
            pos cursor = 0;
            pos height = 0, width = 0;
            std::string contents;
      };
  31. 定义在类内部的成员函数是自动inline的。咱们能够在类的内部把inline做为声明的一部分显式地声明成员函数,一样的,也能在类的外部用inline关键字修饰函数的定义。

  32. 虽然咱们无须在声明和定义的地方同时说明inline,但这么作实际上是合法的。不过,最好只在类外部定义的地方说明inline,这样可使类更容易理解。和咱们在头文件中定义inline函数的缘由同样,inline成员函数也应该与相应的类定义在同一个头文件中。

  33. 一个可变数据成员永远不会是const,即便它是const对象的成员。所以,一个const成员函数能够改变一个可变成员的值。

    class Screen
      {
      public:
            void some_member() const;
      private:
            // 该成员是个可变成员,所以任何成员函数,包括const函数在内都能改变它的值
            mutable size_t access_ctr; // 即便在一个const对象内也能被修改
            // 其余成员与以前的版本一致
      };
      void Screen::some_member() const
      {
            ++access_ctr; // 保存一个计数值,用于记录成员函数被调用的次数
            // 该成员须要完成的其余工做
      }
  34. 当咱们提供一个类内初始值时,必须以符号=或者花括号表示(不能用圆括号,会被当成函数)。

  35. 一个const成员函数若是以引用的形式返回*this,那么它的返回类型将是常量引用。

  36. 即便两个类的成员列表彻底一致,它们也是不一样的类型。对于一个类来讲,它的成员和其余任何类(或者任何其余做用域)的成员都不是一回事儿。

  37. 咱们能够把类名做为类型的名字使用,从而直接指向类类型。或者,咱们也能够把类名跟在关键字class或struct后面。

    Sales_data item1; // 默认初始化Sales_data类型的对象
      class Sales_data item1; // 一条等价的声明
  38. 就像能够把函数的声明和定义分离开来同样,咱们也能仅仅声明类而暂时不定义它:class Screen;这种声明有时被称为前向声明,它向程序中引入了名字Screen而且指明Screen是一种类类型。在它声明以后定义以前是一个不彻底类型(不清楚它到底包含哪些成员)。

  39. 对于不彻底类型:能够定义指向这种类型的指针或引用,也能够声明(可是不能定义)以不彻底类型做为参数或者返回类型的函数。

  40. 对于一个类来讲,在咱们建立它的对象以前该类必须被定义过,而不能仅仅被声明。不然编译器就没法了解这样的对象须要多少存储空间。相似的,类也必须首先被定义,而后才能用引用或指针访问其成员。毕竟,若是类还没有定义,编译器也就不清楚该类到底有哪些成员。

  41. 由于只有当类所有完成后类才算被定义,因此一个类的成员类型不能是该类本身。然而,一旦一个类的名字出现后,它就被认为是声明过了(但还没有定义),所以类容许包括指向它自身类型的引用或指针:

    class Link_screen
      {
            Screen window;
            Link_screen *next;
            Link_screen *prev;
      };
  42. 在类中,能够把普通的非成员函数是定义成友元,也能够把其余类定义成友元,也能够把其余类(以前已定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。

    class Screen
      {
            // Window_mgr::clear必须在Screen类以前被声明
            friend void Window_mgr::clear(ScreenIndex);
            // Screen类的剩余部分
      };
  43. 若是一个类指定了友元类,则友元类的成员函数能够访问此类包括非公有成员在内的全部成员。

    class Screen
      {
            // Window_mgr的成员能够访问Screen类的私有部分
            friend class Window_mgr;
            // Screen类的剩余部分
      };
  44. 友元关系不存在传递性。每一个类负责控制本身的友元或友元函数。

  45. 要想令某个成员函数做为友元,咱们必须仔细组织程序的结构以知足声明和定义的彼此依赖关系。

  46. 若是一个类型想把一组重载函数声明成它的友元,它须要对这组函数中的每个分别声明。

  47. 类和非成员函数的声明不是必须在它们的友元声明以前。当一个名字第一次出如今一个友元声明中时,咱们隐式地假定该名字在当前做用域中是可见的。然而,友元自己不必定真的声明在当前做用域中。甚至就算在类的内部定义该函数,咱们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即便咱们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。

    struct X
      {
            friend void f() { /*友元函数能够定义在类的内部*/ }
            X() { f(); } // 错误:f尚未被声明
            void g();
            void h();
      };
      void X::g() { return f(); } // 错误:f尚未被声明
      void f(); // 声明那个定义在X中的函数
      void X::h() { return f(); } // 正确:如今f的声明在做用域中了
  48. 友元声明的做用是影响访问权限,它自己并不是普通意义上的声明。

  49. 每一个类都会定义它本身的做用域。在类的做用域以外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用做用域运算符访问。不论哪一种状况,跟在运算符以后的名字都必须是对应类的成员。

  50. 一旦遇到了类名,定义的剩余部分就在类的做用域以内了,这里的剩余部分包括参数列表和函数体。

    void Window_mgr::clear(ScreenIndex i)
      {
            Screen &s = screens[i];
            s.contents = string(s.height * s.width, ' ');
      }
  51. 函数的返回类型一般出如今函数名以前,所以当成员函数定义在类的外部时,返回类型中使用的名字都位于类的做用域以外。这时,返回类型必须指明它是哪一个类的成员(尾置返回类型在当前类的定义域中)。

  52. 通常的名字查找(寻找与所用名字最匹配的声明的过程):

    • 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用以前出现的声明。
    • 若是没找到,继续查找外层做用域。
    • 若是最终没有找到匹配的声明,则程序报错。
  53. 对于定义在类内部的成员函数来讲,解析其中名字的方式与上述的查找规则有所区别:

    • 首先,编译成员的声明
    • 直到类所有可见后才编译函数体
  54. 编译器处理完类中的所有声明后才会处理成员函数的定义。这样能够简化类代码的组织方式。这种两段的处理方式只适用于成员函数体中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。若是某个成员的声明使用了类中还没有出现的名字,则编译器将会在定义该类的做用域中继续查找(进行通常的名字查找过程)。

    // 理解下面这段程序代码
      typedef double Money;
      string bal;
      class Account
      {
      public:
            Money balance() { return bal; } // 返回的是成员bal,而非外层做用域的string对象
      private:
            Money bal; // balance函数体在整个类可见后才被处理
            // ...
      };
  55. 通常来讲,内层做用域能够从新定义外层做用域中的名字,即便该名字已经在内层做用域中使用过。然而在类中,若是成员使用了外层做用域中的某个名字,而该名字表明一种类型,则类不能在以后从新定义该名字(由于两段式的名字查找)。

    typedef double Money;
      string bal;
      class Account
      {
      public:
            Money balance() { return bal; } // 使用外层做用域的Money
      private:
            typedef double Money; // 错误:不能从新定义Money,即便与外层做用域中的定义彻底一致
            Money bal;
            // ...
      };
  56. 类型名的定义一般出如今类的开始处,这样就能确保全部使用该类型的成员都出如今类名的定义以后。

  57. 成员函数中使用的名字按照以下方式解析:

    • 首先,在成员函数内查找该名字的声明。和前面同样,只有在函数使用以前出现的声明才被考虑。(成员函数做用域)
    • 若是在成员函数内没有找到,则在类内继续查找,这时类的全部成员均可以被考虑。(类做用域)
    • 若是类内也没找到该名字的声明,在成员函数定义以前(成员函数能够定义在类外)的做用域内继续查找。(类的外层做用域)
  58. 尽管类的成员被隐藏了,但咱们仍然能够经过加上类的名字或显式地使用this指针来强制访问成员。

    void Screen::dummy_fcn(pos height) // 不建议隐藏类中同名的成员
      {
            cursor = width * this->height;
            // 另外一种表示该成员的方式
            cursor = width * Screen::height;
      }
  59. 尽管外层的对象被隐藏掉了,但咱们仍然能够用做用域运算符访问它。

    void Screen::dummy_fcn(pos height) // 不建议隐藏外层做用域中可能被用到的名字
      {
            cursor = width * ::height; // 全局的那个height
      }
  60. 当成员定义在类的外部时,名字查找的第三步不只要考虑类定义以前的全局做用域中的声明,还须要考虑在成员函数定义以前的全局做用域中的声明。

    // 代码能够被正常使用
      int height; // 定义了一个名字,稍后将在Screen中使用
      class Screen
      {
      public:
            typedef std::string::size_type pos;
            void setHeight(pos);
            pos height = 0; // 隐藏了外层做用域中的height
      };
      Screen::pos verify(Screen::pos);
      void Screen::setHeight(pos var)
      {
            // var:参数
            // height:类的成员
            // verify:全局函数
            height = verify(var);
      }
  61. 若是没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体以前执行默认初始化。

  62. 构造函数的初始值有时必不可少:若是成员是const或者是引用的话,必须将其初始化。相似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。

  63. 随着构造函数体一开始执行,初始化就完成了。咱们初始化const或者引用类型的数据成员的惟一机会就是经过构造函数初始化。

    class ConstRef
      {
      public:
            ConstRef(int ii);
      private:
            int i;
            const int ci;
            int &ri;
      };
    
      // 错误:ci和ri必须被初始化
      ConstRef::ConstRef(int ii)
      {
            // 赋值
            i = ii; // 正确
            ci = ii; // 错误:不能给const赋值
            ri = i; // 错误:不能给const赋值
      }
    
      // 正确形式:显式地初始化引用和const成员
      ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
  64. 若是成员是const、引用,或者属于某种未提供默认构造函数的类类型,咱们必须经过构造函数初始值列表为这些成员提供初值。

  65. 成员的初始化顺序与它们在类定义中的出现顺序一致。构造函数初始值列表中初始值的先后位置关系不会影响实际的初始化顺序。

    class X
      {
            int i;
            int j;
      public:
            // 未定义的:i在j以前被初始化
            X(int val): j(val), i(j) { } // 错误:试图使用未定义的值j初始化i
      };
  66. 最好令构造函数初始值的顺序与成员声明的顺序保持一致。并且若是可能的话,尽可能避免使用某些成员初始化其余成员。

  67. 若是可能的话,最好用构造函数的参数做为成员的初始值,而尽可能避免使用同一个对象的其余成员。这样的好处是咱们能够没必要考虑成员的初始化顺序。

  68. 若是一个构造函数为全部参数都提供了默认实参,则它实际上也定义了默认构造函数。

  69. 一个委托构造函数使用它所属类的其余构造函数执行它本身的初始化过程,或者说它把它本身的一些(或者所有)职责委托给了其余构造函数。

    class Sales_data
      {
      public:
            // 非委托构造函数使用对应的实参初始化成员
            Sales_data(std::string s, unsigned cnt, double price):
                  bookNo(s), units_sold(cnt), revenue(cnt*price) { }
            // 其他构造函数全都委托给另外一个构造函数
            Sales_data(): Sales_data("", 0, 0) { }
            Sales_data(std::string s): Sales_data(s, 0, 0,) { }
            Sales_data(std::istream &is): Sales_data() { read(is, *this); }
            // 其余成员与以前的版本一致
      }
  70. 当一个构造函数委托给另外一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行(而后才会执行委托者的函数体)。

  71. 当对象被默认初始化或值初始化时自动执行默认构造函数。类必须包含一个默认构造函数以便在下述状况下使用:

  72. 默认初始化在如下状况下发生:

    • 当咱们在块做用域内不使用任何初始值定义一个非静态变量或者数组时。
    • 当一个类自己含有类类型的成员且使用合成的默认构造函数时(那么这个成员执行默认构造函数)。
    • 当类类型的成员没有在构造函数初始值列表中显式地初始化时(那么这个成员执行默认构造函数)。
  73. 值初始化在如下状况下发生:

    • 在数组初始化的过程当中若是咱们提供的初始值数量少于数组的大小时。
    • 当咱们不使用初始值定义一个局部静态变量时。
    • 当咱们经过书写形如T()的表达式显式地请求值初始化时,其中T是类型名(例如vector)。
  74. 不那么明显的一种状况是类的某些数据成员缺乏默认构造函数,代码以下。在实际中,若是定义了其余构造函数,那么最好也提供一个默认构造函数。

    class NoDefault
      {
      public:
            NoDefault(const std::string&);
            // 还有其余成员,可是没有其余构造函数了
      };
      struct A
      { 
            // 默认状况下my_mem是public的
            NoDefault my_mem;
      };
      A a; // 错误:不能为A合成构造函数
      struct B
      {
            B() { } // 错误:b_member没有初始值
            NoDefault b_member;
      }
  75. 使用默认构造函数:

    Sales_data obj(); // 错误:声明了一个函数而非对象
      Sales_data obj2; // 正确:obj2是一个对象而非函数
  76. 若是构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时咱们把这种构造函数称做转换构造函数。能经过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

  77. 编译器只会自动地执行一步类型转换。例如,由于下面的代码隐式地使用了两种转换规则,因此它是错误的:

    // 错误:须要用户定义的两种转换:
      // (1)把"9-999-99999-9"转换成string
      // (2)再把这个(临时的)string转换成Sales_data
      item.combine("9-999-99999-9");
    
      // 正确:显式地转换成string,隐式地转换成Sales_data
      item.combine(string("9-999-99999-9"));
      // 正确:隐式地转换成string,显式地转换成Sales_data
      item.combine(Sales_data("9-999-99999-9"));
    
      // 经过读取标准输入建立了一个(临时的)Sales_data对象,随后将获得的对象传递给combine。
      item.combine(cin);
    
      // Sales_data对象是个临时量,一旦combine完成咱们就不能再访问它了。实际上,咱们构建了一个对象,先将它的值加到item中,随后将其丢弃。
  78. 在要求隐式转换的程序上下文中,咱们能够经过将构造函数声明为explicit加以阻止。此时,没有任何构造函数能用于隐式地建立Sales_data对象,以前的两种用法都没法经过编译:

    item.combine(null_book); // 错误:string构造函数是explicit的
      item.combine(cin); // 错误:istream构造函数时explicit的
  79. 关键字explicit只对一个实参的构造函数有效。须要多个实参的构造函数不能用于执行隐式转换,因此无需将这些构造函数指定为explicit的。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不该重复。

    • inline是用于实现的关键字(放在定义处)
    • static是用于声明的关键字(放在声明处)
    • explicit是用于声明的关键字(放在声明处)
    • friend是用于声明的关键字(放在声明处)
  80. 发生隐式转换的一种状况是当咱们执行拷贝形式的初始化时(使用=)。此时,咱们只能使用直接初始化而不能使用explicit构造函数。

    Sales_data item1(null_book); // 正确:直接初始化
      // 错误:不能将explicit构造函数用于拷贝形式的初始化过程
      Sales_data item2 = null_book;
  81. 当咱们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。并且,编译器将不会在自动转换过程当中使用该构造函数。

  82. 尽管编译器不会将explicit的构造函数用于隐式转换过程,可是咱们可使用这样的构造函数显式地强制进行转换。

    // 正确:实参是一个显式构造的Sales_data对象
      item.combine(Sales_data(null_book));
      // 正确:static_cast可使用explicit的构造函数
      item.combine(static_cast<Sales_data>(cin));
  83. 咱们用过的一些标准库中的类含有单参数的构造函数:

    • 接受一个单参数的const char*的string构造函数不是explicit的。
    • 接受一个容量参数的vector构造函数是explicit的。
  84. 聚合类使得用户能够直接访问其成员,而且具备特殊的初始化语法形式。当一个类知足以下条件时,咱们说它是聚合的:

    • 全部成员都是public的
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数
    struct Data
      {
            int ival;
            string s;
      };
  85. 咱们能够提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:Data val1 = {0, "Anna"};。初始值的顺序必须与声明的顺序一致Data val2 = {"Anna", 1024};错误。若是初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。

  86. 显式地初始化类的对象存在三个明显的缺点:

    • 要求类的全部成员都是public的
    • 将正确初始化每一个对象的每一个成员的重任交给了类的用户(而非类的做者)。由于用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,因此这样的初始化过程冗长乏味且容易出错。
    • 添加或删除一个成员以后,全部的初始化语句都须要更新。
  87. constexpr函数的参数和返回值必须是字面值类型。除了算术类型、引用和指针外,某些类也是字面值类型。和其它类不一样,字面值类型的类可能含有constexpr函数成员。这样的成员必须符合constexpr函数的全部要求,它们是隐式const的。

  88. 数据成员都是字面值类型的聚合类是字面值常量类。若是一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:

    • 数据成员都必须是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 若是一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者若是成员属于某种类类型,则初始值必须使用成员本身的constexpr构造函数。
    • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
  89. constexpr构造函数必须既符合构造函数的要求(意味着不能包含返回语句),又符合constexpr函数的要求(意味着它能拥有的惟一可执行语句就是返回语句)。综合这两点可知,constexpr构造函数体通常来讲应该是空的。

    class Debug
      {
      public:
            constexpr Debug(bool b = true): hw(b), io(b), other(b) { }
            constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) { }
            constexpr bool any() { return hw || io || other; }
            void set_io(bool b) { io = b; }
            void set_hw(bool b) { hw = b; }
            void set_other(bool b) { other = b; }
      private:
            bool hw; // 硬件错误,而非IO错误
            bool io; // IO错误
            bool other; // 其余错误
      };
    
      constexpr Debug io_sub(false, true, false); // 调试IO
      if (io_sub.any()) // 等价于if(true)
            cerr << "print appropriate error messages" << endl;
      constexpr Debug prod(false); // 无调试
      if (prod.any()) // 等价于if(false)
            cerr << "print an error message" << endl;
  90. constexpr构造函数必须初始化全部数据成员。初始值(或者)使用constexpr构造函数或者是一条常量表达式。

  91. constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。

  92. 咱们经过在成员的声明以前加上关键字static使得其与类关联在一块儿。和其余成员同样,静态成员能够是public的或private的。静态数据成员的类型能够是常量、引用、指针、类类型等。

  93. 类的静态成员存在于任何对象以外,对象中不包含任何与静态数据成员有关的数据。

  94. 相似的,静态成员函数也不与任何对象绑定在一块儿,它们不包含this指针。做为结果,静态成员函数不能声明成const的,并且咱们也不能在static函数体内使用this指针。这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。

  95. 使用类的静态成员:

    • 使用做用域运算符直接访问静态成员:r = Account::rate();
    • 虽然静态成员不属于类的某个对象,可是咱们仍然可使用类的对象、引用或者指针来访问静态成员:r = ac1.rate()r = ac2->rate()
    • 成员函数不用经过做用域运算符就能直接使用静态成员:
    class Account
      {
      public:
            void calculate() { amount += amount * interestRate; }
            static double rate() { return interestRate; }
            static void rate(double);
      private:
            std::string owner;
            double amount;
            static double interestRate;
            static double initRate();
      };
  96. 咱们既能够在类的内部也能够在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出如今类内部的声明语句:

    void Account::rate(double newRate)
      {
            interestRate = newRate;
      }
  97. 和类的全部成员同样,当咱们指向类外部的静态成员时,必须指明成员所属的类名。

  98. 由于静态数据成员不属于类的任何一个对象,因此它们并非在建立类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。并且通常来讲,咱们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每一个静态成员。和其它对象同样,一个静态数据成员只能定义一次。要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其余非内联函数的定义放在同一个文件中。

  99. 相似于全局变量,静态数据成员定义在任何函数以外。所以一旦它被定义,就将一直存在于程序的整个生命周期中。

  100. 从类名开始,这条定义语句的剩余部分就都位于类的做用于以内了。和其余成员的定义同样,interestRate的定义也能够访问类的私有成员。

    // 定义并初始化一个静态成员
    double Account::interestRate = initRate(); // 能够直接使用initRate函数
  101. 一般状况下,类的静态成员不该该在类的内部初始化。然而,咱们能够为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。(除了静态常量成员以外,其余静态成员不能在类的内部初始化。)初始值必须是常量表达式,由于这些成员自己就是常量表达式,因此它们能用在全部适合于常量表达式的地方。例如:

    class Account
    {
    public:
          static double rate() { return interestRate; }
          static void rate(double);
    private:
          static constexpr int period = 30; // period是常量表达式
          double daily_tbl[period];
    };
  102. 若是在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了。

    // 一个不带初始值的静态成员的定义
    constexpr int Account::period; // 初始值在类的定义内提供
  103. 即便一个常量静态数据成员在类内部被初始化了,一般状况下也应该在类的外部定义一下该成员。

  104. 静态成员的优势包括:做用域位于类的范围以内,避免与其余类的成员或者全局做用域的名字冲突;能够是私有成员,而全局对象不能够;经过阅读程序能够很是容易地看出静态成员与特定类关联,使得程序的含义清晰明了。

  105. 静态数据成员能够是不彻底类型。特别的,静态数据成员的类型能够就是它所属的类类型,非静态数据成员只能声明成它所属类的指针或引用。

  106. 咱们可使用静态成员做为默认实参。非静态数据成员不能做为默认实参,由于它的值自己属于对象的一部分,这么作的结果是没法真正提供一个对象以便从中获取成员的值,最终将引起错误。(普通成员函数包含this形参,但由于函数参数解析顺序是未定的,因此该默认值也是未定的,int function(class_type *this, int n = this->a)

  107. 构造函数初始值列表:未经初始值列表初始化的成员将被默认初始化。

  108. 成员函数:类的函数成员。普通的函数经过隐式的this指针与类的对象绑定在一块儿;静态成员函数不与对象绑定在一块儿也没有this指针。成员函数能够重载,此时隐式的this指针参与函数匹配的过程。

相关文章
相关标签/搜索