文章目录
继承简介
继承(inheritance)是C++的三大特性之一,继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。
我们称已存在的用来派生新类的类为基类
,又称为父类
。由已存在的类派生出的新类称为派生类
,又称为子类
。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
继承的定义
本文所有测试环境均为:VS2017
//基类
class Base
{
public:
int _b;
};
//派生类
class Derived : public Base
{
public:
int _d;
};
int main()
{
Derived derived;
derived._b = 3;
derived._d = 5;
cout << "derived._b = " << derived._b << " derived._d = " << derived._d << endl;
return 0;
}
运行结果:
内存分布
我们可以看到,在新定义的Derive
类中不仅有它自己的成员变量_d
,也有Base
类中的成员变量_b
,这就是继承。
继承的定义格式
在上面的代码中,我们可以看到在定义Derived
类时,在类的后面多了: public Base
几个字符,这个就是继承的定义方式,我们可以总结如下:
因此,在上面的代码中 Base
类是基类, Derived
类是派生类,继承方式为公有继承。那什么又是继承方式呢?
继承的方式
我们知道,类的访问限定符分为三种,分别为:public
private
protected
。不同的的访问限定符,标志着你能不能访问类的数据成员。继承方式与它相似,也分为三种,分别为:public
private
protected
。不同之处在于,继承方式可以理解为,派生类在继承基类的过程中,对得到的基类中的数据成员做怎样的处理。也就是要对得到的基类中的数据成员再加一层怎样的访问限定符。具体的处理方式,我们可以从下面的几组例子来总结:
//首先,我们先定义一个基类 Base
class Base
{
public:
int _pubb;
protected:
int _prob;
private:
int _prib;
};
我们再以三种不同的继承方式分别继承基类,并且通过分别在派生类的类中和类外访问基类的成员来研究它们:
public继承
class Derived : public Base
{
public:
void test()
{
_pubb = 1;
_prob = 2;
_prib = 3;
}
public:
int _pubd;
protected:
int _prod;
private:
int _prid;
};
运行结果:
我们可以看到,在编译时,编译器提醒基类Base
中的私有成员在被继承后,不能在派生类中被访问。
我们再在类外测试:
int main()
{
Derived derived;
derived._pubb = 1;
derived._prob = 2;
derived._prib = 3;
return 0;
}
运行结果:
编译器提示,
_prob
是保护成员,无法在类外访问。_prib
就更不用说了,在类中都无法访问,在类外就更不行了。而_pubb
没有提示错误。
我们可以得出结论:public
继承(公有继承)方式继承后,基类中各成员的访问权限没有发生变化,依旧保持着原来的访问权限,基类中的私有成员,在派生类中不可见(这里说的不可见是说不能访问)。注意,是在派生类中不可见,而不是不继承。
运行结果:
这里我们可以看到,派生类确实继承到了基类中的私有成员,只是不可见(无法访问)。
protected继承
class Derived : protected Base
{
public:
void test()
{
_pubb = 1;
_prob = 2;
_prib = 3;
}
public:
int _pubd;
protected:
int _prod;
private:d
int _prid;
};
运行结果:
在保护继承时,基类中的私有成员依旧不能在派生类中访问。
我们再在类外进行测试:
int main()
{
Derived derived;
derived._pubb = 1;
derived._prob = 2;
derived._prib = 3;
return 0;
}
运行结果:
编译器提示,_pubb
是保护成员,无法在类外访问。_prob
是保护成员,无法在类外访问。_prib
是原来的错误,在派生类中不可见。
我们可以得出结论:protected
继承(保护继承)方式继承后,基类中public
成员的访问权限变为protected
。基类中protected
成员的访问权限不变。基类中的私有成员,在派生类中不可见。
有人可能会说,上面提示的_pubb没有说是保护的,只是访问错误,那会不会访问权限变为了private
呢,我们可以对Derived
类再派生一次,这次我们采用public
的继承方式,我们直接在新的派生类中进行访问,如果它能访问,则说明是保护的。如果在新派生的类中直接不可见(也就是不可访问),那么_pubb
在进行了protected
继承后,就变成了私有的。
class Derived1 : public Derived
{
void test1()
{
_pubb = 1;
_prob = 2;
//_prib = 3; //我们知道私有的成员肯定不能访问,所以就不测试了
}
};
类中测试,没有错误。
我们再在类外进行测试:
int main()
{
Derived1 derived1;
derived1._pubb = 1;
derived1._prob = 2;
//derived1._prib = 3; //私有成员也不进行测试
return 0;
}
运行结果:
综合上面的运行结果,我们可以知道,它只是在类外调用出错了,而在类内部调用没有出错,我们可以得出结论:在protected
中,访问权限为public
的成员,访问权限变为了protected
。其余的不变。
private继承
class Derived : private Base
{
public:
void test()
{
_pubb = 1;
_prob = 2;
_prib = 3;
}
public:
int _pubd;
protected:
int _prod;
private:
int _prid;
};
运行结果:
在私有继承时,基类中的私有成员依旧不能在派生类中访问。
我们再在类外进行测试:
int main()
{
Derived derived;
derived._pubb = 1;
derived._prob = 2;
derived._prib = 3;
return 0;
}
运行结果:
编译器提示,_pubb
无法在类外访问。_prob
无法在类外访问。_prib
是原来的错误,在派生类中不可见。
我们可以得出结论:private
继承(私有继承)方式继承后,基类中public
成员的访问权限变为private
。基类中protected
成员的访问权限变为private
。基类中的私有成员,在派生类中不可见。
为了确保派生类在私有继承后,父类中的所有成员都变成了私有的,我们对派生类Derived
再进行一次派生。
class Derived1 : public Derived
{
void test1()
{
_pubb = 1;
_prob = 2;
//_prib = 3; //我们知道私有的成员肯定不能访问,所以就不测试了
}
};
运行结果:
直接在类中访问便出错,因此,我们可以确认,在经过private
(私有)继承后,父类数据成员的访问权限都变为private
。
总结
如果是用class继承类,在继承的时候,不显式地给出继承方式关键字,系统默认为private(私有)继承。如果是用struct继承类,在继承的时候,不显式地给出继承方式关键字,系统默认为public(公有)继承。
- 基类成员在派生类中的访问属性:
其实可以将三个不同的访问属性当成三个等级:公有 < 保护 < 私有
。在继承的过程中,它的访问属性会向自身的访问属性
和继承方式
中更高的看齐。 - 基类
private
成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的private(私有)
成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
继承中的作用域
我们先来看个例子:
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
void test(char i)
{
cout << "Base::test" << endl;
}
public:
int _pubb;
protected:
int _prob;
private:
int _prib;
};
class Derived : public Base
{
public:
void test(int i)
{
cout << "Derived::test" << endl;
}
public:
int _pubd;
protected:
int _prod;
private:
int _prid;
};
int main()
{
Derived derived;
derived.test('a');
derived.test(5);
return 0;
}
先不看运行结果,猜一下它会输出什么呢?
运行结果:
按道理来说,当派生类继承了父类后,两个test
函数应该在同一个类中,那么它们应该构成了重载,那为什么只调用了派生类的test
呢?
- 在继承体系中,父类和子类都有独立的作用域,因此我们需要知道,在继承中,父类和子类的成员函数之间不会构成函数重载。
- 子类和父类如果有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。隐藏的条件很简单,只要是函数名相同便会构成隐藏。
注意:在实际中在继承体系里面最好不要定义同名的成员。
- 如果要调用基类的成员函数,在子类成员函数中,可以使用
基类::基类成员
,显式访问
derived.Base::test('a');
derived.test(5);
运行结果:
继承与静态数据成员
如果在基类中定义了静态数据成员,那么由基类派生出去的静态数据成员会不会在每个派生类中都存在一份呢?
//基类
class Base
{
public:
int _b;
static int _bb;
};
int Base::_bb = 5;
//派生类
class Derived1 : public Base
{
public:
int _d1;
};
class Derived2 : public Base
{
public:
int _d2;
};
class Derived3 : public Base
{
public:
int _d3;
};
int main()
{
Derived1 derived1;
Derived2 derived2;
Derived3 derived3;
Base::_bb = 5;
cout << Base::_bb << endl;
cout << derived1._bb << endl;
cout << derived2._bb << endl;
cout << derived3._bb << endl;
derived1._bb = 3;
cout << Base::_bb << endl;
cout << derived1._bb << endl;
cout << derived2._bb << endl;
cout << derived3._bb << endl;
return 0;
}
运行结果:
由此可知:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
赋值兼容规则
//基类
class Base
{
public:
Base()
:_b(0)
{}
~Base()
{}
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base(),_d(0)
{}
~Derived()
{}
int _d;
};
- 派生类对象可以向基类对象赋值,即用派生类对象中从基类继承来的数据成员,逐个赋值给基类对象的数据成员。
Derived d;
Base b;
d._b = 6666;
d._d = 7777;
b = d;
cout << b._b << endl;
运行结果:
- 派生类对象可以初始化基类对象的引用。
Derived d;
Base& b = d;
- 派生类对象的地址可以赋给指向基类对象的指针(基类类型的指针)。
Derived d;
Base *pb = &d;
- 如果函数的形参是基类对象或基类对象的引用,在调用函数时,可以用派生类对象作为实参。
void test(Base& b)
{
cout << b._b << endl;
}
int main()
{
Derived d;
test(d);
return 0;
}
- 基类对象不能赋值给派生类对象。
- 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
在这里插入代码片
派生类中的默认成员函数
我们知道,在C98中,类中有6个默认的成员函数,那么在派生类中,因为有了基类的成员,那么它的这6个成员函数需要进行特殊处理吗?在之前的代码中,我们没有显式的定义基类和派生类中的相关函数,都是让系统默认生成。看起来好像没有什么问题,那到底有没有问题呢?
构造函数
我们看下面几组例子:
//基类
class Base
{
public:
Base(int a = 0) //基类中显式的给出缺省的构造函数。
{}
~Base()
{}
private:
int _b;
};
//派生类
class Derived : public Base
{ //不定义派生类的构造函数。
private:
int _d;
};
int main()
{
Derived d;
return 0;
}
运行结果正确,我们来看在定义派生类对象时的反汇编代码:
我们对代码进行修改:
//基类
class Base
{
public:
Base(int a = 0) //基类中显式的给出缺省的构造函数,并且在派生类构造函数的初始化列表中不进行调用。
{}
~Base()
{}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived() //显式定义出派生类的构造函数,并且在里面执行语句。
{
_d = 5;
}
~Derived()
{}
private:
int _d;
};
int main()
{
Derived d;
return 0;
}
运行结果没有错误,我们再来看它的反汇编:
有人可能会说,没有手动的写,编译器怎么会自动的添加上呢,会不会是在别的地方调用的?我们继续测试:
//基类
class Base
{
public:
Base(int a) //基类中显式的给出非缺省的构造函数,并且在派生类的初始化列表中不进行调用
{}
~Base()
{}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
// 派生类的构造函数显式给不给出结果都相同
Derived()
{
_d = 5;
}
~Derived()
{}
private:
int _d;
};
int main()
{
Derived d;
return 0;
}
运行结果:
这是由于,我们给出的基类的构造函数是非缺省的,因此,当编译器将其自动添加到派生类构造函数的初始化列表中时,没有能力为它传一个参数,因此只能弹出错误,没有合适的构造函数可用,如果我们手动将其添加到派生类构造函数的初始化列表,并且为其传递一个参数,那么程序运行就没有错误了:
Derived()
:Base(1)
{
_d = 5;
}
我们看它运行的反汇编:
这也就印证了我们上面的猜想,因此,我们得出结论:
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数(也就是我们自己在代码中显式给出了),则必须在派生类构造函数的初始化列表阶段显示调用(如果基类中包含有缺省的构造函数,派生类中没有显式给出构造函数,那么编译器一定会给派生类生成一个默认的构造函数;如果基类中包含有非缺省的构造函数,派生类的构造函数一定要定义,并且要在派生类构造函数的初始化列表位置调用基类的构造函数,完成基类的初始化)。
拷贝构造函数
拷贝构造函数是特殊的构造函数,因此它需要注意的地方与构造函数相似:
- 如果基类的拷贝构造函数和派生类的拷贝构造函数都没有显式给出,那么编译器会判断是否需要自动生成。
//基类
class Base
{
public:
Base()
{}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果正确,我们看反汇编:
编译器在这里没有生成,而是直接进行赋值。
- 如果基类的拷贝构造函数给出了,派生类的拷贝构造函数没有显式给出,那么编译器会自动为派生类生成拷贝构造函数,并在派生类拷贝构造函数的初始化列表中自动调用基类的构造函数
//基类
class Base
{
public:
Base()
{}
Base(Base& b)
{}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果正确,反汇编如下:
3. 如果基类的拷贝构造函数没有显式给出,派生类的拷贝构造函数显式给出了,那么编译器会先查找基类中的构造函数(是构造函数这里没有写错)。
再次声明,调试环境为VS2017
a. 如果基类里面有缺省的构造函数,那么编译器会在派生类拷贝构造函数的初始化列表阶段调用基类缺
省构造函数
b. 如果基类中有非缺省的构造函数,编译器会报错
c. 如果基类中没有构造函数,那么编译器会根据需要判断会不会为基类生成一个拷贝构造函数,并且判
断是否需要在派生类拷贝构造函数的初始化列表阶段调用。
//基类
class Base
{
public:
Base() //基类中有缺省的构造函数,但是没有拷贝构造函数
{
cout << "Base::Base()" << endl;
}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{
cout << "Derived:: Derived()" << endl;
}
Derived(Derived& d)
{
cout << "Derived:: Derived(Derived& d)" << endl;
}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果及反汇编如下:
//基类
class Base
{
public: //基类中没有构造函数,也没有拷贝构造函数
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{
cout << "Derived:: Derived()" << endl;
}
Derived(Derived& d)
{
cout << "Derived:: Derived(Derived& d)" << endl;
}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果及反汇编:
//基类
class Base
{
public:
Base(int i) //基类中有非缺省的构造函数,没有拷贝构造函数
{}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base(1)
{}
Derived(Derived& d)
{}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果:
4. 如果基类的拷贝构造函数和派生类的拷贝构造函数都显式给出了,那么就必须在派生类拷贝构造函数的初始化列表阶段显式调用基类的拷贝构造函数,不然就会变成 3 中的问题,并不会自动调用基类中定义出来的拷贝构造函数
//基类
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
Base(Base& b)
{
cout << "Base::Base(Base& b)" << endl;
}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{
cout << "Derived:: Derived()" << endl;
}
Derived(Derived& d)
{
cout << "Derived:: Derived(Derived& d)" << endl;
}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果及反汇编如下:
只有当基类和派生类的拷贝构造函数都给出,并且基类的拷贝构造函数在派生类中调用时,才会正确:
//基类
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
Base(Base& b)
{
cout << "Base::Base(Base& b)" << endl;
}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base()
{
cout << "Derived:: Derived()" << endl;
}
Derived(Derived& d)
:Base(d)
{
cout << "Derived:: Derived(Derived& d)" << endl;
}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2(d1);
return 0;
}
运行结果:
5. 注意:编译器默认生成的拷贝构造函数可能有浅拷贝问题。
赋值运算符重载函数
当基类中有私有成员时,因为基类的私有成员在派生类中是不可访问的,因此,派生类的operator=中必须要调用基类的operator=完成基类的复制。
//基类
class Base
{
public:
Base()
:_b(0)
{}
~Base()
{}
Base& operator=(Base& b) //基类的赋值运算符重载
{
if (this != &b)
_b = b._b;
return *this;
}
private:
int _b;
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base(),_d(0)
{}
~Derived()
{}
Derived& operator=(Derived& d) //派生类的赋值运算符重载
{
if (this != &d) {
Base::operator=(d);
_d = d._d;
}
return *this;
}
private:
int _d;
};
int main()
{
Derived d1;
Derived d2;
d2 = d1;
return 0;
}
析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员后清理基类成员的顺序。
还是上面的代码,这次我们只创建一个对象,直接看反汇编:
继承与友元函数
同样先看一个例子:
//基类
class Derived;
class Base
{
friend void test(Base& b, Derived& d);
private:
int _b;
};
//派生类
class Derived : public Base
{
private:
int _d;
};
void test(Base& b, Derived& d)
{
cout << b._b << endl;
cout << d._d << endl;
}
int main()
{
Base b;
Derived d;
test(b, d);
return 0;
}
运行结果:
因此,我们可以得出结论:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。# 基类与派生类对象之间的赋值兼容规则
多继承与虚继承
多继承其实分为两种:多层继承和多重继承,多层继承就是多次继承,多重继承就是一个派生类继承多个基类。当多层继承与多重继承结合时,会产生菱形继承的情况。
举个例子:
class Base
{
public:
int _b;
};
class Derived1 : public Base
{
public:
int _d1;
};
class Derived2 : public Base
{
public:
int _d2;
};
class Derived3 : public Derived1, public Derived2
{
public:
int _d3;
};
int main()
{
Derived3 d3;
cout << sizeof(d3) << endl;
d3.Derived1::_b = 1;
d3.Derived2::_b = 2;
return 0;
}
运行结果:
内存分配:
- 在上面的代码中
Derived1
和Derived2
分别继承了Base
,这样它们就分别拥有了Base
的成员,当Derived3
同时继承它们两个时就会产生数据冗余的问题,即:派生类Derived3
中有两对Base
类中的成员。 - 同时,对于基类成员的访问也会存在二义性的问题,必须在访问时加上基类名和作用域限定符来限定。
为了解决这些问题,就引入了虚继承的知识:
虚继承,就是当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类的成员只保留一次。其声明格式如下:
class 派生类名 : virtual 继承方式 基类名
我们对上面的代码进行更改:
class Base
{
public:
int _b;
};
class Derived1 : virtual public Base
{
public:
int _d1;
};
class Derived2 : virtual public Base
{
public:
int _d2;
};
class Derived3 : public Derived1, public Derived2
{
public:
int _d3;
};
int main()
{
Derived3 d3;
cout << sizeof(d3) << endl;
d3._b = 1;
d3._d1 = 2;
d3._d2 = 3;
d3._d3 = 4;
return 0;
}
运行结果:
内存分配:
我们可以看到,通过赋值后,d3
对象的24个字节全部有了值,Base
类的成员只存储了一份,且在最后。同时,还用了8个字节来存储了不知道是什么的东西,按照分布来看应该是Derived1
和 Derived2
中各有一份。这里的8个字节到底存储的是什么呢?
我们知道Base
类的成员放在了Derived3
的最下面,成为了公共的成员,那么Derived1
类和Derived2
类是怎么去访问的呢?这就需要用到那8个字节里面的东西。它们是两个指针,叫做虚基表指针,这两个指针分别指向两个表,叫做虚基表,表中存储的就是公共成员位置的偏移量。通过这个偏移量就可以访问到公共成员。
我们再来看一下这个偏移量:
前面的是Derived1
里面的,后面的 Derived2
里面的,14
对应的就是20个字节,0c
对应的就是12个字节,它们表示的就是公共成员的偏移值。我们可以用下面的图来理解:
以上即为本篇内容,不足之处还望指正!