现在的位置: 首页 > 综合 > 正文

C 语言复习与提高—II. 表达式

2013年08月10日 ⁄ 综合 ⁄ 共 7405字 ⁄ 字号 评论关闭
II. 表达式

C 语言的最基本元素:表达式。 表达式由 C 的基础元素:数据(变量、常量、函数返回值)和操作符构成。

一、基本数据类型:

1、C89 定义了五种基本数据类型:char,int,float,double,void。

这五种类型构成了其它几种类型的基础。它们的长度和域随处理机类型和编译器的不同而改变(char 型的对象在任何情况下都是 1B)。C 只规定了每种数据类型的最小范围,而不是字节大小。

2、C99 新增加的四种类型:_Bool,_Complex,_Imaginary,long long。

(1、)_Bool 型的对象的值可以是 1(true)或 0(false),它是一个整数类型。 [注意]它与 C++ 中的 bool 不同。C++ 定义了内置的 Bool 常量 true 和 false,而 C99 中是在 <stdbool.h> 中定义了宏 bool、true 和 false。这样使得程序员可以自由编写出与 C/C++ 都兼容的代码。

(2、)对复数运算的支持:_Complex,_Imaginary,以及附加的头文件和几个新的库函数。增加复数运算主要是为了给与数值相关的程序提供更好的支持。

--> C99 中定义的复数类型: float_Complex float_Imaginary double_Complex double_Imaginary long double_Complex long double_Imaginary

--> <complex.h> 中定义了宏 complex 和 imaginary,并扩充为 _Complex 和 _Imaginary。 在编写代码时,最好包含此头文件,并使用 complex 和 imaginary 宏。

(3、)C99 对 int 型的扩充:long long 型允许整数的长度为 64 位。

--> long long int:从 -(2^63-1)到 2^63-1。

--> unsigned long long int:从 0 到 2^64-1。

二、修饰基本类型:除 void 类型外,基本类型前均可加各种修饰符(signed,unsigned,long,short)。

1、int 类型前可以加 signed、unsigned、long、short 修饰符。 char 类型前可以加 unsigned 和 signed 修饰符。 double 类型前可以加 long 修饰符。 [习惯]一般定义 unsigned int 为 UINT:typedef unsigned int UINT;

2、int 型使用 signed 修饰是冗余的。 某些实现允许使用 unsigned double 型,但降低了代码的可移植性,一般不鼓励使用。 对 ANSI C 类型的任何扩充都可能在某些编译器上出错。

三、类型修饰符:

1、C89 定义了两个:const 和 volatile。

(1、)const 型变量的值只随外部事件变化,而不能被程序修改,但能被初始化。编译器把这类变量放到只读区域中。 const 通过直接初始化或某些依赖于硬件的方法取得初值。如:const int max=10; const 可以保护函数变量指向的对象,避免它被函数修改。在函数的形参列表中,把指针定义为 const 类型,那么函数中的代码就不能修改传入指针指向的实际变量。 const 也可以使变量不受任何程序的修改。const 型变量可以由程序外的成分修改(如硬设备可以 const 型变量的值)。

(2、)volatile 告速编译器,变量的值可能以未在程序中明确的表达方法改变。 volatile int i=10; /* 可变变量,主要用于和硬件打交道 */ 如:把全局变量的地址传人到 OS 的时钟子程序,用该全局变量保存系统的实时时钟值。 volatile 的重要性在于防止编译器的自动优化操作。

(3、)const 和 volatile 可以同时使用,表示这个变量只受外部条件的修改。如: const volatile char *port=(const volatile char *)0x30; /* 定义 0x30 为一个端口 */

2、C99 中新增加了 restrict 修饰的指针: 由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。

由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。 编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。

[典型例子] memcpy() 在 C99 中,restrict 可明确用于 memcpy() 的原型,而在 C89 中必须进行解释。 void *memcpy(void *restrict str1, const void *restrict str2, size_t size); /* 通过使用 restrict 修饰 str1 和 str2 来保证它们指向不重叠的对象 */

