指针与普通指针的对比


74

指针的目的是保存特定变量的地址。然后,以下代码的内存结构应类似于:

......内存地址......值 a ... 0x000002
................... 5
b ... 0x000010 ..... .............. 0x000002

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

然后,内存结构如下所示:

......内存地址......值 a ... 0x000002
................... 5
b ... 0x000010 ..... .............. 0x000002
c ... 0x000020 ................... 0x000010

因此,** c表示* b的地址。

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

产生警告?

如果指针的目的只是保存内存地址,我想如果我们要保存的地址是指变量,指针,双指针等,则应该没有层次结构,因此以下类型的代码应该有效。


13
除了这么多好的答案,我可以发表一个简单的评论。当尝试编译代码的问题部分时,Clang编译器会发出此明确的警告warning: incompatible pointer types initializing 'int *' with an expression of type 'int **'; remove & [-Wincompatible-pointer-types]。这可能使一切都变得清楚了。
user3078414

14
初学者经常会感到困惑,因为他们将“地址”本身视为一种数据类型。事实并非如此。地址类型为X的地址。对于不同类型,它们是不同的。这使您相信int *和int **是相同的。
Michel Billaud,2016年

4
如果指针的目的只是为了保存内存地址”,则不是。指针的目的是保存对象的“内存地址”及其类型。只需开始取消对指针的引用,您就会看到。
玛格丽特·布鲁姆

8
抱歉,不要失礼,只是想知道,是什么使这个问题如此有用?在任何适中的C书中,指针第二章或第三章都有这方面的内容,最多也是如此,在SO中进行了多次讨论。我错过了明显的事情吗?
Sourav Ghosh

3
@SouravGhosh这名男子可能是通过将其与一些人口稠密的初学者论坛
MM MM

Answers:


90

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

举一个较长的例子,您想工作,如果我们尝试取消引用,f则编译器会给我们一个int,而不是我们可以进一步取消引用的指针。

还要注意,在许多系统上intint*它们的大小并不相同(例如,指针的长度可能为64位,而长度可能为int32位)。如果取消引用f并获得int,则会丢失一半的值,甚至无法将其强制转换为有效的指针。


4
在为C设计的系统(以及一些现代嵌入式系统)上,同一程序中存在不同类型的指针-例如,近和远指针,或数据和代码指针。指针不是int值,人;停止假装它仅仅是因为它“大部分有效”
六安2013年

2
@Luaan:C是为早期DEC小型计算机设计的,最常见于1970年代的PDP-11。这些机器没有“近”和“远”指针。“ Near”和“ far”是Microsoft对C的扩展,以支持IBM PC上脑筋急转弯的Intel 8086/8088分段架构。(在PC项目启动时,IBM正在另一个站点生产和销售基于Motorola 68000的实验室计算机。想象一下,如果两个站点都在交谈,则避免了段头痛就可以单独使用阿司匹林节省的费用...)
约翰R.Strohm

1
@卢安实际上不是真的。直到其他供应商和ANSI / ISO着手解决此类可移植性问题。1972年C对互换指针和整数非常乐观。两者都是16位的,并且指针是平坦的。void *甚至不存在,您总是可以使用char *:)
hobbs

53

如果指针的目的仅仅是保存内存地址,我想如果我们要保存的地址是变量,指针,双指针等,则应该没有层次结构。

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

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

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


如果我理解正确,那么指针的类型会告诉cpu从引用内存地址开始读取了多少内存。因此,表达式'int * b =&a; printf(“%d',* b);'表示从a的地址开始,我们读取4个字节。这是可能的,因为我们将int的大小定义为4个字节,并打印了该整数。但是,大小是多少int *,int **,int ***的区别?系统之间有什么不同吗?那么什么(编译器,cpu或其他?)定义了它的大小?
user42298

1
类型的大小只是一个考虑因素。还有其他
格雷戈里·柯里

考虑一个指向p的指针。现在假设您要存储p指向的值。int i = * p; 编译器需要知道指针的类型。在内存中,双精度型与int有很大不同。这与指针的大小无关,但与指针应如何处理所指向的数据无关。
Gregory Currie

1
@ user42298指针的大小通常取决于CPU和编译器。在64位CPU上运行的针对64位编译的程序具有64位宽的指针。但是,您也可以编译32位并在64位CPU上运行它,然后指针是32位,并且操作系统会负责正确运行它。顺便说一句int也可以具有不同的大小,但是int32_t可以保证精确地具有32位。
alain

