目录
(3)用虚函数实现多态(给同名函数加上virtual关键字)
(1)多态是什么?
多态就是同一个动作能表现出来多种形态,比如对于吃饭来说,人吃饭和猫吃饭所表现的形态动作是不同的。
根据继承的类型兼容原则,父类指针可以直接指向子类对象,父类引⽤可以直接引用子类对象。
对于有同名成员函数的父类和子类来说,如果类型为父类对象的指针(或引用),分别在指向父类对象和指向子类对象时调用该函数,调用的是各自指向类型的成员函数,而不是变量本身类型(父类类型)的成员函数,那么这个就是类的多态,这也叫做动态绑定。
- 这里的多态指的动态多态,也叫运行时多态,即在运行时根据传递对象的不同而动态地选择对应的函数地址。
- 函数重载 和 运算符重载属于静态多态,就是编译时多态,在编译时就已经确定了是使用哪个函数地址。
(2)子类和父类存在同名函数就能实现多态吗?
问题引出:如果子类定义了与父类中原型相同的函数会发生什么?
子类存在与父类相同的函数(函数名、参数和返回值相同) ,可以通过编译。
结论:用指针或者引用去调用子类中与父类同名的函数时,始终是调用父类的函数,因此不能实现多态。
示例代码
#include <iostream>
class Parent
{
public:
Parent(int a)
{
this->a = a;
std::cout << "generate Parent a=" << a << std::endl;
}
public:
void print()
{
std::cout << "print Parent a=" << a << std::endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
std::cout << "generate Child b=" << b << std::endl;
}
public:
void print()
{
std::cout << "print Child b=" << b << std::endl;
}
protected:
private:
int b;
};
void PrintObject(Parent *base)
{
base->print();
}
void PrintObject(Parent &base)
{
base.print();
}
void PrintObject2(Parent base)
{
base.print();
}
int main()
{
Parent *base = NULL;
std::cout << "---------------------------" << std::endl;
Parent parent1(20);
std::cout << "---------------------------" << std::endl;
Child child1(30);
// 1.用指针
std::cout << "---------------------------" << std::endl;
base = &parent1;
base->print(); //执行谁的函数?父类的
base = &child1;
base->print(); //执行谁的函数? 父类的
// 2.用引用
std::cout << "---------------------------" << std::endl;
Parent &base2 = parent1; //执行谁的函数?父类的
base2.print();
Parent &base3 = child1; //执行谁的函数?父类的
base3.print();
// 3.用函数调用
std::cout << "---------------------------" << std::endl;
PrintObject(&parent1);
PrintObject(&child1);
PrintObject(parent1);
PrintObject(child1);
// 普通函数调用
PrintObject2(parent1);
PrintObject2(child1);
return 0;
}
运行结果
---------------------------
generate Parent a=20
---------------------------
generate Parent a=10
generate Child b=30
---------------------------
print Parent a=20
print Parent a=10
---------------------------
print Parent a=20
print Parent a=10
---------------------------
print Parent a=20
print Parent a=10
print Parent a=20
print Parent a=10
print Parent a=20
print Parent a=10
从运行结果可以看出,对于同名函数print,无论是指针,还是引用,还是函数调用,指向父类对象和子类对象的base,在调用print函数时,都是在调用父类的print函数,因此没有实现多态。
(3)用虚函数实现多态(给同名函数加上virtual关键字)
实际需求:父类对象调用父类函数,子类对象需调动子类的同名函数。
多态的含义:一种调用语句,有多种表现形态。同名函数根据调用对象来区分调用具体哪一个函数。只需要在同名函数前加上virtual关键字即可。
-
父类的同名函数必须要加virtual关键字
-
子类的同名函数virtual关键字可写可不写
总结:父类和子类同名函数要实现多态,需同时满足两个条件:
- 用指针或者引用
- 同名成员函数加上virtual关键字变成虚函数
示例代码
#include <iostream>
class Parent
{
public:
Parent(int a)
{
this->a = a;
std::cout << "generate Parent a=" << a << std::endl;
}
public:
virtual void print() // 父类的同名函数必须要加virtual关键字
{
std::cout << "print Parent a=" << a << std::endl;
}
protected:
private:
int a;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
std::cout << "generate Child b=" << b << std::endl;
}
public:
// virtual void print()
void print() // 子类的同名函数virtual关键字可写可不写
{
std::cout << "print Child b=" << b << std::endl;
}
protected:
private:
int b;
};
void PrintObject(Parent *base)
{
base->print();
}
void PrintObject(Parent &base)
{
base.print();
}
void PrintObject2(Parent base)
{
base.print();
}
int main()
{
Parent *base = NULL;
std::cout << "---------------------------" << std::endl;
Parent parent1(20);
std::cout << "---------------------------" << std::endl;
Child child1(30);
// 1.用指针
std::cout << "---------------------------" << std::endl;
base = &parent1;
base->print(); //执行谁的函数?父类的
base = &child1;
base->print(); //执行谁的函数? 子类的
// 2.用引用
std::cout << "---------------------------" << std::endl;
Parent &base2 = parent1; //执行谁的函数?父类的
base2.print();
Parent &base3 = child1; //执行谁的函数?子类的
base3.print();
// 3.用函数调用
std::cout << "---------------------------" << std::endl;
PrintObject(&parent1);
PrintObject(&child1);
PrintObject(parent1);
PrintObject(child1);
// 普通函数调用
PrintObject2(parent1); //执行谁的函数?父类的
PrintObject2(child1); //执行谁的函数?父类的
return 0;
}
运行结果
---------------------------
generate Parent a=20
---------------------------
generate Parent a=10
generate Child b=30
---------------------------
print Parent a=20
print Child b=30
---------------------------
print Parent a=20
print Child b=30
---------------------------
print Parent a=20
print Child b=30
print Parent a=20
print Child b=30
print Parent a=20
print Parent a=10
从运行结果可以看出,父类和子类同名函数要实现多态,需同时满足两个条件:
- 用指针或者引用
- 同名函数加上virtual关键字变成虚函数
(4)多态与框架
面向对象3大概念:
- 封装。突破了C语言函数的概念。用类做函数参数的时候,可以使用对象的属性和对象的方法,而且可以结合反复使用改变。
- 继承。代码复用 。。。。我复用原来写好的代码。。。
- 多态。多态可以提前搭好框架,80年代写了⼀个框架,90年代继续在这个基础上写代码。
多态与框架实现了子任务的调用者和实现者的分离,这被称为"解耦合"。框架是子任务的调用者,子任务的具体开发由实现者操作,各自干各自的,降低了耦合度,所以被称为"解耦合"。
#include <iostream>
class GrandParent
{
public:
virtual int asset()
{
return 10;
}
};
class House
{
public:
virtual int price()
{
return 15;
}
};
class Parent : public GrandParent
{
public:
virtual int asset()
{
return 20;
}
};
class Uncle : public GrandParent
{
public:
virtual int asset()
{
return 20;
}
};
class ImproveHouse: public House
{
public:
virtual int price()
{
return 100;
}
};
// 1.普通使用
void main1()
{
GrandParent grand_parent;
Parent parent;
Uncle uncle;
House house;
if (grand_parent.asset() > house.price())
std::cout << "can buy" << std::endl;
else
std::cout << "can not buy" << std::endl;
if (parent.asset() > house.price())
std::cout << "can buy" << std::endl;
else
std::cout << "can not buy" << std::endl;
return;
}
// 2.用多态
// 用PlayObj函数给对象搭建舞台,让对象唱戏 看成一个框架
// 这个框架 能把我们后来人写的代码,给调用起来
void PlayObj(GrandParent *gparent, House *house)
{
// gparent->asset()函数调用会有多态发生
if (gparent->asset() > house->price())
std::cout << "can buy" << std::endl;
else
std::cout << "can not buy" << std::endl;
}
void main2()
{
GrandParent gparent;
House hose;
PlayObj(&gparent, &hose); // can not buy
// 这个框架 能把我们后来人写的代码,给调用起来
Parent par;
Uncle uncle;
PlayObj(&par, &hose); // can buy
PlayObj(&uncle, &hose); // can buy
return;
}
int main()
{
// main1();
main2();
return 0;
}
(5)总结多态成立的条件
三个条件:
- 要有继承
- 要有函数重写,且用virtual修饰为虚函数
- 要有父类指针(父类引用)指向子类对象的函数框架
多态是设计模式的基础,多态是框架的基础。
(6)函数重载和函数重写的区别
函数重载
- 在同一个类的内部进行
- 重载是在编译期间根据参数类型和个数决定函数调用
函数重写
- 发生于父类与子类之间,并且父类与子类中的函数必须有完全相同的原型
- 使用virtual声明之后能够产生多态(也叫做多态重写,不变成虚函数的话,为非多态重写)
注意:在子类和父类之间,如果存在同名但参数或返回值不同的函数,则子类无法重载父类的函数。即子类调用该函数时,只会在子类内部去寻找是否有符合的函数,而不会在父类中区寻找。
示例代码理解:
#include <iostream>
using namespace std;
class Parent
{
public:
void func()
{
cout << "parent func()" << endl;
}
void func(int i, int j)
{
cout << "parent func(int i, int j)" << endl;
}
};
class Child: public Parent
{
public:
void func()
{
cout << "child func()" << endl;
}
void func(int i)
{
cout << "child func(int i)" << endl;
}
};
int main()
{
Child child;
child.func(); // child func()
child.func(1); // child func(int i)
//child.func(2, 3); // error,因为子类不会重载父类的函数
return 0;
}
(7)构造函数可以是虚函数吗
不能。
虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。
而构造函数是在创建对象时自己主动调用的,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数)。
并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。
(8)构造函数中调用虚函数,能实现多态吗
构造函数中调用虚函数能发生多态吗?
能的。
执行谁的构造函数时就调用谁的虚函数。比如子类初始化时,
先执行父类的构造函数,构造函数里若有虚函数的调用,则调用父类虚函数
再执行子类的构造函数,构造函数里若有虚函数的调用,则调用子类虚函数
示例代码
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
print();
}
virtual void print()
{
cout << "I am father" << endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int a = 0, int b = 0) : Parent(a)
{
this->b = b;
print();
}
virtual void print()
{
cout << "I am child" << endl;
}
private:
int b;
};
int main()
{
Child c1;
// c1.print(); // I am child
// c1.Parent::print(); // I am father
return 0;
}
运行结果
I am father
I am child
(9)析构函数可以是虚函数吗
析构函数可以且常常是虚函数。
将父类的析构函数用virtual修饰,即为虚析构函数。
作用:能通过父类指针释放所有的子类资源 ,把所有的子类对象的析构函数都执行一遍。
示例代码
#include <iostream>
#include <cstring>
class A
{
public:
A()
{
p = new char[20];
strcpy(p, "obja");
printf("A()\n");
}
//~A()
virtual ~A()
//父类的析构函数必须virtual才能通过父类指针参数释放子类内存资源
{
delete[] p;
printf("~A()\n");
}
private:
char *p;
};
class B : public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objb");
printf("B()\n");
}
~B()
{
delete[] p;
printf("~B()\n");
}
private:
char *p;
};
class C : public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objc");
printf("C()\n");
}
~C()
{
delete[] p;
printf("~C()\n");
}
private:
char *p;
};
// 想通过父类指针 把 所有的子类对象的析构函数 都执行一遍
// 父类的析构函数必须要用virtual修饰
void howtodelete(A *base)
{
delete base;
}
int main()
{
C *myC = new C; // 执行构造函数
//delete myC; //直接通过子类对象释放全部资源 不需要写virtual
// 用引用或者指针,父类析构函数需要写virtual,才能释放全部资源
// 如果父类的析构函数没有virtual,只会执行父类的析构函数,即~A()
//如果父类的析构函数有virtual,会释放全部资源 即~C()、~B()、~A()
howtodelete(myC);
return 0;
}
运行结果
A()
B()
C()
~C()
~B()
~A()
(10)多态的实现原理(vptr指针与虚函数表)
理解静态联编和动态联编:
- 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用的就是静态联编。
- 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 语句和 if 语句是动态联编的例子。
关于继承同名函数时是静态联编还是动态联编:
- 不写virtual关键字,是静态联编。不管是指针还是引用,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象,所以编译器认为父类指针指向的是父类对象。
- 加上virtual后就是动态联编:迟绑定:在运行的时候,根据具体对象(具体的类型),执行不同对象的函数,表 现成多态。
多态实现原理:
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)
说明:
- 用类定义对象的时候 C++编译器会在对象中添加一个vptr指针
- 通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操 作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率 上,虚函数的效率要低很多。
- 出于效率考虑,没有必要将所有成员函数都声明为虚函数
证明vptr指针的存在:
示例代码
#include <iostream>
using namespace std;
class Parent1
{
public:
Parent1(int a = 0)
{
this->a = a;
}
void print()
{
cout << "I am father" << endl;
}
private:
int a;
};
class Parent2
{
public:
Parent2(int a = 0)
{
this->a = a;
}
virtual void print()
{
cout << "I am father" << endl;
}
private:
int a;
};
int main()
{
cout << "sizeof(Parent1) = " << sizeof(Parent1) << endl; //4
cout << "sizeof(Parent2) = " << sizeof(Parent2) << endl; //16
return 0;
}
运行结果
sizeof(Parent1) = 4
sizeof(Parent2) = 16
通过运行结果可以看出,Parent2就比Parent1仅仅多了一个virtual,但内存占用却增加了,这是因为vptr指针的存在。
(11)纯虚函数和抽象类
基本概念
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,这要求任何派生类都定义自己的版本。
纯虚函数说明形式:
virtual 类型 函数名(参数表) = 0;
纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)
一个具有纯虚函数的基类称为抽象类。抽象类也被称为接口类。
抽象类注意事项:
- 抽象类不能建立对象
- 抽象类不能作为返回对象
- 抽象类不能作为参数类型
- 可以声明抽象类的指针
- 可以声明抽象类的引用
class point{/*...*/};
class shape
{
public:
point center;
public:
point where(){return center;}
void move(point p){center=p;draw();}
virtual void rotate(int) = 0; // 纯虚函数
virtual void draw() = 0; // 纯虚函数
};
shape x; // error
shape *p; // ok
shape f(); // error
void g(shape); // error
shape &h(shape &); // ok
对于派生类,要成为非抽象类,必须对基类的纯虚函数作具体的定义
class point{/*...*/};
class shape
{
public:
point center;
public:
point where(){return center;}
void move(point p){center=p;draw();}
virtual void rotate(int) = 0; // 纯虚函数
virtual void draw() = 0; // 纯虚函数
};
class circle: public shape
{
public:
int radius;
public:
void rotate(int)
{
// 提供具体定义
}
void draw()
{
// 提供具体定义
}
};
实际工程经验证明
- 接口类只是一个功能说明,而不是功能实现。
- 子类需要根据功能说明定义功能实现。
- 多重继承接口不会带来二义性和复杂性等问题
接口类示例
#include <iostream>
using namespace std;
#include <cmath>
class Figure
{
public:
//约定一个统一的接口,让子类去实现
virtual void getArea() = 0; //纯虚函数
protected:
private:
};
class Circle : public Figure
{
public:
Circle(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea()
{
cout << "circle area is " << 3.14 * a * b << endl;
}
private:
int a;
int b;
};
class Tri : public Figure
{
public:
Tri(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
}
virtual void getArea()
{
int p = (a + b + c) / 2;
float s = sqrt(p * (p - a) * (p - b) * (p - c));
cout << "Tri area is " << s << endl;
}
private:
int a;
int b;
int c;
};
class Square : public Figure
{
public:
Square(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea()
{
cout << "Square area is " << a * b << endl;
}
private:
int a;
int b;
};
void objplay(Figure &obj)
{
obj.getArea();
}
void objplay(Figure *base)
{
base->getArea(); //会发生多态
}
int main()
{
// Figure f; //抽象类不能被实例化
Circle myc(3, 5);
objplay(myc);
Tri *trip = new Tri(3, 4, 5);
objplay(trip);
delete trip;
Square mysq(5, 6);
objplay(mysq);
return 0;
}
end