C++ Primer 第七章 类
- 类的基本思想是数据抽象和封装
- 数据抽象是一种依赖于接口和实现分离的编程技术
- 类的接口包括用户所能执行的操作
- 类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数
- 封装实现了类的接口和实现的分离
- 类要想实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节
定义抽象数据类型
设计 Sales_data 类
- 一个 isbn 成员函数,用于返回对象的 ISBN 编号
- 一个 combine 成员函数,用于将一个 Sales_data 对象加到另一个对象上
- 一个名为 add 的函数,执行两个 Sales_data 对象的加法
- 一个 read 函数,将数据从 istream 读入到 Sales_data 对象中
- 一个 print 函数,将 Sales_data 对象的值输出到 ostream
使用改进的 Sales_data 类
Sales_data total;
if (read(cin, total)) {
Sales_data trans;
while (read(cin, trans)) {
if (total.isbn() == trans.isbn())
total.combine(trans);
else {
print(cout, total) << endl;
total = trans;
}
}
print(cout, total) << endl;
} else {
cerr << "No data?!" << endl;
}
定义改进的类
-
bookNo,string类型 表示 ISBN 编号
-
units_sold,unsigned 类型 表示某本书的销量
-
revenue,double 类型 表示这本书的总销售输入
-
成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如 add、read 和 print 等,它们的定义和声明都在类的外部。
struct Sales_data {
// 新成员:关于 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
- 成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象,this 是一个常量指针,我们不允许改变 this 中保存的地址
**尽管没必要,但是我们还是能把 isbn 定义成如下的形式
std::string isbn() const { return this -> bookNo; }
引入 const 成员函数
- 把 this 设置为指向常量的指针有助于提高函数的灵活性
- C++ 语言的做法是允许把 const 关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的 const 表示 this 是一个指向常量的指针。像这样使用 const 的成员函数被称作常量成员函数
类作用域和成员函数
- 类的成员函数的定义嵌套在类的作用域之内,因此,isbn 中用到的名字 bookNo 其实就是定义在 Sales_data 内的数据成员
- 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。
在类的外部定义成员函数
- 当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。同时,类外部定义的成员的名字必须包含它所属的类名
- 使用作用域运算符,让编译器理解剩余的代码是位于类的作用域内的,因此当 avg_price 使用 revenue 和 units_sold 时,实际上它隐式地使用了 Sales_data 的成员
double Sales_data::avg_price() const {
if (units_sold) return revenue / units_sold;
else return 0;
}
定义一个返回 this 对象的函数
- 内置的赋值运算符把它的左侧运算对象当成左值返回,因此为了与它保持一致, combine 函数必须返回引用类型。
- 我们无须使用隐式的 this 指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
定义类相关的非成员函数
- 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内
定义 read 和 print 函数
- print 函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行
std::istream &read(std::istream &is, Sales_data &item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
定义 add 函数
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
构造函数
- 类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数
- 构造函数不能被声明成 const 的
合成的默认构造函数
- 类通过一个特殊的构造函数来控制默认初始化过程。这个函数叫做默认构造函数
- 如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,称为合成的默认构造函数
如果存在类内的初始值,用它来初始化成员
否则,默认初始化该成员
某些类不能依赖于合成的默认构造函数
- 编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数
- 如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的
- 有时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
定义 Sales_data 的构造函数
对于我们的 Sales_data 类来说,我们将使用下面的参数定义 4 个不同的构造函数
- 一个 istream&,从中读取一条交易信息
- 一个 const string&,表示 ISBN 编号;一个 unsigned,表示售出的图书数量;以及一个 double,表示图书的售出价格
- 一个 const string&,表示 ISBN 编号;编译器将赋予其他成员默认值
- 一个空参数列表(即默认构造函数),正如刚刚介绍的,既然我们已经定义了其他构造函数,那么也必须定义一个默认构造函数
struct Sales_data {
// 新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data(std::istream &);
// 新成员:关于 Sales_data 对象的操作
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
// 数据成员
std::string bookNo; // ISBN 编号
unsigned units_sold = 0; // 销量
double revenue = 0.0; // 总销售收入
};
- 如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成的构造函数。
构造函数初始值列表
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
冒号到花括号之间的部分称为构造函数初始值列表
当某个数据成员被构造函数初始值列表忽略时,他将以与合成默认构造函数相同的方式隐式初始化
// 等价于
Sales_data(const std::string &s) : bookNo(s), units_sold(0), revenue(0) { }
在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is) {
read(is, *this);
}
拷贝、赋值和析构
如果我们不主动定义这些操作,则编译器将替我们合成它们
total = trans;
// 它的行为与下面的代码相同
// Sales_data 的默认赋值操作等价于
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold;
total.revenue = trans.revenue;
某些类不能依赖于合成的版本
当类需要分配类对象之外的资源时,合成的版本常常会失效
完整的 Sales_data 类
#pragma once
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <iostream>
#include <string>
struct Sales_data;
std::istream &read(std::istream&, Sales_data&);
struct Sales_data {
// 新增的构造函数
Sales_data() = default;
//Sales_data() : bookNo(" "), units_sold(0), revenue(0) { }
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data(std::istream &is) { read(is, *this); }
// 新成员:关于 Sales_data 对象的操作
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
double avg_price() const;
// 数据成员
std::string bookNo; // ISBN 编号
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&);
double Sales_data::avg_price() const {
if (units_sold) return revenue / units_sold;
else return 0;
}
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
std::istream &read(std::istream &is, Sales_data &item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
std::ostream &print(std::ostream &os, const Sales_data &item) {
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
//Sales_data::Sales_data(std::istream &is) {
// read(is, *this);
//}
#endif
访问控制与封装
访问说明符
- 定义在 public 说明符之后的成员在整个程序内可被访问,public 成员定义类的接口
- 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了(即隐藏了)类的实现细节
使用 class 或 struct 关键字
- 唯一的一点区别是,struct 和 class 的默认访问权限不太一样
struct:定义在第一个访问说明符之前的成员是 public 的
class:定义在第一个访问说明符之前的成员是 private 的
友元
- 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成文它的友元。如果类想把一个函数作为它的友元,只需要增加一条以 friend 关键字开始的函数声明语句即可。
class Sales_data {
// 为 Sales_data 的非成员函数所作的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
public:
// 新增的构造函数
Sales_data() = default;
//Sales_data() : bookNo(" "), units_sold(0), revenue(0) { }
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data(std::istream &is) { read(is, *this); }
// 新成员:关于 Sales_data 对象的操作
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data&);
private:
double avg_price() const;
// 数据成员
std::string bookNo; // ISBN 编号
unsigned units_sold = 0; // 销量
double revenue = 0.0; // 总销售收入
};
- 友元不是类的成员也不受它所在区域访问控制级别的约束
- 一般来说,最好在类定义开始或结束前的位置集中声明友元
友元的声明
- 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。
类的其他特性
类成员再探
定义一个类型成员
class Screen {
public:
typedef std::string::size_type pos; // 在 public 部分定义 pos,这样用户就可以使用这个名字
//using pos = std::string::size_type;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
Screen 类的成员函数
class Screen {
public:
typedef std::string::size_type pos; // 在 public 部分定义 pos,这样用户就可以使用这个名字
//using pos = std::string::size_type;
Screen() = default;
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
char get() const { // 读取光标处的字符
return contents[cursor]; // 隐式内联
}
inline char get(pos ht, pos wd) const; // 显式内联
Screen &move(pos r, pos c); // 能在之后被设为内联
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};
令成员作为内联函数
- 定义在类内部的成员函数是自动 inline 的
- 我们可以在类的内部把 inline 作为声明的一部分显式地声明成员函数,同样的,也能在类的外部用 inline 关键字修饰函数的定义
inline
Screen &Screen::move(pos r, pos c) {
pos row = r * width;
cursor = row + c;
return *this;
}
char Screen::get(pos r, pos c) const {
pos row = r * width;
return contents[row + c];
}
重载成员函数
- 同非成员函数重载
// 例:
Screen myscreen;
char ch = myscreen.get(); // 调用 Screen::get()
ch = myscreen.get(0, 0); // 调用 Screen::get(pos, pos)
可变数据成员
可以通过 mutable关键字做到这一点
class Screen {
public:
void some_member() const;
private:
mutable size_t access_ctr; // 即使在一个 const 对象内也能被修改
// 其他成员与之前的版本一致
};
void Screen::some_member() const {
++ access_ctr; // 保存一个计数值,用于记录成员函数被调用的次数
// 该成员需要完成的其他工作
}
类数据成员的初始化
- 默认情况下,我们希望 Window_mgr 类开始时总是拥有一个默认初始化的 Screen。在 C++11 新标准中,最好的方式就是把这个默认值声明成一个类内初始值
class Window_mgr {
private:
// 这个 Window_mgr 追踪的 Screen
// 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
- 当我们提供一个类内初始值时,必须以符号 = 或者花括号表示
返回 *this 的成员函数
在类中新加函数
inline Screen &Screen::set(char c) {
contents[cursor] = c; // 设置当前光标所在位置的新值
return *this; // 将 this 对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch) {
contents[r * width + col] = ch; // 设置给定位置的新值
return *this;
}
和 move 操作一样,我们 set 成员的返回值是调用 set 的对象的引用。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。如果我们把一系列这样的操作连接在一条表达式中的话
// 把光标移动到一个指定的位置,然后设置该位置的字符集
myScreen.move(4, 0).set('#');
这些操作在同一个对象上执行,等价于
myScreen.move(4, 0);
myScreen.set('#');
如果令 move 和 set 返回 Screen 而非 Screen& 则上述语句的行为将大不相同。在此例中等价于
Screen temp = myScreen.move(4, 0);
temp.set('#');
假如当初我们定义的返回类型不是引用,则 move 的返回值将是 *this 的副本,因此调用 set 只能改变临时副本,而不能改变 myScreen 的值
从 const 成员函数返回 *this
- display 是一个 const 成员,返回引用
- 如果令 display 返回一个 const 的引用,则我们不能把 display 嵌入到一组动作的序列中去
- 因为一个 const 成员函数如果以引用的形式返回 *this 那么它的返回类型将是常量引用
Screen myScreen;
// 如果 display 返回常量引用,则调用 set 将引发错误
myScreen.display(cout).set('#');
基于 const 的重载
class Screen {
public:
Screen &display(std::ostream &os) {
do_display(os);
return *this;
}
const Screen &display(std::ostream &os) const {
do_display(os);
return *this;
}
private:
void do_display(std::ostream &os) const {
os << contents;
}
};
当 do_display 完成后,display 函数各自返回解引用 this 所得的对象。在非常量版本中, this 指向一个非常量对象,因此 display 返回一个普通的(非常量)引用,而 const 成员则返回一个常量引用。
当我们在某个对象上调用 display 时,该对象是否是 const 决定了应该调用 display 的哪个版本
Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // 调用非常量版本
blank.display(cout); // 调用常量版本
类类型
- 每个类定义了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。
struct First {
int memi;
int getMem();
};
struct Second {
int memi;
int getMem();
};
First obj1;
Second obj2 = obj1; // 错误:obj1 和 obj2 的类型不同
- 我们可以把类名作为类型的名字使用,从而直接指向类类型。或者,我们也可以把类名跟在关键字 class 或 struct 后面;
Sales_data item1; // 默认初始化 Sales_data 类型的对象
class Sales_data item1; // 一条等价的声明
类的声明
class Screen; // 类的声明
- 前向声明:仅仅声明类而暂时不定义它
- 不完全类型:声明之后,定义之前
- 不完全类型使用情景:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
- 对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。否则,编译器就无法了解这样的对象需要多少存储空间。类似的,类也必须首先被定义,然后才能用引用或者指针访问其成员。
- 一旦一个类的名字出现后,他就被认为是声明过了,因此类允许包含指向它自身类型的引用或指针
class Link_screen {
Screen window;
Link_screen *next;
Link_screen *prev;
};
友元再探
- 类可以把其他的类定义成友元,也可以把其他类的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的
类之间的友元关系
- 例:为 Window_mgr 添加一个名为 clear 的成员,它负责把一个指定的 Screen 的内容都设为空白。
public:
friend class Window_mgr;
};
class Window_mgr {
public:
using ScreenIndex = std::vector<Screen>::size_type;
// 按编号将指定的 Screen 重置为空白
void clear(ScreenIndex);
private:
// 这个 Window_mgr 追踪的 Screen
// 默认情况下,一个 Window_mgr 包含一个标准尺寸的空白 Screen
std::vector<Screen> screens{Screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
Screen &s = screen[i];
s.contents = string(s.height * s.width, ' ');
}
每个类负责控制自己的友元类或友元函数
令成员函数作为友元
- Screen 还可以只为 clear 提供访问权限
class Screen {
// Window_mgr::clear 必须在 Screen 类之前被声明
friend void Window_mgr::clear(ScreenIndex);
// Screen 类的剩余部分
};
函数重载和友元
- 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
// 重载的 storeOn 函数
extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
// storeOn 的 ostream 版本能访问 Screen 对象的私有部分
friend std::ostream& storeOn(std::ostream &, Screen &);
// ...
};
Screen 类把接受 ostream& 的 storeOn 函数声明成它的友元,但是接受 BitMap& 作为参数的版本仍然不能访问 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 的声明在作用域中了
类的作用域
在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。
Screen::pos ht = 24, wd = 80; // 使用 Screen 定义的 pos 类型
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get(); // 访问 scr 对象的 get 成员
c = p -> get(); // 访问 p 所指对象的 get 成员
作用域和定义在类外部的成员
- 一旦遇到了类名,定义的剩余部分就在类的作用域之内了
void Window_mgr::clear(ScreenIndex i) {
Screen &s = screens[i]; // 前面已经说明了在 Window_mgr 作用域内了,因此可以直接使用 ScreenIndex
s.contents = string(s.height * s.width, ' ');
}
增加一个函数addScreen
class Window_mgr {
public:
using ScreenIndex = std::vector<Screen>::size_type;
ScreenIndex addScreen(Screen&);
};
// 想使用 ScreenIndex 也需要指明作用域
Window_mgr::ScreenIndex Window_mgr::addScreen(Screen &s) {
screens.push_back(s);
return screens.size() - 1;
}
名字查找与类的作用域
类的定义分两步处理
- 首先,编译成员的声明
- 直到类全部可见后才编译函数体
- 编译器处理完类中的全部声明后才会处理成员函数的定义
用于类成员声明的名字查找
- 这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找
typedef double Moeny;
string bal;
class Account {
public:
// 编译器只考虑 Account 中在使用 Money 前出现的声明
Money balance() { return bal; } // balance 函数体在整个类可见后才被处理,因此返回名为 bal 的成员
private:
Money bal;
};
类型名要特殊处理
- 在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字
typedef double Moeny;
string bal;
class Account {
public:
Money balance() { return bal; } // 使用外层作用域的 Money
private:
typedef double Money; // 错误:不能重新定义 Money
Money bal;
};
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后
成员定义中的普通块作用域的名字查找
- 首先,在成员函数内查找该名字的声明
- 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑
- 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找
int height;
class Screen {
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height;// 使用的参数名
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
- 可以通过 this 指针强制访问
// 不建议的写法
void Screen::dummy_fcn(pos height) {
cursor = width * this -> height;
cursor = width * Screen::height;
}
// 建议的写法
void Screen::dummy_fcn(pos ht) {
cursor = width * height;
}
类作用域之后,在外围的作用域中查找
- 如果需要外层作用域中的名字,可以显式地通过作用域运算符来进行请求
// 不建议的写法 不要隐藏外层作用域中可能被用到的名字
void Screen::dummy_fcn(pos height) {
cursor = width * ::height; // 全局height
}
在文件中名字的出现处对其进行解析
int height;
class Screen {
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0;
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var) {
// var: 参数
// height: 类的成员
// verify: 全局函数
height = verify(var);
}
- 名字查找的第三步包括了成员函数出现之前的全局作用域。verify 的声明位于 setHeight 的定义之前,因此可以被正常使用。
构造函数再探
构造函数初始值列表
Sales_data::Sales_data(const string &s, unsigned cnt, double price) {
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
- 区别是原来的版本初始化了它的数据成员,而这个版本是对数据成员执行了赋值操作
构造函数的初始值有时必不可少
- 如果成员是 const 或者是引用的话,必须将其初始化。当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
// 错误:ci 和 ri 必须被初始化
ConstRef::ConstRef(int ii) {
i = ii; // 正确
ci = ii; // 错误
ri = ii; // 错误
}
// 正确:显式地初始化引用和 const 成员
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(ii) { }
成员初始化的顺序
- 在构造函数初始值中每个成员只能出现一次
- 构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序,成员的初始化顺序与它们在类定义中的出现顺序一致
class X {
int i;
int j;
public:
// 未定义的,i 在 j 之前被初始化
X(int val) : j(val), i(j) { }
};
默认实参和构造函数
Sales_data(std::string s = "") : bookNo(s) { }
委托构造函数
- 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程
class Sales_data {
public:
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
Sales_data() : Sales_data("", 0, 0) { }
Sales_data(std::string s) : Sales_data(s, 0, 0) { }
Sales_data(std::istream &is) : Sales_data() { read(is, *this); }
};
- 当一个构造函数委托给另一个构造函数时,收委托的构造函数的初始值列表和函数体被依次执行。假如受委托的构造函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
默认构造函数的作用
- 当对象被默认初始化或值初始化时自动执行默认构造函数
使用默认构造函数
Sales_data obj; // 正确:obj 是个默认初始化的对象
隐式的类类型转换
- 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
string null_book = "9-999-99999-9";
// 构造一个临时的 Sales_data 对象
// 该对象的 units_sold 和 revenue 等于 0, bookNo 等于 null_book
item.combine(null_book);
只允许一步类型转换
// 错误:需要两步
// 一:"9-999-99999-9" 转换成 string
// 而:string 转换成 Sales_data
item.combine("9-999-99999-9");
// 正确
item.combine(string("9-999-99999-9"));
item.combine(Sales_data("9-999-99999-9"));
类类型转换不是总有效
抑制构造函数定义的隐式转换
- 我们可以通过将构造函数声明为
explicit加以阻止 - 只对一个实参的构造函数有效
- 只能在类内声明构造函数时使用 explicit 在类外部定义时不应重复
explicit Sales_data(const std::string &s) : bookNo(s) { }
explicit Sales_data(std::istream &is) { read(is, *this); }
explicit 构造函数只能用于直接初始化
Sales_data item1(null_book); // 正确:直接初始化
Sales_data item2 = null_book; // 错误:不能将 explicit 构造函数用于拷贝形式的初始化过程
为转换显式地使用构造函数
尽管编译器不会将 explicit 的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显式地强制进行转换
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));
标准库中含有显式构造函数的类
- 接受一个单参数的 const char* 的 string 构造函数不是 explicit 的
- 接受一个容量参数的 vector 构造函数时 explicit 的
聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有 virtual 函数
例:
// 下面的类是一个聚合类
struct Data {
int ival;
string s;
};
// 可以使用 花括号 赋值,顺序不能变,如果初始化列表中的元素个数少于类的成员数量,则靠后的成员被值初始化
Data val1 = {0, "Anna"};
明显缺点
- 要求类的所有成员都是 public 的
- 将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。因为用户很容易忘掉某个初始值,或者提供一个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错
- 添加或删除一个成员之后,所有的初始化语句都需要更新
字面值常量类
- 数据成员都是字面值类型的聚合类是字面值常量类
或者
- 数据成员必须是字面值类型
- 类必须至少含有一个 constexpr 构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的 constexpr 构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr 构造函数
- 尽管构造函数不能是 const 的,但是字面值常量类的构造函数可以是 constexpr 的
- constexpr 构造函数必须初始化所有数据成员,初始值或者使用 constexpr 构造函数,或者是一条常量表达式
#pragma once
#ifndef DEBUG_H
#define DEBUG_H
#include <iostream>
class Debug {
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) { }
constexpr bool any() const { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) {hw = b; }
private:
bool hw; // 硬件错误,而非 IO 错误
bool io; // IO 错误
bool other; // 其他错误
};
#endif
- constexpr 构造函数用于生成 constexpr 对象以及 constexpr 函数的参数或返回类型
constexpr Debug io_sub(false, true, false);
if (io_sub.any()) {
cerr << "print appropriate error messages" << endl;
}
constexpr Debug prod(false);
if (prod.any()) {
cerr << "print an error message" << endl;
}
类的静态成员
声明静态成员
- 通过在成员得声明之前加上关键字
static使得其与类关联到一起。
举例:定义一个类表示银行的账户记录
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
类的静态成员存在于任何对象之外,对象中不包含任何于静态数据成员有关的数据。因此,每个 Account 对象将包含两个数据成员;owner 和 amount。只存在一个 interestRate 对象,而且它被所有 Account 对象共享。
类似的,静态成员函数也不与任何对象绑定在一起,它们不包含 this 指针。作为结果,静态成员函数不能声明成 const 的,而且我们也不能在 static 函数体内使用 this 指针。这一限制既适用于 this 的显式使用,也对调用非静态成员的隐式使用有效。
使用类的静态成员
- 我们使用作用域运算符直接访问静态成员
double r;
r = Account::rate();
- 虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2 -> rate();
- 成员函数不用通过作用域运算符就能直接使用静态成员
class Account {
public:
void calculate() { amount += amount * interestRate; }
private:
static double interestRate;
};
定义静态成员
- 和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。static 关键字则只出现在类内部的声明语句中。
void Account::rate(double newRate) {
interestRate = newRate;
}
- 必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次
- 类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中
double Account::interestRate = initRate();
要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
静态成员的类内初始化
- 我们可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型 constexpr
class Account {
public:
static double rate() { return interestRate; }
static void rate(double);
private:
static constexpr int period = 30; // period 是常量表达式
double daily_tbl[period];
};
- 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员
constexpr int Account::period; // 初始值在类的内部提供
静态成员能用于某些场景,而普通成员不能
- 静态数据成员可以是不完全类型
- 静态数据成员的类型可以就是它所属的类型
class Bar {
public:
// ...
private:
static Bar mem1; // 正确:静态成员可以是不完全类型
Bar *mem2; // 正确:指针成员可以是不完全类型
Bar mem3; // 错误:数据成员必须是完全类型
};
- 可以使用静态成员作为默认实参
class Screen {
public:
// bkground 表示一个在类中稍后定义的静态成员
Screen& clear(char = bkground);
private:
static const char bkground;
};
非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终将引发错误

浙公网安备 33010602011771号