四、常量与变量:

1、常量不占用内存空间,不可变。 C 语言规定,编译器把数值常量表示成最小兼容类型。缺省情况下,编译器把数字常量放入能够存放它的最小的兼容数据类型中。 如:10 --> int 型,60000 --> unsigned 型,100000 --> long 型。

2、C89 规定,在任何动作语句之前,在块的开头声明所有局部变量。但在 C99 以及 C++ 中则无此限制(在首次使用前,可在块的任何位置声明变量)。

3、变量在声明时初始化可以减少程序的执行时间。 全局和静态全局变量只在程序开始运行时初始化。局部和静态变量在进入被定义的代码块时初始化。 局部变量不经初始化,初值不确定,直到首次赋值时才有确定值。未经初始化的全局和静态局部变量自动设置为 0。 若局部变量和全局变量同名,在声明局部变量的代码块中只引用局部变量,不影响全局变量。引用全局变量的方法是在变量名前加两个冒号。

[例]#include <stdio.h> int gv; void main() { int gv=8; ::gv=gv; /* 引用全局变量 */ printf("%d/n", ::gv); }

4、程序中应减少不必要的全局变量,因为全局变量在程序执行期间一直占用内存空间。并且,若在局部变量可以满足要求时使用全局变量,则函数的通用性变差(因为函数依赖于外部定义的内容)。

在程序中大量使用全局变量可能引起未知或非预期的程序错误和副作用(只使用全局变量的标准 BASIC 语言就是最好的例子)。尤其是在开发大型项目时,一个主要的问题就是在程序的其它地方意外地改变全局变量的值。

5、编程时的注意事项:

--> int 型变量与 char 型变量不是在任何情况下都可以相互替代的,因为 ASCII 的范围是 0 到 255。

--> float a=1.0, b=1.0; if (a == b) { /* 处理 */ } 其中,a == b 永远为假。因为计算机的内部表示法会把它们表示为(例如)a=1.00003,b=1.00002。 正确的解决方法是:|a-b| < 1e-6; /* 给定一个范围 */

五、存储类型说明符:extern,static,regester,auto。

这些说明符告诉编译器应该如何保存有关变量。它们放在其修饰的变量声明之前:Storage_specifier type var;

1、在现实工作中,多文件程序和 extern 声明通常包含在头文件中。

2、static 型局部变量只在它被声明的代码块中可见,static 型全局变量只在它驻留的文件中可见。 本质上,static 允许为需要它且唯一认可的函数声明变量,使其在函数调用之间保持变量值的同时又没有意想不到的副作用。

[例]while(1) { while(1) { int i=10; static int i=10; i++; i++; } } /* 每次 i 都为 11 */ /* i == 11, 12, 13, … */

3、register 变量:

(1、)只能用于局部变量和函数形参(全局 register 变量是非法的)。

(2、)应用于作用最大的地方(通常是多次引用同一变量的地方)。

(3、)任意代码块中同时得到速度优化的 register 变量的数目有限。超限时,编译器自动把 register 变量转换成普通变量(保证了代码在大量处理机间的移植性)。

(4、)register 变量不能进行编址(即不能用 & 操作符取地址),因为寄存器没有地址。

4、关于 auto 型变量的初始化问题:

(1、)考虑如下代码: int i; printf("%d/n", i); /* 编译时报错 */

对自动变量赋初值前其值不定。在为一个自动变量分配内存空间后,C 并不清理,因此它是一个随机值。

(2、)初始化的方式: 注意形参不能被初始化。

--> void f1(int variable) { int var1=varable+1, var2=0; /* variable 的内存空间已分配 */ }

--> void f2() { int var1=0, var2=var1; /* var1 的内存空间已分配 */ }

--> void f3(int variable) { int var1, var2; var1=2; var2=variable+2; /* 定义后赋值 */ }

