类对象与类指针_虚函数与多态

来源:互联网 发布:广发信德投资 知乎 编辑:程序博客网 时间:2024/06/15 18:11

基类

class Shape {public:    Shape() {        cout << "construct Shape" << endl;    }    ~Shape() {        cout << "deconstruct Shape" << endl;    }    virtual double calArea() {        cout << "Shape calArea" << endl;        return 0;    }};

子类

class Circle : public Shape {public:    Circle() {        cout << "construct Circle" << endl;    }    ~Circle() {        cout << "deconstruct Circle" << endl;    }    //此处的virtual不是必须的,如果不加,系统会自动加上,    //为了代码的易读性,推荐加上    virtual double calArea(){        cout << "Circle calArea" << endl;        return 0;    }};


类对象与类指针对比

类的对象: Circle circle;
类的指针: Circle* p_circle = new Circle();

类对象通过 “ . ”调用函数:

circle.calArea();

类指针通过“ -> ”调用函数:

p_circle->calArea();


类的对象用的是栈内存,是个局部的临时变量。
类的指针:用的是堆内存,是个永久变量,除非你释放它。

为什么有了类的对象还要存在类的指针这种用法呢?

  1. 指针可以实现多态,类的对象不行
  2. 在函数调用,尽量传引用或是指针参数。不管你的对象或结构参数多么庞大,用指针,传过去的是一个地址,就是4个字节。如果用对象,参数传递占用的资源就太大了。

什么是多态?
相同对象收到不同消息或不同对象收到相同消息时产生的不同的动作。


类的对象调用的函数为其本身的成员函数:

// main1int main() {    Circle c;    c.calArea();    cout << endl;    Shape s;    s.calArea();    cout << enld;}

******* 执行结果 ***********
construct Shape
construct Circle
Circle calArea

construct Shape
Shape calArea

deconstruct Shape //析构Shape的对象s
deconstruct Circle
deconstruct Shape

注意,上述结果说明:

  1. 构造子函数前先调用父类的构造函数(析构过程相反,先调用子类的析构函数再调用父类的)
  2. 类的对象调用的函数为其本身的成员函数:


注意,基类Shape中的calArea( )函数是虚函数(virtual),如果不声明为虚函数,执行以下代码的结果:

// main2int main() {    Circle c;    c.calArea();    cout << endl;    Shape* p = new Circle();    p->calArea();    delete p;    //调用delete才析构,否则不析构    cout << endl;}

******* 执行结果 ***********
construct Shape
construct Circle
Circle calArea

construct Shape
construct Circle
Shape calArea // 指针p调用的是父类的函数

deconstruct Shape // 【1】 delete p时,指针p只调用了父类的析构函数,没有析构子类,内存泄漏

deconstruct Circle
deconstruct Shape


父类Shape中的calArea函数声明为virtual后,再执行main2 代码,结果如下:

******* 执行结果 ***********
construct Shape
construct Circle
Circle calArea

construct Shape
construct Circle
Circle calArea // 指针p调用的是子类的函数

deconstruct Shape

deconstruct Circle
deconstruct Shape


为避免【1】中所说的内存泄漏问题,把父类的析构函数声明为虚函数即可:

~virtual Shape(){    cout << "deconstruct Shape" << endl;}

这样父类指针指向的是哪个对象,哪个对象的构造函数就会先执行,然后执行父类的构造函数。销毁的时候子类的析构函数也会执行。

为什么virtual可以实现多态

静态绑定:编译时绑定,通过对象调用。程序执行之前,编译器就已经解析出该调用哪个函数。
动态绑定:运行时绑定,通过地址实现(虚函数表)。程序在运行的时候才解析该调用哪个函数。

虚函数可以实现动态绑定,要触发动态绑定,需满足两个条件:

  1. 只有虚函数才能进行动态绑定,非虚函数不进行动态绑定。
  2. 必须通过基类类型的引用或指针进行函数调用。

virtual在函数中的使用限制

  • 普通函数不能是虚函数,也就是说这个函数必须是某个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
  • 静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
  • 内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
  • 构造函数不能是虚函数,否则会出现编译错误。

多态的实现原理

虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。

当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。

如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。

函数的覆盖和隐藏

父类和子类出现同名函数称为隐藏。

  • 父类对象.函数函数名(…); //调用父类的函数
  • 子类对象.函数名(…); //调用子类的函数
  • 子类对象.父类名::函数名(…); //子类调用从父类继承来的函数。

父类和子类出现同名虚函数称为覆盖
- 父类指针=new 子类名(…); 父类指针->函数名(…);//调用子类的虚函数


纯虚函数

纯虚函数定义:
纯虚函数没有函数体,同时在定义的时候函数名后面要加“=0”。

class Shape{public:    virtual  double calcArea()//虚函数    {}    virtual  double calcPerimeter()=0;//纯虚函数};

含有纯虚函数的类成为抽象类抽象类不能被实例化,继承该抽象类的子类也可以是抽象类,抽象类子类只有把抽象类当中的所有的纯虚函数都做了实现才可以实例化对象。

如果在抽象类当中仅含有纯虚函数而不含其他任何东西,我们称之为接口类

  • 没有任何数据成员
  • 仅有成员函数
  • 成员函数都是纯虚函数
原创粉丝点击