为什么要使用虚函数和 指针(或是引用)才能实现多态?

网上找的 搜集在一起 

https://www.cnblogs.com/bofengyu/p/6761389.html

http://blog.csdn.net/sangyongjia/article/details/50888705

https://segmentfault.com/a/1190000004359057

http://www.cnblogs.com/0xcafebabe/p/4423699.html

http://blog.csdn.net/piaoxuezhong/article/details/54348787

http://www.cnblogs.com/staring-hxs/p/3669497.html

http://blog.csdn.net/fu_zk/article/details/13001649

http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html

http://blog.csdn.net/fengxinlinux/article/details/71037244

http://velep.com/archives/581.html

http://blog.csdn.net/ljianhui/article/details/22895505


http://blog.jobbole.com/83461/

http://blog.csdn.net/china1000/article/details/47925617

https://www.cnblogs.com/qicosmos/p/4480460.html

http://blog.csdn.net/lotluck/article/details/49448755


http://blog.csdn.net/z_h_s/article/details/50699905

补充:(2016.04.05)

上次使用了g++ -fdump-tree-original test3.cpp 命令看到了编译器补充后的函数是什么样子的,但是没有记录,今天补充下:

   下文中第一main函数的代码使用如上命名编译后生成一个名为:test3.cpp.003t.original的文件。打开文件查看后会发下编译器给补充的default构造函数和copy函数分别如下图所示:




       从图中可以看到,编译器会自动给代码添加默认的构造函数和copy构造函数,同时在此两个构造函数中加入,对 _vptr.shape(指向虚表的,虚指针)赋值,指向Shape类的虚函数表,之所以附带贴上编译器添加的默认构造函数,是为了说明copy构造函数中的虚指针确实是指向Shape类的虚函数表。

       在咱们的例子中,当以shape1,shape2作为OutputShape()函数的参数时,由于OutputShape函数定义时的默认参数为Shape类型,而传进去的参数均为Shape类型的子类,此时会隐式调用Shape类的copy构造函数。而copy构造函数是只对成员变量进行拷贝, 虚指针(_vpyr)指向Shape的虚函数表。所以,输出结构就是……。

       对下文的第二个main函数的代码使用如上命名编译后生成一个名为:test3.cpp.003t.original的文件。打开文件查看后会发下编译器给补充的default构造函数和copy函数之外,还给默认补充了copy assignment operator【注意,上边的代码中并没有生成copy assignment operator函数,《深入理解C++对象模型》中说,编译器默认会给代码生成4个函数,分别是:default构造函数(前提是类没有声明任何构造函数)、copy构造函数、copy assignment operator和析构函数;但同时也说,如果四种函数在函数中根本就没有使用的时候,则不会生成之,有用的叫non-trivial的,没用的叫trivial的;此次生成copy assignment operator函数,而上边的情况编译器并没有生成此copy assignment operator函数也印证了此说法】,下图所示为编译器生成的copy assignment operator函数:


观察发现此copy assignment operator函数的形式参数为Shape的const的引用,所以此问题其实是对上边描述的情况的一个包装!

        对下文的第三个main函数的代码使用如上命名编译后生成一个名为:test3.cpp.003t.original的文件。

    此部分需要自己对照第3个main函数仔细分析,其实我现在还是没有完全明白!因为我手工更改了_vptr指针的值,所以以指针的方式的temp1->DrawSelf()调用DrawSelf函数时,编译器将其翻译为:

OBJ_TYPE_REF(*NON_LVALUE_EXPR <NON_LVALUE_EXPR <temp1>->_vptr.Shape>;NON_LVALUE_EXPR <temp1>->0) (NON_LVALUE_EXPR <temp1>) >>

所以执行后输出结果为“连接各顶点”;但是以temp.DrawSelf()的方式调用DrawSelf函数时,编译器将其翻译为:DrawSelf (&temp) >>>

但是我查找了整个文件,并没有找到参数为Shape*的DrawSelf函数,我猜测此处是因为DrawSelf被声明为虚函数,此处是直接以Shape::DrawSelf的方式调用了Shape的虚函数???此处仍是一个疑问,倘若有高手看到,还望指教,感谢!

至此,此文此文完结,剩余的一个疑问后续学习时留心解决!(2016.04.05晚20:57)

补充:

