具体类
在很多应用中经常大量使用较小的抽象。这种例子包括整数、浮点数、复数、点、指针、指标、时间等等。每个应用都会使用这种小的抽象,其中一些简单的具体类型经常被大量使用。一个典型的应用会直接使用一些类型,还有更多的类型是通过库间接使用的。
C++直接支持一部分抽象作为其内置类型。但是,大多数抽象并不支持,因为数量是在太多了,不可能囊括。而且,一个通用编程语言的设计者不可能预见所有应用的细节需求。因此,语言必须为用户提供小的具体类型的机制。这种机制被称为具体类型或者具体类,以区别于抽象类和类层次中的类。
如果一个类的表示是其定义的一部分,我们就称它是具体的(具体类)。这将它与抽象类区分开来,后者为多种实现提供一个公共接口。在定义中明确类的表示方式令我们能:
- 将对象置于栈、静态分配的内存以及其他对象中
- 拷贝和移动对象
- 直接引用具名对象(与通过指针和引用访问不同)
这令具体类易于推断,编译期也容易为之生成优化的代码。因此,我们倾向于对频繁使用而且性能修改的小类型使用具体类,比如复数、智能指针、容器
很好的支持这种用户自定义类型和使用是C++早期就明确的目标,这时优雅程序设计的基础:
namespace Chrone{
//定义枚举类Month是为了避免比如6月7日是写成{6, 7}(美国风格)还是{7, 6}(欧洲风格)的混肴
enum class Month {jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};
class Date{
public:
class Bad_date{}; // 异常类
// {}默认值是0,但是实际中我们最好避免无意义的初始化
explicit Date{int dd = {}, Month mm = {}, int yy = {}}; //表示“选择默认值”
int day() const;
Month month() const;
iny year() const;
string string_rep() const; //字符串表示
void char_req() const; //C风格字符串表示
Date& add_year(int n);
Date& add_month(int n);
Date& add_day(int n);
private:
bool is_vaild(); //检查Date是否表示一个日期
int d, m, y;
};
// 辅助函数:这些函数不是类成员,不能直接访问Date,但是我们认为它们与名字空间chrono的使用是相关的
bool is_date(int d, Month m, int y); //对合法日期返回true
bool is_leapyear(int y); //闰年返回true
bool operator==(const Date& a, const Date & b);
bool operator!=(const Date& a, const Date & b);
const Date& default_date(); //默认日期
ostream& operator<<(ostream &os, const Date& d); //将d打印到os
ostream& operator>>(istream &is, Date & d); //从is读取Date存入d
};
成员函数
- 对每个成员函数都必须在某处给出它的实现,比如:
Date::Date(int dd, Month mm, int yy) : d{dd}, m{mm}, y{yy}{
if(y == 0) y = default_date().year();
if(m == Month()) m = default_date().month();
if(d == 0) d = default_date().day();
if(!is_valid()) throw Bad_date();
}
bool Date::is_valid(){
return is_date(d, m, y);
}
...
- Date提供默认赋值和拷贝初始化,而且Date不需要析构函数,因为Date不拥有资源,因此在离开作用域的时候不需要清理操作
辅助函数
一般而言,一个类都会有一些无须定义在类内的关联函数,因为它们不需要直接访问类的表示,比如:
int diff(Date a, Date b);
bool is_date(int d, Month m, int y); //对合法日期返回true
bool is_leapyear(int y); //闰年返回true
const Date& default_date(); //默认日期
Date next_weekday(Date d);
Date next_saturday(Date d);
将这种函数定义在类内会增加类接口的复杂性,还会增加那些考虑修改类表现时需要检测的函数数目。
这种函数如果与类Date“关联呢”?
- 在早期的C++中,与C语言一样,这些函数的声明简单的放在类Date声明所在的文件中。Date用户只需要包含定义接口的文件即可使用所有辅助函数。比如:
#include "Date"
- 现在,可以将类和辅助函数放在一个名字空间中来显示表明两者的关系。
namespace Chrone{
class Date{ };
// 辅助函数:这些函数不是类成员,不能直接访问Date,但是我们认为它们与名字空间chrono的使用是相关的
bool is_date(int d, Month m, int y); //对合法日期返回true
bool is_leapyear(int y); //闰年返回true
bool operator==(const Date& a, const Date & b);
bool operator!=(const Date& a, const Date & b);
const Date& default_date(); //默认日期
ostream& operator<<(ostream &os, const Date& d); //将d打印到os
ostream& operator>>(istream &is, Date & d); //从is读取Date存入d
};
名字空间Chrono自然还可能包含Time和Stopwatch等相关的类和它们的辅助函数。
bool Chrono:: is_date(int d, Month m, int y){
...
return 1 <= d && d <= nday;
}
const Date& Chrono::default_date(){
static Date d{1, Month::jan, 1950};
return d;
}
重载运算符
添加一些函数使得用户自定义类型能够使用人们习惯的符号时很有用的,比如:
inline bool operator==(Date a, Date b){
return a.day() == b.day() && a.month() == b.month() && a.year() == b.year();
}
具体类的重要性
类似Date这样的就是具体类,它们与int、char这样的内置类型相似。具体类的使用就像是内置类型一样。具体类型也叫做值类型,使用它们编程成为面向值的程序设计。它们的使用模型和设计后面的“哲学”与面向对象编程十分不同。
一个具体类型的目标是高效的做好一件相对简单的事情,为用户提供修改其行为的特性通常不是其目标。特别是,展现运行时多态也不是其意图。
如果不喜欢一个具体类型的某些细节,你可以构建一个新的类型,只需要实现所需行为即可。如果希望“重用”一个具体类型,你可以用它实现新类型,就像int一样。
class Date_and_time{
Date d;
Time t;
public:
...
}
一种替换方法是派生类机制,可以从具体类派生新类型,只要描述两者的差异即可。但是我们很少从一个具体类派生新类,即使这样做的话也要也被小心,因为具体类型缺乏虚函数和运行时类型信息
如果有一个很好的编译期,Date这样的具体类不会有隐含的时空开销。具体类型的大小在编译期就已知,可以从运行时栈中分配对象。对象的内存布局也在编译时就已经知道,从而内联操作很容易实现。类似的,无须特别努力就可以实现与其他语言比如C实现内存布局上的兼容