【问题标题】:When to use const void*?何时使用 const void*?
【发布时间】:2016-01-17 18:49:12
【问题描述】:

我有一个非常简单的测试函数,我用它来弄清楚 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 T* 表示指针指向的数据是const。第二个非常脆弱,因为它抛弃了 const。
  • @ColonelThirtyTwo 但是从我在其他 stackoverflow 帖子上读到的内容来看,不是 const void* 经常用于防止数据被修改吗?但在我看来,通过强制转换可以很容易地对其进行修改。
  • @MoneyBall 是的,但铸造就是这样。这就是强制转换很危险的原因——我可以将您的“const int *”转换为“int64_t *”和 SIGSVF 或损坏其他数据。问题在于 casting 而不是 const 声明。
  • @Mσᶎ 对,我认为 const 限定符可以防止数据被修改。事实证明,C 可以让你强制转换和修改它而不会引发任何错误。
  • 请原谅我的措辞,但是....如果const 在你将某些东西转换为非常量类型的情况下会起作用,那该有多愚蠢?试想一下,你有一个给定的原型,用于某个回调或出于内部目的而具有参数 const 的东西。但是,如果您知道您的实现将永远不会提供 const 参数,或者您能够捕捉到这种情况,为什么编译器如果您通过强制转换明确表示它不必被视为常量,无论如何将其视为常量?

标签: c pointers type-safety


【解决方案1】:
const int *var;

const 是一个合同。通过接收const int * 参数,您“告诉”调用者您(被调用函数)不会修改指针指向的对象。

您的第二个示例通过丢弃 const 限定符然后修改接收到的指针指向的对象明确地打破该合同。永远不要这样做。

这个“契约”是由编译器强制执行的。 *dummy = 1 不会编译。演员表是一种绕过它的方法,它告诉编译器你真的知道你在做什么并让你去做。不幸的是,“我真的知道我在做什么”通常不是这样的。

const 也可以被编译器用来执行它不能以其他方式进行的优化。


未定义行为说明:

请注意,虽然强制转换本身在技术上是合法的,但修改声明为 const 的值是未定义行为。所以从技术上讲,原始函数是可以的,只要传递给它的指针指向声明为可变的数据。否则是未定义的行为。

更多内容见文末


至于动机和用途,让我们采用strcpymemcpy 函数的参数:

char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );

strcpy 对字符字符串进行操作,memcpy 对通用数据进行操作。虽然我使用 strcpy 作为示例,但下面的讨论对于两者来说是完全相同的,但是对于 strcpyvoid *memcpy 使用 char *const char *const void *

destchar *,因为在缓冲区dest 中,该函数将放置副本。该函数将修改此缓冲区的内容,因此它不是 const。

srcconst char *,因为该函数只读取缓冲区src 的内容。它不会修改它。

只有通过查看函数的声明,调用者才能断言以上所有内容。按照约定,strcpy 不会修改作为参数传递的第二个缓冲区的内容。


constvoid 是正交的。以上就是关于const 的所有讨论适用于任何类型(intcharvoid、...)

void * 在 C 中用于“通用”数据。


关于未定义行为的更多信息:

案例一:

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

案例 2:

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

案例 2:

int const b = 0;
foo(&b);     // the write inside foo causes Undefined Behavior

我必须再次强调:除非你真的知道自己在做什么,并且现在和将来从事代码工作的所有人员都是专家并且理解这一点,并且你有良好的动力,除非以上所有内容都是遇见,永远不要抛弃不变!

【讨论】:

  • @MoneyBall 可以肯定地说,C 为您提供了在腿上开枪的方法(允许抛弃 const 限定符就是其中之一)。话虽如此,假设函数不会丢弃 const 限定符是安全的。除非它是恶意或错误的功能。
  • @MoneyBall 我添加了一条关于执行此合同的编译器的注释
  • 你说如果被调用者收到const int* vpointervpointer指向的4Bytes数据 是常数。所以如果const是调用者和被调用者之间的contract,如果给被调用者const void*,它是做什么的意思是?被调用者如何知道有多少数据是不变的?
  • @DavidC.Rankin 英语不是我的母语,所以我用起来可能有误,但是“正交”也有“独立”的意思oxforddictionaries.com/definition/american_english/orthogonalmerriam-webster.com/dictionary/orthogonal
  • @bolov:FWIW,“正交”的版本实际上比“不耦合”的版本更好(更清晰)。在任何拥有大量受过良好教育、具有技术头脑的人的网站上,您都会遇到很多关于“应该”如何使用单词或它们“实际上”意味着什么的说法。其中许多说法是完全错误的。 David C. Rankin 就是其中之一。
【解决方案2】:
int test(const int* dummy)
{
   *(char*)dummy = 1;
   return 0;
}

不,这不起作用。抛弃 constness(真正的 const 数据)是未定义的行为,如果实现将 const 数据放入 ROM 中,您的程序可能会崩溃。 “它有效”这一事实并没有改变您的代码格式错误的事实。

至少对我来说,当我使用 void* 时,我倾向于将其转换为 char* 堆栈中的指针运算或用于跟踪。如何 const void* 防止子程序函数修改 vpointer 所在的数据 是指点吗?

const void* 表示指向某些无法更改的数据的指针。为了阅读它,是的,您必须将其转换为具体类型,例如char。但我说的是阅读,而不是写作,这又是UB。

here 对此进行了更深入的介绍。 C 允许您完全绕过类型安全:防止这种情况发生是您的工作。

【讨论】:

  • 实际上抛弃常量并不是未定义的行为。即使修改指向的值,它也被很好地定义,条件是对象被声明为可变的。有关详细示例,请参阅我的答案。但我 100% 同意这不是允许抛弃 constness 的理由
  • @bolov 是的,感谢您的指出。当我提到将 const 数据放入 ROM 的实现时,我有点暗示了这一点。
【解决方案3】:

给定操作系统上的给定编译器可能会将其一些 const 数据放入只读内存页面中。如果是这样,尝试写入该位置将在硬件中失败,例如导致一般保护故障。

const 限定符只是意味着写在那里有未定义的行为。这意味着如果您这样做(或其他任何事情),语言标准允许程序崩溃。尽管如此,如果你认为自己知道自己在做什么,C 会让你在自己的脚下开枪。

您无法阻止子例程重新解释您提供给它的位,并根据它的需要运行任何机器指令。您正在调用的库函数甚至可能是用汇编程序编写的。但是对const 指针这样做是未定义的行为,你真的不想调用未定义的行为

在我的脑海中,一个罕见的例子可能有意义:假设你有一个传递句柄参数的库。它是如何生成和使用它们的?在内部,它们可能是指向数据结构的指针。所以这是一个你可以typedef const void* my_handle; 的应用程序,所以如果你的客户试图取消引用它或错误地对其进行算术运算,编译器将抛出一个错误,然后将它转换回指向你的库函数中的数据结构的指针。这不是最安全的实现,您要小心攻击者,他们可以将任意值传递给您的库,但它的开销非常低。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    • 1970-01-01
    • 2020-03-21
    • 1970-01-01
    相关资源
    最近更新 更多