一篇文章深入理解指针

目录

一、理解指针

1.1指针是什么

1.2指针的大小

1.3指针的基本操作

1.3.1定义

 1.3.2赋值

1.3.3解引用

1.4不同类型指针的区别

1.5野指针

1.5.1野指针的成因

1.5.2规避野指针

1.6字符指针

二、指针的运算

2.1指针+-整数

2.2指针-指针

2.3指针的关系比较

三、二级指针

四、指针和数组

4.1指针和数组的关系

4.2字符指针

4.3指针数组

4.4数组指针

五、数组参数传参or指针参数传参

5.1一维数组传参

5.2二维数组传参

六、指针和函数

6.1函数指针

6.2函数指针数组

6.3回调函数


一、理解指针

1.1指针是什么

        在32位机器上,共有32根地址线,通过给每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)来产生0、1信号,而这一串由32个01数字组成的序列,就可以代表运行内存中变量的地址。

        在32位机器中每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB ==
2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB)
4G的空间进行编址。

        而我们平时所说的指针,其实指的就是指针变量,也就是用于存放内存地址的变量,我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个
变量就是指针变量。

1.2指针的大小

        在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
        在64位机器上,有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

1.3指针的基本操作

1.3.1定义

        定义指针的方式是  类型+*+指针变量名

char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;

 1.3.2赋值

首先介绍一个操作符&(取地址符),它可以取出一个变量的地址,从而对指针进行赋值

int a=10;
int *p=&a;

我们也可以用同类型or不同类型+强制类型转换的方式对指针进行赋值

int a=10;
int *p=&a;
int *p1=p;
char *pc=(char *)p;

甚至也可以用01序列对指针进行赋值(用的较少)

//二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头
//十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头

int *p=0b00000000 00000000 00000000 00000001;
int *p1=0x0000CFC;

1.3.3解引用

可以通过  *+指针变量名  对指针变量进行解引用,解引用后代表指针所指向的变量。

例如以下两段代码是等效的:

int a=10;
int *p=&a;
*p=20;
b=*p;
int a=10;
int *p=&a;
a=20;
b=a;

1.4不同类型指针的区别

       根据上面的叙述,我们知道指针有许多类型。

        虽然不同的变量存的内容不同,但是它们的地址都是01序列,那么指针具有不同类型的意义在哪?

        指针类型可以决定指针的权限。

(1)不同类型的指针解引用时访问的字节数不同。

        例如:int *的指针解引用时访问4个字节的空间。

                   char*的指针解引用时访问8个字节的空间。

(2)不同类型的指针在进行+-运算时,所跳过的字节数不同。

        整形指针+1时跳过一个整形的大小,即4个字节。

        字符指针+1时跳过一个字符的大小,即1个字节。

1.5野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)


1.5.1野指针的成因

(1)指针未初始化

int *p;//局部变量指针未初始化,默认为随机值
*p = 20;

(2)指针越界访问

int arr[10] = {0};
int *p = arr;
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}

(3) 指针指向的空间释放

int *p=(int*)malloc(sizeof(int));
free(p);
*p=3;//p所指向的空间已经被释放,此时p就是野指针

1.5.2规避野指针

(1) 指针初始化
(2) 小心指针越界
(3)指针指向空间释放即使置NULL
(4)避免返回局部变量的地址

int* add(int x,int y)
{
    int z=x+y;
    return &z;//z的空间在add函数结束时已经被释放,返回的地址是未知的。
}


(5)指针使用之前检查有效性

        可以使用assert函数进行判断(头文件assert.h)

1.6字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* 。

一般如下使用:

char ch = 'w';
char *pc = &ch;

但还有一种使用方式:

const char* pstr = "hello world.";

这种写法很容易让人以为是把字符串 hello world. 放到字符指针 pstr 里了,但是本质是把字符串 hello world. 首字符的地址放到了pstr中

二、指针的运算

2.1指针+-整数

指针+n表示向后跳过n个该指针所指向的类型大小的空间。

指针-n表示向前跳过n个该指针所指向的类型大小的空间。

例如:整形指针+1时跳过一个整形的大小,即4个字节。

int arr[5];
int*p;
for (p = &arr[0]; p < &arr[5];)
{
*vp++ = 0;
}

2.2指针-指针

指针-指针所得值的绝对值表示两个指针之前元素的个数。

注意:用指针-指针时,前后两个指针的类型必须相同且指向同一块连续空间。

