前面操作符详解的博客中,单目操作符解引用那里,就已经比较详细地讲解了指针是什么。
有的小伙伴们应该初步理解了指针,没有理解也没有关系。接下来会有更详细的讲解。
内容有:指针是什么?大小有多大?不同类型的指针可以访问多少字节?“实现逆序数组中的元素”的题目中,引出野指针的概念。二级指针、指针数组等问题。
1. 指针是什么
指针理解的2个要点:
1 指针是内存中一个最小单元的编号,也就是地址。
2 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
问题1:内存单元编号是什么?一个小的单元有多大?
对于32位的机器,假设有32根物理的地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000(编号0x00000000)
00000000 00000000 00000000 00000001(编号0x00000001)
…
11111111 11111111 11111111 11111111(编号0xFFFFFFFF)
这里就有2的32次方个地址,编号采用的是16进制的编号。
每个地址标识一个字节,那我们就可以给 4G的空闲进行编址,这些编号是硬件电路产生的,没有必要把它存起来。
注意:字节(byte)是最小的存储单元,等于8bit。
对于32位机器而存放地址编号需要4个字节,也就是32bit,4GB;对于64位机器而存放地址编号需要8个字节,也就是64bit,8GB。
问题2:如何理解指针就是地址
内存单元都有一个编号,而这个编号被我们称为地址,其实我们平时说的指针都是“存放地址的变量”,可以通过这个变量找到这个地址。
所以从某种意义上来说内存单元=编号=地址=指针。(参考前面:32根物理的地址线)
2. 指针和指针类型
这里我们在讨论一下:指针的类型。
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?当然有啊。
int main()
{
char* pc;
int* pa;
double* pd;
printf("%d\n", sizeof(pc));
printf("%d\n", sizeof(pa));
printf("%d\n", sizeof(pd));
return 0;
}
x86环境下(32位)运行的结果都是4;x64环境下(64位)运行的结果都是8。
可以得知指针的大小和指针的类型无关。
指针的解引用
下面来看这个样的一个代码,进一步理解一下指针:
int main()
{
int a = 10;
int* p = &a;
*p=20;//解引用操作
printf("%d", a);//通过地址解引用,来操作a
return 0;
}
大致过程是这样的:首先定义一个变量a,这个变量有一个地址(假设是0x0012ff40),还有一个初始值10。&a就相当于获取了地址0x0012ff40,赋值给指针变量p。使用*对p进行解引用操作,可以修改指针指向的变量。
打个比方:变量a是宝藏的位置,变量的值是宝藏的内容,p就是这个宝藏的藏宝图,有了这个图,你可以解引用操作,找到宝藏然后把它拿走(更改值)!
下面再来讨论一下指针的访问权限的问题:
int main()
{
int a = 0x11223344;
//int* pa = &a;
//*pa = 0;
char* pc = &a;//理论上是可以存放的
*pc = 0;
//指针类型决定了在解引用时一次能访问几个字节?
return 0;
}
按下F10进行调试,在调试->窗口->内存->内存1 中点开这个窗口,可以看到我们使用char类型的指针,成功低更改了a的内容,只有1字节。
使用同样的步骤,int类型一次性更改了4字节的值:
指针±整数
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("%p\n", pa);
printf("%p\n", pa+1);
printf("%p\n", pc);
printf("%p\n", pc+1);
return 0;
}
运行结果如下:
总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。
int型的指针一次操作四个字节,char型的指针一次操作一个字节。
不难得出double*型的一次访问8字节。
3. 野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
3.1野指针的形成原因
原因一:指针未初始化
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
原因二:指针越界访问
笔者在自己写程序的时候,也写出过野指针。
例如需要实现逆序数组中的元素,注意这个是错误的代码,出现了野指针:
#include<stdio.h>
//用来打印数组中的数
void print(int* arr,int x)
{
for (int i = 0; i<x; i++)
{
printf("%d ", *(arr+i));
}
printf("\n");
}
//逆序数组中的元素
void maker_reverse(int* arr,int x)
{
int tmp = 0;
for (int i = 0; i < x/2; i++)
{
tmp = *(arr+i);
*(arr + i) = *(arr - i-1);
*(arr - i - 1) = tmp;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf("交换后:\n");
maker_reverse(arr,sz);
print(arr,sz);
return 0;
}
我的思路是:第一个元素和最后一个元素交换,第二个和倒数第二个交换……
如果该数组的长度是奇数,那么中间那个元素其实可以不用参与交换。
如果该数组的长度是x偶数,那么刚好循环(x/2)次,刚好完成交换。
而我的代码中:
当i等0的时候arr - i - 1 相当于 arr - 1, 这个不是相当于越界访问了
思考一下,然后再来看看正确的代码:
void maker_reverse(int* arr,int x)
{
int tmp = 0;
for (int i = 0; i < x/2; i++)
{
tmp = *(arr+i);
*(arr + i) = *(arr +x - i-1);
*(arr +x - i -1) = tmp;
}
}
3.2 如何避免野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
4. 指针运算
- 指针± 整数
- 指针-指针
- 指针的关系运算
4.1指针± 整数
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
*(p + i) = i;
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
自己编写一个求字符串长度的函数。
int my_strlen(char*s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abc";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
4.2指针-指针
指针减去指针的前提是,两个指针指向同一个连续空间。
int main()
{
int a[10] = { 0 };
printf("%d\n", &a[9] - &a[0]);//地址从低往高
printf("%d\n", &a[0] - &a[9]);
return 0;
}
随着数组下标的增加,地址增加。
用这种方法编写字符串长度的函数:
int my_strlen(char* s)
{
int* start = s;//先把这个地址保存一下
while (*s != '\0')
{
s++;
}
return s-start;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
5. 指针和数组
见数组名和数组首元素的地址是一样的。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//arr 是数组首元素地址
int* p = arr;
int i = 0, sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%p==%p\n", p + i, &arr[i]);//都是相连通的
}
printf("%d\n", sz);
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
结论:数组名表示的是数组首元素的地址。
6. 二级指针
指针变量也是变量,是变量就有地址,存放指针变量的地址就是 二级指针 。
int main()
{
int a = 10;
//int* p1, * p2;//注意不同的写法
int* pa = &a;//*靠近哪都可以
int** ppa = &pa;//ppa就是一个二级指针
int*** pppa = &ppa;
**ppa = 20;//对于二级指针,使用两次解引用访问
printf("%d ", a);
return 0;
}
7. 指针数组
指针数组是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。
int main()
{
int arr[10];//整型数组
char ch[5];//字符数组
//指针数组
int a = 0;
int b = 9;
int c = 8;
int* arr2[5] = {&a,&b,&c};//存放整型指针的数组,不完全初始化
//通过arr2打印abc
for (int i = 0; i < 3; i++)
{
printf("%d ", *(arr2[i]));
}
return 0;
}
指针数组和整型数组的区别:
这些只是指针的初级阶段,要想成为“江大指针王”,还需努力!