【问题标题】:Triple pointers in C: is it a matter of style?C中的三重指针:是风格问题吗?
【发布时间】:2014-01-31 19:35:36
【问题描述】:

我觉得 C 中的三重指针被视为“坏”。对我来说,有时使用它们是有意义的。

从基础开始,单指针有两个用途:创建数组,以及允许函数更改其内容(通过引用传递):

char *a;
a = malloc...

void foo (char *c); //means I'm going to modify the parameter in foo.
{ *c = 'f'; }

char a;
foo(&a);

双指针可以是二维数组(或数组数组,因为每个“列”或“行”的长度不必相同)。我个人喜欢在需要传递一维数组时使用它:

void foo (char **c); //means I'm going to modify the elements of an array in foo.
{ (*c)[0] = 'f'; }

char *a;
a = malloc...
foo(&a);

对我来说,这有助于描述 foo 正在做什么。但是,这不是必需的:

void foo (char *c); //am I modifying a char or just passing a char array?
{ c[0] = 'f'; }

char *a;
a = malloc...
foo(a);

也可以。

根据this question的第一个答案,如果foo要修改数组的大小,则需要双指针。

人们可以清楚地看到如何需要 三重指针(实际上还有更多)。在我的情况下,如果我传递一个指针数组(或数组数组),我会使用它。显然,如果您要传入一个正在更改多维数组大小的函数,则需要这样做。数组数组的数组当然不是太常见,但其他情况是。

那么有哪些约定呢?这真的只是样式/可读性的问题,再加上许多人很难理解指针这一事实吗?

【问题讨论】:

  • 创建一个指向不透明类型的双指针(它本身可能最终成为一个指针)是不是很罕见?即typedef struct foo *foo_t;,然后是foo_t **bar
  • 看看指向指针的指针如何成为 C 语言的独特“卖点”(与 Java 和其他语言相比):stackoverflow.com/questions/18373657/…
  • 此外:如果我要“从基础开始”关于指针,它们与数组的关系并不是我认为的基础。指针的基本目的是将存储位置作为数据进行操作。其中一些存储位置恰好是特定数量的数组,这是一个有趣的细节,但不是基本的。
  • 通过使用typedef,代码可以减少变量或参数声明中* 的数量。 foo(bar *a) 很好地将a 呈现为指向bar 类型的指针,即使typedef int **** bar 也是如此。建议应始终避免使用 3+ *,仅在特定情况下使用 2 *
  • @EricLippert:StackOverflow 是放置信息的好地方。我正在问一个问题,虽然是一个不太实际的问题。尽管如此,其中许多问题(即使是不太受欢迎的问题)仍会出现在搜索结果中并为人们提供帮助。

标签: c pointers coding-style


【解决方案1】:

使用 Triple+ 指针会损害可读性和可维护性。

假设您在这里有一个小函数声明:

void fun(int***);

嗯。参数是三维交错数组,还是指向二维交错数组的指针,还是指向数组的指针(如函数分配一个数组并在函数内分配一个指向 int 的指针)

让我们比较一下:

void fun(IntMatrix*);

当然,您可以使用指向 int 的三重指针来操作矩阵。 但它们不是这样的。它们在此处实现为三重指针这一事实与用户无关

复杂的数据结构应该被封装。这是面向对象编程的明显思想之一。即使在 C 语言中,您也可以在一定程度上应用此原则。将数据结构包装在一个结构中(或者,在 C 中很常见,使用“句柄”,即指向不完整类型的指针——这个习惯用法将在后面的答案中解释)。

假设您将矩阵实现为double 的锯齿状数组。与连续的 2D 数组相比,它们在迭代时更差(因为它们不属于单个连续内存块),但允许使用数组表示法进行访问,并且每行可以有不同的大小。

所以现在的问题是你现在不能改变表示,因为指针的使用是硬连线在用户代码上的,现在你被困在低劣的实现上。

如果你把它封装在一个结构体中,这甚至都不是问题。

typedef struct Matrix_
{
    double** data;
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i][j];
}

直接改成

typedef struct Matrix_
{
    int width;
    double data[]; //C99 flexible array member
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i*m->width+j];
}

句柄技术的工作原理是这样的:在头文件中,声明一个不完整的结构以及所有作用于指向该结构的指针的函数:

