【问题标题】:Confused when I should and shouldn't use "const" in C当我应该和不应该在 C 中使用“const”时感到困惑
【发布时间】:2009-04-02 17:14:22
【问题描述】:

我有一本这样的字典:

typedef struct dictNode {
    int key;
    char *value;
    struct dictNode *next;
} Dict;

还有一个 get() 函数,如下所示:

char *get(const Dict *dict, int key) {
    if(!dict) return NULL;

    Dict *currPtr = dict;

    while(currPtr) {
        if(currPtr->key == key) {
            return currPtr->value;
        }

        currPtr = currPtr->next;
    }
}

编译此代码会产生以下错误:
dict.c:85: 警告:初始化会丢弃指针目标类型的限定符

此警告指的是以下行:

Dict *currPtr = dict;

如果我在该行之前添加一个“const”,如下所示:

const Dict *currPtr = dict;

警告消失...

1) 我不明白的第一件事是:我在 get()dict 参数 中添加了“const”,所以如果我尝试更改 dict 指向的地址,编译器会警告我,否则我将无法访问主程序中的字典,因为我也丢失了我指向的地址。现在,我正在创建一个新指针 currPtr,它指向与 dict 相同的位置。这样,我改用这个指针来遍历字典并保持 dict 指针不变。为什么我还需要为 currPtr 设置 const

2) 我不明白的第二件事是:currPtr = currPtr->next; 行正在改变 currPtr 指针,那么,如果我将 const 添加到 currPtr,为什么编译不会警告我?

然后我有一个 del() 函数,如下所示:

Dict *del(const Dict *dict, int key) {
    if(!dict) return NULL;

    Dict *currPtr = dict;
    Dict *prevPtr = dict;

    while(currPtr) {
        if(currPtr->key == key) {
            prevPtr->next = currPtr->next;
            free(currPtr);
        }

        prevPtr = currPtr;
        currPtr = currPtr->next;
    }

    return dict;
}

请注意,我知道此删除功能代码不完整,例如,如果我想删除第一个元素,它就无法正常工作。没关系,我晚点说完,证明我的问题就够了……

3) 所以,在 get() 函数中,我需要将 const 添加到 currPtr 但在这个 del() 函数,我不需要将 const 添加到 currPtrprevPtr 吗?在这两个函数中,我都在更改 currPtr 指针,在 del() 函数的情况下,我也在更改 prevPtr 指针。为什么 get() 函数需要我在 currPtr 之前添加 constdel() 函数需要不需要我在 currPtrprevPtr 之前添加 const 吗?

我基本上可以将整个帖子恢复到:我应该在何时何地使用 get()del() 函数> 为什么,以及,何时何地我不应该?

