- 第 7 章 类
- 7.1 定义抽象数据类型
- 7.1.1 设计 Sales_data类(*)
- 7.1.2 定义改进的 Sales_data类
- 7.1.3 定义类相关的非成员函数(*)
- 7.1.4 构造函数
- 7.1.5 拷贝、赋值和析构(*)
- 7.2 访问控制与封装
- 7.2.1 友元
- 7.3 类的其他特性
- 7.3.1 类成员再探
- 7.3.2 返回 \*this 的成员函数
- 7.3.3 类类型
- 7.3.4 友元再探
- 7.4 类的作用域
- 7.4.1 名字查找与类的作用域
- 7.5 构造函数再探
- 7.5.1 构造函数初始值列表
- 7.5.2 委托构造函数
- 7.5.3 默认构造函数的作用(*)
- 7.5.4 隐式的类类型转换(转换构造函数)
- 7.5.5 聚合类
- 7.5.6 字面值常量类
- 7.6 类的静态成员
struct Sales_data {
// 新成员∶关于 Sales data 对象的操作
std::string isbn() const { return this->bookNo; }
Sales_data& combine (const Sales_data&);
double avg price() const;
// 数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data 的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales data&);
定义在类内部的函数是隐式的 inline 函数。
(1)引入 this
成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化 this。
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作 this 的隐式引用,也就是说,当 isbn 使用 bookNo 时,它隐式地使用 this 指向的成员,就像我们书写了 this->bookNo
一样。
this 形参是隐式定义的。任何自定义名为 this 的参数或变量的行为都是非法的。我们可以在成员函数体内部使用 this。尽管没有必要,但我们还是能把 isbn 定义成如下的形式:
std::string isbn() const { return this->bookNo; }
this 是一个常量指针,我们不允许改变 this 中保存的地址。
(2)引入 const 成员函数
isbn 函数的另一个关键之处是紧随参数列表之后的 const 关键字,这里,const 的作用是修改隐式 this 指针的类型。
默认情况下,this 的类型是指向类类型非常量版本的常量指针。(在默认情况下)我们不能把 this 绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。
如果 isbn 是一个普通函数而且 this 是一个普通的指针参数,则我们应该把 this 声明成const Sales data *const
。毕竟,在 isbn 的函数体内不会改变 this 所指的对象,所以把 this 设置为指向常量的指针有助于提高函数的灵活性。
C++语言允许把 const 关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的 const 表示 this 是一个指向常量的指针。像这样使用 const 的成员函数被称作常量成员函数。
(3)类作用域和成员函数
类本身就是一个作用域。类的成员函数的定义嵌套在类的作用域之内,因此,isbn 中用到的名字 bookNo 其实就是定义在 Sales_data 内的数据成员。
值得注意的是,即使 bookNo 定义在 isbn 之后,isbn 也还是能够使用 bookNo。其原因是,编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
7.1.3 定义类相关的非成员函数(*) 7.1.4 构造函数struct Sales data{
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data(std::istream &);
// 之前已有的其他成员
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price () const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
(1)默认构造函数
当类没有声明任何构造函数时,编译器会自动地生成默认构造函数,也叫合成的默认构造函数。其将按照以下规则初始化类的数据成员:
- 如果存在类内的初始值,用它来初始化成员
- 否则,默认初始化
(2)= default
在 C++11 新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。这个函数的作用完全等同于之前使用的合成默认构造函数。
如果 = default 在类的内部,则默认构造函数是内联的;
如果它在类的外部,则该成员默认情况下不是内联的。
(3)构造函数初始值列表
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
通常情况下,构造函数使用类内初始值不失为一种好的选择。
7.1.5 拷贝、赋值和析构(*) 7.2 访问控制与封装 访问说明符:public、private
struct 和 class 的唯一区别: 默认访问权限不一样。struct 的默认访问权限是 public,而 class 是 private。
7.2.1 友元 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
为了使友元对类的用户可见,通常把友元的声明与类本身放置在同一个头文件中,并提供独立的声明。
class Sales_data
{
// 为 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&, const Sales_data&); // 其他成员及访问说明符与之前一致
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * 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 = 0;
double revenue = 0.0;
};
// Sales data 接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
7.3 类的其他特性
定义一对相互关联的类来展示新特性:Screen 和 Window_mgr。
Screen 表示显示器中的一个窗口。每个 Screen 包含一个用于保存 Screen 内容的 string 成员和三个 string::size type 类型的成员,它们分别表示光标的位置以及屏幕的高和宽。
class Screen
{
public:
typedef std::string::size_type pos; // 等价于 using pos = std::string::size_type;
Screen() = default; // 因为 Screen 有另一个构造函数,所以本函数是必需的
// cursor 被其类内初始值初始化为 0
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents (ht * wd, c) { }
char get() const { return contents[cursor]; } // 读取光标处的字符
inline char get(pos ht, pos wd) const; // 显式内联
Screen &move (pos r, pos c); // 能在之后被设为内联
private:
pos Cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
7.3.1 类成员再探
(1)令成员作为内联函数
可以在类的外部用 inline 关键字修饰函数的定义:
inline // 在函数的定义处指定 inline
Screen &Screen::move(pos r, pos c)
{
pos row = r * width; // 计算行的位置
cursor = row + c; // 在行内将光标移动到指定的列
return *this; // 以左值的形式返回对象
}
char Screen::get(pos r, pos c) const // 在类的内部声明成 inline
{
pos row = r * width; // 计算行的位置
return contents[row + c]; // 返回给定列的字符
}
最好再类外部定义的地方说明 inline,使类更容易理解。
inline 函数也应该与相应的类定义在同一个头文件中。
(2)可变数据成员
使用关键字 mutable 声明一个可变数据成员,即使是 const 成员函数也能改变其对应的值。
class Screen
{
public:
void some_member() const;
private:
mutable size_t access_ctr; // 即使在一个const 对象内也能被修改
// 其他成员与之前的版本一致
};
void Screen::some_member() const // 保存一个计数值,用于记录成员函数被调用的次数
{
++access_ctr;
//该成员需要完成的其他工作
}
(3)类数据成员的初始值
定义一个窗口管理类 Window_mgr 来表示显示器上的一组 Screen。我们希望 Window_mgr 类开始时总是有一个默认初始化的 Screen,因此采用类内初始值的方式声明:
class Window_mgr
{
private:
// 这个 Window_mgr 追踪的 Screen
// 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
std::vector screens{ Screen(24, 80, ' ') };
};
当我们提供一个类内初始值时,必须以符号 ‘=’ 或者花括号表示。
7.3.2 返回 *this 的成员函数 添加一些函数,负责设置光标所在位置的字符或者其他任意给定的位置的字符:
class Screen
{
public:
Screen &set(char);
Screen &set(pos, pos, char);
// 其他成员和之前的版本一致
};
inline Screen &Screen::set(char c)
{
contents[cursor] = c; // 设置当前光标所在位置的新值
return *this; // 将 this 对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
contents [r * width + col]= ch; // 设置给定位置的新值
return *this; // 将 this 对象作为左值返回
}
因为返回类型为引用类型,因此可以进行下列的拼接操作:
myScreen.move(4, 0).set('#');
(1)从 const 成员函数返回 *this
添加一个 display 的操作,负责打印 Screen 的内容,令其为 const 成员函数,返回一个 const 引用。
如若此做,我们则不能将 display 嵌入到一组动作的序列中去:
myScreen.display(cout).set('*');
(2)基于 const 的重载
我们将定义一个名为 do_display 的私有成员函数,由它负责打印 Screen 的实际工作。所有的 display 操作都将调用这个函数,然后返回执行操作的对象:
class Screen { public: // 根据对象是否是 const 重载了 display 函数 Screen &display(std::ostream &os) { do_display(os); return *this; } const Screen &display(std::ostream &os) const { do_display(os); return *this; } private: // 该函数负责显示 Screen 的内容 void do_display(std::ostream &os) const { os
vector:是 explicit 7.5.5 聚合类 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:
- 所有成员都是 public 的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual 函数。
struct Data
{
int ival;
string s;
}
可以使用花括号来初始化聚合类:
Data val1 = { 0, "Anna" };
7.5.6 字面值常量类
在 C++ 中,类也可以是字面值类型。和其他类不同,字面值类型的类可能含有 constexpr 函数成员。这样的成员必须符合 constexpr 函数的所有要求,它们是隐式 const 的。
数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类∶
- 数据成员都必须是字面值类型。
- 类必须至少含有一个 constexpr 构造函数。
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达 式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
(1)constexpr 构造函数
一个字面值常量类必须至少提供一个 constexpr 构造函数。其要么声明成 = default,要么函数体为空。
class Debug
{
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw (h), io(i), other(o) { }
constexpr bool any() { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { hw = b; }
private:
bool hw; // 硬件错误,而非 IO 错误
bool io; // IO 错误
bool other; // 其他错误
};
7.6 类的静态成员
class Account
{
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
(1)使用类的静态成员
- 使用作用域运算符访问静态成员:
double r;
r = Account::rate();
- 可以使用类的对象、引用或者指针访问静态成员:
Account ac1;
Account *ac2 = &ac1;
// 调用静态成员函数 rate() 的等价形式
r = ac1.rate();
r = ac2->rate();
- 成员函数能直接访问静态成员。
(2)定义静态成员
static 关键字只出现在类内部的声明语句中,在类外部定义静态成员时,不能重复出现 static。
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。
类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中。
double Account::interestRate = initRate(); // 类外部初始化静态成员
从类名开始,这条定义语句的剩余部分就都位于类的作用域内了。尽管 initRate 函数是 private 的,也能使用它进行初始化。
要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
(3)静态成员的类内初始化
可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr。
如果在类的内部提供了一个初始值,则成员的定义就不能在指定一个初始值了。
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
(4)某些特殊场景的应用
- 静态数据成员可以是不完全类型。特别的,可以就是他所属类的类型。
- 可以使用静态成员作为默认实参,而普通成员不可以。