标题:[C++] 多态
@水墨不写bug
目录
(一)什么是多态
在继承体系中,不同的class实例化的对象调用同一个函数,产生了不同的行为。
实现多态的条件:
通过基类的指针或者引用调用重写过的虚函数。
class Person
{
public:
virtual void buyticket()
{
cout << "100¥" << endl;
}
};
class VIP : public Person
{
public:
virtual void buyticket()
{
cout << "50¥" << endl;
}
};
void Enter(Person& p)
{
p.buyticket();
}
int main()
{
Person p1;
VIP vp1;
Enter(p1);
Enter(vp1);
return 0;
}
( 生活中,想要入场需要买票,普通人Person买票需要100元;VIP买票需要50元)
在上例中,基类中buyticket()函数前 声明有 “virtual” ,是虚函数。在派生类VIP中,我们重写了虚函数buyticket()。在main函数中,通过一个Enter函数(形参为基类的引用)调用buyticket()函数,满足了上述两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
满足上述两个条件,就构成多态。
(二)什么是虚函数
在class中,被 virtual 修饰的member function 就是虚函数(virtual function)。
(1)虚函数的重写:
虚函数的重写(覆盖):基类中声明了一个虚函数,派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
在派生类中,重写虚函数前面可以不加“virtual”,但是为了规范,建议加上“virtual”:
class Person
{
public:
virtual void buyticket()
{
cout << "100¥" << endl;
}
};
class VIP : public Person
{
public:
//可以不加virtual
void buyticket()
{
cout << "50¥" << endl;
}
/*
virtual void buyticket()
{
cout << "50¥" << endl;
}
*/
};
虚函数重写的例外:
1.协变
2.析构函数的重写***(重要)
I,协变(基类 和 派生类 虚函数的返回值类型不同)
派生类重写虚函数时,和基类的返回值类型不同,但是两个虚函数返回值的类型必须是 拥有继承关系的类:
基类返回基类指针,派生类返回派生类指针。
class A
{};
class B : public A
{};
class Person
{
public:
virtual A* GetObj()
{
cout << "100¥" << endl;
return new A;
}
};
class VIP : public Person
{
public:
virtual B* GetObj()
{
cout << "50¥" << endl;
return new B;
}
};
II,析构函数的重写(基类 和 派生类 析构函数名不同)
基类的析构函数建议设置为虚函数,目的是为了多态做准备。如果基类的析构函数是虚函数,此时派生类的析构函数如果有定义,无论是否加上virtual,都与基类的析构函数构成重写。
为什么呢?
析构函数是类内部特殊处理的一类成员函数,在编译时,析构函数的名称会被编译器统一处理为destructor,这就为重写奠定基础。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
/*只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。*/
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
输出:
~Person()
~Student()
~Person()
(三)C++11 新增的 override 和 final
override和final是对虚函数的实现与否进行限制的关键字。
1. final:修饰虚函数,表示该虚函数不能再被重写
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
(四)重载,重写(覆盖),隐藏(重定义)的辨析
重载:在同一个作用域中,如果两个或者多个函数的函数名相同,形参列表不同,这些函数就构成重载。
比如:
//这两个函数构成重载 long long Add(int x, long y) { return x + y; } long long Add(int x, int y) { return x + y; }
形参列表不同指的是:1)形参的数目不同,2)形参的类型不同,3)形参的顺序不同
需要注意:仅仅返回值类型不同,不构成函数重载
重写(覆盖):在继承关系中,派生类的虚函数与基类的虚函数完全相同(函数名,形参列表,返回值都相同(协变除外)),这两个函数构成重写(覆盖)。(重写之所以叫做“重写”“覆盖”,在后面多态的原理会让你有深刻的理解)
比如:
class Person { public: virtual void buyticket() { cout << "100¥" << endl; } }; class VIP : public Person { public: virtual void buyticket() { cout << "50¥" << endl; } };
重定义(隐藏):在继承关系中,派生类的函数与基类的函数名相同(仅仅是函数名称相同),并且不构成重写(覆盖),就是重定义(隐藏)。
比如:
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域 // B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。 class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { cout << "func(int i)->" << i << endl; } };
由于B类的func和A类的func构成隐藏关系:B类实例化的对象默认调用的是fun(int i), 而不是fun() 。
(五)抽象类
在虚函数的函数头后面加上“ =0 ”,则这个函数就被声明为纯虚函数,含有纯虚函数的类称为抽象类(也叫接口类),抽象类不能实例化出对象。
抽象类的派生类在没有重写纯虚函数的情况下也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。纯虚函数的声明规范了派生类必须重写,纯虚函数也体现了接口继承。
class PC
{
public:
//纯虚函数
virtual void Band()=0;
};
class Thinkbook : public PC
{
public:
virtual void Band()
{
cout << "Thinkbook" << endl;
}
};
class Huawei : public PC
{
virtual void Band()
{
cout << "Huawei" << endl;
}
};
int main()
{
//纯虚函数不能实例化出对象
PC p; //(错误)
Thinkbook tb; //(正确)
Huawei hw; //(正确)
return 0;
}
什么是接口继承?
普通函数继承是一种实现继承,派生类继承了基类函数,可以调用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是重写,构成多态,继承的是接口。所以如果不实现多态,不要把函数定义为虚函数。
看了下面这道题,你会对接口继承有更深的理解:
// 以下程序输出结果是什么()
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->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
//A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
稍加分析,你会发现这道题不同寻常:
B对象调用A类成员函数,实际是B*赋值给A的this,再调用test(),test()内调用func():基类指针调用重写后的虚函数,构成多态,于是你会选择D选项;
但是你要明白一个事实:接口继承会保留原函数的接口(体现接口继承)——走原函数的函数头,函数体部分根据多态,走重写后的部分函数体。
所以这道题的正确选项应该是B。
完~
未经作者同意禁止转载