文章目录
1、什么是指针?
C语言当中最能体现C语言特色的应该就是指针的存在了。
在学习C语言之前,就听很多的编程学习者提到指针奇妙之处,有人说其晦涩难懂,也有人说不难懂。
说指针之前,先明确一下内存当中的概念:
以32位机器为例,它可以产生2^32个种电信号,每一个电信号转化为数字信号后为一个字节,那么2的32次方这么多的字节单位,就引出了地址的概念,每一个字节都有自己的地址,地址是一个16进制的数字,指针指向的其实就是地址。
把握住以下两点,指针的概念应该就很容易懂了:
1、指针是内存当中最小的单元编号,就是地址。
2、平常所说的指针,指的是指针变量,指针变量是一个用来存放内存地址的变量
那么,可以明确一点就是:指针就是地址,地址就是指针,且指针作为一个变量,它在内存中也占有自己的一块空间。
那我们就可以这样理解:
指针指向的就是这样的一个个字节空间。
1.1 指针变量
前面说了指针就是一个变量,并且在内存角度上考虑,指针存放的就是一个个字节的地址。于是可以明确指着就是一个存放地址的变量。
那么我们就可以通过之前说的 & 操作符取到一个变量的地址,进而将其存放到一个指针变量当中。
int main()
{
int a = 10; //定义一个整形变量a并初始化为10(4个字节)
int* pa = &a;//定义整形指针变量pa,其存放的是a的地址
return 0;
}
int形变量在内存当中占4的字节大小的空间,我们说每个字节都是一个地址,那么pa指向的是4个地址当中的哪一个地址呢?
指针指向的地方并不是随便指的哈,指针指向的都是第一个字节的地址。
1.2 指针大小
之前说了,32位机器上可以产生2^32次方个字节空间也就是2的32次方个地址。
2^32字节换算过来就是4GB的大小,要明确的是,指针在内存当中的大小和指针所指向的数据类型是无关的,指针是用来存放地址的变量,那么它在内存当中所占空间的大小只有两种情况:32位机器上,指针的大小就是4个字节;64位机器上,就是8个字节。
2、指针类型
变量都是有类型的,如int float等。
那么指针作为一个存放地址的变量,它也应该有类型才对。
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
可以发现,指针类型就是type+* ,这里的type和指针所指向数据的类型有关。
与其说是有关,不如说是一致,因为我们希望:
char *的类型的指针就指向char类型的数据
int *的类型的指针就指向int类型的数据
以此类推,那么指针类型的意义是什么呢?
2.1 指针±整数
先看一段程序
int main()
{
int a = 10;
int* pa = &a;
char* paa = (char*)&a; //a是int类型,这里需要强制类型转换成char*,否则会报警告
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", paa);
printf("%p\n", paa + 1);
return 0;
}
看一下运行结果:
打开监视窗口观察一下:
可以发现,pa和paa以及&a都是一样的,不难理解,因为pa和paa都指向的是a的第一个字节的位置,所以他们的地址都相同。
但pa+1和paa+1却不相同,为什么呢?明明都指向的是一个对象a,怎么+1之后就不同了呢?
这里就涉及到了指针类型的问题了,我们知道int类型在内存当中占据的是4个字节,pa+1之后可以发现取到的是a的地址+4个字节之后的地址;而char类型在内存当中占据的是1个字节,paa+1之后可以发现取到的是a的地址+1之后的地址。于是我们可以得出结论:
指针类型决定了指针±整数这个整数实际的跨步有多大(跨过多少个字节)。
2.2 指针解引用
再看程序:
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0;
*pi = 0;
return 0;
我们打开调试窗口一步一步观察内存变化:
1、变量全部创建完毕:
2、指针对内存的修改:
可以发现在pc=0这条语句执行完后,n的第一个字节位置处的数据被修改成了00,这是因为pc是char类型的指针,通过它的解引用只能访问一个字节的空间。
类似的,执行完*pi=0之后,n的4个位置处全部被改为0了,这是因为pi是int类型指针,通过它的解引用可以访问四个字节的空间。
指针类型决定了通过他的解引用可以操作多大的字节空间。
3、野指针
所谓野指针,就是其指向位置,对他的一系列操作很危险并且不合法。
3.1 野指针成因
1、指针未初始化
int main()
{
int* p; //未初始化,其指向未知,默认为随机值
*p = 20;//非法,p的指向位置,直接解引用很危险
return 0;
}
2、指针越界访问
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、指针指向的空间被释放(动态内存管理再研究)
3.2 如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放,及时置NULL
- 避免返回局部变量的地址
- 指针使用之前检查有效性
4、指针运算
4.1 指针±整数
在2.1当中说啦,不赘述了。
4.2 指针 - 指针
这个操作有一个前提:两个指针必须要指向同一块空间(比如指向同一个数组)
指针 - 指针得到的是两个指针之间元素的个数
以此也可以有一个新的求字符串长度的方法:
int my_strlen(char* string)
{
char* start = string;
char* end = string;
while (*end != '\0')
{
end++;
}
return end - start;
}
int main()
{
char string[] = "Hello world!";
printf("%d\n", my_strlen(string));
return 0;
}
当用低地址的指针减去高地址的指针时,得到的是元素个数的负值
4.3 指针的关系运算
比较两个指针的大小(高低)
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--) {
*vp = 0; }
但实际用到的很少,也不怎么这样写,了解即可,因为标准不一定保证这样可行:
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与
指向第一个元素之前的那个内存位置的指针进行比较。
什么意思,就是说C语言保证整个数组后面的第一个指针有效,不保证整个数组前面的第一个地址有效。
5、指针与数组
指针与数组的关系十分密切,之前说了数组名就是指针,是首元素的地址(两种情况除外)
那么学习了指针±整数的操作之后,就可以通过指针±整数的操作来访问数组当中的元素而不需要通过下标运算符[]来访问数组了:
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; }
实际上,通过指针±整数来访问数组更接近机器语言,计算机在处理arr[1]、arr[2]这样的表达式时,也是将其转化为指针来操作的。
6、二级指针
指针变量作为一个变量,它也有属于自己的地址,那么指针的地址存在哪呢?
答案是指针的地址也是存在指针里的,存放一个指针变量的地址的指针成为二级指针。
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
*ppa取到的是pa,*pa取到的是a,那么通过ppa访问a,只需要对ppa解引用2次即可,即**ppa。
举个例子:**ppa=30等同于 *pa=30等同于a=30
7、指针数组
看到这个标题,首先向一个问题,指针数组是指针还是数组?
答案是它是数组,是一个存放指针的数组
int main()
{
int a = 10;
int b = 20;
int c = 30;
int d = 40;
int e = 50;
int* arr3[5] = { &a,&b,&c,&d,&e }; //指针数组
return 0;
}
arr3里储存的元素全部都是指针。
那么来看一个变式:
int main()
{
int a[] = { 1,2,3,4 };
int b[] = { 2,3,4,5 };
int c[] = { 3,4,5,6 };
int* arr[3] = { a,b,c };
return 0;
}
对于arr怎么理解?
首先arr是一个指针数组!
为什么?因为数组名是数组首元素的地址,是个指针,而arr中存储的是三个数组的数组名,就是存储的三个指针。
arr[0]表示a,而a是a数组当中首元素的地址,arr[0][1]表示a当中的2,即*(arr[0]+1)。