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 int和int 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.