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

CC++返回内部静态成员的陷阱,第1张

CC++返回内部静态成员的陷阱,第2张

背景

在用C/C++开发的过程中,总有一个问题会带给我们苦恼。这个问题是函数内和函数外的代码需要通过内存块相互交互(比如函数返回一个字符串)。这个问题困扰着很多开发者。如果你的内存被分配在函数内部的堆栈上,那么这个内存将随着函数的返回而被堆栈释放。因此,必须返回一个在函数之外仍然有效的内存。

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

1)通过函数内部的malloc或new在堆上分配内存,然后返回这个内存(因为堆上分配的内存是全局可见的)。这种问题是潜在的记忆问题。因为,如果返回的内存没有被释放,那就是内存泄漏。或者是多次释放,导致程序崩溃。这两个问题都比较严重,不推荐这种设计方式。(在一些Windows API中,当你调用一些API的时候,也必须调用它们的一些API来释放这个内存。)

2)让用户传入一个自己的内存地址,在函数中把要返回的内存放在这个内存中。这是目前常用的方法。许多Windows API函数或标准C函数要求你传入一个缓冲区和这个缓冲区的长度。这种方式应该是我们常见的。这种方法的好处是由函数外的程序维护这种记忆,简单直观。但问题是使用起来有点麻烦。然而,这种方法最大限度地减少了出错的机会。

3)第三种方法截然不同。他利用了静电的特性。一旦静态的堆栈内存被分配,这个内存就不会随着函数的返回而被释放。而且,它是全局可见的(只要你有这个内存的地址)。所以有些函数利用了static的这个特性,就是不需要使用堆上的内存,也不需要用户传入一个缓冲区及其长度。所以自己用的功能都很漂亮,很好用。

在这里,我想讨论第三种方法。使用静态内存的方法看起来不错,但是它有你想象不到的陷阱。让我们以一个实际案例为例。

例子

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

char * inet _ ntoa(struct in _ addr in);

显然这个函数不会分配堆上的内存,他也没有让你传入字符串的缓冲区,所以他必须使用“返回静态char[]”的方法。在继续讨论之前,我们先了解一下IP地址。下面是函数inet_ntoa需要传入的参数:(也许你会惊讶,为什么一个只有一个成员的struct要放在struct里?这应该是为了程序将来的可伸缩性)

struct in _ addr {
unsigned long int s _ addr;
}

对于IPV4,一个IP地址由4个8位组成,放在s_addr中,最高位在后面,这是为了网络传输方便。如果你得到的一个s_addr的整数值是:3776385196。那么,打开你的Windows计算器,看看它的二进制是什么?我们从右到左设置8位(如下图)。

11100001 00010111 00010000 10101100

然后把每组转换成十进制,那么我们得到:225 23 16 172,那么IP地址就是172.16.23.225。

好了,言归正传。我们有一个程序想要记录网络数据包的源地址和目的地址,所以我们有下面的代码:

结构in_addr src,des
...
...
fprintf (FP,"源IP地址\t目的IP地址\ 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()“聪明地”返回了内部静态char[],而我们的程序正好踏入了这个陷阱。我们来分析一下fprintf代码。我们在fprintf的时候,编译器先计算inet_ntoa(des),所以返回一个字符串的地址,然后程序去找表达式inet_ntoa(src)得到一个字符串的地址。这两个字符串的地址是inet_ntoa()中的静态char[],显然是同一个地址。第二次计算src的IP时,该值的des的IP地址内容将被src的IP覆盖。所以这两个表达式的字符串内存是一样的。此时,程序会调用fprintf将这两个字符串(实际上是一个)输出到一个文件中。所以得到同样的结果也就不足为奇了。

仔细看inet_ntoa的man,可以看到这句话:字符串返回到一个静态分配的缓冲区,subsequence调用会覆盖这个缓冲区。这证实了我们的分析。

总结

大家都问问自己,我们在写程序的过程中有没有用过这种方法?这是一种危险且容易出错的方法。这种陷阱让人防不胜防。想想看,如果你有一个这样的程序:

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

本想判断两个IP地址是否相同,没想到却掉进了那个陷阱——让这个条件表达式永远为真。

这件事告诉我们以下几个道理:

1)以这种方式小心设计。返回函数内部的静态内存有一个很大的陷阱。

2)如果一定要用这个方法。你必须告诉所有认真使用这个函数的人,永远不要在一个表达式中多次使用这个函数。而且,告诉他们,保存返回的内存地址或引用,而不是复制函数返回的内存内容,是没有用的。否则后果概不负责。

3)C/C++是一个危险的世界,如果你不清楚的话。我们回火星吧。

P.S .看过eftive c++的朋友一定知道里面有一个子句(第23项):不要试图返回对象的引用。该子句还讨论了是否返回函数内部的静态变量。结果也是否定的。

位律师回复
DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
白度搜_经验知识百科全书 » CC++返回内部静态成员的陷阱

0条评论

发表评论

提供最优质的资源集合

立即查看 了解详情