【C++】---继承
- 一、继承的概念及定义
- 1、继承的概念
- 2、定义语法格式
- 3、继承基类成员访问方式的变化
- 二、基类 和 派生类 的对象之间的赋值转换
- 1、赋值规则
- 2、切片
- (1)子类对象 赋值 给 父类对象
- (2)子类对象 赋值 给 父类指针
- (3)子类对象 赋值 给 父类引用
- 三、继承中的作用域
- 1、父类和子类都有独立的作用域
- 2、隐藏的概念
- 3、如果是成员函数的隐藏,只需要函数名相同 就构成 隐藏
- 4、继承中最好不要定义同名成员
- 四、派生类的默认成员函数
- 1、规则①
- 2、规则②
- 3、规则③
- 4、规则④
- 五、继承和友元
- 六、继承和静态成员
- 七、菱形继承
- 1、单继承
- 2、多继承
- 3、菱形继承
- 八、菱形虚拟继承
- 1、虚继承
- 2、虚继承的解决数据冗余 和 二义性的原理
- 九、继承和组合
一、继承的概念及定义
1、继承的概念
继承的概念:
继承机制是面向对象程序设计当中使代码复用的重要手段,它允许程序员,在保持原有类特性的基础上,进行拓展,增加功能,产生新的类,这个新的类就叫做:派生类。
继承的出现,呈现出面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
以前我们所接触到的复用都是函数的复用,而继承的出现则是体现了:类设计层次的复用。
2、定义语法格式
#include<iostream>
using namespace std;// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}
protected:string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
protected:int stu_ID;
};
// 派生类:tea
class Teacher :public Person
{
protected:int job_ID;
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
3、继承基类成员访问方式的变化
不需要记住总结上面的表
我给大家总结两点,只需要记住下面两点规律,一切都好说:
(1)基类private成员在派生类中无论以什么方式继承都是不可见的。
(2)基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),访问权限,由大到小:public>protected>private。(Min(成员在基类的访问限定符,继承方式):意思是 取 两者访问权限较小的!)
注意:基类private成员在派生类中无论以什么方式继承都是不可见的
关于什么叫做不可见:
假如将父类成员改为private,那么子类继承了父类的私有成员,内存上,子类对象有这个成员,但是语法上不允许访问:
(可以理解为父类就是你的父亲,而父类的私有成员就是你父亲藏的私房钱。子类就是你自己,不可见的意思,就是说:你知道你父亲藏有私房钱,但是你找不到你见不到!)
二、基类 和 派生类 的对象之间的赋值转换
1、赋值规则
(1)派生类对象可以赋值给:基类对象 / 基类的指针 / 基类的引用。这里有一个形象的说法叫做切割或者切片,就是将派生类中父类那一部分切割下来,赋值过去。(子可以给父)
(2)基类对象不能赋值给派生类对象。(父不能给子)
(3)基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTimeType Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)
2、切片
(1)子类对象 赋值 给 父类对象
子可以赋值给父,但 父不可以赋值给子:
// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象Person p;// p是一个 父类对象s._name = "yjl";s._age = 20;s.stu_ID = 20221071;p = s;s = p;return 0;
}
(2)子类对象 赋值 给 父类指针
① 子类对象可以赋值给父类指针
// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象//Person p;// p是一个 父类对象Person* p;// p是一个 父类对象指针s._name = "yjl";s._age = 20;s.stu_ID = 20221071;p = &s;return 0;
}
② 若父类指针要赋值给子类指针,必须将父类指针强转为子类指针:
int main()
{//父类指针赋值给子类指针Person p;//父类对象Student *s;//子类指针p._name = "yjl";p._sex = "male";p._age = 6;Person* pp = &p;//父类指针s = (Student*)pp;//将父类指针强转为子类型指针后,再赋值给子类型指针return 0;
}
③ 指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界
int main()
{//指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界Person p;p._name = "yjl";p._sex = "male";p._age = 7;//s._No = 21530;Person* pp = &p;Student* ss = (Student*)pp;//ss->_No = 6;访问越界造成程序崩掉return 0;
}
(3)子类对象 赋值 给 父类引用
// 基类
class Person
{
public:void Print(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}string _name = "peter";int _age = 18;
};// 派生类:stu
class Student :public Person
{
public:int stu_ID;
};int main()
{Student s;// s是一个 子类对象//Person p;// p是一个 父类对象//Person* p;// p是一个 父类对象指针s._name = "yjl";s._age = 20;s.stu_ID = 20221071;Person& p=s;// p是一个 父类对象引用return 0;
}
三、继承中的作用域
1、父类和子类都有独立的作用域
2、隐藏的概念
(1)子类和父类中有同名成员,子类成员将屏蔽父类,对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
(2)通过代码来演示:隐藏!
class Person
{
public:string _name = "yjl";int _age = 20;void Print1(){cout << "name->" << _name << endl;cout << "age->" << _age << endl;}
};class Student :public Person
{
public:int _age = 100;void Print2(){cout << "age->" << _age << endl;}
};int main()
{Person p;Student s;s.Print2();return 0;
}
3、如果是成员函数的隐藏,只需要函数名相同 就构成 隐藏
// 父类
class Person
{
public:string _name = "yjl";int _age1 = 20;void Print(){cout << "name->" << _name << endl;cout << "age->" << _age1 << endl;}
};
// 子类
class Student :public Person
{
public:int _age2 = 100;void Print(int count){while (count){cout << "age->" << _age2 << endl;count--;}}
};
int main()
{Person p;Student s;// 这里的s.Print();与父类的Print 构成函数重载×××,构成函数隐藏√√√// 父子类函数构成隐藏,只需要满足一点即可(函数名相同)s.Print();// 这里认为你用子类对象调用的是子类的打印函数,而不是你想要调用的父类打印函数。return 0;
}
4、继承中最好不要定义同名成员
四、派生类的默认成员函数
(1)什么叫做默认成员函数?
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
(2)派生类的默认成员函数和普通类的默认成员函数的不同区别:
- 派生类的构造函数必须要调用基类的构造函数来初始化基类的那一部分成员。如果基类没有写默认的构造函数,那么就必须要在派生类的初始化列表里面对基类的构造函数进行显示调用。(构造:先父后子)
- 派生类的拷贝构造函数必须要调用基类的拷贝构造函数,来进行对基类成员的拷贝构造。
- 派生类的operator=必须要调用基类的operator=完成基类的赋值
- 派生类的析构函数,会在被调用完成之后,自动调用基类的析构函数来清理基类的成员。因为这样才能保证析构函数作用的时候是先析构子类的成员,最后再析构父类的成员的顺序(析构:先子后父)
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加显示调用的情况下,子类析构函数和父类析构函数构成隐藏关系。
总之:派生类=基类+派生类成员(①把基类看作是一个自定义类型的整体->无论是构造机构,拷贝构造,基类就调用自己的默认写的!②派生类成员看作一个普通的类!)
// 父类
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(){cout << "~Person()" << endl;}// 赋值运算符重载Person& operator=(const Person& p){cout << "Person& operator=()" << endl;if (this != &p){_name = p._name;}return *this;}
protected:string _name;
};// 子类
class Student :public Person
{
public:// 构造Student(const char* name,int num):Person(),_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=" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;}// 析构~Student(){cout << "~Student()" << endl;}
protected:int _num;// 学号
};void test()
{Person p1;Student s1("yjl", 2022);//Student s2("yjl666", 2024);//Student s3(s1);//Person p2(s3);}int main()
{test();return 0;
}
1、规则①
规则①:子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。
(1)回忆一下什么是默认构造函数?
- 我们没有写编译器,自动生成的。
- 我们自己写了无参的构造函数。
- 我们自己写了的,带参的全缺省构造函数。
(2)父类里面没有写默认的构造函数和拷贝构造函数:
当父类没有写默认的构造函数的时候,显式调用父类默认构造函数,是把当作一个整体去调父类的构造函数!
(3)父类里面写了默认的构造函数:
总结:
(1)父类有默认构造函数,子类只需要在构造函数中初始化子类自己的成员
(2)父类没有默认构造函数,子类要在构造函数初始化列表显式调用父类的构造函数,再初始化子类自己的成员
2、规则②
规则②:子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
子类的拷贝构造函数该如何写呢?
调用父类拷贝构造函数时,将子类对象作为参数传给父类拷贝构造函数,这时会发生切片行为,将子类对象中父类成员部分切片赋值给父类成员,完成父类拷贝构造;再将子类自己的成员初始化为拷贝对象的子类成员值:
3、规则③
规则③ 子类的operator=必须要调用父类的operator=完成父类的赋值
如果子类的赋值运算符重载函数里只调用子类的operator=,那么会发生栈溢出:
// 赋值运算符重载Student& operator=(const Student& s){cout << "Student& operator=" << endl;if (this != &s){operator=(s);_num = s._num;}return *this;}void test()
{//Person p1;Student s1("yjl", 2022);Student s2("yjl666", 2024);s1 = s2;// 赋值的时候,会发生:栈溢出//Person p2(s3);}
这样写才对:
// 赋值运算符重载Student& operator=(const Student& s){cout << "Student& operator=" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;}
4、规则④
规则④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序
1、
(1)析构顺序:先子后父!
(2)构造顺序:先父后子!
2、注意:由于多态的缘故,对析构的统一处理
// 对析构的统一处理:~Student(){//Person::~Person();~Person();cout << "~Student()" << endl;}
这是由于父类的析构函数和子类的析构函数构成隐藏,奇不奇怪?
按照隐藏的定义,父类和子类的同名函数才会构成隐藏,为什么父类和子类的析构函数不同名也会构成隐藏呢?
这是因为多态的缘故,任何类的析构函数名都会被统一处理成destructor( )。所以父类和子类析构函数同名,会构成隐藏。解决方法就是加父类作用域,指定访问的是父类的析构函数就可以了:
// 析构~Student(){Person::~Person();//~Person();cout << "~Student()" << endl;}
五、继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
void main()
{Person p;Student s;Display(p, s);
}
六、继承和静态成员
如果在父类里面定义了一个static静态成员,那么在整个继承体系当中,无论父类派生出多少个子类,整个体系只有这一个静态成员!
下面这些代码是对静态成员变量的诠释,在整个继承体系中,只要有static静态成员变量,那么它就是同一个!
#include<iostream>using namespace std;class Person
{
public:Person() { _count++; }
protected:string _name;
public:static int _count;
};int Person::_count = 0;class Student : public Person
{
protected:int _ID;
};class Graduate : public Student
{
protected:string _course;
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;// 在上面的继承体系中,它是单继承。// 创建子类对象的时候必须要先构造父类对象,所以进行到此处_count等于4。cout << "人数:" << Person::_count << endl;Student::_count = 0;// 因为已经说明了_count是一个静态的,在整个继承体系中它就是一个整体就是一个// 对_count修改后他就是0了。cout << "人数:" << Person::_count << endl;
}int main()
{TestPerson();return 0;
}
七、菱形继承
1、单继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
2、多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
3、菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
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 _course;
};void Test()
{Assistant a;a._name = "peter";// 这样会有二义性无法明确知道访问的是哪一个_name// 因为,a访问的_name没有指明是student继承person的_name,还是teacher继承person的_name。// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "xxx";
}
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份。
八、菱形虚拟继承
虚拟继承:可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
需要注意的是,虚拟继承不要在其他地方去使用。
1、虚继承
虚继承的语法格式:在两个直接父类的继承方式访问限定符前加vitual:
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 _course;
};void Test()
{Assistant a;a._name = "peter";/*a.Student::_name = "xxx";a.Teacher::_name = "xxx";*/
}int main()
{Test();return 0;
}
2、虚继承的解决数据冗余 和 二义性的原理
虚拟继承解决数据冗余和二义性的原理:
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
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;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余:
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
下面是上面的Person关系菱形虚拟继承的原理解释:
九、继承和组合
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
- 继承和组合:
(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
(2)组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
(3)优先使用对象组合,而不是类继承 :链接:优先使用对象组合,而不是类继承
(4)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。
// 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; // 轮胎};
好了,今天的分享就到这里了
如果对你有帮助,记得点赞👍+关注哦!
我的主页还有其他文章,欢迎学习指点。关注我,让我们一起学习,一起成长吧!