« | October 2025 | » | 日 | 一 | 二 | 三 | 四 | 五 | 六 | | | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | | |
| 公告 |
戒除浮躁,读好书,交益友 |
Blog信息 |
blog名称:邢红瑞的blog 日志总数:523 评论数量:1142 留言数量:0 访问次数:9722089 建立时间:2004年12月20日 |

| |
[c++]从一道面试题看C/C++返回内部静态成员的陷阱  文章收藏, 软件技术
邢红瑞 发表于 2005/11/2 10:09:26 |
很久以前的一道面试题
int a=88,b=99; char c[10]; printf("%s %s\n",_itoa(a,c,10),_itoa(b,c,10));结果是 88 88 原因很简单c语言传递参数是从右向左传递,返回值保存在eax中。 在我们用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.225des.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函数返回的内存的内容,而只是保存返回的内存地址或是引用是没用的。不然的话,后果概不负责。
附:看过Efftive C++的朋友一定知道其中有一个条款(item 23):不要试图返回对象的引用。这个条款中也对是否返回函数内部的static变量进行了讨论。结果也是持否定态度的。以下是我的看法:简述: 将网络地址转换成“.”点隔的字符串格式。
char * inet_ntoa( struct in_addr in);
in:一个表示Internet主机地址的结构。
The inet_ntoa() function converts the Internet host address in given in network byte order to a string in standard numbers-and-dots notation. The string is returned in a statically allocated buffer, which subsequent calls will overwrite.
返回值: 若无错误发生,inet_ntoa()返回一个字符指针。否则的话,返回NVLL。其中的数据应在下一个WINDOWS套接口调用前复制出来。
参见: inet_addr().
测试代码如下include <stdio.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <string.h>int main(int aargc, char* argv[]){ struct in_addr addr1,addr2; ulong l1,l2; l1= inet_addr("192.168.0.74"); l2 = inet_addr("211.100.21.179"); memcpy(&addr1, &l1, 4); memcpy(&addr2, &l2, 4);
printf("%s : %s\n", inet_ntoa(addr1), inet_ntoa(addr2)); //注意这一句的运行结果
printf("%s\n", inet_ntoa(addr1)); printf("%s\n", inet_ntoa(addr2)); return 0;}实际运行结果如下:192.168.0.74 : 192.168.0.74 //从这里可以看出,printf里的inet_ntoa只运行了一次。192.168.0.74211.100.21.179
inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配的,所以inet_ntoa后面的调用会覆盖上一次的调用。第一句printf的结果只能说明在printf里面的可变参数的求值是从右到左的,仅此而已。gdb调试情况:(gdb) rStarting program: /home/wangyao/test/a.out
Breakpoint 1, process_tcp (argument=0x0, packet_header=0xbfd38718, packet_content=0x804c682 "") at t1.c:192192 printf("\n#### Get A Pattern Packet ####\n");(gdb) s
#### Get A Pattern Packet ####194 printf("%s\n",inet_ntoa(ip_protocol->ip_src_address));(gdb) p inet_ntoa(ip_protocol->ip_src_address)$1 = -1209189324(gdb) x -12091893240xb7ed3c34 <_res+564>: 0x2e313132(gdb) x/s -12091893240xb7ed3c34 <_res+564>: "211.94.144.100"(gdb) s211.94.144.100195 printf("%s\n",inet_ntoa(ip_protocol->ip_dst_address));(gdb) p inet_ntoa(ip_protocol->ip_dst_address)$2 = -1209189324(gdb) x/s -12091893240xb7ed3c34 <_res+564>: "173.26.100.34"(gdb) p ip_protocol->ip_dst_address$3 = {s_addr = 576985773}(gdb) p ip_protocol->ip_src_address$4 = {s_addr = 1687183059}(gdb) s173.26.100.34196 printf("%s:%d--->%s:%d\n",inet_ntoa(ip_protocol->ip_src_address),ntohs(tcp_protocol->tcp_src_port),inet_ntoa(ip_protocol->ip_dst_address),ntohs(tcp_protocol->tcp_dst_port));(gdb) x/s inet_ntoa(ip_protocol->ip_src_address)0xb7ed3c34 <_res+564>: "211.94.144.100"(gdb) x/s inet_ntoa(ip_protocol->ip_dst_address)0xb7ed3c34 <_res+564>: "173.26.100.34"(gdb) s211.94.144.100:80--->211.94.144.100:49983(gdb) q
从上面可以看出,inet_ntoa返回的char*地址是一样的,这样的话,在最后的printf部分就会产生数据的覆盖问题。inet_ntoa是不可重入函数啊。。。里面用了静态变量。建议用inet_ntop来代替。这种程序多线程是有问题,这是msdn分割字符串的一段函数#include <string.h>#include <stdio.h>
char string[] = "A string\tof ,,tokens\nand some more tokens";char seps[] = " ,\t\n";char *token;
void main( void ){ printf( "%s\n\nTokens:\n", string ); /* Establish string and get the first token: */ token = strtok( string, seps ); while( token != NULL ) { /* While there are tokens in "string" */ printf( " %s\n", token ); /* Get next token: */ token = strtok( NULL, seps ); }}微软说的很好:
Warning Each of these functions uses a static variable for parsing the string into tokens. If multiple or simultaneous calls are made to the same function, a high potential for data corruption and inaccurate results exists.
Therefore, do not attempt to call the same function simultaneously for different strings and be aware of calling one of these functions from within a loop where another routine may be called that uses the same function. However, calling this function simultaneously from multiple threads does not have undesirable effects. |
|
|