为什么要使用双重间接性?或者为什么要使用指向指针的指针?

356

在C语言中什么时候需要使用双重间接?可以举一个例子来解释吗?

我知道双重间接是指指向指针的指针。那么为什么需要指向指针的指针呢?


70
注意:术语“double pointer”也指类型double*。请小心处理。 - Keith Thompson
5
请注意:这个问题的答案在 C 和 C++ 中是不同的——不要给这个很古老的问题添加 c++ 标签。 - BЈовић
2
@BЈовић 虽然这是一个老问题和旧评论,但在C和C++中使用双指针有什么区别?看到你的评论说它们不同,我试图自己回答,但我仍然看到C和C++中使用双指针的一些小差异。 - Sangjun Lee
1
可以用于字符的锯齿数组,即每个列表具有不同长度的列表列表。 - daparic
19个回答

575
如果你想要一个字符列表(一个单词),你可以使用char *word 如果你想要一个单词列表(一个句子),你可以使用char **sentence 如果你想要一个句子列表(一个独白),你可以使用char ***monologue 如果你想要一个独白列表(一个传记),你可以使用char ****biography 如果你想要一个传记列表(一个生物资料库),你可以使用char *****biolibrary 如果你想要一个生物资料库列表(一个??lol),你可以使用char ******lol ... ...
使用非常无聊的lol举例说明。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int wordsinsentence(char **x) {
    int w = 0;
    while (*x) {
        w += 1;
        x++;
    }
    return w;
}

int wordsinmono(char ***x) {
    int w = 0;
    while (*x) {
        w += wordsinsentence(*x);
        x++;
    }
    return w;
}

int wordsinbio(char ****x) {
    int w = 0;
    while (*x) {
        w += wordsinmono(*x);
        x++;
    }
    return w;
}

int wordsinlib(char *****x) {
    int w = 0;
    while (*x) {
        w += wordsinbio(*x);
        x++;
    }
    return w;
}

int wordsinlol(char ******x) {
    int w = 0;
    while (*x) {
        w += wordsinlib(*x);
        x++;
    }
    return w;
}

int main(void) {
    char *word;
    char **sentence;
    char ***monologue;
    char ****biography;
    char *****biolibrary;
    char ******lol;

    //fill data structure
    word = malloc(4 * sizeof *word); // assume it worked
    strcpy(word, "foo");

    sentence = malloc(4 * sizeof *sentence); // assume it worked
    sentence[0] = word;
    sentence[1] = word;
    sentence[2] = word;
    sentence[3] = NULL;

    monologue = malloc(4 * sizeof *monologue); // assume it worked
    monologue[0] = sentence;
    monologue[1] = sentence;
    monologue[2] = sentence;
    monologue[3] = NULL;

    biography = malloc(4 * sizeof *biography); // assume it worked
    biography[0] = monologue;
    biography[1] = monologue;
    biography[2] = monologue;
    biography[3] = NULL;

    biolibrary = malloc(4 * sizeof *biolibrary); // assume it worked
    biolibrary[0] = biography;
    biolibrary[1] = biography;
    biolibrary[2] = biography;
    biolibrary[3] = NULL;

    lol = malloc(4 * sizeof *lol); // assume it worked
    lol[0] = biolibrary;
    lol[1] = biolibrary;
    lol[2] = biolibrary;
    lol[3] = NULL;

    printf("total words in my lol: %d\n", wordsinlol(lol));

    free(lol);
    free(biolibrary);
    free(biography);
    free(monologue);
    free(sentence);
    free(word);
}

输出:

我的英雄联盟中总单词数为:243

18
只是想指出,arr[a][b][c]不是***arr。指针的指针使用引用的引用,而arr[a][b][c]按行主序存储为常规数组。 - MCCCS
@pmg,我可以使用char *ptr =“我的句子”;并将ptr传递给函数,为什么要使用**呢? - naumaan
1
@user143252 -- 如果你有 "my sentence", "his sentence", "her sentence", 和 "their sentence",你可以这样做 char *ptr1 = "my sentence"; 等等... 但是更方便的方法是创建一个大小为5(4 + NULL)指针的数组:char *ptrs[5] = {"my sentence", "his sentence", ..., NULL}。当你把这个数组传递给函数 (foo(ptrs)) 时,数组会自动转换为类型 char** - pmg

