C++学习系列笔记(五)

举报
小小谢先生 发表于 2022/04/16 01:31:35 2022/04/16
【摘要】 1、多态 多态(Polymorphism)是面向对象语言的一种特征,让你能够以类似的方式处理不同类型的对象。1.1、使用虚函数实现多态行为 可通过Fish指针或Fish引用访问Fish对象,这种指针或引用可指向Fish、Carp等对象。但你不需要知道也不关心它们指向的是哪种对象。可以用下面代码所示: pFish->Swim();...

1、多态
多态(Polymorphism)是面向对象语言的一种特征,让你能够以类似的方式处理不同类型的对象。
1.1、使用虚函数实现多态行为
可通过Fish指针或Fish引用访问Fish对象,这种指针或引用可指向Fish、Carp等对象。但你不需要知道也不关心它们指向的是哪种对象。可以用下面代码所示:


  
  1. pFish->Swim();
  2. myFish.Swim();

你希望通过这种指针或引用调用Swim()时,如果它们指向的是Tuna对象,则可像Tuna那样游泳,若指向的是Carp对象,则可以像Carp那样游泳,若指向的是Fish,则可像Fish那样游泳。为此,可在基类Fish中将Swim声明为虚函数;


  
  1. class Base
  2. {
  3. virtual ReturnType FunctionName (parameter List);
  4. };
  5. class Derived
  6. {
  7. FunctionName (parameter List);
  8. };

通过使用关键字virtual,可确保编译器调用覆盖版本。也就是说,如果Swim()被声明为虚函数,则将参数myFish(其类型为Fish&)设置为一个Tuna对象时,myFish.Swim()将执行Tuna::Swim(),程序如下所示:


  
  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Fish
  5. {
  6. public:
  7. virtual void Swim()
  8. {
  9. cout << "Fish swims!" << endl;
  10. }
  11. };
  12. class Tuna :public Fish
  13. {
  14. public:
  15. void Swim()
  16. {
  17. cout << "Tuna swims!" << endl;
  18. }
  19. };
  20. class Carp :public Fish
  21. {
  22. public:
  23. void Swim()
  24. {
  25. cout << "Carp swims!" << endl;
  26. }
  27. };
  28. void MakeFishSwim(Fish& InputFish)
  29. {
  30. InputFish.Swim();
  31. }
  32. int main()
  33. {
  34. Tuna myDinner;
  35. Carp myLunch;
  36. MakeFishSwim(myDinner);
  37. MakeFishSwim(myLunch);
  38. return 0;
  39. }

输出为:
Tuna swims!
Carp swims!
首先,根本没有调用Fish::Swim() ,因为存在覆盖版本 Tuna::Swim()和 Carp::Swim() ,它们优先于被声明为虚函数的Fish::Swim()。这很重要,它意味着在MakeFishSwim()中,可通过Fish&参数调用派生类定义的Swim(),而无需知道该参数指向的是哪种类型的对象。
这就是多态:将派生类对象视为基类对象,并执行派生类的Swim()实现。
1.2、为什么需要虚构函数
上面的代码如果加入析构函数释放内存,对于使用new在自由储存区中实例化的派生类对象,如果将其赋值给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数,这可能导致资源未释放、内存泄露等问题,必须引起重视。要避免这种问题,可将基类析构函数声明为虚函数。如下面代码所示:


  
  1. class Fish
  2. {
  3. public:
  4. Fish()
  5. {
  6. cout<<"construct Fish"<<endl;
  7. }
  8. virtual ~Fish()
  9. {
  10. cout<<"Destroy Fish"<<endl;
  11. }
  12. };

输出还表明,无论Tuna对象是使用new在自由存储区中实例化的,还是以局部变量的方式在栈中实例化的,构造函数和析构函数的调用顺序都相同。
1.3、抽象基类和纯虚函数
不能实例化的基类被称为抽象基类,这样的基类只有一个用途,那就是从它派生出其他类。在 C++中,要创建抽象基类,可声明纯虚函数。
以下述方式声明的虚函数被称为纯虚函数:


  
  1. class AbstractBase
  2. {
  3. public:
  4. virtual void DoSomething()=0; //纯虚函数
  5. };
  6. 该声明告诉编译器, AbstractBase的派生类必须实现方法DoSomething();
  7. class Derived : public AbstractBase
  8. {
  9. public:
  10. void DoSomething()
  11. {
  12. cout<<"Implemented virtual function"<<endl;
  13. }
  14. };

