陈皓专栏 【空谷幽兰,心如皓月】

芝兰生于深谷,不以无人而不芳;君子修道立德,不为困穷而改节。

陈皓ID:haoel
474354次访问,排名99好友16人,关注者46
暂无
haoel的文章
原创 71 篇
翻译 0 篇
转载 0 篇
评论 1037 篇
陈皓的公告
Email & MSN
haoel@hotmail.com
最近评论
poscard:因为要了解java.nio而搜到了你的文章。才发现,以前就看过关于你的介绍。
感觉学到了不少知识,主要是研究问题的方法。
谢谢~,先收藏下,以后陷入百思不得其解的难题时,就读读这几篇文章找找感觉:)
babaoqi:学习一下
顶一个
Scratch:感谢陈老师。您让人真的很尊敬!!!
yafeng0120:#define MAX( (a), (b) ) (a)>(b)?(a):(b)

像这样的宏还是不安全的,应该在整体前后再加上括号,因为MAX((a),(b))还可能会与其他表达式共同计算,例如 5 * MAX(5,1)。

#define MAX( (a), (b) ) ((a)>(b)?(a):(b))
jzgnh:C++是天才写给天才用的 这个有点过了~
c++ 其实没你说的那么复杂,我感觉如果明白了C语言,那些C++的东西都一样的,只是有些工作本来是自己做,但是现在交给了编译器而已,如果你有兴趣的话可以通过n多种方法来分析到底编译器是如何做的,明白了其实C跟C++也就没什么区别了~~

