【C】 02 - 程序结构和预处理

  在正式进入C的语法之前,有必要对其整体外观和组成元素作一个浏览。这部分内容对大多数人是比较陌生的,但它们却是C的起点和骨架。而这些内容涉及的背景或细节又可以展开为专门的课题,这里也只是浅尝则止,说明个大概即可。

1. C程序组成

  任何一个程序都首先以源文件(source file)的形式存在,它是一个普通的文本文件。C程序一般由一系列后缀为.c和.h的文件组成,前者包含了程序的执行内容,后者包含了各种声明或定义。其实文件名并不重要,这样的后缀名仅是约定俗成的习惯。但建议保持这样的风格,一是为了看程序的人能一目了然,二是在集成开发环境(IDE)里它们已经成为C文件的标识。

  文本文件有许多字符组成,这些字符的编码方法由编辑器决定。对C编译器有意义的是它们所表示的字符而非编码本身,预处理开始前会将这些字符映射成source character set(一般是UTF-8)。预处理就是在该字符集下进行的,预处理后还会将字符和字符串映射成execution character set,它由目标平台决定,但一般和前者相同。

  这两种字符集都包含base character set,它就是我们正常使用英文和符号(编码在两个字符集中相同),字母是区分大小写的。新标准还支持extended character set,它可出现在两种字符集中。比如在source字符集中可在多处使用unicode:identifier、char constant、string literal、headfile name、comment、preprocessing token。示例如下(需编译器支持或打开开关),但不建议这样的编码风格。

// Define variable α
wchar_t \u03B1 = L‘α‘;

  C语言的编译是以translation unit为单元的,它是预处理后的.c文件。各单元的编译互不相干,连接器最终会把它们和库一起整合成执行文件。关于编译、连接和调试,我打算另开课题,这里不深入讨论。以下是一个多文件程序的常见错误,但在编译连接时并不会报错,因为无法跨unit检查语法。运行时文件2中会把数组a的元素当地址使用,出现错误。

// File 1
int a[3];

// File 2
extern int *a;  // should be a[]

  C程序可能独立运行(嵌入式),也可能运行在操作系统中。C规范对这两种情况的要求稍微有点不同,分别叫freestanding implementation和hosted implementation。其中后者要求实现更多的库,而且必须有一个main函数。而前者只需要少数必要的库,程序入口不作规定(但建议也用main)。main函数可有以下两种形式,对第二种形式,规范要求argv[0]为程序名,argv[argc] = NULL。

int main(void);
int main(int argc, char* argv[]);  // or char** argv

  程序运行时,内存中除了常量区(代码和字符串)、数据区外还会有堆和栈区。一般栈底在高地址,向低地址增长。hosted程序的地址一般是逻辑地址,运行时由OS负责映射为物理地址。

2. 预处理步骤

  第0步,字符集映射。将源程序文本文件的字符映射为source字符集,甚至包括将换行符的统一编码。C还要求每行都以换行符结束,如果文件尾没有换行,编译器会warning(需打开)。

  第1步,trigraph sequance。为支持某些古老的键盘,C使用??x来转义它们没有的符号。所以请在代码中避免使用??序列,字符串中可使用\?转义。下表是规范支持的trigraph sequance,不在该表中的不进行转义。

??( ??) ??< ??> ??= ??/ ??‘ ??! ??-
[ ] { } # \ ^ | ~

  第2步,去除“\+回车”。将该组合去除,不产生或消除空白。所以identifier中也可以被断开,但它一般用于宏定义和字符串换行(见示意代码)。注意下一行的前导空白会被保留,所以不能为了格式对齐而添加空格。

#define INC(a)           \
{                           a++;                  }

char str[] = "this is a  long string";

  第3步,preprocessing token。将注释换成一个空格,解析pp token和空白(white space)。空白包括空格、换行、tab等,换行被保留,其它空白的处理基于实现。注释/**/可跨行,不可以嵌套。如果想临时注释掉一段代码,最好用#if 0。注释//作用到本行末,旧C不支持该用法。

// should use #if 0
/*
int a; /* declare var */
*/

  第4步,预处理指令。展开宏,导入包含文件,执行预处理指令,直至结束。预处理指令(directive)以#开头,仅包括当前行,#前后可以有空白。

  第5步,字符串处理。字符(串)映射为execution字符集,包括将转义序列\x编码。将相邻字符串拼接为一个字符串,只添加一个结束符‘\0‘。

char str[] = "This is a "
             "long string";

  第6步,C token。将pp token映射为C token,空白被丢弃。

