目录
一.习题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 < p3C: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" ——有些坑人这道题。