浅谈 C++ 多态性
多态
定义:指同样的消息被不同类型的对象接收时导致不同的行为。
实现:编译时多态和运行时多态。
编译时多态
运算符重载
C++中默认运算符的操作对象只局限于基本的内置数据类型,对于我们自定义的类是没有办法进行操作的,为了解决这个问题,提出了运算符重载,其本质还是函数重载。我们可以对运算符进行重新定义,赋予其新的功能,满足自身的需求。
定义:改变运算符的运算过程叫运算符重载。
实现:(1)令运算符重载函数作为类的成员函数。(2)令运算符重载函数作为类的友元函数
格式如下:
<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
可重载运算符/不可重载运算符(出自菜鸟驿站)
下面是可重载的运算符列表:
下面是不可重载的运算符列表:
在重载时,要先判断返回的类型是对象还是引用,引用可以作为左值,而对象只能作为右值,如果一定要返回引用,但又不希望作为左值,就用常引用。
下面用一个复数的类来具体说明:
#include <iostream>
using namespace std;
class Complex
{
public:
Complex(int real, int imag) : m_real(real), m_imag(imag) {}
Complex(const Complex &other) : m_real(other.m_real), m_imag(other.m_imag) {}
~Complex(){}
void show()
{
cout << m_real << "+" << m_imag << "i" << endl;
}
//运算符重载,主要是这个operator关键字。在重载时,要先判断返回的类型是对象还是引用。
Complex operator +(const Complex &other) const
{
Complex ret(this->m_real + other.m_real, this->m_imag + other.m_imag);
return ret;
}
bool operator >(const Complex &other) const
{
return this->m_real > other.m_real;
}
//前置++和后置++
Complex &operator ++() //前置++运算返回的是引用,是可以作为左值运算
{
this->m_imag += 1;
this->m_real += 1;
return *this; //返回对象本身
}
Complex operator ++(int) //后置++运算返回的是对象,只能作为右值运算
{
static Complex ret(*this); //因为是后置运算,所以直接利用this指针构造一个对象用于返回
this->m_imag += 1;
this->m_real += 1;
return ret;
}
//赋值运算符重载
Complex &operator =(const Complex &other) // =返回的是引用
{
cout << "operator!" << endl;
this->m_imag = other.m_imag;
this->m_real = other.m_real;
return *this;
}
//友元函数,帮重载函数获取权限
friend ostream &operator << (ostream &out, const Complex &other);
private:
int m_real;
int m_imag;
};
//输出流重载,因为传入的参数数据类型限定了,所以不影响打印其他的数据类型
ostream &operator << (ostream &out, const Complex &other)
{
out << other.m_real << "+" << other.m_imag << "i" << endl;
return out;
}
//选择排序
void sort(Complex *c, size_t len)
{
for(size_t i=0; i<len-1; ++i)
{
for(size_t j=i+1; j<len; ++j)
{
if(c[i] > c[j])
{
Complex temp(c[i]);
c[i] = c[j];
c[j] = temp;
}
}
}
}
int main(void)
{
Complex c[] = { Complex(7,5), Complex(6,0), Complex(5,20), Complex(4,2) };
sort(c, 4);
for(auto &x : c)
{
x.show();
}
cout << "******************" << endl;
Complex c1(10,20);
(++c1).show();//分别验证前置和后置++运算符重载
(c1++).show();
cout << "******************" << endl;
cout << c1 << endl;//验证重载<<运算符
cout << "******************" << endl;
Complex c2(20,30);
c2 = c1; //验证重载=运算符
cout << c2 << endl;
system("pause");
return 0;
}
再来一个String类来体会运算符重载。
#include <iostream>
#include <cstring>
using namespace std;
class String
{
public:
String(const char *s = " ") : mPstr(new char[strlen(s) + 1])
{
strcpy(mPstr,s);
}
String(const String &other) : mPstr(new char[strlen(other.mPstr) + 1])
{
strcpy(mPstr, other.mPstr);
}
//利用匿名对象创建对象,深拷贝会开辟两份空间,为了提高效率,将两个指针指向同一份空间,再将匿名对象的指针置空
//这样的做法与浅拷贝一致,因为匿名对象销毁可以,但其他的有名对象不可以销毁
String(String &&other)
{
cout << "String&" << endl;
this->mPstr = other.mPstr;
other.mPstr = nullptr;
}
//同理,利用匿名对象直接赋值,也可以采用相同的方式处理,先释放自身的空间,再指向匿名对象的空间
String operator =(String &&other)
{
delete []this->mPstr;
this->mPstr = other.mPstr;
other.mPstr = nullptr;
return *this;
}
~String()
{
delete []mPstr;
}
void show() const
{
cout << mPstr << endl;
}
void append(const String &other) //实现一个追加功能
{
char *p = new char[strlen(this->mPstr) + strlen(other.mPstr) + 1];
strcpy(p, this->mPstr);
strcat(p, other.mPstr);
delete []this->mPstr;
mPstr = p;
//因为p是局部变量,所以不需要手动销毁
}
void assign(const String &other) //实现一个覆盖功能
{
if(this != &other) //如果覆盖的字符是本身,就不覆盖
{
char *p = new char[strlen(other.mPstr) + 1];
strcpy(p, other.mPstr);
delete []this->mPstr;
mPstr = p;
}
}
//重载=运算符
String &operator =(const String &other)
{
if(this != &other)
{
char *p = new char[strlen(other.mPstr + 1)];
strcpy(p, other.mPstr);
delete []mPstr;
mPstr = p;
}
return *this;
}
//重载+=运算符
String &operator +=(const String &other)
{
char *p = new char[strlen(mPstr) + strlen(other.mPstr) + 1];
strcpy(p, mPstr);
strcat(p, other.mPstr);
delete []mPstr;
mPstr = p;
return *this;
}
//重载+运算符
String operator +(const String &other)
{
String ret(*this); //这时候利用深拷贝来构造,会创建一个新的空间
ret += other;
return ret;
}
private:
char *mPstr;
friend ostream &operator <<(ostream &out, const String &s);
};
//重载<<运算符
ostream &operator <<(ostream &out, const String &s)
{
out << s.mPstr;
return out;
}
int main(void)
{
String s1("Hello");
cout << s1 << endl;
String s2("World");
s1 = s2;
cout << s1 << endl;
String s3("!");
s1 += s3;
cout << s1 << endl;
cout << (s1 + s3) << endl;//+不会像+=一样,改变s1内容
cout << s1 << endl;
String s4(String("Hello World!")); //利用匿名对象构造一个对象
cout << s4 << endl;
s1 = String("ABC"); //利用匿名对象进行赋值
cout << s1 << endl;
}
运行时多态
虚函数
虚函数是 C++ 实现动态单分派子类型多态(dynamic single-dispatch subtype polymorphism)的方式。
动态:在运行时决定的(相对的是静态,即在编译期决定,如函数重载、模板类的非虚函数调用)。
单分派:基于一个类型去选择调用哪个函数(相对于多分派,即由多个类型去选择调用哪个函数)。
子类型多态:以子类型-超类型关系实现多态(相对于用参数形式,如函数重载、模版参数)。
直接看代码:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void f1() {cout << "Base::f1()" << endl;}
void f2() {cout << "Base::f2()" << endl;}
virtual void f3() {cout << "Base::f3()" << endl;}
void f4() {cout << "Base::f4()" << endl;}
virtual void f5() {cout << "Base::f5()" << endl;}
void f6() {cout << "Base::f6()" << endl;}
int m_i;
};
class Derived : public Base
{
public:
void f1() {cout << "Derived::f1()" << endl;}
virtual void f2() {cout << "Derived::f2()" << endl;}
virtual void f3() {cout << "Derived::f3()" << endl;}
void f4() {cout << "Derived::f4()" << endl;}
virtual void f7() {cout << "Derived::f7()" << endl;}
void f8() {cout << "Derived::f8()" << endl;}
};
int main(void)
{
Base *p;
Derived d;
p = &d;
p->f1();
p->f2();
p->f3();
p->f4();
p->f5();
p->f6();
}
1、利用虚表(静态的虚函数的指针的数组)来确定执行的是哪一个函数。
2、得知道虚表怎么画,父类的虚表存放父类的虚函数,如代码中的f1、f3、f5,子类的虚表先吸收,继承父类的虚函数,比如图中子类虚表的前三个f1、f3、f5,然后改造,观测这三个是不是父类和子类中同名同参,如代码中的f1、f3,是就把这俩改为Derived,f5父类中有,但子类中没有,所以不改变,依旧是Base,最后添加,把子类中的虚函数都加到虚表中,如f2和f7。加入虚函数以后,会多一个四个字节的指针,它指向自己虚表的地址。
3、画完虚表以后,就可以知道父类指针指向子类对象时,调用的是哪一个函数了,如果是虚函数,就查子类的虚表,如果不是就直接调用。
它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是父类的函数还是哪个子类的函数,所以被成为“虚”函数。
纯虚函数
作者:wuxinliulei
链接:https://www.zhihu.com/question/23971699/answer/69592611
一、定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
如:virtual void funtion1()=0
二、引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
抽象类
抽象类的介绍抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有纯虚函数的类为抽象类。
(2)抽象类的作用:抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
(3)使用抽象类时注意:抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。
总结:
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错,错误提示为:error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
9、全局函数不能是虚函数、内联函数不能是虚函数、静态函数不能是虚函数、构造函数不能是虚函数。