如果余弦是 fptr,如何解析 *(void **) (&cosine)

How to parse *(void **) (&cosine) if cosine is a fptr

找到此代码示例

void *handle;
double (*cosine)(double);
handle = dlopen("libm.so", RTLD_LAZY);
*(void **) (&cosine) = dlsym(handle, "cos");

我使用从右到左的阅读规则来解析变量的类型:

double (*cosine)(double);

这里我从左到右书写,但移动了 LTR:"cosine" -> "*" -> "is a pointer" 然后 "(" 我们走出最里面的 () 范围 -> "(double)" -> "to function taking one double" -> 并返回最左边的 "double"

但这到底是什么东西?我什至不知道从哪里开始解析)“&cosine”是地址还是引用? (void **) 是什么意思?为什么它最左边的“*”在外面???是解引用还是类型?

*(void **) (&cosine)

是的,那是一口。

cosine 是一个函数指针。所以 &cosine 是指向该指针的指针。然后当我们在它前面打一个 * 时,我们正在更改原始指针,使其指向其他地方。

有点像这样:

int i = 5;
int *ip = &i;
*ip = 6;         /* changes i to 6 */

或者更像这样:

char a[10], b[10];
char *p = a;
*(&p) = b;       /* changes p to point to b */

但在你的情况下它更棘手,因为 cosine 是一个指向函数的指针,而不是指向数据的指针。大多数时候,函数指针指向您在程序中定义的函数。但是在这里,我们安排 cosine 指向一个动态加载的函数,由 dlsym() 函数加载。

dlsym 非常特别,因为它可以 return 指向数据的指针, 以及 指向函数的指针。所以它有一个不可能定义的 return 类型。它被声明为 returning void *,当然,因为那是 C 中的 "generic" 指针类型。(想想 malloc。)但是在纯 C 中,void *是通用的 data 指针类型;它不能保证能够与函数指针一起使用。

最直接的做法就是直接说

cosine = dlsym(handle, "cos");

但是现代编译器会报错,因为dlsym returns void *,而cosine的类型是double (*)(double)(即指向函数的指针取双倍和 returning 双倍),这不是可移植的转换。

所以我们绕过谷仓,间接设置 cosine 的值,而不是说

cosine = something

而是说

*(&cosine) = something

但是在 dlsym 的情况下这仍然不好,因为类型仍然不匹配。我们在右边有 void *,所以我们需要在左边有 void *。解决方案是获取地址 &cosine,否则它是指向函数指针的指针,并将其转换为指向指向指针的指针void,或 void **,所以当我们在它前面打一个 * 时,我们又得到了一个 void *,这是分配 dlsym 的 return 的正确目的地价值。所以我们以您询问的行结束:

* (void **) (&cosine) = dlsym(handle, "cos");

现在,重要的是要注意我们在这里如履薄冰。我们使用 & 和强制转换来绕过这样一个事实:将指向 void 的指针分配给“指向函数的指针”在严格意义上是不合法的。在这个过程中,我们已经成功地消除了编译器的警告,即我们正在做的事情并不严格合法。 (事实上​​,消除警告正是程序员使用这种闪避的初衷。)

潜在的问题是,如果数据指针和函数指针具有不同的大小或表示形式怎么办?这段代码花了一些时间来处理函数指针 cosine,就好像它是一个数据指针一样,将数据指针的位塞入其中。比方说,如果数据指针比函数指针大,这将产生可怕的影响。 (而且,在你问 "But how could a data pointer ever be bigger than a function pointer?" 之前,这正是它们的情况,例如,在 "compact" 内存模型中,回到 MS-DOS 编程时代。)

通常情况下,玩这样的游戏来打破规则并关闭编译器警告是一个坏主意。不过,在 dlsym 的情况下,没关系,我会说完全可以接受。 dlsym 不能存在于函数指针与数据指针不同的系统上,因此如果您使用 dlsym,您必须在所有指针都相同的机器上,而这代码将起作用。

同样值得一提的是,如果我们在调用 dlsym 时必须使用 cast 来玩游戏,为什么还要用指针对指针绕着 barm 多跑一圈?为什么不直接说

cosine = (double (*)(double))dlsym(handle, "cos");

答案是,我不知道。我很确定这个更简单的转换也能正常工作(同样,只要我们在 dlsym 可以存在的系统上)。也许有些编译器会警告这种情况,只能通过使用骗子、双指针技巧来让这种情况保持沉默。

另见

这是讨厌的东西。 cosine 是一个指向函数的指针,该函数接受类型为 double 和 returns double 的参数。 &cosine 是那个指针的地址。演员说假装那个地址是一个指向空指针的指针。 cast前面的*是通常的解引用运算符,所以结果就是告诉编译器假装cosine的类型是void*,这样代码就可以赋值给return 值从对 dlsym 的调用到 cosine。呸;好痛。

而且,只是为了好玩,void* 和指向函数的指针根本不相关,这就是代码必须经过所有转换的原因。 C++ 语言定义不保证这会起作用。即,结果是未定义的行为。

对于 C,指向 void 的指针可以转换为 any 指向 object 的指针没有演员表。但是,C 标准 保证 void * 可以转换为 指向函数 的指针 - 完全没有,因为函数是不是 对象.

dlsym是一个POSIX函数; POSIX 要求作为扩展,指向函数的指针必须可转换为 void * 并再次返回。但是 C++ 不允许在没有强制转换的情况下进行此类转换。

在任何情况下,*(void **) (&cosine) = dlsym(handle, "cos"); 强制转换意味着 指向函数指针的指针 (double) returning double 被强制转换为 指向指向 void 的指针的指针,然后取消引用以获取 void * 类型的 lvaluedlsym 的 return 值分配给该 lvalue .这相当难看,在需要转换的地方最好写成 cosine = (double (*)(double))dlsym(handle, "cos") 。就 C 而言,两者都是未定义的行为,但后者并不是那么黑魔法。