C++ 继承多态关系中的赋值运算符的重载=operator()

Posted 小丑快学习

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 继承多态关系中的赋值运算符的重载=operator()相关的知识,希望对你有一定的参考价值。

1.异形赋值

操作符的重载是非常有用也是非常常见的,但是当涉及到继承和多态时情况可能就比较复杂了。
对于多态,则一般都会涉及到虚函数重载,而我们在继承体系中如果重载赋值运算符则需要考虑对父类的赋值。假设有如下的继承体系:
在这里插入图片描述
Animal是Dog和Cat的父类,则我,我们可以写出如下所示的代码:

class Animal {
public:
    Animal(std::string name_ = "animal"):name(name_) {}
    Animal(const Animal & animal):name(animal.name){}
    ~Animal() {}

    //赋值运算符重载
   Animal & operator=(const Animal & animal) {
        //自我赋值处理
        if (this == &animal) {
            return *this;
        }
        name = animal.name;
        return *this;
    }

  virtual  std::string getName() {
        return name;
    }

private:
    std::string name;//表示动物种类
};

class Dog : public Animal {
public:
    Dog(const std::string& name_ , const std::string& Animal_name = "animal") : 
        Animal(Animal_name) , name(name_) {}
   
    //拷贝构造函数考虑基类,因此在初始化列表中调用父类拷贝构造
    Dog(const Dog& dog) : Animal(dog), name(dog.name) {}

    //赋值运算符承载
    Dog & operator = (const Dog & dog) {
        if (this == &dog) {
            return *this;
        }
        Animal::operator=(dog);//调用父类的赋值运算符实现父类的赋值
        name = dog.name;
        return *this;
    }

   virtual std::string getName() {
        return name;
    }

private:
    std::string name;

};

class Cat : public Animal {
public:
    Cat(const std::string& name_, const std::string& Animal_name = "animal") : Animal(Animal_name), name(name_) {}
    Cat(const Cat& Cat) : Animal(Cat), name(Cat.name) {}

    //赋值运算符承载
     Cat& operator = (const Cat& Cat) {
        if (this == &Cat) {
            return *this;
        }
        Animal::operator=(Cat);//调用父类的赋值运算符实现父类的调用
        name = Cat.name;
        return *this;
    }

  virtual  std::string getName() {
        return name;
    }
   
  //获取父类的对象
  std::string getBaseName() {
        return Animal::getName();
    }

private:
    std::string name;

};

似乎上述使得继承体系的代码没有什么很大的问题,对于操作运算符的重载充分考虑了自我赋值和父类的赋值情况,因此诸如下述的使用方式;

Cat c("kitty", "cat_like_animal");
Cat c1("kitty_clone", "cat_like_animal_clone");
Animal * cat = &c;
Animal * cat_clone = &c1;
*cat_clone = *cat;

上述代码编译完全没问题,但是,我们忽略了一个问题,这是在多态的环境下操作,因而,我们的赋值运算实际上仅仅改变对象的基类部分,而不属于基类的部分却没被赋值,因此如果我们这样打印:

 std::cout << cat_clone->getName() << "   "<<dynamic_cast<Cat*>(cat_clone)->getBaseName() << std::endl;

将会得到这样的结果:

kitty_clone   cat_like_animal

针对这种情况,我们有很好的解决方案,我们只要将上述代码代码中的赋值运算符重载改成虚函数则能够解决多态的问题,也就是将上述代码中的所有的赋值运算符重载前加上virtual关键字,并把参数改为基类的引用即可。

//Dog类的修改
    virtual Dog & operator = (const Animal & dog) {
        if (this == &dog) {
            return *this;
        }
        Animal::operator=(dog);//调用父类的赋值运算符实现父类的赋值
        name = dog.getName();
        return *this;
}

//Cat类的修改
	virtual Cat& operator = (const Animal & cat) {
	   if (this == &cat) {
	       return *this;
	   }
	   Animal::operator=(cat);//调用父类的赋值运算符实现父类的调用
	   name = cat.getName();
	   return *this;
	}

现在我们可以使用多态的方式进行赋值运算了。

Cat c("kitty", "cat_like_animal");
Cat c1("kitty_clone", "cat_like_animal_clone");
Animal * cat = &c;
Animal * cat_clone = &c1;
*cat_clone = *cat;
std::cout << cat_clone->getName() << "   "<<dynamic_cast<Cat*>(cat_clone)->getBaseName() << std::endl;

现在得到的结果则是这样的:

kitty   cat_like_animal

赋值很成功,没有任何问题,而且父类和子类的数据均完成了赋值。但是当我们把赋值运算符重载写为虚函数时,有种意想不到的情况发生了,那就是我们可以通过多态的方式将一个Dog的对象赋值于一个Cat对象,如下操作:

 Cat c("kitty", "cat_like_animal");
 Dog d("DaHuang", "dog_like_animal");
 Animal * cat = &c;
 Animal * dog = &d;
 *cat = *dog;//异形赋值
 std::cout << cat->getName()  << "   "<< dynamic_cast<Cat*>(cat)->getBaseName() << std::endl;

我们将会得到这样一个结果:

DaHuang   dog_like_animal

阿这,我们声名的是一个Cat的对象,然而它居然被赋值成了一个狗的对象,这未免也太狗了吧!我说有一只猫它的名字叫大黄,他是一种犬科动物,你会相信嘛?这就是异形赋值 ,没错,我们将赋值运算符写为虚函数虽然解决了问题,但是却带来了更大的问题。但是能够解决的,我们有种东西叫做dynamic_cast。

