进行一个通读

注意到 空指针 deref 是 undefined behavior。曾经在 gcc 中遇到过因为分析出空指针导致整个函数体被优化的情况。我现在 -O0 用多了已经忘了这些东西了。

image-20250823122854969

shall be a null pointer。如果不是,那就是有问题的…

注意到 char **argv 是被允许的写法

image-20250823123055486

所以,这些参数都不是 constant。似乎不应该写:

1
int main(int argc, const char *argv[]);
image-20250823124501357

编译优化的基础:只要可观测行为以及跨单元函数调用的结果与抽象语义一致。当然也可以像 E1 一样,Serializable

image-20250823125336800

如果能推导出用 Float 和 Double 出来的结果一样,那么大可以用 Float,虽然这经典不可能;

浮点计算通常不能改变顺序。

image-20250823125648799 image-20250823125922490

有符号整数 int 的加法受到溢出的影响,也不能随意改变顺序。底层原因是:

有符号整数的加法溢出结果是未定义行为。

例子就是说,overflow 产生了一个 trap。我们必须设想这种行为在某些平台上是可能的。

image-20250823131606072

。。。

image-20250823141448783 image-20250823140009503

注意到 一辈子没用过的浮点数定义 (float.h ),以及十进制浮点数类型 _Decimal32/64/128

img

居然真的准…而且可以显式转换

注意正常 gcc 是有针对十进制浮点数的格式化描述符扩展的。十进制浮点是 c23 吸收 gcc 的。然而你需要在configure gcc 的时候 --enable-decimal-float 才可以正常使用 printf。可以这样判断:

1
gcc -v 2>&1 | grep "\-\-enable\-decimal\-float"

没结果就烂了

Decimal floating types are a conditional feature that implementations need not support; see 6.10.9.3.

注意到 stdc 早就有 thread.h 了。

1
2
extern int thrd_create (thrd_t *__thr, thrd_start_t __func, void *__arg);
extern int thrd_join (thrd_t __thr, int *__res);

用了一辈子 pthread

注意到 C 语言有复数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <complex.h>
#include <stdio.h>

void print_complex(float complex z) {
printf("%f%+f*i\n", crealf(z), cimagf(z));
}

int main() {
float complex a = 3 + 4i;
float complex b = 4 + 3i;
print_complex(a + b);
print_complex(a - b);
print_complex(a * b);
print_complex(a / b);
}
1
2
3
4
7.000000+7.000000*i
-1.000000+1.000000*i
0.000000+25.000000*i
0.960000+0.280000*i

注意到 C 语言已经引入了 _BitInt 类型。我这里最大支持到 65535 位,似乎再也不用高精了。但是输入输出支持是个问题

但是,注意到 可以定义 printf specifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYNOPSIS
#include <printf.h>

int register_printf_specifier(int spec, printf_function func,
printf_arginfo_size_function arginfo);
int register_printf_modifier(const wchar_t *str);
int register_printf_type(printf_va_arg_function fct);

Callbacks
typedef int printf_function(FILE *stream, const struct printf_info *info,
const void *const args[]);
typedef int printf_arginfo_size_function(const struct printf_info *info,
size_t n, int argtypes[n], int size[n]);
typedef void printf_va_arg_function(void *mem, va_list *ap);

我的建议是,可以提前写好一套框架,并在要使用的时候填空。

注意到 printf%n 格式化说明符:将当前 printf 的已输出字节数写入指针位置。

格式化字符串漏洞:如果一个程序允许用户输入格式化字符串,并且没有进行过滤,恶意用户可以利用 %n 来进行任意地址写入。攻击者可以传递一个格式化字符串,例如 "%100$n",这会尝试将一个值写入堆栈上第 100 个参数的位置。如果这个位置正好是攻击者想要覆盖的内存地址,就可以用来修改程序行为或执行恶意代码。

注意到 现在有 nullptr_t type。需要 #include <stddef.h> 。用 clangd 分析器的时候一定要加上 -std=c23 才有

image-20250823214635623

注意力涣散
$$
\begin{align}
\text{Standard floating types} = & float, \\ &double,\\ &long\ double
\end{align}\\
$$

$$
\begin{align}
\text{Decimal floating types} = &\_Decimal32, \\ &\_Decimal64, \\ &\_Decimal128
\end{align}\\
$$

$$
\begin{align}
\text{Real floating types} = \text{Standard floating types} \cup \text{Decimal floating types}\\
\end{align}\\
$$

