主要是备忘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 int
和unsigned long long int
,大小至少64位宽。同时,以LL
或ll
(所有字母的大小写要一致)结尾的整数常量是long long int
类型的,以ULL
或ull
(所有字母的大小写要一致)结尾的整数常量是unsigned long long int
类型的。
除了标准的整数类型外,C99标准还允许在具体实现时定义扩展的数类整数
(包括有符号的和无符号的)。比如:128位的整数类型。
隐式转换的变化
因为C99中增加了一些类型(_Bool、long long类型、扩展的整数类型和复数类型)。新的隐式转换规则略有变化(这里忽略了扩展整数类型和枚举类型):
- long long int、unsigned long long int
- long int、unsigned long int
- int、unsigned int
- short int、unsigned short int
- char、signed char、unsigned char(这里要注意char类型和signed char类型是不同的类型)
- _Bool
C99用整数提升(integer promotion)取代了C89中的整值提升(integral promotion),可以将任何等级低于int和unsigned int的类型转换为int(只要该类型的所有值都可以用int类型表示)或unsigned int。
和C89一样类似,C99中执行常用的算术转换的规则可以划分为两种情况:
任一操作数的类型是浮点数类型的情况。
只要两个操作数都不是复数型,规则如下(下面的内容摘录自C程序设计语言(第2版_新版)):- 如果任何一个操作数为long double类型,将另一个操作数转换为long double类型,过程结束。
- 如果任何一个操作数为double类型,将另一个操作数转换为double类型,过程结束。
- 如果任何一个操作数为float类型,将另一个操作数转换为float类型,过程结束。
两个操作数的类型都不是浮点类型的情况。
首先对两个操作数进行整数提升。如果这时两个操作数的类型相同,过程结束。否则,依次尝试下面的规则,一旦遇到可应用的规则就不再考虑别的规则:- 如果两个操作数都是有符号型或都是无符号型,将整数转换等级低的操作数转换为等级较高的操作数的类型。
- 如果无符号操作数的等级
高于或等于
有符号操作数的等级,将有符号操作数转换为无符号操作数的类型。 - 如果有符号操作数类型可以表示无符号操作数类型的所有值,将无符号操作数转换为有符号操作数的类型。
- 否则,将两个操作数都转换为与有符号操作数的类型相对应的无符号类型。
另外,所有算术类型都可以转换为_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 m
和int 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标准指定的浮点状态标志和控制模式。