3. Token 解析

  编程语言在字符集的基础上进行词法(Lexical)、语法(Syntax)和语义(Semantic)的分析。C词法分析就是将程序分解为token,这一步在预处理阶段完成。token一般不会被赋予太多的意义,只是根据序列特征大致分类,编译器根据这些特征解析出一个个token。token解析采用贪婪原则(也称最长原理),一个token的下一个字符与它不能再组成有意义的token。不满足贪婪原则的分割,即使有意义,也是不被采用的。预处理将token大致分为四类:identifier,data constant,string,punctual。

  identifier即标识,它包括directive、keyword、object、function、tag、member、name、lable、macro等,简单说就是用来表示某个东西的名字。identifier的词法大家都熟悉,就是由字母、数字和‘_‘组成,但不由数字开头。identifier不宜过长,因为有些编译器会进行截取。另外在起名字时尽量回避关键字还有__xxx__和_Axx_(大写字母开头),它们都预留给系统使用。

  data constant就是各种常量,包括整形、浮点、字符等。它们有自己的格式,在下一章将有描述。headfile name和string literal以<>或“”作为边界,其中可以含有空格。在其它场合,空白和符号往往是分割token的边界。

  punctual就是各种符号。它同样也遵循贪婪原则,以最长的有意义符号串作为一个token,注意其中不能有空白。另外C还支持digraph转义序列(下表),但和trigraph不同,它是在token解析时进行的。

