cppprimer第七章

第7章 类

7.1 定义抽象数据类型

7.1.2 定义改进的sales_data类

引入 this

成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。如果调用 total.isbn ()

则编译器负责把 total 的地址传递给 isbn 的隐式形参 this,等价于: Sales_data::isbn(&total)

在成员函数内部,可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作 this 的隐式引用:当 isbn 使用 bookNo 时,它隐式地使用 this 指向的成员,就像我们书写了 this->bookNo一样。

this 形参是隐式定义的。任何自定义名为 this 的参数或变量的行为都是非法的。我们可以在成员函数体内部使用 this,尽管没有必要: std::string isbn () const {return this->bookNo;}

因为 this 的目的总是指向这个对象,所以 this 是一个常量指针,不允许改变 this 中保存的地址。

常量对象,以及常量对象的引用或指针都只能调用常量成员函数,const 在参数列表后,this 此时指向常量指针,常量成员函数不能改变调用它的对象的内容:string isbn() const {return bookNo;}

成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

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

定义在类外部的函数要加上类作用域。

定义一个返回 this 对象的函数

函数combine的设计初衷类似于复合赋值运算符+=:

1
2
3
4
Sales_data& Sales_data::combine (const Sales_data &rhs) {
units_sold += rhs.units_sold; // 把rhs的成员加到this对象的成员上
return *this; // 返回调用该函数的对象
}

当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左侧运算对象当成左值返回,为了与它保持一致,combine函数必须返回引用类型。

7.1.3 定义类相关的非成员函数

如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。

1
2
3
4
5
ostream &print (ostream &os, const Sales_data &item) { // IO类属于不能被拷贝的类型,只能通过引用传递
os << item.isbn() << " " << item.units_sold() << ""
<< item.revenue() <<" " << item.avg _price();
return os;
}

7.1.4 构造函数

构造函数用来初始化类对象的数据成员。当类的对象被创建时,就会执行构造函数。

构造函数没有返回类型。

构造函数可以重载。

构造函数不可以声明为 const。当创建一个 const 对象时,直到构造函数完成其初始化过程,对象才真的取得其常量属性。

