【问题标题】:Can GCC warn me about modifying the fields of a const struct in C99?GCC 可以警告我修改 C99 中 const 结构的字段吗?
【发布时间】:2016-06-09 08:38:42
【问题描述】:

我在尝试编写 const 正确代码时偶然发现了一个小问题。

我本来想写一个函数,它接受一个指向 const 结构的指针,告诉编译器“请告诉我是否正在修改结构,因为我真的不想”。

我突然想到编译器会允许我这样做:

struct A
{
    char *ptrChar;
};

void f(const struct A *ptrA)
{
    ptrA->ptrChar[0] = 'A'; // NOT DESIRED!!
}

这是可以理解的,因为实际上 const 是指针本身,而不是它指向的类型。我希望编译器告诉我,我正在做一些我不想做的事情,如果可能的话。

我使用 gcc 作为我的编译器。虽然我知道上面的代码应该是合法的,但我还是检查了它是否会发出警告,但没有任何反应。我的命令行是:

gcc -std=c99 -Wall -Wextra -pedantic test.c

是否有可能解决这个问题?

【问题讨论】:

  • 此案例适用于成员的分配。例如ptrA->ptrChar = malloc(2);不是指哪个成员。
  • 为此,我通常有一个头文件,它只是向前声明struct a;,而a_do_something(const struct *a); 中的一些函数声明很少需要在头文件中包含struct A 的整个定义。您将很多函数(例如 f)放在单独的翻译单元中,其中 f 只会以某种方式使用 ptrA stuff = a_do_something(ptrA);

标签: c constants compiler-warnings c99


【解决方案1】:

不,除非您将结构定义更改为:

struct A
{
    const char *ptrChar;
};

另一个保持旧结构定义完整的复杂解决方案是定义一个具有相同成员的新结构,其相关指针成员设置为:指向 const 类型。然后您正在调用的函数被更改为采用新结构。定义了一个包装函数,它采用旧结构,逐个成员复制到新结构并将其传递给函数。

【讨论】:

  • 我想知道对仅在成员的 cv-qualification 上不同的结构进行简单转换是否会是 UB?
  • @PeterA.Schneider 是的,类型不兼容。
【解决方案2】:

这是实现与接口的示例,或“信息隐藏” - 或者更确切地说是非隐藏 ;-) - 问题。在 C++ 中,只需将指针设为私有并定义合适的公共 const 访问器。或者可以用访问器定义一个抽象类——一个“接口”。适当的结构将实现这一点。不需要创建结构实例的用户只需要查看界面即可。

在 C 中,可以通过定义一个函数来模拟这一点,该函数将指向结构的指针作为参数并返回指向 const char 的指针。对于不创建这些结构的实例的用户,甚至可以提供一个“用户标头”,它不会泄漏结构的实现,而只定义采用(或像工厂一样返回)指针的操作函数。这使结构成为不完整的类型(因此只能使用指向实例的指针)。这种模式有效地模拟了 C++ 在后台使用 this 指针所做的工作。

