C语言中**是做什么的?

What does ** do in C language?

我是 C 的新手,在 java 方面有很好的背景,我正在尝试理解指针和数组。

我知道下标 operator[] 是数组定义的一部分,所以:

int numbers[] = {1,3,4,5};

会创建一个整数数组,在内存中表示为 16 字节,4 批 4 字节:

numbers[0] = 1, address 0061FF1C
numbers[1] = 3, address 0061FF20
numbers[2] = 4, address 0061FF24
numbers[3] = 5, address 0061FF28

然而,当谈到指针时,我的知识开始崩溃,所以如果我要创建一个指向数组数字的指针,我会执行以下操作:

int *pNumbers = &numbers[0];

看起来像这样:

我猜它的大小是 4 个字节?

然而 ** 我读作“指向指针的指针”,这对我来说毫无意义,为什么有人想要指向指针的指针,当然如果 a->b->c 那么 a- >c 就足够了吗?我知道我遗漏了一些东西,它一定与数组有关,因为 argv 可以是 char[ ]char ** 类型,如下所示:

int main(int argc, char **argv){}

所以:

我会将 char **argv 理解为 char** argv。现在,char* 基本上是 char 的数组,所以 (char*)*char.

的数组

换句话说,argv 是一个字符串数组。在此特定示例中:调用

myExe dummyArg1 dummyArg2

在控制台中会使 argv 成为

argv[0] = "myExe"
argv[1] = "dummyArg1"
argv[2] = "dummyArg2"

argv 参数的传统方法是 char *argv[] ,它提供了更多关于它是什么的信息,一个指向字符的指针数组(即字符串数组)。

但是,当将数组传递给函数时,它会衰减为指针,留下指向 charchar **.

的指针

当然,在取消引用指向指针的指针时也可以使用双星号,因此在问题末尾没有添加上下文的情况下,** 在 C 中的含义这个问题有两个答案,取决于上下文。

继续 argv 示例,获取 argv 中第一个元素的第一个字符的一种方法是 argv[0][0] 您可以使用两次取消引用运算符,如 **argv.

数组索引和取消引用在大多数地方是可以互换的,因为对于任何指针 数组 p 和索引 i 表达式 p[i]相当于 *(p + i)。如果 i0 那么我们有 *(p + 0) 可以缩短为 *(p)*p.

相同

出于好奇,因为 p[i] 等价于 *(p + i) 和加法的 commutative property,表达式 *(p + i) 等于 *(i + p) 这导致p[i] 等于 i[p].


最后一个关于过度使用指针的警告,您有时可能会听到短语 three-star programmer,这是当一个人使用三个星号时,如 ***(例如在指向指针的指针中指针)。但引用 link

Just to be clear: Being called a ThreeStarProgrammer is usually not a compliment

还有另一个警告:An array of arrays is not the same as a pointer to a pointer(Link 我的一个旧答案,它还显示了指向指针的指针的内存布局,而不是数组的数组。)

声明中的

**表示指向指针的指针。指针本身是一种数据类型,并且像其他数据类型一样它可以有一个指针。

int i = 5, j = 6; k = 7;
int *ip1 = &i, *ip2 = &j; 
int **ipp = &ip1;  

指向指针的指针在分配动态二维数组的情况下很有用。分配一个 10x10 二维数组(可能不连续)

