参考资料:
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是依赖于**接口(interface)和实现(implementaion)**分离的编程技术。类的接口包括用户能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体、私有函数。
Sales_data
类(P228)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;
}
如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
**构造函数(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),它负责为新创建的对象的数据成员赋初值。如果某个数据成员被构造函数初始值列表忽略,它将以与合成的默认构造函数相同的方式隐式初始化。
拷贝、赋值和销毁对象操作和初始化一样,如果我们不主动定义,编译器将替我们合成它们。
我们使用**访问说明符(access specifiers)**加强类的封装性:
public
之后的成员在整个程序内可以被访问,public
成员定义类的接口。private
之后的成员可以被类的成员函数访问。一个类可以包含零个或多个访问说明符,某种访问说明符能出现多少次也没有规定。
class
或struct
关键字类中所有出现在第一个访问说明符之前的成员使用默认的访问权限,class
默认 private
、struct
默认 public
。
类可以允许其他类或函数访问它的非公有成员,方式是令其他类或者函数称为它的友元(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&);
友元声明只能出现在类定义的内部,不受所在区域访问控制级别的约束。
友元的声明仅仅指定了访问权限,而非通常意义上的函数声明,所以我们需要在友元声明之外再对函数进行一次声明。
我们通常将对友元函数的额外声明与类本身放置在同一个头文件中。需要注意的是,很多编译器允许在尚无友元函数真正声明的情况下就调用它,但为了保险起见最好还是提供一个独立的声明。
类可以自定义某种类型在类中的别名,由类定义的类型别名和其他成员一样可以设置访问限制:
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, ' ') };
};
提供类内初始值时,必须使用
=
或花括号。
*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
一处进行修改比较简单。对于两个类来说,即使它们的成员完全一样,这两个类也是不同的类型。
我们可以把类名当作类型的名字来使用也可以把类名跟在 class
或 struct
后面:
Sales_data item1;
class Sales_data item2; // 等价的声明,从C语言继承而来
我们可以仅仅声明类而暂时不定义:
class Screen;
这种声明被称作前向声明(forward declaration),在 Screen
被定义之前是一个不完全类型(incomplete type)。不完全类型只能在非常有限的场景下使用:可以定义指向这种类型的指针或引用、可以声明(不能定义)以不完全类型为返回类型或参数的函数。
一旦一个类的名字出现后,就被认为是声明过了(但尚未定义,所以此时类是不完全类型),所以类允许包含自身类型的引用或指针。
类除了可以把普通函数声明成友元外,还可以把其他类或类的成员函数声明成友元。友元函数也可以定义来类内部。
假设我们为 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还没有被声明
};
友元声明不要求名字在之前被声明过,但要想使用这个名字,就必须在真正声明之后。