关于C++:const指针的意义是什么?

What's the point of const pointers?

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

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

那么,有一个函数头的意义是什么呢?

1
void foo(int* const ptr);

在这样的函数中,不能使ptr指向其他对象,因为它是常量,您不希望修改它,而是像这样的函数:

1
void foo(int* ptr);

工作也做得很好!因为指针是以任何方式复制的,即使修改了副本,调用方中的指针也不会受到影响。那么警察的优势是什么呢?


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

Find bugs at compile-time, rather than run-time, by getting the compiler to enforce what you mean.

尽管它不会改变功能,但是添加const会在您做不想做的事情时生成一个编译器错误。想象一下以下打字错误:

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

如果使用int* const,这将生成一个编译器错误,因为您要将值更改为ptr。一般来说,通过语法添加限制是一件好事。不要太过分——你举的例子是,大多数人不需要使用const


我强调只使用const参数,因为这样可以进行更多的编译器检查:如果我不小心在函数内部重新分配了参数值,编译器会咬我。

我很少重用变量,创建新的变量来保存新的值比较干净,所以基本上我所有的变量声明都是const,除了一些情况,比如循环变量,const会阻止代码工作。

注意,这只在函数的定义中有意义。它不属于声明,这是用户看到的。用户不关心我是否使用const作为函数内部的参数。

例子:

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

注意参数和局部变量是怎样的const。这也不是必需的,但功能更大一些,这让我反复犯错。


您的问题涉及到更一般的内容:函数参数应该是常量吗?

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

1
void foo(T);

完全取决于函数的实现者,她是希望以可变的方式还是以恒定的方式使用函数范围参数变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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,它不会在函数体中被修改。


const关键字有很多,它是一个相当复杂的关键字。一般来说,在程序中添加大量常量被认为是一种良好的编程实践,在Web上搜索"常量正确性",您会发现很多关于这个的信息。

const关键字是所谓的"类型限定符",其他的是volatilerestrict。至少volatile遵循与const相同的(混淆的)规则。

首先,const关键字有两个用途。最明显的一个方法是通过使数据(和指针)只读来防止故意或意外地误用。编译器将在编译时发现任何修改常量变量的尝试。

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

使用const关键字有几种不同的方法:

声明常量变量。

这可以作为

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

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

第二行被视为不好的样式的原因可能是"存储类说明符",如static和extern也可以在实际类型、int static等之后声明,但是这样做对于存储类说明符被C委员会标记为过时的功能(ISO 9899 N1539草案,6.11.5)。因此,为了一致性,也不应该以这种方式编写类型限定符。不管怎样,它除了迷惑读者之外,没有别的用处。

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

1
const int* ptr = &X;

这意味着不能修改"x"的内容。这是像这样声明指针的正常方法,主要是作为"const correction"函数参数的一部分。因为"x"实际上不必声明为const,所以它可以是任何变量。换句话说,您可以将变量"升级"为常量。从技术上讲,C还允许通过显式类型转换将const降级为普通变量,但这样做被认为是不好的编程,编译器通常会发出警告。

声明常量指针

1
int* const ptr = &X;

这意味着指针本身是常量。您可以修改它指向的内容,但不能修改指针本身。它没有很多用途,有一些用途,比如确保指向的指针(指向指针的指针)在作为参数传递给函数时不更改其地址。你必须写一些不太可读的东西,比如:

1
void func (int*const* ptrptr)

我怀疑很多C程序员都能把const和*放在里面。我知道我不能-我必须和GCC核实一下。我认为这就是为什么您很少看到指针指向指针的语法,尽管它被认为是良好的编程实践。

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

当然,如其他答案所示,常量指针也可以用来强制"常量正确性"。

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

1
const int* const ptr=&X;

这是上面描述的两种指针类型的组合,它们的所有属性都是。

声明只读成员函数(C++)

由于这是标记C++,我还应该提到,您可以声明一个类的成员函数为const。这意味着函数在被调用时不允许修改类的任何其他成员,这既可以防止类的程序员意外出错,也可以通知成员函数的调用者他们不会通过调用它来弄乱任何东西。语法是:

1
void MyClass::func (void) const;


你说得对,对打电话的人来说,这完全没有区别。但是对于这个函数的作者来说,它可以是一个安全网,"好吧,我需要确保我没有把这一点指向错误的事情"。不是很有用,也不是没用。

这基本上与在程序中使用int const the_answer = 42相同。


...today I realized that pointers are passed by value to functions, which makes sense.

(imo)作为违约,这真的没有意义。更合理的默认值是作为不可重新分配的指针传递(int* const arg)。也就是说,我宁愿将作为参数传递的指针隐式声明为const。

So what's the advantage of const?

其优点是,当您修改参数指向的地址时,它非常容易,有时也不清楚,这样,当错误不是很容易创建时,您就可以引入它。改变地址是不典型的。如果您的目的是修改地址,那么更清楚地创建一个局部变量。此外,原始指针操作是引入错误的一种简单方法。

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

1
2
3
4
5
void func(int* const arg) {
    int* a(arg);
    ...
    *a++ = value;
}

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

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