int **m = malloc(sizeof(int *)*10;  
for(int i = 0; i < 10; i++)
    m[i] = malloc(sizeof(int)*10  

当你想通过一个函数来改变一个指针的值时也会用到它。

void func (int **p, int n)  
{
    *p = malloc(sizeof(int)*n); // Allocate an array of 10 elements 
}

int main(void)
{
    int *ptr = NULL;
    int n = 10;
    func(&ptr, n);
    if(ptr)
    {
        for(int i = 0; i < n; i++)
        {  
             ptr[i] = ++i;
        }  
    }

    free(ptr);
}

延伸阅读:Pointer to Pointer.

**表示指向指针的指针。如果你想通过引用传递参数,你会使用*,但是如果你想通过引用传递指针本身,那么你需要一个指向指针的指针,因此 **.

例如**是指向指针的指针。 char **argvchar *argv[] 相同,这与 char argv[][] 相同。这是一个矩阵。

例如,您可以声明一个包含 4 行但列数不同的矩阵,例如 JaggedArrays。

它被表示为一个矩阵。

Here你在记忆中有一个表示。

考虑一下您是否有 table 个指针 - 例如 table 个字符串(因为 "C" 中的字符串只是作为指向字符串第一个字符的指针处理) .

那么你需要一个指向table中第一个指针的指针。因此 "char **".

如果您有一个包含所有值的内联 table,例如整数的 two-dimensional table,那么完全有可能只使用一个间接级别(即只是一个简单的指针,例如 "int *")。但是当中间有一个指针需要取消引用才能得到最终结果时,就会创建第二层间接寻址,然后 pointer-to-pointer 就必不可少了。

此处再次说明。在 "C" 中,通过指针表示法(例如“*ptr”)取消引用与数组索引表示法(例如 ptr[0])没有什么区别,除了数组表示法中明显的索引值。唯一一次星号与括号真正重要的是分配变量时(例如 int *x; 与 int x[1] 非常不同)。

你说的int *例子

And I'm guessing it would be of size 4 bytes?

与 Java 不同,C 不指定其数据类型的确切大小。不同的实现可以并且确实使用不同的大小(但每个实现必须一致)。现在 4 字节的 ints 很常见,但是 ints 可以小到两个字节,没有什么本质上将它们限制为四个。指针的大小甚至更少指定,但它通常取决于 C 实现所针对的硬件体系结构。最常见的指针大小是四个字节(通常用于 32 位体系结构)和八个字节(通常用于 64 位体系结构)。

what is this (**)?

在您提供的上下文中,它是类型指示符 char ** 的一部分,它描述了指向指向 char 的指针的指针,正如您所想的。

what use does it have?

与指向任何其他数据类型的指针的用法大致相同。有时您希望或需要间接访问指针值,就像您可能希望或需要间接访问任何其他类型的值一样。此外,它对于指向指针数组(的第一个元素)很有用,这就是它在 C main() 函数的第二个参数中的使用方式。

在这种特殊情况下,pointed-to 数组中的每个 char * 本身都指向程序的一个 command-line 参数。

how is it represented in memory?

C 没有指定,但通常指向指针的指针与指向任何其他类型值的指针具有相同的表示形式。它指向的值只是一个指针值。

** 代表指向指针的指针,如您所知。我会解释你的每一个问题:

what is this (**)?

指向指针的指针。有时人们称之为双指针。例如:

int a = 3;
int* b = &a; // b is pointer. stored address of a
int**b = &b;  // c is pointer to pointer. stored address of b
int***d = &c; // d is pointer to pointer to pointer. stored address of d. You get it. 

how is it represented in memory?

上例中的

c 只是一个普通变量,与其他变量(指针、整数 ...)具有相同的表示形式。变量 c 的内存大小与 b 相同,它取决于平台。例如,在32位计算机上,每个变量地址包含32bit,因此大小为4字节(8x4=32位) 在64位计算机上,每个变量地址为64bit,因此大小为8字节(8x8=64位)。

what use does it have?

pointer to pointer有很多用法,看你的情况。例如,这是我在算法 class 中学到的一个例子。你有一个链表。现在,您想编写一个方法来更改该链表,并且您的方法可能会更改链表的头部。 (示例:删除一个值为 5 的元素,删除头元素,交换,...​​)。所以你有两种情况:

1.如果只是传一个head元素的指针。也许那个 head 元素将被删除,并且这个指针不再有效。

2。如果你传递 head 元素的指针。 如果你的 head 元素被删除,你不会遇到任何问题,因为 pointer 的指针仍然存在。它只是改变另一个头节点的值。

上面的例子可以参考这里:pointer to pointer in linked list

另一种用法是在 two-dimensional 数组中使用。 C 不同于 Java。 C中的二维数组,实际上只是一个连续的内存块。 Java中的二维数组是多内存块(取决于你的矩阵行)

希望对您有所帮助:)

它是一个指向指针的指针。如果您问为什么要使用指向指针的指针,这里有一个类似的线程,它以各种好的方式回答了这个问题。

Why use double pointer? or Why use pointers to pointers?

事实上,在 C 中数组是指针 :

char* string = "HelloWorld!";

等同于:char string[] = "HelloWorld"; 而这个:char** argv 就像你说的 "pointer to a pointer".

可以看做是一个字符串数组,即多个字符串。但请记住,字符串是字符指针!

参见:Java 中的 main 方法类似于 C main 函数。是这样的:

public static void main(String[] args){}

即字符串数组。它在 C 中的工作方式相同,String[] args 变为 char** argschar* args[].

总结一下:type* name = blablabla; 可能是 "type" 的数组。 type** name = blabla; 可能是一个数组数组。

在 C 中,参数是按值传递的。例如,如果您在 main

中有一个整数变量
int main( void )
{
    int x = 10;
    //...

和以下函数

void f( int x )
{
    x = 20;
    printf( "x = %d\n", x );
} 

那么如果你像这样调用 main 中的函数

f( x );

然后参数获取main中变量x的值。然而,参数本身在内存中占用的范围与参数不同。所以函数中参数的任何变化都不会影响main中的原始变量,因为这些变化发生在不同的内存范围内。

那么如何在函数中改变main中的变量呢?

您需要使用指针传递对变量的引用。

在这种情况下,函数声明如下所示

void f( int *px );

函数定义为

void f( int *px )
{
    *px = 20;
    printf( "*px = %d\n", *px );
} 

在这种情况下,原始变量 x 占用的内存范围发生了变化,因为在函数中我们可以使用指针访问该范围

    *px = 20;

当然函数必须像main一样调用

f( &x );

请注意,作为指针的参数本身 px 通常是函数的局部变量。即函数创建此变量并使用变量 x.

的地址对其进行初始化

现在假设您在 main 中声明了一个指针,例如以下方式

int main( void )
{
   int *px = malloc( sizeof( int ) );
   //..

函数定义如下

void f( int *px )
{
    px = malloc( sizeof( int ) );

    printf( "px = %p\n", px );
}

因为参数px是一个局部变量,赋给它任何值都不会影响原始指针。该函数更改的内存范围与原始指针 px 在 main.

中占用的范围不同

如何改变函数中原来的指针? 只需通过引用传递即可!

例如

f( &px );
//...

void f( int **px )
{
    *px = malloc( sizeof( int ) );

    printf( "*px = %p\n", *px );
}

在这种情况下,存储在原始指针中的值将在函数内更改,因为使用解引用的函数访问定义原始指针的相同内存范围。

问:这是什么 (**)?

A:是的,正是这样。一个指针 指针。

问:有什么用?

A:它有很多用途。特别是在表示二维数据(图像等)时。在您的示例中,char** argv 可以被认为是 char 数组的数组。在这种情况下,每个 char* 都指向一个字符串的开头。您实际上可以像这样自己明确声明此数据。

char* myStrings[] = {
    "Hello",
    "World"
};

char** argv = myStrings;

// argv[0] -> "Hello"
// argv[1] -> "World"

当您访问像数组这样的指针时,您索引它的数字和元素本身的大小用于偏移到数组中下一个元素的地址。您也可以像这样访问所有数字,实际上这基本上就是 C 正在做的事情。请记住,编译器知道像 int 这样的类型在编译时使用了多少字节。所以它知道下一个元素的每一步应该有多大。

*(numbers + 0) = 1, address 0x0061FF1C
*(numbers + 1) = 3, address 0x0061FF20
*(numbers + 2) = 4, address 0x0061FF24
*(numbers + 3) = 5, address 0x0061FF28

* 运算符称为取消引用运算符。它用于从指针指向的内存中检索值。 numbers 实际上只是指向数组中第一个元素的指针。

在我的示例中,假设 pointer/address 是 4 个字节,myStrings 可能看起来像这样,这意味着我们在 32 位机器上。

myStrings = 0x0061FF14

// these are just 4 byte addresses
(myStrings + 0) -> 0x0061FF14 // 0 bytes from beginning of myStrings
(myStrings + 1) -> 0x0061FF18 // 4 bytes from beginning of myStrings

myStrings[0] -> 0x0061FF1C // de-references myStrings @ 0 returning the address that points to the beginning of 'Hello'
myStrings[1] -> 0x0061FF21 // de-references myStrings @ 1 returning the address that points to the beginning of 'World'

// The address of each letter is 1 char, or 1 byte apart
myStrings[0] + 0 -> 0x0061FF1C  which means... *(myStrings[0] + 0) = 'H'
myStrings[0] + 1 -> 0x0061FF1D  which means... *(myStrings[0] + 1) = 'e'
myStrings[0] + 2 -> 0x0061FF1E  which means... *(myStrings[0] + 2) = 'l'
myStrings[0] + 3 -> 0x0061FF1F  which means... *(myStrings[0] + 3) = 'l'
myStrings[0] + 4 -> 0x0061FF20  which means... *(myStrings[0] + 4) = 'o'

首先,请记住 C 对待数组的方式与 Java 非常不同。像

这样的声明
char foo[10];

为 10 个 char 值和 分配足够的存储空间 (模数任何额外的 space 以满足对齐要求);没有为指向第一个元素的指针或任何其他类型的元数据(例如数组大小或元素 class 类型)预留额外的存储空间。除了数组元素本身1,没有对象foo。相反,语言中有一条规则,编译器在任何时候看到数组 expression 不是 sizeof 或一元 & 运算符(或string literal used to initialize another array in a declaration), 它隐式地将 expression 从类型 "N-element array of T" 转换为 "pointer to T", 表达式的值是地址数组的第一个元素。

这有几个含义。首先是,当你将数组表达式作为参数传递给函数时,函数实际接收的是一个指针值:

char foo[10];
do_something_with( foo );
...
void do_something_with( char *p )
{
  ...
}

实参foo对应的形参p是指向char的指针,不是char的数组。为了使事情变得混乱,C 允许将 do_something_with 声明为

void do_something_with( char p[] )

甚至

void do_something_with( char p[10] )

但是在函数参数声明的情况下,T p[]T p[N]等同于T *p,并且这三个都将p声明为指针,而不是数组2。请注意,这仅适用于函数参数声明。

第二个含义是下标运算符[]既可以用在指针操作数上,也可以用在数组操作数上,例如

char foo[10];
char *p = foo;
...
p[i] = 'A'; // equivalent to foo[i] = 'A';

最后的含义导致处理指向指针的指针的一种情况 - 假设您有一个指针数组,例如

const char *strs[] = { "foo", "bar", "bletch", "blurga", NULL };

strs是一个5元素数组const char *3;然而,如果你将它传递给一个像

这样的函数
do_something_with( strs );

那么函数接收到的实际上是一个指向指针的指针,而不是指针数组:

void do_something_with( const char **strs ) { ... }

指向指针(和更高级别的间接)的指针也出现在以下情况中:

  • 写入一个指针类型的参数:记住C是按值传递所有参数;函数定义中的形参与函数调用中的实参在内存中是不同的对象,所以如果想让函数更新实参的值,必须传递一个指针 该参数:

    void foo( T *param ) // for any type T
    {
      *param = new_value(); // update the object param *points to*
    }
    
    void bar( void )
    {
      T x;
      foo( &x );   // update the value in x
    }
    
    现在假设我们将类型T替换为指针类型R *,那么我们的代码片段如下所示:

    void foo( R **param ) // for any type R *
    {
      ...
      *param = new_value(); // update the object param *points to*
      ...
    } 
    
    void bar( void )
    {
      R *x;
      foo( &x );   // update the value in x
    }
    
    相同的语义 - 我们正在更新 x 中包含的值。只是在这种情况下,x已经有了指针类型,所以我们必须传递一个指针给指针。这可以扩展到更高层次的方向:

    void foo( Q ****param ) // for any type Q ***
    {
      ...
      *param = new_value(); // update the object param *points to*
      ...
    } 
    
    void bar( void )
    {
      Q ***x;
      foo( &x );   // update the value in x
    }
    
  • Dynamically-allocated multi-dimensional arrays:在 C 中分配 multi-dimensional 数组的一种常用技术是分配一个指针数组,并且对于该数组的每个元素分配一个指针指向的缓冲区:

    T **arr;
    arr = malloc( rows * sizeof *arr );  // arr has type T **, *arr has type T *
    if ( arr )
    {
      for ( size_t i = 0; i < rows; i++ )
      {
        arr[i] = malloc( cols * sizeof *arr[i] ); // arr[i] has type T *
        if ( arr[i] )
        {
          for ( size_t j = 0; j < cols; j++ )
          {
            arr[i][j] = some_initial_value();
          }
        }
      }
    }
    
    这可以扩展到更高级别的间接寻址,因此您可以使用 T ***T **** 等类型。


1. 这是数组表达式可能不是赋值目标的部分原因;没有什么可以分配给 .

  1. 这是从 C 派生而来的 B 编程语言的遗留问题;在 B 中,指针声明为 auto p[]

  2. 每个字符串文字都是 char 的数组,但是因为我们没有使用它们来初始化 char 的单个数组,所以表达式被转换为指针值。

我想我要在这里添加我自己的答案,因为每个人都做了很棒的工作,但我真的很困惑指针指向指针的意义所在。我之所以想到这个是因为我的印象是除了指针之外的所有值都是按值传递的,而指针是按引用传递的。请参阅以下内容:

void f(int *x){
    printf("x: %d\n", *x);
    (*x)++;
}

void main(){
   int x = 5;
   int *px = &x;
   f(px);
   printf("x: %d",x);
}

会产生:

x: 5
x: 6

这让我认为(出于某种原因)指针是通过引用传递的,因为我们正在传递指针、操纵它然后分解并打印新值。如果您可以在函数中操作指针...为什么要有指向指针的指针才能开始操作指针!

这对我来说似乎是错误的,这是正确的,因为当您已经可以在函数中操作指针时,用指针来操作指针是愚蠢的。不过 C 的问题; 一切都是按值传递的,甚至是指针。让我使用一些伪值而不是地址进一步解释。

//this generates a new pointer to point to the address so lets give the
//new pointer the address 0061FF28, which has the value 0061FF1C.
void f(int 0061FF1C){
    // this prints out the value stored at 0061FF1C which is 5
    printf("x: %d\n", 5);
    // this FIRST gets the value stored at 0061FF1C which is 5
    // then increments it so thus 6 is now stored at 0061FF1C
    (5)++;
}

void main(){
   int x = 5;

   // this is an assumed address for x
   int *px = 0061FF1C;

   /*so far px is a pointer with the address lets say 0061FF24 which holds
    *the value 0061FF1C, when passing px to f we are passing by value...
    *thus 0061FF1C is passed in (NOT THE POINTER BUT THE VALUE IT HOLDS!)
    */

   f(px);

   /*this prints out the value stored at the address of x (0061FF1C) 
    *which is now 6
    */
   printf("x: %d",6);
}

我对指针的指针的主要误解是按值传递与按引用传递。原始指针根本没有传递到函数中,所以我们不能改变它指向的地址,只能改变新指针的地址(它有一种旧指针的错觉,因为它指向旧指针的地址指向!)。