一.继承的概念
继承是为了更好的完成代码的复用,从而减少做重复事情的时间。例如,我们定义有一个Person类,它的成员变量有如下图,之后我们又定义了一个Student类,如下图,Student类想要拥有Person类的成员变量,这时我们就可以使用继承来帮助我们完成代码的复用。
class Person
{
protected:
string _name;
int _age;
};
class Student : public Person
{
protected:
string _stu_id;
};
int main()
{
Person p;
Student s;
return 0;
}
我们在调试时可以看到,s中包含了一个基类,除此之外还有他的成员变量。
那么我们是怎样完成继承的呢?
我们的继承方式有多种,分别是public,protected,private继承。
继承方式的区别:
1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 ,都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
二、基类和派生类
1.派生类与基类的转化
我们可以看到,派生类中包含一个基类。所以这里便拥有一项规定,派生类的对象可以赋值给基类的对象,基类的引用,基类的指针。而这个赋值时类似切片的操作,把派生类中的基类切出来。如下:
class Person
{
public:
void Change(const string& s1, const int age)
{
cout << "Person :: Change" << endl;
_name = s1;
_age = age;
}
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
void Change(const string& s1, const int age)
{
cout << "Student :: Change" << endl;
_name = s1;
_age = age;
}
protected:
string _stu_id;
};
int main()
{
Student s;
Person p1 = s;
Person& p2 = s;
Person* p3 = &s;
s.Change("张三", 1);
p1.Change("李四", 2);
p2.Change("王五", 3);
p3->Change("刘六", 4);
return 0;
}
2.派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。如果我们在派生类中显示调用基类的析构函数,则会造成多次析构。
7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲 解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
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 ;
}
3.友元函数
我们可以记住,父亲的朋友不一定是我们的朋友。所以基类的友元不会是派生类的友元。
4.静态成员
如果基类定义了一个静态成员,则整个继承体系中,只有这一个静态成员。
三、菱形继承
单继承:一个子类只有一个直接弗雷时的继承关系
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:
当产生菱形继承时,便会出现数据冗余和二义性的问题。
对于上面的菱形继承。
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时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他方去使用。这里便要用到关键字virtual。
在虚拟继承中,我们把上面的B,C共有的A当成了的最下面一个部分,而B,C中多加了一个指针,用来指向所包含的A,实现方式如下:
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";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}