1.C语言和C程序

C语言(1.C语言和C程序)
目录二、程序环境 3. 运行环境三、预处理 3. \#. 条件编译 5. \# 6. 其他预处理指令
一、C语言概述
? C语言:一门通用的计算机编程语言
1. C语言的发展历程
二进制指令:可以直接被计算机识别的机器语言,是由0、1组成的代码汇编语言:将机器语言使用助记符来封装,形成汇编语言B语言:对汇编语言进行封装,形成高级语言C语言:对汇编语言进行封装 , 形成高级语言 2. C语言的标准
? 由于互联网各厂对C语言的使用和修改 , 导致C语言的通用性越来越差,国际标准化组织将C语言进行标准化 。常用的标准为C11,C89,C99 , C90 。
? ANSI C为最初的标准,以后经常能看到这个标准
3. C语言结构
? C语言一共有32个关键字,但是C99标准后有增加了5个关键字,目前主流编译器对C99的支持并不好,所以在这里我们默认使用C90的标准 , 即只使用32个关键字
4. 注释
? 注释:用于对代码进行解释说明的,注释不参与编译过程,也不是代码的一部分
? C语言标准定义的注释格式如下:
/* 注释内容 */
? 注意:该注释不能嵌套
? 在我们常见的代码中还见过行注释:
//注释内容
? 行注释是C++的语法,在C语言标准里未定义,但是绝大多数编译器都支持行注释,且可以嵌套
二、程序环境
? C语言的实现中有两种环境,分别是翻译环境和执行环境
1. C程序结构可执行文件:经过编译链接后的可以直接运行的二进制文件,也是我们需要的最终结果2. 翻译环境
? 源代码转换成可执行程序要经过2个步骤:编译 + 链接
? 编译又可以细分为3个步骤:预编译 + 编译 + 汇编
以Linux系统下的gcc编译器演示编译过程,将源文件test.c 和add.c进行编译和链接:
2.1 预编译/预处理2.2 编译2.3 汇编
作用:将汇编代码转换成二进制指令
将汇编转换成二进制指令形成符号表:对于每个文件,都对其全局符号申请空间 , 并将其对应的内存地址存储在符号表中 定义的标识符:符号表中存储其真实内存地址声明的标识符:符号表中存储无效的地址,由编译器指定无效地址2.4 链接
作用:将多个 .o文件 和 链接库 链接成一个可执行文件
合并段表:就是将多个.o文件按照特定内容进行合并
合并符号表并重定向:将多个.o文件的符号表进行合并
声明的标识符中无效地址会被该标识符的真实内存地址取代
3. 运行环境 3.1 C程序内存
C程序的内存分布,只介绍几个简单的,笼统的说一下
3.2 程序执行过程 程序必须载入内存中 在有操作系统的环境中,一般由操作系统完成在独立的环境中,必须由手工安排,也可以是通过可执行代码置入只读内存来完成载入 程序执行开始,调用main函数开始执行程序代码 。此时程序使用一个运行时堆栈(stack),存储局部变量和返回地址 。程序同时也使用一个静态内存(),该内存中的变量在整个程序执行时一直存在并且不能修改值终止程序 。正常终止main函数,也可能是意外终止 三、预处理 1. 预定义符号
? C语言中有些存在的预定义符号可以供我们使用,它们都是库中用#定义的符号
__FILE__ //源文件路径,字符串__LINE__ //当前行号,int__DATE__ //编译日期,字符串__TIME__ //编译时间,字符串__STDC__ //标准C,遵循ANSI C , 其值为1 , 否则未定义
? 不遵循ANSI,gcc遵循,值为1
2. # 2.1 定义标识符
#define NAME stuff
注意:
标识符建议全部为大写,用"_"来分隔单词定义标识符时,建议后面不要加分号,容易导致问题如果stuff太长,可以分行写,每行后加一个反斜杠做续行符(最后一行不用续行符)
? 举个栗子:
#define MAX 1000//将MAX定义为整型常量1000#define REG register//将register标识符定义为更简洁的标识符REG#define DO_FORVER for(;;)//将一段语句定义为一个标识符#define BEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n," \ //使用\做续行符__FILE__, __LEIN__, __DATE__, __TIME__)
2.2 定义宏
? 宏(macro):#允许把参数替换到文本中,这种实现称为宏或定义宏
? 如:定义一个宏,参数为x,替换内容为x*x
#define SQUARE(x) x*x
? 使用,并传递参数5
SQUARE(5);
? 预编译期间,源代码的此语句就被替换为
5*5;
注意:
1. 宏中的每个参数和表达式建议带上小括号,为避免替换后运算的优先级跟预想的不一样1. 使用宏时,不要传入带副作用的参数,否则结果是难以预测的
? 如:1. 使用宏计算5+1的平方,并加上2,结果却为13,错误
#define SQUARE(x) x*x //宏定义SQUARE(5+1) + 2;//使用该宏5+1*5+1 + 2;//替换后的式子
? 将宏的参数加上小括号后,计算结果为38,正确
#define SQUARE(x) ((x)*(x))//加了括号的宏定义SQUARE(5+1) + 2;//使用该宏((5+1)*(5+1)) + 2;//替换后的式子
? 如:2. 将a++和b++作为参数传入一下宏
#define MAX(A, B) (((A)>(B)) ? (A) : (B)) //宏定义int a=5, b=8;int c = MAX(a++, b++);//宏调用int c = (((a++)>(b++)) ? (a++) : (b++)); //替换后的式子 , 结果为a=6,b=10,c=9
2.3 替换规则
? 在程序预编译时,会对#定义的宏或标识符进行替换,步骤如下
调用宏时,首先检查参数,如果参数包含由#定义的符号,则它被首先替换将替换文本直接替换到该标识符出现的位置,对于宏还有参数的替换最后对文件进行检查,如果还有#定义的符号,则重复以上步骤
注意:
宏参数中可以出现其他#定义的符号,但是对于宏不能递归定义预处理器搜索#时,字符串常量中出现该符号时,则不被替换,如:char str[] = "(5)"; 2.4 # 和
? 在宏的参数中使用该符号
? #参数名:将该参数加上 “” 替换到替换文本中
//宏定义#define PRINT(FORMAT, VALUE) printf("the value of "#VALUE" is "FORMAT"\n", VALUE)int a = 10;PRINT("%d", a); //宏调用//替换后的式子,VALUE替换为a , #VALUE替换为"a"printf("the value of ""a"" is ""%d""\n", 10);
? 参数名##参数名:将两边的符号合成一个符号
//宏定义#define ADD_TO_SUM(NUM, VALUE) sum##NUM += valueint sum5 = 10;ADD_TO_SUM(5, 10); //调用宏sum5 += 10;//替换后的式子,sum和5拼接 , 合成sum5
注意:这样的连接必须产生一个合法的标识符,否则就是未定义的
2.6 宏和函数对比
宏的优点:
宏的速度比函数快 。宏是在编译期直接进行替换,源文件直接编译链接为可执行文件进行运行函数是在运行期间对参数进行复制 , 创建函数栈帧进行运行,运行完销毁在栈中的内存 宏是类型无关的 宏是直接替换,不受类型检查函数要对参数进行检查,参数必须要相互对应
宏的缺点:
每次调用宏时,都会直接将替换文本替换到标识符位置 , 多次调用或替换文本内容很多时,将大大增加程序的长度宏是没办法调试的宏的类型无关,导致不够严谨宏可能会带来算术优先级问题
宏和函数的区别:
属性#定义宏函数
代码长度
每次调用都会替换 , 除非宏小,否则程序长度大幅增加
函数只出现在一个地方,每次调用函数都是调用这一个地方的代码
执行速度
更快
存在调用和返回的开销,相对慢一点
操作符优先级
直接替换容易产生优先级问题,建议书写时使用括号
将参数直接拷贝,对参数进行操作 , 操作符优先级问题不容产生
带有副作用的参数
直接替换导致每次使用参数都会产生副作用,容易产生不可预料的后果,所以不要使用带副作用的参数
形参只在传值的时候求一次值,结果更容易控制
参数类型
【1.C语言和C程序】参数类型无关,只要操作数合法 , 都能使用
参数类型必须一一对应
调试
宏不方便调试
函数可以逐语句调试
递归
不能递归
可以递归
3. #undef
? 用于移除一个宏定义
#undef NAME //移除NAME符号的定义
注意:一个符号要被重新定义,那么它要先移除
4. 条件编译
? 在编译一个程序时,我们可以使用条件编译语句来决定内容是否被编译
4.1 常见的条件编译指令 判断表达式结果,决定是否编译,常量表达式为0时不编译
#if 常量表达式//...#endif
多分支 , 跟if \- else if使用方式一样
#if 常量表达式1//...#elif 常量表达式2//...#elif 常量表达式3//...#else//...#endif
判断是否被定义
? 判断该符号定义时,执行编译,有两种语句
//第一种,用#if判断defined中的符号#if defined(symbol)//...#endif//第二种,直接用#ifdef判断符号#ifdef symbol//...#endif
? 判断该符号未定义时,执行编译,也是两种语句,并且跟上面两种对应
//第一种 , 用!对defined取反#if !defined(symbol)//...#endif//第二种,用#ifndef判断符号 , 中间多个n哦#ifndef symbol//...#endif
嵌套条件指令
? 跟if语句使用一样,可以嵌套使用
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif#endif
4.2 使用风格 每一个#if,#ifdef,# 都有一个#endif作为结束标志 , 并且匹配最近的判断每一个#endif后面建议加上注释,用来标识是由哪个符号判断的结束指令理论上条件指令只要结果为假,内容不会被编译,但是并不建议使用它来当注释使用 注释是用来提醒该代码的功能,对于一些不重要的内容程序员可能并不阅读条件指令是代码的一部分,若是当注释来使用 , 则程序员不得不关注其内容
//使用注释标识#if指令的结束标志#if __DEBUG__//...#endif //__DEBUG__
5. #
? #指令包含头文件 , 预处理器先删除这条指令,在将头文件中的内容粘贴到源文件中
5.1 包含方式 本地文件包含
#include "filename.h"
? 查找策略:先在源文件目录下查找,如果该头文件未找到 , 则在库函数头文件下查找,若是还没有则编译错误
库文件包含
#include
? 查找策略:直接在库函数头文件下查早,若是没有则编译错误
嵌套文件包含
? 如:test.h中包含了add.h,add.h中包含了stdio.h , test.h中嵌套包含了stdio.h
? 若是头文件很多,并且各自有很多引用,则容易发生头文件的重复包含
注意:
使用""和包含头文件的区别是查找策略不同""可以用来包含库文件,但是不建议 使用双引号包含库文件会使查找效率减低不容易区分库文件和本地文件,增加代码的阅读难度5.1 重复引用
? 一个头文件被包含了10次,那么源文件中就有10个该头文件的内容,也会被编译10次,大大增加了代码长度,所以头文件被重复包含是非常糟糕的
? 解决重复引用有两种方式
使用条件编译指令
? 如果没有被定义 , 则定义该符号,并完成头文件的编译,否则不对条件指令内的内容进行编译
#ifndef __TEST_H__#define __TEST_H__//头文件内容#endif //__TEST_H__
使用# once
? 在头文件前加上该指令
#pragma once
6. 其他预处理指令
#error#pragma#line...不做介绍,自己了解#pragma pack()在结构体已介绍