目录
前言
一、指针是什么
二、指针和指针类型
2.1指针类型
2.2指针+-整数
2.2指针的解引用
三、野指针
3.1野指针的成因
3.2如何规避野指针
四、指针的运算
4.1指针+- 整数
4.2指针-指针
4.3指针的关系运算
五、指针和数组
六、二级指针
七、指针数组
总结
指针是C语言的特色。在C程序设计中,C语言被广泛使用。使用指针可以直接操作地址,可以使程序高效简洁。本文主要介绍指针的一些常规的应用。
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
总结:指针就是地址,口语中说的指针通常指的是指针变量。
指针变量: 我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个 变量就是指针变量
#include
int main()
{
int a = 10;//在内存中开辟一块空间
int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
return 0;
}
总结: 指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。 那这里的问题是: 一个小的单元到底是多大?(1个字节) 如何编址? 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电 平(低电压)就是(1或者0); 那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
...
11111111 11111111 11111111 11111111
这里就会有2的32次方个地址。
每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB == 2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。
同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算。
这里我们就明白: 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。
总结: 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。 指针的大小在32位平台是4个字节,在64位平台是8个字节。
这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢? 准确的说:有的。
这里可以看到,指针的定义方式是: type + * 。
其实: char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。
在上面我们提到指针是用来存放地址的,而指针变量的大小在32位机器上是4个字节,在64位机器上是8个字节,所以指针变量的大小与类型并没有关系,那么为什么还要给指针变量分这么多种类型呢,而不是统一一种类型呢?
下面我们就来探讨一下指针变量为什么要分类型?
#include
//演示实例
int main()
{
int n = 10;
char *pc = (char*)&n;
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);
return 0;
}
运行结果:
接下来我们来分析一下这段代码:我们知道用%p是用来打印地址的,打印的是这个变量第一个字节的地址,这里指针pc、pi也都是指向变量n的,所以在这里打印n的地址还有pc、pi输出都会是一样的,都是变量n第一个字节的地址。
而不同的是打印pc+1和pi+1结果是不一样的,指针pc是char类型的,存放的是int类型的n的地址,并强制把n的地址转换成char*类型,而指针pi是int类型的,存放的是int类型的n的地址,通过比较,我们发现pc+1地址只变化了一个字节,而pi+1地址变化了4个字节,由此,我们可以得出
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
在VS2019里面调试起来,然后在内存中观察n的值的变化:
通过图片我们可以看出我们对整型指针变量pa解引用并赋值为0,会直接把变量a四个字节的数据都被修改为0;
接下来我们再看一张图片:
通过图片我们知道我们把一个整型变量a的地址赋给了一个char*类型的指针变量pc然后我们对指针pc解引用后赋值为0,但是我们发现这时只修改了变量a第一个字节的数据。
通过对比我们可以得出结论:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
总结一下指针类型的意义:
1、指针的类型决定了指针向前或者向后走一步有多大(距离)。
2、指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1. 指针未初始化
#include
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
这段代码在VS2019的编译器下是直接会编译不通过的,而在某一些编译器编译是可以通过的;
但是使用的话是有风险的。一般不允许使用未初始化的指针变量。
2. 指针越界访问
#include
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
这个数组越界造成的野指针,编译器也会报警告
3. 指针指向的空间释放
这个涉及到动态内存开辟的知识,当前只需要先作了解一下即可
1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
这里只是列举出一部分情况,当然还有别的情况也会造成野指针的产生,所以需要大家在以后的编程过程中自己去发现和总结。
什么是指针的运算呢?下面我们将直接通过代码展示出来指针运算的操作:
#include
int main()
{
int arr[5] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i < 5; i++)//给数组元素赋值1~5
{
*(p + i) = i + 1;//这里是通过指针加整数操作访问数组各个元素
}
return 0;
}
上面这一段代码就是指针加整数的操作,通过指针加上一个整数,使指针变量向后产生偏移(偏移多大取决于指针类型,这里int*指针+1会向后偏移4个字节),指向内存的不同位置。当然如果是减上一个整数指针变量就会往前偏移。
在开始讲指针-指针之前,先说一下,指针和指针之间是没有加法运算的,我们可以想一下,两个地址相加能有什么意义呢?就好像日期减日期可以得到中间差几天,而日期加日期又有什么意义呢?
#include
int main()
{
int arr[10] = { 0 };
int* p = arr;
printf("%d\n",(p+9)-(p+0));
printf("%d\n",(p+0)-(p+9));
return 0;
}
输出:
结论:通过指针-指针我们发现指针减指针的运算结果的绝对值就是两个指针间的元素个数。
但是指针相减的前提是两个指针指向的是同一块连续的空间。而不能是两个完全不相关的指针,两个完全不相关的指针是不可以相减的。
指针-指针的一个例子:利用指针-指针的方式来求字符串长度 :
#include
int my_strlen(char* str)
{
char* start = str;
while (*str)//其实应该是*str != '\0',但是'\0'本质是0;所以可以直接写成while(*str)
{
*str++;
}
return str - start;
}
int main()
{
char arr[] = "abcde";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
输出结果:
这里其实就是利用了指针减指针的运算结果的绝对值就是两个指针间的元素个数这个结论。
#include
#define N 5
int main()
{
int values[N];
int* vp = values;
for (vp = &values[N]; vp > &values[0];)//通过比较指针的大小来判断数组是否越界
{
*--vp = 0;
}
return 0;
}
接下来我们来解释一下上面这段代码:首先我们定义了一个整型数组values,然后定义了一个整型指针vp指向这个数组首元素,在for循环中我们让指针vp指向了数组values最后一个元素接下去一个元素的地址,(这里我们只是让vp指向了那个地址,并没有对其进行操作和访问,所以不会造成指针越界的问题),然后我们通过比较两个指针(vp > &values[0])的大小来判断数组是否越界,这就是指针的关系运算,最后我们通过这段代码成功的将数组元素全部赋值为了0。
开始讲之前我们先来看一段代码:
#include
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
输出结果:
可见数组名和数组首元素的地址是一样的。
结论:数组名表示的是数组首元素的地址。
那么我们这样写代码就是可行的:p存放的就是数组首元素地址
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。
举个例子:
#include
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
这段代码输出结果就是:
所以我们得出结论p+i 其实计算的是数组 arr 下标为i的地址。
这就是指针和数组之间的关系,所以我们就可以直接通过指针来访问数组。
指针变量也是变量,是变量就有地址,那么指针变量的地址存放在哪里? 这就是二级指针 。
二级指针的定义是:类型+**
我们如何理解二级指针的这个定义呢?我们知道一级指针定义是类型+*(比如int*就是整型加上一个*,这就是整型指针),而对于二级指针:定义是这样的(int**+变量名)在这里两个*意义是不一样的,我们可以这样理解成int*+*就是我们要定义的变量类型是int*类型的,然后第二个*则是告诉编译器我们要定义的变量是指针类型的。
在这里我们就暂时先简单介绍二级指针的一些运算:
#include
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
1、基于上面那段代码*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .
int b = 20;
*ppa = &b;//等价于 pa = &b;
2、**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
在这里我们就简单的介绍一下指针数组的定义和简单应用。
指针数组数组是指针还是数组呢?
答案是:数组,是存放指针的数组
目前我们已经知道的数组有整形数组,字符数组。
那指针数组是怎样的?
int* arr3[5];
在这里很容易看出来arr3是一个数组,有五个元素,每个元素的类型是一个整形指针,所以arr3就是一个指针数组。
这里我们来简单的使用指针数组来模拟二维数组:
#include
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int arr4[5] = { 4,5,6,7,8 };
int arr5[5] = { 5,6,7,8,9 };
int* arr[5] = { arr1,arr2,arr3,arr4,arr5 };
int i = 0;
int j = 0;
for (i = 0; i < 5; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", *(*(arr + i) + j));//这里也可以写成arr[i][j];
}
printf("\n");
}
return 0;
}
输出结果:
这只是指针数组的一种简单应用,当然指针数组还有很多用法,这里就不多介绍了。
本文主要介绍了指针初阶的一些基础知识和一些简单的应用,因为是初阶,并没有去介绍太多内容和使用场景。
希望我的文章能够为大家再写代码时提供帮助!
最后希望大家在阅读过程中如果有发现错误或不足,能够及时指出,我好加以改正。