ANSI标准对语言的基本类型与表达式做了许多小的修改与增补。
所有整型都包括signed(带符号)和unsigned(无符号)两种形式。
浮点运算可以以单精度进行,还可以使用更高精度的long double类型运算。
字符串常量可以在编译时连接。
ANSI C还支持枚举类型。
对象可以声明为const类型,表明其值不能修改。
2.1 变量名
名字是由字母和数字组成的序列,但其第一个字符必须为字母。
下划线“_”被看做是字母,例程的名字通常以下划线开头。
对于内部名,至少前31个字符是有效的。
函数名和外部变量名包含的字符数目可能小于31,因为汇编程序和加载程序可能会使用这些外部名。
ANSI标准仅保证前6个字符的唯一性,并且不区分大小写。
2.2 数据类型及长度
C语言只提供了下列几种基本数据类型:char, int, float, double。
此外还可以在这些基本数据类型的前面加上一些限定符。
short与long两个限定符用于限定整型,关键字int可以省略。
short int sh; long int counter;
short与long限定符的引用提供了满足实际需要的不同长度的整型数。
各编译器遵循下列限制:
short与int至少16位;long至少32位。short不得长于int;int不得长于long。
signed与unsigned可用于限定char类型或任何整型。
如果char占用8位,那么unsigned char取值范围为0~255,signed char则为-128~127
(在采用对二补码的机器上)。不带限定符的char是否带符号取决于机器,但打印字符总是正值。
float、double、long double长度也取决于具体实现。
练习2-1 编写一个程序,确定分别由signed及unsigned限定的char、short、int与long类型取值范围。
采用打印标准头文件中的相应值以及直接计算两种方式实现。
答:
#include <stdio.h>
#include <limits.h>
main()
{
printf("signed char min = %d\n", SCHAR_MIN);
printf("signed char max = %d\n", SCHAR_MAX);
printf("signed short min = %d\n", SHRT_MIN);
printf("signed short max = %d\n", SHRT_MAX);
printf("signed int min = %d\n", INT_MIN);
printf("signed int max = %d\n", INT_MAX);
printf("signed long min = %ld\n", LONG_MIN);
printf("signed long max = %ld\n", LONG_MAX);
printf("unsigned char max = %u\n", UCHAR_MAX);
printf("unsigned short max = %u\n", USHRT_MAX);
printf("unsigned int max = %u\n", UINT_MAX);
printf("unsigned long max = %lu\n", ULONG_MAX);
}
#include <limits.h>
main()
{
printf("signed char min = %d\n", SCHAR_MIN);
printf("signed char max = %d\n", SCHAR_MAX);
printf("signed short min = %d\n", SHRT_MIN);
printf("signed short max = %d\n", SHRT_MAX);
printf("signed int min = %d\n", INT_MIN);
printf("signed int max = %d\n", INT_MAX);
printf("signed long min = %ld\n", LONG_MIN);
printf("signed long max = %ld\n", LONG_MAX);
printf("unsigned char max = %u\n", UCHAR_MAX);
printf("unsigned short max = %u\n", USHRT_MAX);
printf("unsigned int max = %u\n", UINT_MAX);
printf("unsigned long max = %lu\n", ULONG_MAX);
}
2.3 常量
long类型常量以字母l或L结尾。无符号常量以字母u或U结尾。后缀ul或UL表明是unsigned long类型。
浮点数常量包含一个小数点如123.4或一个指数1e-2,也可以两者都有。
没有后缀的浮点数常量为double类型。后缀f或F表示float类型,而后缀l或L则表示long double类型。
带前缀0的整型常量为八进制形式;前缀为0x或0X,则为十六进制形式。
转义字符序列看起来像两个字符,但只表示一个字符。另外可以用'\ooo'表示任意的字节大小的位模式。
ooo代表1~3个八进制数字。还可以用'\xhh'表示,hh是十六进制数字。
#define VTAB '\013' (或'\xb')
#define BELL '\007' (或'\x7')
ANSI C语言中的全部转义字符序列如下:
常量表达式是仅仅只包含常量的表达式,在编译时求值,而不在运行时求值。
字符串常量也叫字符串字面值,是用双引号括起来的字符序列。
字符常量与仅包含一个字符的字符串之间的区别:'x'与"x"是不同的。
'x'是一个整数,其值是字母x在字符集中对应的数值;后者是一个包含一个字符x以及结束符'\0'的字符数组。
枚举是一个常量整型值的列表,如 enum boolean { NO, YES }
没有显示说明的情况下,enum类型中第一个枚举名的值为0,第二个为1。
如果只指定了部分枚举名的值,那么未指定的枚举名的值将依照最后一个指定值向后递增。
enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t' };
enum months { JAN = 1, FEB, MAR ... NOV, DEC };
不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。
相比于#define语句来说,enum的优势在于常量值可以自动生成。
2.4 声明
一个声明指定一种变量类型,但后面所带的变量表可以包含一个或多个该类型的变量。
int lower, upper, step;
默认情况下,外部变量与静态变量将被初始化为0。未经显示初始化的自动变量的值为未定义值。
const限定符指定变量的值不能被修改。对数组而言,const限定符指定数组所有元素的值都不能被修改。
const double e = 2.71828;
const char msg[] = "warning:";
const也可配合数组参数使用,表明函数不能修改数组元素的值。
int strlen(const char[])
2.5 算术运算符
取模运算符%不能应用于float或double类型。
在有负数操作符的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现。
2.6 关系运算符与逻辑运算符
由逻辑运算符&&与||连接的表达式按从左到右的顺序进行求值,在知道结果值为真或假后立即停止计算。
练习2-2 在不使用运算符&&或||的条件下编写一个与上面for循环语句等价的循环语句。
for (i=0; i<lim-1 && (c=getchar()) != '\n' && c != EOF; ++i)
答:
for (i=0; i<lim-1; ++i) {
if (c=getchar() == '\n')
break;
else if (c == EOF)
break;
...
}
2.7 类型转换
自动转换是指把“比较窄的”操作数转换为“比较宽的”,并且不丢失信息的转换。
针对可能导致信息丢失的表达式,编译器可能会给出警告信息,但这些表达式并不非法。
由于char类型就是较小的整型,因此在算术表达式中可以自由使用char类型的变量。
/* atoi: convert s to integer */
int atoi(char s[])
{
int i, n;
n = 0;
for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 * n + (s[i] - '0');
return n;
}
/* lower: convert c to lower case; ASCII only */
int lower(int c)
{
if (c >= 'A' && c <= 'Z')
return c + 'a' - 'A';
else
return c;
}
标准头文件<ctype.h>定义了一组与字符集无关的测试和转换函数。
tolower(c)函数将c转换为小写形式,可以替代上述lower函数。
测试语句c >= '0' && c <= '9'可以用函数isdigit(c)替换。
C语言中没有指定char类型的变量是无符号还是带符号变量。
当把一个char类型的值转换为int类型时,结果有可能是负整数。
C语言的定义保证了机器的标准打印字符集中的字符不会是负值。???
注意,表达式中float类型的操作数不会自动转换为double类型。
使用float类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间
(双精度算术运算特别费时)。
当表达式中包含unsigned类型的操作数时,转换规则要复杂一些。因为带符号值与无符号值之间的
比较运算是与机器相关的。假如int类型占16位,long类型占32位。那么-1L < 1U,因为unsigned int
类型1U被提升为signed long类型;但-1L > 1UL,因为1L将被提升为unsigned long类型而成为一个比较大的正数。
练习2-3 编写函数htoi(s),把由十六进制数字组成的字符串(包含可选的前缀0x或0X)转换为等价的
整数值。字符串中允许包含的数字包括:0~9、a~f以及A~F。
答:
#include <stdio.h>
int htoi(char s[]);
main()
{
char s1[] = "10";
char s2[] = "2D";
char s3[] = "3f";
char s4[] = "0X4F";
char s5[] = "0x3a";
printf("%s -> %d\n", s1, htoi(s1));
printf("%s -> %d\n", s2, htoi(s2));
printf("%s -> %d\n", s3, htoi(s3));
printf("%s -> %d\n", s4, htoi(s4));
printf("%s -> %d\n", s5, htoi(s5));
}
int htoi(char s[])
{
int n = 0;
int i = -1;
while (s[++i] != '\0') {
if (i == 0 && s[i] == '0')
continue;
else if (s[i] == 'x' || s[i] == 'X')
continue;
else if ('0'<= s[i] && s[i] <= '9')
n = n * 16 + (s[i] - '0');
else if ('a'<= s[i] && s[i] <= 'f')
n = n * 16 + (s[i] - 'a' + 10);
else if ('A' <= s[i] && s[i] <= 'F')
n = n * 16 + (s[i] - 'A' + 10);
else
return -1;
}
return n;
}
int htoi(char s[]);
main()
{
char s1[] = "10";
char s2[] = "2D";
char s3[] = "3f";
char s4[] = "0X4F";
char s5[] = "0x3a";
printf("%s -> %d\n", s1, htoi(s1));
printf("%s -> %d\n", s2, htoi(s2));
printf("%s -> %d\n", s3, htoi(s3));
printf("%s -> %d\n", s4, htoi(s4));
printf("%s -> %d\n", s5, htoi(s5));
}
int htoi(char s[])
{
int n = 0;
int i = -1;
while (s[++i] != '\0') {
if (i == 0 && s[i] == '0')
continue;
else if (s[i] == 'x' || s[i] == 'X')
continue;
else if ('0'<= s[i] && s[i] <= '9')
n = n * 16 + (s[i] - '0');
else if ('a'<= s[i] && s[i] <= 'f')
n = n * 16 + (s[i] - 'a' + 10);
else if ('A' <= s[i] && s[i] <= 'F')
n = n * 16 + (s[i] - 'A' + 10);
else
return -1;
}
return n;
}
2.8 自增与自减运算符
++与--特殊的地方表现在:它们既可以用作前缀运算符,也可以用作后缀。效果都是将变量值加1。
区别是++n先将n的值加1,然后再使用变量n的值;表达式n++则是先使用变量n的值,然后再加1。
自增与自减运算符只能作用于变量,类似表达式(i+j)++是非法的。
/* strcat: concatenate t to end of s; s must be big enough */
void strcat(char s[], char t[])
{
int i, j;
i = j = 0;
while (s[i] != '\0') /* find end of s */
i++;
while ((s[i++] = t[j++]) != '\0') /* copy t */
;
}
练习2-4 squeeze(s1, s2),将字符串s1中任何与字符串s2中字符匹配的字符都删除。
答:
#include <stdio.h>
void squeeze(char s1[], char s2[]);
main()
{
char s1[] = "thisiscdai,doyouknowcdai.cdaiiscoder.";
char s2[] = "cdai";
squeeze(s1, s2);
printf("%s\n", s1);
}
void squeeze(char s1[], char s2[])
{
int i, j, k;
for (i = k = 0; s1[i] != '\0'; i++) {
for (j = 0; s2[j] != '\0' && s2[j] != s1[i]; j++)
;
if (s2[j] == '\0')
s1[k++] = s1[i];
}
s1[k] = '\0';
}
void squeeze(char s1[], char s2[]);
main()
{
char s1[] = "thisiscdai,doyouknowcdai.cdaiiscoder.";
char s2[] = "cdai";
squeeze(s1, s2);
printf("%s\n", s1);
}
void squeeze(char s1[], char s2[])
{
int i, j, k;
for (i = k = 0; s1[i] != '\0'; i++) {
for (j = 0; s2[j] != '\0' && s2[j] != s1[i]; j++)
;
if (s2[j] == '\0')
s1[k++] = s1[i];
}
s1[k] = '\0';
}
结果为:thss,oyouknow.soer.
练习2-5 编写函数any(s1, s2),将字符串s2中的任一字符在字符串s1中第一次出现的位置作为结果返回。
如果s1中不包含s2中的字符,则返回-1。
答:
#include <stdio.h>
int any(char[], char[]);
main()
{
char s1[] = "thisiscdaiacoder";
char s2[] = "cdai";
printf("%d\n", any(s1, s2));
char s3[] = "thisiscoder";
printf("%d\n", any(s3, s2));
}
int any(char s1[], char s2[])
{
int i, j;
for (i = 0; s1[i] != '\0'; ++i) {
for (j = 0; s1[i+j] == s2[j] && s2[j] != '\0'; ++j)
;
if (s2[j] == '\0')
return i;
}
return -1;
}
int any(char[], char[]);
main()
{
char s1[] = "thisiscdaiacoder";
char s2[] = "cdai";
printf("%d\n", any(s1, s2));
char s3[] = "thisiscoder";
printf("%d\n", any(s3, s2));
}
int any(char s1[], char s2[])
{
int i, j;
for (i = 0; s1[i] != '\0'; ++i) {
for (j = 0; s1[i+j] == s2[j] && s2[j] != '\0'; ++j)
;
if (s2[j] == '\0')
return i;
}
return -1;
}
2.9 按位运算符
C语言提供了6个位操作运算符。只能作用于整型,带符号或无符号的char、short、int、long。
& 按位与
经常用于屏蔽某些二进制位,例如:n = n & 0177; 将n中除7个低二进制位外的其他各位均置为0。
| 按位或
常用于将某些二进制位置为1,例如:x= x | SET_ON; 将x中对应于SET_ON中为1的那些二进制位置为1。
^ 按位异或
当两个操作数的对应为不相同时将该位设置为1。
必须将位运算符&、|同逻辑运算符&&、||区分开来。例如,x=1,y=2,那么x & y = 0, x && y = 1。
移位运算符<<和>>
x << 2将把x的值左移2位,右边空出的2位用0填补。该表达式等价于对左操作数乘以4。
当对signed类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),
而另一些机器则对左边空出的部分用0填补(“逻辑移位”)。
~ 按位求反
用于求整数的二进制反码,即分别将操作数各二进制位上的1变为0,0变为1。
例如x = x & ~077 将把x的最后6位设置为0。注意,表达式x & ~077与机器字长无关,它比形式
为x & 017700的表达式更好,后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,
因为~077是常量表达式,可以在编译时求值。
例子:getbits(x,4, 3)返回x中第4、3、2三位的值。
/* getbits: get n bits from position p */
unsigned getbits(unsigned x, int p, int n)
{
return (x >> (p-n+1)) & ~(~0 << n);
}
x >> (p-n+1)将期望获得的字段移位到字的最右端。
~0的所有位都是1。~(~0 << n)建立了最右边n位全为1的屏蔽码。
练习2-6 编写一个函数setbits(x, p, n, y),返回对x执行下列操作后的结果值:
将x中从第p位开始的n位设置为y中最右边n位的值,x的其余各位保持不变。
练习2-7 编写一个函数invert(x, p, n),返回对x执行下列操作后的结果值:
将x中从第p位开始的n位求反,x的其余各位保持不变。
练习2-8 编写一个函数rightrot(x, n),返回将x循环右移(即从最右端移出的位将
从最左端移入)n位后所得到的值。
答:
#include <stdio.h>
main()
{
unsigned x = 0645; // 110_100_101
int p = 5;
int n = 3;
int leftMask = ~(~0 << (p+1-n)); // 111
int rightMask = ~0 << (p+1); // 11...1000000
int midMask = ~(leftMask | rightMask); // 111000
int leftBit = x & leftMask; // 101
int midBit = x & midMask; // 100000
int rightBit = x & rightMask; // 110000000
// Practice 2-6: replace [p, p-n+1]
int replace = 02;
int replaceBit = replace << (p+1-n); // 10000
int resultBit1 = rightBit | replaceBit | leftBit;
printf("%o\n", resultBit1);
// Practice 2-7: invert [p, p-n+1]
int midInvert = ~(leftMask | midBit | rightMask);//11000
int resultBit2 = rightBit | midInvert | leftBit;
printf("%o\n", resultBit2);
main()
{
unsigned x = 0645; // 110_100_101
int p = 5;
int n = 3;
int leftMask = ~(~0 << (p+1-n)); // 111
int rightMask = ~0 << (p+1); // 11...1000000
int midMask = ~(leftMask | rightMask); // 111000
int leftBit = x & leftMask; // 101
int midBit = x & midMask; // 100000
int rightBit = x & rightMask; // 110000000
// Practice 2-6: replace [p, p-n+1]
int replace = 02;
int replaceBit = replace << (p+1-n); // 10000
int resultBit1 = rightBit | replaceBit | leftBit;
printf("%o\n", resultBit1);
// Practice 2-7: invert [p, p-n+1]
int midInvert = ~(leftMask | midBit | rightMask);//11000
int resultBit2 = rightBit | midInvert | leftBit;
printf("%o\n", resultBit2);
// Improve 2-6
// xxx...x000x...xxx x
// 000...0nnn0...000 y
// ( x & ~(~(~0 << n) << (p+1-n) ) | ( (y & ~(~0 << n)) << (p+1-n) )
// Improve 2-7 using XOR
int xorMask = (~(~0 << n)) << (p-n+1);
printf("%o\n", x ^ xorMask);
}
2.10 赋值运算符与表达式
统计整型参数的值为1的二进制位的个数。
/* bitcount: count 1 bits in x */
int bitcount(unsigned x)
{
int b;
for (b = 0; x != 0; x >>= 1)
if (x & 01)
b++;
return b;
}
将x声明为无符号类型是为了保证将x右移时,无论该程序在什么机器上运行,左边空出的位都用0
(而不是符号位)填补。
练习2-9 在求对二的补码时,表达式x &= (x - 1)可以删除x中最右边值为1的一个二进制位。
请解释这样做的道理。用这一方法重写bitcount函数,以加快其执行速度。
答:
// If x like XXX1, it should be XXX0 & XXX1 = XXX0
// Example: 110_100_101 => 110_100_100
// Else x like XX10..00, it should be XX01..11 & XX10..00 = XX00..00
// Example: 110_100_100 => 110_100_000
#include <stdio.h>
main()
{
unsigned x = 0645; // 110_100_101
int count = 0;
while (x != 0) {
x &= (x-1);
count++;
}
printf("1-bit count: %d\n", count);
}
main()
{
unsigned x = 0645; // 110_100_101
int count = 0;
while (x != 0) {
x &= (x-1);
count++;
}
printf("1-bit count: %d\n", count);
}
2.11 条件表达式
如果expr2与expr3的类型不同,结果的类型将由转换规则决定。
例如f为float类型,n为int类型,则表达式(n > 0) ? f : n是float类型,与n是否为正值无关。
for (i = 0; i < n; i++)
printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : ' ';
在每10个元素之后以及第n个元素之后都要打印一个换行符,所有其他元素后打印一个空格。
练习2-10 重新编写将大写字母转换为小写字母的函数lower,用条件表达式替代其中的if-else结构。
答:
#include <stdio.h>
char lower(char c)
{
return ('A'<=c && c<='Z') ? c-('A'-'a') : c;
}
main()
{
printf("%c\n", lower('f'));
printf("%c\n", lower('Y'));
printf("%c\n", lower('5'));
}
char lower(char c)
{
return ('A'<=c && c<='Z') ? c-('A'-'a') : c;
}
main()
{
printf("%c\n", lower('f'));
printf("%c\n", lower('Y'));
printf("%c\n", lower('5'));
}
2.12 运算符优先级与求值次序
位运算符&、^与|的优先级比==与!=低。这意味着,位测试表达式,如if ((x & mask) == 0)
必须用圆括号括起来才能得到正确的结果。
同大多数语言一样,C语言没有指定同一运算符中多个操作数的计算顺序(&&、||、?:和,)除外。
例如x = f() + g();的语句中,f()可以在g()之前计算,也可以在g()之后计算。因此,如果函数f或g
改变了另一个函数所使用的变量,那么x的结果可能会依赖于这两个函数的计算顺序。
类似地,C语言也没有指定函数各参数的求值顺序。
printf("%d %d\n", ++n, power(2, n));
在不同的编译器中可能会产生不同的结果,取决于++n在power调用之前还是之后。
函数调用、嵌套赋值语句、自增与自减都有可能产生“副作用”。
表达式何时产生副作用由编译器决定,因为最佳的求值顺序同机器结构有很大关系。