C++之继承和多态

本文参考《C++入门经典》

继承和派生

1.使用继承扩展类

继承性是面向对象程序设计的第二大特性,它允许在既有类的基础上创建新类,新类可以继承既有类的数据成员和成员函数,可以添加自己特有的数据成员和成员函数,还可以对既有类中的成员函数重新定义。利用类的继承和派生实现了更高层次的代码可重用性,符合现代软件开发的思想。

1.1派生语法

在C++中,要从一个类中派生出另一个类,可在类声明中的类名后面加上冒号,再指定类的访问控制(public protected private)以及基类

class Dog:public Mammal

上述语句创建了一个名为Dog的派生类,它继承了即类Mammal。
Dog从Mammal类那里继承了变量,还有除构造函数,析构函数,和复制构造函数的其他所有成员函数。

1.2私有和保护

在这里插入图片描述
参考:https://www.cnblogs.com/cocos2d-html/p/3629146.html

1.3构造函数和析构函数

  • 对于C++继承,当创建派生类的对象时,将调用多个构造函数。
  • 拿上述的Dog和Mammal举例。当创建Dog的对象时,将首先调用基类构造函数创建一个Mammal对象,然后调用Dog类的构造函数完成对象的创建。
  • 销毁对象时,将首先调用Dog类的析构函数,然后调用Mammal类的析构函数。
  • 构造函数按继承顺序调用,而析构函数按相反的顺序调用。

1.4将参数传递给基类构造函数

  • 你可能想重载Mammal和Dog的构造函数,使其分别设置age和breed。如果将参数age和weight传递给Mammal的构造函数呢?如果Dog想要初始化weight,而Mammal没有这样做,该怎么办呢,
  • 请看以下程序,嘻嘻嘻
#include<iostream>

enum BREED
{
	YORKIE,CAIRN,DANDIE,SHETLAND,DOBERMAN,LAB
};

class Mammal
{
public:
	Mammal();
	Mammal(int age);
	~Mammal();

	int getAge() const{return age;}
	void setAge(int newAge){age=newAge;}
	int getWeight() const{return weight;}
	void setWeight(int newWeight){weight=newWeight;}

	void speak() const{std::cout<<"Mammal sound!\n";}
	void sleep() const{std::cout<<"shhhh,I'm sleeping.\n";}

protected:
	int age;
	int weight;

};

class Dog:public Mammal
{
public:
	Dog();
	Dog(int age);
	Dog(int age,int weight);
	Dog(int age,BREED breed);
	Dog(int age,int weight,BREED breed);
	~Dog();

	BREED getBreed() const{return breed;}
	void setBreed(BREED newBreed){breed=newBreed;}

	void wagTail(){std::cout<<"Tail wangging...\n";}
	void begForFood(){std::cout<<"Begging for food...\n";}
private:
	BREED breed;

};


Mammal::Mammal():age(1),weight(5)
{
	std::cout<<"Mammal constructor...\n";
}

Mammal::Mammal(int age):age(age),weight(5)
{
	std::cout<<"Mammal(int) constructor...\n";
}
Mammal::~Mammal()
{
	std::cout<<"Mammal destructor...\n";
}

Dog::Dog():Mammal(),breed(YORKIE)
{
	std::cout<<"Dog constructor...\n";
}
Dog::Dog(int age):Mammal(age),breed(YORKIE)
{
    std::cout<<"Dog(int) constructor...\n";
}
Dog::Dog(int age,int newWeight):Mammal(age),breed(YORKIE)
{
	weight=newWeight;
	std::cout<<"Dog(int,int) constructor...\n";
}
Dog::Dog(int age,BREED newBreed):Mammal(age),breed(newBreed)
{
	std::cout<<"Dog(int,BREED) constructor...\n";
}
Dog::Dog(int age,int newWeight,BREED newBreed):Mammal(age),breed(newBreed)
{
	weight=newWeight;
	std::cout<<"Dog(int,int,BREED) constructor...\n";
}
Dog::~Dog()
{
	std::cout<<"Dog destructor...\n";
}
int main()
{
	Dog fido;
	Dog rover(5);
	Dog buster(5,8);
	Dog yorkie(3,YORKIE);
	Dog dobbie(4,20,DOBERMAN);
	fido.speak();
	rover.wagTail();
	system("pause");
	return 0;
}

程序运行结果如下

Mammal constructor…
Dog constructor…
Mammal(int) constructor…
Dog(int) constructor…
Mammal(int) constructor…
Dog(int,int) constructor…
Mammal(int) constructor…
Dog(int,BREED) constructor…
Mammal(int) constructor…
Dog(int,int,BREED) constructor…
Mammal sound!
Tail wangging…

