Reading Note of CppPrimer-Chapter7

Classes

Class Scope

  • 类的定义域

    类内,一个类本身就是一个作用域,类似于 namespace,在作用域内部使用域内的对象,不需要使用域作用符::

    类外,遇到类名,定义的剩余部分 (包含参数的函数体) 就都在类的定义域内部了;另一方面,类成员函数类外定义的时候的返回值,出现在函数名之前,不在类的定义域内

    1
    2
    3
    4
    // define member function outside class
    Screen::pos Screen::GetCurrentPos(pos curr_pos){ // 第一个pos要用域运算符,第二个pos不需要
    // do something here
    }
  • 编译器在处理完类的全部声明之后,才会处理类的成员函数的定义;这样以来就不需要特别注意类内部成员、函数的顺序;类型成员除外 (因为类型成员的位置会影响类内使用的同名类型的解析,一般类型成员要放在类定义开始的地方)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef int length;

    class Point3D{
    public:
    void muable(length val) {_val = val;} // type of val is int
    private:
    typedef float length;
    length _val; // type of _val is int
    };
  • 一般来说,内层作用域可以重新定义外层作用域相同名字的变量;类型变量除外

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef double Money;
    std::string bal;

    class Account{
    public:
    Money balance(){return bal}; // bal: type Money
    private:
    typedef float Money; // invalid, can not overwrite type-member in outer scope
    Money bal; // valid, overwrite bal in outer scope
    };
  • 成员定义中普通块作用域 (block scope) 的名字查找 (name lookup)

    1. 首先在成员函数中查找该名字的声明,比如函数的参数可以 overwrite 类成员

    2. 在类内继续查找,这时类的所有成员都可以被考虑

    3. 如果类内也没有找到,就在成员函数定义之前的作用域继续查找;

