1. 继承
当几个类拥有多个相同的内容,我们可以将这些相同的内容写成一个新的类,并让这几个类继承它的成员函数及成员变量。
基类,也称父类。派生类,也称子类
class Person
{
public:
void Print()
{
cout<<"name:"<<_name<<endl;
cout<<"age:"<<_age<<endl;
}
string _name="zhangsan"; //运用补丁进行初始化
int _age="18";
};
class Student : public Person
//继承方式 继承对象
{
public:
void Set(const char* name, int age)
{
_name = name;
_age = age;
}
protected:
int _stuid;
};
int main()
{
Studen s;
//s._name="zhangsan";
//s._age=18;
s.Set("zhang san",18);
return 0;
}
1.1 继承方式
- 不可见:基类private成员。不可见是指基类的私有成员还是被继承到了派生类对象中,但派生类对象不管在类里面还是类外面都不能去访问它。
- 基类的除private成员,在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 不指定继承方式默认为private继承
1.2 继承中的作用域
在继承体系中基类和派生类都有独立的作用域。
当父类和子类有同一名称的成员函数(或变量)时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。可以通过访问限定符去访问父类中的该同名成员函数(或变量)。只要名字相同就构成隐藏 。
注意在实际中在继承体系里面最好不要定义同名的成员。
1.3 父类和子类对象赋值转换
子类对象可以赋值给父类的对象/指针/引用。会将子类中父类那部分切来赋值过去,也称切片。
class Person
{
public:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
在实例化一个 Student对象 chy后 Student chy;
该对象可以给赋值Person对象/指针 Person abc=chy;
和引用。这里是语法支持,并不是 Person* Cui=&chy;
强制类型转化。 Person& cui=chy;
父类对象不能给子类对象赋值,但父类指针可以通过强制类型转换赋值给子类指针
Student* sp=(Student*)cui;
1.4 继承与友元
友元(在B类中声明A是B的友元,A可以用B的私有成员,B不能用A的)关系不能被继承。
1.5 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 注:这里是声明 不能给初始值。统计人的个数。
};
int Person_count=0;//static成员定义 不用加static
假如有 class Student:pubilc Person{}; class Teacher:pubilc Person{};
class Other:public Person{};这些类,每当其实例化一个对象,都会优先调用Person的构造函数,都会使同一个count++;
2. 子类的默认成员函数
默认成员函数,即不显示的写,编译器会自动生成的成员函数。构造函数,拷贝构造函数,析构函数,赋值重载,取地址重载,const对象取地址重载
2.1 构造函数
class Person
{
public:
//这里省略了父类的成员函数
string _name;//姓名
}
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
protected:
int _num;
}
子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。子类对象初始化先调用父类构造再调子类构造。
如果父类有默认构造函数,此时就不需要显式调用基类的构造函数,以完成基类部分成员的初始化 。
2.2 析构函数
每个子类析构函数后面,会自动调用父类析构函数。
class Student : public Person
{
public:
~Student()
{
//这里实现子类自己成员的析构
Person::~Person(); //其实无需手动调用父类析构函数
//每个子类析构函数后面,会自动调用父类析构函数
//以保证先析构子类,在析构父类。
}
}
这里Person::~Person()才可以调用父类的析构函数,原因是子类的析构函数跟父类的析构函数构成隐藏。由于后面多台的需要,析构函数名会被编译器同一处理为destructor()
2.3 拷贝构造
class Student:public Person
{
public:
Student(const Student& s)
:Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
}
子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
2.4 赋值运算符重载
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
派生类的operator=必须要调用基类的operator=完成基类的复制。
如果父类的赋值运算符重载定义为私有成员函数,那么子类无法生成默认的赋值运算符重载。
3. 不能被继承的类
3.1 构造函数私有
当子类成员实例化时报错
class A
{
private:
A()
{}
protected:
int _a;
};
class B : public A
{
};
3.2 final关键字
在编译报错
class A final
{
public:
A()
{}
protected:
int _a;
};
class B : public A
{
};
4. 菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。
菱形继承是多继承的特殊情况。
4.1 多继承的成员变量的存放位置
先继承的存放在对象的前面,这里先继承Base1,p3指向的Derive对象d是一块空间,空间的一部分是Base1类中对象的,一部分是Base2类中对象的,由于先继承Base1,所以先访问Base1的成员变量。即p3和p1指向的起始位置相同,但访问形式(跟指针的类型有关)和结束位置不同。
这里p2的地址是高于p1和p3的,原因是这些数据存放在栈上,栈是从高地址往地地址申请空间的。
4.2 菱形继承的问题
一、数据冗余
在Assistant的实例化对象中,有两块空间都有Person类中的成员函数(string _name),一块是从Student类中继承来的,一块是从Teacher类中继承来的。
二、二义性
访问时无法得知是哪一个,需要显示的访问。
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
5. 虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。在Student和 Teacher的继承Person时使用虚拟继承。虚拟继承不要在其他地方去使用。
在肩上的两个类使用 virtual public
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter";
}
5.1 虚拟继承解决数据冗余和二义性的原理
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
不使用虚拟继承的内存对象成员模型:
可以发现内存有既有B类中的_a,又有C类中的_a
菱形虚拟继承的内存对象成员模型
这里A的成员变量放在高地址处,放在哪个位置与编译器有关,没有规定。
B C原本留给自己类中的_a的空间,变成了一个个地址(指针),称为虚基表指针,指向“虚基表”,虚基表中存放的偏移量(十六进制),可以通过这个偏移量与当前的空间进行运算找到真正存放_a的地址。
这样使得B类和C类中的_a是同一个。从而解决了数据冗余的问题。
6. 继承和组合
public继承是一种is-a的关系,每个子类对象都是一个父类对象。
组合是一种has-a的关系。用queue实现priority_queue, 每个priority_queue都有queue对象。
优先使用对象组合,而不是类继承。继承一定程度破坏了父类的封装,子类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。组合类之间没有强依赖关系,耦合度低。
7. 相关考点
- 继承可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展
- 继承除了吸收基类成员之外,一般还需要扩充自己的数据成员,跟基类有所不一样。
- 子类对象不一定比基类对象大,有可能子类只是改写父类的方法而已,并没有增加其自身的数据成员,则大小一样。
- 继承呈现了面相对象程序设计的层次结构,体现了有简单到复杂的认知过程。
- 由于权限问题,基类私有的成员变量在子类中都不能直接访问。
- 如果父类有默认构造函数,此时就不需要显式调用基类的构造函数,以完成基类部分成员的初始化 。
- 友元关系不继承。
- 静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份。静态成员一定是不被包含在对象中的。
- 基类对象中包含了(除静态成员的)所有基类的成员变量。
- 子类对象中不仅包含了(除静态成员的)所有基类成员变量,也包含了(除静态成员的)所有子类成员变量。