- 类通常应定义一个默认构造函数
为了例示需要默认构造函数的情况,假定有一个 NoDefault 类,它没有定义自己的默认构造函数,却有一个接受一个 string 实参的构造函数。
定义这样的一个类没有错,这样编译会编译成功:
class NoDefault {
public:
void output() { cout << units_sold << endl; } NoDefault(const std::string &book) : isbn(book), units_sold(0), revenue(0.0) { cout << "first constructor" << endl; } /*NoDefault() : units_sold(0), revenue(0.0) { cout << "second constructor" << endl; }*/ private: std::string isbn; unsigned units_sold; double revenue; }; int main() { system("pause"); return 0; }
但如果定义一个对象就会报错:
因为该类定义了一个构造函数,因此编译器将不合成默认构造函数。NoDefault 没有默认构造函数,这意味着:
(1)具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
如果NoDefault有默认构造函数:
class NoDefault { public: NoDefault(const std::string book) : isbn(book), units_sold(0), revenue(0.0) { cout << "NoDefault Constructor" << endl; } NoDefault() : units_sold(0), revenue(0.0) { cout << "NoDefault default constructor" << endl; } private: std::string isbn; unsigned units_sold; double revenue; }; class Sales_item { public: double avg_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } void output() { cout << units_sold << endl; } Sales_item(const std::string &book) : isbn(book), units_sold(0), revenue(0.0) { cout << "Sales_item constructor" << endl; } Sales_item() : units_sold(0), revenue(0.0) { cout << "Sales_item default constructor" << endl; } private: std::string isbn; NoDefault ND; unsigned units_sold; double revenue; }; int main() { Sales_item si; system("pause"); return 0; }
输出结果:
如果NoDefault没有默认构造函数:
输出结果:
(2)编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员(如上例)指望编译器为Sales_item合成默认构造函数会报错:
class NoDefault { public: NoDefault(const std::string book) : isbn(book), units_sold(0), revenue(0.0) { cout << "NoDefault Constructor" << endl; } private: std::string isbn; unsigned units_sold; double revenue; }; class Sales_item { public: double avg_price() const; bool same_isbn(const Sales_item &rhs) const { return isbn == rhs.isbn; } void output() { cout << units_sold << endl; } private: std::string isbn; NoDefault ND; unsigned units_sold; double revenue; }; int main() { Sales_item si; system("pause"); return 0; }
报错:
如果没有NoDefault作为Sales_item的成员,则会编译成功。
(3)NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式
输出结果:
如果NoDefault没有默认构造函数,则会报错:
这种数组也无法给每个NoDefault类型的成员传递初始值。
(4)NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
否则仍然会报错:
(5)如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
使用默认构造函数定义一个对象的正确方法:
// ok: defines a class object ... Sales_item myobj;
如果定义成这样:
Sales_item myobj(); // ok: but defines a function, not an object
myobj 的定义被编译器解释为一个函数的声明,该函数不接受参数并返回一个 Sales_item 类型的对象。
这样定义也是正确的:
// ok: create an unnamed, empty Sales_itemand use to initialize myobj Sales_item myobj = Sales_item();
我们创建并初始化一个 Sales_item 对象,然后用它来按值初始化 myobj。编译器通过运行 Sales_item 的默认构造函数来按值初始化一个 Sales_item。
- 隐式类类型转换
C++ 语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型。为了定义到类类型的隐式转换,需要定义合适的构造函数。可以用单个实参来调用的构造函数,定义了从形参类型到该类类型的一个隐式转换。
让我们再看看定义了两个构造函数的 Sales_item 版本:
class Sales_item { public: // default argument for book is the empty string Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(0.0) { } Sales_item(std::istream &is); // as before };
这个类中有一个成员函数
bool same_isbn(const Sales_item &rhs) const//const 成员不能改变其所操作的对象的数据成员。 { return isbn == rhs.isbn; }
当像下面这样使用这个成员函数时,这里的每个构造函数都定义了一个隐式转换:
string null_book = "9-999-99999-9"; // ok: builds a Sales_itemwith 0 units_soldand revenue from // and isbn equal to null_book item.same_isbn(null_book);
因此,在期待一个 Sales_item 类型对象的地方,可以使用一个 string 或一个 istream。
如果没有这两个构造函数,就会报错:
这段程序使用一个 string 类型对象作为实参传给 Sales_item 的 same_isbn 函数。该函数期待一个 Sales_item 对象作为实参。编译器使用接受一个 string 的 Sales_item 构造函数从 null_book 生成一个新的 Sales_item 对象。新生成的(临时的)Sales_item 被传递给 same_isbn。这个行为是否我们想要的,依赖于我们认为用户将如何使用这个转换。在这种情况下,它可能是一个好主意。book 中的 string 可能代表一个不存在的 ISBN,对 same_isbn 的调用可以检测 item 中的 Sales_item 是否表示一个空的 Sales_item。另一方面,用户也许在 null_book 上错误地调用了 same_isbn。
更成问题的是从 istream 到 Sales_item 的转换:
// ok: uses the Sales_item istream constructor to build an object // to pass to same_isbn item.same_isbn(cin);
这段代码将 cin 隐式转换为 Sales_item。这个转换执行接受一个 istream 的 Sales_item 构造函数。该构造函数通过读标准输入来创建一个(临时的)Sales_item 对象。然后该对象被传递给 same_isbn。
这个 Sales_item 对象是一个临时对象。一旦 same_isbn 结束,就不能再访问它。实际上,我们构造了一个在测试完成后被丢弃的对象。这个行为几乎肯定是一个错误。
- 抑制由构造函数定义的隐式转换
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:
class Sales_item { public: // default argument for book is the empty string explicit Sales_item(const std::string &book = ""): isbn(book), units_sold(0), revenue(0.0) { } explicit Sales_item(std::istream &is); // as before };
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。现在,两个构造函数都不能用于隐式地创建对象。前两个使用都不能编译:
item.same_isbn(null_book); // error: string constructor is explicit item.same_isbn(cin); // error: istream constructor is explicit
当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符。
- 为转换而显示的使用构造函数
只要显式地按下面这样做,就可以用显式的构造函数来生成转换:
string null_book = "9-999-99999-9"; // ok: builds a Sales_itemwith 0 units_soldand revenue from // and isbn equal to null_book item.same_isbn(Sales_item(null_book));
在这段代码中,从 null_book 创建一个 Sales_item。尽管构造函数为显式的,但这个用法是允许的。显式使用构造函数只是中止了隐式地使用构造函数。任何构造函数都可以用来显式地创建临时对象。
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
- 类成员的显示初始化
尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:
struct Data { int ival; char *ptr; }; // val1.ival = 0; val1.ptr = 0 Data val1 = { 0, 0 }; // val2.ival = 1024; // val2.ptr = "Anna Livia Plurabelle" Data val2 = { 1024, "Anna Livia Plurabelle" };
根据数据成员的声明次序来使用初始化式。例如,因为 ival 在 ptr 之前声明,所以下面的用法是错误的:
// error: can't use "Anna Livia Plurabelle" to initialize the int ival Data val2 = { "Anna Livia Plurabelle" , 1024 };
这种形式的初始化从 C 继承而来,支持与 C 程序兼容。显式初始化类类型对象的成员有三个重大的缺点:要求类的全体数据成员都是 public;将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式;如果增加或删除一个成员,必须找到所有的初始化并正确更新。
- 友元
在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问。
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方(只是声明谁是这个类的友元而已):友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。
想像一下,除了 Screen 类之外,还有一个窗口管理器,管理给定显示器上的一组 Screen。窗口管理类在逻辑上可能需要访问由其管理的 Screen 对象的内部数据。假定 Window_Mgr 是该窗口管理类的名字,Screen 应该允许 Window_Mgr 像下面这样访问其成员:
class Screen { // Window_Mgr members can access private parts of class Screen friend class Window_Mgr;//这里的意思是,类Window_Mgr是类Screen 的友元,而并非是Screen 的类类型的成员 // ...restofthe Screen class };
Window_Mgr 的成员可以直接引用 Screen 的私有成员。例如,Window_Mgr 可以有一个函数来重定位一个 Screen:
Window_Mgr& Window_Mgr::relocate(Screen::index r, Screen::index c, Screen& s) { // ok to refer to height and width s.height += r; s.width += c; return *this; }
缺少友元声明时,这段代码将会出错:将不允许使用形参 s 的 height 和 width 成员。因为 Screen 将友元关系授予 Window_Mgr,所以,Window_Mgr 中的函数都可以访问 Screen 的所有成员。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
- 使用其它类的成员函数成为友元
如果不是将整个 Window_Mgr 类设为友元,Screen 就可以指定只允许 relocate 成员(Window_Mgr类的一个成员函数)访问:
class Screen { // Window_Mgrmust be defined before class Screen friend Window_Mgr& Window_Mgr::relocate(Window_Mgr::index, Window_Mgr::index, Screen&); // ...restofthe Screen class };
当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
- 友元声明与作用域
为了正确地构造类,需要注意友元声明与友元定义之间的互相依赖。在前面的例子中,类 Window_Mgr 必须先定义。否则,Screen 类就不能将一个 Window_Mgr 函数指定为友元。然而,只有在定义类 Screen 之后,才能定义 relocate 函数——毕竟,它被设为友元是为了访问类 Screen 的成员。
更一般地讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。友元声明将已命名的类或非成员函数引入到外围作用域中。此外,友元函数可以在类的内部定义,该函数的作用域扩展到包围该类定义的作用域。
用友元引入的类名和函数(定义或声明),可以像预先声明的一样使用:
class X { friend class Y; friend void f() { /* ok to define friend function in the class body */ } }; class Z { Y *ymem; // ok: declaration for class Y introduced by friend in X void g() { return ::f(); } // ok: declaration of f introduced by X };
- 重载函数与友元
// overloaded storeOn functions extern std::ostream& storeOn(std::ostream &, Screen &); extern BitMap& storeOn(BitMap &, Screen &); class Screen { // ostream version of storeOn may access private parts of Screen objects friend std::ostream& storeOn(std::ostream &, Screen &); // ... };
类 Screen 将接受一个 ostream& 的 storeOn 版本设为自己的友元。接受一个 BitMap& 的版本对 Screen 没有特殊访问权。
- static类成员
对于特定类类型的全体对象而言,访问一个全局对象有时是必要的。也许,在程序的任意点需要统计已创建的特定类类型对象的数量;或者,全局对象可能是指向类的错误处理例程的一个指针;或者,它是指向类类型对象的内在自由存储区的一个指针。然而,全局对象会破坏封装:对象需要支持特定类抽象的实现。如果对象是全局的,一般的用户代码就可以修改这个值。类可以定义类 静态成员,而不是定义一个可普遍访问的全局对象。通常,非 static 数据成员存在于类类型的每个对象中。不像普通的数据成员,static 数据成员独立于该类的任意对象而存在;每个 static 数据成员是与类关联的对象,并不与该类的对象相关联。正如类可以定义共享的 static 数据成员一样,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。
例如,考虑一个简单的表示银行账户的类。每个账户具有余额和拥有者,并且按月获得利息,但应用于每个账户的利率总是相同的。可以按下面的这样编写这个类
class Account { public: // interface functions here void applyint() { amount += amount * interestRate; } static double rate() { return interestRate; } static void rate(double); // sets a new rate private: std::string owner; double amount; static double interestRate; static double initRate(); };
这个类的每个对象具有数据成员:owner 和 amount。对象没有与 static 数据成员对应的数据成员,但是,存在一个单独的 interestRate 对象,由 Account 类型的全体对象共享。
可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。
Account ac1; Account *ac2 = &ac1; // equivalent ways to call the static member rate function double rate; rate = ac1.rate(); // through an Account object or reference rate = ac2->rate(); // through a pointer to an Account object rate = Account::rate(); // directly from the class using the scope operator
像使用其他成员一样,类成员函数可以不用作用域操作符来引用类的 static 成员:
class Account { public: // interface functions here void applyint() { amount += amount * interestRate; } };
Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处:
void Account::rate(double newRate) { interestRate = newRate; }
- static成员函数
提示:static成员没有this指针。因为原本static成员就不是属于某个对象的,而是属于这个类的。static 成员是类的组成部分但不是任何对象的组成部分,因此,static 成员函数没有 this 指针。通过使用非 static 成员显式或隐式地引用 this 是一个编译时错误。
因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const//const函数是用来作为类的成员函数,保证这个成员函数不去修改类对象,而static成员函数并不是属于某个对象的真正的成员函数,所以不存在const。毕竟,将成员函数声明为 const 就是承诺不会修改该函数所属的对象。最后,static 成员函数也不能被声明为虚函数//虚函数在继承中实现多态,而static成员函数是用来保证每个类对象都相同的。
- static数据成员
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中。
可以定义如下 interestRate:
// define and initialize static class member double Account::interestRate = initRate();
这个语句定义名为 interestRate 的 static 对象,它是类 Account 的成员,为 double 型。像其他成员定义一样,一旦成员名出现,static 成员的就是在类作用域中。因此,我们可以没有限定地直接使用名为 initRate 的 static 成员函数,作为 interestRate 初始化式。注意,尽管 initRate 是私有的,我们仍然可以使用该函数来初始化 interestRate。像任意的其他成员定义一样,interestRate 的定义是在类的作用域中,因此可以访问该类的私有成员。
像使用任意的类成员一样,在类定义体外部引用类的 static 成员时,必须指定成员是在哪个类中定义的。然而,static 关键字只能用于类定义体内部的声明中,定义不能标示为 static。
- 特殊的整形const static成员
一般而言,类的 static 成员,像普通数据成员一样,不能在类的定义体中初始化。static 数据成员通常在定义时才初始化。 这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化//因为const成员要在初始化时就赋予初始值:
class Account { public: static double rate() { return interestRate; } static void rate(double); // sets a new rate private: static const int period = 30; // interest posted every 30 days double daily_tbl[period]; // ok: period is constant expression };
const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。//???
在类内部提供初始化式时,成员的定义不必再指定初始值://为什么初始化完还需要定义?
// definition of static member with no initializer; // the initial value is specified inside the class definition const int Account::period;
- 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 to the static member // declared later in the class definition Screen& clear(char = bkground); private: static const char bkground = '#'; };
非 static 数据成员不能用作默认实参,因为它的值不能独立于所属的对象而使用。使用非 static 数据成员作默认实参,将无法为对象提供自身的值,因而是错误的。