目录
1. 继承的概念及定义
1.1. 继承的概念
继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许实现者保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称之为派生类;
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程;
以前,我们接触的复用都是函数复用,继承是类设计层次的复用。
下面是一个继承的简单 demo,如下:
namespace Xq
{
class father
{
public:
void print() { std::cout << "name: " << _name << std::endl; }
protected:
std::string _name = "lisi";
};
// 继承后父类 (class father) 的成员(成员方法+成员属性)都会变成子类 (class child) 的一部分
// 子类(派生类) 复用了父类 (基类) 的成员
class child : public father
{
public:
void print() { std::cout << "tele: " << _tele << std::endl; }
protected:
std::string _tele = "123";
};
}
void Test1(void)
{
// 定义一个子类对象
Xq::child sp;
// 默认情况下, 调用的是子类的print();
sp.print();
// 如果要调用父类的print, 需要指明类域
sp.father::print();
}
现象如下:
1.2. 继承定义
1.2.1. 继承格式
1.2.2. 继承关系和访问限定符
1.2.3. 继承基类成员访问方式的变化
接下来用实例理解上面的表。
1.2.4. 公有继承
namespace Xq
{
class person
{
public: //(1)
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected: //(2)
std::string _name = "lisi";
size_t _age = 20;
};
class superman : public person //(3)
{
public:
void print()
{
cout << "tele: " << _tele << endl;
}
protected:
std::string _tele = "123";
};
}
(1)是公有访问限定符,(3)继承方式是公有,公有遇公有还是公有,因此,派生类可以在类外和类里面都可以访问基类的公有属性和成员方法;
void Test2(void)
{
Xq::superman sp;
sp.person::print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
name: lisi
age: 20
[Xq@VM-24-4-centos 8_15]$
(2)是保护访问限定符,(3)继承方式是公有,保护遇公有为保护,因此派生类中不可以在类外访问基类的保护成员属性和成员方法;
void Test3(void)
{
Xq::superman sp;
sp._name = "wangwu"; //error
sp._age = 21; //error
}
//对于派生类公有继承基类的保护成员属性和成员方法不可以在派生类外访问;
// ‘std::string Xq::person::_name’ is protected std::string _name = "lisi";
// ‘size_t Xq::person::_age’ is protected size_t _age = 20;
但是对于基类的protected成员可以在派生类中进行访问:
class superman : public person
{
public:
//在类外更改基类保护属性
void print()
{
cout << "tele: " << _tele << endl;
cout << "original name: " << _name << " " << "origianl age: " << _age << endl;
_name = "wangwu";
_age = 21;
cout << "changed name: " << _name << " " << "changed age: " << _age << endl;
}
protected:
std::string _tele = "123";
};
void Test3(void)
{
Xq::superman sp;
sp.print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
tele: 123
original name: lisi origianl age: 20
changed name: wangwu changed age: 21
[Xq@VM-24-4-centos 8_15]$
//对于派生类公有继承基类的保护成员属性和成员方法不可以在派生类外访问,但可以在派生类内部访问;
1.2.5. 保护继承
namespace Xq
{
class person
{
public: //(1)
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected: //(2)
std::string _name = "lisi";
size_t _age = 20;
};
class superman : protected person //(3)
{
public:
void print()
{
cout << "tele: " << _tele << endl;
}
protected:
std::string _tele = "123";
};
}
(1)是公有访问限定符,(3)是继承方式为保护,二者结合为保护;
(2)是保护访问限定符,(3)是继承方式为保护,二者结合还是保护;
void Test4(void)
{
Xq::superman sp;
sp.person::print();
sp._name = "wangwu";
}
//编译报错
class Xq::person Xq::person::person’ is inaccessible(达不到的、不能接近的的)
‘std::string Xq::person::_name’ is protected std::string _name = "lisi";
对于派生类的保护继承基类的公有成员属性和成员方法以及保护成员属性和成员方法都不可以在类外面访问;
namespace Xq
{
class person
{
public: //(1)
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected: //(2)
std::string _name = "lisi";
size_t _age = 20;
};
class superman : protected person //(3)
{
public:
void print()
{
cout << "tele: " << _tele << endl;
//访问派生类的公有成员
person::print();
//访问派生类的保护成员
cout << "original name: " << _name << " " << "origianl age: " << _age << endl;
_name = "wangwu";
_age = 21;
cout << "changed name: " << _name << " " << "changed age: " << _age << endl;
}
protected:
std::string _tele = "123";
};
}
void Test4(void)
{
Xq::superman sp;
sp.print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
tele: 123
name: lisi
age: 20
original name: lisi origianl age: 20
changed name: wangwu changed age: 21
[Xq@VM-24-4-centos 8_15]$
可以看到,对于派生类保护继承基类的公有成员和保护成员只可以在派生类类中访问;
1.2.6. 私有继承
(1)是公有访问限定符,(3)是继承方式为私有,二者结合为私有;
(2)是保护访问限定符,(3)是继承方式为私有,二者结合还是私有;
namespace Xq
{
class person
{
public: //(1)
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected: //(2)
std::string _name = "lisi";
size_t _age = 20;
};
class superman : private person //(3)
{
public:
void print()
{
cout << "tele: " << _tele << endl;
}
protected:
std::string _tele = "123";
};
}
void Test5(void)
{
Xq::superman sp;
//访问派生类的公有
sp.person::print();
//访问派生类的保护
sp._name = "cuihua";
}
//编译报错:
test.cc:74:14: error: ‘Xq::person’ is an inaccessible base of ‘Xq::superman’
sp.person::print();
test.cc:17:25: error: ‘std::string Xq::person::_name’ is protected
std::string _name = "lisi"
对于派生类的私有继承基类的公有成员属性和成员方法以及保护成员属性和成员方法都不可以在类外面访问;
namespace Xq
{
class person
{
public:
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected:
std::string _name = "lisi";
size_t _age = 20;
};
class superman : private person
{
public:
void print()
{
cout << "tele: " << _tele << endl;
//访问派生类的公有成员
person::print();
//访问派生类的保护成员
cout << "original name: " << _name << " " << "origianl age: " << _age << endl;
_name = "wangwu";
_age = 21;
cout << "changed name: " << _name << " " << "changed age: " << _age << endl;
}
protected:
std::string _tele = "123";
};
}
void Test5(void)
{
Xq::superman sp;
sp.print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
tele: 123
name: lisi
age: 20
original name: lisi origianl age: 20
changed name: wangwu changed age: 21
[Xq@VM-24-4-centos 8_15]$
可以看到,对于派生类私有继承基类的公有成员和保护成员只可以在派生类类中访问;
而以上三种继承方式,对于基类的私有成员(成员属性/成员方法)在派生类中不可见,即不论在派生类的类外还是类内都不可以访问:
类外访问:
namespace Xq
{
class person
{
public: //(1)
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
private: //(2)
std::string _name = "lisi";
size_t _age = 20;
};
class superman : public person //(3)
{
public:
void print()
{
cout << "tele: " << _tele << endl;
}
protected:
std::string _tele = "123";
};
}
(2)是私有访问限定符,(3)代表公有继承;二者相遇,基类中的私有成员在派生类中不可见:
void Test6(void)
{
Xq::superman sp;
//类外访问基类私有成员
sp._name = "hehe";
sp._age = 18;
}
//编译报错,基类私有成员在派生类中不可见
test.cc: In function ‘void Test6()’:
test.cc:18:25: error: ‘std::string Xq::person::_name’ is private
std::string _name = "lisi";
^
test.cc:84:6: error: within this context
sp._name = "hehe";
^
test.cc:19:19: error: ‘size_t Xq::person::_age’ is private
size_t _age = 20;
^
test.cc:85:6: error: within this context
sp._age = 18;
类内访问:
namespace Xq
{
class person
{
public:
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
// protected:
private:
std::string _name = "lisi";
size_t _age = 20;
};
class superman : public person
{
public:
void print()
{
cout << "tele: " << _tele << endl;
//访问派生类的私有成员
cout << "original name: " << _name << " " << "origianl age: " << _age << endl;
_name = "wangwu";
_age = 21;
cout << "changed name: " << _name << " " << "changed age: " << _age << endl;
}
protected:
std::string _tele = "123";
};
}
void Test6(void)
{
Xq::superman sp;
sp.print();
}
//编译报错:
test.cc:18:25: error: ‘std::string Xq::person::_name’ is private
std::string _name = "lisi";
结论就是,不管是何种继承方式 (public/protected/private),对于基类的私有成员,派生类不可见;
对于基类中的私有成员的意义:不想被派生类继承的成员,可以设计成私有;
1.2.7. 继承方式总结:
- 基类 private 成员在派生类中无论以什么方式继承都是不可见的。 这里的不可见指的是基类的私有成员虽然被继承到了派生类对象中,但是语法上限制了派生类对象访问它,无论是在派生类的类内还是类外,都无法访问;
- 如果基类成员不想让派生类在类外访问,但需要让派生类类内可以访问,那么该基类成员就可以定义为 protected,可以看到,protected 在继承角度有着很大的存在意义;
- 我们总结一下就会发现,除开基类的私有成员,基类的其他成员被子类继承后的访问方式 == Min (基类成员的访问限定符, 继承方式),private < protected < public;
- class 的默认继承方式是 private,struct 的默认继承方式是public,但实际中,最好显示声明继承方式;
- 事实上,大多数的实际运用中,public 继承占绝大多数, 几乎很少使用 protected/private 继承,也不提倡 protected/private 继承, 因为 protected/private 继承下来的成员只能在派生类的类内使用,实际中扩展性并不强。
2. 基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象、基类的指针、基类的引用,这里有一个形象地说法叫切片或者切割,寓意把派生类中基类的成员属性切割过去;
- 基类对象不能赋值给派生类对象, 因为基类中没有派生类的成员属性;
- 基类的指针或者引用可以通过类型转换赋值给派生类的指针或者引用,但是必须是基类的指针是指向派生类对象时才是安全的,这里基类如果是多态类型,可以使用RTTI (Run Time Type Information) 的 dynamic_cast 来进行识别后进行安全转换。
验证切片的demo 如下:
namespace Xq
{
class person
{
protected:
size_t _age;
std::string _name;
std::string _sex;
};
class worker : public person
{
protected:
std::string _job_number;
};
}
void Test1(void)
{
Xq::worker wobj;
//派生类对象可以赋值给基类对象/指针/引用
Xq::person pobj = wobj; //(1)
Xq::person* p_pobj = &wobj; //(2)
Xq::person& r_pobj = wobj; //(3)
}
首先,上面的代码是可以编译成功的。
- (1) 间接告诉我们,子类对象是可以赋值给父类对象的,但需要注意的是这里并不是隐式类型转换,为什么呢?
- (3) 就可以解释这里绝对不是隐式类型转换,因为,如果 (3) 是隐式类型转换,那么就会生成一个临时变量,而临时变量具有常性,权限被放大,因此反推这里绝对不是隐式类型转换。
在这里,实际上是切片的动作,即派生类的对象可以切片赋值给基类对象、派生类的指针可以切片赋值给基类指针、派生类的引用可以切片赋值给基类引用;
过程如下:
我们可以把派生类的对象切片赋值给基类对象/指针/引用,但是不能倒过来,即基类的对象无法切片赋值给派生类,因为基类中就没有派生类的成员属性;
如果将基类的对象赋值给派生类对象,会编译报错,具体如下:
void Test2(void)
{
Xq::person pobj;
Xq::worker wobj = pobj;
}
// 编译报错,无法将基类对象赋值给派生类对象
error: conversion from ‘Xq::person’ to non-scalar type ‘Xq::worker’ requested Xq::worker wobj = pobj;
//哪怕我们此时强转都不行
void Test2(void)
{
Xq::person pobj;
Xq::worker wobj = (Xq::worker)pobj;
}
//依旧会编译报错
error: conversion from ‘Xq::person’ to non-scalar type ‘Xq::worker’ requested Xq::worker wobj = pobj;
3. 继承中的作用域
- 在继承体系中基类和派生类都有独自的作用域,即类域;
- 基类和派生类中若有同名成员,派生类会优先访问派生类的成员 (就近原则),这种情况我们称之为隐藏,也叫做重定义;
- 当然,如果派生类想访问基类的成员,可以使用基类::基类成员,进行显示访问;
- 注意, 如果是成员函数的隐藏,只需要函数名相同就构成隐藏;
- 在实际运用中,继承体系中最好不要定义同名成员。
首先,我们要明确基类和派生类是有自己独立作用域的,即类域;假设:
namespace Xq
{
class father
{
public:
std::string _name = "lisi";
};
class child : public father
{
public:
void print()
{
cout << "name: " << _name << endl; //这里的name是派生类的_name还是基类的_name呢?
}
public:
std::string _name = "cuihua";
};
}
void Test7(void)
{
Xq::child c;
c.print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
name: cuihua
[Xq@VM-24-4-centos 8_15]$
可以看到,在派生类中访问同名成员,默认是派生类成员;
但如果我想访问基类的成员,该如何访问?
需要显示访问,即基类::基类成员,具体如下:
namespace Xq
{
class father
{
public:
std::string _name = "lisi";
};
class child : public father
{
public:
void print()
{
cout << "name: " << father::_name << endl; //指明类域
}
public:
std::string _name = "cuihua";
};
}
void Test7(void)
{
Xq::child c;
c.print();
}
[Xq@VM-24-4-centos 8_15]$ ./test
name: lisi
[Xq@VM-24-4-centos 8_15]$
接下来有一个容易误解的问题:
namespace Xq
{
class A
{
public:
void fun()
{
cout << "hehe" << endl;
}
};
class B : public A
{
public:
void fun(int num)
{
cout << "num = " << num << endl;
}
};
}
请问,A类中的 fun() 和B类中的 fun() 构成函数重载吗?
- 不构成。可能我们看到这种情况,一眼就想到了函数重载,因为参数个数不同;
- 但是在这里,这两个函数不构成函数重载,它们的关系是隐藏关系,为什们呢?
- 因为函数重载的前提条件是在同一作用域内 ( 而父类和子类是两个独立的作用域 ),参数的个数/顺序/类型不同才构成函数重载;
- 而 A 和 B 是两个类域,即两个不同的作用域,因此这里的 fun() 不构成函数重载,而对于基类和派生类来说,只要函数名相同就构成隐藏关系,不关心参数。
void Test8(void)
{
Xq::B b;
b.fun(10);
}
[Xq@VM-24-4-centos 8_15]$ ./test
num = 10
[Xq@VM-24-4-centos 8_15]$
//但如果我想显式调用基类中的fun()呢?
void Test8(void)
{
Xq::B b;
b.fun();
}
//编译报错,因为它会默认调用派生类中的fun(),参数不匹配导致编译报错
test.cc:131:9: error: no matching function for call to ‘Xq::B::fun()’
b.fun();
^
test.cc:131:9: note: candidate is:
test.cc:67:12: note: void Xq::B::fun(int)
void fun(int num)
//正确调用方式:需要指明类域
void Test8(void)
{
Xq::B b;
b.Xq::A::fun();
}
[Xq@VM-24-4-centos 8_15]$ ./test
hehe
[Xq@VM-24-4-centos 8_15]$
4.派生类的默认成员函数
默认成员函数即便我们不写,编译器也会自动生成一份,那么在派生类中,这些成员函数如何处理呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类的构造函数的初始化列表阶段显式初始化基类的成员属性;
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类成员属性的初始化;
- 派生类的 operator= 必须要调用基类的 operator= 完成基类成员属性的赋值;
- 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类成员属性,因为这样才能保证派生类对象先清理派生类的成员属性再清理基类的成员属性;
- 派生类对象初始化资源时 (调用构造),先初始化基类成员属性,后初始化派生类成员属性;
- 派生类对象清理资源时 (调用析构),先清理派生类资源,在清理基类资源,即先调用派生类析构,然后编译器自动调用基类析构;
- 我们知道,重写的条件之一是函数名相同,可是基类和派生类的析构函数的函数名不相同啊,实际上,当派生类继承基类后,编译器会对析构函数进行特殊处理,析构函数的函数名统一为 destructor,所以,如果基类的析构函数不是虚函数,那么基类析构函数和父类析构函数构成隐藏关系。
接下来,我们用实例验证派生类的六个默认成员函数的相关特性。
person 是一个基类,worker 是一个派生类,实现具体如下:
namespace Xq
{
// 基类
class person
{
public:
person(const char* name = "supercow")
:_name(name)
{
cout << "person()" << endl;
}
person(const person& copy)
:_name(copy._name)
{
cout << "person(const person& copy)" << endl;
}
person& operator=(const person& copy)
{
if (this != ©)
{
_name = copy._name;
}
cout << "person& operator=(const person& copy)" << endl;
return *this;
}
~person()
{
cout << "~person()" << endl;
}
protected:
std::string _name;
};
// 派生类
class worker : public person
{
public:
//...
protected:
std::string _job_number;
};
}
首先,我们单独看基类的话,它就是一个普通的类,它的六个默认成员函数不做特殊处理,跟以前一样,但是对于派生类来说,因为它继承了基类的成员,因此在这几个默认成员函数会有相应的变化;因此我们的重点就是派生类的默认成员函数
在类和对象篇章,我们学习过六个默认成员函数:
4.1. 派生类的构造函数
namespace Xq
{
class person
{
public:
person(const char* name = "supercow")
:_name(name)
{
cout << "person()" << endl;
}
person(const person& copy)
:_name(copy._name)
{
cout << "person(const person& copy)" << endl;
}
person& operator=(const person& copy)
{
if (this != ©)
{
_name = copy._name;
}
cout << "person& operator=(const person& copy)" << endl;
return *this;
}
~person()
{
cout << "~person()" << endl;
}
protected:
std::string _name;
};
class worker : public person
{
public:
//...
protected:
std::string _job_number;
};
}
void Test3()
{
Xq::worker wobj;
}
实例化一个派生类对象,结果如下:
通过结果我们可以看到,实例化一个派生类的对象后,结果却调用了基类的构造;
那么如果此时的基类没有默认构造呢 (当派生类是编译器默认生成的)?
可以看到,如果此时基类没有默认构造,那么实例化派生类对象时,会发生编译报错。
因此,实现者往往需要自己显式定义派生类的构造函数,具体如下:
worker(const char* name = "lisi",const char* num = "111")
:_name(name)
, _job_number(num)
{
cout << "work()" << endl;
}
void Test3(void)
{
Xq::worker wobj;
}
但是我们发现,依旧无法编译成功;
原因是因为,在这里C++规定,派生类就去初始化派生类的成员,基类成员需要调用基类构造函数初始化,因此,更改后的实现:
class worker : public person
{
public:
worker(const char* name = "lisi",const char* num = "111")
// 注意这里看起来像匿名对象一样, 但不是匿名对象
// 而是显式调用基类的构造, 初始化基类的成员属性
: Xq::person(name) // 基类成员调用基类的构造函数
, _job_number(num) // 派生类成员,派生类自己处理
{
cout << "work()" << endl;
}
protected:
std::string _job_number;
};
对于派生类的构造函数来说:
- 对于派生类的成员属性,跟以前的处理方式一样( 对于内置类型不处理,对于自定义类型调用它的默认构造函数 );
- 对于基类的成员属性,必须调用基类的构造函数初始化。
4.2. 派生类的拷贝构造
对于派生类的拷贝构造来说:
- 对于派生类成员属性,跟以前的处理方式一样( 对于内置类型按照字节序的方式进行拷贝,对于自定义类型调用它的拷贝构造函数 );
- 对于基类成员属性,必须调用基类的拷贝构造函数初始化基类成员属性;
namespace Xq
{
class person
{
public:
person(const char* name)
:_name(name)
{
cout << "person()" << endl;
}
person(const person& copy)
:_name(copy._name)
{
cout << "person(const person& copy)" << endl;
}
person& operator=(const person& copy)
{
if (this != ©)
{
_name = copy._name;
}
cout << "person& operator=(const person& copy)" << endl;
return *this;
}
~person()
{
cout << "~person()" << endl;
}
protected:
std::string _name;
};
class worker : public person
{
public:
worker(const char* name = "lisi",const char* num = "111")
: Xq::person(name)
, _job_number(num)
{
cout << "work()" << endl;
}
protected:
std::string _job_number;
};
}
void Test4(void)
{
Xq::worker wobj;
Xq::worker copy = wobj;
}
对于派生类而言,如果派生类没有显示定义拷贝构造,那么编译器默认生成的拷贝构造会干什么呢?
运行结果,如下:
我们发现,派生类调用拷贝构造时,调用了基类的拷贝构造,这也验证了我们上面的说法,对于继承的基类成员而言,需要调用基类的拷贝构造初始化基类的成员属性。
跟构造函数一样,派生类需要显示实现拷贝构造:
worker(const worker& copy)
: person()
, _job_number(copy._job_number)
{
}
但是我们遇到的一个问题就是,person() 里面的参数要填什么?
因为这要调用基类 person 的拷贝构造,那么也就意味着需要一个 person 的对象,我们如何得到一个 person 对象呢?
很简单,我们之前已经说过了,派生类对象可以切片赋值转换为基类对象,因此:
worker(const worker& copy)
: person(copy) //利用了派生类对象可以切片赋值转换为基类对象
, _job_number(copy._job_number)
{
cout << "worker(const worker& copy)" << endl;
}
现象如下:
4.3. 派生类的赋值运算符重载
对于派生类来说,编译器默认生成的赋值运算符重载会:
- 对于自己本身的成员,跟以前的处理方式一样(对于内置类型按照字节序的方式进行拷贝,对于自定义类型调用它的赋值运算符重载);
- 对于继承父类的成员,必须调用父类的赋值运算符重载;
// 派生类默认生成的赋值运算符重载
void Test5(void)
{
Xq::worker wobj("wangwu", "111");
Xq::worker copy("list", "222");
copy = wobj;
}
现象如下:
可以发现,编译器默认生成的赋值运算符重载会调用父类的赋值运算符重载;
如果我们要显式定义派生类的赋值运算符重载:
根据前两次实现,我们可能会写出这种代码:
worker& operator=(const worker& copy)
{
if (this != ©)
{
operator=(copy);
_job_number = copy._job_number;
}
cout << "worker& operator=(const worker& copy)" << endl;
return *this;
}
void Test5(void)
{
Xq::worker wobj("wangwu", "111");
Xq::worker copy("list", "222");
copy = wobj;
}
但是当我们运行这个进程的时候, 发现进程崩溃了;经过调试发现:
调用派生类的operator=发生了死递归, 最后栈溢出(Stack over)导致进程崩溃; 为啥会死递归呢?
这里的原因是因为我们想去调用基类的operator=,结果却调用的是派生类的operator= (就近原则),为什么呢?
首先我们要知道,基类和派生类的operator=函数名是相同的,因此它们构成隐藏关系,即如果我们想显式调用基类的operator=,那么必须要显示声明基类的作用域,如下:
worker& operator=(const worker& copy)
{
if (this != ©)
{
// 需要指明类域, 即基类的类域
person::operator=(copy);
_job_number = copy._job_number;
}
cout << "worker& operator=(const worker& copy)" << endl;
return *this;
}
void Test5(void)
{
Xq::worker wobj("wangwu", "111");
Xq::worker copy("list", "222");
copy = wobj;
}
现象如下:
4.4. 派生类的析构函数
对于派生类来说,编译器默认生成的析构函数,分两种情况:
- 对于自身的成员,对内置类型不处理,对自定义类型去调用它的析构函数,根以前一样;
- 对于继承的成员,需要去调用父类的析构函数处理。
//派生类中默认生成的析构函数会做什么呢?
void Test6(void)
{
Xq::worker wobj("wangwu", "111");
}
结果如下:
与上面判断一样,对于派生类继承的成员会去调用基类的析构函数;那如果我们想显示实现派生类的析构函数呢?该如何实现:
~worker()
{
~person();
//...
//派生类自己成员的一些资源清理工作
}
void Test6(void)
{
Xq::worker wobj("wangwu", "111");
}
但是我们发现,这个进程跑不起来,发生了编译报错:
并且我们发现这里报了一个非常莫名其妙的错误,它说基类没有合适的默认构造,那是为什么呢?
首先我们要知道,派生类的析构函数和基类的析构函数构成隐藏关系;
诶,不对啊,不是说基类和派生类的函数名相同才构成隐藏吗?这两个函数名也不相同啊; 事实上,这里编译器做了特殊处理,由于多态的需要,对于基类的析构和派生类的析构函数名会被统一处理为 destructor,因此它们构成隐藏关系,即当我们要调用基类的析构,需要指明类域,如下:
~worker()
{
person::~person();
//...
//派生类自己成员的一些资源清理工作
cout << "~worker()" << endl;
}
void Test6(void)
{
Xq::worker wobj("wangwu", "111");
}
此时就可以正常编译了,结果如下:
但是我们又发现一个问题,此时运行结果有问题啊;
我们只定义了一个派生类对象,但是基类调用了两次析构,嗯?很奇怪,那为什么呢?
原因就是对于派生类的析构来说,不需要我们显式调用基类的析构,编译器自动调用;
~worker()
{
//person::~person(); //这里不需要我们显式调用基类的析构函数
//...
//派生类自己成员的一些资源清理工作
cout << "~worker()" << endl;
}
void Test6(void)
{
Xq::worker wobj("wangwu", "111");
}
为什么编译器要自动调用基类的析构函数呢?
首先我们需要了解一般的构造和析构顺序:
void Test7(void)
{
Xq::worker obj1;
Xq::worker obj2;
}
在这里:构造顺序(FIFO): obj1先,obj2后;析构顺序(LIFO):obj2先,obj1后;
而对于派生类来说,它需要保证:
- 派生类对象初始化时,基类成员先初始化 (先调用基类构造),派生类成员后初始化 (后调派生类构造);
- 派生类对象清理时,派生类成员先释放资源 (先调用派生类析构),基类成员后释放资源 (后调用基类析构);
因此编译器为了确保这种顺序,会自动调用基类的析构函数,不需要我们显式调用基类的析构函数。
此时的现象如下:
其实,我们也可以换个角度思考一下,为什么编译器在释放派生类对象时,先调用派生类的析构,后调用基类的析构呢?
因为,如果先调用基类的析构,可能会出问题,比如,假设基类中有一个指针,如果先调用基类的析构,这个指针被释放 (delete),此时在调用派生类的析构时,可能会使用这个指针,即可能会出现非法访问的问题,因此,编译器在这里会先调派生类的析构,在调用基类的析构。
4.5. 取地址重载和const取地址重载
这两个函数跟以前一样,不需要做特殊处理,一般情况下我们也没必要显示实现;
worker* operator*()
{
return this;
}
const worker* operator*() const
{
return this;
}
继承上的内容到此结束,C++继承 - 下-CSDN博客