指针与普通指针的指针

时间:2016-06-28 13:01:04

标签: c pointers

指针的目的是保存特定变量的地址。那么下面代码的内存结构应该是这样的:

int a = 5;
int *b = &a;
  

......记忆地址......值
  a ... 0x000002 ................... 5
  b ... 0x000010 ................... 0x000002

好的,好的。然后假设现在我想保存指针* b的地址。然后我们通常将双指针** c定义为

int a = 5;
int *b = &a;
int **c = &b;

然后内存结构如下:

  

......记忆地址......值
  a ... 0x000002 ................... 5
  b ... 0x000010 ................... 0x000002
  c ... 0x000020 ................... 0x000010

所以** c指的是* b。

的地址

现在我的问题是,为什么这种类型的代码,

int a = 5;
int *b = &a;
int *c = &b;

生成警告?

如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址是指一个变量,一个指针,一个双指针等,那么应该没有层次结构,所以下面的类型代码应该有效。

int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;

12 个答案:

答案 0 :(得分:91)

int a = 5;
int *b = &a;   
int *c = &b;

您收到警告,因为&b的类型为int **,并且您尝试初始化int *类型的变量。这两种类型之间没有隐式转换,导致警告。

要采用您想要工作的较长示例,如果我们尝试取消引用f,编译器将为我们提供int,而不是我们可以进一步取消引用的指针。

