C++继承
继承的概念及定义
继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称子类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类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 = 18; // 年龄 string _address; // 地址 string _tel; // 电话 string _title; // 职称
};
int main()
{return 0;
}
下面我们公共的成员都放到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();t.identity();return 0;
}
继承定义
继承格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。(因为翻译的原因,所以既叫⽗类/⼦类,也叫基类/派生类)
继承父类成员访问方式的变化
1. 父类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类里面还是类外面都不能去访问它。
2. 父类private成员在子类中是不能被访问,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,父类的私有成员在子类都是不可见。父类的其他成员在子类的访问方式==Min(成员在父类的访问限定符,继承方式),public >protected>private。
4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运⽤中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在子类的类里面使用,实际 中扩展维护性不强。
// 实例演⽰三种继承关系下⽗类成员的各类型成员访问关系的变化
class Person
{
public :void Print (){cout<<_name <<endl;}
protected :string _name ; // 姓名
private :int _age ; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :int _stunum ; // 学号
};
继承类模板
namespace bit
{//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-a template<class T>class stack : public std::vector<T>{public:void push(const T& x){// ⽗类是类模板时,需要指定⼀下类域, // 否则编译报错:error C3861: “push_back”: 找不到标识符 // 因为stack<int>实例化时,也实例化vector<int>了 // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}
int main()
{bit::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
类模板继承的时候不会将全部模板实例化,编译器遵循的是按需实例化,
在模板类中,直到模板参数被具体化之前,编译器不知道如何为这个类生成具体的代码。
模板本身并不会在定义时生成代码,只有在具体使用某种类型来实例化模板时,编译器才会生成相应的代码。
父类和子类对象赋值兼容转换
public继承的子类对象可以赋值给 父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切来赋值过去。在赋值的时候并没有产生临时对象,临时对象具有常性,引用临时对象的时候需要加上const。
父类对象不能赋值给子类对象。
父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用(父类的指针或引用有可能指向子类)。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast来进行识别后进行安全转换。
继承中的作用域
隐藏规则:
1. 在继承体系中父类和子类都有独立的作用。
2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用 父类::父类成员 显示访问)子类和父类只要具有同名函数就构成隐藏
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
继承作用域相关选择题
A和B类中的两个func构成什么关系()
A.重载 B.隐藏 C.没关系
下面程序的编译运行结果是什么()
A.编译报错 B.运⾏报错 C.正常运行
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" <<i<<endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
函数重载要求作用在同一作用域,A和B两个同名函数构成同名函数,为隐藏函数。B
构成隐藏函数的时候,调用父类的隐藏函数只能指定作用域才能生效,直接fun(10)会造成编译错误,第二题选A
子类的默认成员函数
默认生成构造函数的行为
内置类型->不确定
自定义类型->调用默认构造
继承父类成员看作一个整体对象,要求调用父类的默认构造
子类成员在设计开空间的时候,由于是深拷贝,需要自己写构造函数
4个常见默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会为我们自动生成一个,那么在子类中,这几个成员函数是如何生成的呢?
1. 子类的构造函数必须调用父类的构造函数初始化父类的那⼀部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
2. 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。
3. 子类的operator=必须要调用父类的operator=完成父类的复制。需要注意的是子类的operator=隐 藏了父类的operator=,所以显示调用父类的operator=,需要指定父类作用域
4. 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。
5. 子类对象初始化先调用父类构造再调子类构造。
6. 子类对象析构清理先调用子类析构再调父类的析构。
实现⼀个不能被继承的类
方法1:父类的构造函数私有,子类的构成必须调用父类的构造函数,但是父类的构成函数私有化以后,子类看不见就不能调用了,那么子类就无法实例化出对象。
方法2:C++11新增了⼀个final关键字,final修改父类,子类就不能继承了。
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的⽅法 /*Base(){}*/
};
继承与友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员,解决这个问题的话只需要在子类中插入友元函数即可
继承与静态成员
父类定义了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;return 0;
}
同名的静态成员地址都是同一份,说明子类和父类共用同一份静态成员,在公有的情况下,父子类指定类域都可以访问静态成员
多继承及其菱形继承问题
继承模型
单继承:⼀个子类只有⼀个直接父类时称这个继承关系为单继承
多继承:⼀个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的父类在前面,后面继承的父类在后面,子类成员在放到最后面。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,支持多继承就⼀定会有菱形继承,像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";a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
在访问Assistant对象的name时编译器会报错,因为Assistant存在两个父类,编译器不知道使用哪一个的_name,这时需要指定哪个父类的成员便可以解决,但是数据冗余无法解决,菱形继承太过浪费空间
虚继承
使用虚继承需使用关键字virtual,虚继承能够解决不同父类中变量共用的问题。
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()
{// 使⽤虚继承,可以解决数据冗余和⼆义性 Assistant a;a._name = "peter";return 0;
}
在菱形继承中并不是每个类都需要进行虚继承,谁会产生数据冗余,谁就加虚继承。
在菱形继承中只添加一个虚继承会产生报错。
io流就是一种菱形继承
继承和组合
public继承是⼀种is-a的关系。也就是说每个子类对象都是⼀个父类对象。
组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
继承允许你根据父类的实现来定义子类的实现。这种通过生成子类的复用通常被称为白箱复用 (white-boxreuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见 。继承⼀定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度⾼。
对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接⼝。这种复用风格被称为黑箱复用(black-boxreuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。