用转换操作符保护代码的安全

  • 来源: 互联网 作者: rocket   2008-03-18/13:36
  • 某些对象必须要被转换成低级形式,反之亦然。例如,使用 std::string 对象的程序员必须将之转换为 char 指针,请看下面例子:
    string inf="mydata.txt";
    
    ifstream infile(inf.c_str());// 必须要转成 const char* 
    同样,PSOIX 程序员需要将 <fstream> 对象转换成文件描述符以便 使用本地系统调用。

    问题: 如何在不危及代码安全性的前提下让对象自动转换到其底层类型呢?

    答案: 使用转换操作符和 explicit 构造函数来创建具备双接口的对象,从而避免病态行为转换。

    提出问题
    商业和金融应用常常将币值表示成对象,而不是原始的浮点类型。之所以要这样做有几个原因:
  • 类型安全:人为错误更容易被发现;
  • 可移植性:由于对用户隐藏实现细节,代码具有更好的可移植性;
  • 业务逻辑:类允许你强化了业务逻辑规则。例如:美元(US dollar)类知道一美元是 100 美分(cents),而科威特第纳尔(dinar)类知道一第纳尔是 1000 菲尔斯(fils)。这种差别将影响 I/O 格式。
  • 下面是一个简化了的表示美国货币的类:
    class USD
    
    {
    
    private:
    
    	__int64 dollars; //或者 long long, 依赖编译器 
    
    	int cents;
    
    public:
    
    	USD(__int64 d=0, int c=0) : 
    
    	  dollars(d), cents(c) {}
    
    	  friend bool operator==(const USD& d1, const USD& d2);
    
    	  //...other overloaded operators and functions
    
    };    
    唉,许多数学函数如:pow() 和 sqrt()都只认浮点变量。为了克服这个问题人们总是去重载关系操作符和算子。然而,你会发现这将带来大量无谓的编码,测试和维护工作。你想要的只不过是一个双接口:在适当的上下文中,USD 类对象除了应该提供安全的自动的到基本类型的转换外,它还应该提供上述所列的优点。

    使用转换操作符:
    转换操作符在适当的上下文中将其对象自动转换成不同的类型。在类 USD 中,最自然的转换是到浮点的转换:
    USD payment(203, 67);
    此时,你想将支付对象转换为浮点值 203.76。
    转换操作符没有返回值(从操作符的名字上判断),也不带任何参数:
    class USD
    
    {
    
    public:
    
    	operator double() //conversion operator
    
    	{
    
    		double temp=(dollars*100)+cents;
    
    		return temp/100;
    
    	}
    
    };    
    让我们看看它是如何工作的。假设你想增加 5% 的支付:
    double res=payment*1.05; //res=210.70
    这样能工作得很好,因为进行乘法之前转换操作符自动将支付转换为值 200.67。但是,在 USD 类的设计上有一个严重的缺点。请看下面的代码:
    payment=payment*1.05; // 很糟
    现在,支付等于 210.00,这当然不是所期望的结果。让我们来看看为什么。右边子表达式 payment*1.05 首先被求值。正如你所看到的,这一部分没什么问题,产生的结果也是正确的。问题出在赋值上。编译器进行赋值的表达式如下:

     

    payment=USD(__int64(200.67*1.05), 0);
    乘法表达式的结果被暗中转换为整型(因此丢失分数部分),然后被用作一个临时 USD 对象的参数。这就是为什么它会产生这样一个令人为难的结果。

    声明 ‘explicit’构造函数:
    为了解决这个问题,首先将构造函数声明为 explicit:
    class USD 
    
    {
    
    public:
    
    	explicit USD(__int64 d=0, int c=0): 
    
    	dollars(d), cents(c){}
    
    	...    
    这样,只有 USD 对象的赋值才被接受:
    payment=USD(payment*1.05); // 没问题 
    
    payment=payment*1.05; // 编译出错 
    添加另一个构造函数:
    其次是添加另一个构造函数,该构造函数带一个 double 类型的参数:
    class USD 
    
    {
    
    public:
    
    	explicit USD(double val)
    
    	{
    
    		dollars=val; // 拷贝整型部分
    
    		long double temp=(val-dollars)*100; // 吸取美分
    
    		// 避免截断 e.g., .6699 是 66 而不是 67
    
    		cents=temp+0.5;
    
    	}
    
    };    
    在此,构造函数被声明为‘explicit’,以避免不经意的赋值。为了让支付增加 5%,使用如下形式:
    payment = USD(payment*1.05); 
    现在,一切都没有问题。混杂的构造函数不经意的转换被阻止,允许依赖转换操作符到 double 的正当转换。

    以安全为重:
    程序员常常抱怨在类 std::string 中缺乏 const char * 转换操作符。如果 std::string 具备这样的操作符,你可能会写:

     

    string filename;
    
    ifstream inf(filename);
    代替丑陋的:
    ifstream inf(filename.c_str());
    不管怎样,C++ 标准化委员会决定在 std::string 中不包含此类转换操作符,因为它在某些广泛使用 char * 的库中可能导致令人厌恶的 bug。在这种情况下,标准委员会本着以安全为重的思路。与之对照,<fstream> 对象包含到 void * 类型的转换操作符,你可以象下面这样来使用它:

     

    ifstream inf("myfile");
    
    if (inf) // 使用 void * 转换操作符
    
    // 使用此文件
    
    else
    
    // 失败
    当你设计自己的类时,应该考虑启用/禁用哪种类型的自动转换。然后,通过定义适当的转换操作符来启用合法转换,而通过声明 explicit 构造函数来阻止不合需要的转换。

    评论 {{userinfo.comments}}

    {{money}}

    {{question.question}}

    A {{question.A}}
    B {{question.B}}
    C {{question.C}}
    D {{question.D}}
    提交

    驱动号 更多