81、C99与C11标准新特性解析

C99与C11标准新特性解析

1. C99标准的其他特性
1.1 restrict关键字

restrict关键字用于声明受限指针。当一个指针需要对一块内存区域具有独占访问权时,就可以声明为受限指针。通过受限指针访问的对象,不能被其他指针访问,除非这些指针的值是从该受限指针的值派生而来的。例如:

int *restrict ptr;
void *memcpy(void *restrict s1, const void *restrict s2, size_t n);

受限指针能让编译器优化程序访问内存的方式。以标准库函数memcpy为例,C99标准规定它不能用于重叠内存区域的复制。使用受限指针,编译器能识别这一要求,并通过一次复制多个字节来优化复制过程,提高效率。但如果错误地将一个指针声明为受限指针,而另一个指针也指向同一内存区域,就会导致未定义行为。

1.2 可靠的整数除法

在C99之前的编译器中,整数除法的行为在不同实现中有所不同。有些实现会将负商向负无穷方向舍入,而有些则向零舍入。当其中一个整数操作数为负数时,会得到不同的结果。例如,计算 -28 除以 5,精确结果是 -5.6。如果向零舍入,整数结果是 -5;如果向负无穷舍入,整数结果是 -6。C99消除了这种歧义,总是将商向零舍入来执行整数除法(和整数取模),使得所有符合C99标准的平台对整数除法的处理方式一致。

1.3 灵活数组成员

C99允许将一个未指定长度的数组声明为结构体的最后一个成员。例如:

struct s {
    int arraySize;
    int array[];
};

灵活数组成员通过指定空方括号 [] 来声明。要分配一个包含灵活数组成员的结构体,可以使用如下代码:

int desiredSize = 5;
struct s *ptr;
ptr = malloc(sizeof(struct s) + sizeof(int) * desiredSize);

sizeof 运算符会忽略灵活数组成员, sizeof(struct s) 计算的是结构体 s 中除灵活数组外所有成员的大小。额外分配的 sizeof(int) * desiredSize 空间就是灵活数组的大小。使用灵活数组成员有很多限制:
- 只能作为结构体的最后一个成员声明,每个结构体最多只能有一个灵活数组成员。
- 不能是结构体的唯一成员,结构体必须有一个或多个固定成员。
- 包含灵活数组成员的结构体不能作为另一个结构体的成员。
- 包含灵活数组成员的结构体不能进行静态初始化,必须动态分配。

1.4 聚合初始化约束的放宽

在C99中,不再要求像数组、结构体和联合体这样的聚合体必须用常量表达式进行初始化。这使得可以使用更简洁的初始化列表,而不是用多个单独的语句来初始化聚合体的成员。

1.5 类型通用数学

C99引入了 <tgmath.h> 头文件,它为 <math.h> 中的许多数学函数提供了类型通用宏。例如,包含 <tgmath.h> 后:
- 如果 x float 类型, sin(x) 会调用 sinf sin float 版本)。
- 如果 x double 类型, sin(x) 会调用 sin (接受 double 参数)。
- 如果 x long double 类型, sin(x) 会调用 sinl sin long double 版本)。
- 如果 x 是复数, sin(x) 会调用适合该复数类型的 sin 函数版本( csin csinf csinl )。

1.6 内联函数

C99允许像C++一样,在函数声明前加上 inline 关键字来声明内联函数,例如:

inline void randomFunction();

从用户的角度来看,这对程序的逻辑没有影响,但可以提高性能。函数调用会消耗时间,当一个函数被声明为内联函数时,程序可能不再调用该函数。编译器可以选择将对内联函数的每次调用替换为该函数的代码体副本,这能提高运行时性能,但可能会增加程序的大小。因此,只有当函数简短且被频繁调用时,才应该将其声明为内联函数。 inline 声明只是给编译器的建议,编译器可以选择忽略它。

1.7 无表达式返回

