C++多态练习题

目录

一.习题1: 解决下列测试代码所出现的问题

测试用例1:

测试用例2: 

代码改进: 

习题1总结:

 二.习题2.

         求类对象的大小

三.习题3:

代码解析 :

 解析图:

四.习题4: 

 代码解析:


一.习题1: 解决下列测试代码所出现的问题

class Person {
public:

	~Person() {
		cout << "~Person()析构函数: " <<_p<< endl;
		delete[] _p;
	}
public:
	int* _p=new int[10];
};

class Student :public Person {
public:
	 ~Student() {
		cout << "~Student()析构函数: "<<_s<< endl;
		delete[] _s;
	}
public:
	int* _s=new int[20];
};

        在上方代码中有一对父子类,父子类各有一个指向堆区空间的整型指针成员变量,那么意味着创建类对象会开辟空间。在这两个类中,都各有一个显式的析构函数。

 

测试用例1:

int main() {
	Person p1;
	Student s1;
    return 0;
}

        在测试用例中,创建了两个父子类对象。

运行结果:

        由上图结果可知:结果显示很正常,s1是子类对象,继承了父类的成员变量,那么析构的时候需要调用父类的析构函数,没什么问题。

测试用例2: 

int main(){   
    Person* ptr1 = new Person;
	Person* ptr2 = new Student;
	delete ptr1;
	delete ptr2;
    return 0;
    }

        在这个测试用例中,父类创建了两个指针对象,分别指向Person类型的堆区空间和Student类型的堆区空间。由于new了堆区空间,肯定需要释放了这两个指针指向的堆区空间。

运行结果:

        通过结果可知,ptr1指针对象的释放没啥问题,因为ptr1开辟的是Person类的空间,那么肯定会调用Person类的析构函数;而ptr2指针对象的释放出现了问题:它只析构了它自己,没有释放Student类的堆区空间,因为释放Student类的堆区空间会调用其析构函数,但结果显示没有调用!于是出现了内存泄漏。

        主要原因:当父类指针指向子类的时候(Person* ptr2=new Student),若父类析构函数不声明为虚函数,在编译器delete时,只会调用父类而不会调用子类的析构函数,从而导致内存泄露。!!!!!


         通过之前学习多态特性我们了解到:普通调用是和调用对象的类型有关,那么编译器认为我们创建的Person类两个指针对象在delete时,采用的是普通调用,所以调用的依据是与调用对象ptr2的类型(Person)有关,所以编译器就只调用了Person类的析构函数;
       而我们真实想要采用的是多态调用,所谓多态调用的依据是指针(ptr2)或者引用与指向的对象(new Student)有关。


        所以需要在父类和子类的析构函数上加上virtual,让析构函数变成虚析构,这样编译器就认定我们采用的是多态调用了!!!

        小知识扩展:任何类的析构函数虽然都是已~符号+各自类命名,但底层上编译器统一看作是destructor。

        该句解析:虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

        多态的条件有三个:

        1是在父类的函数上加virtual,形成虚函数;

        2是子类需要重写父类的虚函数,重写的要求是三同(同名、同参、同返回值类型)

        3.使用父类指针或者引用。

        所以现在父子类的析构函数都可以看作是三同函数,也都有virtual,有父类指针对象去开辟父类和子类的堆区空间,实现了多态调用。

代码改进: 

class Person {
public:
	virtual ~Person() {    
		cout << "~Person()析构函数: " <<_p<< endl;
		delete[] _p;
	}
public:
	int* _p=new int[10];
};

class Student :public Person {
public:
	virtual ~Student() {    //构成重写
		cout << "~Student()析构函数: "<<_s<< endl;
		delete[] _s;
	}
public:
	int* _s=new int[20];
};

        只有子类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。 

 运行结果:

 

