一、什么是指针
-
指针是一种特殊的***数据类型***,使用它可以定义***指针变量***
-
指针变量存储的是***整型数据***,代表了所指向的内存地址编号,通过这个地址编号可以访问地址对应的内存数据。
-
指针也是一种变量,也需要内存存储,指针也有地址
-
知乎上有一篇大佬的分享非常有助于大家对于指针的了解
知乎大佬高赞描述指针
二、为什么需要指针
- 变量是为了更好的表示数据,那么指针就是更方便的传递数据,指针大部分时间占用更小内存空间。由于指针变量存储的是地址,地址中的数据无需被拷贝赋值,直接利用指针找到数据所在地址即可使用。
- 由于函数之间传参是值传递(内存拷贝),对于字节较多的变量值传递效率较低,如果传递的是变量地址只需要传递 4(32位)\8(64位)字节,提高传参效率
- 函数之间相互独立,有时候需要共享一些变量。命名空间相互独立,但是地址空间只有一个。
- 堆内存无法取名字,它不像 data\bss\stack 内存段可以让变量名和内存建立联系, 只能使用指针记录堆内存的地址,以此来使用堆内存
以下代码可以看到,函数之间共享了变量
#include <stdio.h>
void func(int *p)
{
printf("func:%d\n",*p);
*p = 100;
printf("func:%d\n",*p);
}
int main(int argc,const char* argv[])
{
int num = 0;
printf("main:%d\n",num);
func(&num);
printf("main:%d\n",num);
}
三、如何使用指针
-
指针变量定义: 类型* 变量名_p;
1、指针变量与普通变量用法区别很大,建议取名最后加p
2、指针变量存储的永远是整型,前面的类型名规定了指针变量可以访问地址的字节数
3、一个*只能定义一个指针变量
int* p1,p2,p3; //只有p1是指针
4、指针变量与普通变量初始化值一样是随机的,一般初始化为NULL
-
赋值: 变量名_p = 地址; //必须是有权限且有意义的地址
指向栈内存: int* p = & num;
指向堆内存: int* p = malloc(4);
-
解引用: *p <==> num;
通过指针变量记录内存编号去访问内存,该过程可能产生段错误,原因是赋值指针变量时存储了一个非法的内存变量及指针变量的值。num是该内存编号下内存中的值。注意:解引用访问的字节数取决于定义指针变量时的类型。
练习1:实现一个变量的交换函数,调用它对一个数组排序
#include <stdio.h>
void func(int* p1,int* p2)
{
int t = NULL;
t = *p1;
*p1 = *p2;
*p2 = t;
}
int main(int argc,const char* argv[])
{
int arr[10] = {12,3,45,534,5,6,7,54,13,89};
for(int i=0; i<9; i++)
{
for(int j=i+1; j<10; j++)
{
if(arr[j] < arr[i])
{
func(&arr[j],&arr[i]);
}
}
}
for(int i=0; i<10; i++)
{
printf("%d\n",arr[i]);
}
}
练习2:实现一个函数,计算两个整数的最大公约数和最小公倍数,return返回最大公约数,最小公倍数用指针处理。
#include <stdio.h>
int func(int n1,int n2,int* p )
{
for(int i=n1; i<=n1*n2; i++)
{
if(0 == i%n2 && 0 == i%n1)
{
*p = i;
}
}
for(int j=n1; j>=1; j--)
{
if(0 == n2%j && 0 == n1%j)
{
return j;
}
}
}
int main(int argc,const char* argv[])
{
int num1 = 0,num2 = 0;
int max = 0,min = 0;
scanf("%d %d",&num1,&num2);
max = func(num1,num2,&min);
printf("最大公因数:%d\n最小公倍数:%d\n",max,min);
}
四、使用指针需要注意的问题
-
空指针:值为NULL的指针变量叫做空指针,如果进行解引用就会产生段错误
NULL会作为错误标志的一种,当一个函数的返回值是指针类型时,函数如果执行出错返回值就是NULL
-
如何避免空指针带来的段错误:
使用来历不明的指针前先做判断
1、从函数中获取的指针返回值,可能是空指针
2、当函数的参数是指针,别人传给你的就可能是空指针
if(NULL == p)
if(!p)
注意:NULL在绝大多数系统中是0,个别系统是1
-
野指针:指向不确定的内存空间
解引用野指针的后果:
1、一切正常
2、脏数据
3、段错误
野指针比空指针的危害更严重,因为野指针无法被判断出来,而且可能是隐藏性的错误短时间不暴露
-
所有的野指针都是程序员自己制造出来的,如何避免产生野指针:
1、定义指针变量时一定要初始化
2、函数不要返回栈内存的地址
3、指针指向的内存释放后,指针变量要及时置空
五、指针的运算
指针变量中存储的是整型,理论上整型数据可以使用的运算符它都可以使用,但是大多数都是无意义。
-
指针 + n 指针+指针类型字节数*n 前进了n个元素
-
指针 - n 指针-指针类型字节数*n 后退了n个元素
-
指针 - 指针 (指针-指针)/类型字节数 计算出两个指针之间间隔了多少个指针元素
-
注意:指针相减,指针类型必须一致
#include <stdio.h>
int main(int argc,const char* argv[])
{
int a = 1,b = 2;
int* p1 = &a;
int* p2 = &b;
printf("p1 = %p\np2 = %p\np1+1 = %p\np1-1 = %p\np2-p1 = %d ",p1,p2,p1+1,p1-1,p2-p1);
}
六、const 与指针
-
当我们为了提高传参效率而使用指针时,传参的效率提高了,但是变量被共享后有了被修改的风险,可以借助const保护指针所指向的内存。
const int* p; 保护指针所指向的内存不被修改 int const *p; 同上 int* const p; 保护指针变量不能修改 const int* const p; 指针变量和指针所指向的内存都不能修改 int const* const p; 同上
七、指针数组与数组指针
-
指针数组:是一个数组,由指针变量组成,该数组的成员是指针变量
int* arr[10];
-
数组指针:是一个指针,专门指向数组的指针
类型 (*arrp)[长度]
int (*arrp)[10];
八、数组名和指针
-
数组名可以看做一种特殊的指针,它是常量,不能修改他的值,
-
数组名与数组的内存之间是映射关系,而指针变量和数组内存间是指向关系
-
数组名是没有自己的存储空间的
数组名 == &数组名 == &数组名[0]
-
如果指针变量中存储的是数组的首地址,指针可以当做数组使用,数组名也可以当做指针来使用,数组作为函数的参数是变成了指针,所以长度丢失
数组名[i] == *(数组名+i)
*(p+i) == p[i]
九、二级指针
-
二级指针是指向指针的指针,里面存储的是指针变量的地址
-
定义: 类型** 变量名_pp;
-
赋值: 变量名_p = &指针变量; // 存储指针变量的地址
-
解引用: *变量名_p = 指针变量; // 指针变量的值即指针变量指向内存的地址
**变量名_p <=> *指针变量 <=> 普通变量
十、函数指针
函数名就是该函数在代码中的内存首地址
调用函数其实就是跳转到该函数所在代码段中去执行二进制指令
函数指针就是用来专门指向函数的指针,里面存储的是函数的首地址,对函数指针解引用就可以执行函数
函数指针可以当做函数使用
-
定义函数指针:返回值 (*指针变量名)(类型1,类型2,…);
-
赋值: 指针变量名 = 函数名;
-
调用函数:指针变量名(实参);
通过函数指针,把函数当做参数一样传递给另一个函数使用,这就是回调
学无止境,无限进步!