218

一个原因是您想在函数参数中传递指针并更改其值,为此您需要指向指针的指针。

简单来说,当您希望在函数调用之外保留(或保持更改)内存分配或赋值时,请使用**。(因此,请将具有双指针参数的函数传递给它。)

这可能不是一个非常好的例子,但它将向您展示基本用法:

#include <stdio.h>
#include <stdlib.h>

void allocate(int **p)
{
    *p = (int *)malloc(sizeof(int));
}

int main()
{
    int *p = NULL;
    allocate(&p);
    *p = 42;
    printf("%d\n", *p);
    free(p);
}

15
如果allocatevoid allocate(int *p),而你调用它时使用allocate(p),那么不会有任何不同。 - Incerteza
@AlexanderSupertramp 是的。代码将会发生段错误。请看Silviu的回答。 - Abhishek
@Asha,allocate(p)和allocate(&p)有什么区别? - user2979872
1
@Asha - 我们不能直接返回指针吗?如果我们必须保持它为空,那么这种情况的实用案例是什么? - Shabirmean
@user2979872 allocate(p):p是按值传递的,因此在函数中进行的更改不会反映在主方法中。 allocate(&p):p是按引用传递的,因此p中的更改会反映在主方法中。然而,这里有一个问题。如果使用allocate(p)并且我们更改了指向b的地址中的值,则更改将直接反映在main()中的值,因为更改发生在内存位置上。 再次强调,p中的值更改仍然不会反映出来。 - Ankit Arora
OP表示使用**的一个原因是更改传递给函数的指针的值。难道指针不是通过引用/地址传递的,因此对其进行的任何更改都会在函数范围之外进行“永久”更改吗?(通过访问指针地址和取消引用进行更改) - henhen

136
  • 假设您有一个指针,它的值是一个地址。
  • 但现在您想改变那个地址。
  • 您可以通过执行pointer1 = pointer2来实现,从而将pointer1的地址设置为pointer2的地址。
  • 但如果您在函数内部执行此操作,并且希望结果在函数完成后仍然存在,则需要进行一些额外的工作。您需要一个新的指针3来指向pointer1。将pointer3传递给函数。

  • 下面是一个例子,请先查看下面的输出以了解情况。

#include <stdio.h>

int main()
{

    int c = 1;
    int d = 2;
    int e = 3;
    int * a = &c;
    int * b = &d;
    int * f = &e;
    int ** pp = &a;  // pointer to pointer 'a'

    printf("\n a's value: %x \n", a);
    printf("\n b's value: %x \n", b);
    printf("\n f's value: %x \n", f);
    printf("\n can we change a?, lets see \n");
    printf("\n a = b \n");
    a = b;
    printf("\n a's value is now: %x, same as 'b'... it seems we can, but can we do it in a function? lets see... \n", a);
    printf("\n cant_change(a, f); \n");
    cant_change(a, f);
    printf("\n a's value is now: %x, Doh! same as 'b'...  that function tricked us. \n", a);

    printf("\n NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' \n");
     printf("\n change(pp, f); \n");
    change(pp, f);
    printf("\n a's value is now: %x, YEAH! same as 'f'...  that function ROCKS!!!. \n", a);
    return 0;
}

void cant_change(int * x, int * z){
    x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", x);
}

void change(int ** x, int * z){
    *x = z;
    printf("\n ----> value of 'a' is: %x inside function, same as 'f', BUT will it be the same outside of this function? lets see\n", *x);
}