http://blog.csdn.net/zoopang/article/details/14071779此文中一楼的朋友的回复:——"使用对象时,成员函数的调用派遣是由编译器确定后硬编码进程序的,没有经过虚函数表。"符合了我上文的猜测,但是原理还是不太明了!(2017.2.10-16:43)

补充:

今天(2016.3.16)使用如下命令在查看编译器是如何忘构造函数里加代码的时候突然有了个疑问:虚函数表是属于类的还是对象的,因为我看到貌似是直接赋一个固定的值给_vptr。网上搜索了下,确定了自己的猜测:虚函数表是属于类的!

g++ -fdump-tree-original test3.cpp 

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

正文:

        首先说说为什么是这样一个题目,其实我最开始思考的是子类实例化的对象赋值给父类实例化的对象,为什么不能实现多态!搜索后发现一个哥们儿跟我的疑问一样,但是题目是这个,我也觉的这个题目更好些。

        最近我在学习《深度探索C++对象模型》这本书,明白了C++对象模型的内存布局。但也恰巧是这个内存布局让我又一次陷入了深深的疑惑之中。先看看我的例子:

[cpp]  view plain  copy
  1. 注:此例也是引用某位博主的,只是搜索的内容太多了,找不到原连接的位置了……。  

[cpp]  view plain  copy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Polygon;  
  5. class Shape//形状  
  6. {  
  7. public:  
  8.     virtual void DrawSelf()//绘制自己  
  9.     {  
  10.        cout << "我是一个什么也绘不出的图形" << endl;  
  11.     }  
  12. };  
  13.   
  14. class Polygon:public Shape//多边形  
  15. {  
  16. public:  
  17.     void DrawSelf()   //绘制自己  
  18.     {  
  19.        cout << "连接各顶点" << endl;  
  20.     }  
  21. };  
  22.   
  23. class Circ:public Shape//圆  
  24. {  
  25. public:  
  26.     void DrawSelf()   //绘制自己  
  27.     {  
  28.        cout << "以圆心和半径为依据画弧" << endl;  
  29.     }  
  30. };  
  31.   
  32. void OutputShape(Shape arg)//专门负责调用形状的绘制自己的函数  
  33. {  
  34.     arg.DrawSelf();  
  35. }  
  36.   
  37. int main()  
  38. {  
  39.     Polygon shape1;  
  40.     Circ shape2;  
  41.     //Shape temp;  
  42.     //temp=shape1;  
  43.     //temp.must(shape1);  
  44.     OutputShape(shape1);  
  45.     OutputShape(shape2);  
  46. }  
上述代码执行后输出的结果为:

        为什么是这样的结果?因为在调用OutputShape(shape1) 和 OutputShape(shape2)时,在参数传递时会调用arg(Shape)的copy构造函数,arg的copy构造函数中应该仅仅是对成员变量的拷贝!(已经确认仅仅只拷贝成员变量的值,不会涉及_vptr的赋值),所以_vptr还是指向原来虚函数表,所以调用的仍为基类的DrawSelf()函数。(注意此处将会产生slice问题,即将子类实例内存布局中从父类继承来的变量copy 到父类实例的对应成员变量中,子类自身的成员变量被舍弃。)

