Chinaunix首页 | 论坛 | 博客
  • 博客访问: 24853
  • 博文数量: 5
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 14
  • 用 户 组: 普通用户
  • 注册时间: 2015-04-18 13:55
文章分类
文章存档

2016年(5)

我的朋友

分类: C/C++

2016-04-12 20:40:12

这个问题曾经困扰过我一阵子。请先看一下下面的源代码:


class A1
{
public:
        int operator=(int a)
        {
                return 8;
        }

        int operator+(int a)
        {
                return 9;
        }
};

class B1 : public A1
{
public:
        int operator-(int a)
        {
                return 7;
        }
};

int main()
{        
        B1 v;
        cout << (v + 2) << endl; // OK, print 9
        cout << (v - 2) << endl; // OK, print 7
        cout << (v = 2) << endl; // Error, see below

        return 0;
}

VC
编译器的错误提示:

error C2679: binary '=' : no operator defined which takes a right-hand operand of type 'const int' (or there is no acceptable conversion)

意思是说编译器找不到int operator=(int a)这个成员函数可以调用。

真是怪了?明明int operator=(int a)这个函数是我从基类公有继承下来的函数,怎么编译器识别不了呢?遇到这种问题,第一反应就是查找MSDN以求得解释,微软告诉我:

“All overloaded operators except assignment (operator=) are inherited by derived classes.”

意思是说:除了赋值运算符重载函数以外,所有的运算符重载函数都可以被派生类继承。

我这个函数int operator=(int a)很不幸就是微软所说的赋值运算符重载函数,当然不能被继承!

可是到此为止,我心中的疑问依然没有消除。为什么赋值运算符重载函数不能被派生类继承呢?从C++语义上讲,不允许这个函数被派生类继承并没有充足的理由,一个类对象实例完全可以被任何一个其他类对象实例所赋值!比如一个颜色对象实例可以被一个整数赋值,甚至可以被一个小白兔实例所赋值。赋值运算符既然允许重载,就应该允许被继承,就像其他的运算符重载之后都可以被派生类继承一样。微软的解释并没有说明为什么赋值运算符重载函数不能被继承的幕后原因。

我又查找了C++ Primer和其他一些重量级的C++经典,可是这些书籍对此问题都避而不谈,抑或是语焉不详,我都没有找到答案。于是,我只好反身求诸己,从C++类对象的构造开始分析,结果找到了我认为是正确的答案:

1
,每一个类对象实例在创建的时候,如果用户没有定义赋值运算符重载函数,那么,编译器会自动生成一个隐含和默认的赋值运算符重载函数。所以,B1的实际上的声明应该类似于下面这种情况:

class A1
{
public:
        int operator=(int a)
        {
                return 8;
        }

        int operator+(int a)
        {
                return 9;
        }
};

class B1 : public A1
{
public:
        B1& operator =(const B1& robj); //
 
注意这一行是编译器添加的
        int operator-(int a)
        {
                return 7;
        }
};

2
C++标准规定:如果派生类中声明的成员与基类的成员同名,那么,基类的成员会被覆盖,哪怕基类的成员与派生类的成员的数据类型和参数个数都完全不同。显然,B1中的赋值运算符函数名operator =和基类A1中的operator =同名,所以,A1中的赋值运算符函数int operator=(int a);B1中的隐含的赋值运算符函数B1& operator =(const B1& robj);所覆盖。 A1中的int operator=(int a);函数无法被B1对象访问。

3
,程序中语句v = 2实际上相当于v.operator =(2);,但是A1中的int operator=(int a);已经被覆盖,无法访问。而B1中默认的B1& operator =(const B1& robj);函数又与参数2的整数类型不相符,无法调用。

4
,为了确认B1中默认的B1& operator =(const B1& robj);函数的存在性,可以用以下代码验证:

B1 b;
B1 v;

v = b; // OK, 
相当于调用v.operator =(b);

5
,所以,赋值运算符重载函数不是不能被派生类继承,而是被派生类的默认赋值运算符重载函数给覆盖了。

这就是C++赋值运算符重载函数不能被派生类继承的真实原因!

 

关于本帖问题正确性的解释

C++程序员的必读经典《Effective C++》这么说:

条款45: 弄清C++在幕后为你所写、所调用的函数

一个空类什么时候不是空类? ---- C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。换句话说,如果你这么写:

class Empty{};

和你这么写是一样的:

class Empty {
public:
  Empty();                        // 
缺省构造函数
  Empty(const Empty& rhs);        // 拷贝构造函数

  ~Empty();                       // 析构函数 ---- 是否
                                  // 为虚函数看下文说明
  Empty&
  operator=(const Empty& rhs);    // 
赋值运算符

  Empty* operator&();             // 取址运算符
  const Empty* operator&() const;
};

但是Effective C++依然不能作为最后的判决。让我们从C++宪法中寻找答案...

ISO/IEC 14882
C++的国际标准。该标准于199891日通过并且定案。当然,这个标准已经不是最新标准了,但这个标准却是目前最被广泛支持的C++标准。所以,我一向称之为C++宪法

C++“宪法12 Special Member Functions (第185页)开宗明义:

The default constructor, copy constructor and copy assignment operator, and destructor are special member functions. The implementation will implicitly declare these member functions for a class type when the program does not explicitly declare them, except as noted in 12.1. The implementation will implicitly define then if they are used, as specified in 12.1, 12.4 and 12.8. Programs shall not define implicitly-declared special member functions. Programs may explicitly refer to implicitly declared special member functions.

译文:
缺省构造函数,拷贝构造函数,拷贝赋值函数,以及析构函数这四种成员函数被称作特殊的成员函数。如果用户程序没有显式地声明这些特殊的成员函数,那么编译器实现将隐式地声明它们。12.1中有特别解释的例外。如果用户程序调用了这些特殊的成员函数,那么编译器就会定义这些特殊的成员函数,在12.112.412.8中分别规定了编译器对这些函数的定义方式。用户程序不能定义隐式声明的特殊成员函数。用户程序可以显式地调用隐式声明的特殊成员函数。

译注:
根据C++标准的规定:

声明(Declare)代表头文件中的那部分代码,比如下面就是一个声明:

class A
{
public:
    A();
}

定义(Define)代表源文件中的代码,比如下面就是一个定义:

A::A()
{}


综上所述,可知,我的第一个说法是正确的。

关于我的第二个说法的正确性,可参见C++“宪法”3.3.7 Name Hiding (第28页)(由于我手上的C++“宪法是扫描版,无法直接拷贝文字,且文字较多,懒得输入了。)

我的第345点说法都是常识性的知识,可以直接验证。

阅读(1949) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~