内存地址是指计算机内存中存储变量或对象的地址。内存空间大小就是寻址能力,即能访问到多少个地址,比如 32 位机器内存空间大小就是 2^32 = 4294967296
,也就是 4 GB 。每个变量或对象在内存中都有一个唯一的地址,通过该地址可以访问和操作该变量或对象。注意一 个内存地址对应一个字节,以 int 类型的变量为例,其占据 4 个内存地址,其中首个内存地址就是这个变量的地址。
#include
int main()
{
int vals[4]{};
printf("val1 address = %p\n", &vals[0]);
printf("val2 address = %p\n", &vals[1]);
printf("val3 address = %p\n", &vals[2]);
printf("val4 address = %p\n", &vals[3]);
return 0;
}
上面代码的输出为:
val1 address = 0000005420F6F978
val2 address = 0000005420F6F97C
val3 address = 0000005420F6F980
val4 address = 0000005420F6F984
为了能够说明 1 个 int 类型的变量占据 4 个内存地址,我们在上面的代码中使用占据连续内存的数组来做测试,由这个输出可以看出:数组 vals 的第一个元素所占据的内存地址由 0000005420F6F978
到 0000005420F6F97B
(再往下的一个地址就是第二个元素的首地址 0000005420F6F97C
),刚好是 4 个内存地址,其首个内存地址 0000005420F6F978
就是这个数组 vals 的第一个元素的地址(同时也是这个数组变量 vals 的地址)。
常量指针(const pointer)和指针常量(pointer to const)是两个不同的概念,常量指针指的是其指向变量的值不可改变,但是指针本身是可以改变的,可以指向其他变量;指针常量指的是指针本身是常量,其不可以再指向其他变量。
常量指针的样例代码:
const int val1 = 1;
int *ptr1 = &val1; //错误:必须使用常量指针
const int *ptr1 = &val1; //OK
*ptr1 = 2;
指针常量的样例代码:
int val1 = 1;
int val2 = 2;
int const *ptr1 = &val1; //OK
*ptr1 = &val2; //错误:指针本身是常量,其不可以再指向其他变量。
野指针出现的原因主要有以下三种:
(1)指针变量未初始化。局部指针变量的默认值是一个随机值,如果此时访问该指针则会引起程序崩溃。所以,指针变量在创建的同时应当被初始化,要么将指针设置为 nullptr ,要么让它指向合法的内存( new 出来的对象或者现有的一个对象)。
(2)释放内存后没有将指针设置为 nullptr 。不管是 free 还是 delete 在释放内存时,只是把指针所指的内存给释放掉了,但此时指针的值依然是之前内存空间的首地址。此时访问该指针则会引起程序崩溃。
(3)指针操作超越变量作用范围。栈内存在函数结束时会被释放,如果将其内存地址通过指针返回给调用者,此时再访问则会引起程序崩溃。
nullptr 关键字是在 C++11 标准中引入的,用于表示空指针。在 C++11 及以后的版本中,nullptr 替代了 C++98/03 中的 NULL 或 0 作为空指针的表示。该关键字可以避免函数重载问题,如下为样例代码:
void overLoadFunc(int* val);
void overLoadFunc(int val);
int main()
{
overLoadFunc( NULL ); // 期待调用 overLoadFunc(int* val); 但实际调用却是 overLoadFunc(int val);
}
上面代码中的 overLoadFunc( NULL );
实际调用的是 overLoadFunc(int val);
。其原因是 NULL 本身就是整数 0 ,因此进入了整型参数的重载函数。
指针的指针是一个指向指针的指针。指针可以指向所有数据类型的变量(基本类型、结构体类型、类类型等),而指针自身也是一种变量,所以指针自然也可以指向指针。指针的指针通常用于处理二维数组、动态分配的二维数组或处理指针数组等。
如下样例可以帮助理解指针的指针:
#include
int main()
{
int val1 = 1;
int *ptr1 = &val1;
int **ptr2 = &ptr1;
printf("ptr1 address = %p\n", &ptr1);
printf("ptr1 address = %p\n", &(*ptr2));
printf("ptr2 value = %p\n", ptr2);
return 0;
}
上面代码的输出为:
ptr1 address = 000000C4A839F758
ptr1 address = 000000C4A839F758
ptr2 value = 000000C4A839F758
由结果可以看出,指向指针的指针变量 ptr2
保存了指针变量 ptr1
的地址( 000000C4A839F758
)。 其中代码第 10 行 int **ptr2 = &ptr1;
定义了一个指向指针的指针,这里用了两个星号*
,其保存的值就是指针变量 ptr1
的地址。
第 11、 12、 13 行代码尤为重要:
第 11 行代码 printf("ptr1 address = %p\n", &ptr1);
,其中的 &ptr1
是对指针变量 ptr1
做取地址操作。
第 12 行代码 printf("ptr1 address = %p\n", &(*ptr2));
,其中的 (*ptr2)
是对指针变量 ptr2
做解引用操作,再对其做取地址操作,相当于直接对指针变量 ptr1
做取地址操作。
第 13 行代码 printf("ptr2 value = %p\n", ptr2);
,对指向指针的指针取值,直接用其变量名即可。
使用指针的指针来处理二维数组时,其对应内存的创建以及释放都需要使用循环:
(1)创建二维数组
int** vals = new int*[2];
for (size_t i = 0; i < 2; i++)
{
vals[i] = new int[3]();
}
(2)释放二维数组的内存
int** vals = new int*[2];
for (size_t i = 0; i < 2; i++)
{
vals[i] = new int[3]();
}
for (size_t i = 0; i < 2; i++)
{
delete[] vals[i]; //注意 delete 一定要加上中括号 []
}
这里尤其注意 delete[]
的使用。