1.5重写函数

如果派生类创建了一个返回类型和签名(函数名,参数列表和关键字const)都与基类成员函数相同的函数,但是提供了新的实现,就称之为重写该函数。

  • 重载与重写的区别
    两者类似,重载成员函数时,创建了多个名称相同的但签名不同的函数。
    而重写成员函数时,是在派生类中创建了一个名称和签名都与基类函数相同的函数。

  • 重写函数的注意点:
    如果Mammal有三个move()函数的重载版本,而Dog(继承了Mammal)只重写了其中一个版本,那么使用Dog对象将难以访问其他两个版本。相当于其他两个版本被隐藏了。

1.6调用基类方法

即使重写了基类方法,仍可使用全限定名来调用它。为此,可指定基类名,冒号和方法名。这里有两个类Dog类和Mammal类,Dog类继承了Mammal类,并重写了Move方法,那么该如何调用Mammal类中的Move方法呢?

Dog fido;
fido.Mammal::Move(3);

值得注意的点有:

  • Q:继承的数据和函数会传递给后代吗?换句话说,如果Dog是从Mammal派生而来的,而Mammal是从Animal派生而来的,那么Dog能否继承Animal的函数和数据?
  • A:会,连续派生时,派生类将继承其所有基类的函数和数据
  • Q:在派生类中,可以将公有的基类函数改为私有的吗?
  • A: 可以。在接下来的派生中,它将始终保持为私有的。

2.使用多态和派生类

2.1使用虚函数实现多态

1.多态让您能够将派生类对象视为基类对象,例如:假设你创建了从Mammal派生出的Dog, Cat,Horse类,而Mammal类包含很多适用于这些派生类的成员函数,而其中speak(),就是其中之一。它实现了所有哺乳动物都能发出声音的功能。但是现在我们想让每个派生类都发出特殊的声音:如犬吠,猫叫等。每个派生类都必须能够重写speak()方法的实现。
与此同时,如果有一系列Mammal对象,如有Dog,Cat,Horse,Cow对象的农场,你希望让这些对象发出声音,而无需知道或关心它们的speak()实现。将这些对象都视为哺乳动物而调用方法Mammal.speak()时,便使用了多态。

2.为了实现多态,我们需要借助虚函数。
以下程序演示了,如何借助虚函数来实现多态。

#include<iostream>
class Mammal
{
public:
	Mammal():age(1){}
	~Mammal(){}
	virtual void speak() const{std::cout<<"Mammal speak!"<<std::endl;}

private:
	int age;
};

class Dog:public Mammal
{
public:
	void speak() const{std::cout<<"Woof!"<<std::endl;}
};

class Cat:public Mammal
{
public:
	void speak() const{std::cout<<"Meow!"<<std::endl;}
};
class Horse:public Mammal
{
public:
	void speak() const{std::cout<<"Whinny!"<<std::endl;}
};
class Pig:public Mammal
{
public:
	void speak() const{std::cout<<"Oink!"<<std::endl;}
};

int main()
{
	Mammal* array[5];
	Mammal* ptr;
	int choice,i;
	for(i=0;i<5;i++)
	{
		std::cout<<"(1)dog(2)cat(3)horse(4)pig: ";
		std::cin>>choice;
		switch (choice)
		{
		case 1:
			ptr=new Dog;
			break;
		case 2:
			ptr=new Cat;
			break;
		case 3:
			ptr=new Horse;
			break;
		case 4:
			ptr=new Pig;
			break;
		default:
			ptr=new Mammal;
			break;
		}
		array[i]=ptr;
	}
	for (int i = 0; i < 5; i++)
	{
		array[i]->speak();
	}
	system("pause");
	return 0;
}

程序运行结果如下:

(1)dog(2)cat(3)horse(4)pig: 1
(1)dog(2)cat(3)horse(4)pig: 2
(1)dog(2)cat(3)horse(4)pig: 3
(1)dog(2)cat(3)horse(4)pig: 4
(1)dog(2)cat(3)horse(4)pig: 5
Woof!
Meow!
Whinny!
Oink!
Mammal speak!

在编译阶段,无法知道将创建什么类型的对象,因此无法知道将调用哪个speak()方法。ptr指向的对象是在运行阶段确定的,这被称为后期绑定(晚绑定)或运行阶段绑定,与此相对的是静态绑定(早期绑定)或编译阶段绑定。
关于C++多态更多详解,请参考
https://www.cnblogs.com/cxq0017/p/6074247.html

2.2虚成员函数工作原理

