C++ 学习笔记之(7)-类
类的基本思想是数据抽象和封装。封装实现了类的接口和实现的分离。数据抽象是依赖于接口和实现分离的编程技术。
定义抽象数据类型
定义改进的Sales_data
类
struct Sales_data{
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
// 数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
}
// Sales_data 的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
定义在类内部的函数是隐式的
inline
函数所有成员必须在类内部声明,但成员函数体定义可在类内或类外
this
为成员函数的隐式参数,由函数对象地址初始化,为常量指针,指向对象本身,不可改变。常量成员函数(
const member function
):成员函数参数列表后加const
关键字,作用是修改隐式this
指针的类型,使其成为指向常量对象的常量指针const Sales_data *const
。因为默认this
类型为指向类类型非常量版本的常量指针Sales_data *const
,故this
无法绑定到常量对象。常量成员函数不能修改对象内容。std::string isbn() const { return bookNo; }
常量对象以及常量对象的引用或指针都只能调用常量成员函数
编译器分两步处理类,首先编译类成员声明,然后到成员函数体。故成员函数体可随意使用类其他成员。
定义类相关的非成员函数
类需要一些辅助函数比如上述add
, read
等,这些函数属于类的接口组成部分,实际不属于类本身
如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件中
IO类属于不能被拷贝的类型,所以只能通过引用传递他们
istream &read(istream &is, Sales_data &item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is; }
构造函数
构造函数是用来控制其对象的初始化过程。
构造函数名字和类名相同,没有返回类型
构造函数不能被声明成
const
,当创建类的const
对象时,知道构造函数完成初始化过程,对象才真正取得常量
属性默认构造函数:类通过一个特殊的构造函数控制默认初始化过程,无需任何实参。当类没有显示定义构造函数时,编译器会隐式定义默认构造函数,又被称为
合成的默认构造函数
如果类包含内置类型或复合类型的成员,则应赋予其类内初始值,这样类才适合使用合成的默认构造函数,否则默认初始化,其值未定义。
C++11新标准定义在参数列表后使用
= default
要求编译器生成构造函数。struct Sales_data{ Sales_data() = default; }
构造函数初始值列表:负责为新创建对象的一个或几个数据成员赋初值
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenus(p*n) {}
访问控制与封装
C++使用访问说明符(access specifiers)加强累的封装性
- 定义在
public
说明符之后的成员在整个程序内可被访问 - 定义在
private
说明符后的成员可被类成员函数访问,但不能被使用该类的代码访问
- 定义在
struct
和class
的默认访问权限不一样。class
关键字的默认为private
友元:类可以使其他类或函数成为该类友元,就可以使其他类或函数访问它的非公有成员
friend Sales_data add(const Sales_data&, const Sales_data&);
友元声明仅指定了范文权限,而非通常意义的函数声明,最好在友元声明外再对函数进行一次声明
类的其他特性
类成员再探
inline
可用在类内部函数声明,也可用在类外部函数定义处可变数据成员(mutable):永远不会是
const
, 即使它是const
对象的成员,即一个const
成员函数可以改变一个可变成员的值class A { public: void add() const { ++b; // 成立,因为是可变成员 mutable } private: mutable int b; // 即使在一个`const`对象内也能被修改 }
返回 *this
的成员函数
- 一个
const
成员函数如果以引用形式返回*this
, 那么它的返回类型将是常量引用 - 通过区分成员函数是否为
const
,可以对其进行重载,原因类似于函数参数中指针是否为const
类类型
- 前向声明:函数声明和定义分离,在声明之后定义之前是一个不完全类型,即只知类类型,却不清楚包含那些成员
- 不完全类型的使用非常有限:可以定义指向不完全类型的指针或引用个也可以声明(但不能定义)以不完全类型作为参数或返回类型的函数。
友元再探
类之间的友元关系
class Screen{
friend class Window_mgr; // Window_mgr 的成员可以访问 Screen 类的私有部分
};
- 若类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员
- 友元关系不存在传递性,即
window_mgr
的友元类不具有访问Screen
的特权 - 每个类负责控制自己的友元类或友元函数
令成员函数作为友元
class Screen{
friend void Window_mgr::clear(ScreenIndex); // Window_mgr::clear必须在Screen类之前被声明
}
友元函数设计规则
- 首先定义
Window_mgr
类,其中声明clear
函数,但不能定义。在clear
使用Screen
的成员之前必须先声明Screen
- 接下来定义
Screen
, 包括对于clear
的友元声明 - 最后定义
clear
,此时它才可以使用Screen
的成员
友元声明和作用域
友元声明的作用是影响访问权限,并非普通意义的声明
struct X{ friend void f() { /* 友元函数可以定义在类内部 */} X() { f(); } // 错误:f 还没有被声明 void g(); void h(); }; void X::g() { return f(); } // 错误:f 还没有被声明 void f(); // 声明那个定义在 X 类中的函数 void X::h() { return f(); } // 正确:现在 f 的声明在作用域中了
类的作用域
一个类就是一个作用域,故外部定义类成员函数需要提供类名和函数名,在类外部,成员的名字会被隐藏
编译器处理完类中的全部声明后才会处理成员函数的定义
外层对象若被隐藏,可使用作用域运算符访问
void Screen::dummy_fcn(pos height){ cursor = width * ::height; // 全局变量height }
构造函数再探
构造函数初始值列表
- 若在构造函数的初始值列表中未显示初始化成员,则成员会在构造函数体之前执行默认初始化
- 构造函数初始值是进行初始化,构造函数体中执行的是赋值
- 若成员是
const
、引用,或者某种未提供默认构造函数的类类型,必须通过构造函数初始值列表进行初始化。 - 初始化和赋值的区别事关底层效率问题
- 初始化是直接初始化数据成员
- 赋值是先初始化后赋值
- 成员初始化顺序和在类定义中的出现顺序一致,并且尽量避免使用某些成员初始化其他成员
- 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数
委托构造函数
C++11定义了 委托构造函数(delegating constuctor):即可使用其所属类的其他构造函数执行它的初始化过程
class Sales_data{
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt * price) {}
// 委托构造函数将初始化过程委托给另一个构造函数,先执行被委托的上述构造函数,在执行本函数体
Sales_data(): Sales_data("", 0, 0) {}
}
- 先执行被委托的构造函数,再执行委托者的函数体
隐式的类类型转换
转换构造函数:若构造函数只接受一个实参,则实际上定义了转换为此类类型的隐式转换机制,这种构造函数被称为转换构造函数
string null_book = '9-999=99999-9'; // 构造临时Sales_data对象,对象的units_sold 和 revenue 为0, bookNo 等于 null_book item.combine(null_book);
编译器只会自动执行一步类型转换
// 错误:需要两步转换,先将字符串字面值转为string,再将临时的string对象转换成Sales_data对象 item.combine("9-999-99999-9"); item.combine(string("9-999-99999-9")); // 正确:显示转换成 string,隐式转换成 Sales_data
可以通过将构造函数声明为
explicit
阻止隐式转换,explicit
只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。class Salse_data{ public: explicit Sales_data(const std::string &s): bookNo(s) {} }
只能在类内声明构造函数时使用
explicit
关键字,类外部定义时不应重复explicit
构造函数只能用于直接初始化,不能用于拷贝形式初始化
聚合类
聚合类是指用户可以直接访问其成员,并且有特殊的初始化语法形式,条件如下
- 所有成员都是
public
的 - 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有
virtual
函数
字面值常量类
字面值常量类的要求
- 数据成员都必须是字面值类型
- 类必须至少含有一个
constexpr
构造函数 - 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的
constexpr
构造函数 - 类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr
构造函数
构造函数不能为const
,但字面值常量类的构造函数可以使constexpr
函数
constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
constexpr
构造函数必须初始化所有数据成员,初始值使用constexpr
构造函数或者是常量表达式
类的静态成员
与类而非对象关联的成员为静态成员,可在成员声明之前加上关键字static
, 静态成员可以是public
或private
的,类型可以是常量、引用、指针或类类型等
静态成员函数不能声明成
const
static
函数体内不能使用this
指针,因为静态成员函数不与对象绑定,没有this
指针可以使用作用域运算符直接访问静态成员,可以使用类对象、引用或指针访问静态成员
double r; r = Account::rate(); // 使用作用域运算符访问静态成员 Account ac1, *arc = &ac1; // 调用静态成员函数 rate 的等价形式 r = ac1.rate(); // 通过 Account 的对象或引用 r = ac2->rate(); // 通过指向 Account 对象的指针
成员函数不需要通过作用域运算符就能直接使用静态成员
static
关键字只能出现在类内部的声明语句中因为静态数据成员不属于累的任何一个对象,所以不能再类内部初始化静态成员,必须在类外部定义和初始化每个静态成员,静态数据成员只能定义一次
若静态成员为字面值常量类型的
constexpr
,可以为静态成员提供const
整数类型的类内初始值,初始值必须是常量表达式静态数据成员可以使不完全类型,特别的,可以就它所属的类类型。而非静态成员则收到限制,只能声明成它所属类的指针或引用
静态成员可以做默认实参,而普通成员不行
结语
类是C++语言中最基本的特性,有两项基本能力:数据抽象,即定义数据成员和函数成员的能力;二是封装,即保护类的成员不被随意访问的能力。通过将类的实现细节设为private
,就可以实现封装。类可以将其他类或者函数设为友元,这样它们就能访问类的非公有成员
类可以定义构造函数,用来初始化对象。构造函数可以重载,切应该使用构造函数初始值列表来初始化所有数据成员
类还能定义可变或静态成员,一个可变成员永远不会是const
, 即使在const
成员函数内也能修改它的值;一个静态成员可以使函数也可以是数据,静态成员存在于所有对象之外。