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

原文链接:Elliot's Blog - 技术摘要| 现代 C99, C11 标准下的 C 语言编程

现代 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 来声明一个复数。此外还有一些常用的复数函数,例如:

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C99,C11和C17都是C语言标准规范。每个标准都对C语言的语法,数据类型,函数库和编译器行为做出了规定。 C99是C语言的第三个标准,于1999年发布。它引入了一些新的功能,例如可变长度数组,复合文字,以及对于实数和复数的支持。C99还增加了新的编译指令和预处理器宏,以提高代码的可读性和可移植性。C99标准的目的是提供更好的编程工具和更强的类型检查,以便开发人员能够更容易地编写健壮的代码。 C11是C语言的第四个标准,于2011年发布。它在C99的基础上进一步扩展了语言的功能和库支持。C11增加了对多线程编程的原生支持,并引入了原子操作和线程局部存储等新的特性。此外,C11还对内存模型和并发性做出了明确的规定,并提供了一些新的预定义宏来检测编译器的特性和支持程度。 C17是C语言的第五个标准,于2018年发布。它修复了C11中的一些漏洞,并进行了一些小的改进。C17引入了一些新的库函数,并对一些现有的库函数进行了修改,以提供更好的性能和功能。此外,C17还对于对于内存模型和类型的处理做出了一些调整,以提高编程的可靠性和可移植性。 总结来说,C99,C11和C17都是C语言标准规范,它们分别于1999年,2011年和2018年发布。每个标准都引入了新的功能和改进,以提供更好的编程工具和更强的类型检查。开发人员可以参考这些标准来编写可靠且具有可移植性的C语言代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值