习题1总结:


        首先我们需要确定,新创建的类(Person)是否会有后代。如果创建的新类肯定会有后代(Student类),那就声明析构函数为虚函数,以防万一有后代,可能会发生的内存泄露。有后代继承,是造成内存泄漏的首要条件。

        创建的新类具有后代是造成内存泄漏的先决条件,但这并不是触发条件。如果我们使用的指针不是父类指针引用子类指针,那么永远不会触发因为析构而产生的内存泄漏。所以父类指针指向、引用子类对象是触发析构函数内存泄漏的条件。

        当我们创建的新类会被继承,我们会用到父类指针指向子类指针,我们必须使用虚析构函数,所以以后每当遇到有后代的父类,就无脑加上virtual即可。

 


 二.习题2.

         求类对象的大小

class Base{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};


class Base2{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	double _f = 1;
	char _c = 'a';
};

class Base3{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

	 void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _i = 1;
	char _c = 'b';
};

int main() {

	Base b;
	cout << sizeof(Base) << endl;		//第一小问

	Base2 b2;
	cout << sizeof(Base2) << endl;		//第二小问

	Base3 b3;
	cout << sizeof(Base3) << endl;		//第三小问
	return 0;
}

三个小问的结果为:

8字节、24字节、12字节 

        结果解析:求Base类的大小,是需要看该类的成员变量的字节大小,注:只看成员变量,成员函数不会算进类的大小中的。

        在Base类中,成员变量有1个,为int类型,是4字节大小,根据内存对齐,所以总大小为4字节,但是类中还有虚函数,虚函数中因为有虚表指针(虚表指针是指向虚函数表的地址的)!虚函数表中存放着各个虚函数的地址)而指针是默认占4个字节的,加上共占8个字节,
// 并且还需要进行内存对齐,取最大的变量的字节倍数作为最终的类的字节大小,最大变量字节为4字节,所以8符合4的倍数,所以结果为8字节。


注:采用f11进行调试,你就可以看到Base的对象b中多出了一个虚表指针vfptr,它是指向虚函数表的地址的指针!!! 

        第二小题:Base2类的成员变量有俩,一个double-8字节,一个char-1字节,共9字节,根据内存对齐,最终大小得是8的倍数,所以补齐到16字节,因为还有虚表指针,占4字节,加上就是20字节,最终大小得是8的倍数,所以补齐到24字节,所以最终大小是24字节。

        第三小题:Base3类中有两个成员变量,一个int-4字节,一个char-1字节,内存对齐,补齐到8字节,但是此时Base3类中有两个虚函数,那么是不是意味着就会有两个虚表指针呢???



        答案:不是!!! 无论一个类中有多少个虚函数,而虚表指针只会有一个,尽管Base3中有俩虚函数,但在虚表指针指向的虚函数表(可以理解为一个指针数组,由指针指向的一个数组)中,有俩个地址这俩地址就代表着这两个虚函数的地址!虚函数的增多仅仅代表着虚函数表指针指向的虚函数表中多了两个虚函数的地址,只是表增大了,而不是虚表指针变多了!
        所以虚表指针-4字节,再进行内存对齐,8+4=12,12是最大对齐数4的倍数,所以结果为12字节 

强调:虚表指针在32位机器下是4字节大小,而在64位机器下是8字节大小,我测试的机器是32位的!所以是4字节


三.习题3:

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main() {
		Derive d;
		Base1* p1 = &d;
		Base2* p2 = &d;
	    Derive* p3 = &d;
		return 0;
			}

请计算最终的结果,选择下面正确的选项:
        A:p1 == p2 == p3    B:p1 < p2 < p3

        C:p1 == p3 != p2     D:p1 != p2 != p3 

代码解析 :

