指向指针和普通指针的指针

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

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;
7236 次浏览

进去

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

由于 &b的类型为 int **,因此会得到一个警告,并尝试初始化类型为 int *的变量。这两种类型之间没有隐式转换,导致出现警告。

举一个长一点的例子,如果我们尝试解引用 f,编译器会给我们一个 int,而不是一个我们可以进一步解引用的指针。

还要注意的是,在许多系统中,intint*的大小是不一样的(例如,一个指针可能是64位长,而一个 int是32位长)。如果取消对 f的引用并得到一个 int,则会丢失一半的值,然后甚至无法将其强制转换为一个有效的指针。

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

如果取消引用类型 int**,就知道得到的类型是 int*,类似地,如果取消引用 int*,得到的类型是 int。根据你的建议,这种类型将是模棱两可的。

从你的例子来看,不可能知道 c指向的是 int还是 int*:

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

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

*c;

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

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

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

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

将类型信息附加到 C 指针系统上是 C 指针系统的关键。

如果你愿意的话

int a = 5;

&a意味着你得到的是一个 int *,所以如果你解引用它又是一个 int

把它带到下一个层次,

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

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

如果您认为在您的环境中不应该存在类型的层次结构,那么您总是可以使用 void *,尽管直接的可用性非常有限。

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

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

有一种类型,void*,可以完成你想要的任务: 它只存储一个地址,你可以为它分配任何地址:

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

但是,当你想解除 void*的引用时,你需要自己提供“丢失”的类型信息:

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

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

这对机器来说是正确的(毕竟大体上所有的东西都是一个数字)。但是在许多语言中,变量是类型化的,这意味着编译器可以确保您正确地使用它们(类型对变量施加正确的上下文)

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

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

你可以在汇编中编程,做出你想要的东西,但是你会发现它有多危险,因为你几乎可以做你想做的事情,特别是奇怪的事情。是的,修改地址值是危险的,假设你有一辆自动驾驶汽车,它应该按照一个远距离表达的地址送货: 1200内存街(地址) ,假设街道房子之间相隔100英尺(1221是一个无效地址) ,如果你能够操纵地址作为整数,你将能够尝试在1223送货,并让包在人行道中间。

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

我认为,如果我们要保存的地址引用了变量、指针、双指针,那么就不应该存在层次结构

如果没有“层次结构”,就很容易在没有任何警告的情况下生成所有的 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,即可能读取4或8个字节,即使我们只保留1个字节。那是 UB。

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

结论

类型系统通过将 int * 、 int * * 、 int * * * 等视为不同的类型来帮助我们检测 bug。

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

首先,不能保证 int *int **具有相同的大小或表示形式(在现代桌面体系结构中它们具有相同的大小或表示形式,但是你不能指望它们是普遍正确的)。

其次,类型对指针算法很重要。给定一个 T *类型的指针 p,表达式 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给我们 0x1004 lp + 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
}

如果我们用指针类型 P *替换 T,代码就会变成

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多一个间接级别。

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

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

&操作符生成一个指向对象的指针,即 &aint *类型的,因此将它赋值(通过初始化)给 b也是 int *类型的 b是有效的。&b产生一个指向对象 b的指针,即 &b是指向 int *的类型指针,即。E,int **.

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

因此,在 int *c = &b;初始化中存在一个约束冲突,这意味着编译器需要一个诊断。

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

有不同的类型,而且有一个很好的理由:

有..。

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

... 表情..。

*b * 5

... 是有效的,而表达..。

*c * 5

没道理啊。

重要的不是存储 怎么做指针或指针到指针,而是它们引用的 什么

我的问题是,为什么这种代码,

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也是如此。)

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

我们怎么知道的?回到最基本的。你说 c = &b。那么 c的值是多少呢?一个指针。为什么?b.

确保你了解 完全的基本规则。

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

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

在你的例子中:

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

a的类型是 int,而 b的类型是 int *(读作“指向 int的指针”)。使用您的示例,内存将包含:

..... 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 **,读作“指向指向 int的指针”。这意味着,对于编译器:

  • c是一个指针;
  • 读取 c时,得到另一个指针的地址;
  • 当你读取另一个指针时,你会得到一个 int的地址。

就是,

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

记忆将包含:

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

由于“ type”不与值存储在一起,而且指针可以指向任何内存地址,编译器知道地址上值的类型的方法基本上就是获取指针的类型,然后删除最右边的 *


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

..... memory address .............. value ................ type
a ... 0x0000000000000002 .......... 5 .................... int
b ... 0x0000000000000010 .......... 0x0000000000000002 ... int*
c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**

地址现在是每个8字节,而 int仍然只有4字节。由于编译器知道每个变量的 类型,它可以很容易地处理这种差异,并且读取8字节的指针和4字节的 int

这是因为任何指针 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的值。返回类型可以看作是截断最左边的“指向 a (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 操作符在类型中添加一个 *,解引用运算符从类型中删除一个 *,任何不匹配都是一个错误。对于一个程序员来说,*的数量是一个直接指示你正在处理多少层次的间接问题(int*总是指向 intfloat**总是指向 float*,而 float*又总是指向 float,等等)。


现在,考虑到这一点,无论间接级别的多少,只使用单个 *都存在两个主要问题:

  1. 对于编译器来说,取消引用指针要困难得多,因为它必须返回引用最近的赋值来确定间接级别,并适当地确定返回类型。
  2. 指针对于程序员来说更难理解,因为很容易忘记有多少间接层。

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

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().
}

这是一个非常危险的问题,通过使用 *的数量直接表示间接的级别,这个问题很容易解决。