【问题标题】:What's the point of const pointers?const 指针有什么意义?
【发布时间】:2011-12-04 15:33:20
【问题描述】:

我说的不是指向 const 值的指针,而是 const 指针本身。

我正在学习 C 和 C++,而不是非常基础的东西,直到今天我才意识到指针是按值传递给函数的,这是有道理的。 这意味着在函数内部,我可以使复制的指针指向其他值,而不会影响调用者的原始指针。

那么有一个函数头有什么意义:

void foo(int* const ptr);

在这样的函数中,你不能让 ptr 指向别的东西,因为它是 const 并且你不希望它被修改,而是这样的函数:

void foo(int* ptr);

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

【问题讨论】:

  • 如果你想在编译时保证指针不能也不应该被修改为指向别的东西怎么办?
  • 与任何const 参数完全相同。
  • @PlatinumAzure,我有几年的编程经验,但我的软件实践欠缺。我很高兴我提出了这个问题,因为我从来没有觉得需要让编译器指出我自己的逻辑错误。我提交问题时的最初反应是“我为什么要关心指针是否被修改?它不会影响调用者”。直到我阅读了所有答案,我才明白我应该关心,因为如果我尝试修改它,我和我的编码逻辑是错误的(或者有一个错字,比如缺少星号),如果编译器没有,我可能还没有意识到告诉我。
  • @R.Ruiz。毫无疑问,即使是我们中最有经验的人也可以额外保证正确性。因为软件工程是一种可以接受更多余地的工程形式(随机的 HTTP 502、缓慢的连接、偶尔无法加载的图像并非特殊情况,但飞机上的引擎故障是不可接受的并且可能很严重),有时人们过分仓促地编程。证明编写单元测试的同一论点将解释使用const-正确性保证。它只是让我们更加确信我们的代码无疑是正确的。

标签: c++ c pointers constants


【解决方案1】:

const 是一个工具,你应该使用它来追求一个非常重要的 C++ 概念:

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

即使它不会改变功能,但添加const 会在您执行您不打算执行的操作时生成编译器错误。想象一下以下错字:

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

如果您使用int* const,则会产生编译器错误,因为您将值更改为ptr。通过语法添加限制通常是一件好事。只是不要走得太远——你给出的例子是大多数人都懒得使用const

【讨论】:

  • 谢谢,这是一个让我信服的答案。您放置 const 以便编译器警告您有关您自己的赋值错误。您的示例非常适合说明此概念,因为这是指针的常见错误。比你!
  • “帮助编译器帮助你”是我通常为此吟诵的口头禅。
  • +1,但这里有一个值得商榷的有趣案例:stackoverflow.com/questions/6305906/…
  • 所以这与您对“为什么将变量设为私有,当您不能在课堂外使用它们时给出的答案相同。”
  • 这可能有点跑题了,但是这个 const 指针在内存中的什么位置呢?我知道所有 const 都位于内存的全局部分,但我不能 100% 确定你作为 func var 传入的 const。如果这是题外话,谢谢和抱歉
【解决方案2】:

我强调只使用 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。两者都不是必要的,但功能稍微大一点,这让我一再避免犯错。

【讨论】:

  • +1 来自另一个 const-痴迷的狂热分子。但是,我更喜欢我的编译器对我咆哮。我犯了太多错误,如果他们咬我会很痛苦。
  • +1,我强烈推荐“const,除非有充分的理由”策略。不过也有一些很好的例外,例如Copy and swap
  • 如果我正在设计一种新语言,默认情况下声明的对象将是只读的(“const”)。您需要一些特殊的语法,也许是“var”关键字,才能使对象可写。
  • @Keith 我很想说“当然”。在这一点上,其他一切都是愚蠢的。但不幸的是,大多数语言设计师似乎不同意我们的观点……事实上,我已经发布了很多(在一个现已删除的问题中,“你最有争议的编程观点是什么?”)。
  • @KubaOber 我根本看不出这是一种悲观。如果您后来发现需要修改在函数体中传递的内容,那么只需从参数中删除 const,并最好注释原因。这一点额外的工作并不能证明默认情况下将所有参数标记为非const 并让自己面对所有可能产生的错误。
【解决方案3】:

