继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
2 作用域
在继承体系中基类和派生类都有独立的作用域。
隐藏/重定义: 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,函数名相同即可
切片/切割: 派生类对象可以赋值给基类的对象 / 指针 / 引用。
注意:
namespace test3 {
class Person {
protected:
std::string _name;
std::string _grand;
int _age;
};
class Student : public Person {
public:
int _ID;
};
void mytest() {
Student s1;//子类
//派生类可以给基类的对象 指针 引用赋值 叫做切片
Person p1 = s1;
Person* p2 = &s1;
Person& p3 = s1;
//基类对象一般不可以给派生类对象赋值 下面两种情况除外
Person* p4 = &s1;
Student* s2 = (Student*)p4;//对基类进行强制类型转换
s2->_ID = 100;
Person* p5 = &p1;//p1实际是存储的派生类 但是切片了 因此当作基类对象指针使用
Student* s3 = (Student*)p5;
s3->_ID = 100;
}
};
以下是关于派生类和基类的默认成员函数之间的一些注意事项。
简单来说分为三点,派生类必须对基类进行初始化, 派生类的拷贝构造和赋值运算符重载必须完成对基类的对应函数的调用,构造函数和析构函数顺序问题。
具体如下
namespace test5 {
class Person {
public:
Person(const char* name = "Adam")
:_name(name)
{
std::cout << "Person()" << std::endl;
}
Person(const Person& p)
:_name(p._name)
{
std::cout << "Person(&p)" << std::endl;
}
Person& operator=(const Person& p) {
std::cout << "operator" << std::endl;
if (this != &p) {
_name = p._name;
}
return *this;
}
~Person() {
std::cout << "~person()" << std::endl;
}
protected:
std::string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
std::cout << "Student()" << std::endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
std::cout << "Student(const Student& s)" << std::endl;
}
Student& operator = (const Student& s)
{
std::cout << "Student& operator= (const Student& s)" << std::endl;
if (this != &s)
{
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
std::cout << "~Student()" << std::endl;
}
protected:
int _num;
};
void mytest() {
Student s1("Adam", 20);
Student s2(s1);
//Student s3("vive", 20);
//s1 = s3;
}
};
c++中继承分成单继承和多继承
因为存在多继承于是可能存在下图所示的继承关系
这样的话我们的Assistant类中就会存储两份Person中的数据, **造成数据冗余与二义性。**这就是菱形继承。 而解决的办法就是虚拟继承。
虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
我们借助代码来研究。
namespace test7 {
class A {
public:
int a;
};
class B : public A {
public:
int b;
};
class C : public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
void mytest() {
D d;
d.B::a = 0;
d.C::a = 1;
d.b = 2;
d.c = 3;
d.d = 4;
printf("B->a: %p\n", &(d.B::a));
printf("C->a: %p\n\n", &(d.C::a));
printf("D->b: %p\n", &(d.b));
printf("D->c: %p\n", &(d.c));
printf("D->d: %p\n", &(d.d));
}
};
namespace test6 {
class A {
public:
int a;
};
class B : virtual public A {
public:
int b;
};
class C : virtual public A {
public:
int c;
};
class D : public B, public C {
public :
int d;
};
void mytest() {
D d;
d.B::a = 0;
d.C::a = 1;
d.b = 2;
d.c = 3;
d.d = 4;
printf("B->a: %p\n", &(d.B::a));
printf("C->a: %p\n\n", &(d.C::a));
printf("D->a: %p\n", &(d.a));
printf("D->b: %p\n", &(d.b));
printf("D->c: %p\n", &(d.c));
printf("D->d: %p\n", &(d.d));
}
};
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A.
继承是一种代码复用的手段,但是多继承,菱形继承却带给了我们一些不便,因此在使用继承的时候尽量少用多继承,避免出现菱形继承。
继承与组合类似但是又不同,继承一定程度上的破坏了封装性,增加了程序的耦合性,而组合则不同,因此在平时程序设计中可以选择使用组合和继承的结合使用,不要一味的使用继承。
继承的优缺点
组合的优缺点
其他
友元关系不能被继承,基类友元函数不能访问子类私有和保护成员
基类中有静态成员,派生类继承后仍旧只有一个静态成员。
虚基表存储空间 数据段
菱形继承(不加virtual关机字)类的大小计算。有数据冗余
虚基类需要加上虚基表指针大小,发生菱形继承的类存在多个虚基表指针。