本章学习目标 .掌握函数、指针的定义和使用。 .理解编译预处理的作用和用法。 .掌握结构体、共用体和枚举的定义和使用。 .掌握零长数组、变长数组和动态数组的定义和使用。 .掌握indent命令的使用。 3.函数 1 C语言通过函数实现模块化程序设计,用函数实现功能模块的定义。一个C程序可以 由一个主函数和若干个自定义函数构成,主函数调用其他函数,其他函数也可以互相调用, 通过函数之间的调用实现程序功能。 3.1.1 C 语言库函数 C语言提供了功能丰富的库函数,一般库函数的定义放在头(库)文件中。头文件是扩 展名为.h的文件。使用库函数时在程序开头用“#inlde<*.或“#icuh” cuh>” nlde"*." 将头文件包含进来。C语言常用头文件及主要函数如表3. 1所示。 3.1.2 函数定义和声明 C程序的基本单位是函数,函数是一段封装好的、可以重复使用的代码,有助于程序的 模块化设计。函数有库函数和用户自定义函数,一个实用的C程序都应该包含自定义函 数,通常一个函数执行一个特定的任务。一个C程序至少包含一个主函数(main函数), 并 且主函数只能有一个,主函数的特殊之处在于函数名(main)是固定的,操作系统就认这个 表3. 1C语言常用头文件及主要函数 头文件主要函数 stdio.h 包含输入/输出函数的声明,共6大类别。①文件操作类:删除文件remove、修改文件名 称rename、生成临时文件名称tmpfile、得到临时文件路径tmpnam、关闭文件fclose、刷新 缓冲区 flush、打开文件fopen、将已存在的流指针和新文件连接freopen、设置磁盘缓冲区 setbuf、设置磁盘缓冲区setvbuf;②格式化输入与输出类:格式输出fprintf、格式输入 fscanf、格式输出(控制台)printf、格式输入(控制台)scanf、格式输出到缓冲区sprintf、从缓 冲区中按格式输入 scanf、格式化输出vfprintf、格式化输出vprintf、格式化输出vsprintf; ③字符输入/输出类:输入一个字符fgetc、字符串输入fgets、字符输出fputc、字符串输出 fputs、字符输入(控制台)getc、字符输入(控制台)getchar、字符串输入(控制台)gets、字符 输出(控制台)putc、字符输出(控制台)putchar、字符串输出(控制台)puts、字符输出到流的 头部ungetc;④直接输入/输出类:直接流读操作fread、直接流写操作fwrite;⑤文件定位 类:得到文件位置fgetpos、文件位置移动fsek、文件位置设置fsetpos、得到文件位置 ftel 、文件位置复零位remind;⑥错误处理类:错误清除clearer 、文件结尾判断feof、文件 错误检测feror、得到错误提示字符串peror stdlib.h 包含编程所必须要的实用工具函数的声明,共4大类别。①字符串转换类:字符串转换为 整数atoi、字符串转换为浮点数atof、字符串转换为长整数atol、字符串转换为浮点数 strtod、字符串转换为长整数strtol、字符串转换为无符号长整型strtoul、浮点数转换成字 符串ecvt;②产生伪随机序列类:产生随机数rand、设置随机函数的起动数值srand;③存 储管理类:分配存储空间maloc、释放存储空间fre 、分配存储空间caloc、重新分配存储 空间realoc;④环境通信类:中止程序abort、退出程序执行并清除环境变量atexit、退出程 序执行exit、读取环境参数getenv、挂起当前进程去临时执行一个其他程序system、二分查 找已排序的数据bsearch、快速排序qsort、整数运算求绝对值abs、得到除法运算底商和余 数div、求长整型的绝对值labs、求长整型除法的商和余数ldiv、得到多字节字符的字节数 mblen、得到多字节字符的字节数mbtowc、多字节字符转换wctomb、将多字节串转换为整 数数组mbstowcs ctype.h 包含对单个字符进行处理的函数声明,包括字符类别测试和字母大小写转换:是否为字母 和数字isalnum、是否为字母isalpha、isasci 判断字符ASCI 码是否属于[0,127 ]、是否为控 制字符iscntrl、是否为数字isdigit、是否为可显示字符(空格除外)isgraph、是否为可显示字 符(包括空格)isprint、是否既不是空格又不是字母和数字的可显示字符ispunct、是否为空 格ispace、是否为大写字母isupper、是否为十六进制数字(0-9、a-f、A-F)字符isxdigit、转换 为大写字母toupper、转换为小写字母tolower string.h 包含字符串处理函数的声明,包括对字符串进行合并、比较等操作:块复制(目的和源存储 区不可重叠)memcpy、块复制(目的和源存储区可重叠)memmove、串复制strcpy、按长度 的串复制strncpy、串连接strcat、按长度的串连接strncat、块比较memcmp、字符串比较 strcmp、字符串比较(用于非英文字符)strcol 、按长度对字符串比较strncmp、求字符串长 度strlen、字符串转换strxfrm、块中字符查找memchr、串中字符查找strchr、字符串查找 strstr、字符串分解strtok、字符串设置memset、错误字符串映射streror math.h 包含数学函数的声明:反余弦acos、反正弦asin、反正切atan、反正切2atan2、余弦cos、正 弦sin、正切tan、双曲余弦cosh、双曲正弦sinh、双曲正切tanh、指数函数exp、指数分解函 数frexp、自然对数log、以10为底的对数lg、浮点数分解函数modf、幂函数pow、平方根函 数sqrt 名字,程序执行时main函数自动被操作系统调用,除此之外,main函数和其他自定义 45 函数没有区别,因此可以将主函数看成一个特殊的自定义函数。各个函数在定义时彼此独 立,在执行时可以互相调用,其他函数不能调用主函数。下面主要介绍自定义函数。编程时 不仅可以调用C标准库提供的库函数,也可以调用自定义函数。 用户自定义函数的一般形式是:“函数类型函数名(形参列表){函数体}”。一个函 数包括函数头和函数体,函(“) 数类型函数名(形参列表)”为函数头。①函数类型是指函数 返回值的数据类型,可以通过函数体中的return语句返回函数值,如果函数没有返回值,则 函数类型应写为空类型void,如果省略函数类型,则默认为int型;②函数名是用户自定义 的一个标识符,要符合标识符的命名规则;③定义有参函数时,函数名后一对圆括号内的形 参列表至少一个形式参数(形参),多个参数之间用逗号隔开,每个形参都应指定其类型; ④定义无参函数时,函数名后圆括号内应该为空或void;⑤函数体是函数的主体部分,是由 一对花括号{}括起来的一个语句块,即一个复合语句,可以由若干条语句和声明组成,C89 要求所有声明写在所有语句之前,而C99允许语句和声明可以按任意顺序排列,只要每个 标识符都遵循先声明后使用的原则即可。 函数(整型或void型函数除外)应该先定义后调用,或者先声明(也称说明)后调用。如 果函数定义放在了调用它的函数之后,则一定要在调用它的函数之前对该函数进行声明。 函数声明的一般形式是:“函数类型函数名(形参列表);”。函数声明和语句类似,都是以 分号结尾,但是语句只能出现在函数体中,而函数声明既可以出现在函数体中,也可以出现 在所有函数体之外。 3.1.3 函数调用及参数传递 函数调用就是使用已定义好的函数。函数调用的一般形式为:“函数名(实参列表)”。 通过调用函数完成已定义的任务。调用无参函数时实参列表为空,但是括号不能省略。当 一个函数(主调函数)调用另一个函数(被调函数)时,程序控制权会从主调函数转移到被调 函数,被调函数执行完已定义的任务后(当执行返回语句或到达函数体右花括号时),会把程 序控制权再次转移到主调函数。 C语言中有多种函数调用方式,示例如下。 Linux操作系统通过虚拟内存的方式为所有应用程序(进程)提供了统一的虚拟内存地 址,如图3.内核地址空间部分由所有应用程序共享), 1所示的用户地址空间部分( 从上到下 分别是环境变量、命令行参数、栈、共享库和mmap内存映射区、堆、数据段、代码段和未用 地址空间。①代码段存放着程序的机器码和只读数据,可执行指令就是从这里取得的。这 个段在内存中一般标记为只读,任何对该区的写操作都会导致段错误。②数据段包括已初 始化数据段和未初始化数据段,已初始化数据段用来存放全局的和静态的已初始化变量,未 初始化数据段用来存放全局的和静态的未初始化变量。③堆用来存放程序运行时分配的变 量。堆的大小并不固定,可动态扩张或缩减。其分配由maloc等函数实现,其释放由fre 等函数实现,通常一个maloc函数要对应一个fre 函数。堆内存由程序员负责分配和释 放,如果程序员没有释放,那么在程序结束后由操作系统自动回收。④栈用来存放函数调用 46 时的临时信息,如函数调用所传递的参数、函数的返回地址、函数 的局部变量等。栈通常称为先进后出队列。栈的基本操作是 PUSH 和POP,PUSH 操作称为压栈,将数据放置在栈顶;POP 操作相反,将栈顶元素移出,称为出栈。⑤命令行参数是指从命 令行执行程序时传递给程序的参数。C语言总是从main函数开 始执行,它的原型声明为“n(c,v[]);,(”) 参 intmaiintargchar*arg 数argc保存程序执行时命令行输入的参数个数。参数argv保存 命令行参数。ISOC 和POSIX 都要求argv[argc]是一个空指针。 历史上,大多数UNIX 系统都是支持3个参数的main函数,原型 声明为“iniitagc,hr*agv[],hr*ev,(”) tman(nrcarcanp[]);其中 第3个参数envp是环境变量列表。现在POSIX 建议不使用第三 个参数,而是使用getenv和putenv函数访问环境变量,getenv函 数用来获取一个环境变量的内容,putenv函数用来改变或增加环 境变量的内容。另外,也可以使用全局变量environ查看整个环 境变量。命令行参数和环境变量示例如表3.图3.进程地址空间 2所示。 1 本章几乎所有源代码都在本书配套资源的“sr/第2-3章/oec”文件中。 ccd. 表3.命令行参数和环境变量示例 2 扫一扫 示例代码运行结果 参数是主调函数和被调函数进行信息通信的接口,函数在被调用之前,其形参在内存中 是不存在的。函数只有被调用时,其形参才被定义且分配栈中的内存单元。调用函数时,实 际参数(实参)与形参之间要进行数据传递,会将实参的值计算出来分别赋值给对应的形参, 这一过程称为参数传递。C语言中函数参数有两种传递方式:值传递、地址传递。①值传 递是将实参的值复制到形参相应的存储单元中,即形参和实参分别占用不同的存储单元。 47 值传递的特点是参数值的单向传递,即主调函数调用被调函数时把实参的值传递给形参,之 后实参与形参之间不再有任何关系,形参值的任何变化都不会影响到实参的值,调用结束 后,形参的存储单元被释放;②地址传递是将实参的地址复制到形参相应的存储单元中,即 形参是指针类型的变量,指向实参的存储单元,可以通过形参(指针)读写实参占用的存储单 元。地址传递的特点是双向传递,即对形参指向变量的改变也是对实参的改变。值传递和 地址传递示例如表3. 3所示。 扫一扫 表3.值传递和地址传递示例 3 值传递示例代码运行结果地址传递示例代码运行结果 函数调用是通过栈实现的,栈由高地址向低地址方向生长,栈有栈顶和栈底,入栈或出 栈的位置叫栈顶。函数在被调用执行时都会在栈空间中开辟一段连续的空间供该函数使 用,这一空间称为该函数的栈帧。栈帧就是一个函数执行的环境。每个函数的每次调用都 有它自己独立的一个栈帧。当进行函数调用时,我们经常说先将函数压栈(局部变量从后向 前、形参从右向左依次压入栈中), 其实是为被调函数分配栈帧,当函数调用结束后,再将函 数出栈,其实是释放该函数的栈帧。这一过程是系统帮我们自动完成的,在x86 系统的 CPU 中,涉及三个寄存器:EIP 、ESP 、EBP 。指令指针寄存器EIP 中存储的是CPU 下次要 执行的指令的地址。基址指针寄存器EBP 永远指向当前栈帧(栈中最上面的栈帧)的底部 (高地址), 栈指针寄存器ESP 存储着栈顶地址,永远指向当前栈帧的顶部(低地址), 因此函 数栈帧主要由EBP 和ESP 这两个寄存器确定。当程序运行时,ESP 可以移动,元素的进栈 或出栈操作通过ESP 实现,EBP 是不移动的,访问栈帧里的元素可以通过EBP 加减偏移量 实现。表3.3中的示例代码的函数栈帧结构如图3.2所示。main函数调用swap时,首先在 自己的栈帧中压入返回地址(断点), 然后分配swap的栈帧。在swap返回时,swap的栈帧 被弹出,main函数栈帧作为当前栈帧,此时处于栈顶的返回地址被写入EIP 中,处理器跳到 main函数代码区的断点处继续执行。在程序实际运行过程中,main函数并不是第一个被 调用的函数,图3.2(中, 2(中,即使形参名称与实 2只是函数调用过程中栈变化的示意图。图3.a) 参名称一样,它们在内存的地址也不一样。图3.b) 即使形参名称与实参名称一样,它 们的类型也不一样,是将整型变量的地址赋值给指针变量。 当实参列表有多个实参时,对实参的求值顺序是不确定的,有的系统按自左向右的顺序 求值,有的系统按自右向左的顺序求值。不同编译系统以及同一编译系统的不同版本规定 的求值顺序是不同的。有的编译器会进行优化,优化策略不同,求值顺序可能不同。实参求 值顺序示例如表3. 4所示。 48 图3.函数栈帧 2 表3.实参求值顺序示例 4 示例代码运行结果 从输出结果可知,该系统按自右向左的顺序进 行求值。编程时不要使函数实参之间存在关 联,尽量避免在函数参数表达式中对变量进行 赋值操作 扫一扫 3.1.4 函数的嵌套与递归 C语言中的函数定义都是互相平行、独立的,在定义一个函数时,不允许在其函数体内 再定义另一个函数。C语言不能嵌套定义函数,但是C语言可以嵌套调用函数,即在调用一 个函数的过程中,在该函数的函数体内又调用另一个函数。例如,主函数调用函数A,函数 A又调用函数B。 一个函数在它的函数体内直接或间接地调用该函数本身,这种函数称为递归函数,这种 调用关系称为函数的递归调用。在函数内部又调用它本身称为直接递归,在函数A内部调 用函数B,在函数B中又调用函数A称为间接递归。函数的递归调用属于函数嵌套调用的 一种。递归调用的实质就是将原来的问题分解为新的问题,而解决新问题时又用到了原有 问题的算法。 递归函数执行时将反复调用其自身,每调用一次就进入新的一层(即在栈顶分配一个栈 帧), 当最内层的函数(满足递归结束条件)执行完毕后,再一层一层地由里向外退出,每 return一次就释放栈顶的栈帧。注意,如果无递归结束条件,则会无穷递归,直到程序栈空 间耗尽导致程序崩溃(段错误)。下面通过求阶乘的例子理解递归函数的运行情况。n的阶 乘(Factorial)的定义为:n的阶乘等于n乘以n-1的阶乘,0的阶乘等于1。具体地,0!= 49 1,1!=1*(1-1)!=1*0!=1*1=1,n!=n*(n-1)!。阶乘示例如表3. 5所示。 表3.阶乘示例 5 扫一扫 示例代码运行结果 输入5,求5!。调用fac(5),函数体中 执行fac(4)*5,调用fac(4),函数体 中执行fac(3)*4,调用fac(3),函数 体中执行fac(2)*3,调用fac(2),函 数体中执行fac(1)*2,调用fac(1), 函数体中直接返回常量1。n=1时递 归进到最内层,递归结束,开始逐层退 出,也就是逐层执行return语句 扫一扫 3.1.5 回调函数 C语言中,回调函数(CalbackFunction)就是一个通过函数指针调用的函数。回调函 数允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事 件的时候可以灵活使用不同的方法。因为可以把调用者与被调用者分开,所以调用者不关 心谁是被调用者,只需知道存在一个具有特定原型和限制条件的被调用函数。回调函数示 例如表3. 6所示。 表3.回调函数示例 6 示例代码运行结果 main函数中调用handle函数时传递了 不同的函数指针,进而通过handle函数 调用了不同的函数,如add、sub。输出 结果中第6行,$? 的值就是上一条命 令的返回值,也就是主函数的返回值 3.1.6 return 语句 函数有主调函数和被调函数,被调函数完成一定功能后可以通过return语句向主调函 数返回一个确定的值,该值就是函数的返回值。return语句表示把程序流程从被调函数转 50 向主调函数。reun语句有两种:带表达式的rtrrtr”省略表达式的 treun语句“eun表达式;、 n语句“ ”。①带表达式的rn语句结束函数的执行并把表达式的值返回给 returreturn;etur 主调函数,定义函数时必须指定函数类型,函数类型和返回值类型要一致,如果不一致,系统 规定返回值以函数类型为准进行自动转换,转换后的值为最终的函数值。②省略表达式的 return语句结束函数的执行并返回到主调函数中该函数的调用处,定义函数时必须指定函 数类型为空类型void。③函数体中可以有多个return语句,执行到任何一条return语句都 会结束函数的执行,返回到主调函数中该函数的调用处。④函数体中如果没有return语 句,则执行完函数体的最后一条语句后返回主调函数。main函数是整型函数,如果main函 数体中没有return语句,则默认的返回值是0。通过执行echo$? 命令查看main函数的 返回值,如表3. 6所示。 3.1.7 全局变量、局部变量和作用域 作用域描述程序中可以访问标识符的区域,一旦离开其作用域,程序便不能再访问该标 识符。C语言中的变量的作用域可以是块作用域、函数作用域、函数原型作用域、文件作用 域。块是用一对花括号括起来的代码区域,整个函数体是一个块,函数中的复合语句也是一 个块,定义在块中的变量具有块作用域。变量在块作用域的可见范围从变量定义处到包含 该定义的块的末尾。函数形参声明虽然在函数左花括号前,但是它们属于函数体这个块,具 有函数作用域。函数原型作用域的范围从形参定义处到函数原型声明结束,这意味着编译 器在处理函数原型中的形参时只关心它的类型,而形参名通常无关紧要,即使有形参名,也 不必与函数定义中的形参名相同。定义在所有函数外的变量具有文件作用域,文件作用域 变量(全局变量)从它定义处到该定义所在文件的末尾均可见。 全局变量也称为外部变量,是在函数外定义的变量,它不属于任何一个函数,其作用域 从变量的定义处开始,到本程序文件的结尾。全局变量只能用常量表达式初始化。在作用 域内全局变量可以被各个函数使用。全局变量也遵循先定义后使用的原则。如果在函数中 使用该函数后面定义的全局变量,应在此函数内使用extern关键字声明该全局变量,语法 为“extern类型说明符变量名;,(”) 全局变量的声明表明在函数内要使用某全局变量,因此声 明时不能进行赋初值等操作。在程序开始运行时为全局变量分配存储空间,在程序结束时 释放全局变量占用的存储空间。当需要函数返回多个值时,除函数体中的return语句返回 其中一个外,其他的返回值可以通过定义全局变量来处理。因为根据全局变量的特点,在被 调用函数中改变了多个全局变量的值,相当于其主调函数全局变量的值也发生了变化,也就 相当于返回了多个值。 局部变量也称为内部变量,是在函数内定义的变量,其作用域仅限于函数内(从该变量 被定义开始到函数结束), 一个函数中定义的局部变量不能被其他函数使用。函数内定义的 变量、函数的形参变量都属于局部变量。注意,在复合语句(由花括弧括起来)内定义的局部 变量,其作用域仅限于该复合语句块内。局部变量仅在其作用域内可见,在作用域外不能被 访问。局部变量的存储空间在每次函数调用时分配(存在于该函数的栈帧), 在函数返回时 释放。 如果函数内定义的局部变量与全局变量重名,则函数在使用该变量时会用到同名的局 部变量,而不会用到全局变量,也就是会用局部变量覆盖全局变量,只有局部变量起效果。 51 3.1.8 变量的存储类别及生存期 变量的数据类型说明它占用多大的内存空间、可以对其进行什么样的操作。除了数据 类型,变量还有一个属性,称为存储类别。存储类别决定变量的作用域和生存期。存储类别 是指变量占用内存空间的方式,也称为存储方式。存储类别分为两种:静态存储、动态存 储。①静态存储变量通常在变量定义时就分定存储单元并一直保持不变,直至整个程序结 束。全局变量属于此类存储方式。②动态存储变量是在程序执行过程中定义它时才分配存 储单元,使用完毕立即释放。比如函数的形参,在函数定义时并不给形参分配存储单元,只 在函数被调用时才予以分配,调用函数完毕立即释放。如果一个函数被多次调用,则反复地 分配、释放形参的存储单元。 生存期表示变量存在的时间,是程序运行过程中变量从创建到销毁的一段时间。生存 期的长短取决于变量的存储类别,也就是它所在的内存区域。因此,知道变量的存储类别, 就可以知道变量的生存期。静态存储变量是一直存在的,而动态存储变量则因程序执行需 要而时而存在时而消失。生存期和作用域是从时间和空间这两个不同的角度描述变量的特 性,这两者既有联系,又有区别。 如图3.1所示,在进程地址空间中,常量区(代码段)、数据区和栈区可用来存放变量的 值。①常量区和数据区的内存在程序启动时就已经由操作系统分配好了,占用的空间固定, 程序运行期间不再改变,程序运行结束后才由操作系统释放;它可以存放全局变量、静态变 量、一般常量和字符串常量。②栈区的存储单元在程序运行期间由系统根据需要分配,占用 的空间实时改变,使用完毕后立即释放,不必等到程序运行结束;它可以存放局部变量、函数 参数等。 C语言有4个关键字用来控制变量在内存中的存放区域:auto(自动变量)、static(静态 变量)、register(寄存器变量)、extern(外部变量)。自动变量和寄存器变量属于动态存储方 式,外部变量和静态变量属于静态存储方式。 auto:自动变量的作用域仅限于定义该变量的函数或复合语句内。自动变量属于动态 存储方式,只有在定义该变量的函数被调用时才给它分配存储单元,开始它的生存期。函数 调用结束,释放存储单元,结束生存期。因此,函数调用结束后,自动变量的值不能保留。在 复合语句中定义的自动变量,在退出复合语句后也不再使用,否则将引起错误。由于自动变 量的作用域和生存期都局限于定义它的函数或复合语句内,因此不同的作用域中使用同名 的变量而不会混淆,在函数内定义的自动变量可与在该函数内复合语句中定义的自动变量 同名。定义自动变量时如果没有赋初值,则其值是一个随机数。因为所有的局部变量默认 是auto,所以auto很少用到。也就是说,定义变量时加不加auto都一样,所以一般把它省 略,不必多此一举。例如“n=”atn=” itn1;与“uoitn1;的效果完全一样。 static:static声明的变量称为静态变量。静态变量属于静态存储方式,不管它是全局 的还是局部的,都存储在静态数据区(全局变量本来就存储在静态数据区,即使不加static)。 自动变量属于动态存储方式,但是可以用static定义它为静态自动变量(静态局部变量), 从 而属于静态存储方式。①静态局部变量在函数或复合语句内定义,在作用域结束时并不消 失,它的生存期为整个源程序,其作用域仍与自动变量相同,即只能在定义该变量的函数或 复合语句内使用该变量。退出该函数或复合语句后,尽管该变量还继续存在,但不能使用 52 它。②在全局变量(外部变量)的说明之前冠以static就构成了静态全局变量(静态外部变 量), 全局变量和非静态全局变量在存储方式上相同,都属于静态存储方式,区别在于作用域 不同,一个源程序由多个源文件构成时,全局变量的作用域是整个源程序,而静态全局变量 的作用域只在定义该变量的源文件内有效,在其他源文件中不能使用它。由于静态全局变 量的作用域局限于一个源文件内,因此可以避免和其他源文件中的同名变量冲突。静态数 据区的数据在程序启动时就会初始化,直到程序运行结束;对于函数或复合语句内的静态局 部变量,即使代码块执行结束,也不会销毁。注意:静态数据区的变量只能初始化(定义)一 次,以后它不能再被初始化, 示例如表3.dd 函数中定义了一个静 只能改变它的值,7所示。a 态局部变量sum,它存储在静态数据区(静态数据区的变量若不赋初值,则默认初值为0), add 函数即使执行结束,也不会销毁,下次调用它继续有效。静态数据区的变量只能初始化 一次,第一次调用add 函数时已经对sum 进行了初始化,所以再次调用时就不会初始化了, 也就是说,再次调用add 函数时“staticintsum=0;”语句无效。静态局部变量虽然存储在 静态数据区,但是它的作用域仅限于定义它的代码块,add 函数中的sum 在函数外无效,与 main函数中的sum 不冲突,除了变量名一样,没有任何关系。 表3.7 静态变量和extern示例 静态变量示例代码运行结果extern示例:1. c extern示例:2. c 运行结果如下: 扫一扫 register:是寄存器变量的说明符,寄存器变量存放在CPU 的寄存器中,使用时,不需 要访问内存,直接从寄存器中读写,这样,可提高效率。一般情况下,变量的值是存储在内存 中的,CPU 每次使用数据都要从内存中读取。如果有一些变量使用非常频繁,从内存中读 ofr(n= 取就会消耗很多时间,例如fr循环语句“oiti0;i<1000;i++){},C(”) PU 为了获得 i,会读取1000 次内存。为了解决这个问题,可以将使用频繁的变量放在CPU 的通用寄存 器中,这样,使用该变量时就不必访问内存了,直接从寄存器中读取,可大大提高程序的运行 效率。不过,寄存器的数量是有限的,通常把使用最频繁的变量定义为register。寄存器的 长度一般和机器的字长一致,只有较短的类型(如int、char、short等)才适合定义为寄存器 变量,如double等较大的类型,不推荐将其定义为寄存器类型。注意,为寄存器变量分配寄 存器是动态完成的,属于动态存储方式,只有局部自动变量和形参才能定义为寄存器变量, 局部静态变量不能定义为寄存器变量。另外,CPU 的寄存器数目有限,即使定义了寄存器 变量,编译器也可能并不真正为其分配寄存器,而是将其当作普通的auto变量对待,为其分 配栈内存。当然,有些优秀的编译器,能自动识别使用频繁的变量,如循环控制变量等,在有 可用的寄存器时,即使没有使用register关键字,也自动为其分配寄存器,无须由程序员 53 54 指定。 extern:外部变量的类型说明符是extern。当一个源程序由若干个源文件构成时,在一 个源文件中定义的全局变量(外部变量)可以在所有源文件中访问,不过需要使用extern关 键字对该变量进行声明,示例如表3.7所示。 变量说明的完整形式为:“存储类别说明符数据类型说明符变量名,变量名,…;”,例 如“staticinta,b,c[5];”“autodoubled1,d2;”“externfloatx,y;”“charc1,c2;”。C语言规 定,函数内未加存储类别说明的变量均视为自动变量,也就是说,自动变量可省去存储类别 说明符auto。 3.1.9 内部函数和外部函数 函数一旦定义后,就可被其他函数调用。C语言把函数分为内部函数和外部函数。 内部函数:当一个源程序由多个源文件构成时,如果在一个源文件A 中定义的函数只 能被源文件A 中的函数调用,而不能被同一源程序其他文件中的函数调用,则这种函数称 为内部函数。定义内部函数的一般形式是“static类型说明符函数名(形参列表){}”。内部 函数也称为静态函数,此处static的含义不是指存储方式,而是指对函数的调用范围只局限 于本文件。因此,在不同的源文件中定义同名的静态函数不会引起冲突。 外部函数:外部函数在整个源程序中都有效,其定义的一般形式是“extern类型说明符 函数名(形参列表){}”。在函数定义中如果没有说明extern或static,则隐含为extern。在 一个源文件的函数中调用其他源文件中定义的外部函数时,应该使用extern对被调函数进 行声明,声明的一般形式是“extern类型说明符函数名(形参列表);”,示例如表3.7所示。 3.2 预处理 3.2.1 预处理的步骤 编译预处理是指对C源程序正式编译之前所做的文本替换之类的工作,它由C预处理 器负责完成。C语言对一个源文件进行编译时,系统将自动调用预处理器对源程序中的预 处理指令进行处理,预处理结束后自动调用编译器对源程序进行编译(词法扫描和语法 解析)。 预处理的具体步骤如下。 1. 把三联符替换成相应的单字符 三联符(Trigraph)是以?? 开头的三个字符,预处理器会将这些三联符替换成相应的单 字符,替换规则为:?? =替换为#,??/替换为\,?? '替换为^,?? (替换为[,?? )替换为],?? ! 替换为|,?? <替换为{,?? >替换为},??-替换为~。 2. 将多个物理行替换成一个逻辑行 把用\字符续行的多行代码接成一行,这种续行的写法要求\后面紧跟换行符(回车),中 间不能有其他空白字符。 3. 将注释替换为空格 表3. 不管是单行注释还是多行注释,都替换成一个空格。三联符、换行符和注释的示例如 8所示。 表3.三联符、换行符和注释的示例 8 示例代码,源代码文件1.c运行结果 4. 将逻辑行划分成预处理标记(Token)和空白字符 经过上面两步处理之后,去掉了一些换行,剩下的代码行称为逻辑代码行。预处理器把 逻辑代码行划分成预处理Token和空白字符,这时的Token包括标识符、整数常量、浮点数 常量、字符常量、字符串、运算符和其他符号。 5. include 预处理和宏展开 在Token中识别预处理标记,如果遇到#include标记,则把相应的源文件包含进来,并 对源文件做以上1~4步的预处理。如果遇到宏定义,则做宏展开。 6. 将转义序列替换 找出字符常量或字符串中的转义序列,用相应字节替换它,比如把\n替换为字节0x0a。 7. 连接字符串 把相邻的字符串连接起来。 8. 丢弃空白字符 经过以上处理之后,把空白字符(包括空格、换行、水平Tab、垂直Tab、分页符)丢弃,把 Token交给C编译器做语法解析,这时就不再是预处理Token,而是CToken。 3.2.2 宏定义和内联函数 用一个标识符表示一个字符串称为宏定义,标识符称为宏名。宏名遵循标识符的命名 规则,习惯上全大写(也允许用小写字母), 便于与变量区别。在编译预处理时,用宏定义中 的字符串替换程序中出现的所有宏名,这个过程称为宏替换或宏展开。宏定义时要用宏定 义指令define,无参宏定义的语法为“#define宏名宏体字符串”,例如“#definePI 1415926”,PI 为宏名,1415926 为宏体字符串。宏名也叫符号常量,可以像变量一样在 代码中使用。#开头表示这是一条预处理指令,预处理器指令从#开始运行,到后面的第一 个换行符为止,也就是说,指令的长度仅限于一行(逻辑行)。宏体字符串是宏名所要替换的 一串字符,字符串不需要用双引号括起来,如果用双引号括起来,那么双引号也是宏体的一 3.3. 55 部分,将被一起替换。宏体字符串可以是任意形式的单行连续字符序列,中间可以有空格和 制表符Tab。宏定义必须写在函数之外,其作用域,为从宏定义指令开始到源文件结束。如 果要终止其作用域,可使用undef指令,语法为“#undef宏名”,例如“#undefPI”。 C标准规定了几个特殊的宏,它们在不同地方使用可以自动展开成不同的值,常用的有 __FILE__ 和__LINE __。__FILE__ 展开为当前源文件的文件名,是一个字符串,__LINE__ 展开为当前代码行的行号,是一个整数。这两个宏不是用define指令定义的,它们是编译器 内建的特殊宏。在打印调试信息时,除文件名和行号之外,还可以打印当前函数名,C99 引 入一个特殊的标识符__func__ 支持这一功能。标识符__func__ 不属于预处理的范畴,但是 它的作用和__FILE __、LINE__ 类似,8所示。 __示例如表3. 宏定义包含无参宏定义和带参宏定义,无参宏定义也称为宏变量定义或变量式宏定义, 带参宏定义也称为宏函数定义或函数式宏定义。带参宏定义的语法为“#define宏名(形参 列表)宏体字符串”,宏名和形参列表之间不能有空格,例如“#defineMAX(x,y)x>y?x: y”。带参宏调用的语法为“宏名(实参列表),(”) 例如“max=MAX(a,b);,(”) 在宏调用时,用实 参a和b分别替换形参x和y,经预处理后宏展开为“max=a>b?a:b;”。 函数式宏定义和真正的函数调用的不同之处有:①宏函数定义中的形参是标识符,没 有类型,宏函数调用中的实参可以是表达式,在宏函数调用时只是进行符号替换,不存在值 传递的问题,不为形参分配内存单元,不做参数类型检查,所以宏函数调用传参时要格外小 心。真正函数的形参和实参是两个不同的量,各有各的作用域,调用时要把实参值赋给形 参,并且进行参数类型检查。②调用真正函数的代码和调用函数式宏定义的代码编译生成 的指令不同。真正函数的函数体要编译生成机器指令,而函数式宏定义本身不必编译生成 机器指令,宏函数调用替换为宏函数体。③定义宏函数时最好将宏函数体中的每个形参都 用括号括起来,比如#defineMAX(a,b)((a)>(b)?(a):(b))。如果省去内层括号,则为 #defineMAX(a,b)(a>b?a:b), 对MAX(x||y,i&&j)宏展开为(x||y>i&&j?x||y: i&&j), 可见运算的优先级发生了错乱。 尽管函数式宏定义和真正的函数相比有很多缺点,但只要小心使用,还是会显著提高代 码的执行效率,毕竟省去了分配和释放栈帧、传参等一系列工作,因此那些简短并且被频繁 调用的函数经常用函数式宏定义来代替实现。为了避免函数调用带来的开销,C99 还提供 了另外一种方法,即内联函数(ninefncto iluin)。 C99 引入了一个新关键字inline,用于定义内联函数。内联函数会在它被调用的位置上 展开,也就是说,内联函数的函数体代码会替换内联函数调用表达式,因此系统在调用内联 函数时,无须再为被调函数分配和释放栈帧,少了普通函数的调用开销,程序执行效率会得 到一定提升,所以内联函数的调用不同于普通函数的调用。C语言标准规定内联函数的定 义与调用该函数的代码必须在同一文件中。因此,通常同时使用函数说明符inline和存储 类别说明符staic定义内联函数,例如sainieitmx(nnrtrb;}, 这 tttcilnnaita,itb){euna>b?a: 种用法在Linux内核源代码中很常见。虽然内联函数可以避免函数调用带来的开销,但是 要付出一定的代价。普通函数只需要编译出一份二进制代码就可以被其他函数调用,而内 联函数只是将自身代码展开到被调用处,会使整个程序代码变长,占用更多的内存空间。采 用内联函数实质是以空间换时间。因此,建议把那些对时间要求比较高、代码长度比较短的 函数定义为内联函数。 56 3.2.3 条件编译 按不同的条件编译程序的不同部分,从而产生不同的目标代码文件,这称为条件编译。 条件编译有三种形式,如表3.f后面跟的是整型常量表达式,而#ifdef 9所示。#i 和#ifndef后面跟的只能是一个宏名。 表3.条件编译的三种形式 9 第一种形式第二种形式第三种形式 功能:若标识符1被define指令 定义过,则对程序段1进行编译, 若标识符2被define指令定义 过,则对程序段2进行编译,否则 对程序段3进行编译 功能:若标识符未被define指令 定义过,则对程序段1进行编译, 否则对程序段2进行编译 功能:若条件表达式1的值为真 (非0), 则对程序段1进行编译, 若条件表达式2的值为真,则对 程序段2进行编译,否则对程序 段3进行编译 条件编译主要应用于程序的移植和调试。条件预处理指令用于源代码配置管理的示例 代码,如表3.在xhort 10 所示。假设这段程序是为多平台编写的, 86 平台上需要定义x为s 型,在x64 平台上需要定义x为long型,在51 单片机(x51)上需要定义x为char型,对其他 平台暂不提供支持,就可以用条件预处理指示写。在预处理这段代码时,若满足不同的编译 条件,则包含不同的代码段。 表3.条件编译的两种形式 10 第1种形式运行结果第2种形式运行结果 3.2.4 文件包含 头文件是扩展名为.建议把所有的常量、全局变量和函数原型声明写 h的文件, 宏定义、 . 在头文件中,使它们可被多个源文件(c文件)引用。在源文件中使用头文件,需要用C预 处理指令include引用它。一条include指令只能包含一个头文件,若要包含多个头文件,则 57 需用多条include指令。引用头文件相当于复制头文件的内容,也就是用头文件的内容替换 include指令行,从而把头文件和当前源文件连接成一个源文件。由于头文件有两种(程序 员编写的头文件、编译器自带的头文件),因此文件包含指令有两种形式:“#include<头 文件名>”、“#include"头文件名"”。使用尖括号和双引号的区别在于头文件的搜索路径 不同,一般情况下,使用尖括号主要包含的是编译器自带的头文件,在文件包含目录中查找, 而不在源文件目录查找;使用双引号主要包含的是程序员编写的头文件,首先在当前的源文 件目录中查找,如果没有找到,再到文件包含目录中查找。文件包含目录是由程序员在设置 编译器环境时设置的。也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更 为强大。前面示例中一直使用尖括号引用标准头文件,其实也可以使用双引号,例如 #inlde"tih。stih是标准头文件,存放于系统路径(文件包含目录)下,所以使用 cusdo." do. 尖括号和双引号都能成功引用。自己编写的头文件一般存放于当前项目路径中,因此不能 使用尖括号,只能使用双引号。不过,如果把当前项目所在路径添加到系统路径(可以使用 gc 的-I选项添加),这样也可以使用尖括号,但是不建议这么做。建议读者使用尖括号引 用标准头文件,使用双引号引用自定义头文件,这样可以提高代码的可读性,很容易看是哪 类头文件。 如果一个头文件被引用多次,编译器会对该头文件处理多次,这将产生错误。为了防止 这种情况,标准的做法是把文件的整个内容放在条件编译语句中, 11 使用语法及示例如表3. 所示。ifndef指令后面的宏名的命名方法是将头文件名中的字母都改为大写,“.”修改为 “_”,这种命名方法可以尽可能避免一个项目中的宏名冲突问题。 表3.防止头文件被引用多次 11 条件编译语句示例,头文件headerfile.h 3.3 指针 指针是C语言中最重要的组成部分,使用指针编程便于表示各种数据结构,可提高程 序的编写质量、编译效率和执行速度。通过指针可使主调函数和被调函数之间共享变量或 数据结构,并且可以实现动态内存分配。 3.3.1 指针的基本运算 1. 指针和指针变量 在计算机中,所有要被CPU处理的数据都需要存放在内部存储器(内存)中。一般把 内存中的一个字节称为一个内存单元,为了正确访问这些内存单元,必须为每个内存单元进 行编号。内存单元的编号也叫地址。根据内存单元的编号或地址就可以准确找到所需的内 58 存单元。通常把这个地址称为指针。通俗地讲,指针就是地址,地址就是指针。对于一个内 存单元来说,单元的地址,即为指针,其中存放的数据才是该单元的内容。 存放指针的变量称为指针变量。一个指针变量的值就是某个内存单元的地址,或称为 某个内存单元的指针。严格地说,一个地址值是一个常量。一个指针变量却可以被赋予不 同的地址值,是变量。指针的值是一个地址。 2. 指针变量的定义 在C语言中,指针是有类型的,在定义指针变量时指定指针类型。例如“ int*pa, *pb;在同一语句中定义多个指针变量,每个变量都要有*号,定义指针的*号前后的空 格都可以省略,写成“”也算对,但*号通常和类型it之间留空格,而和变量名 写在一起。 int*p,*q;n,(”) 一种数据类型往往都占有一组连续的内存单元,例如一个double型的指针,它的值是 一个内存单元的地址,这个地址值其实是一个double型数据的首地址,这个数据实际占据8 字节的内存单元。也就是说,根据指针的值可以找到它所指向数据的首地址,根据指针类 型,可以知道它所指向数据占用多少个内存单元。指针变量的定义:“类型说明符*变量 名;”。例如,int(“) *p;表(”) 示p是一个指针变量,它的值是某个整型变量的地址。或者说p 指向一个整型变量。至于p究竟指向哪一个整型变量,应由向p赋予的地址决定。“float *f;”表示f是指向单精度浮点变量的指针变量。“char*c;”表示c是指向字符变量的指 针变量。一个指针应尽量始终指向同一类型的变量。 在栈上分配的变量的初始值是不确定的,也就是说,指针p所指向的内存地址是不确定 的,后面用*p访问不确定的地址就会导致不确定的后果,如果导致段错误还比较容易改 正,如果意外改写了数据而导致在随后的运行中出错,就很难找到错误原因了。像这种指向 不确定地址的指针称为野指针,为避免出现野指针,在定义指针变量时应该给它赋予明确的 int*p=NULL;*p= 初值,或者把它初始化为NULL,例如“,(”) 此时如果“0;,(”) 程序运 行时会段错误。NULL在C标准库的头文件ste.#dfnvi ddfh中定义“eieNULL((od*) 0)”,就是把地址0转换成指针类型,称为空指针,它的特殊之处在于,操作系统不会把任何 数据保存在地址0及其附近,也不会把地址0~0xf 的页面映射到物理内存,所以任何对地 址0的访问都会立刻导致段错误。“*p=0;”会导致段错误,相比之下,野指针的错误很 难发现和排除。 3. 指针变量的赋值 C语言提供了两个指针运算符:取地址运算符&和取内容运算符*。取地址运算符 &用来求变量的地址,取内容运算符*用来取出指针变量所指向的变量(存储单元)的值。 指针变量的赋值包括:①把变量地址赋予指针变量,例如“ inta,*pi,*p=&a;pi= &a;,(”) 定义指针变量p时赋初值,使用赋值语句为指针变量pi单独赋值;②同类型指针变 量相互赋值,例如“n*p*p&a;p=p;”字符串的首地址赋予指针变 ita,i,=i;③把数组 、 inta[*pa=char*pc="abc" 量,例如“5],a;;”;④把函数入口地址赋予指针变量。 把一个数值赋予指针变量是危险的,如“n=” it*p;p1000; 。 取内容运算符*之后跟的变量必须是指针类型,例如 “ *p;”。 inta,b=5,*p=&b;a= 59 正确的指针变量赋值示例:“charc,*pc=&c;inti,*pi= &i;”,内存布局如 图3.3左侧所示。这里的&是取地址运算符,&i表示取变量i的地址,表示 int*pi= &i; 定义一个指向int型的指针变量pi,并用i的地址初始化pi。还定义了一个字符型变量c和 一个指向c的字符型指针pc,注意pi和pc虽然是不同类型的指针变量,但它们的内存单元 都占8字节,因为是在64位平台,所以要保存64位的虚拟地址。 图3.变量的内存布局以及它们之间的关系 3 指针之间可以相互赋值,也可以用一个指针初始化另一个指针, it*pi=p” 例如“nti; 或“int*pti;pti=pi;表(”) 示pi和pti指向同一个变量。用一个指针给另一个指针赋值时 要注意,两个指针必须是同一类型的。pi是int*型的指针,pc是char*型的指针,pi= pi= int*)p c;这样赋值就是错误的。但是可以先强制类型转换,然后赋值:p(c;,把 char*指针的值赋给int*指针。 ac=ni=(n 错误的指针变量赋值示例:“chrc,*p&c;iti,*pit*)&c;”,内存布 局如图3.3右侧所示,此时pi指向的地址和pc一样,都是0x7f0e9f51f 。通过*pc只能访 问一字节,而通过*pi可以访问4个字节,后3字节已经不属于变量c了。pi指向的从 0x7f0e9f51f开始的4字节的数如果不是一个有意义的数,则肯定是一个错误的数。pi指 向的是从0x7f0e9f51f开始的4字节的整数,肯定是一个错误的数。 因此,使用指针要特别小心,很容易将指针指向错误的地址,访问这样的地址可能导致 段错误,可能读到无意义的值,也可能意外改写了某些数据,使得程序在随后的运行中出错。 为了描述方便,图3. 3没有考虑字节对齐的情况。 如果要改变pi所指向的整型变量的值,比如把变量i的值增加5,可以写成“*pi= *pi+5;”,这里的*号是指针间接寻址运算符,*pi表示取指针pi所指向变量的值,指针 有时称为变量的引用。 &运算符的操作数必须是左值,因为只有左值才表示一个内存单元,才会有地址,运算 结果是指针类型。*运算符的操作数必须是指针类型,运算结果可以做左值。所以,如果表 达式E可以做左值,则*&E和E等价,如果表达式E是指针类型,则&*E和E等价。 4. 指针变量的加减运算 对指向数组、字符串的指针变量可以进行加减运算。指向同一数组的两个指针变量可 以相减。对指向其他类型的指针变量作加减运算是无意义的。 若pa是指针变量,n是整数,则指针变量与整数的加减运算pa+n 、pa-n 、pa++ 、 60 ++pa、pa--、--pa等都是合法的。指针变量加或减一个整数n的意义是把指针指向 的当前位置(通常指向某数组元素)向前或向后移动n个位置(以指针指向的数据类型大小 为一个单位)。注意,数组指针变量向前或向后移动一个位置和地址值加1或减1在概念上 是不同的,因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的,示 例如下。 两个指针相减所得之差是两个指针所指向的数组元素下标的差,也就是两个元素相差 的元素个数,只有指向同一数组中元素的指针之间相减才有意义。两个指针变量的加法运 算没有意义。 5. 指针变量的关系运算 指针之间的比较运算比的是地址值,两个指针变量只有指向同一数组的元素时才能进 行比较或相减运算,否则运算无意义。指向同一数组的两个指针变量可以进行关系运算 (>、>= 、<、<= 、== 、!=)。假设pa1和pa2两个指针变量指向同一数组中的元素,pa1 ==pa2表示pa1和pa2指向同一数组元素;pa1>pa2表示pa1变量的值(地址值)大于pa2 变量的值,也就是pa1指向数组元素的下标值大于pa2指向数组元素的下标值;pa1pa2相反。 指针可与0比较,p==0表示p为空指针,不指向任何变量。 6. 通用指针 void* 类型指针称为通用指针或万能指针。可以将通用指针转换为任意其他类型的 指针,任意其他类型的指针也可以转换为通用指针,void* 指针与其他类型的指针之间可 以隐式转换,而不必用强制类型转换运算符。注意,只能定义void* 指针,不能定义void型 变量,因为void* 指针和别的指针一样都占8字节(64 位平台是8字节,32 位平台是4字 节), 如果定义void型变量(也就是类型暂时不确定的变量), 编译器不知道该分配几字节给 变量。void* 指针常用于函数接口。 7. 指针类型的参数 再看表3.wap函数。调用函数的传参过程相当于定义并用实参 3中采用地址传递的s 初始化形参,swap(&a,&b)这个调用相当于“int*pa=&a;int*pb=&b;,(”) 所以pa 和pb分别指向main函数的局部变量a和b,在swap函数中读写*pa和*pb就是读写 main函数的a和b。虽然在swap函数的作用域中访问不到a和b这两个变量名,但是可以 通过地址访问它们,最终swap函数交换了a和b的值。 3.3.2 指针与数组 数组是内存中的一组连续地址空间,其首地址是数组名。数组名作为数组的首地址,它 是一个常量指针。指向数组的指针变量称为数组指针变量。指向一维数组的指针变量称为 一维数组指针变量。指向二维数组的指针变量称为二维数组指针变量。 61 1. 一维数组 一维数组指针变量加1表示让指针指向后一个数组元素,对该指针变量不断加1就能 访问到该数组的所有元素。例如“5},*p,(”) 此时指针p指向 inta[6]={0,1,2,3,4,=a; a[0]。4所示。图3. 一维数组名和一维数组指针的用法如图3.4中说明了地址之间的关系 (a[1]位于a[0]之后4字节处)以及指针与变量之间的关系(指针保存的是变量的地址)。 可以让指针p直接指向某个数组元素,例如“p=&a[3];,(”) 或可以写成“p=a+3;”。由于数 组名a是常量指针,因此改变其值(如a++ 、++a)是错误的。在取数组元素时用数组名和 用指针的语法一样,但如果把数组名做左值使用,和指针就有区别了。例如pa++ 是合法 的,但a++ 就不合法,pa=a+1 是合法的,但a=pa+1 就不合法。一维数组元素及其地址 图3.一维数组名和一维数组指针的用法 4 的表示方法可以有多种方式,如表3. 12 所示。 表3.一维数组元素及其地址的表示方法 12 值各种等价的表示方法 a[0]的地址a a+0 p p+0 &a[0] &p[0] a[0]的值*a *(a+0) *p *(p+0) a[0] p[0] a[i]的地址a+i p+i &a[i] &p[i] a[i]的值*(a+i) *(p+i) a[i] p[i] 例如“0];p++;”,首先指针p指向a[0]的地址,由于后缀运算符 [] 的优先级高于取地址运算符&,所以是取a[0]的地址,而不是取a的地址。然后p++ 让p指向下一个元素(也就是a[1]), 由于p是int* 指针,一个int型元素占4字节,所以 p++ 使p所指向的地址加4,不是加1。既然指针可以用++ 运算符,当然也可以用+、 运算符,p+2 这个表达式也是有意义的,4所示,0], 则p+2 指向a[2]。 inta[6],*p=&a[ 如图3.p指向a[ *(p+2)也可写成p[2],p就像数组名一样,a[2]之所以能取数组的第2个元素,是因为它 等价于*(a+2),a[2]和p[2]本质上是一样的,都是通过指针间接寻址访问数组元素。由 于a做右值使用时和&a[0]是一个意思,所以“0];”通常写成更简洁的形式 “”。 int*p=&a[ iuc(n int*p=a; 在函数原型中,如果参数是数组,则等价于参数是指针的形式,例如vodfnit a[10]){} 等价于vc(t*a ){}。第一种形式方括号中的数字可以不写,如v oidfuninoidfunc 62 (n都表示这个参数是指 ita []){}。参数写成指针形式还是数组形式对编译器来说没区别, 针,之所以规定两种形式,是为了给读代码的人提供有用的信息,如果这个参数指向一个元 素,通常写成指针的形式,如果这个参数指向一串元素中的首元素,则写成数组的形式。 2. 二维数组 二维数组可以看成由若干个一维数组组成的数组,例如“3][3]={{0,1,2},{3, 4,5},{6,7,8}};int(*pp)[3]=a;”。该数组由三个一维数组构成,它们是a[0]、a[1]和 a[2]。把a[0]看成一个整体,它是一个一维数组的名字,也就是这个一维数组的首地址,这 个一维数组有3个元素,它们是a[0][0]、a[0][1]、a[0][2]。可以把二维数组看成一个一维 数组,其中每个元素又是一个一维数组。既然数组a可以看成一个一维数组,那么a[0]就 是其第0个元素(也就是第0行的首地址)、a[1]就是其第1个元素(也就是第1行的首地 址)、a[2]就是其第2个元素(也就是第2行的首地址)。 int*p=*a; inta[ 可以定义指向“一个变量”的指针,例如“”。也可以定义一个指向“一行变 量(一维数组)”的指针变量,其语法为“类型说明符(*指针变量名)[ 一行元素的长度];”, 类型说明符为所指数组的基本数据类型。*表示其后的变量是指针类型。例如“int(*pp) [3]=a;”,定义了一个指针变量pp,其指向固定有3个元素的一维数组。“int(*pp)[3]= 0];”等价于“t(3]a;”。此时执行pp++ 操作后,pp 指向二维数组a的下一 &a[in*pp)[= 行的首地址,即pp 的实际地址值增加12 字节。 5所示。图3.二维数组名和二维数组指针的用法如图3.5中说明了地址之间的关系以 及指针与变量之间的关系。 图3.二维数组名和二维数组指针的用法 5 把二维数组a看成一维数组后,其元素a[0]的大小就是原二维数组一行所包含元素的 个数。所以,第0行的首地址是&a[0], 也可以写成a+0(也就是a[0][0]的地址)。同理, 第一行的首地址是&a[1], 也可以写成a+1(也就是a[1][0]的地址)。既然数组a[0]是一 个一维数组,那么a[0][0]就是其第0个元素(也就是二维数组的第0行第0列)、a[0][1]就 是其第1个元素(也就是二维数组的第0行第1列)、a[0][2]就是其第2个元素(也就是二 维数组的第0行第2列)。 a[0]是一个一维数组,那么a[0]本身就是这个数组的首地址。根据一维数组元素地址 63