现代 C99, C11 标准下的 C 语言编程
一、摘要
一直以来,我们所学习的 C 语言大多是 ANSI-C 标准,也就是后来被标准化的 C89 标准。在 1999 年发布的 C99 和 2011 年发布的 C11 标准在此之上,引入了许多新的特性,也解决了许多问题。因此,随着标准的发布,我们的 C 语言规范和写法也要发生相应的变化。
C++ 同样也发布了 C++99,C++11,C++14 甚至 C++17 规范。从变化上看,C++11 规范之后的 C++ 语言已经焕然一新,引入了大量非常现代化的特性。C 语言规范的最大的变化则发生在 C99 规范之中。其后的 C11 虽然也有一些特性,但更多的算是为了于 C++ 同步而引入的新特性。
目前的 GCC 和 Clang 编译器都已经完整支持 C99 和 C11 的特性,默认都是支持 C11 规范。如果需要显式指定的时候,则在编译时加入 -std=c99
或者 -std=c11
即可。
本文将介绍这两个协议下带来的新特性,和我们新的编码习惯的变化。
二、新的基本数据类型规范
在 C99 规范中,有着大量对于新的数据类型的定义和补充。这是非常有必要的,原先的 int,long 等变量基本类型在不同架构的机器上,会有不同的长度,往往会导致不可预期的问题。64 位数值、布尔类型和复数类型的缺失、以及 Unicode 的缺失也阻碍了 C 语言在现代的进一步发展。因此,C99 类型中带来了大量编码类型的变化。
2.1 数值类型
我们经常因为数据类型在不同架构机器上的不同表现,而感到困扰。因此在 C99 规范中,引入了标准的固定长度数据类型的规范,并且引入了 64 位数据类型的支持。在 32 位机器上,你可能需要使用 long long 来建立一个 64 位的数据类型。而在 64 位机器上,long 即表示 64 位数据类型。
在 C99 中,引入了新的头文件 <stdint.h>
在这个头文件中,同一规范了不同长度数据类型的定义:
- int8_t, int16_t, int32_t, int64_t 分别代表 8, 16, 32, 64 位的整型
- uint8_t, uint16_t, uint32_t, uint64_t 分别代表 8, 16, 32, 64 位的无符号整数
- float, double 分别代表了 32, 64 位浮点数
因此,推荐使用引入 #include <stdint.h>
,并使用这些固定长度的数据类型,来代替传统的 int, short, long 等。
有时候,如果需要使用原生机器字长的数值类型,以实现最佳性能时,应当使用 intptr_t
类型,它在 32 位机器上等价于 int32_t
而在 64 位机器上等价于 int64_t
。无符号的 uintptr_t
也是如此。
此外,利用 sizeof
返回的类型 size_t 也是这样的。其在不同架构的机器上字长不同。
如果需要确保使用长度最长的数值类型。可以使用类型 intmax_t
和 uintmax_t
作为最大的容器,来确保类型转换时,没有损失和溢出。
2.2 字符类型,宽字节和多字节
传统 C89 标准只支持 ascii 码,而你可能发现 C 语言其已经具有了处理 Unicode 字符集的能力。这最早在 95 年引入,并成为 C99 标准的一部分。
在 C99 中引入了 <wchar.h>
和 <wctype.h>
两个头文件,用于处理宽字节。传统的 char 只有 8 位数,因此原生只能容纳所有的 ascii 和 扩展 ascii 字符。而 wchar_t 类型则是 32 位或 16 位,可以容纳所有的 Unicode 字符。但是这只在用于字符统计等需求时,才需要使用到宽字符类型,因此不常见其使用。
而 UTF-8, UTF-16, UTF-32 等字符编码格式都是用不同的编码方式来实现 Unicode 字符集。因此,宽字符类型可以直接容纳 UTF-32 格式的字符,也可以正确的用于统计字数。而 UTF-8 这种通用的字长无关的编码可以直接放在 char 类型的数组中,也可以直接被系统所读取。唯一的问题在于 sizeof 获取的长度并不是真正的字数。
在 C11 中 <uchar.h>
头文件对字符集的 Unicode 支持进一步扩充。支持定义如下字符串:
char s1[] = "你好"; // 标准支持
char s2[] = u8"你好"; // utf-8 编码
char16_t s3[] = u"你好"; // 16 位宽字符
char32_t s4[] = U"你好"; // 32 位宽字符
wchar_t s5[] = L"你好"; // 根据本机架构决定宽字符长度
2.3 布尔类型
在 C99 规范中引入了新的布尔类型,再也不需要要自行定义了。头文件 <stdbool.h>
包括其实现。布尔类型的关键字是 _Bool
,也有一个宏定义为 bool
,取值为 true
和 false
。
因此我们可以这样使用了:
bool found = true;
bool empty = false;
bool is_foo();
2.4 复数类型
C99 中引入了复数类型,这意味着我们可以直接表示复数或者平面中的一个点。其声明在 <complex.h>
头文件中。分别有三种类型的复数类型:
- double complex
- float complex
- long double complex
有宏 _Complex_I
或者 I
来声明一个复数。此外还有一些常用的复数函数,例如:
ccos, csin, ccos, csinh 等三角函数和双曲函数
cexp, clog, cabs, cpow, csqrt 等数学函数
carg, cimag, creal 获取象限角、虚数部分、实数部分等函数
下面是一个简单的例子:
double complex a = 1.0 + 2.0 * I;
double complex b = 5.0 + 4.0 * I;
a *= b;
a = csin(b);
a = creal(b);
2.5 指针类型
通产需要使用 void* 等来声明一个指针,或者需要使用强制类型转换为 long 来进行运算。在 <stdint.h>
中定义了专门的指针类型: unitptr_t
和在 <stddef.h>
终端指针差值类型 ptrdiff_t
。
ptrdiff_t diff = (uintptr_t)ptrOld -
(uintptr_t)ptrNew;
三、数组和结构体
在 C99 和 C11 中引入了新的特性,可以使我们更加灵活地使用数组和结构体以及联合体。
3.1 可变长数组(VLA)
在 C99 之前,如果数组的长度在编译时无法确定,遇到这种情况,我们通常只有两种做法:一是申请一个足够长度数组(需要对长度进行估计,否则很可能会溢出),一个是使用 malloc 在堆中分配数组(但是需要维护,需要释放等)。
在 C99 之后,引入了可变长数组(VLA)的概念,可以实现数组的长度在编译时不一定需要确定。这样可以实现在运行时确定数组长度,而作用于结束后自动释放。
比如:
int n;
int array[n];
但是这种用法也有一些限制,比如:
- n 和 array 必须位于同一个文件作用域
- 不可以用于 typedef
- 不可使用在结构体中
- 不可以申明为 static 变量
- 不可以申明为 extern 变量或 extern 变量的指针
3.2 灵活的初始化
在 C99 中带来了非常灵活的初始化数组和结构体的方法,我们不在需要对完整的数组或者结构体进行初始化,可以只对其一部分进行初始化。比如:
uint32_t a1[64] = {0}; // 全部填充 0
struct thing {
uint64_t index;
uint32_t counter;
};
struct thing t1 = {0}; // 填充 0
uint32_t a2[10] = {[2] = 1, [4] = 6}; //对数组部分位置赋值。
struct thing t2 = {.index = 3} // 结构体部分位置赋值
struct thing t3 = {counter: 0}; // 也可以使用类似 Python 的形式
3.3 alignof
在 C11 标准中,定义了新的 alignof 运算符,和 sizeof 相对应。在头文件 <stdalign.h>
中申明。定义了一个对象的对齐要求。
alignof(char); // 1
alignof(struct {char c; int n;}; // 4
alignof(float[1024]); // 4
四、宏定义和预编译
C99 在宏定义部分有一些新的变化,最常用的就是 Pragma 运算符和可变宏的引入。
4.1 Pragma 运算符
C99 中引入。主要有 _Pragma
运算符和 #pragma
宏。是用于指定编译时的行为,比如:
# 编译时显示消息
#pragma message(“_X86 macro activated!”)
# 注释
#pragma comment(…)
此外,#pragma once
使用的非常多,这是一个非标准但是被普遍实现的特性(Clang, GCC, Visual C 等主流编译器均支持)。用于指出该头文件只引入一次。和下面语句等效:
#ifndef xxx
#def xxx
#endif
4.2可变长宏
定义宏的时候可以引入不定长度的输入参数,具体用法不在列出。
五、兼容 C++ 的改变
这里是一些引入的 C++ 中的特性。
5.1 单行注释
在 C99 中引入了单行注释 //
这个在 C++ 中早已实现,也被较多编译器所支持。在此被列入了标准。
5.2 任意位置申明
早前的 C 语言申明语句一定位于语句块的最开头。而 C99 之后打破了这种约定,可以在任意位置申明语句。因此下面的内联计数器也可以直接使用:
for(int i = 0; i < 10 ; ++i)
{
//do something.
}
六、堆的分配
在《how to c in 2016》中指出,应当尽可能使用 calloc 函数代替 malloc 函数,因为其分配空间时会自动初始化为 0,比 malloc 分配后再使用 memset 高效。
此外也建议不再使用 memset 函数。
函数原型是: calloc(object count, size per object)
七、几个关键字
7.1 restrict 关键字
这个关键字是函数的输入参数为指针时候的可选关键字。比如下面的两个 restrict 表明了 s1 和 s2 不可以指向同一地址。用于防止未定义行为的发生。
void *memcpy(void *restrict s1, const void *restrict s2,size_t size);
7.2 inline 关键字
用于定义函数的关键字。使得函数在编译时在被调用位置直接展开,因此可以极大的提高效率。它比宏的好处在于可以可读性好,也有编译时类型检查。
7.3 _Noreturn
修饰符
在 C11 中定义,用于表示函数无返回值,防止未定义行为发生。在 <stdnoreturn.h>
中定义了 noreturn 宏:
八、输出和输入
8.1 gets_s
在 C11 中定义,一个安全的读取字符串函数,取代了危险的 gets 函数。
char *gets_s( char *str, rsize_t n );
8.2 fopen “x” 模式
fopen()
的新的打开、创建模式"x"。用于表明其对于文件的独占。常常用于文件锁中。
九、C11 的轻量级泛型支持
从 C11 开始,引入了对于泛型的简单支持。引入了 _Gerneric
关键字。其作用是把一族相似功能的函数聚合成一个对外接口。比如:
_Generic((x), int:abs, float:fabsf, double:fabs)
首先接受参数 x,而后根据 x 的类型匹配不同的函数来分别调用。此时可以使用一个 #define
来完成聚合,比如:
#define GENERAL_ABS(x) _Generic((x),int:abs,float:fabsf,double:fabs)(x)
GENERAL_ABS(1);
GENERAL_ABS(1.1);
在此基础上,C11 提供了基于泛型的数学函数库 <tgmath.h>
其中的函数全部是在 <math.h>
和 <complex.h>
中定义的数学函数所聚合而成的。因此可以无需再根据输入参数的不同而选用不同的函数了。
十、C11 线程
在 C11 中,引入了轻量级线程的标准实现。在 <thread.h>
中,有主要线程使用的函数声明以及互斥等的声明,比如线程创建函数 thrd_create
, 线程等待合并函数 thrd_join
等。
此外在 <stdatomic.h>
头文件中引入了原子类型的相关定义。其中
_Atomic
类型修饰符可以用于申明一个类型的相关读写操作是原子的。使用这个申明可以避免一些并发引起的冲突。
十一、总结
在这篇博客中,主要简洁地介绍了一些 C99 和 C11 中引入 C 语言的新特性和用法。其他诸如变量长度限制、递归限制等诸多细节也并没有加以介绍。从结果上来看,这些特性的引入使得 C 语言程序的现代化有所提升,更加安全、更加通用、也更加简洁。因此只算是一个引子,具体的诸多用法还要在实际编写中加以体会。
本人保留对侵权者及其全家发动因果律武器的权利
版权提醒
如无特殊申明,本站所有文章均是本人原创。转载请务必附上原文链接:https://www.elliot98.top/post/tech/modern_c_standard/。
如有其它需要,请邮件联系!版权所有,违者必究!