您现在的位置是:首页 >技术杂谈 >【C++】面试官:你小子,继承与多态的题你都会网站首页技术杂谈
【C++】面试官:你小子,继承与多态的题你都会
前言
上一篇文章我们详细了介绍了多态,用汇编一步一步的查看了多态的实现原理,讲解了这么多理论知识该做一些面试题了,这些面试题都是历年来大厂所出的笔试题,希望大家可以把我今天所讲的全部学懂~
一、理论知识类
7.关于虚函数说法正确的是( B)
A.被virtual修饰的函数称为虚函数
B.虚函数的作用是用来实现多态
C.虚函数在类中声明和类外定义时候,都必须加虚拟关键字
D.静态虚成员函数没有this指针
A:被virtual修饰的成员函数称为虚函数 B:虚函数的作用就是用来实现多态的,正确 C:virtual只需要在声明的时候加上,在类外实现不能加。D:静态成员函数不可以设置为虚函数
8.关于多态,说法不正确的是(C )
A.C++语言的多态性分为编译时的多态性和运行时的多态性
B.编译时的多态性可通过函数重载实现
C.运行时的多态性可通过模板和虚函数实现
D.实现运行时多态性的机制称为动态绑定
C错误的点在于运行时的多态性通过虚函数实现,与模板无关。
9.关于重载、重写和重定义的区别说法正确的是( AF)【不定项选择】
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
A:虚函数重写就是发生在继承体系中的,重定义就是隐藏,隐藏也是发生在继承体系中并且是在继承体系中的子类中。B:重载不可以在继承体系中,重载必须在同一个作用域。C:重载,重写,重定义都要求函数名相同,但是只要重写要求三同。D:重写和重定义是不同的概念。E:重定义是隐藏,重写是多态。F:重写的条件需要三同,而重定义对于成员函数而言只需要函数名相同就构成隐藏。
10.关于重载和多态正确的是 (B )
A.如果父类和子类都有相同的方法,参数个数不同, 将子类对象赋给父类对象后, 采用父类对象调用该同名方法时,实际调用的是子类的方法
B.选项全部都不正确
C.重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用
D.class A{ public: void test(float a) { cout << a; } }; class B :public A{ public: void test(int b){ cout << b; } }; void main() { A *a = new A; B *b = new B; a = b; a->test(1.1); } 结果是1
A:多态必须用父类的指针或引用。C:重载要求在同一个作用域,不能再两个类中 D:父类A指针存放A的对象,子类B指针存放B的对象,将子类指针给父类指针后调用test函数,由于test函数不是虚函数不构成多态,父类指针只能调用父类的方法,所以打印1.1
11.关于抽象类和纯虚函数的描述中,错误的是 ( D)
A.纯虚函数的声明以“=0;”结束
B.有纯虚函数的类叫抽象类,它不能用来定义对象
C.抽象类的派生类如果不实现纯虚函数,它也是抽象类
D.纯虚函数不能有函数体
D:纯虚函数可以有函数体,并且函数体内也可以实现一些内容。
12.假设A为抽象类,下列声明(B )是正确的
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
抽象类不可以实例化对象,只要是有A对象都错误,B答案中是指针类型,不管是什么类型的指针都是内置类型。
13.如果类B继承类A,A::x()被声明为虚函数,B::x()重写了A::x()方法,下述语句中哪个x()方法会被调用:(B ) B b;b.x();
A.A::x()
B.B::x()
C.A::x() B::x()
D.B::x() A::x()
因为b是一个子类对象,对象调用只能调用自己的函数。
14.关于不能设置成虚函数的说法正确的是(D )
A.友元函数可以作为虚函数,因为友元函数出现在类中
B.成员函数都可以设置为虚函数
C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写
D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数
A:友元函数不可以设为虚函数 B:成员函数static不可以设置为虚函数 C:静态成员函数不能设置为虚函数的原因是静态成员函数没有this指针。D:析构函数建议设置为虚函数是正确的。
15.要实现多态类型的调用,必须( D)
A.基类和派生类原型相同的函数至少有一个是虚函数即可
B.假设重写成功,通过指针或者引用调用虚函数就可以实现多态
C.在编译期间,通过传递不同类的对象,编译器选择调用不同类的虚函数
D.只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
A:必须将基类原型相同的函数设置为虚函数 B:必须是父类的指针或引用 C:是在运行期间(编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。)
16.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(B )
A.A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类中的内容完全一样,但是A类和B类使用的不是同一张虚表
A:父类对象和子类对象的前4个字节都是虚表地址 C:A类和B类是不同的类如果都有虚函数各自有各自的虚表 D:A类和B类不是同一个类内容一定不同。
17.假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:(B )
A.D类对象模型中包含了3个虚表指针
B.D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后
C.D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后
D.以上全部错误
首先这是多继承问题,D继承了B1 B2而且B1 B2都有虚函数所以他们都有虚表,所以D有2张虚表,因为先继承的B1所以B1的虚表是第一张,而派生类新增加的虚函数会放在第一张虚表的最后
(D类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表)
18.下面关于继承说法不正确的是(C)
A.继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展
B.继承体系中子类必须要体现出与基类的不同
C.子类对象一定比基类对象大
D.继承呈现了面相对象程序设计的层次结构,体现了有简单到复杂的认知过程
C:中错误的原因是如果基类实现了虚函数那么就会多出来一个虚表指针这个指针4/8字节,通常在子类没有比父类多出变量并且子类没有虚函数的时候子类就不一定比基类对象大了。
19.关于继承说法正确的是(D )
A.所有的类都可以被继承
B.Car(汽车)类和Tire(轮胎)类可以使用继承方式体现
C.继承是实现代码复用的唯一手段
D.狗是一种动物,可以体现出继承的思想
A:final说明的类不能被继承 B:car和tire类是has a 的关系,不能用继承应该用组合。(因为轮胎不能代表汽车,而狗继承了动物后不仅有自己独特的特征同时自己也有动物的所有特征)
20.下面关于访问权限与继承权限说法不正确的是( C)
A.访问权限和继承权限是不同的概念
B.访问权限和继承权限关键字上是一样的,但是出现位置不一样
C.如果是protected继承方式,基类public的成员变量能通过基类对象在类外直接访问
D.基类私有的成员变量在子类中都不能直接访问,因为没有被子类继承了
D选项错误的原因是子类也会继承基类的私有成员,但是因为权限问题访问不了基类的私有成员。
21.关于同名隐藏的说法正确的是(D )
A.同一个类中,不能存在相同名称的成员函数
B.在基类和子类中,可以存在相同名称但参数列表不同的函数,他们形成重载
C.在基类和子类中,不能存在函数原型完全相同的函数,因为编译时会报错
D.成员函数可以同名,只要参数类型不同即可,成员变量不能同名,即使类型不同
A:一个类中可以存在,比如函数重载 C:重写要求三同,就是函数原型完全相同的函数
22.下面说法正确的是(D )
A.派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化
B.派生类构造函数先初始化子类成员,再初始化基类成员
C.派生类析构函数不会自动析构基类部分成员
D.子类构造函数的定义有时需要参考基类构造函数
A:不显示调用编译器会默认调用基类的构造函数初始化基类部分(前提是基类一定要有默认的构造函数) B:派生类构造函数先初始化父类成员 C:派生类析构函数会在子类析构函数结束后自动调用父类的析构函数,完成先析构子,再析构父
23.关于派生类构造函数与析构函数说法正确的是( A)
A.在派生类对象构造时,先调用基类构造函数,后调用子类构造函数
B.在派生构造函数初始化列表的位置必须显式调用基类构造函数
C.在派生类对象销毁时,先调用基类析构函数,后调用子类析构函数
D.派生类的析构函数只需析构派生类的资源即可
这题与上题同理。
24.关于基类哪些成员被子类继承说法不正确的是(C )
A.静态成员函数
B.所有成员变量
C.基类的友元函数
D.静态成员变量在整个继承体系中只有一份
C:友元函数不可以被继承
25关于基类与派生类对象模型说法正确的是(E)
A.基类对象中包含了所有基类的成员变量
B.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量
C.子类对象中没有包含基类的私有成员
D.基类的静态成员可以不包含在子类对象中
E.以上说法都不对
A:静态成员变量属于所有类 B:同理静态成员变量 C:子类对象也会继承父类的私有成员 D:基类的静态成员一定不能包含在子类对象中
26.关于基类与子类对象之间赋值说法不正确的是(B )
A.基类指针可以直接指向子类对象
B.基类对象可以直接赋值给子类对象
C.子类对象的引用不能引用基类的对象
D.子类对象可以直接赋值给基类对象
只需要记住父类不能直接赋值给子类(可以动态转换)包括引用。
27.下面关于继承权限说法正确的是(C )
A.派生类在继承基类时,必须明确指定继承方式
B.Class定义的类,默认的访问权限是protected
C.struct定义的类,默认访问权限是public
D.子类没有继承基类私有的成员
A:可以不明确,默认私有继承 B:class的类默认访问权限为私有
28.关于以下菱形继承说法不正确的是(C )
class B {public: int b;};
class C1: public B {public: int c1;};
class C2: public B {public: int c2;};
class D : public C1, public C2 {public: int d;};
A.D总共占了20个字节
B.B中的内容总共在D对象中存储了两份
C.D对象可以直接访问从基类继承的b成员
D.菱形继承存在二义性问题,尽量避免设计菱形继承
A:D继承了C1,C1继承了B有b和c1两个变量一共八字节,C2继承了B有b和c2两个变量一共8字节,又因为D有自己的d变量,所以一共20字节。 C:D对象不可以直接访问从基类继承的b成员,因为b成员有两份这里有二义性的问题,需要前面加上域名限定符才能正确访问。
二、编程题选择类
1.下面代码输出结果:( D)
class A
{
public:
void f(){ cout<<"A::f()"<<endl; }
int a;
};
class B : public A
{
public:
void f(int a){cout<<"B::f()"<<endl;}
int a;
};
int main()
{
B b;
b.f();
return 0;
}
A.打印A::f()
B.打印B::f()
C.不能通过编译,因为基类和派生类中a的类型以及名称完全相同
D.以上说法都不对
首先看main函数,定义了一个对象b,既然是对象先去B类中找f()函数如果没有才去父类中寻找,而子类中由于f()函数与父类函数同名构成隐藏,隐藏了父类的函数实现方法所以只能调用子类的f(int a)函数,而由于缺乏参数所以编译报错。
2.下面哪项结果是正确的(C )
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
首先d继承了base1和base2,有一个base1的指针存储d对象,base2的指针也存储d对象,派生类指针存储子类对象,下面我们画图看看他们的关系:
由于d先继承的base1所以只有base1的首地址与d相同,而base2的首地址在base1类的下一个所以和d不相同。
3.下列代码中f函数执行结束后输出(C )
class A
{
public:
A() { cout<<"A::A()"<<endl; }
~A() { cout<<"A::~A()"<<endl; }
int a;
};
class B : public A
{
public:
B() { cout<<"B::B()"<<endl; }
~B() {cout<<"B::~B()"<<endl; }
int b;
};
void f()
{
B b;
}
A.B::B() B::~B()
B.B::B() A::A() A::~A() B::B()
C.A::A() B::B() B::~B() A::~A()
D.以上都不对
首先从f()函数开始看起,有一个子类对象b,这时候调用B的构造函数,进入B的构造函数初始化列表调用父类A的构造函数,所以先打印A(),然后进入B构造函数的函数体打印B(),然后函数结束开始析构,先析构子类对象打印~B(),然后子类析构结束后自动调用父类析构~A()。
4.以下哪项说法时正确的(D )
class A
{
public:
void f1(){cout<<"A::f1()"<<endl;}
virtual void f2(){cout<<"A::f2()"<<endl;}
virtual void f3(){cout<<"A::f3()"<<endl;}
};
class B : public A
{
public:
virtual void f1(){cout<<"B::f1()"<<endl;}
virtual void f2(){cout<<"B::f2()"<<endl;}
void f3(){cout<<"B::f3()"<<endl;}
};
A.基类和子类的f1函数构成重写
B.基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字
C.如果基类指针引用子类对象后,通过基类对象调用f2时,调用的是子类的f2
D.f2和f3都是重写,f1是重定义
f1由于在基类中没有加virtual关键字,所以只能构成隐藏。f2和f3满足重写的条件。
5.以下程序输出结果是( C)
class A
{
public:
A ():m_iVal(0){test();}
virtual void func() { std::cout<<m_iVal<<‘ ’;}
void test(){func();}
public:
int m_iVal;
};
class B : public A
{
public:
B(){test();}
virtual void func()
{
++m_iVal;
std::cout<<m_iVal<<‘ ’;
}
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对
首先从main函数看起,父类指针存放子类是多态的信号,先自动调用B类的构造函数,在B的构造函数的初始化列表调用A的构造函数,然后将mval初始化为0,调用A类中的test函数,在test函数中又调用了func函数,这个时候由于派生类的构造函数初始化列表还没走完,所以没有虚表指针不构成多态,只能调用A类中的func打印0,然后进入B类的构造函数的函数体中调用test函数,由于B中无test函数只能去父类A中调用,在A类中的test函数体中调用func函数,这个时候因为派生类的初始化列表已经走完了虚表指针形成了,并且func被子类重写由this指针也就是A*父类指针调用func满足多态所以在B类中的func中先让mval++变成1然后打印1,接下来由父类指针P主动调用test函数,同样满足多态调用B类中的func函数,mval++变成2然后打印2,所以答案是0 1 2.
6.下面函数输出结果是( A)
class A
{
public:
virtual void f()
{
cout<<"A::f()"<<endl;
}
};
class B : public A
{
private:
virtual void f()
{
cout<<"B::f()"<<endl;
}
};
int main()
{
A* pa = (A*)new B;
pa->f();
}
A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用
先从main函数看起,父类指针存放子类,先调用子类的构造函数,无构造我们就直接往下讲了,由于继承中天生的赋值类型转换,所以子类到父类并不需要强转,所以这一步没有作用,父类指针调用f函数,进入A类中发现f是虚函数子类重写了这个虚函数所以调用B类中的f()函数,虽然这个时候B类中的f()函数是私有的,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变只是执行函数发生了变化,所以还是打印B()
7.下面 C++ 程序的运行结果是(C)
class parent {
int i;
protected:
int x;
public:
parent() { x = 0; i = 0; }
void change() { x++; i++; }
void display();
};
class son :public parent {
public:
void modify();
};
void parent::display() {
cout << "x=" << x << endl;
}
void son::modify() {
x++;
}
int main()
{
son A;
parent B;
A.display();
A.change();
A.modify();
A.display();
B.change();
B.display();
return 0;
}
A:x=1 x=0 x=2
B :x=2 x=0 x=1
C :x=0 x=2 x=1
D: x=0 x=1 x=2
先进入main函数,有一个子类对象A,有一个父类对象B,都经过构造函数初始化x=0,A调用A中的display打印x = 0,然后A调用父类中的change函数(因为子类无change函数),x变成1,然后A调用A中的modify函数x++变成2然后打印2,由于A和B是两个不同的类,所以B调用B中的change函数后x从0变成1,然后打印1。
8.分析一下这段程序的输出(A)
class B
{
public:
B()
{
cout << "default constructor" << " ";
}
~
B()
{
cout << "destructed" << " ";
}
B(int i): data(i)
{
cout << "constructed by parameter" << data << " ";
}
private: int data;
};
B Play( B b)
{
return b;
}
int main(int argc, char *argv[])
{
B temp = Play(5);
return 0;
}
A constructed by parameter5 destructed destructed
B constructed by parameter5 destructed
C default constructor" constructed by parameter5 destructed
D default constructor" constructed by parameter5 destructed destructed
首先进入main函数,调用play函数,而play函数的参数是B类对象,所以这里生成一个B类的临时对象将5拿来构造B然后调用B的构造函数打印constructed by parameter 5,然后调用拷贝构造将这个对象给play函数中的形参,返回的时候本来要创建一个B类的临时对象调用拷贝构造将b给临时对象后再释放原来形参中的那个b变量,但是由于编译器的优化会直接将play的返回值给temp,然后析构掉刚刚那个返回值对象,所以返回值析构打印destructed,函数结束后temp对象析构打印destructed.
9.以下程序的输出是(C)
class Base {
public:
Base(int j): i(j) {}
virtual~Base() {}
void func1() {
i *= 10;
func2();
}
int getValue() {
return i;
}
protected:
virtual void func2() {
i++;
}
protected:
int i;
};
class Child: public Base {
public:
Child(int j): Base(j) {}
void func1() {
i *= 100;
func2();
}
protected:
void func2() {
i += 2;
}
};
int main() {
Base * pb = new Child(1);
pb->func1();
cout << pb->getValue() << endl; delete pb;
}
A 11
B 101
C 12
D 102
首先基类指针存放子类对象是多态的信号,child调用自己的构造函数,然后在初始化列表中用1初始化基类对象,然后去基类的构造函数中用1初始化i,然后调用func1函数,由于func1不是虚函数所以调用基类的func1i*10==10,然后调用func2函数,发现func2是虚函数并且this指针是父类指针所以调用子类中的func2函数i变成12.
10.下面 C++ 代码的运行结果为(A)
class B0 {
public:
virtual void display() {
cout << "B0::display0" << endl;
}
};
class B1 :public B0 {
public:
void display() { cout << "B1::display0" << endl; }
};
class D1 : public B1 {
public:
void display() {
cout << "D1::display0" << endl;
}
};
void fun(B0 ptr) {
ptr.display();
}
int main() {
B0 b0;
B1 b1;
D1 d1;
fun(b0);
fun(b1);
fun(d1);
}
A B0::display0 B0::display0 B0::display0
B B0::display0 B0::display0 D1::display0
C B0::display0 B1::display0 D1::display0
D B0::display0 B1::display0 B1::display0
首先main函数有3个对象,然后都去调用fun函数,而fun函数参数是基类对象,由对象调用display只能调用父类B0自己的函数,所以打印三次B0::display0
11.下面 C++ 程序的运行结果为(A)
class A {
public: A(const char* s) { cout << s << endl; } ~A() {}
};
class B : virtual public A {
public: B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C : virtual public A {
public: C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D : public B, public C {
public: D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1)
{ cout << s4 << endl; }
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D"); delete p; return 0;
}
A class A class B class C class D
B class D class B class C class A
C class D class C class B class A
D class A class C class B class D
首先这是个多继承问题,我们可以看到D类先继承B,再继承C,而B类中先继承了A,所以D中构造函数的初始化顺序为A B C D,A用s1初始化进入s1的构造函数打印class A,然后用s1和s2初始化B,由于B虚继承A只有一份A所以刚开始在A初始化一次后后面在其他类的初始化列表就不再初始化A了,所以打印S2也就是classB,然后用s1和s3初始化C,与B同理A不初始化打印S3也就是class C,最后进入D的构造函数函数体打印classD
12.有如下C++代码(A)
struct A{
void foo(){printf("foo");}
virtual void bar(){printf("bar");}
A(){bar();}
};
struct B:A{
void foo(){printf("b_foo");}
void bar(){printf("b_bar");}
};
A *p = new B;
p->foo();
p->bar();
A barfoob_bar
B foobarb_bar
C barfoob_foo
D foobarb_fpp
首先A和B是struct,struct默认访问权限和继承权限都是公有,new一个B对象,调用B的构造函数在B的构造函数的初始化列表调用A的构造函数,在A的构造函数中调用bar函数,由于初始化列表没有结束只能调用A的bar函数所以打印bar,然后父类指针调用foo函数foo不是虚函数所以调用A中的foo打印foo,然后父类指针调用bar函数,bar是虚函数满足多态条件所以打印b_bar.
13.以下程序输出结果是(B)
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*p = new B;
p->test();
return 0;
}
A :A->0
B :B->1
C :A->1
D: B->0
首先子类指针存放子类对象,然后调用test函数,进入B类中发现没有test函数只能去父类中找,然后调用调用父类中的test函数,函数体中调用func函数,这时的this指针为A*,func为虚函数去B类中调用func函数,由于缺省参数是编译期间就完成的,而虚函数是运行期间完成的,所以当调用func函数的时候缺省参数还是父类的int val = 1,所以这时候打印B->1.
14.下面程序的输出是(B)
class A
{
public:
void foo()
{
printf("1");
}
virtual void fun()
{
printf("2");
}
};
class B: public A
{
public:
void foo()
{
printf("3");
}
void fun()
{
printf("4");
}
};
int main(void)
{
A a;
B b;
A *p = &a;
p->foo();
p->fun();
p = &b;
p->foo();
p->fun();
A *ptr = (A *)&b;
ptr->foo();
ptr->fun();
return 0;
}
A 121434
B 121414
C 121232
D 123434
首先父类指针p存放父类对象,所以调用的函数都是父类的,打印1 2,然后p又存放子类对象,由于fun是虚函数所以打印1 4,因为子类对象本来就可以直接给父类,所以强转无任何意义与p = &b一模一样,还是打印1 4.
15.下面这段代码运行时会出现什么问题(B)
class A
{
public:
void f()
{
printf("A
");
}
};
class B: public A
{
public:
virtual void f()
{
printf("B
");
}
};
int main()
{
A *a = new B;
a->f();
delete a;
return 0;
}
A 没有问题,输出B
B 不符合预期的输出A
C 程序不正确
D 以上答案都不正确
因为父类指针保存的子类对象,f不是虚函数正常调用A的f函数,但是delete的时候只会调用父类的析构函数这里是会崩溃的,正确的做法是将父类的析构函数定义为虚函数,就可以正常释放子类的资源了。