首页 > 试题广场 >

在32位环境下,以下代码输出的是( )

[单选题]
32位环境下,以下代码输出的是()
#include<stdio.h>
class A {
public:
    A(){ printf("A");}
    ~A(){ printf("~A");}
};
class B : public A {
public:
    B(){ printf("B");}
    ~B(){ printf("~B");}
};

int main() {
    A *c = new B[2];
    delete[] c;
    return 0;
}
  • ABAB~A~A
  • ABAB~B~A~B~A
  • ABAB~B~A
  • ABAB~A~B~A~B
在C++中,析构函数的作用是:当一个对象被销毁时,调用析构函数对类对象和对象成员进行释放内存资源。
当我们定义一个指向派生类类型对象指针时,构造函数按照从基类到派生类的顺序被调用,但是当删除指向派生类的基类指针时,派生类的析构函数没有被调用,只是调用了基类的析构函数,此时派生类将会导致内存泄漏
我们需要将基类的析构函数声明为虚函数,此时在调用析构函数的时候是根据ptr指向的具体类型来调用析构函数,此时会调用派生类的析构函数。
发表于 2017-08-25 14:17:48 回复(0)
即使把析构函数定义为virtual
依然会无法调用到派生类的析构函数
因为数组的多态会导致未定义的行为

编译器需要建立起遍历数组来调用析构函数的代码
这样他不得不先确定数组的大小
调用如下语句时
//p为指向基类的指针
delete [] p;
编译器把指针p指向的静态类型的大小析构函数指针一并传给delete运算符
而这二者都与实际不符
所以最终没有调用到派生类的destructor
大家会困惑
为何delete数组时不能像delete单个对象一样
使用虚拟化 动态决议删除的实际对象类型
这里涉及数组的实例化机制
数组的元素数量事实上在初始化时被存储于一个hash map中
hash key就是数组首元素地址
但是 并未保存元素的大小和元素的构造函数和析构函数指针等内容
销毁数组的时候
对数组中的每个元素迭代调用类似如下全局函数
void * vec_delete (
    //数组首地址
    void *array,
    //元素大小
    size_t elem_size,
    //元素个数
    int elem_count,
    //析构函数指针
    void (*destructor)(void *)
);
这里的array传入p
因为p是个基类指针
当它是一个非第一直接基类时
与一个正确的派生类指针的地址相比
会有一定的偏移
elem_size和destructor都是根据p的静态类型来获取
显然都是不符合实际的
elem_count则是通过hash map取出
由于hash key是数组首地址
这个都不一定是对的
那么就不一定能取得正确的elem_count
甚至当对应的hash槽位为空时根本取不到
以上所言种种变数导致了使用基类指针来做delete[]
是一个未定义行为
只需要对题目的示例程序稍作改变立刻能玩崩:

class Base {

public:

Base() {printf("Base\n");}

~Base() {printf("~Base\n");}

int i = 1;

};


class A : virtual public Base

{

public:

A() { printf("A\n");}

~A(){ printf("~A\n");}

};


class B : virtual public Base

{

public:

B() { printf("B\n");}

~B(){ printf("~B\n");}

};


class C : public A, public B

{

public:

C() { printf("C\n");}

~C(){ printf("~C\n");}

};

int main()

{

C *c = new C[2];

/*

分别将如下各变体拿到注释外执行:

Base *p = c;

A *p = c;

B *p = c;

*/

printf("%p %p\n", p, c);

delete[] p;

}

我们修改了原来的单继承为多继承+虚继承
诸位可以用示例代码中的注释中的变体程序一一尝试
各种崩溃的结果可以玩出花来
原来的A *p如果说还能碰巧因为和C *c的初始地址对齐而勉强执行成功
而新来的非第一直接基类B *p和虚基类Base *p则会带来不一样的初始地址
和酸爽的崩溃
触目惊心之余得来一句逆耳忠言
别用基类指针释放动态数组内存



下面对比一下单个对象的delete
delete p;
这里如果p的析构函数是个非虚函数
那么就直接调用了
如果是个虚析构函数
编译器会直接进入p所指对象的虚表里
通过thunk技术检索得到派生类的析构函数指针
并将this指针完成一定偏移指向派生类对象的初始地址
所以单个变量的析构过程是可以动态化的

进一步思考,假如我们在题目示例代码的程序层面上做些调整;
    const static int N = 2;
    A* c = new B[N];
    for (int i=0; i<N; ++i) {
        delete ((B*)c+i);
    }
这样也是不行的
C++标准表示,对数组调用delete而不是delete[]运算符会导致未定义行为
以上代码会出现一个运行时错误:
ABAB~B~A
TestCPP(1739,0x10012c3c0) malloc: *** error for object 0x1004067e8: pointer being freed was not allocated
可见已经析构到了第一个对象的基类了
但是显示的错误是重复delete
这就是未定义行为

