1.缘起
接触C语言有三四年时间了,工作中也一直使用C语言。但对于一些C语言的特性和定义还存在一些疑问,这里总结一下,作为以后参考。
2.C语言的链接属性
工作中无意发现了C语言一个有趣的问题,在两个源文件中定义了同一个未初始化的变量,编译器竟然不报错,但是如果在其中一个文件中定义并初始化,那就会报错。我测试使用的代码如下(测试环境window7(32位)gcc 4.5.0):
main.c:
/* main.c */ #include <stdio.h> char G_TestValue; int G_TestValue2; void PrintTestValue(void); int main(int argc,char *argv[]) { PrintTestValue(); printf("main:\t\t(&G_TestValue)=0x%08x\n\t\t" "(G_TestValue)=%d\n\t\tsizeof((G_TestValue))=%d\n" "\t\tG_TestValue2=%d\n" "\t\t&G_TestValue2=0x%08x\n" ,&(G_TestValue),G_TestValue, sizeof(G_TestValue),G_TestValue2,&G_TestValue2); return 0; }
global_test.c:
/* global_test.c*/ #include <stdio.h> typedef struct test_struct_t{ char a; int b; } TestStruct_t; TestStruct_t G_TestValue = {2,8}; int G_TestValue2; void PrintTestValue(void) { printf("PrintTestValue:\t(&G_TestValue)=0x%08x\n\t\t" "(G_TestValue.a)=%d\n\t\tsizeof(G_TestValue)=%d\n" "\t\tG_TestValue2=%d\n" "\t\t&G_TestValue2=0x%08x\n" ,&(G_TestValue), G_TestValue.a, sizeof(G_TestValue),G_TestValue2,&G_TestValue2); printf("-----------------------------------------\n"); }
Makefile:
test: global_test.o main.o gcc -o test global_test.o main.o -std=gnu99 global_test.o: global_test.c gcc -c global_test.c -std=gnu99 main.o: main.c gcc -c main.c -std=gnu99 clean: rm *.o test
代码执行结果如下:
根据上述代码的分析,我们发现这样一个现象,在main.c文件里定义了char型变量G_TestValue但未初始化,在global_test.c文件里定义了结构体类型的变量G_TestValue同时并初始化了,两个文件里都定义了同一个全局变量,一个未初始化,一个初始化了。编译上述程序时并未报错,运行结果发G_TestValue的地址是一样的(本人实验环境下是(&G_TestValue)=0x00402000),并且在main.c中打印出的G_TestValue的值为2(即在global_test.c文件里定义并初始化的值).这也就说明C语言链接器链接时,为这个变量只分配了一个存储空间,如果两次定义同一个变量名称的类型不一样,以占用空间大的那一个来分配空间。另外我使用G_TestValue2做了验证,在两个文件都定义了并且都没有初始化,从打印结果看,他们是同一个地址。
为什么会这样呐?
这涉及到C编译器对多重定义的全局符号的解析和链接。在编译阶段,编译器将全局符号信息隐含地编码在可重定位目标文件的符号表里。这里有个“强符号(strong)”和“弱符号(weak)”的概念——前者指的是定义并且初始化了的变量,比如global_test.c里的结构体G_TestValue,后者指的是未定义或者定义但未初始化的变量,比如main.c里的整型G_TestValue和G_TestValue2,当符号被多重定义时,GNU链接器(ld)使用以下规则决议:
- 不允许出现多个相同强符号。
- 如果有一个强符号和多个弱符号,则选择强符号。
- 如果有多个弱符号,那么先决议到size最大的那个,如果同样大小,则按照链接顺序选择第一个。
像上面这个例子中,全局变量G_TestValue存在重复定义。如果我们将main.c中的b初始化赋值,那么就存在两个强符号而违反了规则一,编译器报错。如果满足规则二,则仅仅提出警告,实际运行时决议的是global_test.c中的强符号。而变量global_test2都是弱符号,所以只选择一个(按照目标文件链接时的顺序)。
关于C语言的链接属性最权威的解释是在ISO/IEC 9899 Programming languages — C(见文章附件)的6.2.2章节,有兴趣的哥们可以下载下来看看。另外推荐一篇关于C语言链接属性描述不错的文章:C语言中标识符的作用域、命名空间、链接属性、生命周期、存储类型。
具体关于linker相关的知识,比较系统的可以参阅:linker and loader。
3.malloc的申请空间、及释放空间
有一天上班,想到这样一个问题:定义了一个结构体类型,成员变量有一个指针行变量。定义一个结构体类型的变量,为其分配空间,然后再给成员变量分配空间,释放结构体变量时,是否释放了结构体成员变量申请的空间?还是需要单独进行释放?
为了测试这种情况,写了代码想验证一下:
/* main.c */ #include <stdio.h> #include <stdlib.h> typedef struct malloc_test_tag { char *str; int n; }MallocTest_t ,*P_MallocTest_t; int main(int argc,char *argv[]) { P_MallocTest_t test,saved_test; printf("malloc for test\n"); test = (P_MallocTest_t) malloc(sizeof(MallocTest_t)); if (NULL == test ) { exit (1); } printf("the address of (test) = 0x%x\n",test); saved_test = test; printf("malloc for test->str\n"); test->str = (char *)malloc(6); if (NULL == test->str) { exit (1); } printf("the address of (test->str[0]) = 0x%x\n",test->str); printf("-----------------------------------------\n"); printf("free for test->str\n"); free(test->str); printf("free for test\n"); free(test); test = NULL; printf("malloc for test again!!!\n"); test = (P_MallocTest_t) malloc(sizeof(MallocTest_t)); if (NULL == test ) { exit (1); } printf("the address of (test) = 0x%x\n",test); printf("malloc for test->str again!!!!\n"); test->str = (char *)malloc(6); if (NULL == test->str) { exit (1); } printf("the address of (test->str[0]) = 0x%x\n",test->str); printf("free for test->str 2\n"); free(saved_test->str); saved_test->str = NULL; printf("free for test 2\n"); free(test); test = NULL; return 0; }
测试思路是这样,根据malloc基本原理,申请后接着再申请,申请的是上次释放的空间。我先申请test所需要的结构体空间,再接着申请结构体成员变量test->str的空间,然后释放test->str的空间,释放test空间,再接着申请上述两个空间。执行结果是这样的:
可以看到两次申请的test->str空间是一样的,都是the address of (test->str[0]) = 0x4c0ed0。
然后,我在试验第一次申请test->str空间后不释放,在第二次申请中重新申请空间。代码执行情况如下:
可以看到两次申请的test->str空间是不一样的,一个是the address of (test->str[0]) = 0x4c0ed0,一个是the address of (test->str[0]) = 0x570ee0。
从上面的实验我们可以看到,使用malloc、free管理内存空间时,malloc和free为基本单元的,你使用malloc申请多大的空间,就应该在使用完毕后,free多大的空间。向上面的例子中,你在结构体内又申请的空间,需要单独释放。个人猜想,malloc和free的基本实现:系统管理着一段内存空间(堆),你使用malloc申请的时候,系统会记住你申请空间首地址,你申请的大小等信息,当你free时会根据你要释放的内存地址,然后查询你申请空间时系统记录的大小等其他信息,进行释放操作。因此,你释放malloc申请的空间时,传的地址参数应该是你申请空间得到的那个地址,否则free函数可能执行失败。
4.C语言变长数组
C99规范里规定了可以使用变长数组,只是知道,但实际项目中没用过。我写了以下代码:
/* main.c */ #include <stdio.h> #include <stdlib.h> int n = 10; int test_array[n] = {1,2,3,4,5,}; int main(int argc,char *argv[]) { printf("the value test_array[0] is %d\n",test_array[0]); return 0; }
但编译的时候给了报了一堆错误:
其中第一句 error:定义了一个变长数组在文件作用域。咦,奇怪,C99不是支持定义变长数组吗?我的编译器是指定使用C99标准编译的呀!
后来我将
int n = 10;
int test_array[n] = {1,2,3,4,5,};
挪到了函数内部,就没有错误了。后来查阅ISO/IEC 9899 Programming languages — C得知,C99不支持文件作用域的变长数组定义和使用。并且变长数组不支持初始化。只能定义好变量后赋值。了然了之后,做了以下测试:
/* main.c */ #include <stdio.h> #include <stdlib.h> void test(int n) { char test_array[n]; printf("the size of test_array is %d\n",sizeof(test_array)); } int main(int argc,char *argv[]) { int n = 10; int test_array[n]; test_array[0] = 10; printf("the value test_array[0] is %d\n",test_array[0]); test(4); test(5); return 0; }
看来C99支持的这个变长数组,还是挺有用的哈。
5.C语言长语句分割、换行
写代码时一个语句太长,C有用支持直接分割吗?
printf("PrintTestValue:\t(&G_TestValue)=0x%08x\n\t\t" "(G_TestValue.a)=%d\n\t\tsizeof(G_TestValue)=%d\n" "\t\tG_TestValue2=%d\n" "\t\t&G_TestValue2=0x%08x\n" ,&(G_TestValue), G_TestValue.a, sizeof(G_TestValue),G_TestValue2,&G_TestValue2);
C语言的语句分割符号是分号,空格和换行会被解析器忽略掉,所以一般语句可以分开多行书写。
像上面的printf函数,前面的格式化语句需要在每行都加上双引号。
还有宏定义时比较特殊:
#define SAFE_DELETE(p) do { delete p; p = NULL; } while(0)
需要使用‘\’分隔符对语句进行换行。