Class Members

  • 类的封装:可以使用 publicprivateprotected 等访问说明符 (access specifiers) 来加强类的封装

    封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别

    • 可以确保用户代码不会无意间破坏封装对象的状态
    • 被封装的对象的具体细节可以随时改变而无需调整用户级别的代码
  • 使用 class 或者 struct 的唯一区别就是默认访问权限不同class 默认是 private 的,struct 默认是 public 的;

  • 编译器首先编译类的声明,然后才是成员函数的定义;所以类成员的声明的顺序可以随意,类的类型成员例外一般出现在类定义开始的地方);如果一个类只有声明,而没有定义,则这个类是个不完全类型

  • 不完全类型:在声明后没有定义之前,此时的类型是不完全类型 (incomplete type)

    不完全类型只能在有限的情景下使用:

    • 定义指向这种类型的指针或者引用

    • 声明 (但不能定义) 用这种不完全类型作为形参或者返回值的函数;

  • 即使两个类的成员列表完全一致,他们也是不同类型

  • 定义类型成员:除了定义数据和函数成员外,类还可以自定义在某种类型在类内的别名,即类型成员;

    由类定义的类型成员和其他成员一样存在访问限制;

    用来定义类型的类型成员必须先定义后使用,这点和普通成员有区别;所以类型成员一般在类声明开始的地方集中定义

    1
    2
    3
    4
    5
    6
    7
    8
    class Screen {
    publib:
    typedef std::string::size_type pos;
    using pos1 = std::string::size_type;
    pos GetCurrentPos(pos curr_pos);
    private:
    pos cursor; // 此处使用了前面定义的类型成员;用来定义类型的成员必须先定义后使用
    };
  • 类的隐式对象形参 (implicit object parameter)

    调用类的成员函数的时候,成员函数是通过一个名为 this 的额外的隐式形参 (implicit object parameter) 来访问那个他调用的对象;

    this 是个常量指针,不允许改变 this 保存的地址,即类对象的地址;注意是 this常量指针,不是指向常量的指针;任何对类成员的直接访问都被看作是 this 的隐式引用;实际上,任何自定义名为 this 的参数或者变量的行为都是非法的,this 是一个关键字;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Sale_data{
    public:
    std::string isbn{return bookNo;};
    private: bookNo;
    };

    Sale_data total;
    total.isbn();

    //伪代码
    Sale_data::isbn(&total);
    // this类型: Sale_data* const, NOT Const Sale_data*
  • const 成员函数

    this 默认是指向类的非常量 non-const 对象的常量指针,这样的指针无法指向类的 const 对象,这将使得常量对象无法调用普通成员函数,所以将 this 声明为指向常量的常量指针比较好,这样对于 const 和非 const 的对象都可以用;

    C++ 允许将 const 放在成员函数参数列表后面,表示 this 是指向常量的指针;常量对象或者常量对象的指针引用都只能调用常量成员函数;如果是在类外定义成员函数,也需要加上 const 限定;

    1
    2
    3
    4
    5
    6
    7
    8
    class Sale_data{
    std::string isbn() const {return this->bookNo};
    std::string bookNo;
    };

    // Sale_data* const -> const Sale_data* const
    //等价于伪码
    std::string Sale_data::isbn(const Sale_data* const this){return this->bookNo;}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Test {
    public:
    Test() = default;
    void print(){
    std::cout << "print from Test" << std::endl;
    }
    void print() const { // const overload
    std::cout << "print from const Test" << std::endl;
    }
    };

    Test t;
    const Test t1 = t;
    t.print(); // print from Test

    // if no overloaded const print function
    // error: 'this' argument to member function 'print' has type 'const Test',
    // but function is not marked const
    t1.print(); // print from const Test
  • mutable 数据成员:适用于即使是在 const 对象中也需要修改的类成员;一个 mutable 数据成员永远不会是 const,即使他是 const 对象的成员;

    1
    2
    3
    4
    5
    6
    class Screen {
    mutable size_t access_ctr = 0;
    };
    void Screen::some_member() const{
    ++access_ctr; // 即使some_member声明了const,但是仍然可以修改assess_ctr;
    }
  • 通过区分成员函数参数列表后面是不是有 const可以实现函数的重载;原因和形参为 pointer to constconst reference 的函数可以实现重载一样;

    1
    2
    3
    4
    5
    class Screen {
    public:
    Screen &display(std::ostream &os); // 非const会优先调用
    const Screen &display(std::ostream &os) const; // 只有const对象可以调用
    };
  • 返回 *this 的成员函数

    类成员函数可以返回 *this,此时返回引用的函数是左值的,即函数返回的是对象本身而不是对象的副本;const 成员函数返回的将是常量引用或指针,因为此时 this 的类型为 const class_type* const

    1
    2
    3
    4
    5
    class Screen{
    public:
    Screen& set(char){return *this;}
    const Screes& get() const {return *this;} // return const reference
    };
  • 定义在类内部的成员函数默认是 inline 函数;

    尽管是隐式 inline,我们也可以在声明和定义的地方都加上 inline,一般只在函数定义的地方加上 inline;

    如果成员函数不在类体内定义,而在类体外定义,系统并不会默认把它当作 inline 函数,此时应该显式加 inline

    如果在类体外定义 inline 函数,则心须将类定义和成员函数的定义都放在同一个头文件中,否则编译时无法进行置换;

    inline 函数并不是在 preprocess 阶段置换的,而是在编译后直接拷贝指令块;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // header begin
    class student {
    public:
    inline void display(); //声明此成员函数inline函数
    private:
    int num;
    string name;
    char sex;
    };

    // 普通inline函数的定义也应该在头文件中
    inline void print(){
    }

    // 类体外定义inline函数,则心须将类定义和成员函数的定义都放在同一个头文件中,否则编译时无法进行置换
    inline void student::display() {
    }
    // header end
  • 在类外定义成员函数的时候,要确保返回类型、参数列表、函数名都与类内部的声明保持一致

  • 友元 (friend):类可以允许其他类或者其他函数 (一般函数和类成员函数) 访问他的非公有成员;方法是令其他类或函数作为他的友元;

    友元的声明仅仅制定了访问权限,并不是一个普通意义上的函数声明;我们需要在友元声明之外专门对这些函数进行声明,尽管有些编译器不强制要求这么干;友元的关系不具有传递性 (不会继承),仅限于当前类和友元类的一对一的关系;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Sales_data{
    // 友元声明
    friend std::ostram &print(std::ostream&, const Sales_data){
    // 友元函数可以直接定义在类内部
    }
    // other list below
    };

    // 最好进行友元函数的正式声明,尽管有些编译器不强制要求这么干
    // 友元函数的正式声明最好和类在同一个头文件,方便类的使用者访问友元函数
    std::ostram &print(std::ostream&, const Sales_data);
  • 友元使用场景:实际上具体大概有下面两种情况需要使用友元函数:

    1. 运算符重载的某些场合需要使用友元
    2. 两个类要共享数据的时候
  • 友元参数:因为友元函数没有 this 指针,则参数要有三种情况:

    1. 要访问 non-static 成员时,需要对象做参数

    2. 要访问 static 成员或全局变量时,则不需要对象做参数;

    3. 如果做参数的对象是全局对象,则不需要对象做参数;

