const指针有什么用处?

158

我不是在谈论指向常量值的指针,而是常量指针本身。

我正在学习C和C++的进阶内容。直到今天,我才意识到函数传递指针时是按值传递的,这很合理。 这意味着在函数内部,我可以使复制的指针指向其他值而不影响调用者的原始指针。

那么,声明以下函数头的意义是什么:

void foo(int* const ptr);

在这样的函数内部,您不能使 ptr 指向其他东西,因为它是 const 类型,而且您不希望它被修改。但是像这样的函数:

void foo(int* ptr);

这样做完全没有问题!因为指针会被复制,即使您修改了副本,调用者中的指针也不会受到影响。那么const有什么好处呢?


31
如果您希望在编译时保证指针不能且不应被修改以指向其他内容,该怎么办? - Platinum Azure
26
和任何 const 参数一样的意思。 - David Heffernan
4
@PlatinumAzure,我有几年编程经验,但我的软件实践经验不足。我很高兴我提出了这个问题,因为我从未感到有必要让编译器指出自己的逻辑错误。当我提交这个问题时,我的最初反应是“如果指针被修改,我为什么应该在意?它不会影响调用者。”直到我读完所有的答案,我才明白我应该关注这个问题,因为如果我试图修改它,那么我和我的编程逻辑就是错误的(或者可能是一个错别字,比如缺少星号),如果编译器没有告诉我,我可能没有意识到。 - R. Ruiz.
4
毫无疑问,即使是我们中经验最丰富的人也需要更多正确性的保证。因为软件工程是一种可以接受更多余地的工程形式(如随机的HTTP 502、慢速连接、偶尔的图片加载失败并不罕见,但在飞机上发动机出现故障是不可接受且可能很严重的),有时人们会过于匆忙地编写程序。同样支持编写单元测试的理由也说明了使用const正确性保证的必要性。这只是让我们更加确信我们的代码是毋庸置疑的正确的。 - Platinum Azure
3
相关问题:https://dev59.com/Q3VC5IYBdhLWcg3wqzHV这个问题是关于使用const指针和指向常量对象的指针有什么区别。 - Raedwald
@Raedwald 这些问题看起来对我来说是完全重复的。这个问题有更多好的答案,但它们都变得更加普遍,而不仅仅是关于指针的参数上的 const,因为它们谈到了整体。 - underscore_d
17个回答

217

const是一个工具,用于追求C++中非常重要的概念:

在编译期间查找错误而不是运行时,并通过让编译器强制执行你的意图来实现。

即使它不改变功能,添加const会在你做了不想做的事情时生成编译器错误。想象一下以下的拼写错误:

void foo(int* ptr)
{
    ptr = 0;// oops, I meant *ptr = 0
}

如果你使用 int* const,这会生成一个编译器错误,因为你正在改变指针的值。const 语法的限制通常是一件好事。只是不要走得太远 - 你提到的例子是大多数人不会费心使用 const 的情况。


11
谢谢,这个回答让我信服了。你加上了const,这样编译器就能警告你自己的赋值错误。你的例子非常适合阐述这个概念,因为这是处理指针时常见的错误。谢谢! - R. Ruiz.
28
"帮助编译器帮助你"是我通常会反复念诵的口头禅。 - Flexo
1
+1,但这里有一个有趣的案例,可以争论一下:https://dev59.com/Tm015IYBdhLWcg3w7wXF - Raedwald
4
为什么要把变量设为私有的呢,不设为私有不就能在类外使用吗?对这个问题你的回答是一样的。 - Lee Louviere
这可能有点偏题,但是这个const指针在内存中的哪个位置?我知道所有的const都位于内存的全局部分,但我不确定作为函数变量传递的const的位置。谢谢,如果这不是主题,对不起。 - Tomer
1
@tomer - const变量可以是局部的。我认为你可能混淆了static。由于其本质,静态变量不能在堆栈中分配。(那个评论已经几年了,不过你可能已经知道了) - ysap