<: :>  <%  %>  %:  %:%: 
 [ ## 

  需要强调的是,token解析是在预处理阶段完成的,而且除特殊情况外不重新解析。预处理里中的token到C编译时会做一些调整(字符转义、丢弃空白等),但token的分割已经完成。也就是说token解析完成后,程序的组成单位就是token,而不是字符集了。由此可见,宏定义不光是简单的字符串替换,至少它还影响了token的解析。以下的例子能很好的说明本段的一些内容。

#define plus     +

a+++b;           // (a++) + b
a+ ++b;          // a + (++b)
a+ + +b;         // illegal
a plus++b;       // a + (++b)
a+ =b;           // illegal
a plus=b;        // illegal
a/*p;            // should be a/ *p

4. 预处理指令

4.1 宏

  宏是预处理中最复杂也是最强大的功能,这里用单独篇幅说明宏的使用。简单来说,宏就是将宏identifier用其定义的token序列替代。替代的token序列的头和尾的空白被去除,中间的空白可能被合并,包括宏参数也是这样处理的,这与我们一直认识的“替代”还是有差别的。另外,宏只做替换,对常量表达式并不做计算。

#define r    1
#define C    (2*r*3.14)        // (2 * 1 * 3.14), not 6.28
#define mul(a, b)             (    (a)     *     (b)    )
mul(  2   +    3   ,   5   );  // ( (2 + 3) * (5) ), attention to the space

  宏中有两个可以改变token的操作符:#和##。#叫stringify operator,它可以将宏参数字符串化。宏实参可能为token序列,其中可能有字符串,这时将"和串中的\用\转义(非串中的\不转)。#仅作用于宏参数,不可用于一般token。

#define str    #hi    // not "hi"
#define str(a) #a
str(\a"hi\!");        // "\a\"hi\\!\""

  ##叫token pasting operator,用于合并token。它的左右操作对象可以是宏参数,也可以是一般token,它与操作对象间的空白会被去除。##甚至可以连用,串起更多的操作对象。#和##只在宏展开时起作用,如果宏结果中出现#或##,将不再起作用。

#define twoj  # ## #                     // ##
#define fun(pre, post)   pre##_f_##post
fun(res, get)();                         // res_f_get()

  不带参数的宏叫object-like macro,带参数的叫function-like macro。函数宏的定义中,宏名与()之间不能有空白,参数可为空。调用时宏名与()之间可有空白,而且新规范允许宏实参为空。函数宏定义最好能让使用者自由添加‘;‘,见do while语句。

#define add1 (a, b)   (a+b)
add1(1, 1);                        // wrong. (a, b) (a+b)(1, 1)
#define add2(a, b)    (a+b)
add2 (1, 1);                       // ok, 1+1

#define fun1()                     // ok
#define fun2(a, b)    add##a##b
fun2();                            // add
fun2(1);                           // add1
fun2(, 2);                         // add2

  新规范中支持宏的变长参数,只需将末尾的参数用...表示即可。不同于C的变长参数,宏中不需要前导参数。在定义中用_VA_ARGS_代替实参,实参为token序列(包含‘,‘),头尾没有空白,中间可能有空白。

#define show(...)    printf(#_VA_ARGS_)
show(  hi, there!  );                    // printf("hi, there!")

#define fun(a, ...) a##_VA_ARGS_
fun(1, 2, 3);                            // 12, 3
fun(1);                                  // 1

  宏展开中最复杂的情况是宏嵌套,但其实只要弄清三点即可:(1)宏实参遇到#或##时,立即产生作用,不再继续展开;(2)其它地方宏实参要先自行展开再带入结果;(3)对结果中出现的曾经完整展开的宏或#、##,不作处理,其它宏则继续展开。结合(1)(2),如果想先展开再做#或##,可以将#或##操作本身包在宏里。以下代码中,展开内层M(0)时外层M尚未完全展开,所以内层M(0)可展开。而f()展开为f后,f()不可再次展开。

#define one      1
#define show(a)  printf(#a" = %d\n", a)
show(one);                               // printf("one = %d\n", 1)

#define name     edward
#define str(s)   #s
#define show(a)  printf(str(a))
show(name);                              // printf("edward")

#define M(x)     x
#define f()      f
M(M(0));                                 // 0
f()();                                   // f()

  宏展开是比较靠前执行的,#include和#if指令中都可以使用宏定义。由于整个文件名(包括<>"")是一个token,宏也要定义完整的文件名。宏不可以重定义,除非先#undef或定义完全一样,这里的一样是指参数个数和token序列一样,参数名可以不一样。

#define name1       stdio
#define name2       <stdio.h>
#include <name1.h>             // <name1.h>
#include name2                 // <stdio.h>

#define add()
#undef add
#define add(a, b)   a+b     // ok
#define add(x, y)   x+y     // ok
#define add(x, y)   x + y   // illegal

  系统提供了一些预定义宏,可以在程序中使用。以下是规范要求必须定义的宏,这些宏不可以#undef。有些不断变化的宏(如__LINE__)其实是系统变量,规范要求每个函数开始都有一个隐藏定义static const char __FUNC__[] = file_name。

Macro Type Description
 __FILE__  "path\name"  包含路径,实际文件
 __LINE__  integer  实际文件
 __DATE__  "Mmm dd yyyy"  无值的位补0
 __TIME__  "hh:mm:ss"  无值的位补0
 __STDC__  0 or 1  是否与规范兼容
 __STDC_VERSION__  yyyymmL  所兼容规范版本
 __STDC_HOSTED__  0 or 1  是否有OS

  因为宏只是token替换,它隐含很多不利之处,有时要使用其它替代方法。用宏定义的整型无法在调试时显示,可以用枚举常量替代。宏定义的字符串常量可能产生多份,可以用const string替代。函数宏无参数检查且有副作用,可以用inline函数替代。

4.2 其它指令

  #include指令将头文件包含在本unit中,后面跟头文件名。< >包含的文件到库目录中寻找,编译环境一般可以指定该目录。" "包含的文件先从当前目录下查找,再到库中查找,从而可以先使用自定义的库。为了消除重复包含,可以用宏(见示例)或编译器扩展语句。

  为增加移植性和灵活性,预处理支持conditional compiling。它一般以#if、#ifdef或#ifndef分支开头,后面跟着0个或多个#elif分支,末尾最多一个#else分支,最后以#endif结束。条件表达式的结果要是整型,可以是整型常量、宏或defined运算符,不可使用字符(串)、浮点常量。defined是预处理的唯一关键字,它有defined(M)和defined M两种形式。

  #line指令后跟行号和可选的文件名,这个指令一般用于生成.c文件的文件中,使行号(文件名)指向原始文件,而非.c文件。#error指令后跟任意token序列,这些token不能展开宏。预处理遇到#error会挂起,并且显示token序列。

  #pragma指令由编译器自定义行为,STDC开头的指令预留给规范使用,目前已定义了一些功能开关。#pragma中不可以展开宏,新规范中使用关键字_Pragma("command")支持宏展开,它等价于#pragma command。

// headfile, only included once
#ifndef HEAD.H
#define HEAD.H
// ...
#endif

#if defined X            // the same as #ifdef X
#elif defined(Y)
// ...
#elif
#else
#endif

#line 100
#line 100 "\test.c"

#define name  edward
#error fail, name!       // show fail, name!

#define str(cmd)  #cmd
_Pragma(str(align(4)));  // the same as #pragma align(4)
时间: 2024-10-24 20:43:02

【C】 02 - 程序结构和预处理的相关文章

程序结构~编译预处理和宏

/*            #define    #define<名字><值>    注意没有结尾的分号,因为不是C的语句    名字必须是一个单词,值可以是各种东西    在C语言的编译器开始之前,编译预处理程序    (cpp)会把程序中的名字换成值        完全的文本替换 宏    如果一个宏的值中有其他的宏的名字,也会被替换的    如果一个宏的值超过一行,最后一行之前的行末需要加\    红的值后面出现的注释不会被当做宏的值的一部分 *//*        没有值得

一步步AS400-Cobol 上手自学入门教程02 - 程序结构和标识部(原创)

标识部的格式: 代码范例: IDENTIFICATION DIVISION.PROGRAM-ID. IDSAMPLE.AUTHOR. PROGRAMMER NAME.INSTALLATION. COBOL DEVELOPMENT CENTER.DATE-WRITTEN. 08/27/88.DATE-COMPILED. 09/01/88 12:57:53.SECURITY. NON-CONFIDENTIAL. 环境部的格式: 代码范例 ENVIRONMENT DIVISION.CONFIGURAT

lua参考手册02—程序接口

3 - 程序接口(API) 这个部分描述了 Lua 的 C API , 也就是宿主程序跟 Lua 通讯用的一组 C 函数. 所有的 API 函数按相关的类型以及常量都声明在头文件 lua.h 中. 虽然我们说的是“函数”,但一部分简单的 API 是以宏的形式提供的. 所有的这些宏都只使用它们的参数一次 (除了第一个参数,也就是 lua 状态机), 因此你不需担心这些宏的展开会引起一些副作用. 在所有的 C 库中,Lua API 函数都不去检查参数的有效性和坚固性. 然而,你可以在编译 Lua 时

网易云课堂_C语言程序设计进阶_第六周:程序结构:内存模型(全局变量和局部变量)、头文件、宏定义、函数指针和回调函数,ACL图形库的消息机制

6.1 全局变量 6.2 编译预处理 6.3 大程序结构 6.1 全局变量 全局变量 定义在函数外面的变量是全局变量 全局变量具有全局的生存期和作用域 它们与任何函数都无关 在任何函数内部都可以使用它们 全局变量初始化 没有做初始化的全局变量会得到0值 指针会得到NULL值 只能用编译时刻已知的值来初始化全局变量 它们的初始化发生在main函数之前 被隐藏的全局变量 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏 6.2 编译预处理 6.3 大程序结构

C语言学习系列(三)C程序结构

一.C程序结构 C 程序主要包括以下部分: 预处理器指令 函数 变量 语句 & 表达式 注释 new C program demo: 1 #include <stdio.h> /*预处理器指令*/ 2 /* 第一个中文程序实例 */ 3 int main() /*main函数*/ 4 { 5 int i; /*变量*/ 6 i=1; /*语句&表达式*/ 7 printf("我的第%d个C程序\n",i); /*语句&表达式*/ 8 return 0

《C程序设计语言(第2版&#183;新版)》第4章 函数与程序结构

函数功能:隐藏操作细节,结构更加清晰,降低修改难度: 4.1 函数基本知识 返回值类型 函数名(参数声明表) { 声明和语句 } 函数在源文件中出现的次序可以任意: 返回值类型省略则默认int:return可不带表达式,执行到最后右花括号也会返回:都是没有返回值的,合法,但未成功返回的“值”肯定是无用的: 程序可看做变量定义与函数定义的集合:函数通过参数.返回值和外部变量通信: 4.2 返回非整型的函数 函数与调用它的主函数在同一源文件中,并且类型不一致时,编译就会发现该错误: 隐式声明:如果未

C语言函数与程序结构

title : C语言函数与程序结构 tags : C语言作用域规则 , 外部变量 ,静态变量 ,寄存器变量,宏定义 grammar_cjkRuby: true --- 外部变量 变量声明用于说明变量的属性(类型),而变量定义还会引起存储器分配 int sp; double s[MAX]; 声明地方:函数外 如果上面的变量定义在所有函数之外,即为外部变量,并为这两个外部变量sp.s[MAX],分配储存单元以及数组的长度,在其源文件中的所有函数都可以使用这两个外部变量. extern int sp

第五章 C程序结构

一.数值类型 1.实数常量的表示:3.5(双精度),3.5f(单精度),3.5L(长双精度) 2.整数常量:char字符常量('a','b','0')当做一个整型常量参加运算 3.数字字符与英文字母字符的编号(Ascll码)都是顺序连接的 二.控制语句 1.while和do while的区别:当while条件不成立时,while()循环不执行循环语句,而do while会循环执行一次循环语句再判断 2.流程控制语句:continue(中断本次循环)    break(跳出整个循环) 3.开关语句

C#学习笔记二:C#程序结构

从最简单的HelloWorld开始入手,这是一个最低限度的C#程序结构. C# Hello World 示例 一个C#程序主要由以下几部分组成: 命名空间声明 一个类 类方法 类属性 一个Main方法 语句和表达式 注释 先看看下面的示例,将打印字的简单的代码 "Hello World": using System; namespace HelloWorldApplication { class HelloWorld { static void Main(string[] args)