Class Constructor

  • 构造函数与其他函数不一样的是,他没有返回类型,而且不能被声成 const;当我们创建一个 const 对象的时候,直到构造函数完成初始化,对象才真正取得其常量 (const) 的属性;因此构造函数在 const 对象的构造过程中可以对其写值;

  • 默认构造函数 (default constructor)

    当类没有任何其他的用户定义的构造函数的时候,编译器才会为其合成默认构造函数(只是需要的时候才会合成);

    如果有定义其他构造函数,需要使用默认构造函数的时候,必须手动定义;

    一般来说,如果有其他的构造函数的时候,可以习惯性为类定义一个默认构造函数;

    只有当类内的内置类型或者复合类型成员都被赋予了初始值的时候,才适合使用合成的默认构造函数;

    1
    2
    3
    4
    struct Sales_data{
    Sales_data() = default; // 手动定义默认构造函数,C++11
    Sales_data(std::istream &);
    }
  • 编译器有可能不会为类合成默认构造函数

    • 定义了其他构造函数
    • 类内的某个成员没有默认构造函数的时候;
    • 其他情况,比如一个类是 bitwise copy semantics ,则不需要构造函数 [inside the cpp object model P47];
  • 默认构造函数的初始化过程:

    • 如果存在类内的初始值,用它来初始化变量;
    • 否则默认初始化
  • 如果一个构造函数的所有参数都有默认参数,则它实际上相当于也定义了默认构造函数

    比如只接受 string 和 istream & 作为参数的构造函数都定义了实参,此时会有二义性 (ambiguous),默认构造函数不知道应该调用哪一个;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct Test1{
    Test1() = default; // ambiguous
    Test1(std::string name=""); // ambiguous
    Test1(std::istream& name_stream=std::cin); // ambiguous
    };

    Test1 t2;

    // error: call to constructor of 'Test1' is ambiguous
    // note: candidate constructor
    // Test1() = default;
    // ^
    // note: candidate constructor
    // Test1(std::string name=""){
    // ^
    // note: candidate constructor
    // Test1(std::istream& name_stream=std::cin){
  • 构造函数类成员的初始化可以通过初始化列表构造函数体内赋值实现,前者是初始化,后者是赋值;

    • 如果成员是 const、引用,或者属于某种未提供默认构造函数的类类型,必须通过初始化列表进行初始化;
    • 当程序执行进入函数体内时,初始化已经完成了,这时候如果在构造函数体内进行赋值初始化,相当于数据成员先进行了默认初始化,然后进行了赋值覆盖默认初始化的值;
    • 构造函数不应该轻易覆盖类内的初始值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct Test1 {
    Test1(int a):i(a){}
    int i ;
    };

    struct Test2 {
    Test1 test1 ;
    Test2(Test1 &t1){
    // invalid, 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,必须通过初始化列表进行初始化
    test1 = t1;
    }
    };

    struct Test2 {
    Test1 test1 ;
    Test2(Test1 &t1):test1(t1){} // valid
    }
  • 类的数据成员是按照他们在类中声明的顺序进行初始化的,而不是按照他们在初始化列表出现的顺序初始化的;一个好的习惯是,按照成员定义的顺序进行初始化;避免使用某些成员初始化其他成员;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct foo {
    int i ;
    int j ;
    // 这里i的值是未定义的,虽然j在初始化列表里面出现在i前面,但是i先于j定义,
    // 所以先初始化i,但i由j初始化,此时j尚未初始化,所以导致i的值未定义
    foo(int x):j(x), i(j){}

    // valid
    foo(int x):i(x), j(i){}
    };
  • 委托构造函数 (deleagting constructor)

    一个委托构造函数使用它所属类的其他先于自己定义的构造函数执行它自己的初始化

    当一个构造函数委托给了另一个构造函数,首先会执行受委托的构造函数的初始值列表和函数体,然后才指向该构造函数的函数体;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Sales_data{
    public:
    Sales_data(std::string s, unsigned cnt, double price)
    :bookNo(s), units_sold(cnd), revenue(cnt*price){}
    Sales_data() : Sales_data("", 0, 0) // delegating constructor using 1st ctor
    Sales_data(std::string s) : Sales_data(s, 0, 0) // delegating constructor using 1st ctor
    Sales_data(std::istream& stream) : Sales_data(){ // delegating constructor using 2nd ctor
    read(stream, *this);
    }
    }
  • 转换构造函数 (converting constructor)

    如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换规则,也即转换构造函数;编译器只支持一步转换,多于一步的都是错误的;事实上我们经常使用的 std::string 就经常使用隐式转换,将 const char* 转换成 std::string;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // definition refer to last item
    class Sales_data {
    Sales_data& combine(const Sales_data&);
    };

    Sales_data item;

    // valid, convert once
    std::string null_book = "9-999-999-9";
    item.combine(null_bool) // convert string to Sales_data, using 7.27 constructor 3
    item.combine(std::cin) // convert istream to Sales_data, using 7.27 constructor 4

    // invalid, convert twice:
    // 1) convert const char* to string,
    // 2) convert string to Sales_data
    item.combine("9-999-999-9");

    // common case
    std::string str = "PangWong"; // implicit converting from char* to string
    std::vector<int> vec(10); // not implicit converting, or the vec will be a vector with only one item
  • explicit 关键字

    在可以隐式转换的程序上下文中,可以通过将构造函数声明为 explicit阻止隐式的类型转换,即隐式的初始化;

    explicit 只对一个实参的构造函数有效,需要多个实参的构造函数本身并不能成为 converting constructor;

    只在类内声明构造函数的时候写 explicit,类外定义的时候不需要再写

    explicit 的构造函数只能使用直接初始化,不能用于拷贝形式的初始化

    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
    // subtitude constructor 3,4 in class Sales_data with explicit version
    explicit Sales_data(std::string s) : Sales_data(s, 0, 0){} /*3*/
    explicit Sales_data(std::istream& stream): Sales_data(){ /*4*/
    read(stream, *this);
    }
    // copy costructor set to explicit
    explicit Sales_data(const Sales_data &s){} /*5*/

    // define constructor function outside class
    // invalid, explicit keyword can only appear at constructor declaration inside class
    explicit Sales_data::Sales_data(std::istream& stream): Sales_data(){
    read(stream, *this);
    }

    Sales_data item1(null_book);

    // invalid, 当用explicit关键字声明构造函数时,他将只能以直接初始化形式来使用;
    // 在自动转换的过程中,编译器不会再使用该构造函数
    Sales_data item2 = null_bool;

    // 实参是显式构造的Sales_data对象
    item.combine(Sales_data(null_book));
    // static cast可以使用explicit的构造函数
    item.combine(static_cast<Sales_data>(cin));

    // 以下都是explicit构造函数
    std::string(const char* );
    std::vector(vec_size);
  • 聚合类 (aggrerate class):成为一个聚合类的条件为:

    1. 所有成员均为 public 的
    2. 没有定义构造函数
    3. 没有类内初始值
    4. 没有基类,也没有虚函数
  • 聚合类可以直接使用列表初始化;初始化列表初始化聚合类的时候,初始值的顺序和声明的顺序一致

    1
    2
    3
    4
    5
    6
    7
    struct Data{
    int ival;
    std::string s;
    };
    // 初始化
    Data vall = {0, "PangWong"}; // valid, 初始值的顺序必须和声明的顺序一致
    Data vall = {"PangWong", 0}; // invalid,var type mismatch
  • 字面值常量类:字面值的常量类有两种定义:

    • 数据成员都是字面值类型 (算术类型,引用和指针,以及字面值常量类) 的聚合类 (aggrerate class) 是字面值常量类

    • 或者满足如下的定义:
      1. 数据成员都必须是字面值类型 (算术类型,引用和指针,以及字面值常量类)

      2. 类必须至少含有一个 constexpr 构造函数 ; 尽管类的构造函数不能是 const 的,但是却可以是 constexpr 的;

      3. 如果一个数据成员含有类内初始值,则内置类型的初始值必须是一条常量表达式。或者如果成员属性某种类类型,则初始值必须使用成员自己的 constexpr 构造函数
      4. 类必须使用析构函数的默认定义,该成员负责销毁类的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class ConstLiteralClass{
    constexpr int a = 10;
    constexpr float *b = nullptr;
    };

    class CPoint {
    public:
    constexpr CPoint(int xx, int yy) : x(xx), y(yy){}
    void setx(int xx){x = xx;}
    void sety(int yy){y=yy;}
    constexpr int getx() const {return x;}
    constexpr int gety() const {return y;}
    private:
    int x;
    int y;
    };

    constexpr CPoint point(100, 200); // 这种对象可以在编译的时候展开
    CPoint point(100, 200); // 不会编译器展开了