除其他外,这是最全面的答案。
edmz

23

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

产生警告?

您需要回到基础知识。

  • 变量有类型
  • 变量保存值
  • 指针是一个值
  • 指针指向变量
  • 如果p是指针值,*p则为变量
  • 如果v是变量,则&v是指针

现在,我们可以在您的帖子中查找所有错误。

然后假设现在我要保存指针的地址 *b

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

**c指的地址*b

不不不。绝对不。你必须要正确理解这一点,如果你要了解的指针。

*b是一个变量;它是变量的别名a。变量的地址是变量a的值b**c没有引用的地址a。相反,它是一个变量,其为别名为变量a。(也是*b。)

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

我们怎么知道呢?回到基本面。你这么说c = &b。那么,什么是价值c?指针。要什么 b

确保您完全了解基本规则。

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


我认为每次OP在帖子中说出b或** c时,他们实际上都是在试图说b和c。您介绍了很多内容,但是并没有真正涉及到问题的最后一部分(“为什么int不能指向另一个int *”),这是OP真正要问的IMO。
mbrig

究竟是谁的天才想法使反引用运算符和“是指针”限定词成为相同的符号……
mbrig

6
@mbrig:这是丹尼斯·里奇(Dennis Ritchie)的天才想法。如果不清楚为什么这是个天才的想法,则说明您在精神上没有正确地解析语言。当我们说int * b;我们不只是说“b是类型的变量int*我们也说*b是类型的变量int这里的天才的想法是,你可以心理上认为它是两者兼而有之。int* b 并且int *b,和任何的解释是正确的。
埃里克利珀

是的,我看了一些相关的问题,这对我来说实际上是有史以来第一次。我还是有点质疑。
mbrig

20

如果您想获得正确的警告并且希望代码完全进行编译,则C的类型系统需要这样做。仅使用一个深度的指针,您就不会知道指针是指向指针还是实际整数。

如果取消引用类型int**,则知道获得的类型,int*而如果取消引用int*类型,则类似int。根据您的建议,类型将是不明确的。

从您的示例来看,不可能知道是否c指向aintint*

c指向什么类型?编译器不知道,因此下一行是无法执行的:

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


17

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

首先,有没有保证,int *int **具有相同的尺寸或代表(他们做现代桌面架构,但你不能依赖它被普遍正确的)。

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

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

所以,给定

b + 1让我们接下来的地址int,并c + 1给我们的下一个地址指针,以int

如果要让函数写入指针类型的参数,则需要指针对指针。采取以下代码:

这是真正的任何类型T。如果我们将其替换T为指针类型P *,则代码变为

语义完全相同,只是类型不同而已。形式参数p总是比变量多一个间接级别val


11

我认为如果要保存的地址引用变量,指针,双指针,则应该没有层次结构

如果没有“层次结构”,那么很容易在没有任何警告的情况下全面生成UB-这将是可怕的。

考虑一下:

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

但是没有“层次结构”,例如:

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

但是当行:

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

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

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

结论

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


10

如果指针的目的仅仅是保存内存地址,我想如果我们要保存的地址是变量,指针,双指针等,则应该没有层次结构,因此下面的代码类型应该是有效的。

我认为这是您的误解:指针本身的目的是存储内存地址,但是指针通常也具有类型,以便我们知道指针所指向的位置。

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

C的指针系统的重点就是附加类型信息。

如果你这样做

&a意味着您得到的是一个,int *因此,如果取消引用,它又是一个int

将其带入新的高度,

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

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


9

如果指针的目的仅仅是保存内存地址,我想如果我们要保存的地址是变量,指针,双指针等,则应该没有层次结构,因此下面的代码类型应该是有效的。

嗯,这对于机器是正确的(毕竟,几乎所有东西都是数字)。但是在许多语言中,变量是类型化的,这意味着编译器可以确保正确使用它们(类型将正确的上下文强加给变量)

的确,指向指针的指针和指针(可能)使用相同数量的内存来存储其值(请注意,对于int和指向int的指针而言,这不是正确的,地址的大小与a的大小无关。屋)。

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

您可以进行汇编编程并完成所需的操作,但是您会发现它有多危险,因为您几乎可以执行所需的操作,尤其是奇怪的事情。是的,修改地址值是危险的,假设您有一辆自动驾驶汽车,应在以距离表示的地址处传送东西:1200条记忆街(地址),并假定街道房屋之间相距100英尺(1221是无效地址),如果您能够按自己喜欢的方式操作地址(作为整数),则可以尝试在1223传递,并将数据包放在人行道的中间。

