这是讲类型的最后一章!我们要谈两件事:
这两者并没有特别的关系,但各自也撑不起一章。所以我就像个叛逆者一样把它们塞到这里了!
这是这门语言一个很妙的功能,让你可以随手创建一个某种类型的对象,而完全不必把它赋给变量。简单类型、数组、struct,什么都可以。
它的主要用途之一是:当你不想为了存放某个值而专门创建一个临时变量时,把复杂的实参传给函数。
创建复合字面量的方法是:把类型名放在圆括号里,然后在后面跟一个初始化器列表。例如,一个无名的 int 数组可能长这样:
(int []){1,2,3,4}不过,这一行代码本身什么也不做。它创建了一个含 4 个 int 的无名数组,然后没使用就把它们扔掉了。
我们可以用一个指针来保存对这个数组的引用……
int *p = (int []){1 ,2 ,3 ,4};
printf("%d\n", p[1]); // 2但这看起来有点像为了得到一个数组而绕了一大圈。我的意思是,我们本来可以直接这样做178:
int p[] = {1, 2, 3, 4};
printf("%d\n", p[1]); // 2所以让我们看一个更有用的例子。
假设我们有一个函数,用来对 int 数组求和:
int sum(int p[], int count)
{
int total = 0;
for (int i = 0; i < count; i++)
total += p[i];
return total;
}如果要调用它,通常我们得这样做:声明一个数组,把值存进去,再传给函数:
int a[] = {1, 2, 3, 4};
int s = sum(a, 4);但无名对象让我们可以跳过变量,直接把它传进去(形参名见上面)。看看——我们要用作为第一个实参传入的无名数组,替换掉变量 a:
// p[] count
// |-----------------| |
int s = sum((int []){1, 2, 3, 4}, 4);相当巧妙!
struct
对 struct 我们也可以做类似的事。
首先,我们不用无名对象来做这件事。我们定义一个 struct 来存放 x/y 坐标。然后定义一个该类型的变量,在它的初始化器里传入数值。最后把它传给一个函数来打印这些值:
#include <stdio.h>
struct coord {
int x, y;
};
void print_coord(struct coord c)
{
printf("%d, %d\n", c.x, c.y);
}
int main(void)
{
struct coord t = {.x=10, .y=20};
print_coord(t); // 打印出 "10, 20"
}够直白吧?
我们改一下,用无名对象代替我们传给 print_coord() 的变量 t。
我们直接把 t 从那儿拿掉,换成一个无名的 struct:
//struct coord t = {.x=10, .y=20}; // 旧的写法
print_coord((struct coord){.x=10, .y=20}); // 打印出 "10, 20"照样能用!
你可能已经注意到,在上一个例子里,虽然我们用的是一个 struct,但传给 print_coord() 的是该 struct 的副本,而不是指向该 struct 的指针。
事实证明,我们完全可以像平常一样用 & 取无名对象的地址。
这是因为,一般来说,如果某个运算符能作用于该类型的变量,你就能把它用于该类型的无名对象。
我们把上面的代码改一下,改为传入一个指向无名对象的指针
#include <stdio.h>
struct coord {
int x, y;
};
void print_coord(struct coord *c)
{
printf("%d, %d\n", c->x, c->y);
}
int main(void)
{
// 注意这个 &
// |
print_coord(&(struct coord){.x=10, .y=20}); // 打印出 "10, 20"
}此外,这还是一种很不错的办法,连简单对象的指针也能这样传:
// 传入一个指向值为 3490 的 int 的指针
foo(&(int){3490});就这么简单。
无名对象的生命期在它的作用域结束时终止。最容易咬你一口的情形是:你新建了一个无名对象、取得指向它的指针,然后离开了该对象的作用域。这种情况下,那个指针会指向一个已经死掉的对象。
所以下面这是未定义行为:
int *p;
{
p = &(int){10};
}
printf("%d\n", *p); // 无效:(int){10} 已离开作用域同样,你也不能从函数返回指向无名对象的指针。该对象在退出作用域时就被释放了:
#include <stdio.h>
int *get3490(void)
{
// 不要这么做
return &(int){3490};
}
int main(void)
{
printf("%d\n", *get3490()); // 无效:(int){3490} 已离开作用域
}就把它们的作用域想成普通局部变量的作用域。你同样不能返回指向局部变量的指针。
你可以在里面放任何类型来造一个无名对象。
例如,下面这些实际上是等价的:
int x = 3490;
printf("%d\n", x); // 3490(变量)
printf("%d\n", 3490); // 3490(常量)
printf("%d\n", (int){3490}); // 3490(未命名对象)最后那个是无名的,但很傻。还不如用上一行那个简单的写法。
但愿这能让你对这套语法看得更清楚一点。
这是一个表达式,让你可以根据该表达式第一个实参的类型来选择不同的代码片段。
我们马上就看一个例子,但重要的是要知道:它是在编译期处理的,而不是在运行期。这里没有任何运行期分析。
这个表达式以 _Generic 开头,作用有点像 switch,至少接受两个实参。
第一个实参是一个具有类型的表达式(或变量179)。所有表达式都有类型。_Generic 其余的实参就是各个分支:如果第一个实参是某个类型,就用对应的内容替换整个表达式的结果。
啥?
我们来试一试,看看效果。
#include <stdio.h>
int main(void)
{
int i;
float f;
char c;
char *s = _Generic(i,
int: "that variable is an int",
float: "that variable is a float",
default: "that variable is some type"
);
printf("%s\n", s);
}看看从第 9 行开始的 _Generic 表达式。
编译器看到它时,会先看第一个实参的类型(在本例中就是变量 i 的类型)。然后它会在各个分支中查找与该类型匹配的一项。接着它用对应的实参替换掉整个 _Generic 表达式。
在本例中,i 是 int,所以匹配那一个分支。然后字符串被替换进这个表达式。于是编译器看到的那一行就变成了这样:
char *s = "that variable is an int";如果编译器在 _Generic 中找不到类型匹配,它就会去找可选的 default 分支并使用它。
如果找不到类型匹配,又没有 default,你就会遇到编译错误。第一个表达式必须匹配其中某个类型或 default。
由于一遍遍写 _Generic 很不方便,它常被用来构成宏的主体,这样就可以轻松地反复复用。
我们来写一个宏 TYPESTR(x),它接收一个实参,并返回一个含有该实参类型的字符串。
所以举例来说,TYPESTR(1) 会返回字符串 "int"。
开始吧:
#include <stdio.h>
#define TYPESTR(x) _Generic((x), \
int: "int", \
long: "long", \
float: "float", \
double: "double", \
default: "something else")
int main(void)
{
int i;
long l;
float f;
double d;
char c;
printf("i is type %s\n", TYPESTR(i));
printf("l is type %s\n", TYPESTR(l));
printf("f is type %s\n", TYPESTR(f));
printf("d is type %s\n", TYPESTR(d));
printf("c is type %s\n", TYPESTR(c));
}输出为:
i is type int
l is type long
f is type float
d is type double
c is type something else这应该不奇怪,因为正如我们所说,main() 里的那段代码在编译时会被替换成下面这样:
printf("i is type %s\n", "int");
printf("l is type %s\n", "long");
printf("f is type %s\n", "float");
printf("d is type %s\n", "double");
printf("c is type %s\n", "something else");而这正是我们看到的输出。
再来一个。我在这里加了一些宏,这样当你运行:
int i = 10;
char *s = "Foo!";
PRINT_VAL(i);
PRINT_VAL(s);就会得到输出:
i = 10
s = Foo!为此我们得用上一点宏的魔法。
#include <stdio.h>
#include <string.h>
// 返回某个类型对应的格式说明符的宏
#define FMTSPEC(x) _Generic((x), \
int: "%d", \
long: "%ld", \
float: "%f", \
double: "%f", \
char *: "%s")
// TODO: 添加更多类型
// 以 "name = value" 形式打印变量的宏
#define PRINT_VAL(x) do { \
char fmt[512]; \
snprintf(fmt, sizeof fmt, #x " = %s\n", FMTSPEC(x)); \
printf(fmt, (x)); \
} while(0)
int main(void)
{
int i = 10;
float f = 3.14159;
char *s = "Hello, world!";
PRINT_VAL(i);
PRINT_VAL(f);
PRINT_VAL(s);
}得到输出:
i = 10
f = 3.141590
s = Hello, world!我们本可以把它全塞进一个大宏里,但我把它拆成了两个,以免看得眼睛流血。