C++ - 继承
继承的基本概念
继承就是一种代码的复用.
子类通过继承父类, 就能使用父类的变量, 方法.
学生和老师这两种身份, 他们都有共同的属性: 他们都有名称, 年龄, 性别 ....
当然他们也有各种独有的属性, 学生有学号, 老师有工号 ....
对于这些共有的属性, 我们可以将它们提取出来:
class person
{public:string name;string sex;int age;
};
然后让学生类和老师类去继承这个 person 类.
class student : public person
{
public:string student_ID; // 学号
};
class teacher : public person
{
public:string job_ID;
};
class student : public person
student: 派生类 (又称为子类)
public: 继承方式
person: 基类 (又称为父类)
现在实例化出一个 student 对象之后, 这个对象也有 person 中的属性.
student st;
st.name = "张三";
st.age = 14;
st.student_ID = "123456";
继承关系和访问限定符
C++ 中存在三种访问限定符
- private: 只能在类内访问
- protected: 允许类内和派生类 (子类) 访问
- public: 无论类内类外都可以访问
那么当这三种访问限定符用在继承中, 有什么样的效果.
子类继承父类时, 会继承父类的成员属性和成员方法.
继承存在自己的继承关系 (private ...), 属性和方法也有自己的访问修饰限定符.
这两者之间的关系是如何的呢?
结论: 取 继承关系的限定符 和 访问修饰限定符中, 权限小的那一个
class person
{
public:int a;
protected:int b;
public:int c;
};// 1. public 继承
class student1 : public person
{//父类所有的属性都会被继承
public:int a;
protected: // protect 和 public 取权限小的那一个int b;
};// 2. private 继承
class student2 : private person
{
private: // private 和 public 取权限小的那一个int a;
private:int b;
};
我们可以看到, 继承得到的属性和方法的访问限定符, 是取二者之间较小的那一个.
但是在上面的两个继承的 student 类中, 我没有将变量 c 写出来, 这是为什么?
很简单, 变量 c 由 private 修饰, 所以只能在 person 中访问, 即便 student 继承了 person,
student 是独立的, 不是 person 的一部分, 也就无法访问 c.
一般而言, 都是使用 public 继承.
这里用一张图来表明继承后的方法和属性的访问权限
继承的作用域
从上面的成员的继承, 我们能察觉到, 派生类和基类是两个不同的空间.
那么既然是不同的空间, 在派生类中, 就可以定义和父类一样的成员属性和成员方法.
class person
{
public:int add(int a, int b){}string name;int age;
};class student : public person
{
public:int add(){return person::add(10, 20);}string name;
};student st;
st.name; // 就近原则, 默认访问子类的
st.person::name; // 带上基类的作用域, 才会访问到基类的属性
在同一个作用域中, 是不能存在两个同名变量的, 但是派生类和基类是不同的作用域. 他们的作用域都是独立的.
所以, 在派生类和基类中定义名称相同的变量也是可以的. (但是在实际使用中, 是不推荐这样写的)
那么这里就有一个很容易犯错的点. 上面代码中的 add 函数, 是不是构成了重载?
当然不是, 重载的条件之一就是: 同名函数处于同一作用域, 派生类和基类属于不同作用域.
当然也就不构成重载.
事实上, 这种情况称为隐藏 (也成为重定义). 在子类成员函数中,可以使用 基类::基类成员 访问.
父子类的对象赋值转换
- 子类对象赋值给基类的 对象/基类指针/基类引用
- 父类对象不能赋值给派生类对象
因为子类拥有父类的所有属性和方法, 父类赋值给子类, 就是把子类中, 父类的那部分给切出来.
这个过程称为"切片".
子类中的默认成员函数
- 子类的构造/拷贝构造/赋值重载函数, 都需要去调用父类的构造/拷贝构造/赋值重载.
- 子类的析构函数, 会自动的调用父类的析构函数, 所以不用显示的写出来
- 当实例化一个子类对象时, 会先初始化父类的成员变量, 然后再初始化子类的成员变量
- 当析构一个子类对象时, 会先调用子类的析构函数, 然后再调用父类的析构函数. (与构造函数调用顺序相反)
下面代码可以通过打印顺序观察各自的调用顺序.
class person
{
public:person(const char* name = "parent"):_name(name){cout << "person()" << endl;}person(const person& p){_name = p._name;cout << "person(const person&)" << endl;}person& operator= (const person& p){cout << "person operator=" << endl;if(this != &p){_name = p._name;}return *this;}~person(){cout << "~person()" << endl;}
private:string _name
};class student : public person
{
public:student(const char* name, int age):person(name),_age(age){cout << "student()" << endl;}student(const student& st):person(s),_age(st._age){}student& operator= (const student& st){cout << "student operator=" << endl;person::operator=(st);_age = st._age;return *this;}~student(){cout << "~student" << endl;}
private:int _age;
};int main()
{student s1("zhangsan", 10);student s2(s1);student s3("lisi", 20);s1 = s3;return 0;
}
菱形继承和虚继承
在C++中, 允许一个派生类继承多个基类, 这样会导致一个问题.
那么此时问题来了, D同时继承了 B和C, B和C 都继承了A.
那么D中就存在两份类A的成员属性, 一份来自B, 一份来自C.
此时D中就多余了一份数据, 占用了额外的空间.
更重要的是, 造成了二义性. 当我们访问继承至类A的属性时,
访问的是通过B继承得来的, 还是通过继承C得来的. 无法区分.
所以这里就引出了一个方法来解决这个菱形继承的问题.
虚继承: 在 B, C 继承前加上 virtual 关键字.
class A
{int a;
};class B : virtual public A
{int b;
};class C : virtual public A
{int c;
};class D : public B, A
{int d;
};
virtual 关键字要加在 B, C前, 而不是 D 前.
在实际写代码中, 是很少会使用多继承的, 这样带来的不可控影响太大了.
所以在实际写代码中, 能不用多继承就不用多继承. 这部分了解即可