按引用传递、常量正确性和对引用结构体的读写访问 - 未定义行为

3

我有一堆代码需要分析并准备导入到一个新项目中。通常会出现以下模式:

typedef struct t_Substruct
{
   /* some elements */
} ts;


typedef struct t_Superstruct
{
   /* some elements */
   ts substruct;
   /* some more elements */
} tS;


void funct1(const tS * const i_pS, tS * const o_pS)
{ /* do some complicated calculations and transformations */  }

void funct2(const ts  * const i_ps, tS * const o_pS)
{ /* do some complicated calculations and transformations */  }

void funct3(const tS  * const i_ps, ts * const o_ps)
{ /* do some complicated calculations and transformations */  }

一般来说,会从i_参数进行读取并向o_参数进行写入。现在可能会有如下调用:

void some_funct()
{
     tS my_struct = {0};

     /* do some stuff */

     funct1(&my_struct,&my_struct);

     funct2(&my_struct.substruct, &my_struct);

     funct3(&my_struct, &my_struct.substruct);
}

我不确定这些函数和调用上存在哪些可能的问题和调用环境:

  • 在语言约束和/或未定义行为方面,是否允许在const正确性的上下文中进行此类声明或调用?
  • 在同一功能中更改被保护引用和非保护引用的对象是否被允许?
  • 我知道在序列点之间多次访问/修改同一变量时会出现一些问题(虽然我不完全确定我是否完全理解了序列点事物)。这些或类似的问题是否适用于此处,以及以何种方式适用?
  • 如果不是未定义行为,存在任何其他典型问题会降低上述情况的可移植性吗?
  • 如果存在问题,那么什么是一个好的(安全的,并且如果可能的话开销尽可能小)通用模式,以允许这些调用,以便这些问题可能不会在每个其它行发生?

我必须在C90中实施,但是如果在上述情况下移植到其他C版本中存在问题,则对我也很重要。

提前感谢您。


所有这些括号都不是必要的,只需要&my_struct.substruct,因为.&的优先级更高。 - unwind
谢谢你的提示。 :-) - Mark A.
2个回答

2

与指针 S* p 相关的 const 有两种不同的方面。

  1. 是否允许更改 指针?例如:p=5;
  2. 是否允许更改所指向的 对象?例如:p->x = 5;

这是四种可能性:

  • T* p:两种更改都允许
  • const T* p:不允许更改对象
  • T* const p:不允许更改指针
  • const T* const p:既不允许更改对象也不允许更改指针

在你的例子 void funct1(const tS * const i_pS, tS * const o_pS) 中,这意味着以下内容:

  • 你不能更改指针 i_pSo_pS
  • 你不能更改由 i_pS 指向的对象。
  • 你可以更改由 o_pS 指向的对象。

第一个条件看起来相当无意义,所以可能是这样的:

void funct1(const tS* i_pS, tS* o_pS)

更易读。


关于第二和第三种情况,即您有两个指针指向对象的同一部分:在代码中要小心,不要做出错误的假设,例如由const指针指向的对象实际上没有发生变化。

请记住,const指针从来不意味着对象不会改变,只是不能通过该指针更改它。

有问题的代码示例:

void foo(const S* a, S* b) {
   if(a->x != 0) {
       b->x = 0;
       b->y = 5 / a->x; // why is a->x suddenly 0 ??
   } 
}
S s;
foo(&s, &s);

关于未定义行为和序列点。我建议阅读这篇回答:Undefined behavior and sequence points 例如,如果a和b指向同一个对象,则表达式i = a->x + (b->x)++;一定是未定义的行为。
函数void funct1(const tS* i_ps, tS* o_pS)被称为funct1(&my_struct, &my_struct);会导致混淆和错误。
C库也知道这个问题。例如考虑memcpymemmove
所以我建议构建您的函数,以便您可以确定不会发生未定义的行为。最激烈的措施是完全复制输入结构。这会有开销,但在您的特定情况下,只复制输入参数的某个小部分就足够了。
如果开销太大,请在函数文档中明确说明不允许将同一对象作为输入和输出。然后,如果可能和必要,创建第二个函数来处理输入和输出相同的情况,需要付出必要的代价。

那么…像这样的代码:int i = ((((a->x)++) == ((b->x)++))); 不会引起问题吗? - Mark A.
这段代码实际上无法编译,因为(a->x)++会尝试更改a所指向的对象。由于a是一个const指针,所以这是不允许的。 - Danvil
很好。那i = a->x + (b->x)++;这段代码怎么样?再说一遍:我并不完全理解序列点和与其相关的未定义行为(UB)。哦...我看到你刚刚编辑掉了有关序列点的那句话。你认为类似上面的代码会触发一些UB条件吗? - Mark A.

0

此调用无效。例如:

funct1(&my_struct.substruct, &my_struct.substruct);

因为funct1期望一个tS *,但这些是ts *。你需要进行强制类型转换才能使其编译通过。代码将会正常工作(因为实际上指向的位置有一个tS),但这样说起来有点奇怪,你应该只需将其改为&my_struct而不是添加强制类型转换。

另外,请使用与tstS不同的命名约定。

正如Danvil所说,重要的是你的代码要考虑到这两个指针可能指向同一个对象的部分。

就风格而言,我不喜欢“顶层const”。它使函数头更难阅读,并且你需要花一些时间来确定什么是const,什么不是。


当然...那是无意义的,并且是从某个复制粘贴中剩下的。谢谢您提供信息。我的问题是关于对指向同一位置的2个指针的影响。就命名而言,没有什么好争取的。请确保在生产代码中存在更有表现力的命名方式。 - Mark A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接