被误解的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可以象内置类型那样使用了。 必须说明的是,实现variant所用的的C++特性,有些是非常危险的角色。不恰当地使用类型转换操作符和类型转换构造函数,会让人得到意想不到的结果。比如,一个类中定义了operator   int操作符: class   X { public: operator   int()   {…} … }; 尽管我们没有为X重载operator+操作符,但是我们依然可以合法的编写这样的代码: X a,   b; a+b; 因为编译器会很“积极”地使用这个操作符,将a和b隐式地转换成int,而int之间是可以“+”的。有时候,这可能不是我们所需要的。所以,一般情况下,尽可能不要使用类型转换操作符。用一个成员函数实现这种操作,就像std::string::c_str()那样。 如果实在需要这样做,(就像variant那样)那么,所有类型转换操作符所对应的类型上,可能用到的操作符或函数都应该重载一遍。因为编译器倾向于选择不需要执行类型转换的操作符或函数,它会优先选择为类重载的操作符或函数。 variant是一个特殊的类。它本身就是为了承载和模拟内置类型而创建的。所以,所有相关的运算操作都需要重载,这样也不会出现这种问题。但是,如果一个类没有variant那么“特殊”的话,最好还是不要使用类型转换操作符。 类似的问题也发生在类型转换构造函数上。因此,我们也应该尽量利用explicit关键字禁止编译器执行相应的隐式类型转换。所带来的不便,也仅仅局限在无法使用这个操作: variant   a=10.2; //double常量10.2不能通过explicit   variant(double   v)隐式 //   地转换成variant,不能进行这种初始化。 variant   a(10.2); //可以使用这种形式。 所以,灵活性和安全性之间需要权衡。通常,安全性更重要。 总结一下,C++通过一系列的操作符重载和特殊构造函数,允许我们将一个类设计成如同内置类型那样的行为。这项任务的实现,完全依赖于C++巨大的灵活性。但是,灵活性是一柄双刃剑,带来好处的同时,也会带来很多问题。只有在特定的场合下,在具备一定的条件下,才能保证这些灵活性不带来伤害。 此外,操作符重载会带来大量语义问题。对于每个内置类型,操作符都有特定的语义,但是毫无顾忌的重载经常会破坏这些语义。因此,在重载操作符时,最好保持操作符原有的语义。否则,也应预先做好约定,以避免使用上的问题。而且,重载操作符还可能引发语义上的矛盾。比如string上的+已被定义成为接合字符串。那么如果variant内的实际类型是string,也应认为此时variant上的+是接合字符串。但是如果variant中的string包含的是文本化的数字,而程序员下意识地会把它作为数字处理,用+来累加这两个数字,于是便产生了问题。所以,操作符的重载必须谨慎。 variant类型本身是面向过程编程方式的产物,现今的一些语言,特别是能够很好地支持泛型编程的语言,已经基本上无需variant类型。现在variant的主要用途已集中在不同语言之间的交互接口上。在单一语言的编程中,泛型具有更好的类型安全性和更高的效率。 在编写variant的时候,如此众多的操作符重载,必然是一项艰巨的工作。在实际应用中,实打实地去写这些代码,太浪费了。(或许ms有这实力和耐心)。所以,我决定利用C++提供的强大的抽象机制,设法简化variant的实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值