第 7 章:类
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。
数据抽象依赖于接口(interface)和实现(implementation)分离的编程技术。
7.1 定义抽象数据类型
引入 this
在类外部定义成员函数
stracut
Type Class::Func() {}
返回 this 对象的函数
return *this;
构造函数
构造函数的任务是初始化类的成员,只要类对象被创造,就会执行构造函数。
= default
默认构造函数
struct Some_class {
Some_class() = default;
}
构造函数初始值列表
struct Some_class {
Some_class(string &s) : a(s) {};
// 等同于 Some_class(string &s) : a(s), b(0), c(0) {};
Some_class(string &s, int i1, int i2) : a(s), b(i1), c(i1 * i2) {};
string a;
int b;
int c;
}
在类外部定义构造函数
拷贝、赋值和析构
7.2 访问控制与封装
使用访问说明符加强类的封装:public、private
class 和 struct
他们的唯一区别就是默认访问:
class 在第一个访问说明符前的定义都是 private
struct 在第一个访问说明符前的定义都是 public
统一风格:成员都是 private 时用 class,成员都是 public 用 struct
友元
友元不是类的成员也不受它所在区域访问控制级别的约束。
Class TestClass {
friend xxxx
friend xxxx
public:
xxxx
xxxx
private:
xxxx
xxxx
}
封装的优点:
- 确保用户代码不会无意间破坏封装对象的状态。
- 被封装的代码可以随时改变,不用改用户代码(但是要重新编译)
7.3 类的其他特性
类型成员、类成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回 *this、如何定义并使用类类型以及友元类。
class Screen {
public:
typedef std::string::size_type pos;
// using pos = std::string::size_type;
}
令成员作为内联函数
定义在类内部的成员函数默认是 inline 的。
重载成员函数
成员函数也可以被重载。
可变数据成员
mutabel data member
加入 mutable 关键字。
class Screen {
private:
mutable size_t size;
}
类成员的初始值
使用 = 或者 {} 初始化值。
返回 *this 的成员函数
返回 *this 后可以进行链式调用。
从 const 成员函数返回 *this
将不能进行链式调用。返回了一个常量引用,无权 set 一个常量对象。
建议:对于公共代码使用私有功能函数
- 同一段代码,到处使用
- 隐式地被声明成内联函数,不会增加额外开销
类类型
每个类定义了唯一类型。
class Screen;
向前声明,在定义之前是一个不完全类型
友元再探
可以指定一个类为当前类的友元,被指定的类可以访问当前类的私有变量。
class Screen {
friend class Widow_mgr;
}
每个类负责控制自己的友元类或友元函数。
也可以指定类的成员函数
class Screen {
friend void Widow_mgr::clear(ScreenIndex);
}
友元声明与函数作用域
声明友元类的成员调用该友元函数,必须是被声明过的。
7.4 类的作用域
作用域与定义在类外部的成员
一个类就是一个作用域。所以在类外部定义成员必须同时提供类名和函数名。
名字查找与类的作用域
名字查找:
- 在块中寻找声明语句(名字使用之前)
- 没有找到继续查找外层作用域
- 最终没有匹配,那么程序报错
类的定义:
- 首先编译成员的声明
- 直到类全部可见后编译函数体
类型名要特殊处理
当成员已经使用了外层的名字(某一种类型),则不能在类之后重新定义该名字。
类型名的定义通常出现在类的开始处,确保所有成员都能使用该名字。
7.5 构造函数再探
构造函数初始值列表
如果成员是 const、引用、未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
成员初始化顺序
最好令构造函数初始值的顺序与成员声明的顺序一致。最好避免成员初始化其他成员。
默认实参与构造函数
如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认构造函数。
委托构造函数
class MyClass {
MyClass(string s, int i): myStr(s), myInt(i) {};
MyClass(): MyClass("", 0) {};
}
默认构造函数的作用
使用默认构造函数
obj 是一个默认初始化对象。
Sales_data obj;
隐式的类类型转换
通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
string null_book = "999-999-999";
item.combine(null_book);
null_book 会通过 Sales_data(string){}
的构造函数来隐式转换。
但是隐式转换只能处理一次。可以先显示转为 string,再隐式转为 Sales_data。
抑制构造函数定义的隐式转换
添加关键字 explicit
来抑制。 explicit Sales_data(string) {}
。
explicit
只对一个实参的构造函数有效。
explicit
关键字声明构造函数时,它将只能以直接初始化的形式使用。
显示的使用转换
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));
聚合类
聚合类(aggregate class)使得用户可以直接访问其成员。需要满足一下条件:
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有 virtual 函数
struct {
int iVal;
string s;
}
字面值常量类
数据成员都是字面值类型的聚合类是字面值常量类。
或者符合如下要求:
- 数据成员必须都是字面值类型
- 类必须至少有一个 constexpr 构造函数
- 如果有类内初始值。初始值必须是一条常量表达式,如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
7.6 类的静态成员
声明静态成员
使用 static
关键字。
静态成员可以是 public 或 private 的。
想要确保对象只定义一次,最好的办法是把静态数据成员的定义和其他非内联函数定义放在同一个文件。
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。