继承是C++语言的三大特性之一,通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类型特性的基础上进行扩展,增加功能。
一、继承的实例演示
#include<iostream>
#include<vector>
using namespace std;
// 声明一个基类Person
class Person
{
public:
Person(string n = "张三",int age = 18):_name(n),_age(age) {}
protected:
string _name;
private:
int _age;
};
// 声明一个派生类Student,继承自Person(公有继承)
class Student : public Person {
public:
void Print()
{
cout << _name << endl;
cout << _age <<endl;//这句会报错,因为_age属于Persong私有,派生类中不可见
}
protected:
string _stuid;
};
int main() {
Student s;
s.Print();
return 0;
}
继承的三种方式
继承的方式有三种:公有继承、私有继承、保护继承
基类的private成员在派生类中不可见,如上述例子中Person中的_age成员在派生类Student中不可见。
如果基类成员不想在类外面被访问,但需要其派生类可以访问,那么我们将基类成员定义为protected,这也是private和protected的区别。
限定域:public>protected>private
若基类中的A种成员被派生类以B方式继承,那么基类的成员在派生类中的访问方式我们取A和B限定域小的那个,如:基类中的public成员被派生类以protected方式继承,那么派生类访问该成员的方式为protected;基类中的protected成员被派生类以public方式继承,那么派生类访问该成员的方式为protected。
二、基类和派生类对象赋值转换
在C++中,派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这是因为由于派生类继承了基类的成员,所以派生类对象在内存中的布局与基类对象是兼容的,可以直接进行类型转换。
具体地说,将一个派生类对象赋值给基类对象时,编译器会自动调用从基类继承的构造函数,将派生类对象的基类部分复制到基类对象中,而忽略掉派生类独有的成员。这样就可以通过基类对象来访问派生类继承的成员了。
上述这种赋值行为也被叫做切片,即切去派生类中属于派生类的独有部分,而将属于基类的部分赋值给基类对象。
注意一点,除非基类的指针指向派生类对象,否则基类对象不能给派生类对象赋值。
#include <iostream>
using namespace std;
class Person {
public:
string _name;
int _age;
char _sex;
Person(string name, int age, char sex)
: _name(name), _age(age), _sex(sex) {}
void Print()
{
cout << "基类" << endl;
cout << "name: " << _name << "\nage: " << _age << "\nsex: " << _sex << endl<<endl;
}
};
class Student : public Person {
public:
int _uid;
Student( string name, int age, char sex,int uid)
: Person(name, age, sex), _uid(uid) {}
void display() {
cout << "派生类";
cout << "\nname: " << _name << "\nage: " << _age << "\nsex: " << _sex << "\nuid: " << _uid << endl << endl;
}
};
int main() {
Student s("张三", 18, 'm',1);
Person p = s; // 派生类对象赋值给基类对象
s.display();
p.Print();
//这样也可以
Person* ptr = &s;//指向子类当中父类对象的一部分
Person& ref = s;//变成子类对象当中父类一部分的别名
return 0;
}
结果演示:
三、继承中的作用域
在派生类中,如果定义了与基类同名的成员变量或成员函数,那么该成员会屏蔽基类中同名的成员。这种情况下,在派生类内部访问同名成员时,实际上是访问派生类自己的成员。
下面以Student类为例,演示派生类对基类相同成员的屏蔽:
#include <iostream>
using namespace std;
class Person
{
protected:
string _name = "张三";
int _age = 18;
int _num = 10;
};
class Student :public Person
{
public:
void Print()
{
cout << _num << endl;//默认访问的是Student中的_num
cout << Person::_num<<endl;//可以指定访问Persong中的_num
}
protected:
int _num = 20;
};
int main()
{
Student s;
s.Print();
return 0;
}
我们可以总结以下几点:
在继承体系中基类和派生类都有独立的作用域,因此当派生类中也定义了与基类相同的_num时,不会报错。
基类与派生类中有同名成员时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐蔽,也叫重定义。
对于成员函数来说,只需要函数名相同就构成隐蔽/重定义。
虽然有隐蔽,但我们仍然可以指定访问父类中的成员(如上述例子中的Person::_num),实际中我们应该尽量避免在派生类中定义与基类相同的成员名,即要避免隐蔽/重定义。
一定要区分开函数重载与函数重定义,前者发生在同一作用域,后者发生在不同作用域。
四、派生类的默认成员函数
#include <iostream>
using namespace std;
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 stuid)
:Person(name)//显式调用基类构造函数
,_stuid(stuid)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)//显式调用基类构造函数,派生类赋值给基类(切割)
,_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
//s1 = s3
Student& operator=(const Student& s)
{
if (this != &s)
{
//由于函数名相同即构成隐藏,所以需要显式调用基类的赋值
this->Person::operator=(s);
_stuid = s._stuid;
}
cout << "Student& operator=(con) " << endl;
return *this;
}
//派生类的析构函数与基类的析构函数也构成隐藏
~Student()
{
cout << "~Student()" << endl;
//这里我们不需要显式调用基类的析构函数了
//派生类析构结束以后会自动调用基类的析构函数
}
protected:
int _stuid;
};
总结以下几点:
基类的构造函数,赋值重载需要再派生类中显式调用。
为保证先析构派生类,再析构基类,基类的析构函数会在派生类完成析构后自动调用,所以不需要我们再隐式调用了。
五、友元不会被继承
在C++中,友元关系不会被继承。也就是说,派生类不会自动获得基类的友元关系。我们将基类看做父类,派生类看做子类,还可以理解为:你父亲的朋友不是你的朋友。
虽然友元关系不会被继承,但是我们可以在派生类中重新声明一下友元关系。
#include <iostream>
using namespace std;
class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
string _name;// 姓名
};
class Student :public Person
{
protected:
int _stuNum;// 学号
};
void Print(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;//编译不通过
}
int main()
{
Person p;
Student s;
Print(p, s);
return 0;
}
由于Print不是Student的友元,所以Print不能访问到Student里面的成员这时候我们可以给Student里面声明一下friend void Display(const Person& p, const Student& s);
这样Print就能访问到Student里面的成员了。
#include <iostream>
using namespace std;
class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
string _name = "张三";// 姓名
};
class Student :public Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
int _stuNum = 1;// 学号
};
void Print(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Print(p, s);
return 0;
}
六、继承与静态成员
静态成员在C++中属于整个类,而不是属于某个具体的对象。因此,静态成员可以被继承,但不会被子类所隐藏。
如下面:基类定义了static静态成员,无论派生出多少个子类,整个继承体系里面都只有一个这样的成员。
#include <iostream>
using namespace std;
class A {
public:
static int num;
};
int A::num = 1;
class B : public A {
public:
};
int main() {
A::num = 2;
B::num = 3;//改的是同一个num
cout << "A::num = " << A::num << endl;//3
cout << "B::num = " << B::num << endl;//3
return 0;
}
静态成员变量的继承方式跟普通静态变量一样,在子类中可以直接使用父类的静态成员变量。如果在子类中重新定义同名的静态成员变量,则会覆盖父类中的静态成员变量。
静态成员函数也可以被继承,在子类中可以不加任何修饰符地直接使用父类的静态成员函数。注意,静态成员函数中不能使用this指针,因为this指针只能用于非静态成员函数中。
#include <iostream>
using namespace std;
class A {
public:
static int num;
static void print() {
cout << "A::num = " << num << endl;
}
};
int A::num = 1;
class B : public A {
public:
static int num;
static void print() {
cout << "B::num = " << num << endl;
}
};
int B::num = 5;
int main() {
A::print();
B::print();
return 0;
}
从输出结果来看,静态成员变量num被成功继承,并且在子类中重新定义同名的静态成员变量也能被成功使用;静态成员函数print()也被成功继承,并且子类中重新定义同名的静态成员函数也能被成功调用。
七、复杂继承-菱形继承
单继承是指一个派生类只继承自一个基类。在单继承中,每个类仅具有一个直接基类,并与该基类构成了一条继承链。通常情况下,子类与父类之间具有某种"是一种"("is-a")的关系。例如,一只狗是一种动物,一个圆形是一种图形等等。
多继承是指一个派生类可以继承自多个基类(子类有两个或两个以上的直接父类)。在多继承中,每个类可以具有多个直接基类,并形成多个继承链。
菱形继承是多继承的一种特殊情况,由多继承引发的菱形继承问题会造成数据冗余与二义性。
我们看下面代码:
#include<iostream>
using namespace std;
class A
{
public:
int num;
};
class B:public A
{
public:
int b;
};
class C :public A
{
public:
int c;
};
class D :public B, public C
{
public:
int d;
};
int main()
{
D tmp;
tmp.num = 1;//由于指向不明确,所以此句编译不通过
//我们需要指定才能修改
tmp.B::num = 2;
tmp.C::num = 3;
return 0;
}
图解:
我们可以看到,d中有两个num,所以我们需要指定B中或C中才能访问,虽然解决了问题,但这并不是我们的初衷,D中有两个num,我们认为这种菱形继承造成了数据冗余与二义性。
那么如何具体解决菱形继承造成的数据冗余与二义性问题呢?我们选择引入虚拟继承。
虚拟继承
虚拟继承的核心思想是在多重继承中,将公共基类在派生类中仅存在一份副本,而不是每个基类都继承自一份公共基类,从而避免了菱形继承问题的出现。
我们观察一下使用虚拟继承前后的内存情况:
使用虚拟继承前:
class A
{
public:
int num;
};
class B: public A
{
public:
int b;
};
class C : public A
{
public:
int c;
};
class D :public B, public C
{
public:
int d;
};
int main()
{
D tmp;
cout << sizeof(tmp) << endl;//这里变成了24
tmp.B::num = 1;
tmp.C::num = 2;
tmp.b = 3;
tmp.c = 4;
tmp.d = 5;
return 0;
}
使用虚拟继承之后:
class A
{
public:
int num;
};
class B:virtual public A
{
public:
int b;
};
class C :virtual public A
{
public:
int c;
};
class D :public B, public C
{
public:
int d;
};
int main()
{
D tmp;
cout << sizeof(tmp) << endl;//这里变成了24
tmp.B::num = 1;
tmp.C::num = 2;
tmp.b = 3;
tmp.c = 4;
tmp.d = 5;
tmp.num = 6;
return 0;
}
num放到了公共位置上去了,既没有放到B中,也没有放到C。
B和C中原来存储num的位置存储了一个指针,我们称之为虚基表指针,它指向一个虚基表,虚基表中存储了到num位置的偏移量。所以,由于原来存储num的位置转而存储虚基表指针,又将num存到了公共位置,所以多了四个字节(32位情况下)。
小结:
关键字virtual告诉编译器将类B和类C对类A的继承定义为虚拟继承,这样在类D中就只有一份类A的副本了(由于A被虚拟继承了,A因此也被称为虚基类),避免了菱形继承问题。
使用虚拟继承可以避免由于多重继承而导致的变量或函数二义性问题,但同时也会增加一定的开销,因为编译器需要维护虚基表,以便在派生类中访问正确的基类成员。同时,使用虚拟继承可能也会增加代码复杂度和可读性,因此需要根据具体情况进行选择。比如,在都适用的情况下,我们优先选择类的组合而非类的继承。