如果类没有任何构造函数,则编译器自己会创建默认构造函数:

  • 如果存在类内的初始值,用它来初始化成员。
  • 否则,默认初始化该成员。
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Sales_data {
friend istream &read(istream &is, Sales_data &item);
friend ostream &print(ostream &os, const Sales_data &item);
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

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

string isbn() const {return bookNo;};
Sales_data &combine(const Sales_data &);

private:
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

Sales_data &Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

istream &read(istream &is, Sales_data &item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}

ostream &print(ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}

= default 的含义

默认构造函数,这里没有参数的原因是类内有初始值。如果没有,需要用构造函数初始值列表。

构造函数初始值列表

冒号和冒号和花括号之间的代码。

在类的外部定义构造函数

1
2
3
Sales_data::Sales_data(istream &is) {
read(is, *this); // read函数的作用是从is中读取一条交易信息然后存入this对象中
}

7.1.5 拷贝、赋值和折构

对于拷贝、赋值和销毁对象等操作,类都通过相应的成员函数实现其功能,如果不主动定义这些操作,编译器就合成默认的版本。

对于某些类来说,无法使用默认合成的版本,比如管理动态内存的类就不能。

7.2 访问控制与封装

访问说明符:class, public。可以在类内出现多次。

class 和 struct 的唯一一点区别就是默认访问权限不同。

当希望类的所有成员是 public 时,用 struct。

7.2.1 友元

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

友元声明只能出现在类的内部,最好在类的开始或结束位置集中声明友元。

7.3 类的其他特性

7.3.1 类成员再探

定义类型成员

类可以自定义某种类型在类内的别名。类型成员一样有访问限制。

类型成员必须先定义后使用,因此类型成员应该出现在类开始的地方。

默认构造函数

当定义了构造函数,不会再有默认构造函数,如果需要必须显式声明:Student() = default;

类内初始值

成员变量可以在类内定义的时候直接初始化。

此时构造函数的初始化列表可以不包含该成员变量,隐式使用其类内初始值。

类内初始值必须使用等号或花括号初始化。

内联成员函数

4种方式使成员成为内联函数:

  1. 在类内定义函数,为隐式内联。
  2. 在类内用关键字 inline 显式声明成员函数。
  3. 在类外用关键字 inline 定义成员函数。
  4. 同时在类内类外用 inline 修饰。

inline 成员函数应该与类定义在同一个头文件中。

可变数据成员

const 成员函数不能修改成员变量。

但是用 mutable 将成员修饰为可变数据成员,就可以修改了。

7.3.2 返回 *this 的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
inline screen &Screen::move(pos r, pos c) {
pos row = r * width; // 计算行的位置
cursor = row + C; // 在行内将光标移动到指定的列
return *this; // 以左值的形式返回对象
}

inline screen &screen::set(char c) {
contents [cursor] = C; // 设置当前光标所在位置的新值
return *this; // 将this对象作为左值返回
}

myscreen.move(4,0).set ('#’); // 把光标移动到一个指定的位置,然后设置该位置的字符值

可以定义返回类型为类对象的引用的函数。如果定义的返回类型不是引用,返回的是 *this 的副本。

const 函数如果以引用的形式返回 this,返回类型就是一个常量引用。

7.3.3 类类型

类名不同,类不同。

一个类的成员类型不能是它自己,但是一旦一个类的名字出现后,它就被认为是被声明过了,所以类允许包含指向它自身类型的引用或指针。

1
2
3
4
5
class Link_screen {
screen window;
Link_screen *next;
Link_screen *prev;
};

练习:定义一对类 X 和 Y,其中 X 包含一个指向 Y 的指针,而Y 包含一个类型为 X 的对象。

1
2
3
4
5
6
7
8
9
class Y;

class X{
Y* y = nullptr;
};

class Y{
X x;
};

7.3.4 友元再探

1
2
3
4
class screen {
friend class Window_mgr; // 其他类作为友元
friend void Window_mgr::clear(screenIndex); // 其他类的成员函数作为友元
};

要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系,顺序:

  • 定义Window mgr类,其中声明clear函数,但是不能定义它。
  • 在clear使用Screen 的成员之前必须先声明Screen。
  • 定义Screen,包括对于clear的友元声明。
  • 最后定义clear,此时它才可以使用screen的成员。

如果一个类指定了友元类。则友元类的成员函数可以访问此类的所有成员。

友元关系不具有传递性。

重载函数名字相同,但是是不同的函数。如果想把一组重载函数声明为类的友元,需要对每一个分别声明。

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

1
2
3
4
5
6
7
8
9
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的声明在作用域中了

友元声明的作用是影响访问权限,它本身并非普通意义上的声明。

7.4 类的作用域

类名表示了作用域,一个函数只需要在函数名前授权一次作用域,其他变量就知道这是在类中,无需重复授权。

当类的成员函数的返回类型也是类的成员时,在定义它时要指明类 Student::age Student::Getage() {}

7.4.1 名字查找与类的作用域

普通程序名字查找的过程

  1. 首先在名字所在的块中寻找声明语句
  2. 如果没找到,继续查找外层作用域
  3. 如果最终还是没找到,报错

类的定义过程

  1. 编译成员的声明。
  2. 直到全部类可见后才编译成员函数体。

在类内定义的类型名要放在类的开始,放在后面其他成员是看不见的。

类型名如果在类外已经定义过,不能在类内重定义。

7.5 构造函数再探

7.5.1 构造函数初始值列表

使用初始值列表对类的成员初始化才是真正的初始化,在构造函数的函数体内赋值并不是初始化:

1
2
3
4
5
Sales_data::Sales_data(const string &s, unsigned cnt, double price) {
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}

建议构造函数使用初始值。

如果成员是 const 或者是引用的话,必须初始化。赋值是错误的。

如果成员是类并且该类没有定义构造函数的话,必须初始化。

使用初始值列表初始成员时,成员初始化的顺序是按照类定义出现的顺序初始化的。

默认实参和构造函数

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

如果接受string 的构造函数和接受 istream& 的构造函数都使用默认实参,调用默认实参函数时不知道应该重载哪一个函数,非法。

7.5.2 委托构造函数

委托构造函数通过其他构造函数来执行自己的初始化过程。

1
2
3
4
5
6
7
8
9
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);}
};

7.5.4 隐式的类类型转换

如果构造函数只接受一个实参,则称作转换构造函数,它实际上定义了转换为此类类型的隐式转换机制。

一个实参的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则

只允许一步类型转换

在进行隐式转换时,编译器只会自动地执行一步类型转换。

