7.类
类
1 定义抽象数据类型
类的基本思想是
- 数据抽象(data abstraction):接口与实现分离
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
- 封装(encapsulation):把实现对外隐藏起来
1.1 例子:
设计 Sales_data 类
- 一个 isbn 成员函数,用于返回对象的 ISBN 编号
- 一个 combine 成员函数,用户一个 Sales_data 对象加到另一个对象上
- 一个名为 add 的函数,指向两个 Sales_data 对象的加法
- 一个 read 函数,将数据从 istream 读入到 Sales_data 对象中
- 一个 print 函数,将 Sales_data 对象的值输出到 ostream
类的用户,即类的使用者,也是开发人员,包括类的开发人员本人。 但设计类的接口时,需要假设类的用户对类的细节并不知情。
1.2 成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
1.3 类的成员函数
-
成员函数的声明必须在类的内部。
-
成员函数的定义既可以在类的内部也可以在外部。
-
使用点运算符
.
调用成员函数。 -
必须对任何
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
是一个常量指针。- 形参表后面的
const
,改变了隐含的this
形参的类型,如bool same_isbn(const Sales_item &rhs) const
,这种函数称为“常量成员函数”(this
指向的当前对象是常量)。 return *this;
可以让成员函数连续调用。- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向 const 类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
- 每个成员函数都有一个额外的,隐含的形参
-
类作用域和成员函数
- 成员体可以随意使用类中的其他成员而无需在意这些成员出现的次序。
- 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。
1.4 非成员函数
- 和类相关的非成员函数,定义和声明都应该在类的外部。
- 如果非成员函数是类接口的组成部分,则应该与类在同一个头文件中声明
1.5 类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public
部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }
=default
要求编译器合成默认的构造函数。(C++11
)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
- 构造函数与类名同名,没有返回值,它的任务是初始化类对象的数据成员
- 类可以包括多个构造函数,和重载函数差不多
- 构造函数不能被声明为 const 的
- 直到构造函数完成初始化,对象才能真正得到“常量”属性。
- 只有当类没有声明任何构造函数,并且所有类类型的成员都有默认构造函数是,编译器才会自动地生成默认构造函数。
- 建议手动声明类的默认构造函数
2 访问控制与封装
使用访问说明符加强类的封装性
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏了类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类。唯一的却别在于默认访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
2.1 封装的益处
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
2.2 友元
- 允许特定的非成员函数访问自己类的私有成员.
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元声明成组地放在类定义的开始或者结尾。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
3 类的其他特性
- 成员函数作为内联函数
inline
:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline
。
- 可变数据成员 (mutable data member):
mutable size_t access_ctr;
- 永远不会是
const
,即使它是const
对象的成员。
- 类类型:
- 每个类定义了唯一的类型。
4 类的作用域
4.1 名字查找与类的作用域
- 一个类就是一个作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
5 构造函数再探
5.1 构造函数初始值列表
- 类似
python
使用赋值的方式有时候不行,比如const
或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别) - 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
- 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。
5.2 委托构造函数(delegating constructor, C++11
)
- 委托构造函数将自己的职责委托给了其他构造函数。
Sale_data(): Sale_data("", 0, 0) {}
5.3 默认构造函数的作用
一个构造函数为所有参数都提供了默认实参,也就定义了默认构造函数
5.4 隐式的类类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
- 编译器只会自动地执行
仅一步
类型转换。 - 抑制构造函数定义的隐式转换:
- 将构造函数声明为
explicit
加以阻止。 explicit
构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 将构造函数声明为
5.5 聚合类
- 满足以下所有条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
如果成员是 const、引用、或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始列表提供初始值
5.6 字面值常量类
constexpr
函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
6 类的静态成员
- 非
static
数据成员存在于类类型的每个对象中。 static
数据成员独立于该类的任意对象而存在。- 每个
static
数据成员是与类关联的对象,并不与该类的对象相关联。 - 声明:
- 声明之前加上关键词
static
。
- 声明之前加上关键词
- 使用:
- 使用作用域运算符
::
直接访问静态成员:r = Account::rate();
- 也可以使用对象访问:
r = ac.rate();
- 使用作用域运算符
- 定义:
- 在类外部定义时不用加
static
。
- 在类外部定义时不用加
- 初始化:
- 通常不在类的内部初始化,而是在定义时进行初始化,如
double Account::interestRate = initRate();
- 如果一定要在类内部定义,则要求必须是字面值常量类型的
constexpr
。
- 通常不在类的内部初始化,而是在定义时进行初始化,如
7 小结
类是 C++语言中最基本的特性。类允许我们为自己的应用定义新类型,从而使得程序更加简洁且易于修改。
类有两项基本能力:一是数据抽象,定义数据成员和函数成员的能力:二是封装,即保护类的成员不被随意访问的能力。通过将类的实现细节设为 private,我们就能完成类的封装。类可以将其他类或者函数设为友元,这样它们就能访问类的非公有成员了。
类可以定义一种特殊的成员函数:构造函数。其作用是控制初始化对象的方式。构造函数可以重载,构造函数应该使用构造函数初始值列表来初始化所有数据成员。
类还能定义可变或者静态成员。一个可变成员永远都不会是 const,即使在 const 成员函数内也能修改它的值:一个静态成员可以是函数也可以是数据,静态成员存在于所有对象之外。
8 术语表
抽象数据类型(abstract data type): 封装(隐藏)了实现细节的数据结构。
访问说明符(access specifier): 包括关键字 public 和 private。用于定义成员对类的用户可见还是只对类的友元和成员可见。在类中说明符可以 i 出现多次,每个说明符的有效范围从它自身开始,到下一个说明符为止。
聚合类(aggregate class): 只含有公有成员的类,并且没有类内初始值或者构造函数。聚合类的成员可以用花括号括起来的初始值列表进行初始化。
类(class): C++提供的自定义数据类型的机制。类可以包含数据,函数和类型成员。一个类定义一种新的类型和一个新的作用域。
类的声明(class declaration): 首先是关键字 class(或者 struct),随后是类名以及分号。如果类已经声明而尚未定义,则它是一个不完全类型。
类的作用域(class scope): 每个类定义一个作用域。类作用域比其他作用域更加复杂,类中定义的成员函数甚至有可能使用定义语句之后的名字。
常量成员函数(const member function): 一个成员函数,在其中不能修改对象的普通(即既不是 static 也不是 mutable)数据成员。const 成员的 this 指针是指向常量的指针,通过区分函数是否是 const 可以进行重载。
构造函数(constructor): 用于初始化对象的一种特殊的成员函数。构造函数应该给每个数据成员都赋一个合适的初始值。
构造函数初始值列表(constructor initializer list): 说明一个类的数据成员的初始值,在构造函数体执行之前首先用初始值列表中的值初始化数据成员将被默认初始化。
转换构造函数(converting constructor): 可以用一个实参调用的非显式构造函数。这样的函数隐式地将参数类型转换成类类型。
数据抽象(data abstraction): 着重关注类型接口的一种编程技术。数据抽象令程序员可以忽略类型的实现细节,只关注类型执行的操作即可。数据抽象是面对对象编程和泛型编程的基础。
默认构造函数(default constructor): 当没有提供任何实参时使用的构造函数。
委托构造函数(delegating constructor): 委托构造函数的初始值列表只有一个入口,指定类的另一个构造函数执行初始化操作。
封装(encapsulation): 分类类的实现与接口,从而隐藏了类的实现细节。在 C++语言中,通过把实现部分设为 private 完成封装的任务。
显式构造函数(explicit constructor): 可以用一个单独的实参调用但是不能用于隐式转换的构造函数。通过在构造函数的声明之前加上 explicit 关键字就可以将其声明成显式构造函数。
前向声明(forward declaration): 对尚未定义的名字的声明,通常用于表示位于类定义之前的类声明。参见“不完全类型”
友元(friend): 类向外部提供其非公有成员访问权限的一种机制。友元的访问权限与成员函数一样。友元可以是类,也可以是函数。
实现(implementatation): 类的成员(通常是私有的),定义了不希望为使用类类型的代码所用的数据及任何操作。
接口(interface): 类型提供的(公有)操作。通常情况下,接口不包含数据成员。
成员函数(member function): 类的函数成员。普通的成员函数通过隐式的 this 指针与类的对象绑定在一起;静态成员函数不与对象绑定在一起也没有 this 指针。成员函数可以重载,此时隐式的 this 指针参与函数匹配的过程。
可变数据成员(mutable data member): 这种成员永远不是 const,即使它属于 const 对象。在 const 函数内可以修改可变数据成员。
名字查找(name lookup): 根据名字的使用寻找匹配的声明过程。
私有成员(private member): 定义在 private 访问说明符之后的成员,只能被类的友元或类的其他成员访问。数据成员以及仅同类本书使用而不作为接口的功能函数一般设为 private。
公有成员(public member): 定义在 public 访问说明符之后的成员,可以被类的所有用户访问。通常情况下,只有实现类的接口的函数才被设为 public。
合成默认构造函数(synthesized default constructor): 对于没有显式地定义任何构造函数的类,编译器为其创建(合成)的默认构造函数。该构造函数检查类的数据成员,如果提供了类内初始值,就用它执行初始化操作;否则就对数据成员执行默认初始化。
this 指针(this pointer): 是一个隐式的值,作为额外的实参传递给类的每个非静态成员函数。this 指针指向代表函数调用者的对象。
= default: 一种语法形式,位于类内部默认构造函数声明语句的参数列表之后,要求编译器生成构造函数,而不管类是否已经有了其他构造函数。