什么时候使用 const void*?
When to use const void*?
我有一个非常简单的测试函数,我用它来弄清楚 const 限定符发生了什么。
int test(const int* dummy)
{
*dummy = 1;
return 0;
}
使用 GCC 4.8.3 时,这个问题向我抛出一个错误。
然而这个编译:
int test(const int* dummy)
{
*(char*)dummy = 1;
return 0;
}
所以似乎 const 限定符只有在我使用参数而不强制转换为其他类型时才有效。
最近我看到使用
的代码
test(const void* vpointer, ...)
至少对我而言,当我使用 void* 时,我倾向于将其转换为 char* 以用于堆栈中的指针运算或跟踪。 const void* 如何防止子程序函数修改 vpointer 指向的数据?
const int *var;
const
是一个 合同 。通过接收 const int *
参数,您 "tell" 您(被调用函数)的调用者将不会修改指针指向的对象。
你的第二个例子明确地打破了契约,放弃了 const 限定符,然后修改接收到的指针指向的对象。永远不要这样做。
此 "contract" 由编译器强制执行。 *dummy = 1
无法编译。演员是一种绕过它的方法,通过告诉编译器你真的知道你在做什么并让你去做。不幸的是 "I really know what I am doing" 通常情况并非如此。
const
也可以被编译器用来执行优化,否则无法执行优化。
未定义的行为注释:
请注意,虽然转换本身在技术上是合法的,但修改声明为 const
的值是未定义的行为。所以从技术上讲,原始函数是可以的,只要传递给它的指针指向声明为可变的数据。否则就是未定义行为。
在 post
末尾有更多相关信息
至于动机和使用,让我们采用 strcpy
和 memcpy
函数的参数:
char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );
strcpy
对 char 字符串进行操作,memcpy
对通用数据进行操作。虽然我使用 strcpy 作为示例,但以下讨论对于两者完全相同,但是 char *
和 const char *
用于 strcpy
以及 void *
和 const void *
用于 memcpy
:
dest
是 char *
因为在缓冲区 dest
中函数将放置副本。该函数将修改此缓冲区的内容,因此它不是常量。
src
是 const char *
因为函数只读取缓冲区 src
的内容。它不会修改它。
只有通过查看函数的声明,调用者才能断言以上所有内容。根据合同 strcpy
不会修改作为参数传递的第二个缓冲区的内容。
const
和 void
是正交的。以上所有关于 const
的讨论都适用于任何类型 (int
, char
, void
, ...)
void *
在 C 中用于 "generic" 数据。
关于未定义行为的更多信息:
案例一:
int a = 24;
const int *cp_a = &a; // mutabale to const is perfectly legal. This is in effect
// a constant view (reference) into a mutable object
*(int *)cp_a = 10; // Legal, because the object referenced (a)
// is declared as mutable
案例二:
const int cb = 42;
const int *cp_cb = &cb;
*(int *)cp_cb = 10; // Undefined Behavior.
// the write into a const object (cb here) is illegal.
我从这些例子开始,因为它们更容易理解。从这里到函数参数只有一步:
void foo(const int *cp) {
*(int *)cp = 10; // Legal in case 1. Undefined Behavior in case 2
}
案例一:
int a = 0;
foo(&a); // the write inside foo is legal
案例二:
int const b = 0;
foo(&b); // the write inside foo causes Undefined Behavior
我必须再次强调:除非你真的知道自己在做什么,并且现在和将来从事代码工作的所有人都是专家并且理解这一点,并且你有良好的动力,除非所有以上都满足了,永远不要抛弃constness!!
int test(const int* dummy)
{
*(char*)dummy = 1;
return 0;
}
不,这行不通。丢弃常量性(使用真正的 const
数据)是 未定义的行为 并且您的程序可能会崩溃,例如,如果实现将 const
数据放入 ROM。 "it works" 的事实不会改变您的代码是 ill-formed.
的事实
At least for me, when I used void*, I tend to cast it to char* for
pointer arithmetic in stacks or for tracing. How can const void*
prevent subroutine functions from modifying the data at which vpointer
is pointing?
A const void*
表示指向某些无法更改的数据的指针。是的,为了阅读它,您必须将其转换为具体类型,例如 char
。但是我说的是阅读,而不是写作,这又是UB。
这方面的内容更深入 here。 C 允许您完全绕过 type-safety:防止这种情况是您的工作。
给定 OS 上的给定编译器可能会将其某些 const
数据放入 read-only 内存页中。如果是这样,尝试写入该位置将在硬件中失败,例如导致一般保护错误。
const
限定符只是意味着写有 未定义的行为 。这意味着语言标准允许程序在您这样做(或其他任何事情)时崩溃。尽管如此,如果您认为自己知道自己在做什么,C 会让您搬起石头砸自己的脚。
您无法阻止子例程重新解释您提供给它的位以及它想要的 运行 任何机器指令。您正在调用的库函数甚至可能是用汇编语言编写的。但是对 const
指针这样做是 未定义的行为 ,你真的不想调用 未定义的行为 .
在我的脑海中,一个罕见的例子可能有意义:假设你有一个传递句柄参数的库。它如何生成和使用它们?在内部,它们可能是指向数据结构的指针。所以这是一个你可能 typedef const void* my_handle;
的应用程序,所以如果你的客户试图取消引用它或错误地对其进行算术运算,编译器将抛出一个错误,然后将它转换回你的库函数中指向你的数据结构的指针。这不是最安全的实现,你要小心可以将任意值传递给你的库的攻击者,但它非常 low-overhead.
我有一个非常简单的测试函数,我用它来弄清楚 const 限定符发生了什么。
int test(const int* dummy)
{
*dummy = 1;
return 0;
}
使用 GCC 4.8.3 时,这个问题向我抛出一个错误。 然而这个编译:
int test(const int* dummy)
{
*(char*)dummy = 1;
return 0;
}
所以似乎 const 限定符只有在我使用参数而不强制转换为其他类型时才有效。
最近我看到使用
的代码test(const void* vpointer, ...)
至少对我而言,当我使用 void* 时,我倾向于将其转换为 char* 以用于堆栈中的指针运算或跟踪。 const void* 如何防止子程序函数修改 vpointer 指向的数据?
const int *var;
const
是一个 合同 。通过接收 const int *
参数,您 "tell" 您(被调用函数)的调用者将不会修改指针指向的对象。
你的第二个例子明确地打破了契约,放弃了 const 限定符,然后修改接收到的指针指向的对象。永远不要这样做。
此 "contract" 由编译器强制执行。 *dummy = 1
无法编译。演员是一种绕过它的方法,通过告诉编译器你真的知道你在做什么并让你去做。不幸的是 "I really know what I am doing" 通常情况并非如此。
const
也可以被编译器用来执行优化,否则无法执行优化。
未定义的行为注释:
请注意,虽然转换本身在技术上是合法的,但修改声明为 const
的值是未定义的行为。所以从技术上讲,原始函数是可以的,只要传递给它的指针指向声明为可变的数据。否则就是未定义行为。
在 post
末尾有更多相关信息至于动机和使用,让我们采用 strcpy
和 memcpy
函数的参数:
char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );
strcpy
对 char 字符串进行操作,memcpy
对通用数据进行操作。虽然我使用 strcpy 作为示例,但以下讨论对于两者完全相同,但是 char *
和 const char *
用于 strcpy
以及 void *
和 const void *
用于 memcpy
:
dest
是 char *
因为在缓冲区 dest
中函数将放置副本。该函数将修改此缓冲区的内容,因此它不是常量。
src
是 const char *
因为函数只读取缓冲区 src
的内容。它不会修改它。
只有通过查看函数的声明,调用者才能断言以上所有内容。根据合同 strcpy
不会修改作为参数传递的第二个缓冲区的内容。
const
和 void
是正交的。以上所有关于 const
的讨论都适用于任何类型 (int
, char
, void
, ...)
void *
在 C 中用于 "generic" 数据。
关于未定义行为的更多信息:
案例一:
int a = 24;
const int *cp_a = &a; // mutabale to const is perfectly legal. This is in effect
// a constant view (reference) into a mutable object
*(int *)cp_a = 10; // Legal, because the object referenced (a)
// is declared as mutable
案例二:
const int cb = 42;
const int *cp_cb = &cb;
*(int *)cp_cb = 10; // Undefined Behavior.
// the write into a const object (cb here) is illegal.
我从这些例子开始,因为它们更容易理解。从这里到函数参数只有一步:
void foo(const int *cp) {
*(int *)cp = 10; // Legal in case 1. Undefined Behavior in case 2
}
案例一:
int a = 0;
foo(&a); // the write inside foo is legal
案例二:
int const b = 0;
foo(&b); // the write inside foo causes Undefined Behavior
我必须再次强调:除非你真的知道自己在做什么,并且现在和将来从事代码工作的所有人都是专家并且理解这一点,并且你有良好的动力,除非所有以上都满足了,永远不要抛弃constness!!
int test(const int* dummy) { *(char*)dummy = 1; return 0; }
不,这行不通。丢弃常量性(使用真正的 const
数据)是 未定义的行为 并且您的程序可能会崩溃,例如,如果实现将 const
数据放入 ROM。 "it works" 的事实不会改变您的代码是 ill-formed.
At least for me, when I used void*, I tend to cast it to char* for pointer arithmetic in stacks or for tracing. How can const void* prevent subroutine functions from modifying the data at which vpointer is pointing?
A const void*
表示指向某些无法更改的数据的指针。是的,为了阅读它,您必须将其转换为具体类型,例如 char
。但是我说的是阅读,而不是写作,这又是UB。
这方面的内容更深入 here。 C 允许您完全绕过 type-safety:防止这种情况是您的工作。
给定 OS 上的给定编译器可能会将其某些 const
数据放入 read-only 内存页中。如果是这样,尝试写入该位置将在硬件中失败,例如导致一般保护错误。
const
限定符只是意味着写有 未定义的行为 。这意味着语言标准允许程序在您这样做(或其他任何事情)时崩溃。尽管如此,如果您认为自己知道自己在做什么,C 会让您搬起石头砸自己的脚。
您无法阻止子例程重新解释您提供给它的位以及它想要的 运行 任何机器指令。您正在调用的库函数甚至可能是用汇编语言编写的。但是对 const
指针这样做是 未定义的行为 ,你真的不想调用 未定义的行为 .
在我的脑海中,一个罕见的例子可能有意义:假设你有一个传递句柄参数的库。它如何生成和使用它们?在内部,它们可能是指向数据结构的指针。所以这是一个你可能 typedef const void* my_handle;
的应用程序,所以如果你的客户试图取消引用它或错误地对其进行算术运算,编译器将抛出一个错误,然后将它转换回你的库函数中指向你的数据结构的指针。这不是最安全的实现,你要小心可以将任意值传递给你的库的攻击者,但它非常 low-overhead.