非常感谢各位读者的热情捧场。笔者这里先要感谢大家对本书大力支持,尤其感谢那些能为本书提出一些有误处的建议与指导意见。毕竟本身是由笔者在繁忙工作之余抽空书写,错误之处也难免。所以这里为了给大家一个积极反馈,将把对本身中有明显疏漏的地方罗列在此博文中,感谢大家的友情支持!
以下问题主要是因为笔者在书写本书时使用的是Clang编译器。毕竟当前iOS、Android开发都强制性使用Clang编译器;并且Visual Studio官方也能直接支持对Clang编译工具链的安装与使用。所以对GCC的测试确实比较少。因此,以下错误基本是在Clang上能正常通过编译,而在GCC上无法正常使用的请看。
关于复数的初始化
在Clang编译器中可直接使用一个初始化列表对复数进行初始化,而GCC则不能支持。笔者特地再仔细看了一下C语言标准手册,确实没有明确提出复数是一个与结构体兼容的数据类型。因此,这里官方推荐的初始化方法有两种:一个是直接用复数字面量;另一个是使用 <complex.h>
中提供的宏—— CMPLX
、CMPLXF
以及 CMPLXL
,分别用于初始化 双精度类型的复数、单精度类型 的复数,以及 扩展双精度类型 的复数。此外,GCC在默认情况下对于直接使用 1.5fI
这种虚数字面量会报警:“warning: imaginary constants are a GCC extension
”,当然我们也可以选择直接无视此报警,或者通过编译选项将它屏蔽掉。但如果我们想直接避开这个报警的话,需要将虚数字面量改写为:1.5f * I
这种形式。
由于Visual Studio 2022下的MSVC仍然没有支持C11标准中的复数字面量表达方式,因此以下代码只能在Clang或GCC等编译器下编译与运行。下面请看正式的对复数对象的初始化方法:
float complex floatComp = 3.5f - 1.5f * I;
printf("float real: %f, imag: %f\n", crealf(floatComp), cimagf(floatComp));
floatComp = CMPLXF(3.5f, -1.5f);
printf("float real: %f, imag: %f\n", crealf(floatComp), cimagf(floatComp));
// 以上均输出:float real: 3.5, imag: -1.5
double complex doubleComp = 3.5 + 8.5 * I;
printf("double real: %f, imag: %f\n", creal(doubleComp), cimag(doubleComp));
doubleComp = CMPLX(3.5, 8.5);
printf("double real: %f, imag: %f\n", creal(doubleComp), cimag(doubleComp));
// 以上均输出:double real: 3.5, imag: 8.5
long double ldComp = 3.5L + 9.5L * I;
printf("long double real: %Lf, imag: %Lf\n", creall(ldComp), cimagl(ldComp));
ldComp = CMPLXL(3.5L, 9.5L);
// 以上均输出:long double real: 3.5, imag: 9.5
// GNU扩展:整数类型的复数
int complex intComp = 3 - 2 * I;
printf("int real: %d, int imag: %d\n", __real__(intComp), __imag__(intComp));
// 注意!这里的CMPLX参数类型仍然是double类型!
// 因此这里的CMPLX宏操作的逻辑其实是先把实参转为double类型,
// 然后再把宏的形参转为int类型,最后赋值给intComp对应的实数和虚数成员。
intComp = CMPLX(3, -2);
printf("int real: %d, int imag: %d\n", __real__(intComp), __imag__(intComp));
// 以上均输出:int real: 3, int imag: -2
数组对象的初始化
GCC和MSVC不支持对数组对象使用匿名数组字面量进行初始化,但Clang却能支持。因此对于GCC而言,我们只能用常规的数组初始化列表方式对数组进行初始化。而匿名数组对象只能对一个指针类型的对象进行初始化或赋值。
以下代码可在Visual Studio 2017开始的MSVC、GCC以及Clang下通过编译与运行。见以下代码:
// 如果写成:int arr[] = (int[]){ 1, 2, 3, 4, 5 };
// GCC会报:“array initialized from non-constant array expression”这个错误;
// MSVC会报:“"arr": 初始化需要带括号的初始化表达式列表”
int arr[] = { 1, 2, 3, 4, 5 };
// 三大编译器均没问题
int* p = (int[]){ 1, 2, 3, 4, 5 };
// 下面pArr指向了一个匿名二维数组,
// 该匿名二维数组的类型为:int[4][3]
int (*pArr)[3] = (int[][3]){
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 },
{ 10, 11, 12 }
};
// 输出:value = 13
printf("value = %d\n", pArr[0][0] + pArr[3][2]);
不可修改的左值
本书 14.4 C语言的左值 小节中,第345页的 “代码清单14-6 C语言的左值” 代码中的第21行,所写的注释有一个小笔误需要补充——“数组类型的对象不能作为左值”。
这里需要修正为:“数组的对象不能作为 可修改的左值。”
在讲解对线程章节中所使用的pthread_t类型
在讲解 _Thread_local
相关的多线程例子中,由于使用了 pthread 库,所以有些代码中会出现 pthread_t thread = NULL;
这种对 pthread_t
类型的对象 thread 进行初始化的语句。
这种初始化其实是属于不太规范的…因为Linux、Windows、macOS各家实现pthread_t的方式各有不同!
Windows系统中,pthread_t
是一个结构体;Linux系统中是一个整数类型;而macOS下是一个指向某个结构体的指针。由于这些代码主要是在macOS下完成的,所以这里出现了可能造成跨平台错误的代码。在Linux上编译时很可能会出现warning,Windows上编译的话也有可能会出现error。
所以这里大家 不需要对 thread 对象进行初始化。对它的初始化直接交给后面的 pthread_create
函数,通过其返回值来判断当前线程是否分配创建成功。或是通过 { 0 }
对 thread 对象进行初始化。关于这一万用的初始化方式,详情请参考此博文:深入探讨C语言中的声明与初始化😄
在GCC编译器下使用类型自动推导时只能用单一声明符
GCC 4.9起,Clang 3.8起可以使用GNU语法扩展——类型自动推导。我们直接使用 __auto_type
关键字来作为类型自动推导的类型声明。然而对于GCC编译器而言,当我们使用类型自动推导时,只能使用 单一声明符,而不能使用多个!我们看以下代码示例:
#ifndef let
#define let __auto_type
#endif
int main(void)
{
// GCC不允许针对类型自动推导使用多个声明符,
// 但Clang编译器允许
let a = 10, b = 20; // GCC将会报错
let a = 10; // OK
let b = 20; // OK
}
Clang编译器允许const修饰的对象作为一个常量表达式,但GCC和MSVC均不允许
Clang编译器允许一个使用 const
修饰的对象作为一个编译时的常量表达式来使用。因此,即便在C语言中我们如果用的是Clang编译器的话也能将一个使用const修饰的对象作为数组的长度或是用于其他只能使用常量表达式的场合。下面将为大家介绍一些例子。以下例子将只能使用Clang编译器通过编译,而GCC不行。
int main(void)
{
const int n = 10;
// GCC会报错:variable-sized object may not be initialized
int arr[n] = { 1, 2, 3, 4, 5};
switch(arr[0])
{
// GCC会报错:case label does not reduce to an integer constant
case n:
++arr[0];
break;
default:
break;
}
}
不过取而代之的是,针对上述代码中的情况,我们可以将 n 定义为一个枚举值,因为枚举值是属于编译时常量。当然,枚举的局限性在于,它是属于一个整数常量,不能用于表示一个浮点常量值。不过好消息是,C23标准中将会引入C++11里所引入的 constexpr
常量表达式,这将会使得上述代码情况得到跨平台的更好的支持。
(笔误)20.2 UTF-16字符编码格式
在书中第477页一开始有一个笔误。原文是:“(1) 从 0x0000 到 0x7FFF 以及从 0xE000 到 0xFFFF 两个区间范围”,这里需要修改为:“(1) 从 0x0000 到 0xD7FF 以及从 0xE000 到 0xFFFF 两个区间范围”。