// struct declaration with no body. 
struct Matrix_;
// optional: allow people to declare the matrix with Matrix* instead of struct Matrix*
typedef struct Matrix_ Matrix;

Matrix* create_matrix(int w, int h);
void destroy_matrix(Matrix* m);
double get_element(Matrix* m, int i, int j);
double set_element(Matrix* m, double value, int i, int j);

在源文件中声明实际结构并定义所有函数:

typedef struct Matrix_
{
    int width;
    double data[]; //C99 flexible array member
} Matrix;

double get_element(Matrix* m, int i, int j)
{
    return m->data[i*m->width+j];
}

/* definition of the rest of the functions */

世界其他地方不知道struct Matrix_ 包含什么,也不知道它的大小。这意味着用户不能直接声明值,而只能使用指向Matrix 的指针和create_matrix 函数。然而,用户不知道大小的事实意味着用户不依赖它——这意味着我们可以随意删除或添加成员到struct Matrix_

【讨论】:

  • +1。这是一个很好的示例,并且清楚地显示了定义struct 时定义指针类型的价值。如果我在我的代码中这样做了,我就可以避免使用三重指针表示法,因为我将一个指针传递给一些 struct 的指针数组。
  • "复杂的数据结构应该封装" +1
【解决方案2】:

大多数时候,使用 3 级间接性是程序其他地方做出错误设计决策的征兆。因此,这被认为是不好的做法,并且有关于“三星级程序员”的笑话,与餐厅的评级不同,星级越多,质量越差。

对 3 级间接的需求通常源于对如何正确动态分配多维数组的困惑。即使在编程书籍中,这也经常被错误地教授,部分原因是在 C99 标准之前正确地做到这一点是很麻烦的。我的问答帖子Correctly allocating multi-dimensional arrays 解决了这个问题,还说明了多级间接性如何使代码越来越难以阅读和维护。

尽管正如那篇文章所解释的那样,在某些情况下type** 可能有意义。具有可变长度的字符串的可变表就是这样的示例。当出现对type** 的需求时,您可能很快就会想使用type***,因为您需要通过函数参数返回您的type**

这种需求通常出现在您正在设计某种复杂的 ADT 的情况下。例如,假设我们正在编写一个哈希表,其中每个索引都是一个“链式”链表,链表中的每个节点都是一个数组。正确的解决方案是重新设计程序以使用结构而不是多级间接。哈希表、链表和数组应该是不同的类型,自治的类型,相互之间没有任何意识。

因此,通过使用适当的设计,我们将自动避免多颗星。


但正如每条良好编程习惯的规则一样,总会有例外。完全有可能出现这样的情况:

  • 必须实现一个字符串数组。
  • 字符串的数量是可变的,并且可能会在运行时发生变化。
  • 字符串的长度是可变的。

可以将上述实现为 ADT,但也可能有正当理由保持简单,只需使用 char* [n]。然后,您有两个选项可以动态分配它:

char* (*arr_ptr)[n] = malloc( sizeof(char*[n]) );

char** ptr_ptr = malloc( sizeof(char*[n]) );

前者在形式上更正确,但也很麻烦。因为它必须用作(*arr_ptr)[i] = "string";,而替代可以用作ptr_ptr[i] = "string";

现在假设我们必须将 malloc 调用放在一个函数中并且返回类型是为错误代码保留的,就像 C API 的自定义一样。这两个替代方案将如下所示:

err_t alloc_arr_ptr (size_t n, char* (**arr)[n])
{
  *arr = malloc( sizeof(char*[n]) );

  return *arr == NULL ? ERR_ALLOC : OK;
}

err_t alloc_ptr_ptr (size_t n, char*** arr)
{
  *arr = malloc( sizeof(char*[n]) );

  return *arr == NULL ? ERR_ALLOC : OK;
}

很难说前者更具可读性,而且它还带有调用者所需的繁琐访问。在这个非常具体的情况下,三星级替代方案实际上更优雅。

因此,教条地忽略 3 级间接性对我们没有好处。但是使用它们的选择必须充分知情,并意识到它们可能会创建丑陋的代码并且还有其他选择。

【讨论】:

  • 哦,所以你的意思是我们应该有意识地设计代码,而不是依赖朗朗上口的“X被认为是有害的”陈词滥调?
  • @DavidBowling 一般来说,是的。但是,为了决定何时使用某些东西,尽管它被广泛认为是不好的做法,你需要相当多的知识。所以这绝对不是初学者的决定;他们应该遵循最佳实践,期间。
