需要注意的是,错误行为不是未定义行为。
char c; long i; // 这个行为非常不恰当,会导致紧接着`c`后面的3个字节被访问,这3个字节不属于`c`。 // 但它只是错误行为,不是未定义行为。 // 这个行为会发生什么具有明确的定义,就是`c`所指向的内存地址及其后方3个字节一同被赋值给`i`,在所有平台上都会发生同样的事情。 // 所以,这里不含未定义行为,只含编程错误。 i = * ( long * ) &c;
@无名啊,我已经对上述问题进行了回答。
@无名啊,把这段代码拆分成多个部分,应该有助于理解为什么没有未定义行为:
long i; long *p_i; float y; float *p_y; p_y = &y; // 只是一个简单的取地址操作,不是未定义行为 p_i = (long *) p_y; // 对指针进行类型转换不是未定义行为,所有指针类型都是互相兼容的 i = *p_i; // i 和 *p_i 类型一致,没有未定义行为
操作的每一步都不含未定义行为,所以整体不含未定义行为。
@无名啊,
i = * ( long * ) &y
不含未定义行为,因为long i
,所以* ( long * )
即long
显然是它的兼容类型。当赋值发生时,类型已经是long
了。而把一个float
指针转换为long
指针显然也不是未定义行为,因为实际上只是绕过了编译器的类型检查,对于代码生成来说相当于什么也没有发生,指针的值没有任何变化。
鼠标垫脏了或者不平也会有这种现象
至于
i = 0x5f3759df - ( i >> 1 )
到底意味着什么,其实也可以有纯数学的解释。
0x5f3759df
和i
其实都是浮点数,但是使用整数规则进行了运算,这些运算同时操作了浮点数的指数和尾数部分。比如
i >> 1
也就是把指数和尾数同时向后挪动一位,两者的最后一位都被抛弃,然后指数的最后一位变成尾数的第一位。
0x5f3759df - $x
也就是把指数和尾数同时减小,并且尾数减到小于0时向指数借位。这些操作都可以写成数学公式,从而让运算具有数学上的解析表达——也就是说,运算结果是确定的,没有未定义行为。
@无名啊,这是这个函数的PHP版本,有助于理解为什么没有未定义行为:
<?php function Q_rsqrt(float $number) { $threehalfs = 1.5; $x2 = $number * 0.5; $y = $number; $i = unpack("l", pack("f", $y))[1]; $i = 0x5f3759df - ($i >> 1); $y = unpack("f", pack("l", $i))[1]; $y = $y * ( $threehalfs - ($x2 * $y * $y) ); $y = $y * ( $threehalfs - ($x2 * $y * $y) ); return $y; } printf("%0.7f\n", Q_rsqrt(3.14)); printf("%0.7f\n", Q_rsqrt(1024.0)); printf("%0.7f\n", Q_rsqrt(10086.0)); printf("%0.7f\n", Q_rsqrt(2147483647.0)); printf("%0.14f\n", Q_rsqrt(3.14)); printf("%0.14f\n", Q_rsqrt(1024.0)); printf("%0.14f\n", Q_rsqrt(10086.0)); printf("%0.14f\n", Q_rsqrt(2147483647.0));
在给定的定义域和有效数字范围内,它和C版本的结果一致。如果继续增加输出的位数,结果就开始不一致了,因为PHP在内部使用64位整数和双精度浮点数,而非C代码的32位整数和单精度浮点数,只在
pack
和unpack
时才转换为32位单精度,所以两者会有精度差异。此外32位和64位在处理符号位上可能也有差异,所以C版给出负数解的情况下PHP给出的是正数解。当然两者都是正确的解,因为负数的平方也是正数。
C版本:
#include <stdio.h> float Q_rsqrt( float number ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; // evil floating point bit level hacking i = 0x5f3759df - ( i >> 1 ); // what the fuck? y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed return y; } int main() { printf("%0.7f\n", Q_rsqrt(3.14)); printf("%0.7f\n", Q_rsqrt(1024.0)); printf("%0.7f\n", Q_rsqrt(10086.0)); printf("%0.7f\n", Q_rsqrt(2147483647.0)); printf("%0.14f\n", Q_rsqrt(3.14)); printf("%0.14f\n", Q_rsqrt(1024.0)); printf("%0.14f\n", Q_rsqrt(10086.0)); printf("%0.14f\n", Q_rsqrt(2147483647.0)); return 0; }
@无名啊,此外,
Q_rsqrt()
函数中没有未定义行为,IEEE 754 标准已经精确的定义了单精度浮点数(float
)的二进制表示,所以把它的二进制表示做为long
使用不是未定义行为,结果应该是很明确的:符号位依然是符号位,指数和尾数则被拼接在一起做为整数的值。反向操作(把整数的二进制表示做为单精度浮点数使用)结果也很明确:符号位依然是符号位,然后接下来8位成为指数,最后23位成为尾数。
所以,这只是一个“用户定义浮点数算法”,它与GMP等其他用户定义数学库中的自定义浮点数算法没有本质区别。代码中的每次类型转换在C中都有明确的定义。在所有使用IEEE754单精度浮点数的计算机中,结果都应该是一致的。
其实无论是
const
,还是volatile
和restrict
,都是为了解决内存空间的所有权问题。因为C/C++可以操作原始指针,所以内存空间的所有权可以在多个线程、函数、变量之间以任意方式共享和转移,导致编译器优化很容易出问题,所以才需要这些标记加以指示。
其他编程语言不能直接操作原始指针,所以内存空间的所有权是明确的,不需要这些编译器优化限定符。
当然
const
也有语法上的含义,表明你希望编译器帮你阻止对该变量的修改,所以其他编程语言里也存在该关键字。但是volatile
和restrict
在语法上没有任何含义,所以在内存空间所有权明确的编程语言中完全不存在。不能对原始指针解引用的语言都是所有权明确的,带GC的语言通常属于此类。所谓原始指针解引用,就是类似这样的代码:
y = * ( float * ) &i;
它在语法上提供了无限的灵活性,实际上可以用于读写任意内存地址:
int main() { long i = 1; float y = -1; const int x = 12306; // 以下代码没有语法错误,可以编译通过。 // 读取原始指针 y = * ( float * ) (&i + 10086); y = * ( float * ) 10086; // 写入原始指针 * ( float * ) (&i + 10086) = y; * ( float * ) 10086 = y; // 写入 const 变量 * (int *) &x = 10010; return 0; }
因为这种灵活性,所以在C/C++中跟踪内存空间所有权变得不可能,于是需要对所有权进行人工标记。而
const
、volatile
和restrict
正是这样的标记。
const
:我保证不写入这块内存空间。如果我通过原始指针解引用实现了写入,结果是未定义的。
restrict
:我保证不把内存空间的所有权转移给其他变量(也就是创建别名)。如果我确实转移了,结果是未定义的。
volatile
:我对该内存空间的使用不进行任何保证,请不要假设它可以被优化。至于到底能阻止哪些优化,由实现定义。需要说明的是:
volatile
不是线程同步措施,它不能提供多核CPU间的内存一致性。想实现多线程内存一致性必须使用同步原语(比如互斥锁 mutex)。
一个指针经 restrict 修饰后,它(可能经过指针运算后)指向的对象
不会不能有其它别名。并非不会,而是不能。
不会意味着编译器会阻止你为它创建别名,创建别名会导致编译错误。
但实际上只是不能,创建别名最多产生警告,程序还是能运行,而且还可能完全无错(因为编译器优化后程序出问题只是概率事件)。
所以,
restrict
体现的是你的自信,你得首先保证你的代码没有对该变量创建别名,然后才能给它加上restrict
。就像
volatile
,是你不自信,觉得优化这个变量会出问题,才给它加上volatile
。至于不加会不会出问题,得具体问题具体分析。
@无名啊,
volatile
和restrict
是编译器优化指示标记,其中volatile
阻止对该标识符进行优化,restrict
建议编译器对该标识符进行优化。
volatile
的语义:小心,这个变量的用途很复杂,优化这个变量很可能会导致程序出问题!
restrict
的语义:我保证我只通过这个变量访问它指向的内存区域,你随便优化它,绝对不会出问题!这些都只是给编译器的提示,编译器不一定会遵循指示。比如,使用
-O0
编译时,加不加volatile
和restrict
参数都没有任何区别。只有-O1
、-O2
、-O3
等有区别。对于VC++编译器,Debug模式应该体现不出区别,只有Release模式才有区别。
const
与它们不一样,它不仅是编译器优化指示标记,还进行了语法上的限制。如果不通过强制类型转换去除const
标记,则无法对变量进行写入。不过,因为
const
也是编译器优化指示标记,它的语义是:我保证不会对该变量进行写入,你放心优化。所以如果后续通过强制类型转换去掉const并写入变量,则Release版程序可能会出问题。注意只是可能,编译器会尽量给出不出问题的代码,所以真想遇到问题也需要碰运气。
已经添加了一个Vortex模组管理器,亲测可以正常安装mod
能用纯SQL实现
要我的话肯定得用常规编程语言,甚至上人工智能
@张小强,这里进行了
i++
。var ccc = '0c4a2013ebd12f0de2b54163fe318b1e'.charCodeAt(i++);
@ysyvsl,那要选的可能就是文件夹,你选skyim special endition不行吗
@ysyvsl,点“.”开头的文件夹在Linux中是隐藏文件夹,在Wine中不会显示。你可以创建一个符号连接以便能直接看到。在终端运行:
ln -s /home/deck/.local/share/steam/steamapps /home/deck/steamapps
这样Wine里就能看到steamapps文件夹了
@晨曦,你可以说继续,然后它会继续发。
@胡椒舰长,我现在也登不进去了,提示”ChatGPT 现在满负荷运转“
@胡椒舰长,之前在用的就还能用