《C++primer》第七章读书笔记


《C++Primer 第五版》

? ——读书随笔集

第七章

7.1.4 构造函数

  • 合成的默认构造函数

    如果我们没有显示地定义构造函数,编译器就会为我们构造一个默认构造函数(合成的默认构造函数),构造规则如下:

    • 如果存在类内的初始值,用它来初始化成员。
    • 否则,默认初始化该成员。
  • 某些类不能依赖于合成的默认初始化构造函数

    • 编译器只有在没有发现类中不包含任何构造函数的情况下,才会为我们生成一个默认的构造函数。一旦我们定义了其它的构造函数,编译器就不会为我们定义默认的构造函数,除非我们自己再定义一个默认构造函数。总结来说:如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。
    • 对于某些类来说,默认的构造函数很可能会执行错误的操作。如果我们定义的内置类型或者复合类型的对象被默认初始化,则它们的值是未定义的。同理,含有复合类型或者内置类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数,否则用户在创建类的对象时,会得到未定义的值。
    • 编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其它类类型的成员且这个成员的类型没有默认构造函数,那么编译器无法初始化该成员。对于这种情况,我们必须自己定义默认构造函数,否则该类没有可用的默认构造函数。
  • 定义构造函数

    Sales_data() = default;
    

? 上述函数不接受任何实参,所以它是一个默认构造函数。此函数的作用完全等同于之前使用的合成的默认构造函数。

? 在C++11新标准中,如果我们需要默认的行为,没那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。其中, 如果 = default 出现在类的内部,则默认构造函数是内联的,如果它在类的外部,则该成员默认情况下不是内联的。

  • 构造函数初始值列表

    Sales_data(const std::string &s) : bookNo(s) {}
    

    其它没有出现的成员会与默认初始构造函数一样进行隐式初始化。如果你的编译器不能支持类内初始值,你应该显式的初始化每个内置类型的成员。

  • 在类的外部定义构造函数

    Sales_data::Sales_data(std::istream &is) {
        read(is, *this);
    }
    

    必须指明具体的类,没有出现在构造函数初始值列表中的值将执行默认初始化或者通过相应的类内的初始值。

7.1.5 拷贝,赋值和析构

  • 某些类不能依赖于合成的版本

    对于某些类来说合成的版本无法正常工作。特别是,当类需要分配对象之外的资源时,合成的版本会失效。

    但是很多需要动态内存的类能使用string和vector对象管理必要的存储空间。使用string和vector能避免分配和释放内存带来的复杂性。

7.2 访问控制和封装

  • 访问说明符

    public说明符后的成员表示整个程序内都可以访问,其用来定义类的接口。

    private说明符后的成员可以被类内的代码访问,用来进行封装类的实现细节。

  • class和struct

    两者实际上差不多,只不过默认的访问权限不同

    class的默认访问权限是private,而struct的访问权限是public。

7.2.1 友元

类可以允许其他类的友元函数访问它的非公有成员,方法是令其他类或者函数成为他的友元。以friend关键字开始的函数声明语句即可。

class Sales_data {
    friend Sales_data add(const Sales_data& ,const Sales_data&);
    friend std::istream &read(std::istream&, Sales_data&);
    friend std::ostream &print(std::ostream&, Sales_data&);
    
    public:
    Sales_data() = default;
    Sales_data(const std::string& s,unsigned n, double p): bookNo(s), units_sold(n), revenue(n) {};
    Sales_data(const std::string& s): bookNo(s) {};
    Sales_data(std::istream&);
    std::string isbn() const {return bookNo;};
    Sales_data &combine(const Sales_data&);
    
    private:
    std::string bookNo;
    unsigned units_sold;
    double revenue;
};
Sales_data add(const Sales_data& ,const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, Sales_data&);

一般来说,最好在类定义开始或者结束的位置集中声明友元。

7.3 类的其它特性

7.3.1 类成员再探

  • 定义一个类型成员

  • 令类成员作为内联函数

    在类外部定义的地方说明inline,这样更好理解。

  • 重载成员函数

  • 可变数据成员

    有时我们希望修改类的某个数据成员,即使是在一个const成员函数中,这个时候我们就可以通过在变量的声明中加入mutable关键字。一个可变的数据成员永远不可能是const,即使它是const对象的成员。

    class Screen {
        public:
        void some_member() const;
        
        private:
        mutable size_t access_ctr;
    };
    void Screen::some_member() const {
        ++access_ctr;
    }
    
  • 类数据成员的初始值

    将默认值声明成一个类内初始值。

7.3.2 返回*this的成员函数

实际上返回的是类对象的引用。

  • 从const成员函数返回*this

    一个const成员函数如果以引用的形式返回*this,那么他的返回类型将是常量引用。

  • 基于const的重载

7.3.3 类类型

即使两个类的成员列表完全一样,它们也是不同的类型。对于一个类来说,它的成员和其它任何类的成员都不是一回事。

  • 类的声明

    只声明,不定义。这种被称为前向声明

7.3.4 友元再探

