• 当模板函数遇上数组参数


      在伯乐在线上看到一篇关于数组和指针的文章(文章链接:http://blog.jobbole.com/44863/),突然想到自己最近也遇到一个类似的有趣的案例,于是决定写下来和大家分享。

    1. 我的初衷

      我的初衷是想写一个简单通用的函数PrintIntArray用于打印一个int数组的各个元素。因为我想数组的长度是数组的属性,我不想每次调用此函数的时候手动传入数组长度,于是我将函数声明为PrintIntArray(int arr[]),然后写一个简单的内联函数(为了通用,声明为模板函数)用于动态获取数组长度(如下):

    template <class T>
    inline int GetArrayLen(T& array)
    {
      //数组占用内存数除以单个元素占用内存数得到数组长度
        return sizeof(array)/sizeof(array[0]);
    }

      这样,我的PrintIntArray函数就可以这样写:

    void PrintIntArray(int arr[])
    {
        int len = GetArrayLen(arr);
        for (int i = 0; i < len; i++)
        {
            printf("%d ", arr[i]);
        }
        printf("
    ");
    }

    2. 初衷很美好,问题跑不掉

      为了测试打印函数,写一个main函数进行测试:

    void main(int argc, char* argv[])
    {
        int arr[] = {1, 2, 3, 4, 5};
        printf("array length: %d
    ", GetArrayLen(arr));
        printf("elements of array:
    ");
        PrintIntArray(arr);
        getchar();
    }

      当运行测试程序的时候,运行结果却出乎我的意料:

      可以看到,GetArrayLen函数可以正确地计算数组长度,但是PrintIntArray却只打印出了数组的第一个元素。

      于是调试进到PrintIntArray函数的 int len = GetArrayLen(arr); 这句话,发现返回的值是1,怪不得只打印了第一个元素:

      这就奇怪了,GetArrayLen在PrintIntArray函数外面(main函数里面)的时候明明可以正确返回数组长度,为什么进到函数里面的时候行为就变得异常了?大家都知道,数组作为函数参数的时候传递的是指针,如果这是造成异常的原因,那么在main函数里面GetArrayLen(arr)这句话也是传递的指针,应该同样返回1才对,为什么它就可以正确地返回数组的长度?

    3. 问题分析

      后来经过更多的测试分析,发现问题出在GetArrayLen这个模板函数的声明、以及c++对模板的解析机制上。在PrintIntArray函数内部, int len = GetArrayLen(arr); 这句话返回1的原因稍微分析一下其实是容易理解的,因为当你将数组变量arr传递给PrintIntArray函数时,它其实已经退化成了指针,你再将指针传递给GetArrayLen函数,sizeof(arr)求得的是指针占用的内存数,结果是4;而sizeof(arr[0])返回的是arr第一个元素占用的内存字节数,因为是int数组,所以结果也是4。这就是GetArrayLen(arr)函数最后返回1的原因。

      而在main函数里面调用GetArrayLen(arr)函数的时候,在arr退化成指针之前,它要先被GetArrayLen模板函数解析,解析的结果就是模板函数形参中的T被解析为int[5](这里似乎很奇怪,后面还有更详细的分析),形参array被当做实参arr的别名。实际上arr还是被当成数组看待的,即一块连续的内存,并没有退化成指针,因此此时sizeof(arr)的结果为5个int的长度,即20字节;而sizeof(arr[0])的结果依然是arr第一个元素占用的内存,即4字节,因此此时会返回正确的数组长度。

      为了验证这一猜想,我另外写了个测试程序,但此时我用的是double型数组,按照上面的分析,如果将数组名直接传递给GetArrayLen函数,它将依然被当成数组看待,因此sizeof(arr)的结果应该是40(5个double数据的长度),而sizeof(arr[0])的结果应该是8(double型数据长度),最终GetArrayLen函数返回正确的数组长度——5;但是如果将此数组的指针传递给GetArrayLen函数,那么sizeof(arr)的结果应该是4(指针占用内存数),而sizeof(arr[0])的结果依然是8,最终GetArrayLen函数返回0。

      为了调试方便,我将GetArrayLen函数重写为:

    template <class T>
    int GetArrayLen(T& array)
    {
        int len1 = sizeof(array);
        int len2 = sizeof(array[0]);
        return len1 / len2;
    }

      测试用main函数如下:

    1 void main(int argc, char* argv[])
    2 {
    3     double arrDouble[] = {1, 2, 3, 4, 5};
    4     double* ptrDouble = arrDouble;
    5     printf("pass array to func GetArrayLen :%d
    ", GetArrayLen(arrDouble));
    6     printf("pass pointer to func GetArrayLen :%d
    ", GetArrayLen(ptrDouble));
    7     getchar();
    8 }

      当传递数组arrDouble进去的时候(第5行),单步调试到GetArrayLen函数内部,结果如下:

      当传递指针ptrDouble进去的时候(第6行),单步调试到GetArrayLen函数内部,结果如下:

      可见,上面的分析是正确的。程序最终的运行结果如下:

    4. 寻求改进

      经过这么多的分析最后发现,自己一开始写的打印函数PrintIntArray其实根本无法工作,因为他限制传入的数组不能为引用,这与数组传引用的机制相矛盾。其实如果清楚c++模板的解析机制,就不用绕这么多弯了,不仅可以写出数组打印函数,而且是对所有基础数据类型数组都有效的打印函数。

      我们继续分析。

      前面的分析写到,(GetArrayLen)“模板函数形参中的T被解析为int[5]”,不仅如此,如果你传递的数组长度为8,T就被解析为int[8],长度为10,T就被解析为int[10]……我们发现模板解析机制可以自动得到输入数组的长度,这给了我们巨大的惊喜和启发,是不是可以利用此机制自动获取传入的数组长度呢?答案是肯定的,我们还是慢慢来看。

      一开始,针对通用数组打印函数问题,我也是百度了一下,得到的一个版本如下:如果你想打印长度为10的数组,那么可以这样写:

    template <class T>
    void PrintArray(T (&arr)[10])
    {
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", arr[i]);
        }
        printf("
    ");
    }
    
    void main(int argc, char* argv[])
    {
        int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        PrintArray(arr);
        getchar();
    }

      但是这样还是不够完美,因为只能限制打印的数组长度为10,如果改变一下数组,例如arr[] = {0, 1, 2, 3, 4},因为传入数组长度和模板函数声明的长度不一致,编译都不会通过,会报如下错误:

    error C2784: “void PrintArray(T (&)[10])”: 未能从“int [5]”为“T (&)[10]”推导 模板 参数

      虽然不够完美,但是也正是这个不完美的版本以及这句编译提示,让我想到了c++背后的模板解析机制,以及做出“模板函数形参中的T被解析为int[5]”这句结论的原因。其实这个版本已经非常接近最终版本了,既然数组长度是动态解析的,那么我们只需要将模板函数声明中的常量10改为变量是不是就可以了呢?

      答案就是这样的,只需多加一个模板参数声明,最终完美版便诞生了:

    template <class T, int size>
    void PrintArray(T (&arr)[size])
    {
        for (int i = 0; i < size; i++)
        {
            cout<<arr[i]<<" ";
        }
        cout<<endl;
    }

      为了通用性,改用cout输出,为此,需要添加如下两句预编译指令:

    #include <iostream>
    
    using namespace std;

      这样,你就可以用PrintArray打印任意(基础数据)类型、任意长度的数组了。

      这里再回过头来看一下模板解析过程。以array[] = {0, 1, 2, 3, 4}为例,当调用PrintArray(array)函数,遇到void PrintArray(T (&arr)[size])这样的模板函数声明时,编译器将形参arr作为实参array的别名,同时T被解析为int,size被解析为5(数组长度,可变),这样就可以正确打印出数组内容了。

    5. 结论

    • 数组传递给函数时会退化成指针
    • 模板是C++中一种灵活又复杂的机制,弄清楚这种机制能帮助你更简单高效地解决实际问题
    • 老生常谈:学习编程语言没有捷径,平时多动手敲代码,遇到问题善于分析,针对你想到的所有可能设计测试用例,证明或证伪它!

      

  • 相关阅读:
    chrome浏览器postman 插件安装
    使用poi解决导出excel内下拉框枚举项较多的问题
    Nginx(三)------nginx 反向代理
    webpack 内存溢出 Allocation failed
    Postman 安装及使用入门教程 (谷歌浏览器插件版)
    jquery 控制 video 视频播放和暂停
    百度编辑器ueditor 光标位置的坐标
    mkdocs 生成帮助文档
    js 日期 相关
    vue-cli3 第三版安装搭建项目
  • 原文地址:https://www.cnblogs.com/kane1990/p/3260844.html
Copyright © 2020-2023  润新知