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 called 221times
function f2 called 53times
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 called 7683times
function f2 called 393times
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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM