继承的概念和定义
概念
继承是面向对象程序设计使代码复用的重要手段 ,它允许程序员在保证原有类特性的基础上进行扩展 ,增加新的功能,新生成的类叫做子类,也可以成为派生类 ,原有类被称为父类,也叫做基类 。继承呈现了面向对象程序设计的层次结构,是类设计层次的复用。
定义
B也被称为父类,A也被称为子类

继承关系和访问限定符:

继承基类成员访问的变化:

总结:
- 基类private成员在派生类中无论以哪种方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到派生类中,但在语法上限制派生类对象不管在类里面还是类外面都不能去访问。
- 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外面直接被访问,但需要在类中访问,就定义为protected。从这里可以看出保护成员限定符是因为继承才出现的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。最好显示写出继承方式。
- 在实际使用中一般都是public继承,几乎很少使用protected/private继承,也不提倡使用protected/private继承。
基类和派生类对象赋值转换
- 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这可以被形象的称为切片或切割。
- 基类对象不能赋值给派生类对象
- 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用 。但必须是基类的指针是指向派生类对象时才是安全的。

class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public:
int _stuid;
};
void Test()
{
Student sobj;
//1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派生类对象
//sobj = pobj;
//3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj;
Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
ps1->_stuid = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_stuid = 10;
}
继承中的作用域
-
在继承体系中基类和派生类都拥有独立的作用域
-
如果基类和派生类中有同名成员,派生类成员会屏蔽基类对同名成员的直接访问,这用情况叫做隐藏,也叫做重定义。(这种情况需要显示访问,基类::基类成员)
-
如果是成员函数的隐藏,只需要函数名相同就会构成隐藏。
-
在实际的继承体系中最好不要定义同名的成员
class Person
{
protected:
string _name = "Peter";
int _num = 18;
};
class Student : public Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "Person num:" << Person::_num << endl;//指明作用域
cout << "Student num:" << _num << endl;
}
int _num = 20;
};
void Test()
{
Student s;
s.Print();
}//B中的fun和A中的fun构成隐藏,不构成重载,因为不在同一作用域内
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
}
派生类的默认成员函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
- 派生类的operator=必须调用基类的operator=完成基类的复制
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
- 派生类对象初始化先调用基类构造在调用派生类构造
- 派生类对象析构清理先调用派生类析构再调用基类的析构

class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name) //显示调用基类的构造函数
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s) //显示调用基类的拷贝构造函数
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s); //声明调用的operator的类域
//不声明会重复调用Student中的operator
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num;
};
void Test()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
}
输出结果如图:

从上面的输出结果可以看出,构造函数先父后子,析构函数先子后父
继承与友元
友元关系不能被继承,即父类的友元不能访问子类私有和保护成员
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "peter"; // 姓名
};
class Student : public Person
{
protected:
int _stuNum = 18; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
//cout << s._stuNum << endl;
}
void Test()
{
Person p;
Student s;
Display(p, s);
}
继承与静态成员
基类定义了static静态成员,则在整个继承体系中仅有一个这样的成员
class Person
{
protected:
string _name = "peter";
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _num;
};
void Test()
{
Person p;
Student s;
cout << &p._count << endl;
cout << &s._count << endl;
}
输出结果:

复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或两个以上直接父类时称这个继承关系是多继承

菱形继承:菱形继承是多继承的一种特殊关系

从上图我们可以发现,菱形继承的出现会导致Assistant对象中有两份Person成员,这会导致数据的冗余和二义性。可以通过显示指定访问呢来解决二义性问题,但却无法解决代码的冗余问题。
class A
{
public:
A()
:_a(1)
{ }
int _a;
};
class B : public A
{
public:
B()
:_b(2)
{ }
int _b;
};
class C : public A
{
public:
C()
:_c(3)
{ }
int _c;
};
class D : public B, public C
{
public:
D()
:_d(4)
{ }
int _d;
};
void Test()
{
D d;
}
上述代码中我们发现这是个菱形继承,我们通过内存窗口可以发现D对象中存储了两份A,如图:

其中,B中存储了一份A,C中存储了一份A。这就导致了冗余问题的出现。因此为了避免冗余问题的出现,在B和C的继承A时使用虚拟继承就可以解决这一问题。虚拟继承就是在继承方式的前面加一个关键字virtual。代码如下:
class A
{
public:
A(int a = 1)
:_a(a)
{ }
int _a;
};
class B : virtual public A
{
public:
B()
:_b(2)
{ }
int _b;
};
class C : virtual public A
{
public:
C()
:_c(3)
{ }
int _c;
};
class D : public B, public C
{
public:
D(int a = 6)
:A(a)
,_d(4)
{ }
int _d;
};
void Test()
{
D d;
}
内存窗口如图:

从上图我们可以发现,在B和C原本存储A数据的地址存储的不再是A的数据了,存储的是一个地址,而A的数据则存储在D地址的下面。那么B和C中存的地址是个什么的?
这一问题我们也可以通过内存窗口来看,
从内存窗口中,搜索00927bdc这一地址,我们可以发现,在这个地址内存储了一个14,但是由于在内存中存储用到的是16进制,因此换算成10进制就是20,这时我们发现0019F6D8和0019F6EC这两个地址正好差了20个字节,因此我们可以猜测B和C中存储的地址,指向的数据存储的是A的偏移量,通过搜索00927be4我们可以断定这一猜测。如图:

总结:这里是通过B和C的两个指针,指向的一张表来找公共的A。这两个指针叫做虚基表指针,这两个表叫做虚基表。虚基表中存的是偏移量,通过偏移量可以找到公共的A
注:如果一个类不想被继承,可以在类名后面加一个关键字final
class A final
{
};
//class B : public A//无法继承A,编译器报错
//{
//};