C、指向函数转换的指针,代码不清晰
C, pointer to function cast, unclear code
来自 Mike Ash 的评论:
https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html#comment-3abf26dd771b7bf2f28d04106993c07b
代码如下:
void Tester(int ign, float x, char y)
{
printf("float: %f char: %d\n", x, y);
}
int main(int argc, char **argv)
{
float x = 42;
float y = 42;
Tester(0, x, y);
void (*TesterAlt)(int, ...) = (void *)Tester;
TesterAlt(0, x, y);
return 0;
}
我不太清楚他在 main 函数中所做的转换。
TesterAlt 是指向函数 returning void 的指针,它与函数 Tester 的类型相同 return。他分配给这个函数指针,函数 Tester,但他正在将后者 return 类型转换为 void 类型的指针(我不确定这一点)。
如果我编译更改该行的代码:
void (*TesterAlt)(int, ...) = (void)Tester;
我收到一个编译器错误:
initializing 'void (*)(int, ...)' with an expression of incompatible type 'void'
void (*TesterAlt)(int, ...) = (void) Tester;
他为什么要做这个选角?他的语法是什么意思?
编辑:
我对我原来的问题不是很清楚,我不明白这个语法以及我必须如何阅读它。
(void *)Tester;
据我所知,Tester 被转换为 "a pointer to void",但看来我的解释是错误的。如果它不是指向 void 的指针,那么您如何阅读该代码以及为什么?
您收到此错误消息是因为您无法对已强制转换为 (void)
的表达式执行任何有用的操作。原代码中的(void *)
是指指针本身,而不是return类型。
实际上 (void *)Tester
是从函数指针 Tester
到 void 指针的转换。这是一个指向给定地址的指针,但没有任何有用的信息。
对 (void)Tester
的强制转换就是对 "void type" 的强制转换 - 这会产生一个您无法分配给任何内容的表达式。
让我们 return 到 (void *)Tester
- 您可以通过将其转换回正确的类型来使用此指针。但是在这个意义上 "proper" 是什么?那么,"proper"就是说原函数的函数签名和后面使用的指针类型必须完全一致。违反此要求不会导致编译时错误,但会导致执行时出现未定义的行为。
有人可能会认为带有一个 int 然后省略号的签名将涵盖具有固定参数计数的情况,但事实并非如此。确实有一些系统,例如 AVR platform 会纯粹用寄存器调用 void ()(int ign, float x, char y)
,而 void ()(int, ...)
会通过将参数压入堆栈来调用。
看看这段代码:
int va(int, ...);
int a(int, int, char);
int test() {
int (*b)(int, int, char) = va;
int (*vb)(int, ...) = a;
a(1, 2, 3);
va(1, 2, 3);
b(1, 2, 3);
vb(1, 2, 3);
}
(请注意,我将 float
更改为 int
...)
在分配 b
和 vb
时,我交换了各自的函数原型。这样做的结果是,通过引用 b
,我确实调用了 va
,但编译器假设了一个错误的函数原型。 vb
和 a
.
也是如此
请注意,在 x86 上,这可能有效(我没有检查),我从这段代码中获得的 AVR 程序集就像
# a(1, 2, 3):
ldi r24,lo8(gs(va))
ldi r25,hi8(gs(va))
std Y+2,r25
std Y+1,r24
ldi r24,lo8(gs(a))
ldi r25,hi8(gs(a))
std Y+4,r25
std Y+3,r24
ldi r20,lo8(3)
ldi r22,lo8(2)
ldi r23,0
ldi r24,lo8(1)
ldi r25,0
rcall a
# va(1, 2, 3):
push __zero_reg__
ldi r24,lo8(3)
push r24
push __zero_reg__
ldi r24,lo8(2)
push r24
push __zero_reg__
ldi r24,lo8(1)
push r24
rcall va
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
# b(1, 2, 3):
ldd r18,Y+1
ldd r19,Y+2
ldi r20,lo8(3)
ldi r22,lo8(2)
ldi r23,0
ldi r24,lo8(1)
ldi r25,0
mov r30,r18
mov r31,r19
icall
# vb(1, 2, 3)
push __zero_reg__
ldi r24,lo8(3)
push r24
push __zero_reg__
ldi r24,lo8(2)
push r24
push __zero_reg__
ldi r24,lo8(1)
push r24
ldd r24,Y+3
ldd r25,Y+4
mov r30,r24
mov r31,r25
icall
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
这里我们看到 a()
是非可变参数,通过 r20..r25
获取数据,而 va()
是可变参数,通过 push
获取数据堆叠.
关于b()
和vb()
,我故意混淆了定义,忽略了我得到的警告。所以调用如上,但由于混淆,它们使用了错误的调用约定。这就是成为 UB 的原因。留在 x86 上时,OP 中的代码可能会或可能不会工作(可能会工作),但在切换到 x64 之后,它可能会开始失败,乍一看没有人明白为什么会这样。所以我们再次看到:避免未定义的行为是一个严格的要求。它可能会按预期工作,但您根本无法保证。更改编译器标志可能足以更改行为。或者将代码移植到不同的体系结构。
来自 Mike Ash 的评论: https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html#comment-3abf26dd771b7bf2f28d04106993c07b
代码如下:
void Tester(int ign, float x, char y)
{
printf("float: %f char: %d\n", x, y);
}
int main(int argc, char **argv)
{
float x = 42;
float y = 42;
Tester(0, x, y);
void (*TesterAlt)(int, ...) = (void *)Tester;
TesterAlt(0, x, y);
return 0;
}
我不太清楚他在 main 函数中所做的转换。
TesterAlt 是指向函数 returning void 的指针,它与函数 Tester 的类型相同 return。他分配给这个函数指针,函数 Tester,但他正在将后者 return 类型转换为 void 类型的指针(我不确定这一点)。
如果我编译更改该行的代码:
void (*TesterAlt)(int, ...) = (void)Tester;
我收到一个编译器错误:
initializing 'void (*)(int, ...)' with an expression of incompatible type 'void'
void (*TesterAlt)(int, ...) = (void) Tester;
他为什么要做这个选角?他的语法是什么意思?
编辑: 我对我原来的问题不是很清楚,我不明白这个语法以及我必须如何阅读它。
(void *)Tester;
据我所知,Tester 被转换为 "a pointer to void",但看来我的解释是错误的。如果它不是指向 void 的指针,那么您如何阅读该代码以及为什么?
您收到此错误消息是因为您无法对已强制转换为 (void)
的表达式执行任何有用的操作。原代码中的(void *)
是指指针本身,而不是return类型。
实际上 (void *)Tester
是从函数指针 Tester
到 void 指针的转换。这是一个指向给定地址的指针,但没有任何有用的信息。
对 (void)Tester
的强制转换就是对 "void type" 的强制转换 - 这会产生一个您无法分配给任何内容的表达式。
让我们 return 到 (void *)Tester
- 您可以通过将其转换回正确的类型来使用此指针。但是在这个意义上 "proper" 是什么?那么,"proper"就是说原函数的函数签名和后面使用的指针类型必须完全一致。违反此要求不会导致编译时错误,但会导致执行时出现未定义的行为。
有人可能会认为带有一个 int 然后省略号的签名将涵盖具有固定参数计数的情况,但事实并非如此。确实有一些系统,例如 AVR platform 会纯粹用寄存器调用 void ()(int ign, float x, char y)
,而 void ()(int, ...)
会通过将参数压入堆栈来调用。
看看这段代码:
int va(int, ...);
int a(int, int, char);
int test() {
int (*b)(int, int, char) = va;
int (*vb)(int, ...) = a;
a(1, 2, 3);
va(1, 2, 3);
b(1, 2, 3);
vb(1, 2, 3);
}
(请注意,我将 float
更改为 int
...)
在分配 b
和 vb
时,我交换了各自的函数原型。这样做的结果是,通过引用 b
,我确实调用了 va
,但编译器假设了一个错误的函数原型。 vb
和 a
.
请注意,在 x86 上,这可能有效(我没有检查),我从这段代码中获得的 AVR 程序集就像
# a(1, 2, 3):
ldi r24,lo8(gs(va))
ldi r25,hi8(gs(va))
std Y+2,r25
std Y+1,r24
ldi r24,lo8(gs(a))
ldi r25,hi8(gs(a))
std Y+4,r25
std Y+3,r24
ldi r20,lo8(3)
ldi r22,lo8(2)
ldi r23,0
ldi r24,lo8(1)
ldi r25,0
rcall a
# va(1, 2, 3):
push __zero_reg__
ldi r24,lo8(3)
push r24
push __zero_reg__
ldi r24,lo8(2)
push r24
push __zero_reg__
ldi r24,lo8(1)
push r24
rcall va
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
# b(1, 2, 3):
ldd r18,Y+1
ldd r19,Y+2
ldi r20,lo8(3)
ldi r22,lo8(2)
ldi r23,0
ldi r24,lo8(1)
ldi r25,0
mov r30,r18
mov r31,r19
icall
# vb(1, 2, 3)
push __zero_reg__
ldi r24,lo8(3)
push r24
push __zero_reg__
ldi r24,lo8(2)
push r24
push __zero_reg__
ldi r24,lo8(1)
push r24
ldd r24,Y+3
ldd r25,Y+4
mov r30,r24
mov r31,r25
icall
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
pop __tmp_reg__
这里我们看到 a()
是非可变参数,通过 r20..r25
获取数据,而 va()
是可变参数,通过 push
获取数据堆叠.
关于b()
和vb()
,我故意混淆了定义,忽略了我得到的警告。所以调用如上,但由于混淆,它们使用了错误的调用约定。这就是成为 UB 的原因。留在 x86 上时,OP 中的代码可能会或可能不会工作(可能会工作),但在切换到 x64 之后,它可能会开始失败,乍一看没有人明白为什么会这样。所以我们再次看到:避免未定义的行为是一个严格的要求。它可能会按预期工作,但您根本无法保证。更改编译器标志可能足以更改行为。或者将代码移植到不同的体系结构。