【C++ primer】第7章 类 (2)


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;
};

【C++ primer】目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值