int my_strlen(char *s)
{
    char *p = s;
    while(*p != '\0' )
    p++;
    return p-s;
}

2.3指针的关系比较

int arr[5];
int*p;
for (p = &arr[0]; p < &arr[5];)
{
*vp++ = 0;
}

这段代码中,for循环的判断条件即运用了指针关系的比较。

当然,这段代码也可以作如下修改

int arr[5];
int*p;
for (p = &arr[4]; p >= &arr[0];)
{
*vp++ = 0;
}

实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

ps:标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
 

三、二级指针

指针变量也是变量,是变量就有地址。所以存放指针变量地址的指针就是二级指针。

int a=0;
int *p=&a;//一级指针
int *pp=&p;//二级指针
**pp = 30;
//等价于*p = 30;
//等价于a = 30;

四、指针和数组

4.1指针和数组的关系

先给结论:数组名就是数组首元素的地址,但是以下两种情况除外

int arr[10]=0;
//1、
sizeof(arr)//结果为40
//2、
&arr

再sizeof()和&(取地址操作符)之后的数组名表示整个数组

ps:如上&arr得到的是一个指向含有10个整形元素数组的指针,加1跳过40个字节

        但是&arr和arr的地址是一样的,只是指针类型不同(决定着它们俩的权限不同)

所以我们常用的arr[i]其实本质就是*(arr+i),这也是[ ]这个操作符的底层逻辑

4.2字符指针

让我们来看以下代码

int main()
{
char str1[] = "hello word.";
char str2[] = "hello word.";
const char *str3 = "hello word.";
const char *str4 = "hello word.";

if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");

return 0;
}

最终输出结果是

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存(也就是这几个指针变量所储存的地址是相同的)

但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存空间给str1和str2这两个数组。所以str1和str2不同,str3和str4不同。

4.3指针数组
 

我们已经知道,整形数组里存放的是整形,字符数组(包括字符串)存放的是字符。

那么以此类推,所谓指针数组,也就是存放指针的数组。

例如:

int* arr[5];

arr是一个数组,有五个元素,每个元素是一个整形指针。

而除了两种特殊情况下,数组名arr表示的是首元素的地址,又因为arr中的元素是一级指针,所以arr这里表示的是一个指向整形指针的二级指针。
 

4.4数组指针

数组指针也就是能够指向数组的指针,本质是指针。

那以下两种定义方式哪个才表示数组指针呢?

int *p1[10];
int (*p2)[10];

答案是int (*p)[10];
原因:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
注:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

数组指针应用举例:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p

五、数组参数传参or指针参数传参

5.1一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

对于一维数组来说,以上五种形参设计方法均可。(一维数组传参的本质就是传过去数组名所代表的首元素的地址,形参并不需要知道数组中有多少个元素)

5.2二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//不ok,未声明一行有多少个元素
{}
void test(int arr[][5])//ok
{}
void test(int *arr)//不ok,这个为一维数组传参
{}
void test(int* arr[5])//不ok,此处穿的形参是一个以int*为数组元素的指针
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//不ok,未声明一行有多少个元素,本质原因见下注
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}

总结:二维数组传参,函数形参的设计只能省略第一个[ ]的数字。
因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。

注:函数形参必须标明一行有多少个元素的本质原因:二维数组可以理解为以含有n个元素的一维数组为元素的数组而含有5个元素的一维数组和含有十个元素的一维数组明显是不同的,所以为了让函数明白数组中的元素到底是什么,必须声明一行有多少个元素

六、指针和函数

6.1函数指针

先看下面的代码

#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}

运行结果是

也就是说,函数名本身就是一个指针,对函数名进行取地址得到的还是它本身。

那如果想要储存这个指针,应该怎么储存呢?

下面pfun1和pfun2哪个有能力存放test函数的地址?

void test()
{
printf("hehe\n");
}
void (*pfun1)();
void *pfun2();

答案:pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
 

那函数指针如何使用typedef进行重命名or化简呢?

例如:

typedef void(*func)(int);
//要重命名的名称再*后面。

经过此次重命名后,func就与void(*)(int)是等效的了。

6.2函数指针数组

要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义?

int (*arr1[10])();
int *arr2[10]();
int (*)() arr3[10];

答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。


6.3回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
例如在qsort函数的使用中

int cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), cmp);
return 0;
}

我们所传入的用于比较大小的cmp就是回调函数。

个人理解,欢迎指正

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值