【问题讨论】:

    标签: c pointers constants


    【解决方案1】:

    没有指针,你会有

    const Dict currPtr
    

    这是一个常量字典。现在,如果你把它变成一个指针,你就有了

    const Dict *currPtr
    

    这是一个指向常量字典的指针。这并不意味着指针是恒定的。但这确实意味着指向的 Dict 被视为常量

    currPtr->key = 10; // error, dict is treated as constant. 
    

    但是指针不是

    currPtr = otherPtr; // possible: the pointer is not constant
    

    如果将指针设为常量,则第二种情况会出错。保持指向的 dict 不变,这看起来像这样

    const Dict * const currPtr = init;
    

    现在您不能将 currPtr 设置为指向不同的东西,因为指针现在是常量,而不仅仅是指针指向的内容被如此处理。如果 const 总是对它所产生的东西是正确的,那么有些人喜欢这种外观。这看起来像这样

    Dict const * const currPtr = init;
    

    这和之前的sn-p一样。如果您然后从右到左阅读它,它会告诉您它是什么“指向 const Dict 的 const 指针”。如果你有一个类型,那么你如何排序说明符并不重要

    int const a = 10;
    const int b = 10;
    

    两者都是常数整数。这就是为什么我们可以把 c​​onst 放在 Dict 类型说明符的右边。

    现在,如果你有一个指针,你总是可以假装你指向一个常量对象,即使这个对象没有被声明为 const。但是如果你指向的是一个 const 对象,你就不能假装使用一个非常量对象:

    int const *p = NULL;
    // doesn't work without a cast. Will at least provoke a warning
    int *pn = p;
    
    int *p = NULL;
    // always works: pretending to point to something const doesn't harm.
    int const *pc = p;
    

    请注意,如果您将指针本身设为 const,则规则与此不同。它们类似于应用于其他类型的 const:

    int const i = 0;
    int j = i; // works. we only read the value of i. its const doesn't matter. 
    
    int * const p = NULL;
    int * q = p; // works: we only read the value of p (a null pointer).
    

    将值复制到新变量(无论是否为指针)后,新变量不会以任何方式连接到另一个变量,因为读取的值与最初创建值的方式没有关联。其他变量的 const 无关紧要。

    【讨论】:

      【解决方案2】:

      “我在 get() 的 dict 参数中添加了“const”,以便在我尝试更改 dict 指向的地址时编译器会警告我”

      在这种情况下,您指的是Dict *const dict,而不是const Dict *dict。您已将结构声明为 const,而不是指向它的指针。

      “为什么currPtr也需要const”

      因为否则你可以使用 currPtr 来修改字典结构本身,它在这个函数中应该是 const。

      “currPtr = currPtr->next; 行正在更改 currPtr 指针,所以,如果我在 currPtr 中添加了一个 const,为什么编译器不会警告我”

      同样的原因:如果 const 在你放置的地方,这不是错误。如果 const 位于 currPtr 的另一个位置。

      否则我将无法在主程序中访问字典,因为我丢失了我指向的地址

      否 - 当主程序调用 get 时,它会将一个指针值传递给例程,这将成为 get 函数中 dict 的值。但是 get 有自己的指针变量,与主程序中的指针变量分开。即使它们恰好都被称为“dict”,改变一个指针也不会改变另一个。

      两个变量都指向同一个结构体,所以如果你使用变量来修改结构体中的任何字段,那么结果当然会影响这两个代码位。通常,名为“get”的函数将是一个只读操作,因此您可以设置参数const Dict *。但我认为这是正确的原因,与您的想法略有不同。这是为了保护字典的内容不被改变,而不是为了保护你对它的地址记录。

      然而,对于像这样的链表来说,const-safety 有点尴尬。即使你有一个 const Dict,它的“next”字段仍然不指向 const。因此,尽管您不能修改第一个节点,但 const-system 不会保护您免于修改列表中的其他节点。 C++ 通过函数重载解决了这个问题,所以你可以有一个“getnext”函数,如果输入是 const,则返回一个 const 输出,如果输入是非常量,则返回一个非常量输出。您可以使用“getnext”和“getnext_c”在 C 中实现类似的功能,后者具有 const 参数并返回。但是大多数人都不会打扰,包括像 strchr 这样的标准库函数,它在 C 中接受 const char* 但返回 non-const char*。因此,strchr 可能会意外地在 C 中用于将 const 字符串转换为非 const 字符串,而不会发出任何警告。或者故意,在这种情况下,这有点像通过“合法”业务洗钱;-)

      del() 函数不需要我在 currPtr 和 prevPtr 之前添加 const 吗?

      我承认我对此感到困惑。我希望得到与“get”中相同的警告 - 你确定它不存在吗?但是 del 的参数无论如何都不应该是 const,因为您可能会修改字段。

      【讨论】:

        【解决方案3】:

        const Dict *dict 表示这是一个指向常量 Dict 值的指针

        你可以修改指针,但不能修改值;

        【讨论】:

          【解决方案4】:

          我认为你的主要困惑来自误解了

          const char* p;
          

          这定义了一个指向常量字符的指针。 p 中的地址可以改变,但值指针不能。编译器禁止的是改变*p,而不是p。

          【讨论】:

            【解决方案5】:

            向后阅读指针声明可能会有所帮助:

            const Dict *dict
            

            意思

            'dict' 是一个指向常量 Dict 的指针。

            你可以移动它所指向的地址,但它所指向的结构被视为常量。

            【讨论】:

              【解决方案6】:

              const 是一种程序员文档。它告诉自己,你不会尝试修改在constqualified 中传递的任何内容。

              您的 get 函数应该采用 const Dict * 而您的 del 函数不应该。 currPtr 是一个walker——它不会改变getDict 对象的逻辑状态,所以它也应该是const 限定的。但是,在您的 del 中,它会更改结构,因此这里没有 const

              【讨论】:

                【解决方案7】:

                编译器会跟踪什么是 const,什么不是 const。一开始让它们正确可能会很棘手。

                对于您的第一点,您传入const Dict *dict,这意味着您保证不会通过dict 修改任何内容。然后将其分配给Dict * currptr。由于currptr 不是const,您可以通过*currptr 进行更改。编译器必须在某些时候警告您,即使它是 const,您正在尝试修改某些东西,正确的位置是当您将 const 指针分配给非 const 指针时。

                对于第二点,您完全错过了 const 的内容。如果您有const Dict * currptr,则您将currptr 描述为指向const Dict 的指针,这意味着您可以更改currptr,但不能更改*currptr。您可以将currptr const 设为Dict * const currptr,或将其设为指向常量值的常量指针与const Dick * const currptr

                我真的不明白你的第三点。正如您发布的那样,您应该从del() 收到编译器警告,因为您将指向 const 值的指针分配给指向值的指针。您需要有prevptr 非常量,因为您使用它来修改一个值。函数签名应该是Dict *del(Dict *dict, int key),因为你保证不会修改dict 指向的任何东西,并且无论如何都要这样做。

                【讨论】:

                  【解决方案8】:

                  广告1&2:原型中的const适用于Dict,也就是说

                  char *get(const Dict *dict, int key)
                  

                  读取为 ... 函数,其类型为指向常量 Dict ... 的指针的(非常量)参数,这意味着您可以更改 dict,但不能更改例如。 dict->next.

                  如果你想得到你可能想要的效果,就这样声明

                  char *get(Dict * const dict, int key)
                  

                  规则是:const 适用于它左边的单词,除非它在类型名称中的第一个,然后它适用于下一个单词。

                  另外,更改dict 不会更改主程序中的指针,因为指针是按值传递的。

                  分配给currDict 指针时,编译器会检查类型是否兼容,在这种情况下,这意味着您只能添加顶级const 限定符,而不能像您那样删除。

                  你应该在哪里使用 const:

                  get() 函数中,您应该在所有 Dicts 上使用 const(就像您所做的那样),因为您可能不想在那里更改字典。

                  del(), 函数中,我不会使用任何consts,因为要在那里更改数据。一个好的签名是:

                  char *get(Dict ** dict, int key)
                  

                  这样您就可以更改调用函数的指针以删除第一个元素。

                  【讨论】:

                    【解决方案9】:

                    请记住,const 可能出现在指针声明中* 的任一侧或两侧,这在语义上很重要...

                    const char * foo = bar;
                    

                    不一样

                    char * const foo = bar;
                    

                    第一个说内容const,第二个说指针const

                    对于最大的常数有:

                    const char * const foo = bar;
                    

                    【讨论】:

                    • 不是反过来吗" "char * const foo" 是常量指针,"const char * foo" 是指向常量数据的指针。
                    【解决方案10】:

                    你们说的大部分我都听懂了,我现在没听懂,这是……

                    考虑以下 list() 函数:

                    void list(Dict *dict) {
                        if(!dict) return;
                    
                        while(dict) {
                            printf("KEY: %d\n", dict->key);
                            printf("VALUE: %s\n", dict->value);
                    
                            dict = dict->next;
                        }
                    }
                    

                    (使用这个比使用 get() 更简单)

                    在我的 main 我有两个调用这个函数,比如:

                    list(dict);
                    list(dict);
                    

                    当然,dict插入了一些条目...

                    据我了解,只有 first list() 应该输出一些东西,因为 dict 指针list() 函数中被更改并最终指向 NULL (否则我们将在 list() 调用中处于无限循环中)。发生的事情是 second list() 调用也输出相同的字典,而我认为它不应该输出任何内容,因为 dict 应该为 NULL,不是吗?

                    【讨论】:

                    • 这是一个完全不同的问题,但是......“dict”参数仅在 list() 的范围内更改。 C参数是值参数。
                    • C 是一种“按值传递”的语言。当您调用 list() 时,会创建一个新的堆栈帧。 dict* dict 被复制到那个帧上,所以只能在本地修改;如果你说 dict = null,你不会写入 list(dict) 读取的同一个内存位置。
                    【解决方案11】:

                    “const 的傻瓜指南”:

                    尽可能使用它。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-12-29
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2021-09-16
                      相关资源
                      最近更新 更多