目录
何为指针?
指针是内存中一个最小单位(一个字节)的编号,也就是地址。而平时说的指针,指的是指针变量,是用来存放地址的变量,也可以说,存放在指针变量中的值都被当做地址处理。我们可以通过&(取地址操作符)取出变量的地址,并把这个地址存放到另一个变量中,这个变量就是指针变量,当然了,指针变量也有自己的地址。
如何编址?
如果是32位的机器,就会有32个二进制位来进行编址,那就会有个地址。每一个地址标示一个字节,但每一个地址本身大小却是4个字节的。
上述图的意思是,char类型的内存大小是1个字节,只用一个地址,而int类型的内存是4个字节,那么就会用4个连续的地址来标识,但取地址时会显示首地址。
注:只是为了好画,变量内存的位置并不是挨着的,空几个取决于编译器。
指针类型
既然指针变量的大小都一样,为什么指针变量还需要那么多种类型?
指针的类型决定了
1.该指针变量解引用操作能访问多少个字节(权限)
char*类型的指针,解引用能访问1个字节
int*类型的指针,解引用能访问4个字节
double*类型的指针,解引用能访问8个字节的指针
2.该指针变量的步长(向前/向后走一步都有多大距离)
char*类型的指针,指针名+1的意思是跳过一个字符,也就是往后走1个字节
int*类型的指针,指针名+1的意思是跳过一个整型,也就是往后走4个字节
double*类型的指针,指针名+1的意思是跳过一个double,也就是往后走8个字节
#include<stdio.h>
int mian() {
int a = 0x11223344;
int* pa = &a;
char* pc = &a;//2022的vs,这一行直接报错
printf("%p\n", pa);
printf("%p\n", pc);
printf("%p\n", pa+1);
printf("%p\n", pc+1);
return 0;
}
但如果可以运行的话,就会发现,pa,pc的地址是一样,但pa+1的地址是在pa地址上+4,而pc+1是在pc地址上+1。
野指针
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),比如
1.未初始化的指针,如
#include<stdio.h>
int main() {
int* p;//未初始化的指针,默认为随机值
*p = 20;
return 0;
}
未初始化的指针,会存储一个随机值,并把这个随机值当成是地址,解引用的话,就会访问这个地址,并把这个地址上的值修改成20,但问题是,这个地址我们并未对其声明,是不属于我们的。
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=10; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
当i=10的时候,p指向arr[10],一个未被声明的整型,并把arr[10]的值修改成10,属于越界访问了,此时的p就是野指针。
3.指针指向的空间被释放了
int* test()
{
int num = 100;
return #
}
int main()
{
int* p = test();
*p = 200;
return 0;
}
num是局部变量。调用完函数test()之后,内存就被释放,但地址却传给了p,并解引用修改成200,这也使得p是个野指针。
总结一下,野指针就是指针指向的空间不属于我们当前程序。
如何规避野指针
1. 指针初始化
#include<stdio.h>
int main() {
int a = 20;
int* p = &a;//明确初始化的空间
int* pa = NULL;//还未确定指针指向哪里前,初始化为空指针
return 0;
}
2. 小心指针越界
3. 指针指向空间释放,及时置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性
空指针是不能直接访问的
int main()
{
int a = 10;
int* p = NULL;
printf("%d\n",*p);//报错
*p=20;//这也会报错,因为p是空指针,不能访问
return 0;
}
指针的运算
指针+-整数
#define N_VALUES 5
int main(){
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)//判断表达式中,指针比较大小
{
*vp++ = 0;
//由于++的优先级高于*,因此上面的代码等价于*(vp++)
//*vp=0;vp++;
}
return 0;
}
虽然最后vp越界了,但并没有进行修改或读取,这也是可行的,只是有隐藏的危险。
对上述代码进行修改,使其更加简洁:
#define N_VALUES 5
float values[N_VALUES];
float* vp;
int main()
{
for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
{
*vp = 0;
}
return 0;
}
上述修改造成指针最后指向数组首元素之前的位置,虽然在绝大多数编译器上可行,但标准并不允许。所以我们最好避免这样写。
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
指针-指针
int main()
{
//两个指针相减的前提是:指针指向的同一块连续的空间
int arr[10] = {0};
printf("%d\n", &arr[9] - &arr[0]);
printf("%d\n", &arr[0] - &arr[9]);
return 0;
}
输出:9
-9
可以利用指针-指针来计算字符串的长度
int my_strlen(char* str)
{
char* start = str;
while (*str)
str++;
return str - start;
}
int main()
{
char arr[] = "abcdefg";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
输出:7
指针和数组
数组名是首元素的地址,可以通过把地址存到一个指针变量中,来间接访问数组。
二级指针
上面说过,指针变量也有自己的地址,这时,该指针变量就是一级指针,如果把指针变量的地址再一次地赋给一个新的指针变量,那么这个新的指针变量就是一个二级指针。三级、四级指针以此类推。
#include<stdio.h>
int main() {
int a = 12;
int* p = &a;
int** pp = &p;
**pp = 250;//*(*pp)
printf("%d\n", a);
return 0;
}
pp的类型是 int**,其中int*指的是pp指向的类型是个一级指针,而第二个*表明pp是个指针。同样的,p的类型是int*,其中int指的是p指向的类型是个int,而*表明p是个指针。
上述代码输出:250,通过对pp的两次解引用,访问a的值。
指针数组
指针数组是数组,是存放指针的数组。
#include<stdio.h>
int main()
{
//整型数组-存放整型的数组
int arr[10];
//字符数组-存放字符的数组
char arr2[5];
//指针数组-存放指针的数组
int* arr3[5];//存放整型指针的数组
char* arr4[6];//存放字符指针的数组
return 0;
}
示例:
int main()
{
//用一维数组模拟一个二维数组
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[4] = {arr1, arr2, arr3};
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
由上图可知两件事,一是数组在内存中的存放是连续的,地址随下标的增加而增加;二是先声明的变量地址会比后声明的变量地址高。
arr[i]其实相当于*(arr+i),arr本身就是地址,是首元素的地址,arr[i]就是对arr自身的地址解引用,访问里面存储的内容,只是这个内容又是一个地址而已,再一次arr[i][j],即*(*(arr+i)+j),才能访问到三个数组里面的数字。
例题
1.下面代码输出?
#include<stdio.h>
int main() {
int a = 99;
int* p = &a;
int** pp = &p;
int* ps = *pp;
*ps = 250;
printf("%d\n", a);
return 0;
}
输出:250
解析:pp存放了p的地址,*pp是把p存放的内容,即a的地址,赋给了ps,因此*ps访问a。
2.下面代码运行的结果是:
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5};
short *p = (short*)arr;
int i = 0;
for(i=0; i<4; i++)
{
*(p+i) = 0;
}
for(i=0; i<5; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
输出:0 0 3 4 5
解析:考察前面介绍的指针类型的用处。一是访问的权限,short*只能访问2个字节,二是指针的步长,因此p+1,每次跳过2个字节。
3.下列程序段输出的结果为:
unsigned long pulArray[] = {6,7,8,9,10};
unsigned long *pulPtr;
pulPtr = pulArray;
*(pulPtr + 3) += 3;
printf(“%d,%d\n”,*pulPtr, *(pulPtr + 3));
输出:6 12
解析:*(pulPtr+3)+=3等价于*(pulPtr+3)=*(pulPtr+3)+3,等价于pulArray[3]=pulArray[3]+3