一般来说,将const添加到值、参数和地址是一个好主意,因为您并不总是了解编译器乐于实施的副作用。因此,它与其他几种情况下使用的const一样有用(例如,问题类似于"为什么要声明值const?")。幸运的是,我们还有引用,不能重新分配。


如果您在有内存映射设备的地方进行嵌入式系统或设备驱动程序编程,则通常使用两种形式的"const",一种是防止指针重新分配(因为它指向固定的硬件地址),另一种是,如果它指向的外围寄存器是只读硬件寄存器,则另一个const将检测到大量编译时出错,而不是运行时出错。

一个只读的16位外围芯片寄存器可能看起来像:

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

然后,您可以轻松地读取硬件寄存器,而无需使用汇编语言:

input_word = *peripheral;


您的问题实际上是关于为什么将任何变量定义为常量而不仅仅是函数的常量指针参数。这里应用的规则与将任何变量定义为常量(如果变量是函数或成员变量的参数或局部变量)时相同。

在您的特定情况下,从功能上讲,它不像在许多其他情况下将局部变量声明为常量那样有区别,但它确实设置了一个限制,即您不能修改该变量。


对于任何其他类型(不仅仅是指针),都可以问同样的问题:

1
2
3
4
5
6
7
/* 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";
}


IVAL=10;int*const ipptr=&ival;

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

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

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


一个常量指针高度适用的例子可以很好地演示。假设您有一个包含动态数组的类,并且您希望将用户访问权传递给该数组,但不授予他们更改指针的权限。考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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
"
,Temp.GetArray());
    Temp.GetArray()[0] = ' '; //You can still modify the chars in the array, user has access
    Temp.GetArray()[1] = ' ';
    printf("%s
"
,Temp.GetArray());
}

产生:

Input data
put data

但如果我们尝试一下:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
    TestA Temp;
    printf("%s
"
,Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s
"
,Temp.GetArray());
    Temp.GetArray() = NULL; //Bwuahahahaa attempt to set it to null
}

我们得到:

error: lvalue required as left operand of assignment //Drat foiled again!

所以很明显,我们可以修改数组的内容,但不能修改数组的指针。如果您想确保指针在返回给用户时具有一致的状态,那就太好了。不过,有一个要点:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
    TestA Temp;
    printf("%s
"
,Temp.GetArray());
    Temp.GetArray()[0] = ' ';
    Temp.GetArray()[1] = ' ';
    printf("%s
"
,Temp.GetArray());
    delete [] Temp.GetArray(); //Bwuahaha this actually works!
}

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

因此,如果您希望内存引用始终指向某个对象(即永远不要修改,类似于引用当前的工作方式),那么它非常适用。如果您希望用户拥有完全访问权并对其进行修改,那么非常量就是为您准备的。

编辑:

在注意到Okorz001注释由于getArray()是正确的值操作数而无法赋值后,他的注释是完全正确的,但如果要返回对指针的引用(我假设getArray引用的是引用),则上述注释仍然适用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}

将返回导致错误的第一个:

error: assignment of read-only location 'Temp.TestA::GetArray()'

但第二种情况会很好地发生,尽管其背后可能会有潜在的后果。

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

考虑一下,如果我们有一个接受常量指针的函数(而不是不接受常量指针的函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
"
,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
"
,Data[1]);
        }
};

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

常量中的错误会产生以下消息:

error: decrement of read-only parameter 'Data'

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

NonConst: A
Const: B

显然,即使是"data[1]",它也被视为"data[0]",因为非字符串指针允许递减操作。当const实现后,正如另一个人所写,我们会在潜在的bug发生之前捕获它。

另一个主要考虑因素是常量指针可以用作伪引用,因为引用点所指向的对象不能更改(人们想知道,这是否是实现它的方法)。考虑:

1
2
3
4
5
6
7
8
9
int main()
{
    int A = 10;
    int * const B = &A;
    *B = 20; //This is permitted
    printf("%d
"
,A);
    B = NULL; //This produces an error
}

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

error: assignment of read-only variable 'B'

如果需要经常引用a,这可能是一件坏事。如果B = NULL被注释掉,编译器会很高兴地让我们修改*B和a。这对于ints可能不太有用,但是考虑一下,如果您有一个图形应用程序的单一姿态,您需要一个引用它的不可修改指针,您可以传递它。

它的用法是可变的(请原谅意料之外的双关语),但是使用正确,它是盒子中另一个帮助编程的工具。


我想一个优点是编译器可以在函数内部执行更具攻击性的优化,因为知道这个指针不能更改。

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


将常量指针传递给函数毫无意义,因为它无论如何都将通过值传递。它只是通用语言设计允许的一种功能。仅仅因为没有意义而禁止它,只会使语言规范更大。

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


声明任何变量的类型,如-
(1)声明常量变量。
DataType const varibleName;
ZZU1〔0〕
(2)声明指向常量变量的指针
const dataType* PointerVaribleName=&X;
ZZU1〔1〕
dataType* const PointerVaribleName=&X;
ZZU1〔2〕


我相信这将防止代码在函数体中增加或减少指针。


指针没有什么特别的地方,你永远不会希望它们是常量。正如类成员常量int值一样,也可以有常量指针,原因类似:您希望确保没有人更改所指向的内容。C++引用有点解决这个问题,但是指针行为是从C.继承的。