六、操作符:

1、赋值符(=)左边(赋值目标)必须是变量或指针,而不能是函数或常量。注意赋值中的类型转换。 在专业程序中,常使用多重赋值使多个变量被赋予公共值:x=y=z=0;

2、复合赋值:简化了一定类型的赋值操作的编码,被广泛应用于专业 C 程序的编写。如:a+=10;

3、++ 和 --:在多数编译器中,++ 和 -- 操作符生成的代码比等价的赋值语句生成的代码的运行速度快得多,且目标代码的效率更高。

4、关系(relational)和逻辑(logic)操作符:它们的运算顺序均低于算术操作符,并且只产生 0 和 1。 关系:指各值之间的关系。(>,<,>=,<=,==,!=) 逻辑:指如何组合各值之间的关系。(&&,||,!)

[例1] 10>5&&!(10<9)||3<=4 /* true */

[例2]实现异或操作符: #include <stdio.h> int xor(int a, int b) { return (a||b) && !(a&&b); }

5、位运算(&,|, ^,~,>>,<<): C 是专门为多数程序设计任务中取代汇编语言而设计的,所以 C 支持许多汇编操作。

按位操作(bitwise operation)指测试、抽取、设置或移位字节和字中实际的二进制位。位操作只能作用于标准的 char 和 int 型数据。

(1、)按位与可以作为一种清楚某位的手段(0 与任何数均为 0)。

(2、)或操作可以用于置位(1 或任何数均为 1)。

(3、)异或用于检查两个操作数的某对应位是否一致(不一致则为 1)。

(4、)移位操作:主要用于对外部设备的输入解码,D/A 转换,读状态信息等。也常被用于加密子程序。 --> 左移(<<):相当于乘以 2。 --> 右移(>>):相当于除以 2。 [注意]左移并不能恢复右移丢失的信息。

(5、)编译时操作符:sizeof() 在编译时求值。 返回其操作数(变量、类型)对应数据类型的字节数。主要用于生成依赖 C 内设数据类型尺寸的可移植代码。

[例1]double f; printf("%d/n", sizeof f); printf("%d/n", sizeof(int));

[例2]/* 把 6 个整数存放到磁盘的文件里 */ /* 这段代码在任何环境下都可以通过编译,正常运行 */ void put2disk(int chiffres[6], FILE *fp) { int longeur; if ((longeur=fwrite(chiffres, sizeof(int)*6, 1, fp)) !=1) printf("Write Error!/n"); }

七、表达式:C 表达式是操作符、常量和变量的任意组合。多数表达式基本遵循代数规则。

1、求值顺序:C 表达式的子表达式以未定顺序求值,允许编译器自由重排表达式的顺序,由此产生最优代码。 同时,不允许程序依赖于对子表达式的求值顺序。如: f = f1() + f2(); /* 不保证先调用 f(),再调用 f2() */

[例]关于 printf() 的一些问题。 [程序1]#include <stdio.h> [程序2]#include <stdio.h> main() { main() { int i; int i; i=1; i=1; printf("%d,%d,%d/n",i,i++,i++); printf("%d,%d,%d/n",i,++i,++i); } }

以第一个程序为例。运行前我们判断程序的运行结果当然是 1,2,3。但在运行之后,却发现结果是 3,2,1。

现在 SCO UnixWare 7.1.3 平台下,用 GNU gcc 编译器编译第一个程序,使其生成汇编代码: $gcc -S test_printf.c .file "test_printf.c" .def ___main; .scl 2; .type 32; .endef .text LC0: .ascii "%d,%d,%d/12/0" .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax movl %eax, -8(%ebp) movl -8(%ebp), %eax call __alloca call ___main movl $1, -4(%ebp) movl -4(%ebp), %eax   pushl %eax leal -4(%ebp), %eax incl (%eax) movl -4(%ebp), %eax pushl %eax leal -4(%ebp), %eax incl (%eax) pushl -4(%ebp) pushl $LC0 call _printf addl $16, %esp leave ret .def _printf; .scl 2; .type 32; .endef

