考纲
C语言的基本构成
常量、变量和表达式
常量
数字常量
整数
可由十进制,十六进制,八进制,二进制表示,其中十六进制以0x
开头
十进制数转换为十六进制数的基本方法是对需要转换的十进制数用16连续整除,将整除后的余数按顺序作为十六进制数中由低到高各位的数字,直至商等于0。下面我们看一个例子。
1 | 12345/16→771(9) |
十六进制数各位的权重分别是16的整数次幂,从低位到高位逐位递增,其最低位的权重为16。这样,在将十六进制数转换为十进制数算法时,只需对该十六进制数从低位到高位将分别乘以各位的权重,并将结果累加起来即可。下面我们看一个例子。
1 | 因为十六进制数0x1357的十进制值可以表示为 |
此外,在描述一个整数时,也可以同时说明该数在计算机中的保存格式。一个整数常量在计算机中可以保存为普通整数或长整数。长整数需要在数字后面加上后缀L,例如,56L 0x12345L、0L等都是长整数。普通整数则不加后缀。普通整数和长整数的区别取决于具体的计算平台。在有些计算平台上,这两种保存方式是相同的,而在有些计算平台上,表示长整数所使用的二进制位要多于普通整数。
实数
对于实数,在C语言中有两种基本表示方法。
第一种方法和日常算术中的书写方式相同, 即直接使用十进制数字表示数据的整数和小数,并使用小数点分隔数字的整数部分和小数部分。与常规方法略有不同的是,当整数或小数部分为0时,相应的部分可以不写数字0,而只写上小数点即可。下面是这种表示方法的几个例子:
1 | 0.12 |
第二种方法被称为科学表示法。这种方法把一个数据表示为有效数字部分和指数部分。有效数字部分可以是整数,也可以是用常规方式表示的小数。指数部分是以字母e或E开头的整数,可以带正负号,表示有效数字所要乘以的10的幂。下面是几个用科学表示法表示的小数:
1 | 0.12E3 // 120.0 |
此外,同一个实数,在C语言中也有两种不同的保存类型:一种是单精度实数,另一种是双精度实数。两种数据类型在数值的表示范围、数据的有效数字位数以及所占用的存储空间等方面都不相同。C语言中默认的实数类型是双精度类型,上面的几个例子所表示的都是双精度实数。当需要描述单精度实数时,需要在数据后面加上后缀f或F。下面是几个单精度实数的例子:4.5f、6.3F、0.123E3f、5.6E-6F
字符常量
用一对单引号引起来的单个字符
1 | '0' //数字0 |
在ASCI编码中,数字字符09,字母az、A~Z之间的所有字符都是连续编码的。这就可以使我们在不知道这些字符的具体编码的情况下对这些字符的编码进行计算。例如,字符’5’的编码等于’0’+5,也就是0x35;’a’+2等于’c’;’8’-‘0’的结果等于8;等等。ASCⅡ编码的这一特点在对字符处理的程序中是会经常用到的。
字符串常量
用一对双引号引起来的0个或多个连续的字符。
变量
必须是合法的标识符。一个标识符是由字母或下划线开头,由字母、数字和下划线组成的字符串。a、ab、_cd、_6、x8
等都是合法的变量名,而3x、5b、x+y、b.3、sum*co、x%y
等则不是合法的变量名。
定点型又称为整型,可分为char,short,int,long四个小类,每个小类还根据能否表示负数分为有符号数(signed)和无符号数(unsigned)。
1 | int a,b,year_days; |
设某整型数据的长度是n个二进制位,则其无符号类型的表示范围是02^n - 1;其有符号类型的表示范围是-2^n-12^n-1 - 1。
浮点型数据使用标准数据格式,分为float和 double两个小类,与计算平台的硬件无关。foat类型是单精度类型,长度为32位二进制位,占4个字节,其有效数字大约相当于十进制的7位,表示范围约为-3.4x10^383.4x10^38,能表示的绝对值最小的数值为±10^-44.85。
double类型是双精度类型,长度为64位二进制位,占8个字节,其有效数字大约相当于十进制的15位,表示范围约为-1.7×10^308~1.7x10^308,能够表示的最小绝对值约为10^-323.3。下面是一些浮点数变量定义的例子:
1 | double sum, avg, salary; |
数据宽度:double > float > int > short > char,将较窄类型的表达式向较宽类型的变量赋值不会有任何问题。
算数表达式
算数运算符
模操作只能应用于整数,运算优先级与乘除法相同。
进行算数运算时,运算符两端运算对象的类型必须一致,如果不一致就会进行自动类型转换,将较窄的数据类型转换为较宽的数据类型。当触发运算符两端的运算对象都是整数时,除法按整除规则进行,即除法的商只保留整数部分
位运算符
位运算只能作用于整型数据,是对数据以二进制位为单位进行的二元运算,包括对数据中二进制位的移动,以及两个数据中对应位的按位运算。
移位运算包括对数据的左移和右移,运算符分别是<<和>>。x<<y的结果等于x的值左移y位,并在移位后空出来的低y位补0。x>>y的结果等于x的值右移y位,并根据x的符号类型在移位后空出来的高y位补位:如果x是有符号整数,则补x的符号位,否则补0。下面是几个移位运算的例子:
1 | int x = 1, y = 2, z = 125, a, b, c; |
将一个整数左移n位等价于将该整数乘以2^n,将一个整数右移n位等价于将该整数除以2^n。如果左移结果超出表示范围则会造成错误。
按位运算包括“按位与”、“按位或”和“异或”,其运算符分别是&、|和^。在这些运算中,数据被看作是一个个独立的二进制位,而不是一个整体。两个运算数据中对应的二进制位进行运算,不受数据中其他二进制位的影响,运算结果也不影响其他二进制位。表2-5是这些运算符的运算规则
强制类型转换
(<类型名>) <表达式>
1 | int a = 5, b = 7; |
输出输入
输出
输入
常量的符号表示方法
常量宏
# define
是编译预处理中的宏定义命令,使用排 define可以定义一个符号和它所代表的字符串。在编译时这一符号被替换为对应的字符串。例如:
define PI 3.141592653
就定义了一个符号PI,它代表的是π的近似值3.141592653。在此后的表达式中,凡是需要用到π的近似值的地方,就都可以用符号PI来表示,而不必写3.141592653了。
枚举常量
enum {A, B, C, D, E = 50, F, G, H, I, J, K};
上面语句中的枚举符表包含11个枚举符,定义了从A到K共11个枚举常量。当枚举符的形式为“标识符=常量表达式”时,该校举常量的值等于该常量表达式的值。当枚举符只是个标识符时,该枚举常量的值等于其前面枚举常量的值加1。当第一个枚举符只是一个标识符时,该枚举常量的值等于0。因此在上述枚举常量中,从A到D的值分别是从0到3;从E 到K的值分别是从50到56。
条件语句和开关语句
运算符的优先级
循环语句
do while
do while语句至少会执行一次
循环语句选择
从功能上讲,上述三种循环控制语句基本相同,没有本质上的区别。一般来说,用一种循环语句实现的描述也可以使用其他类型的循环语句实现。例如,我们在前面的例子中看到了分别使用 while语句、for语句和 do while语句实现的求最大公约数的程序。在程序中使用哪种语句经常受到编程人员个人习惯的影响。但是如果能够根据程序的具体情况选择最适当的语句就可以使代码显得更加精练、自然和易于维护。
般情况下 while和for语句可以直接互换。当使用 while I语句替换for语句时,需要将循环的初始化操作放置到 while语句的前面,将修改循环控制状态的操作放在循环体的最后。当使用for语句替换 while语句时,只需将循环初始化操作和循环控制状态修改操作放在for语句圆括号中相应表达式的位置上即可。由于在for语句中可以一目了然地看清与循环相关的各个元素,在没有特殊要求和强烈的个人偏好时,在一般的循环计算描述中往往选择使用for语句。
当在循环中既没有循环初始操作,也没有循环控制状态修改操作时,多选择使用 while语句。
例如,在程序中经常遇到的一种情况是从外部的文件或标准输入设备上读入数据直至数据的结尾。这时,循环结束的控制条件只取决于外部数据。在循环操作中既不需要做任何的数据初始化,也不需要修改任何与循环控制相关的状态。在这种情况下选择使用 while语句就更加自然了。
goto 语句
语句是和语句标号一起使用的。语句标号在程序中标志一条语句的位置,其语法格式与普通的标识符相同。在程序中使用标号时,需要将语句标号的标识符放在一条语句的前面, 并用一个冒号将这个标识符与它所标志的语句分开。下面是一个标识号的例子:
A: x = y + z;
这里标识符A就是语句“x=y+z”的标号。
goto语句的语法格式如下:
goto <标号>
函数
函数的调用
函数调用是C程序中最常用的语句之一。从程序执行的角度看,当一个函数被调用时,程序执行该函数定义中的各个语句。在函数执行完毕后,程序的控制权就返回给了函数的调用者:被调用函数的返回值被放在函数调用的位置,程序将继续执行后续的语句。从语法的角度看,函数调用就是一个表达式,并且具有该函数返回值的类型,因此可以被用在任何需要相应类型表达式的地方。
一个函数在被调用时,编译系统必须知道它的函数原型,以便对函数实际参数和返回值的类型进行检查和必要的类型转换。当函数定义在调用该函数的语句之前时,自然就说明了该函数的原型。如果函数定义在调用该函数的语句之后,或者定义在与函数调用不同的源文件中, 就需要在函数调用之前使用函数说明语句说明该函数的原型。在程序中经常可以见到这样对函数进行说明、定义和使用的模式:一个函数首先由函数原型说明语句进行说明,在后续的程序段落中包含有调用该函数的语句,而对该函数的定义则可能放在调用该函数的程序段落之后。
函数说明语句的语法格式如下:
<返回值类型> <函数名> (<参数表>)
一个函数在一个程序中只能定义一次,但是其原型可以在程序中多次说明,只要各次说明是一致的即可。在一个程序中使用函数说明语句多次说明一个函数的情况是经常出现的。
函数调用关系和返回值
C程序中的函数之间没有从属关系,也不可以嵌套定义:一个函数不能被定义在另一个函数体之中。即使某个函数只被一个函数调用,也必须定义成为一个独立的函数。对于函数之间的调用关系,C语言没有任何限制:一个函数可以调用程序中的其他函数,也可以被程序中的其他函数调用。无论被调用的函数是标准库函数还是编程人员自行定义的函数, 只要被调用函数的原型在对该函数调用的语句之前声明过,并且在编译时有定义即可。C语言对于函数嵌套调用的深度没有理论上的限制,函数嵌套调用的深度只受运行环境所提供的资源的限制。对于一般的程序,这些资源所能保证的嵌套深度远远超过了程序正常运行的需要。
局部变量和全局变量
在C程序中,变量既可以定义在函数的内部,也可以定义在函数的外部。定义在函数内部的变量称为自动变量,也称为局部变量;定义在函数外部的变量称为外部变量,也称为全局变量。局部变量和全局变量是两类存储性质不同的变量,其有效期间和使用范围都不相同。
局部变量
在没有被初始化也没有被赋值的情况下,局部变量的值是一个没有意义的不确定的值。
根据C语言的规定,一个函数中所有的局部变量必须集中定义在函数体中第一个执行语句的前面,而不能穿插在执行语句之间。下面是一个这类错误的例子:
1 | int main() |
全局变量
全局变量具有确定的默认初始值。无论是浮点数还是整型数,其默认的初始值均为0。因此如果在程序中需要某个全局变量的初始值为0,就不必再为它赋初值。全局变量的这一特点也使得调试与全局变量初始值相关的错误比调试与局部变量未赋初始值相关的错误更容易些,因为如果一个程序运行的错误是由某个全局变量未被正确地设置初始值而引起的,那么这个错误是确定的,并且是可重复的。
当全局变量被分散地定义在程序的多个地方时,如果某个函数需要使用在其后面或在其他源文件中定义的全局变量,就需要使用变量声明语句说明该变量的类型。变量声明语句的语法格式如下:
extern <类型> <变量名> [ <变量名> ...];
全局变量与局部变量分处于不同的命名空间,因此相同变量名的局部变量和全局变量不发生冲突。全局变量在程序运行时始终存在,并且可以被其后面定义的所有函数使用,只要该函数中没有与该全局变量重名的局部变量。当在函数中访问一个变量且该函数中有同名的局部变量时,程序选择该局部变量。当该函数中没有同名的局部变量时,程序选择此前定义或声明过的全局变量。
函数内部使用的规模较大的数组应定义为全局变量,这一点在第6章中将会进一步说明。
一般情况下,在函数中应避免对全局变量的直接访问,而应通过函数的参数间接进行。这样做的目的是保证函数的独立性,使函数的行为只受函数代码和参数的控制,而不会通过全局变量受到其他函数隐含或间接的影响。这样既可以使函数功能的描述准确清晰,也便于函数代码的调试和维护。
在函数中直接访问全局变量最常见的情况有两种。一种是函数的功能较为复杂,需要访问和处理大量全局性数据或与其他函数共享的数据。这时如果仍然使用参数传递这些数据,会使得函数的定义和调用显得臃肿,而适当地直接访问全局变量会使函数的定义和调用简洁一些。
另一种是函数在执行过程中需要使用一些与程序中某些整体结构或配置相关的外部数据。这些数据具有含义明确的变量名。在函数内部直接使用这些全局变量不但可以减少函数参数的传递,而且可以使函数代码的描述更加清晰。
标准库函数
常用的数据输入/输出函数
除了说明输出字段的数据类型外,在函数pint()的字段说明序列中还可以规定其他格式细节,如字段宽度、小数位数等。下面是一个完整的字段说明的字符序列的语法格式:
[ flags ][ width ] [.precision ]type
字符类型判断函数
字符串处理函数
数组
在定义数组时也可以同时对它进行初始化,即对数组中的元素赋初值。当不对数组进行初始化时,全局数组变量中各个元素的初始值是0,而局部数组变量中各个元素的初始值不确定。在对数组元素初始化时,需要按照元素下标的顺序将初始值放在由大括号括起来的初始化数据表中。例如,下面是一条数组定义和初始化的语句:
double angles[6] = {0.1, 0.3, 0.6, 6.5, 2.8, 3.2}
字符串和字符数组
字符串
字符串经常以常量的形式出现。以常量形式出现的字符串必须由一对双引号引起来。这时,字符串结東符’\0’隐含在字符串的末尾,而不需直接写出。根据规定,字符串结束符不计算在字符串的长度之内。例如,”a+b-c”是一个包含有6个字符的字符串常量,其中最后一个字符就是\0,但在计算字符串长度时,只统计\0之前的5个字符,因此长度为5。在对双引号中不包含任何字符的字符串(“”)称为空串,它只包含一个字符串结東符\0,因此长度为0。
很多时候,字符串也以字符数组内容的形式出现。此时,字符数组中在字符串的结尾处必须是一个字符串结東符\0。否则,我们只能说该数组保存了一个字符序列,而非字符串。
由于字符串的末尾需要包含\0,字符串所占用的数组元素数量等于字符串长度加1。实际上,字符串常量在程序内部也是保存在字符数组中的,并在其后以字符\0
来标志字符串的结東。
与一般保存在字符数组中的字符串不同的是,字符串常量位于特殊的存储区域,其内容在字符串定义时被初始化、以字符串结束符\0结尾,并且只供用户程序以只读的方式访问。
试图对字符串常量中的内容进行修改是一种非法的操作,有可能引起无法预期的程序运行错误。
字符数组
char str[64]
定义了一个包含有64个元素的字符数组,数组中的每个元素均为单个字符。一般的字符数组可保存任意字符序列,其内容不一定是字符串,并且可以被程序自由地读写。只有当数组中的字符序列以\0结東时,才可以说该字符数组中保存了一个字符串。
全局数组元素的初始值均为0,也就是字符串结束符’\0’,而未指定初始值的局部数组元素的初始值不确定。字符数组的初始化有两种方式。第一种方式是把所要初始化的内容以字符的方式依次放在初始化表中。例如,如果我们想把字符串” Hellot”作为初始化的内容保存在全局数组str1[]中,就可以写成下面的语句:
`char str_1[64] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’};
数组str_1[]前5个元素的值分别是H、e、l、l、o,而其余未被初始化的数组元素的内容是’\0’。为了描述方便起见,C语言中也提供了直接使用字符串作为字符数组初始化内容的方式。使用这种方式,上述的语句可以改写为下面的形式:
1 | char str_1[] = {'H', 'e', 'l', 'l', 'o'}; |
但是,由于未显式地给出字符数组的长度,上述两种初始化方式的含义是不同的。在初始化完毕后,数组str_1中只有5个元素,分别保存着初始化表中的5个字符,但是没有字符串结束符,因此并不是一个完整的字符串。而数组str_2中有6个元素:除了用于初始化的字符串中的5个字符外,还包括字符串结束符’\0’,因此是一个完整的字符串。
常用的标准字符串函数
字符串输人/输出函数的原型定义在标准头文件< stdio. h>中,其他各类字符串处理函数的函数原型定义在标准头文件< string. h>中,程序在使用这些函数前必须通过# include 引用这些头文件。
字符串输出函数
puts()
函数在标准输出上输出参数字符串s,并在其结尾输出一个换行符。该函数的参数既可以是一个字符串常量,也可以是一个字符数组,但其中的内容必须是一个以\0结尾的字符串,否则函数在执行时可能会产生难以预料的运行错误。下面是一个使用函数puts()的例子:
1 | char a[] = "hello"; |
它会自动输出换行符
字符串输入函数
scanf()
定的内部格式。 scanf()对字符串的字段格式描述符是%s,表示从标准输入上读入以空格、tab 键或换行符分隔的字符序列,并且不包含用于分隔各个输人字段的空白符,也不进行任何数据转换。格式描述符%s在变长参数表中所对应的参数必须是一个字符数组,并且数组的大小必须能够容纳所输入的字符串。下面我们看一个利用 scant()读入字符串的例子。
1 |
|
gets()
char *gets(char s[]);
1 | char string[N]; |
这段代码将标准输人上的字符序列保存到数组 string中。当函数运行正常时,条件语句中的pint()将其输出到标准输出上。因为gets()不保存输入字符序列中的换行符,所以需要在printf()格式串中加上换行符\n,以便使输出的数据单占一行。与 scanf()读人字符串时的情形相同,函数gets()在从标准输入上读入字符序列时也不检査参数数组s[]的大小。使用者必须保证参数数组中有足够的空间存储读入的字符串,否则程序在运行时可能会产生难以预期的错误。
fgets()
函数 fgets(()的第一个参数与gets(O)的参数相同,是保存读入数据的字符数组;第二个参数是一个整数,说明保存读人数据的字符数组的长度;第三个参数的类型FILE*是文件指针, 说明读入数据的来源。对应于标准输入,需要使用系统定义的标识符 stdin。与函数gets()相类似,fges()从标准输入上读入一个连续的字符序列,直至换行符或输人数据的结尾,将其保存在参数s指定的字符数组中,并在输人数据的末尾加上字符串结束符\0。其与gets()的不同之处有三点。第fgets()最多只读入n-1个字符,以确保任何输人数据都不会造成缓冲区的溢出。第二,如果缓冲区s足够大,并且输入数据中包含换行符,则该换行符会被保存在缓冲区s中。第三,在使用fgets()时必须指明数据的来源,例如使用标识符 stdin说明数据来自标准输人。下面是个使用 fgets()的例子:
1 | char string[N]; |
字符串复制与追加函数
这些标准函数包含在<string.h>
中
标准字符串复制函数有两个,原型如下:
1 | char *strcpy(char dest[], char src[]); |
字符串追加函数:
1 | char *strcat(char dest[], char src[]); |
字符串比较函数:
1 | int strcmp(char s1[], char s2[]); |
字符串检查函数
检查字符串长度:
1 | int strlen(char s[]); |
返回字符串除了\0的字符数量
二维数组
当定义一个二维数组时,可以同时对其中的各个元素进行初始化。在对二维数组初始化时,每一行的初始值由大括号括起来的初始化表描述,各行的初始化表之间用逗号分隔,外面再用一对大括号括起来。例如,下面的语句定义并初始化了一个2行12列的二维数组,表示平年和闰年的每个月各有多少天:
1 | data_tab[2][12] = { |
如果一个二维数组定义中对行数没有显式地说明,则该数组的行数由其初始化表中的行数决定。例如,上面的二维数组day_tab的定义也可以写成下面的样子
day_tab[][12]={{31,28,31,30,31,30,31,31,30,31,30,31 31,29,31,30,31,30,31,31,30,31,30,31}};
但是在任何情况下,在二维数组的定义中必须显式地说明该数组的列数。
访问数组时采取的方式是i * M + j
,其中M为列数
###作为参数的二维数组
当一个函数的参数是二维数组时,在函数的参数表中只需要说明该二维数组的列数以及数组元素的类型,不必说明数组的行数。这与一维数组作为函数参数时的情况有些类似:当一维数组作为函数的参数时,只需说明该参数是一个一维数组以及数组元素的类型,不必说明数组元素的数量。作为函数参数的二维数组的这种说明方式表明,一个函数对于实际参数数组的行数没有限制。当函数被调用时,只要实际参数所表示的二维数组的类型及列数与形式参数相同即可。
指针
指针变量
<类型> *<变量名>
1 | int *pi; |
数组元素等价于普通变量,其地址可以赋值给类型相同的指针变量。数组名的值是该数组下标为0的元素的地址,因此数组也可以直接赋值给类型相同的指针变量。下面是几个将数组赋值给指针变量的例子:
1 | int i_arr[MAX_N], *pi; |
指针运算
指针与整数的加减
对于指向数组中某个元素的指针,加上一个整数n表示让其指向当前位置后面第n个元素, 而减去一个整数则表示使其指向前面第n个元素。例如,对指针变量p的赋值p=&arr[5]使p 指向了数组arr[]中的下标为5的元素。在执行了p=p+3之后,p就指向了ar[]中的下标为8 的元素。
只有指向同一数组中元素的指针之间オ可以相减。指针相减所得到的结果是一个int型的整数,表示这两个指针所指向元素之间下标之差。
指针的比较
常用的指针比较有两种:第一种是两个指针间的比较,第二种是指针与0的比较。在程序中经常需要判断两个指针是否相等,即两个指针是否指向同一个元素。此外,有时也会比较两个指向同一数组中元素的指针的大小,以判断其所指向的元素在数组中的前后顺序。
指针与0的比较是编程中常用的一种比较,它多与对指针的赋0值一起,用于对指针进行标记和判断指针是否有效。在指针未指向任何实际的存储单元时,或指针所指向的存储单元已经不存在时,需要将其标记为无效指针。按照C语言程序设计的惯例,一般将无效指针赋值为0。这样,在通过指针对一个存储区进行访问之前,就可以通过判断指针的值是否等于0来判断该指针是否有效。为了表示指针的0在类型上不同于整数类型的0,在C的标准头文件中定义了一个等于0的符号常量NULL。在C语言的标准库函数中,几乎所有需要返回某种类型指针的函数在遇到异常情况或运行错误而无法实现其正常功能时,都会返回NUL。
指针的强制类型转换和void *
指针的强制类型转换操作与其他类型数据的强制类型转换操作方式相同,即在指针前加上以圆括号括起来的目标类型。下面是几个指针的强制类型转换的例子:
1 | int *ia, *ip, n, arr[3][6]; |
这一空间所存储数据的类型。为了描述这种情况,C语言中定义了通用指针类型void*
。与一般指针类型不同的是,具有void*
类型的指针可以赋给任意类型的指针变量,具有void*
类型的指针变量可以接受和保存任意类型的指针。这样就避免了很多不必要的指针类型转换。函数malloc()和fre()都使用了void*
类型,它们的函数原型定义如下:
1 | void *malloc(size_t size); |
下面的语句为int型指针p申请保存一个整数的存储空间
int*ip malloc(sizeof(int));
而下面的语句为 double型指针id申请保存200个 double类型数据的存储空间:
double *id malloc(200 *sizeof(double))
指针类型与数组类型的差异
指针类型和数组类型尽管在很多情况下可以互换使用,但是它们仍然是两种不同的类型, 其间的差别也是显著的。数组和指针之间的主要区别有三点。
首先,数组是一片连续的存储空间,在定义时已为所有的数组元素分配了位置,而指针只是一个保存数据地址的存储单元,未经正确赋值之前不指向任何合法的存储空间,因此不能通过它进行任何数据访问。使用指针时常见的错误就是在没有对指针正确赋值前通过指针保存数据。例如,下面的代码:
1 | double d, *dp; |
就是一个这种类型的错误:指针ρ未被赋值,没有指向任何有效的存储空间。当通过该指针进行间接赋值时,数据被写人一个未知的地址。这类指针一般称为野指针,是引起无法预知的程序运行错误,特别是引起程序崩溃的最常见原因。
其次,通过数组所能访问的数据的数量在数组定义时就已确定,即数组元素的个数。而通过指针所能访问的数据的数量取决于指针所指向的存储空间的性质和规模。例如,如果一个指针只是指向一个变量,那么通过这个指针就只可以访问该变量;而当这个指针指向一个数组或由动态内存分配获得的存储空间时,通过这个指针就可以访问该数组或存储空间中所有的元素。
第三,数组名是一个常量而不是一个变量,是与一片固定的存储空间相关联的。我们可以对数组元素赋值而不可以对数组变量本身赋值。而指针变量本身是一个变量,可以根据需要进行赋值, 从而指向任何合法的存储空间。
指针与数组
实际上,除了数组可以赋值给指针变量,以及在函数参数中指针类型与数组类型可以互换外,在表达式中数组与指针也可以互换。对下面的代码:
1 | int i, a[N], *p = a; |
我们可以把其中的a[i] = i * i
改写为下面的任意一种:
1 | *(a + i) = i * i; |
指向二维数组的指针
一个指向一维数组的指针所指向的数据实体是一维数组中的元素,而一个指向二维数组的指针所指向的数据实体是二维数组中的一行元素。如果我们把一个二维数组看成是一个由各行元素组成的一维数组,就可以发现,无论是一维数组还是二维数组,指针与数组的关系是相同的,不同的只是数组元素的类型。指向二维数组的指针所直接指向的数据实体不是单个的数组元素,而是一个一维数组,其所包含的元素个数等于该二维数组的列数。因此在定义一个指向二维数组的指针时,不但需要说明该数组元素的类型,而且需要说明该二维数组的列数。根据语法,一个指向类型为<类型>
的M行N列二维数组的指针变量可以定义如下:
<类型> (*<标识符>)[N];
1 | double a_arr[32][64], b_arr[64][128], c_arr[16][128]; |
在上面的例子中,指针bp既可以指向数组b_ar,也可以指向数组c_ar,或者这两个数组中的任意一行,因为这两个数组的元素类型以及列数与bp的定义相同;指针即则只能指向a_ar或其中的任意一行,但不能指向b_am和e_arm,因为这两个数组的列数与ap的定义不同。
一个二维指针被赋值后就指向了二维数组中的一行,或者说指向了从那一行开始的二维数组中的各行。在上面的代码中,ap=a_ar使得即指向了a_arr中下标为0的行,即从该行开始的整个二维数组。此时ap等价于a_arr。对二维指针既可以按指针方式操作,也可以按数组方式操作。例如,*ap或ap[0]
都等价于a_arr[0]
;(*ap)[3]、*(*ap+3)或ap[0][3]
都等价于a_ar[0][3]
。对p的第二次赋值bp=&c_ar[5]
使得bp指向了c_arr中下标为5的行。
此时bp等价于从c_arr[5]
开始的二维数组,*bp或bp[0]
都等价于c_arr[5]
,(*bp)[3]、bp+3)或bp[0][3]
都等价于c_ar[5][3]
。
需要注意,当指针操作与下标操作混合使用时需要注意这两种操作与指针的结合关系。例如,*ap[3]与(*ap)[3]
所表示的是两种完全不同的含义。对于*ap[3]
,指针ap 首先与[3]
结合,再与*
结合,因此*ap[3]
等价于ap[3][0]
;而(ap)[3]
中指针ap首先与*
结合,再与[3]
结合,因此(*ap)[3]
等价于ap[0][3]
。
多重指针
简单地说,多重指针就是指向指针的指针。例如,一个指针变量的地址就是一个二重指针。多重指针是通过在变量名左側使用多个一元运算符*
定义的,变量名与类型名之间*的个数就是指针的重数。一般情况下,多重指针变量保存的是比其低一重的指针变量的地址。例如,二重指针变量保存的是普通指针变量的地址,三重指针变量保存的是二重指针变量的地址,依此类推。下面是几个多重指针的例子
1 | int **ipp, *ip, *ip2, i, j; |
指针数组
元素类型为指针的数组称为指针数组。在较为复杂的程序中,指针数组常常用作数组等各类数据的索引,以便有效地组织数据、简化程序、提高程序的运行速度。
一维指针数组
int *p_arr[N];
1 | double d1[N], d2[2 * N], d3[3 * N], avg, sum; |
一般来说,指针数组与二维数组的区别有以下三点:
- 指针数组中只为指针分配了存储空间,其所指向的数据元素所需要的存储空间是通过其他方式另行分配的。
- 二维数组每一行中元素的个数是在数组定义时明确规定的,并且是完全相同的;而指针数组中各个指针所指向的存储空间的长度不一定相同。
- 二维数组中全部元素的存储空间是连续排列的;而在指针数组中,只有各个指针的存储空间是连续排列的,其所指的数据元素的存储排列顺序取决于存储空间的分配方法, 并且常常是不连续的。
函数指针
函数名表示的是一个函数的可执行代码的入口地址,也就是指向该函数可执行代码的指针。
函数指针类型为提高程序描述的能力提供了有力的手段,是实际编程中一种不可或缺的工具。
函数指针类型是一种泛称,其具体的类型由函数原型确定。例如,可以指向具有两个double型参数、返回值类型为int的函数的指针在类型上就不同于可以指向具有一个 double型参数、返回值类型为 double的函数的指针。每一种具体的函数指针都必须保存在与其类型匹配的函数指针变量中。定义一个函数指针类型的变量需要按顺序说明下面这几件事:
说明指针变量的变量名。
说明这个变量是指针。
说明这个指针指向一个函数。
说明这个变量所指向函数的原型,包括参数表和函数的返回值类型。
按照顺序说明这几件事,需要借助于必要的括号,按下列方式进行:<类型> (*<标识符>) (<参数表>);
1 | double (*func)(double x, double y); |
结构和联合
结构
定义一个结构类型的变量的语法有两种。第一种方法是在结构类型定义的后面直接跟上变量名表,例如:
1 | struct pt_3d { |
1 | struct { |
1 | struct pt_3d pt_3d[N], pt3_4; |
联合
联合的作用是使一组类型不同的变量共享同一块存储空间。换一个角度看,也可以认为联合使得一个变量可以根据需要存储不同类型的数据,或者可以对同一个数据按不同的类型进行解释。定义和使用联合类型的语法以及相关的术语与结构类型很相似,只是将关键字 struct换为 union。联合与结构的根本区别在于,结构中各个成员变量的存储空间是独立的,而联合中各个成员变量的存储空间是共享的,因此在任一时刻,一个联合中只能保存一个数据。
定义例子:
1 | union data_t { |
在上面的例子中,联合类型data_t有3个数据长度各不相同的成员变量sum、nane和salary。当成员变量的数据长度不同时,联合类型的数据长度等于其中最长的成员变量的长度。
1 | union { |
当访问联合体的不同属性时表达方式会不同。
类型定义(typedef)语句
1 | typedef int Length; |
输入/输出和文件
输入/输出的基本过程和文件类型
文件的打开、创建和关闭
打开文件的函数是 fopen(),它的函数原型如下:
FILE fopen (const char *path, const char *mode );
其第一个参数path是一个字符串,指定需要打开的文件的路径名。路径名的描述必须符合运行平台对文件路径名的规范。例如,在 UNIX/Linux系统上,”/home/yin/ sprog/file.c”、”./test”、”doc等都是合法的路径名。在 Windows系统上,”C: \Windows\system32Nabc.dlln、”\debug \data. txt”等也都是合法的路径名。需要注意的是, Windows系统使用反斜线作为目录的界限符,而反斜线在C语言中是作为转义引导字符使用的。因此在C程序中,带目录名的路径名必须双写反斜线符。例如,上述路径名需要写成”C: \l Windows \l system32 \abc.dll 和” \debug \data.txt”。此外,在 Windows系统上不区分文件名中字母的大小写,而在UNIX Linux系统上,文件名中字母的大小写是严格区分的。
函数 fopen()的第二个参数mode也是一个字符串,指定打开文件的方式。该字符串由一个或多个字符组成。在 UNIX/Linux平台上,这些字符串及其含义如表10-2所示:
文件打开的结果是生成一个FILE类型的数据结构,保存与被打开文件相关的属性和资源, 一般称为字符流。函数open()的返回值就是一个FILE*类型的指针,指向被打开的文件。在随后的读写操作中,相应的函数需要使用这一指针说明所要操作的文件。当 fopen()无法打开指定的文件时,返回NUL。当需要向用户报告 fopen()失败的原因时,可以使用函数 peror()。
该函数的原型如下:
void perror(const char *string);
perror()首先在标准错误输出 stderr上输出参数 string,然后再输出前一个执行失败的库函数所产生的错误信息,说明错误产生的原因。
1 | int main() |
在 Windows平台上,在上述打开方式字符串中还可以加入字符b或t,分别表示文件按照二进制方式和正文方式打开。例如, fopen(“ile_1”,”b”)表示按二进制的读方式打开文件ile_1, fopen(“file_2”,”wt”)表示按正文的写方式打开文件file_2。读写函数在读写以二进制方式打开的文件时不对其进行任何解释,也不在数据流中添加任何其他字符,数据在程序的内存中和在文件中是完全一致的。以正文方式打开的文件在数据的读写过程中会附加其他的操作和解释,其对文件的读写有两方面的影响。
在对以正文方式打开的文件进行读操作时, 系统将文件中的字符Crl-Z(0xla)解释成为文件的结尾,而不管该字符是否真的是该文件的最后一个字符。
输入/输出操作对回车换行符进行转换,因此程序中看到的换行符与文件中的换行符不同。
Windows平台上的文件以回车符(‘\r’,0x0d)和换行符(‘\n’,0xoa) 的组合表示一个正文行的结束,而在C程序中只用换行符’\n’来表示一个正文行的结束。当以正文方式读入文件时,文件中的回车符/换行符组合会被自动地转换成一个换行符,而当以正文方式写人文件时,换行符会被自动地转换成一个回车符/换行符的组合。正文方式是Windows平台上打开文件的默认方式,当在 Windows平台上读入二进制文件时,如果在文件打开时没有使用描述符b,就可能产生错误。例如,如果文件中包含字符0x1a,程序就无法读取文件的全部内容。
UNIX/Linux平台不区分文件打开的正文方式和二进制方式,因此打开方式描述符b或t对文件的打开和读写操作没有任何影响,文件的读写方式等价于 Windows平台上的二进制方式。
因此在 UNIX/Linux平台上读取在 Windows平台上生成的正文文件时,就需要注意其与UNIX/ Linux平台上生成的文件在每行末尾的换行符和文件的结尾时的差别。对初学者来说,处理这种差异可能会有些麻烦。为此我们可以尽量使用fgets()等函数一次读入一行,由库函数去处理这种差异,而避免使用 getchar()等函数以字符为单位地读入一行数据。
文件数据的正文格式的读写
按正文格式读写是程序中常用的读写方式,主要是为了生成和访问便于用户阅读的正文文件。我们在前面的章节中用过一些对标准输人/输出文件进行读写的标准函数,如 printf()、scanf(O、getc(),putc()等。这些函数隐含地指定对标准文件进行正文读写,因此不需要指定被操作的文件。与这些函数相对应的,在C的标准函数库中也提供了对指定文件进行正文读写的函数。表10-3是对标准文件进行正文读写的函数与对应的对指定文件进行正文读写的函数的对照表。
除了 gets()和puts()外,表10-3中列出的这些函数都等价于在需要指定操作文件的函数中将相应的参数设为 stdin或 stdout。例如, printf(“ Hello \ n”)等价于 fprintf( stdout,” Hello \ n”), getchar()等价于getc(stdin) 。 gets()和puts()与fgets()和fputs()在功能上不完全对应。gets()与fgets()的区别有两点。
- 为了防止输入数据过长而引起缓冲区溢出, gets()以第二个参数n 说明缓冲区的长度,并最多读人n-1个字符。
- 当缓冲区足够长时, fgets()将换行符作为读人数据的一部分。puts()与印fputs()在功能上的区別在于,puts()在输出了参数字符串s之后自动输出一个换行符’\n’,而 fputs()只输出参数字符串s。