所以在对数组元素执行虚函数时
还是要用派生类的指针来delete
B* p = new B[N];
delete[] p;

编辑于 2018-04-09 11:58:04 回复(11)

#include<stdio.h>

class A

{

    public:

    A(){ printf("A");}

    ~A(){ printf("~A");}

};

class B: public A

{

    public:

        B(){ printf("B");}

        ~B(){ printf("~B");}

};

    

int main()

{

    A* c = new B[2];

    delete[] c;

    return 0;

}

ABAB~A~A

#include<stdio.h>

class A

{

    public:

    A(){ printf("A");}

    virtual ~A(){ printf("~A");}

};

class B: public A

{

    public:

        B(){ printf("B");}

        ~B(){ printf("~B");}

};

    

int main()

{

    A* c = new B[2];

    delete[] c;

    return 0;

}
ABAB~A~A
3
#include<stdio.h>

class A

{

    public:

    A(){ printf("A");}

    ~A(){ printf("~A");}

};

class B: public A

{

    public:

        B(){ printf("B");}

        ~B(){ printf("~B");}

};

    

int main()

{

    B* c = new B[2];

    delete[] c;

    return 0;

}

ABAB~B~A~B~A

4

#include<stdio.h>

class A

{

    public:

    A(){ printf("A");}

    virtual ~A(){ printf("~A");}

};

class B: public A

{

    public:

        B(){ printf("B");}

        ~B(){ printf("~B");}

};

    

int main()

{

    B* c = new B[2];

    delete[] c;

    return 0;

}
 ABAB~B~A~B~A

结合程序1、2可知,不管基类析构函数加不加virtual,只要是声明的是基类的指针,即使实际指向的实例是派生类的,也不会调用派生类的析构方法。
由程序3、4可知,不管基类析构函数加不加virtual,只要是声明的是派生类的指针,并且实际指向的实例是派生类的,会先调用派生类的析构方法,然后级联调用父类的析构方法。


发表于 2017-08-29 11:21:45 回复(12)
因为是delete []c,c的类型是A,所以只会调用A的析构函数,如果想默认调用子类B的析构函数,必须把父类的析构函数声明为虚析构函数
发表于 2017-08-18 17:42:20 回复(1)

继承和派生中的 类型兼容规则

  • 概念
    • 在需要基类对象的任何地方,都可以使用共有派生类的对象来替代
    • 且替代之后,派生类的对象就可以作为基类对象使用,但是只能使用从基类继承的成员。
    • 也就是替代后的派生类仅仅发挥出基类的作用

那么和多态的关系呢?

  • 多态的设计方法,可以保证类型兼容规则的前提下,基类、派生类可以分别以不同的方式来响应相同的消息
  • 虚析构函数
    • 如果一个类的虚构函数为虚函数时,那么它派生而来的所有子类的析构函数也是虚函数
    • 析构函数设置为虚函数后,在使用指针引用时可以动态绑定,实现运行时的多态,可以保证在使用基类的指针 就能够调用 适当的析构函数 对不同的 对象进行清理工作
    • 如果有可能通过基类的指针调用对象的析构函数(通过delete),就需要让基类的虚构函数为虚函数,否则会产生不确定的结果。
#include<stdio.h>