2.dynamic_cast解决异形赋值问题

我们可以通过使用dynamic_cast来判断执行是否成功,如果转换失败则dynamic_cast将会抛出异常,这样我们的程序将会终止,因而,用户在使用的时候应该做出相应的异常处理。
因而。我们将Cat类和Dog类的赋值运算符函数做出如下的改进:

//用于多态的赋值运算
virtual Dog& operator = (const Animal& dog) {
    return operator=(dynamic_cast<const Dog&>(dog));//转换失败则会抛出异常
}

//重载版本的赋值运算符
Dog& operator = (const Dog& dog) {
    if (this == &dog) {
        return *this;
    }
    Animal::operator=(dog);
    name = dog.name;
    return *this;
}
    
//赋值运算符承载
virtual Cat& operator = (const Animal & cat) {
    return operator=(dynamic_cast<const Cat & >(cat));
}
//赋值运算符重载
Cat& operator = (const Cat & cat) {
    if (this == &cat) {
        return *this;
    }
    Animal::operator=(cat);//调用父类的赋值运算符实现父类的调用
    name = cat.name;
    return *this;
}

每个类中均有两个版本的赋值运算符重载操作,一个用于非多态版本,而另一个则用于多态版本。当调用非多态版本的函数时,我们将其进行强制类型转换为派生类的对象,然后再调用非多态版本的赋值运算符。当强制类型转换失败后,dynamic_cast将会抛出异常,因而,异形赋值将会失败。

3.抽象基类解决异形赋值问题

上述的改进虽然能够解决异形赋值问题,但是,我们再编译期间并不知道的我们是否使用了异形赋值问题,因而,需要一种能够再编译期间就能检查出这种问题的话将会提高用户的使用效果,也不需要去处理异常等麻烦事。

事实上,对于基类来说,我们往往把他设计为抽象类。这对这种情况,我们把animal实现为抽象基类,但是这时候我们将没法产生Animal的对象,因而,我们应该写出一个新的抽象类,然后让Animal、Dog、Cat类均继承并实现该类。为了防止异形赋值的问题我们需要付出一些代价,那就是牺牲通过多态的方式进行赋值运算。怎样做呢,就是将抽象基类的赋值运算符放置于protected作用域内。这样就只有他的派生类能够访问赋值运算符。
如下:
在这里插入图片描述涉及如下的抽象基类:

class Abstract_Animal {
public:
    Abstract_Animal(std::string name_ = "animal") :name(name_) {}
    Abstract_Animal(const Abstract_Animal& animal) :name(animal.name) {}
    virtual ~Abstract_Animal() = 0;

protected:
    //赋值运算符重载
    Abstract_Animal& operator=(const Abstract_Animal& animal) {
        //自我赋值处理
        if (this == &animal) {
            return *this;
        }
        name = animal.name;
        return *this;
    }
public:
    virtual  std::string getName() const
    {
        return name;
    }

private:
    std::string name;
};
//纯虚函数类外实现
Abstract_Animal::~Abstract_Animal() {
}

因此我们的Cat类和Dog类继承体系如下:

class Dog : public Abstract_Animal {
public:
    Dog(const std::string& name_ , const std::string& Animal_name = "animal") : 
        Abstract_Animal(Animal_name) , name(name_) {}
   
    //拷贝构造函数考虑基类,因此在初始化列表中调用父类拷贝构造
    Dog(const Dog& dog) : Abstract_Animal(dog), name(dog.name) {}

    //重载版本的赋值运算符
    Dog& operator = (const Dog& dog) {
        if (this == &dog) {
            return *this;
        }
        Abstract_Animal::operator=(dog);
        name = dog.name;
        return *this;
    }


   virtual std::string getName() const 
   {
        return name;
    }

private:
    std::string name;

};

class Cat : public Abstract_Animal {
public:
    Cat(const std::string& name_, const std::string& Animal_name = "animal") : Abstract_Animal(Animal_name), name(name_) {}
    Cat(const Cat& cat) : Abstract_Animal(cat), name(cat.name) {}

    //赋值运算符重载
    Cat& operator = (const Cat & cat) {
        if (this == &cat) {
            return *this;
        }
        Abstract_Animal::operator=(cat);//调用父类的赋值运算符实现父类的调用
        name = cat.name;
        return *this;
    }
  virtual  std::string getName() const
  {
        return name;
   }
  //获取父类的对象名称
  std::string getBaseName() {
        return Abstract_Animal::getName();
    }
private:
    std::string name;
};

这样的我们能够完全的杜绝部分赋值问题和异形赋值问题。而这种方式是在编译器杜绝通过多态的方式进行赋值来解决的,因为再抽象基类中,已经将赋值运算符设置为protected,这种方式的就会导致基类的指针或者引用没法实现赋值运算符的调用,因而也就杜绝了部分赋值和异形赋值问题。而将基类设置为抽象类也是一种常见的手段,且一般为了降低耦合性,都会使用这种继承体系。

参照《More Effective C++》条款 33了解更多详情。

以上是关于C++ 继承多态关系中的赋值运算符的重载=operator()的主要内容,如果未能解决你的问题,请参考以下文章

C++多态 --- 多态实现原理简析

C++中的重载赋值运算符

如何为从C++中的模板继承的类重载赋值运算符

赋值运算符的布尔和字符串重载 (C++)

c++ 拷贝构造函数与赋值运算符重载函数的区别是

C++继承