Part I: The Basics
Chapter 7. Classes
7.4 类的作用域
作用域与定义在类外部的成员
函数的返回类型通常出现在函数名字的前面。当一个成员函数定义在类的外部时,返回类型中使用的名字是在类的作用域外部。因此,返回类型必须指明它是哪个类的成员。
class Window_mgr {
public:
// add a Screen to the window and returns its index
ScreenIndex addScreen(const Screen&);
// other members as before
};
// return type is seen before we're in the scope of Window_mgr
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s){
screens.push_back(s);
return screens.size() - 1;
}
名字查找与类的作用域
名字查找 (name lookup) —— 查找与所用名字匹配的过程:
- 首先,在使用的名字的所在块中寻找这个名字的声明。只考虑在名字使用之前的声明。
- 如果名字没有找到,查找外层作用域。
- 如果没有找到声明,程序报错。
定义在类内的成员函数中的名字的解析方式,与上面的查找规则有所不同。类定义分两步处理:
- 首先,编译成员的声明。
- 直到整个类可见之后,才编译函数体。
类型名要特殊处理
一般来说,内层作用域可以重新定义外层作用域的名字,即使该名字已在内层作用域中使用过。然而,在类中,如果一个成员使用外层作用域的名字,且这个名字是一个类型,那么该类随后不能重新定义该名字:
typedef double Money;
class Account {
public:
Money balance() { return bal; } // uses Money from the outer scope
private:
typedef double Money; // error: cannot redefine Money
Money bal;
// ...
};
建议:类型名的定义通常应该出现在类的开始。这样,任何使用这个类型的成员都出现在类型名定义之后。
成员定义中的普通块作用域的名字查找
成员函数体内使用的名字按如下方式解析:
- 首先,在成员函数中寻找名字的声明。只考虑函数体内名字使用前的声明。
- 如果在成员函数内没有找到声明,在类内寻找声明。考虑类中的所有成员。
- 如果在类中没有没有找到这个名字的声明,在成员函数定义之前的作用域中寻找声明。
7.5 构造函数再探
构造函数初始值列表
如果没有在构造函数初始值列表显式初始化一个成员,那么在构造函数体开始执行之前,该成员默认初始化。
// legal but sloppier way to write the Sales_data constructor: no constructor initializers
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;
};
// error: ci and ri must be initialized
ConstRef::ConstRef(int ii) {
// assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // error: ri was never initialized
}
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
建议:使用构造函数初始值。
在很多类中,初始化和赋值的区别关乎底层效率:前者直接初始化数据成员,后者先初始化后赋值。
成员初始化的顺序
成员初始化的顺序与它们在类定义中出现的顺序一致:第一个成员第一个初始化,然后第二个,依此类推。
在构造函数初始值列表中初始值出现的顺序不会改变初始化的顺序。
class X {
int i;
int j;
public:
// undefined: i is initialized before j
X(int val): j(val), i(j) { }
};
最好将构造函数的初始值的顺序写的与成员声明的顺序一致。如果可能的的话,避免使用成员初始化其他成员。
// In this version, the order in which i and j are initialized doesn’t matter.
X(int val): i(val), j(val) { }
默认实参和构造函数
class Sales_data {
public:
// defines the default constructor as well as one that takes a string argument
Sales_data(std::string s = ""): bookNo(s) { }
// remaining constructors unchanged
Sales_data(std::string s, unsigned cnt, double rev): bookNo(s), units_sold(cnt), revenue(rev*cnt) { }
Sales_data(std::istream &is) { read(is, *this); }
// remaining members as before
};
委托构造函数
C++11标准扩展了构造函数初始值的使用,可以定义委托构造函数。
委托构造函数 (delegating constructor) 使用它所属类的其他构造函数执行它的初始化过程。即是说将自己的一些(或所有)的工作“委托”给其他构造函数。
class Sales_data {
public:
// nondelegating constructor initializes members from corresponding arguments
Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt*price) { }
// remaining constructors all delegate to another constructor
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); }
// other members as before
};
接受 istream& 参数的构造函数是构造函数。它委托默认构造函数,默认构造函数又委托接受三个参数的构造函数。一旦这些构造函数完成它们的工作,istream& 构造函数体开始运行。
当一个构造函数委托另一个构造函数时,受委托的构造函数的构造函数初始值列表和函数体都运行。在 Sales_data 类中,受委托的构造函数的函数体是空的。如果这个函数体包含代码,那么这片代码会先运行,然后才将控制权交还给委托函数的函数体。
默认构造函数的作用
定义一个使用默认构造函数进行初始化的对象:
Sales_data obj(); // oops! declares a function, not an object
Sales_data obj2; // ok: obj2 is an object, not a function
隐式的类类型转换
可以使用单个实参调用的构造函数,定义了从构造函数的形参类型到该类类型的隐式转换。这类构造函数有时被称为转换构造函数 (converting constructors)。
Sales_data 类中接受一个 string 类型和接受一个 istream 类型的构造函数都定义了从这些类型到 Sales_data 类的隐式转换。也就是说,在需要使用 Sales_data 类型对象的地方中,可以使用 string 或 istream 对象作为替代:
string null_book = "9-999-99999-9";
// constructs a temporary Sales_data object
// with units_sold and revenue equal to 0 and bookNo equal to null_book
item.combine(null_book);
只允许一步类类型转换
编译器只会自动执行一步自动类型转换。
// error: requires two user-defined conversions:
// (1) convert "9-999-99999-9" to string
// (2) convert that (temporary) string to Sales_data
item.combine("9-999-99999-9");
如果想要完成上述调用,可以将字符串显式地转换成 string 或 Sales_data 对象:
// ok: explicit conversion to string, implicit conversion to Sales_data
item.combine(string("9-999-99999-9"));
// ok: implicit conversion to string, explicit conversion to Sales_data
item.combine(Sales_data("9-999-99999-9"));
类类型转换不总是有用的
// uses the istream constructor to build an object to pass to combine
item.combine(cin);
上面的隐式转换执行了 Sales_data 中接受 istream 的构造函数。这个构造函数通过读取标准输入创建了一个(临时的)Sales_data 对象。然后将这个对象传递给 combine。
因为这个对象是临时的,所以一旦 combine 完成,就无法访问它了。
抑制构造函数的隐式转换
将构造函数声明为 explicit
可以阻止这个构造函数的隐式转换:
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) { }
explicit Sales_data(const std::string &s): bookNo(s) { }
explicit Sales_data(std::istream&);
// remaining members as before
};
item.combine(null_book); // error: string constructor is explicit
item.combine(cin); // error: istream constructor is explicit
explicit
关键字只对可以被单个实参调用的构造函数有意义。
explicit
关键字只能使用在类内的构造函数声明中。不能在类外部的定义中重复。
// error: explicit allowed only on a constructor declaration in a class header
explicit Sales_data::Sales_data(istream& is) {
read(is, *this);
}
explicit 构造函数只能用于直接初始化
隐式转换发生的一种情况是使用拷贝形式的初始化(使用 =
)。
Sales_data item1 (null_book); // ok: direct initialization
// error: cannot use the copy form of initialization with an explicit constructor
Sales_data item2 = null_book;
显式使用构造函数进行转换
// ok: the argument is an explicitly constructed Sales_data object
item.combine(Sales_data(null_book));
// ok: static_cast can use an explicit constructor
item.combine(static_cast<Sales_data>(cin));
第一个调用直接使用构造函数。
第二个调用使用 static_cast 执行显式转换。static_cast 使用 istream 构造函数创建了一个临时的 Sales_data 对象。
使用 explicit 构造函数的标准库类
- 接受单个 const char* 类型的形参的 string 构造函数不是 explicit。
- 接受一个容量参数的 vector 构造函数是 explicit。
聚合类
聚合类 (aggregate class) 给用户直接访问其成员的权限,具有特殊的初始化语法方式。聚合类需满足下述条件:
- 所有数据成员是 public。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类或 virtual 函数。
struct Data {
int ival;
string s;
};
可以通过提供用花括号括起来的成员初始值列表,来初始化聚合类的数据成员:
// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };
初始值的顺序必须与声明中的数据成员的顺序一致。
// error: can't use "Anna" to initialize ival, or 1024 to initialize s
Data val2 = { "Anna", 1024 };
与数组元素的初始化类似,如果初始值列表中的元素个数比类中的成员数量少,则靠后的成员被值初始化。初始值列表中元素的个数不能超多类中的成员数量。
显式地初始化类类型的对象的成员存在 3 个明显的缺点:
- 要求类的所有成员都是 public。
- 类的用户(而不是类的作者)需要正确初始化每个对象的每个成员,这给类用户带来了负担。
- 如果增加或删除成员,所有的初始化都需要更新。
字面值常量类
数据成员都是字面值常量类型的聚合类是字面值常量类 (literal class)。如果非聚合类满足下面的条件,也是字面值常量类:
- 所有数据成员必须是字面值常量类型。
- 类必须至少有一个 constexpr 构造函数。
- 如果一个数据成员有类内初始值,则内置类型的初始值必须是常量表达式;或者如果成员是类类型,初始值必须使用该成员自己的 constexpr 构造函数。
- 类必须使用析构函数的默认定义。
constexpr 构造函数
尽管构造函数不可能是 const,字面值常量类中的构造函数可以是 constexpr 函数。
constexpr 构造函数可以声明成 = default
(或者声明成删除函数)。否则,constexpr 构造函数必须满足构造函数的要求——意味着它没有 return 语句,且必须满足 constexpr 函数的要求——意味着它拥有的唯一可执行语句是 return 语句。因此,constexpr 构造函数体一般是空的。
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() { 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; // hardware errors other than IO errors
bool io; // IO errors
bool other; // other errors
};
constexpr 构造函数必须初始化每个数据成员,初始值必须使用 constexpr 构造函数或者常量表达式。
constexpr 构造函数用于生成 constexpr 对象,或者用于 constexpr 函数的参数或返回类型:
constexpr Debug io_sub(false, true, false); // debugging IO
if (io_sub.any()) // equivalent to if(true)
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); // no debugging during production
if (prod.any()) // equivalent to if(false)
cerr << "print an error message" << endl;
7.6 static 类成员
类有时需要一些成员与类相关联,而不是与类类型的单个对象相关联。
声明 static 成员
通过在成员的声明中加上关键字 static
将其与类关联起来。
static 成员可以是 public 或 private。
static 数据成员的类型可以是 const、引用、数组、类类型等。
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();
};
类的 static 成员存在于任何对象之外。对象不包含与 static 数据成员相关联的数据。
因此,每个 Account 对象包含 2 个数据成员——owner 和 amount。
类似地,static 成员函数不与任何对象绑定;它们没有 this 指针。
因此,static 成员函数不能声明成 const,也不能在 static 成员函数体内指向 this。这个限制既适用于 this 的显式使用,也适用于调用非静态成员时 this 的隐式使用。
使用类 static 成员
可以通过使用作用域运算符直接访问 static 成员:
double r;
r = Account::rate(); // access a static member using the scope operator
可以使用该类类型的对象、引用或者指针访问 static 成员:
Account ac1;
Account *ac2 = &ac1;
// equivalent ways to call the static member rate function
r = ac1.rate(); // through an Account object or reference
r = ac2->rate(); // through a pointer to an Account object
成员函数可以直接使用 static 成员,不用通过作用域运算符:
class Account {
public:
void calculate() { amount += amount * interestRate; }
private:
static double interestRate;
// remaining members as before
};
定义 static 成员
可以在类内或类外定义 static 成员函数。当在类的外部定义 static 成员时,不需要重复 static 关键字。这个关键字只出现在类内的声明中。
void Account::rate(double newRate) {
interestRate = newRate;
}
因为 static 数据成员不属于类类型对象的一部分,所以它们不是在创建类对象时定义的。因此,它们不是由类的构造函数进行初始化。而且,一般来说,不能在类内初始化 static 成员。
必须在类的外部定义和初始化每个 static 数据成员。一个 static 数据成员只能定义一次。
类似于全局变量,static 数据成员定义在任何函数之外。因此,一旦它们被定义,它们一直存在直到程序完成。
// define and initialize a static class member
double Account::interestRate = initRate();
与其他成员的定义一样,interestRate 也可以访问类的 private 成员。
Tip: 确保对象只定义一次的最佳方法是,将 static 数据成员的定义与类非内联成员函数的定义放入同一文件中。
static 数据成员的类内初始化
通常,类的 static 成员不能在类内初始化。
然而,可以为具有 const 整型的 static 成员类内初始值,且对于 static 成员,如果它是字面值常量类型的 constexpr,那么必须为它提供类内初始化。初始值必须是常量表达式。这样的成员本身就是常量表达式;它们可以用在需要常量表达式的地方。
class Account {
public:
static double rate() { return interestRate; }
static void rate(double);
private:
static constexpr int period = 30;// period is a constant expression
double daily_tbl[period];
};
如果仅在编译器可以替换成员值的情境中使用成员,则无需单独定义初始化的 const 或 constexpr static。但是,如果在无法替换值的情境中使用该成员,则必须为该成员定义。
例如,如果 period 只是用于定义 daily_tbl 的维度,那么就没有必要在 Account 外部定义 period。但是,如果向接受 const int& 的函数传递 Account::period,那么 period 必须定义。
如果在类内提供了初始值,那么成员的定义不能在指定一个初始值。
// definition of a static member with no initializer
constexpr int Account::period; // initializer provided in the class definition
即使在类主体中初始化了 const static 数据成员,通常也应在类定义之外定义该成员。
static 成员可以使用的方式,而普通成员不能使用
- static 数据成员可以有不完全类型。特别地,static 数据成员的类型可以是它所属的类类型。非static 数据成员则受到限制,只能声明成它所属类的对象的指针或引用。
class Bar {
public:
// ...
private:
static Bar mem1; // ok: static member can have incomplete type
Bar *mem2; // ok: pointer member can have incomplete type
Bar mem3; // error: data members must have complete type
};
- 可以使用 static 成员作为默认实参。非static 数据成员不能用作默认实参,因为它的值是其成员的对象的一部分。使用非static 数据成员作为默认实参不会提供一个对象以便从中获取成员值,因此会出错。
class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground;
};