3.1 Some Tips
使用类的一些小贴士。
- 类定义一定要用分号结束,这个纸笔手写代码是容易错误。
- 使用类型别名来简化类。例如:
class Screen { public: // interface member functions typedef std::string::size_type index; private: std::string contents; index cursor; index height, width; };
- const 成员不能改变其所操作的对象的数据成员。const 必须同时出现在声明和定义中,若只出现在其中一处,就会出现一个编译时错误。用class关键字定义的类,默认的访问标号为private。
- 使用类成员的集中方法,例如:
Class obj; // Class is some class type Class *ptr = &obj; // member is a data member of that class ptr->member; // fetches member from the object to which ptr points obj.member; // fetches member from the object named obj // memfcn is a function member of that class ptr->memfcn(); // runs memfcn on the object to which ptr points obj.memfcn(); // runs memfcn on the object named obj
- 一旦一个名字被作为类型名,那么该名字就不能被重复定义。
3.2 this指针
何时使用this指针
有一种情况下必须这样做:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
我们可以看如下的一个例子:
class Screen {
public:
// interface member functions
Screen& move(index r, index c);
Screen& set(char);
Screen& set(index, index, char);
// other members as before
};
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
Screen& Screen::move(index r, index c)
{
index row = r * width; // row location
cursor = row + c;
return *this;
}
有时,我们在调用函数的时候,希望能够将这些操作的序列连接成一个单独的表达式。
// move cursor to given position,and set that character
myScreen.move(4,0).set('#');
this指针与const
在普通的非 const 成员函数中,this 的类型是一个
指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个
指向 const 类类型对象的 const 指针。
既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。
但是,有时候,我们需要类似如下的一种应用:
// move cursor to given position, set that character and display the screen
myScreen.move(4,0).set('#').display(cout);
其中,display 操作在给定的 ostream 上打印 contents。逻辑上,这个操作应该是一个 const 成员。打印 contents 不会改变对象。如果将 display 作为 Screen 的 const 成员,则 display 内部的 this 指针将是一个 const Screen* 型的 const。这个用法暗示了 display 应该返回一个 Screen 引用,并接受一个 ostream 引用。
如果 display 是一个 const 成员,则它的返回类型必须是 const Screen&,但下面的代码将是非法的:
// move cursor to given position, set that character and display the screen
myScreen.move(4,0).set('#').display(cout);
基于const的重载
为了解决这个问题,我们必须定义两个 display 操作:一个是 const,另一个不是 const。基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const,可以重载一个函数。const 对象只能使用 const 成员。非 const 对象可以使用任一成员,但非 const 版本是一个更好的匹配。
在此,我们将定义一个名为 do_display 的 private 成员来打印 Screen。每个 display 操作都将调用此函数,然后返回调用自己的那个对象:
class Screen {
public:
// interface member functions
// display overloaded on 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 to do the work of displaying a Screen,
// will be called by the display operations
void do_display(std::ostream &os) const
{ os << contents; }
// as before
};
这样,编译器便可以自主选择何时的重载版本。
可变的数据成员
有时(但不是很经常),我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。
可变数据成员(mutable data member)永远都不能为 const,甚至当它是 const 对象的成员时也如此。因此,const 成员函数可以改变 mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前:
class Screen {
public:
// interface member functions
private:
mutable size_t access_ctr; // may change in a const members
// other data members as before
};
这样,任意函数,包括const函数都可以改变acces_ptr的值。
3.3 类的构造函数
3.3.1 构造函数初始化式
构造函数的初始化式是形如:
// recommended way to write constructors using a constructor initializer
Sales_item::Sales_item(const string &book):
isbn(book), units_sold(0), revenue(0.0) { }
的形式。以一个:开始,接着是各个数据成员列表,用逗号分隔。可能我们会认为,这样的写法和构造函数中直接对变量进行初始化相同,但事实上不然。
如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。比如:
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
// no explicit constructor initializer: error ri is uninitialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // assigns to ri which was not bound to an object
}
可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中:
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
同样,初始化式可以是任意表达式,甚至可以以类类型的数据成员作为初始化式。
3.3.2 默认实参与构造函数
可以为构造函数提供默认的实参:
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
};
为book形参提供了默认的实参。默认实参可以减少代码的长度。
3.3.3 默认的构造函数
只要定义一个对象时没有提供初始化式,就使用默认构造函数。
它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
类通常应当定义一个默认构造函数。如果一个类NoDefault没有默认构造函数,且仅有一个以string为实参的构造函数,就意味着:
- 具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。
- 编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。
- NoDefault 类型不能用作动态分配数组的元素类型。
- NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。
- 如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。