C语言篇:复合字面量

10 阅读4分钟

前言

复合字面量是C语言用来创建匿名对象的一种方式,在C99中引入。它实际上是一种语法糖,就算没有它也不影响C语言的表达能力。它常用于函数的参数传递,当程序员不想为只使用一次的对象创建标识符时,即可使用该匿名对象。

因为它是匿名的,没有关联的标识符,所以针对标识符的申明、定义、作用域、名字空间、链接类型等都跟它无关。唯一跟它相关的是对象的生存期

它是C语言特有的功能,C++不支持复合字面量。

语法

复合字面量的语法是:

(类型) {初始化列表}

复合字面量的类型不能是VLA。

这实际上是一个表达式,它的值就是对应匿名对象的值,并且是左值。

复合字面量的语法和显式类型转换非常相似,二者的区别是有无初始化列表{}。而且复合字面量是左值,显式类型转换不是左值。

int num = 0;
int *p1 = &(int) { num };   // 复合字面量,左值,可以取地址
int *p2 = &(int)num;        // 显式类型转换,非左值,不能取地址,编译错误

初始化

初始化列表的语法在复合字面量中几乎都适用。

数组初始化,包括省略数组大小、指定初始化、字符串字面量初始化等:

int *p = (int[3]) { 0 };        
int *p = (int[]) { 1, 2, 3 };
int *p = (int[5]) { [2] = 5 };
char *s = (char[]) { "hello" };

结构体初始化,包括指定初始化等:

struct S {
    int a;
    double b;
};

struct S *p = &(struct S) { 1, 0.5 };
struct S *p = &(struct S) { .b = 0.5 };

生存期

如果复合字面量出现在文件范围内,则对应的匿名对象默认具有静态生存期,程序启动时创建,程序退出时销毁;如果该表达式出现在块内,则对应的匿名对象默认具有自动生存期,程序执行进入该块时创建,退出该块时销毁。

例子1:

#include <stdio.h>

int *p = &(int) { 0 };

int main(void) {
    printf("%d\n", *p); // print 0
    *p = 5;;
    printf("%d\n", *p); // print 5
    return 0;
}

这个例子使用复合字面量(int) { 0 }创建了一个匿名对象,并用p来指向它。该对象具有静态生存期,所以可以在main函数里访问它,并且因为它是左值,还可以对其进行修改。

例子2:

#include <stdio.h>

int main(void) {
    int *p = NULL;
    {
        p = &(int) { 5 };
    }
    printf("%d\n", *p);     // undefined behavior
}

在这个例子中,(int) { 5 }出现在子块内,当执行printf函数时,该子块已经退出,匿名对象已经被销毁,这时对p解引用结果是未定义的。

C23允许为复合字面量指定存储类型说明符,比如constexprstaticregisterthread_local,其效果相当于在当前位置使用这些说明符定义一个标识符。(static int) { 0 }相当于static int id = { 0 };,只是id被省略了,只有编译器知道。这个规则也适用于一般的复合字面量。

例子3:

#include <stdio.h>

int main(void) {
    int result = 0;
    for (int iter = 0; iter < 5; ++iter) {
        int *p = &(static int) { 0 };
        ++*p;
        result = *p;
    }
    printf("%d\n", result);
}

在这个例子中,因为匿名对象具有静态生存期,所以只会初始化一次,每轮循环递增1,最后输出5。如果把static去掉,最后输出1

使用

因为复合字面量创建的对象是匿名的,没有任何标识符与它关联,所以表达式的值应该马上被使用,比如赋值或者取地址,否则就白创建了。

int a = (int) { 0 };        // 赋值
int *b = &(int) { 0 };      // 取地址
(int) { 0 };                // 无意义

上面的两种情况其实意义不大,还不如直接定义一个int变量并初始化来的方便。

复合字面量的使用场景主要是作为函数的实际参数。

例子1:

#include <stdio.h>

int sum(int const *array, int count) {
    int result = 0;
    for (int iter = 0; iter < count; ++iter) {
        result += array[iter];
    }
    return result;
}

int main(void) {
    printf("%d\n", sum((int[]) { 1, 2, 3 }, 3));
    printf("%d\n", sum((int[]) { 1, 2, 3, 4 }, 4));
    return 0;
}

例子2:

#include <stdio.h>
#include <math.h>

struct Point {
    double x;
    double y;
};

double distance(struct Point const *p1, struct Point const *p2) {
    double h = p1->x - p2->x;
    double v = p1->y - p2->y;
    return sqrt(h * h + v * v);
}

int main(void) {
    printf("%f\n", distance(&(struct Point) { 0, 0 }, &(struct Point) { 1, 1 }));
    printf("%f\n", distance(&(struct Point) { 0, 0 }, &(struct Point) { 3, 4 }));
    return 0;
}

编写测试代码时,经常需要手动构建多个测试数据,然后传递给测试函数。这些数据往往只会使用一次,如果每个数据都进行申明,会非常麻烦,可以使用复合字面量减少代码量。