本文参考《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派生而来的对象调用该函数,该函数旨在给所有重写函数提供通用功能。
问:向上提供功能是什么意思?
答:这指将共享功能向上提升,将其放在基类中。对于许多类都需要的函数,最好将其放在一个合适的基类中。
问:向上提升是否总是好事
答:如果提升的是共享功能,就是好事。如果提升的是接口,就是坏事
问:为什么动态强制类型转换是糟糕的
答:使用虚函数旨在让虚函数(而不是程序员)来判断对象的运行阶段类型。