1. 继承是什么?
继承是面向对象编程的三大特征之一,也是代码复用的手段之一。之前我们在很多的地方尝试函数的复用,而继承是为了类的复用提供了很好的方式。
(1)继承的代码怎么写
在一个类后面使用 :继承方式 类名 表示以某种方式继承某个类
下面的代码的意思是学生类和老师类都继承了人类,而且是public继承(这个是什么意思请往后看),也就是说,老师和学生的public成员都可以访问对应人类的public成员,protected成员可以访问对应的protected成员(这个可以看后面关于继承方式对基类成员访问的影响)
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "Gogo";
int _age = 18;
};
class Student : public Person
{
protected:
int _stuID; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
(2)三种继承方式
三种访问限定符对应三种继承方式
访问限定符:public protected private
继承方式: public protected private
基类/父类:指的是被继承的类
派生类/子类:指的是继承的父类的类
类成员\继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类private成员 | 派生类中不可访问 | 派生类中不可访问 | 派生类中不可访问 |
三种访问限定符的大小:public > protected > private
一般来说,派生类的成员的访问方式是继承方式和成员在基类的访问方式的较小的那个
(a)其实,我们多数时候用的都是public继承,因为后两种继承方式的成员只能在派生类中使用,扩展性不够,class默认的继承方式private,struct是public,和成员的默认访问权限是一样的;
(b)基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就需要定义为protected,由此可以看出,protected限定符是因继承才出现的。
2. 赋值转换
看回刚刚那个例子,学生类作为人类的派生类,可以将自己的对象赋值给基类的对象、指针、引用
注意:基类的对象不能赋值给派生类,因为其没有独属于派生类的成员;基类的指针可以强制类型转换成派生类指针,但是基类的指针必须指向派生类对象才行。
class Person
{};
class Student : public Person
{};
int main()
{
Student s;
Person p = s; // 派生类对象赋值给基类对象
Person *ptr = &s; // 派生类对象赋值给基类指针
Person &ref = s; // 派生类对象赋值给基类引用
return 0;
}
【图解】
把派生类中基类的部分赋值过去,这就是所谓的切片或切割,很形象对不对。
3. 隐藏/重定义
在继承关系中,基类和派生类都回有各自的作用域。因此当两者存在同名成员的时候,派生类使用这个名字的成员时,会调用自己类内部的,这被称为隐藏,也叫做重定义。
当然,如果想访问父类的同名成员,也可以用域作用限定符 :: 来进行访问
【例子】
class Person
{
public:
void f(int x)
{
cout << x << endl;
}
protected:
int _telephone = 10086;
};
class Student : public Person
{
public:
void fun()
{
cout << _telephone << endl;
}
void f(double x)
{
cout << x << endl;
}
protected:
int _telephone = 10010;
};
int main()
{
Student s;
s.fun(); // 10010,子类对象对父类成员变量的隐藏
s.f(1.23); // 调用子类的f函数,对成员函数的隐藏
s.Person::f(1); // 调用父类的f函数
return 0;
}
注意:虽然是合法的,但是在继承体系中最好不要定义同名的成员
4. 默认成员函数
关于默认成员函数是什么,可以翻阅我之前的文章,包括类和对象的基础知识都需要了解,这部分内容才能看懂。而在继承的体系中,也有一些特殊的规则需要了解。
构造函数:自动调用基类中的构造函数初始化基类部分的成员,若基类中无默认的构造函数,则需要显示调用基类的构造函数。
先调用基类的构造,再调用派生类的,因为先定义的先构造。
拷贝构造:调用基类的拷贝构造完成对基类成员的拷贝构造
赋值重载:调用基类的赋值重载实现对基类成员的赋值
析构:先调用派生类的析构,再调用基类的析构函数,和构造的顺序正好相反
class Person
{
public:
Person(const string &name, int age)
: _name(name), _age(age)
{
cout << "Person()" << endl;
}
Person(const Person &p)
: _name(p._name), _age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person &operator=(const Person &p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
private:
string _name = "Gogo";
int _age = 18;
};
class Student : public Person
{
Student(const string &name, int age, int id)
: Person(name, age) // 调用基类的构造函数初始化基类的部分成员
, _stuID(id) // 初始化派生类的成员
{
cout << "Student()" << endl;
}
Student(const Student &s)
: Person(s) // 调用基类的拷贝构造函数完成基类成员的拷贝构造
,_stuID(s._stuID) // 拷贝构造派生类的成员
{
cout << "Student(const Student& s)" << endl;
}
Student &operator=(const Student &s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s); // 调用基类的operator=完成基类成员的赋值,切片
_stuID = s._stuID; // 完成派生类成员的赋值
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
// 派生类的析构函数会在被调用完成后自动调用基类的析构函数
}
private:
int _stuID;
};
【重点】
1. 派生类的赋值运算符重载和基类的函数名相同,构成隐藏,在派生类当中调用基类的赋值重载要用域作用限定符;
2. 因为多态的需要,基类和派生类的析构会统一成 destructor() 。因此,要调用父类的析构函数需要域作用限定符
5. 友元和静态成员
友元关系不能继承,当基类的友元函数想访问派生类的私有和保护是不行的。要想访问,必须得成为派生类的友元才能实现。
静态成员:已经定义了一个静态成员,那么在一个继承体系中只能有一个该名称的静态成员
class Person
{
public:
Person()
{
_count++;
}
Person(const Person& p)
{
_count++;
}
protected:
string _name = "Gogo";
int _age = 18;
public:
static int _count;
};
class Student : public Person
{
protected:
int _stuID;
};
int Person::_count = 0; // 静态成员变量在类外初始化
int main()
{
Student s1;
Student s2;
Student s3(s1);
cout << Person::_count << endl; // 运行结果为3
cout << Student::_count << endl; // 也为3
return 0;
}
6. 单继承多继承菱形继承
单继承:一个子类只有一个父类
多继承:一个子类有两个或两个以上的直接父类
菱形继承:多继承的意外
菱形继承看起来平平无奇,但是其实他是存在一定的问题的,比如如果创建了一个Assistant的对象,如果给 _name 成员赋值,其实是无法明确是赋给 Student 还是 Teacher 的成员。
当然我们也可以通过域作用限定符来指定那个 _name ,可以解决二义性的问题,但是没有办法解决冗余的问题,因为 Assistant 对象在 Person 的成员会存在两份。
Assistant a;
a.Student::_name = "Micheal";
a.Teacher::_name = "Micheal";
7. 菱形虚拟继承
这个算是菱形继承的解决方案,那么我们来比较一下普通菱形继承和菱形虚拟继承有什么区别吧
【菱形继承】
class Person
{
public:
int _person;
};
class Student : public Person
{
public:
int _student;
};
class Teacher : public Person
{
public:
int _teacher;
};
class Assistant : public Student, public Teacher
{
public:
int _assistant;
};
int main()
{
Assistant ass;
ass.Student::_person = 1;
ass.Teacher::_person = 2;
ass._student = 3;
ass._teacher = 4;
ass._assistant = 5;
return 0;
}
【菱形虚拟继承】
仅展示变化的部分:
class Student : virtual public Person
{
public:
int _student;
};
class Teacher : virtual public Person
{
public:
int _teacher;
};
从代码上看,似乎没有多大的变化,但是成员变量在内存中的存储方式发生了改变
【普通菱形继承】
可见,_person在Student和Teacher中各存了一份,导致ass对象中存了两个_person,从而导致二义性和冗余。
【菱形虚拟继承】
而菱形虚拟继承是在原先放_person对象的位置存放了公共虚基类成员变量的地址,保证两个_person存在同一个地址,_student和_teacher都能通过地址找到同一个_person。
【总结】
一般不建议使用菱形继承,会导致代码的复杂性和性能出现问题,而且可能使得代码难以分析。
8. 继承和组合
【代码对比】
// 继承
class Car
{
protected:
string _colour;
string _num;
};
class BMW : public Car
{
public:
void Drive()
{
cout << "this is BMW" << endl;
}
};
// 组合
class Tire
{
protected:
string _brand;
size_t _size;
};
class Car
{
protected:
string _colour;
string _num;
Tire _t;
};
1. 继承是 is-a 的关系,组合是 has-a 的关系;
解释:BMW is a Car. / Car has a Tire.
2. 继承在一定程度上破坏了基类的封装,也就是基类的改变对派生类有很大的影响,依赖关系很强,也就是所谓的耦合度高,也称为白箱复用。组合则相反,属于黑箱复用,耦合度较低。
3. 所以在实践中尽可能多使用组合,提高代码的维护性。不过多态的实现必须依赖继承。
如果你能看到这里,给你点个赞!
如果觉得这篇文章不错的话,不妨点个赞支持一下,你的支持是我持续更新的动力~