C中的三重指针:是风格问题吗?



我觉得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);

也会起作用。

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

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

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

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

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

void fun(int***);

嗯。参数是三维交错数组,还是指向二维交错数组的指针,还是指向指向数组的指针(例如,函数分配数组并将指针分配给函数中的 int)

让我们将其与:

void fun(IntMatrix*);

当然,您可以使用三重指针来对矩阵进行操作。但事实并非如此。它们在这里作为三重指针实现的事实与用户无关

封装复杂的数据结构。这是面向对象编程的明显思想之一。即使在 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_

大多数情况下,使用 3 个间接寻址级别是程序中其他地方做出错误设计决策的症状。因此,这被认为是不好的做法,并且有关于"三星程序员"的笑话,与餐厅的评级不同,更多的星级意味着质量更差。

对 3 级间接寻址的需求通常源于对如何正确动态分配多维数组的困惑。即使在编程书籍中也经常错误地教授这一点,部分原因是在 C99 标准之前正确执行此操作是很麻烦的。我的问答文章正确分配多维数组解决了这个问题,也说明了多级间接寻址将如何使代码越来越难以阅读和维护。

尽管正如该帖子所解释的那样,在某些情况下,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 个间接级别对我们没有好处。但是选择使用它们必须充分知情,并意识到它们可能会创建丑陋的代码,并且还有其他选择。

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

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

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

那么当然,typedef走了。但是,如果h要取消引用,例如

printf( "Handle value is %dn", *h );

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

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


1.或者指针到指针到int,或者指针到指针到指针到指针到struct grdlphmp,或者其他什么。

经过两个层次的间接处理,理解变得困难。 此外,如果你将这些三重(或更多)指针传递到方法中的原因是为了让它们可以重新分配和重置一些指向的内存,这就摆脱了方法作为"函数"的概念,这些"函数"只返回值而不影响状态。 这也会对理解和可维护性产生负面影响。

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

人们可以清楚地看到如何需要三重指针(以及更多,真的)。

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

不幸的是,你误解了 C 中指针和数组的概念。 请记住,数组不是指针

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

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

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

又错了。数组不是指针,反之亦然。指针到指针不是 2D 数组。
我建议您阅读c-FAQ第6节。数组和指针。

最新更新