另请注意,在许多系统上intint*的大小不同(例如,指针可能长64位,长int 32位。如果您取消引用f并获得int,则会丢失一半的值,然后您甚至无法将其转换为有效指针。

答案 1 :(得分:54)

  

如果指针的目的只是为了保存内存地址,我想   如果我们要保存的地址应该没有层次结构   引用变量,指针,双指针......等等

在运行时,是的,指针只保存一个地址。但是在编译时,还有一个与每个变量相关联的类型。正如其他人所说,int*int**是两种不同的,不兼容的类型。

有一种类型,void*可以满足您的需求:它只存储一个地址,您可以为其分配任何地址:

int a = 5;
int *b = &a;
void *c = &b;

但是当您要取消引用void*时,您需要自己提供“缺失”类型信息:

int a2 = **((int**)c);

答案 2 :(得分:23)

  

现在我的问题是,为什么这种类型的代码,

int a = 5; 
int *b = &a; 
int *c = &b; 
     

生成警告?

你需要回到基础。

  • 变量有类型
  • 变量保持值
  • 指针是值
  • 指针指的是变量
  • 如果p是指针值,则*p是变量
  • 如果v是变量,则&v是指针

现在我们可以找到你发布的所有错误。

  

然后假设现在我想保存指针*b

的地址

没有。 *b是int类型的变量。它不是指针。 b是一个变量,其是一个指针。 *b变量,其值为整数。

  

**c是指*b的地址。

不,不,不。绝对不。如果您要了解指针, 可以正确理解这一点。

*b是一个变量;它是变量a的别名。变量a的地址是变量b的值。 **c未提及a的地址。相反,它是变量,它是变量a别名。 (同样是*b。)

正确的陈述是:变量cb地址。或者,等效地:c的值是指向b的指针。

我们怎么知道这个?回到基础。你说c = &b。那么c的价值是多少?一个指针。要什么? b

确保完全了解基本规则。

现在您希望了解变量和指针之间的正确关系,您应该能够回答有关代码为什么会出错的问题。

答案 3 :(得分:20)

C的类型系统需要这个,如果你想获得正确的警告,并且你想要编译代码。只有一个指针深度,你不知道指针是指向一个指针还是一个实际的整数。

如果您取消引用某个类型int**,则表示您获得的类型为int*,如果您取消引用int*,则类似为int。根据您的提案,类型将不明确。

从您的示例中,我们无法知道c是指向int还是int*

c = rand() % 2 == 0 ? &a : &b;

c指向什么类型?编译器不知道,所以下一行是不可能执行的:

*c;

在C中,所有类型信息在编译后都会丢失,因为每个类型都在编译时检查,不再需要。您的提议实际上会浪费内存和时间,因为每个指针都必须有关于指针中包含的类型的其他运行时信息。

答案 4 :(得分:17)

指针是具有附加类型语义的内存地址的抽象,并且在类似C类的语言中很重要。

首先,不能保证int *int **具有相同的大小或代表性(在他们现代的桌面架构上,但你不能依赖它普遍正确)。

其次,类型对指针算法很重要。给定类型为p的指针T *,表达式p + 1将生成类型为T的下一个对象的地址。因此,假设以下声明:

char  *cp     = 0x1000;
short *sp     = 0x1000;  // assume 16-bit short
int   *ip     = 0x1000;  // assume 32-bit int
long  *lp     = 0x1000;  // assume 64-bit long

表达式cp + 1为我们提供了下一个char对象的地址,即0x1001。表达式sp + 1为我们提供了下一个short对象的地址,即0x1002ip + 1为我们提供了0x1004lp + 1为我们提供了0x1008

所以,给定

int a = 5;
int *b = &a;
int **c = &b;

b + 1为我们提供了下一个int的地址,c + 1为我们提供了下一个指针int的地址。

如果希望函数写入指针类型的参数,则需要指针指针。请使用以下代码:

void foo( T *p )    
{
  *p = new_value(); // write new value to whatever p points to
}

void bar( void )
{
  T val;
  foo( &val );     // update contents of val
}

对于任何类型T 都是如此。如果我们用指针类型T替换P *,则代码变为

void foo( P **p )    
{
  *p = new_value(); // write new value to whatever p points to
}

void bar( void )
{
  P *val;
  foo( &val );     // update contents of val
}

语义完全相同,它只是不同的类型;形式参数p总是比变量val多一个间接层。

答案 5 :(得分:11)

  

如果我们要保存的地址是变量,指针,双指针

,我认为应该没有层次结构

如果没有“层次结构”,那么在没有任何警告的情况下生成UB将非常容易 - 这将是非常糟糕的。

考虑一下:

char c = 'a';
char* pc = &c;
char** ppc = &pc;
printf("%c\n", **ppc);   // compiles ok and is valid
printf("%c\n", **pc);    // error: invalid type argument of unary ‘*’

编译器给了我一个错误,因此它帮助我知道我做错了什么,我可以纠正错误。

但没有“等级”,如:

char c = 'a';
char* pc = &c;
char* ppc = &pc;
printf("%c\n", **ppc);   // compiles ok and is valid
printf("%c\n", **pc);    // compiles ok but is invalid

编译器不能给出任何错误,因为没有“层次结构”。

但是当行:

printf("%c\n", **pc);

执行,它是UB(未定义的行为)。

首先*pc读取char,好像它是一个指针,即使我们只保留1个字节,也可能读取4或8个字节。那是UB。

如果程序因上面的UB没有崩溃而只是返回了一些垃圾值,那么第二步就是取消引用垃圾值。 UB再一次。

<强>结论

类型系统通过将int *,int **,int ***等视为不同类型来帮助我们检测错误。

答案 6 :(得分:10)

  

如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址是变量,指针,双指针等等,那么应该没有层次结构,所以下面的代码类型应该是是有效的。

我认为这是你的误解:指针本身的目的是存储内存地址,但指针通常也有一个类型,以便我们知道在它指向的位置会发生什么。

特别是,与你不同,其他人真的想拥有这种层次结构,以便知道如何处理指针所指向的内存内容。

C指针系统的特点是附加了类型信息。

如果你这样做

int a = 5;

&a表示您得到的是int *,因此如果您取消引用,则会再次int

将其提升到下一个级别,

int *b = &a;
int **c = &b;

&b也是一个指针。但是不知道背后隐藏着什么,分别是。它指向什么,它是无用的。重要的是要知道取消引用指针会显示原始类型的类型,因此*(&b)int *,而**(&b)是我们使用的原始int

如果您认为在您的情况下不应该有类型的层次结构,您可以始终使用void *,尽管直接可用性非常有限。

答案 7 :(得分:9)

  

如果指针的目的只是为了保存内存地址,我认为如果我们要保存的地址是变量,指针,双指针等等,那么应该没有层次结构,所以下面的代码类型应该是是有效的。

这对于机器来说是真的(毕竟大概都是一个数字)。但是在许多语言中输入变量,意味着编译器可以确保您正确使用它们(类型对变量强加正确的上下文)

指向指针和指针(可能)的指针确实使用相同数量的内存来存储它们的值(请注意,对于int和指向int的指针不是这样,地址的大小与房子的大小。)

因此,如果您有一个地址的地址,您应该按原样使用而不是简单地址,因为如果您将指针作为一个简单的指针访问指针,那么您将能够操作int的地址,就好像它一样是一个int,它不是(没有任何其他东西替换int,你应该看到危险)。你可能会感到困惑,因为所有这些都是数字,但在日常生活中你不会:我个人在1美元和1只狗身上有很大的不同。狗和$是类型,你知道你可以用它们做什么。

你可以在装配中编程并制作你想要的东西,但是你会观察到它有多危险,因为你几乎可以做你想做的事情,特别是奇怪的事情。是的,修改一个地址值是危险的,假设你有一辆自动驾驶汽车应该以距离表示的地址提供东西:1200内存街道(地址),并假设街道房屋相隔100英尺(1221是无效地址),如果您能够按照整数操作地址,那么您将能够尝试在1223处交付并将数据包放在人行道中间。

另一个例子可能是房子,房子的地址,该地址的地址簿中的条目号码。所有这三个都是不同的概念,不同的类型......

答案 8 :(得分:9)

有不同的类型。并且有充分的理由:

有......

int a = 5;
int *b = &a;
int **c = &b;

......表达......

*b * 5

...有效,而表达式......

*c * 5

毫无意义。

重要的不是,如何指针或指针指针存储,而指向它们引用的内容。

答案 9 :(得分:9)

C语言是强类型的。这意味着,对于每个地址,都有一个类型,它告诉编译器如何解释该地址的值。

在你的例子中:

int a = 5;
int *b = &a;

a的类型为intb的类型为int *(读作&#34;指向int&#34的指针)。使用您的示例,内存将包含:

..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*

类型实际上并未存储在内存中,只是编译器知道,当您阅读a时,您会找到int },当您阅读b时,您会找到可以找到int的地方的地址。

在你的第二个例子中:

int a = 5;
int *b = &a;
int **c = &b;

c的类型为int **,读为&#34;指向int&#34;的指针。这意味着,对于编译器:

  • c是一个指针;
  • 当您阅读c时,您会获得另一个指针的地址;
  • 当您阅读其他指针时,您会获得int的地址。

即,

  • c是一个指针(int **);
  • *c也是指针(int *);
  • **cint

内存将包含:

..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*
c ... 0x00000020 .......... 0x00000010 ... int**

由于&#34;类型&#34;不与值一起存储,并且指针可以指向任何内存地址,编译器知道地址值的类型的方式基本上是通过获取指针的类型,并删除最右边的{{1 }}

顺便说一下,这是一个常见的32位架构。对于大多数64位体系结构,您将拥有:

*

地址现在每个8个字节,而..... memory address .............. value ................ type a ... 0x0000000000000002 .......... 5 .................... int b ... 0x0000000000000010 .......... 0x0000000000000002 ... int* c ... 0x0000000000000020 .......... 0x0000000000000010 ... int** 仍然只有4个字节。由于编译器知道每个变量的类型,它可以轻松处理这种差异,并为指针读取8个字节,为int读取4个字节。

答案 10 :(得分:6)

  

为什么这种类型的代码会产生警告?

int a = 5;
int *b = &a;   
int *c = &b;

&运算符生成指向对象的指针,即&a类型为int *,因此将其(通过初始化)分配给b,它也是类型int *有效。 &b生成指向对象b的指针,&b是指向int *的类型指针,即int **

C在赋值运算符(保持初始化)的约束中表示(C11,6.5.16.1p1):&#34;两个操作数都是指向兼容类型的合格或非限定版本的指针&#34 ; 。但在C语言中,什么是兼容类型int **int *不是兼容的类型。

因此int *c = &b;初始化中存在约束违规,这意味着编译器需要进行诊断。

这里规则的一个基本原理是标准不保证两个不同的指针类型具有相同的大小(void *和字符指针类型除外),即{{1} }和sizeof (int *)可以是不同的值。

答案 11 :(得分:4)

那是因为任何指针T*实际上都是pointer to a T(或address of a T)类型,其中T是指向类型。在这种情况下,*可以被理解为pointer to a(n),而T是指向的类型。

int     x; // Holds an integer.
           // Is type "int".
           // Not a pointer; T is nonexistent.
int   *px; // Holds the address of an integer.
           // Is type "pointer to an int".
           // T is: int
int **pxx; // Holds the address of a pointer to an integer.
           // Is type "pointer to a pointer to an int".
           // T is: int*

这用于解除引用目的,其中取消引用运算符将​​采用T*,并返回类型为T的值。返回类型可以看作是截断最左边的“指向(n)”的指针,并且是剩下的任何内容。

  *x; // Invalid: x isn't a pointer.
      // Even if a compiler allows it, this is a bad idea.
 *px; // Valid: px is "pointer to int".
      // Return type is: int
      // Truncates leftmost "pointer to" part, and returns an "int".
*pxx; // Valid: pxx is "pointer to pointer to int".
      // Return type is: int*
      // Truncates leftmost "pointer to" part, and returns a "pointer to int".

请注意,对于上述每个操作,取消引用运算符的返回类型与原始T*声明的T类型匹配。

这极大地帮助了原始编译器和程序员解析指针的类型:对于编译器,address-of运算符向类型添加*,dereference运算符从类型中删除* ,任何不匹配都是错误的。对于程序员来说,*的数量直接表明您正在处理的间接级别(int*始终指向intfloat**始终指向到float*float反过来总是指向*等。)

现在,考虑到这一点,无论间接级别的数量如何,仅使用单个void f(int* pi); int main() { int x; int *px = &x; int *ppx = &px; int *pppx = &ppx; f(pppx); } // Ten million lines later... void f(int* pi) { int i = *pi; // Well, we're boned. // To see what's wrong, see main(). } 有两个主要问题:

  1. 指针对于编译器解除引用要困难得多,因为它必须返回最近的赋值以确定间接级别,并适当地确定返回类型。
  2. 指针对于程序员来说更难理解,因为很容易忘记有多少个间接层。
  3. 在这两种情况下,确定值的实际类型的唯一方法是回溯它,迫使你去别的地方寻找它。

    *

    这是一个非常危险的问题,通过让{{1}}的数量直接代表间接的水平就可以轻松解决这个问题。