面向对象的三大特性:封装、继承、多态,今天就让我们来了解什么是C++中的继承吧。
然我们先来看看继承长什么样子。
#include
using namespace std;
class Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "身份证号:" << _id << endl;
cout << "电话:" << _tele << endl;
}
protected:
string _name = "张三";
string _id = "000000";
string _tele = "123456";
};
class Student : public Person
{
public:
protected:
string _stu_id = "00000000";
};
class Teacher : public Person
{
public:
protected:
string _job_num = "00000000";
};
int main()
{
Student s1;
Teacher t1;
s1.Print();
t1.Print();
return 0;
}
像上面这样学生类和老师类包含人物类中的一些成员,相当于继承了人物类的一部分特性,这样就叫继承。写法就是在类名的后面 + 冒号 + 继承方法 + 要继承的类名。
被继承的类叫基类或者父类(Person),继承的类叫做派生类或子类(Student)。
子类可以访问继承到的成员,会有一份父类的成员变量。
而是否可以访问继承到的成员受到两个条件的限制:父类成员访问限定符和子类的继承方式。
需要先介绍一下这个新的访问限定符:protected,这个访问限定符,对于在类域外是不可访问的,但是对于继承的类来说可以访问到它的成员。
访问限定符(父类):
private成员:类域外和子类都不可访问
protected成员:类域外不可访问,子类可以访问
public成员:类域外和子类都可以访问。
继承方式:
private继承:子类不可以访问任何父类成员
protected继承:子类可以访问protected成员和public成员
public继承:子类可以访问所有成员
我们在以前学过一个函数可以被多个函数调用,这叫函数的复用。那这里一个类被多个类继承,拥有了它的特性,其实也是类的复用。
我们知道相近类型的对象会在赋值的过程中发生隐式类型转换。比如double赋值给int。而隐式类型转换的本质是生成临时对象然后再赋值给左值。但是临时对象具有常性。所以:
int main()
{
int a = 0;
double b = 1.1;
a = b;
//所以这么写是错误的
//int& ra = b;
//正确写法
const int& ra = b;
return 0;
}
那我们在父子类中看看是怎么一回事呢?首先子类对象是可以赋值给父类对象的,父类对象中的成员会变得和子类对象中继承的成员的值变得一样。
class Person
{
public:
Person(string name = "张三", string id = "000000", string tele = "123456")
:_name(name)
,_id(id)
,_tele(tele)
{}
void Print()
{
cout << "姓名:" << _name << endl;
cout << "身份证号:" << _id << endl;
cout << "电话:" << _tele << endl;
}
protected:
string _name;
string _id;
string _tele;
};
class Student : public Person
{
public:
Student(string name, string id, string tele)
:Person(name, id, tele)
{}
protected:
string _stu_id = "00000000";
};
int main()
{
Person p1("李四", "111111", "123");
Student s1("张三", "222222", "321");
p1.Print();
s1.Print();
cout << endl;
p1 = s1;
p1.Print();
return 0;
}
那我们再来看一个现象(这里沿用上面代码中的类):
int main()
{
Student s1("李四", "111111", "123");
Person& p1 = s1;
p1.Print();
return 0;
}
我们发现,这段代码竟然可以编译通过还能运行。这里其实是编译器做了特殊处理,当子类赋值给父类的引用时是采用了赋值兼容的方式,类似于切片。
父类引用只是这段空间的别名,而如果是父类的指针被子类对象赋值的话,那父类的指针所指向的内容也只是一个父类的大小。
要注意父类对象不能赋值给子类对象
父类和子类的成员是相互独立的,意味着父类中的成员名可以和子类中的成员名可以一样。
class A
{
public:
void Print()
{
cout << "_a" << " " << "_name" << endl;
}
int _a = 0;
int _num = 1;
};
class B : public A
{
public:
void Print()
{
cout << "_b" << " " << "_name" << endl;
}
int _b = 0;
int _num = 0;
};
int main()
{
B b1;
cout << b1._num << endl;
b1.Print();
return 0;
}
在这里,子类的成员对父类成员构成了隐藏,当使用子类对象调用成员时默认是子类的成员,需要调用父类成员时需要指明类域。
需要注意:只要子类的成员函数和父类的成员函数名字相同,就构成隐藏。
所以尽量父子类中成员名字不要一样。
当子类对象调用子类的六大成员函数时,会自动调用父类的六大成员函数,唯一不同的是,
当实例化子类对象调用子类构造函数时会先调用父类的默认构造函数,如果父类没有默认构造函数,则需要初始化列表中显式调用,所以是先构造父类成员变量,再构造子类成员变量。而子类对象析构函数是调用子类析构函数先析构子类成员变量,再析构父类成员变量(这样做的目的是,防止析构子类成员时需要父类成员,而父类成员的析构是跟子类没有关系的(对父类而言,根本不知道有子类的存在,所以父类的析构就不会含子类任何信息了))(而在多态中析构函数又会有特殊之处,日后再说)。
父类的static成员变量不会被子类继承,而是和子类一起共享这一个静态变量。
class A
{
public:
void Print()
{
num++;
cout << "_a" << " " << "_name" << " " << num << endl;
}
int _a = 0;
int _num = 1;
protected:
static int num;
};
int A::num = 0;
class B : public A
{
public:
void Print()
{
num++;
cout << "_b" << " " << "_name" << " " << num << endl;
}
};
int main()
{
A a1;
B b1;
a1.Print();
b1.Print();
return 0;
}
父类的友元关系无法被继承,意味着父类的友元函数无法访问子类的私有或保护成员。
class A
{
public:
void Print()
{
cout << "_a" << " " << _a << endl;
}
char _a = 'A';
};
class B : public A
{
public:
void Print()
{
cout << "_b" << " " << _b << endl;
}
char _b = 'B';
};
class C : public B
{
public:
void Print()
{
cout << "_c" << " " << _c << endl;
}
char _c = 'C';
};
int main()
{
A a1;
B b1;
C c1;
a1.Print();
b1.Print();
c1.Print();
return 0;
}
像这种只有一个父类叫单继承,但是在现实社会中我们一个人可能扮演着多种角色。比如对于一个具体的人小明来说,他可能是学生兼老师。那么我们用一个类来描述这类人就少不了继承老师类和学生类:
class Student
{
protected:
string _stu_id;
};
class Teacher
{
protected:
string _job_id;
};
class Xiaoming : public Student , public Teacher
{
protected:
//其他信息
int a = 0;
};
int main()
{
Xiaoming x1;
return 0;
}
这里我们需要观察内存所以全使用整形比较方便
像这种有多个父类的继承方式叫多继承。但是学生和老师也有共同点啊。比如他们都是人类。那么:
class Person
{
public:
Person(int name = 1, int gender = 2)
:_name(name)
,_gender(gender)
{}
protected:
int _name;
int _gender;
};
class Student : public Person
{
public:
Student(int stu_id = 3)
:_stu_id(stu_id)
{}
protected:
int _stu_id;
};
class Teacher : public Person
{
public:
Teacher(int job_id = 4)
:_job_id(job_id)
{}
protected:
int _job_id;
};
class Xiaoming : public Student , public Teacher
{
public:
Xiaoming(int a = 5)
:_a(a)
{}
void Print()
{
cout << Student::_name << endl;
}
protected:
//其他信息
int _a;
};
int main()
{
Xiaoming x1;
return 0;
}
这里我们就发现一个问题,学生类中有人物类,老师类中有人物类,小明类中有俩人物类。
当我们要使用其中的变量时:
我们还需要指明类域才可以:
这里出现了数据的二义性,可以通过指明类域的方式来避免,但是数据重复呢?一个人同时有两个正式名字吗?同时有两个性别吗?显然是不正常的。
所以,这里就提出了虚继承:
我们在这里引入一个关键字virtual,虚拟的意思。解决方式如下:
class Person
{
public:
Person(int name = 1, int gender = 2)
:_name(name)
,_gender(gender)
{}
protected:
int _name;
int _gender;
};
class Student : virtual public Person
{
public:
Student(int stu_id = 3)
:_stu_id(stu_id)
{}
virtual void Print()
{
cout << _stu_id << " ";
}
protected:
int _stu_id;
};
class Teacher : virtual public Person
{
public:
Teacher(int job_id = 4)
:_job_id(job_id)
{}
virtual void Print()
{
cout << _job_id << " ";
}
protected:
int _job_id;
};
class Xiaoming : public Student , public Teacher
{
public:
Xiaoming(int a = 5)
:_a(a)
{}
virtual void Print()
{
cout << _a << " ";
}
protected:
//其他信息
int _a;
};
int main()
{
Xiaoming x1;
return 0;
}
这里好像看着没什么变化,这是VS2022给我们看到的表面现象。我们在内存中去看:
先看没有虚拟继承的时候:
显然:
让我们再来看虚拟继承中的:
我们看到是这样一个情况,我们可以大概看出人物类没有重复了,但是他被放在了最下面,而且在原本的学生类和老师类里除了自身的数据外,还有两个数据。这两个数据其实是两个地址。让我们来看学生类中这个地址里是什么:
老师类中:
在这里面,两个都会有这个两个数据,第一个数据先不做解释,日后再说,第二个数据就是偏移量,当派生类对象需要使用基类的成员变量时,会根据这个地址找到存放偏移量的表,然后根据这个偏移量来找到对象中的基类的成员变量。比如当对于老师类找人物类的话。只需要在老师类 + 12个字节就可以找到了。c是十六进制,他的十进制12。
如果这么看的话何必呢,这样不是浪费内存了吗,不虚拟继承的话只需要占用28个字节,需要改基类中的成员变量的话同意改了就可以了,这样的话目前就需要使用44个字节。这么想是错误的,那假如人物类中有一百个int类型的成员变量呢?不虚拟继承的话会凭空多出来400多个字节,虚拟继承的话只需要两张大小8个字节的虚基表就可以正常使用了。其实是节省了内存和开销。这是一个比较简单的认识。
需要注意:
在使用虚拟继承的时候只要在两个继承了同一个类的类上添加关键子virtual就可以了。
举个例子:
可以看到在使用多继承特别是出现菱形继承的情况下,关于派生类和子类之间的处理就会特别麻烦,之后还要有多态的参与,会更加的复杂。所以一般情况下不建议使用多继承特别是菱形继承,而是使用组合的方式。那么什么是组合呢?
class Person
{
public:
Person(int name = 1, int gender = 2)
:_name(name)
, _gender(gender)
{}
protected:
int _name;
int _gender;
};
class Student
{
public:
Student(int stu_id = 3)
:_stu_id(stu_id)
{}
void Print()
{
cout << _stu_id << " ";
}
protected:
Person _p;
int _stu_id;
};
int main()
{
Student s1;
return 0;
}
上述代码就是组合的方式,在需要使用其它类的特性的话可以直接在自己的成员变量中加入人物类。
继承和组合的从意思上来理解就是一个is-a和has-a的理解,继承中你可以说学生“是”一个人,但是使用组合的方法就是学生“有”一个人。
之所以推荐在多继承的复杂场景下使用组合的原因是因为继承从一定程度上提高了耦合度,大家想一想,我们可以在派生类中可以直接对基类的保护或者公共成员变量进行修改。而组合只可以对基类的公共成员进行修改,从逻辑上来说公共成员就是默认可以让你修改的。高耦合度意味着维护代码的代价变高。继承中当你派生类出了问题你还可能得去基类中查找问题,而组合就不需要。但是在日后开发的过程中还要少不了多态,而多态必定意味着继承,那也必须使用继承了。所以关于组合和继承的使用场景,还是需要具体场景具体对待。