什么是继承?
- 继承是面向对象程序设计使代码可以复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。
继承的格式:
实例:
#include <iostream>
using namespace std;
class Base
{
public:
void TestFunc()
{
cout << "Base::TestFunc()" << endl;
}
private:
int _b;
};
class Derived :public Base
{
public:
void TestFunc2()
{
cout << "Derived:TestFunc()" << endl;
}
private:
int _d;
};
继承权限和访问限定符
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
---|---|---|---|---|
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在派生类的访问属性都不变 |
protected继承 | 变为protected成员 | 仍为protected成员 | 不可见 | 基类的非私有成员都成为派生类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类的非私有成员都成为派生类的私有成员 |
protected成员和private成员
class Base
{
protected:
int _b1;
private:
int _b2;
};
class Derived : public Base
{
public:
void SetValue()
{
_b1 = 10;
_b2 = 20;
}
};
- 区别:基类中的protected成员可以在派生类中访问,但是基类中的private成员不能在派生类中访问。
class Base
{
protected:
int _b1;
private:
int _b2;
};
class Derived : public Base
{
public:
int _d;
};
int main()
{
Derived d;
d._b1 = 10;
d._b2 = 20;
}
- 相同点:protected对象和private对象都不能被其派生类对象访问。
注:
- 使用关键字class时默认继承方式为private,使用struct时默认的继承方式是public。
单继承对象模型
class Base
{
public:
int _b;
};
class Derived : public Base
{
public:
int _d;
};
int main()
{
Derived d;
d._b = 10;
d._d = 20;
}
下断点,调出内存窗口,&d,然后单步执行,我们可以发现,先给地址较小的赋值,所以,单继承对象模型如下:
派生类的默认成员函数
- 继承体系下,派生类中如果灭有显示定义这六个成员函数,编译器则会合成这六个默认的成员函数(不是每次都合成,看有没有必要)。
- 说明:
基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
注:
静态成员也会被继承,友元函数不会被继承(因为友元函数不属于类的成员函数)
- 继承体系下派生类和基类构造函数和析构函数的调用次序?
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
~Base()
{
cout << "Base::~Base()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived::Derived()" << endl;
}
~Derived()
{
cout << "Derived::~Derived()" << endl;
}
};
void TestFunc()
{
Derived d;
}
int main()
{
TestFunc();
return 0;
}
- 从上面的打印结果我们不难看出,继承体系下,派生类对象在构造时先去调用自己的构造函数,但在初始化列表中先去调用基类的构造函数,然后再构造自己;在销毁时,调用派生类自己的析构函数,在对自己的资源释放完后,出析构函数之前,再去调用基类的析构函数。
继承体系中的作用域
- 在继承体系中基类和派生类是两个不同作用域
- 子类和父类中有同名成员,这种情况就是同名隐藏,子类成员将屏蔽父类对成员的直接访问(在子类成员函数中,可以使用 基类::基类成员 访问)
- 注意在实际中在继承体系里面最好不要定义同名的成员
class Base
{
public:
void Test()
{
cout << "Base::Test()" << endl;
}
int _b;
int _a;
};
class Derived : public Base
{
public:
void Test()
{
cout << "Derived::Derived()" << endl;
}
int _a;
};
void TestFunc()
{
Derived d;
d._a = 10;
d.Test();
}
int main()
{
TestFunc();
return 0;
}
可以由内存窗口看出来赋值是给派生类中的_a赋值的(单继承对象模型),掉Test函数也是调的派生类中的,这里不会形成函数重载,因为不是一个作用域(基类和派生类是两个不同的作用域)
赋值兼容规则
在public继承权限下,子类和父类对象之间有:
子类对象可以赋值给父类对象, 父类对象不能赋值给子类对象
为什么是这样呢?
父类的指针/引用可以指向子类的对象,子类的指针/引用不可以指向父类的对象。
因为指针要解引用,要是父类的指针指向子类的对象,解引用后只是会访问子类对象中属于父类的成员,这样并没有什么影响;但是,如果是子类的指针指向父类的对象,解引用后就可能会造成非法访问。
什么叫菱形继承
如下,这种继承方式就叫做菱形继承
菱形继承的对象模型
#include <iostream>
using namespace std;
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;
};
int main()
{
cout<<sizeof(D)<<endl;
d1._b = 10;
return 0;
}
但是,这里又会有一个问题,那就是在最底下的派生类D中,会有两份_b,在对象d1中访问_b就会报错,如果想要访问,就必须加访问限定符::
如何解决这种问题呢?就引出了虚拟继承。
什么是虚拟继承?
就是在继承权限的前面再加上virtual关键字
class B
{
public:
int _b;
};
class D : virtual public B
{
public:
int _d;
};
int main()
{
cout << sizeof(D) << endl;
return 0;
}
虚拟继承的对象模型
从单继承对象模型,上面的D大小应该为8字节,但是打印出的结果为12,那么多出的四个字节用来放什么呢?
class B
{
public:
int _b;
};
class D : virtual public B
{
public:
int _d;
};
int main()
{
D d1;
d1._b = 10;
d1._d = 20;
return 0;
}
我们可以看到编译器为d1合成了构造函数,我们之前说过,若非必要,编译器是不会合成构造函数的,那么,这里构造函数做了什么呢?
在对_b和 _d赋值之前,前面的4个字节还存放了类似地址的数据。那么这个指针指向的是什么呢?其实这个指针指向的是一张存放偏移量的表格,所以可以得出虚拟继承的对象模型如下:
菱形虚拟继承
什么是菱形虚拟继承?
如图,就是在上面的两条边的继承加上virtual关键字
菱形虚拟继承的对象模型
class B
{
public:
int _b;
};
class C1: virtual public B
{
public:
int _c1;
};
class C2: virtual public B
{
public:
int _c2;
};
class D : public C1, public C2
{
public:
int _d;
};
int main()
{
D d1;
cout << sizeof(D) << endl;
d1._b = 10;
d1._c1 = 20;
d1._c2 = 30;
d1._d = 40;
return 0;
}
由打印结果可知,大小为24,由内存窗口,可以得出,基类的成员_b确实只存放了一份,这24个字节,除 _b,_c1,_c2,_d外还有8字节,这八字节都放的是地址,指向的也都是一张存放偏移量个表格,所以,菱形虚拟继承的对象模型如下: