const指针的意义是什么?


149

我不是在谈论const值的指针,而是const指针本身。

我在学习C和C ++的基础知识之外,直到今天,我才意识到指针是通过值传递给函数的,这很有意义。这意味着在函数内部,我可以使复制的指针指向其他某个值,而不会影响调用者的原始指针。

那么,有一个函数标头说:

void foo(int* const ptr);

在这样的函数中,您不能使ptr指向其他东西,因为它是const并且您不希望对其进行修改,而是这样的函数:

void foo(int* ptr);

工作也一样!因为无论如何都会复制指针,并且即使您修改了副本,调用者中的指针也不会受到影响。那么const的优点是什么?


29
如果要在编译时保证指针不能并且不应修改为指向其他内容,该怎么办?
白金Azure

25
与任何const参数完全相同。
David Heffernan

2
@PlatinumAzure,我有几年编程经验,但缺乏软件实践。我很高兴提出这个问题,因为我从未感到有必要让编译器指出我自己的逻辑错误。在我提交问题时,我的最初反应是“为什么我应该关心指针是否被修改?它不会影响调用者”。在阅读完所有答案之前,我知道我应该在意,因为如果我尝试修改它,我和我的编码逻辑是错误的(或者有错字,例如缺少星号),并且我可能还没有意识到编译器是否告诉我。
R. Ruiz。

3
@ R.Ruiz。毫无疑问,即使是我们中最有经验的人也可以额外保证正确性。因为软件工程是一种工程形式,可以接受更多的回旋余地(随机HTTP 502,慢速连接,偶尔无法加载图像的情况并不常见,但是飞机引擎故障是不可接受的,并且可能很严重),有时人民程序过度匆忙。证明编写单元测试合理的相同论点将解释const-correctness保证的使用。这只是让我们更加确定我们的代码正确无误。
白金Azure

Answers:


207

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

通过使编译器执行您的意思,在编译时而不是运行时查找错误。

即使不改变功能,添加const内容也会在您执行本不希望执行的操作时产生编译器错误。想象以下错别字:

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

如果使用int* const,这将产生编译器错误,因为您将值更改为ptr。通常,通过语法添加限制是一件好事。只是不要太远-您举的例子是大多数人不用费心的情况const


8
谢谢,这是一个使我信服的答案。您放置const,以便编译器警告您有关自己的分配错误。您的示例很好地说明了此概念,因为这是指针的常见错误。比你!
R. Ruiz。

25
我通常会为此赞叹“帮助编译器帮助您”。
柔印

1
+1,但这是一个有趣的案例,值得争论:stackoverflow.com/questions/6305906/…–
Raedwald

3
因此,您将得到与“为什么不能在类外使用变量时为什么将变量设为私有”的相同答案。
李·路维耶

这可能有点不合时宜,但是此const指针在内存中位于何处?我知道所有const都位于内存的全局部分中,但是我不确定100%会以func var形式传入的const。感谢和抱歉,如果这不是主题
Tomer,

77

我之所以 使用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。两者都不是必需的,但是功能甚至更大一些,这使我免于犯错误。


17
+1来自另一个const痴迷的狂热者。但是,我更喜欢编译器对我咆哮。我犯了太多错误,他们会咬得很厉害。
2011年

2
+1,我强烈建议您采用“ const除非有充分的理由”策略。但是也有一些很好的例外,例如,复制和交换
Flexo

2
如果我正在设计一种新语言,则默认情况下,声明的对象将是只读的(“ const”)。您需要一些特殊的语法,例如“ var”关键字,以使对象可写。
基思·汤普森

@基思我很想说“当然”。在这一点上,其他一切都是愚蠢的。但是不幸的是,大多数语言设计师似乎不同意我们的观点……事实上,我已经发表了很多(在一个已删除的问题中,“您最有争议的编程观点是什么?”)。
康拉德·鲁道夫

1
@KubaOber我根本看不到它是多么的悲观。如果以后发现需要修改在函数主体中传递的内容,则只需const从参数中删除,最好注释一下原因。很少的额外工作并不能证明const默认情况下将所有参数标记为非自变量,并使自己不愿面对所有可能产生的错误。
underscore_d

