本章主要关注数据抽象的主要性,数据抽象能帮我们将对象的具体实现与对象所能执行的操作分离开来。
Classes 之间的关系
- 继承(inheritance)
- 复合(composition)
- 委托(delegation)
定义抽象数据类型
- 类的基本思想:数据抽象(data abstraction)和封装(encapsulation)。
- 数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。
类成员 (Member)
- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。
类的成员函数
- 成员函数的声明必须在类的内部。
- 成员函数的定义既可以在类的内部也可以在外部。
- 使用点运算符
.
调用成员函数。实际上是在替.
前面的对象调用它。 - 必须对任何
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 &obj) const
,这种函数称为 常量成员函数(this
指向的当前对象是常量)。 return *this;
返回调用该函数的对象。- 普通的非
const
成员函数:this
是指向类类型的const
指针(可以改变this
所指向的值,不能改变this
保存的地址)。 const
成员函数:this
是指向const类类型的const
指针(既不能改变this
所指向的值,也不能改变this
保存的地址)。
- 每个成员函数都有一个额外的,隐含的形参
- 常量对象只能调用常量成员函数(避免普通成员函数修改常量对象成员的值)。
- 常量对象的 this 指针的类型:
const Sales_data *const
,与普通成员函数接收的this
指针类型不匹配。 - 常量成员函数:
string isbn() const
- 常量对象的 this 指针的类型:
- 普通对象既可以调用常量成员函数也可以调用普通成员函数。
非成员函数
- 和类相关的非成员函数,定义和声明都应该在类的外部。
- 声明与类在同一个头文件中。
类的构造函数
- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
- 只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。
- 构造函数是特殊的成员函数。
- 构造函数放在类的
public
部分。 - 与类同名的成员函数。
Sales_item(): units_sold(0), revenue(0.0) { }
=default
要求编译器合成默认的构造函数。(C++11
)- 初始化列表:冒号和花括号之间的代码:
Sales_item(): units_sold(0), revenue(0.0) { }
访问控制与封装
- 访问说明符(access specifiers):
public
:定义在public
后面的成员在整个程序内可以被访问;public
成员定义类的接口。private
:定义在private
后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问;private
隐藏类的实现细节。
- 使用
class
或者struct
:都可以被用于定义一个类,唯一的区别在于访问权限。- 使用
class
:在第一个访问说明符之前的成员是priavte
的。 - 使用
struct
:在第一个访问说明符之前的成员是public
的。
- 使用
友元(friend)
- 允许特定的非成员函数访问一个类的私有成员.
- 友元的声明以关键字
friend
开始。friend Sales_data add(const Sales_data&, const Sales_data&);
表示非成员函数add
可以访问类的非公有成员。 - 通常将友元声明放在类定义的开始或者结尾。
- 在类外也要为友元函数提供独立的声明。
- 类之间的友元:
- 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
- 如果仅指定其它类的成员函数为友元函数,须指明成员函数属于哪个类。
- 友元函数能定义在类的内部,这样的函数是隐式内联的。
- 相同 class 的各个 objects 互为 friends(友元)。
封装
封装是实现与接口的分离。它隐藏了类型的实现细节。 (在C ++中,通过将实现放在类的私有部分中来实现封装)
- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
类的其他特性
- 成员函数作为内联函数
inline
:- 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
- 定义在类内部的函数是自动内联的。
- 在类外部定义的成员函数,也可以在声明时显式地加上
inline
。 - inline 成员函数应该与相应的类定义在同一个头文件中。
- 可变数据成员 (mutable data member):
mutable int count;
- 永远不会是
const
,即使它是const
对象的成员。
- 类类型:
- 每个类定义了唯一的类型。
- 一个 const 成员函数如果以引用的形式返回 *this ,那么它的返回类型将是常量引用。
class Screen
仅声明类,而暂时不定义它。
类的作用域
- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
- 类中的类型名定义都要放在一开始。
构造函数再探
单例模式中构造函数在 private 区域。
- 构造函数初始值列表:
- 如
const
或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别) - 如果成员是
const
、引用,或者是某种未提供默认构造函数的类类型,我们必须同感构造函数初始值列表为这些成员提供初值。- 初始化列表是初始化。
- { } 内是赋值操作。
- 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
- 如果一个构造函数为所有参数都提供了默认实参,那么它实际上也定义了默认的构造函数。
- 不能有多个提供了所有实参的构造函数,会导致默认构造函数不明确。
- 如
- 如果定义了其它构造函数,那么最好也提供一个默认构造函数。
委托构造函数 (delegating constructor, C++11
)
- 委托构造函数将自己的职责委托给了类内想其他构造函数。
- 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
Sale_data(): Sale_data("", 0, 0) {}
隐式的类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数又叫转换构造函数(converting constructor)。
1 | string null_book = "9-999-99999-9"; |
- 编译器只会自动地执行 仅一步 类型转换。
- 抑制构造函数定义的隐式转换:
- 将构造函数声明为
explicit
加以阻止。 - 只能在类内声明构造函数时使用
explicit
关键字。 explicit
构造函数只能用于直接初始化,不能用于拷贝形式的初始化。
- 将构造函数声明为
聚合类 (aggregate class)
- 满足以下所有条件:
- 所有成员都是
public
的。 - 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有
virtual
函数。
- 所有成员都是
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。
字面值常量类
constexpr
函数的参数和返回值必须是字面值。- 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
- 数据成员都必须是字面值类型。
- 类必须至少含有一个
constexpr
构造函数。 - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数。 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
- 构造函数不能是
const
的,但是字面值常量类的构造函数可以是constexpr
。
类的静态成员
- 非
static
数据成员存在于类类型的每个对象中。 static
数据成员独立于该类的任意对象而存在。- 每个
static
数据成员是与类关联的对象,并不与该类的对象相关联。 - 声明:
- 声明之前加上关键词
static
,只出现在类内。
- 声明之前加上关键词
- 使用:
- 使用作用域运算符
::
直接访问静态成员:r = Account::rate();
- 也可以使用对象访问:
r = ac.rate();
- 使用作用域运算符
- 定义:
- 在类外部定义时不用加
static
。也可在类内定义。
- 在类外部定义时不用加
- 初始化:
- 通常不在类的内部初始化,而是在类外定义和初始化
double Account::interestRate = initRate();
- 如果一定要在类内部定义,则要求静态成员必须是字面值常量类型的
constexpr
。static constexptr int period = 30
- 通常不在类的内部初始化,而是在类外定义和初始化
- 静态成员函数不包含 this 指针,也不能声明为 const 的。
- 静态数据成员可以作为默认实参。
- 静态数据成员的类型可以就是它所属的类类型:
1 | class Bar { |