$$
\begin{align}
\text{Complex types} = &float\ \_Complex, \\&double\ \_Complex, \\&long\ double\ \_Complex\\
\end{align}\\
$$

$$
\begin{align}
\text{Floating types} = \text{Real floating types} \cup \text{Complex types}\\
\end{align}\\
$$

$$
\text{Corresponding real type}(float) = float\\
\text{Corresponding real type}(\_Decimal32) = \_Decimal32\\
\text{Corresponding real type}(float\ \_Complex) = float\\

$$

接下来介绍了这些类型的对齐之类的。

还有更多的变量分类学我懒得弄了,搞了一堆名词(

构造 derived(衍生?派生?)类型的方法有:数组、结构、联合、函数、指针、_Atomic (这个居然算衍生类型)

浮点类型(当然,包括实浮点和复浮点)和整数类型(单包括一个 char)构成算数类型(Arithmetic),这些再与单独的空指针类型 nullptr_t 和指针类型统称为标量类型。数组和结构联合都是聚合类型

。。为什么我要热衷于这种概念背诵的东西,是因为名词罗列整理起来很爽吗,感觉有点收集癖

注意到 分析器经常报的类型不完整(incomplete type)是有严格定义的。

image-20250823224903011

注意到 这些是 qualifier。qualified type 和 unqualified type 有相同的表达和对齐但是是不同的类型。

image-20250823225343005

_Atomic 不一样….

image-20250823225535141

居然可以有不同的表达和类型。我感觉很难受。想想可能会添加一些保证原子操作需要的的 attr 之类吧。甚至附加一个锁在里面都有可能呢?

gemini老师说:

举个例子,假设你有一个64位的整型变量,但在一个32位的计算机上,读取这个变量需要两次内存操作:先读取低32位,再读取高32位。如果在两次操作之间,另一个线程修改了这个变量,那么当前线程就会读到一个被损坏的、不完整的值。

为了防止这种情况,编译器在处理 _Atomic 变量时,可能会采取以下措施:

  • 添加填充(Padding):编译器可能会在变量周围添加额外的空间,以确保该变量在内存中是对齐的。这种对齐方式允许硬件在一个单一、不可中断的指令中完成对该变量的读或写。这会改变变量的大小对齐方式
  • 改变表示方式(Representation):为了使用特定的硬件原子指令(例如 compare-and-swap),编译器可能会为原子变量使用一种不同的内部表示形式。这种新的表示形式可能与原始的非原子类型不同。
  • 使用锁(Locks):如果一个数据类型太大,硬件无法一次性进行原子操作,编译器可能会退而求其次,使用内部的锁机制。这虽然不会改变变量的大小,但它从根本上改变了变量的访问方式,这也是其“表示方式”的一部分。

关于 qualifier…

image-20250823230138641

const 修饰的是它前面的类型。如果 const 在开头,才修饰后面的。

image-20250823230951227

比如多个编译单元里,对同一个符号进行多次声明但类型不兼容。结果是未定义的。

image-20250823231417146

神秘的合成大类型。while 我想说,其实编译器一般不会管两个编译单元对一个类型声明不一样,之类的事。大概是标准的特色吧

注意力涣散 整数类型居然有精度,但实际上就是位宽

image-20250823232156830

类型提升规则大致就是 Decimal > Floating > Integer,不同宽的 integer 选更宽,同宽有 unsigned 选 unsigned

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_BitInt(2) a2 = 1;
_BitInt(3) a3 = 2;
_BitInt(33) a33 = 1;
char c = 3;
a2 * a3 /* As part of the multiplication, a2 is converted to
_BitInt(3) and the result type is _BitInt(3). */
a2 * c /* As part of the multiplication, c is promoted to int,
a2 is converted to int and the result type is int. */
a33 * c /* As part of the multiplication, c is promoted to int,
then converted to _BitInt(33) and the result type
is _BitInt(33), provided int has a width of at most
32 */
void func(_BitInt(8) a1, _BitInt(24) a2) {
/* Cast one of the operands to 32-bits to guarantee the
result of the multiplication can contain all possible
values. */
_BitInt(32) a3 = a1 * (_BitInt(32))a2;
}

接下来讲了讲左值

注意到 结构体的成员可以是 const 的。感觉有点反人类。只能初始化

今天已经燃尽了,之后再说吧。