C++类开发第五篇(继承和派生的初体验)

inheritance

在 C++ 中,继承是一种面向对象编程的特性,允许一个类(称为子类或派生类)从另一个类(称为基类或父类)那里继承属性和行为。通过继承,子类可以获得父类的数据成员和成员函数,从而可以重用父类的代码并扩展其功能。这样可以提高代码的复用性和可维护性,同时也符合面向对象编程的封装和抽象特性。

一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。

派生类中的成员,包含两大部分:

  • 类是从基类继承过来的,一类是自己增加的成员。

  • 从基类继承过过来的表现其共性,而新增的成员体现了其个性。

简单的类派生

#CLUB.h
#ifndef CLUB_H
#define CLUB_H

using namespace std;
#include<iostream>
#include<string>

class ProgramCLub {
private:
	string firstname;
	string lastname;
	bool haveLaptop;
public:
	ProgramCLub(const string &fname = "None", const string &lname = "None", bool haveLap = false);
	/*const string &fname 和 const string &lname 是引用参数,
	它们允许函数通过引用来操作传入的实参,而不是进行值的拷贝。
	使用引用参数可以减少内存消耗和提高性能,特别是在传递大型对象时。*/
	void name() const;
	bool HaveLaptop() const { return haveLaptop; };
	void Reset(const bool v) { haveLaptop = v; };
};

#endif // !CLUB_H

# program.cpp
#include<iostream>
#include "club.h"
using namespace std;

ProgramCLub::ProgramCLub(const string& fname,
	const string& lname, bool hl) :
	firstname(fname), lastname(lname), haveLaptop(hl) {}

void ProgramCLub::name() const{
	cout << lastname << " " << firstname << endl;
}
				

这个基类本身没有太多的功能,就是记录人名和是否有电脑。为了之后做继承和派生丰富功能起个头。

# main.cpp
#include<iostream>
using namespace std;
#include "club.h"

int main() {
	ProgramCLub user1("zhang","regina",  false);
	ProgramCLub user2("lee","ivan",  true);
	user1.name();
	if (user1.HaveLaptop()) {
		cout << " has a laptop. \n" << endl;
	}
	else {
		cout << " has not a laptop. \n" << endl;
	}
	user2.name();
	if (user2.HaveLaptop()) {
		cout << " has a laptop. \n" << endl;
	}
	else {
		cout << " has not a laptop. \n" << endl;
	}
	return 0;
}

现在要派生一个VIP客户,这些客户不光要保存一些基础信息,还有他们打比赛的比分。所以与其从零开始写,不如继承上面的类的功能,再派生一个新的。

派生类定义格式:

