目录
- 一、继承的概念及定义
- 1.1继承的概念
- 1.2继承的定义
- 1.2.1定义格式
- 1.2.2继承方式与访问限定符的组合
- 二、基类和派生类对象赋值转换
- 三、继承中的作用域
- 四、派生类的默认成员函数
- 五、继承与友元
- 六、继承与静态成员
- 七、复杂的菱形继承及菱形虚拟继承
- 八、虚拟继承的原理
一、继承的概念及定义
1.1继承的概念
继承是面向对象的三大特性之一。从字面上再结合程序员的特性就可以大概了解继承的主要思想,
通俗的讲就是继承过来的拥有被继承的属性。它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生的新类,称派生类(或子类),被继承的类称基类(或父类)。如:
#include<iostream>
using namespace std;
class person
{
public:
char* _name;//名字
char* _Id; //身份证
size_t _age;//年龄
int _sex; //1表示男 0表示女
};
class student :public person
{
public:
int _level;//年级
string _major;//专业
};
class teacher :public person
{
public:
int _job_title;//职称
};
int main()
{
return 0;
}
简单来说呢就是person是teacher与student相同的部分所抽出来的。这样的好处就是方便了代码的编写。程序员的最大特点就是会考虑代码的复用,特别是资深的程序员极其讨厌写重复的代码。
1.2继承的定义
1.2.1定义格式
继承方式与访问限定符一样一共有三种。
1.2.2继承方式与访问限定符的组合
注:在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。
二、基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。如:
class person
{
public:
char* _name;//名字
char* _Id; //身份证
size_t _age;//年龄
int _sex; //1表示男 0表示女
};
class student :public person
{
public:
int _level;//年级
string _major;//专业
};
int main()
{
person p;
student s;
p = s;
return 0;
}
注意这里并没有发生隐式的类型转化。如何来证明呢?如下:
三、继承中的作用域
1.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员 显示访问)
2.需要注意的是如果成员函数的隐藏,只需要函数名相同就构成隐藏。
3.注意在实际中在继承体系里面最好不要定义同名的成员。
如:
两个func不会构成重载,是隐藏/重定义关系。要记住重载是发生在同一作用域下的。
四、派生类的默认成员函数
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
class person
{
public:
person(const char* name="左方婷")
:_name(name)
{
}
protected:
string _name;
};
class student : public person
{
public:
student(const char* name = "zuofangting", int num = 20)
:_num(20),
person()
{}
protected:
int _num;
};
int main()
{
return 0;
}
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
student(const student& s)
:_num(s._num),
person(s)
{
cout << "student的拷贝构造函数调用" << endl;
}
这里的person(s)就是上面讲的切片现象。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
student& operator=(const student& s)
{
if (this != &s)
{
_num = s._num;
person::operator=(s);
}
return *this;
}
这里必须要指定是person的作用域下的=重载,不然会无线调用自己的operator=。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。不需要我们自己调用派生类的析构。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个后面会讲
解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。如:
五、继承与友元
一句话概括就是你与父亲是好朋友并不能保证你与我也是好朋友。
class student;
class person
{
public:
friend void Print(const person& p, const student& s);
person(const char* name="左方婷")
:_name(name)
{
cout << "person的构造调用" << endl;
}
person(const person& p)
:_name(p._name)
{
cout << "person的拷贝构造调用" << endl;
}
person& operator=(const person& p)
{
_name = p._name;
}
~person()
{
cout << "person的析构函数调用" << endl;
}
protected:
string _name;
};
class student : public person
{
public:
friend void Print(const person& p, const student& s);
student(const char* name = "zuofangting", int num = 20)
:_num(20),
person(name)
{
cout << "student的构造调用" << endl;
}
student(const student& s)
:_num(s._num),
person(s)
{
cout << "student的拷贝构造函数调用" << endl;
}
student& operator=(const student& s)
{
if (this != &s)
{
_num = s._num;
person::operator=(s);
}
return *this;
}
~student()
{
//person::~person();
cout << "student的析构函数调用" << endl;
}
protected:
int _num;
};
void Print(const person& p, const student& s)
{
cout << p._name << endl;
cout << s._num << endl;
}
int main()
{
student s1("wuzhilong",12);
//student s2(s1);
return 0;
}
六、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。
class person
{
public:
person(const char* name="左方婷")
:_name(name)
{
++_count;
cout << "person的构造调用" << endl;
}
~person()
{
cout << "person的析构函数调用" << endl;
}
static int _count;
protected:
string _name;
};
int person::_count = 0;
class student : public person
{
public:
student(const char* name = "zuofangting", int num = 20)
:_num(20),
person(name)
{
cout << "student的构造调用" << endl;
}
~student()
{
//person::~person();
cout << "student的析构函数调用" << endl;
}
protected:
int _num;
};
int main()
{
student s1("wuzhilong",12);
//student s2(s1);
cout << person::_count << endl;
return 0;
}
七、复杂的菱形继承及菱形虚拟继承
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在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";
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在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";
}
int main()
{
return 0;
}
八、虚拟继承的原理
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下
面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指
向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量
可以找到下面的A。