多态的概念
多态的基本概念
多态支持不同对象完成不同的行为。
定义“人”为一种对象,那么不同的人对于一件事会有不同的行为。
比如:普通人买票时,票价为全价。
学生买票时,是半价买票。
军人买票时则可优先买票。同属于人这一大类,但是会有不同,多态的特征就是支持对一类中的某些不同做出不同的反映。
多态的构成条件
必须通过基类的指针或引用调用虚函数
虚函数在派生类中需要被重写
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
class Soldier :public Person
{
public:
virtual void BuyTicket()
{
cout << "优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
ps,st,sd对象被指向到三个派生类的实现中。输出结果就是派生类中实现的结果。
虚函数
即被virtual修饰的类成员函数称为虚函数。
问题一:C++的类中,那些函数不能用virtual修饰,为什么?
不能是虚函数的成员函数有:静态成员函数,内联成员函数,构造函数,友元函数。
编译器会为每一个含有virtual函数生成一个vtablel。构造函数调用时,Vtable没有建立,当然不能使用虚函数。构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类。
静态成员函数不能是virtual的,因为静态成员函数属于类而非单个具体对象,所有的对象共享一份代码,没有实现多态的必要。
inline成员函数可以声明为virtual,但是在编译时不会实际将代码直接在调用处展开。
友元函数也不能声明为virtual,因为友元关系是不能被继承的,编译会出错。
注:一般情况下,父类的析构函数需要定义为虚函数。
普通成员函数也不能是virtual的,否则不能通过编译,virtual只能出现在类声明中。
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
不符合多态的条件
一:不是virtual函数
class Person
{
public:
void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
class Soldier :public Person
{
public:
void BuyTicket()
{
cout << "优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
ps,st,sd在Func中都被当做Person对象,不用virtual则没有vtable没法指向具体派生类中方法。所有都调用的是基类Person中的BuyTicket函数。
二:不是父类的指针或者引用调用
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
class Soldier :public Person
{
public:
virtual void BuyTicket()
{
cout << "优先买票" << endl;
}
};
void Func(Person p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
这样在Func函数中传递的直接就是一个基类person对象,是可能调用派生类中的方法的。
三:派生类与基类函数参数不同
//(3)多态条件探究三:不符合重写 -- 参数不同
class Person
{
public:
virtual void BuyTicket(int)
{
cout << "买票-全价" << endl;
}
};
class Student :public Person
{
public:
//Student中函数的参数类型与父类不同
virtual void BuyTicket(char)
{
cout << "买票-半价" << endl;
}
};
class Soldier :public Person
{
public:
//Soldier中函数的参数类型与父类相同
virtual void BuyTicket(int)
{
cout << "优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket(1);
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
这里Soldier的函数参数类型与基类相同,student中参数类型不同。无法构成重写。则,student与基类没有构成多态,soldier与基类构成多态。
四:返回值类型不同
class Person
{
public:
virtual int BuyTicket()
{
cout << "买票-全价" << endl;
return 0;
}
};
class Student :public Person
{
public:
//Student中函数的返回值类型与父类不同
virtual double BuyTicket()
{
cout << "买票-半价" << endl;
return 1.1;
}
};
class Soldier :public Person
{
public:
//Soldier中函数的返回值类型与父类相同
virtual int BuyTicket()
{
cout << "优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
返回值类型不同,必须是协变才可以,但是上边的例子也不是协变,所有就无法编译通过。
如果改成如上,那么就是重载。通过对象的方法才可调用。
st.BuyTicket(1)是对象的方法调用,调用的是返回值为double的方法。
st.Person::BuyTicket()调用的是父类中的方法。
Func(st)调用的是p.BuyTicket(),而student中没有重写BuyTicket()方法,所以,只能继承基类中的“全票”方法。
虚函数重写的两个例外
一:协变
基类与派生类虚函数的返回值类型不同
这个不同指的是基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用。并不是随便一个虚函数的返回值类型不同。
class Person
{
public:
//返回父类的指针
virtual Person* BuyTicket()
{
cout << "买票-全价" << endl;
return this;
}
};
class Student :public Person
{
public:
//返回子类的指针
virtual Student* BuyTicket()
{
cout << "买票-半价" << endl;
return this;
}
};
class Soldier :public Person
{
public:
//返回子类的指针
virtual Soldier* BuyTicket()
{
cout << "优先买票" << endl;
return this;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
返回的是this指针。
注意协变要求的是返回指针或引用必须是父子关系。则,在一个基类中,可以返回类型可以另一个基类,同理,派生类中的返回类型也必须和基类返回类型有父子关系。
class A
{};
class B :public A
{};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票-全价" << endl;
return nullptr;
}
};
class Student :public Person
{
public:
virtual B* BuyTicket()
{
cout << "买票-半价" << endl;
return nullptr;
}
};
class Soldier :public Person
{
public:
virtual B* BuyTicket()
{
cout << "优先买票" << endl;
return nullptr;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sd;
Func(ps);
Func(st);
Func(sd);
return 0;
}
Person基类中返回类型是A基类,Student和Soldier中返回的是B派生类,他们都不是返回自己那个类对象。
二:析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
编辑器为什么会这样处理呢?
当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。
析构派生类对象为什么会首先调用基类的析构函数?
析构函数的主要作用,就是在该类实例对象不需要的时候,对这个对象进行一些清理作用(主要是内存的释放.)一个基类,它也是一个类,它也可以有对象, 所以基类的析构函数负责基类内部开辟的一些内存的清理工作.
那么, 对于派生类. 可能会产生一些内存的开销, 那么派生类也需要构造函数. 基类只了解基类内部的内存开销, 派生类只了解派生类自己的内存开销. 他们之间互不了解. 继承关系, 多数都是建立在扩充基类的关系上.
派生类丰富了基类, 如果派生类的实例对象在销毁的时候, 不去自动调用基类的析构函数, 那么请问? 基类的清理工作谁去做? 基类的内部, 有一些是派生类不了解的. 为了方便,为了安全,为了管理. 所以派生类的对象销毁的时候, 继承机制会分级调用各级的析构函数.
内存的清理工作必须严格要求. 谁开辟的,谁最后释放. 一人做事儿一人当. 孩子闯了祸, 不应该让老爹去认错. 同时继承关系, 基类有基类的责任, 派生类有派生类的责任. 分工明确了, 不能越俎代庖. 各部门相互协调, 程序才能顺畅运行.
谁开辟的必须谁清理,派生类中继承着基类中的部分,那就要基类的析构函数清理。
例子
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) {
std::cout << "B->" << endl;
std::cout<< val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
一:final:修饰虚函数,表示该虚函数不能再被重写。
class A
{
public:
virtual void Func() final {}
};
class B: public A
{
public:
virtual void Func() { cout << "Func" << endl; }
};
二:override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class A
{
public:
virtual void Func(){}
};
class B: public A
{
public:
virtual void Func1() override {}
};
重载,重写,重定义
重载
函数名相同,参数列表不同(参数类型、参数顺序),不能用返回值区分。
特点:
作用域相同;
函数名相同;
参数列表必须不同,但返回值无要求;
特殊情况:若某一重载版本的函数前面有virtual关键字修饰,则表示它是虚函数,但它也是重载的一个版本。
作用效果:编译器根据函数不同的参数列表,将函数与函数调用进行早绑定,重载与多态无关,与面向对象无关,它只是一种语言特性。
重写
派生类重定义基类的虚函数,既会覆盖基类的虚函数(多态).
特点:
作用域不同;
函数名、参数列表、返回值相同;
基类函数是virtual;
特殊情况:若派生类重写函数是一个重载版本,那么基类的其他同名重载函数将在子类中隐藏。
作用效果:父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数,这就是C++的多态。多态是是使用虚函数表(vtable)技术来实现的。
重定义
派生类对基类函数得重定义,派生类函数名与基类某函数同名。
特点:
作用域不同,既一个在基类一个在子类中;
函数名相同;
参数列表、函数返回值无要求;
特殊情况:若派生类定义的函数与基类的成员函数完全一样(名字、参数列表、返回值),且基类的该函数为virtual,则属于派生类重写基类的虚函数。
class Base
{
public:
// 三个重载函数
void fun()
{
std::cout << "base fun()" << std::endl;
}
void fun(int)
{
std::cout << "base fun(int)" << std::endl;
}
virtual void fun(int, double)
{
std::cout << "base fun(int,double)" << std::endl;
}
};
class Derive : public Base
{
public:
// 重定义基类fun函数,隐藏了基类的三个重载函数
void fun(double)
{
std::cout << "Drive fun(int)" << std::endl;
}
};
class Derive1:public Base
{
public:
// 重写基类的fun(int,double)函数,同时隐藏了基类的两个重载函数
void fun(int, double)
{
std::cout << "Derive1 fun(int,double)" << std::endl;
}
};
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
为什么会有抽象类?
1.最重要的原因是,可以将接口与实现分离。接口是软件产品最有价值的资源,设计接口比实现接口需要耗费更昂贵的成本。因此,要将接口保护起来,以免在针对客户需求修改实现的时候,程序员不小心把接口破坏掉。
2.引入抽象基类和纯虚函数方便实现C++的多态特性。可以用抽象基类的指针去调用子类对象的方法。
3.很多时候,许多基类被实例化是不合理的。例如“形状”这个基类,被实例化之后反而会让人相当费解,所以干脆将“形状”这个类定义为抽象类,由它派生出正方形,三角形等子类。
注:抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态实现原理
在实际中,基类与派生类对于virtual函数都会建立一个虚表。子类中的函数进行重写,所以地址发生变化。当调用一个已经重写过的函数时,底层会寻找不同的虚表指针。对于不是普通成员函数,未加virtual修饰的的函数,就不会进入虚表中,也就不会有多态的特性。
(1)虚表是一个指针(void**),存的也是指针,图中的void*。
(2)同一个类型的对象共用一张虚表,vs下,不管是否完成重写,子类虚表跟父类虚表都不是同一个。
(3)派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。