首页 > 学院 > 开发设计 > 正文

C++--第五课-继承

2019-11-14 08:48:30
字体:
来源:转载
供稿:网友

继承中的几种关系

1. is a的关系

比如说基类狗,派生了肉狗和宠物狗,我们就可以说肉狗是狗,也可以说宠物狗是狗,这就是is a的关系。但是需要注意的是,并非所有的继承都是is a的关系。

2. has a的关系

继承中的权限继承

class Base{public: int public_i_; // 外部可访问PRotected: int protected_i_; // 内部可访问,外部无法访问, 可继承,子类可访问private: int private_i_; // 内部可访问,外部不可访问, 不可继承,子类不可访问};class A : public Base // public继承所有的权限都没有发生改变{ void SetI() { protected_i_ = 100; }};class B : protected Base // protected 继承会将基类中public的权限改为protected权限{ void SetI() { protected_i_ = 100; }};class C : private Base // 将基类中的protected和public权限都改为private权限{ void SetI() { protected_i_ = 100; }};

基类中有默认构造函数的继承中的构造和析构顺序

#include <iostream>class Base{public: Base() { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } int GetNum() { return num_; }private: int num_;};class A : public Base{public: A() { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; }};int main(){ A demo; demo.GetNum(); return 0;}

这里写图片描述

基类中没有默认构造函数的继承中的构造和析构顺序

#include <iostream>class Base{public: Base(int num) : num_(num) { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } int GetNum() { return num_; }private: int num_;};class A : public Base{public: A() : Base(10) // 此时会默认的调用基类的默认构造函数,如果基类中没有默认的构造函数时, // 那么必须在派生类中的初始化列表中显示的来构造 { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; }};int main(){ A demo; demo.GetNum(); return 0;}

继承中的函数,派生类中没有实现基类的函数方法

#include <iostream>class Base{public: Base(int num) : num_(num) { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } int GetNum() const { return num_; }private: int num_;};class A : public Base{public: A(int num) : Base(0), num_(num) // 此时会默认的调用基类的默认构造函数,如果基类中没有默认的构造函数时, // 那么必须在派生类中的初始化列表中显示的来构造 { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; } //int GetNum() const //{ // return num_; //}private: int num_;};int main(){ A demo(100); demo.GetNum(); std::cout << demo.GetNum() << std::endl; return 0;}

结果是 这里写图片描述

另一种情况是,派生类中实现了基类的函数方法

#include <iostream>class Base{public: Base(int num) : num_(num) { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } int GetNum() const { return num_; }private: int num_;};class A : public Base{public: A(int num) : Base(0), num_(num) // 此时会默认的调用基类的默认构造函数,如果基类中没有默认的构造函数时, // 那么必须在派生类中的初始化列表中显示的来构造 { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; } int GetNum() const { return num_; }private: int num_;};int main(){ A demo(100); demo.GetNum(); std::cout << demo.GetNum() << std::endl; return 0;}

输出结果是 这里写图片描述

基类指针指向派生类对象的函数调用情况

#include <iostream>class Base{public: Base(int num) : num_(num) { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } int GetNum() const { std::cout << "Base::GetNum()" << std::endl; return num_; }private: int num_;};class A : public Base{public: A(int num) : Base(0), num_(num) // 此时会默认的调用基类的默认构造函数,如果基类中没有默认的构造函数时, // 那么必须在派生类中的初始化列表中显示的来构造 { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; } int GetNum() const // 这个函数被重写了,也就是把基类中的方法给覆盖了,基类中的方法就不存在了 { std::cout << "A::GetNum()" << std::endl; return num_; }private: int num_;};int main(){ Base base(100); A a(200); Base *pBase = &a; // 因为 A is Base // 宠物狗 Is 狗 对 狗 泛指 宠物狗 // 狗 Is 宠物狗 错 std::cout << pBase->GetNum() << std::endl; // 调用的是 A 中的 Base中的GetNum方法,但是这样很显然不是我们想要的结果 // 这种用基类的指针指向派生类的对象的情况很多 return 0;}

输出结果是 这里写图片描述

使用虚函数在继承中函数调用的好处

#include <iostream>class Base{public: Base(int num) : num_(num) { std::cout << "Base::Base()" << std::endl; } ~Base() { std::cout << "Base::~Base()" << std::endl; } virtual int GetNum() const { std::cout << "Base::GetNum()" << std::endl; return num_; }private: int num_;};class A : public Base{public: A(int num) : Base(0), num_(num) // 此时会默认的调用基类的默认构造函数,如果基类中没有默认的构造函数时, // 那么必须在派生类中的初始化列表中显示的来构造 { std::cout << "A::A()" << std::endl; } ~A() { std::cout << "A::~A()" << std::endl; } int GetNum() const // 这个函数被重写了,也就是把基类中的方法给覆盖了,基类中的方法就不存在了 { std::cout << "A::GetNum()" << std::endl; return num_; }private: int num_;};int main(){ Base base(100); A a(200); Base *pBase = &a; // 因为 A is Base // 宠物狗 Is 狗 对 狗 泛指 宠物狗 // 狗 Is 宠物狗 错 std::cout << pBase->GetNum() << std::endl; // 调用的是 A 中的 Base中的GetNum方法,但是这样很显然不是我们想要的结果 // ,使用虚函数就可以满足我们的需求,我们在基类中的GetNum前面加上virtual关键字 // 这种用基类的指针指向派生类的对象的情况很多 return 0;}

结果是 这里写图片描述 为什么会这样呢?因为使用了虚函数,虚函数用来指明派生类中需要调用的函数,也就是说会在派生类中优先寻找要调用的函数,而不是在基类中寻找这个函数。

虚函数的其它例子

派生类继承并操作了基类中的成员变量

继承了基类中的str_变量,并对其进行了操作,导致内存泄漏,并且在析构的时候重复析构。

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class String{public: String(const char *str = "") { unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str); } ~String() { delete[] str_; } String &Operator+=(const String &other) { unsigned int len = strlen(str_) + strlen(other.str_); char *temp = new char[len + sizeof(char)]; strcpy(temp, str_); strcat(temp, other.str_); delete[] str_; str_ = temp; return *this; } String operator+(const String &other) { String demo; demo += other; return demo; }protected: char *str_;};class MyString : public String{public: MyString(const char *str = "PoEdu") { unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str); } MyString operator+(const MyString &other) { } ~MyString() { delete[] str_; str_ = nullptr; }};int main(){ MyString str; //MyString other("Hello"); return 0;}
调用基类的构造函数来操作基类的成员变量,而不是在派生类中直接操作基类只能怪的成员变量
#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class String{public: String(const char *str = "") { unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str); } ~String() { delete[] str_; } String &operator+=(const String &other) { unsigned int len = strlen(str_) + strlen(other.str_); char *temp = new char[len + sizeof(char)]; strcpy(temp, str_); strcat(temp, other.str_); delete[] str_; str_ = temp; return *this; } String operator+(const String &other) { String demo; demo += other; return demo; }protected: char *str_;};class MyString : public String{public: MyString(const char *str = "PoEdu") : String(str) { /*unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str);*/ } MyString operator+(const MyString &other) { } ~MyString() { delete[] str_; str_ = nullptr; }};int main(){ MyString str; //MyString other("Hello"); return 0;}

这样虽然保证了只对基类中的成员变量操作一次,但是在析构的时候还是会重复析构。

虚析构函数的使用情形

没有虚析构函数的析构情形

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class A{public: ~A() { std::cout << "~A()" << std::endl; }};class B : public A{public: ~B() { std::cout << "~B(()" << std::endl; }};int main(){ A *pA = new B(); delete pA; return 0;}

运行结果如下图所示 这里写图片描述 这样只是调用了基类中的析构函数,并没有调用派生类中的析构函数

有虚析构函数的析构情形

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class A{public: virtual ~A() { std::cout << "~A()" << std::endl; }};class B : public A{public: ~B() { std::cout << "~B(()" << std::endl; }};int main(){ A *pA = new B(); delete pA; return 0;}

运行结果如下: 这里写图片描述 这样就会把派生类中的析构函数也调用了。

正确的做法是只需要管理好本类中的成员变量即可,无需管理继承下来的成员变量(其实我觉得在实际的应用中,成员变量应该都是private的,是不需要被子类继承的,只有函数方法才需要被继承下来)
#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class String{public: String(const char *str = "") { unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str); } ~String() { delete[] str_; } String &operator+=(const String &other) { unsigned int len = strlen(str_) + strlen(other.str_); char *temp = new char[len + sizeof(char)]; strcpy(temp, str_); strcat(temp, other.str_); delete[] str_; str_ = temp; return *this; } String operator+(const String &other) { String demo; demo += other; return demo; }protected: char *str_;};class MyString : public String{public: MyString(const char *str = "PoEdu") : String(str) { length_ = new int(0); /*unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str);*/ } MyString operator+(const MyString &other) { } ~MyString() { //delete[] str_; //str_ = nullptr; // 正确的方法是不要去管理基类中的成员变量,只需要管理本类中的成员变量即可 delete length_; // 只需要管理本类中的length变量就可以了 }private: int *length_;};int main(){ //MyString str; //MyString other("Hello"); return 0;}

这样就保证了程序的正确运行。

虚函数的调用

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class String{public: String(const char *str = "") { unsigned int len = strlen(str); str_ = new char[len + sizeof(char)]; strcpy(str_, str); } String(const String &other) { unsigned int len = strlen(other.str_); str_ = new char[len + sizeof(char)]; strcpy(str_, other.str_); } ~String() { delete[] str_; } String &operator+=(const String &other) { unsigned int len = strlen(str_) + strlen(other.str_); char *temp = new char[len + sizeof(char)]; strcpy(temp, str_); strcat(temp, other.str_); delete[] str_; str_ = temp; return *this; } String operator+(const String &other) { String demo(*this); demo += other; return demo; } friend std::ostream &operator<<(std::ostream &os, const String &other) { os << other.str_; return os; }protected: char *str_;};class MyString : public String{public: MyString(const char *str = "PoEdu") : String(str) { } MyString operator+(const MyString &other) { MyString temp(*this); temp += other; // 因为从基类中继承了一个 += 操作符 temp += "---------PoEdu"; return temp; // 此时在返回的时候会调用拷贝构造函数,如果没有实现拷贝构造函数, // 那么就会出现浅拷贝的情况,导致程序运行错误,所以我们必须手动实现拷贝构造函数 }};int main(){ MyString str("I Love "); String *pString = &str; std::cout << str + ("Mark") << std::endl; std::cout << *pString + ("Mark") << std::endl; return 0;}

运行结果 这里写图片描述

继承中的虚析构函数

基类中的析构函数不是虚析构函数,而是普通函数的情况:

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class A{public: ~A() { std::cout << "~A()" << std::endl; }};class B : public A{public: B() : A() { demo_ = new int(0); } ~B() { std::cout << "~B()" << std::endl; delete demo_; }private: int *demo_;};int main(){ A * pA = new B(); delete pA; return 0;}

运行结果是: 这里写图片描述 从结果上可以看出,在delete的时候,只是调用了基类中的析构函数,并没有调用派生类中的析构函数,那么,如何能让派生类中的析构函数也被调用呢?请看下面的情况!!! 基类中的析构函数是虚析构函数的情况:

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class A{public: virtual ~A() { std::cout << "~A()" << std::endl; }};class B : public A{public: B() : A() { demo_ = new int(0); } ~B() { std::cout << "~B()" << std::endl; delete demo_; }private: int *demo_;};int main(){ A * pA = new B(); delete pA; return 0;}

运行结果: 这里写图片描述 关于继承的构造和析构顺序的总结: 一般的继承体系的类 基类构造 基类成员构造 子类构造 子类析构 基类成员析构 基类析构

继承中的虚继承

著名的菱形继承

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>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{ // a_ b_ c_,并且a_是两个 // 我们称这种继承方式为菱形继承};int main(){ D d; d.a_; return 0;}

此时在编译的时候就会出错,如下所示: 这里写图片描述 但是,我们也是可以访问的

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>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{ // a_ b_ c_,并且a_是两个 // 我们称这种继承方式为菱形继承};int main(){ D d; d.B::a_; d.C::a_; return 0;}

指明要访问哪个基类中的变量,就不会出现编译错误了。 这样虽然解决了编译错误的问题,但是我们不需要这样的情况,我们只需要一份变量就可以了,那么这样该怎么解决呢?我们可以通过虚继承来解决这个问题!!!

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class A{public: int a_;};class B : virtual public A{public: int b_;};class C : virtual public A{public: int c_;};class D : virtual public B, virtual public C{ // a_ b_ c_,并且a_是两个 // 我们称这种继承方式为菱形继承};int main(){ D d; d.a_; return 0;}

此时编译也就可以通过了。

继承中的纯虚函数

#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class Animal{public: virtual void Cry() = 0; // 纯虚函数,无需实现,需要子类来实现};class Dog : public Animal{public:};int main(){ Dog dog; // 此时编译不能通过,因为没有实现基类中的纯虚函数 return 0;}#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>class Animal{public: virtual void Cry() = 0; // 纯虚函数,无需实现,需要子类来实现};class Dog : public Animal{public: void Cry() { }};int main(){ Dog dog; // 当实现了基类中的纯虚函数后,就能实例化对象了 return 0;}#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>// 纯虚函数 不能实例化class Animal{public: virtual void Cry() = 0; // 纯虚函数,无需实现,需要子类来实现(强制性的进行实现)};class Dog : public Animal{public: void Cry() { }};class A : public Dog{};int main(){ Dog dog; // 当实现了基类中的纯虚函数后,就能实例化对象了 // 只要有纯虚函数的类,我们称之为抽象类,抽象类是无法实例化的 A a; // 此时A类就可以实例化,因为Dog类中已经实现了纯虚函数 return 0;}#define _CRT_SECURE_NO_WARNINGS#include <iostream>#include <cstring>// 纯虚函数 不能实例化 是其它类的基类 经常用来作为接口使用 接口是用来进行解耦的// 接口// 没有任何的数据成员,只是把函数名列出来class Animal{public: virtual void Cry() = 0; // 纯虚函数,无需实现,需要子类来实现(强制性的进行实现) virtual void Jump() = 0; // 跳的接口};class Dog : public Animal{public: void Cry() { }};class A : public Dog{};int main(){ Dog *dog = new A(); // 用来指向子类 dog->Cry(); return 0;}
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表