现代 C99, C11 标准下的 C 语言编程

2019-04-13 675 Words C Linux

现代 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_tuintmax_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 ,取值为 truefalse

因此我们可以这样使用了:

bool found = true;
bool empty = false;
bool is_foo();

2.4 复数类型

C99 中引入了复数类型,这意味着我们可以直接表示复数或者平面中的一个点。其声明在 <complex.h> 头文件中。分别有三种类型的复数类型:

  • double complex
  • float complex
  • long double complex

有宏 _Complex_I 或者 I 来声明一个复数。此外还有一些常用的复数函数,例如:

  1. ccos, csin, ccos, csinh 等三角函数和双曲函数

  2. cexp, clog, cabs, cpow, csqrt 等数学函数

  3. 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 语言程序的现代化有所提升,更加安全、更加通用、也更加简洁。因此只算是一个引子,具体的诸多用法还要在实际编写中加以体会。