20

您的问题涉及更笼统的问题:函数参数应为const吗?

值参数(如您的指针)常量性是一个实现细节,它并没有形成函数声明的一部分。这意味着您的功能始终是这样的:

void foo(T);

她是否要以可变或恒定方式使用functions-scope参数变量完全取决于函数的实现者:

// 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声明(标头),如果不想或不需要修改变量,则将其放入定义(实现)。


5
我同意这一点,但是我对使声明和定义有所不同的想法感到不满意。对于以外的其他东西const,声明和定义的(原型部分)通常是相同的。
基思·汤普森

@KeithThompson:好吧,如果您不想这样做,就不必这样做。只是不要放在const声明中。如果要在实现中添加限定符,完全由您决定。
Kerrek SB 2011年

1
正如我所说,我同意提出const声明而不是提出定义是有意义的。在语言中感觉就像是一个小故障,这是唯一使声明和定义不一致的情况。(当然,这几乎不是C的唯一故障。)
Keith Thompson

16

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


我不知道 因此,如果我尝试使用void foo(int * t ptr)重载void foo(int * const ptr),则会出现编译器错误。谢谢!
R. Ruiz。

@ 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 intint const是等效的,而const int*int* const具有两种不同的含义!
伦丁

1
@lundin让我们假设该函数很大,并且其中包含其他内容。偶然地,它可能指向其他对象(在函数堆栈上)。对于呼叫者来说这没有问题,但是对于被呼叫者来说当然可以。我的陈述中没有任何误导性的内容:“ 对于函数的编写者来说,它可以是一个安全网”。
cnicutar 2011年

1
@Lundin关于int const部分;我将类型放在const之前(听起来确实不自然)已经有一段时间了,我知道它们之间的区别。本文可能证明是有用的。我本人有一些略有不同的理由切换到这种风格。
cnicutar 2011年

1
好的。我只是写了一个长篇大论的答案,以防其他人(但您和我)正在阅读这篇文章。我还介绍了为什么int const是不良样式而const int是良好样式的理由。
伦丁

伦丁,我也在读书!我同意const int是更好的样式。@cnicutar,您对Lundin的第一个答复是我提交此问题时正在寻找的内容。因此,问题不在调用者中(正如我无意识地想到的那样),但const是对被调用者的保证。我喜欢这个主意,谢谢。
R. Ruiz。

14

const关键字很多,这是一个相当复杂的关键字。通常,在您的程序中添加很多const被认为是良好的编程习惯,在网络上搜索“ const正确性”,您会找到很多有关此的信息。

const关键字是所谓的“类型限定符”,其他是volatileand restrict。至少volatile遵循与const相同(令人困惑)的规则。


首先,const关键字有两个用途。最明显的方法是将数据(和指针)设置为只读,以防止它们被有意或无意地滥用。编译器会在编译时发现任何修改const变量的尝试。

但是,在任何具有只读存储器的系统中,还有另一个目的,即确保在该存储器中分配了某个变量-例如,它可以是EEPROM或闪存。这些被称为非易失性存储器NVM。当然,在NVM中分配的变量仍将遵循const变量的所有规则。

有多种使用const关键字的方法:

声明一个常量。

这可以通过以下方式完成

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”的内容无法修改。这是您这样声明指针的正常方式,主要是作为“常量正确性”的函数参数的一部分。因为实际上不必将'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;

8

...今天,我意识到指针是通过值传递给函数的,这很有意义。

(imo)作为默认设置确实没有意义。更明智的默认设置是将其作为不可分配的指针(int* const arg)传递。也就是说,我希望将作为参数传递的指针隐式声明为const。

那么const的优点是什么?

这样做的好处是它很容易,有时甚至在您修改参数所指向的地址时还不清楚,这样,当它不是const时就很容易引入错误。更改地址是非典型的。如果您打算修改地址,则创建一个局部变量会更加清楚。同样,原始指针操作也是引入错误的简便方法。

因此,当您想要更改参数所指向的地址时,通过不可变地址并创建副本(在那些非典型情况下)会更加清楚。

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

添加本地实际上是免费的,它减少了出错的机会,同时提高了可读性。