C99对函数返回增加了更严格的限制。在返回非 void 值的函数中,不再允许使用 return; 语句。在C99之前的编译器中,这是允许的,但如果调用者尝试使用该函数的返回值,会导致未定义行为。同样,在不返回值的函数中,也不再允许返回值,例如 void returnInt() {return 1;} 这样的语句不再被允许。C99要求兼容的编译器在上述情况下产生警告消息或编译错误。

1.8 __func__预定义标识符

__func__ 预定义标识符类似于 __FILE__ __LINE__ 预处理器宏,它是一个保存当前函数名称的字符串。与 __FILE__ 不同的是,它不是字符串字面量,而是一个真正的变量,因此不能与其他字面量进行拼接。这是因为字符串字面量的拼接是在预处理阶段进行的,而预处理器对C语言的语义并不了解。

1.9 va_copy宏

C99添加了 va_copy 宏,它接受两个 va_list 参数,并将第二个参数复制到第一个参数中。这允许对变长参数列表进行多次遍历,而无需每次都从头开始。

2. C11标准的新特性
2.1 新的C11头文件

C11引入了一些新的标准库头文件,如下表所示:
| 标准库头文件 | 说明 |
| ---- | ---- |
| <stdalign.h> | 提供类型对齐控制 |
| <stdatomic.h> | 提供对对象的不间断访问,用于多线程 |
| <stdnoreturn.h> | 非返回函数 |
| <threads.h> | 线程库 |
| <uchar.h> | UTF - 16和UTF - 32字符实用工具 |

2.2 多线程支持

多线程是C11标准中最重要的改进之一。尽管多线程技术已经存在了几十年,但由于多核系统的普及,对它的兴趣正在迅速增长。如今,大多数新处理器至少有两个核心,三核、四核和八核处理器也很常见,核心数量还会继续增加。在多核系统中,硬件可以让多个核心同时处理任务的不同部分,从而使任务(和程序)更快完成。要充分利用多核架构,就需要编写多线程应用程序。当一个程序将任务拆分成多个独立的线程时,多核系统可以并行运行这些线程。

之前,C语言的多线程库是非标准的、特定于平台的扩展。C程序员通常希望他们的代码能够在不同平台上移植,这正是标准化多线程的一个关键优势。C11的 <threads.h> 头文件声明了新的(可选)多线程功能,使你能够编写更具可移植性的多线程C代码。目前,很少有C编译器提供C11多线程支持。

下面通过两个程序来展示多线程在多核系统上的性能优势:
- 顺序执行两个计算密集型任务

// Fig. E.11: figE_11.c
// Fibonacci calculations performed sequentially
#include <stdio.h>
#include <time.h>

unsigned long long int fibonacci(unsigned int n); // function prototype

// function main begins program execution
int main(void)
{
    puts("Sequential calls to fibonacci(50) and fibonacci(49)");

    // calculate fibonacci value for 50
    time_t startTime1 = time(NULL);
    puts("Calculating fibonacci(50)");
    unsigned long long int result1 = fibonacci(50);
    time_t endTime1 = time(NULL);

    printf("fibonacci(%u) = %llu\n", 50, result1);
    printf("Calculation time = %f minutes\n\n", 
           difftime(endTime1, startTime1) / 60.0);

    time_t startTime2 = time(NULL);
    puts("Calculating fibonacci(49)");
    unsigned long long int result2 = fibonacci(49);
    time_t endTime2 = time(NULL);

    printf("fibonacci(%u) = %llu\n", 49, result2);
    printf("Calculation time = %f minutes\n\n", 
           difftime(endTime2, startTime2) / 60.0);

    printf("Total calculation time = %f minutes\n", 
           difftime(endTime2, startTime1) / 60.0);
} 

// Recursively calculates fibonacci numbers
unsigned long long int fibonacci(unsigned int n)   
{                                                    
    // base case                                      
    if (0 == n || 1 == n) {                         
        return n;                                      
    }                                        
    else { // recursive step                          
        return fibonacci(n - 1) + fibonacci(n - 2);
    }                                      
} 

该程序顺序计算 fibonacci(50) fibonacci(49) ,并记录每个计算的时间和总计算时间。

  • 多线程执行两个计算密集型任务
