我们都能定义一个类,可是如何定义一个正确的类,这是一个需要我们深入理解的问题。C++之父曾经说过定义新类型的基本思想就是将实现一个类的时候并非必要的细节(存储该类型的对象采用的布局细节)和对于这个类的正确使用至关重要的性质(访问数据的成员函数)分开设计。这种区分的最好实现方式是提供一个特定的表层接口,所有对于类内部数据结构和内部维护的调用都通过这个表层接口。
1.类该怎么定义
(1)首先我们要明白,建立一个对象,构造函数把成员变量都放在了堆之中(除了static变量之外,static变量放在全局变量区),而所有成员函数的参数则放在了栈中。特别说明一下static静态成员变量,它是类的一部分,而不是每个类的对象的一部分,所以每个类之中仅仅只有一个,在static静态成员变量定义时,不要在头文件中定义,这样其他文件多次包含头文件的时候,静态成员变量的赋值被执行了多次,这是不允许的。另外,在类中声明static静态成员变量时需要加“static”关键字,而定义时不要加“static”关键字。
(2)当类中有一个成员函数在逻辑上,他应该是const的,但是函数中仍然需要修改一些成员变量,我们知道这两者是矛盾的,但是有没有解决办法呢?有,那就是“mutable”,它的意思是这个成员能用能更新的方式存储,即使是const成员变量。
class Date{ mutable bool cache_valid; mutable string cache; void compute_cache_value() const; //填充缓存 public: string string_rep() const; //字符串表示 }; string Date::string_rep() const{ if(!cache_valid){ compute_cache_value(); cache_valid = true; } return cache; }
当我们需要一个对象在逻辑上保持const但是实际上需要修改的时候,更好的选择是将需要修改的数据放到另一个独立的对象之中。
struct cache{ bool valid; string rep; }; class Date{ cache *c; void compute_cache_value() const; public: string string_rep() const; } string Date::string_rep() const{ if(!c -> valid){ compute_cache_value(); c -> valid = true; } return c -> rep; }
(3)在C++中,在类内部定义函数都是inline函数,但是为了代码清晰,建议还是在类的定义之后再紧跟定义inline函数。
class Date{ public: int day() const; //... private: int d, m, y; //... }; inline int Date::day() const{ return d; }
(4)下面是一个高效的用户定义Date类型,值得我们参考:
class Date{ public: //公共界面 enum Month {jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct,nov,dec}; class Bad_date{} //异常类,用于异常报告错误 Date(int dd = 0, Month mon = Month(0), int yy = 0); //0是默认值,构造函数描述如何初始化。 //查看Date的函数: int day() const; Month month() const; int year() const; string string_rep() const; void char_rep(char s[]) const; //修改Date的函数 Date& add_day(int n); Date& add_month(int n); Date& add_year(int n); private: int d,m,y; static Date default_date; }
(5)除了类的成员函数,还有一些普通函数与类有关联,我们选择把它们放入一个命名空间中,使类的使用更清晰。
namespace Chrono{ class Date{ /*...*/ }; int diff(Date a, Date b); bool leapyear(int y); Date next_weekday(Date d); }
2.类中到底有什么
(1)默认构造函数。默认构造函数是不用参数的构造函数,编译器生成的默认构造函数将隐式调用类成员和基类的默认构造函数。因为const和引用必须进行初始化,所以包含const和引用的类不能进行默认构造,除非显式定义了默认构造函数。
(2)对象的复制。t1 = t2的默认含义是把t2的成员逐个复制到t1。如果类中存在指针,那么指针指向的数据只有一份,但是将会在删除t1,t2的时候被删除两次,这是相当危险的,所以复制构造函数是完成对未初始化的存储区的初始化,而复制赋值运算符必须正确处理一个结构良好的对象。
(3)类对象作为成员。成员的构造函数在类本身的构造函数执行之前执行,这些构造函数按照成员在类中的声明顺序执行。当对象被销毁的时候,它自己的析构函数将首先被执行,而后将按照成员声明的逆顺序执行各个成员的析构函数。构造函数自下而上的构造对象,析构函数自上而下的拆除对象。另外,对于静态常量整型成员,可以在成员声明中加一个常量表达式作为初始化。
//Curious.h class Curious{ public: static const int c1 = 7; //ok,但是记得定义 static int c2 = 11; //错误,不是const const int c3 = 13; //错误,不是static static const int c4 = f(17); //错误,不是常量表达式 static const double c5 = 7.0; //错误,不是整型 }; //Curious.cpp const int Curious::c1; //...
(4)成员数组。如果在构造类的对象时有默认构造函数,不用显式提供初始式,那么就可以定义这个类的数组。
(5)非局部对象存储。有时,我们定义一个静态的类成员,只使用一次:
class Zlib_init{ Zlib_init(); //使Zlib能够使用 ~Zlib_init(); //使Zlib做最后的清理 }; class Zlib{ static Zlib_init x; //... };
在一个由若干个编译单元组成的程序里,无法保证“x”这种静态成员对象一定能在它第一次使用之前初始化,在其最后使用之后销毁。我们可以采用第一次开关技术解决顺序依赖性问题,但是没有类似的最后时间开关结构:
class Zlib{ static bool initialized; static void initialize() {/*初始化*/ initialized = true;} public: //无构造函数 void f(){ if(initialized == false) initialize(); } }
(6)临时对象。一个临时对象会在建立它的表达式结束后销毁,但是临时对象成为const引用或者用于命名对象的初始式后,还会继续存在。
(7)对于联合,最好仅仅将它运用在底层代码,或者类实现的一部分,由类去维护联合之中存储什么信息。