所以我正在做CS50第4讲"记忆"。
David说,当我们声明int*x时,我们必须分配一个地址;作为指针,以便在其中存储值(例如45、23等)。他还说,如果你不初始化指针,然后试图通过取消引用来将值放入其中,如下面的y所示,你是在要求计算机将值存储在伪造地址还是随机地址中?
int main(void)
{
int *x;
int *y;
x = malloc(sizeof(int));
*x = 42;
*y = 13;
}
但什么是虚假地址?当我声明int*x;里面已经有地址了吗?这怎么可能?我知道存储指针x值的内存位置可能有一些prev操作的残余,但我不明白那里怎么会有地址。
首先,请记住,x
和y
是独立于它们指向的任何对象而存在的变量。x
和y
的初始值是不确定的-它们可能是0x00000000
,也可能是0xdeadbeef
,它们可能是一个根本不对应于有效地址值的位模式。
x
和y
变量的空间必须取自某个的地方,并且由于内存不是无限的,内存位置会被重用;一些内存位置被重复使用很多。在大多数实现中,当您完成1时,内存不会自动被擦除,因此当您创建一个新对象时,它将包含上次写入这些字节的位模式2。
C有一个对象的生存期的概念,这是程序执行期间,保证为该对象保留存储空间。如果指针在对象的生存期内存储对象的地址,则该指针有效。有效的指针值可以通过以下两种方式之一获得:
- 在对象的生存期内对该对象使用
&
运算符 - 调用
malloc
、calloc
或realloc
,为对象动态分配空间,就像对x
3所做的那样
例如:
void foo( void )
{
int *ptr; // ptr is initially indeterminate and invalid
for ( int i = 0; i < 10; i++ )
{
ptr = &i; // i's lifetime is each iteration of the for loop;
printf( "%d = %dn", *ptr, i ); // ptr is valid within the loop;
}
// ptr still stores the address of i, but i's lifetime has ended,
// so ptr is *no longer valid* - attempting to read or write it now
// will lead to undefined behavior
}
在i
的生命周期结束后,为其保留的空间可以被其他人使用。如果我们在循环完成后尝试通过ptr
对其进行读取或写入,结果可能不是我们所期望的。这样做的行为是未定义的,这意味着编译器和运行时环境不需要以任何特定的方式处理这种情况。它可能如我们所期望的那样工作,我们可能在某个地方损坏数据,我们可能导致运行时错误,或者其他任何事情都可能发生。
类似地,执行
*y = 13;
程序中的对象将具有未定义的行为,因为y
在对象的生存期内不会在程序中存储该对象的地址。实际上,任何事情都可能在这一点上发生——你的程序可能看起来像预期的那样工作,你可能损坏程序中其他地方的数据,你可能导致程序分支到随机函数,你可能造成运行时错误,或者任何其他事情都可能发生。每次运行的结果可能会有所不同
编辑
回答评论中的一个问题:
你指的是这里的指针吗?指针可以被视为对象吗?还是仅仅是int和chars被称为对象?
是的,指针变量x
和y
是对象(在C意义上,它们是可以存储值的内存区域)。为了更好地说明这一点,我写了以下内容:
#include <stdio.h>
#include <stdlib.h>
#include "dumper.h"
int main( void )
{
int *x;
int *y;
int a;
char *names[] = { "a", "x", "y", "*x", "*y" };
void *addrs[] = { &a, &x, &y, NULL, NULL };
size_t sizes[] = { sizeof a, sizeof x, sizeof y, sizeof *x, sizeof *y };
puts( "Initial states of a, x, and y:" );
dumper( names, addrs, sizes, 3, stdout );
x = calloc( 1, sizeof *x ); // makes sure *x is initialized to 0
if ( x )
{
addrs[3] = x;
puts( "States of a, x, and y after allocating memory for x" );
dumper( names, addrs, sizes, 4, stdout );
*x = 0x11223344;
puts( "States of a, x, y, and *x after assigning *x" );
dumper( names, addrs, sizes, 4, stdout );
}
y = &a;
addrs[4] = y;
puts( "States of a, x, y, *x, and *y after assigning &a to y" );
dumper( names, addrs, sizes, 5, stdout );
*y = 0x55667788;
puts( "States of a, x, y, *x, and *y after assigning to *y" );
dumper( names, addrs, sizes, 5, stdout );
free( x );
return 0;
}
dumper
是我编写的一个小实用程序,用于将对象的地址和内容转储到指定的输出流。
在构建并运行代码之后,我得到了变量初始状态的输出:
Initial states of a, x, and y:
Item Address 00 01 02 03
---- ------- -- -- -- --
a 0x7ffee3bc59f4 2c b3 0c 1b ,...
x 0x7ffee3bc5a00 01 00 00 00 ....
0x7ffee3bc5a04 00 00 00 00 ....
y 0x7ffee3bc59f8 80 5b bc e3 .[..
0x7ffee3bc59fc fe 7f 00 00 ....
变量a
位于地址0x7ffee3bc59f4
,占用了4个字节——此运行的初始内容为0x1b0cb32c
(x86是小端序,因此字节从最低有效位到最高有效位排序)。由于a
没有明确初始化,它的初始内容是不确定的——每次我运行这个程序时,a
的初始值可能会不同(它的地址也是如此——为了防御恶意软件,大多数操作系统会在不同的运行中随机化位置)。
变量x
从地址0x7ffee3bc5a04
开始,占用8个字节(x86上的堆栈"向下"增长,所以我们从更高的地址开始)。类似地,变量y
存在于地址0x7ffee3bc59fc
,并且也占用8个字节。与a
一样,x
和y
的初始内容是不确定的,并且会随着运行而变化。
在为x
将指向的int
对象分配空间后,我得到了以下内容:
States of a, x, and y after allocating memory for x
Item Address 00 01 02 03
---- ------- -- -- -- --
a 0x7ffee3bc59f4 2c b3 0c 1b ,...
x 0x7ffee3bc5a00 a0 25 50 1e .%P.
0x7ffee3bc5a04 c2 7f 00 00 ....
y 0x7ffee3bc59f8 80 5b bc e3 .[..
0x7ffee3bc59fc fe 7f 00 00 ....
*x 0x7fc21e5025a0 00 00 00 00 ....
变量x
现在存储值0x7fc21e5025a0
,该值是足够大以存储int
值的存储器块的地址。由于我使用calloc
来分配内存,所以它的初始内容都是0位。现在,我可以通过表达式*x
为该对象分配一个新的int
值,这给了我:
States of a, x, y, and *x after assigning *x
Item Address 00 01 02 03
---- ------- -- -- -- --
a 0x7ffee3bc59f4 2c b3 0c 1b ,...
x 0x7ffee3bc5a00 a0 25 50 1e .%P.
0x7ffee3bc5a04 c2 7f 00 00 ....
y 0x7ffee3bc59f8 80 5b bc e3 .[..
0x7ffee3bc59fc fe 7f 00 00 ....
*x 0x7fc21e5025a0 44 33 22 11 D3".
因此,我更新了x
指向的int
对象(即存储的地址)。
最后,我将y
设置为指向a
,得到:
States of a, x, y, *x, and *y after assigning &a to y
Item Address 00 01 02 03
---- ------- -- -- -- --
a 0x7ffee3bc59f4 2c b3 0c 1b ,...
x 0x7ffee3bc5a00 a0 25 50 1e .%P.
0x7ffee3bc5a04 c2 7f 00 00 ....
y 0x7ffee3bc59f8 f4 59 bc e3 .Y..
0x7ffee3bc59fc fe 7f 00 00 ....
*x 0x7fc21e5025a0 44 33 22 11 D3".
*y 0x7ffee3bc59f4 2c b3 0c 1b ,...
存储在变量y
中的值是变量a
:0x7ffee3bc59f4
的地址。如您所见,表达式*y
的值与变量a
的值相同。我现在可以通过写入*y
来更改a
的值,这就留下了:
States of a, x, y, *x, and *y after assigning to *y
Item Address 00 01 02 03
---- ------- -- -- -- --
a 0x7ffee3bc59f4 88 77 66 55 .wfU
x 0x7ffee3bc5a00 a0 25 50 1e .%P.
0x7ffee3bc5a04 c2 7f 00 00 ....
y 0x7ffee3bc59f8 f4 59 bc e3 .Y..
0x7ffee3bc59fc fe 7f 00 00 ....
*x 0x7fc21e5025a0 44 33 22 11 D3".
*y 0x7ffee3bc59f4 88 77 66 55 .wfU
指针变量并没有什么神奇之处——它们只是存储某种类型值(地址)的内存块。不同的指针类型可以具有不同的大小和/或表示(即,int *
变量看起来可能与char *
变量不同,后者看起来可能不同于struct foo *
变量)。唯一的规则是
char *
和void *
具有相同的尺寸和排列- 指向限定类型的指针与指向其非限定等价物的指针具有相同的大小和对齐方式(即,
const int *
和int *
应该具有相同的尺寸和对齐方式) - 所有
struct
指针类型都具有相同的大小和对齐方式(例如,struct foo *
和struct bar *
看起来相同) - 所有
union
指针类型都具有相同的大小和对齐方式
指针值的操作是特殊的,它们的语法可能会令人困惑。但是指针只是另一种数据类型,指针变量只是另一类对象。
- 也就是说,设置为全位-0或其他定义明确的"非值"位模式。
- 我们在这里不讨论虚拟内存和物理内存之间的区别。
- 您没有为
x
本身分配空间,而是为x
将指向的int
对象分配空间
我知道存储指针x值的内存位置可能有一些prev操作的残余,但我不明白那里怎么会有地址。
这就是为什么你会看到像";伪造地址";或";随机地址";。垃圾是一个伪造的地址。如果垃圾被理解为一个地址,那么它就是一个随机地址。
幸运的是,那里只能有一个有效的地址(好还是坏是另一个问题)。但如果有随机垃圾,并且你将其用作地址,那么它很可能是一个"垃圾";伪造地址";或一个";随机地址";。
这两个变量具有自动存储持续时间
int *x;
int *y;
当然有他们的地址。您可以通过以下方式输出他们的地址
printf( "&x = %pn", ( void * )&x );
printf( "&y = %pn", ( void * )&y );
然而,变量x和y本身没有初始化,并且具有不确定的值。因此,在语句中取消引用这些指针
*y = 13;
导致未定义的行为。
如果你想取消引用一个指针,它必须像在语句中那样指向一个对象
x = malloc(sizeof(int));
*x = 42;
在上面的第一个语句之后,指针x指向为int
类型的对象分配的存储器。因此取消引用指针
*x = 42;
您可以更改对象。
我知道存储指针x值的内存位置可能有一些prev操作的残余,但我不明白那里怎么会有地址。
假设前面的操作将该内存用作uint32_t
对象(一个无符号的32位整数),并且假设C实现中的int *
也是32位。例如,假设系统上的某个地址为0x103F0。当内存用于uint32_t
时,它可能已用于存储无符号整数值66544。66544的十六进制为0x103F0。因此,内存将包含0x103F0,这与假设的地址相同。
每个有效地址都是位1的特定设置。每个位的设置都是一些无符号整数。因此,在x
的未初始化存储器中可以很容易地存在表示地址的位。其他类型也可能发生这种情况。x
的存储器可能已被用作char
的阵列或float
,用于这些存储器的位也可能是用于表示0x103F0的相同位。
另一个问题是,当您定义int *x;
然后使用x
时,现代编译器不会只是机械地为x
保留一些内存,然后从内存加载该值的内容。他们试图优化你的程序(除非优化被关闭)。在这样做的时候,他们试图寻找实现源代码定义行为的"最佳"程序。但是,当您使用未初始化的变量时,该变量的值不是由C标准定义的。根据具体情况,标准可能根本没有定义使用它的行为。那么,你的程序没有定义的行为,实现源代码中该部分定义行为的"最佳"指令集根本没有指令——编译器可能只是删除程序的该部分,或者可能只是用程序的其他部分替换它,或者表现出其他让新程序员感到惊讶的行为。
脚注
1有时可以有多个表示相同地址的位设置,例如当一些位未使用或存储器是与段重叠的基址和偏移方案中的地址时。
这里int*x是一个通配符指针,这意味着它可能被初始化为一个非NULL垃圾值,该值可能不是有效地址。