另一个示例可能是房屋,房屋地址,该地址的通讯录中的条目号。这三个都是不同的概念,不同的类型...


这台机器也不一定是正确的。在较旧的系统(和某些现代嵌入式系统)中,您拥有不同类型的指针-例如,x86体系结构具有近(16位)和远(32位)指针。C向您隐藏(抽象)了这个事实,但是对于应用程序以16位模式在x86计算机上运行,​​这至关重要。还有其他示例,例如在分段模式下(分段+偏移,其中实际机器代码中的空指针不为零)。C没有很多高级抽象,但是它有很多低级抽象-这是C的全部目的
。– Luaan

@卢安(Luaan):在许多机器中,指向函数的指针和指向数据的指针是不同的。存在指针目标类型会影响其大小的机器,但这种机器要少得多。如果C定义了一个可选的“指向任何类型的指向数据的指针”类型,这将在所有类型的数据使用相同表示形式的实现中进行定义,因为这是目前编写可与之一起使用的函数的唯一方法,这将很有用。所有此类指针(例如,用于排序的指针)都是使用memcpy,memmove或字符类型,并使用其中任何一种技术来操纵它们的
超级猫

...将需要编译器将双间接指针视为可能混淆每种类型的所有内容(包括字符,整数和浮点值),而不仅仅是指针,但是C标准的作者传统上不愿定义所有实现都无法处理的任何内容。
超级猫

我知道,所有这些都是完全正确的,有时有必要简化事情。我的意思是,在原始机器中,事情要比在语言中没有那么严格,并且在使用语言时,我们不能像在原始机器中那样认为。
Jean-BaptisteYunès16年

9

有不同的类型。这是有充分的理由的:

有...

… 表达方式 …

…有效,而表达式…

没有意义。

大不了就是没有,怎么指针或指针到指针存储,但什么他们参考。


9

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

在您的示例中:

的类型aint,类型bint *(称为“指向”的指针int)。使用您的示例,内存将包含:

类型实际上并没有存储在内存中,只是编译器知道,当您阅读时,a您会发现一个int,而当您阅读时,b您将找到一个可以找到的地址int

在第二个示例中:

的类型 cint **,称为“指向的指针int”。这意味着对于编译器:

  • c 是一个指针;
  • 当您阅读时c,您将获得另一个指针的地址;
  • 当您读取另一个指针时,您将获得一个 int

那是,

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

内存将包含:

由于“类型”不与值一起存储,并且指针可以指向任何内存地址,因此编译器知道地址中值类型的方式基本上是通过获取指针的类型并删除最右边的 *


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

现在,每个地址都是8个字节,而地址int仍然只有4个字节。由于编译器知道每个变量的类型,因此可以轻松处理此差异,并为指针读取8个字节,为读取4个字节int


6

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

&操作者产生一个指向对象,即&a是类型的int *左右(通过初始化)在分配给b这也是类型int *是有效的。&b产生一个指向object的指针b,该&b指针的类型为int *,即int **

C在赋值运算符(用于初始化的条件)的约束中说(C11,6.5.16.1p1):“两个操作数都是指向兼容类型的合格或不合格版本的指针”。但是在C中定义什么是兼容类型int **int *哪些不是兼容类型。

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

该规则的基本原理之一是标准未保证两种不同的指针类型具有相同的大小(void *和字符指针类型除外),即sizeof (int *)并且sizeof (int **)可以是不同的值。


4

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

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

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

这极大地帮助了原始编译器和程序员解析指针的类型:对于编译器,address-of运算符*向该类型添加a ,解引用运算符*从该类型中删除a ,并且任何不匹配都是错误。对于程序员而言,*s的数量直接表明您正在处理多少个间接寻址级别(int*始终指向intfloat**始终指向float*,依次指向始终指向float等等)。


现在,考虑到这一点,*无论间接级别的数量如何,仅使用单个存在两个主要问题:

  1. 对于编译器而言,指针要解除引用的难度要大得多,因为指针必须返回最新的赋值以确定间接级别,并适当地确定返回类型。
  2. 对于程序员而言,指针更难理解,因为很容易失去对间接访问层数的跟踪。

在这两种情况下,确定该值的实际类型的唯一方法是回溯该值,迫使您在其他地方查找它。

这是一个非常危险的问题,可以通过使*s的数目直接表示间接级别来轻松解决。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.