// Fig. E.12: figE_12.c
// Fibonacci calculations performed in separate threads
#include <stdio.h>
#include <threads.h>
#include <time.h>

#define NUMBER_OF_THREADS 2

int startFibonacci(void *nPtr);
unsigned long long int fibonacci(unsigned int n); 

typedef struct ThreadData {
    time_t startTime; // time thread starts processing
    time_t endTime;   // time thread finishes processing
    unsigned int number; // fibonacci number to calculate
} ThreadData; // end struct ThreadData

int main(void) 
{
    // data passed to the threads; uses designated initializers
    ThreadData data[NUMBER_OF_THREADS] = 
    { [0] = {.number = 50},   
      [1] = {.number = 49}};

    // each thread needs a thread identifier of type thrd_t
    thrd_t threads[NUMBER_OF_THREADS];

    puts("fibonacci(50) and fibonacci(49) in separate threads");

    // create and start the threads
    for (size_t i = 0; i < NUMBER_OF_THREADS; ++i) {
        printf("Starting thread to calculate fibonacci(%d)\n", 
               data[i].number);

        // create a thread and check whether creation was successful
        if (thrd_create(&threads[i], startFibonacci, &data[i]) !=
            thrd_success) {                                       
            puts("Failed to create thread");
        } 
    } 

    // wait for each of the calculations to complete
    for (size_t i = 0; i < NUMBER_OF_THREADS; ++i)
        thrd_join(threads[i], NULL);

    // determine time that first thread started
    time_t startTime = (data[0].startTime < data[1].startTime) ? 
        data[0].startTime : data[1].startTime;

    // determine time that last thread terminated
    time_t endTime = (data[0].endTime > data[1].endTime) ? 
        data[0].endTime : data[1].endTime;

    // display total time for calculations
    printf("Total calculation time = %f minutes\n", 
           difftime(endTime, startTime) / 60.0);
} 

// Called by a thread to begin recursive Fibonacci calculation
int startFibonacci(void *ptr)
{
    // cast ptr to ThreadData * so we can access arguments
    ThreadData *dataPtr = (ThreadData *) ptr;

    dataPtr->startTime = time(NULL); // time before calculation
    printf("Calculating fibonacci(%d)\n", dataPtr->number);
    printf("fibonacci(%d) = %lld\n", 
           dataPtr->number, 
           fibonacci(dataPtr->number));
    dataPtr->endTime = time(NULL); // time after calculation
    printf("Calculation time = %f minutes\n\n", 
           difftime(dataPtr->endTime, dataPtr->startTime) / 60.0);
    return thrd_success; 
} 

// Recursively calculates fibonacci numbers
unsigned long long int fibonacci(unsigned int n)      
{                                                         
    // base case                                           
    if (0 == n || 1 == n) {                               
        return n;                                            
    }                                            
    else { // recursive step                            
        return fibonacci(n - 1) + fibonacci(n - 2);        
    }                                         
} 

该程序将 fibonacci(50) fibonacci(49) 的计算分别放在两个线程中执行,并记录总计算时间。

通过在单核和双核Windows计算机上运行这两个程序,可以看到多线程程序在多核系统上的执行时间明显缩短,但在单核系统上,由于多线程本身的开销,执行时间可能会比顺序执行更长。

创建和执行线程的步骤如下:
1. 创建一个 thrd_t 类型的数组来存储线程ID。
2. 调用 thrd_create 函数创建线程,该函数接受三个参数:
- 一个 thrd_t 指针,用于存储线程的ID。
- 一个指向函数的指针,指定线程要执行的任务。该函数必须返回一个 int 类型的值,并接受一个 void 指针作为参数。
- 一个 void 指针,指向要传递给第二个参数中函数的参数。
3. 调用 thrd_join 函数等待线程执行完成。该函数接受两个参数:
- 一个 thrd_t 类型的线程ID。
- 一个 int 指针,用于存储线程返回的状态。

函数 startFibonacci 用于指定线程要执行的任务,即调用 fibonacci 函数进行递归计算,记录计算时间,并显示计算结果和计算时间。当 startFibonacci 函数返回线程的状态(如 thrd_success )时,线程终止。