您的问题涉及更笼统的问题:函数参数应该是 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 放在声明(标题)中,如果您不想或不需要修改变量,请将其放在定义(实现)中。

【讨论】:

  • 我同意这一点——但我对使声明和定义不同的想法有些不舒服。对于const 以外的东西,声明和定义的(原型部分)通常是相同的。
  • @KeithThompson:好吧,如果你不想这样做,你就不必这样做。只是不要将const 放在声明中。如果您想在实现中添加限定符,这完全取决于您。
  • 正如我所说,我同意将const 放在声明而不是定义上是有意义的。感觉就像语言中的一个小故障,这是使声明和定义不相同的唯一情况是有意义的。 (当然,这不是 C 的唯一故障。)
【解决方案4】:

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

【讨论】:

  • 我不知道。所以如果我尝试用 void foo(int* t ptr) 重载 void foo(int* const ptr) 我会得到一个编译器错误。谢谢!
  • @R.Ruiz。如果你的减速是 void foo(int* t ptr) 并且你的定义是 void foo(int* const ptr),你不会得到编译器错误。
  • @TrevorHickey 他们会……但不是他们想的那样。 int* t ptr 是语法错误。没有它,两者对于重载目的是相同的。
【解决方案5】:

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

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


首先,const 关键字有两个用途。最明显的一种方法是通过将数据(和指针)设置为只读来保护数据(和指针)免受有意或意外的滥用。编译器会在编译时发现任何修改 const 变量的尝试。

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

有几种不同的方式可以使用const 关键字:

声明一个常量变量。

这可以作为

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

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

之所以认为第二行是不好的样式,可能是因为静态和外部等“存储类说明符”也可以在实际类型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;

