C++语言提供了一些内置的类型,如char、int和double。对于一个类型,如果编译器无须借助程序员在源码中提供的任何声明,就知道如何表示这种类型的对象以及可以对它进行什么样的运算,我们就称这种类型是内置的。
非内置的类型称为用户自定义类型(user-defined type , UDT)。用户自定义的类型包括每个ISO标准C++实现都提供给程序员的标准库类型,如string,vector和ostream。
我们为什么要创建自定义类型呢?原因在于编译器不知道我们想在程序中使用的所有类型。它也不可能知道,因为有用的类型实在太多了--没有语言设计者或者编译器开发者能预知所有类型。通过类型,我们可以在代码中直接、有效地表达我们的思想。
类
C++提供了两类用户自定义数据类型:类和枚举。类是到目前为止最常用,也是最重要的概念描述机制,因此我们首先把注意力放在类上。类能够在程序中直接地表达概念。一个类是一个(用户自定义)类型是,它指出这种类型的对象如何表示,如何创建,如何使用,以及如何销毁。在C++中(以及大多数现代程序设计语言中),类是构造大型程序的关键的基本组成部分,对小程序也同样的非常有用。
一个类就是一个用户自定义类型,由一些内置类型和其他用户自定义类型的对象以及一些函数组成
。这些用来定义类的组成部分称为成员(member)。一个类可以有0个或多个成员,例如:
class X{public: int m; //data member int mf(int v) {int old = m; m = v; return old;} //成员函数}
成员可以有许多种类别,大多数要么是数据成员,定义了类对象的表示方法,要么是函数成员,提供类对象之上的运算。类成员的访问使用这种符号:对象.成员。
我们通常把一个类看做一个接口加上一个实现。接口是类声明的一部分,用户可以直接访问它。实现是类声明的另一部分,用户只能通过接口间接访问它。公共的接口用标号public:标识,实现用private:标识。你可以将一个类声明理解为如下形式:
class X{ public: private:}
类成员默认是私有的,也就是说,如下代码所示:
class X{ int mf(int); //...};
等价于
class X{ private: int mf(int); // ...};
因此,下面的代码是错误的:
X x;int y = x.mf();
用户不能直接访问一个私有成员,应通过一个公有函数来访问,例如:
class X{ int m; int mf(int); public: int f(int i) { m = i; return mf(i);}};X x;int y = x.f(2);
我们用私有和公用之间的差别来描述接口(类的用户视图)和实现细节(类的实现者视图)之间的重要区别
。C++提供了一种很有用的简化的功能,可用来描述没有私有实现细节的类。这种语法功能就是结构,一
个结构就是一个成员默认为公用属性的类:
struct X{ int m; // ...};
意味着
class X{ public: int m; // ...};
结构主要用于成员可以取任意值的数据结构,即我们不能定义结构有意义的不变式。
与类同名的成员函数是特殊的成员函数,称为构造函数(constructor),专门用于类对象的初始化("构造
")。如果一个类具有需要参数的构造函数,而程序员忘记利用它初始化类对象,则编译器会捕获这个错
误。C++提供了一种专用的,而且很方便的语法来进行这种初始化,例如:
struct Date {int y,m,d; Date(int y, int m, int d); void add_day(int n);};Date my_birthday; //错误,没有初始化成员。Date today(12,24,2007); //错误,运行时错误。Date last(2000,12,31); //正确。
运算符重载
你可以在类或枚举对象上定义几乎没有C++运算符,这通常称为运算符重载(operator overloading)。这
种机制用于为用户自定义类型提供习惯的符号表示方法。
enum Month {Jan = 1,Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};Month operator++(Month& m){ m = (m == Dec)?Jan:Month(m+1); return m;}
其中?:是“算术if"运算符:当(m == Dec)时m的值变为Jan,否则m的值为Month(m+1)。这是十二月后"绕回"一月这一事实的一种非常简洁的描述方法。我们可以像如下代码一样使用Month类型。
Month m = Sep;++m; //m变为Oct++m; //m变为Nov++m; //m变为Dec++m; //m变为Jan
你可能觉得增加Month对象值这样的操作没那么常用,不至于设计一个专门的运算符。可能确实是这样,
那么输出运算符又如何呢?如下代码定义了一个输出运算符:
vectormonth_tbl;
ostream& operator<<(ostream& os, Month m) { return os << month_tbl[m];}
你可以为自己的类型重新定义几乎没有的C++运算符,如+、-、*、/、%、[]、()、^、!、&、<、<=
、>和>=等。但你不能定义新的运算符。
一个重载的运算符必须作用于至少一个用户自定义类型的运算对象:
int operator+(int ,int); //错误,不能重载内置运算符+vector operator+(const vector&, const vector &); //OKvector operator+=(const vector&, int); //OK
一个一般性的原则是:除非你真正确定重载运算符能大大改善代码,否则不要为你的类型定义运算符。而且,重载运算符应该保持其原有意义:+就应该是加法,二元运算符*就应该表示乘法,[]表示元素访问,()表示调用,等等。这只是建议,并不是C++语言规则,但这是一个有益的建议:按习惯使用运算符,例如+只用做加法,对我们理解程序会有极大的帮助。毕竟,这种习惯用法源于人们千百年使用数学符号的经验。相反,含混不清的运算符和常规不符的使用方式是混乱和错误之源。
类接口
类的公有接口和实现部分应该分离。但是,如何来设计一个好的接口呢?
(1)保持接口的完整性。
(2)保持接口的最小化。
(3)提供构造函数。
(4)支持(或禁止) 拷贝。
(5)使用类型来提供完善的类型检查。
(6)支持不可修改的成员函数。
(7)在析构函数中释放所有的资源。
前两条原则可以归结为"保持接口尽可能小,但不要更小了“。我们希望接口尽量小,是因为小的接口易于学习和记,而实现者也不会为不必要的和很少使用的功能浪费大量时间。小的接口还意味着当错误发生时,我们中需要检查很少的函数来定位错误。平均来看,公有成员函数越多,查找bug就越困难--调试带公有数据的类是非常复杂的,不要让陷入其中。当然,前提还是要保持完整性,否则,接口就没有用处了。如果一个接口无法完成我们真正需要做的全部工作,我们是不会使用它的。