除了上述基本的多线程支持外,C11还包括其他特性,如 _Atomic 变量和原子操作、线程局部存储、条件和互斥锁等。

C99与C11标准新特性解析

3. C99和C11特性总结与流程梳理

为了更清晰地理解C99和C11的新特性,我们对上述内容进行总结,并梳理一些关键操作的流程。

3.1 C99和C11特性总结表格
标准 特性 描述
C99 restrict关键字 用于声明受限指针,使编译器优化内存访问,但使用不当会导致未定义行为
C99 可靠的整数除法 统一整数除法和取模的舍入规则,向零舍入
C99 灵活数组成员 允许在结构体最后声明未指定长度的数组,但使用有诸多限制
C99 聚合初始化约束的放宽 聚合体初始化不再要求使用常量表达式
C99 类型通用数学 <tgmath.h> 头文件为 <math.h> 中的数学函数提供类型通用宏
C99 内联函数 通过 inline 关键字声明,可提高性能,但可能增加程序大小
C99 无表达式返回 对函数返回增加严格限制,避免未定义行为
C99 __func__预定义标识符 保存当前函数名称的字符串变量
C99 va_copy宏 用于复制变长参数列表,方便多次遍历
C11 新的C11头文件 引入 <stdalign.h> <stdatomic.h> 等头文件,提供新功能
C11 多线程支持 <threads.h> 头文件提供标准化多线程功能,提高代码可移植性
3.2 多线程创建与执行流程

下面是使用C11进行多线程编程时,创建和执行线程的详细流程:

graph TD
    A[开始] --> B[定义线程数据结构体]
    B --> C[创建thrd_t类型数组存储线程ID]
    C --> D[初始化线程数据]
    D --> E[循环调用thrd_create创建线程]
    E --> F{线程创建成功?}
    F -- 是 --> G[继续创建下一个线程]
    F -- 否 --> H[输出创建失败信息]
    G --> I{所有线程创建完成?}
    I -- 否 --> E
    I -- 是 --> J[循环调用thrd_join等待线程完成]
    J --> K[计算总执行时间]
    K --> L[输出总执行时间]
    L --> M[结束]

4. 关键特性的使用建议与注意事项

4.1 C99特性使用建议
  • restrict关键字 :在确定指针具有独占访问权时使用,可提高编译器优化能力,但要确保不会出现多个指针指向同一区域的情况,否则会导致未定义行为。
  • 内联函数 :仅对简短且频繁调用的函数使用 inline 声明,避免对长函数使用,以免增加程序大小。同时要明白 inline 只是给编译器的建议,编译器可能会忽略。
  • 灵活数组成员 :在需要动态调整数组大小的结构体中使用,但要严格遵守其使用限制,如只能作为结构体最后一个成员、不能静态初始化等。
4.2 C11多线程使用注意事项
  • 多核与单核性能差异 :在多核系统上,多线程可以充分发挥硬件优势,提高程序执行效率。但在单核系统上,由于多线程的开销,执行时间可能会比顺序执行更长。因此,在编写多线程程序时,要根据目标系统的硬件情况进行性能评估。
  • 线程同步与互斥 :当多个线程访问共享资源时,可能会出现数据竞争和不一致的问题。C11提供了原子操作、条件和互斥锁等机制来解决这些问题,在编写多线程程序时要合理使用这些机制,确保线程安全。

5. 总结

C99和C11标准为C语言带来了许多重要的新特性,这些特性不仅提高了代码的性能和可维护性,还增强了代码的可移植性。C99的特性如 restrict 关键字、可靠的整数除法等,为编译器优化和代码正确性提供了支持。C11的多线程支持则顺应了多核系统普及的趋势,使C语言能够更好地利用硬件资源。

在实际编程中,我们应该根据具体需求合理使用这些新特性。对于C99的特性,要注意其使用条件和限制,避免出现未定义行为。对于C11的多线程特性,要充分考虑多核和单核系统的性能差异,以及线程同步和互斥的问题。通过合理运用这些特性,我们可以编写出更加高效、安全和可移植的C语言程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值