AbstractBase类要求Derived类必须提供虚方法DoSomething()的实现。这让基类可指定派生类中方法的名称和特征(Signature),即指定派生类的接口。
虽然不能实例化抽象基类,但可将指针或引用的类型指定为抽象基类。抽象基类提供了一种非常好的机制,让您能够声明所有派生类都必须实现的函数。抽象基类常被简称为ABC。 ABC有助于约束程序的设计。
1.4、使用虚继承解决菱形问题

如图程序所示,如果没有使用虚继承,则会输出:


  
  1. Animal constructor
  2. Animal constructor
  3. Animal constructor
  4. Platypus constructor

输出表明,由于采用了多继承,且 Platypus 的全部三个基类都是从 Animal 类派生而来的,因此第38行创建Platypus实例时,自动创建了三个Animal实例。Animal 有一个整型成员——Animal::Age,为方便说明问题,将其声明成了公有的。如果您试图通过Platypus 实例访问 Animal::Age(如第 42 行所示),将导致编译错误,因为编译器不知道您要设置Mammal::Animal::Age、Bird::Animal::Age还是Reptile::Animal::Age。更可笑的是,如果您愿意,可以分别设置这三个属性:


  
  1. duckBilledp.Mamal::Animal::Age=25;
  2. duckBilledp.Bird::Animal::Age=25;
  3. duckBilledp.Reptile::Animal::Age=25;

显然,鸭嘴兽应该只有一个Age属性,但您希望Platypus类以公有方式继承 Mammal、Bird 和Reptile。解决方案是使用虚继承。如果派生类可能被用作基类,派生它是最好使用关键字virtual:


  
  1. class Derived1 : public virtual Base
  2. {
  3. //members and funnctions
  4. };
  5. class Derived2 : public virtual Base
  6. {
  7. //members and funnctions
  8. };

在继承层次结构中,继承多个从同一个类派生而来的基类时,如果这些基类没有采用虚继承,将导致二义性。这种二义性被称为菱形问题(Diamond Problem)。其中的“菱形”可能源自类图的形状。
注意:C++关键字virtual的含义随上下文而异(我想这样做的目的很可能是为了省事),对其含义总结如下:
在函数声明中,virtual意味着当基类指针指向派生对象时,通过它可调用派生类的相应函数。从Base类派生出Derived1和Derived2类时,如果使用了关键字virtual,则意味着再从Derived1和Derived2派生出Derived3时,每个Derived3实例只包含一个Base实例。也就是说,关键字virtual被用于实现两个不同的概念。
1.5、可将复制构造函数声明为虚函数吗
根本不可能实现虚复制构造函数,因为在基类方法声明中使用关键字virtual时,表示它将被派生类的实现覆盖,这种多态行为是在运行阶段实现的。而构造函数只能创建固定类型的对象,不具备多态性,因此C++不允许使用虚复制构造函数。虽然如此,存在一种不错的解决方案,就是定义自己的克隆函数来实现上述目的:

虚函数Clone模拟了虚复制构造函数,但需要显式地调用,如下面程序所示:

输出为:


  
  1. Tuna swims fast in the sea
  2. Carp swims slow in the lake
  3. Tuna swims fast in the sea
  4. Carp swims slow in the lake

