被误解的C++——类型
类型
任何一种语言都有类型。对类型的不同的态度,造就了语言的个性。我们通常会将语言分为“强类型”和“弱类型”。
通常认为C++是强类型的。但也有反对意见。反对者认为,既然C++拥有隐式类型转换,那么就不该作为强类型语言。我这里不打算趟这潭混水,强类型还是弱类型,没有什么实际意义。
这里,我打算认真地考察一下C++独特的类型系统,来探寻C++在语言中特立独行的根源。我会尽可能不涉及语言的比较,至少不涉及他们的好坏,以免引发新一轮的口水仗。
强类型提供了很好的类型安全,但缺少灵活性。弱类型化后,灵活性提高了,但类型安全无法保障。C++所作的探索,就是寻找一种方式,在强类型的情况下,允许提供灵活,但又安全的类型系统。
让我们先从C++的内置类型说起。
C++的类型分为内置类型和用户定义类型。内置类型主要包括整数类型、浮点类型、引用类型(指针和引用)等等。我们先来分析一下内置类型,以整数类型为例。
我们知道,一个整数类型可以进行初始化、赋值、算术运算、比较、位操作,以及参与逻辑运算:
int a=10; //初始化
int b;
b=a; //赋值
int c=(a+b)*(a-b); //算术运算
if(c==b) … //比较
a=c&b; //位操作
if(c==b || !a) … //逻辑运算
当然,其他的还包括取地址、取引用等类型的基本操作。
这些操作都是语言赋予整数类型的基本操作,我们无需对其进行而外的转换或者处理。但是,当我们把目光转向用户定义类型后,问题就复杂化了。由于C++被定位于系统级开发语言(实际上C++什么开发领域都可以胜任,但最初发明它时是打算用于开发系统软件的),所以时常会需要一些古怪的操作,比如把一个用户定义类型赋值给int类型,这种操作在强类型语言中是不合规矩的。
如果我们不管三七二十一,把用户定义类型按位拷贝给int类型(这是int类型之间赋值操作的语义),那么准保会惹上大麻烦的。但如果在特定情况下,这种操作是需要的(当然不一定是必需的)。那么,我们就应当提供一种方法,允许这种赋值操作在受控的情况下进行。
为此,C++引入了操作符重载(学自Ada),以及一些相关的机制。通过这些机制,使我们(几乎)可以按照内置类型(如整数)的行为设计用户定义的类型。下面我通过一个案例慢慢讲述如何把一个用户类型变成内置类型的模仿者。这个案例来源于前些日子论坛上的口水仗,就是开发variant类型。为了简化问题,我选取了三种具有代表性的类型int,double,char*作为variant包容的目标,并且不考虑性能问题。
首先,我定义了一个枚举,为了使代码能够更加清晰:
enum
{
vt_empty=-1, //空variant
vt_double=0, //double类型
vt_int=1, //int类型
vt_string=2 //字符串类型
};
然后,定义variant的基本结构。我使用了最传统的手法,union。
class variant
{
private:
int var_type; //variant包含的类型标记
union
{
double dbval;
int ival;
char* csval; //由于union不能存放拥有non-trivial构造函数等成员,
// 所以只能用char*,提取数据时另行处理
};
};
现在,我们一步步使variant越来越像一个内置类型。看一下int类型的初始化方式:
int a(0), b=0;
int(0); //创建并初始化一个int临时对象
我们先来考虑用一个variant对象初始化另一个variant对象。实现这个功能,需要通过重载构造函数:
class variant
{
public:
variant(const variant& v) {…}
…
};
这是一个拷贝构造函数,使得我们可以用一个variant对象初始化另一个variant对象:
variant x(a), y=a; variant(a); //假设a是一个拥有有效值的variant对象
如果我们没有定义任何构造函数,那么编译器会为我们生成一个复制构造函数。但这不是我们要的,因为编译器生成的复制构造函数执行浅拷贝,它只会将一个对象按位赋值给另一个。由于variant需要管理资源引用,必须执行深拷贝,所以必须另行定义一个赋值构造函数。
按C++标准,一旦定义了一个构造函数,那么编译器将不会再生成默认构造函数。所以为了能够如下声明对象:
variant x;
我们必须定义一个默认构造函数:
class variant
{
public:
variant(): var_type(vt_empty) {…}
…
};
下一步,实现variant对象间的赋值。C++中内置类型的对象间赋值使用=操作符:
int a=100, b;
b=a;
用户定义的类型间的赋值也使用=操作符。所以,只需重载operator=便可实现对象间的赋值:
class variant
{
public:
variant& operator=(const variant& v) {…}
…
};
variant x, y;
x=y;
int是一种可以计算的数值类型。所以,我们可以对int类型的变量执行算术运算、比较、逻辑运算、位运算等:
int a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;
…
同样,variant涵盖了几种数值类型,那么要求其能够进行这些运算,也是理所当然的:
variant a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;
…
为实现这一点,C++提供了大量的操作符重载。在C++中,除了“.” 、“.*”、“? :”、“#”、“##”五个操作符,RTTI操作符,以及xxx_cast外,其余都能重载。操作符可以作为类的成员,也可以作为全局函数。(类型转换操作符和“=”只能作为类的成员)。通常,将操作符重载作为全局函数更灵活,同时也能避免一些问题。
我们先重载操作数都是variant的操作符:
bool operator==(const variant& v1, const variant& v2) {…}
bool operator!=( const variant& v1, const variant& v2) {…}
variant& operator+=( const variant& v1, const variant& v2) {…}
variant operator+( const variant& v1, const variant& v2) {…}
…
需要注意的是,对与variant而言,他可能代表了多种不同的类型。这些类型间不一定都能进行运算。所以,variant应当在运算前进行类型检查。不匹配时,应抛出运行时错误。
C++允许内置类型按一定的规则相互转换。比如:
int a=100;
double b=a;
a=b; //可以转换,但有warning
为了使variant融入C++的类型体系,我们应当允许variant同所包容的类型间相互转换。C++为我们提供了这类机制。下面我们逐步深入。
我们先处理初始化。非variant类型初始化也是通过重载构造函数:
class variant
{
public:
variant(double val) {…}
variant(int val) {…}
variant(const string& val) {…}
…
}
这些是所谓的“类型转换构造函数”。它们接受一个其它类型的对象作为参数,在函数体中执行特定的初始化操作。最终达到如下效果:
int a=10;
double b=23;
string c(“abc”);
variant x(a), y=b; variant(c);
接下来,处理不同类型和variant对象赋值的问题。先看向variant对象赋值。同样通过=操作符:
class variant
{
public:
variant& operator=(double v) {…}
variant& operator=(int v) {…}
variant& operator=(const string& v) {…}
variant& operator=(const char*) {…}//该重载为了处理字符串常量
…
};
这样,便可以如下操作:
int a=10;
double b=23;
string c(“abc”);
variant x,y,z;
x=a;
y=b;
z=c;
然后再看由variant对象向其它类型赋值。实现这种操作需要利用类型转换操作符:
class variant
{
public:
operator double() {…}
operator int() {…}
operator string() {…}
…
};
使用起来和内置类型赋值或初始化一样:
variant x(10), y(2.5), z(“abc”);
int a=x;
double b=y;
string c;
c=z;
现在,variant已经非常“象”内置类型了。最后只需要让variant同其它类型一起参与运算便大功告成了。我们依然需要依靠操作符重载,不过此处使用全局函数方式的操作符重载:
bool operator==(const variant& v1, int v2){…}
bool operator==(int v1, const variant& v2){…}
bool operator==(const variant& v1, double v2){…}
bool operator==(double v1, const variant& v2){…}
bool operator==(const variant& v1, const string& v2){…}
bool operator==(const string& v1, const variant& v2){…}
bool operator==(const variant& v1, const char* v2){…}
bool operator==(const char* v1, const variant& v2){…}
…
variant& *=(const variant& v1, double v2){…}
variant& *=(double v2, const variant& v2){…}
…
我们可以看到,对于每个非variant类型,操作符都成对地重载。通过交换参数的次序,实现不同的操作数类型次序:
10+x; x+10;
至此,variant已经基本完成了。variant可以象内置类型那样使用了。