子类Derive继承了两个父类Base1,Base2,当Derive创建对象时,d的成员变量有三部分:
        第一部分是Base1继承过来的成员变量_b1;

        第二部分是Base2继承过来的成员变量_b2;
        第三部分就是自家类创建的成员变量内置类型 _d。


         在代码中,类Base1创建了一个指针,指向了子类对象d的地址,根据切割原理,子类对象d是向上赋值转换父类对象的,子类对象d会将从Base1那里继承来的成员变量切割出来值赋给父类Base1的对象,那么p1指向的就是子类对象从Base1那里继承的成员_b1的地址
       

        同理,Base2* p2指向的就是子类对象d从Base2那里继承的成员_b2的地址,而子类Derive又创建了一个指针,指向了子类对象d,那么p3指向的就是整个子类对象d的地址了。根据之前学的数组原理,当指针p1指向一整个数组时,其实指针指向的是数组的首地址,而p3指向数组的第一个元素时,指针指向的也是数组的首地址,p1与p3虽然指向的具体内容不同,但它们都指向了同一块地址。

        基于此,我们回到题中,p1和p3虽然指向的内容不一样,但都指向了对象d的首部,所以p1==p3,p2指向的位置!=p1和p3

 解析图:


四.习题4: 

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* p3 = new B;
	p3->test();
	cout << endl;

	return 0;
}

该题运行的结果是什么?
A:A->1    B. B->0        C. A->0      D. B->1  E.编译出错 

 代码解析:

       B类型创建了一个指针对象,指向一块B类型的地址空间    ,之后指针对象调用test()函数,这个test()函数是从A类继承过来的成员函数,在调用test()函数后,test中需要调用func()函数,而在调用func()函数的时候,这是一个陷阱,我们都会认为对象在调用的时候,用的是this指针,这个this指针是A类型的,虽然test()被B继承过来了,但是原封不动的继承过来,在继承前就是A* this指针才能调用,继承后仍然由A* this指针调用,千万不要以为是B* 本类的this指针去调用的test()函数!!!

  至此,多态的特性就体现出来了!

        之后,this指针就会采用多态调用!
        再温习一下:我们在学习多态前,采用的调用都是普通调用,该调用的原理是根据调用对象的类型有关。而学习了多态特性后,看题时就需要再多考虑一种新的调用——多态调用了。
        多态调用特性:某类的指针或者引用采用的调用方向 ->是与指向的对象有关的。

        如上代码: B* p3=new B; 指针(p3) 的调用方向是与指向的对象(=new B)有关的,所以隐式的this指针调用的func函数是B类的func函数,所以是 "B->"(多态调用),但val的参数采用的仍是A类的val缺省值(原因:this指针类型是A*),所以答案为:"B->1" ——有些坑人这道题。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,我可以为你提供一个简单的继承多态练习题,希望能帮助你巩固相关知识。 假设有一个图形类 Shape,其中包含一个纯虚函数 getArea(),用于计算该图形的面积。现在要定义两个子类,分别是矩形类 Rectangle 和圆形类 Circle。请完成以下任务: 1. 定义 Shape 类: ```c++ class Shape { public: virtual double getArea() = 0; // 纯虚函数 }; ``` 2. 定义 Rectangle 类,继承自 Shape 类,包含两个成员变量 width 和 height,以及实现 getArea() 函数的具体方法: ```c++ class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) { width = w; height = h; } double getArea() { return width * height; } }; ``` 3. 定义 Circle 类,同样继承自 Shape 类,包含一个成员变量 radius,以及实现 getArea() 函数的具体方法: ```c++ class Circle : public Shape { private: double radius; public: Circle(double r) { radius = r; } double getArea() { return 3.1415926 * radius * radius; } }; ``` 4. 在主函数中创建一个 Shape 指针数组,分别指向一个 Rectangle 和一个 Circle 对象,并计算它们的面积: ```c++ int main() { Shape* shapes[2]; shapes[0] = new Rectangle(4, 5); shapes[1] = new Circle(3); for (int i = 0; i < 2; i++) { cout << "Area of shape " << i << ": " << shapes[i]->getArea() << endl; delete shapes[i]; } return 0; } ``` 以上就是一个简单的继承多态练习题,希望能够帮助你巩固相关知识。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橙予清的zzz~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值