Class 派生类名 : 继承方式 基类名{ //派生类新增的数据成员和成员函数 }

三种继承方式:

  • public : 公有继承,使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问

  • private : 私有继承

  • protected : 保护继承

从继承源上分:

  • 单继承:指每个派生类只直接继承了一个基类的特征

  • 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征

class VIPProgramClub : public ProgramCLub {};// ProgramCLub的所有函数都可以使用

派生类需要添加自己的构造函数以及额外的数据成员和成员函数,在这个例子里面派生类需要另一个数据成员记录必死啊的比分,还应该包括重置和搜索比分的成员方法。

class VIPProgramClub : public ProgramCLub {
private:
	unsigned int rating;
public:
	VIPProgramClub(unsigned int r = 0, 
					const string& fname = "None", 
					const string& lname = "None", 
					bool haveLap = false);
	VIPProgramClub(unsigned int r = 0, const ProgramCLub& pc);
	/*构造函数必须给新成员和继承的成员提供数据。在第一个VIPProgramClub
	里面每一个成员对应一个形参,第二个VIPProgramClub里面使用一个ProgramCLub
	参数*/
	unsigned int Rating() const { return rating; };
	void ResetRating(unsigned int r) { rating = r; };
};

访问权限

//基类
class A{
public:
	int mA;
protected:
	int mB;
private:
	int mC;
};

//1. 公有(public)继承
class B : public A{
public:
	void PrintB(){
		cout << mA << endl; //可访问基类public属性
		cout << mB << endl; //可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}  
};
class SubB : public B{
	void PrintSubB(){
		cout << mA << endl; //可访问基类public属性
		cout << mB << endl; //可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
void test01(){

	B b;
	cout << b.mA << endl; //可访问基类public属性
	//cout << b.mB << endl; //不可访问基类protected属性
	//cout << b.mC << endl; //不可访问基类private属性
}

//2. 私有(private)继承
class C : private A{
public:
	void PrintC(){
		cout << mA << endl; //可访问基类public属性
		cout << mB << endl; //可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
class SubC : public C{
	void PrintSubC(){
		//cout << mA << endl; //不可访问基类public属性
		//cout << mB << endl; //不可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
void test02(){
	C c;
	//cout << c.mA << endl; //不可访问基类public属性
	//cout << c.mB << endl; //不可访问基类protected属性
	//cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:
	void PrintD(){
		cout << mA << endl; //可访问基类public属性
		cout << mB << endl; //可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
class SubD : public D{
	void PrintD(){
		cout << mA << endl; //可访问基类public属性
		cout << mB << endl; //可访问基类protected属性
		//cout << mC << endl; //不可访问基类private属性
	}
};
void test03(){
	D d;
	//cout << d.mA << endl; //不可访问基类public属性
	//cout << d.mB << endl; //不可访问基类protected属性
	//cout << d.mC << endl; //不可访问基类private属性
}

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。比如说VIPProgramClub构造函数不能直接设置继承的成员(firstname,lastname和havelaptop),而必须使用基类的公有方法来访问私有的基类成员,具体说就是派生类函数必须使用基类构造函数。

创建派生类对象时,程序首先创建积累对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创造。c++使用成员初始化列表语法来完成这种工作。例如下面是一段VIPProgramClub构造函数的代码:

VIPProgramClub::VIPProgramClub(unsigned int r = 0,
	const string& fname,
	const string& lname, bool hl) : ProgramCLub(fname, lname, hl)
{
	this->rating = r;
}

其中的ProgramCLub(fname, lname, hl)是成员的初始化列表。它是可执行的代码,调用VIPProgramClub构造函数,假设有定义VIPProgramClub vip1(127,"zhang","regina",true),那么VIPProgramClub构造函数会首先将"zhang","regina",true赋值给形参fname, lname, hl,然后将这些参数作为实参传递给ProgramCLub构造函数。后者会创建一个嵌套的ProgramCLub对象,并将数据"zhang","regina",true存储在该对象中。然后程序才会进入VIPProgramClub构造函数体完成对vip1的创建,并将参数r的值赋给rating。

如果省去成员列表初始化,情况会怎样?

必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数。除非要使用默认构造函数,否则应显式调用正确的基类构造函数。

如果是这种类型的构造函数,也将ProgramCLub的信息传递给了它的构造函数。因为pc的类型是ProgramCLub&, 因此将调用基类的复制构造函数。基类没有定义复制构造函数,如果需要使用复制构造函数但又没有定义,编译器会自动生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配。

#include<iostream>
using namespace std;
#include "club.h"

int main() {
	ProgramCLub user1("zhang","regina",  false);
	VIPProgramClub user2(127,"lee","ivan",  true);
	user1.name();
	if (user1.HaveLaptop()) {
		cout << " has a laptop. \n" << endl;
	}
	else {
		cout << " has not a laptop. \n" << endl;
	}
	user2.name();
	if (user2.HaveLaptop()) {
		cout << " has a laptop." ;
	}
	else {
		cout << " has not a laptop." ;
	}
	cout << " rating: " << user2.Rating() << endl;
	return 0;
}

继承中的构造和析构

在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成:

class Aclass {
public:
	int mA;
	int mB;
};
class Bclass : public Aclass {
public:
	int mC;
};
class Cclass : public Bclass {
public:
	int mD;
};
void test() {
	cout << "A size:" << sizeof(Aclass) << endl;
	cout << "B size:" << sizeof(Bclass) << endl;
	cout << "C size:" << sizeof(Cclass) << endl;
}

int main() {
	test();
	return 0;
}

在面向对象编程中,子类继承父类的成员是一种常见的机制。当子类继承一个父类时,子类会包含父类的所有成员变量和成员函数。这种继承关系通常被称为is-a关系,即子类是父类的一种特殊类型。

当子类继承父类时,子类对象中会包含父类对象的内存布局。这意味着子类对象中会包含父类的成员变量,并且在内存中排列顺序是先父类的成员变量,然后是子类的新成员变量。这样就实现了父类成员叠加子类新成员的效果。

这种设计有助于代码的复用和扩展。子类可以重用父类的成员变量和成员函数,同时可以添加新的成员变量和成员函数来扩展父类的功能。通过这种方式,我们可以构建出更加灵活和复杂的对象体系。

class A {
public:
	A() {
		cout << "A类构造函数!" << endl;
	}
	~A() {
		cout << "A类析构函数!" << endl;
	}
};

class B : public A {
	public:
		B() {
			cout << "B类构造函数!" << endl;
		}
		~B() {
			cout << "B类析构函数!" << endl;
		}
};

class C : public B {
public:
	C() {
		cout << "C类构造函数!" << endl;
	}
	~C() {
		cout << "C类析构函数!" << endl;
	}
};

void test() {
	C c;
}



int main() {
	test();
	return 0;
}

继承中同名成员的处理方法

  • 当子类成员和父类成员同名时,子类依然从父类继承同名成员

  • 如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)

  • 在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)

class Base {
public:
	string param;
	Base() : param("Base") {}
	void print() { cout << "Base class : " << param << endl; }
};

class Derived : public Base {
public:
	string param;
	Derived() : param("Derived") {}
	void print() { 
		cout << "Derived class : " << param 
			<< ", Base class : " << Base::param << endl; }
	string getBaseParam() {
		return Base::param;
	}
	void setBaseParam(string m) {
		 Base::param = m;
	}
};
void test() {
	Derived d;
	//派生类和基类成员属性重名,子类访问成员默认是子类成员
	cout << d.param << endl;
	d.print();
	d.getBaseParam() = "d.getBaseParam()";
	cout << "Base:mParam:" << d.getBaseParam() << endl;
	d.setBaseParam("d.setBaseParam()");
	cout << "Base:mParam:" << d.getBaseParam() << endl;

}

如果重新定义了基类中的重载函数,将会发生什么?

class Base {
public:
	void func1() {
		cout << "Base::void func1()" << endl;
	};
	void func1(int param) {
		cout << "Base::void func1(int param)" << endl;
	}
	void myfunc() {
		cout << "Base::void myfunc()" << endl;
	}

};

class Derived1 : public Base {
public:
	void myfunc() {
		cout << "Derived1::void myfunc()" << endl;
	}
};
class Derived2 : public Base {
public:
	//改变成员函数的参数列表
	void func1(int param1, int param2) {
		cout << "Derived2::void func1(int param1,int param2)" << endl;
	};
};

class Derived3 : public Base {
public:
	//改变成员函数的返回值
	int func1(int param) {
		cout << "Derived3::int func1(int param)" << endl;
		return 0;
	}
};

如果在派生类中重新定义了基类中的重载函数,会导致基类中对应的所有重载版本都被隐藏,而只有派生类中重新定义的版本可见。这样做会覆盖基类中的同名函数,使得派生类中的函数成为唯一可访问的版本。

具体来说,如果派生类中重新定义了基类中的重载函数,无论是改变参数列表、返回值类型或者函数实现,都会导致基类中的所有相关版本被隐藏。此时,通过派生类对象只能访问到派生类中重新定义的版本,而无法再访问基类中被隐藏的同名函数。

Derived1 derived1;
derived1.func1();
derived1.func1(20);
derived1.myfunc();
cout << "-------------" << endl;
Derived2 derived2;
//derived2.func1();  //func1被隐藏
//derived2.func1(20); //func2被隐藏
derived2.func1(10, 20); //重载func1之后,基类的函数被隐藏
derived2.myfunc();
cout << "-------------" << endl;
Derived3 derived3;
//derived3.func1();  //没有重新定义的重载版本被隐藏
derived3.func1(20);
derived3.myfunc();

Derive1 重定义了Base类的myfunc函数,derive1可访问func1及其重载版本的函数。

Derive2通过改变函数参数列表的方式重新定义了基类的func1函数,则从基类中继承来的其他重载版本被隐藏,不可访问

Derive3通过改变函数返回类型的方式重新定义了基类的func1函数,则从基类继承来的没有重新定义的重载版本的函数将被隐藏。

非自动继承的函数

不是所有的函数都能自动从基类继承到派生类中。构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。

另外operator=也不能被继承,因为它完成类似构造函数的行为。也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。

在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。

继承中的静态成员特性

静态成员函数和非静态成员函数的共同点:

  1. 他们都可以被继承到派生类中。

  2. 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。

  3. 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。

class Base{
public:
	static int getNum(){ return sNum; }
	static int getNum(int param){
		return sNum + param;
	}
public:
	static int sNum;
};
int Base::sNum = 10;

class Derived : public Base{
public:
	static int sNum; //基类静态成员属性将被隐藏
#if 0
	//重定义一个函数,基类中重载的函数被隐藏
	static int getNum(int param1, int param2){
		return sNum + param1 + param2;
	}
#else
	//改变基类函数的某个特征,返回值或者参数个数,将会隐藏基类重载的函数
	static void getNum(int param1, int param2){
		cout <<  sNum + param1 + param2 << endl;
	}
#endif
};
int Derived::sNum = 20;

多继承

简单接触多态公有继承

如果希望某一个方法在派生类和基类中的作用是不同的,方法的行为取决于调用该方法的对象。这种机制称为多态性。有两种重要的机制可用于实现多态公有继承:

  • 在派生类重新定义基类的方法
  • 使用虚方法

多态公有继承的特点包括:

  1. 基类和派生类之间使用公有继承关系。
  2. 基类中的成员函数被声明为虚函数(virtual function),以便在派生类中进行重写。
  3. 通过基类的指针或引用来操作派生类对象,可以根据实际对象类型动态调用对应的成员函数。
  4. 多态性可以提高代码的灵活性和可扩展性,使得程序更容易适应变化和扩展。

虚函数是 C++ 中用于实现多态性的重要概念。当基类中的成员函数被声明为虚函数时,它可以在派生类中被重写,并且在运行时根据对象的实际类型来调用相应的函数。

class Regina {
public:
	virtual void func() const {
		cout << "this is Teacher Regina Zhang." << endl;
	}
};
class Regina_after_class : public Regina{
public:
	void func() const override {
		cout << "this is ivanlee's wife" << endl;
	}
	void goHome() const {
		cout << "Regina_after_class:goHome()" << endl;
	}
};
class Regina_at_home : public Regina {
public:
	void func() const override {
		cout << "this is ivanlee's M baby" << endl;
	}
	void play() const {
		cout << "Regina_at_home:play()" << endl;
	}
};

"override" 关键字并不是必须的,但它在 C++ 中是一个很有用的辅助工具。使用 "override" 关键字可以帮助程序员避免一些常见的错误,例如意外创建了一个新函数而不是重写基类中的虚函数,或者函数签名不匹配导致无法正确覆盖基类函数。

如果没有使用 "override" 关键字,编译器仍然会尝试将函数与基类中的虚函数进行匹配,并且在大多数情况下会正常工作。但是,如果存在某些潜在的问题(比如函数签名不匹配),这些问题可能不会被及时发现,从而导致程序出现意料之外的行为或错误。

void test() {
	vector<Regina*> r;//通过基类指针或引用来调用派生类对象的函数,所以类型为Regina。
	r.push_back(new Regina_after_class());
	r.push_back(new Regina_at_home());
	for (const auto i : r) {
		i->func();
		// 尝试调用特定于派生类的函数
		Regina_after_class* rac = dynamic_cast<Regina_after_class*>(i);
		/*使用 dynamic_cast 运算符尝试将 i 指针转换为 Regina_after_class* 指针*/
		if (rac) {
			rac->goHome();
		}
		Regina_at_home* rah = dynamic_cast<Regina_at_home*>(i);
		if (rah) {
			rah->play();
		}
		cout << "---------------------" << endl;
	}
	for (const auto i : r) {
		delete i;
	}
}

上述例子里使用的是基类指针调用派生类对象函数

  1. 首先,定义了一个存储 Regina指针的 vector r,并向其中添加了一个 Regina_after_class对象和一个 Regina_at_home对象。
  2. 接着,通过循环遍历 r 中的每个元素,每次迭代都会调用 r 指针指向对象的 func() 函数。
  3. 然后,使用 dynamic_cast 运算符尝试将 r指针转换为 Regina_after_class* 指针。如果转换成功(即该对象是 Regina_after_class类型),则执行 rac->goHome();同样的,也尝试将 i转换为 Regina_at_home* 指针,如果转换成功(即该对象是 Regina_at_home类型),则执行 rah->play()。

现在改为使用引用调用

	for (const auto& i : r) {
		i->func();
		if (const auto rac = dynamic_cast<Regina_after_class*>(i)) {
			rac->goHome();
		}
		if (const auto rah = dynamic_cast<Regina_at_home*>(i)) {
			rah->play();
		}

虚析构函数在 C++ 中的作用主要是为了正确释放多态对象的内存。当基类指针指向派生类对象时,如果基类的析构函数不是虚函数,那么在使用 delete 删除指向派生类对象的基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类的资源没有得到释放,造成内存泄漏。

通过将基类的析构函数声明为虚函数,可以实现多态对象的正确销毁。当使用 delete 删除指向派生类对象的基类指针时,会先调用派生类的析构函数,然后再调用基类的析构函数,确保所有相关资源都能被正确释放。

更多的细节知识点在下一篇

多继承

我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。但是由于多继承是非常受争议的,从多个类继承可能会导致函数、变量等同名导致较多的歧义。

class Base1 {
public:
	void func1() { cout << "Base1::func1" << endl; }
};
class Base2 {
public:
	void func1() { cout << "Base2::func1" << endl; }
	void func2() { cout << "Base2::func2" << endl; }
};

//派生类继承Base1、Base2
class Derived : public Base1, public Base2 {};
class Base1 {
public:
	void func1() { cout << "Base1::func1" << endl; }
};
class Base2 {
public:
	void func1() { cout << "Base2::func1" << endl; }
	void func2() { cout << "Base2::func2" << endl; }
};

//派生类继承Base1、Base2
class Derived : public Base1, public Base2 {};

此时一共有两个基类,并且同时又func1的成员函数。多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本?

void test() {
	Derived derived;
	derived.func2();

	//解决歧义:显示指定调用那个基类的func1
	derived.Base1::func1();
	derived.Base2::func1();
}

菱形继承和虚继承

两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。

这种继承所带来的问题:

  1. 羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。

  2. 草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

class BigBase{
public:
	BigBase(){ mParam = 0; }
	void func(){ cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};

int main(){

	Derived derived;
	//1. 对“func”的访问不明确
	//derived.func();
	//cout << derived.mParam << endl;
	cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
	cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

	//2. 重复继承
	cout << "Derived size:" << sizeof(Derived) << endl; 

>

解决办法:虚基类

当一个类被声明为虚基类时,它的派生类中的虚基类子对象只会被构造一次,从而避免出现菱形继承中出现的二义性和资源浪费问题。通过将共同的基类声明为虚基类,可以确保在最终的派生类中只有一个基类子对象,从而保证程序的正确性和高效性。

在菱形继承中,只需要将最顶层的共同基类声明为虚基类即可,中间的两个类不需要变成虚基类。

假设有如下的菱形继承关系:

	A
/ \
B   C
\ /
 D

在这种情况下,如果 B、C、D 都直接或间接地继承自 A,并且 D 继承自 B 和 C,那么只需要将 A 声明为虚基类,避免了 D 中包含两份 A 的实例。至于 B 和 C,它们并不需要被声明为虚基类。

class BigBase {
public:
	BigBase() { mParam = 0; }
	void func() { cout << "BigBase::func" << endl; }
public:
	int mParam;
};

class Base1 : virtual public BigBase {};
class Base2 : virtual public BigBase {};
class Derived : public Base1, public Base2 {};
void test() {

	Derived derived;

	derived.func();
	cout << derived.mParam << endl;
	cout << "derived.Base1::mParam:" << derived.Base1::mParam << endl;
	cout << "derived.Base2::mParam:" << derived.Base2::mParam << endl;

	//2. 重复继承
	cout << "Derived size:" << sizeof(Derived) << endl; 

}

在这个例子中,因为使用了虚拟继承,所以会涉及到虚表指针(vptr)和虚表(vtable)的处理,以及对齐等问题。在大多数情况下,编译器为了内存对齐的目的会在数据成员后面填充一些字节,以保证存取效率。因此,尽管只有一个成员变量,但实际占用的空间会比较大。

具体地说,在这个例子中,BigBase 类中有一个 int 类型的成员变量,通常情况下占用 4 个字节。另外,由于虚拟继承引入了虚表指针,通常情况下会占用 8 个字节(在 64 位系统上)。此外,由于内存对齐的原因,也会有一些填充字节。这些因素导致了 Derived 类的大小为 24 字节。(具体讲解请看这篇)

以上程序Base1 ,Base2采用虚继承方式继承BigBase,那么BigBase被称为虚基类。通过虚继承解决了菱形继承所带来的二义性问题。

热门相关:神秘老公,晚上见!   三国之袁氏枭雄   公子风流   盛宠之嫡女医妃   铁血大明