《C语言程序设计:现代方法(第2版)》读书笔记


主要是备忘C99标准中的一些变化。

允许代码段与声明混合

允许在程序块中任何地方声明变量,只要在第一次调用该变量之前声明就可以。

支持//风格注释

变量不再隐式声明为int类型

不支持隐式声明函数

更准确的整型除法

C89中i/j的两个整数操作数中有负数时,除法的结果既可以向上取整,也可以向下取整,在C99中总是向0取整。C89中如果i或j为负数,i%j结果与实现有关,在c99中结果符号总与i的符号相同。

布尔类型

新的布尔类型_Bool,本质是无符号的整型。_Bool类型的变量只能存储0或1,所以,将非0的值赋值给_Bool类型的变量都会导致变量的值变为1。同时还提供了新的头文件 <stdbool.h> ,内容如下:

#define __bool_true_false_are_defined   1
#define bool    _Bool
#define false   0
#define true    1

for语句的变化

在C99中,for语言的第一个表达式可以替换为一个声明,下面的代码也是合法的了:

for (int i = 0; i < N; ++i)
    ...

64位整数和扩展整数类型的支持

C99提供了两个额外的标准整数类型:long long intunsigned long long int,大小至少64位宽。同时,以LLll(所有字母的大小写要一致)结尾的整数常量是long long int类型的,以ULLull(所有字母的大小写要一致)结尾的整数常量是unsigned long long int类型的。

除了标准的整数类型外,C99标准还允许在具体实现时定义扩展的数类整数(包括有符号的和无符号的)。比如:128位的整数类型。

隐式转换的变化

因为C99中增加了一些类型(_Bool、long long类型、扩展的整数类型和复数类型)。新的隐式转换规则略有变化(这里忽略了扩展整数类型和枚举类型):

  1. long long int、unsigned long long int
  2. long int、unsigned long int
  3. int、unsigned int
  4. short int、unsigned short int
  5. char、signed char、unsigned char(这里要注意char类型和signed char类型是不同的类型)
  6. _Bool

C99用整数提升(integer promotion)取代了C89中的整值提升(integral promotion),可以将任何等级低于int和unsigned int的类型转换为int(只要该类型的所有值都可以用int类型表示)或unsigned int。

和C89一样类似,C99中执行常用的算术转换的规则可以划分为两种情况:

  • 任一操作数的类型是浮点数类型的情况。只要两个操作数都不是复数型,规则如下(下面的内容摘录自C程序设计语言(第2版_新版)):

    1. 如果任何一个操作数为long double类型,将另一个操作数转换为long double类型,过程结束。
    2. 如果任何一个操作数为double类型,将另一个操作数转换为double类型,过程结束。
    3. 如果任何一个操作数为float类型,将另一个操作数转换为float类型,过程结束。
  • 两个操作数的类型都不是浮点类型的情况。首先对两个操作数进行整数提升。如果这时两个操作数的类型相同,过程结束。否则,依次尝试下面的规则,一旦遇到可应用的规则就不再考虑别的规则:

    1. 如果两个操作数都是有符号型或都是无符号型,将整数转换等级低的操作数转换为等级较高的操作数的类型。
    2. 如果无符号操作数的等级高于或等于有符号操作数的等级,将有符号操作数转换为无符号操作数的类型。
    3. 如果有符号操作数类型可以表示无符号操作数类型的所有值,将无符号操作数转换为有符号操作数的类型。
    4. 否则,将两个操作数都转换为与有符号操作数的类型相对应的无符号类型。

另外,所有算术类型都可以转换为_Bool类型。如果原始值为0则转换结果为0,否则结果为1。

数组初始化

这条C99中没有变化,只是C和C++略有不同。

如果数组的初始化式比数组短,那么数组中剩余的元素赋值为0:

int a[3] = {1, 2};
/* initial value of a is {1, 2, 0} */

但是在C中初始化式完全为空是非法的,如果想要把数组全部初始化为0,必须要在大括号中放一个0。这点要求在C++中是没有的。

// initial value of a to {0, 0, 0}
int a[3] = {}; // invalid in c but valid in c++

