继承
继承的概念:
指的是基类派生出派生类,派生类从基类那儿继承了已有的一些代码模块,包括数据和函数,可以为自己所使用或者重新定义 以实现新的功能。
继承的作用:
实现代码复用,实现多态
继承的表达形式:
class 派生类 :继承权限 基类 {};
继承权限:
@public成员可以在类内和类外访问
@protected成员可以在类内访问,不可以在类外访问。
@private成员在可以在类内访问,不可以在类外访问。
@protected和private的区别在于,被private继承时,protected变为private,private变为不可访问。
@private 和 不可访问的区别在于,private可以在类内访问,不可再类外访问,不可访问 指类内外都不可访问。
public | protected | private | 继承权限 | |
public | public | protected | private | |
protected | protected | protected | private | |
private | 不可访问 | 不可访问 | 不可访问 | |
基类 |
总结:
@public继承是一个接口继承,保持is-a原则,每个父类可用的成员对 子类也可用,因为每个子类对象也都是一个父类对象
意思就是所有基类出现的地方都可以用子类来代替,因为public继承方式使得子类内部有一个完整父类。
@使用关键字class时默认的继承方式是private,使用struct时默认的 继承方式是public,不过最好显示的写出继承方式
派生类的构造函数与析构函数
1,继承体系下派生类和基类构造函数的调用次序
if(子类没有自定义构造函数)
{
if(基类没有自定义构造函数){
用子类定义对象时,先自动调用基类的默认构造函数,再调子类的默认构造函数。
} else if(基类有自定义无参构造函数){
用子类定义对象时,先自动调用基类的自定义无参构造函数,再调子类的默认构造函数。
}else {
用子类定义对象时,编译提示没有默认构造函数可用
}
}
else if(子类自定义构造函数但没有调用基类的某个构造函数)
{
if(基类没有自定义构造函数){
用子类定义对象时,先自动调用基类的默认构造函数,再调子类的自定义构造函数。
}else if(基类有自定义无参构造函数){
用子类定义对象时,先自动调用基类的自定义无参构造函数,再调子类的自定义构造函数。
}else {
用子类定义对象时,编译提示没有默认构造函数可用
}
}
else{
}
先进入派生类的构造函数,在派生类构造函数的初始化列表执行初始化时调用基类的构造函数,基类构造函数执行完毕,
再接着执行派生类的构造函数。
这个过程就类似入A类的成员包含一个B类对象,在构造A类对象时,也要在执行初始化列表时表用B类的构造函数。
,2,基类没有缺省的构造函数,也就是基类的构造函数必须传参数,派生类必须要在构造函数初始化列表中显示给出基类名和参数 列表。
不然的话,编译器不知道该传什么参数给基类析构函数。
3,基类没有定义构造函数,则派生类也不用定义,全部使用缺省构造函数。就是空的派生类构造函数初始化列表里面有一个空的 基类构造函数。
总结上面3句绕口的话:
就是,基类的构造函数必须要传参时,派生类就必须得定义构造函数,并在初始化列表掉一下基类的构造函数。如果是基类是缺省的构造函数,也就是不必须传参时,可以定义,也可以不用定义。
@继承体系下派生类和基类析构函数的调用次序
》先调用派生类析构函数,执行完析构任务后,在出函数体之前,调用基类的析构函数,执行基类的析构任务,完成后回 到派生类析构函数。
同名隐藏
@(在继承体系中)当基类和派生类的成员变量发生重名时,通过派生类调用同名成员(包括成员变量和成员函数)时,优先调用 派生类的成员。
1,与成员变量类型无关,与成员函数原型无关,只与名字有关。
若就想通过派生类给同名的基类成员赋值,那么就要在成员前加上作用域。
赋值兼容规则
@在公有继承体系下(大前提),派生类可以直接赋值给基类 ,就是因为公有继承关系,基类和派生类是 is-a 的关系,派生类可 以代替基类。
但是基类不能赋值给派生类,因为派生类的空间往往大于基类,强行赋值会导致访问内存出错。
@派生类对象的指针和引用可以指向基类的对象(但是很不安全,容易访问基类以外的空间,不建议使用)。基类对象的指针和引 用可以指向派生类对象,但是指针的指向范围仍然是基类大小。
友员关系不能继承。
若可以继承友员关系,则派生类的私有成员变量,在友员函数中也可以被访问
但是结果是,编译不通过。所以友员关系不能继承。
void FunTest()
{
B b;
b._b1 = 1;
D d;
d._d1 = 1;
}
class B
{
friend void FunTest();
public:
int _b;
private:
int _b1;
};
class D :public B
{
public:
int _d;
private:
int _d1;
};
静态成员变量是可以继承
在基类中定义静态变量,派生类继承基类 ,打印基类和派生类中静态变量的地址,
发现两个地址相同 ,所以静态成员可以继承
class B
{
public:
B()
{
count++;
}
~B()
{
count--;
}
public:
int _b;
static int count ;
private:
int _b1;
};
int B:: count = 0;
class D :public B
{
public:
D()
{
count++;
}
~D()
{
count--;
}
public:
int _d;
private:
int _d1;
};
int main()
{
cout << &B::count << " " << &D::count << endl;
return 0;
}
派生类对象模型
1,单继承:基类在上,派生类特有的空间在下。
2,多继承:先继承的基类在上,后继承的基类在下,派生类特有的空间在最下面。
菱形继承
什么是菱形继承?
B2和B1类都公有继承A类,另一个C类将B1和B2两个类都公有继承
class A
{
public:
int _a;
};
class B1 :public A
{
public:
int _b1;
};
class B2:public A
{
public:
int _b2;
};
class C:public B1,public B2
{
public:
int _c;
};
int main()
{
C c;
c._a = 1;
return 0;
}
编译报错:因为 C类对象访问 _a 成员时,不知道是继承自那个类,有二义性
解决菱形继承二义性的方法?虚拟继承。
虚拟继承
1,虚拟继承的派生类,类的大小为啥会多四个字节?
class B2
{
public:
int _b2;
};
class C: virtual public B2
{
public:
int _c;
};
int main()
{
C c;
cout << sizeof(C);
return 0;
}
运行结果是 :12
解答:虚拟继承的对象模型会多一个指针,这个指针指向偏移量表格,表格中右派生类相对于自己的偏移量和 相对于基类的偏移 量。
多出来的4个字节所指向的偏移量表格,至少有8个字节
2,编译器为派生类合成的构造函数的参数?功能?
答:虚拟继承创建派生类时,必会调用合成构造函数,构造函数的一个参数是一个标记位(另一个是this指针),用来区分是虚拟继 承(标记=1)还是普通继承(标记=0)。是虚拟继承的话,就将偏移量表格的地址放入对象的前四个字节。
3,虚拟继承如何解决菱形继承的二义性?
引进虚基类之后,派生类(子类)的对象中只存在一个虚基类的子对象;当一个类拥有虚基类的时候,编译系统会为这个类的对象定义一个指针成员,并让它指向偏移量表格;(其实就是相对于虚基类的偏移量);这个概念与虚函数表指针不同;在内存中,一般情况下,虚基类子对象在派生类对象中是放置在派生类对象所占内存块的尾部,这一点由编译器来决定的;
class A
{
public:
int _a;
};
class B1 :virtual public A
{
public:
int _b1;
};
class B2:virtual public A
{
public:
int _b2;
};
class C: public B2,public B1
{
public:
int _c;
};
int main()
{
C c;
cout << sizeof(C);
c._a = 1;
c._b1 = 2;
c._b2 = 3;
c._c = 4;
return 0;
}
运行结果是:24 包括4个int变量,和两个表格地址