83

我强调使用只有const参数,因为这样可以启用更多的编译器检查: 如果我在函数内部意外重新分配了参数值,编译器会提示错误。

我很少重复使用变量,创建新变量来保存新值更加清晰,所以基本上所有我的变量声明都是const(除了某些情况,例如循环变量,其中const会阻止代码工作)。

请注意,这仅在函数定义中才有意义。它不属于声明,而这是用户可以看到的。用户并不关心我是否在函数内部使用const参数。

示例:

// foo.h
int frob(int x);
// foo.cpp
int frob(int const x) {
   MyConfigType const config = get_the_config();
   return x * config.scaling;
}

注意参数和局部变量都是const。虽然这不是必须的,但对于稍微大一点的函数来说,这种做法已经多次帮我避免犯错。


20
另一位沉迷于“常量”的狂热者给了一个“+1”。然而,我更喜欢编译器向我发出警告。我犯了太多错误,如果它们咬我那么厉害,我会很苦恼的。 - sbi
4
+1,我强烈推荐“除非有充分的理由,否则使用const”策略。不过也有一些好的例外情况,例如拷贝并交换 - Flexo
4
如果我正在设计一种新的语言,声明对象默认情况下将是只读的(“const”)。您需要一些特殊的语法,例如“var”关键字,才能使对象可写。 - Keith Thompson
1
@KubaOber 我不认为这是一种悲观的做法。如果你后来发现需要修改函数体内传递的参数,那么只需从参数中删除 const,最好注释一下原因。这点额外的工作并不能证明默认将所有参数标记为非 const 并开放自己面临所有可能产生的错误是有道理的。 - underscore_d
2
我是从答案错误的角度进行争论的。可惜它不是错的。现在我已经深入研究了标准——很高兴知道我错了。很酷的东西,除非你知道你在寻找什么,否则很容易错过。可惜,它还暴露了另一个问题:对于那些不便宜的值,当你从“const T&”切换到“T”时,实现会泄漏,并且这些绝对不是ABI兼容的:( - Kuba hasn't forgotten Monica
显示剩余9条评论

20
你的问题涉及到一个更普遍的问题:函数参数是否应该是const?
值参数(比如你的指针)的const属性是一种实现细节,它不构成函数声明的一部分。这意味着你的函数总是这样的:
void foo(T);

完全由函数实现者决定是否以可变或不可变的方式使用函数作用域参数变量:

// implementation 1
void foo(T const x)
{
  // I won't touch x
  T y = x;
  // ...
}

// implementation 2
void foo(T x)
{
  // l33t coding skillz
  while (*x-- = zap()) { /* ... */ }
}

所以,遵循简单的规则,永远不要把 const 放在声明 (头文件) 中,如果您不想或不需要修改变量,请将其放在定义 (实现) 中。


6
我同意这一点,但是让声明和定义不同的想法让我感到有些不舒服。对于除了“const”之外的其他内容,声明和(定义的原型部分)通常是相同的。 - Keith Thompson
@KeithThompson:如果你不想这样做,那就不用做。在声明中不加const完全由你决定。如果你想在实现中添加限定符,那也可以。 - Kerrek SB
1
正如我所说,我同意在声明中放置const而不是定义是有道理的。只是感觉这是语言中唯一一个使声明和定义不相同有意义的情况,这让人感到有点奇怪。(当然,这并不是C语言唯一的缺陷。) - Keith Thompson

18
const int x = 42;

or as

#define x 42

The first method is preferable because it ensures type-checking and has other advantages. The second method is a preprocessor macro and does not have type-checking.

Declare a pointer to a constant variable.

This can be done as

const int *px = &x;

This means that the value of *px cannot be changed, but px itself can be assigned to point to a different constant variable.

Declare a constant pointer to a variable.

This can be done as

int y; int *const py = &y;

This means that py always points to y, but the value of y can be changed through py.

Declare a constant pointer to a constant variable.

This can be done as

const int *const pxy = &x;

This means that both the value of *pxy and pxy itself cannot be changed.

const int X=1; or
int const X=1;

这两种形式是完全等效的。后一种方式被认为是不好的风格,不应该使用。

第二行被认为是不好的风格,可能是因为“存储类说明符”(如static和extern)也可以在实际类型之后声明,例如int static等。但是,C委员会(ISO 9899 N1539草案,6.11.5)将此类存储类说明符标记为过时特性。因此,出于统一性考虑,也不应以这种方式编写类型限定符。这只会让读者感到困惑,没有其他用途。

声明指向常量变量的指针。

const int* ptr = &X;

这意味着 'X' 的内容无法被修改。这是声明指针的正常方式,尤其是作为“const正确性”的函数参数的一部分。因为 'X' 实际上不必声明为const,它可以是任何变量。换句话说,您始终可以将变量“升级”为const。从技术上讲,C语言也允许通过显式类型转换从const降级为普通变量,但这样做被认为是不良的编程习惯,并且编译器通常会发出警告。

声明一个常量指针

int* const ptr = &X;

这意味着指针本身是常量。您可以修改它所指向的内容,但不能修改指针本身。这没有太多用途,有一些用途,比如确保在将指向指针(指针指针)作为参数传递给函数时,其地址不会更改。您需要编写类似于以下内容的不太易读的代码:

void func (int*const* ptrptr)

我怀疑许多C程序员无法正确地在其中使用const和*。我知道我做不到 - 我不得不检查GCC。我认为这就是为什么即使它被认为是良好的编程实践,你很少看到指向指针的语法。

常量指针也可以用于确保指针变量本身在只读内存中声明,例如您可能希望声明某种基于指针的查找表并将其分配给NVM。

当然,正如其他回答所示,常量指针也可以用于强制执行“常量正确性”。

声明一个指向常量数据的常量指针

const int* const ptr=&X;

这是上述两种指针类型的结合,具有它们两者的所有属性。

如何声明一个只读成员函数(C++)

既然标记了C++,我应该也提一下你可以将类的成员函数声明为const。这意味着在调用该函数时,不允许修改类的其他成员,这既可以防止类的程序员意外出错,也可以告知调用该成员函数的调用方他们不会因调用它而搞乱任何东西。语法如下:

void MyClass::func (void) const;

17

顶层const限定符在声明中被丢弃,因此问题中的声明声明了完全相同的函数。另一方面,在定义(实现)中,编译器将验证如果将指针标记为const,则其在函数体内不会被修改。


我不知道这一点。所以,如果我尝试使用void foo(int* const ptr)重载void foo(int* t ptr),我将会得到编译器错误。谢谢! - R. Ruiz.
如果你的声明是void foo(int* t ptr),而你的定义是void foo(int* const ptr),那么你将不会得到编译器错误。 - Trevor Hickey
1
@TrevorHickey 他们会……但不是他们想的那个原因。int* t ptr 是语法错误。没有它,两者在重载目的上是相同的。 - underscore_d

14

你说得对,对调用者来说并没有任何区别。但对函数作者来说,它可以是一种安全保障“好的,我需要确保我不会把这个指针指向错误的东西”。虽然不是非常有用,但也不是毫无用处。

就像在程序中有一个int const the_answer = 42一样。


1
它们指向哪里有什么关系,它是在堆栈上分配的指针?函数无法搞砸它。在处理指向指针时使用只读指针更有意义。这与int const不是同一回事!我会因为这个误导性的陈述而投反对票。const int和int const是等效的,而const int和int const具有两个不同的含义! - Lundin
1
@lundin 假设这个函数很大并且还有其他东西。不小心可能会使它指向其他东西(在函数的堆栈上)。这对调用者来说并不是问题,但对被调用者来说肯定是问题。我在我的陈述中没有看到任何误导性:“对于函数的编写者,它可以是一个安全网”。 - cnicutar
1
关于“int const”部分,我已经将类型放在const之前(听起来不太自然)有一段时间了,并且我知道它们之间的区别。这篇文章可能会有所帮助。不过,我自己转换到这种风格的原因略有不同。 - cnicutar
1
好的。我刚刚写了一个冗长的答案,以防其他人除了你和我之外也在阅读这篇帖子。我还解释了为什么int const是不好的风格,而const int是好的风格。 - Lundin
Lundin,我也在阅读!虽然如此,我同意const int是更好的风格。@cnicutar,你对Lundin的第一次回应就是我提交这个问题时所寻找的。因此问题不在调用者(像我一样毫无头绪地想),而是const是对被调用者的保证。我喜欢这个想法,谢谢。 - R. Ruiz.
显示剩余2条评论

8

......今天我意识到指针被按值传递给函数,这很有道理。

(在我看来)作为默认选项真的没有太多意义。更合理的默认选项是将其作为不可重新分配的指针传递(int* const arg)。也就是说,我更喜欢将传递的指针隐式声明为const。

那么const有什么好处呢?

好处在于当你修改参数指向的地址时,很容易让人感到困惑,从而引入错误,如果它不是const的话。修改地址是不常见的操作。如果你的意图是修改地址,那么创建一个局部变量会更清晰。此外,原始指针操作是引入错误的简单方式。

因此,通过不可变地址进行传递并在需要修改参数指向的地址时创建副本(在那些不寻常的情况下),会更加清晰:

void func(int* const arg) {
    int* a(arg);
    ...
    *a++ = value;
}

补充说明,使用本地变量几乎是免费的,它可以降低错误的发生概率,同时提高可读性。

在更高的层面上,如果你要将参数作为数组进行操作,通常将参数声明为容器/集合会更清晰、更少出现错误,对客户端更友好。

总的来说,向值、参数和地址添加const是一个好主意,因为你并不总是意识到副作用,而编译器会很高兴地强制执行。因此,它与其他几种情况中使用的const一样有用(例如,“为什么应该声明值为const?”)。幸运的是,我们还有引用,它们不能被重新分配。


4
+1 表示支持此默认设置。与大多数编程语言一样,C++ 的做法是错误的。它应该有一个 "可变的" 关键字来代替 "const" 关键字(虽然它已经有了,但是语义使用方式错了)。 - Konrad Rudolph
2
有趣的想法,将const作为默认值。感谢您的回答! - R. Ruiz.

6

同样的问题也可以针对其他类型(不仅仅是指针)进行提问:

/* Why is n const? */
const char *expand(const int n) {
    if (n == 1) return "one";
    if (n == 2) return "two";
    if (n == 3) return "three";
    return "many";
}

哈哈,你说得对。指针的情况首先浮现在我的脑海中,因为我认为它们很特别,但实际上它们就像通过值传递的任何其他变量一样。 - R. Ruiz.

6
如果您从事嵌入式系统或设备驱动程序编程,其中涉及内存映射设备,则通常使用两种形式的 'const',一种是为了防止指针被重新分配(因为它指向固定的硬件地址)。如果它所指向的外设寄存器是只读硬件寄存器,则另一个 const 将在编译时检测到许多错误,而不是在运行时。
一个只读的16位外设芯片寄存器可能长这样:
static const unsigned short *const peripheral = (unsigned short *)0xfe0000UL;
然后,您可以轻松地读取硬件寄存器,而无需使用汇编语言:
input_word = *peripheral;

5

int iVal = 10; int *const ipPtr = &iVal

与普通的 const 变量一样,const 指针必须在声明时初始化,并且其值不能被更改。

这意味着 const 指针将始终指向同一个值。在上面的例子中,ipPtr 将始终指向 iVal 的地址。但是,由于被指向的值仍然是非 const 的,因此可以通过解引用指针来更改被指向的值:

*ipPtr = 6; // 允许,因为 ipPtr 指向一个非 const 的 int


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接