这里是输出结果: (请先阅读本文)

 a's value: bf94c204

 b's value: bf94c208 

 f's value: bf94c20c 

 can we change a?, lets see 

 a = b 

 a's value is now: bf94c208, same as 'b'... it seems we can, but can we do it in a function? lets see... 

 cant_change(a, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c208, Doh! same as 'b'...  that function tricked us. 

 NOW! lets see if a pointer to a pointer solution can help us... remember that 'pp' point to 'a' 

 change(pp, f); 

 ----> value of 'a' is: bf94c20c inside function, same as 'f', BUT will it be the same outside of this function? lets see

 a's value is now: bf94c20c, YEAH! same as 'f'...  that function ROCKS!!!. 

10
这是一个很好的回答,真正帮助我想象了双指针的目的和用处。 - Justin
1
@Justin,你看了我上面那个答案了吗?它更简洁 :) - Brian Joseph Spinos
15
很好的回答,只是需要解释一下<code>void cant_change(int * x, int * z)</code>为什么失败,因为它的参数只是新的本地作用域指针,其初始化方式与a和f指针相同(因此它们与a和f不同)。 - Pedro Reis
1
简单?真的吗?;) - alk
1
这个答案真的很好地解释了指向指针最常见的用法之一,谢谢! - tonyjosi
这就是我一直在寻找的答案。谢谢! - Nisal Dissanayake

70

Asha 的回答基础上,如果你使用单个指针来表示下面的示例(例如,alloc1()),则会丢失函数内分配的内存的引用。

#include <stdio.h>
#include <stdlib.h>

void alloc2(int** p) {
    *p = (int*)malloc(sizeof(int));
    **p = 10;
}

void alloc1(int* p) {
    p = (int*)malloc(sizeof(int));
    *p = 10;
}

int main(){
    int *p = NULL;
    alloc1(p);
    //printf("%d ",*p);//undefined
    alloc2(&p);
    printf("%d ",*p);//will print 10
    free(p);
    return 0;
}

发生这种情况的原因是在alloc1中指针是按值传递的。因此,当它被重新赋值为alloc1内部malloc调用的结果时,更改不涉及不同作用域中的代码。


1
如果p是静态整数指针会发生什么?会得到分段错误。 - kapilddit
不能仅仅用free(p),还需要if(p) free(*p) - Shijing Lv
@ShijingLv:*p 评估为一个值为10的 int,将其传递给 free() 是个坏主意。 - alk
1
alloc1() 函数中的内存分配引入了内存泄漏。由于从函数返回,指向要释放的指针值已经丢失。 - alk
3
在C语言中,不需要对malloc的结果进行强制类型转换。 - alk
这段与编程有关的内容是:它会解决内存泄漏的问题吗?int* alloc1(int* p) { p = (int*)malloc(sizeof(int)); *p = 10; return p} 当然没有意义,因为你不需要向这样的函数传递任何参数。 - Serhii

27

1. 基本概念 -

当您声明如下内容时:-

1. char *ch - (称为字符指针)
- ch 包含单个字符的地址
- (*ch) 将解引用为字符的值。

2. char **ch -
'ch' 包含字符指针数组的地址。(与 1 中相同)
'*ch' 包含单个字符的地址。(注意,由于声明方式不同,它与 1 不同)。
(**ch) 将解引用为字符的确切值。

增加更多指针会扩展数据类型的维度,从字符到字符串,到字符串数组等等... 您可以将其与 1d、2d、3d 矩阵进行关联。

因此,指针的使用取决于您如何声明它。

这是一个简单的代码示例:

int main()
{
    char **p;
    p = (char **)malloc(100);
    p[0] = (char *)"Apple";      // or write *p, points to location of 'A'
    p[1] = (char *)"Banana";     // or write *(p+1), points to location of 'B'

    cout << *p << endl;          //Prints the first pointer location until it finds '\0'
    cout << **p << endl;         //Prints the exact character which is being pointed
    *p++;                        //Increments for the next string
    cout << *p;
}

2. 双指针的另一种应用 -
(这也涵盖了传引用)

假设你想要从一个函数中更新一个字符。如果你尝试以下操作:-

void func(char ch)
{
    ch = 'B';
}

int main()
{
    char ptr;
    ptr = 'A';
    printf("%c", ptr);

    func(ptr);
    printf("%c\n", ptr);
}