前面我们把函数定义成了类的友元。类还可以把类定义成友元,也可以把其它类的成员函数定义成友元。

  • 类之间的友元关系

    每个类负责控制自己的友元类或友元函数

  • 令成员函数作为友元

    • 首先定义第二个类,其中声明要使用的函数,但是不能定义它。
    • 然后定义第二个类,其中声明友元函数。
    • 最后定义该函数,此时它可以使用第二个类的成员。
  • 友元声明和作用域

    即使我们在类的内部定义了该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。否则函数只在类的内部可见。

    友元声明的作用是影响访问权限

  • 函数重载和友元

    尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如何一个类把一组重载函数声明为它的友元,它需要对这组函数中的每一个分别声明。

7.4 类的作用域

  • 作用域和定义在类外部的成员

    使用定义在类内部的成员时,必须指明类名。类内部定义的数据成员的作用域只在类的内部。

7.4.1 名字的查找与类的作用域

编译器处理完类中的全部声明之后,才会处理成员函数的定义。

  • 用于类成员声明的名字查找

  • 类型名要特殊处理

    类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

  • 成员定义中的普通块作用域的名字查找

  • 类作用域之后,在外围的作用域中查找

    尽管外层的对象被隐藏掉了,但是我们仍然可以使用作用域运算符访问它。

  • 在文件中名字的出现处对其进行解析

7.5 构造函数再探

7.5.1 构造函数初始值列表

  • 构造函数的初始值列表有时必不可少

    对于成员是const或者引用的话,必须将其[1]。随着构造函数体的一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。

    如果成员是const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

    建议养成使用构造函数初始化值列表的习惯,这样可以避免某些意想不到的编译错误。

  • 成员初始化的顺序

    成员的初始化顺序与它们在类定义中出现的顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中的初始值的前后位置不会影响实际的初始化顺序。

    最好令构造函数的初始值顺序与成员声明的顺序保持一致。尽量避免使用某个成员去初始化另外一个成员。

  • 默认实参和构造函数

    如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

7.5.2 委托构造函数

C++11扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。

class Sales_data {
    public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(std::string ,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);}
};

注意执行顺序:对于最后一个委托函数,先执行受委托构造函数的初始值列表和函数体,然后再执行委托函数的函数体。

7.5.3 默认构造函数的作用

在实际中,如果我们定义了其它构造函数,我们最好也提供一个默认构造函数。

7.5.4 隐式的类类型转换

能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

  • 只允许一步类类型转换

  • 类类型转换不是总有效

  • 抑制构造函数定义的隐式转换

    在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:

    class Sales_data {
        public:
        Sales_data() = default;
        Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {};
        explicit Sales_data(const std::string &s): bookNo(s) {};
        explicit Sales_data(std::istream&);
    }
    

    关键字explicit只对一个实参的构造函数有用。需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit的。只能在类内构造函数时使用explicit关键字,在类外定义时,不应重复。

  • explicit构造函数只能用于直接初始化

    Sales_data item1(null_book) //正确,直接初始化
    Sales_data item2 = null_book //错误,不能将explicit构造函数用于拷贝形式的初始化过程
    

    当我们使用explicit关键字声明的构造函数时,它只能以直接初始化的形式使用。

  • 为转化显示地使用构造函数

  • 标准库中含有显示构造函数的类

    • 接受一个单参数的const char* 的string构造函数,不是explicit
    • 接受一个容量参数的vector构造函数是explicit

7.5.5 聚合类

  • 聚合类可以使用户直接访问其成员,并且具有特殊的初始化语法形式。当一个类具有如下条件时,我们可以说它是聚合的:

    • 所有类的成员都是public
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数。
    //定义一个类,符合聚合类的条件
    struct Data {
        int ival;
        string s;
    }
    //特殊的初始化形式
    Data vall = {0, "Anna"};
    

    需要注意的是,初始值的顺序必须与定义的顺序一致,否则会出错。

7.5.6 字面值常量类

数据成员都是字面值类型的聚合类(参见7.5.5 节,第266页)是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:

  • 数据成员都必须是字面值类型。
  • 类必须至少含有一个constexpr构造函数。
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一 条常量表达式(参见2.4.4节,第58页);或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象

7.6 类的静态成员

  • 声明静态成员

    直接在成员的声明之前加上关键字static使得其与类关联在一起。类的静态成员存在于类的所有对象中,类的所有对象共享同一个静态成员。静态成员函数不能与任何对象绑定在一起,他们不包含this指针。

  • 使用类的静态成员

    • 我们使用作用域运算符来直接访问静态成员

      double r;
      r = Account::rate()
      
    • 还可以使用类的对象来访问静态成员

      Account ac1;
      Account *ac2 = &ac1;
      r = ac1.rate();
      r = ac2->rate();
      
    • 成员函数可以直接使用静态成员,不用通过域作用符。

  • 定义静态成员

    既可以类的内部定义静态成员,也可以在类的外部定义,但是在类的外部定义的时候,不能重复关键字static。该关键字只出现在类内部的声明中。

    不能在类的内部初始化静态成员。必须在类的外部定义和初始化每一个静态成员。静态成员一经定义,就存在于程序的整个生命周期中。

  • 静态成员的类内初始化

    可以为静态成员提供const整数类型的初始值,不过要求静态成员必须为字面值常量类型的constexpr。

    即使一个常量静态数据成员在类的内部被初始化了,通常情况下也应该在类的外部定义一下该成员。

  • 静态成员能用于某些场景,而普通成员不行。


  1. 使用初始化列表进行初始化 ??