​ string null_book = "9-999"; item.combine(null_book); //conbine 函数接受 Sales_data 类类型,但该类定义了一个接受 string 参数的转换构造函数,所以这里会执行从 string 到该类类型的隐式转换,是正确的。 item.combine("9-999"); //隐式地使用了两种转换规则,所以是错误的。 item.combine(string("9-999")); //先显示地转换为 string,再隐式地转换为 Sales_data 类类型。是正确的。

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

将转换构造函数声明为 explicit 会阻止隐式转换。

关键字 explicit 只对一个实参的构造函数有效。因为需要多个实参的构造函数本来就不执行隐式转换。

explicit 只在类内声明构造函数时使用,在类外定义时不加。类似 static 成员函数

1
2
item.combine (null_book);//错误:string构造函数是explicit的
item. combine (cin);//错误: istream构造函数是explicit的

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

explicit 构造函数只能用于直接初始化,不能用于使用 "=" 的拷贝初始化。理解:因为 “=” 实际上是采用了拷贝赋值运算符,在传参时会进行隐式转换。

理解:不加 explicit 的转换构造函数,可以在赋值、传参、从函数返回等场合执行隐式转换,加了 explicit 后,就不能隐式转换了,也就是加了 explicit 的转换构造函数的意义就只是定义了一个新的构造函数,不具有提供隐式转换机制的额外功能了。

1
2
Sales_data item1(null_book);  //正确
Sales_data item2 = null_book; //错误

为转换显式地使用构造函数

explicit 只是阻止了构造函数进行隐式转换,但是在传递实参时可以显式转换。

可以使用 explicit 的构造函数显式地强制进行转换。

1
2
iter.combine(Sales_data(null_book));  //正确
iter.combine(static_cast<Sales_data>(null_book)); //正确,static_cast 可以使用 explicit 的构造函数

7.5.5 聚合类

满足以下四个条件的类是聚合类:

  1. 所有成员都是public的
  2. 没有定义任何构造函数
  3. 没有类内初始值
  4. 没有基类和 virtual 函数

聚合类可以像结构体一样用花括号初始值列表初始化。如果花括号内元素数量少于类成员数量,靠后的成员将被值初始化。

1
Student stu = {"Li Ming", 18};

7.5.6 字面值常量类

constexpr 函数的参数和返回值都必须是字面值类型。

算术类型、引用和指针都是字面值类型,此外字面值常量类也是字面值类型。

字面值类型属于常量表达式,constexpr 就是用来声明常量表达式的。

聚合类属于字面值常量类。

如果不是聚合类,满足以下四个条件的类也是字面值常量类:

  1. 数据成员都是字面值类型。
  2. 类至少含有一个 constexpr 构造函数
  3. 如果一个数据成员有类内初始值,则初始值必须是常量表达式(如果成员是类,则初始值必须使用成员自己的 constexpr 构造函数)
  4. 类必须使用析构函数的默认定义。

constexpr 构造函数

类的构造函数不能是 const 的,但字面值常量类的构造函数可以是 constexpr 函数。

constexpr 构造函数可以声明成 =default 或 =delete。

constexpr 构造函数的函数体应该是空的(原因:constexpr 函数的函数体只能包含一条返回语句,而构造函数不能包含返回语句)

constexpr 构造函数必须初始化所有数据成员。初始值必须是常量表达式或使用其自己的 constexpr 构造函数。

使用前置关键字 constexpr 来声明 constexpr 构造函数

​ class Debug{ public: constexpr Debug(bool b=true):a(b){}; private: bool a; };//定义一个类记得加分号 constexpr Debug prod(false);//定义一个 Debug 类型的对象。实参应为常量表达式。

7.6 类的静态成员

类的静态成员与类本身直接关联,而不是与类的对象保持关联。

静态成员可以是 public 或 private 的。

静态成员不与任何对象绑定在一起。

静态成员函数不包含 this 指针,不能声明为 const 的,不能在 static 函数体内使用 this 指针。

理解:因为 static 函数不能使用 this 指针,所以它是无法使用类的非 static 数据成员的。

定义静态成员

可以在类内或类外定义静态成员。当在类外定义时,不能重复 static 关键字,static 只出现在类内的声明中。

只有 constexpr 类型的静态数据成员可以在类内初始化,但是也需要在类外定义。

其他的静态数据成员都在类内声明,类外定义并初始化

静态成员可以用的特殊场景

静态数据成员可以是不完全类型,比如静态数据成员的类型可以是它所属的类类型本身。

静态成员可以作为默认实参。