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

pgccezyw  于 2023-02-18  发布在  其他
关注(0)|答案(5)|浏览(178)

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

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

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要修改数组的大小,则需要双指针。
我们可以清楚地看到如何需要一个三重指针(实际上还有更多)。在我的例子中,如果我传递一个指针数组(或数组的数组),我会使用它。显然,如果你传递给一个改变多维数组大小的函数,它是必需的。当然,数组的数组的数组不是太常见,但其他情况是。
那么,有哪些约定呢?这真的只是一个风格/可读性的问题,再加上许多人很难把头缠在指针上的事实吗?

hc2pp10m

hc2pp10m1#

使用三重+指针会损害可读性和可维护性。
让我们假设你有一个小函数声明在这里:

void fun(int***);

参数是三维交错数组,还是指向二维交错数组的指针,或者指向数组指针的指针(例如,函数分配一个数组,并在函数中分配一个指向int的指针)
让我们将其与以下内容进行比较:

void fun(IntMatrix*);

当然,你可以使用指向int的三重指针来操作矩阵,**但它们不是这样的,事实上,它们在这里被实现为三重指针,与用户无关。
复杂的数据结构应该被封装。这是面向对象编程的一个明显的思想。即使在C语言中,你也可以在一定程度上应用这个原则。将数据结构 Package 在一个结构体中(或者,在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);

在源文件中,你声明了实际的struct并定义了所有的函数:

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_中删除或添加成员。

u2nhd7ah

u2nhd7ah2#

大多数情况下,使用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个层次的间接性对我们没有好处,但是选择使用它们必须是明智的,要意识到它们可能会产生丑陋的代码,而且还有其他的选择。

njthzxwz

njthzxwz3#

那么,有哪些约定呢?这真的只是一个风格/可读性的问题,再加上许多人很难把头缠在指针上的事实吗?
多重间接不是坏的风格,也不是黑魔法,如果您处理高维数据,那么您将处理高级别的间接;如果你真的要处理一个指向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 %d\n", *h );

那么 * don't typedef it *.如果你的用户 * 必须知道 * h是指向int 1的指针才能正确使用它,那么这个信息 * 不 * 应该隐藏在typedef后面。
我会说,在我的经验中,我还没有处理过更高层次的间接;三重间接是最高级的,我使用它的次数不超过两次。如果你经常发现自己在处理三维以上的数据,那么你会看到高级别的间接,但是如果你了解指针表达式和间接的工作原理,这应该不是一个问题。
1.或者是指向int的指针,或者是指向struct grdlphmp的指针,或者其他什么。

qyuhtwio

qyuhtwio4#

经过两级间接寻址之后,理解变得困难了。此外,如果你将这些三重(或更多)指针传递到你的方法中的原因是为了让它们能够重新分配和重新设置一些被指向的内存,那就脱离了方法作为“函数”的概念,即只返回值而不影响状态。这也会在一定程度上负面影响理解和可维护性。
但更根本的是,你已经碰到了对三重指针的一个主要的文体异议:
人们可以清楚地看到如何需要三重指针(实际上,不止如此)。
这里的问题在于“超越”:一旦你达到了三个层次,你会停在哪里呢?当然,有一个任意数量的间接层次是“可能的”。但是,最好只是在某个地方有一个习惯性的限制,那里的可理解性仍然很好,但灵活性是足够的。2是一个很好的数字。“星星编程”,因为它有时被称为,充其量是有争议的;它要么很聪明,要么让那些以后需要维护代码的人头疼。

gupuwyp2

gupuwyp25#

不幸的是,你误解了C中指针和数组的概念。记住数组不是指针
从基础开始,单指针有两个用途:创建一个数组,并允许函数更改其内容(按引用传递):
当你声明一个指针时,你需要在程序中使用它之前初始化它。它可以通过传递一个变量的地址给它或者通过动态内存分配来完成。
在后一种情况下,指针可以用作索引数组(但它不是数组)。
双指针可以是一个2D数组(也可以是数组的数组,因为每个"列"或"行"的长度不必相同),我个人喜欢在需要传递一个1D数组时使用它:
又错了。数组不是指针,反之亦然。指针对指针不是二维数组。
我建议您阅读c-faq section 6. Arrays and Pointers

相关问题