✍作者:阿润菜菜
专栏:C++
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
这里Person是父类,也叫基类。Student是子类,也叫派生类。派生类以public的方式继承了父类。
那继承方式都有什么呢?他们都各自有什么特性?
继承关系和访问限定符是C++中重要的概念,它们决定了基类和派生类之间的成员可见性和访问权限。
继承关系有三种,分别是public、protected和private
,它们指定了派生类对基类成员的访问方式。
访问限定符也有三种,分别是public、protected和private,
它们指定了类内成员的可见范围。
继承后基类成员的访问权限取决于继承关系和访问限定符的组合,一般来说,基类的private成员在派生类中不可见,而public和protected成员在派生类中的访问权限不会高于继承关系所指定的。
总览表:(比较复杂,我们只需要记住继承后的特性即可)
基类成员 | 继承关系 | 派生类内部 | 派生类外部 |
---|---|---|---|
public | public | public | public |
public | protected | protected | private |
public | private | private | private |
protected | public | protected | private |
protected | protected | protected | private |
protected | private | private | private |
private | public | 不可见 | 不可见 |
private | protected | 不可见 | 不可见 |
private | private | 不可见 | 不可见 |
其实这里派生类三种继承方式,继承关系就是小小取小
同时父类中定义:protected是防外人,private是防儿子
class 默认私有继承 struct默认公有继承
继承大致分为两类 — 基类私有继承 和 基类公有保护继承,实际上一般都用公有继承:
下面是总结特性:
总结:
我们知道隐式类型转换会产生临时变量赋值;而对象赋值转换不会产生临时对象
测试代码:
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
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->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
引用赋值转换:只是子类中父类那一部分的别名 — 切片
指针形式:也是切片
不同的作用域能不能定义同名的变量吗? 可以,我们知道在同一个作用域中不能出现两个名字相同的变量,否则会产生命名冲突;但是在不同的作用域中,允许出现名字相同的变量,它们的作用范围不同,彼此之间不会产生冲突。
那如果如果子类定义了与父类同名变量或者成员函数呢?答案是:子类会优先访问自己类定义的变量,或者成员函数,这种情况叫做隐藏 — 就想访问父类的怎么办?指定作用域就可以了
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// 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);
};
如果是成员函数,函数名相同就构成隐藏了! 未来访问很蛮烦,所以不要定义同名成员函数!
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类
中,这几个成员函数是如何生成的呢?
测试代码:
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);
_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 ;
}
所以,派生类和父类的成员函数分的很清,自己干自己的,父类处理父类的!你不写就默认调用父类的,你写了也要用父类的构造函数
赋值转换:其实就是调用了父类的拷贝构造
注意上面的operator = 与父类构成隐藏了,怎么解决? 加域作用限定符!
结果:
析构我们不需要显示调用 (析构函数还涉及多态问题destructor()),因为涉及析构顺序问题,所以我们不需要显示调用 — 我们不能保证先析构子类后析构父类的顺序
** 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员**。无论派生出多少个子
类,都只有一个static成员实例 。
注意静态成员变量的继承和普通成员变量的继承是不一样的! 子父类共享 static成员
如果我们在父类中添加一个static int _count 成员变量,那么结果会是下面这样:
静态成员变量子父类共享,所以打印出的地址是一样的
测试代码:
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人数 :"<< Person ::_count << endl;
}
那么问题来了如何计算创建了多少个类 ?就是用这个静态变量加到父类构造函数里
如何实现一个不能被继承的类? 把父类的构造函数搞成私有的!那么子类就不能初识化了!继而父类不能被继承了
先来看看图解
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
测试代码:
class Person
{
public :
string _name ; // 姓名
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
二义性可以用空间域作用符解决,但想一想当你需要一个特定ID的时候怎么办?数据冗余的本质 : 就是空间浪费,显然不符合C嘎嘎的风范 ---- C++ 3.0开始填坑 解决菱形继承问题 — 采用虚继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和
Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地
方去使用。
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";
}
我们借助内存窗口观察对象成员的模型
测试代码:
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;
}
编译器的内存观测窗口不一定是真实的!所以我们从内存观察!更底层和真实
这是未采用虚继承的结果:
可以看到成员变量_a内存中产生了两份数据冗余了,而虚继承可以解决解决数据冗余和二义性 :
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
为什么第一行为空? 后续的多态会有解释
下面是上面的Person关系菱形虚拟继承的原理解释:
答案是构造顺序 — A B C D
关于继承的构造顺序一定是先构造基类,虽然声明列表的顺序是先初始化C
总结:不要使用菱形继承
is-a
的关系。也就是说每个派生类对象都是一个基类对象。has-a
的关系。假设B组合了A,每个B对象中都有一个A对象示例代码:
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
1.什么是菱形继承?菱形继承的问题是什么?
2.什么是菱形虚拟继承?如何解决数据冗余和二义性的
3.继承和组合的区别?什么时候用继承?什么时候用组合?
菱形继承是指一个子类多继承两个父类,而这两个父类又同时继承于同一个祖父类,从而形成一个菱形的结构。例如:
class A {
// 祖父类
};
class B : public A {
// 父类1
};
class C : public A {
// 父类2
};
class D : public B, public C {
// 子类
};
菱形继承的问题是,子类会继承两个父类的成员,但两个父类同时继承了祖父类,所以在子类中会有两份祖父类的成员,这就造成了数据冗余和二义性。数据冗余是指子类占用了不必要的空间,浪费了内存资源;二义性是指子类访问祖父类的成员时,需要通过域运算符(::)进行区分,否则编译器无法判断使用哪个父类的成员。
虚拟继承是指在声明派生类时,使用关键字virtual
指定继承方式,从而使得间接继承共同基类时只保留一份基类成员。例如:
class A {
// 祖父类
};
class B : virtual public A {
// 父类1
};
class C : virtual public A {
// 父类2
};
class D : public B, public C {
// 子类
};
虚拟继承可以解决数据冗余和二义性的问题,因为它使得子类只有一份祖父类的成员,而不是两份。这样就节省了空间,并且可以直接通过子类访问祖父类的成员,而不需要域运算符。
继承和组合都是面向对象编程中实现代码复用和抽象的方法。继承是指一个类(子类)可以从另一个类(父类)获得其属性和方法,并且可以对其进行修改或扩展;组合是指一个类(整体)可以包含另一个类(部分)的对象作为其属性,并且可以调用其方法。
继承和组合有各自的优缺点。继承可以实现代码复用和多态性,但也可能导致耦合度过高、层次过深、修改困难等问题;组合可以实现更灵活和松耦合的设计,但也可能导致代码冗余和管理复杂等问题。
一般来说,如果两个类之间存在“是一种”(is-a)的关系,那么可以使用继承;如果两个类之间存在“有一个”(has-a)或“包含(contains)的关系,那么可以使用组合。例如,汽车是一种交通工具,所以汽车类可以继承交通工具类;汽车有一个引擎,所以汽车类可以包含引擎类的对象作为属性。
当然,这些只是一些基本的原则,并不是绝对的。在实际的设计中,需要根据具体的需求和场景来选择合适的方法。有时候,继承和组合也可以结合使用,以达到最佳的效果。