C++ Primer(第五版)第七章内容——类。

注意:定义类之后,需要一个分号!!

  1. 定义类的目的

    数据抽象与封装。

    对外指提供接口,隐藏实现。或者说接口与实现分离。

  2. 类成员函数声明与定义

    类的成员函数都要声明在类内部,但定义可以外部。

    定义在内部的成员函数默认是内联(inline)的(隐式inline)。

  3. 类成员函数的隐式this指针

    在成员函数内部访问类成员或函数,都隐式的通过this指针访问。

     ```cpp
     string Info::get_info()
     {
         return info ; 
     }
        
     Info inf ;
     cout << inf.get_info() ;
        
     ==>
    
     string Info::get_info(Info * const this )
     {
         return this->info
     }
     Info inf ;
     cout << get_info(&inf) ;
     ```
    

    如上,就大概是编译器实际实现类成员函数调用、访问的过程。这个通过Python中类成员函数定义就比较清晰。

     Class Info(object) :
         def get_info(self) :
             return self.info
    

    需要注意的是其中的const,根据书上的描述,this的值是不能被改变的。所以我想它传递的应该是一个底层const的指针。

    因为this变量被隐式定义,所以我们不能再定义名为this的变量。也不能修改this变量的值。

  4. 定义const成员函数

    如果成员函数不修改成员变量值,那么通过在函数形参列表后加入const关键字来实现该逻辑。

     string Info::get_info() const {...}
    

    实际上是通过给隐式的this指针加上顶层const

     string Info::get_info(const Info *const this) {...}
    

    如果类要支持常量定义,那么定义const的函数是必须的。因为如果定义类的const实例,而函数都是非const的,由于非const实例不能绑定到顶层const指针上,所以成员函数不可用。

  5. 类作用域

    类本身就是一个作用域。

    在外部定义类的成员函数时,需要使用作用域运算符::。 string Info::get_info() { return info . }

  6. 类成员变量、成员函数的定义顺序与调用顺序无关

    如果函数A定义在函数B的后面,但是在函数B也可以直接调用函数A。

    因为编译器分两步处理类:

    首先编译成员的声明。

    然后再编译函数体(如果有的话)。

  7. 返回调用对象本身的引用

    使用*this来返回调用的对象本身。

     Info& Info::add(Info & b) 
     {
         info += b.info ;
         return *this ;
     } 
    

    注意函数返回类型的引用符号——返回的是调用对象的引用,即左值。

  8. 类的拷贝

    IO类不能拷贝。只能传引用。

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

  9. 默认构造函数

    当类中没有定义任何构造函数时,编译器尝试创建一个默认构造函数,也叫 合成的默认构造函数(synthesized default constructor)。

    有3点:

    1. 如果自定义了构造函数,则默认构造函数不会被创建。

      不过可以快捷的定义一个默认的构造函数。

       Info::Info() = default ;
      

      对,就是这么定义。

    2. 默认构造函数,编译器对成员变量执行初始化的流程:

      • 如果指定了默认值,用默认值初始化。

      • 没有,则尝试用默认值初始化该成员变量。

    3. 由上,引出了默认构造函数可能导致的问题:

      • 对内置类型的成员变量,因为成员变量属于块内变量(局部变量),故执行随机初始化。

      • 如果成员变量类型是另外一个类类型,且该类型没有默认构造函数,那就报错了。

  10. 构造函数的初始值列表

    在参数列表后、函数体前(左大括号前)使用:member_var(param_var),...来定义初始值列表。

     Sales_data(string &s , unsigned n , double p):
                bookNo(s) , revenue(p*n)} {}
    

    必须为const成员、引用、不能默认初始化的类提供初始值列表的初始方式(或者使用类内初始值)。

  11. 总结成员变量赋值顺序[已更新]

    1. 初始化过程

      按照成员变量的定义顺序,依次初始化各成员变量,初始化规则为:

      a. 如果成员变量在初始化列表中有定义,那么用初始化列表中的定义去初始化。

      b. 否则,查看成员变量的定义形式:

       1. 如果是类内初始值形式,用该值初始化
      
       2. 如果无类内初始值,那么用默认方式初始化。
      
       > 默认初始化,如前面所言:对内置类型,随机赋值,或者说直接分配空间,不赋初值;对类类型,调用默认构造函数,没有则报错。
      
       > 用类内初始值定义时不能使用小括号!因为编译器在语法解释时其形式会跟函数声明的形式冲突!!!可以使用`=`或者`{}`来定义。使用`=`并不代表就是赋值!
      

      由上,可知两点:

      1. 成员变量间的初始化顺序,是由其定义顺序决定的!(与初始化列表中顺序无关)

      2. 在进入构造函数体之前,各成员变量都已完成了初始化操作。 -> 所以const变量、引用,都必须在这个阶段给予初始值!而不能在构造函数体中赋值。

      这样看来,每个构造函数都对应一段不同的初始化代码。

    2. 赋值过程

      在构造函数体中做赋值操作。

    之前对 类内初始值与初始化列表、默认值 三者的初始化选择优先级顺序有疑惑,后来无意间初始化一个const变量得到了答案!要知道const变量是只能做初始化而不能做赋值的,然而当同时在初始值列表和类内初始值同时给于const变量不同的初始值,得到的结果是初始值列表中指定的值。且不报错。说明整个过程只做了初始化操作,且初始值列表的优先级高于类内初始值。猜测类内初始值与默认值是同等级别,一个东西。

  12. 类的拷贝、赋值和析构

    这里讲的应该仍然是基础。

    类对象间的=操作会发生赋值(拷贝),对象生命结束会发生析构。

    如果我们未定义自己的赋值(拷贝)、析构操作,那么编译器将自动合成这种操作!

    编译器的行为是:对类对象的每个成员,做赋值(拷贝)、析构操作。

    所以,如果类中的成员,除了类对象本身的空间外,没有额外的空间占用(我的理解,就是指针!如果有new操作,那么显然类本身只有指针本身的大小,但是在类空间之外,还有由指针指向的堆空间),那么编译器默认的操作就是没有危险的。赋值(拷贝)不会有遗漏,析构不会漏掉内存泄露。

    如果有动态内存操作,那么必须要手动管理这些操作!(应该在后续介绍,目前所知,对于析构就是定义析构函数了)

    使用vector完全没有问题! 因为vector本身有完善的赋值(拷贝)、析构操作,不会有内存问题。vector在赋值时(= , push_back`等操作),会拷贝元素值!在析构时,会清空vevtor装的所有元素值!

    使用vector时就可以直接用默认的操作,这告诉我们封装的好处。即,自己的事情自己做,外部只需要告诉你命令。

  13. 访问控制与封装

    主要说一下publicprivate的依据:

    public 下的成员应该是要提供给外部的接口。

    private 下的内容应该是被隐藏的细节。

    以上就实现了封装!

  14. 关键字classstruct的区别

    在C++中,classstruct都是定义的类类型!他们唯一的区别就是——默认的访问权限

    classprivatestructpublic

    不要受C的影响。

  15. 友元

    友元函数定义的方法:

    1. 在类内部声明友元函数,使用friend关键字 [作为类的友元的声明]

    2. 在类声明所在的头文件中声明友元函数。[作为函数的声明] (经过测试,在GCC、MSVC2010下可省略。)

    建议:

    友元声明在类的开始或者结束 [无所谓public 或 private ,无访问控制符影响,因为友元不是类的成员!]

    性质:

    友元函数不属于类的成员,不受所在位置的访问控制级别约束。

    友元函数可以访问类的非公有成员。

    写的一个例子:

    ------
    test.h
    
    class People
    {
        // friend function declaration
        friend void teach_knowledge(std::string , People &) ;
    
        public :
            void make_self_introduction() ;
        private :
            std::string knowledge ;
    } ;
    
    // function declaration
    void teach_knowledge(std::string , People &) ; 
    
    --------
    test.cpp(调用部分)
    
    int main(int argc ,  char *argv[] )
    {
        People p ;
        teach_knowledge("my name is xuwei" , p ) ;
        teach_knowledge("I am 5 years old " , p ) ;
        p.make_self_introduction() ;
        return 0 ;
    }
    

    友元类:

    与友元函数类似,还可以在类内部定义某个类为该类的友元,称为友元类。
    
    友元类可以访问类的所有成员。
    

    友元类成员函数:

    不仅可以指定某个类为友元,还可以特指某个类的特定成员函数为该类的友元。
    
    这时需要一个确定依赖顺序!
    
    试想,有类People,和类Ball 。 类Ball中想要把类People中的play函数作为自己的友元,
    
    1. 那么在Ball之前People中的Ball函数必须被声明
    
    2. People中的play要访问Ball中的私有成员(友元函数的意义),则要求Ball必须先有定义!(因为只有完整的定义完一个类,才能访问该类的成员,否则报错—— 错误:对不完全的类型‘class Ball’的非法使用)
    
    这似乎形成了一个环——其实不是,这里需要用到声明的作用。
    
    第2条,明确要Ball有定义,但是注意到我们这个函数本身可以先声明再定义,所以这个友元函数放在所有类定义完后定义。
    
    第一条,决定类People必须先定义。如果类People中需要用到Ball这个名称,那么还需要在定义People之前声明一下Ball。
    
    如下顺序:
    
    
        1.  class Ball ; // just declaration
        2.  class People
            {
                void play(Ball& ball) ;
            } ;
        3.  class Ball
            {
                friend void People::play(Ball& ball) ; // 友元函数,无所谓public or private。
                private : 
                    std::string name ;
            } ;
        4.  void People::play(Ball &ball)
            {
                std::cout << "Playing " + ball.name << std::endl ;
            }
    
  16. 在类中定义一个类型成员

    即是,在类作用域下,定义一个类型成员。

    如书上的例子:

    class Screen
    {
    public :
        typdedef std::string::size_type pos  ;
            
        //using pos = std::string::size_type 
    }
    

    在类中定义的类型,作用域在类中,且受到访问控制的约束!即上述定义的类型pos是public的,可以在外部被访问。

    与其他类成员不同,类型必须先定义,才能使用。所以应该定义在类的开始。

  17. 令成员成为内联函数

    前面说到,如果在类内部声明并定义,那么这个函数是隐式内联的。

    也可以使用inline显式内联。此时,可以在声明位置或定义位置使用inline表示,也可以在两个位置都写上inline.

    书上说声明为内联的函数应该声明和定义在同一个(头)文件中;不过在GCC中,不在一个文件中也没有问题。

  18. 可变数据成员

    使用mutable关键字来定义可变数据成员。

    看到的应用场景是:如果一个成员函数定义为const的,那么理论上它不能修改任何成员变量。但是如果一个变量使用mutable修饰,那么这个成员就可以被const函数修改。可以用来统计类的操作被调用次数。

    例: mutable size_t access_ctr ;

  19. 类数据成员的初始化

    在C++11的新标准中,建议使用类内初始值的方式。即,直接使用={}赋初值。

  20. 返回一个*this对象引用

    如果想要支持链式处理,那么就有必要返回一个*this的对象引用!

    注意的一点是,一个返回*this引用的成员函数,如果对象被定义const的。那么这个函数就不能够被调用了!因为对象为const时,传入的this指针是const的,然而返回时却要求返回非const的对象,我们知道可以将非const隐式转为const的,但是const的转为非const的,需要使用const_cast

  21. 不完全的类型

    类同样可以只声明,不定义。这时称其为不完全类型(incomplete type)

    此种类型只可用于定义指针或者引用(引用可能在定义函数形参时使用,指针还可以用来定义指向该类型的指针对象)。

    但,

    • 不能定义对象

      报错: 错误:类型不完全,无法被定义

    • 不能访问成员

      加入Ball类型是不完全类型。那么以下声明合法:

      void play(Ball& ball) ;

      但是定义就不合法了:

      void play(Ball &ball) { cout « ball.name ; }

    因为构造对象需要知道内存空间大小,没有定义编译器没法计算!访问成员编译器需要知道该成员地址,没有定义也没法知道。只有指针或引用是可以的,因为编译时仅仅生成一个符号。

  22. 类的作用域

    [推翻之前观点]在类外部定义成员函数时,从参数列表开始就是在类作用域下了!

    举例,设成员函数move和成员类型Position都是在对象People下的,则该函数的定义如下:

    People::Position People::move( Position moving_pos )
    {
        pos += moving_pos ;
        return pos ;
    }
    

    如上可以看出: 返回类型的作用域需要使用People::来指定在类下,成员函数也需要其指定,但是成员函数的参数列表中就可以不再需要类作用域的指定了。

  23. 类内部名字查找顺序

    我们知道,类声明或定义时一般都是先函数再变量,且不会考虑变量、函数间的顺序的。

    这与类外的名字查找顺序有些不一样,因为在外部我们都是从上到下的,后面的才能看到前面的。

    为什么类内似乎就忽略这这种先后关系呢?

    这其实是错觉

    事实是,编译器在处理类内成员时,是先整体从上到下扫描一遍名字(声明),不会去查看其内部的定义!只有在扫描完类内所有的名字后,才会再去查看定义,而此时,类内部所有的名字(成员变量、函数)都已经被扫描存储到了编译器的表中了,即都是可见的!

    所以本质仍然是从前往后,只不过不先对定义做解释罢了。

    因此,在使用typedefusing定义的名字去定义参数前,这些名字必须出现在这些参数的前面。

  24. 成员变量的初始化顺序

    是按照成员变量定义的先后顺序,而非初始值列表中的顺序。

    这可以认为,编译器在扫描成员变量时,便开始做初始化了。 [猜测] -> 应该是针对每个构造函数做初始化。

  25. 使用默认构造函数定义对象[易错!]

    不能加括号!!

    即:

    T t ;
    

    而不能是

    T t() ; // 错误!这是在声明函数,而非定义一个类对象!
    
  26. 隐式类类型转换及explicit

    含义

    如果只接受一个参数的构造函数,实际上定义了一种隐式转换机制,这种机制通过隐式调用该构造函数,将构造函数需要的参数类型转为类类型。

    这样的构造函数称为转换构造函数

    常见的隐式转换:

    string s = "implict cast" ; 
    // 将 const char* 隐式转为 string类型,这个很常见,但从未想过为何可以。
    // 其实是隐式调用了 string(const char*)这一构造函数。
    

    隐式转换只能跨一步,即不能在一条语句里,先隐式转为一个类型,再从转换后的类型转为新的类型,即使这种转换能够连起来。如书上的例子,在一个函数调用的,想要完成const char* -> string -> Sales_data , 结果不能通过编译。

    首先明确,这种隐式转换,本质是说是容易带来歧义的,不应该广泛使用。除非像 const char* -> string这样看起来可以理所当然的转换。

    套用Python的话,Explicit is better than implicit.

    explicit关键字

    在构造函数前加上explicit 关键字,就可以禁止隐式转换!

    关于此关键字,两点注意:

    1. 只能出现在类内部的声明处,不能出现在类外部的定义位置。

    2. 只能禁止隐式转换,不能禁止强制转换——static_cast

      就是说,只要有只接受一个参数的构造函数,这个类就具有这种转换能力。explicit只是禁止的自动隐式转换的能力。

    例子:

    1. 多次转换出错
    
    构造函数:People(std::string name) : name(name){} ;
    使用:
    
    People p = "LiMing" ;
    
    编译报错: 错误:请求从‘const char [7]’转换到非标量类型‘People’
    如上,因为想要将 const char* -> string -> People ,出现连续两次隐式转换而出错。
    不过,因为const char* -> string 实在是太常用了,以致于常常忽略了其中存在隐式转换的过程。
    因此,一定注意此点!
    
    改:
    
    string name = "LiMing" ;
    People p = name ; // 编译通过
    
    2. 使用explicit,禁止隐式转换
    
    上面 “People p = name ;” 的语句看起来实在别扭!对应的,像vector中一些单参数的构造函数,
    也是用explicit禁止隐式转换的。
    因为它带来了不明确、不直观的含义!
    
    改构造函数: explicit People(std::string name) : name(name){} ;
    
    调用不变,编译结果:错误:请求从‘std::string {aka std::basic_string<char>}’转换到非标量类型‘People’
    
    试试强制转换:
    
    People p = static_cast<People>(name) // 成功编译
    
    3. 在定义出使用explicit关键字
    
    将之前inline的定义变为声明,同时在外部定义,且加上explicit关键字:
    explicit People(std::string name) : name(name){} ;
    编译结果:错误:只有构造函数才能被声明为‘explicit’
    
    错误说明有些奇怪,可能这样之后构造函数的定义就不能被正确识别的。
    

    最后,explicit对只有一个参数的构造函数才有意义,但是对任意构造函数加上explicit,似乎并不会影响编译。

    最后,所谓只有一个参数的构造函数,是指在调用时只需一个参数的构造函数——即,包含默认参数的构造函数也同样有此能力。

    explicit People(std::string name , std::string knowledge="haha") // 同样是转换构造函数。
    
  27. 聚合类

    满足:

    1. 所有成员都是public

    2. 没有定义任何构造函数

    3. 没有类内初始值

    4. 没有基类,没有virtual函数

    如:

    struct ClassKnowledge
    {
       std::string class_name ;
       std::string knowledge ;
    } ;
    

    可以如下初始化:

    ClassKnowledge yuwen = {“语文” , “兰亭集序”} ;

    如其名,一堆变量的聚合。不利于维护,不建议使用。

  28. 字面值常量

    数据成员都是字面值类型的聚合类是字面值常量。

    或者满足:

    1. 所有成员都是字面值类型

    2. 类必须至少有一个constexpr构造函数

    3. 如果数据成员有类内初始值,那么该值必须是字面值;如果数据成员是类,那么类必须也有constexpr构造函数

    4. 类必须使用析构函数的默认定义。

    这个感觉意义不大。不过有些疑惑的是什么叫 “字面值类型”?

    我想应该是初始化时,使用的是字面值,而不是变量。这样在编译阶段,变量的值就定了。这么说,其实这个字面值常量不过就是一堆常量(字面值)的聚合。

  29. 类的静态成员[重点]

    我想这个是比较重要的、有用的一个点。

    类的静态成员,是与类本身相关的变量。所有该类的对象都可以访问类的静态成员。

    static关键字来声明类的静态成员。 用类作用域或者对象来访问类的静态成员。

    关键是静态成员的定义(初始化)。

    静态变量:

    1. 尝试类内初始化(类内初始值)

      static double average_height = 1.68 ;

      报错:错误:‘constexpr’ needed for in-class initialization of static data member ‘double People::average_height’ of non-integral type

      就是说,如果要在类内初始化一个静态变量,那么这个静态变量必须是const的,用const或者constexpr修饰。

      改为:

       static constexpr double average_height = 1.68 ;
      

      这样,要想类内初始化,那么这个值必须是常量的。或者该反过来说,只有静态常量表达式才可以在类内初始化

    2. 尝试类内初始化一个非字面类型

      已经按照第一条,使用constexpr , 但是初始化一个string出错:

       static constexpr string attribute = "lovely" ;
      

      报错一大串:

       错误:广义常变量‘People::attribute’的类型‘const string {aka const std::basic_string<char>}’不是字面常量
       ‘std::basic_string<char>’ is not literal because: (不是字面的)
       ‘std::basic_string<char>’ has a non-trivial destructor(有非平凡析构函数)
       错误:in-class initialization of static data member ‘const string People::attribute’ of non-literal type
       (需要一个类外的初始化)
       ‘People::attribute’不能由一个声明时非常量的表达式初始化
      

      观察上面的错误输出,猜测编译器对静态变量做类内初始化,应该是在编译阶段就做的!所以一个确定的内存开销、确定的字面值是必须的。

    由上,要初始化一个静态变量,最好是在类外初始化。类内初始受到很大的限制——只能是字面值类型,且必须常量(有确定内存开销,有确定值,这样才能在编译阶段就分配空间)。

    那么又如何类外初始化呢?

    1. 直接赋值(定义、初始化)

      • 在全局状态下(非main函数中)

        在private控制下静态变量,直接赋值(这个说法不准确,这里指用“=”的方式初始化;以后一定谨慎使用“赋值”二字)

        使用“=”好做初始化,成功!!

          string People::attribute = "666" ; // 成功编译
        

        当尝试再次用“=”号访问时,报错:

        错误:‘std::string People::attribute’ 重定义

          string People::attribute = "666" ;
          string People::attribute = "777" ;
        
      • 在main函数中

        用“=”初始化失败,报错:

        错误:对限定名‘People::attribute’的使用无效

      故应该在全局环境下,在类外使用“=”做定义(初始化),且定义只能一次。

    2. 使用接口(静态成员函数)

      在不做1的条件下,直接定义一个函数来尝试设置该变量的值:

       void People::set_attribute(string attr)
       {
          attribute = attr ;
       } ;
      

      结果报错:

       /tmp/ccGt9VJ7.o:在函数‘People::set_attribute(std::string)’中:
       test.cpp:(.text+0x16c):对‘People::attribute’未定义的引用
       collect2: 错误:ld 返回 1
      

      可知是链接时出错!

      也就是说,在类中声明一个静态变量,如果不在类外部初始化,那么其实际上根本没有被分配内存空间!链接时根本找不到这个变量!而由之前的部分知,如果在类内部初始化,那么该值又必须是常量!外部不能更改。

    在类外定义静态变量似乎是一件很神奇的事——因为看起来在类的外部访问了类的私有变量。但是我们可以这样认为,成员的定义是由类的作者完成的,是在类的使用者使用之前必须完成的。由于其只能被定义一次,所以类的使用者就再也不能去访问它了。