旅游网站开发工程师,网站改版seo方案,网站建设 599,乡镇网站建设和培训目录
一.习题1#xff1a; 解决下列测试代码所出现的问题
测试用例1#xff1a;
测试用例2#xff1a;
代码改进#xff1a;
习题1总结#xff1a; 二.习题2. 求类对象的大小
三.习题3#xff1a;
代码解析 #xff1a; 解析图#xff1a;
四.习题4#xff…目录
一.习题1 解决下列测试代码所出现的问题
测试用例1
测试用例2
代码改进
习题1总结 二.习题2. 求类对象的大小
三.习题3
代码解析 解析图
四.习题4 代码解析 一.习题1 解决下列测试代码所出现的问题 class Person {
public:~Person() {cout ~Person()析构函数: _p endl;delete[] _p;}
public:int* _pnew int[10];
};class Student :public Person {
public:~Student() {cout ~Student()析构函数: _s endl;delete[] _s;}
public:int* _snew 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* ptr2new 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* _pnew int[10];
};class Student :public Person {
public:virtual ~Student() { //构成重写cout ~Student()析构函数: _s endl;delete[] _s;}
public:int* _snew 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字节再进行内存对齐841212是最大对齐数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;} 请计算最终的结果选择下面正确的选项 Ap1 p2 p3 Bp1 p2 p3 Cp1 p3 ! p2 Dp1 ! 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的首部所以p1p3p2指向的位置!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;
} 该题运行的结果是什么 AA-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* p3new B; 指针(p3) 的调用方向是与指向的对象(new B)有关的所以隐式的this指针调用的func函数是B类的func函数所以是 B-(多态调用)但val的参数采用的仍是A类的val缺省值原因this指针类型是A*所以答案为B-1 ——有些坑人这道题。