重新认识C语言

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)

需要使用‘\’分隔符对语句进行换行。

时间: 2024-10-14 08:57:45

重新认识C语言的相关文章

使用R语言计算均值,方差等

R语言对于数值计算很方便,最近用到了计算方差,标准差的功能,特记录. 数据准备 height <- c(6.00, 5.92, 5.58, 5.92) 1 计算均值 mean(height) [1] 5.855 2 计算中位数 median(height) [1] 5.92 3 计算标准差 sd(height) [1] 0.1871719 4 计算方差 var(height) [1] 0.03503333 5 计算两个变量之间的相关系数 cor(height,log(height)) [1] 0

GCC在C语言中内嵌汇编 asm __volatile__ 【转】

转自:http://blog.csdn.net/pbymw8iwm/article/details/8227839 在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C 变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可, GCC会自动插入代码完成必要的操作. 1.简单的内嵌汇编 例: __asm__ __volatile__("hlt"); "__asm__"表示后面的

C语言轻松高效学习方法之:多种方法实现

多种方法实现同一个功能,可以调动你学的所有知识去做,有助于你学的融会贯通. 下面举例来看: 实现功能:求一个整数的位数: 实现语言:C语言: 开发环境:Visual Studio 2017 如:3215是4位数 实现原理: 3215/10 = 321 ----1位数 321/10 = 32 ----又是1位数 32/10 = 3 ----又是1位数 3/10 = 0 ----又是1位数 共4位数,且终止计算条件是/10结果为0的时候: 根据这个原理,先写一个最笨的原始方法: 效果: 这种实现方案

轻松学习C语言编程的秘诀:总结+灵感

目前在准备一套C语言的学习教程,所以我这里就以C语言编程的学习来讲.注意,讲的是"轻松学习",那种不注重方法,拼命玩命的方式也有其效果,但不是我提倡的.我讲究的是在方式方法对头.适合你.减轻你学习负担和心里压力的前提下,才适当的抓紧时间. 因此,探索一种很好的学习方法就是我所研究的主要内容. 众所周知,学习C语言并非易事,要学好它更是难上加难.这和你期末考试背会几个题目的答案考上满分没多大关系,也就是说你考试满分也说明不了你学好.学精通了C语言.那么怎么才算学精通C语言?闭着眼睛对自己

详解go语言的array和slice 【二】

上一篇  详解go语言的array和slice [一]已经讲解过,array和slice的一些基本用法,使用array和slice时需要注意的地方,特别是slice需要注意的地方比较多.上一篇的最后讲解到创建新的slice时使用第三个索引来限制slice的容量,在操作新slice时,如果新slice的容量大于长度时,添加新元素依然后使源的相应元素改变.这一篇里我会讲解到如何避免这些问题,以及迭代.和做为方法参数方面的知识点. slice的长度和容量设置为同一个值 如果在创建新的slice时我们把

自动生成小学四则运算题目(C语言)

这个简易四则运算是我在百度上找的博主叫53余雅诗的一篇c语言代码,网址为http://www.cnblogs.com/ys1101/p/4368103.html,功能是可以选择加减乘除进行简易的四则运算,判断对错.我在VS2017上编译没有bug,因为功能只有整数运算,所以我在此基础上加了真分数的四则运算以及统计得分等,最后成功运行程序.我把我的源代码放在github上,地址为https://github.com/xiaofancheng/helloworld.

PAT 1009 说反话 C语言

给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出. 输入格式:测试输入包含一个测试用例,在一行内给出总长度不超过80的字符串.字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用1个空格分开,输入保证句子末尾没有多余的空格. 输出格式:每个测试用例的输出占一行,输出倒序后的句子. 输入样例: Hello World Here I Come 输出样例: Come I Here World Hello 1 #include<stdio.h> 2 #

PAT 1006 换个格式输出 C语言

让我们用字母B来表示"百".字母S表示"十",用"12...n"来表示个位数字n(<10),换个格式来输出任一个不超过3位的正整数.例如234应该被输出为BBSSS1234,因为它有2个"百".3个"十".以及个位的4. 输入格式:每个测试输入包含1个测试用例,给出正整数n(<1000). 输出格式:每个测试用例的输出占一行,用规定的格式输出n. 输入样例1: 234 输出样例1: BBSSS1

Go语言 IDE之Gogland配置使用

Gogland 是 JetBrains 公司推出的 Go 语言集成开发环境.Gogland 同样基于 IntelliJ 平台开发,支持 JetBrains 的插件体系.目前正式版尚未发布.官方:https://www.jetbrains.com/go/.关于使用,即将开始咯! 一.安装Golang 1) 首先到https://golang.org/dl/选择适合你系统的安装包,(墙内:http://golangtc.com/download). 2)下载完成安装到指定目录即可.我这里是(D:\G

R语言快速上手入门

R语言快速上手入门 课程学习网址:http://www.xuetuwuyou.com/course/196 课程出自学途无忧网:http://www.xuetuwuyou.com 课程简介 本教程深入浅出地讲解如何使用R语言玩转数据.课程中涵盖R语言编程的方方面面,内容涉及R对象的类型.R的记号体系和环境系统.自定义函数.if else语句.for循环.S3类R的包系统以及调试工具等.本课程还通过示例演示如何进行向量化编程,从而对代码进行提速并尽可能地发挥R的潜能.本课程适合立志成为数据科学家的