目录
1.指针是什么
指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
每个内存都有一个唯一的编号,这个编号被称为地址
编号 == 地址 == 指针
写C语言程序的时候,创建的变量,数组等都要在内存上创建空间。
int main()
{
int a = 100;
int* pa = &a;//pa是专门用来存放地址(指针)的,这里的pa就被称为指针变量
/*int arr[10];
printf("%p\n", &a);*/
return 0;
}
对于32位机器,假设有32根电线,假设每根地址线在寻址的时候产生高电平(高电压)和低电平就是1或0;
那32位地址线产生的地址是:
00000000000000000000000000000000
00000000000000000000000000000001
.....
1111111111111111111111111111111111111
这里有2^32个地址,每个地址标识一个字节,那就有2^32byte == 2 ^ 32/1024/1024/1024 GB = 4GB的空间进行编址
指针的大小在32位平台是4个字节,在64位平台上是8个字节
2.指针和指针类型
64位平台
32位平台
int main()
{
int a = 0x11223344;//0x开头的是16进制的数字
int* pa = &a;
*pa = 0;
return 0;
}
最右边的是解析内存中的数据,参考性不大
当走到0的时候,内存里面显示0
把pa类型改成char呢
int*的解引用可以访问4个字节,而char*只能访问一个字节
结论:指针类型可以决定指针解引用的时候访问多少个字节(指针的权限)。
short* --> 2 float* -->4 double* --> 8
type * p,这里的type可以这么理解👇
1.决定了p指向的对象的类型;2.p解引用的时候访问的对象大小是sizeof(type)
指针类型决定指针+1/-1操作时的步长
整型指针+1跳过4个字节
字符指针+1跳过1个字节
+/- n type * p 跳过的是:n*sizeof(type)这么多个字节
如果你输入的时abcdef\0,每次解引用想跳到下一个字符,这时候可以选择char类型,每次纸条一个字节;而输入arr[10] = {0,1,2,3,4,5,6}的时候,每次解引用要调到下一个数字,数字之间间隔4个字节,这个时候就可以用int类型来解引用
3.野指针
(1)产生野指针的原因
看看这个代码有没有问题
int main()
{
int* p;
*p = 20;
printf("%d\n", *p);
return 0;
}
1. 局部变量不初始化的的时候,内容是随机值
这里的p没有初始化,称为野指针
野指针指向的位置是不可知的
2. 指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)//这里的11明显超出数组的范围
{
*(p++) = i;//当指针指向的范围超出数组arr的范围时,p就是野指针
}
return 0;
}
3.非法访问空间
先来看一个生动的例子:
我在如家花了300元住进302房间,休息了一晚上后我感觉房间不错,于是我告诉张三:“明天你来302房住吧。“但是第二天我退房了, 张三记住了我的话,来到302房间住(没付钱)。很明显,他没办法住进去。张三这种行为属于非法访问。
上面的p和张三一样,非法访问a(也就是我退了)的空间,这种行为跟野狗一样,所以在这里p属于野指针。
(2)如何规避野指针
1.指针初始化
这里要进行两步:a.明确指针应该初始化为谁的地址,就直接初始化
b.不知道指针初始化为什么值,就暂时初始化为NULL。
int main()
{
int a = 10;
int* p = &a;
int* ptr = NULL;//NULL其实就是指针类型的0
//ptr是一个空指针,没有指向任何有效的空间。这个指针不能直接使用
//int* ptr2;//野指针
if (ptr != NULL)
{
//使用
}
return 0;
}
2.小心指针越界
3.指针指向的空间释放,及时置NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性 (不为空就使用它)
4.指针运算
(1)指针+-整数
int main()
{
int arr[10] = { 0 };
//不适用下标访问数组
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//第一种写法
for (i = 0; i < sz; i++)
{
*p = i;
p++;//加四个字节到下一个地址
}
//第二种写法
p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
/*for (i = 0; i < 10 ; i++)
{
printf("%d", arr[i]);
}*/
return 0;
}
//int arr[10];
//int* p = arr;
//*(p+i) == arr[i]
//*(arr+i) == arr[i]
第二种写法的图片理解
arr是数组名,数组名是首元素的地址,也就是指针
延伸一下:
那根据加法交换律可不可以推出下面的等式呢
arr[i] == *(arr+i) == *(i+arr) == i[arr]
看起来是对的
于是我们更加清楚一点:[]只是一个操作符,这个操作符跟加法那些其实一样,可以交换
而且这个操作符本质是采用指针来做的。
3+2-->2+3 arr[i]-->i[arr]
(2)指针-指针
猜一下这个程序结果是多少
int main()
{
int arr[10] = { 0 };
printf("%d\n",&arr[9] - &arr[0]);
return 0;
}
答案是9
所以指针-指针得到的数值的绝对值:指针与指针之间的元素个数
指针相减的前提:两个指针指向了同一块空间
int main()
{
int arr[10] = { 0 };
char ch[5] = { 0 };
printf("%d\n", &arr[9] - &ch[0]);//error
return 0;
}
回顾一下my_strlen的写法
int my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
这里的s++是地址不断地往后面加,也就是指针不断往后面挪
这里没必要在s前面解引用,因为解引用是找到s指向空间或内容,没有意义
//另外两种写法
int my_strlen(char* s)
{
char* start = s;
while (*s != '\0')
{
s++;
}
return s - start;
}
int my_strlen(char* s)
{
char* start = s;
while (*s)//a b c d e f \0->0的ASCII码值:0,0扔进去为假,循环也就暂停了
{
s++;
}
return s - start;
}
还有一种常见的错误写法
结果为7明显不对,这是因为当*s = \0的时候,因为后置++,优先级比*高,s还跳到后面一位去了(相当于越界访问),这个时候结果就多出来一个1,所以要在return那里再多减一个1
(3)指针的关系运算
指针是有大小的,关系运算就是比大小
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float* vp;
for (vp == &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
return 0;
}
vp指针向左挪动,并把空间值变成0,直到最左边value = vp
这里的停止条件就用到了两个指针的大小比较
精简代码
for (vp == &values[N_VALUES-1]; vp > &values[0];vp--)
{
*vp = 0;
}
vp不断向左挪,一直挪到最左边,假设values左边还有地址,vp挪到该地址发现小于&values[0],循环终止。
这种代码虽然在绝大部分编译器上可以完成任务,但是因为C语言的标准规定:允许指向数组元素的指针与指向数组最后一个元素后面那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。所以在有些编译器可能会出现问题。
也就是允许P和P2进行比较,不允许P和P1进行比较
5.指针和数组
指针和数组有什么关系?
指针变量就是指针变量,不是数组,指针变量的大小就是4/8个字节,专门用来存放地址的
数组就是数组,不是指针,数组是一块连续的空间,可以存放1个或者多个类型相同的数据。
联系:
数组中,数组名其实是数组首元素的地址,数组名 == 地址 == 指针
有两个例外:sizeof(arr)和&arr,这两个一个是数组大小,一个是取整个数组地址
当我们直到数组首元素的地址的时候,因为数组是连续存放的,所以通过指针遍历数组实现数组是通过指针访问
从起始地址通过加i的方式,就可以计算出下标为i位置的地址,程序和结果如下:
int arr1[10] 和 int arr2[8]两个数组一样吗?
不一样的,因为元素个数不一样,空间就不一样
6.二级指针
这里的pp是二级指针,二级指针变量用来存放一级指针变量的地址
第二颗*还是在说明pp是指针变量,第一颗*是在说明pp指向的是int*类型的变量
多级指针以此类推
注意:二级指针跟二维数组之间没有必然联系,别搞混了!!!
一个小应用
int main()
{
char arr1[] = "abcdef";
char arr2[] = "hello world";
char arr3[] = "cui hua";
char* parr[] = { arr1, arr2, arr3 };
char** p = parr;//存放的是首元素也就是arr1的地址
return 0;
}
用个图来理解一下
7.指针数组
指针数组的本质是存放指针的数组
这个可以参考上方这个parr的例子,这个parr就是存放指针的数组
int main()
{
char arr1[] = "abcdef";
char arr2[] = "hello world";
char arr3[] = "cui hua";
//指针数组
char* parr[] = { arr1, arr2, arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%s\n", parr[i]);//把三个数组的字符串全部打印出来
}
return 0;
}
整型数组没有字符数组那么方便一个%s直接打印字符串
整型数组要模拟二维数组进行打印
但是这道题跟真实的二维数组又有些不一样
真实的二维数组是连续的
但这道题的parr模拟的二维数组是这样的