参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是依赖于**接口(interface)和实现(implementaion)**分离的编程技术。类的接口包括用户能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体、私有函数。
7.1 定义抽象数据类型(P228)
7.1.1 设计Sales_data
类(P228)
7.1.2 定义改进的Sales_data
类(P230)
struct Sales_data {
string isbn() const { return bookNo; }
Sales_data& conbine(const Sales_data&);
double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);
成员函数必须声明在类的内部,但即可以定义在类内部,也可以定义在类外部。定义在类内部的函数是隐式 inline
函数。
定义成员函数
引入this
我们调用成员函数时,实际上是替某个对象调用它。成员函数通过一个名为 this
的额外隐式参数来访问调用它的对象。当我们调用一个函数时,编译器负责用请求该函数的对象地址初始化 this
。
在成员函数内部,任何对类成员的直接访问都被看作 this
的隐式引用。因为 this
总是指向“这个”对象,所以 this
是一个指针常量。
引入const
成员函数
上面的 isbn
函数在其参数列表紧跟了一个 const
关键字,作用是修饰 this
的类型。
由于默认情况下 this
是指向非常量的指针常量,所以普通成员函数是不能被常量对象调用的。通过 const
关键字将 this
设置为指向常量的指针常量可以提高函数的灵活性,这样的函数叫做常量成员函数。
常量成员函数不能改变调用它的对象的内容。
类作用域和成员函数
编译器分两步处理类:先编译成员(数据成员和函数成员)的声明,然后再编译函数成员的函数体。(所以成员函数体可以随意使用类中的其他成员,而无需在意出现的次序)。
在类外部定义成员函数
double Sales_data::avg_price() const{
if(units_sold)
return revenue/units_sold;
else
return 0;
}
定义一个返回this
对象的函数
Sales_data& Sales_data::combine(const Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
7.1.3 定义类相关的非成员函数(P234)
如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
7.1.4 构造函数(P235)
**构造函数(constructor)**的任务是初始化类对象的数据成员,只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同,没有返回类型。构造函数不能声明为 const
。当我们创建一个类的 const
对象时,对象直到构造函数完成初始化过程后(注意:完成初始化过程不等于执行完构造函数的函数体)才获得“常量”属性。
struct DATA{
int x;
DATA(int x):x(x){
x = 1;
}
};
const DATA d(2); // d.x的值为2
合成的默认构造函数
如果我们的类没有显式定义任何构造函数,编译器将为我们隐式定义一个默认构造函数(default constructor),称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,合成的默认构造函数按照如下规则初始化类的数据成员:
- 如果存在类内初始值,用它来初始化成员。
- 否则,执行默认初始化。
某些类不能依赖于合成的默认构造函数
对于一个普通的类来说,必须定义它自己的默认构造函数,原因如下:
- 编译器只有在类中不包含任何构造函数时才会合成默认构造函数。
- 合成的默认构造函数可能执行错误的初始化,如内置类型的默认初始化。
- 编译器不能为某些类合成默认构造函数,如类中包含一个其他类类型的成员,而这个成员的类型没有默认构造函数。
= default
的含义
Sales_data() = default;
在 C++11 新标准中,允许我们通过在参数列表后面加上 = default
来要求编译器生成默认构造函数。
构造函数初始值列表
Sales_data(string &s): bookNo(s) {}
Sales_data(const string &s, unsigned n, double p):
bookNo(s), units_sold(n), revenue(p*n) {}
上述两个构造函数的定义中,使用了构造函数初始值列表(constructor initialize list),它负责为新创建的对象的数据成员赋初值。如果某个数据成员被构造函数初始值列表忽略,它将以与合成的默认构造函数相同的方式隐式初始化。
在类的外部定义构造函数
7.1.5 拷贝、赋值和析构(P239)
拷贝、赋值和销毁对象操作和初始化一样,如果我们不主动定义,编译器将替我们合成它们。
7.2 访问控制与封装(P240)
我们使用**访问说明符(access specifiers)**加强类的封装性:
- 定义在
public
之后的成员在整个程序内可以被访问,public
成员定义类的接口。 - 定义在
private
之后的成员可以被类的成员函数访问。
一个类可以包含零个或多个访问说明符,某种访问说明符能出现多少次也没有规定。
使用class
或struct
关键字
类中所有出现在第一个访问说明符之前的成员使用默认的访问权限,class
默认 private
、struct
默认 public
。
7.2.1 友元(P241)
类可以允许其他类或函数访问它的非公有成员,方式是令其他类或者函数称为它的友元(friend):
class Sales_data {
friend Sales_data add(const Sales_data&, const Sales_data&);
friend ostream& print(ostream&, const Sales_data&);
friend istream& read(istream&, Sales_data);
public:
string isbn() const { return bookNo; }
Sales_data& conbine(const Sales_data&);
private:
double avg_price() const;
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
ostream& print(ostream&, const Sales_data&);
istream& read(istream&, Sales_data&);
友元声明只能出现在类定义的内部,不受所在区域访问控制级别的约束。
友元的声明
友元的声明仅仅指定了访问权限,而非通常意义上的函数声明,所以我们需要在友元声明之外再对函数进行一次声明。
我们通常将对友元函数的额外声明与类本身放置在同一个头文件中。需要注意的是,很多编译器允许在尚无友元函数真正声明的情况下就调用它,但为了保险起见最好还是提供一个独立的声明。
7.3 类的其他特性(P243)
7.3.1 类成员再探(P243)
定义一个类型成员
类可以自定义某种类型在类中的别名,由类定义的类型别名和其他成员一样可以设置访问限制:
class Screen {
public:
using pos = string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
string contents;
};
注意:类型成员必须先定义后使用(与其他成员不同),所以类型成员通常写在类开始的地方。
Screen
类的成员函数
class Screen {
public:
using pos = string::size_type;
Screen() = default; // 使用生成的默认构造函数
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;
string contents;
};
令成员作为内联函数
我们可以在类内部定义函数,从而使其自动成为 inline
函数,也可以在类内声明时显式添加 inline
,还可以在类外定义是再用 inline
修饰。
最好在类外部定义的地方说明
inline
。
重载成员函数
和普通重载函数相同。
可变数据成员
我们可以在变量声明中加入 mutable
来定义可变数据成员(mutable data member)。可变数据成员永远不会是 const
,即使它是 const
对象的成员。所以 const
成员函数可以修改可变成员的值。
类数据成员的初始值
class Window_mgr {
private:
vector<Screen> screens{ Screen(24, 80, ' ') };
};
提供类内初始值时,必须使用
=
或花括号。
7.3.2 返回*this
的成员函数(P246)
我们继续向上面的 Screen
类中添加一些函数:
class Screen{
public:
Screen &set(char);
Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char ch){
contents[cursor] = ch;
return *this; // 返回对象本身
}
inline Screen &Screen::set(pos r, pos col, char ch){
contents[r*width + col] = ch;
return *this; // 返回对象本身
}
返回引用的函数可以连用:
myScreen.move(4, 0).set('#');
从const
成员返回*this
假设我们需要在 Screen
中添加一个 display
操作,并希望它能和 move
、set
等操作连用,那么 display
也应该返回 *this
。由于 display
只负责打印而不改变内容,所以我们应该将它定义成 const
成员,这时 *this
是常量对象,不能用来初始化普通对象的引用。
基于const
的重载
class Screen {
public:
// 根据对象是否为const选择重载函数
Screen &display(ostream &os) { do_display(os); return *this; }
const Screen &display(ostream &os) const { do_display(os); return *this; }
private:
void do_display(ostream &os) const { os << contents; }
};
单独设计一个 do_display
私有成员的好处:
- 避免在多出使用同样的代码。
- 只在
do_display
一处进行修改比较简单。 - 这个额外函数不会产生额外开销,因为它是隐式内联的。
7.3.3 类类型(P249)
对于两个类来说,即使它们的成员完全一样,这两个类也是不同的类型。
我们可以把类名当作类型的名字来使用也可以把类名跟在 class
或 struct
后面:
Sales_data item1;
class Sales_data item2; // 等价的声明,从C语言继承而来
类的声明
我们可以仅仅声明类而暂时不定义:
class Screen;
这种声明被称作前向声明(forward declaration),在 Screen
被定义之前是一个不完全类型(incomplete type)。不完全类型只能在非常有限的场景下使用:可以定义指向这种类型的指针或引用、可以声明(不能定义)以不完全类型为返回类型或参数的函数。
一旦一个类的名字出现后,就被认为是声明过了(但尚未定义,所以此时类是不完全类型),所以类允许包含自身类型的引用或指针。
7.3.4 友元再探(P250)
类除了可以把普通函数声明成友元外,还可以把其他类或类的成员函数声明成友元。友元函数也可以定义在类内部。
类之间的友元关系
假设我们为 Window_mgr
添加一个名为 clear
的成员,负责把一个指定的 Screen
对象的内容设为空白。显然,clear
需要访问 Screen
的私有成员,所以我们把 Window_mgr
声明成 Screen
的友元:
class Screen{
friend class Window_mgr;
};
如果指定一个类为友元,则友元类的成员函数可以访问本类的所有成员。需要注意的是,友元关系不存在传递性,每个类负责控制自己的友元。
令成员函数作为友元
把成员函数声明成友元时,须指定该成员函数属于哪一个类:
class Screen{
friend void Window_mgr::clear(ScreenIndex);
};
这部分有点小坑,似乎和类的作用域有关,待更新。。。
函数重载和友元
重载函数虽然名字相同,但本质上是不同的函数,每个重载函数需要单独的友元声明。
友元声明和作用域
类和非成员函数的声明不是必须在它们的友元声明之前(类成员函数的友元声明必须在其类的定义之前)。
前面提到,友元声明并不是真正的函数声明,所以我们就算在类内部定义了友元函数,也必须在类外部提供相应的声明。
class X {
public:
friend void f() { cout << "hello"; }
X() { f(); } // 错误:f还没有被声明
};
友元声明不要求名字在之前被声明过,但要想使用这个名字,就必须在真正声明之后。