class A
{
public:
A(){ printf(“A”);}
~A(){ printf(“~A”);
}; // 基类A  析构函数不是虚函数
class B:public A
{
public;
B(){ printf(“B”);}
~B(){ printf(“~B”);}
};//派生类 B

int main()
{
A*c = new B[2];  //定义了一个 A类的对象指针 ,只不过用派生类B来替代  也就是派生类动态分配的内存
//初始化从基类开始,然后是派生类 ABAB
delete[] c;// 释放内存,通过的是 基类指针,调用的是基类的析构函数,那么派生类对象中的动态内存分配的空间没有释放,造成了内训泄露
//派生类对象的内存 在对象消失后,既不能被本程序使用,也没有被释放。
//对于内存需求量大,长期连续运行的内存来说,如果持续发生这样的错误是很危险的,最终导致内存不足引起程序的终止
return 0;
}
发表于 2019-08-31 18:34:23 回复(0)
不管基类析构函数加不加virtual,只要声明的是基类的指针,即使实际指向的实例是派生类的,也不会调用派生类的析构函数;
不管基类析构函数加不加virtual,只要声明的是派生类的指针,并且实际指向的是派生类的,会先调用派生类的析构函数,然后级联调用父类的析构方法;
发表于 2018-08-30 18:12:36 回复(0)
这题感觉得看编译器
发表于 2020-01-21 22:08:24 回复(0)
// 1
/*
#include<stdio.h>
class A
{
public:
A(){ printf("A");}
~A(){ printf("~A");}
};
class B: public A
{
public:
B(){ printf("B");}
~B(){ printf("~B");}
};

int main()
{
A* c = new B[2];
delete[] c;// ABAB~A~A
return 0;
}
*/



// 2
/*
#include<stdio.h>
class A
{
public:
A(){printf("A");}
virtual ~A(){printf("~A");}
};
class B: public A
{
public:
B(){printf("B");}
~B(){printf("~B");}
};

int main()
{
A* c = new B[2];
delete[] c; // ABAB~B~A~B~A
return 0;
}
*/

//3
/*
#include<stdio.h>
class A
{
public:
A(){printf("A");}
~A(){printf("~A");}
};
class B: public A
{
public:
B(){printf("B");}
~B(){printf("~B");}
};

int main()
{
B* c =new B[2];
delete[] c;// ABAB~B~A~B~A
return 0;
}
*/

//4
/*
#include<stdio.h>
class A
{
public:
A(){ printf("A");}
virtual ~A(){ printf("~A");}
};
class B: public A
{
public:
B(){ printf("B");}
~B(){ printf("~B");}
};

int main()
{
B* c = new B[2];
delete[] c;//ABAB~B~A~B~A
return 0;
}
*/
// 基类的析构函数是虚函数,delete销毁对象时,执行顺序:派生类的析构函数->对象成员的析构函数->基类的析构函数

发表于 2018-07-12 21:05:11 回复(1)
按照其声明类型析构
发表于 2017-08-18 13:47:19 回复(0)
释放基类指针调用的是基类的析构函数,不会调用派生类的析构函数,如果想要调用派生类的析构函数可以将基类的析构函数定义为虚函数
发表于 2023-12-27 19:23:36 回复(0)
这就是为什么要用虚析构了,在运行多态的情况下,如果不是虚函数的话只调用父类的析构
发表于 2019-09-04 17:07:54 回复(0)
析构函数不是虚函数,每次子类析构都会调用父类析构函数
发表于 2018-08-31 10:28:18 回复(0)
new的B对象所以构造是ABAB delete的是A类指针所以是AA
发表于 2024-09-14 10:40:49 回复(0)
父类没加上virtual就是切片,调用的就是父类的析构
加上virtual,析构函数多态,结束的时候会自动调用父类的析构函数,不需要自己显示调用


发表于 2024-04-01 14:36:02 回复(0)
因为析构函数没有定义为虚函数,所以delete时只会析构变量类型本身的析构函数
发表于 2023-08-24 23:27:15 回复(0)
发表于 2023-06-10 10:06:44 回复(0)
#include<stdio.h> class A { public: A(){ printf(“A”);} ~A(){ printf(“~A”); }; // 基类A 析构函数不是虚函数 class B:public A { public; B(){ printf(“B”);} ~B(){ printf(“~B”);} };//派生类 B int main() { A*c = new B[2]; //定义了一个 A类的对象指针 ,只不过用派生类B来替代 也就是派生类动态分配的内存 //初始化从基类开始,然后是派生类 ABAB delete[] c;// 释放内存,通过的是 基类指针,调用的是基类的析构函数,那么派生类对象中的动态内存分配的空间没有释放,造成了内训泄露 //派生类对象的内存 在对象消失后,既不能被本程序使用,也没有被释放。 //对于内存需求量大,长期连续运行的内存来说,如果持续发生这样的错误是很危险的,最终导致内存不足引起程序的终止 return 0; }</stdio.h>
发表于 2022-11-28 14:48:00 回复(0)
在C++中,析构函数的作用是:当一个对象被销毁时,调用析构函数对类对象和对象成员进行释放内存资源。 当我们定义一个指向派生类类型对象指针时,构造函数按照从基类到派生类的顺序被调用,但是当删除指向派生类的基类指针时,派生类的析构函数没有被调用,只是调用了基类的析构函数,此时派生类将会导致内存泄漏 。 我们需要将基类的析构函数声明为虚函数,此时在调用析构函数的时候是根据ptr指向的具体类型来调用析构函数,此时会调用派生类的析构函数。
发表于 2022-09-16 17:25:19 回复(0)
析构函数不是virtual
发表于 2022-09-16 14:53:56 回复(0)
在C++中,析构函数的作用是:当一个对象被销毁时,调用析构函数对类对象和对象成员进行释放内存资源。
当我们定义一个指向派生类类型对象指针时,构造函数按照从基类到派生类的顺序被调用,但是当删除指向派生类的基类指针时,派生类的析构函数没有被调用,只是调用了基类的析构函数,此时派生类将会导致内存泄漏
我们需要将基类的析构函数声明为虚函数,此时在调用析构函数的时候是根据ptr指向的具体类型来调用析构函数,此时会调用派生类的析构函数。
发表于 2022-07-21 16:52:36 回复(0)