C11 标准下的 C 语言编程

news/2024/11/20 17:26:45/

一、摘要

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


http://www.ppmy.cn/news/615652.html

相关文章

Visual C++ 的 fatal error C1001

如果你是一位老资格的 Visual C 用户&#xff0c;那一定会对 Visual C 的 fatal error C1001 不会陌生。如果你在用 VC6 编一个规模较大的工程&#xff0c;如果你在用 VC6 编译一个用到了模板的工程&#xff0c;那恭喜你&#xff0c;这个错误会时不时冒地出来和你打招呼&#x…

C++ 错误error C2011:类型重定义的解决办法

当在.h文件中定义一个枚举&#xff08;enum&#xff09;类型时&#xff0c;由于这个.h的头文件&#xff0c;会被其他好几个.h文件引用&#xff0c;所以在编译的时候&#xff0c;会出现error C2011:类型重定义的报错。 这样的错误一度让楼主怀疑人生啊。 其解决办法是&#xf…

五、菜单管理

云尚办公系统&#xff1a;菜单管理 B站直达【为尚硅谷点赞】: https://www.bilibili.com/video/BV1Ya411S7aT 本博文以课程相关为主发布&#xff0c;并且融入了自己的一些看法以及对学习过程中遇见的问题给出相关的解决方法。一起学习一起进步&#xff01;&#xff01;&#x…

【FoxMail】无法登录, 一直让创建问题.

问题原因: 每次打开FoxMail的时候都显示创建, 创建完成之后,再次打开FoxMail还是显示创建. 出现这个现象的原因是因为使用的公司电脑, 我登录电脑的账号并非超级管理员, 因此导致无法登录. 解决方式: 右键快捷图标, 点击"以管理员身份运行", 即可直接登录进去. 但是…

MAC 系统下使用邮件客户端登录腾讯企业邮箱失败问题

mac邮件客户端版本&#xff1a;版本 12.4 (3445.104.14) 第一步&#xff1a;选择添加账户中其他选项 第二步&#xff1a; 类型设置&#xff1a;选择IMAP 收件服务器设置为&#xff1a;imap.exmail.qq.com 发件服务器设置为&#xff1a;smtp.exmail.qq.com 官方文档&#x…

mac 邮箱客户端之腾讯企业邮箱设置 无法验证账号或密码

1.首先我们要先登录腾讯企业邮箱网页版点击 ”设置“——“账号” 2.找到客户端专用密码——生成新密码&#xff08;也就是授权码&#xff09; 3.去添加用户 4.输入用户名/密码&#xff08;上面获得的授权码&#xff09;&#xff0c;点击登录他还是会提示 无法验证账号或密码…

邮箱无法登陆的解决方法

regsvr32 jscript.dll 今天去找同学玩,顺便帮他看机器的奇怪问题,用ie无法登陆邮箱。。。。。但是gmail可用。 下载firefox后也可登陆邮箱,但是依旧不可使用ie登陆。 具体现象为 输入帐号密码后页面空白,状态栏显示完毕。 知之为知之,不知google之为解决疑难问题的好方法,…

outlook添加账号发送服务器,如何正确在Outlook登陆QQ邮箱账户-outlook设置

工作中为了简单方便收发邮件&#xff0c;可以使用outbook收发邮件&#xff0c;而QQ邮件又是比较常用而且难设置的邮箱QQ邮箱官网里的方法都不行&#xff0c;还有人说用不了QQ邮箱&#xff0c;其实不然。 很多小伙伴在工作的时候可能要用到outlook&#xff0c;outlook配合office…