C语言万字精讲指针

前言

很多初学者学C语言都会卡在指针:分不清数组指针和指针数组、搞不懂 sizeof 和 strlen 、二维数组传参一头雾水、函数指针看不懂、笔试题一做全错。
指针本质只有一句话:指针 = 内存单元编号 = 地址。本文结合全套课件知识点,由浅入深逐层拆解,配套可运行代码+高频笔试题,看完彻底告别指针困惑。

一、指针底层基础:内存与地址

1.1内存编址原理

计算机内存被切分成一个个1字节内存单元,每个单元分配唯一编号,这个编号就是地址(也就是指针)。

32位系统:32根地址总线,地址占4字节;

64位系统:地址占8字节。所有指针变量大小只由系统位数决定,和指向类型无关。

1.2两大核心操作符

1.取地址操作符(&)

&a就可以取出地址,拿的是字节较小的那个地址。int*pa=&a(pa可以用来存放地址(指针),所以pa叫指针变量)。这个int*pa叫做整型指针变量。*是在说明指针变量,int是告诉我们pa指向的变量是int类型。pa类型是int*但是理解时int和*都有意义。

2.取地址操作符(间接访问操作符)

*pa:*——解引用操作符,通过pa里存的地址找到a

1.3指针类型的意义

1.指针的解引用一次访问几个字节:比如int*访问4个字节、char*访问1个字节、double*访问8个字节、short*访问2个字节。

2.

由上两图可知,p所存放的地址往后跳了四个字节。这是因为p的类型是int*,p认为自己指向的是int类型数据,pi+1就应该跳过1个int类型的数据也就是4个字节。由此可知,指针类型决定了向前或向后跳过的距离有多大。

3.void*指针(理解为无具体类型的指针)

不能直接进行解引用和+或-整数的操作。一般就是使用在函数参数的部分,用来接受不同类型数据的地址

二、指针进阶:const修饰指针、野指针与传址调用

2.1const修饰指针

(1)const修饰变量

比如const int n = 10 //常变量,是变量但是具有常属性(不变的属性)。那么const修饰指针时也有异曲同工之妙。

(2)const修饰指针

1)const放在*的左边 const int * p = &n---------------在这种情况下,不让改*p但是可以通过修改p=&n这种方式修改n的值。这种写法限制的是指针变量指向的内容。

2)const放在*的右边 int * const p = &n----------------在这种情况下,不让修改p=&n但是可以通过修改*p这种方式改变n的大小。这种写法限制的是p这个指针变量。

3)const放在*的左右两边:p和*p指向内容都不能修改了。

2.2野指针成因与规避

1. 野指针:它是指针但是指向的位置是不可知的。

2.成因:(1)指针未初始化

(2)指针越界访问:指针指向的范围超出arr(数组)的范围时,p就是野指针

(3)指针指向的空间释放。比如说当n是局部变量时,如果在主函数中再用p来保存n的地址这时p就是野指针。

3.规避:(1)指针初始化:如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL。int *p = NULL

(2)小心指针越界

(3)避免返回局部变量地址

2.3传值调用和传址调用

1.传值:函数创建临时副本,无法修改外部实参;

2.传址:传递变量地址,函数内可直接修改外部变量;

交换数字实例:

三、数组与指针核心规则

3.1数组名两大例外

1.sizeof(数组名):数组名代表整个数组,计算数组总字节;

2.&数组名:取出整个数组的地址,类型为数组指针;

3。其余场景:数组名=数组首元素地址。

区分arr与&arr:int arr[10]={0}

arr + 1;//跳过1个int,+4字节

&arr + 1;//跳过整个数组,+40字节

3.2sizeof和strlen

特性sizeofstrlen
本质C语言操作符string.h函数
计算内容占用内存总字节\0前有效字节个数
参数类型、变量、数组、指针均可仅接收字符指针,必须有\0终止
风险无越界问题无\0会持续越界查找,返回随机值

3.3字符数组和字符串常量指针

char arr[] = "abcdef";//栈上数组,可修改元素

char* p = “abcdef”;//指向常量字符串,只读,“p”修改会崩溃,首字符a的地址放到了p里

常量字符串会存入只读数据段,多个同字面量指针指向同一块内存;字符数组每次开辟独立栈空间。

3.4指针数组和数组指针

1.指针数组:int*p[10]:数组,每个元素是int*指针;

2.数组指针:int(*p)[10]:指针,专门存放数组指针。

3.5二位数组与数组指针

二维数组的本质:存放一维数组的数组,数组名arr是第一行一维数组的地址,类型为int(*)[列数]。

访问元素:*(*(p+i)+j) == p[i][j]

四、二级指针、指针运算与经典笔试题

4.1二级指针(char**或int**)

存储一级指针变量的地址,两大用途:

(1)函数内修饰一级指针本身;

(2)接收指针数组数组名;

int a = 10;

int* p = &a;

int**pp = &p;

**pp = 100;//等价a = 100

*pp = NULL;//等价p = NULL

4.2三种指针运算

1.指针+或-整数:按指向类型步长偏移;

2.指针-指针:仅同一块连续内存有效,返回中间元素个数;

3.指针关系运算:< > == 比较地址高低,仅数组内合法。

4.3笔试例题解析(X86 32位,指针4字节)

例1

例2

五、函数指针、回调函数、qsort快速排序

5.1函数指针基础

函数名=函数地址,&函数名等价函数名;定义格式:返回类型(*指针名)(参数列表)

例如:int Add(int x, int y) { return x+y; }
int (*pf)(int, int) = Add;
printf("%d", pf(2,3)); // 等价(*pf)(2,3)

5.2回调函数定义

将函数指针作为参数传给另一个函数,由对方在内部调用,被调用的函数即为回调函数。

5.3qsort快速排序(回调函数核心应用)

函数原型:void qsort(void*base,//数组首地址

size_t num,//base指向的数组的元素个数

size_t size,//每个元素的大小,单位是字节

int(*compare)(const void*p1,const void* p2)//函数指针,指向的函数可以比较数组中的两个元素

);//p1指向的元素比p2指向的元素小,返回小于0的数字;一样大返回0;p1比p2大返回大于0的数字。

例子:测试qsort来排序整型数组

六、指针避坑总结

1. 数组传参退化为指针,函数内部无法用 sizeof 求数组长度;

2. strlen 参数必须带 \0 ,字符数组无结束符会越界;

3. 常量字符串 char* p = "xxx" 只读,禁止解引用修改;

4. 区分 int (*p)[N] 数组指针和 int* p[N] 指针数组;

5. 函数返回禁止返回局部栈变量地址;

6. void* 不能直接解引用、不能±整数;

7. &数组名 是整个数组地址,+1跳过全部数组元素;

8. 二级指针只能接收一级指针的地址,不能直接赋值普通变量地址。