一、定义
指针也就是内存地址,指针变量是用来存放内存地址的变量。
将内存以一个字节分为一个个内存单元,每个内存单元都进行编号,这个编号就是地址,也就是指针。
cpp
int b = 1;
int *pb = &b;
//这里的pb变量是一个整型指针变量,用来存放整型变量b的地址
我们可以通过&(取地址操作符)得到一个变量的地址,然后将地址存到一个指针变量中,可以用这个指针变量来访问那个变量。
二、指针的大小
对于32位机器,假设CPU与内存之间有32条地址线,每一根寻址线在工作时会产生高电平(代表1)和低电平(代表0),则这个机器可以产生
00000000 00000000 00000000 00000000
到
11111111 11111111 11111111 11111111
这么多的地址,一共是2 ^ 32 个地址,一个地址指向的内存单元是一字节,所以这么多地址可以指向大约4GB的内存。
对于64位机器,假设CPU与内存之间有64条地址线,每一根寻址线在工作时会产生高电平(代表1)和低电平(代表0),则这个机器可以产生
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
到
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
这么多的地址,一共是2 ^ 64 个地址,一个地址指向的内存单元是一字节,所以这么多地址可以指向更大的内存。
这里我们发现,地址的大小是取决于地址线的多少的(或者是系统位数),对于32位机器,地址是32位的,也就是32bit的大小,所以是4字节的大小,所以32位机器的地址大小是4字节;相应的64位机器的地址是8字节。
cpp
#include <stdio.h>
int main()
{
int b = 1;
int *pb = &b;
printf("%zu\n", sizeof(pb));
return 0;
}
64位平台输出结果:
32位平台输出结果:
三、指针的类型
1、指针变量的类型
int *
:指向整型数据的指针char *
:指向字符数据的指针float *
:指向浮点数据的指针double *
:指向双精度浮点数据的指针void *
:通用指针,可以指向任何类型的数据int **
:指向int *
指针的指针char ***
:指向char **
指针的指针的指针
等等。
2、指针类型的意义
对于不同类型的指针,只要平台位数是一定的,则指针的大小是一定的。
cpp
int* pa = NULL;
double* pb = NULL;
char* pc = NULL;
printf("%zu\n", sizeof(pa));
printf("%zu\n", sizeof(pb));
printf("%zu\n", sizeof(pc));
运行结果:
那既然说这样的话,不同类型指针的大小是相同的,为什么不只设定一种指针呢?
(1)指针解引用
这是因为不同类型指针指向的变量类型不同,而不同变量的大小有不同,我们知道一个变量的指针指向的是变量的第一个地址,例如int类型有4个字节,则这4个字节有4个指针,而指向这个int类型变量的指针是这个变量的首地址,如果只有一种指针的话,那我们通过指针就不能正确地访问不同的变量了。
不同类型的指针虽然在内存中占用相同大小的空间(在特定的平台上),但它们指向的数据类型不同,这就要求指针在解引用(dereferencing)时能正确地解释所指向的内存区域的大小和类型。当你解引用指针获取它所指向的值时,指针的类型决定了从指针指向地址开始的内存中读取多少数据,以及如何解释这些数据。例如,整型指针会读取4个字节(在大多数现代平台上),而字符指针只会读取一个字节。
cpp
int a = 0x11223344;
int* pa = &a;
*pa = 0;//通过解引用经由整型指针变量pa对整型变量a操作
对于变量a,它占4个字节,通过解引用经由整型指针变量pa对整型变量a操作后,直接改变了4个字节的内容。
这里我们强行将整型变量a的地址存在char型指针变量中:
cpp
int a = 0x11223344;
char* pc = (char*)&a;//强行将整型变量a的地址存在char型指针变量中
*pc = 0;//通过解引用经由整型指针变量pc对整型变量a操作
对于变量a,它占4个字节,这次通过解引用经由整型指针变量pc对整型变量a操作后,只改变了1个字节的内容。
这足以体现指针变量类型的重要性。
(2)指针算数
以及指针在进行指针算数时,指针类型还决定了执行指针算术时的行为,或者说决定了指针的步长。比如说,对于指向一个整数(通常占4个字节)的指针int *p
,执行p + 1
时,地址会增加sizeof(int)
个单位,确保p + 1
指向下一个整数的起始位置。而对于指向一个字符(通常占1个字节)的指针char *c
,执行c + 1
时,地址只会增加sizeof(char)
个单位。
cpp
int a = 1;
int* pa = &a;
char* pc = (char*)&a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("----------------\n");
printf("%p\n", pc);
printf("%p\n", pc + 1);
运行结果:
对于int*类型指针的步长是4字节,而char*类型指针的步长是1字节。
(3)类型安全
不同类型的指针帮助语言保持类型安全,确保不会将整型数据解释为浮点数,或者反过来。这有助于避免许多类型相关的错误。
cpp
int a = 1;
int* pi = &a;
*pi = 100;
这里正常将整型指针变量指向整型变量,然后通过解引用经由整型指针变量pi对整型变量a操作后,a中的值是正确的:
cpp
int a = 1;
float* pf = (float*)&a;
*pf = 100.0f;
这里我们将浮点数指针变量指向整型变量,然后通过解引用经由浮点数指针变量pf对整型变量a操作后,a中的值是错误的:
(4)其他意义
还有一些原因:
-
函数指针:函数指针是另一种特别的数据类型,它们的大小也是统一的,但是它们指向的是函数而不是普通的数据类型。它们允许程序动态地调用不同的函数,并且为了安全和正确地执行函数调用,需要对应的类型信息。
-
抽象和接口设计:在面向对象的编程中,特别是在使用多态的情况下,不同类型的指针可以指向一个继承体系中的不同对象。这样,同一个函数可以接受不同类型的对象作为参数,根据对象的实际类型来调用相应的方法。
-
数据对齐:某些类型的数据需要在内存中特定的对齐方式。类型化指针确保了正确的对齐,这对于硬件访问是很重要的,因为某些硬件架构要求特定类型的数据在内存中的特定对齐。
通过使用不同类型的指针,编程语言提供了一种丰富的方法来操作内存中的数据,同时也确保了访问这些数据的正确性和效率。
这些同样也体现了指针类型的重要意义。
四、野指针
野指针是指那些没有被初始化的指针,或者说它们的值是随机的,因此它们指向的是不确定的内存位置。访问野指针指向的内存同样会导致不可预料的行为或程序崩溃。
(1)未初始化的指针
当一个指针变量被声明但没有被显式初始化时,它将包含一个随机的内存地址,这种指针是野指针。
cpp
int* p;//未初始化的指针,没有明确指向,是野指针
(2)指针操作越界
指针在进行算术运算时超过了其所指向的缓冲区或数组边界,可能会导致指针指向一个非法区域。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//这里的整型指针指向的是数组首元素
printf("%d\n", p + 10);//这样操作是访问了数组的第十一个元素,而这个数组只有十个元素,所以是越界访问,p + 10是野指针
(3)其他情况
- 指针赋值错误: 由于编程错误,指针可能被赋值为一个意外的非法地址,例如指针类型转换错误等。
使用野指针进行操作是非常危险的,因为它们可能导致程序崩溃、数据损坏或安全漏洞。因此,最佳实践是始终确保指针在使用前已正确初始化,避免野指针的出现。
五、悬空指针
悬空指针(Dangling Pointer),也称为悬挂指针或迷失指针,是指向一块曾经分配过的内存的指针。但由于某些原因,这块内存已经不再有效,指针仍然指向那个地址,此时的指针被称为悬空指针。使用悬空指针访问数据是危险的,因为原来的内存可能已经被重新分配或释放,其内容可能已发生变化或不再属于程序的地址空间。以下是造成悬空指针的一些常见情况:
(1)指向栈内存的指针在函数返回后
如果一个指针指向了一个函数内的局部变量(栈内存),而该函数返回后该变量的生命周期结束,这时候这个指针也会变成悬空指针。
cpp
#include <stdio.h>
int* test()
{
int a = 0;
return &a;
}
int main()
{
int* p = test();//整型指针变量p接受的值是a的地址,所以整型指针变量p指向a,函数返回后,整型变量a的生命周期结束,然而p依旧指向a原来的地址,p变成野指针
return 0;
}
(2)已释放的内存指针:
当使用free
或delete
释放了某个指针指向的内存后,如果没有将该指针置为NULL
,它仍然包含释放内存的地址,这种指针也成为悬空指针。
cpp
int *ptr = malloc(sizeof(int));
*ptr = 1;
free(ptr); // ptr现在是悬空指针。
最佳的做法是在释放指针指向的内存后,立即将指针设置为NULL
,这有助于防止悬空指针的出现,因为NULL
指针的解引用是确定性的行为,通常会导致程序的安全终止。
六、如何避免野指针和悬空指针
1、在创建指针变量后初始化
cpp
int a = 0;
int* p = &a;//初始化p的值为a的地址
如果没有明确的值去初始化,则初始化为NULL:
cpp
int* p = NULL;//初始化p的值为NULL
因为NULL
指针的解引用是确定性的行为,通常会导致程序的安全终止。
2、其他方法
(1)小心数组越界
(2)指针指向的空间释放时,及时置为NULL
(3)避免返回局部变量的地址
(4)使用指针之前检查有效性
七、指针的算数运算
1、指针加法 (ptr + n
)
一个指针与一个整数相加时,结果是一个新的指针,它指向相对于原指针向后移动n
个元素的位置。这里的n
是与指针类型对应的数据类型的大小的倍数。例如,如果你有一个指向int
的指针int *p;
,并且int
占用4个字节,则p + 1
会得到一个新的地址,比p
的地址高4个字节。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
printf("%p\n%p\n", p, p + 1);
地址相差四个字节。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];//指向数组首元素的地址
if (*(p + 1) == arr[1])//数组第二个元素
{
printf("相等!\n");
}
p + 1直接跳到数组的下一个元素。
2、指针减法 (ptr - n
)
一个指针与一个整数相减时,结果是一个新的指针,它指向相对于原指针向前移动n
个元素的位置。与指针加法类似,移动的实际字节数取决于指针类型对应的数据类型的大小。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[1];
printf("%p\n%p\n", p, p - 1);
地址相差四个字节。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[1];//指向数组第二个元素的地址
if (*(p - 1) == arr[0])//数组首元素
{
printf("相等!\n");
}
p - 1直接跳到数组的上一个元素。
3、指针间的减法 (ptr1 - ptr2
)
两个类型相同的指针相减时,结果是它们之间相隔的元素个数。如果ptr1
和ptr2
指向同一个数组的不同元素,则ptr1 - ptr2
将得到一个整数,指示它们之间的距离。这个结果通常用于确定数组中的位置或计算偏移量。指向同一块内存空间的两个指针相减才有意义。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p1 = &arr[0];
int* p2 = &arr[9];
printf("%d\n", p2 - p1);
第1个元素与第10个元素之间相差9个元素。
4、指针递增和递减
使用++
和--
运算符可以使指针向前或向后移动一个元素。例如,++ptr
将指针移向下一个元素,而--ptr
将指针移向前一个元素。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p;
for (p = &arr[0]; p <= &arr[9]; )
{
*p = 0;
p++;
}
对数组的每个元素操作,将数组的每个元素赋值为0。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p;
for (p = &arr[9]; p >= &arr[0]; )
{
*p = 10;
p--;
}
对数组的每个元素操作,将数组的每个元素赋值为10。
5、指针比较
指针可以使用关系运算符(>
, <
, >=
, <=
, ==
, !=
)进行比较。这些运算符通常用于比较同一数组或内存块内的指针位置。尝试比较不同数组或内存块的指针是未定义行为。
cpp
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
if (p == &arr[0])
{
printf("相等!\n");
}
八、指针和数组
1、数组名
数组名在大多数情况下是首元素地址,两种情况除外,详见我之前的文章《C语言------数组》。
九、二级指针
二级指针是存放指针变量的指针变量。
在C语言中,二级指针是指向指针的指针,也就是说,它存储的是另一个指针的地址。二级指针通常用于动态多维数组的分配、函数中修改指针本身的值、以及处理指针数组等场景。
cpp
#include <stdio.h>
int main()
{
int var = 10; //普通的整型变量
int* ptr = &var; //一级指针,指向整型变量var的指针
int** pptr = &ptr; //二级指针,指向一级指针ptr的指针
printf("var = %d\n", var);
printf("*ptr = %d\n", *ptr);
printf("**pptr = %d\n", **pptr);//使用二级指针访问var的值
return 0;
}
十、指针数组
1、介绍
在C语言中,指针数组是一个数组,其每个元素都是一个指针。换句话说,指针数组是用来存储指针的数组。这种数据结构经常被用来存储字符串数组或者动态分配的结构体数组的指针。
cpp
int a = 0;
int b = 1;
int c = 2;
int* parr[3] = { &a,&b,&c };//指针数组存放指针
2、用指针数组模拟二维数组
因为数组名是首元素地址,所以可以用指针数组存数组的首元素地址实现二维数组的模拟。
cpp
#include <stdio.h>
int main()
{
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 3,4,5,6 };
int arr3[4] = { 5,6,7,8 };
int* parr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", *(parr[i] + j));
}
printf("\n");
}
return 0;
}
运行结果:
还可以这样:
cpp
#include <stdio.h>
int main()
{
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 3,4,5,6 };
int arr3[4] = { 5,6,7,8 };
int* parr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
运行结果:
这个版本与上一个是一样的作用。