第七章 类 (Class)
定义抽象数据类型
- 类背后的基本思想:数据抽象(data abstraction)和封装(encapsulation)。
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
类成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
类的成员函数
- 成员函数的声明必须在类的内部。
- 成员函数的定义既可以在类的内部也可以在外部
- 定义在类内部的函数是隐式的
inline
函数 - 使用点运算符
.
调用成员函数。 - 必须对任何
const
或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。 ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
- 默认实参:
Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }
- this:
- 每个成员函数都有一个额外的,隐含的形参
this
。 - 成员函数通过
this
的隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象的地址初始化this
,例如:
- 每个成员函数都有一个额外的,隐含的形参
total.isbn()
//编译器负责把total的地址传递给isbn的隐式形参this,可以等价的认为编译器将调用重写成了以下形式
// pseudo-code illustration of how a call to a member function is translated
Sales_data::isbn(&total)
~~~~~~ 对于成员函数来说,成员函数可以直接使用调用该函数的对象的成员,而无须通过成员访问符来做到这一点,因为
this
所指向的正是这个对象。任何对类成员的直接访问都被看作this
的隐式引用,也就是说,当isbn
使用bookNo
的时候,它隐式的使用了this
指向的成员,就像我们书写了this->bookNo
一样。
~~~~~~ 但是对用户来说,this
形参是隐式定义的。实际上,任何自定义名为this
的参数或者变量的行为都是违法的,我们可以在成员函数内部使用this
,因此尽管没有必要,但是我们还是可以把isbn
定义成如下形式std::string isbn() const { return this->bookNo; }
this
总是指向当前对象,因此this
是一个常量指针,如Sales date*const(顶层const
)。- 形参表后面的
const
,改变了隐含的this
形参的类型(本来是底层const
,现在this
既是底层const
又是顶层const
),如bool same_isbn(const Sales_item &rhs) const
,这种函数称为“常量成员函数”(this
指向的当前对象是常量)。 return *this;
返回调用该函数的对象- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向const类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
非成员函数
- 和类相关的非成员函数,定义和声明都应该在类的外部。
类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数是特殊的成员函数。
- 构造函数不能是
const
的 - 构造函数放在类的
public
部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }
=default
要求编译器合成默认的构造函数。(C++11
)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
构造函数不能被声明成
const
,因为当我们创造一个类的const
对象时,直到构造函数完成初始化的整个过程,对象才能真正的取得其常量
属性,因此,构造函数可以在const
对象的构造过程中向其写值
访问控制与封装
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏了类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类。唯一的却别在于访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
友元
- 允许特定的非成员函数访问一个类的私有成员.
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元声明成组地放在类定义的开始或者结尾。
- 友元可以定义在类内部,这样的函数时隐式内联的
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
友元的声明仅仅指定了访问的权限,而非一个普通的意义上的函数声明,如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元函数之外再专门对函数进行一次声明
封装的益处
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
类的其他特性
- 成员函数作为内联函数
inline
:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline
,如
inline Screen &Screen::move(pos r, pos c)
{
// we can specify inline on the definition
pos row = r * width; // compute the row location
cursor = row + c ; // move cursor to the column within that row
return *this; // return this object as an lvalue
}
- 可变数据成员 (mutable data member)
mutable size_t access_ctr;
mutable
修饰的变量可以被const
函数改变值,如:
class Screen {
public:
void some_member() const;
private:
mutable size_t access_ctr; // may change even in a const object
// other members as before
};
void Screen::some_member() const
{
++access_ctr; // keep a count of the calls to any member function
// whatever other work this member needs to do
}
//以上例子,尽管some_member是一个const成员函数,但是它仍然能够改变access_ctr的值
- 永远不会是
const
,即使它是const
对象的成员, - 类类型:
- 每个类定义了唯一的类型。(这里我觉得中文版翻译不是很好,我搬上来英文版的)
- Every class defines a unique type(拙见:每个类定义了一个唯一的类型)
- 对于两个类来说,即使他们的成员完全一致,但是这两个类也是不同的类型,如
struct First { int memi; int getMem();
};
struct Second {
int memi; int getMem();
};
First obj1;
Second obj2 = obj1; // error: obj1 and obj2 has different types
类的作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
Just as we can declare a function apart from its definition, we can
also declare a class without defining it: class Screen; // declaration of the Screen class
This declaration, sometimes referred to as a forward declaration, introduces the
name Screen into the program and indicates that Screen refers to a class type.
After a declaration and before a definition is seen, the type Screen is an incomplete
type—it’s known that Screen is a class type but not known what members that type
contains.
We can use an incomplete type in only limited ways: We can define pointers or references to such types, and we can declare (but not define) functions that use an
incomplete type as a parameter or return type.
With one exception that we’ll describe in § 7.6 (p. 300), data members can be
specified to be of a class type only if the class has been defined. The type must be
complete because the compiler needs to know how much storage the data member
requires. Because a class is not defined until its class body is complete, a class cannot
have data members of its own type. However, a class is considered declared (but not
yet defined) as soon as its class name has been seen. Therefore, a class can have
data members that are pointers or references to its own type:
class Link_screen
{ Screen window;
Link_screen *next;
Link_screen *prev;
};
构造函数再探
- 构造函数初始值列表:
- 如果成员是
const
或者引用类型的数据,只能初始化,不能赋值。而我们初始化const
或者引用类型的数据成员的唯一机会就是通过构造函数厨师长,但是切记,一旦构造函数体开始执行,那么初始化就完成了,所以只能使用构造函数初始值列表为这些成员初始化,如:
- 如果成员是
class ConstRef {
public:
ConstRef(int ii);
private:
int i; const int ci; int &ri;
};
// error: ci and ri must be initialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // error: ri was never initialized
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
}
- 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
~~~~~~ 建议使用构造函数初始值,因为初始化和赋值的区别事关底层效率问题,前者直接初始化数据成员,后者则先初始化再赋值
~~~~~~ 最好让构造函数初始值的顺序和成员声明的顺序保持一致,因为构造函数初始值列表只说明初始化成员的值,而不限定初始化的具体执行顺序,而他们的顺序取决于它们在类定义中出现的顺序,这样想是不是初始化的顺序就没什么特别的要求的,但是!如果一个成员是用另一个成员来初始化的,那这两个成员的顺序就比较重要了
class X
{
int i;
int j;
public:
// undefined: i is initialized before j
X(int val): j(val), i(j) { }
};
委托构造函数 (delegating constructor, C++11
)
- 委托构造函数将自己的职责委托给了其他构造函数,如:
class Sales_data {
public:
// defines the default constructor as well as one that takes a string argument
Sales_data(std::string s = ""): bookNo(s) { }
// remaining constructors unchanged
Sales_data(std::string s, unsigned cnt, double rev): bookNo(s), units_sold(cnt), revenue(rev*cnt) { }
Sales_data(std::istream &is) { read(is, *this); }
// remaining members as before
};
隐式的类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
- 编译器只会自动地执行
一步
类型转换,如
// error: requires two user-defined conversions:
// (1) convert "9-999-99999-9" to string
// (2) convert that (temporary) string to Sales_data
item.combine("9-999-99999-9");
//正确的应该
// ok: explicit conversion to string, implicit conversion to Sales_data
item.combine(string("9-999-99999-9"));
// ok: implicit conversion to string, explicit conversion to Sales_data
item.combine(Sales_data("9-999-99999-9"));
- 抑制构造函数定义的隐式转换:
- 对于那种只接受一个实参的构造函数,可以加上关键字
explicit
阻止隐式类型转换,而接受多个实参的不会发生隐式转换,所以用不上explicit
,如:
- 对于那种只接受一个实参的构造函数,可以加上关键字
class Sales_data {
public:
Sales_data() = default; Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
explicit Sales_data(const std::string &s): bookNo(s) { }
explicit Sales_data(std::istream&);
// remaining members as before
};
item.combine(null_book); // error: string constructor is explicit
item.combine(cin); // error: istream constructor is explicit
};
// error: explicit allowed only on a constructor declaration in a class header
explicit Sales_data::Sales_data(istream& is)
{
read(is, *this);
}
- 发生隐式转换的一种情况就是当我们执行拷贝形式的初始化。所以这种情况只能使用直接初始化,不能用于拷贝形式的初始化,也就是说,
explicit
关键字声明构造函数时,这个函数只能已直接初始化形式使用,并且编译器将不会在自动转换过程中使用该构造函数,如:
Sales_data item1 (null_book); // ok: direct initialization
// error: cannot use the copy form of initialization with an explicit constructor
//翻译:错误,不能将explicit构造函数用于拷贝形式的初始化
Sales_data item2 = null_book;
尽管编译器不会将explicit的构造函数用于隐式类型转换中,但是我们可以适用这样的构造函数显式的强制进行转换,如
// ok: the argument is an explicitly constructed Sales_data object
item.combine(Sales_data(null_book));
// ok: static_cast can use an explicit constructor
item.combine(static_cast<Sales_data>(cin));
聚合类 (aggregate class)
- 满足以下所有条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
- 如:
struct Data
{
int ival;
string s;
};
//所以我们可以这样初始化成员
// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };
字面值常量类
constexpr
函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
类的静态成员
- 非
static
数据成员存在于类类型的每个对象中。 - 静态成员函数不与任何对象绑定在一起,他们不包括
this
指针,不能被声明成const
,也不能在函数体内使用this
static
数据成员独立于该类的任意对象而存在。- 每个
static
数据成员是与类关联的对象,并不与该类的对象相关联,所以他们不是由类的构造函数初始化的 - 声明:
- 声明之前加上关键词
static
。
- 声明之前加上关键词
- 使用:
- 使用作用域运算符
::
直接访问静态成员:r = Account::rate();
- 也可以使用对象访问:
r = ac.rate();
- 使用作用域运算符
- 定义:
- 在类外部定义时不用加
static
。
- 在类外部定义时不用加
- 初始化:
- 通常不在类的内部初始化,而是在定义时进行初始化,如
double Account::interestRate = initRate();
- 如果一定要在类内部定义,则要求必须是字面值常量类型的
constexpr
- 类被声明时,称之为不完全声明,此时类内只能有指针或者引用,不能有数据成员,但是静态数据成员却可以
- 此外,可以把静态成员作为默认实参
- 通常不在类的内部初始化,而是在定义时进行初始化,如
即使在类内初始化了一个常量静态数据成员,通常情况下也应该在类的外部定义一下该成员