数组指定初始化式

C99中,提供了指定下标的方式来初始化数组中指定位置的值。

int a[5] = {[3] = 3, [1] = 1}; /* a is {0, 1, 0, 3, 0} */

同时,初始化式中老方法(逐个元素初始化)和新方法(指定初始化式)可以同时使用。

int b[] = {1, [2] = 2}; /* b is {1, 0, 2} */
int b[] = {[2] = 2, 1}; /* b us {0, 0, 2, 1}*/

变长数组

下面的代码在C99标准下是合法的:

int n = 0;
scanf("%d", &n);

int a[n]; /* C99 only*/

变长数组的长度是在程序执行时计算的,而不是在编译时计算的。变长数组的主要限制是它们没有静态存储时限(因为它是放在栈上面的),另一个限制是变长数组没有初始化式。

变长数组形式参数

下面的代码在C99标准下是合法的:

int sum_2d_array(int m, int n, int arr[m][n])
{
    ...
}

注意:参数的顺序很重要。int mint n必须要在int arr[m][n]的左边。

在声明包含有变长数组形式的函数时,可以使用*,比如上面的函数可以声明为:

int sum_2d_array(int m, int n, int arr[m][n]);
int sum_2d_array(int m, int n, int arr[*][*]);
int sum_2d_array(int m, int n, int arr[][m]);
int sum_2d_array(int m, int n, int arr[][*]);

变长数组参数对编译器来说,只是提示性的,编译器并不进行额外的错误检测,只是方便编译优化等。所以实际上变长数组的大小和变长数组的参数可能是无关的。

在数组参数声明中使用static

C99允许在数组参数声明中使用关键字static。在下面的代码中,将static放在数字3之前表示数组a的长度至少可以保证为3:

int sum_array(int arr[static 3], int n)
{
    ...
}

这样使用static对程序的行为不会有任何影响。static的存在只是提示编译器,方便编译器根据此提示优化指令。

最后,如果数组参数是多维的,static仅可用于第一维(比如,指定二维数组的行数)。

数组复合字面量

代码:

int b[] = {3, 0, 3, 4, 1};
total = sum_array(b, 5);

在C99中,可以简化为:

total = sum_array((int []){3, 0, 3, 4, 1}, 5);

其中,(int []){3, 0, 3, 4, 1}就是复合字面量

复合字面量是通过指定其包含的元素而创建的没有名字的数组。其格式为:先在一对圆括号内给定类型名,随后在一对花括号内设定所包括元素的值。

复合字面量类似于应用于数组初始化式的强制转换。事实上,复合字面量和数组初始化式遵守同样的规则。复合字面量可以包含指示符,就像指定初始化式一样;可以不提供数组完全的初始化(未初始化的元素默认被初始化为0)。例如:复合字面量(int[10]){8,6}有10个元素,前面两个元素的值为8和6,剩下的元素值为0。

函数内部创建的复合字面量可以包含任意表达式,不限于常量。例如:

total = sum_array((int []){2*i, i+j, j*k}, 3);

其中i、j、k都是变量。

复合字面量为左值,所以其元素的值可以改变。如果要求其值为”只读”,可以在类型前面加上const,如(const int[]){5,4}

指向常量数组复合字面量的指针

指针指向复合字面量创建的数组中的某个元素是合法的。下面两段代码都是合法的,并且意义相同。

代码一:

int a[] = {3, 0, 3, 4, 1};
int* p = &a[0];

代码二:

int* p = (int []){3, 0, 3, 4, 1};

C99中的指针和变长数组

指针可以指向变长数组中的元素。如果变长数组是多维的,指针的类型取决于除第一维外每一维的长度。下面是二维的情况:

void fun(int m, int n)
{
    int a[m][n], (*p)[n];
    p = a;
    ...
}

因为p的类型依赖于n,而n不是常量,所以说p具有可改变类型。需要注意的是,编译器并非总能确定p = a这样的赋值语句的合法性,例如,下面的代码可以通过编译,但只有当m = n是才正确:

int a[m][n], (*p)[m];
p = a;

如果m != n,后续对p的使用都将导致未定义的行为。

