C/C++语言概述
将介绍 C/C++语言的基本语法,但由于篇幅所限,不涉及面向对象的内容。本书中的所有程序,都没有使用面向对象的编程方法,但都应保存为 .cpp 文件,按 C++的语法进行编译。实际上,如果不涉及面向对象的部分,那么 C++语言和 C 语言的语法 90%以上是一样的,只不过略有扩充,用起来更为方便而已。因此,当提及的某项语法特性在 C 语言和 C++语言中都适用时,我们就会说:“在 C/C++语言中,……”
本书提到的 C/C++语言特性,以目前流行的 32 位计算机和操作系统上的情况为准。本书的重点是通过一些编程实例介绍程序设计中常用的思想方法和实现手段,不侧重介
绍某种高级程序设计语言的语法细节。在这一章里,我们对将要使用的 C/C++语言的相关内容做一个概要介绍。主要包括:变量、常量、表达式、赋值语句、分支语句、循环语句、数组、指针、函数等内容。
每个程序都描述了一个计算过程,计算过程的输入数据、中间结果和最终结果都存储在程序的变量中。计算的每一步用一个表达式来描述,即用运算符对一些变量的值、常量进行处理。这种运算符可以是加、减、乘、除等算术运算;也可以是大于、小于、等于等关系运算;或者是与、或、非等逻辑运算符。表达式的结果可以存储在变量中。一个程序的基本组成单位是语句。连续的多个语句可以构成一个语句组。最基本的语句有变量定义语句和变量赋值语句。在程序执行过程中,语句按其出现的先后被顺序执行。分支语句可以根据不同的情况执行不同的语句组,而循环语句可以重复执行同一个语句组。当一个程序由很多语句组成时,可以将其中与某个功能相关的一组语句抽象出来定义成函数,并用函数名来代替原来的多个语句,这样可以隐蔽程序中的一些细节,使得程序逻辑更简单清晰。
1.1 程序的基本框架
我们以简单程序Hello World 为例说明程序的基本框架。此程序在屏幕上输出一行“Hello World!”:
#include <stdio.h> void main(){
printf("Hello World!\n");
}
这段程序包括二个部分:
1)#include <stdio.h>
#include 是C 语言的保留字,表示要把另一个文件中的内容包含在本文件中。<stdio.h> 是被包含的文件的文件名。C 语言中提供了一些可以被直接拿来使用、能够完成某些特定功能的库函数,分别声明于不同的头文件中。例如:stdio.h 中定义了一些与输入输出有关的函数。printf 就是一个能往屏幕上输出一串字符的库函数。
2)void main(){ printf("Hello World!\n");
}
程序的主函数。每个程序都必须包含这个 main()函数。程序运行时,从 void main(){…}
的第一个语句开始执行。用户编写的程序的主要框架写在 main 函数里。
printf("Hello World!\n");
这条语句的作用是在屏幕上输出一串字符“Hello Word!”然后换行。“\n”的作用就是换行。换行后,如果以后再用 printf 语句来输出,那么输出的内容就会出现在屏幕的下一行。
1.2 变量
变量是内存中的一块区域,在程序运行过程中可以修改这块区域中存放的数值。变量由两个要素构成:变量的名称和变量的类型。变量的名称是这个内存区域的唯一标识。变量的类型决定了这个内存区域的的大小、对所存储数值的类型要求。在程序中,有三种与变量有关的语句:变量的定义、变量的赋值、和变量的引用。
1.2.1 变量的定义
如下的语句定义了一个变量:
int number;
这里‘number’是变量名,‘int’代表该变量是整数类型的变量,‘;’表示定义语句结束。
在目前流行的机器配置下,整型变量一般占 4 个字节的内存空间。变量的名字是由编写程序的人确定的,它一般是一个单词或用下划线连接起来的一个词组,说明变量的用途。在
C/C++语言中变量名是满足如下规定的一个符号序列:1) 由字母、数字或(和)下划线组成;2) 第一个符号为字母或下划线。需要指出的是,同一个字母的大写和小写是两个不同的符号。所以,team和 TEAM 是两个不同的变量名。
定义变量时,也可以给它指定一个初始值。例如:
int numberOfStudents = 80;
对于没有指定初始值的变量,它里面的内容可能是任意一个数值。变量一定要先定义,然后才能使用。
1.2.2 变量的赋值
给变量指定一个新值的过程称为变量的赋值,通过赋值语句完成。例如:
number = 36 ;
表示把 36 写入变量 number 中。下面给出一些变量赋值语句的例子:
int temp; int count; temp = 15; count = temp;
count = count +1; temp = count;
1.2.3 变量的引用
变量里存储的数据可以参与表达式的运算,或赋值给其它变量。这一过程称为变量的引用。例如:
int total = 0 ; int p1 = 5000 ; int p2 = 300 ; int p3 = 1000 ; int p4 = 1000 ;
total = p1 + p2 + p3 + p4 ;
最后一个赋值语句表示把变量‘p1’,‘p2’,‘p3’和‘p4’的值取出来相加,得到的和赋给变量‘total’。最后一句执行后,‘total’的值变为 7300。
1.3 C/C++语言的数据类型
前面我们介绍了变量的定义语句:
int nNumber;
此处的“int”表示了变量 nNumber 的“数据类型”,它说明 nNumber是一个“整型变量”,即 nNumber 中存放的是一个整数。“数据类型”能够说明一个变量表示什么样的数据(整数,浮点数,还是字符等)。不同数据类型的变量,占用的存储空间大小不同。除了“int” 以外,C/C++中还有其他一些基本数据类型,现列举其中几个如下:
int: 整型。int 型变量表示一个整数,其范围是 -231 -231-1,占用 4 个字节。
long:长整型。和 int类型一样,也占用 4 个字节。
short:短整型。short 型变量表示一个整数,但它占用 2个字节,因而能表示的数的范围是 -215 -215-1。
unsigned int:无符号整型。unsigned int 类型的变量表示一个非负整数,占用 4 个字节, 能表示的数的范围是:0 -232-1
unsigned long :和 unsigned int 一样。
unsigned short:无符号短整型。unsigned short 类型的变量表示一个非负整数,占用 2
个字节,能表示的数的范围是:0 -216-1。
本书中,我们将上面几种类型统称为“整数类型”。
char : 字符型。char 类型的变量表示一个字符,如’a’,’0’等。占用 1 个字节。字符型变量存放的实际上是字符的 Ascii 码。比如’a’的 Ascii码是 97,即 16进制的 0x61,那么如果有:
char c = ‘a’;
则实际上 c 中就存放着 16 进制数 0x61,或二进制数 01100001。
unsigned char:无符号字符型。unsigned char 类型的变量表示一个字符,占用 1 个字节。float :单精度浮点型。float 类型的变量表示一个浮点数(实数),占用 4个字节。 double :双精度浮点型。double 类型的变量也表示一个浮点数,但它占用 8 个字节,
因而精度比 float 类型高。
以上的“int”、“double”、“short”、”unsigned char”等标识符,都是“类型名”。C++中的“类型名”可以由用户定义,后文“结构”一节会进一步阐述。
在赋值语句中,如果等号左边的变量类型为 T1,等号右边的变量或常量类型为 T2,T1
和 T2 不相同,那么编译器会将等号右边的变量或常量的值,自动转换为一个 T1 类型的值,再将此值赋给等号左边的变量。这个过程叫做“自动类型转换”。自动类型转换不会改变等号右边的变量。能进行自动类型转换的前提是,T1和 T2 是两个兼容的类型。上面提到的所有类型,正好都是两两互相兼容的。但是后面会碰到一些类型,比如指针类型,结构类型,它们和上述所有的类型都不兼容。如果等号左边是个整型变量,等号右边是个“结构类型” 的变量,这样的赋值语句在编译的时候就会报错。
下面以一个程序来说明上述数据类型之间的自动转换:
1. #include <stdio.h>
2. int main()
3. {
4. int n1 = 1378;
5. short n2;
6. char c = 'a';
7. double d1 = 7.809;
8. double d2;
9. n2 = c; //n2 变为 97
10. printf("c=%c,n2=%d\n", c, n2);
11. c = n1; //c 变为 ’b’
12. printf("c=%c,n1=%d\n", c, n1); 13. n1 = d1; //n1 变为 7
14. printf("n1=%d\n", n1); 15. d2 = n1; //d2 变为 7
16. printf("d2=%f", d2);
17. return 0;
18. }
上面程序的输出结果是:
c=a,n2=97 c=b,n1=1378 n1=7 d2=7.000000
该程序中 printf 语句的用法比较复杂,请参看 2.10.1 节关有printf 语句的详细说明。
执行语句 9 时,由于变量 c 内存放的是字符’a’的 Ascii 码,即十进制整数 97,因此本条赋值语句使得 n2 的值变为 97。
语句 11中,等号的左边是 char 类型的变量、右边是 int 类型的变量。语句执行时,先将右边的 int值自动转换成一个 char 类型的值,再赋值给 c。由于 char类型的变量只要 1 个字节,所以自动转换的过程就是丢弃 n1的高 3 字节,只取 n1中最低的那个字节赋值给 c。n1 的值是 1378,表示成十六进制是 562,最低的字节是 0x62。本条语句执行完毕后,c 的值就是 0x62,换算成十进制就是 98。98 是字母’b”的 Ascii 码,因此,本语句执行后,c 中就存放着字母’b’。需要指出的是,本语句的自动转换过程不会改变 n1 的值。
语句 13 执行时,须将浮点数值 7.809 自动转换成一个整型值,再赋给 n1。在 C/C++中,浮点数自动转换成整数的规则是去掉小数部分,因此 n1 的值变为 7。d1 的值不改变。
思考题:1.3.1:假定 char 类型的变量 c 中存放着一个’w’之前的小写字母,请写一条赋值语句,使得 c 变为其后的第 4 个字母(比如,将 c 从’a’变成’e’)。解答见后。
提示:小写字母的 Ascii 码是连续的。
1.4 常量
常量是程序需要访问的一个数据,它在程序的运行过程中不发生改变。常量有两种表现形式:直接写出值,或用 #define 语句为数据定义一个由符号组成的标识符,标识符的命名规则与变量的命名规则相同。不同的数据类型有不同形式的常量。例如:123,-56,0,38,
-1 是整数类型的常量;1.5,23.6, 0.0, -0.6789, 100.456 是浮点类型的常量;'a','p', '0' ,'¥' ,'#' 是字符类型的常量;“abc”,“definitely”,“1234”,“0.6”,“AE4%(Ap)”等是字符串类型的常量。这些都是直接给出数据值的常量,它们的类型可以很容易地从数据形式上判断。另一种用#define 语句,为需要访问的数据指定一个容易理解的名字(标识符),例如:
#define MAPLENGTH 100
#define MAPWIDTHTH 80 void main( ){
int mapSize;
mapSize = MAPLENGTH * MAPWIDTH ;
printf(“The map size is %d\n”, mapSize);
}
这段代码中 MAPLENGTH是一个整数类型的常量,它的值是 100。在定义语句之后,所有出现符号 MAPLENGTH 的地方,都等效于出现数值 100。同样地,MAPWIDTH 也是一个整数类型的常量,它的值是 80。这段程序的运行结果是输出一个整数 8000。
C/C++语言中,整数类型常量还可以有八进制、十六进制的写法。
八进制常量以数字“0”开头的,比如 0123就是八进制的 123。0987 是不合法的常量, 因为以 0 开头代表是八进制数,而八进制数中是不能出现数字 8 和 9 的。
十六进制常量以“0x”开头。比如,0x12就是 16 进制的 12,换算成十进制就是 18。
0xfd0678、0xff44f 都是合法的十六进制常量。十六进制表示法中,用 a代表 10、b 代表 11、
c 代表 12、d 代表 13、e 代表 14、f 代表 15。这几个字母大、小写均可。由于 16 进制中的每一位正好对应于二进制的 4 位,因此,十六进制常量用起来十分方便,也非常有用。
有一些字符常量的写法比较特殊,比如单引号应写为 '\'' ,“\”应写为 '\\'。
思考题 1.4:什么样的常量在程序运行期间会象变量一样,需要用一片内存空间来存放,什么样的常量不需要?
1.5 运算符和表达式
C/C++语言中的“+”、“-” “*” “/”等符号,表示加、减、乘、除等运算,这些表示数据运算的符号称为“运算符”。运算符所用到的操作数个数,称为运算符的“目数”。比如,“+”运算符需要两个操作数,因此它是双目运算符。
将变量、常量等用运算符连接在一起,就构成了“表达式”。如“n+5”、“4-3+1”。实际上,单个的变量、常量也可以称为“表达式”。表达式的计算结果称为“表达式的值”。如表达式“4-3+1”的值就是 2,是整型的。如果 f 是一个浮点型变量,那么表达式“f”的值就是变量 f 的值,其类型是浮点型。
C/C++语言的运算符有赋值运算符、算术运算符、逻辑运算符、位运算符等多类。常用的介绍如下。
1.5.1 算术运算符
算术运算符用于数值运算。包括加(+)、减(-)、乘(*)、除(/)、求余数(%)、自增(++)、自减(--)共七种。
1.5.1.1 模运算符
求余数的运算符“%”也称为模运算符。它是双目运算符,两个操作数都是整数类型的。
a % b 的值就是 a 除以b 的余数。
1.5.1.2 除法运算符
C/C++的除法运算符有一些特殊之处,即如果 a、b是两个整数类型的变量或者常量, 那么 a/b 的值是 a 除以b 的商。比如,表达式 “5/2”的值是 2,而不是 2.5。请看下面的程序片断:
1. int main()
2. {
3. int a = 10;
4. int b = 3;
5. double d = a/b;
6. printf("%f\n", d);
7. d = 5/2;
8. printf("%f\n", d);
9. d = 5/2.0;
10. printf("%f\n", d);
11. d = (double)a/b;
12. printf("%f\n", d);
13. return 0;
14. }
上面程序的输出结果是:
3.000000
2.000000
2.500000
3.333333
语句 5 中,由于 a、b 都是整型,所以表达式 a/b 的值也是整型,其值是 3,因此 d 的值就变成 3.0。
语句 7 和语句 5 类似,执行后 d 的值变为 2.0。
语句 9中,要求 5 除以 2的精确值,为此要将 5 或者 2 表示成浮点数。除法运算中,如果有一个操作数是浮点数,那么结果就也会是较为精确的浮点数。因此表达式 5/2.0 的值是2.5。
语句 11求 a 除以 b 的较为精确的小数形式的值。“(double)”的是一个“强制类型转换运算符”,它是一个单目运算符,能将其右边的操作数强制转换成 double 类型。用此运算符先将 a的值转换成一个浮点数值,然后再除以 b,此时算出来的结果就是较为精确的浮点型的了。
1.5.1.3 自增自减运算符
自增运算符“++” 用于将整型或浮点型变量的值加 1。只有一个操作数,是单目运算符。它有两种用法:
用法 1:
变量名 ++;
用法 2:
++变量名;
这两种用法都能使得变量的值加 1,但它们是有区别的,请看例子:
1. #include <stdio.h>
2. main()
3. {
4. int n1 , n2 = 5;
5. n2 ++;
6. ++ n2;
7. n1 = n2 ++;
8. n1 = ++ n2;
9. }
语句 5 执行后,n2 的值是 6; 语句 6 执行后,n2的值是 7;
语句 7 执行过程,是先将 n2 的值赋给 n1,然后再增加 n2的值,因此语句 7 执行后,
n1 的值是 7,n2 的值是 8。也可以说,表达式“n2++”的值,就是 n2 加 1 以前的值;
语句 8 的执行过程,先将 n2的值加 1,然后再将 n2的新值赋给 n1。因此语句 8 执行后,n1的值是 9,n2 的值也是 9。也可以说,表达式“++n2”的值,就是 n2加 1 以后的值;
语句 7 和 8 体现了“++”写在变量前面和后面所造成的不同。
自减运算符“--” 用于将整型或浮点型变量的值减 1。它的用法和“++”相同,不再赘述。
1.5.2 赋值运算符
赋值运算符用于对变量进行赋值,分为简单赋值(=)、复合算术赋值(+=, -=, *=, /=, %=)
和复合位运算赋值(&=, |=, ^=, >>=, <<=)三类共十一种。
表达式“a = b”的值就是a,类型和 a 的类型一样。因此,可以写:
int a, b; a = b = 5;
上面这条语句先将 b 的值赋为 5;然后求得 b=5 这个表达式的值 5,再赋值给 a。
a += b 等效于 a = a + b,但是前者执行速度比后者快。
-=、*=、/=、%= 的用法和 += 类似。
1.5.3 关系运算符
关系运算符用于数值的大小比较。包括大于(>)、小于(<)、等于(==)、大于等于(>=)、小于等于(<=)和不等于(!=)六种。他们都是双目运算符。
关系运算符运算的结果是整型,值只有两种:0 或非 0。0 代表关系不成立,非 0 代表关系成立。
比如表达式“3>5”,其值就是 0,代表该关系成不成立,即运算结果为假;表达式“3==3”,其值就是非 0,代表该关系成立,即运算结果为真。至于这个非 0 值到底是多少,C/C++语言没有规定,我们编程的时候也不需要关心这一点。C/C++语言中,总是用 0 代表“假”,用
非 0 代表“真”,在后面的 2.7 节会看到其用法。请看下面的例子:
main()
{
int n1 = 4, n2 = 5, n3;
n3 = n1 > n2; // n3 的值变为 0
n3 = n1 < n2; // n3 的值变为某非 0 值n3 = n1 == 4; // n3 的值变为某非 0 值n3 = n1 != 4; // n3 的值变为 0
n3 = n1 == 5; // n3 的值变为 0
}
1.5.4 逻辑运算符
逻辑运算符用于数值的逻辑操作。包括与(&&)、或(||)、非(!)三种。前二者是双目运算符,第三个是单目运算符。其运算规则如下:
当且仅当表达式 exp1 和表达式 exp2 的值都为真(非 0)时,“exp1 && exp2” 的值为真,其他情况,“exp1 && exp2”的值均为假。例如,如果 n = 4,那么“n > 4 && n < 5 ”的值就是假,“n >= 2 && n < 5”的值就是真。
当且仅当表达式 exp1和表达式 exp2 的值都为假(就是 0)时,“exp1 || exp2” 的值为假,其他情况,“exp1 || exp2”的值均为真。例如,如果 n = 4,那么“n > 4 || n < 5 ”的值就是真,“n <= 2 || n > 5”的值就是假。
如果表达式 exp 的值为真,那么“ ! exp”的值就是假;如果 exp 的值为假,那么“ ! exp” 的值就是真。比如,表达式“! (4 < 5 )”的值就是假。
1.5.5 位运算符
有时我们需要对某个整数类型变量中的某一位(bit)进行操作,比如,判断某一位是否为 1,或只改变其中某一位,而保持其他位都不变。C/C++语言提供了“位运算”的操作, 实现类似的操作。C/C++语言提供了六种位运算符来进行位运算操作:
& 按位与
| 按位或
^ 按位异或
~ 取反
<< 左移
>> 右移
位运算的操作数是整数类型(包括 long、int、short、unsigned int 等)或字符型的,位运算的结果是无符号整数类型的。
1.5.5.1 按位与运算符
按位与运算符"&"是双目运算符。其功能是,将参与运算的两操作数各对应的二进制位进行与操作。只有对应的两个二进位均为 1 时,结果的对应二进制位才为 1,否则为 0。
例如:表达式“21 & 18 ”的计算结果是 16(即二进制数 10000),因为:
21 用二进制表示就是: |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0001 |
0101 |
18 用二进制表示就是: |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0001 |
0010 |
二者按位与所得结果是: |
0000 |
0000 |
0000 |
0000 |
0000 |
0000 |
0001 |
0000 |
按位与运算通常用来将某变量中的某些位清 0 或保留某些位不变。例如,如果需要将
int 型变量n 的低 8 位全置成 0,而其余位不变,则可以执行: n = n & 0xffffff00;
也可以写成:
n &= 0xffffff00;
如果 n 是short 类型的,则只需执行: n &= 0xff00;
如果要判断一个 int 型变量 n 的第 7 位(从右往左,从 0 开始数)是否是 1,则只需看表达式 “n & 0x80”的值是否等于 0x80 即可。
1.5.5.2 按位或运算符
按位或运算符"|"是双目运算符。其功能是将参与运算的两操作数各对应的二进制位进行或操作。只有对应的两个二进位都为 0 时,结果的对应二进制位才是 0,否则为 1。
例如:表达式“21 | 18 ”的值是 23(即二进制数 10111)。
按位或运算通常用来将变量中的某些位置 1 或保留某些位不变。例如,如果需要将 int型变量 n 的低 8 位全置成 1,而其余位不变,则可以执行:
n |= 0xff;
1.5.5.3 按位异或运算符
按位异或运算符"^"是双目运算符。其功能是将参与运算的两操作数各对应的二进制位进行异或操作。只有对应的两个二进位不相同时,结果的对应二进制位才是 1,否则为 0。
例如:表达式“21 ^ 18 ”的值是 7(即二进制数 111)。
异或运算的特点是:如果 a^b=c,那么就有 c^b == a 以及 c^a==b。此规律可以用来进行最简单的快速加密和解密。
思考题 2.5.5.3:如何用异或运算对一串文字进行加密和解密?进一步,如果只使用一个字符做密钥,恐怕太容易被破解,如何改进?
1.5.5.4 按位非运算符
按位非运算符"~"是单目运算符。其功能是将操作数中的二进制位 0 变成 1,1 变成 0。例如,表达式“~21”的值是无符号整型数 0xffffffea,下面的语句:
printf("%d,%u,%x", ~21, ~21, ~21); 输出结果是:
-22,4294967274,ffffffea
1.5.5.5 左移运算符
左移运算符“<<”是双目运算符。其计算结果是将左操作数的各二进位全部左移若干位后得到的值,右操作数指明了要左移的位数。左移时,高位丢弃,左边低位补 0。左移运算符不会改变左操作数的值。
例如,常数 9 有 32 位,其二进制表示是:
0000 0000 0000 0000 0000 0000 0000 1001
表达式“9<<4”就是将上面的二进制数左移 4 位,得到:
0000 0000 0000 0000 0000 0000 1001 0000
即为十进制的 144。
实际上,左移 1 位,就等于是乘以 2,左移 n 位,就等于是乘以 2n。而左移操作比乘法操作快得多。
请看下面的例子程序:
1. #include <stdio.h>
2. main()
3. {
4. int n1 = 15;
5. short n2 = 15;
6. unsigned short n3 = 15;
7. unsigned char c = 15;
8. n1 <<= 15;
9. n2 <<= 15;
10. n3 <<= 15;
11. c <<= 6;
12. printf( "n1=%x,n2=%d,n3=%d,c=%x,c<<4=%d", n1, n2, n3, c, c << 4);
13. }
上面程序的输出结果是:
n1=78000,n2=-32768,n3=32768,c=c0,c<<4=3072
语句 12 中printf 的用法比较复杂,请参看后面的 2.10.1 节关于printf 函数的说明。语句 8 对 n1 左移 15 位。将 32 位的 n1 用二进制表示出来后,即可得知新的 n1 值是
0x78000。
语句 9 将 n2 左移 15 位。注意,n2 是 short 类型的,只有 16 位,表示为二进制就是0000 0000 0000 1111,因此左移 15 位后,一共从左边移出去了(丢弃了)3 个 1,左移后
n2 中存放的的二进制数就是 1000 0000 0000 0000。由于 n2 是 short 类型,此时 n2 的最高位是 1,因此 n2 实际上表示的是负数,所以在语句 12 中输出为-32768。
语句 10 将 n3 左移 15 位。左移后 n3 内存放的二进制数也是 1000 0000 0000 0000, 但由于 n3 是无符号的,表示的值总是非负数,所以在语句 12 中,n3 输出为 32768。
语句 11 将c 左移 6 位。由于 c 是unsigned char 类型的,一共只有 8 位,其二进制表示就是 00001111,因此左移 6 位后,就变为 11000000,在语句 12 中以 16 进制输出为 c0。
语句 12 中,表达式 “c<<4”的计算过程是首先将 c 转换成一个 int 类型的临时变量
(32 位,用 16 进制表示就是 0000 0000 0000 00c0 ),然后将该临时变量左移 4 位,得到
的结果是十六进制的 0000 0000 0000 0c00,换算成十进制就是 3072。
表达式“c<<4”的求值过程不会改变 c 的值,就像表达式“c+4”的求值过程不会改变
c 的值一样。
1.5.5.6 右移运算符
右移运算符“>>”是双目运算符。其计算结果是把“>> ”的左操作数的各二进位全部右移若干位后得到的值,要移动的位数就是“>>”的右操作数。移出最右边的位被丢弃。
对于有符号数,如 long、int、short、char 类型变量,在右移时,符号位(即最高位)将一起移动,并且大多数 C/C++编译器规定,如果原符号位为 1,则右移时右边高位就补充
1,原符号位为 0,则右移时高位就补充 0。
对于无符号数,如 unsigned long、unsigned int、 unsigned short、 unsigned char类型的变量,右移时高位总是补 0。
右移运算符不会改变左操作数的值。请看例子程序:
1. #include <stdio.h>
2. main()
3. {
4. int n1 = 15;
5. short n2 = -15;
6. unsigned short n3 = 0xffe0;
7. unsigned char c = 15;
8. n1 = n1>>2;
9. n2 >>= 3;
10. n3 >>= 4;
11. c >>= 3;
12. printf( "n1=%x,n2=%d,n3=%x,c=%x", n1, n2, n3, c);
13. }
上面的程序输出结果是: n1=3,n2=-2,n3=ffe,c=1
语句 8 中,n1 的值是 0xf,右移 2 位后,变成 0x3。
语句 9 中,n2 是有符号 16 位整数,而且原来值为负数,表示成二进制是 1111 1111 1111
0001。由于最高位(符号位)是 1,右移时仍然在高位补充 1,所以右移完成后其二进制形式是 1111 1111 1111 1110,对于一个有符号 16 位整数来说,这个二进制形式就代表-2。语句 10 中,n3 是无符号的 16 位整数,原来其值为 0xffe0。尽管最高位是 1,但由于
它是无符号整数,所以右移时在高位补充 0,因此右移 4 位后,n3 的值变为 0xffe。语句 11,c 是无符号的,原来值为 0xf,右移动 3 位后自然就变成 1。
实际上,右移 n 位,就相当于左操作数除以 2n,并且将结果往小里取整。
思考题 1.5.5.6:有两个 int 型的变量 a 和 n(0 <= n <= 31),要求写一个表达式,使该表达式的值和 a 的第n 位相同。
2.5.5.7 sizeof 运算符
“sizeof”是 C/C++语言中的保留字,也是一个运算符。它的作用是求某一个变量占用内存的字节数,有两种用法:
第一种用法:
sizeof(变量名)
比如,表达式 sizeof(n)的值是 n这个变量占用的内存字节数。如果 n 是 short 类型的变量,那么 sizeof(n)的值就是 2。
第二种用法:
sizeof(类型名)
比如,sizeof(int) 的值是 4,因为一个 int类型的变量占用 4 个字节。
1.5.5.8 类型强制转换运算符
强制类型转换运算符的形式是:
(类型名)
比如,(int) 、(double)、(char)等,都是强制类型转换运算符。它是单目运算符,功能是将其右边的操作数的值转换得到一个类型为“类型名”的值,它不改变操作数的值。
比如:
1. double f = 9.14
2. int n = (int) f ;
3. f = n / 2 ;
4. f = (double) n / 2 ;
上面的语句 2将 f 的值 9.14强制转换成一个 int 型的值,即转换成 9,然后赋值给 n。这条语句中是否使用(int)运算符结果都一样,因为编译器会自动转换。但是有时我们需要在类型不兼容的变量之间互相赋值,这时就需要在赋值时对等号右边的变量、常量或表达式进行强制类型转换,转换成和等号左边的变量类型相同的一个值。
上面的语句 3 执行后,f的值是 4.0,因为表达式 n/2 的值是整型的,为 4。
而语句 4 使用强制转换运算符(double)将 n的值转换为一个浮点数,然后再除以 2,那么得到的值就是一个浮点数。因此本语句执行后,f 的值为 4.5。2.5.1.2 小节的例子程序的语句 11,也说明了强制转换运算符的这种用法。
1.5.5.9 运算符的优先级
一个表达式中可以有多个、多种运算符。不同的运算符优先级不同,优先级决定了表达式该先算哪部分、后算哪部分。
比如表达式 4 & 2 + 5,由于“+”的优先级高于“&”,所以这个表达式是先算 2 + 5,再算 4 & 7,结果是 4。
可以用括号来规定表达式的计算顺序,比如 ( 4 & 2 ) + 5 的值是 5,先算 4 & 2 。下表列出了大部分运算符的优先级:
优先级 |
描述 |
运算符 |
1 |
最高优先级 |
. [ ] ( ) |
2 |
单目运算 |
- ~ ! ++ -- 强制类型转换 sizeof |
3 |
算术乘除运算 |
* / % |
4 |
算术加减运算 |
+ - |
5 |
移位运算 |
>> << >>= <<= |
6 |
大小关系运算 |
< <= > >= |
7 |
相等关系运算 |
== != |
8 |
按位与 |
& |
9 |
按位异或 |
^ |
10 |
按位或 |
| |
11 |
逻辑与 |
&& |
12 |
逻辑或 |
|| |
14 |
赋值 |
= |
1.6 注释
有时我们会需要在程序中用自然语言写一段话,提醒自己或者告诉别人,某些变量代表什么,某段程序的逻辑是怎么回事,某几行代码的作用是什么,等等。当然,这部分内容不
能被编译,不属于程序的一部分。这样的内容,称为“注释”。
C++的注释有两种写法。第一种注释可以是多行的,以 “/*”开头,以“*/”结尾。例
如:
/* mp3 解码程序
author : Guo Wei programed on 2004.5.18
*/ main() {
int nBitrate; /* 比特率,以 kb为单位 */
int nSize; /* 以字节为单位 */
……
}
注释可以出现在任何地方,注释里的内容不会被编译的,因此,随便写什么都行。
第二种注释是单行的。写法是使用两个斜杠“//”。从“//”开始直到行末的内容,就都算是注释了。例如:
main() {
int nBitrate; // 比特率,以 kb 为单位
int nSize; // 以字节为单位
……
}
注释非常重要。它的主要功能是帮助理解程序。一定不要认为程序是自己写的,自己当然能理解。只要程序稍长一些、或者变量名不够直观,那么写时能理解,并不意味着一个星期后自己还能理解。更何况,软件开发是团队工作,没有人希望在看别人的程序的时候如读天书,恨不得自己重写一个。所以,在程序中加入足够的、清晰易懂的注释,是程序员的基本修养。
1.7 分支语句
在 C/C++语言中,语句以“ ;”结束。某些情况下,一组语句在一起共同完成某一特定的功能,可以将它们用大括号括起来。我们称之为语句组。语句组可以出现在任何单个语句出现的地方。一般情况下,语句的出现顺序就是其执行顺序。但是在某些情况下,需要根据不同的运行情况而执行不同的语句组。这时可以选用分支语句。我们介绍两种分支语句:if和 switch。
1.7.1 if 语句
if 语句有两种形式:
if ( 表达式) 语句/语句组
如果表达式 的值为真(非零),则其后的语句/语句组 被执行。如果表达式的值为假
(等于零),则其后的语句/语句组被忽略。
if ( 表达式) 语句/语句组 1else 语句/语句组 2
如果表达式 的值为真(非零),则其后的语句/语句组 1被执行,语句/语句组 2被
忽略。如果表达式的值为假(等于零),则其后的语句/语句组 1 被忽略,语句/语句组
2 被执行。
下面是一个 if 语句的例子: if ( i > 0 )
y = x / i; else {
x = i; y = - x;
}
在这个例子中, i, x, y 是变量。如果 i 的值大于 0,则 y 被赋值为 x/i;如果 i 的值小于或等于 0,则x 被赋值为 i,y 被赋值为-x。当if 语句后面只有一个语句时,可以不用大括号将其括起来。
if 语句可以嵌套使用。在没有大括号来标识的情况下,else 语句被解释成与它最近的
if 语句共同构成一句。例如:
if ( i > 0 ) /* 没有大括号 */ if ( j > i )
x = j;
else
x = i;
如果上面的例子中 else 是与第一个 if 配对的,则应该写成如下格式:
if ( i > 0 ) { /* 加上括号 */
if ( j > i )
x = j;
}
else
x = i;
2.0.1 switch语句
switch 和case 语句用来控制比较复杂的条件分支操作。switch 语句的语法如下:switch ( 表达式 ){
case 常量表达式 1: 语句/语句组 1
case 常量表达式 2: 语句/语句组 2
…
default : 语句/语句组n
}
switch 语句可以包含任意数目的case 条件, 但是不能有两个 case后面的常量表达式完全相同。进入 switch 语句后,首先表达式 的值被计算、并与case 后面的常量表达式逐一匹配,当与某一条 case 分支的常量表达式匹配成功时,则开始执行它后面的语句/语句组,然后顺序执行之后的所有语句,直到遇见一个整个 switch 语句结束,或者遇到一个
break 语句(break 语句后面会有介绍)。 如果表达式与所有的常量表达式 都不相同,则从 default 后面的语句开始执行到switch 语句结束。
各 case 分支后的“常量表达式”必须是整数类型或字符型的。
如果各个case 分支后面的语句/语句组彼此独立,即在执行完某个 case 后面的语句/语句组后,不需要顺序执行下面的语句,可以用 break 语句将这些分支完全隔开。在 switch 语句中,如果遇到break 语句,则整个 switch 语句结束。例如:
switch ( 表达式){
case 常量表达式 1: 语句/语句组 1;break;
case 常量表达式 2: 语句/语句组 2;break;
…
default : 语句/语句组n
}
default 分支处理除了明确列出的所有常量表达式以外的情况。switch 语句中只能有一个default 分支,它不必只出现在最后,事实上它可以出现在任何case 出现的地方。
switch 后面的表达式与 case 后面的常量表达式必须类型相同。 象if 语句一样,case 语句也可以嵌套使用。
下面是一个 switch 语句的例子: switch( c ) {
case 'A':
capa++; case 'a':
lettera++; default :
total++;
}
因为没有break 语句,如果 c 的值等于'A',则switch 语句中的全部三条语句都被执行;如果 c 的值等于'a',则 lettera 和 total 的值加 1。如果 c 的值不等于'a'或'A',则只有 total 的值加 1。下面是一个加入了break 语句的例子:
switch( i ) {
case -1:
case 0 :
case 1 :
}
n++;
break;
z++;
break;
p++;
break;
在这个例子中,每个分支都加入了一个break 语句,使得每种情况处理完之后,就结束 switch 语句。如果 i 等于 –1, 只有 n 加 1;如果 i 等于 0, 只有 z 加 1; 如果 i 等于 1, 只有 p 加 1。最后一个break 不是必须的,因为程序已经执行了最后,保留它只是为了形上的统一。
如果有多种情况要执行的任务相同,可以用如下的方式表达: case 'a' :
case 'b' :
case 'c' :
case 'd' :
case 'e' :
case 'f' :
x++;
在这个例子中,无论表达式取值在'a' 到 'f'之间的哪个值, x 的值都加 1。
1.8 循环语句
在有些程序中,需要反复执行某些语句。将 n 条相同的语句简单地复制会使程序变得不合理的冗长,因此高级语言中提供了支持程序重复执行某一段程序的循环控制语句。相关的语句有:for; while; do while; break; continue; 等。
1.8.1 for语句
for 可以控制一个语句或语句组重复执行限定的次数。for的语句体可以执行零或多次,直到给定的条件不被满足。可以在 for 语句开始时设定初始条件,并在语句的每次循环中改变一些变量的值。 for 语句的语法如下:
for ( 初始条件表达式; 循环控制表达式 ;循环操作表达式 ) 语句/语句组
执行一个 for 语句包括如下操作:
1. 初始条件表达式被分析执行。这个条件可以为空。
2. 循环控制表达式被分析执行。这一项也可以为空。循环控制表达式一定是一个数值表达式。在每次循环开始时,它的值都会被计算。计算结果有三种可能:
· 如果循环控制表达式 为真(非零), 语句/语句组被执行;然后循环操作表达式 被执行。循环操作表达式在每次循环结束时都会被执行。下面就是下一次循环开始,循环操作表达式 被执行。
· 如果循环控制表达式 被省略,它的值定义为真。一个for 循环语句如果没有循环控制表达式 ,它只有遇到 break 或return 语句时才会结束。
· 如果循环控制表达式 为假(零), for 循环结束,程序顺序执行它后面的语句。
break, goto, 或return 语句都可以结束 for 语句。continue 语句可以把控制直接转移至for 循环的循环控制表达式。当用 break 语句结束 for 循环时,循环控制表达式不再被执行。下面的语句经常被用来构造一个无限循环,只有break 或 return 语句可以从这个循环中跳出来。
for( ;; );
下面是一个 for 循环语句的例子:
for ( i = n2 = n3 = 0; i <= 100; i++ ) { if ( i % 2 == 0 )
n2 ++; else if (i % 3 == 0 )
n3 ++;
}
这个例子计算从 0 到 100 的整数中,有多少个数是偶数(包括 0 在内),有多少个数是 3 的整数倍。最开始 i、 n2 和 n3 被初始化成 0。然后把i 与 100 做比较,之后 for内部的语句被执行。根据 i 的不同取值,n2 被加 1、或者n3 被加 1、或者两者都不加。然后 i++ 被执行。接下来把 i 与 100 做比较,之后 for 内部的语句被执行。如此往复直到 i的值大于 100。
1.8.2 while 语句
while 语句重复执行一个语句或语句组,直到某个特定的条件表达式的值为假。它的语法表示如下:
while ( 表达式) 语句/语句组
式中的表达式 必须是数值表达式。while 语句执行过程如下:
1. 表达式 被计算。
2. 如果表达式的值为假,while 下面的语句被忽略,程序直接转到while 后面的语句执行。
3. 如果表达式的值为真(非零),语句/语句组被执行。之后程序控制转向 1。下面是一个while 语句的例子:
int i = 100; int sum = 0;
while ( i > 0 ) {
sum = sum + i*i ;
i--;
}
上面的例子计算从 1 到 100 的平方和,结果保存在 sum 中。循环每次判断 i 是否大于 0, 如果 i 大于 0,则进入循环,在 sum 上累加 i 的平方,将 i 的值减 1,到此次循环结束。下一步重新判断 i 是否大于 0。当某次判断 i 不大于 0 时,while 语句结束。
1.8.3 do-while 语句
do-while 语句重复执行一个语句或语句组,直到某个特定的条件表达式的值为假。下面是它的语法表示:
do 语句/语句组while ( 表达式 ) ;
do-while 语句中,表达式是在语句/语句组被执行之后计算的。所以 do 后面的语句/语句组 至少被执行一次。其中表达式必须是一个数值表达式。do-while 语句的执行过程如下:
1. do 后面的语句/语句组被执行
2. 表达式 被计算。如果其值为假,则 do-while 语句结束,程序继续执行它后面的语句。如果表达式 的值为真(非零),跳转回 1 重复执行do-while 语句。
do-while 语句同样可以通过break, goto, 或 return 语句结束。
下面是一个 do-while 的例子:
int i = 100; int sum = 0; do {
sum = sum + i*i; i--;
} while ( i > 0 );
这个 do-while 语句完成跟了上面的while 相同的功能,即计算从 1 到 100 的平方和。前面两句定义了两个整型变量 i 和 sum。在进入do-while 语句后,i 的平方被累加到 sum中,之后 i 的值被减 1。接下来判定 i 是否大于 0,如果 i 大于 0,则重复 do 后面的语句, 否则do-while 语句结束。
1.8.4 break 语句
break 语句用来结束离它最近的do、for、switch、或while 语句。它的语法表示如
下:
break;
下面是一个 break 语句的例子:
for ( i = 0; i < 10; i++ ){ /* Execution returns here when
statement is executed */
break
j++) {
for ( j = 1; j <= 5;
if ( (i+j )% 5 ==0 ) {
printf(“i= %d j=%d\n”, i, j); break;
}
}
}
这个例子中,i 从 0 循环到 9,每次 j 从 1 循环到 5,如果有某个 j 值使得 i+j 是 5 的整数倍,则输出 i 和j 的值,并跳出 j 循环,开始下一轮的 i 循环。这段程序的输出结果如下:
1 4
2 3
3 2
4 1
5 5
6 4
7 3
8 2
9 1
1.8.5 continue 语句
在 do、for、或while 语句中,continue 语句使得其后的语句被忽略,直接回到循环的顶部,开始下一轮的循环。continue语句的语法表示如下:
continue; do、for、或while 语句的下一轮循环用如下方法确定:
· 对于 do 或 while 语句, 下一轮循环从计算条件表达式的值开始。
· 对于 for 语句,下一轮循环从计算第一个循环控制条件表达式的值开始。下面是一个continue 语句的例子:
int i=100; int x=0; int y=0;
while ( i > 0 ) { i--;
x = i % 8;
if ( x == 1 )
continue; y = y + x;
}
这段程序计算 i 从 99 开始到 0 为止,累加除了 8 的倍数加 1 以外的所有数模 8 而得到的值。每次 while 循环开始,判断 i 的值是否大于 0,如果 i 大于 0,则进入循环体,先将i 的值减 1,然后将 i 模 8 的值赋给 x, 下面的 if 语句判断 x 是否等于 1,如果 x 等于 1,则回到 while 语句的开始,判断 i 是否大于 0;如果 x 不等于 1,则将 x 的值累加到 y 中。循环在 x 等于 0 时结束。
1.9 函数
函数是 C/C++语言中的一种程序组件单位。一个函数通常代表了一种数据处理的功能,由函数体和函数原型两部分组成。函数原型为这个数据处理功能指定一个标识符号(函数的名称)、说明被处理数据的组成及其类型、处理结果的类型。函数体由一组语句组成,具体实现数据处理的功能。这称为函数的定义。在某段程序中,一个函数可以被当作一个表达式来运行,称为函数的调用。函数的定义并不执行函数体中的语句,只是声明该函数包含这些
语句、以及这些语句的运行顺序。函数在被调用之前,必须说明它的原型。被函数处理的数据一般作为函数的参数,在函数调用时确定它们的值。但是在函数体的语句中,可以直接访问函数的参数。函数运行后可以把它的结果返回给调用它的程序。
如果一个程序代码中需要多次实现同一种数据处理功能,通常将这个数据处理功能定义成一个函数,开发成一个单独程序组件。使得整个程序开起来更简洁。此外,当一个程序代码段实现的功能很复杂时,也常常将这个功能分解成若干个相对简单的子功能。每个子功能分别作为一个函数,用一个程序组件实现。
1.9.1 函数的定义
函数的定义形式如下:
返回值类型 函数名([参数 1 类型参数名 1,参数 2 类型参数名 2,……]){
语句 1; //语句可能与参数有关语句 2; // 语句可能与参数有关
……
return 返回值; // 如果返回值类型为 void,则不用返回语句
}
其中,返回值类型表示该函数如果被调用,它执行完之后向调用它的程序返回何种数据类型的值。函数名是程序员自己定义的、能够表明函数用途的标识符号,命名规则与变量的命名规则相同。参数是可选的,有些函数没有参数,有些可以有一至多个参数。每个参数都应说明其类型,以便调用它的程序可以填入正确的参数值。小括号和大括号是必须的。语句中可以把参数当作变量来使用。下面是一个函数定义的例子:
int add(int x, int y){ return x+y;
}
这个函数的函数名是 add,它有两个参数分别是整数类型的 x和整数类型的 y;它的返回值类型也是整型,功能是计算两个整数的和,执行的结果是将计算出来的和返回给调用它的程序。两个参数 x 和y 的值是调用它的函数给定的。
函数定义也可以分成两部分,即函数原型说明和函数体。函数原型说明必须在函数调用之前。函数体可以紧跟着函数原型说明,也可以放在程序中间的位置。例如:
int multiple(int x, int y); // 函数说明
void main(){
int a=0, b=0;
scanf(“%d %d”, &a, &b);
printf(“%d\n”, multiple(a, b)); // 函数调用
}
int multiple(int x, int y){ // 函数体
return x * y;
}
1.9.2 函数的调用
在一段程序中引用一个已经定义过的函数称为函数的调用。在调用函数时要给出每个参数的取值。如果函数有返回值,可以定义一个与返回值类型相同的变量,存储函数的返回值。下面是函数调用的例子:
int add(int x, int y){ return x+y;
}
void main(){
int n1=5, n2=6, n3; n3=add(n1, n2); printf(“%d\n”, n3);
}
这段程序,调用函数 add计算 n1 加 n2的值,并将计算结果存入 n3。最后输出 n3 的值。这里要注意的是:如果函数的返回值是整型的,则函数调用表达式本身可以被看作是一个整数,它可以出现在任何整数可以出现的地方。其它类型的返回值也是一样。
有返回值的函数调用可以出现在表达式中,比如 n3 = add(n1, n2) + 7;也是合法的语句。
1.9.3 参数传递和返回值
函数调用可以看作在程序组件 A的执行过程中,跳出 A 的代码段,转去执行另外一段代码 B,等 B执行完之后,再回到 A 中函数调用的位置,继续执行后面的语句。在函数调用的过程中,程序组件A可以通过参数向程序组件 B 传送信息;程序组件 B 结束后,可以通过返回值将其执行结果传回程序组件 A。
1.9.3.1 参数传递
参数作为数值传递给被调用的函数,在函数内部等同于内部变量。下面是一个例子:
int max(int a, int b){ if(a>=b) return a; else return b;
}
void main( ){
int x=0, y=0, z=0; x = 20;
y = 45;
int z = max(x, y);
……
}
在主函数开始执行之前系统为它分配了空间存放变量 x, y, z。第一条赋值语句结束后,
x 的值修改为 20;第二条赋值语句结束后,y的值修改为 45;执行到第三条赋值语句时,“=”号右边是函数调用,于是装入函数 max 的代码。max函数所在的程序段,系统为参数 a, b 分配了空间(注意:参数的名字是独立于调用它的程序的),并将调用时的参数值填入分配的空间。也就是说调用函数时,将数值 45和 20 传给被调用的函数。这时 main 暂时停止执行,max开始执行,它执行的结果是将参数 b 的值 45 通过return 语句返回给 main。main 接收到 max返回的 45,并且把它赋值给变量 z,此时 z变量的内容修改为 45。程序继续执行。这里需要注意的是:在 max函数中对 a,b 的任何操作不影响 x,y 的值。
1.9.3.2 返回值
函数执行完以后可以向调用它的程序返回一个值,表明函数运行的状况。很多函数的功能就是对参数进行某种运算,之后通过函数返回值给出运算结果。函数的返回值可以有不同的类型,返回值类型在函数定义时说明。下面是一些函数定义的例子:
int min(int x, int y); // 返回值类型为 int,有两个整型参数,函数名为 min double calculate( int a, double b);
// 返回值类型为 double,有一个整型参数,一个 double型参数,函数名为 calculate
char judge(void); // 返回值类型为 char,没有参数,函数名为 judge void doit(int times);
// 返回值类型为 void,表示不返回任何值,有一个整型参数,函数名为 doit
1.9.4 库函数和头文件
C/C++语言标准中,规定了完成某些特定功能的一些函数,这些函数是不同厂商的
C/C++语言编译器都会提供的,并且在用 C/C++语言编程时可以直接调用的。这样的函数统称为 C/C++标准库函数。比如,前面看到的 printf 函数就是一例。
函数必须先声明原型,然后才能调用。C/C++语言规定,不同功能的库函数,在不同的头文件里进行声明。头文件就是编译器提供的,包含许多库函数的声明,以及其他内容(比如用#define 语句定义一系列标识符)的文件。头文件的后缀名是.h。编程时若要使用某个库函数,就需要用#include 语句将包含该库函数原型声明的头文件,包含到程序中,否则编译器就会认为该函数没有定义。比如,printf函数就是在 stdio.h 这个头文件中声明的,因此若要使用该函数,那么就要在程序开头加入:
#include <stdio.h>
1.10 标准输入输出
C/C++语言中,有一类库函数,称之为标准输入输出函数,可以用来从键盘读取键入的字符,以及将字符在屏幕上输出。这些函数的声明都包含在头文件 stdio.h中。我们介绍以下两个主要的标准输入输出函数:
printf scanf
1.10.1 printf函数(标准输出函数)
printf 函数的作用是将一个或多个字符按照程序员指定的格式输出到屏幕上。printf 函数调用的一般形式为:
printf(“格式控制字符串”,待输出项 1,待输出项 2……)
其中格式控制字符串用于指定输出格式,是用一对双引号括起来的。例如:
printf( “x = %d” , 50);
上面这条语句中,格式控制字符串就是“x = %d”,待输出项就是 50。其输出结果是: x = 50
象”%d”这样由一个“%”和其后一个(或多个)字符组成的字符串,称为“格式控制符”。它说明待输出项的类型、输出形式(比如以十进制还是二进制输出,小数点后面保留几位等等)。“%d”表示其对应的待输出项是整型。
“%”和特定的一些字符组合在一起,构成“格式控制符”。常见的格式控制符有:
%d 要输出一个整数
%c 要输出一个字符
%s 要输出一个字符串
%x 要输出一个十六进制整数
%u 要输出一个无符号整数(正整数)
%f 要输出一个浮点数。
“格式控制字符串”中, “格式控制符”的个数应该和待输出项的个数相等,并且类型须一一对应。“格式控制字符串”中非格式控制符的部分,则原样输出。例如:
printf(“Name is %s, Age = %d, weight = %f kg, 性别:%c, code = %x” ,
“Tom”, 32, 71.5, ‘M’, 32 );
输出结果是:
Name is Tom, Age = 32, weight = 71.500000 kg, 性别:M, code = 20
最后的待输出项“32” 对应的输出结果是“20”。因为它对应的输出控制符是“%x”,这就导致十进制数“32”被以十六进制的形式输出为“20”。
如果就是想输出“%d”这样一个字符串,怎么办呢?做法是,想输出一个“%”,就要连写两个“%”。例如:
printf(“a%%d”); 输出结果是: a%d
如果想让输出换行,则需输出一个换行符 “\n”。如: printf(“What’s up?\nGreat!\nLet’s go!”);
输出结果是:
What’s up?
Great!
Let’s go!
1.10.2 scanf函数(标准输入函数)
scanf 函数的一般形式为:
scanf(“格式控制字符串”,变量地址 1, 变量地址 2……);
scanf 函数的作用是从键盘接受输入,并将输入数据存放到变量中。“变量地址”的表示方法是在变量前面加”&”字符。“格式控制字符串”说明要输入的内容有几项、以及这几项分别是什么类型的。函数执行完后,输入内容的每一项分别被存放到各个变量中。例如:
#include <stdio.h> main()
{
char c; int n;
scanf("%c%d",&c, &n);
printf("%c,%d", c, n);
}
scanf 语句中的“%c%d”说明待输入的数据有两项,第一项是一个字符,第二项是一个整数。这两项之间可以用空格或换行进行分隔,也可以不分隔。scanf 函数会等待用户从键盘敲入数据,用户输完后必须再敲回车,scanf 函数才能继续执行,将两项输入数据存放到变量 c 和 n 中。上面的程序,不论敲入“t456 回车”,还是“t 空格 456 回车”还是“t 回车
456 回车”,结果都是一样的。输出结果为: t,456
即字符’t’被读入,存放在变量 c 中,”456”被读入,存放于变量 n 中。
如果要输入的是两个整数,那么这两个整数输入的时候必须用空格或回车分隔。下面的程序,提示用户输入矩形的高和宽,然后输出其面积。
#include <stdio.h> main()
{
int nHeight, nWidth;
printf("Please enter the height:\n"); scanf("%d",& nHeight);
printf("Please enter the width:\n"); scanf( "%d",& nWidth);
printf("The area is: %d", nHeight * nWidth);
}
试着运行一下,看一看结果。
1.11 全局变量和局部变量
定义变量时,可以将变量写在一个函数内部,这样的变量叫局部变量;也可以将变量写
在所有函数的外面,这样的变量叫全局变量。全局变量在所有函数中均可以使用,局部变量只能在定义它的函数内部使用。请看下面的程序:
例程 2.11.cpp:
1. int n1 = 5, n2 = 10;
2. void Function1() 3. {
4. int n3 =4;
5. n2 = 3;
6. }
7. void Function2() 8. {
9. int n4;
10. n1 = 4;
11. n3 = 5; //编译出错
12. }
13. int main()
14. {
15. int n5;
16. int n2;
17. if( n1 == 5 ) {
18. int n6;
19. n6 = 8;
20. }
21. n1 = 6;
22. n4 = 1; //编译出错
23. n6 = 9; //编译出错
24. n2 = 7;
25. return 0;
26. }
上面的程序中,n1、n2 是全局变量,所以在所有的函数中均能访问,例如语句 5、10、
21;n3 是在函数 Function1 里定义的,在其他函数中不能访问,因此语句 11 会导致“变量没定义”的编译错误;语句 22 也是一样。
一个局部变量起作用的范围(称为“作用域”),就是从定义该变量的语句开始,到包含该变量定义语句的第一个右大括号为止,因此语句 19 定义的变量 n6,其作用域就是从语句
19 开始直到语句 20 的位置。在语句 23 中试图访问 n6,导致“变量没定义”的编译错误。如果某局部变量和某个全局变量的名字一样,那么在该局部变量的作用域中,起作用的
是局部变量,全局变量不起作用。例如语句 16 定义的局部变量 n2 和全局变量 n2 同名,那么语句 24 改变的就是局部变量 n2 的值,不会影响全局变量 n2。
1.12 数组
1.12.1 一维数组
想想如何编写下面的程序:
接收键盘输入的 100 个整数,然后将它们按从小到大的顺序输出。
要编写这个程序,首先要解决的问题就是:如何存放这 100 个整数?直观的想法是定义
100 个 int 型变量,n1, n2, n3 ……n100,用来存放这 100 个整数。可这样的想法真让人受不了。
幸好,C/C++语言中“数组”的概念,为我们解决上述问题提供了很好的办法。实际上,几乎所有的程序设计语言,都支持数组,用来表达同类型数据元素的集合。在 C/C++中,数组的定义方法如下:
类型名 数组名[元素个数];
其中“元素个数”必须是常数或常量表达式,不能是变量,而且其值必须是正整数。元素个数也称作“数组的长度”。例如:
int an[100];
上面的语句就定义了一个名字为 an的数组,它有 100 个元素,每个元素都是一个 int
型变量。我们可以用 an 这个数组来存放上述程序所需要存储的 100 个整数。一般地,如果我们写:
T array[ N ]; //此处 T 可以是任何类型名,如 char,double,int 等。N 是一个正整数,
//或值为正整数的常量表达式
那么,我们就定义了一个数组,这个数组的名字是 array。array 数组里有 N 个元素,每个元素都是一个类型为 T 的变量。这 N 个元素在内存里是一个挨一个连续存放的。array数组占用了一片连续的、大小总共为 N × sizeof(T)字节的存储空间。
如何访问数组中的元素呢?实际上,每个数组元素都是一个变量,数组元素可以表示为以下形式:
数组名[下标]
其中下标可以是任何值为整型的表达式,该表达式里可以包含变量、函数调用。下标如为小数时,编译器将自动去尾取整。例:如果 array 是一个数组的名字,i,j 都是 int 型变量,那么
array[5] array[i+j] array[i++]
都是合法的数组元素。
在 C/C++语言中,数组的“下标”是从 0 开始的。也就是说,如果有数组: T array[N];
那么 array[N]中的 N 个元素,按地址从小到大的顺序,依次是 array[0], array[1], array[2]……array[N-1]。array[i](i 为整数)就是一个 T 类型的变量。如果 array[0]存放在地址 n, 那么 array[i]就被存放在地址 n + i * sizeof(T)。
好了,现在让我们来看看如何编写程序,接收键盘输入的 100 个整数,并排序后从小到
大输出。先将 100 个整数输入到一个数组中,然后对该数组进行排序,最后遍历整个数组,逐个输出其元素。对数组排序有很多种方法,这里我们采用一种最直观的方法,叫做“选择排序”,其基本思想是:如果有 N 个元素需要排序,那么首先从 N 个元素中找到最小的那个(称为第 0 小的)放在第 0 个位子上,然后再从剩下的 N-1 个元素中找到最小的放在第 1 个位子上,然后再从剩下的N-2 个元素中找到最小的放在第2 个位子上……直到所有的元素都就位。
例程 2.12.1.cpp:
1. #include <stdio.h>
2. #define MAX_NUM 100
3. int main()
4. {
5. int i, j;
6. int an[MAX_NUM];
7. //下面两行输入 100 个整数
8. for( i = 0;i < MAX_NUM ;i ++ ) 9. scanf("%d", & an[i]);
10. //下面对整个数组进行从小到大排序
11. for( i = 0; i < MAX_NUM - 1; i ++ ) { //第 i 次循环后就将第 i 小的数组元素放好
12. int nTmpMin = i; //用来记录从第 i 个到第 MAX_NUM-1 个元素中,最小的
//那个元素的下标
13. for( j = i; j < MAX_NUM; j ++ ) {
14. if( an[j] < an[nTmpMin] )
15. nTmpMin = j;
16. }
17.
18. //将第i 小的元素放在第 i 个位子上,并将原来占着第 i 个位子的那个元素挪到
//后面
19. int nTmp = an[i];
20. an[i] = an[nTmpMin];
21. an[nTmpMin] = nTmp;
22. }
23. //下面两行将排序好的 100 个元素输出
24. for( i = 0;i < MAX_NUM ;i ++ )
25. printf("%d\n", an[i]);
26. return 0;
27. }
思考题 1.12.1:请自己想想,用另外一种算法来编写排序程序。
本节中提到的数组,其元素都是用数组名加一个下标就能表示出来。这样的数组称为一维数组。实际上,C/C++还支持二维数组乃至多维数组。二维数组中的每个元素,需要用两个下标才能表示。
1.12.2 二维数组
如果需要存储一个矩阵,并且希望只要给定行号和列号,就能立即访问到矩阵中的元素, 该怎么办?一个直观的想法是矩阵的每一行都用一个一维数组来存放,那么矩阵有几行,就需要定义几个一维数组。这个办法显然很麻烦。C/C++语言支持 “二维数组”,能很好的解决这个问题。
如果我们写:
T array[N][M]; // 此处T 可以是任何类型名,如 char , double, int 等。M、N 都是
//正整数,或值为正整数的常量表达式
那么,我们就定义了一个二维数组,这个数组的名字是 array。array 数组里有 N×M个元素,每个元素都是一个类型为 T 的变量。这 N×M 个元素在内存里是一个挨一个连续存放的。array 数组占用了一片连续的、大小总共为 N×M×sizeof(T)字节的存储空间。
array 数组中的每个元素,都可以表示为: 数组名[行个标][列下标]
行下标和列下标都是从 0 开始的。
我们也可以称上面的二维数组 array 是N 行M 列的。其每一行都有 M 个元素,第 i 行的元素就是 array[i][0]、array[i][1]……array[i][M-1]。同一行的元素,在内存中是连续存放的。而第 j 列的元素的元素,就是 array[0][j]、array[1][j]……array[N-1][j]。
array[0][0] 是数组中地址最小的元素。如果 array[0][0] 存放在地址 n , 那么
array[i][j](i,j 为整数)存放的地址就是 n + i * M * sizeof(T) + j * sizeof(T)。
下图显示了二维数组 int a[2][3] 在内存中的存放方式。假设 a[0][0]存放的地址是100,那么 a[0][1]的地址就是 104,以此类推。
图 1-12-2 二维数组的一行
从上图可以看出,二维数组的每一行,实际上都是一个一维数组。对上面的数组 int
a[2][3]来说,a[0],a[1]都可以看作是一个一维数组的名字,不需要另外声明,就能直接使用。二维数组用于存放矩阵特别合适。一个 N行 M 列的矩阵,恰好可以用一个 N行 M 列
的二维数组进行存放。
遍历一个二维数组,将其所有元素依次输出的代码如下:
#define ROW 20
#define COL 30 int a[ROW][COL];
for( int i = 0; i < ROW -1 ; i ++) { for( int j = 0; j < COL -1 ; j ++ )
printf(“%d ”, a[i][j]); printf(“\n”);;
}
上面的代码将数组 a 的元素按行依次输出,即先输第 0 行的元素,然后再输出第 1 行的元素、第 2行的元素……
思考题 1.12.2:如果要将数组 a 的元素按列依次输出,即先输出第 0 列,再输出第 1 列……,该如何编写?
1.12.3 数组的初始化
在定义一个一维数组的同时,就可以给数组中的元素赋初值。具体的写法是:类型名 数组名[常量表达式]={值,值……值};
其中在{ }中的各数据值即为各元素的初值,值之间用逗号间隔。例如:
int a[10]={ 0,1,2,3,4,5,6,7,8,9 };
相当于 a[0]=0;a[1]=1...a[9]=9;
数组初始化时,{ }中值的个数可以少于元素个数。此时,相当只给前面部分元素赋值,而后面的元素,其存储空间里的每个字节都被写入二进制数 0。
例如:
int a[10]={0,1,2,3,4};
表示只给 a[0]~a[4]5 个元素赋值,而后 5 个元素自动赋 0 值。
在定义数组的时候,如给全部元素赋值,则可以不给出数组元素的个数。例如:
int a[]={1,2,3,4,5};
是合法的,a 就是一个有 5 个元素的数组。
二维数组也可以进行初始化。例如对于数组 int a[5][3],可用如下方式初始化: int a[5][3]={ {80,75,92},{61,65,71},{59,63,70},{85,90},{76,77,85} };
每个内层的{},初始化数组中的一行。例如, {80,75,92}就对数组第 0 行的元素进行初始化,结果使得 a[0][0] = 80, a[0][1] = 75, a[0][2] = 92。
1.12.4 数组越界
数组元素的下标,可以是任何整数,可以是负数,也可以大于数组的元素个数。如果出现这种情况,编译的时候是不会出错的。例如:
1. int an[10]; 2. an[-2] = 5;
3. an[200] = 10;
4. an[10] = 20;
5. int m = an[30];
这些语句的语法都没有问题,编译的时候都不会出错。那么,a[-2]是什么含义呢?如果数组 a的起始地址是 n,那么 a[-2]就代表位于地址 n + (-2) * size(int)处的一个 int 型变量。即位于地址 n-8 处的一个int型变量。编译器就是这样理解的。因此语句 2 的作用就是往地址 n – 8处写入数值 5(写入 4 个字节)。地址 n – 8 处,有可能存放的是其他变量,也有可能存放的是指令,往该处写入数据,就有可能意外更改了其他变量的值,甚至更改了程序的指令。程序继续运行就可能会出错。有时,n-8 处的地址可能是操作系统不允许程序进行写操作的,碰到这种情况,程序执行到语句 2 就会立即出错。因此,语句 2 是不安全的。
象语句 2 这样,要访问的数组元素并不在数组的存储空间内,这种现象就叫“数组越界”。语句 3、4、5 都会导致数组越界。要特别注意,an有 10 个元素,有效的元素是 an[0]
到 an[9],an[10]已经不在数组 an的地址空间内了。这是初学者经常会忽略的。语句 5 会导致 m被赋了一个不可预料的值。在有的操作系统中,程序的某些内存区域是不能读取的, 如果 an[30]正好位于这样的区域,执行到语句 5 就会立即引发错误。
除非有特殊的目的,一般我们不会写出象 an[-2] = 5 这样明显越界的语句。但是我们经常会用含有变量的表达式作为数组元素的下标使用。该表达式的值有可能会变成负数,或大于等于数组的长度。这就会导致数组越界。
数组越界是实际编程中常见的错误,而且这类错误往往难以捕捉。因为越界语句本身并不一定导致程序立即出错,但是它埋下的隐患可能在程序运行一段时间后才发作。甚至,运气好的话,虽然由于数组越界,意外改写的别的变量或者指令,但是在程序后续沿某个分支运行时并没有用到这些错误的变量或指令,那么程序就不会出错。
如果在跟踪调试程序的时候,发现某个变量变成了一个不正确的值,然而却想不出为什么这个变量会变成该值,就要考虑一下是否是由于某处的数组越界,导致该变量的值被意外修改了。尤其是定义该变量的附近,也定义了数组的时候。因为在一起定义的一些变量,它们的储存空间一般也是相邻的。
如果由于数组越界导致指令被修改的话,甚至会发生在调试器里调试的时候,程序不按照应当的次序运行的怪现象。比如,单步调试程序的时候,明明碰到一个条件为真的 if 语句,却就是不执行为真的那个分支。
1.13 字符串
C/C++中,字符串有两种形式。
第一种形式就是字符串常量,如"CHINA" , “C program”。
第二种形式的字符串,存放于字符数组中。该字符数组中包含一个’\0’字符,代表字符串的结尾。我们不妨将用来存放字符串的字符数组,称为“字符串变量”。
C/C++中有许多用于处理字符串的函数,它们都可以用字符串常量或字符数组的名字作为参数。请参见 2.17.3小节“字符串和内存操作函数”。
1.13.1 字符串常量
字符串常量是由一对双引号括起的字符序列。例如: "CHINA", “C program” ,
"$12.5" “a” 等都是合法的字符串常量。
一个字符串常量占据的内存字节数等于字符串中字符数目加 1。多出来的那个字节位于字符串的尾部,存放的是字符’\0’。字符’\0’的 Ascii码就是二进制数 0。C/C++中的字符串,都是以’\0’结尾的。
例如:
字符串 "C program" 在内存中的布局表示为:
C |
|
p |
r |
o |
g |
r |
A |
m |
\0 |
图 1-13-1-1
"" 也是合法的字符串常量。该字符串里没有字符,称为“空串”,但是仍然会占据一个字节的存储空间,就是用来存放代表结束位置的’\0’。
如果字符串常量中包含双引号,则双引号应写为“\"”。而“\”字符在字符串中出现时,须连写两次,变成“\\”。例如:
printf( "He said: \"I am a stu\\dent.\"");
该语句的输出结果是:
He said: "I am a stu\dent."
1.13.2 用字符数组存放的字符串
字符数组的形式与前面介绍的整型数组相同。例如:
char szString[10];
字符数组的每个元素占据一个字节。可以用字符数组来存放字符串,此时数组中须包含一个 ‘\0’字符,代表字符串的结尾。因而字符数组的元素个数,应该不少于被存储字符串的字符数目加 1。前面提到,不妨将存储字符串的数组称为“字符串变量”,那么,字符串变量的值,可以在初始化时设定,也可以用一些 C/C++库函数进行修改,还可以用对数组元素赋值的办法任意改变其中的某个字符。
下面通过一个例子程序来说明字符串变量的用法。例程 2.13.2.cpp:
1. #include <stdio.h>
2. #include <string.h>
3. int main() {
4. char szTitle[] = "Prison Break";
5. char szHero[100] = "Michael Scofield";
6. char szPrisonName[100];
7. char szResponse[100];
8. printf("What's the name of the prison in %s?\n", szTitle);
9. scanf( "%s", szPrisonName);
10. if( strcmp( szPrisonName, "Fox-River") == 0 ) {
11. printf("Yeah! Do you love %s?\n", szHero);
12. }
13. else {
14. strcpy( szResponse, "It seems you haven't watched it!\n");
15. printf( szResponse);
16. }
17. szTitle [0] = 't';
18. szTitle [3] = 0; //等效于 szTitle [3] = ‘\0’;
19. printf(szTitle);
20. return 0;
21. }
语句 4: 定义了一个字符数组 szTitle,并进行初始化,使得其长度自动为 13(字符串"Prison Break"中的字符个数再加上结尾的’\0’) 。初始化后 szTitle 的内存布局图为:
P |
r |
i |
s |
o |
n |
|
B |
r |
e |
a |
k |
\0 |
图 1-13-1-2
语句 5: 定义了一个有 100 个元素的字符数组 szHero,并初始化其前 17 个元素
(”Micheal Scofield”再加上结尾的’\0’)。语句 8:输出:
What's the name of the prison in Prison Break?
语句 9: 等待用户输入监狱的名字,并将用户的输入存放到 szPrisonName 数组中,在输入字符串的末尾自动加上’\0’。如果用户输入超过了 99 个字符,那么加上’\0’后,就会发生数组越界。scanf 函数的格式字符串中,“%s”表示要输入的是一个字符串。要注意,用 scanf 输入字符串时,输入的字符串中不能有空格,否则被读入的就是空格前面的那部分。例如,如果在本程序运行时输入“Fox River”再敲回车,那么 szPrisonName 中就会存入“Fox”而不是“Fox River”。
如果想要将将用户输入的包含一个甚至多个空格的一整行,都当作一个字符串读入到
szPrisonName 中,那么语句 9 应改成: gets( szPrisonName);
此时如果用户输入“Fox River”然后敲回车,则 szPrisonName 中就会存放着“Fox
River”。
gets 是一个标准库函数,它的原型是: char *gets(char *s);
功能就是将用户键盘输入的一整行,当作一个字符串读入到 s 中。当然,会自动在 s 后面添加’\0’。
语句 10:调用 string.h 中声明的字符串比较库函数 strcmp 和标准答案进行比较,如
果该函数返回值为 0,则说明比较结果一致。语句 11: 输出:
Yeah! Do you love Michael Scofield?
语句 14: 调用字符串拷贝库函数 strcpy 将"It seems you haven't watched it!"拷贝到数组 szResponse 中。使用字符串拷贝函数的时候一定要看看,数组是否能装得下要拷贝的字符串。要特别注意,该拷贝函数会在数组中自动多加一个表示结尾的’\0’
语句 15: 输出:
It seems you haven't watched it!
语句 17、18 执行后,szTitle 的内存图变为:
t |
r |
i |
\0 |
o |
n |
|
B |
r |
e |
a |
k |
\0 |
图 1-13-1-3
语句 19:由于在 C/C++中对字符串进行处理时,碰到’\0’就认为字符串结束了,因此本条语句输出:
tri
上面说的是用一维字符数组来存放字符串。实际上,二维字符数组也可以用来存放字符串。比如:
char szFriends[6][30] = { "Joey", "Phoebe", "Monica", "Chandler", "Ross", "Rachel" };
则printf(szFriends[0]); 会输出:
Joey printf(szFriends[5]); 会输出:
Rachel
思考题 1.13.3:编写一个函数int MyItoa( char * s ) ;
其功能是将 s 中以字符串形式存放的非负整数,转换成相应整数返回。例如,如果 s中存放字符串 “1234”,则该函数的返回值就是 1234。假设 s 中的字符全是数字,且不考虑 s 是空串或 s 太长的情况。
1.14 指针
1.14.1 指针的基本概念
程序运行时,每个变量都被存放在从某个内存地址开始的若干个字节中。所谓“指针”,也称作“指针变量”,是一种大小为 4 个字节的变量,其内容代表一个内存地址。大家应该还记得,内存地址的编排,是以字节为单位的。通过一个指针,我们能够对该指针所代表的内存地址开始的若干个字节进行读写。指针的定义方法是:
类型名 * 指针变量名;
例如:int * p; // p 是一个指针,变量 p 的类型是 int *
例如:char * pc; // pc 是一个指针, 变量 pc 的类型是 char * 例如:float * pf; // pf 是一个指针,变量 pf 的类型是 float *
下面的语句经过强制类型转换,将数值 10000 赋值给一个指针: int * p = ( int * ) 10000;
此时,p 这个指针的内容,就代表内存地址 10000。我们也可以说,p 指向内存地址 10000。请注意:在后文中,为了描述方便,如果 p 是一个指针,那么我们将“p 指向的内存地址”简称为“地址 p”。上面的语句执行后,如果我们想对内存地址 10000 起始的若干个字节进行读写,就可以通过表达式“* p” 来进行。因为表达式 “* p ”就代表地址 p 开始的若干字节。请看下面连续执行的两条语句的作用:
* p = 5000; // 往内存地址 10000 处起始的若干个字节的内存空间里写入数值 5000 int n = * p; // 将内存地址 10000 处起始的若干字节的内容赋值给 n ,实际效果是
// 使得 n = 5000
显然,从等号两边的表达式类型应该兼容,可以推想出,表达式 “* p”的类型应该是int。
前面的几行文字多次提到了“若干字节”,这个“若干字节”到底是多少字节呢?具体到 int * p 的这个例子,这个“若干字节”就是 4 个字节,因为,sizeof(int) = 4。
总结一下一般的规律: 如果定义:
T * p ; // T 可以是任何类型的名字,比如 int, double ,char 等等。下文中碰到的
//“T ”也都是这个意思
那么变量 p 就是一个“指针变量”(简称 “指针”),p 的类型是 T *,表达式“* p” 的类型是 T。而通过表达式“* p”,我们就可以读写从地址 p 开始的 sizeof(T)个字节。
通俗地说,就是可以认为,“* p” 这个表达式,等价于存放在地址 p 处的一个 T 类型
的变量。表达示 “* p”中的“*”,被称作“间接引用运算符”。
需要记住的是,不论 T 表示什么类型,sizeof(T *)的值都是 4。也就是说,所有指针变量,不论它是什么类型的,其占用的空间都是 4 个字节。因为,指针表示的是地址,而当前流行的 CPU 的内存寻址范围一般都是 4G,即 232,所以一个地址正好用 32 位,即 4 字节来表示。也许当 64 位的计算机普及后,新的 C/C++编译器会将指针处理成 8 个字节。
在实际编程中,我们极少需要象前面的“int * p = (int *) 10000”那样,直接给指针赋一个常数地址值。实际上直接读写某个常数地址处的内容,常常会导致程序出错,因为象 10000 这个地址里存放的是什么,谁也不知道,往 10000 这个地址里写数据,也许会造成一些破坏。指针的通常用法是:将一个 T 类型的变量 x 的地址,赋值给一个类型为 T *的指针 p(俗称“让 p 指向 x”),此后表达式“* p”即代表 p 所指向的变量(即 x),通过“* p”就能读取或修改变量 x 的值。请看下面的程序片段:
char ch1 = ’A’; // (1)
char * pc = &ch1; // (2) 使得 pc 指向变量 ch1
* pc = ’B’; // (3) 执行效果是使得 ch1 = ’B’ char ch2 = * pc; // (4) 执行效果是使得 ch2 = ch1 pc = & ch2; // (5) 使 得 pc 指 向 变 量 ch2 。
// 同一指针在不同时刻可以指向不同变量
* pc = ’D’; // 语句 6,执行效果是使得 ch2 = ’D’
上面的语句(2),所做的操作,是将变量 ch1 的地址写入指针 pc 中。通俗的说法,就是让指针 pc 指向变量 ch1。“&”符号,在此处被称作“取地址运算符”,功能是取得其操作数的地址。显然,“取地址运算符”是一个单目运算符。
请记住:对于类型为 T 的变量 x,表达式“&x”就表示变量 x 的地址,表达式“&x” 的类型是 T *。
语句(3)的作用,是往 pc 指向的地方写入字符’b’。由于 pc 指向的地方就是存放变量
ch1 的地方,“* pc” 等效于变量 ch1,因此语句(3)的作用就是往变量 ch1 里写入字符’b’。同样,在语句(4)中, * pc 等效于变量 ch1,因此语句 4 等效于用 ch1 对ch2 进行赋值。
也许有同学会问:如果我们需要修改一个变量的值,直接使用该变量就可以了,不需要通过指向该变量的指针来进行吧?那么指针到底有什么用呢?的确,并不是所有的程序设计语言都有“指针”的概念,Basic,Java都没有。但是“指针”在 C/C++中是十分重要的概念,有了指针,用 C/C++编写程序可以更加灵活,更加高效。同时,需要注意的是,指针的灵活性带来的副作用就是,大量使用指针的程序更容易出错。下面举一个例子来说明指针用途的一个方面:
假设需要编写一个函数 swap,执行 swap(a, b)的效果是将 a, b 两个变量的值互换。如果没有指针,那么在 C 语言中是无法实现这个功能的(在 C++中可以通过“引用”实现)。为什么呢?我们来看,假定 a, b 都是 int 型,那么有了下面的 swap 函数:
void swap(int n1, int n2)
{
int nTmp = n1; n1 = n2;
n2 = nTmp;
}
执行 swap(a, b)能够实现交换 a、b 的值吗?答案显然是否定的。因为在函数内部,n1、
n2 分别是 a、b 的一个拷贝,n1, n2的值改变了,不会影响到 a、b。正确的 swap函数的 C 语言实现方法,需要使用指针。代码如下:
void swap( int * pn1, int * pn2)
{
int nTmp = * pn1; // 将 pn1 指向的变量的值,赋给 nTmp
* pn1 = * pn2; // 将 pn2 指向的变量的值,赋给 pn1 指向的变量
* pn2 = nTmp; // 将 nTmp的值赋给 pn2 指向的变量。
}
而调用上述函数交换两个 int 型变量a、b 的值,则应该写为:
swap(& a, & b);
由于 “& a” 即是 a 的地址(其类型是 int * ),因此,swap 函数执行期间,pn1 的值即为 a 的地址,也可以说,pn1指向 a。那么“* pn1”就等价于 a, 同理,pn2指向 b, “* pn2” 就等价于 b。因此上面的函数能够实现交换 a、b 的值。
不同类型的指针,如果不经过强制类型转换,是不能直接互相赋值的。请看下面的程序片段:
int * pn, char * pc, char c = 0x65; pn = pc; //(1)
pn = & c; //(2)
pn = (int * ) & c; //(3)
int n = * pn; //(4)
* pn = 0x12345678; //(5)
语句(1)和(2)都会在编译的时候报错,错误信息是类型不兼容。因为在这两条语句中,等号左边的类型是 int * ,而等号右边的类型是 char * 。语句(3)则没有问题,虽然表达式“& c”的类型是 char * ,但是其值经过强制类型转换后,赋值给 pn 是可以的。语句(3)执行的效果是使得 pn 指向 c 的地址。
思考题 1.14.1:语句(4)的执行结果,是使得 n 的值变为 0x65 吗?语句(5)编译会不会出错?如果不出错,执行后会有什么结果?会不会有问题?
1.14.2 指针运算
指针变量可以进行以下运算:
1) 两个同类型的指针变量,可以比较大小
2)两个同类型的指针变量,可以相减。
3)指针变量可以和整数类型变量或常量相加。
4)指针变量可以和减去一个整数类型变量或常量。
5)指针变量还可以自增、自减。
比较大小的意思是:p1、p2 是两个同类型的指针,那么,如果地址 p1<地址 p2,则表达式“p1<p2”的值就为真,反之亦然。p1>p2, p1==p2 的意义也同样很好理解。
指针相减的定义是:如果有两个 T *类型的指针 p1 和p2,那么表达式“p1 – p2”的类型就是 int,其值可正可负,其值的绝对值表示在地址 p1 和 p2 之间能够存放多少个 T 类型的变量。写成公式就是:
p1 – p2 = ( 地址 p1 –地址 p2 ) / sizeof( T)
指针和整数相加的定义是:如果 p是一个 T * 类型的指针,而 n是一个整型变量或常量,那么表达式“p + n”就是一个类型为 T * 的指针,该指针指向的地址是:
地址 p + n × sizeof(T)
“n+p”的意义与“p+n”相同。
指针减去整数的定义是:如果 p是一个 T * 类型的指针,而 n 是一个整型变量或常量,那么表达式“p - n”就是一个类型为 T * 的指针,该指针指向的地址是:
地址 p - n × sizeof(T)
当然,按照上面的定义,*( p + n) , * (p – n) 都是有意义的了。请同学们自己思考其含义。
思考题 1.14.2.1:如果 p 是一个 T * 类型的指针,那么 p++、 ++p、p--、--p 分别是什么意思呢?
下面通过一个具体的实例来说明指针运算的用法。例程 2.14.2.cpp
1. #include <stdio.h>
2. int main()
3. {
4. int * pn1, * pn2;
5. int n = 4;
6. char * pc1, * pc2;
7. pn1 = (int *) 100; //地址pn1 为 100
8. pn2 = (int *) 200; //地址pn2 为 200
9. printf("%d\n", pn2 - pn1); //输出 25, 因为(200-100)/sizeof(int) = 100/25 = 4
10. pc1 = (char * ) pn1; //地址pc1 为 100
11. pc2 = (char * ) pn2; //地址pc2 为 200
12. printf("%d\n", pc1 - pc2 ); //输出 -100,因为(100-200)/sizeof(char) = -100 13. printf("%d\n", (pn2 + n) - pn1); //输出 29
14. int * pn3 = pn2 + n; // pn2 + n 就是一个指针,当然可以用它给 pn3 赋值
15. printf("%d\n", pn3 - pn1); // 输出 29 16. printf("%d", (pc2 - 10) - pc1);
17. return 0;
18. }
在语句 13 中,表达式“pn2 + n”实际上是一个 int * 类型的指针,其值为: 地址 pn2 + n × sizeof(int) = 200 + 4 ×4 = 216
(pn2 + n)- pn1 实际上就是两个 int * 类型的指针相减,结果是:
(216-100)/sizeof(int) = 116/4 = 29
思考题 1.14.2.2:上面语句 16 的输出结果是什么?
这里只讲明了指针运算的定义。指针运算的作用,在后文“指针和数组”一节里有示例。
1.14.3 空指针
在 C/C++中,可以用“NULL”关键字对任何类型的指针进行赋值。值为 NULL 的指针,被称作空指针。空指针指向地址 0。一般来说,程序不需要,也不能够在地址 0 处进行读写。
如:int * pn = NULL; char * pc = NULL;
1.13.4 指向指针的指针
如果一个指针里存放的是另一个指针的地址,则称这个指针为指向指针的指针。前面提到的指针定义方法是:
T * p;
这里的 T 可以是任何类型的名字。实际上,“char *”, “int *”也都是类型的名字。因此,
int * * p;
这样的写法也是合法的,它定义了一个指针 p,变量 p的类型是 int ** 。“* p”则表示一个类型为 int * 的指针变量。在这种情况下,我们说,p是“指针的指针”,因为 p 指向的是个类型为 int *的指针,即可以认为 p指向的地方存放着一个类型为 int * 的指针变量。
总结一般的规律,如果定义:
T ** p; // 此处 T 可以是任何类型名
那么 p 就被称作“指针的指针”。p 这个指针,其类型是 T **,而表达式“* p”的类型是 T *,“* p”表示一个类型为 T * 的指针。
同理,int *** p; int **** p; int ***** p ; 等,不论中间有多少个“*”,都是合法的定义。
再次强调一下,不论 T 表示什么类型,sizeof(T *)的值都是 4。也就是说,所有指针变量,不论它是什么类型的,其占用的空间都是 4 个字节。
我们还可以定义指针数组,如:
int * array[5];
那么 array 数组里的每个元素都是一个类型为 int * 的指针。
1.14.5 指针和数组
一个数组的名字,实际上就是一个指针,该指针指向这个数组存放的起始地址。即,如果我们定义数组
T array[N];
那么标识符“array”的类型就是 T *。可以用 array 给一个 T * 类型的指针赋值,但是,array实际上是编译时其值就确定了的常量,对 array 进行赋值。例如,如果定义:
int array[5];
那么 array 的类型就是 int *。如果定义:
int * array[5];
那么 array 的类型就是 int **。请看下面的程序:
例程 2.14.5.1.cpp:
1. #include <stdio.h>
2. int main()
3. {
4. int an[200];
5. int * p ;
6. p = an; // p 指向数组an 的起始地址,亦即 p 指向了 an[0]
7. * p = 10; //使得 an[0] = 10
8. *( p + 1 ) = 20; //使得 an[1] = 20
9. p[0] = 30; // p[i] 和 *(p+i) 是等效的,此句使得 an[0] = 30
10. p[4] = 40; //使得 a[4] = 40
11. for( int i = 0;i < 10;i ++) //通过一个循环对数组 an 的前 10 个元素进行赋值
12. *( p + i) = i;
13. p++; // p 指向 a[1]
14. printf("%d\n", p[0]); // 输出 a[1]的值,即 1。p[0] 等效于 * p 15. p = an + 6; // p 指向 a[6]
16. printf("%d\n", * p); // 输出 6
17. return 0;
18. }
上面程序的输出结果是:
1
6
语句 8,回顾前面学过的指针运算,表达式“p+1”就是一个 int * 类型的指针,而该指针指向的地址就是:地址 p+sizeof(int),而此时 p指向 a[0],那么 p+1自然就指向 a[1]了。
语句 9 的注释提到:p[i] 和 *(p+i)是等效的。这是 C/C++语法的规定,任何情况下都是如此,不论 p 是否指向一个数组。
下面的例程中编写了一个对数组进行排序的函数 BubbleSort,该函数的第一个参数对应于数组起始地址,第二个参数对应于数组的元素个数。
例程 2.14.5.2.cpp
1. #include <stdio.h>
2. void BubbleSort( int * pa, int nNum) 3. {
4. for( int i = nNum -1 ; i > 0; i -- )
5. for( int j = 0; j < i; j ++)
6. if( pa[j] > pa[j+1] ) {
7. int nTmp = pa[j];
8. pa[j] = pa[j+1];
9. pa[j+1] = nTmp;
10. }
11. }
12. #define NUM 5
13. int main()
14. {
15. int an[NUM] = {5,4,8,2,1 };
16. BubbleSort(an, NUM); //将数组 an 从小到大排序
17. for( int i = 0;i <NUM;i ++ )
18. printf("%d\n", an[i]);
19. return 0;
20. }
在上面这个例程中,排序的算法称做“起泡排序”。其过程是:先让 pa[0]和 pa[1]比较,如果 pa[0]>pa[1],那么就交换 pa[0]和 pa[1];然后 pa[1]和 pa[2]比较,如果 pa[1]>pa[2],则交换 pa[1] 和 pa[2] ……一直做到 pa[nNum-2] 和 pa[nNum-1] 比较,如果pa[nNum-2]>pa[nNum-1],则交换 pa[nNum-2]和 pa[nNum-1]。经过这一轮的比较和交换,最大的那个元素就会被排在数组末尾,像气泡逐渐浮出水面一样。接下来再从头进行第二轮的比较和交换,让次大的元素浮出到次末尾的位置。一轮轮进行下去,最终将整个数组排好序。
上面的 BubbleSort 函数定义,写成:void BubbleSort( int pa[], int nNum) 而其他地方都不变,也是一样的。
上面讲述的是指针和一维数组的关系。对于二维数组来说,如果我们定义:
T array[M][N];
那么,array[i](i 是整数)就是一个一维数组,所以 array[i]的类型是 T *。array[i]指向的地址,等于:数组 array 的起始地址+ i×N。因此 array 的起始地址,实际上就是 array[0]。
假定有数组:
int array[4][5];
那么如下调用上面那个例程中的函数:
BubbleSort( array[1], 5);
就能将 array 数组的第 1 行排序。而执行 BubbleSort(array[0], 3)则能将第 0 行的前 3
个元素排序。
思考题 1.14.5:编写一个函数,参数是 int 型二维数组的起始地址以及行数、列数, 函数将此二维数组逐行输出。
1.14.6 字符串和指针
字符串常量的类型就是 char *。字符数组名的类型当然也是 char * 。因此可以用一个字符串,或一个字符数组名,给一个 char * 类型的指针赋值。例如:
1. #include <stdio.h>
2. #include <string.h>
3. int main() {
4. char * p = "Tom \n";
5. char szName[20];
6. char * pName = szName;
7. scanf( "%s", pName);
8. printf( p);
9. printf( "Name is %s", pName);
10. return 0;
11. }
上面的程序等待用户输入一个字符串,如果用户输入“Jack”那么输出结果就是:
Tom
Name is Jack
可见,在 printf, scanf 函数的输入输出格式字符串中,“%s”所对应的项目,一定是一个类型为 char * 的表达式。
语句 7 执行时,将用户输入写入到 pName 指向的地方,即 szName 数组。如果用户输入的字符超过 19 个,则会发生 szName 数组越界。
一种初学者常犯的错误如下:
char * p;
scanf( “%s”, p);
scanf 语句会将用户输入写入到 p指向的地方。可是此时 p 指向哪里呢?不确定。往一个不知是哪里的地方写入数据,是不安全的,很可能导致程序的异常错误。
1.14.7 void指针
void * p;
上面的语句定义了一个指针 p,其类型是 void *。这样的指针称之为 void 指针。可以用任何类型的指针对 void 指针进行赋值。例如:
double d = 1.54; void * p = & d;
但是,由于 sizeof(void) 是没有定义的,所以对于 void * 类型的指针 p, 表达式“* p” 也没有定义,而且所有前面所述的指针运算对 p 也不能进行。
void 指针主要用于内存拷贝。将内存中某一块的内容拷贝到另一块去,那么源块和目的块的地址就都可以用 void 指针表示。C/C++中有以下标准库函数:
void *memcpy(void *dest, const void *src, unsigned int n);
它在头文件 string.h和 mem.h 中声明,作用就是将地址 src开始的 n 字节内容,拷贝到地址 dest。返回值就是 dest 。
下面的程序片段,能将数组 a1的内容拷贝到数组 a2 中去。结果就是 a2[0] = a1[0], a2[1]
= a1[1]……a2[9] = a1[9] :
int a1[10];
int a2[10];
memcpy( a2, a1, 10*sizeof(int));
如果我们自己编写一个这样的内存拷贝函数 MyMemcpy,那么可以如下编写: void * MyMemcpy( void * dest , const void * src, int n)
{
char * pDest = (char * )dest; char * pSrc = ( char * ) src;
for( int i = 0; i < n; i ++ ) { //逐个字节拷贝源块的内容到目的块
* (pDest + i) = * ( pSrc + i );
}
return dest;
}
思考题 1.14.7:上面的 MyMemcpy 函数是有缺陷的,在某些情况下不能得到正确结果。缺陷在哪里?如何改进?
1.14.8 函数指针
程序运行期间,每个函数的函数体都会占用一段连续的内存空间。而函数名就是该函数体所占内存区域的起始地址(也称“入口地址”)。我们可以将函数体的入口地址赋给一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以调用这个函数。这种指向函数的指针变量称为“函数指针”。
函数指针定义的一般形式为:
类型名 (* 指针变量名)(参数类型 1, 参数类型 2,……);
其中“类型名”表示被指函数的返回值的类型。“(参数类型 1, 参数类型 2,……)”中则依次列出了被指函数的所有参数及其类型。例如:
int (*pf)(int , char);
表示 pf 是一个函数指针,它所指向的函数,返回值类型应是 int,该函数应有两个参数,第一个是 int 类型,第二个是 char 类型。
可以用一个原型匹配的函数的名字给一个函数指针赋值。要通过函数指针调用它所指向的函数,写法为:
函数指针名(实参表);
下面的程序说明了函数指针的用法
1. #include <stdio.h>
2. void PrintMin(int a, int b) 3. {
4. if( a<b )
5. printf("%d", a);
6. else
7. printf("%d", b); 8. }
9. int main(){
10. void (* pf)(int , int); //定义函数指针 pf
11. int x = 4, y = 5;
12. pf = PrintMin; //用 PrintMin 函数对指针 pf 进行赋值
13. pf(x, y); //调用pf 指向的函数,即 PrintMin
14. return 0;
15. }
上面的程序输出结果是: 4
C/C++中有一个快速排序的标准库函数 qsort ,在 stdlib.h 中声明,其原型为:
void qsort(void *base, int nelem, unsigned int width,
int ( * pfCompare)( const void *, const void *));
使用该函数,可以对任何类型的一维数组排序。该函数参数中,base 是待排序数组的起始地址,nelem 是待排序数组的元素个数,width 是待排序数组的每个元素的大小(以字节为单位),最后一个参数 pfCompare 是一个函数指针,它指向一个“比较函数”。排序就是一个不断比较并交换位置的过程。qsort 如何在连元素的类型是什么都不知道的情况下,比较两个元素并判断哪个应该在前呢?答案是,qsort 函数在执行期间,会通过 pfCompare指针调用一个 “比较函数”,用以判断两个元素哪个更应该排在前面。这个“比较函数”不是 C/C++的库函数,而是由使用 qsort 的程序员编写的。在调用 qsort 时, 将“比较函数” 的名字作为实参传递给 pfCompare。程序员当然清楚该按什么规则决定哪个元素应该在前, 哪个元素应该在后,这个规则就体现在“比较函数”中。
qsort 函数的用法规定,“比较函数”的原型应是:
int 函数名(const void * elem1, const void * elem2);
该函数的两个参数,elem1 和 elem2,指向待比较的两个元素。也就是说, * elem1 和
* elem2 就是待比较的两个元素。该函数必须具有以下行为:
1) 如果 * elem1 应该排在 * elem2 前面,则函数返回值是负整数(任何负整数都行)。
2) 如果 * elem1 和* elem2 哪个排在前面都行,那么函数返回 0
3) 如果 * elem1 应该排在 * elem2 后面,则函数返回值是正整数(任何正整数都行)。下面的程序,功能是调用 qsort 库函数,将一个 unsigned int 数组按照个位数从小到
大进行排序。比如 8,23,15 三个数,按个位数从小到大排序,就应该是 23,15,8
1. #include <stdio.h>
2. #include <stdlib.h>
3. int MyCompare( const void * elem1, const void * elem2 ) 4. {
5. unsigned int * p1, * p2;
6. p1 = (unsigned int *) elem1;
7. p2 = (unsigned int *) elem2;
8. return (* p1 % 10) - (* p2 % 10 ); 9. }
10. #define NUM 5
11. int main()
12. {
13. unsigned int an[NUM] = { 8,123,11,10,4 };
14. qsort( an, NUM, sizeof(unsigned int), MyCompare);
15. for( int i = 0;i < NUM; i ++ )
16. printf("%d ", an[i]);
17. return 0;
18. }
上面程序的输出结果是: 10 11 123 4 8
qsort 函数执行期间,需要比较两个元素哪个应在前面时,就以两个元素的地址作为参数,调用 MyCompare 函数。如果返回值小于 0,则 qsort 就得知第一个元素应该在前,如果返回值大于 0,则第一个元素应该在后。如果返回值等于 0,则哪个在前都行。
对语句 6 解释如下:由于 elem1 是 const void * 类型的,是 void 指针,那么表达式
“*elem1”是没有意义的。elem1 应指向待比较的元素,即一个 unsigned int 类型的变量,所以要经过强制类型转换,将 elem1 里存放的地址赋值给 p1,这样,* p1 就是待比较的第一个元素了。语句 7 同理。
语句 8 体现了排序的规则。如果 *p1 的个位数小于 *p2 的个位数,那么就返回负值。其他两种情况不再赘述。
思考题 1.14.8.1:如果要将 an 数组从大到小排序,那么 MyCompare 函数该如何编
写?
思考题 1.14.8.2:请自己写一个和 qsort 原型一样的通用排序函数 MySort,使得上面的程序如果不调用 qsort,而是调用 MySort,结果也一样(当然 MySort 函数需被添加到上面的程序中)。对排序的算法和效率没有要求。
1.14.9 指针和动态内存分配
在数组一章中,曾介绍过数组的长度是预先定义好的,在整个程序中固定不变。C/C++ 不允许定义元素个数不确定的数组。
例如: int n;
int a[n]; //这种定义是不允许的。
但是在实际的编程中,往往会发生所需的内存空间大小,取决于实际要处理的数据多少,在编程时无法确定的情况。如果总是定义一个尽可能大的数组,又会造成空间浪费。何况, 这个“尽可能大”到底是多大才够?
为了解决上述问题,C++ 提供了一种“动态内存分配”的机制,使得程序可以在运行期间,根据实际需要,要求操作系统临时分配给自己一片内存空间用于存放数据。此种内存分配是在程序运行中进行的,而不是在编译时就确定的,因此称为“动态内存分配”。在 C++ 中,通过“new”运算符来实现动态内存分配。new 运算符的第一种用法如下:
P = new T;
T 是任意类型名,P 是类型为 T * 的指针。这样的语句,会动态分配出一片大小为
sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给 P。比如:
int * pn;
pn = new int; //(1)
* pn = 5;
语句(1)动态分配了一片 4 个字节大小的内存空间,而 pn 指向这片空间。通过 pn, 可以读写该内存空间。
new 运算符还有第二种用法,用来动态分配一个任意大小的数组: P = new T[N];
T 是任意类型名,P 是类型为 T * 的指针,N 代表“元素个数”,它可以是任何值为正整数的表达式,表达式里可以包含变量、函数调用。这样的语句动态分配出 N × sizeof(T) 个字节的内存空间,这片空间的起始地址被赋值给 P。
例 如 : int * pn;
int i = 5;
pn = new int[i * 20]; pn[0] = 20;
pn[100] = 30; //(1)
语句(1)编译时没有问题。但运行时会导致数组越界。因为上面动态分配的数组,只有 100 个元素,pn[100]已经不在动态分配的这片内存区域之内了。
如果要求分配的空间太大,操作系统找不到足够的内存来满足,那么动态内存分配就会失败。保险做法是在进行较大的动态内存分配时,要判断一下分配是否成功。
判断的方法是:如果 new 表达式返回值是 NULL,则分配失败,否则分配成功。例如:
int * pn = new int[200000]; if( pn == NULL )
printf( “内存分配失败”); else
printf( “内存分配成功”);
程序从操作系统动态分配所得的内存空间,使用完后应该释放,交还操作系统,以便操作系统将这片内存空间分配给其他程序使用。C++提供 “delete” 运算符,用以释放动态分配的内存空间。
delete 运算符的基本用法是:
delete 指针;
该指针必须是指向动态分配的内存空间的,否则运行时很可能会出错。例如: int * p = new int;
* p = 5; delete p;
delete p; //本句会导致程序异常
上面的第一条 delete 语句,正确地释放了动态分配的 4 个字节内存空间。第二条
delete 语句会导致程序出错,因为 p 所指向的空间已经释放,p 不再是指向动态分配的内存空间的指针了。
再例如:
int * p = new int; int * p2 = p; delete p2;
delete p1;
上面这段程序,同样是第一条 delete 语句正确,第二条 delete 语句会导致出错。
如果是用 new 的第二种用法分配的内存空间,即动态分配了一个数组,那么,释放该数组的时候,应以如下形式使用 delete 运算符:
delete [] 指针; 例如:
int * p = new int[20]; p[0] = 1;
delete [] p;
同样要求,被 delete 的指针 p 必须是指向动态分配的内存空间的指针,否则会出错。如果动态分配了一个数组,但是却用“delete 指针”的方式释放,则编译时没有
问题,运行时也一般不会发现异常,但实际上会导致动态分配的数组没有被完全释放。
请牢记,用 new 运算符动态分配的内存空间,一定要用 delete 运算符予以释放。否则即便程序运行结束,这部分内存空间仍然不会被操作系统收回,从而成为被白白浪费掉的内存垃圾。这种现象也称为“内存泄漏”。
如果一个程序不停地进行动态内存分配而总是忘了释放,那么可用内存就会被该程序大量消耗,即便该程序结束也不能恢复。这就会导致操作系统运行速度变慢,甚至无法再启动新的程序。当然,不用太担心,只要重新启动计算机,症状就会消失了。
编程时如果进行了动态内存分配,那么一定要确保其后的每一条执行路径都能释放它。
1.14.10 误用无效指针
指针提供了灵活强大的功能,但也是程序 bug、尤其是难以捕捉的 bug 的罪魁祸首。许多错误就是因为在指针指向了某个不安全的地方,甚至指针为 NULL 的时候,还依然通过该指针读写其指向的内存区域而引起的。这样的错误导致的现象和上一章中“数组越界”导致的现象几乎完全一样。
例如,新手常写出以下错误的代码:
char * p;
scanf( “%s”, p); //希望将一个字符串从键盘读入,存放到 p 指向的地方
p 并没有经过赋值,不知道指向哪里,此时用 scanf语句往 p 指向的地方读入字符串,当然是不安全的。
1.15 结构
1.15.1 “结构”的概念
在现实问题中,常常需要用一组不同类型的数据来描述一个对象。比如一个学生的学号、姓名和绩点。一个工人的姓名、性别、年龄、工资、电话。如果编程时要用多个不同类型的变量来描述一个这样的对象,当要描述的对象较多的时候,就很麻烦,程序容易写错了。当然希望只用一个变量就能代表一个“学生”这样的对象。
C/C++允许程序员自己定义新的数据类型。因此我们可以定义一种新的数据类型,比如该类型名为 Student,那么一个 Student 类型的变量就能描述一个学生的全部信息。我们还可以定义另一种新的数据类型,比如名为 Worker,那么一个 Worker 类型的变量就能描述一个工人的全部信息。如何定义这么好用的“新类型”呢?
C/C++中有“结构”(也称为“结构体”)的概念,支持在已有基本数据类型的基础上定义复合的数据类型。用“struct”关键字来定义一个“结构”,也就定义了一个新的数据类型。定义“结构”的具体写法是:
struct 结构名 {
成员类型名成员变量名; 成员类型名成员变量名; 成员类型名成员变量名;
……
};
例如:
struct Student { unsigned ID; char szName[20]; float fGPA;
};
在上面这个结构定义中,结构名为 Student。结构名可以作为数据类型名使用。定义了一个结构,亦即定义了一种新的数据类型。在上面,我们就定义了一种新的数据类型,名为
Student。一个 Student 结构的变量是一个复合型的变量,由 3 个成员组成。第一个成员变量 ID 是 unsigned 型的,用来表示学号;第二个成员变量 szName 是字符数组,用来表示姓名;第三个成员变量 fGPA 是 float 型的,表示绩点。不要忘了结构定义一定是以一个分号结束。
象 Student 这样通过 struct 关键字定义出来的数据类型,一般统称为“结构类型”。由结构类型定义的变量,统称为“结构变量”。
1.15.2 结构变量的定义
定义了一个结构类型后,就能定义该结构的变量了。在 C++中,定义方法就是:结构名 变量名;
例如,如果定义了结构: struct Student {
unsigned ID; char szName[20]; float fGPA;
};
那么,
Student stu1, stu2;
就定义了两个结构变量 stu1和 stu2。这两个变量的类型都是 Student。还可以直接写: struct Student {
unsigned ID; char szName[20]; float fGPA;
} stu1, stu2;
也能定义出 stu1, stu2 这两个 Student 类型的变量。
显然,象 stu 1 这样的一个变量,就能描述一个学生的基本信息。两个同类型的结构变量,可以互相赋值。如:stu1 = stu2;
一般来说,一个结构变量所占的内存空间的大小,就是结构中所有成员变量大小之和。所以 sizeof(Student) = 28。结构变量中的各个成员变量在内存中一般是连续存放的,定义时在前面的成员变量,地址也在前面。比如,一个 Student 类型的变量,共占用 28 字节, 其内存布局图如下:
一个结构的成员变量可以是任何类型的,包括可以是另一个结构类型。比如,定义了一个结构:
struct Date {
int nYear; int nMonth; int nDay;
};
之后,我们还可以再定义一个更详细的包括生日的 StudentEx 结构: struct StudentEx {
unsigned ID; char szName[20]; float fGPA;
Date Birthday;
};
后文中还会用到 StudentEx 结构,为节省篇幅在后文里对 StudentEx 就不再说明了。
思考题 1.15.2:StudentEx 变量的内存布局图是什么样的?
1.15.3 访问结构变量的成员变量
一个结构变量的成员变量,可以完全和一个普通变量一样来使用,也可以取得其地址。访问结构变量的成员变量的一般形式是:
结构变量名.成员变量名
假设已经定义了前面的 StudentEx 结构,那么我们就可以写: StudentEx stu;
scanf(“%f”, & stu.fGPA); stu.ID = 12345;
strcpy( stu.szName, ”Tom”); printf(“%f”, stu.fGPA); stu.Birthday.nYear = 1984;
unsigned * p = & stu.ID; //p 指向 stu 中的 ID 成员变量
1.15.4 结构变量的初始化
结构变量可以在定义时进行初始化。例如对前面提到的 StudentEx 类型,其变量可以用如下方式初始化:
StudentEx stu = { 1234,”Tom”,3.78,{ 1984,12,28 }};
初始化后,stu 所代表的学生,学号是 1234,姓名为“Tom”,绩点是 3.78,生日是 1984年 12 月 28 日。
1.15.5 结构数组
数组的元素也可以是结构类型的。在实际应用中,经常用结构数组来表示具有相同属性的一个群体。如一个班的学生等。
定义结构数组的方法是:
结构名 数组名[元素个数]; 例如:
StudentEx MyClass[50];
就定义了一个包含 50 个元素的结构数组,用来记录一个班级的学生信息。数组的每个元素都是一个 StudentEx 类型的变量。标识符“MyClass”的类型就是 StudentEx *。
对结构数组也可以进行初始化。如: StudentEx MyClass[50] = {
{ 1234,”Tom”,3.78,{ 1984,12,28 }},
{ 1235,”Jack”,3.25,{ 1985,12,23 }},
{ 1236,”Mary”,4.00,{ 1984,12,21 }},
{ 1237,”Jone”,2.78,{ 1985,2,28 }}
};
用这种方式初始化,则数组 MyClass 后面的 46 个元素,其存储空间里的每个字节都被写入二进制数 0。
定义了 MyClass 后,以下语句都是合法的: MyClass[1].ID = 1267;
MyClass[2].Birthday.nYear = 1986; int n = MyClass[2].Birthday.nMonth; scanf(“%s”, MyClass[0].szName);
1.15.6 指向结构变量的指针
可定义指向结构变量的指针,即所谓“结构指针”。定义的一般形式为:结构名 * 指针变量名;
例如:
StudentEx * pStudent; StudentEx Stu1; pStudent = & Stu1;
StudentEx Stu2 = * pStudent;
通过指针,访问其指向的结构变量的成员变量,写法有两种:指针->成员变量名
或为:
(* 指针).成员变量名例如:
pStudent->ID; 或者:
(* pStudent).ID;
下面的程序片段通过指针对一个 StudentEx 变量赋值,然后输出其值。StudentEx Stu;
StudentEx * pStu;
pStu = & Stu; pStu->ID = 12345;
pStu->fGPA = 3.48;
printf(“%d”, Stu.ID); //输出 12345 printf( “%f”, Stu.fGPA); //输出 3.48
结构指针还可以指向一个结构数组,这时结构指针的值是整个结构数组的起始地址。结构指针也可指向结构数组的一个元素,这时结构指针的值是该数组元素的地址。
设 ps 为指向某结构数组的指针,则 ps 指向该结构数组的 0 号元素,ps+1 指向 1 号元素,ps+i 则指向 i 号元素。这与普通数组的情况是一致的。
结构变量可以作为函数的参数。如:
void PrintStudentInfo( StudentEx Stu); StudentEx Stu1;
PrintStudentInfo( Stu1);
当调用上面的 PrintStudentInfo 函数时,参数 Stu 会是变量 Stu1 的一个拷贝。如果
StudentEx 结构的体积较大,那么这个拷贝操作就会耗费不少的空间和时间。可以考虑使用结构指针作为函数参数,这时参数传递的只是 4 个字节的地址,从而减少了时间和空间的开销。例如:
void PrintStudentInfo( StudentEx * pStu); StudentEx Stu1;
PrintStudentInfo( & Stu1);
那么在 PrintStudentInfo 函数执行过程中,pStu 指向 Stu1 变量,通过 pStu 一样可以访问到 Stu1 的所有信息。
下面的例程调用 qsort 函数,将一个 Student 结构数组先按照绩点从小到大排序输出,再按照姓名字典顺序排序输出。
1. #include <stdio.h>
2. #include <string.h>
3. #include <stdlib.h>
4. #define NUM 4
5. struct Student {
6. unsigned ID;
7. char szName[20];
8. float fGPA; 9. } ;
10. Student MyClass[NUM] = { 11. { 1234,"Tom", 3.78},
12. { 1238,"Jack",3.25},
13. { 1232,"Mary",4.00},
14. { 1237,"Jone",2.78}
15. };
16. int CompareID( const void * elem1, const void * elem2)
17. {
18. Student * ps1 = (Student *) elem1;
19. Student * ps2 = (Student *) elem2;
20. return ps1->ID - ps2->ID;
21. }
22. int CompareName( const void * elem1, const void * elem2)
23. {
24. Student * ps1 = (Student *) elem1;
25. Student * ps2 = (Student *) elem2;
26. return strcmp( ps1->szName, ps2->szName);
27. }
28. int main()
29. {
30. int i;
31. qsort( MyClass, NUM, sizeof(Student), CompareID);
32. for( i = 0;i < NUM;i ++)
33. printf("%s ", MyClass[i].szName);
34. printf("\n");
35. qsort( MyClass, NUM, sizeof(Student), CompareName);
36. for( i = 0;i < NUM;i ++)
37. printf("%s ", MyClass[i].szName);
38. return 0; 39. };
上面的程序输出结果是:
Mary Tom Jone Jack Jack Jone Mary Tom
1.15.7 动态分配结构变量和结构数组
结构变量、结构数组都是可以动态分配存储空间的,如:
StudentEx * pStu = new StudentEx; pStu->ID = 1234;
delete pStu;
pStu = new StudentEx[20]; pStu[0].ID = 1235;
delete [] pStu;
1.16 文件读写
既可以从文件中读取数据, 也可以向文件中写入数据。读写文件之前,首先要打开文件。读写文件结束后,要关闭文件。C/C++提供了一系列库函数,声明于stdio.h中,用于进行文件操作。这里介绍其中几个常用的文件操作库函数:
1.16.1 用fopen 打开文件
该函数的原型为:
FILE *fopen(const char *filename, const char *mode);
“FILE” 是在 stdio.h中定义的一个结构,用于存放和文件有关的信息,具体内容我们可以不需要知道。第一个参数是文件名,第二个参数是打开文件的模式。
打开文件的模式主要有以下几种:
“r” :以文本方式打开文件,只进行读操作。
“w”: 以文本方式打开文件,只进行写操作。
“a”: 以文本方式打开文件,只往其末尾添加内容。“rb” :以二进制方式打开文件,只进行读操作。
“wb”: 以二进制方式打开文件,只进行写操作。
“ab”: 以二进制方式打开文件,只往其末尾添加内容。
“r+” :以文本方式打开文件,既读取其数据,也要往文件中写入数据。
“r+b”:以二进制方式打开文件,既读取其数据,也要往文件中写入数据。
“文本方式”适用于文本文件,即能在“记事本”中打开的,人能够看明白其含义的文件。“二进制方式”适用于任何文件,包括文本文件,音频文件,视频文件,图像文件,可执行文件等。只不过文本文件用“文本方式”打开,以后读写会方便一些。
fopen函数返回一个 FILE * 类型的指针,称为文件指针。该指针指向的 FILE 类型变量中,存放着关于文件的一些信息,比如,文件的“当前位置”(稍后会详述)。文件打开后,对文件的读写操作就不再使用文件名,而都是通过fopen函数返回的指针进行。
如果试图以只读的方式打开一个并不存在的文件、或因其他原因(比如没有权限)导致文件打开失败, 则fopen返回NULL指针。如果以读写或只写的方式打开一个不存在的文件, 那么该文件就会被创建出来。
FILE * fp = fopen( “c:\\data\\report.txt”, “r”);
上面的语句以只读方式打开了文件 “c:\\data\\report.txt”。给定文件名的时候也可以不给路径,那么fopen函数执行时就在当前目录下寻找该文件:
FILE * fp = fopen( “report.txt”, “r”);
如果当前目录下没有report.txt,则fopen函数返回NULL,此后当然不能进行读写操作了。
对文件进行读写操作前,判断fopen函数的返回值是否是NULL,是非常重要的习惯。
1.16.2 用 fclose 关闭文件
打开文件,读写完毕后,一定要调用fclose函数关闭文件。fclose函数的原型是: int fclose(FILE *stream);
stream即是先前用fopen打开文件时得到的文件指针。
一定要注意,打开文件后,要确保程序执行的每一条路径上都会关闭该文件。一个程序能同时打开的文件数目是有限的,如果总是打开文件没有关闭,那么文件打开数目到达一定限度后,就再也不能打开新文件了。一个文件,可以被以只写的方式同时打开很多次,这种情况也会占用程序能同时打开的文件总数的资源。新手在调程序时常会碰到明明看见文件就在那里,用fopen函数却总是打不开的情况,很可能就是因为总打开文件而不关闭,导致同时打开的文件数目达到最大值,从而再也不能打开任何文件。
调用fclose函数时,如果参数stream的值是NULL, 那么很可能会出现程序异常终止的错误。
1.16.3 用 fscanf 读文件
fscanf函数原型如下;
int fscanf(FILE *stream, const char *format[, address, ...]);
fscanf和scanf函数很象,区别在于多了第一个参数----文件指针stream。scanf函数从键盘获取输入数据,而fscanf函数从与stream相关联的文件中读取数据。该函数适用于读取以文本方式打开的文件。如果文件的内容都读完了,那么fscanf函数返回值为 EOF(stdio.h 中定义的一个常量)。
假设有以下文本文件 students.txt 存放在C盘tmp文件夹下: Tom 08701342 male 1985 11 2 3.47
Jack 08701343 Male 1985 10 28 3.67
Mary 08701344 femal 1984 2 28 2.34
该文件里每行记录了一个学生的信息,依次是:姓名,学号,性别,出生年,月,日,绩点。下面的程序打开此文件,读取其全部内容并输出。
1. #include <stdio.h>
2. int main()
3. {
4. FILE * fp;
5. fp = fopen( "c:\\tmp\\students.txt", "r");
6. if( fp == NULL ) {
7. printf( "Failed to open the file.");
8. return ;
9. }
10. char szName[30], szGender[30];
11. int nId, nBirthYear, nBirthMonth, nBirthDay;
12. float fGPA;
13. while( fscanf( fp, "%s%d%s%d%d%d%f", szName, & nId, szGender, & nBirthYear,
14. & nBirthMonth, & nBirthDay, & fGPA) != EOF) {
15. printf( "%s %d %s %d %d %d %f\r\n", szName, nId, szGender, nBirthYear,
16. nBirthMonth, nBirthDay, fGPA);
17. }
18. fclose(fp);
19. return 0;
20. }
fprintf函数能用于向文件中写入数据,用法和printf、fscanf类似,此处不再赘述。其原型是:
int fprintf(FILE *stream, const char *format[, argument, ...]);
1.16.4 用 fgetc读文件,用fputc写文件
fgetc函数原型如下:
int fgetc(FILE *stream);
它用于从文件中读取一个字节,返回值即是所读取的字节数。每个字节都被当作一个无符号的8位(二进制位)数,因此每个被读取字节的取值范围都是0-255。反复调用fgetc函数可以读取整个文件。如果已经读到文件末尾,无法再读,那么fgetc函数返回EOF(实际上就是-1)。
fputc函数原型如下:
int fputc(int c, FILE *stream);
它将一个字节写入文件。参数c即是要被写入的字节。虽然c是int类型的,但实际上只有其低8位才被写入文件。如果写入失败,该函数返回EOF。
下面的程序实现了文件拷贝的功能。如果由该程序生成的可执行文件名叫 MyCopy.exe, 那么在控制台窗口(也称DOS窗口)输入 “MyCopy 文件名1 文件名2”再敲回车,则能进行文件拷贝操作。比如,如果在DOS窗口输入:
MyCopy c:\tmp\file1.dat d:\tmp2.dat
则本程序的执行结果是将C盘tmp文件夹下的file1.dat文件,复制为到d盘根目录下的
tmp2.dat文件。
1. #include <stdio.h>
2. int main(int argc, char * argv[]) 3. {
4. FILE * fpSrc, * fpDest;
5. fpSrc = fopen( argv[1], "rb");
6. if( fpSrc == NULL ) {
7. printf( "Source file open failure.");
8. return 0;
9. }
10. fpDest = fopen( argv[2], "wb");
11. if( fpDest == NULL) {
12. fclose( fpSrc);
13. printf( "Destination file open failure.");
14. return 0;
15. }
16. int c;
17. while( ( c = fgetc(fpSrc)) != EOF)
18. fputc( c, fpDest);
19. fclose(fpSrc);
20. fclose(fpDest);
21. return 0;
22. }
语句 2 中的 main 函数比以往多了两个参数 argc 和 argv,另外在语句 5 和语句 10 中也用到了 argv 参数,argc 和 argv 的作用请看后文的 2.18 节“命令行参数”。
语句 5 实际上就是以只读方式打开源文件,语句 10 是以写方式打开目标文件。
语句 17 从源文件读取一个字符。表达式 c = fgetc(fpSrc) 的值实际上就是 c 的值,也就是 fgetc 函数的返回值。fgetc的返回值是 EOF,则说明文件已经读完了。
1.16.5 用fgets函数读文件, fputs函数写文件
fgets函数原型如下;
char *fgets(char *s, int n, FILE *stream);
它一次从文件中读取一行,包括换行符,放入字符串s中,并且加上字符串结尾标志符’\0’。参数n代表缓冲区s中最多能容纳多少个字符(不算结尾标志符’\0’)。
fgets函数的返回值是一个char *类型的指针,和s指向同一个地方。如果再没有数据可以读取,那么函数的返回值就是NULL。
fputs函数原型如下;
int fputs(const char *s, FILE *stream);
它往文件中写入字符串s。注意,写完s后它并不会再自动向文件中写换行符。下面的程序将students.txt 内容拷贝到student2.txt
1. #include <stdio.h>
2. #define NUM 200
3. int main()
4. {
5. FILE * fpSrc, * fpDest;
6. fpSrc = fopen( "students.txt", "r");
7. if( fpSrc == NULL ) {
8. printf( "Source file open failure.");
9. return 0 ;
10. }
11. fpDest = fopen( "students2.txt", "w");
12. if( fpDest == NULL) {
13. fclose( fpSrc);
14. printf( "Destination file open failure.");
15. return 0;
16. }
17. char szLine[NUM];
18. while( fgets(szLine, NUM-1, fpSrc)) {
19. fputs(szLine, fpDest);
20. }
21. fclose(fpSrc);
22. fclose(fpDest);
23. return 0;
24. }
调用fgets 时用的参数199改小点,比如150,也是没有问题的,只要能装得下最长的那一行就行了。
1.16.6 用 fread读文件,用fwrite写文件
fread函数原型如下:
unsigned fread(void *ptr, unsigned size, unsigned n, FILE *stream);
该函数从文件中读取n个大小为size字节的数据块, 总计n*size字节,存放到从地址
ptr 开始的内存中。返回值是读取的字节数。如果一个字节也没有读取,返回值就是0。
fwrite函数原型如下:
unsigned fwrite(const void *ptr, unsigned size, unsigned n, FILE *stream);该函数将内存中从地址 ptr 开始的n*size个字节的内容,写入到文件中去。
这两个函数的返回值,表示成功读取或写入的“项目”数。每个“项目”的大小是size 字节。
其实使用这两个函数时,总是将size置为1,n置为实际要读写的字节数,也是没有问题的。
fread函数成功读取的字节数,有可能小于期望读取的字节数。比如反复调用fread读取整个文件,每次读取100个字节,而文件有1250字节,那么显然最后一次读取,只能读取50 字节。
使用fread和fwrite函数读写文件,文件必须用二进制方式打开。
有些文件由一个个“记录”组成,一个记录就对应于C/C++中的一个结构,这样的文件,
就适合用fread和fwrite来读写。比如一个记录学生信息的文件students.dat,该文件里每个“纪录”对应于以下结构:
struct Student{ char szName[20]; unsigned nId;
short nGender; //性别
short nBirthYear, nBirthMonth, nBirthDay; float fGPA;
};
下面的程序先读取前例提到的students.txt中的学生信息, 然后将这些信息写入
students.dat中。接下来再打开students.dat,将出生年份在1985年之后的学生记录提取出来,写到另一个文件 students2.dat中去。
1. #include <stdio.h>
2. #include <string.h>
3. struct Student{
4. char szName[20];
5. unsigned nId;
6. short nGender; //性别
7. short nBirthYear, nBirthMonth, nBirthDay;
8. float fGPA; 9. };
10.
11. int main()
12. {
13. FILE * fpSrc, * fpDest;
14. struct Student Stu;
15. fpSrc = fopen( "c:\\tmp\\students.txt", "rb");
16. if( fpSrc == NULL ) {
17. printf( "Failed to open the file.");
18. return 0;
19. }
20. fpDest = fopen( "students .dat", "wb");
21. if( fpDest == NULL) {
22. fclose( fpSrc);
23. printf( "Destination file open failure.");
24. return 0;
25. }
26. char szName[30], szGender[30];
27. int nId, nBirthYear, nBirthMonth, nBirthDay;
28. float fGPA;
29. while( fscanf( fpSrc, "%s%d%s%d%d%d%f", szName, & nId,
30. szGender, & nBirthYear, & nBirthMonth, & nBirthDay, & fGPA) != EOF) {
31. strcpy(Stu.szName, szName);
32. Stu.nId = nId;
33. if( szGender[0] == 'f' )
34. Stu.nGender = 0;
35. else
36. Stu.nGender = 1;
37. Stu.nBirthYear = nBirthYear;
38. Stu.nBirthMonth = nBirthMonth;
39. Stu.nBirthDay = nBirthDay;
40. fwrite( & Stu, sizeof(Stu), 1, fpDest);
41. }
42. fclose(fpSrc);
43. fclose(fpDest);
44. fpSrc = fopen( "students.dat", "rb");
45. if( fpSrc == NULL ) {
46. printf( "Source file open failure.");
47. return 0;
48. }
49. fpDest = fopen( "students2.dat", "wb");
50. if( fpDest == NULL) {
51. fclose( fpSrc);
52. printf( "Destination file open failure.");
53. return 0;
54. }
55. while(fread( & Stu, sizeof(Stu), 1 , fpSrc)) {
56. if( Stu.nBirthYear >= 1985 )
57. fwrite( & Stu, sizeof(Stu), 1, fpDest);
58. }
59. fclose( fpSrc);
60. fclose( fpDest);
61. return 0;
62. }
我们看到,存放学生信息,可以用 students.txt 文件的格式,也可以用 students.dat文件的格式。到底哪种比较好呢?应该说使用记录文件更好。记录文件可以按名字或学号等关键值排序,排序以后可以用折半查找算法快速查找,这样在一个有N个记录的文件中进行查找,最多只需读取log2N个记录,比较log2N次。而用文本文件的格式存放信息,由于每行长度都不一样,所以要查找名为“jack”的学生信息,只能从头顺序往下找直到找到为止。那么平均要读取整个文件的一半,才能找到。
另外,用记录方式保存信息,比用文本方式通常能节省空间。
文本方式中有很多空格、换行符是冗余的,而且像“08701342”这样的学号等数值信息, 用记录方式存放,只需4个字节的unsigned类型就可以,而以文本方式保存,往往4个字节是
无法表示的,因为一个数字就要占用一个字节。
请注意,打开的文件,一定要关闭。因此在语句22在程序返回前,关闭了曾经打开的源文件。
思考题 1.16.6:一般来说,我们将能在“记事本”程序中打开,并且看起来不包含不可识别的所谓“乱码”的文件,称为文本文件。那么,我们是否能用文本文件来表示一副图片甚至一段声音,一段视频呢?
看一看网站上常用的.htm文件,是不是文本文件?为什么不用也许会更省空间的二进制文件方式来存放网页?
1.16.7 用 fseek 改变文件当前位置
文件是可以随机读写的,即读写文件并不一定要从头开始,而是直接可以从文件的任意位置开始读写。比如,可以直接读取文件的第200个字节,而不需将前面的199个字节都读一遍。同样,也可以直接往文件第1000个字节处写若干字节,覆盖此处原有内容。甚至可以先在文件的第200字节处读取100字节,然后跳到文件的第1000字节处读取20字节,然后再跳到文件的第20字节处写入30字节。这就叫“随机读写”。然而,前面提到的那些文件读写函数, 都没有参数能够指明读写是从哪个位置开始,这又是怎么回事呢?
答案是:所有的文件读写函数,都是从文件的“当前位置”开始读写的。文件的“当前位置”信息保存在文件指针指向的 FILE结构变量中。一个文件在以非 “添加”方式打开,尚未进行其他操作时,其“当前位置”就是文件的开头;以添加方式打开时,其“当前位置” 在文件的末尾。此后调用读写函数读取或写入了n个字节,“当前位置”就往后移动n个字节。如果“当前位置”到达了文件的末尾,那么文件读取函数再进行读操作就会失败。
注意,文件开头的“当前位置”值是0,而不是1。
综上所述,要实现随机读写,前提是能够随意改变文件的“当前位置”。fseek函数就起到这个作用。其原型如下:
int fseek(FILE *stream, long offset, int whence);
该函数将与stream关联的文件的“当前位置”设为距whence处offset字节的地方。whence 可以有以下三种取值,这三种取值都是在stdio.h里定义的标识符:
SEEK_SET: 代表文件开头
SEEK_CUR: 代表执行本函数前文件的当前位置
SEEK_END: 代表文件结尾处
例如,假设 fp是文件指针,那么: fseek(fp, 200, SEEK_SET);
就将文件的当前位置设为200,即距文件开头200个字节处;
fseek(fp, 0, SEEK_SET);
将文件的当前位置设为文件的开头。fseek(fp, -100, SEEK_END);
将文件的当前位置设为距文件尾部100字节处。fseek(fp, 100, SEEK_CUR);
将文件的当前位置往后(即往文件尾方向)移动100个字节。fseek(fp, -100, SEEK_CUR);
将文件的当前位置往前(即往文件开头方向)移动100个字节。
下面的程序,读取文件students.dat中的第4个记录到第10个记录(记录从0开始算), 并将这部分内容写入到第20个记录开始的地方,覆盖原有的内容。
例程 2.16.7.cpp:
1. #include <stdio.h>
2. #include <string.h>
3. #define NUM 10
4. #define NAME_LEN 20
5. struct Student{
6. char szName[NAME_LEN];
7. unsigned nId;
8. short nGender; //性别
9. short nBirthYear, nBirthMonth, nBirthDay;
10. float fGPA; 11. };
12.
13. int main()
14. {
15. FILE * fpSrc;
16. Student aStu[NUM];
17. fpSrc = fopen( "c:\\tmp\\students4.dat", "r+b");
18. if( fpSrc == NULL ) {
19. printf( "Failed to open the file.");
20. return 0;
21. }
22. fseek( fpSrc, sizeof(Student)* 4 , SEEK_SET);
23. fread( aStu, sizeof(Student), 7, fpSrc);
24. fseek( fpSrc, sizeof(Student) * 20, SEEK_SET);
25. fwrite( aStu, sizeof(Student), 7, fpSrc);
26. fclose( fpSrc);
27. return 0;
28. }
1.17 C语言标准库函数
C 语言中有大量的标准库函数,根据功能不同,声明于不同的头文件中。这些库函数在
C++中也能使用。下面分类列举了一些 C 语言常用库函数,由于篇幅所限,只列出函数名字及其作用。
1.17.1 数学函数
数学库函数声明在 math.h 中,主要有:
abs(x) 求整型数 x 的绝对值
cos(x) x(弧度)的余弦
fabs(x) 求浮点数x 的绝对值
ceil(x) 求不小于 x 的最小整数
floor(x) 求不大于 x 的最小整数
log(x) 求 x 的自然对数
log10(x) 求 x 的对数(底为 10)
pow(x, y) 求 x 的y 次方
sin(x) 求 x(弧度)的正弦
sqrt(x) 求 x 的平方根
1.17.2 字符处理函数
在 ctype.h 中声明,主要有:
int isdigit(int c) 判断 c 是否是数字字符int isalpha(int c) 判断 c 是否是一个字母
int isalnum(int c) 判断 c 是否是一个数字或字母int islower(int c) 判断 c 是否是一个小写字母int islower(int c) 判断 c 是否是一个小写字母int isupper(int c) 判断 c 是否是一个大写字母
int toupper(int c) 如果 c 是一个小写字母,则返回其大写字母int tolower (int c) 如果 c 是一个大写字母,则返回其小写字母
1.17.3 字符串和内存操作函数
字符串和内存操作函数声明在 string.h 中,在调用这些函数时,可以用字符串常量或字符数组名,以及 char * 类型的变量,作为其 char * 类型的参数。字符串函数常用的有:
char * strchr(char * s, int c)
如果 s 中包含字符 c, 则返回一个指向 s 第一次出现的该字符的指针, 否则返回NULL
char * strstr(char * s1, char * s2)
如果 s2 是 s1 的一个子串,则返回一个指向 s1 中首次出现 s2 的位置的指针,否则返回 NULL
char * strlwr(char * s)
将 s 中的字母都变成小写char * strupr( char * s)
将 s 中的字母都变成大写
char * strcpy( char * s1, char * s2) 将字符串 s2 的内容拷贝到 s1 中去
char * strncpy( char * s1, char * s2, int n)
将字符串 s2 的内容拷贝到 s1 中去,但是最多拷贝 n 个字节。如果拷贝字节数达到
n,那么就不会往 s1 中写入结尾的’\0’ char * strcat( char * s1, char * s2)
将字符串 s2 添加到 s2 末尾
int strcmp( char * s1, char * s2)
比较两个字符串,大小写相关。如果返回值小于 0,则说明 s1 按字典顺序在 s2 前面;返回值等于 0,则说明两个字符串一样;返回值大于 0,则说明 s1 按字典顺序在
s2 后面。
int stricmp( char * s1, char * s2)
比较两个字符串,大小写无关。其他和 strcmp 同。
int strlen(const char *string)
计算字符串的长度
char * strncat(char *strDestination, const char *strSource, size_t count)
将字符串 strSource 中的前count个字符添加到字符串 strDestination 的末尾
int strncmp(const char *string1, const char *string2, size_t count)
分别取两个字符串的前 count 个字符作为子字符串,比较它们的大小
char * strrev(char *string)
将字符串 string 前后颠倒。
void * memcpy( void * s1, void * s2, int n)
将内存地址 s2 处的 n 字节内容拷贝到内存地址 s1 void * memset( void * s, int c, int n)
将内存地址 s 开始的n 个字节全部置为 c
1.17.4 字符串转换函数
有几个函数,可以完成将字符串转换为整数、或将整数转换成字符串等这类功能。它们定义在 stdlib.h 中:
int atoi(char *s)
将字符串s 里的内容转换成一个整型数返回。比如,如果字符串s 的内容是“1234”,那么函数返回值就是 1234
double atof(char *s)
将字符串 s 中的内容转换成浮点数。
char *itoa(int value, char *string, int radix);
将整型值 value 以 radix 进制表示法写入 string。比如:
char szValue[20];
itoa( 32, szValue, 10); 则使得 szValue 的内容变为 “32”
itoa( 32, szValue, 16); 则使得 szValue 的内容变为 “20”
1.18 命令行参数
如果我们编写了一个在屏幕上输出文本文件内容的程序,编译生成的可执行文件是
listfile.exe,那么,很可能我们希望该程序的用法是,在 Windows 的控制台窗口(也叫
DOS 命令窗口)中输入:
listfile 文件名
然后敲回车,就能启动 listfile 程序,并将“文件名”所指定的文件的内容输出。比如敲“listfile file1.txt”,就能将 file1.txt 这个文件的内容输出。
要做到这一点,显然,listfile 程序必须知道用户输入的那个文件名。我们将用户在
DOS 窗口输入可执行文件名的方式启动程序时,跟在可执行文件名后面的那些字符串,称为
“命令行参数”。比如上例中的“file1.txt”,就是一个命令行参数。命令行参数可以有多个,以空格分隔。比如“listfile file1.txt file2.txt”。
在程序中如何知道用户输入的命令行参数呢?要做到这一点,main 函数的写法须和以往的不同,要增加两个参数:
int main(int argc, char * argv[])
{
……
}
参数 argc 就代表启动程序时,命令行参数的个数。C/C++语言规定,可执行程序程序本身的文件名,也算一个命令行参数,因此,argc 的值至少是 1。argv 是一个数组,其中的每个元素都是一个 char* 类型的指针,该指针指向一个字符串,这个字符串里就存放着命令行参数。例如,argv[0]指向的字符串就是第一个命令行参数,即可执行程序的文件名,
argv[1]指向第二个命令行参数,argv[2]指向第三个命令行参数……。请看例子程序:
例程 2.18.cpp
1. #include <stdio.h>
2. int main(int argc, char * argv[]) 3. {
4. for(int i = 0;i < argc; i ++ )
5. printf( "%s\n", argv[i]);
6. return 0; 7. }
将上面的程序编译成 2.18.exe,然后在控制台窗口敲:
2.18 para1 para2 s.txt 5 4
输出结果就是:
2.18
para1 para2
s.txt 5
4
1.19 C/C++编码规范
一个好的程序,不仅要算法正确、效率高,而且还应该可读性好。所谓程序的可读性,就是程序是否能让人容易读懂。在开发实践中,许多情况下可读性与代码效率同等重要。
软件开发是团队工作,接手别人编的程序并在此基础上进行改进是必不可少的,因此可读性在工程实践中非常重要。就算是自己编写的程序,如果可读性不好,过一段时间需要改进时自己再看,也常会看不懂。
如何提高程序的可读性呢?在标识符命名、书写格式、注释三个方面加以注意,再养成一些好的习惯,就能够有效增强程序的可读性。
1.19.1 标识符命名注意事项
应该对变量、常量、函数等标识符进行恰当的命名。好的命名方法使标识符易于记忆且程序可读性大大提高。
对标识符命名的基本要求是,看到标识符就能想起或猜出它是做什么用的。如果名字能体现变量的类型或作用域等性质,当然更好。标识符命名应注意一下几点:
1) 标识符号应能提供足够信息以说明其用途。一定不要怕麻烦而懒得起足够长的变量名,少敲几个键省下的时间,和日后你自己读该程序或别人读你的程序时揣摩该变量做什么用所花的时间相比,实在不足道。在没有国际合作的项目中编写程序,如果英语实在不好,可以使用拼音,但不要使用拼音缩写。
2) 为全局变量取长的,描述信息多的名字,为局部变量取稍短的名字
3) 名字太长时可以适当采用单词的缩写。但要注意,缩写方式要一致。要缩写就全都缩写。
比如 单词 Number, 如果在某个变量里缩写成了:
int nDoorNum;那么最好包含 Number 单词的变量都缩写成 Num。
4)注意使用单词的复数形式。如
int nTotalStudents, nStudents ;容易让人理解成代表学生数目,而 nStudent 含义就不十分明显
5) 对于返回值为真或假的函数,加“Is”前缀如: int IsCanceled();
int isalpha(); // C 语言标准库函数BOOL IsButtonPushed();
1.19.2 程序的书写格式
书写格式好的程序,看起来才有好心情。谁也不愿意看下面这样的程序: void main()
{
int t, x, y; cin>>t; while (t>0)
{ min=60000;
cin>>N>>x>>y>>max; plat[0].x1=x; plat[0].x2=x; plat[0].h=y;
for (int i=1;i<=N;i++)
{
cin>>plat[i].x1>>plat[i].x2>>plat[i].h; plat[i].t1=-1;
plat[i].t2=-1;
if (plat[i].h>y) {i--; N--; }
}
plat[0].t1=0;plat[0].t2=0;
qsort((void*)(&plat[1]), N, sizeof(plat[0]), compare); tryway(0);
t--;
cout<<min<<endl;
}
}
因此,如果想要让你的程序看起来赏心悦目,应该注意以下几点:
1)正确使用缩进:
首先,一定要有缩进,否则代码的层次不明显。缩进应为 4 个空格较好。需要缩进时一律按 Tab 键,或一律按空格键,不要有时用 Tab 键缩进,有时用空格键缩进。一般开发环境都能设置一个 Tab 键相当于多少个空格,此时就都用 Tab 键
2) 行宽与折行:
一行不要太长,不能超过显示区域。以免阅读不便。太长则应折行。折行最好发生在运算符前面,不要发生在运算符后面。如
if( Condition1() && Condition2() && Condition3() ) {
}
3) ‘{’, ‘}’位置不可随意放置。
建议将‘{’放在一行的右边,而将‘}’单独放置一行。如: if ( condition1() ) {
DoSomething();
}
比较
if ( condition1() )
{
DoSomething();
}
这种写法,前者既不影响可读性,又能节省一行。
但是对于函数体或结构定义的的第一个‘{’,还是单独一行更为清晰。
4) 变量和运算符之间最好加 1 个空格,如: int nAge = 5;
nAge = 4;
if( nAge >= 4 )
printf( “%d”, nAge); for( i = 0; i < 100; i ++ );
1.19.3 注释的写法
在工程实践中,文件开头,全局变量定义处,函数开头,都应该有注释。文件开头的注释模板如下:
/******************************************************************
** 文件名:
** Copyright (c) 1998-1999 *********公司技术开发部
** 创建人:
** 日 期:
** 修改人:
** 日 期:
** 描 述:
**
** 版 本:
**--------------------------------------------------------------------------
---
******************************************************************/
函数开头的注释模板如下:
/*****************************************************************
** 函数名:
** 输 入: a,b,c
** a---
** b---
** c---
** 输 出: x---
** x 为 1, 表示...
** x 为 0, 表示...
** 功能描述:
** 用到的全局变量:
** 调用模块:
** 作 者:
** 日 期:
** 修 改:
** 日 期:
** 版本
****************************************************************/
本书由于篇幅所限,书中程序略去了文件开始处和函数开始处的注释。
1.19.4 一些好的编程习惯
1)尽量不要用立即数,而用#define 定义成常量,以便以后修改。例如:
#define MAX_STUDENTS 20
struct SStudent aStudents [MAX_STUDENTS];
比
struct SStudent aStudents [20];
好。
再例如:
#define TOTAL_ELEMENTS 100
for( i = 0; i < TOTAL_ELEMENTS; i ++) {
}
2)使用 sizeof()宏,不直接使用变量所占字节数的数值。如:应该写:
int nAge;
for( j = 0; j < 100; j++ )
fwrite( fpFile, & nAge, 1, sizeof(int));
不应该写:
for( j = 0; j < 100; j++ )
fwrite( fpFile, & nAge, 1, 4);
4)稍复杂的表达式中要积极使用括号,以免优先级理解上的混乱以及二义性。n = k +++ j; //不好
n = ( k ++ ) + j; //好一点
5)不很容易理解的表达式应分几行写: n = ( k ++ ) + j;应该写成:
n = k + j; k ++;
6)嵌套的 if else 语句要多使用 { }
if( Condition1() )
if( condition2()
else
DoSomething();
NoCondition2();
不够好,应该:
if( Condition1() ) {
if( condition2()
DoSomething();
else
}
NoCondition2();
7) 单个函数的程序行数最好不要超过 100 行(两个屏幕高)。
8) 尽量使用标准库函数和公共函数。
9) 不要随意定义全局变量,尽量使用局部变量。
8) 保持注释与代码完全一致,改了代码别忘改注释。
9)循环、分支层次最好不要超过五层。
10) 注释可以与语句在同一行,也可以在上行。
11) 一目了然的语句不加注释。
文档下载:语言C/C++概述 - 下载频道 - youkuaiyun.com http://download.youkuaiyun.com/detail/liujiuxiaoshitou/9902391