由于需要画图,好麻烦啊,参考这篇文章
https://blog.csdn.net/weixin_40237626/article/details/82313339

2.3虚复制构造函数

构造函数是不能为虚函数的,然而,程序有时候非常需要通过传递一个指向基类对象的指针,创建派生类对象的备份。对于这种问题,一种常见的解决方案是,在基类中创建一个clone()成员函数,并将其设置为虚函数。clone函数创建当前对象的备份,并返回该对象。
由于每个派生类对象都重写了clone函数,因此使其创建派生类对象的备份。

#include<iostream>

class Mammal
{
public:
	Mammal():age(1){std::cout<<"Mammal constructor...\n";}
	virtual ~Mammal(){std::cout<<"Mammal destructor...\n";}
	Mammal(const Mammal &rhs);
	virtual void speak() const{std::cout<<"Mammal speak!\n";}
	virtual Mammal* clone(){return new Mammal(*this);}
	int getAge() const{return age;}
protected:
	int age;
};

Mammal::Mammal(const Mammal &rhs):age(rhs.getAge())
{
	std::cout<<"Mammal copy constructor...\n";
}

class Dog:public Mammal
{
public:
	Dog(){std::cout<<"Dog constructor...\n";}
	virtual ~Dog(){std::cout<<"Dog destructor...\n";}
	Dog(const Dog &rhs);
	void speak() const{std::cout<<"Woof!\n";}
	virtual Mammal* clone(){return new Dog(*this);}
};
Dog::Dog(const Dog &rhs):Mammal(rhs)
{
	std::cout<<"Dog copy constructor...\n";
}
class Cat:public Mammal
{
public:
	Cat(){std::cout<<"Cat constructor...\n";}
	virtual ~Cat(){std::cout<<"Cat destructor...\n";}
	Cat(const Cat &rhs);
	void speak() const{std::cout<<"Meow!\n";}
	virtual Mammal* clone(){return new Cat(*this);}
};
Cat::Cat(const Cat &rhs):Mammal(rhs)
{
	std::cout<<"Cat copy constructor...\n";
}

enum ANIMALS
{
	MAMMAL,DOG,CAT
};
const int numAnimalTypes=3;
int main()
{
	Mammal* array[numAnimalTypes];
	Mammal* ptr;
	int choice;
	for (int i= 0; i < numAnimalTypes; i++)
	{
		std::cout<<"(1)dog(2)cat(3)Mammal: ";
		std::cin>>choice;
		switch (choice)
		{
		case DOG:
			ptr=new Dog;
			break;
		case CAT:
			ptr=new Cat;
			break;
		default:
			ptr=new Mammal;
			break;
		}
		array[i]=ptr;
	}
	Mammal *otherArray[numAnimalTypes];
	for(int i=0;i<numAnimalTypes;i++)
	{
		array[i]->speak();
		otherArray[i]=array[i]->clone();
	}
	for(int i=0;i<numAnimalTypes;i++)
	{
		otherArray[i]->speak();
	}
	system("pause");
	return 0;
}

clone() 方法调用复制构造函数,并将当前对象(*this)作为const引用传递给它,以返回一个新Mammal对象的指针。
Dog方法和Cat方法都重写了Clone()方法,使其调用相应类的复制构造函数,并将当前对象传递给它。由于clone()是虚函数。因此这相当于创建了一个虚复制构造函数。

2.4 使用虚函数的代价

包含虚成员函数的类必须维护一个v-table,因此使用虚函数会带来一些开销。如果类很小,并且不打算从它派生出其他类,就根本没有必要使用虚函数。,将任何函数声明为虚函数后,便付出了创建v-table的大部分代价(虽然每增加一个表项都会增加一些内存开销)。在这种情况下,应将析构函数声明为虚函数,并假设其他所有函数也可能是虚函数。应仔细琢磨非虚函数,确保理解它们为什么不是虚函数。
Q:为什么不将类函数都声明为虚函数
A:创建V-table表的开销伴随第一个虚函数的创建而发生。在此之后,创建其他虚函数带来的开销将很小,很多C++程序员认为,如果一个函数为虚函数,那么其他所有的函数也应该为虚函数;另一些程序员不同意这样的观点,他们认为,无论做什么都应有充分的理由。

终于搞定了这篇文章5555555555555555555555555
2019.5.16 555555555555555555555好吧,并没有搞定

讲一下高级多态。

3高级多态

3.1单继承存在的问题

假设现在我们有一个Mammal基类。然后派生出一个Cat类。
然后使用

Mammal *ptr=new Cat;