至于怪异写法的问题我感觉C的怪异写法似乎更让人难以琢磨,呵呵~但是现在的"unix……
文章分类
收藏
    相册
    我的BLOG
    耗子小筑(非技术)(RSS)
    陈皓专栏(技 术)(RSS)
    存档
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创 C/C++返回内部静态成员的陷阱收藏

    新一篇: 从语句 char* p="test" 说起 | 旧一篇: “抄袭事件”判决书

     

     C/C++返回内部静态成员的陷阱

    陈皓

    背景

    在我们用C/C++开发的过程中,总是有一个问题会给我们带来苦恼。这个问题就是函数内和函数外代码需要通过一块内存来交互(比如,函数返回字符串),这个问题困扰和很多开发人员。如果你的内存是在函数内栈上分配的,那么这个内存会随着函数的返回而被弹栈释放,所以,你一定要返回一块函数外部还有效的内存。

    这是一个让无数人困扰的问题。如果你一不小心,你就很有可能在这个上面犯错误。当然目前有很多解决方法,如果你熟悉一些标准库的话,你可以看到许多各式各样的解决方法。大体来说有下面几种:

    1)在函数内部通过malloc或new在堆上分配内存,然后把这块内存返回(因为在堆上分配的内存是全局可见的)。这样带来的问题就是潜在的内存问题。因为,如果返回出去的内存不释放,那么就是memory Leak。或者是被多次释放,从而造成程序的crash。这两个问题都相当的严重,所以这种设计方法并不推荐。(在一些Windows API中,当你调用了一些API后,你必需也要调用他的某些API来释放这块内存)

    2)让用户传入一块他自己的内存地址,而在函数中把要返回的内存放到这块内存中。这是一个目前普遍使用的方式。很多Windows API函数或是标准C函数都需要你传入一个buffer和这个buffer的长度。这种方式对我们来说应该是屡见不鲜了。这种方式的好处就是由函数外部的程序来维护这块内存,比较简显直观。但问题就是在使用上稍许有些麻烦。不过这种方式把犯错误的机率减到了最低。

    3)第三种方式显得比较另类,他利用了static的特性,static的栈内存一旦分配,那这块内存不会随着函数的返回而释放,而且,它是全局可见的(只要你有这块内存的地址)。所以,有一些函数使用了static的这个特性,即不用使用堆上的内存,也不需要用户传入一个buffer和其长度。从而,使用得自己的函数长得很漂亮,也很容易使用。

    这里,我想对第三个方法进行一些讨论。使用static内存这个方法看似不错,但是它有让你想象不到的陷阱。让我们来用一个实际发生的案例来举一个例子吧。



    示例

    有过socket编程经验的人一定知道一个函数叫:inet_ntoa,这个函数主要的功能是把一个数字型的IP地址转成字符串,这个函数的定义是这样的(注意它的返回值):

    char *inet_ntoa(struct in_addr in);

    显然,这个函数不会分配堆上的内存,而他又没有让你传一下字符串的buffer进入,那么他一定使用“返回static char[]”这种方法。在我们继续我们的讨论之前,让我们先了解一下IP地址相关的知识,下面是inet_ntoa这个函数需要传入的参数:(也许你会很奇怪,只有一个member的struct还要放在struct中干什么?这应该是为了程序日后的扩展性的考虑)

            struct in_addr {
                    unsigned long int s_addr;
            }

    对于IPV4来说,一个IP地址由四个8位的bit组成,其放在s_addr中,高位在后,这是为了方便网络传输。如果你得到的一个s_addr的整型值是:3776385196。那么,打开你的Windows计算器吧,看看它的二进制是什么?让我们从右到左,8位为一组(如下所示)。

    11100001   00010111    00010000    10101100

    再把每一组转成十进制,于是我们就得到:225   23   16   172, 于是IP地址就是 172.16.23.225。

    好了,言归正传。我们有这样一个程序,想记录网络包的源地址和目地地址,于是,我们有如下的代码:

        struct in_addr src, des;
        ........
        ........
        fprintf(fp, "源IP地址<%s>\t  目的IP地址<%s>\n", inet_ntoa(src),   inet_ntoa(des));

    会发生什么样的结果呢?你会发现记录到文件中的源IP地址和目的IP地址完全一样。这是什么问题呢?于是你开始调试你的程序,你发现src.s_addr和des.s_addr根本不一样(如下所示)。可为什么输出到文件的源和目的都是一样的?难道说是inet_ntoa的bug?

        src.s_addr = 3776385196;    //对应于172.16.23.225
        des.s_addr = 1678184620;  //对应于172.16.7.100

    原因就是inet_ntoa()“自作聪明”地把内部的static char[]返回了,而我们的程序正是踩中了这个陷阱。让我们来分析一下fprintf代码。在我们fprintf时,编译器先计算inet_ntoa(des),于是其返回一个字符串的地址,然后程序再去求inet_ntoa(src)表达式,又得到一个字符串的地址。这两个字符串的地址都是inet_ntoa()中那个static char[],显然是同一个地址,而第二次求src的IP时,这个值的des的IP地址内容必将被src的IP覆盖。所以,这两个表达式的字符串内存都是一样的了,此时,程序会调用fprintf把这两个字符串(其实是一个)输出到文件。所以,得到相同的结果也就不奇怪。

    仔细看一下inet_ntoa的man,我们可以看到这句话:The string is returned in a statically allocated buffer,  which  subsequent calls will overwrite. 证实了我们的分析。




    小结

    让我们大家都扪心自问一下,我们在写程序的过程当中是否使用了这种方法?这是一个比较危险,容易出错的方法。这种陷阱让人防不胜防。想想,如果你有这样的程序:

        if ( strcmp( inet_ntoa(ip1), inet_ntoa(ip2) )==0 ) {
            .... ....
       }

    本想判断一下两个IP地址是否一样,却不料掉入了那个陷阱——让这个条件表达式永真。

    这个事情告诉我们下面几个道理:

    1)慎用这种方式的设计。返回函数内部的static内存有很大的陷阱。
    2)如果一定要使用这种方式的话。你就必须严肃地告诉所有使用这个函数的人,千万不要在一个表达式中多次使用这个函数。而且,还要告诉他们,不copy函数返回的内存的内容,而只是保存返回的内存地址或是引用是没用的。不然的话,后果概不负责。
    3)C/C++是很危险的世界,如果你不清楚他的话。还是回火星去吧。

    附:看过Efftive C++的朋友一定知道其中有一个条款(item 23):不要试图返回对象的引用。这个条款中也对是否返回函数内部的static变量进行了讨论。结果也是持否定态度的。


    (转载时请注明作者和出处。未经许可,请勿用于商业用途)

    更多文章请访问我的Blog: http://blog.csdn.net/haoel

     

    发表于 @ 2006年11月16日 17:10:00|评论(loading...)|编辑

    新一篇: 从语句 char* p="test" 说起 | 旧一篇: “抄袭事件”判决书

    评论

    #Hover 发表于2006-11-17 10:03:00  IP: 222.212.196.*
    的确要慎用,否则,出错了会郁闷一万年!

    如果是为了效率,得不偿失!
    传一个参数进来,好处多多!


    #xreborner 发表于2006-11-17 12:43:00  IP: 202.120.224.*
    你真是菜鸟,返回个string或者用类似auto_ptr封装的东西就行了
    #uy 发表于2006-11-17 13:42:00  IP: 222.68.182.*
    丢人了,呵呵
    发现问题所在了
    #uy 发表于2006-11-17 13:42:00  IP: 222.68.182.*
    丢人了,呵呵
    发现问题所在了
    xiexie
    #uy 发表于2006-11-17 13:19:00  IP: 222.68.182.*
    不会吧,怎么我半天没看懂!!
    char *inet_ntoa (struct in_addr in)
    {
    #define UC(b) (((int)b)&0xff)
    static char b[18];
    char *p;

    p = (char *) &in;

    MEMSET(b, 0, 18);
    SPRINTF (b, "%d.%d.%d.%d", UC (p[0]), UC (p[1]), UC (p[2]), UC (p[3]));

    return (b);
    }
    #涔?Url= 发表于2006-11-17 14:19:00  IP: 219.143.47.*
    返回string或者vector还是比较好的
    #boost 发表于2006-11-17 11:00:00  IP: 222.212.201.*
    嗯,赞同,我曾经碰到过类似的问题,调试了半天才发现这个问题
    #陈皓 发表于2006-11-18 13:43:00  IP: 125.33.224.*
    To stefanie924: 看来,你还是没有看懂我这篇文章。你所给出的解决方案同样踩中了这个陷阱。
    #inwindcn 发表于2006-11-18 14:49:00  IP: 218.5.2.*
    std::string sSrcIP = inet_ntoa(src);
    std::string sDesIp = inet_ntoa(des);
    #stefanie924 发表于2006-11-18 13:25:00  IP: 58.47.143.*
    用两个临时变量保存,就可避免了吧
    char *srcIP = inet_ntoa(src);
    char *desIp = inet_ntoa(des);
    #lauxp(Platform::ASAP2) 发表于2006-11-18 17:33:00  IP: 221.220.126.*
    C++基本上关于这种做法是deprecated。C语言里面有太多这样的用法(称为irreentrant的)。确实是陷阱

    最近去了那个公司阿?嗬嗬

    Msn: lauxp1126_AT_hotmail.com
    #菜鸟 发表于2006-11-20 09:11:00  IP: 60.177.174.*
    何必这么麻烦,c++不是有引用的么
    #wuxing 发表于2006-11-30 22:47:00  IP: 61.135.152.*
    多来大哥这学习..
    #阿哲 发表于2006-12-01 20:40:00  IP: 218.83.64.*
    使用static局部变量的函数一般都存在着这种问题的,static局部变量保存了当前调用的“状态”,所以如果你下一次调用,原来的“状态”就被抹去了。
    #chai2010 发表于2007-01-18 14:46:06  IP:
    还有有一个类似的情况:

    printf("%c -> %c\n", getchar(), getchar());

    也会出现意想不到的问题!

    我个人的建议是,如果一个函数调用后会对其下一次
    调用的结果产生影响,那么这个函数最好不要出现在
    同一个表达式中多次!

    因此,设计函数的原则是只有一个入口!
    返回static地址的当然不是一个入口,getchar也不是,
    所以会出现上面的情况 。
    #halve 发表于2007-01-24 19:13:42  IP: 61.185.246.*
    不明白chai2010指的 getchar()不止一个入口是什么意思
    你所举的例子,在很多关于代码风格的书中都有明确的说明:
    函数调用时,参数表达式的求值顺序是不确定的,即第1个,第2个
    getchar()的运算顺序依赖于编译器的实现
    写这样的代码,当然会有问题
    #chai2010 发表于2007-01-26 14:14:55  IP: 218.106.120.*
    to halve

    一个入口是指:
    如果函数的参数相同,那么在任何情况下调用该函数得到的结果应该
    也是相同的(个人理解)!

    例如:
    int sum(int a, int b) { return a+b; }

    在任何时候调用sum(2,3)返回的结果都应该是5!因此我们可以说
    sum函数只有一个入口。

    但是getchar()呢???

    假设输入:abcd

    那么第一次调用返回a,第二次又成了b,...
    因此说getchar()的返回值不是只由函数本身决定的,
    它还受到其他因素(例如stdin)的影响。因此getchar()
    也就不是一个入口的!!

    因为getchar()不是一个入口,和调用的顺序有很大关系;
    而参数表达式的求值顺序又是不确定的;因此,在同一个
    函数参数中多次调用getchar()就可能带来灾难性的后果。
    #halve 发表于2007-01-26 16:16:47  IP: 61.185.246.*
    To chai2010
    多谢你的解释,大概是明白你所说的"一个入口"了,也能够明白你之前的留言的意思了,不过还有问题
    比如,某函数的参数是一个指向个内存单元的地址
    但这块内存单元的内容时刻在变化,只是这个要传入的地址保持不变,这样理解为相同可以吗?
    若理解为不同,那么似乎就又要扩大"一个入口"的内涵了,显然仅仅传入相同的地址还不行,还必须保证这个传入地址的内存单元内容要相同,或者才算"一个入口"?
    "一个入口"是一个好的设计思路,不过在你的解释下,我看不出有何优点。。。
    getchar()本身就是为了从不断变化的输入中读取数据的,他不是"一个入口"没有任何过错,如果要责怪"在同一个函数参数中多次调用getchar()就可能带来灾难性的后果。",为什么不去责怪标准委员会定义标准的时候,为何不把函数参数表达式的计算顺序规定死?我觉得这个责难倒是要理直气壮许多
    另外读取当前系统时间的函数,那可不是调用顺序不同而返回值不同,那根本就是调用的时间稍有不同,返回值就很可能就不同,真是个灾难性的函数。。。
    #kmlxk 发表于2007-01-30 11:15:30  IP: 222.180.184.*
    “C/C++是很危险的世界”

    -_-||| 两篇不同文章有相同的结论~叹~

    另一篇说的是STL string类的copy-on-write
    #kmlxk 发表于2007-01-30 11:16:11  IP: 222.180.184.*
    “C/C++是很危险的世界”

    -_-||| 两篇不同文章有相同的结论~叹~

    另一篇说的是STL string类的copy-on-write
    #zl 发表于2008-07-19 00:56:10  IP: 61.171.161.*
    为什么总把简单的问题复杂呢?其实函数内使用static变量,这个函数就不是线程安全的了。C++不难,不要吓走了一批人,以为C++到处是陷阱。
    发表评论  


    登录
    Csdn Blog version 3.1a
    Copyright © 陈皓