目录
继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段。
继承是类层次的复用。
基类也叫父类,是被继承的类;
派生类也叫子类,是继承基类的类;
//基类/父类
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "zhangsan";
int _age = 18;
};
//继承类/子类
class Student : public Person
{
protected:
int _stuid; // 学号
};
在student中虽然没有定义name和age,但是实例化对象后发现其中包含了name与age,体现了student类继承了person类的成员函数与变量。
继承的定义格式
class student :public person
// 派生类 继承方式 基类
{
//...
}
继承方式public、protected、private
public继承 | protected继承 | private继承 | |
---|---|---|---|
基类public成员 | 派生类public成员 | 派生类protected成员 | 派生类private成员 |
基类protected成员 | 派生类protected成员 | 派生类protected成员 | 派生类private成员 |
基类private成员 | 派生类中不可见(不能用) | 派生类中不可见(不能用) | 派生类中不可见(不能用) |
public:公有
protected:保护
private:私有
provated与private权限在自己的类中没有区别,区别在继承的派生类中。
- private继承不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 实际中一般使用public继承,不用private继承。
- protected是在派生类外不可以直接访问,在派生类内部可以访问;而private在派生类内部也不能直接访问。二者在基类自己中没有区别。
补充:继承方式也可以不显式写出:class student: person
。
关键字class默认为private继承,关键字struct默认为public继承。
基类与派生类对象的赋值转换——切片
- 派生类对象可以赋值给基类的对象、指针、引用——切片/切割
- 不存在隐式类型转换
- 基类对象不可以赋值给派生类(存在一种特殊情况)
student s;
person p = s;//派生类赋值给基类
person& rp = s;//rp是派生类中基类对象那一部分的引用
person* rpp = &s;//rpp是指向派生类中基类对象的地址
继承中的作用域
- 基类和派生类有独立的作用域
- 基类和派生类中有同名成员,构成隐藏/重定义
- 只要函数名相同就构成隐藏,参数和返回值没有要求
- 若存在隐藏,派生类对象默认使用的是派生类中的对象
- 在派生类成员函数中,可以使用
基类::基类成员
显示访问基类隐藏成员b.A::func();
- 基类与派生类的同名成员不构成重载,因为作用域不同,重载只在一个作用域中。
隐藏/重定义
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。
在子类成员函数中,可以使用基类::基类成员
显示访问。
class A
{
public:
void Func(){}
};
class B : public A
{
public:
void Func(){}
}
int main()
{
B b;
b.Func();//默认调用子类函数
b.A::Func();//显式调用父类函数
return 0;
}
派生类默认成员函数
遵循:父类调用自己的函数对属于自己的成员进行处理,不能直接通过子类对父类成员进行操作。
即:自己处理自己的!——>父类处理父类的,子类处理子类的!
构造函数
基类的成员初始化必须调用基类的构造函数,不能在派生类中对基类成员进行初始化。
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表显示调用基类构造函数。
先调用基类构造——>再调用派生类构造
class person
{
string _name = "zhangsan";
};
class student:public person
{
//1.错误
//student(const char* name, int num)
// :_nume(name)父类成员不能在基类初始化,要调用父类构造函数初始化
// ,_num(num)
// {}
//-------------------------------------------------------------
//2.正确
//student(const char* name, int num)
// :person(name)可以显式调用父类构造初始化
// ,_num(num)
// {}
//-------------------------------------------------------------
//3.正确
student(const char* name, int num)
:_num(num)//不显式写出会自动调用父类默认构造
{}
size_t _num = 0;
};
拷贝构造
派生类的拷贝构造函数必须调用基类的拷贝构造完成对基类成员的拷贝初始化
class person
{
person(const person& p)//可以传入派生类-切片
:_name(p.name)
{}
};
class student:public person
{
student(const student& s)
:person(s)//虽然父类要求传入parent&,但是这里可以通过切片传入子类对象拷贝构造
,_num(s.num)//自己对自己的拷贝
{}
};
operator=
派生类的operator=必须要调用基类的operator=完成对属于基类部分的赋值。
class person
{
person& operator=(const person& p)
{
if(this != &s)
{
_name = p._name;
}
return *this;
}
};
class student:public person
{
student& operator=(const student& s)
{
if(this != &s)
{
person::operator=(s);//调用父类的赋值运算符重载对父类的部分进行赋值
_num = s._num;//然年自己对自己的部分赋值
}
return *this;
}
};
析构函数
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
派生类对象先析构派生类——>再析构基类。
析构函数会被重写成destructer,所以虽然父子类的析构函数函数名不同,但是还是会构成隐藏
析构函数不能显式调用,因为一定会自动调用,是为了保持构造析构的顺序:先析构子,再析构父
class person
{
~parent()
{}
};
class student:public person
{
~student()
{
//会构成隐藏,必须写明类域
//person::~porsen();不能显式调用,会多吊一次,因为一定会自动调用
}
};
析构与构造的顺序理解
虽然从整体看是一个类,但是向栈帧看齐,保持后进先出的原则。
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class person
{
friend void void func(const student& s, const person& p);//只在基类中给出友元
string _name = "zhangsan";
};
class student:public person
{
protected:
size_t _num = 0;
};
void func(const student& s, const person& p)
{
cout << p._name << endl;//可以访问
cout << s._num << endl;//不能访问,友元不能继承
}
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
无论派生出多少个子类,都只有一个static成员实例 。
class person
{
public:
string _name = "zhangsan";
static int _count;
};
int person::_count = 0;
class student:public person
{
public:
string _name = "lisi";
};
int main()
{
person p;
student s;
cout << &(p._name) << endl;
cout << &(s._name) << endl;
//发现地址不一样,不是同一个name
cout << &(p._count) << endl;
cout << &(s._count) << endl;
//发现地址一样,是同一个count
}
多继承、菱形继承、虚继承
多继承
菱形继承
多继承可能会导致出现菱形继承
菱形继承有数据冗余和二义性的问题:
数据冗余:继承类中可能会有多个内容相同的成员,它们的名称可能相同或不同,但是内容相同,会造成空间浪费。
二义性:继承类对象访问相同名称的基类成员时会导致不知道访问哪个。
class A
{
string _name;
};
class B : public A
{
protected:
size_t _num = 0;
};
class C : public A
{
protected:
size_t _id = 0;
};
class D : public B, public C
{
protected:
string _m;
};
int main()
{
D d;
cout << d._name << endl;//二义性,不知道选用B/C谁的name
//隐藏出现在父类与子类之间,BC是两个父类,不会出现隐藏
//可以通过指定作用域来确定是哪个类中的name
//但是只需要一个名字,不需要两个名字,造成数据冗余
a.B::_name = "zhaosi";
a.C::_name = "wangwu";
return 0;
}
如下图所示
虚继承
虚继承是为了解决菱形继承中的数据冗余和二义性
格式:
在继承方式前面加virtual即可:class B : virtual public A
出现菱形继承时使用虚继承与不使用虚继承的对比:
1.不使用虚继承
//不使用虚继承
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;
};
int main()
{
D d;
d.B::_a = 1;//此处不指定作用域会报错,二义性
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
结果:
发现格式与前面说的一致,出现了数据冗余和二义性。
2.使用虚继承
//使用虚继承
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
结果:
此时发现A的数据只储存了一份,没有了数据冗余和二义性的问题。
但是B和C中除了本来应该保存的数据外还多保存了一串地址。
(小端字节序存储,低位存在低地址处,倒过来看就是原本的地址)
- B/C中这个指针叫虚基表指针,指向了一张表——虚基表,虚基表中存储的偏移量,B/C可以通过偏移量来找到A:
B的地址+偏移量 == A的地址- 用同一个类实例化多个对象的时候,比如:
D d1, d2, d3, d4;
这些对象公用一个虚基表,不用开多个,因为同一个类的偏移量都相同。
如图所示:
指向的地址中第一行是偏移量相关,第二行是偏移量。
(第一行存储与多态有关的偏移量,先不解释 )
虚继承的切片
//用前面的虚继承代码实例化
B b = d;
B* ptrb = &d;
C* ptrc = &d;
这种情况下:
切片出来的B中虽然没有A,但是存储有虚基表的地址,通过虚基表内的偏移量可以找到A;
切片出来的ptrb指向D中B的地址;
切片出来的ptrc指向D中B的地址;(不是指向D开头,直接指向所属C的区域)
所以通过虚继承可以解决数据冗余和二义性的问题:
相同的内容只存储一份,通过指针来查找,并且虚基表不用开辟多个,节省空间,因为只有一份相同内容,也不会出现二义性。
补充:多继承时,构造的顺序按列表给出的顺序执行,谁先被继承谁先继承:
class D : public B, public C——>先构造B,再构造C
class D : public C, public B——>先构造C,再构造B
组合
继承和组合本质都是复用。
//组合
class C
{
};
class D
{
private:
C _c;
};
- 优先使用组合,而不是继承;
- 公有继承是is-a的关系;
- 组合是has-a的关系;
- 继承中,基类改动保护成员可能会影响派生类;
组合中,私有和保护改动不会有影响;- 组合的耦合度低,代码维护性好;
- 要实现多态,必须要继承;