在main()中,第 40~44行声明了一个静态基类指针(Fish*')数组,并各个元素分别设置为新创建的Tuna、Carp、Tuna和Carp对象。注意到myFishes数组能够存储不同类型的对象,这些对象都是从Fish派生而来的。这太酷了,因为为本书前面的大部分数组包含的都是相同类型的数据,如int。如果这还不够酷,您还可以在循环中使用虚函数Fish::Clone将其复制到另一个Fish*'数组(myNewFishes)中,如第48行所示。注意到这里的数组很小,只有4个元素,但即便数组长得多,复制逻辑也差别不大,只需调整循环结束条件即可。第 52 行进行了核实,它通过新数组的每个元素调用虚函数 Swim(),以验证Clone()复制了整个派生类对象。
2、运算符类型和运算符重载
C++运算符从语法层面来看,除使用关键字operator外,运算符与函数几乎没有差别。
return_type operator operator_symbol(……parameter list……)
其中operator_symbol是程序员可以定义的几种运算符之一。C++运算符分为单目运算符和双目运算符
2.1、单目运算符
在类声明中编写单目前缀递增运算符(++),可采用如下语法:


  
  1. Date& operator ++ ( )
  2. {
  3. //operator implementation code
  4. return *this;
  5. }

而后缀递增运算符(++)的返回值不同,且有一个输入参数


  
  1. Date opeator ++ (int)
  2. {
  3. //store a copy of the current state of the object,before incrementing date
  4. Date Copy (*this);
  5. //operator implementation code
  6. return Copy;
  7. }

前缀和后缀递减运算符的声明语法与递增运算符类似,只是将声明中的++替换成--。
示例代码如下:


  
  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Date
  5. {
  6. private
  7. int Day;
  8. int Month;
  9. int Year;
  10. public:
  11. Date(int InputDay, int InputMonth, int InputYear)
  12. :Day(InputDay), Month(InputMonth), Year(InputYear) {};
  13. Date& operator ++ ()
  14. {
  15. ++Day;
  16. return *this;
  17. }
  18. Date operator ++ (int)
  19. {
  20. Date Copy(Day, Month, Year);
  21. ++Day;
  22. return Copy;
  23. }
  24. Date& operator -- ()
  25. {
  26. --Day;
  27. return *this;
  28. }
  29. Date operator -- (int)
  30. {
  31. Date Copy(Day, Month, Year);
  32. --Day;
  33. return Copy;
  34. }
  35. void DispalyDate()
  36. {
  37. cout << Day << " / " << Month << " / " << Year << endl;
  38. }
  39. };
  40. int main()
  41. {
  42. Date Holiday(25, 12, 2011);
  43. cout << "The Day Holiday is:" ;
  44. Holiday.DispalyDate();
  45. ++Holiday;
  46. cout << "day after:";
  47. Holiday.DispalyDate();
  48. --Holiday;
  49. cout << "day before:";
  50. Holiday.DispalyDate();
  51. return 0;
  52. }

输出为:


  
  1. The Day Holiday is:25 / 12 / 2011
  2. day after:26 / 12 / 2011
  3. day before:25 / 12 / 2011

在上面的代码中,如果cout<<Holiday将出现编译错误,因为cout不知如何解读Holiday对象,cout能够很好地显示 const char※,因此,要让 cout能够显示Date对象,只需添加一个返回 const char的运算符:


  
  1. operator const char*()
  2. {
  3. //operator impletmentation that returns a char*
  4. }

所以上述代码可以改为:


  
  1. #include<sstream>
  2. #include<string>
  3. class Date
  4. {
  5. operator const char*()
  6. {
  7. ostringstream formattedDate;
  8. formattedDate<<Day<<" / "<<Month<<" / "<<Year<<endl;
  9. DateInString=formattedDate.str();
  10. return DateInString.c_str();
  11. }
  12. };

就可以直接输出cout<<Holiday。
2.1.2、解除引用运算符和成员选择运算符(->)
智能指针使用了引用运算符和(->)成员选择运算符,需要添加头文件#include<memory>。智能指针类std::unique_ptr实现了上述用法。
2.2、双目运算符
对两个操作数进行操作的运算符称为双目运算符。以全局函数或静态成员函数的方式实现的双目运算符的定义如下:

return_type operator_type(parameter1,parameter2);

 

以类成员实现的双目运算符如下:

return_type operator_type(parameter);

 

以类成员的方式实现的双目运算符只接受一个参数,其原因是第二个参数通常是从类属性获得的。
双目加法与双目减法运算符
示例代码如下:


  
  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class Date
  5. {
  6. private:
  7. int Day,Month,Year;
  8. public:
  9. Date(int InputDay, int InputMonth, int InputYear)
  10. :Day(InputDay), Month(InputMonth), Year(InputYear) {};
  11. Date operator + (int DaysToAdd)
  12. {
  13. Date newDate(Day + DaysToAdd, Month, Year);
  14. return newDate;
  15. }
  16. Date operator - (int DayToSub)
  17. {
  18. return Date(Day-DayToSub,Month,Year);
  19. }
  20. void DispalyDate()
  21. {
  22. cout << Day << " / " << Month << " / " << Year << endl;
  23. }
  24. };
  25. int main()
  26. {
  27. Date Holiday(25, 12, 2011);
  28. cout << "The Day Holiday is:" ;
  29. Holiday.DispalyDate();
  30. Date PreviousHoliday(Holiday - 16);
  31. cout << "PreviousHoliday is:";
  32. PreviousHoliday.DispalyDate();
  33. Date NextHoliday(Holiday + 6);
  34. cout << "Nextholiday on:";
  35. NextHoliday.DispalyDate();
  36. return 0;
  37. }

输出为:


  
  1. The Day Holiday is:25 / 12 / 2011
  2. PreviousHoliday is:9 / 12 / 2011
  3. Nextholiday on:31 / 12 / 2011

运算符提高了类的可用性,但实现的运算符必须合理。
一般运算符都是下面这种代码格式:


  
  1. void operator += (int DaysToAdd)
  2. {
  3. Day+=DaysToAdd;
  4. }

重载等于运算符(==)和不等于运算符(!=)


  
  1. bool operator == (const Date& compareTo)
  2. {
  3. return ((Day==compareTo.Day)&&(Year==compareTo.Year)
  4. &&(Month==compareTo.Month));
  5. }
  6. bool operator != (const Date& compareTo)
  7. {
  8. return !(this->operator==(compareTo));
  9. }



重载赋值运算符(=)
跟复制构造函数一样,为确保进行深复制,需要提供复制赋值运算符


  
  1. Classtype& operator = (const Classtype& CopySource)
  2. {
  3. if (this != &CopySource) //protection against copy into self
  4. {
  5. //Assignment operator impletment
  6. }
  7. return *this;
  8. }

2.3、下标运算符
下标运算符让您能够像访问数组那样访问类,其典型语法如下:

return_type& operator [] (subscript_type& subscript);

 

经典代码如下:


  
  1. const char& operator [] (int Index) const
  2. {
  3. if(Index<GetLength())
  4. return Buffer[Index];
  5. }

2.4、函数运算符operator
operator()让对象像函数,被称为函数运算符。函数运算符用于标准模板库(STL)中,通常是 STL算法中。其用途包括决策。根据使用的操作数数量,这样的函数对象通常称为单目谓词或双目谓词。
示例代码如下:


  
  1. #include<iostream>
  2. #include<string>
  3. using namespace std;
  4. class CDisplay
  5. {
  6. public:
  7. void operator () (string Input) const
  8. {
  9. cout << Input << endl;
  10. }
  11. };
  12. int main()
  13. {
  14. CDisplay mDisplayFuncObject;
  15. mDisplayFuncObject("Dispaly this string!");
  16. return 0;
  17. }

这个运算符也称为operator()函数,对象CDisplay也称为函数对象或functor。
用于高性能编程的移动构造函数和移动复制函数
移动构造函数和移动赋值运算符乃性能优化功能,属于C++11标准的一部分,旨在避免复制不必要的临时值(当前语句执行完毕后就不再存在的右值)。对于那些管理动态分配资源的类,如动态数组类或字符串类,这很有用。
典型代码如下:


  
  1. //移动构造函数声明
  2. Myclass(Myclass&& MoveSource) //重要&&
  3. {
  4. PtrResource = MoveSource.PtrResource;
  5. MoveSource.PtrResource = NULL;
  6. }
  7. //移动赋值函数
  8. MyClass& operator= (MyClass&& MoveSource)
  9. {
  10. if (this != &MoveSource)
  11. {
  12. delete[] PtrResource;
  13. PtrResource = MoveSource.PtrResource;
  14. MoveSource.PtrResource; = NULL;
  15. }
  16. }

文章来源: blog.csdn.net,作者:小小谢先生,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/xiewenrui1996/article/details/100584713

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。