Class Static Member

  • 类的静态成员 vs 非静态成员

    • 静态成员可以是不完整类型,特别的,静态成员的类型可以就是它所属的类类型,非静态成员必须是完整类型
    • 静态成员可以作为默认参数,非静态成员不可以;
    1
    2
    3
    4
    5
    6
    7
    8
    class Bar{
    public:
    Bar();
    private:
    static Bar mem1; // valid, static member can be incomplete
    Bar *mem2; // invalid
    Bar mem3; // invalid
    };
  • 类的静态成员存在于任何对象之外,对象不包含任何与静态变量有关的数据;

    类的静态函数也不与任何对象绑定在一起,他们不包含 this 指针,作为结果,静态成员函数不能声明为 const 的(因为 const 是修饰 this 的),而且也不能在静态成员函数内使用 this 指针

    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
    class Account{
    public:
    static double rate(){return interest_rate;}
    static double rate1();
    double fee();
    private:
    double amount;
    // static成员的声明
    static double interestRate;
    };

    // 使用域运算符直接访问静态成员
    double r = Account::rate();

    // 使用类的对象、引用、指针来访问静态成员
    Account ac1;
    Account *ac2 = &ac1;
    double r1 = ac1.rate();
    double rc = ac2->rate();

    //成员函数不需要通过域运算符就能直接使用静态成员
    double Account::fee(){
    return amount * interestRate;
    }

    // 在类外定义静态成员函数的时候不用声明static,该关键字只出现在类内部的声明语句
    double Account::rate1(){
    return intersetRate;
    }
  • 由于静态数据成员不属于类的任何一个对象,所以他们并不是在构造函数里面被初始化的;一般不会在类内初始化类的静态成员,相反,必须在类的外部定义和初始化类的静态数据成员;一个静态数据成员只可以被定义一次;类的 static 成员是在定义的时候分配内存的,不是在声明或者创建类对象的时候;

    1
    2
    // static的定义和初始化,分配内存
    double Account::interestRate = rate();
  • 通常情况下,类的静态成员不应该在类的内部初始化。只有一种例外,即静态常量数据成员初始值必须是常量表达式(constexpr);即使一个静态常量数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class A {  
    private:
    static int count = 0; // 静态成员不能在类内初始化
    };

    class A {
    private:
    const int count = 0; // 常量成员也不能在类内初始化,只能在构造函数后的初始化列表中初始化
    };

    class A {
    private:
    static constexpr int count = 0; // 静态常量成员可以在类内初始化,类只有唯一一份拷贝,且数值不能改变
    };
    // 一个不带初始值的静态成员的定义,初始值在类内的定义内提供
    constexpr int A::count;