【讨论】:

    【解决方案3】:

    这是 C 语言的一个已知问题,无法避免。毕竟,您不是在修改结构,而是通过从结构中获得的非const 限定指针来修改单独的对象。 const 语义最初是围绕将内存区域标记为物理上不可写的常量的需要而设计的,而不是围绕防御性编程的任何问题。

    【讨论】:

      【解决方案4】:

      也许如果您决定使用 C11,您可以实现一个通用宏,它引用同一成员的常量或变量版本(您还应该在结构中包含一个联合)。像这样的:

      struct A
      {
          union {
              char *m_ptrChar;
      
              const char *m_cptrChar;
          } ;
      };
      
      #define ptrChar_m(a) _Generic(a, struct A *: a->m_ptrChar,        \
                                       const struct A *: a->m_cptrChar)//,  \
                                       //struct A: a.m_ptrChar,        \
                                       //const struct A: a.m_cptrChar)
      
      void f(const struct A *ptrA)
      {
          ptrChar_m(ptrA) = 'A'; // NOT DESIRED!!
      }
      

      联合为单个成员创建 2 个解释。 m_cptrChar 是指向常量 char 的指针,m_ptrChar 指向非常量。然后宏根据参数的类型决定引用哪个。

      唯一的问题是宏 ptrChar_m 只能与此结构的指针或对象一起使用,而不能同时与两者一起使用。

      【讨论】:

      • 不错的解决方案,但您必须放弃 . 案例,否则您将遇到宏扩展问题。还要避免使用下划线开头的变量名(参见 7.1.3)。
      • 请不要这样做。那太糟了。一年后没人会明白这一点,即使是你。
      • 我不喜欢工会双关语。我们可以改用 _Generic(a, struct A*: a->ptrChar, const struct A*: (const char *) a->ptrChar ) 吗?这样,我们就不必改变结构体的定义了。
      • @FUZxxl 这就是重点。复杂的工作代码是我的最爱。虽然这个解决方案可能真的不是最好的(我个人的看法)。
      【解决方案5】:

      如果需要,一种解决此问题的方法是为同一个对象使用两种不同的类型:一种读/写类型和一种只读类型。

      typedef struct
      {
        char *ptrChar;
      } A_rw;
      
      typedef struct
      {
        const char* ptrChar;
      } A_ro;
      
      
      typedef union
      {
        A_rw rw;
        A_ro ro;  
      } A;
      

      如果函数需要修改对象,则以读写类型为参数,否则为只读类型。

      void modify (A_rw* a)
      {
        a->ptrChar[0] = 'A';
      }
      
      void print (const A_ro* a)
      {
        puts(a->ptrChar);
      }
      

      为了美化调用者接口并使其保持一致,您可以使用包装函数作为 ADT 的公共接口:

      inline void A_modify (A* a)
      {
        modify(&a->rw);
      }
      
      inline void A_print (const A* a)
      {
        print(&a->ro);
      }
      

      使用此方法,A 现在可以实现为不透明类型,以隐藏调用者的实现。

      【讨论】:

      • 我喜欢工会,并且相当肯定它会在实践中发挥作用;允许吗?结构类型 ar、iiuc、不兼容。
      • @Lundin:小心 gcc。基于可以从严格的别名规则推导出来的假设进行优化是非常激进的。
      • 是的。我想你是对的。让我担心的是“它应该在实践中工作”——除非标准规定必须这样做,否则很可能不会(偶尔)。
      • @MartinBonner 我对那个评论的意思是,首先你在一个联合中给编译器两个结构,每个结构都包含一个兼容的指针类型。它不可能以不同的方式分配每个结构,为什么会这样?每个结构 具有相同的内存布局。并且当您将地址存储在任一结构中时,无论 const 限定符如何,它都将存储完全相同。常识表明,这是数据类型必须如何工作的,没有其他实现是有意义的。到那时,标准就变得无关紧要了,因为它所说的一切都无法改变现实。
      • 你说“如果一个函数需要修改对象,它以只读类型为参数,否则它为读写类型”。我认为这是一个错字,这些角色是相反的。
      【解决方案6】:

      我们可以隐藏一些“访问器”函数背后的信息:

      // header
      struct A;           // incomplete type
      char *getPtr(struct A *);
      const char *getCPtr(const struct A *);
      
      // implementation
      struct A { char *ptr };
      char *getPtr(struct A *a) { return a->ptr; }
      const char *getCPtr(const struct A *a) { return a->ptr; }
      

      【讨论】:

        【解决方案7】:

        GCC 可以警告我在 C99 中修改 const 结构的字段吗?

        您没有修改 const 结构的字段。

        结构 A 的值包含一个指向非常量字符的指针。 ptrA 是一个指向 const struct A 的指针。因此您不能在 *ptrA 处更改 struct A 的值。所以你不能在 (*ptrA).Char 又名 ptrA->ptrChar 处更改指向 char 的指针。但是您正在更改 ptrA->ptrChar 指向的值,即 *(ptrA->Char) aka ptrA->Char[0] 处的值。这里唯一的 const 是 struct As 并且您没有更改 struct A 那么究竟什么是“不想要的”?

        如果您不想允许更改结构 A 的 Char 字段指向的值(通过该结构 A),请使用

        struct A
        {
            const char *ptrChar; // or char const *ptrChar;
        };
        

        但也许你认为你在 f 中所做的事情类似于

        void f(const struct A *ptrA)
        {
            const char c = 'A';
            ptrA->ptrChar = &c;
        }
        

        编译器会出错。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-07-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-05-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多