继承
一.继承的概念及定义
1.继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称子类(派生类)。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用,同时类模版也是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
class Student
{
public:
void identity() //身份验证
{}
void study() //学习
{}
protected:
string _name = "peter"; //姓名
string _address; //地址
string _tel; //电话
int _age = 18; //年龄
int _stuid; //学号
};
class Teacher
{
public:
void identity() //身份验证
{}
void teaching() //授课
{}
protected:
string _name = "张三"; //姓名
int _age = 38; //年龄
string _address; //地址
string _tel; //电话
string _title; //职称
};
下面我们公共的成员都放到Person类中,Student和Teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。
class Person
{
public:
void identity() //身份验证
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; //姓名
string _address; //地址
string _tel; //电话
int _age = 18; //年龄
};
class Student : public Person
{
public:
void study() //学习
{}
protected:
int _stuid; //学号
};
class Teacher : public Person
{
public:
void teaching() //授课
{}
protected:
string title; //职称
};
int main()
{
Student s;
Teacher t;
s.identity(); //void identity()张三
t.identity(); //void identity()张三
return 0;
}
2.继承的定义
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。(因为翻译的原因,所以既叫父类/子类,也叫基类/派生类)
二.继承父类后成员访问权限的变化
-
父类的private成员在子类中无论以什么方式继承都是不可见的。这的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
-
父类private成员在子类中是不能被访问,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
虽然不能再子类中直接访问,但是可以调用继承下来的父类中public访问权限的函数接口,间接访问父类中的私有成员,如下:
-
实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类的其他成员在子类的访问方式 == Min(父类成员在父类的访问限定符,继承方式),public > protected > private。
-
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际中扩展维护性不强。
三.类模版的继承
namespace xzy
{
//stack公有继承vector
template<class T>
class stack : public vector<T>
{
public:
void push(const T& x)
{
//push_back(x); 编译报错
//父类是类模板时,需要指定一下类域
//否则编译报错:error C3861: “push_back”: 找不到标识符
//因为stack<int>实例化时,也实例化vector<int>了
//但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x); //先在子类中找,再到父类中找
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
//private:
//vector<T> v; 组合
};
}
int main()
{
xzy::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
//可以利用宏进行替换,使其低层容器修改变得容易
//#define CONTAINER vector
//#define CONTAINER list
#define CONTAINER deque
namespace xzy
{
template<class T>
class stack : public CONTAINER<T>
{
public:
void push(const T& x)
{
CONTAINER<T>::push_back(x);
}
void pop()
{
CONTAINER<T>::pop_back();
}
const T& top()
{
return CONTAINER<T>::back();
}
bool empty()
{
return CONTAINER<T>::empty();
}
};
}
总结:子类继承父类(父类是模版时),子类成员函数调用父类成员函数时,需要指定类域(按需实例化)。
四.父类和子类对象赋值兼容转换
- public继承的
子类对象
可以赋值给父类的对象
/父类的指针
/父类的引用
。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去,注意:这里没有发生类型转换
,可以看成C++中的特殊处理。
-
父类对象不能赋值给子类对象。
-
父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast来进行识别后进行安全转换。
class Person
{
public:
string _name; //姓名
protected:
string _sex; //性别
int _age; //年龄
};
class Student : public Person
{
public:
int _No; //学号
};
int main()
{
int i = 10;
double d = i;
const double& rd = i; //这里产生了临时变量具有常性,要加const
Student s;
//1.子类对象可以赋值给父类对象/指针/引用
Person p = s;
Person* pp = &s;
Person& rp = s; //注意:这里没有产生临时对象,所以不加const
rp._name = "张三"; //rp是子类中(父子类相同的成员变量的引用),修改子类影响父类
//2.父类对象不能赋值给子类对象,这里会编译报错
//s = p;
//3.父类的指针/引用可以通过强制类型转换赋值给子类的指针/引用
//原因:可能父类的指针存放的是子类的地址
//Student* ps = pp; 编译报错,必须强转
Student* ps = (Student*)pp;
Student& rs = (Student&)rp;
return 0;
}
class Person
{
virtual void func() //虚函数
{}
protected:
string _name; //姓名
string _sex; //性别
int _age; //年龄
};
class Student : public Person
{
public:
int _No; //学号
};
int main()
{
Student s;
Person p = s;
Person* pp = &s;
Person& rp = s;
//pp指向子类对象
Student* ps1 = dynamic_cast<Student*>(pp);
cout << ps1 << endl; //输出00DCF824
//pp指向父类对象
pp = &p;
Student* ps2 = dynamic_cast<Student*>(pp);
cout << ps2 << endl; //输出失败的地址:00000000
return 0;
}
五.继承中的作用域
1.隐藏规则
- 在继承体系中父类和子类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用父类::父类成员显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
class Person
{
protected:
string _name = "张三"; //姓名
int _num = 111; //身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
//Student的_num和Person的_num构成隐藏关系
//可以看出这样代码虽然能跑,但是非常容易混淆
//子类中有,默认先访问子类,若子类中没有父类中有,则访问父类
cout << "学号:" << _num << endl;
//若子类和父类同时存在,要想访问父类要加上作用域
cout << "身份证号:" << Person::_num << endl;
}
protected:
int _num = 999; // 学号
};
2.继承作用域相关面试题
六.子类的默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在子类中,这几个成员函数是如何生成的呢?
-
子类的构造函数必须调用父类的构造函数初始化父类的那⼀部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用,否则编译报错。
-
子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
-
子类的operator=必须要调用父类的operator=完成父类的复制。需要注意的是子类的operator=隐藏了父类的operator=,所以显示调用父类的operator=,需要指定父类作用域。
-
子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。
-
子类对象初始化先调用父类构造再调子类构造。
-
子类对象析构清理先调用子类析构再调父类的析构。
-
因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
class Person
{
public:
Person(const char* name = "zhangsan")
:_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:
//默认生成的构造函数的行为
//1.内置类型: 不确定,取决于编译器
//2.自定义类型:调用它的默认构造
//3.继承的父类:看作一个整体,要求调用父类的默认构造
Student()
{}
Student(const char* name, int num, const char* address)
:Person(name) //显示调用默认构造 注意:不能_name(name)这样初始化
//而是看成一个整体,类似匿名父类对象,将其看成一个C++的规定
,_num(num)
,_address(address)
{}
//严格来说Student的拷贝构造默认生成的就够用了
//如果有需要深拷贝的资源,才需要自己实现
Student(const Student& s)
:Person(s) //传入子类对象,进行了赋值兼容转换,切割给父类对象,完成拷贝构造
//拷贝构造也是构造函数,所有的成员都要走初始化列表
//若不写Person(s),则会调用父类的默认构造初始化父类的成员_name
//但是main函数中达不到拷贝需求
,_num(s._num)
,_address(s._address)
{
//深拷贝
}
//严格来说Student的赋值重载默认生成的就够用了
//如果有需要深拷贝的资源,才需要自己实现
Student& operator=(const Student& s)
{
if (this != &s)
{
//注意:需要指定类域,否则默认调用子类中的operator=,造成栈溢出
Person::operator=(s); //父子类的重载operator=构成隐藏,调用父类的operator=,完成父类对象的赋值
_num = s._num;
_address = s._address;
}
return *this;
}
//严格来说Student的析构函数默认生成的就够用了
//如果有需要释放的资源,才需要自己实现
~Student()
{
//~Person(); 编译报错
//析构函数~Student()和~Person()都被特殊处理成destructor11()
//所以子类的析构与父类的析构也构成了隐藏关系
//Person::~Person(); 需要指定类域,析构函数可以直接调用
//注意:父类不需要显示调用析构,子类析构函数之后,会自动调用父类析构
//规定:析构的顺序是——>先析构子类对象,后析构父类对象
//显示调用不能保证先子后父
//delete[] ptr;
}
protected:
int _num = 123456; //学号
string _address = "乐平"; //地址
//int* ptr = new int[10];
};
int main()
{
//若子类和父类都有默认构造,先调用父类的默认构造,再调用子类的默认构造
Student s1;
//若父类没有默认构造,需要在子类的初始化列表中显示调用,否则编译报错
Student s2("张三", 123, "洛阳");
//拷贝构造
Student s3(s2);
//赋值
s1 = s3;
//使用类类型的指针,可以用指针直接调用析构函数
Person* ptr = new Person();
ptr->~Person();
free(ptr);
//delete ptr;
return 0;
}
七.实现一个不能被继承的类
- 父类的构造函数私有,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化以后,子类看不见就不能调用了,那么子类就无法实例化出对象。
- C++11新增了一个final关键字,final修改父类,子类就不能继承了。
//C++11的方法
class Base final
{
public:
void func1() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
//C++98的方法
//Base(){}
};
class Derive :public Base //无法被继承
{
void func2() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
八.继承和友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
//类的前置声明
class Student;
class Person
{
friend void Display(const Person& p, const Student& s);
protected:
string _name; //姓名
};
class Student : public Person
{
//友元关系不能被继承,子类需要自己写友元,Display也变成Student的友元即可
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; //学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
九.继承与静态成员
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
class Person
{
public:
string _name;
static int _count; //静态成员类内声明
};
int Person::_count = 0; //类外初始化
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
//非静态成员_name的地址是不一样的
//说明子类继承下来了,父子类对象各有一份
cout << &(p._name) << endl;
cout << &(s._name) << endl;
//静态成员_count的地址是一样的
//子类和父类共用同一份静态成员
cout << &(p._count) << endl;
cout << &(s._count) << endl;
//公有情况下,由于类域的限制,父子类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
//公有情况下,也可以对象.静态成员进行访问
cout << p._count << endl;
cout << s._count << endl;
return 0;
}
十.多继承及其菱形继承问题
1.继承模型
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的父类在前面,后面继承的父类在后面,子类成员在放到最后面。
菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余
和⼆义性
的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
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; //主修课程
};
int main()
{
Assistant a;
//a._name = "peter"; 编译报错,_name的访问不明确
//此时有两份_name,需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "张三";
a.Teacher::_name = "李四";
return 0;
}
2.菱形虚继承
class Person
{
public:
string _name; //姓名
};
//使用虚继承Person类
class Student : virtual public Person
{
protected:
int _num; //学号
};
//使用虚继承Person类
class Teacher : virtual public Person
{
protected:
int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
//使用虚继承,可以解决数据冗余和二义性,共用一份_name
Assistant a;
a._name = "peter";
a.Student::_name = "张三";
a.Teacher::_name = "李四";
return 0;
}
在菱形虚继承的基础上加上构造函数,就会变得非常的复杂,如下:
class Person
{
public:
Person(const char* name)
:_name(name)
{}
string _name;
};
class Student : virtual public Person
{
public:
Student(const char* name, int num = 0)
:Person(name)
,_num(num)
{}
protected:
int _num;
};
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id = 0)
:Person(name)
,_id(id)
{}
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1, const char* name2, const char* name3)
:Student(name1) //调用Student的构造函数,但是不会初始化_name
,Teacher(name2) //调用Teacher的构造函数,但是不会初始化_name
,Person(name3) //调用Person的构造函数,初始化_name
{}
protected:
string _majorCourse = "数学";
};
int main()
{
//以下有三个_name,真实的_name是王五,其余两个不起作用
Assistant a("张三", "李四", "王五");
return 0;
}
总结:可以使用多继承,但是不要弄出菱形继承。
3.面试题:多继承中指针偏移
4.IO库中的菱形虚继承
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
十二.继承和组合
-
public继承是一种is-a的关系。也就是说每个子类对象都是一个父类对象。
-
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
-
黑盒测试:不了解底层实现,从功能的角度测试;白盒测试:了解底层实现,从代码逻辑运行的角度测试。
-
继承允许你根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被称为白箱复用
。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。继承一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高。 -
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
-
优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。
-
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。
//Tire(轮胎)和Car(车)更符合has-a的关系
class Tire
{
protected:
string _brand = "Michelin"; //品牌
size_t _size = 17; //尺寸
};
class Car
{
protected:
string _colour = "黑色"; //颜色
string _num = "赣H12345"; //车牌号
Tire _t1; //轮胎1
Tire _t2; //轮胎2
Tire _t3; //轮胎3
Tire _t4; //轮胎4
};
//Car和BMW/Benz更符合is-a的关系
class BMW : public Car
{
public:
void Drive()
{
cout << "好开-操控" << endl;
}
};
class Benz : public Car
{
public:
void Drive()
{
cout << "好坐-舒适" << endl;
}
};
template<class T>
class vector
{};
//stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
vector<T> _v;
};