输出将会是AA。这个方法不起作用,因为你已经将参数按值传递给函数。

正确的做法是 -

void func( char *ptr)        //Passed by Reference
{
    *ptr = 'B';
}

int main()
{
    char *ptr;
    ptr = (char *)malloc(sizeof(char) * 1);
    *ptr = 'A';
    printf("%c\n", *ptr);

    func(ptr);
    printf("%c\n", *ptr);
}

现在将这个要求扩展到更新字符串而不是字符。
为此,您需要将参数作为双指针在函数中接收。

void func(char **str)
{
    strcpy(str, "Second");
}

int main()
{
    char **str;
    // printf("%d\n", sizeof(char));
    *str = (char **)malloc(sizeof(char) * 10);          //Can hold 10 character pointers
    int i = 0;
    for(i=0;i<10;i++)
    {
        str = (char *)malloc(sizeof(char) * 1);         //Each pointer can point to a memory of 1 character.
    }

    strcpy(str, "First");
    printf("%s\n", str);
    func(str);
    printf("%s\n", str);
}

在这个例子中,该方法期望一个双指针作为参数以更新字符串的值。


#include int main() { char* ptr = NULL; ptr = (char*) malloc(255); // 分配一些内存 strcpy(ptr, "Stack Overflow Rocks..!!"); printf("%s\n", ptr); printf("%d\n", strlen(ptr)); free(ptr); return 0; }但也可以不使用双指针。 - kumar
*char *ch - 'ch' 包含字符指针数组的地址。 不,它包含了一个 char 指针数组的第一个元素的地址。一个指向 char* 数组的指针的类型可以像这样定义:char(*(*p)[42]),它将 p 定义为指向 42 个指向 char 的指针的数组的指针。 - alk
1
这个 malloc(sizeof(char) * 10); 不是为了分配10个指向 char 的指针,而是只为10个 char 分配空间。 - alk
1
这个循环 for(i=0;i<10;i++) { str = ... 没有使用索引 i - alk
在C语言中,不需要对malloc的结果进行强制类型转换。 - alk
显示剩余3条评论

26

今天我看到了一个非常好的例子,来自这篇博客文章,我将其总结如下。

假设你有一个用于链表节点的结构体,可能是这样的:

typedef struct node
{
    struct node * next;
    ....
} node;

现在您想实现一个remove_if函数,它将接受删除条件rm作为其中一个参数并遍历链表:如果一个条目满足条件(类似于rm(entry)==true),则其节点将从列表中移除。最后,remove_if返回链表的头部(可能与原始头部不同)。

您可以编写:

for (node * prev = NULL, * curr = head; curr != NULL; )
{
    node * const next = curr->next;
    if (rm(curr))
    {
        if (prev)  // the node to be removed is not the head
            prev->next = next;
        else       // remove the head
            head = next;
        free(curr);
    }
    else
        prev = curr;
    curr = next;
}

就像您的for循环一样。这个消息是:没有双指针,你必须维护一个prev变量来重新组织指针,并处理两种不同的情况。

但有了双指针,您实际上可以这样写:

// now head is a double pointer
for (node** curr = head; *curr; )
{
    node * entry = *curr;
    if (rm(entry))
    {
        *curr = entry->next;
        free(entry);
    }
    else
        curr = &entry->next;
}

现在你不需要一个 prev,因为你可以直接修改 prev->next 指向的内容

为了让事情更清楚,我们来跟一下代码。在移除时:

  1. 如果 entry == *head:那么就是 *head (==*curr) = *head->next -- head 现在指向新的头结点的指针。你可以直接改变 head 的内容为一个新的指针来做到这一点。
  2. 如果 entry != *head:同样地,*curr 就是 prev->next 所指向的内容,现在它指向了 entry->next

无论是哪种情况,你都可以用双重指针以一种统一的方式重新组织指针。


19
指向指针的指针也可以作为内存的“句柄”在函数之间传递,以实现可重定位内存的“句柄”传递。这基本上意味着函数可以更改句柄变量内部指针所指向的内存,并且每个使用该句柄的函数或对象都将正确地指向新重新定位(或分配)的内存。类库喜欢使用“不透明”的数据类型来执行此操作,即数据类型是您无需担心正在使用被指向的内存做什么的数据类型,您只需在库的函数之间传递“句柄”,以对该内存执行一些操作...库函数可以在幕后分配和释放内存,而无需显式担心内存管理过程或句柄的指向位置。
例如:
#include <stdlib.h>

typedef unsigned char** handle_type;

//some data_structure that the library functions would work with
typedef struct 
{
    int data_a;
    int data_b;
    int data_c;
} LIB_OBJECT;

handle_type lib_create_handle()
{
    //initialize the handle with some memory that points to and array of 10 LIB_OBJECTs
    handle_type handle = malloc(sizeof(handle_type));
    *handle = malloc(sizeof(LIB_OBJECT) * 10);

    return handle;
}

void lib_func_a(handle_type handle) { /*does something with array of LIB_OBJECTs*/ }

void lib_func_b(handle_type handle)
{
    //does something that takes input LIB_OBJECTs and makes more of them, so has to
    //reallocate memory for the new objects that will be created

    //first re-allocate the memory somewhere else with more slots, but don't destroy the
    //currently allocated slots
    *handle = realloc(*handle, sizeof(LIB_OBJECT) * 20);

    //...do some operation on the new memory and return
}

void lib_func_c(handle_type handle) { /*does something else to array of LIB_OBJECTs*/ }

void lib_free_handle(handle_type handle) 
{
    free(*handle);
    free(handle); 
}


int main()
{
    //create a "handle" to some memory that the library functions can use
    handle_type my_handle = lib_create_handle();

    //do something with that memory
    lib_func_a(my_handle);

    //do something else with the handle that will make it point somewhere else
    //but that's invisible to us from the standpoint of the calling the function and
    //working with the handle
    lib_func_b(my_handle); 

    //do something with new memory chunk, but you don't have to think about the fact
    //that the memory has moved under the hood ... it's still pointed to by the "handle"
    lib_func_c(my_handle);

    //deallocate the handle
    lib_free_handle(my_handle);

    return 0;
}

希望这能帮到你,
Jason

处理类型为什么是unsigned char?void 可以同样工作吗? - Connor Clark
5
使用unsigned char是因为我们要存储指向二进制数据的指针,这些数据将以原始字节形式表示。使用void类型将需要在某个时候进行强制转换,并且通常不如unsigned char易读,无法准确表达意图。 - Jason

9
有些晚了,但希望可以帮到某些人。
在 C 语言中,数组总是在堆栈上分配内存,因此函数无法返回(非静态)数组,因为在当前块的执行结束时,分配在堆栈上的内存会自动释放。在处理二维数组(即矩阵)并实现一些可以修改和返回矩阵的函数时,这真的很麻烦。要实现这个功能,您可以使用指向指针的指针来实现具有动态分配内存的矩阵:
/* Initializes a matrix */
double** init_matrix(int num_rows, int num_cols){
    // Allocate memory for num_rows float-pointers
    double** A = calloc(num_rows, sizeof(double*));
    // return NULL if the memory couldn't allocated
    if(A == NULL) return NULL;
    // For each double-pointer (row) allocate memory for num_cols floats
    for(int i = 0; i < num_rows; i++){
        A[i] = calloc(num_cols, sizeof(double));
        // return NULL if the memory couldn't allocated
        // and free the already allocated memory
        if(A[i] == NULL){
            for(int j = 0; j < i; j++){
                free(A[j]);
            }
            free(A);
            return NULL;
        }
    }
    return A;
} 

这里有一个说明:

double**       double*           double
             -------------       ---------------------------------------------------------
   A ------> |   A[0]    | ----> | A[0][0] | A[0][1] | A[0][2] | ........ | A[0][cols-1] |
             | --------- |       ---------------------------------------------------------
             |   A[1]    | ----> | A[1][0] | A[1][1] | A[1][2] | ........ | A[1][cols-1] |
             | --------- |       ---------------------------------------------------------
             |     .     |                                    .
             |     .     |                                    .
             |     .     |                                    .
             | --------- |       ---------------------------------------------------------
             |   A[i]    | ----> | A[i][0] | A[i][1] | A[i][2] | ........ | A[i][cols-1] |
             | --------- |       ---------------------------------------------------------
             |     .     |                                    .
             |     .     |                                    .
             |     .     |                                    .
             | --------- |       ---------------------------------------------------------
             | A[rows-1] | ----> | A[rows-1][0] | A[rows-1][1] | ... | A[rows-1][cols-1] |
             -------------       ---------------------------------------------------------

双指针指向双指针的A指向一个内存块的第一个元素A[0],该内存块的元素本身是双指针。您可以将这些双指针想象为矩阵的行。这就是为什么每个双指针都会为类型为double的num_cols元素分配内存的原因。此外,A[i]指向第i行,即A[i]指向A[i][0],而这只是第i行内存块中的第一个double元素。最后,您可以很容易地使用A[i][j]访问第i行第j列的元素。下面是演示用法的完整示例:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* Initializes a matrix */
double** init_matrix(int num_rows, int num_cols){
    // Allocate memory for num_rows double-pointers
    double** matrix = calloc(num_rows, sizeof(double*));
    // return NULL if the memory couldn't allocated
    if(matrix == NULL) return NULL;
    // For each double-pointer (row) allocate memory for num_cols
    // doubles
    for(int i = 0; i < num_rows; i++){
        matrix[i] = calloc(num_cols, sizeof(double));
        // return NULL if the memory couldn't allocated
        // and free the already allocated memory
        if(matrix[i] == NULL){
            for(int j = 0; j < i; j++){
                free(matrix[j]);
            }
            free(matrix);
            return NULL;
        }
    }
    return matrix;
}

/* Fills the matrix with random double-numbers between -1 and 1 */
void randn_fill_matrix(double** matrix, int rows, int cols){
    for (int i = 0; i < rows; ++i){
        for (int j = 0; j < cols; ++j){
            matrix[i][j] = (double) rand()/RAND_MAX*2.0-1.0;
        }
    }
}


/* Frees the memory allocated by the matrix */
void free_matrix(double** matrix, int rows, int cols){
    for(int i = 0; i < rows; i++){
        free(matrix[i]);
    }
    free(matrix);
}

/* Outputs the matrix to the console */
void print_matrix(double** matrix, int rows, int cols){
    for(int i = 0; i < rows; i++){
        for(int j = 0; j < cols; j++){
            printf(" %- f ", matrix[i][j]);
        }
        printf("\n");
    }
}


int main(){
    srand(time(NULL));
    int m = 3, n = 3;
    double** A = init_matrix(m, n);
    randn_fill_matrix(A, m, n);
    print_matrix(A, m, n);
    free_matrix(A, m, n);
    return 0;
}

不。请阅读这个链接:https://gustedt.wordpress.com/2014/09/08/dont-use-fake-matrices/。现在回来并重构你的代码。希望我没有打扰到你。 - Chef Gladiator
这也是不正确的:“n C数组总是在堆栈上分配内存...”,正如你在帖子中展示的那样。 - Chef Gladiator

8

一个你可能已经看过很多次的简单示例

int main(int argc, char **argv)

在第二个参数中,你有它:指向指针的指针。
请注意,指针符号(char* c)和数组符号(char c[])在函数参数中是可以互换的。所以你也可以写成char *argv[]。换句话说,char *argv[]和char **argv是可以互换的。
上述内容实际上表示的是一个字符序列数组(程序在启动时给出的命令行参数)。
有关上述函数签名的更多细节,请参见this answer

2
指针表示法(char* c)和数组表示法(char c[])在函数参数中是可以互换的,它们具有完全相同的含义。然而,在函数参数之外,它们是不同的。 - pmg

8

字符串是双指针用法的一个很好的例子。字符串本身是一个指针,所以每当您需要指向一个字符串时,您都需要一个双指针。


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