在更高的层次上:如果您将参数作为数组进行操作,则将参数声明为容器/集合的客户端通常更清晰,更容易出错。

通常,将const添加到值,参数和地址是一个好主意,因为您并不总是意识到副作用,而这些副作用是编译器乐意实现的。因此,它和const在其他几种情况下一样有用(例如,问题类似于“为什么要声明值const?”。)幸运的是,我们也有无法重新分配的参考。


4
+1是默认设置。与大多数语言一样,C ++的处理方式也是错误的。相反,具有的const关键字,它应该有一个mutable关键字(当然,它有,但与错误的语义)。
康拉德·鲁道夫

2
const为默认值的有趣想法。感谢您的回答!
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的地址。但是,由于指向的值仍然是非常量的,因此可以通过取消对指针的引用来更改指向的值:

* ipPtr = 6; //允许,因为pnPtr指向一个非常量int


5

关于任何其他类型(不仅是指针),可以提出相同的问题:

/* 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。

5

您的问题确实更多地是关于为什么将任何变量都定义为const而不仅仅是函数的const指针参数。与将任何变量定义为常量(如果它是函数的参数或成员变量或局部变量)在此处应用的规则相同。

在您的特定情况下,从功能上讲,它不会像在其他情况下将局部变量声明为const一样产生区别,但是它确实限制了您不能修改此变量。


您的评论使我意识到我的问题是徒劳的,因为您是对的:这与将其他任何参数用作const是同一回事。看起来似乎没什么用,但是它可以帮助程序员避免这种限制并避免错误
R. Ruiz。

4

将const指针传递给函数没有什么意义,因为无论如何它将通过值传递。这只是通用语言设计所允许的那些事情之一。仅仅因为它没有意义而禁止它只会使语言规范化。更大。

如果您在函数内部,则当然是另一种情况。拥有无法更改其指向的指针的断言可以使代码更加清晰。


4

我猜一个优点是,编译器可以知道该指针无法更改,从而可以在函数内部执行更积极的优化。

它还避免了。将此指针传递给接受非const指针引用的子函数(因此可能会更改指针,如void f(int *&p)),但我同意,在这种情况下,其实用性受到一定限制。


+1为优化。至少书上说的是真的。我想是准确了解这些优化是
R.鲁伊斯。

4

因此,可以证明const指针高度适用的示例。假设您有一个内部带有动态数组的类,并且希望将用户访问权传递给该数组,但又不授予他们更改指针的权利。考虑:

#include <new>
#include <string.h>

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const GetArray(){ return Array; }
};

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' '; //You can still modify the chars in the array, user has access
    Temp.GetArray()[1] = ' '; 
    printf("%s\n",Temp.GetArray());
}

产生:

输入数据
放置数据

但是,如果我们尝试这样做:

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s\n",Temp.GetArray());
    Temp.GetArray() = NULL; //Bwuahahahaa attempt to set it to null
}

我们得到:

错误:左值必须作为赋值的左操作数// Drat再次被挫败!

所以很明显,我们可以修改数组的内容,但不能修改数组的指针。如果希望在将指针传递回用户时确保指针具有一致的状态,则很好。但是有一个问题:

int main()
{
    TestA Temp;
    printf("%s\n",Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s\n",Temp.GetArray());
    delete [] Temp.GetArray(); //Bwuahaha this actually works!
}

即使我们不能修改指针本身,我们仍然可以删除指针的内存引用。

因此,如果您希望内存引用始终指向某个内容(IE永远都不会被修改,类似于引用当前的工作方式),那么它非常适用。如果您希望用户具有完全访问权限并对其进行修改,那么非常量适合您。

编辑:

在注意到由于GetArray()是一个右值操作数而无法分配okorz001注释后,他的注释完全正确,但是如果您要返回对指针的引用,以上注释仍然适用(我想我假设GetArray是引用参考),例如:

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const &GetArray(){ return Array; } //Note & reference operator
        char * &GetNonConstArray(){ return Array; } //Note non-const
};

int main()
{
    TestA Temp;
    Temp.GetArray() = NULL; //Returns error
    Temp.GetNonConstArray() = NULL; //Returns no error
}

将在第一个中返回并导致错误:

错误:分配只读位置'Temp.TestA :: GetArray()'

但是,尽管可能会对下面的情况造成不利影响,但第二次却是欢乐的。

显然,将提出一个问题:“为什么要返回对指针的引用”?在极少数情况下,您需要直接将内存(或数据)分配给相关的原始指针(例如,构建自己的malloc / free或new / free前端),但在这些情况下,这是一个非常量引用。对const指针的引用我还没有遇到可以保证使用它的情况(除非声明为const引用变量而不是返回类型?)。

考虑一下我们是否有一个使用const指针的函数(与不使用const指针的函数相比):

class TestA
{
    private:
        char *Array;
    public:
        TestA(){Array = NULL; Array = new (std::nothrow) char[20]; if(Array != NULL){ strcpy(Array,"Input data"); } }
        ~TestA(){if(Array != NULL){ delete [] Array;} }

        char * const &GetArray(){ return Array; }

        void ModifyArrayConst(char * const Data)
        {
            Data[1]; //This is okay, this refers to Data[1]
            Data--; //Produces an error. Don't want to Decrement that.
            printf("Const: %c\n",Data[1]);
        }

        void ModifyArrayNonConst(char * Data)
        {
            Data--; //Argh noo what are you doing?!
            Data[1]; //This is actually the same as 'Data[0]' because it's relative to Data's position
            printf("NonConst: %c\n",Data[1]);
        }
};

int main()
{
    TestA Temp;
    Temp.ModifyArrayNonConst("ABCD");
    Temp.ModifyArrayConst("ABCD");
}

const中的错误因此产生消息:

错误:只读参数“ Data”的减量

这样做很好,因为我们可能不想这样做,除非我们要引起注释中指出的问题。如果我们在const函数中编辑减量,则会发生以下情况:

非常量:A
常量:B

显然,即使A是“ Data [1]”,也将其视为“ Data [0]”,因为NonConst指针允许进行减量操作。正如其他人所写,实现const后,我们在潜在错误发生之前就将其捕获。

另一个主要考虑因素是,可以将const指针用作伪引用,因为该引用指向的内容无法更改(一个人想知道,也许这是如何实现的)。考虑:

int main()
{
    int A = 10;
    int * const B = &A;
    *B = 20; //This is permitted
    printf("%d\n",A);
    B = NULL; //This produces an error
}

尝试编译时,产生以下错误:

错误:分配只读变量“ B”

如果要不断引用A,那可能是一件坏事。如果B = NULL被注释掉,编译器会很乐意让我们修改*B,因此对A进行。这对于ints似乎没有用,但请考虑您是否对图形应用程序有单一的立场,希望在其中使用一个不可修改的指针来引用它,然后再传递给它。周围。

它的用法是可变的(例如,意外的双关语),但是用法正确,它是包装盒中另一个有助于编程的工具。


2
Temp.GetArray() = NULL失败Temp.GetArray()是因为它是一个右值,而不是因为它是const限定的。此外,我相信const限定符已从所有返回类型中删除。
奥斯卡·科尔兹

@ okorz001:经过测试,您确实是正确的。但是,如果您返回对指针本身的引用,则上述内容适用。我将相应地编辑我的帖子。
SSight3 2011年

3

指针没有什么特别的,您永远不会希望它们成为const。正如可以拥有类成员常int量值一样,出于类似原因,也可以具有常量指针:您要确保没有人更改所指向的内容。C ++引用在某种程度上解决了这个问题,但是指针行为是从C继承的。



3

声明任何变量的类型,例如-
(1)声明常量。
DataType const varibleName;

 int const x;
    x=4; //you can assign its value only One time
(2)声明一个指向常量的指针
const dataType* PointerVaribleName=&X;
 const int* ptr = &X;
     //Here pointer variable refer contents of 'X' that is const Such that its cannot be modified
dataType* const PointerVaribleName=&X;
 int* const ptr = &X;
     //Here pointer variable itself is constant  Such that value of 'X'  can be modified But pointer can't be modified


当我们不希望更改其实际值时,我们将指针变量作为const提供给函数作为参数
Pyadav 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.