1.048596

区分C++多态和重载

多态和重载在实际运用中是非常重要的部分,多态实现了代码的重用,重载提供了函数或运算符的多样性。

虚函数和抽象类

虚函数是被virtual关键字修饰的成员函数,用来实现继承中函数的多态性。通过不同的环境来使用不同的同名函数,实现类指针类似于普通函数的重载。

引入目的

C++允许基类和派生类之间的直接转化,但这也造成了一些函数调用的混乱,比如父类调用了子类的同名函数,子类错误调用了父类的同名函数,为了避免基类和派生类同名成员函数的错误调用问题。

// It will happen an error!
class Base {
  public:
    void print() { cout << "Base!" << endl; }
};
class Derive : public Base {
    void print() { cout << "Derive!" << endl; }
};
int main() {
  // Conflicts occur if you use pointers to the base class.
  Base a;
  Derive b;
  Base *p1 = &a;
  Base *p2 = &b;
  p1->print(); // output: Base!
  p2->print(); // output: Base!
}

这种情况的发生是因为函数的静态多态是早绑定,函数的地址是在编译阶段绑定给调用。因为函数表是父类对象的函数表,所以绑定的是父类的对象的函数,出现错误!

  • 为了解决基类生成对象的不合理性

手动的多态,可以从派生类中解释提供基类的函数的实例。

  • 为了提高编码的效率方便对代码的重构

这个在纯虚函数中体现比较明显,纯虚函数在基类函数中没有实现,在派生类中如果不提供实现则会报错,但是这也给了一个方便使用多态特性的方式。

使用虚函数

虚函数

class Base {
  public:
    virtual void print() { cout << "Base!" << endl; }
    // Add the prefix to become a virtual function.
};
class Derive : public Base {
    void print() { cout << "Derive!" << endl; }
};

加上前缀之后,函数列表中虚函数就变“虚”了,实现了动态的多态,派生类重写了基类中的虚函数之后,派生类的同名函数可以在函数表中将基类中的虚函数在指向派生类的基类指针调用时把原来对应的基类函数同名覆盖,这样就会调用派生类的函数。

  • 区别重写和重载

重写:名字、参数列表都相同

重载:名字相同,参数列表不相同

纯虚函数

纯虚函数时在基类中声明的虚函数,但是在基类中并没有定义,但要求任何派生类都要定义自己的一套实现方法,在基类中定义虚函数的方法时在函数声明后加=0

class Base {
  public:
    virtual void print() = 0;
};
class DeriveA : public Base {
    void print() { cout << "Derive A!" << endl; }
};
class DeriveB : public Base {
    void print() { cout << "Derive B!" << endl; }
};
class DeriveC : public Base {
    // If a pure virtual function is not implemented in a derived class, an
    // error is occured!
}

在这么实现虚函数时,子类的子类或者孙子类都可以覆盖该虚函数,调用的时候就会根据多态方式动态绑定让指向派生类的基类指针也能使用派生类的函数。

抽象类

当一个类中具有纯虚函数时,则是一个无法实例化的抽象类,它一般作为基类不单独使用。由于纯虚函数的使用,在抽象类中不必具有函数的实现,只需要利用虚函数的性质,在他所派生出来的类中实现符合这些派生类应用实际的函数实现即可。

抽象类使用

class Base {
  public:
    virtual void fun() = 0;
};
Base a; // Error! Because it can't be instantiated.

类运算符重载

运算符重载是一种形式的多态,函数的多态及是它的重载。但是在类中的重载可以扩展到运算符上。例如我们可以扩展运算符+的运用范围。因为普通的运算符对类型的重载只限于基本的内置类型如int等,而实际应用过程中为了简洁需要让普通运算符有更多的适配性。

重载原则

重载不能改变参加运算的对象个数

重载不能改运算符的优先级别

重载函数不能有默认的参数

重载不能改运算符的结合性

重载运算符中至少有一个操作是定义类

对运算符的重载不能完全背离原意

可以设置结果对象来防止对值的修改

普通重载运算符

重载方法

使用operator加上要重载的运算符,类似于方法的声明。而在重载后的运算符就会被附上另一种定义去扩展它的用法使其拥有其他更细致定制化的运算规则。

左侧是运算符调用对象

右侧是传入值的对象

Time Time::operator+(Time &b) {
  // Overloaded operators can customize the rules of the operation.
}
const &Time Time::operator-(const &Time b) const {
  // code...
}

重载的界限

以下的运算符不能重载:

sizeof大小运算符

.成员运算符

.*成员指针运算符

::作用域解析运算符

?;条件运算符

typeid一个 RTTI 运算符

const_cast强制类型运算符

dynamic_cast强制类型转换运算符

reinterpret_cast强制类型转换运算符

static_cast强制类型转换运算符

以下的运算符只能通过成员函数进行重载:

=赋值运算符

()函数调用运算符

[]下标运算符

->通过指针方位类成员运算符

友元运算符重载

通过友元函数能够访问类内成员的特性,可以实现运算符的重载,这种方法更适合的是二元运算中使用。

class Clock {
    friend Clock operator-(Clock a, Clock b);
    // code...
};
Clock operator-(Clock a, Clock b) {
    a.minutes -= b.minutes;
    if (a.minutes < 0) {
        a.hours = a.hours - 1;
        a.minutes += 60;
    }
    a.hours -= b.hours;
    return a;
}
  • 注意

需要注意的是友元函数并不是成员函数,所以不用使用域运算符::