`

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

阅读更多

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

分享到:
评论

相关推荐

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

    它没有使用堆内存分配,也没有要求用户提供缓冲区,而是返回内部静态`char[]`。然而,这种方法隐藏了一个陷阱:由于静态内存是全局共享的,当多个调用者同时或顺序使用`inet_ntoa`时,它们可能会相互覆盖内存,导致...

    C/C++编程指南

    ### C/C++编程指南知识点概览 #### 一、文档结构 - **版权与版本声明**:明确了文档的版权信息及版本控制策略。 - **头文件结构**:讲解了头文件的标准格式,包括宏定义、预处理器指令等,确保代码的一致性和可维护...

    C,C++经典推荐博文汇总

    #### C/C++返回内部静态成员的陷阱 在C++中,从函数返回内部静态成员的引用或指针时,需要注意生命周期管理,避免返回已销毁的对象引用,造成未定义行为。 #### C语言中sizeof()的用法 `sizeof`运算符用于获取变量...

    30天精通C++学习C++的不二选择

    - **静态数据成员**和**静态成员函数**属于整个类而不是特定的对象实例。它们可以被所有对象共享,有助于节省内存和提高效率。 ##### 10. 入门教程:实例详解C++友元 - **友元**是一种特殊的机制,允许非成员函数或...

    高质量C C++编程-C++/C编程风格

    【高质量C++/C编程风格】是编程领域中一种重要的规范,它关乎代码的可读性、可维护性和团队协作的效率。以下是对标题和描述中提到的知识点的详细阐述: 1. **文件结构** - **头文件与定义文件**:C++/C程序通常...

    高效C++:从C到C++

    ### 高效C++:从C到C++ #### 第一章:从C转向C++ **条款1:尽量用const和inline而不用#define** - **背景**:在C语言中,`#define`宏定义被广泛用于常量的定义。然而,在C++中,我们有更好的替代方案——`const`...

    嵌入式C精华

    - **C++代码与C代码混合编译**: 如果C++代码中包含了C代码,或者需要将C++代码导出为C语言能够调用的函数,则需要使用 `extern "C"`。 3. **具体用法** ```cpp extern "C" { void myFunction(); // 这里的函数...

    c++面试经典题目大会

    例如,使用静态类型转换(`static_cast`)、显式类型转换(`reinterpret_cast`)和C风格的类型转换(`(type)`)等。每种类型转换都有其特定的用途和限制。 #### 7. 引用与指针的区别 引用和指针都可以用来间接访问...

    C++点滴(初学者)

    #### 一、C/C++基础 **1.1 什么是变量** 在C++中,变量是用来存储数据的一种基本单位。它由标识符(变量名)、类型以及其所存储的值组成。例如,`int age = 25;` 这里 `age` 就是一个整型变量,存储的值为25。 **...

    C++编程规范

    2. 尽量避免使用C风格的指针,使用C++的引用代替。 3. 使用const关键字来表明对象的不可变性。 六、模板与泛型编程 1. 模板应封装在头文件中,因为模板的实例化发生在编译时。 2. 避免使用模板特化,除非必要,因为...

    C++ 面试题

    在C++面试中,了解语言的基本特性和常见陷阱至关重要。以下是一些从题目中提炼出的关键知识点: 1. 字符串常量与指针比较: - 当比较两个字符串数组如`str1`和`str2`时,由于它们都是在栈上创建的局部变量,且内容...

    C语言入门经典(第4版)--源代码及课后练习答案

    9.2.1 静态变量:函数内部的追踪 334 9.2.2 在函数之间共享变量 336 9.3 调用自己的函数:递归 338 9.4 变元个数可变的函数 341 9.4.1 复制va_list 344 9.4.2 长度可变的变元列表的基本规则 344 9.5 main()...

    C++ 经典面试题()

    - `virtual` 只能用于非静态成员函数,并且不能用于构造函数和析构函数。 - **`virtual` 与访问控制符**: - 尽管 `virtual` 关键字可以与 `public`、`protected` 和 `private` 这些访问控制符一起使用,但在实际...

    go语言实战

    Go语言设计之初的目标是为了提高程序员的工作效率,同时解决C++等传统编程语言中存在的一些问题。Go语言具有简洁的语法、强大的标准库以及高效的并发模型等特点,因此在云原生、微服务架构、网络编程等领域得到了...

Global site tag (gtag.js) - Google Analytics