本章来认识一下指针,先对指针有个大体的了解。指针是C语言中最重要的内容,因为可以直接通过指针来操控计算机的内存,其次,指针也可以与数组、函数结合,产生很多有趣的组合,这些内容在之后的指针(进阶)章节中再详细说明。
一、指针是什么
在第一章初识C语言中已经了解过,在此简单提一下:
指针(Pointer)是编程语言中的一个对象,它存放的是另一个变量的内存地址,通过这个地址可以直接找到该变量。
内存:
我们电脑的内存都被划分为很小的内存单元,并给每个内存单元都进行编号,这个编号就是内存的地址。以32位举例,32位有32根地址线/数据线,地址线一旦通电就会有正电(1)和负电(0);通电后电信号转化为数字信号,因此有2^32 个二进制序列,将这些序列作为每个内存单元的编号,就会产生2^32个内存单元,每个内存单元的大小为一个字节。
每一块小的内存都有地址,这就好像某个人住在xx省xx市xx区xx街道xx校区xx楼xx号一样,这个人就是变量,他的房子就是内存单元,而xx省xx市xx区xx街道xx校区xx楼xx号则是内存的地址。通过地址我们就可以找到内存单元,进而找到这个人。
指针:
指针是个变量,存放内存单元的地址。
int num = 10;//在内存中开辟一份空间存放变量num=10
int *p;//p为一个整形指针变量 int* 是p的类型,即整形指针
p = #//p中存放变量num的地址
关于指针的大小为什么是4,这里再说明一下:32位操作系统之所以被称之为32位操作系统, 是因为CPU所能处理的数据的最大位数是32位,32位操作系统所能支持的最大内存的大小是(2^32-1)Byte ≈ 4G。指针存放的是一个地址,地址是一个8位十六进制的数字,也就是32位二进制的数字,而一个字节刚好是8位二进制数字,因此是4个字节。任何类型的指针其大小都是4,因为其存放的都是一个地址,地址的大小就是4个字节。
综上:
- 指针是用来存放地址的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
注意: 在本章中,指针p指向变量a,意思就是指针变量p里面存放的是变量a的地址。
二、指针和指针的类型
指针和变量一样,变量有字符型、整形、浮点型等类型,指针也有不同的类型。
指针的定义方式是: type + * 。 不同类型的指针用来存放对应类型的变量,比如: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放int 类型变量的地址。
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
那如果用一种类型的指针存放另一种类型的变量会发生什么事情呢?或者说不同类型指针的区别是什么呢?
#include <stdio.h>
int main()
{
int n =0x11223344;
char* pc = (char*)&n;//将int*型指针强转为char*型指针
int* pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc + 1);
printf("%p\n", pi);
printf("%p\n", pi + 1);
printf("%d\n", *pc);
printf("%d\n", *pi);
return 0;
}
n是一个十六进制的整形数字,大小为4个字节,它在内存中的存储如图所示。首先可以看到,不管是char* 类型还是int* 型类型,它们存放的地址都是相同的,都是n的起始位置地址。但是+1之后,各自跳过的内存大小是不同的,char* 型+1跳过一个字节的内存,而一个字节正好也是char型变量的大小,同理int* 型则是跳过四个字节。
另外,它们解引用后的值也不相同。char* 型的指针解引用,只能访问到所存内存地址向后一个字节的内容,也就是44,而这是个十六进制数字,转化成十进制则是68,int* 型则能访问到所存内存地址向后四个字节的内容,即11223344,转化成十进制则是287454020。可以看到,不同类型的指针解引用能够访问到其原本类型大小的内存空间。
这也就是为什么各自的指针要存放各自对应类型的变量,因为只有这样在解引用的时候才不会出现访问内存不全或者访问内存越界的情况。当然在某些特殊情况,我们可以用强制类型转换符( )来将一种指针类型的指针转化为另一种指针类型来存储。
2.1野指针
野指针就是一个管理不了的指针,指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
下面的方式都会导致野指针的产生:
- 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 10;
return 0; }
就像局部变量不初始化编译器默认为随机值一样,指针不初始化存储的也是随机的内存地址,如果此时解引用指针,使指针的地址存一个整形变量10,我们是不知道这个变量10存放在哪个地址的,因为p里面存的是一个随机的内存地址。
- 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;//p指向数组的首元素
int i = 0;
for(i=0; i<12; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;//*(p+1)不断访问数组中的元素
}
return 0;
}
p为11时,此时p的地址已经不是数组内元素的地址了,它已经指向了数组后面的内存,这块内存属于哪个变量是未知的,因此访问这块内存空间是非法的。
- 指针指向的空间释放
当我们开辟的一块内存被编译器自动释放或者被我们手动释放时,这块空间已经还给操作系统、不属于编译器了,这个时候在访问就是非法的。
#include <stdio.h>
int* test()//返回值类型是int* 型
{
int a=10;
return &a;
}
int main()
{
int* p=test();
*p=20;//非法访问内存
return 0;
}
当test()函数调用完成以后,变量a的内存已经被释放了,虽然此时p里面存的仍然是原来a的地址,但这块内存已经不属于变量a,已经被编译器还给操作系统,此时再进行解引用的访问是非法的。
规避野指针的方法:
- 指针初始化
在定义指针时就对其进行初始化,即指向一个变量的地址,如果没有变量地址可指向,就赋值为空指针NULL - 小心指针越界
特别是在数组的访问中,注意不要越界访问 - 指针指向空间释放即使置NULL
- 指针使用之前检查有效性
在使用指针前可以先判断指针是否为空指针NULL
#include <stdio.h>
int main()
{
int *p = NULL;//初始化指针为空指针
int a = 10;
p = &a;
if(p != NULL)//检查指针的有效性
{
*p = 20;
}
return 0;
}
三、指针的运算
- 指针±整数
指针加或减一个整数,指针的内存将跳过指针类型个大小,比如:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;//p指向arr的首元素地址
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d\n", arr[i]);
}
return 0;
}
因为p是int* 型的指针,当p+i 时,p跳过 i*4 个字节的内存,而数组中一个元素的大小就是四个字节,这个时候会跳过 i个元素,p将直接指向数组第i个元素的地址,此时 *p访问四个字节的内存,就可以访问到第i个元素,并将第i个元素的值赋值为i。
- 指针-指针
指针减去指针得到的是指针和指针中间的元素个数。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[5] - &arr[0]);
printf("%d\n", &arr[0] - &arr[05]);
return 0;
}
当两个指针相减的时候,两个指针必须指向同一块空间,比如指向同一个数组。如果两个指针指向的空间不同,那么结果是随机的。
- 指针和指针关系运算
指针和指针也是可以比较大小的,数组的地址从首元素到最后一个元素的地址依次增加。
注意:
C语言中允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
允许指向arr数组中元素的指针p1与指向数组最后一个元素后面的指向变量b的指针比较,但是不允许p1与指向第一个元素前面的变量a的指针比较。
四、指针和数组
在数组那一章中,我们已经知道数组名其实就是数组的首元素地址(sizeof和&数组名 除外),因此我们可以直接通过数组名+i得到数组第i个元素的地址,再通过解引用* 得到该地址指向的数组元素。
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
五、二级指针
以上所讲的内容都是一级指针,一级指针存放的是变量的地址,那么二级指针就是存放一级指针的地址。二级指针的创建和一级类似:type + **。
#include <stdio.h>
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
printf("%d",*(*pp));//*pp就相当于p
return 0;
}
与一级指针类似,当我们对二级指针解引用*pp的时候,得到的是一级指针p的内容,p的内容是变量a的地址,此时再解引用即可得到变量a。
类似的,还有三级指针、四级指针… …
六、指针数组
指针可以和数组结合产生指针数组和数组指针,这两个东西完全不相同,不要搞混。在此先介绍下指针数组,数组指针将在指针(进阶)中讲解。
我们前面学过整形数组存放的是多个整形变量,那么顾名思义指针数组存放的就是多个指针。
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
int* arr[3] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d\n", *arr[i]);
}
return 0;
}
可以看到,指针数组arr的类型是整形指针int* 型,里面存放的都是指针指向的地址&a,&b,&c,此时通过for循环可以依次拿到指针数组的元素,再通过解引用即可得到各自指针指向的值。
七、const修饰指针
const 修饰变量,这个变量就会被称为常变量,不能被修改,但本质上还是变量。
const也可以修饰指针:
int main()
{
int num=10;
int n=20;
const int *p=#//限制*p
//*p=20;//错误
p=&n;//正确
printf("%d\n",num);
return 0;
}
int main()
{
const int num=10;
int n=20;
int *const p=#//限制*p
*p=20;//正确
//p=&n;//错误
printf("%d\n",num);
return 0;
}
const放在 * 的左边,称为常量指针,修饰的是 * p,不能改变 * p的值,但是可以改变p的值;也就是指针指向的值不能修改,但指针的指向可以修改
const放在 * 的右边,称为指针常量,修饰的是p,不能改变p的值,但可以改变 * p的值;也就是指针的指向不能修改,但指针指向的值可以修改
总结:
这篇博客带大家初步了解了C语言中的指针。
数组指针、指针和函数相结合的指针的进阶内容将在之后详细说明。
当然,这篇博客如果有任何错误,或者各位大佬有任何建议,可以在评论区指出和提出,我会对博客进行修改和完善。