【讨论】:

    【解决方案6】:

    你是对的,对于调用者来说,这完全没有区别。但是对于函数的编写者来说,它可以是一个安全网“好吧,我需要确保我没有把这一点指向错误的东西”。不是很有用,但也不是没用。

    这与在您的程序中添加int const the_answer = 42 基本相同。

    【讨论】:

    • 指向它的位置有什么关系,它是分配在堆栈上的指针吗?该功能不能搞砸。在处理指针到指针时使用只读指针更有意义。它与 int const 是 not 相同的!我会因为那个误导性的陈述而对此投反对票。 const intint const是等价的,而const int*int* const有两种不同的含义!
    • @lundin 让我们假设函数很大并且其中包含其他内容。意外地,它可能指向别的东西(在函数的堆栈上)。这对调用者来说没有问题,但对于被调用者来说肯定是有问题的。我没有在我的陈述中看到任何误导性:“对于函数的编写者,它可以是一个安全网”。
    • @Lundin 关于int const 部分;一段时间以来,我一直将类型放在 const 之前(听起来确实不自然),并且我知道其中的差异。 This article 可能很有用。然而,我自己切换到这种风格的原因略有不同。
    • 好的。我只是自己写了一个冗长的答案,以防除您和我之外的其他人正在阅读这篇文章。我还说明了为什么 int const 是不好的风格而 const int 是好的风格。
    • 伦丁,我也在看书!我同意 const int 是更好的风格。 @cnicutar,您对 Lundin 的第一反应是我在提交此问题时所寻找的。所以问题不在于调用者(正如我盲目地认为的那样),而是 const 是被调用者的保证。我喜欢这个主意,谢谢。
    【解决方案7】:

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

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

    那么 const 有什么好处呢?

    优点是当你修改参数指向的地址时它很容易并且有时不清楚,这样你可以很容易地在它不是 const 时引入一个错误。更改地址是非典型的。如果您的意图是修改地址,则创建局部变量会更清楚。同样,原始指针操作是引入错误的一种简单方法。

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

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

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

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

    一般来说,将 const 添加到值、参数和地址是一个好主意,因为您并不总是意识到编译器很乐意强制执行的副作用。因此,它与其他几种情况下使用的 const 一样有用(例如,问题类似于“为什么我应该声明值 const?”)。幸运的是,我们也有引用,无法重新分配。

    【讨论】:

    • +1,这是默认值。像大多数语言一样,C++ 有错误的方式。它应该有一个mutable 关键字而不是const 关键字(嗯,它有,但语义错误)。
    • 将 const 作为默认值的有趣想法。感谢您的回答!
    【解决方案8】:

    如果您在有内存映射设备的情况下进行嵌入式系统或设备驱动程序编程,那么通常会使用两种形式的“const”,一种是为了防止指针被重新分配(因为它指向一个固定的硬件地址。)并且,如果它指向的外设寄存器是只读硬件寄存器,那么另一个 const 将在编译时而不是运行时检测到很多错误。

    只读的 16 位外设芯片寄存器可能如下所示:

    static const unsigned short *const peripheral = (unsigned short *)0xfe0000UL;

    然后您可以轻松读取硬件寄存器,而无需求助于汇编语言:

    input_word = *peripheral;

    【讨论】:

      【解决方案9】:

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

      就像一个普通的 const 变量一样,一个 const 指针必须在声明时初始化为一个值,并且它的值不能改变。

      这意味着 const 指针将始终指向相同的值。在上述情况下,ipPtr 将始终指向 iVal 的地址。但是,由于指向的值仍然是非常量,因此可以通过取消引用指针来更改指向的值:

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

      【讨论】:

        【解决方案10】:

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

        /* 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";
        }
        

        【讨论】:

        • 大声笑,你是对的。我首先想到了指针的情况,因为我认为它们很特别,但它们就像任何其他按值传递的变量一样。
        【解决方案11】:

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

        在您的特定情况下,当您将局部变量声明为 const 时,它在功能上并没有像在许多其他情况下那样有所不同,但它确实限制了您不能修改此变量。

        【讨论】:

        • 您的评论让我意识到我的问题是多么无用,因为您是对的:这与将任何其他参数作为 const 相同。它可能看起来没用,但它可以帮助程序员有这个限制并避免错误
        【解决方案12】:

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

        如果你在一个函数内部,那当然是另一种情况。拥有一个不能改变它所指向的指针的断言是使代码更清晰的断言。

        【讨论】:

          【解决方案13】:

          我想一个优点是编译器可以在知道这个指针不能改变的情况下在函数内部执行更积极的优化。

          它也避免了例如。将此指针传递给接受非 const 指针引用的子函数(因此可以像 void f(int *&p) 一样更改指针),但我同意,在这种情况下有用性有所限制。

          【讨论】:

          • +1 用于优化。至少书上说这是真的。我想确切了解这些优化是什么
          【解决方案14】:

          因此可以演示 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
          }
          

          我们得到:

          error: lvalue required as left operand of assignment //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 永远不会被修改,类似于引用当前的工作方式),那么它非常适用。如果您希望用户拥有完全访问权限并对其进行修改,那么非常量适合您。

          编辑:

          在注意到 okorz001 注释由于 GetArray() 是一个右值操作数而无法赋值之后,他的注释是完全正确的,但是如果您要返回对指针的引用,上述内容仍然适用(我想我假设 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 指针的函数(与不接受的函数相比):

          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'递减

          这很好,因为我们可能不想这样做,除非我们想引起 cmets 中指出的问题。如果我们在 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。这对于 int 可能看起来没有用,但考虑一下你是否有一个图形应用程序的单一立场,你想要一个不可修改的指针来引用到它,你可以传递。

          它的用法是可变的(请原谅无意的双关语),但如果使用正确,它是盒子里另一个辅助编程的工具。

          【讨论】:

          • Temp.GetArray() = NULL 失败是因为 Temp.GetArray() 是一个右值,而不是因为它是 const 限定的。此外,我相信 const 限定符已从所有返回类型中剥离。
          • @okorz001:经过测试,您确实是正确的。但是,如果您返回对指针本身的引用,则上述情况适用。我会相应地编辑我的帖子。
          【解决方案15】:

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

          【讨论】:

            【解决方案16】:

            我相信这会阻止代码增加或减少函数体内的指针。

            【讨论】:

              【解决方案17】:

              声明任何变量的类型,如-
              (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 提供给函数作为参数
              猜你喜欢
              • 2018-02-09
              • 2011-06-30
              • 1970-01-01
              • 1970-01-01
              • 2010-09-18
              • 2011-07-01
              • 2010-12-24
              • 2012-08-07
              • 1970-01-01
              相关资源
              最近更新 更多