CPP学习之继承
一、继承概念
继承是一种类设计层次的复用,是面向对象编程使代码可以复用的手段,允许编程者在原有基类(父类)的基础上进行扩展,在此基础上产生的新的类称为派生类(子类)。
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "张三"; // 姓名int _age = 18; // 年龄
};class Student : public Person
{
protected:int _stuid; // 学号
};class Teacher : public Person
{
protected:int _jobid; // 工号
};int main()
{Student s;Teacher t;s.Print(); //可使用基类的非private函数t.Print();return 0;
}
从中我们可以看到定义格式:
继承关系和访问限定符:
不同访问方式对继承基类成员的不同点:
注意:
- 无论如何派生类都无法访问基类的private成员;
- 派生类一般以public形式继承即可,这样派生类就可以继承基类的public和protected成员;
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
二、基类与派生类的赋值转换
- 派生类对象可以赋值给基类的对象/指针/引用,这种赋值转换一般称为切片或切割,即将派生类对象中基类的部分赋值给基类个体。
- 基类对象不可赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
void test_1()
{stu s1;teacher t1;person p1;p1 = s1; //子给父,向上转换可行()person& rp = s1; //赋值兼容,切割或者切片,即中间不会产生临时变量。这里是引用,不是寻址//t1 = p1; //父给子报错person* ptrp = &s1; //指针,引用都可以ptrp->_age = 18; //s1改了rp._age = 20;
}
三、继承的作用域
- 在继承中,基类和派生类都有独立的作用域;
- 基类和派生类中若有同名的成员,派生类成员将屏蔽基类同名成员的直接访问,这种情况被称为隐藏或重定义。在派生类成员函数中,可以通过显式访问基类同名成员(基类::基类成员)。
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 最好不要定义同名成员函数!!!
四、派生类的默认成员函数
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
五、继承与友元与静态成员
基类与其友元函数的关系不会被派生类继承。
基类定义的静态成员有且仅有一份存储放在数据段,为所有基类对象和派生类对象所共用。
六、棱形继承及棱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承;
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承;
菱形继承:
从上图可知,棱形继承具有二义性和数据冗余性问题。assitant对象访问_name时无法指定是哪一个的成员,需要特别显式指定访问。如果person基类成员特别多,那么就会造成assistant对象数据冗余。
为了解决这个问题,我们采用虚拟继承的方法,即在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
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类中的B对象的第一个地址存储的是偏移量的地址,这个偏移量指的是B对象第一个变量的地址到A对象第一个变量的地址之间相隔的距离,如图所示为0x14,即20个字节,0x005EF770 - 0x005EF75C = 0x14 。C对象同理。这样就可以找到公共的A。
总的来说,这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
七、总结
-
继承是C++语法复杂性的其中一种体现;
-
继承和组合:
public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。 -
建议优先使用组合而非继承。组合耦合性低,代码易维护,是一种黑箱复用,我们不用知道其内部逻辑;继承耦合性高,若基类有变动,则会对其派生类造成显著影响,是一种白箱复用,我们需要清楚知道继承细节。
以下是测试代码:
#include<iostream>using namespace std;class stu; //用于父类友元函数
class person
{
public:string _name;int _age;int _num; //身份证号void print(){cout << "person_print" << endl;}person(string name = "sean", int age = 18, int num = 999, int _aa = 0):_name(name),_age(age),_num(num),_aa(0){}~person(){cout << "~person()" << endl;}person& operator=(const person& p){_name = p._name;_age = p._age;_num = p._num;_aa = p._aa;return *this;}private:int _aa;public:friend void display(const person& p, const stu& s); //注意,想使用stu类,就要先声明stu类!static int _count;
};int person::_count = 1; //静态成员属于父类和派生类,在派生类中不会单独拷贝一份,继承的是使用权class stu : public person //继承person的public成员变量,private无法继承
{
private:int _stu_id; //学号int _num = 111; //与父类的同名成员,尽量不要出现同名的成员变量public:void print(){cout << _num << endl; //编译器默认先找子类里的同名成员cout << person::_num << endl; //隐藏/重定义:子类和父类有同名成员,子类里隐藏了父类的同名成员//同名函数也可以,也是隐藏重定义。注意:不是重载!因为这里的作用域是不同的!!!cout << "stu_print" << endl;}stu(string name = "sean", int age = 18, int tel = 1234567, int stu_id = 12345, int num = 111):_stu_id(stu_id),_num(num){_name = name; //虽然不可以在初始化列表初始化父类成员变量,但可以在函数内赋值_age = age;person::_num = tel;}stu(const stu& s)//:_age(s._age) //注意:不能用初始化列表来初始化父类成员!:person(s) //可以使用父类的拷贝构造函数来初始化继承的成员变量,因为赋值兼容的原因,所以可以将子类对象赋值给父类对象//因为成员变量声明的先后顺序,初始化顺序是先父类再子类,_stu_id(s._stu_id),_num(s._num){}~stu(){//person::~person(); //由于多态的原因,调用父类的析构要指定父类,而且也不需要自己显式调用//因为显式调用父类析构函数无法保证析构顺序为先子后父//所以子类析构函数完成后就自动调用父类析构函数,这样就保证了析构顺序为先子后父cout << "~stu()" << endl;//而且先析构子再析构父也符合栈的出入顺序。//假设父类与子类是两个独立的类,先构造父类,再构造子类,那么在析构时就是先析构子类再析构父类//假设先析构父类再析构子类,那么在子类析构过程中就不能访问父类成员,而且父类本身不能访问子类成员,//这使先子后父的析构顺序显得尤为重要。}bool operator!=(const stu& s){if (_name != s._name || _age != s._age || _num != s._num || _stu_id != s._stu_id){return true;}return false;}stu& operator=(const stu& s){if (*this != s){person::operator=(s); //注意:这里是隐藏重定义(不是函数重载!!!),用来赋值父类的成员变量_stu_id = s._stu_id;_num = s._num;}return *this;}friend void display(const person& p, const stu& s);void add_count(){_count++; //可以使用父类的静态成员,但不是单独拷贝一份,而是继承使用权}};void display(const person& p, const stu& s)
{cout << "注意:父类友元不会被继承!!!" << endl;cout << p._name << endl;//cout << s._stu_id << endl; //父类友元不会被子类继承!!!除非这个也是子类的友元cout << s._stu_id << endl; //也成为了子类的友元后才可访问
}class worker
{};class teacher : public person, public worker //一个子类可以有多个父类!
{
private:int _work_id = 0; //工号};class assistance : public teacher, public stu //棱形继承,是多继承的一种特殊情况,但会造成数据冗余和二义性(访问不明确,但可以指定访问谁)
{// 为了解决这种继承方式的冗余性,可以在中间类上都加上virtual关键字,使teacher和stu类都是虚拟继承person类!// 虚拟继承后,assistance上关于person的成员变量就会同时被改变(即继承自teacher的与stu的person的成员变量就会相同,不管是值还是地址)};class senior : public stu //注意:此时senior将不会继承person,不是套娃!!!
{};void test_1()
{stu s1;teacher t1;person p1;p1 = s1; //子给父,向上转换可行()person& rp = s1; //赋值兼容,切割或者切片,即中间不会产生临时变量。这里是引用,不是寻址//t1 = p1; //父给子报错person* ptrp = &s1; //指针,引用都可以ptrp->_age = 18; //s1改了rp._age = 20;
}void test_2()
{stu s1;s1.print();s1.person::print(); //指定在父类找
}void test_3()
{stu s1("chan", 20, 7654321, 54321, 0);stu s2(s1);stu s3;s3 = s2;
}void test_4() //父类友元不会被子类继承!!!除非这个也是子类的友元
{stu s1;person p1;display(p1, s1);
}void test_5()
{assistance a1;stu* s1 = &a1;
}//int main()
//{
// //test_1();
// //test_2();
// //test_3();
// //test_5();
//
// return 0;
//}//关于菱形继承
class A
{
public:int _a;
};class B : virtual public A //棱形继承在腰部位置用虚拟继承
{
public:int _b;
};class C : virtual public A //棱形继承在腰部位置用虚拟继承,这样D继承的_a就会是同一个(不分来自B还是C)
{
public:int _c;
};class D : public B, public C //继承也有顺序!先A后B再C//棱形继承慎用!
{
public:int _d = 0;D() //初始化顺序如下: A(), B(), C(), _d(0){ }
};void test_6()
{D d;d.B::_a = 1; //虚拟继承后,无论是来自B的_a还是C的_a,都在同一地址d.C::_a = 2; //解决了数据冗余和二义性问题d._b = 3;d._c = 4;d._d = 5;// 结构上面先是偏移量(_b到_a的距离)地址的上一个地址,然后是_b;// 然后是偏移量(_c到_a的距离)地址的上一个地址,然后是_c,然后是_d,然后是_a// deviation _b to _a -> _b -> deviation _c to _a -> _d -> _a
}void test_7()
{D d;B* pb = &d;C* pc = &d;D* pd = &d;//问:pb,pc,pd的关系。 答:pd == pb != pc//对象d内包含B的和C的,pd指向对象d开头,由于D先继承B,所以pb与pd指向同一个地方,而pc指向D中的C的开头!
}// 白箱复用:组合(在类内声明其他类的成员)class A{ public: B _b;}; has-a的关系
// 黑箱复用:继承,is-a的关系int main()
{//test_6();test_7();return 0;
}