C 指针学习笔记与理解
-
什么是指针?
学习指针之前,先要理解什么是指针。
所有的数据都要储存在内存中,需要用时,再到内存中取出。在取出数据之前,如何确保计算机能够找到数据在内存块中的对应的位置?
如果将内存块分成一块一块的空间,并且对每一块的内存给上编号,这样每一个数据都有对应的地址了,计算机就可以根据数据的地址准确的寻找到这个数据,这个内存的地址就是指针,而用于储存这个指针的变量就是指针变量,通过解引用操作即可取出地址中的数据
如何定义一个指针变量
int main()
{
int a = 1;
int* a_p = &a; // 在类型后加*可定义指针变量,而通过&可取出数据的地址
printf("a = %d\n", a);
printf("*a_p = %d\n", *a_p); // 通过*解引用操作,取出地址的数据
return 0;
}
运行结果
a = 1
*a_p = 1
获取数据除了可以直接使用变量,还可以通过指针变量进行操作
只指针变量的大小在不同的系统上也有不同的体现,常见的32位系统上,指针变量大小为4byte,64位系统上指针变量大小则为8byte;原因是32位系统,通过32根地址总线去处理数据,即,可以产生2^32个地址编号,也就是说有4byte的数据长度,而64位系统的数据宽度就有8byte。因此,指针变量的大小与系统息息相关。
2. 指针在内存中如何表示?
指针在内存中以16进制进行展示,实际上还是2进制的。
还是以上面的代码为例,可以看到a的地址编号事宜十六进制的形式进行表示的。虽然本质上是二进制,但若是以二进制展示,则会显得太长了
3. 指针的类型
指针与常规的数据一样同样存在数据类型,但是指针不管是什么类型的指针,它的大小都是不变的,指针大小只和系统有关;但是不同类型的指针决定他能操作的空间大小不同
#include <stdio.h>
#include <math.h>
int main()
{
int a = pow(2, 9);
int b = pow(2, 9);
char* pa = &a;
int* pb = &b;
*pa = 0;
*pb = 0;
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
512
0
为什么会产生这样的结果?
原因很简单,虽然char* 和 int* 类型的指针大小虽然都相同,但是它能够操作的空间不同,什么意思?用下面这张图来说明一下
整型数据的大小位4byte,也就是说占据4个内存块儿(内存的最小单位为byte), 那么一个指针如何管理int*型的指针如何操作这个数据,答案肯定是需要占用4个地址的编号,即1-4,只有同时拥有这4个编号,才能对这4个内存块儿进行管理,就相当于不属于自己的房子,跑去住可能会被别人锤一样的道理。
知道了指针的地址操作宽度不同,那么理解为什么这个结果就不难了,int类型的指针可以操作1-4的内存块儿,而char的指针便只能操作5编号的内存块。
那么从二进制进行说明
00000000 00000000 00000010 00000000 //512
pb = 0 时,这四个内存块儿0的二进制分别塞进四个内存块儿中得到了
00000000 00000000 00000000 00000000
再来看看pa
pa = 0时,
00000000 00000000 00000000 00000000原本需要四个内存块才能装得下
现在它只能放一个内存块儿,即它从低地址开始塞进去,高于第8位(从右数)以后的数据就全部被截断了
00000000 00000000 00000010 00000000,这8位塞进来相当于没变
那么将pa修改成1,最后发现结果变成513了
00000000 00000000 00000010 00000001,原因就是这样了
在编号上又怎么体现,我们可以看地址+1得到的结果
可以看到,pa+1以后大于pa 1, 而pb+1则大于pb 4,刚好是char类型的大小,和int类型的大小,花了一大堆说明这个原因,则是在不同的应用场景中,使用合理就无法达到预期的效果
而关于指针的定义则是数据类型加上*
如:int*, char*, float*
当然并不是说int数据就要用int*,具体看引用场景的
4. 指针的使用注意问题
关于指针的定义
类型名+ * +指针变量名
如int* a ;
由于指针变量储存的是变量的地址,通过*即可操作内存块中的变量
printf("%d", *pa);
关于指针的使用时,需要注意野指针
什么是野指针?从名字来观察不难理解,野指针即是不可控的指针,这个不可控指的是没有明确的地址。
产生野指针的以下几个原因
指针变量未初始化
int main()
{
int* b;
// 由于指针变量定义没有初始化,所以这时候内存给指针变量随机分配了一个值
printf("%p\n", b);
return 0;
}
结果为报错
C4700 使用了未初始化的局部变量
解决办法
int <stdio.h>
int main()
{
int a = 1;
int* p = &a; // 首先给指针初始化
// 如果确实需要用到指针,但是暂时又不给指针变量的明确地址时,就可以用到 NULL
int* p1 = NULL;
return 0;
}
指针越界
使用指针超过了它的界定范围进行操作,就会造成指针越界成为野指针,上代码
#include <stdio.h>
int main()
{
char a = 1;
char* pa = &a;
*(pa + 1) = 3;
printf("%p\n", pa); // 指针指向a的地址
printf("%p\n", pa + 1); // 但是pa + 1后,就越过了a的地址,指向了另一个未知的地址
return 0;
}
当程序跑起来就出现了错误
虽然pa+1的地址时可预测在哪里的,但是这一块空间并没有进行分配,相当于是一块儿没有开发权的土地,使用是不合理的。
另外在使用数组的时候,也要切不能让指针越界
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* pa = a;
int i = 0;
for (i = 0; i < 6; i++)
{
printf("%d\n", *(pa + i)); // 当pa + 5时,指针变越过了数组的范围
}
// 展示数组的地址
// 五个元素的地址假设为
// 0, 4, 8, 12, 16
// a的首元素地址为0,当pa+5时,就相当于0 + 4 * 5 = 20, 而20的空间时没有开发权的,所以越界了,因此在使用指针时
// 要注意指针不能越界
return 0;
}
使用释放的变量地址
这个理解起来也并不是很难,还是老样子,上代码
#include <stdio.h>
int* fun()
{
int x = 4;
printf("%p\n", &x);
return &x;
}
int main()
{
int* a = NULL;
a = fun();
printf("%d\n", *a);
printf("%p\n", a);
return 0;
}
虽然运行结果没有报错,但是x却在fun()函数结束后,生命周期结束,内存也重新处于未分配的状态,这块空间除了a能够指向它,却并不存在任何一个变量中
-
一级指针与二级指针
一级指针
一级指针很好理解,上述例子的指针都是一级指针,理解为一级指针是指向某个变量,这个变量是除指针变量外的变量,即这个指针变量指向的地址直接指向另一个变量的内存中,如
int a = 0;
int* pa = &a;
pa就是直接指向a的内存
二级指针
二级指针则是指向指针的指针,什么意思?
int a;
int* pa = &a;
int** ppa = &pa;
printf("%p\n", a);
printf("%p\n", pa);
pa 指向的a,而ppa指向的则是pa的地址,a的地址为1, 那么pa = 1,它就可以通过地址编号1,找到a所在的地方,而ppa 则是储存的是pa的地址,ppa通过pa的地址找到pa,再在pa中找到a的地址。
运行结果
004FFE44
004FFE38
那么如何通过二级指针来使用操作a呢?使用两个**ppa就可以,理解为第一个*通过地址004FFE38,找到pa,第二个 *再通过地址 004FFE44找到a
int main()
{
int a = 1;
int* pa = &a;
int** ppa = &pa;
printf("%d\n", *pa);
printf("%d\n", **ppa);
return 0;
}
运行结果
1
1
当然也还有三级指针,n级指针,但是一般来说二级指针已经是足够了。
数组指针
数组指针是什么?数组指针是一个指针,它指向一个数组,关于它的定义和使用,先上一段代码
int main()
{
int arr[4] = {1, 2, 3, 4};
int (*p_arr)[4] = &arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d\n", *(*p_arr + i));
}
return 0;
}
运行结果
1
2
3
4
int (p_arr)[4] = &arr; 在这一段代码与普通的指针定义大体上是相同的,由于[]的优先级高于 * 所以在进行定义的时候必须要加上() 确保在[] 之前,而数组指针类型就是 int ( * )[],那么为什么不是int ( * )[4] p_arr 这样呢?在数组指针定义是规定就是类型 + ( * 指针名)[]。
当然,若觉得不习惯也可以进行重命名
typedef int (* int_arr_p)[]; // typedef 定义数组指针重新的类型名也必须在()中
int a[] = {1, 2, 3, 4};
int_arr_p arrp = &a; // 这样就顺眼许多了
另外在进行解引用之前,先说明几个点,数组名是首元素的地址,地址+1时不是横跨整个数组,而是到下一个元素的地址
int main()
{
int arr[] = { 1, 2, 3, 4 };
int* arr_p = &arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(arr_p + i));
printf("%p\n", arr_p + i);
}
return 0;
}
运行结果
1 00FFE00
2 00FFE04
3 00FFE08
4 00FFE0C
如果arr_p为整个数组的话,+1就应该为 00FFE10,因此,int*类型只是定义为整数的数组,所以+1就相当于把每个元素独立分开。
既然数组名为首元素地址,指针也是首元素地址,那么这种方式的指针是否能够像数组名一样使用[]而不使用 * 取出元素
int main()
{
int arr[] = {1, 2, 3, 4};
int* arr_p = &arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", arr_p[i]);
}
return 0;
}
运行结果
逆向过来,把arr解引用是否又可行?
int main()
{
int arr[] = {1, 2, 3, 4};
int* arr_p = &arr;
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d ", *(arr + i));
}
return 0;
}
运行结果
结果也是可行的,因此可以得出一个结论,数组名本身就是一个指向首元素地址的元素类型的指针,它并不是横跨整个数组的,除开在&操作和sizeof求大小时表示的是整个数组
所以要想真正用一个指针表示整个数组,使用数组指针是真正能够体现的
int main()
{
int arr[] = {1, 2, 3, 4};
int(*arr_p)[4] = &arr;
printf("%p\n", arr_p);
printf("%p\n", arr_p + 1);
printf("%p\n", arr);
printf("%p\n", arr + 1);
return 0;
}
运行结果
arr_p + 1 比arr 大16,恰好是一整个数组的到小。
讲完了数组指针和普通的指针表示数组的区别,再说说解引用为什么是
*(*arr_p)
// 将这段代码分成两部分来理解
// *arr_p相当于int* arr_pp = &arr中的arr_pp
// 将*(*arr_p) ==> *arr_pp
验证
int main()
{
int arr[] = {1, 2, 3, 4};
int(*arr_p)[4] = &arr;
printf("%d\n", (*arr_p)[1]);
printf("%d\n", *((*arr_p) + 1)); // ==> *(arr_pp + 1)
return 0;
}
运行结果
2
2
指针数组
指针数组为一个数组,用于存放指针变量
int main()
{
int a, b, c, d;
int* a1 = &a;
int* b1 = &b;
int* c1 = &c;
int* d1 = &d;
int* arr[4] = { a1, b1, c1, d1 };
int i;
for (i = 0; i < 4; i++)
{
*(arr[i]) = i;
printf("%d\n", *(*(arr + i)));
printf("%d\n", *(arr[i])); // 两种使用方式都可以用
}
return 0;
}
运行结果
0
0
1
1
2
2
3
3
指针数组与普通的数组并无特别大的区别,主要在于储存的元素为指针
函数指针
函数与数组一样有地址
include <stdio.h>
int fon()
{
return 1;
}
int main()
{
printf("%p\n", fon);
return 0;
}
结果展示
既然函数拥有地址,函数也可以使用指针来调用函数
指向函数的指针就是函数指针了
#include <stdio.h>
int fon()
{
printf("1\n");
return 1;
}
int main()
{
int(*fn)() = &fon; // 函数指针的定义方法与数组指针并无特别大区别
// 只是[]换成(), 前边的类型是函数的返回类型
// ()中也可以写出类型
// int (*fn)(void) = &fon;比如这样
fn();
(*fn)(); // 两种方式均可以调用函数,但是需要注意()的优先级是高于*的
return 0;
}
函数与数组相似,函数名也是地址,所以实际上函数也通过*进行调用,
#include <stdio.h>
int fon()
{
printf("1\n");
return 1;
}
int main()
{
(*fon)();
return 0;
}
运行结果
1
函数指针数组
像指针数组一样,函数也可以存放在数组中,用于存放函数指针的数组就是函数指针数组。
函数指针数组的定义方法
如何去定义一个函数指针数组的
int main()
{
int (*fun)()[3]; // 或许我们会认为这样,但其实这样是错的
int (*fun[3])(); // 正确定义方法其实是这样的
return 0;
}
定义方法为 函数返回类型 + ( * 函数指针数组名[])()
函数指针数组必须是相同返回类型的函数才能储存。
int one()
{
printf("1\n");
return 1;
}
int two()
{
printf("2\n");
return 2;
}
int three()
{
printf("3\n");
return 3;
}
int main()
{
int (*funs[3])() = {&one, &two, &three};
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d\n", (funs[i])());
}
}
运算结果
1
2
3
在需要调用多个相同参数的函数时,使用函数指针数组代码的可维护性是很好的。
比如在计算器程序中
double add(double x, double y)
{
printf("x + y = ");
return x + y;
}
double sub(double x, double y)
{
printf("x - y = ");
return x - y;
}
double mul(double x, double y)
{
printf("x * y = ");
return x * y;
}
double divs(double x, double y)
{
printf("x / y = ");
return x / y;
}
int main()
{
int input = 0;
int x, y;
double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};
do
{
printf("*****************************\n");
printf("**** 0. exit* 1. 加法********\n");
printf("**** 2. 减法* 3. 乘法********\n");
printf("*****4. 除法*****************\n");
printf("*****************************\n");
printf("请输入:");
scanf("%d", &input);
switch (input)
{
case 0:
break;
default:
printf("\n请输入数:");
scanf("%d%d", &x, &y);
printf("%lf\n", (funs[input - 1])(x, y)); // 此处只需一行代码即可调用需要的函数
}
} while (input);
return 0;
}
这就是经常说的转移表,转移表实际上就是一个函数指针数组,在相同返回类型,相同参数的函数调用时,为了提高代码的可维护性,使用函数指针数组减少代码数,不同的部分在函数内实现,使得代码不那么冗长,可维护性更好
double add(double x, double y)
{
return x + y;
}
double sub(double x, double y)
{
return x - y;
}
double mul(double x, double y)
{
return x * y;
}
double divs(double x, double y)
{
return x / y;
}
int main()
{
double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};
int input = 0;
int x, y;
do
{
printf("*****************************\n");
printf("**** 0. exit* 1. 加法********\n");
printf("**** 2. 减法* 3. 乘法********\n");
printf("*****4. 除法*****************\n");
printf("*****************************\n");
printf("请输入:");
scanf("%d", &input);
switch(input)
{
case 0:
break;
case 1:
printf("请输入两个数: ");
scanf("%d%d", &x, &y);
printf("\nx + y = %lf\n", (funs[input - 1])(x, y));
case 2:
printf("请输入两个数: ");
scanf("%d%d", &x, &y);
printf("\nx - y = %lf\n", (funs[input - 1])(x, y));
case 3:
printf("请输入两个数: ");
scanf("%d%d", &x, &y);
printf("\nx * y = %lf\n", (funs[input - 1])(x, y));
case 4:
printf("请输入两个数: ");
scanf("%d%d", &x, &y);
printf("\nx / y = %lf\n", (funs[input - 1])(x, y));
default:
printf("请输入正确的数\n");
}
}while(input)
return 0;
}
如果单纯这样去运行,使用的函数一旦数量多了,就会使得代码十分的冗长,可维护性十分的差。
指向函数指针数组的指针
身为一门套娃语言,指向函数指针数组的指针也是十分正常的,指向函数指针数组的指针,是一个指针,这个指针指向的是函数指针数组,与数组指针有异曲同工之处。
指向函数指针数组的指针的定义方法
double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};
double (*(*fp)[4])(double, double) = &funs;
( * fp)说明fp是一个指针,与[]结合,说明fp指向一个数组,最后一个 * 说明这个数组里面的元素是指针类型,最后面这个 * 与指针数组指针是类似的
int a, b, c;
int* arr1 = &a;
int* arr2 = &b;
int* arr3 = &c;
int* arr_p[3] = { arr1, arr2, arr3 };
int (*(*arr_pp)[3]) = &arr_p; // 比较两段代码发现是相似的
a = 1;
b = 2;
c = 3;
printf("%d\n", *(*(*arr_pp)));