46

我是 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){}

所以:

  • 这是什么(**)?
  • 它有什么用?
  • 它在内存中是如何表示的?
4

14 回答 14

47

在 C 中,参数是按值传递的。例如,如果您在 main 中有一个整数变量

int main( void )
{
    int x = 10;
    //...

和以下功能

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

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

f( x );

x然后参数获取main中变量的值。但是,参数本身在内存中所占的范围与参数不同。因此函数中参数的任何变化都不会影响 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 );
}

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

于 2016-01-26T19:08:51.090 回答
15

问:这是什么(**)?

A:是的,正是这样。指向指针的指针。

问:它有什么用处?

A:它有很多用途。特别是在表示二维数据(图像等)方面。在您的示例char** argv中,可以将其视为 s 数组的数组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实际上只是指向数组中第一个元素的指针。

在我的示例中myStrings,假设指针/地址是 4 个字节,这意味着我们在 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'
于 2016-01-26T19:50:39.297 回答
9

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

但是,当将数组传递给函数时,它会衰减为指针,从而为您留下指向char或的指针char **


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

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

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

作为一个好奇心,由于p[i]等价于*(p + i)和加法的交换性质,表达式*(p + i)等于*(i + p)导致p[i]等于i[p]


最后是关于过度使用指针的警告,您有时可能会听到短语三星级程序员,即使用三个星号,如 in ***(如在指向指针的指针中)。但是从链接中引用

需要明确的是:被称为 ThreeStarProgrammer 通常不是恭维

另一个警告:数组数组指向指针的指针不同(链接到我的旧答案,它还显示了指向指针的指针的内存布局,而不是数组数组。)

于 2016-01-26T18:53:53.800 回答
7

**在声明中表示指向指针的指针。指针本身就是一种数据类型,与其他数据类型一样,它可以有一个指针。

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);
}

进一步阅读:指向指针的指针

于 2016-01-26T18:54:11.463 回答
5

**代表您知道名称的指针的指针。我会解释你的每一个问题:

这是什么 (**)?

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

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. 

它在内存中是如何表示的?

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

它有什么用?

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

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

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

您可以在此处参考上述示例:指向链表中指针的指针

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

希望这有帮助:)

于 2016-01-26T19:02:52.347 回答
4

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

于 2016-01-26T18:55:12.700 回答
4

考虑一下你是否有一个指针表——比如一个字符串表(因为“C”中的字符串被简单地作为指向字符串第一个字符的指针来处理)。

然后你需要一个指向表中第一个指针的指针。因此,“char **”。

如果你有一个包含所有值的内联表,比如一个二维整数表,那么完全有可能只使用一个间接级别(即只是一个简单的指针,比如“int *”)。但是当中间有一个指针需要取消引用才能得到最终结果时,就会创建第二级间接,然后指针指向指针是必不可少的。

这里的另一个澄清。在“C”中,通过指针表示法(例如“*ptr”)与数组索引表示法(例如ptr [0])解引用几乎没有区别,除了数组表示法中明显的索引值。唯一真正重要的星号与括号是在分配变量时(例如 int *x; 与 int x[1] 非常不同)。

于 2016-01-26T18:58:20.267 回答
4

int *说的例子

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

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

这是什么 (**)?

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

它有什么用?

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

在这种特殊情况下,char *指向数组中的每个本身都指向程序的命令行参数之一。

它在内存中是如何表示的?

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

于 2016-01-26T19:01:59.510 回答
4

首先,请记住 C 对数组的处理与 Java 非常不同。像这样的声明

char foo[10];

char为 10 个值分配足够的存储空间,仅此而已(模任何额外空间以满足对齐要求);没有为指向第一个元素的指针或任何其他类型的元数据(例如数组大小或元素类类型)预留额外的存储空间。foo除了数组元素本身1之外,没有其他对象。相反,语言中有一条规则,只要编译器看到一个数组表达式不是sizeof或一元运算符的操作数&(或用于在声明中初始化另一个数组的字符串文字),它就会隐式地将该表达式从类型转换为 " T“到”指针的 N​​ 元素数组T",表达式的值是数组第一个元素的地址。

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

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

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

void do_something_with( char p[] )

甚至

void do_something_with( char p[10] )

但是在函数参数声明的情况下,T p[]andT 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是一个 3 的 5 元素const char *数组;但是,如果您将其传递给类似的函数

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
    }
    
  • 动态分配的多维数组:在 C 中分配多维数组的一种常用技术是分配一个指针数组,并为该数组的每个元素分配一个指针指向的缓冲区:

    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 ***andT ****等 类型。


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

  1. 这是源自 C 的 B 编程语言的保留;在 B 中,指针被声明为auto p[].

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

于 2016-01-26T20:12:52.273 回答
4

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

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);
}

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

于 2016-01-26T22:13:53.330 回答
3

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

为什么要使用双指针?或者为什么使用指向指针的指针?

于 2016-01-26T19:06:18.403 回答
1

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

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

它表示为一个矩阵。

在这里,您在内存中有一个表示。

于 2016-01-26T18:56:03.833 回答
1

我会理解char **argvchar** argv。现在,char*基本上是 的数组char,因此(char*)*也是 的数组的数组char

换句话说(松散的),argv是一个字符串数组。在这个特定的例子中:调用

myExe dummyArg1 dummyArg2

在控制台中会argv变成

argv[0] = "myExe"
argv[1] = "dummyArg1"
argv[2] = "dummyArg2"
于 2016-01-26T18:53:34.073 回答
-4

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

char* string = "HelloWorld!";

相当于 this :char string[] = "HelloWorld"; 而 this :char** argv正如你所说的“指向指针的指针”。

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

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

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

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

总而言之:type* name = blablabla; 可能是一个“类型”数组。并且type** name = blabla;可能是一个数组数组。

于 2016-01-26T19:08:39.573 回答