【解决方案3】:

那么有哪些约定呢?这真的只是样式/可读性的问题,再加上许多人很难理解指针这一事实吗?

多重间接不是不好的风格,也不是黑魔法,如果你要处理高维数据,那么你将要处理高层次的间接;如果您真的在处理指向指向T 的指针的指针,那么不要害怕写T ***p;。不要将指针隐藏在 typedef 后面除非使用该类型的人不必担心它的“指针性”。例如,如果您将类型作为在 API 中传递的“句柄”提供,例如:

typedef ... *Handle;

Handle h = NewHandle();
DoSomethingWith( h, some_data );
DoSomethingElseWith( h, more_data );
ReleaseHandle( h );

那么当然,typedef 离开。但是如果 h 曾经打算被取消引用,例如

printf( "Handle value is %d\n", *h );

然后不要typedef。如果您的用户必须知道h 是指向int1 的指针才能正确使用它,那么该信息应该 > 隐藏在 typedef 后面。

我会说,根据我的经验,我不必处理更高级别的间接性;三重间接是最高的,我用过几次。如果您经常发现自己处理 >3 维数据,那么您会看到高级别的间接,但如果您了解指针表达式和间接的工作原理,这应该不是问题。


1。或者指向指向int 的指针,或者指向指向指向struct grdlphmp 的指针的指针,或者其他。

【讨论】:

    【解决方案4】:

    很遗憾,您误解了 C 中指针和数组的概念。请记住,数组不是指针

    从基础开始,单指针有两个用途:创建数组,以及允许函数更改其内容(通过引用传递):

    当你声明一个指针时,你需要在程序中使用它之前对其进行初始化。它可以通过将变量的地址传递给它或通过动态内存分配来完成。
    在后者中,指针可以用作索引数组(但它不是数组)。

    双指针可以是二维数组(或数组数组,因为每个“列”或“行”的长度不必相同)。我个人喜欢在需要传递一维数组时使用它:

    又错了。数组不是指针,反之亦然。指向指针的指针不是二维数组。
    我建议你阅读c-faq section 6. Arrays and Pointers

    【讨论】:

    • 你说'当你声明一个指针时,你需要在程序中使用它之前对其进行初始化'是什么意思?
    • @musicmatze;我的意思是说你必须通过传递一个有效的内存地址来初始化它。否则它将是一个悬空指针。
    • @haacks:我看不出这个答案的 -2 的理由。我感谢您指出数组不是指针的技术性。但是,如果您在编译时不知道长度,则必须分配内存,然后使用数组表示法取消引用指针。我相信这是故意的。如果在这种情况下差异如此重要,我会认为有人会改变语法。
    • @pelesl;有时我也会因为投票而感到困惑:)
    • @musicmatze;再次阅读我的答案。我说:你需要初始化它在程序中使用它之前,这个地址是读写的。
    【解决方案5】:

    经过两级间接后,理解变得困难。此外,如果您将这些三重(或更多)指针传递给您的方法的原因是为了让它们可以重新分配和重新设置一些指向内存,那么这就脱离了方法作为“函数”的概念返回值并且不影响状态。这也会在某种程度上对理解和可维护性产生负面影响。

    但更根本的是,您在这里遇到了对三重指针的主要风格反对意见之一:

    人们可以清楚地看到如何需要三重指针(实际上还有更多)。

    这里的问题是“和超越”:一旦你达到三个层次,你会在哪里停下来?当然,可能有任意数量的间接级别。但是最好在可理解性仍然很好但灵活性足够的地方设置一个习惯性的限制。两个是个好数字。有时被称为“三星级编程”,充其量是有争议的。对于以后需要维护代码的人来说,这要么很棒,要么令人头疼。

    【讨论】:

    • 我不确定我是否同意“将方法作为‘函数’的概念,只返回值而不影响状态”。函数还通过隐藏“琐碎但冗长”的代码来提高可读性,这些代码不会增加或减损对算法的理解(例如,隐藏在InitMem 函数中的复杂内存分配代码块)。它们在并行化事物(例如使用 OpenMP)时也非常有用,因此共享和私有的错误更少(有人可能认为这只会提高可读性)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多