一、继承的概念和定义
(一)、概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类或子类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,继承是类设计层次的复用。
“继承”就是在一个已存在的类的基础上建立的一个新类。已存在的类(例如"人")称为“基类(base class)”或“父类(father class)”。新建立的类(例如“学生”、“工人”、“军人”等)称为“派生类(derived class)” 或 “子类(son class)”。派生类可以一代一代的派生下去,形成了一个庞大的继承层次结构,所有的子孙都继承了祖辈的基本特征,同时又与之区别和发展。
上述例子的继承关系图:
(二)、继承的声明方式
来看下面的代码:
//基类:人
class Person
{
public:
string _name; //姓名
char _sex; //性别
int _age; //年龄
};
//派生类:学生
class Student : public Person //公有继承
{
public:
string _id;//学号
int _score;//学分
};
int main()
{
Student s1;
return 0;
}
调试并进入监视窗口查看s1的成员变量:
派生类既有自己的成员,也有从基类继承而来的基类的成员。
派生类的构成
声明继承的一般形式为:
class 派生类名 : [继承方式] 基类名
{
派生类的成员
};
(三)、派生类的访问属性
继承方式有三种,分别是:public继承、protected继承、private继承。
派生类的访问属性是由基类的成员访问属性和继承的方式共同决定的。
类成员/继承方式 | public继承 | protected继承 | private继承 |
public成员 | public成员 | protected成员 | private成员 |
protected成员 | protected成员 | protected成员 | private成员 |
private成员 | 不可见 | 不可见 | 不可见 |
1、公有继承
在公有继承得到的派生类中, 在公有继承得到的派生类中,其基类的公有成员在派生类中的访问属性为公有,其基类的保护成员在派生类中的访问属性为保护, 而其基类的私有成员在派生类中的访问属性为不可见。
2、保护继承
在保护继承得到的派生类中,其基类的公有成员在派生类中的访问属性为保护,其基类的保护成员在派生类中的访问属性为保护, 而其基类的私有成员在派生类中的访问属性为不可见。
3、私有继承
在私有继承得到的派生类中,其基类的公有成员在派生类中的访问属性为私有,其基类的保护成员在派生类中的访问属性为私有, 而其基类的私有成员在派生类中的访问属性为不可见。
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
二、基类和派生类对象的赋值转换.
因为派生类具有基类的基本特征,所以派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切来赋值过去。
来看下面代码:
#include <iostream>
using namespace std;
//基类:人
class Person
{
public:
string _name = "Non"; //姓名
int _age = 999; //年龄
};
//派生类:学生
class Student : public Person //公有继承
{
public:
string _id;//学号
int _score;//学分
};
int main()
{
Student s1;
s1._name = "张三";
s1._age = 32;
s1._id = "2024729";
s1._score = 180;
Person per1;
cout << per1._name << " " << per1._age << endl;
//赋值转换
//对象
per1 = s1;
cout << per1._name << " " << per1._age << endl;
//指针
Person* p1 = &s1;
cout << p1->_name << " " << p1->_age << endl;
//引用
Person& ref = s1;
cout << ref._name << " " << ref._age << endl;
return 0;
}
运行结果:
Non 999
张三 32
张三 32
张三 32
可以发现,Student类的值、指针、引用赋值给Person类的值、指针、引用后,相当于其派生类Student中基类的那一部分切割给了Person类。
注意:基类对象不能赋值给派生类对象。
三、继承中的作用域和成员的隐藏关系
在继承体系中父类和子类有各自独立的作用域,就像命名空间一样。如果子类和父类中有同名成员(如果是函数,只需同名即可,参数和返回值可不同),子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(可以使用基类::基类成员来显式指定访问基类成员)
(一)、成员变量的隐藏关系
//派生类中的作用域
//基类:A
class A
{
public:
string space = "A域";
};
//派生类:B
class B : public A //公有继承
{
public:
string space = "B域"; //B里的space与A的space同名,构成隐藏,隐藏了A的space
};
int main()
{
B b;
cout << b.space << endl;
cout << b.A::space << endl;
return 0;
}
b有两份相同的成员变量space
运行结果:
B域
A域
A和B类中都有相同的成员变量space,当我们实例化一个B类的对象输出space的值时,输出的是B类的space, A类的spac被隐藏了,如果需要通过B类访问A类中的space成员变量,需要显式加上访问限定符来访问。B类能继承到A类中的同名成员变量space也说明了继承中的父类和子类都是有独自的作用域的。
(二)、成员函数的隐藏
相同的,当父类和子类的成员函数有相同的名字时,就会构成函数的隐藏,注意:只要成员函数名相同即可构成隐藏;隐藏不是重载,在一个域内的同名函数才能构成重载,隐藏的函数不在一个域内;在实际中在继承体系里面最好不要定义同名的成员。
//派生类中的作用域
//基类:A
class A
{
public:
int a = 1;
void display()
{
cout << "A::display() and a = " << a << endl;
}
void func()
{
cout << "A::func()" << endl;
}
};
//派生类:B
class B : public A //公有继承
{
public:
int a = 2;
void display() //同名函数display构成了隐藏
{
cout << "B::display() and a = " << a << endl;
cout << "B::display() and A::a = " << A::a << endl;
}
void func(int b) //只要函数名相同就可以构成隐藏
{
cout << "B::func() b = " << b << endl;
}
};
int main()
{
B b;
b.display();
b.A::display(); //显式调用
cout << endl;
b.func(233);
//b.func(); //无法调用到A类的func函数,因为构成了隐藏关系,需要指定类域
b.A::func();
return 0;
}
运行结果:
B::display() and a = 2
B::display() and A::a = 1
A::display() and a = 1B::func() b = 233
A::func()
四、派生类中的默认成员函数
(一)、构造函数
派生类在实例化时需要对其继承而来的基类的成员变量进行初始化,还需要对自身新增的数据成员进行初始化,下面介绍一下简单的派生类要怎么定义构造函数。
//基类:人
class Person
{
protected:
string _name;
int _age;
public:
//基类的构造函数
Person(const string& name,int age):_name(name),_age(age){}
void display()
{
cout << _name << endl;
cout << _age << endl;
}
};
//派生类:学生
class Student : public Person //公有继承
{
protected:
string _id;//学号
int _score; //学分
public:
Student(const string& name, int age,const string& id, int score):
Person(name,age), //调用基类的构造函数对基类成员变量进行初始化
_id(id),_score(score) //对派生类的成员变量进行初始化
{}
void display()
{
Person::display(); //调用基类的display成员函数打印成员变量的信息
cout << _id << endl;
cout << _score << endl;
}
};
int main()
{
Student s1("张三", 23, "2024731", 180);
s1.display();
return 0;
}
运行结果:
张三
23
2024731
180
由于基类Person没有默认的构造函数,所以我们需要在派生类Student的构造函数的初始化列表处显式调用基类的构造函数对基类的成员变量进行初始化。如果基类存在默认的构造函数,当用户没有在派生类的初始化列表显式调用基类的构造函数进行初始化时,会自动调用基类的默认构造函数对基类成员进行初始化。
派生类构造函数的定义的一般形式为
派生类构造函数名(总参数列表):基类的构造函数名(参数列表),派生类成员的初始化表
{
对派生类进行初始化的初始化语句;
}
如果派生类的成员中有自定义类型,称为子对象,这个时候派生类的构造函数还需对子对象进行初始化。如下面的例子,学生类除了有学号和学分外,还有入学日期。
//基类:人
class Person
{
protected:
string _name;
int _age;
public:
//基类的构造函数
Person(const string& name,int age):_name(name),_age(age){}
void display()
{
cout << _name << endl;
cout << _age << endl;
}
};
//日期类
class Date
{
public:
int _year;
int _month;
int _day;
Date(int year,int month,int day):_year(year),_month(month),_day(day){}
};
//派生类:学生
class Student : public Person
{
protected:
string _id;
int _score;
Date _date;
public:
Student(const string& name, int age,const string& id, int score,int year, int month, int day):
Person(name,age), //先调用基类的构造函数
_date(year,month,day), //再调用子类对象的构造函数
_id(id),_score(score)
{}
//打印基本信息
void display()
{
Person::display();
cout << _id << endl;
cout << _score << endl;
}
//打印入学日期
void displayDate()
{
cout << _date._year << " " << _date._month << " " << _date._day << endl;
}
};
int main()
{
Student s1("张三", 23, "2024731", 180,2023,9,1);
s1.display();
s1.displayDate();
return 0;
}
运行结果:
张三
23
2024731
180
2023 9 1
上面的例子中,日期类Date没有默认的构造函数,所以在对派生类Student进行初始化时,还需在初始化列表处显式调用其子对象的构造函数对子对象进行初始化,如果子对象存在默认构造函数,用户在定义派生类Student的构造函数时没有显式调用子对象的构造函数时,系统会自动调用其默认的构造函数。
总结:
派生类构造函数的功能包括:
1、对基类的数据成员进行初始化。
2、对子对象数据成员进行初始化。
3、对派生类数据成员进行初始化。
派生类构造函数的执行初始化的顺序:
1、先调用基类的构造函数,对基类的数据成员进行初始化。
2、再调用子对象构造函数,对子对象数据成员进行初始化。
3、最后再执行自身派生类的构造函数,对自身数据成员进行初始化。
(二)、拷贝构造函数和赋值构造函数
在一般情况下,默认的拷贝构造函数和赋值构造函数已经能完成我们的需求,即将一个对象的成员变量按字节序拷贝给待拷贝或待赋值的对象,如果涉及数据结构的深拷贝,则需要我们用户显式定义拷贝构造函数和赋值构造函数来完成深拷贝。
//拷贝构造和赋值构造
//基类:人
class Person
{
public:
string _name;
int _age;
//基类的构造函数
Person(const string& name,int age):_name(name),_age(age) { }
//基类的拷贝构造函数
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
_name = p._name;
_age = p._age;
}
//基类的赋值构造函数
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
};
//派生类:学生
class Student : public Person
{
public:
string _id;
int _score;
//派生类的构造函数
Student(const string& name, int age, const string& id,int score):
Person(name,age),
_id(id),_score(score)
{}
//派生类的拷贝构造函数
Student(const Student& s):Person(s) //先调用基类的拷贝构造函数,这里可以传入s是因为赋值转换
{
//对派生类进行拷贝构造
cout << "Student(const Student& s):Person(s)" << endl;
_id = s._id;
_score = s._score;
}
//派生类的赋值构造函数
Student& operator=(const Student& s)
{
if (this != &s)
{
//复用基类的赋值构造函数
Person::operator=(s);
//再对派生类执行赋值构造
cout << "Student& operator=(const Student& s)" << endl;
_id = s._id;
_score = s._score;
}
return *this;
}
};
int main()
{
Student s1("张三", 22, "2024731", 180);
Student s2("李四", 25, "2023731", 270);
//拷贝构造
Student s3(s1);
cout << endl;
//赋值构造
s2 = s1;
return 0;
}
运行结果:
Person(const Person& p)
Student(const Student& s):Person(s)Person& operator=(const Person& p)
Student& operator=(const Student& s)
如果派生类没显式定义拷贝构造函数和赋值构造函数,系统也会自动调用基类的拷贝构造和赋值构造。在派生类中定义对基类成员初始化和拷贝的方法是不行的,在继承中,复用是十分重要的写法,派生类的拷贝构造函数必须调用基类的拷贝构造完成拷贝初始化,而派生类的赋值构造函数必须调用基类的赋值构造函数完成基类的赋值。
(三)、析构函数
在派生类的构造函数中,在派生类初始化前需要调用基类的构造函数,保证先调用基类后派生类的构造函数的顺序。我们的派生类对象在生命周期结束时,需要先调用自身派生类的析构函数,再调用基类的析构函数,调用基类的析构函数不需要我们在派生类的析构函数中显式调用,系统在执行完派生类的析构函数时,会自动帮我们调用基类的析构函数,这是为了保证先调用派生类的析构函数再基类的析构函数的顺序,如果我们显式调用基类析构函数,会破坏这个顺序。一般情况下,析构函数是不需要自己定义实现的,当类里有动态内存申请的情况时,这个时候析构函数就必须需要我们显式定义了。
//拷贝构造和赋值构造
//基类:人
class Person
{
public:
string _name;
int _age;
//基类的构造函数
Person(const string& name, int age) :_name(name), _age(age) { }
//基类的析构函数
~Person()
{
cout << "~Person()" << endl;
//基类析构的代码
//......
}
};
//派生类:学生
class Student : public Person
{
public:
string _id;
int _score;
//派生类的构造函数
Student(const string& name, int age, const string& id, int score) :
Person(name, age),
_id(id), _score(score)
{}
//派生类的析构函数
~Student()
{
cout << "~Student()" << endl;
//派生类析构的代码
//.....
}
};
int main()
{
Student s1("张三", 22, "2024731", 180);
return 0;
}
运行结果:
~Student()
~Person()
五、继承中的友元关系和静态成员
友元关系
基类中的友元关系是不能被派生类继承的,基类友元不能访问子类的私有和保护的成员。举个例子,一个人是父亲的朋友,其子孙继承父亲,这个人肯定不是子孙的朋友,除非这个人后面和子孙结交了新的朋友关系。
//继承中的友元关系
//声明派生类Derive
class Derive;
//基类
class Base
{
private:
int _val1 = 1;
public:
//基类声明友元函数func
friend void func1(const Base& b,const Derive& d);
};
//派生类
class Derive : public Base
{
private:
int _val2 = 2;
public:
//派生类声明友元函数func
friend void func1(const Base& b, const Derive& d);
};
void func1(const Base& b, const Derive& d)
{
cout << b._val1 << endl;
cout << d._val2 << endl;
}
int main()
{
Base b;
Derive d;
func1(b, d);
return 0;
}
如果派生类中没有重新声明友元关系,func1函数是不能访问派生类的成员变量_val2的。
解决办法就是在派生类中也声明和func1的友元关系。
运行结果:
1
2
静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例。通过这个方法可以知道程序当前实例化了多少子类对象。
//static静态成员
//基类
class Person
{
public:
static int _count;
};
//初始化static成员
int Person::_count = 0;
//派生类1:学生类
class Student :public Person
{
public:
//统计学生类子类对象
Student()
{
_count++;
}
~Student()
{
_count--;
}
};
//派生类2:教师类
class Teacher :public Person
{
public:
//统计教师类子类对象
Teacher()
{
_count++;
}
~Teacher()
{
_count--;
}
};
int main()
{
Student s1;
Student s2;
Student s3;
Teacher t1;
cout << "子类实例个数:" << Person::_count << endl;
return 0;
}
运行结果:
子类实例个数:4
六、复杂的菱形继承中的二义性问题和菱形虚拟继承
(一)菱形继承的数据冗余和二义性
菱形继承是多重继承的一种特殊情况,多重继承指的是:一个子类有两个或以上直接父类时称这个继承关系为多继承。如果已声明了类A、类B和类C,可以声明多重继承的派生类D。如下面所示:
class A
{
//..
};
class B
{
//...
};
class C
{
//...
};
//多重继承的派生类D
class D : private A, protected B, public C
{
//...
};
D是多重继承的派生类,以私有继承方式继承了A,以保护的继承方式继承B,以公有的继承方式继承了C。
菱形继承是多重继承的一种特殊形式,假设我们有一个研究生,它既是学生又是老师。其多重继承的图示为:
上面这种继承结构就是典型的菱形继承,而且存在着数据冗余和二义性的问题。下面我们来看代码:
//菱形继承
class Person
{
protected:
string _name;
int _age;
};
//学生类
class Student : public Person
{
protected:
string _id; //学号
};
//老师类
class Teacher : public Person
{
protected:
string _num; //职工号
};
//研究生类
class Graduate : public Student, public Teacher
{
protected:
string _major; //主修专业
public:
void display()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
cout << _num << endl;
cout << _major << endl;
}
};
int main()
{
Graduate g1;
g1.display();
return 0;
}
这段代码编译会报错,报错原因是在打印姓名和年龄是产生了二义性:
去掉报红的两行代码(图中511行和512行)执行程序用监视窗口查看一下g1的成员变量:
g1里有两份相同的Person的成员变量。
派生类Student和Teacher在继承基类Person时,各自包含了_name和_age的成员变量,当我们的研究生类Graduate在继承Student和Teacher时,将其各自包含的_name和_age的成员变量都继承了下来,导致Graduate的成员变量中有两份_name和_age的成员变量,这就造成了数据冗余,并且在访问这些数据成员时,还伴随着二义性的问题。
数据冗余:
虚拟继承可以解决菱形继承中的二义性和数据冗余的问题。C++提供了虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。如上面的继承关系中,在Student和Teacher继承Person时使用虚拟继承,让基类Person成为虚基类,即可解决问题。
现在,将类Student和类Teacher声明为虚拟继承来继承A类,A作为虚基类。
这样二义性的问题得以解决,此时最后的派生类Graduate中只有一份Person的_name和_age的成员变量。
声明为虚基类后的数据成员:
(二)、菱形继承的初始化
如果虚基类中定义了带参数的构造函数,而且没有定义默认的构造函数,则需要再其所有派生类(包括直接派生类和间接派生类)中,通过构造函数的初始化表对虚基类进行初始化。
现在,为了介绍菱形继承的初始化的过程,给上面的四个类加上构造函数。
//菱形继承
class Person
{
protected:
string _name;
int _age;
public:
Person(const string& name,int age):_name(name),_age(age) {}
};
//学生类
class Student :virtual public Person //Student虚拟继承Person Person是虚基类
{
protected:
string _id; //学号
public:
Student(const string& name, int age, const string& id) :
Person(name, age), //调用虚基类的构造函数
_id(id)
{}
};
//老师类
class Teacher :virtual public Person //Teacher虚拟继承Person Person是虚基类
{
protected:
string _num; //职工号
public:
Teacher(const string& name, int age, const string& num):
Person(name,age), //调用虚基类的构造函数
_num(num)
{}
};
//研究生类
class Graduate : public Student, public Teacher
{
protected:
string _major; //主修专业
public:
Graduate(const string& name, int age, const string& id, const string& num, const string& major)
:Person(name,age), //这里还需调用虚基类的构造函数
Student(name,age,id), //调用直接基类的构造函数
Teacher(name,age,num), //调用直接基类的构造函数
_major(major)
{}
void display()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
cout << _num << endl;
cout << _major << endl;
}
};
int main()
{
Graduate g1("张三",22,"2024731","23xxxx","计算机与科学");
g1.display();
return 0;
}
运行结果:
张三
22
2024731
23xxxx
计算机与科学
注意:在定义类Graduate的构造函数时,与当基类Person不是虚基类时有所不同。如果基类Person不是虚基类,派生类的构造函数只需要负责对其直接基类初始化,即Graduate只需要调用直接基类Student和Graduate的构造函数,再由直接基类Student和Graduate中的构造函数负责对间接基类Person进行初始化。现在,由于虚基类在派生类Graduate中只有一份成员变量,所以这份数据成员的初始化必须在Graduate中直接给出。如果不在Graduate中直接给出Person的初始化,就会导致在Student和Teacher类中的构造函数对虚基类初始化给出了不同的初始化参数而产生矛盾。所以规定,在最后的派生类中不仅要对直接基类进行初始化,还需要对虚基类进行初始化。由于Person虚基类的成员在最后的派生类中只保留了一份,而最后的派生类已经给出了对虚基类的初始化,所以在虚基类的直接派生类Student和Graduate中对虚基类Person初始化所调用的构造函数不会被执行,C++编译系统最后只执行最后的派生类对虚基类构造函数的调用,所以不用担心虚基类的构造函数会被多次调用的问题。
七、继承的总结
1、虚拟继承不要在除了上面菱形继承所导致的数据冗余的情况下使用。因为虚拟继承在最后的派生类中只保留一份成员变量这个功能需要靠虚基表来实现,这个虚基表是会占用一定内存的。
2、多继承和虚拟菱形继承会导致继承体系会变得十分复杂,所以一般情况下不要设计多继承,尤其是菱形继承。
3、继承和组合
继承就是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。比如Dog类继承了Animal的基类,派生类对象Dog就是一个基类对象Animal,Dog is-a Animal。
组合是一种has-a关系。假设有一个车类Car和一个轮胎类Tire,Car类里声明定义一个Tire对类的成员对象,相当于每个车对象中有一个轮胎对象,这就是Car has-a Tire关系。
在平常编程解决实际问题时,能用组合那就用组合,组合不合适的情况下再考虑使用继承。这是因为使用组合不会增加程序之间的耦合度,而继承由于基类和派生类之间的紧密关系,会导致程序的耦合度变高。