C/C++典型错误:宏中使用函数

define min(a,b) ((a) < (b) ? (a) : (b))

#define max(a,b) ((a) > (b) ? (a) : (b))

我常常会用这两个宏,一般来说,宏比函数更高效,但是如果里面元素是函数的话,就不是这样。因为宏只是在编译预处理阶段执行简单替换,如果你有两个函数f和g,代码中写了min(f(a),g(a)),那么就会被展开为 ((f(a)) < (g(a)) ? (f(a)) : (g(a))),这是4次调用,而不是想象中的先算f(a),再算g(a),然后传给min——那是函数的行为,不是宏!

如果f和g是递归函数的话,结果更糟糕

实际上,C++的STL的algorithm中有min,max的模板函数,大概这个样子:

template <class T> const T& min ( const T& a, const T& b ) {
  return !(b<a)?a:b;     // or: return !comp(b,a)?a:b; for the comp version
}

看上去和我们的宏很像嘛,只是多了一层函数的包装——但是它是函数,是要传参的,而不是像宏那样执行替换,所以min(f(a),g(a))完全没有问题,只会调用f和g各一次。

其实说完全没有问题还是有点心虚的,如果f是递归函数,f函数中有调用min(f(a), f(b))的话,递归过程中会有很多个min等待传值(在递归栈中),调用深度似乎是增加了一倍,不过可能这不是很大的问题,不过考虑效率的话,可以这么写

int f(int a){
//some code.....可能是处理递归边界什么的
r1 = f(a - 1) * 3;//递归调用
r2 = f(a - 1) * 2 + 12;
return min(r1,r2);
}

也就是说,先算出来,再用min,这样无论min是函数还是宏都没问题,不过不像直接把函数写在min里那么简洁……简洁有时是要付出一点点效率上的代价的,而追求效率有时候会很罗嗦……

STL中的min/max还有一个好处是支持自定义类型,可以通过重载<运算符,也可以通过提供比较函数

因此,我决定暂时不使用这个min/max宏了,直接用STL中的min/max模板函数,还省得敲这么两行代码

下面是一个例子,展示了我是如何犯了这个错误,然后突然醒悟的(肯定有书上讲过关于宏的这个注意点,貌似我还看到过,但是……人要犯错,你怎么能拦得住?)

原始的错误记录在这里http://www.cnblogs.com/fstang/archive/2012/12/23/2829946.html (这是一篇挺长的文章,且包含新的内容(Topcoder上的一道题),如果你只想了解关于本文宏的相关内容就不必跳过去了)

我还保留了原来错误的地方,为了方便阅读,我把对比代码贴在这里,主要观察f和f2的写法上的区别(其实这个在前面也讲过了,一个是直接把函数放到min宏里,一个是先算,然后用min……别看代码了,直接看最后的函数调用次数的统计结果吧)

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
#define min(a,b) ((a) < (b) ? (a) : (b))

class MonstersValley2{
public:
    int cnt;
    MonstersValley2():cnt(0){}
    int minimumPrice(vector <int> dread, vector <int> price){
        int r1 = f(0, 0, dread, price);
        cout << "function f called " << cnt << "times\n";
        cnt = 0;
        int r2 = f2(0, 0, dread, price);
        cout << "function f2 called " << cnt << "times\n";
        if (r1 != r2) {
            cerr << "error\n";
        }
        return r1;
    }

    int f(long long sum, int idx, vector <int> &dread, vector <int> &price){
        cnt++;//统计调用次数
        if (idx >= dread.size()) {
            return 0;
        }
        if (sum >= dread[idx]) {
            int m = min(price[idx] + f(sum + dread[idx], idx + 1, dread, price), 
                f(sum, idx + 1, dread, price));    
            return m;
        } else {
            int m = price[idx] + f(sum + dread[idx], idx + 1, dread, price);
            return m; 
        }
    }
    int f2(long long sum, int idx, vector <int> &dread, vector <int> &price){
        cnt++;//统计调用次数
        if (idx >= dread.size()) {
            return 0;
        }
        if (sum >= dread[idx]) {
            int m1 = price[idx] + f2(sum + dread[idx], idx + 1, dread, price);//先算,再调用min
            int m2 = f2(sum, idx + 1, dread, price);
            return min(m1,m2);
        } else {
            int m = price[idx] + f2(sum + dread[idx], idx + 1, dread, price);
            return m; 
        }
    }

};

int main(int argc, char* argv[])
{
    int a[] = {200, 107, 105, 206, 307, 400};
    int b[] = {1, 2, 1, 1, 1, 2};
    MonstersValley2 m;
    cout << m.minimumPrice(vector<int>(a, a+sizeof(a)/sizeof(int)), vector<int>(b, b+sizeof(b)/sizeof(int))) << endl;
    return 0;
}

输出:

function f called221times

function f2 called53times

2

如果换一个更大的输入实例:

int a[] = {5216, 12512, 613, 1256, 66, 17202, 30000, 23512, 2125, 33333};
    int b[] = {2, 2, 1, 1, 1, 1, 2, 1, 2, 1};

输出:

function f called7683times

function f2 called393times

5

前一个相差4倍,后一个接近20倍,因为这本来就是一个(随问题规模增大,复杂度指数增长的)递归函数,每个规模为n的问题可能变为2个规模为n-1的问题,也可能变为1个规模为n-1的问题,所以最坏情况复杂度是O(2^n),最好情况时间复杂度为O(n)

但是由于宏的不慎使用,每个规模为n的问题可能变为4个规模为n-1的问题,或变为1个规模为n-1的问题,最坏情况复杂度是O(4^n),最好情况时间复杂度为O(n)
原文链接: https://www.cnblogs.com/fstang/archive/2013/01/07/2849317.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/74715

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 下午4:31
下一篇 2023年2月9日 下午4:33

相关推荐