与变长数组一样,可改变类型也具有特定的限制,其中最重要的限制是,可改变类型的声明必须出现在函数体内部或者在函数原型中。

C99中新增的预定义宏

名字 描述
__STDC_HOSTED__ 如果是托管式实现,值为1;如果是独立式实现,值为0
__STDC_VERSION__ 支持的C标准版本
__STDC_IEC_559__ 如果支持IEC 60559浮点算术运算,则定义该宏,且值为1
__STDC_IEC_559_COMPLEX 如果支持IEC 60559复数算术运算,则定义该宏,且值为1
__STDC_ISO_10646__ 如果wchar_t类型的值由ISO/IEC 10646标准中的码值表示,则定义该宏,且值的格式是yyyymmL(表示修订的年月)

空的宏参数

C99允许宏调用中的任意或所有参数为空。但是这样的调用需要有和一般调用一样多的逗号(方便看出哪些参数被省略了)。

在大多数情况下,实际参数为空的效果是显而易见的。例如:

#define ADD(x,y) (x+y)

i = ADD(j,k);
i = ADD(,k);

经过预处理后变成:

i = (j+k);
i = (+k);

当空参数是#或##运算符的操作数时,用法有特殊规定。例如:

#define MK_STR(x) #x
char empty_string[] = MK_STR();

#define JOIN(x,y,z) x##y##z
int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);

经过预处理后变成:

char empty_string[] = "";

int abc, ab, ac, c;

参数个数可变的宏

在C89中,如果宏有参数,那么参数的个数是固定的。在C99中,这个条件被适当放宽了,允许宏具有可变长度的参数列表。

宏具有可变参数个数的主要原因是:它可以将参数传递给具有可变参数个数的函数。比如:

#define TEST(cond, ...) ((cond) ? printf("pass test: %s\n", #cond) : printf(__VA_ARGS__))

...记号(省略号)出现在宏参数列表的最后,前面是普通参数。__VA_ARGS__是一个专用的标识符,只能出现在具有可变参数个数的宏的替换列表中,代表所有与省略号相对应的参数。(至少有一个与省略号相对应的参数,但该参数可以为空。)

func标识符

每个函数都可以访问__func__标识符,它的行为很像一个存储当前正在执行的函数的名字的字符串变量。作用相当于在函数体的一开始包含了如下声明:

static const char __func__[] = "function-name";

__func__的另一个用法:作为参数传递给函数,让函数知道调用它的函数的名字。

_Pragma运算符

C99引入了与#pragma指令一起使用的_Pragma运算符。其具有如下形式:

_Pragma (字符串字面量)

遇到该表达式时,预处理器通过移除字符串两端的双引号并分别用字符"\代替转义序列\"\\来实现对字符串字面量的”去字符串化”。下面的两行代码意义相同:

_Pragma("data(heap_size => 1000, stack_size => 2000)")

#pragma data(heap_size => 1000, stack_size => 2000)

结构指定初始化式

与数组指定初始化式类似,结构也可以使用指定初始化式。下面两行初始化代码意义相同:

struct KibaZen
{
    int a;
    int b;
    int c;
    int d;
};

struct kibaZen k = { 10, 20, 30, 40 };
struct KibaZen z = { .c = 30, 40, .a = 10, 20 };

结构复合字面量

和数组复合字面量类似,结构也有复合字面量。下面的代码是合法的:

struct KibaZen kz = (struct kibaZen){ .c = 30, 40, .a = 10, 20 };

受限指针

在C99中,用restrict声明的指针叫做受限指针(restricted pointer)。它向编译器保证,在这个指针的生命周期内,任何通过该指针访问的内存,都只能被这个指针改变。目的是为了是给编译器提供额外的信息帮助编译器进行代码优化。

void* memcpy(void* restrict dst, const void* restrict src, size_t n);
void* memmove(void* dst, const void* src, size_t n);

C99标准下,memcpy中的dst和src都使用了restrict,说明复制源和目的地不应互相重叠(但不能确保不重叠)。而memmove中的dst和src没有使用restrict,说明即使在重叠时也能正常复制。

