1. 基于 const的重载
为了解决这个问题,我们必须定义两个display 操作:一个是const,另一个不是const。基于成员函数是否为const,可以重载一个成员函数;
同样地,基于一个指针形参是否指向const(第7.8.4 节),可以重载一个函数。const对象只能使用const 成员。
非const 对象可以使用任一成员,但非const 版本是一个更好的匹配。 在此,我们将定义一个名为do_display 的private 成员来打印Screen。
每个display 操作都将调用此函数,然后返回调用自己的那个对象:
class Screen { public: // interface memberfunctions // display overloadedon whether the object is const or not Screen&display(std::ostream &os) { do_display(os);return *this; } const Screen&display(std::ostream &os) const { do_display(os);return *this; } private: // single function todo the work of displaying a Screen, // will be called bythe display operations voiddo_display(std::ostream &os) const { os <<contents; } // as before }; // 现在,当我们将display 嵌入到一个长表达式中时,将调用非const 版本。 // 当我们display 一个const 对象时,就调用const 版本: Screen myScreen(5,3); const Screen blank(5,3); myScreen.set('#').display(cout); // calls nonconst version blank.display(cout); // calls constversion
2. 可变数据成员(mutable data member)
可变数据成员永远都不能为const,甚至当它是 const 对象的成员时也如此。因此,const成员函数可以改变mutable 成员。
要将数据成员声明为可变的,必须将关键字mutable 放在成员声明之前:
class Screen { public: // interface memberfunctions private: mutable size_t access_ctr; // may change in a const members // other data membersas before };
我们给Screen 添加了一个新的可变数据成员access_ctr。使用 access_ctr 来跟踪调用 Screen 成员函数的频繁程度:
voidScreen::do_display(std::ostream& os) const { ++access_ctr; // keepcount of calls to any member function os << contents; }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
尽管do_display 是const,它也可以增加access_ctr。该成员是可变成员,所以,任意成员函数,包括const 函数,都可以改变access_ctr 的值。
3. 函数返回类型不一定在类作用域中
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,
则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。例如,考虑get_cursor 函数:
class Screen { public: typedef std::string::size_typeindex; index get_cursor()const; }; inline Screen::indexScreen::get_cursor() const { return cursor; }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
该函数的返回类型是index,这是在Screen 类内部定义的一个类型名。如果在类定义体之外定义get_cursor,则在函数名被处理之前,
代码在不在类作 用域内。当看到返回类型时,其名字是在类作用域之外使用。
必须用完全限定的类型名Screen::index 来指定所需要的index 是在类Screen 中定义的名字。
4. 构造函数初始化
因为内置类型的成员不进行隐式初始化,所以对这些成员是进行初始化还是赋值似乎都无关紧要。除了两个例外,
对非类类型的数据成员进行赋值或使用初始化式在结果和性能上都是等价的。例如,下面的构造函数是错误的:
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; // no explicitconstructor initializer: error ri is uninitialized ConstRef::ConstRef(int ii) { // assignments: i = ii; // ok ci = ii; // error:cannot assign to a const ri = i; // assigns tori which was not bound to an object }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
记住,可以初始化const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,
要完成初始化。初始化const 或引用类型数据成员的唯一机会是构造函数初始化列表中。编写该构造函数的正确方式为
// ok: explicitlyinitialize reference and const members ConstRef::ConstRef(int ii): i(ii), ci(i),ri(ii) { }
5. 成员初始化的次序
class X { int i; int j; public: // run-time error: iis initialized before j X(int val): j(val),i(j) { } };
6. 默认实参与构造函数
再来看看默认构造函数和接受一个string 的构造函数的定义:
Sales_item(conststd::string &book): isbn(book),units_sold(0), revenue(0.0) { } Sales_item():units_sold(0), revenue(0.0) { }
这两个构造函数几乎是相同的:唯一的区别在于,接受一个string 形参的构造函数使用该形参来初始化isbn。
默认构造函数(隐式地)使用string 的默认构造函数来初始化isbn。 可以通过为string 初始化式提供一个默认实参将这些构造函数组合起来:
class Sales_item { public: // default argumentfor book is the empty string Sales_item(conststd::string &book = ""): isbn(book),units_sold(0), revenue(0.0) { } Sales_item(std::istream &is); // as before };<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
在这里,我们只定义了两个构造函数,其中一个为其形参提供一个默认实参。对于下面的任一定义,将执行为其string 形参接受默认实参的那个构造函数:
Sales_item empty; Sales_itemPrimer_3rd_Ed("0-201-82470-1");<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
在empty 的情况下,使用默认实参,而Primer_3rd_ed 提供了一个显式实参。类的两个版本提供同一接口:给定一个string 或不给定初始化式,它们都将一个Sales_item 初始化为相同的值。
我们更喜欢使用默认实参,因为它减少代码重复。
只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
7.类通常应定义一个默认构造函数
在某些情况下,默认构造函数是由编译器隐式应用的。如果类没有默认构造函数,则该类就不能用在这些环境中。
为了例示需要默认构造函数的情况,假定有一个NoDefault 类,它没有定义自己的默认构造函数,
却有一个接受一个 string 实参的构造函数。因为该类定义了一个构造函数,因此编译器将不合成默认构造函数。
NoDefault 没有默认构造函数,意味着:
1. 具有NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的string 值给NoDefault 构造函数来显式地初始化NoDefault 成员。
2. 编译器将不会为具有NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,
并且默认构造函数必须显式地初始化其NoDefault 成员。
3. NoDefault 类型不能用作动态分配数组的元素类型。
4. NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
5. 如果有一个保存NoDefault 对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
8. 显式初始化类类型对象的成员有三个重大的缺点。
1. 要求类的全体数据成员都是public。
2. 将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
3. 如果增加或删除一个成员,必须找到所有的初始化并正确更新。
定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运行那个构造函数,以保证每个类对象在初次使用之前正确地初始化。
9. 友元
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字friend 开始。
它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。
将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
10. 类的 static 成员的优点
使用static 成员而不是全局对象有三个优点。
1. static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
2. 可以实施封装。static成员可以是私有成员,而全局对象不可以。
3. 通过阅读程序容易看出static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。
11. static 数据成员
static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
保证对象正好定义一次的最好办法,就是将static 数据成员的定义放在包含类非内联成员函数定义的文件中。
定义static 数据成员的方式与定义其他类成员和变量的方式相同:先指定类型名,接着是成员的完全限定名。
12. static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。
通过使用非static 成员显式或隐式地引用this 是一个编译时错误。
因为static 成员不是任何对象的组成部分,所以static 成员函数不能被声明为const。毕竟,将成员函数声明为const 就是承诺不会修改该函数所属的对象。
最后,static成员函数也不能被声明为虚函数。
static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
13. static 成员不是类对象的组成部分
普通成员都是给定类的每个对象的组成部分。static成员独立于任何对象而存在,不是类类型对象的组成部分。
因为static 数据成员不是任何对象的组成部分,所以它们的使用方式对于非static 数据成员而言是不合法的。
例如,static数据成员的类型可以是该成员所属的类类型。非static 成员被限定声明为其自身类对象的指针或引用:
class Bar { public: // ... private: static Bar mem1; //ok Bar *mem2; // ok Bar mem3; // error };类似地,static数据成员可用作默认实参:
class Screen { public: // bkground refers tothe static member // declared later inthe class definition Screen&clear(char = bkground); private: static const charbkground = '#'; };
非static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。使用非static 数据成员作默认实参,
将无法提供对象以获取该成员的值,因而是错误的。
14. 小结
类是C++ 中最基本的特征,允许定义新的类型以适应应用程序的需要,同时使程序更短且更易于修改。
数据抽象是指定义数据和函数成员的能力,而封装是指从常规访问中保护类成员的能力,它们都是类的基础。成员函数定义类的接口。
通过将类的实现所用到的数据和函数设置为private 来封装类。
类可以定义构造函数,它们是特殊的成员函数,控制如何初始化类的对象。
可以重载构造函数。每个构造函数就初始化每个数据成员。初始化列表包含的是名—值对,其中的名是一个成员,而值则是该成员的初始值。
类可以将对其非public 成员的访问权授予其他类或函数,并通过将其他的类或函数设为友元来授予其访问权。
类也可以定义mutable 或static 成员。mutable成员永远都不能为 const;它的值可以在const 成员函数中修改。static 成员可以是函数或数据,独立于类类型的对象而存在。