这时候如果Cat类中有一个特有的函数(这个函数在Mammal中不存在)purr(),原则上我们是不能通过ptr指针来访问到Cat的purr()函数的。但是在迫不得已的情况下(不建议采用)C++为我们提供了一个欺骗方法。

欺骗的方法就是:将基类的指针转换为派生类的指针。你告诉编译器,我知道这是一个Cat对象。请按我的要求去做。
为了让这种方法可行,您将使用运算符【dynamic_cast】,它确保转换是安全的。
工作原理如下:
如果有一个基类(如Mammal)指针,并将派生类(如Cat)对象的地址赋给了它,可使用该指针访问虚函数。然后,如果需要通过Cat对象调用方法purr()(Cat类特有的方法),可使用运算符将Mammal指针转换为Cat指针。在运行阶段,将检查基类指针,如果转换可行,转换得到的Cat指针将没有任何问题,如果转换不可行,就根本不会有Cat对象,而指针将为空。

#include<iostream>
class Mammal
{
public:
	Mammal():age(1){std::cout<<"Mammal constructor...\n";}
	virtual ~Mammal(){std::cout<<"Mammal destructor...\n";}
	virtual void speak() const{std::cout<<"Mammal speak\n";}
protected:
	int age;
};
class Cat:public Mammal
{
public:
	Cat(){std::cout<<"Cat constructor...\n";}
	~Cat(){std::cout<<"Cat destructor...\n";}
	void speak() const{std::cout<<"Meow!\n";}
	void purr() const{std::cout<<"rrrrrrrrrr\n";}
};
class Dog:public Mammal
{
public:
	Dog(){std::cout<<"Dog constructor...\n";}
	~Dog(){std::cout<<"Dog destructor...\n";}
	void speak() const{std::cout<<"Woof!\n";}
};
int main()
{
	const int numberMammals=3;
	Mammal* zoo[numberMammals];
	Mammal* pMammal;
	int choice;
	for(int i=0;i<numberMammals;i++)
	{
		std::cout<<"(1)Dog (2)Cat: ";
		std::cin>>choice;
		if(choice==1)
			pMammal=new Dog;
		else
		{
			pMammal=new Cat;
		}
		zoo[i]=pMammal;
	}
	std::cout<<"\n";
	for(int i=0;i<numberMammals;i++)
	{
		zoo[i]->speak();
		Cat *pRealCat=dynamic_cast<Cat*>(zoo[i]);
		if(pRealCat)
			pRealCat->purr();
		else
		{
			std::cout<<"not a cat\n";
		}
		delete zoo[i];
		std::cout<<"\n";
	}
	system("pause");
	return 0;
}

程序结果如下

(1)Dog (2)Cat: 1
Mammal constructor…
Dog constructor…
(1)Dog (2)Cat: 2
Mammal constructor…
Cat constructor…
(1)Dog (2)Cat: 1
Mammal constructor…
Dog constructor…

Woof!
not a cat
Dog destructor…
Mammal destructor…

Meow!
rrrrrrrrrr
Cat destructor…
Mammal destructor…

Woof!
not a cat
Dog destructor…
Mammal destructor…

3.2 纯虚函数

C++通过提供纯虚函数来支持抽象数据类型(ADT)。纯虚函数是必须在派生类中重写的虚函数。通过将虚函数初始化为0来将其声明为纯虚的。如下所示

virtual void draw()=0;

任何包含一个或多个纯虚函数的类都是ADT,不能对其实例化,试图这样做将导致编译器错误。将纯虚函数放在类中向其客户指出了以下两点:
<1>:不要创建这个类的对象,而应从其派生
<2>: 务必重写这个类继承的纯虚函数。
在从ADT派生而来的类中,继承的纯虚函数仍是纯虚的,要实例化这种类的对象,务必重写每个纯虚函数,否则它也将是ADT。

3.3实现纯虚函数

通常,不实现抽象基类的纯虚函数。由于不能创建抽象类的对象,因此没有理由提供实现,另外,ADT只用作从其派生而来的类的接口定义。然而,可以给纯虚函数提供实现。这样,就可以通过从ADT派生而来的对象调用该函数,该函数旨在给所有重写函数提供通用功能。

问:向上提供功能是什么意思?
答:这指将共享功能向上提升,将其放在基类中。对于许多类都需要的函数,最好将其放在一个合适的基类中。

问:向上提升是否总是好事
答:如果提升的是共享功能,就是好事。如果提升的是接口,就是坏事

问:为什么动态强制类型转换是糟糕的
答:使用虚函数旨在让虚函数(而不是程序员)来判断对象的运行阶段类型。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值