注:此处我需要搞清楚编译器自动给加的copy构造函数究竟长什么样?但是我现在还没找到好的办法。(已解决,参见2016.04.05的补充

如果将上述的main函数改为:

[cpp]  view plain  copy
  1. int main()  
  2. {  
  3.     Polygo shape1;  
  4.     Circ shape2;  
  5.     Shape temp;  
  6.     temp=shape1;  
  7.     //temp.must(shape1);  
  8.     OutputShape(temp);  
  9.     OutputShape(shape2);  
  10. }  

输出的结果仍然为:


为什么是这样的结果?arg的copy assignment operator中应该仅仅是对成员变量的拷贝!所以_vptr还是指向原来虚函数表,所以调用的仍为基类的DrawSelf()函数。

注:此处我需要搞清楚编译器自动给加的 copy assignment operator函数究竟长什么样?但是我现在还没找到好的办法。(已解决,参见2016.04.05的补充

基于以上两个疑问,我在搜索的过程中又发现了篇关于深拷贝和浅拷贝的文章,觉的写的浅显易懂,也转载了过来。

对于彻底搞清楚copy构造函数和copy assignment operator也需要一篇文章(这篇文章中和effective C++ 中的条款5有不一致的地方需要验证,究竟谁说的对)。


        起初我碰到这个问题时上网搜索了下,发现了这篇文章中作者和我其实有相同的疑问。通过阅读作者的文章,了解到了是因为_vptr没有拷贝的原因,但是作者的疑问也同样困扰了我,到现在也没有想到合理的解释。我的实验待如下(基于上述代码):

[cpp]  view plain  copy
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Polygon;  
  5. class Shape//形状  
  6. {  
  7. public:  
  8.     virtual void DrawSelf()//绘制自己  
  9.     {  
  10.        cout << "我是一个什么也绘不出的图形" << endl;  
  11.     }  
  12.   
  13.     Shape& must (const Polygon& b)  
  14.     {  
  15.         cout<<"i am in"<<endl;  
  16.         *(long *)this=*(long *)&b;  
  17.         this->DrawSelf();  
  18.         return *this;  
  19.     }  
  20. };  
  21.   
  22. class Polygon:public Shape//多边形  
  23. {  
  24. public:  
  25.     void DrawSelf()   //绘制自己  
  26.     {  
  27.        cout << "连接各顶点" << endl;  
  28.     }  
  29. };  
  30.   
  31. class Circ:public Shape//圆  
  32. {  
  33. public:  
  34.     void DrawSelf()   //绘制自己  
  35.     {  
  36.        cout << "以圆心和半径为依据画弧" << endl;  
  37.     }  
  38. };  
  39.   
  40. void OutputShape(Shape arg)//专门负责调用形状的绘制自己的函数  
  41. {  
  42.     arg.DrawSelf();  
  43. }  
  44.   
  45. int main()  
  46. {  
  47.     Polygon shape1;  
  48.     Circ shape2;  
  49.     Shape temp;  
  50.     temp.must(shape1);//i am in   and  连接各个节点  
  51.     Shape *temp1=&temp;  
  52.     temp1->DrawSelf();//连接各个节点  
  53.     temp.DrawSelf();//我是一个什么也绘不出的图形  
  54.     OutputShape(shape1);//我是一个什么也绘不出的图形  
  55.     OutputShape(shape2);//我是一个什么也绘不出的图形  
  56. }  
此段函数的输出结果是:



       我在Shape类中加入一个must()函数来强行改变此Shape实例的_vptr的值为一个Polygo实例的_vptr的值,并直接在must()函数中调用DrawSelf()函数,输出达到预期。在main()函数中使用指针的方式来调用DrawSelf()也符合预期。但是使用temp.DrawSelf()的方式调用时,结果却出乎意料的输出了“我是一个什么也绘不出的图形”,但是预期确实是 “连接各顶点”。这肯定是编译器解释  temp.DrawSelf()  的方式导致的,但是我现在还没有办法解释到底怎么回事儿!


注:此处我需要搞清楚编译器究竟是怎么解释    temp.DrawSelf() 的?但是我现在还没找到好的办法。(已解决,参见2016.04.05的补充



记录下自己的思考过程,请忽略:

为什么是这样的结果?按照之前此文章的实践验证和《深度理解C++对象模型》的理解,shape1和shape2的内存布局以及Shape类的实例的内存布局,如上图所示:

那么将shape1和shape2赋给arg,其内存布局中父类部分的值就都拷贝过去了,由于只有一个_vptr指针,所以此指针就覆盖了arg中原有的_vptr中的值了呀,那么调用虚函数查询虚表时,就应该和使用指针(引用)的效果一样呀,都应该调用的是子类中的DrawSelf()函数呀,怎么最后调用了基类的DrawSelf()函数了呢?too naive o(∩_∩)o 哈哈。


图中“其他成员”代表可以声明别的成员变量,代码中为了简单并未定义成员变量。

==================

1,大于给定元素的最小元素
给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。
数组里字母的顺序是循环的。举个例子,如果目标字母target = 'z' 并且有序数组为 letters = ['a', 'b'],则答案返回 'a'。
示例:
输入:
l
letters = ["c", "f", "j"]
t
target = "a"


输出: "c"






输入:
l
letters = ["c", "f", "j"]
t
target = "c"


输出: "f"






输入:
l
letters = ["c", "f", "j"]
t
target = "d"


输出: "f"






输入:
l
letters = ["c", "f", "j"]
t
target = "g"


输出: "j"






输入:
l
letters = ["c", "f", "j"]
t
target = "j"


输出: "c"






输入:
l
letters = ["c", "f", "j"]
t
target = "k"


输出: "c"




注:
letters长度范围在[2, 10000]区间内。
letters 仅由小写字母组成,最少包含两个不同的字母。
目标字母target 是一个小写字母。


    char nextGreatestLetter(vector<char>& letters, char target) {
      /**
大于target的数有很多,这里只需要找到第一个大于的就行
等效于找到最后一个小于等于target的数
*/
  int left = 0,right=letters.size()-1;
while(left<=right)
{
 int mid = left + (right -left)/2;
 if( letters[mid] <= target)
left = mid+1;
 else
   right =mid -1;
}
  //因为已经说了最少包含两个不同字母,且是循环比较大小,所以最右边的left不存在就在0位
return l>=letters.size()?letters[0]:letters[left];
    }


2,有序数组的 Single Element
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]


输出: 2




示例 2:
输入: [3,3,7,7,10,11,11]


输出: 10




注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。


    int singleNonDuplicate(vector<int>& nums) {
       /**
  思路:这种只出现一次的,最开始想到的是用位运算,但是位运算的事件复杂度是n,
 题目说了是有序数组,那么就尝试二分查找,事件复杂度为logN
 因为是有序,所以可等效于查找
*/ 
  int left =0 ,right = nums.size()-1;
while(left < right) // 这里相等的时候就是所求的值,与其他不同
  {
    int mid = left +(right-left)/2;
 if(mid%2) --mid;
 if(nums[mid] == nums[mid+1]) left = mid+2;
    else
    right = mid ;
   }
return  nums[left]; //left==right
    }


3,第一个错误的版本
你是产品经理,目前正在领导一个团队开发一个新产品。不幸的是,您的产品的最新版本没有通过质量检查。由于每个版本都是基于之前的版本开发的,所以错误版本之后的所有版本都是不好的。
假设你有 n 个版本 [1, 2, ..., n],你想找出第一个错误的版本,导致下面所有的错误。
你可以通过 bool isBadVersion(version) 的接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。您应该尽量减少对 API 的调用次数。


    int firstBadVersion(int n) {
        /**
思路:最简单的二分查找判断
 */
int left =1,right =n;
while(left<right) //这里相等的时候也是最后的结果
{
  int mid = left+(right -left)/2;
if(isBadVersion(mid)) right = mid;  //与上面找有序数组的一样,注意有错的时候那位
else
left = mid +1;
 }
return left; //right==left
    }


4,旋转数组的最小数字
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2])。
找到其中最小的元素。
你可以假设数组中不存在重复元素。
   int findMin(vector<int>& nums) {
   /**
   思路:这题与找循环字母的那题有些类似
 */
/**
int left = 0, right = nums.size()-1;
while(left<right) //left==right同样也是结束条件
{
if(nums[right]>=nums[left]) return nums[left]; //这句话不能少,不然判断会有问题
int mid = left +(right-left)/2;
if(nums[mid] < nums[right]) right =mid;
 else
  left = mid +1;
}
//最后的结束条件
return nums[right];//left ==right
*/
int left = 0, right = nums.size()-1;
while(left<right) //left==right同样也是结束条件
{
if(nums[right]>=nums[left]) return nums[left]; //这句话不能少,不然判断和左边判断的时候会有问题
int mid = left +(right-left)/2;
if(nums[mid] < nums[left]) right =mid;
 else
   left = mid +1;
}
//最后的结束条件
return nums[right];//left ==right
    }


5,搜索范围
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8


输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6


输出: [-1,-1]
    vector<int> searchRange(vector<int>& nums, int target) 
   {
    vector<int> result(2, -1);
    if (nums.size() == 0)
        return result;
    int left = 0, right = nums.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target)
            right = mid - 1;
        else
            left = mid + 1;
    }
    if (left>=nums.size()||nums[left] != target) //这是与之前不同的地方,因为可能不存在,所以要判断边界与值
        return result;
    result[0] = left;
    left = 0, right = nums.size() - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] <= target)
            left = mid + 1;
        else
            right = mid - 1;
    }
    if (right<0||nums[right] != target)
        return result;
    result[1] = right;
    return result;
    }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值