继承 :
继承是面向对象程序设计使代码可以复用的重要手段,允许在保持原有类特性的基础上进行扩展,增加功能。
继承定义格式:
继承关系&访问限定符
- 三种类成员访问限定符(public 公有的,protected 保护的,private 私有的)
- 三中继承关系 (public 公有继承,protected 保护继承,private 私有继承 )
继承方式
|
基类成员为public
|
基类成员为protected
|
基类成员为private
| 继承引起的访问控制关系概括 |
public继承
|
仍为
public
|
仍为 protected
|
不可见
|
基类的非私有成员在派生类中的访问属性不改变
|
protected继承 | 变为protected | 仍为protected |
不可见
|
基类的非私有成员在派生类中的访问属性变为protected
|
private继承 |
变为
private
|
变为
private
|
不可见
|
基类的非私有成员在派生类中访问属性变为private
|
三中继承关系的详细分析:
class Base
{
public :
Base()
:pub(1)
, pro(2)
, pri(3)
{
}
public:
int pub;
protected:
int pro;
private:
int pri;
};
//公有继承
class pubDerive :public Base
{
public:
pubDerive()
: Base()
{
}
};
class pubBBase :public pubDerive
{
void Fun()
{
//Base中的数据成员,当pubDerive公有继承Base时,数据成员的访问属性不变\
,pubBBase公有继承pubDerive时,数据成员的访问属性照样不变。所以\
pub可以访问,pro可以访问,pri不能访问
pub;
pro;
pri;
}
};
//保护继承
class proDerive :protected Base
{
public:
proDerive()
: Base()
{
}
};
class proBBase :public proDerive
{
void Fun()
{
//Base中的数据成员,当proDerive保护继承Base时,\
数据成员的访问属性改变公有访问属性的数据h成员变为保护访问属性(public->protected),\
其他访问属性的数据成员的访问属性不改变,
proBBase公有继承pubDerive时,数据成员的访问属性不改变。所以pub、pro的访问属性都为protected,所以可以 访问,pri不能访问
pub;
pro;
pri;
}
};
//私有继承
class priDerive :private Base
{
public:
priDerive()
: Base()
{
}
};
class priBBase :public priDerive
{
void Fun()
{
//Base中的数据成员,当proDerive私有继承Base时,数据成员的访问属性改变\
其他访问属性的数据成员都变为私有访问属性(public、protected->private),\
其他访问属性的数据成员的访问属性不改变,proBBase公有继承pubDerive时,\
数据成员的访问属性不改变。因为priDerive中的数据成员的访问属性都变为了私有属性,所以不能\
在这里访问!
pub;
pro;
pri;
}
};
总结:
- 基类的private成员不能再派生类中被访问,protected成员可以在派生类中被访问,但它不可以在类外访问,可以看出protected限定符是因继承出现的。
- public继承是一个接口继承,每个父类可用的成员对子类也可用,每个子类的对象都是一个父类对象。
- 不管是哪种继承方式,在派生类的内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能被访问)。
- class关键字的默认继承是private,struct的默认继承是public
派生类中的默认成员函数
在继承关系中,派生类中没有显示定义这六个成员函数,编译器则会默认合成六个默认成员函数。
继承关系中构造函数调用顺序:
class Base
{
public :
Base()
:pub(1)
, pro(2)
, pri(3)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
public:
int pub;
protected:
int pro;
private:
int pri;
};
//公有继承
class Derive :public Base
{
public:
Derive()
:DerPub(4)
, Base()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
public :
int DerPub;
};
void FunTest()
{
Derive a;
}
int main()
{
FunTest();
system("pause");
return 0;
}
给出一个基类,让派生类公有继承基类,给出他们的构造函数和析构函数
会发现在调用构造函数时,在进入Derive(派生类)的函数体之前,在初始化列表时就先进入到Base(基类)的构造函数中,从反汇编代码中可以看到,在初始化派生类成员变量之前就先调用了基类构造函数。
继续执行,发现执行完基类的构造函数时,在去执行了派生类的构造函数
在派生类对象析构时,先调用了派生类的析构函数
在派生类的析构函数调用完后才去调了基类的析构函数
重点关注:
从打印结果可以看出创建派生类对象时先调用了基类的构造函数Base(),然后调用了派生类的构造函数Derive(),析构派生类对象时,先调用派生类的析构函数,然后调用基类的析构函数。虽然执行时先调用的是派生类的构造函数,但是在它的初始化列表中,直接转去执行基类的构造函数Base(),然后执行了Deriver()。
说明:
- 基类没有缺省构造函数时,派生类必须在初始化列表中显示给出基类名和参数列表
- 基类没有定义构造函数,派生类就可以不用定义,全部使用缺省构造函数
- 基类定义了带参数的构造函数,则派生类就一定要定义构造函数
继承体系中的作用域
- 基类和派生类是两个不同的作用域
- 同名隐藏:子类和父类中有同名成员,则子类成员会屏蔽父类成员的直接访问。(在子类成员中,可以使用 基类类名::基类成员 访问)
- 注意在实际中,最好不要定义同名的成员
继承与转换 ——赋值兼容规则(以public继承为前提)
- 子类对象可以给父类对象赋值
- 父类对象不能给子类对象赋值
- 父类指针/引用可以指向子类对象
- 子类指针/引用不能指向父类对象(但是可通过强转完成)
注意:
- 友元关系不能继承,基类友元不能访问子类的私有和保护成员
- 基类定义static成员,则无论派生出多少子类,都只有一个static成员实例
单继承&多继承&菱形继承
单继承
一个派生类只有一个直接基类时,这个继承关系就是单继承。
多继承
一个子类有两个或者两个以上的父类时,为多继承。
*
菱形继承(通过菱形继承进一步分析继承之间的关系)
class A
{
public:
A()
:a(1)
{}
public:
int a;
};
class B1 :public A
{
public:
B1()
:b1(2)
{}
public:
int b1;
};
class B2 :public A
{
public:
B2()
:b2(3)
{}
public:
int b2;
};
class C :public B1, public B2
{
public:
C()
:c(4)
{}
public:
int c;
};
B1类和B2类分别单继承A类,C类又多继承B1类和B2类,创建一个C类的对象,发现C类对象的大小是20个字节。
通过对内存的查看发现,C类对象在内存中是下面这样的。
注意:
在这样进行继承时,会发现当C类的对象在访问A类的成员时,不知道是访问继承B1类的还是B2类的,造成二义性问题,还有在C类对象中,肯定会有两个A类的成员,造成数据冗余的问题。
在这里引入虚继承来解决虚继承中二义性问题和数据冗余问题!
什么是虚继承?就是在进行继承时加上 virtual关键字。
那么虚继承是怎样解决问题的呢?
继续使用上面的代码,在B1、B2继承A的时候改为虚继承!
创建一个C类对象,查看他的内存,发现B1类成员变为一个指针和B1类自己的成员,B2类成员变为一个指针和B2类自己的成员
进入那个指针发里面存放的分别是相对于自己成员的偏移量和相对于基类成员的偏移量,这样就可以通过偏移量找到自己的成员也可以找到唯一的基类成员,这样就解决了二义性的问题,也避免了数据冗余。
编译器怎样判断是虚拟继承?
这个是虚拟继承时的反汇编代码
这个是没有虚拟继承时的反汇编代码
发现在有虚拟继承时,会多push一个1,进行标记!