灵活数组成员

在存储字符串时我们可能会定义下面的结构:

struct vstring
{
    int len;
    char chars[1];
};

struct vstring* str = malloc(sizeof(struct vstring) + n - 1);
str->nlen = n;

这里使用了一种”欺骗”的方法,分配比该结构声明时应具有的内存更多的内存,然后使用这些内存来存储chars数组额外的元素。这种方法称为”struct hack”。C89标准并不能保证struct hack技术工作,也不允许数组长度为0(GCC允许)。

C99提供了灵活数组成员(flexible array member)来达到同样的目的。当结构的最后一个成员是数组时,其长度可以省略:

struct vstring
{
    int len;
    char chars[];   /* flexible array member - c99 only */
};

struct vstring* str = malloc(sizeof(struct vstring) + n);
str->len = n;

具有灵活数组成员的结构是不完整类型(incomplete type)。不完整类型缺少用于确定所需内存大小的信息。

内联函数

C99标准下,可以使用关键字inline创建内联函数。

新的头文件 <stdbool.h>

C99对…printf转换说明的修改

C99对printf函数和fprintf函数的转换说明做了不少修改。

  • 增加了长度修饰符:hh、ll、j、z和t。
  • 增加了转换说明符:F、a和A。
  • 允许输出无穷数和NaN。
  • 支持宽字符输出:%lc%ls
  • C89未定义的转换说明C99允许了。%le、%lE、%lf、%lg和%lG在C99是合法的(l长度修饰符被忽略)。

C99对…scanf转换说明的改变

C99对scanf函数和fscanf函数的转换说明也做了一些修改。

  • 增加了长度修饰符:hh、ll、j、z和t。
  • 增加了转换说明符:F、a和A。
  • 具有读无穷数和NaN的能力。
  • 支持宽字符。%lc转换说明用于读出单个的多字节字符或者一系列多字节字符;%ls用于读取由多字节字符组成的字符串(在结尾添加空字符)。%l[集合]%l[^集合]转换说明也可以读取多字节字符串。

scanf示例:

代码 输入 变量
n = scanf("%i%i%i", &i, &j, &k); 12 012 0x12 n=3; i=12; j=10; k=18;
n = scanf("%[0123456789]", str); 123abc n=1; str=”123”;
n = scanf("%[0123456789]", str); abc123 n=0; str的值不变;
n = scanf("%[^0123456789]", str); abc123 n=1; str=”abc”;
n = scanf("%*d%d%n", &i, &j); 10 20 30 n=1; i=20; j=5;

<math.h> 中增加许多类型、宏和函数

通用字符名

可以用两种方式书写通用字符名(\udddd和\Udddddddd),每个d都是一个十六进制的数字。

UCS的码值可以在www.unicode.org/charts/找到。

支持宽字符的 <wchar.h><wctype.h> 函数库

<stdio.h><wchar.h> 中支持vscanf族函数

新增 <stdint.h> 整数类型

新增 <inttypes.h> 整数类型的格式

新增 <complex.h> 复数算术运算

新增 <tgmath.h> 泛型数学

<tgmath.h> 提供了带参数的宏,宏的名字与 <math.h><complex.h> 中的函数名相匹配。这些泛型宏(type-generic macro)可以检测参数的类型,然后调用 <math.h><complex.h> 中相对应的函数。

比如:sqrt函数不仅有3种复数版本(csqrt、csqrtf和csqrtl),还有double(sqrt)、float(sqrtf)以及long double版本(sqrtl)。使用 <tgmath.h> 后,程序员可以直接使用sqrt,而不用担心需要的到底是哪个版本:根据参数x类型的不同,函数调用sqrt(x)有可能是6个版本sqrt中的任何一个。

顺便提一下,<tgmath.h> 中包含了 <math.h><complex.h>

新增 <fenv.h> 浮点环境

IEEE标准754在表示浮点数时使用最广泛。(C99标准把IEEE 754成为IEC 60559)。<fenv.h> 的目的是使程序可以访问IEEE标准指定的浮点状态标志和控制模式。


文章作者: Kiba Amor
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 Kiba Amor !
  目录