一、继承的概念
继承是指,在原有类的基础上扩展出新的功能,产生新的类,叫做派生类
二、继承的格式
继承方式和访问限定符不同时,继承基类成员的变化
总结:
1、当以 public 方式继承时,基类中的 public 成员还是派生类的 public 成员,protected 成员还是 protected 成员,private 在派生类中不可见。(以 public 方式继承时,与其在基类中限定符一样,除 private 是不可见)
2、当以 protected 方式继承时,基类的 public 和 protected 成员在派生类中都是 protected 成员,基类中的 private 成员依旧是不可见。(基类中比 protected 等级低的在派生类中都是 protected成员,除 private 不可见)
3、当以 private方式继承时,基类的 public 和 protected 成员在派生类中都是 private成员,基类中的 private 成员依旧是不可见。(基类中比 private等级低的在派生类中都是 private成员,除 private 不可见)
三、基类和派生类的对象赋值兼容转换
派生类可以赋值给基类的 对象/指针/引用。这种方式也叫做切割或切片。
class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
private:string _id;// 学号
};
在进行赋值时,父类只拿到子类中属于父类的那部分,而丢弃了只属于子类中的部分,就像切割了一样,因此这种赋值也叫做切割或切片。(这个赋值是天然的,并不会产生临时变量)
Student s("la", "女", 17, "10201");
Person& rp = s;
在赋值给父类的引用时,rp 改变了成员变量,s 中的成员变量也会跟着改变(因为赋值只是把子类 s 中父类的地址给 rp 即可)
四、隐藏
如果基类和派生类中有同名的成员,就构成隐藏,子类将屏蔽父类的同名成员。
class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){}string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string name, string sex, int age, string id):Person(name, sex, age),_id(id),_age(age){}string _id;// 学号int _age;
};int main()
{Student s("la", "女", 17, "10201");s._age = 100;return 0;
}
上述代码有同名成员变量,将 _age 改为 100,默认改的是子类中的 _age 。
可以指定使用 父类 的作用域,来访问 父类中的 _age。
Student s("la", "女", 17, "10201");
s.Person::_age = 100;
同样,成员函数也可以构成隐藏。
class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){}void func(){cout << "Person::func()" << endl;}
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string name, string sex, int age, string id):Person(name, sex, age),_id(id),_age(age){}void func(int x){cout << "Student::func(int x)" << endl;}string _id;// 学号int _age;
};
上述代码中的 func 函数就是构成隐藏,只要是同名成员函数就构成隐藏。与返回值和参数无关
五、派生类的默认成员函数
1、构造函数
派生类的成员分为:
①、派生类的成员:自定义类型和内置类型分别处理
②、父类的成员:调用父类的构造函数
class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){std::cout << "Person::Person()" << endl;}string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string id = "2024593"):_id(id){std::cout << "Student::Student()" << endl;}string _id;// 学号
};int main()
{Student s;return 0;
}
运行结果:
从上述代码可以看出,子类会先调用父类的构造函数,再调用自己的构造函数。
如果父类没有默认构造函数可以使用 Person(...) 在初始化列表显示调用。(如 四 隐藏中的代码)
2、拷贝构造:
class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){cout << "Person::Person()" << endl;}Person(const Person& p):_name(p._name),_sex(p._sex),_age(p._age){cout << "Person::Person(const Person& p)" << endl;}string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string id = "2024593"):_id(id){cout << "Student::Student()" << endl;}Student(const Student& s):Person(s), // 父类调用父类的拷贝构造_id(s._id){cout << "Student::Student(const Student& s)" << endl;}string _id;// 学号
};int main()
{Student s;Student s_copy = s;return 0;
}
3、赋值重载
#include <iostream>using namespace std;class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){cout << "Person::Person()" << endl;}Person(const Person& p):_name(p._name),_sex(p._sex),_age(p._age){cout << "Person::Person(const Person& p)" << endl;}Person& operator=(const Person& p){if(&p != this) // 如果不是自己给自己赋值{_name = p._name;_sex = p._sex;_age = p._age;cout << "operator=(const Person& p)" << endl;}return *this;}string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string id = "2024593"):_id(id){cout << "Student::Student()" << endl;}Student(const Student& s):Person(s), // 父类调用父类的拷贝构造_id(s._id){cout << "Student::Student(const Student& s)" << endl;}Student& operator=(const Student& s){if(&s != this) // 如果不是自己给自己赋值{Person::operator=(s); // 显示调用父类的赋值重载_id = s._id;cout << "operator=(const Student& s)" << endl;}return *this;}string _id;// 学号
};int main()
{Student s;s._age = 100;Student s_copy;s_copy = s;return 0;
}
4、析构函数
子类析构时会自动调用父类的析构函数,不需要显示自己写。
原因是为了保证析构顺序:先子后父。
构造:先父后子;子类构造可能会使用父类的成员
析构:先子后父;子类析构可能需要访问父类的成员
class Person
{
public:Person(string name = "lw", string sex = "男", int age = 18):_name(name),_sex(sex),_age(age){cout << "Person::Person()" << endl;}Person(const Person& p):_name(p._name),_sex(p._sex),_age(p._age){cout << "Person::Person(const Person& p)" << endl;}Person& operator=(const Person& p){if(&p != this) // 如果不是自己给自己赋值{_name = p._name;_sex = p._sex;_age = p._age;cout << "operator=(const Person& p)" << endl;}return *this;}~Person(){cout << "~Person()" << endl;}string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person
{
public:Student(string id = "2024593"):_id(id){cout << "Student::Student()" << endl;}Student(const Student& s):Person(s), // 父类调用父类的拷贝构造_id(s._id){cout << "Student::Student(const Student& s)" << endl;}Student& operator=(const Student& s){if(&s != this) // 如果不是自己给自己赋值{Person::operator=(s);_id = s._id;cout << "operator=(const Student& s)" << endl;}return *this;}~Student(){cout << "~Student()" << endl;}string _id;// 学号
};int main()
{Student s;return 0;
}
子类的析构函数和父类的析构函数构成重载,因为由于多态原因,析构函数被特殊处理,函数名都会被处理成 destruction()。
总结:派生类这些默认成员函数的规则与以前类似,不同的是分为了子类部分和父类部分,父类部分就调用父类的默认成员函数。
六、友元与静态成员函数
1、父类的友元无法继承
2、父类的静态成员在整个继承体系中只有一个
(例:static int count; // 则父类和子类都有count,它们是同一个)
class Person
{
public:string _name; // 姓名string _sex; // 性别int _age; // 年龄static int count;
};int Person::count = 1;class Student : public Person
{
public:string _id;// 学号
};int main()
{Person p;Student s;cout << &p.count << endl;cout << &s.count << endl;return 0;
}
同一个地址,说明是同一个 count。
六、菱形继承
单继承:一个子类只有一个直接父类
多继承:一个子类有多个直接父类
菱形继承:多继承的两个父类继承了同一个类
菱形继承出现的问题:在 Assistant 类中,Person 类会有两份。
会出现数据冗余与二义性的问题。
class Person
{
public:string _name; // 姓名
};class Student : public Person
{
public:string _sid;// 学号
};class Teacher : public Person
{
public:string _tid;// 教工号
};class Assistant : public Teacher, public Student
{
public:string _major;// 专业
};int main()
{Assistant a;a._name = "la";return 0;
}
虽然我们可以用指定类域的方式解决二义性问题(如:Student::_name),但有很多情况我们不需要两份 Person,会造成空间的浪费。
我们可以增加虚继承来解决数据冗余和二义性的问题。
虚继承的方式:就是在有二义性的地方( Student 和 Teacher )继承方式前加 virtual
class Person
{
public:string _name; // 姓名
};class Student : virtual public Person
{
public:string _sid;// 学号
};class Teacher : virtual public Person
{
public:string _tid;// 教工号
};class Assistant : public Teacher, public Student
{
public:string _major;// 专业
};int main()
{Assistant a;a._name = "la";return 0;
}
监视窗口:
虚拟继承原理:原本 Student 中和 Teacher 中各有一个类 Person,但使用虚拟继承后,Student 和 Teacher 有一份公共的类 Person。Student 中和 Teacher 中不保存类 Person ,而是保存 Person 类的偏移量的地址。
公共的类 Person 一般是在最后面(但没规定,也可以不在)
菱形继承的对象模型
在VS环境下
class A
{
public:int _a;
};class B :public A
{
public:int _b;
};class C :public A
{
public:int _c;
};class D :public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
没有使用虚拟继承时的 监视内存窗口:
虚拟继承后 (环境为VS,X86):
class A
{
public:int _a;
};class B : virtual public A
{
public:int _b;
};class C : virtual public A
{
public:int _c;
};class D :public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
内存窗口:
等于在虚拟继承中, D中的 B 和 C 都存的是 A 偏移量的地址,形成了偏移量表,所有的 D 对象都有一份偏移量表,用来找公共的 A。
再定义一个 D dd;
它们有相同的偏移量表。
同理,在 B 和C 对象中也是存了偏移量表和公共的 A。
它们的模型是一样的。