从而得到 printf() 的执行顺序为: 最后一个i++先执行(i=1 入栈,执行i++,得到 2)--> 倒数第2个参数执行(i=2入栈,执行i++,得到 3)--> 执行第一个参数(i=3 入栈)--> 出栈结果为:3,2,1。

那么用同样的方法得到第二个程序的汇编代码: .file "test2.c" .def ___main; .scl 2; .type 32; .endef .text LC0: .ascii "%d,%d,%d/12/0" .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax movl %eax, -8(%ebp) movl -8(%ebp), %eax call __alloca call ___main movl $1, -4(%ebp) leal -4(%ebp), %eax incl (%eax) pushl -4(%ebp) leal -4(%ebp), %eax incl (%eax) pushl -4(%ebp) pushl -4(%ebp) pushl $LC0 call _printf addl $16, %esp leave ret .def _printf; .scl 2; .type 32; .endef

这样的编程方法是现在比较反对的。因为 printf() 是变参数函数,它存在一个问题:若在编译之前不知道参数数目就无法调用。 并且,ANSI 没有明确函数入栈时的计算顺序,即在 ANSI 为了在不同的 CPU 上取得最佳的入栈效率,允许各编译器定义自己的入栈计算顺序(可参阅相关书籍或 ANSI)。不过我们见过的多数编译器都是自右向左的。

另外,上面说的入栈的计算顺序,需要把它跟变量在栈中的存储次序区分开来。 参数在栈中的存储顺序一般是自小向大的,而函数的局部变量在栈内的顺序一般是自大至小的。 函数的参数跟局部变量相当(但不一定在相同的内存区域),都是在该函数被调用时分配内存空间。只是函数的参数是调用程序初始化,而局部变量是由该函数内部初始化。

下面给出一种处理可变参数的非标准方法: 各编译器都将各个参数在内存中连续存放,所以我们可以很容易地根据第一个参数的地址向前和向后取得任意一个参数了。

[例]如何从参数栈中依次取出每个参数(具有很好的移植性)。 在 GNU gcc,Microsoft VC,和 Borland TC 下测试通过。

#include <stdio.h>

typedef struct __TST_S { int var; char * pstr; } TST_S;

int fun (int arg1, ...) { int * pc; TST_S * ptst;

pc = &arg1; printf("arg1: %d./n/r", *pc); pc++; printf("arg2: %d./n/r", *pc); pc++;

printf("arg3: %s./n/r", (char*)(*pc)); pc++;

ptst = (TST_S *)pc; printf("arg4: tst.var: %d, tst.pstr: %s/n/r", ptst->var, ptst->pstr); pc += sizeof(TST_S)/sizeof(pc);/* MOVE A STURCT SIZE); */ /* NOTE pc IS int * type */

printf("arg5: %d./n/r", *pc); pc++;

return 0; }

void main () { TST_S tst;

tst.var = 128; tst.pstr = "This is a test.";

fun (123, 456, "Hello world.", tst, 789); }

输出结果为: arg1: 123. arg2: 456. arg3: Hello world.. arg4: tst.var: 128, tst.pstr: This is a test. arg5: 789.

2、类型转换:

(1、)C 编译器要做类型提升(type promotion):把所有操作数转换成尺寸最大的操作数类型。 [注意]若两个操作数分别是 long 和 unsigned 型的,同时 unsigned 值不能用 long 表示,则两个数均转换成 unsigned long。

(2、)强制类型转换:

--> 35000L,987U,…

--> (type) expr

3、程序员应在表达式中加入空格、括号、制表符,使表达式更可读。冗余号不会降低表达式的求值速度。

[例] y=a/8-26*b+126; y = (a/8) - (26*b) + 126; /* 这个更清楚、更容易理解 */

 

抱歉!评论已关闭.