首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > C++ >

被误解的C++——类型,该如何处理

2012-02-16 
被误解的C++——类型类型任何一种语言都有类型。对类型的不同的态度,造就了语言的个性。我们通常会将语言分为“

被误解的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:
intvar_type;//variant包含的类型标记
union
{
doubledbval;
intival;
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可以象内置类型那样使用了。


[解决办法]
你这样写,当然是不错的,但是性能不佳,而且不能支持标准输出printf、cout。
使用模板实现,性能可以接近原生类型的运算,不过有些运算仍然无法象原生类型那样使用(我理解是如此,如果错了,请指教)。
最佳的办法仍然是让编译器支持它。:)

[解决办法]
LZ提供了使用C++语法跨越强类型的技术方法(variant应该算是一种弱类型吧)。
这些技术(操作符重载,隐式类型转换函数,用于类型转换的单参构造函数,explicit关键字……以及Union类型),都是C++中比较高级的了,需要我们好好学习,并积累经验,呵呵。

性能是一个需要研究的主题。在同样的情况下,使用操作符重载会比隐式类型转换(将variant转换为其他类型)/单参构造函数(将其他类型转换为variant) 性能高,因为后者会建立临时变量;而前者在加上inline后(当然需要编译器支持,一个积极的、关注运行效率的编译器应该支持它),除了必要的步骤,没有多余的性能损失。而对于类似 a = b + c 的形式,为了最大限度提高效率,需要返回值优化(一个积极的……同上,呵呵)。总之性能问题(以及其他许多问题都)需要付出时间和精力,以及经验的积累,通过良好的设计与编码来解决它。(貌似要求很高地说——对于C++程序员要有高要求,呵呵)

期待LZ的下文。
[解决办法]
楼主加油!
有关printf问题,在variant里重新定义一个就行了,仅需对参数引入做一个筛选,就可以达到printf的要求。虽然...在C++里缺乏相应的支持,但C++本身支持C方式的处理(需要显式的声明C方式)函数,因此楼主只要针对格式字符串表采用switch_case方式就可解决这一“难题”。


当然,相对一个大的单一函数来说, < <重载远比一个函数灵活得多,楼主在这里不过是偷个懒而已,并不是C++无法实现相应功能。这一点对scanf同样适用。
[解决办法]
关于printf:

如果是在不考虑效率的前提下,那么可以利用c/c++允许我们去更改系统库函数这个特性;去“篡改”默认的printf功能函数,配合va_start等c时代的遗留物,再加上c++的RTTI特性,以完全支持variant。

大致思路就是:屏蔽系统printf,在自定义printf中利用va_start/va_arg/va_end处理可变数目参数;再利用RTTI识别variant,最后根据格式串进行类型转换,最后把重整过的参数交给系统printf或cout即可。

呵呵,或许我更关心底层,因此想出的办法都比较hack一点。不过这种用法不违反任何c++规范;难看就难看点吧,c这个包袱不是那么容易扔掉的——printf不正是c的遗留物吗。

热点排行