7.1 定义抽象数据类

类背后的基本思想:数据抽象 和 封装。
数据抽象:是一种依赖于 接口和实现分离 的编程技术
封装:将类的内部成员 设置成外部不可见,但提供部分接口给外面

(1) 类成员:
  • 必须在类的内部声明,不能在其他地方增加成员。
  • 成员可以是数据,函数,类型别名。

(2) 类的成员函数:
  • 成员函数的声明 必须在类的内部;成员函数的定义 既可以在类的内部也可以在外部。
  • 对象使用运算符(.) 调用成员函数。
  • 默认实参:Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }
注:
  • 必须对任何 const或引用类型成员以及没有默认构造函数的类类型的任何成员 初始化。
  • 定义在 类内部的函数 是隐式的inline函数。

(3) this指针:
  • 每个成员函数都有一个额外的,隐含的形参this指针。
  • this总是指向当前对象,因此this是一个常量指针。(class_type *const p)
  • 常量成员函数:形参表后面跟const,改变了隐含的this指针的类型(从class_type *const -> const class_type *const),this指向的当前对象是常量。(如:bool same_isbn(const Sales_item &rhs)  const
注:
  • return *this;可以让成员函数连续调用。(对象调用函数又返回对象本身,岂不是可以又接着继续调用函数)

(4) 非成员函数:
  • 和类相关的非成员函数,定义和声明 都应该在类的外部。
  • 经验:一般来说,如果非成员函数是 类接口的组成部分,则这些函数的声明 应该与类在同一个头文件内。

类的构造函数:

类通过一个或者几个特殊的成员函数 来控制其对象的初始化过程,这些函数叫做 构造函数。(构造函数是 特殊的 且 与类同名的 成员函数)
注:
  • 构造函数放在类的public部分。
  • 不同于其他成员函数,构造函数不能被声明成const的。(当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值)

默认构造函数:
类通过一个特殊的构造函数来控制 默认初始化过程,这个函数叫做 默认构造函数。(默认构造函数无须任何实参)
编译器创建的构造函数又被称为 合成的默认构造函数
合成的默认构造函数 按照如下规则 初始化类的数据成员:
  • 如果存在 类内的初始值,则用它来初始化成员。
  • 否则,默认初始化该成员。

对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:
  • 编译器只有在发现 类不包含任何构造函数的情况下 才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义 一个默认的构造函数,否则类将没有默认构造函数。
  • 对于某些类来说,合成的默认构造函数可能执行错误的操作。例如:如果定义在块中的 内置类型或复合类型(比如数组和指针)的对象 被默认初始化,则它们的值将是 未定义的。
  • 有的时候编译器不能为某些类 合成默认的构造函数。例如:如果类中包含一个其他类类型的成员 且这个成员的类型没有默认构造函数,那么编译器将 无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。

  • =default的含义:如果我们需要默认的行为,那么可以通过 在参数列表后面写上 = default 来要求编译器生成构造函数 (C++11)。(其中,= default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果= default在类的内部,则默认构造函数是 内联的;如果它在类的外部,则该成员默认情况下 不是内联的)
  • 构造函数初始化列表:冒号和花括号之间的代码: Sales_item() : units_sold(0), revenue(0.0) { }
注:
  • 构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。
  • 如果你的编译器不支持使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。

7.2 访问控制与封装

(1) 访问说明符:
  • public:定义在 public后面的成员在整个程序内可以被访问(public成员定义类的接口)
  • private:定义在 private后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问(private隐藏了类的实现细节)

(2) class和struct的区别:
class或者 struct 都可以被用于定义一个类。唯一的却别在于 访问权限。
  • 使用class时,定义在第一个访问说明符之前的成员是 priavte的。
  • 使用struct时,定义在第一个访问说明符之前的成员是 public的。

(3) 友元:
  • 友元:允许特定的非成员函数访问 一个类的私有成员,友元的声明以 关键字 friend 开始。
  • 友元类:如果一个类指定了友元类,则友元类的成员函数 可以访问此类包括非公有成员在内 的所有成员。
注:
  • 友元的声明仅仅指定了 访问的权限,而非一个通常意义上的函数声明。
  • 友元关系不存在传递性。(朋友的朋友不一定是我的朋友)

7.3 类的其他特性

(1) 成员函数作为内联函数的 两种方法
  1. 在类内部定义。
  2. 在类外部定义,但显示的加上inline关键字。

(2) 可变数据成员:一个可变数据成员永远不会是const,即使它是const对象的成员。因此,一个const成员函数可以改变一个可变成员的值。(mutable size_t access_ctr;
类类型:每个类定义了唯一的类型。(对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型)

(3) 类的声明:
就像可以把函数的声明和定义分离开来一样,我们也能仅仅声明类而暂时不定义它:
class Screen;      // Screen 类的声明
这种声明有时被称作 前向声明,它向程序中引入了名字Screen 并且指明Sereen是一种类类型。(对于类型Screen来说,在它 声明之后定义之前 是一个不完全类型,也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员)
前向声明只能在非常有限的情景下使用:
  1. 不能定义对象
  2. 可以用于定义指向这个类型的指针或引用
  3. 用于声明作为形参类型或函数的返回值类型
class B;       // 如果注释掉会报错: 找不到标识符"B"

class A {
public:
    void fun_A(B& b);

private:
    B *b;      // 如果改成 B b; 也会报错: 使用未定义class B
    A *a;      // 如果改成 A a; 也会报错: 使用正在定义的"A"
};
注:因为只有当类全部完成后 类才算被定义,所以一个类的成员类型 不能是该类自己。然而,一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含 指向它自身类型的引用或指针:

7.4 类的作用域

每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员 只能由对象、引用、指针使用成员访问运算符来访问;类类型成员 则使用作用域运算符来访问。
(1) 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时 (此时返回类型是在作用域说明符之前),返回类型中使用的名字都位于类的作用域之外。
class A {
public:
    A fun();

private:
    typedef A A_t;
};

A::A_t A::fun( ) {     // A_t前面必须有(A::),作用域运算符A::后面的语句块才在作用域内,如果A_t之前没有(A::),就不在作用域内

}
(2) 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
typedef int int_t;

class A {
public:
    int_t fun();

private:
    typedef double int_t;     // 错误: 即使A中定义的int_t类型 与外层作用域定义的int_t一致, 但仍然是错误的(甚至有的编译器不报错)
    int_t a;
};
经验:类型名的定义 通常出现在类的开始处,这样就能确保所有使用该类型的成员 都出现在类名的定义之后。
(3) 尽管外层的对象被隐藏掉了,但我们仍然可以用 作用域运算符 访问它。
int B = 1;

class A {
public:
    A(int b) :B(b) {}
    int fun() { return ::B; }     // 返回全局变量B

private:
    int B;
};

int main() {
    A a(3);              // A::B=3
    int n = a.fun();     // n=1
}
建议:不要隐藏外层作用域中 可能被用到的名字。(不要同名)

7.5 构造函数再探

构造函数初始值列表:

(1) 如果成员是 const、引用、某种未提供默认构造函数的类类型,则我们必须通过构造函数初始值列表 为这些成员提供初值。
class Demo {
public:
    Demo(int t);

private:
    int A;
    const int B;
    int& C;
};

Demo::Demo(int t) {
    A = t;      // 正确
    B = t;      // 错误: 不能给const赋值
    C = t;      // 错误: C没被初始化  
}

// 随着构造函数体一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过"构造函数初始值"
Demo::Demo(int t) :A(t), B(t), C(t) {      // 正确显式地初始化 引用C和const成员B

}
(2) 最好让构造函数初始值的顺序 和成员声明的顺序 保持一致。(而且如果可能的话,尽量避免使用某些成员初始化其他成员)
class Demo {
public:
    Demo(int t) :b(t), a(b) { }     // 未定义: 成员的初始化顺序 与它们在类定义中的出现顺序一致: 所以先初始化a,再初始化b。因此用未初始化的b来初始化a 将产生错误!         
                          
private:
    int a;
    int b;
};
(3)如果一个构造函数 为所有参数都 提供了默认参数,那么它实际上也定义了默认的构造函数。(相当于默认构造函数)
class Demo {
public:
    Demo(int i=1 ,int j=1) :a(i), b(j) { }           
                        
private:
    int a;
    int b;
};

int main() {
    Demo d;     // a=1,b=1
}

委托构造函数:(C++11)

委托构造函数将自己的职责委 托给了其他构造函数。
class Demo {
public:
    Demo(int i, double j) :a(i), b(j) { }
    
    Demo() :Demo(0, 0.0) { cout << a << " " << b << endl; } 
    Demo(int i) :Demo(i, 0.0) { cout << a << " " << b << endl; } 
    Demo(double i) :Demo(0, i) { cout << a << " " << b << endl; } 

private: 
    int a; 
    double b; 
};

int main() { 
    Demo A;         // a=0,b=0 
    Demo B(2);      // a=2,b=0 
    Demo C(1.5);    // a=0,b=1.5 
}

默认构造函数的作用:

当对象被 默认初始化或值初始化时 自动执行默认构造函数。(类必须 包含一个默认构造函数 以便在下述情况下使用)
默认初始化在以下情况下发生:
  • 当我们在块作用域内 不使用任何初始值定义一个 非静态变量或者数组时。
  • 当一个类本身含有类类型的成员 且使用合成的默认构造函数时。
  • 当类类型的成员 没有在构造函数初始值列表中 显式地初始化时。
值初始化在以下情况下发生: .
  • 在数组初始化的过程中 如果我们提供的初始值数量 小于数组的大小时。
  • 当我们不使用初始值 定义一个局部静态变量时。
  • 当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名。
经验:在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。

隐式的类型转换:

(1) 如果构造函数只接受一个实参,则它实际上定义了 转换为此类类型的 隐式转换机制。这种构造函数又叫转换构造函数
class Demo {
public:
    Demo(string str, int i) :a(str), b(i) { }
    Demo(string str) :a(str), b(0) { }

    string combine_a(const Demo& demo) {     // 参数是const Demo&类型
        return a + demo.a;
    }

private:
    string a;
    int b;
};

int main() {
    string str1 = "aaa", str2 = "bbb";

    Demo A(str1,1);     // a="aaa",b=1
    string result = A.combine_a(str2);      // result="aaabbb" (传递str2将会先调用 构造函数Demo(string i),从而转换为Demo类型对象,再调用combine_a函数)
}
注:编译器只会自动地执行仅一步类型转换。
// 将上列str2改为"bbb"
string result = A.combine_a("bbb");     // 错误:不存在从 cosnt char[4] 到 Demo 的适当构造函数("bbb"不会先转换到string,再转换到Demo)
(2) 将构造函数声明为 explicit 可以抑制构造函数定义的隐式转换
// 将上例中的Demo定义成expilic
explicit Demo(string str) :a(str), b(0) { cout << a << b << endl; } 

string result = A.combine_a(str2);      // 错误: 此时不可以调用Demo(string str)构造函数 将string类型的str2转换为Demo类型对象
(3) explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
explicit Demo(string str) :a(str),b(0) {}

Demo A(str1);       // 正确: 直接初始化
Demo B = str1;      // 错误: 不能将 explicit构造函数 用于拷贝形式的初始化过程 
(4) 尽管编译器不会将 explicit构造函数 用于隐式转换过程,但是可以使用这样的构造函数 显式地进行强制转换。
explicit Demo(string str) :a(str), b(0) { }

A.combine_a(str2);                  // 错误: Demo(string str)为exlicit构造函数,无法隐式调用进行类型转换
A.combine_a(Demo(str2));            // 正确: 显式调用Demo(string str)进行类型转换
A.combine_a(static_cast(str2))      // 正确: static_cast可以使用 explicit构造函数

聚合类:

聚合类使得用户可以 直接访问其成员,并且具有特殊的 初始化语法。
满足以下所有条件的类为 聚合类:
  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。
使用一个花括号括起来的 成员初始值列表 进行初始化(初始值的顺序必须和声明的顺序一致)
// 聚合类示例:
struct Data {
    int val;
    string s;
};

class person {
public:
    int id;
    string name;
};

int main() {
    Data data1 = { 0,"aaa" };       // 正确
    Data data2 = { "aaa",0 };       // 错误: 不能使用"aaa"初始化val

    person per1 = { 123,"bob" };    // 正确
    person per2 = { 456 };          // 正确: name默认初始化为""
}
注:显式地初始化类的对象的成员 存在三个明显的缺点:
  • 要求类的所有成员都是public的。
  • 将正确初始化 每个对象的每个成员的重任 交给了类的用户(而非类的作者)。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程 冗长乏味且容易出错。
  • 添加或删除一个成员之后,所有的初始化语句都需要更新。

字面值常量类:

数据成员都是 字面值类型 的聚合类是字面值常量类。(除了算术类型、引用和指针外,某些类也是字面值类型。
如果一个类不是聚合类,但满足下面所有条件,则它也是一个字面值常量类:
  • 数据成员都必须是字面值类型。
  • 类必须至少含有一个constexpr构造函数。(constexpr函数的参数和返回值必须是字面值。
  • 如果一个数据成员含有 类内部初始值,则内置类型成员的初始值 必须是一条常量表达式;或者如果成员属于某种 类类型,则初始值必须使用 成员自己的constexpr构造函数。
  • 类必须使用 析构函数的默认定义,该成员负责销毁类的对象。

constexpr构造函数:
  • 通过前置constexpr关键字,就可以声明constexpr构造函数。
  • 除了声明为=default或者=delete以外,constexpr构造函数的函数体一般为空。(综合 构造函数没有返回值 且 constexpr函数唯一可执行语句为返回语句,可知constexpr构造函数体一般为空)
  • constexpr构造函数必须 初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。
  • constexpr构造函数用于生成 constexpr对象、constexpr函数的参数或返回类型。
struct Point {
    constexpr Point(int i, int j): x(i), y(j) {}
    constexpr Point(): Point(0, 0) {}
    int x;
    int y;
};

int main() {
    constexpr Point pt1 = { 1,1 };    
    constexpr Point pt2(2, 2);
}
// 这样声明以后,就可以在使用 constexpr表达式或者constexpr函数的地方 使用字面值常量类了。

7.6 类的静态成员

非static数据成员 存在于类类型的每个对象中;static数据成员 独立于该类的任意对象而存在。(每个static数据成员是 与类关联的对象,并不与该类的对象相关联)
静态成员变量 的特性:
  • 属于整个类所有,所有对象共享 类的静态成员变量。
  • 生命期不依赖于任何对象,为程序的生命周期
  • 可以通过类名直接访问 公有静态成员变量。
  • 可以通过对象名、对象指针、对象引用访问 公有静态成员变量。
  • 需要在 类外单独分配空间
  • 在程序内部 位于全局数据区
静态成员函数 的特性:
  • 定义静态成员函数,直接使用static关键字修饰即可。
  • 属于整个类所有,没有this指针
  • 只能直接访问静态成员变量和静态成员函数
  • 可以通过类名直接访问 类的公有静态成员函数。
  • 可以通过对象名、对象指针、对象引用访问 公有静态成员函数。
struct Demo {
public:
    static void show() ;      // 定义类的公有静态成员函数
    static  int   a;          // 定义类的公有静态成员变量

private:
    static int counts;        // 定义类的私有静态成员变量(不能在类的内部初始化静态成员,必须在类的外部定义 和初始化每个静态成员)
};

int Demo::a = 0;
int Demo::counts = 0;
void Demo::show() {       // 当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
    cout << counts << endl;
} 

int main() {
    Demo demo;
    Demo* demo_p = &demo; 
    Demo& demo_t = demo; 

    Demo::a;           // 通过 作用域运算符 访问静态成员) 
    demo.a;            // 通过 Demo对象 访问公有静态成员变量 
    demo_p->a;         // 通过 Demo对象指针 访问公有静态成员变量
    demo_t.a;          // 通过 Demo对象引用 访问公有静态成员变量


    Demo::show();      // 通过 作用域运算符 访问公有静态成员函数
    demo.show();       // 通过 Demo对象 访问公有静态成员函数
    demo_p->show();    // 通过 Demo对象指针 访问公有静态成员函数
    demo_t.show();     // 通过 Demo对象引用 访问公有静态成员函数
}



静态类型类内初始化:
通常情况下,类的静态成员 不应该在类的内部初始化。然而,我们可以为静态成员提供 const整数类型的类内初始值不过要求静态成员必须是 字面值常量类型的constexpr初始值必须是常量表达式。因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。
  • static const + 整数类型 :可以在类内初始化
  • static constexpr + 任何内置类型 :必须在类内初始化
struct Demo {
private:
    static const int a1;                  // 正确
    static const int a2 = 1;              // 正确: const静态整数类型成员 可以类内初始化
    static const double b1;               // 正确
    static const double b2 = 1.1;         // 错误: static const double 不能类内初始化
    static constexpr double c1;           // 错误: constexpr静态数据成员必须初始化
    static constexpr double c2= 3.14;     // 正确
};
建议:即使一个常量静态数据成员 在类内部被初始化了,通常情况下也应该 在类的外部定义一下该成员。

静态成员能用于,而普通成员不能用于 的某些场景:
  • 静态数据成员的类型可以就是 它所属的类类型。而非静态数据成员则受到限制,只能声明成 它所属类的指针或引用
  • 可以使用静态成员作为默认实参
struct Demo {
public:
    void fun(Demo demo= a) {}    // 正确: 静态成员可以作为默认实参,因为静态成员不属于对象
    void fun(int i = m) {}       // 错误: 非静态类型不能作为默认实参,因为它的值本身属于对象一部分

private:
    int m = 5;

    static Demo a;     // 正确: 静态成员可以是不完全类型
    Demo* b;           // 正确: 指针成员可以是不完全类型
    Demo c;            // 错误:数据成员必须是完全类型(而这里Demo是一个不完全类型,因为定义体未完成)
};