语句是可执行程序的一部分,它说明一种行为。C 语言的语句分为以下几类:
选择(selection)、循环(loop)、跳转(jump)、标号(label)、表达式(expr)、块(block)。
一、选择语句:
1、if 语句:if (expr) /* 处理 */;
(1、)expr 应该只产生 scalar(int,char,或 float,以及 C99 中的 _Bool)结果。
(2、)尽量不要在 expr 中使用 float 表达式,因为 float 运算占用几条 CPU 指令,这将明显降低执行速度。
而 int 或 char 运算占用 CPU 指令数较少。
(3、)嵌套使用 if 语句时,C89 规定编译器至少应支持 15 层嵌套,C99 提升到 127 层。
实际上,绝大多数的编译器支持的嵌套数都远远大于 15 层。
(4、)用 ?:操作符代替 if 语句时,并不局限于赋值,表达式中还可以包含函数。如:
vrai ? f1(vrai) + f2() : printf("Error!/n"); /* 允许编译器在优化目标码时重新安排求值顺序 */
2、switch 语句:switch(expr) { case … }。主要用于处理键盘输入(如接口菜单)等操作。
(1、)expr 必须对 int 求值(可以使用 int 或 char 型)。
(2、)C89 规定一个 switch 语句最多可以包含 257 个 case,C99 提升为 1023 个。
实际工作中,为了提高效率,不宜有太多的 case 语句。
(3、)switch 的特性:
- 与 if 语句的区别:switch 只测试是否相等,而 if 可以测试关系和逻辑表达式。
- 每个 case 后面的条件必须不同(嵌套时内层和外层的 case 语句例外)。
- switch 语句中的 char 型常数全部被自动转换成 int 型。
二、循环语句:C 语言的 for 循环是所有面向过程的语言中的循环语句的基础。
1、for (init; condition; inc) {
/* 处理 */
}
/** init 一般为赋值语句(初始化循环变量的作用)。
* condition 是一个关系表达式,指定循环退出的条件。
* inc 修改循环变量。
*
* for 循环中,condition 测试总是在循环的顶部实施(循环体可能得不到执行)。
*/
--> for 语句的变种:
(1、)逗号表达式的使用:for(init1, init2; condition; inc1, inc2) { … }
(2、)condition 不必一定包含对一些目标值测试循环变量,它可以是关系或逻辑语句(可以测试多种可能的终止条件)。
[例]登录密码
void user_login() {
char passcode[8]; /* UNIX 系统一般为 8 位密码,尤其是使用了老版本的 NIS 的系统*/
int fois; /* 测试次数 */
for(fois=0; fois<3 && strcmp(passcode, "a1b2c3d4"); ++fois) {
printf("请输入密码:--> ");
gets(passcode);
}
if(fois==3) return; /* 如果三次输入密码错误,返回 */
else /* 如果输入正确,则使用户登录 */
}
(3、)for(expr1; expr2; expr3) { …}
/* for 语句的三个部分都可以是表达式 */
[例]int i;
for(func1(); i=func2(); func3()) { /* 处理 */}
/** 这个 for 语句的三个部分均为函数调用
* 这是 for 语句的非常规方式,但是合法。
*/
(4、)for 语句的三个部分都可以省略。init 部分通常都在循环体外定义,尤其是当 init 部分需要进行复杂计算时。
[例]在实际编程中,经常使用 for( ; ; ) 来实现一个死循环。
for( ; ; ) {
user_input=getchar();
if(user_input=='Q') break; /* break 的作用是强制退出循环 */
}
(5、)空循环:主要用于改进某些算法,或构造延时循环(time delay loop)。
[例1]中国计算机爱好者论坛的风格:从用户输入中删除多余的空格。
for(; * str==' '; str++);
[例2]耗费时间,这个类似于 UNIX 中的 sleep 命令的作用。
for(temps=0; temps<TEMPS_FIXE; temps++);
/* 注意有些编译器会优化掉时间延迟循环 */
(6、)C99 和 C++ 允许在 for 语句的 init 部分声明变量,使循环变量的作用域限制在语句控制的块内。
如:for(int i=0; i<100; i++) a+=i; /* 在循环体外,i 是未知变量 */
2、while 循环:while(condition) /* 处理 */
/* condition 可以是任何非零值的表达式 */
(1、)while 也是在循环顶部测试条件(循环体可能得不到执行)。若几个条件中的任何一个都能终止循环,则一般由一个循环控制变量来构成 condition,在循环中的不同点为其赋值。
[例]#define TRUE 1
#define FALSE 0
void test_while() {
int traitement=TRUE;
while(traitement) {
traitement = proc1();
if(traitement) traitement=proc2();
if(traitement) traitement=proc3();
}
}
(2、)空循环:
[例]在利用循环,将一个数组中的元素复制到另一个数组中时:
char *from, *to;
while(*from++=*to++);
3、do-while 循环:在底部进行条件测试(循环体至少执行一次)。
do { /* 处理 */
} while(condition);
[例]do-while 循环常用于创建菜单子程序:
void install(); /* for installing the system */
void ReadMe(); /* for reading the ReadMe.txt */
char selection() {
char choice;
do {
switch(choice=getchar()) {
case '1' : choice='1'; break;
case '2' : choice='2'; break;
case '3' : choice='3'; break;
}
} while(choice!='1' && choice!='2' && choice!='3');
return choice;
}
void menu() {
char resultat;
printf("1. Install the system./n");
printf("2. Read the ReadMe file./n");
printf("3. Quit./n");
resultat=selection();
switch(resultat) {
case '1' : install(); break;
case '2' : ReadMe(); break;
case '3' : exit(0); /* return to OS */
}
}
三、跳转语句:return,break,continue,goto 是 C 的无条件跳转语句,其中 return 和 goto 可以用于程序的任何地方,break 和 continue 可以和任何循环语句一起使用。
1、return 语句:返回到函数的被调用点。无值的 return 常用于从 void 型函数中返回。
(1、)函数中使用 return 的次数不限,但第一次使用时立即停止执行,返回到调用点。
(2、)void 型函数不能使用带值的 return。
(3、)在 C89 中,非 void 型函数执行不含值的 return 时返回无用值。这点非常糟糕!
在 C99 和 C++ 中,非 void 型函数中的 return 必须返回一个值。
(然而,如果执行到非 void 型函数的结尾时,则返回无用值。这不是语法错误,应尽量设法避免。)
2、break 与 continue 语句:
(1、)break 语句的两种用途:
--> 在 switch 语句中结束一个 case。
--> 越过常规循环条件的测试,立即强制终止循环。(需要根据某些特殊条件立即终止循环时常用)
[例]for (int i=0; i<100; i++) if (i==25) break;
(2、)continue 语句:强制开始下一轮循环。
--> 对于 for 语句:continue 使循环从条件判断处重新开始。
--> 对于 while 和 do-while 语句:continue 把控制转到条件测试。
3、goto 语句:是一种方便措施,常用于跳出多重循环。
[例]for( ; ; ) {
for ( ; ; ) {
for ( ; ; ) {
for ( ; ; ) {
/* 处理 */
if ( /* 条件 */ ) goto _end; /* 直接跳出所有循环 */
/* 处理 */
}
}
}
}
_end: /* 标号(label)*/
return;
/** 注意与 UNIX 中 Shell 编程的区别。
* Shell 脚本可以直接使用 UNIX 命令 break 4 来跳出上面所有的循环。
*/
4、#include <stdlib.h>
void exit(int return_code);
/** 标准库函数,立即终止程序,并返回 OS。
* 按照 UNIX 系统的标准:
* 返回 0 代表正常终止,
* 返回非 0 代表异常终止。
* return_code 自带宏:EXIT_SUCCESS 和 EXIT_FAILURE
*
* 注意与 UNIX 系统中 exit 命令的格式的区别。
*/
爱拼才会赢~!
Posted: 2005-04-24 19:46 | 3 楼
wzs8801775
级别: 正义の武者
配偶: 单身
精华: 0
发帖: 847
威望: 31 点
金钱: 180 社区币
贡献值: 0 点
在线时间:109(小时)
注册时间:2004-11-21
最后登录:2006-05-27
IV. 数组与指针
C 语言提供访问数组的两种方法:指针算术和数组下标。
指针算术的速度可以高于数组下标。考虑到速度因素,程序员一般都使用指针来访问数组元素。
一、数组(Array):具有相同类型的数据的有序集合,并用唯一的名字来标识。
1、数组必须直接声明,编译器在编译阶段为其分配内存空间。
2、在 C89 中,数组必须是定长的,数组的大小在编译时是固定的;C99 允许使用变长数组(VLA),数组的大小在运行时确定。
但是只有本地数组才可以申请为变长的。增加变长数组是为了支持数值处理。
[例]void f(int longeur, int wide) {
int matrix[longeur][wide]; /* 定义一个矩阵 */
/* 数组的长度由两个参数决定 */
}
3、数组的所有元素占据连续的内存空间,在内存中是线性存放的,保存数组所需的内存空间直接与基类型和数组长度有关。
数组占用内存空间 = sizeof(基类型) * 数组长度。
4、C 不检查数组是否越界,程序可以在两边越界。程序员应自己加入越界检查。数组可以越界使用,但是初始化时不允许!
5、向函数传递数组:
定义数组形参的方法有三种:指针,定长数组,无尺寸数组。
void func1(int *a) {
}
void func2(int a[10]) {
}
void func3(int a[]) {
}
在函数的形参的声明中,数组的尺寸无所谓,因为C语言没有边界检查。
实际上,第二种方法在编译后,编译器产生的代码就是让函数接受指针,并不生成 10 个元素的数组。
(1、)形参中的数组不能再理解为数组,而必须理解为指针:不能用 sizeof() 求大小;但可以再赋值,这与数组名的指针常量性质不一样。传值时有内容的复制,但数组内的元素可能很多,为避免内容的大量复制而占用太多的内存,C 规定数组传参就是传指针。
(2、)int a[][] 不能做形参,因为 a 是指向 int[] 这样一种数据类型的数组指针,但下标大小没有确定。而 int a[][8] 可以,并可以直接用二维数组名(无须显示转换)做其实参。
6、在处理一个数组的元素时,使用指针自增(p++)的方式通常比直接使用数组下标更快,使用指针能够使程序得以优化。
7、C 允许定义多维数组,维数上限由编译器定义。但多于三维的数组并不常用,因为多维数组所需的内存空间对维数呈指数增长。并且,计算各维下标会占用 CPU 时间(存取多维数组元素的速度比存取一维数组元素的速度慢)。
8、对数组初始化时注意,C89 要求必须使用常量初始化字符,而 C99 允许使用非常量初始化字符来初始化本地数组。
二、串(String):数组(尤其是一维数组)最常用的地方。
1、C 没有专门的字符串变量,对于它的操作全部由一维数组实现。字符串是字符数组的一种特殊形式,唯一的区别就在于它是作为一个整体操作,而普通数组则不能。最终的差别就在末尾的 NULL(0)上。
2、注意与 C++ 中 string 类型(为字符串处理提供了 OO 的方法)的区别,C 并不支持它。
3、初始化操作:要使用字符串常量时则把它放到数据区中的 CONST 区(数据区、全局变量区和静态变量区),用字符串常量初始化字符数组时有一个复制内容的操作,而不仅仅是用一个指针指向它。实际上字符串常量是在常量区还是堆区、采用何种存储结构、以及是否连续的问题,取决于不同的编译器。
4、串的输入与输出:下面的函数均由 <stdio.h> 定义。
(1、)printf("%s", str);
(2、)puts(str);
(3、)scanf("%s", str);
(4、)gets(str);
5、串运算:下面的函数均由 <string.h> 定义。
strcpy(s1, s2),strcat(s1, s2),strlen(str),strcmp(s1, s2),strchr(s1, ch),strstr(s1, s2)。
[注意]字符数组、字符指针之间的 == 比较是地址的比较,结果不可能是 true。但字符串常量的 == 比较则不一定:VC 对 == 进行了重载,将字符串常量的地址比较变为内容比较(如同输出字符指针实际是输出字符串一样,都是重载在作怪),因而 ("abc" == "abc") 在 VC 中成立,但在 BC 中则不成立。为避免二义性,应该尽可能用 strcmp() 来比较。
三、指针(Pointer):
指针是 C 语言的精华,正确理解并灵活运用指针是衡量能否成功地编写 C 程序的标准。
1、使用指针的好处:
--> 能够为调用函数灵活地修改实参变量的值。
--> 支持动态内存分配,能够方便地实现动态的数据结构(如二*树和链表)。
--> 可以提高某些程序的效率。
--> 实现缓冲方式的文件存取。
2、指针是地址。技术上,任何类型的指针都可以指向内存的任何位置。但是指针的操作都是基于类型的。
指针操作是相对于指针的基类型而执行的,尽管在技术上指针可以指向对象的其它类型,但指针始终认为它指向的是其基类型的对象。指针操作受指针类型而不是它所指向的对象类型的支配。
3、指针表达式:原则上讲,涉及指针的表达式符合其它 C 表达式的规则。
(1、)printf("%p", …); /* 以宿主计算机使用的格式显示地址 */
(2、)指针转换:
--> void * 型:为 Generic Pointer,常用来说明基类型未知的指针。
它允许函数指定参数,此参数可以接受任何类型的指针变量而不必报告类型失配。
当内存语义不清时,也常用于指原始内存。
[例]一个函数可以返回“多个”类型(类似于 malloc()):
void * f1(void *p) {
return p;
}
void main(void) {
int a=100;
int *pp=&a;
printf("%d/n", *((int*)f1(pp)));
}
--> 其它类型的指针转换必须使用明确的强制类型转换。但要注意,一种类型的指针向另一种类型转换时可能会产生不明确的行为。
--> 允许将 int 型转换为指针或将指针转换为 int 型,但必须使用强制类型转换,且转换结果是已定义的实现,可能导致非定义的行为。(转换 0 时不需要强制转换,因为它是 NULL 指针)
--> 为了与 C++ 更好地兼容,很多 C 程序员舍弃了指针转换,因为在 C++ 中,必须使用强制类型转换,包括 void * 型。
(3、)指针算术:可以作用于指针的算术操作只有加法和减法。
--> 指针与整数的加、减法。
--> 从一个指针中减去另一个指针(主要目的是为了求指针偏移量)。
(4、)指针比较:主要用于两个或多个指针指向共同对象的情况。
4、初始化指针:
(1、)非静态局部指针已声明但未赋值前,其值不确定。
(2、)全局和静态局部指针自动初始化为 NULL。
(3、)赋值前使用指针,不仅可能导致程序瘫痪,还有可能使 OS 崩溃,错误可谓严重之至!
(4、)[习惯用法]对于当前没有指向合法的内存空间的指针,为其赋值 NULL。
因为 C 保证空地址不存在对象,所以任何空指针都意味着它不指向任何对象,不应该使用它。
用空指针来说明不用的指针基本上就是程序员遵守的协定(但并不是 C 的强制规则)。
[例]int *p=NULL;
*p=1; /* ERROR!*/
/* 能通过编译,但对 0 赋值通常会使程序崩溃 */
5、函数指针:void process(char *a, char *b, int (* appel)(const char *, const char *));
/* 函数是通过指针调用的,appel 是函数指针 */
在工作中常需要向过程传入任意函数,有时需要使用函数指针构成的数组。如在解释程序运行时,常需要根据语句调用各种函数。此时,用函数指针构成的数组取代大型 switch 语句是非常方便的,由数组下标实施调用。
6、动态分配(Dynamic Allocation)内存空间:指程序在运行中取得内存空间。
全局变量在编译时分配内存空间,非静态局部变量使用栈区,两者在运行时使用固定长度的内存空间。
(1、)为了实现动态的数据结构,C 动态分配函数在堆区分配内存空间。堆是系统的自由内存区,空间一般都很大。
(2、)核心函数:malloc() 和 free()。
(3、)堆区是有限的,分配内存空间后必须检查 malloc() 的返回值,确保指针使用前它是非空的。
(4、)绝对不要用无效的指针调用 free(),否则将破坏自由表。
7、杂项:主要是一些常见的与指针有关的问题。
(1、)在某些情况下,用 const 来限制指针对提高程序的安全性有重要意义。大家可以仔细看一下微软编写的系统函数便会有所体会。
(2、)指针的错误难于定位,因为指针本身并没有问题。问题在于,通过错误的指针操作时,可能引入最难排除的错误:程序对未知内存区进行读或写操作。
--> 读:最坏的情况是取得无用数据。
--> 写:可能冲掉其它代码或数据。
这种错误可能要到程序执行了相当一段时间后才出现,因此把排错工作引入歧途。
虽然使用指针可能导致奇怪的错误,但是我们不能因此而放弃使用指针。(记得我刚开始做项目的时候,由于惧怕指针,在我做的第一个项目里,我使用了 200 多个数组……)使用指针时应当小心谨慎,请记住,一定要首先确定指针指向内存的什么位置。
[例1]未初始化的指针(uninitialized pointer)。
int *p;
scanf("%d", p); /* ERROR */
/* 把值写到未知的内存空间 */
运行小程序时,p 中随机地址指向安全区域(不指向程序的代码和数据,或 OS)的可能性较大。但随着程序的增大,p 指向重要区域的概率增加,最终使程序瘫痪。
[例2]对内存中数据放置的错误假定。
char a1[80], a2[80];
char *p1=a1, *p2=a2;
if (p1<p2) { /* 处理 */
}
/* 概念错误!*/
通常程序员不能确保数据处于内存中的同样位置,不能确保各种平台都用同样格式保存数据,也不能确保各种编译器处理数据的方法完全相同。比较指向不同对象的指针时,容易产生意外结果。
[例3]假设相邻数组顺序排列,从而简单地对指针增值,希望跨越数组边界。
int a1[10], a2[10];
int *p=a1;
for (int i=0; i<20; i++) *p++=i; /* ERROR */
尽管在某些条件下可适用于某些编译器,但这是假设两个数组在内存中先存放 a1,随后存放 a2 的条件下进行的。这种情况并不常有。
[例4]时刻关注指针的当前位置。
char a[80], *p;
p=a; /* ERROR */
do {
/* p=a; 应该把这句放在循环体内 */
gets(a); /* 读入 */
while(*p) printf("%d", *p++);
} while(strcmp(a, "DONE"));
第一次循环,p 从 a[0] 开始。第二次循环时,p 值从第一次循环的结束点开始。此时 p 可能指向另一个串,另一个变量,甚至是程序的某一段。
[例5]表达式的地址。
int *p=&(a+b); /* ERROR */
在 C 中,& 只能获取一个变量的地址。在程序中,除变量外,其它表达式的存储空间是不可访问的。
四、指针与数组:
数组和指针关系非常紧密,几乎就像 UNIX 和 C 的关系一样:二者是不分家的。实际上,一维数组名可被不严格地认为是指针常量:可执行指针的操作;可按地址单元赋值和引用;也可用来初始化同型的指针变量;但不能对一维数组名赋值。
char p[]="Hello, World!";
char *p="Hello, Wrold!";
/* 两条语句完全等价 */
1、指针数组:常用于放置指向串的指针。
在程序中,对串的操作是很常见的,但就介绍过的技术来看还是有一定的局限性的。此外,在一些特殊的项目中,可能需要把若干个串作为参数传递给函数,但串的长度不能确定,这是采用定长的数组显然不合适。
[例1]给定错误编号后,输出错误信息。
void print_error(int n) {
static char *erreur[]={"Syntax Error/n", "Variable Error/n", "Disk Error/n"};
printf("%s", error[n]);
}
[例2]典型应用:打印命令行参数(类似于 echo 命令)。
void main(int argc, char **argv, char **env) {
while(*++argv) printf("%s ", *argv);
}
[例3]访问命令行参数中的字符(对 argv 施加第二下标)。
void main(int argc, char *argv[], char **env) {
int i, j;
for (i=0; i<argc; ++i) {
j=0;
while(argv[j]) {
putchar(argv[j]);
j++;
}
printf("/n");
}
}
[注意]
--> 不要像数组那样按下标单独赋值,也不要使用 *(p+n) 这样的间接引用来修改某个单元的值,这样做都可能引起运行错,因为字符串常量是在常量区,是不允许被修改的。严格来说,用一个普通指针指向一个常量是不对的,会产生一个“cannot convert from 'const type *' to 'type *'”的编译错误。字符指针的初始化是一个特例,可用一个字符指针变量指向一个字符串常量,但仍然不能修改其内容。将const char * 强制转换成 char * 能获得正确的地址,可引用,但仍然不能修改其内容。(将const int * 转换成 int * 则无法获得正确的地址,这是字符串常量的特殊性)。
--> 按下标或*(p+n)这样的间接引用来读取某单元的内容有时是可行的,这取决于不同的编译器对字符串常量的存放方式:在常量区还是堆区;采用何种存储结构;是否连续(块链就不连续)。而字符数组这样引用和赋值总是可行的,因为它开辟了一块连续的空间并复制了内容。
2、对于数组 a[],输出 a、*a、&a 的值都一样,但我们并不能认为含义一样。a 有双重含义,只不过通过 printf() 表现出来的是首元素地址;*a 中 a 取了其数组指针含义,值为第一个元素的地址;&a 中 a 取了其自定义数据类型含义,值为结构中第一个元素的地址。若把 a 赋给数组指针变量 p,则 &p 是 p 的地址,因为 p 并不具备数组这一层自定义数据类型含义。用数组名初始化指针或数组指针时做了自动类型转化,取了其指针含义。
也就是说:不要把赋值就理解为一样了。这个等到将来学过 C++ 后就会有一个比较深入的认识了。C++ 的运算符重载和自动类型转化隐含了很多东西。
3、纯指针数组:即 JAVA 中定义数组的方法。这样的数组是真正的多级指针,不能用 sizeof() 求数组大小,但可以实现多维动态,而且存取效率较高(无须做乘法来寻址)。
[例]int n1,n2,n3;
int *p1=new int[n1]; //p1[n1],但与普通数组名不同,p1 不是指针常量,而是有内存的指针变量。
int **p2=new int*[n1];
for(int I=0;I<n2;I++) p2[I]=new int[n2];
int ***p3=new int**[n1];
for(int j=0;j<n1;j++) {
p3[j]=new int*[n2];
for(int k=0;k<n2;k++) p2[j][k]=new int[n3]; //三维动态数组p3[n1] [n2] [n3]
} p2-
4、指向指针的指针(Pointers to Pointers):指针数组名就是二级指针。
在实际工作中很少需要使用指向指针的指针,它容易引起概念错误。
5、动态分配的数组(Dynamically Allocated Array):
[例1]char *p=(char *) malloc(80);
/* 必须对 p 进行下面的测试,以避免使用空指针 */
if (!p) {
printf("ERROR/n");
exit(1);
}
get(p);
for (int i=strlen(p)-1; i>=0; i--) putchar(p);
free(p);
[例2]int (*p)[10] = (int (*)[10]) malloc(40*sizeof(int)); /* 为了与 C++ 兼容,必须舍弃所有的指针转换 */
if (!p) {
printf("ERROR/n");
exit(1);
}
爱拼才会赢~!
Posted: 2005-04-24 19:47 | 4 楼
wzs8801775