继承中的几种关系
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;
}