一.指针初阶
1.内存
内存是电脑中特别重要的存储器,计算机中程序的运行都是在内存中进行的。所以为了有效地使用内存,就把内存划分为一个个小的内存单元,每个内存单元的大小是一个字节。为了能够有效地访问内存中的每个单元,就给内存单元进行了编号,这些编号就被称为该内存单元的地址。
2.指针是什么
其实指针就是地址。内存中的每个小的内存单元都有着自己的地址,为了方便找到我们想要的数据,我们可以定义一个指针变量来存放数据的地址。通过这个地址,我们就能很好地找到我们想要的数据。就好比,你的好朋友想来你宿舍找你,他只有知道你宿舍的门牌号(地址),才能更快地找到你。
变量是在内存中创建的(在内存中分配空间),因为每个内存单元都有地址,所以变量也是有地址的。那怎么取出变量的地址呢?请看下方的代码。
如果我们用指针存放一个变量的地址,那么我们对指针进行解引用操作就可以修改这个变量的值了。
3.指针的大小
在32位的机器上,地址是32个0 or 1组成的二进制序列,则地址就得用4个字节的空间来存储,所以一个指针变量的大小为4个字节。 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
4.指针和指针类型
学习这个内容之前,我们先看一下下面的这个代码。
我们可以看到,不管是什么类型指针,它们的大小都是4个字节。那跟类型有什么关系?类型好像没什么意义?我们是不是可以自己创建一直通用类型的指针呢?其实这样是行不通的,这也说明类型是有意义的。
对比上面的四张图片,我们可以发现:当pa是整型指针的时候,对它解引用后它可以访问四个字节并将四个字节的数据改为了00;而当pa是字符指针的时候,对它解引用后它只能访问一个字节并将这个字节的数据改为了00。只是指针类型发生了变化,指针访问的权限大小就发生了变化。那么说明指针类型是有意义的。对整型指针进行解引用操作能访问4个字节,而对字符指针进行解引用操作只能访问1个字节。通过这个例子,我们就可以知道指针类型决定了指针解引用的权限。 那指针类型还有别的意义吗?其实还有,我们通过下面的代码来学习一下。
我们可以看到p和pc打印出来的地址是一样的,但是p+1和pc+1所打印出来的地址是不一样的。p+1的地址比p的地址多加了4,而pc+1的地址比pc的地址只多加了1。这是为什么呢?因为p是整型指针,整形指针+1跳过1个整型,也就是跳过4个字节;而pc是字符指针,字符指针+1跳过一个字符,也就是跳过1个字节。这也就是为什么p+1的地址比p的地址多加了4,而pc+1的地址比pc的地址只多加了1。那么指针类型的第二意义就是:指针类型决定了,指针走一步,能走多远。 那我们再通过对比下面的两个例子来加深对指针类型意义的了解!
第一个程序中的p是整型指针,所以p+i表示跳过4*i个字节,指向数组第i+1个元素,然后通过解引用操作访问4个字节将数组中的元素改成0到9;而第二个程序中的p是字符指针,所以p+i表示跳过i个字节,而整型数据有四个字节,那么arr[0]就改成了0x03020100,对应的十进制数字为50462976.通过这两个代码的对比,我们就更能了解指针类型的意义。
5.野指针
概念:指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
野指针成因
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
如何避免野指针问题
- 指针初始化
- 小心指针越界
- 指针指向的空间释放及时将指针置为空指针
- 指针使用之前检查有效性
6.指针运算
指针+-整数
指针-指针
使用指针-指针定义求字符串长度的函数
指针关系运算
在绝大部分的编译器上第二个代码是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
7.指针和数组
指针和数组有什么关系呢?其实数组名就是数组首元素的地址,我们可以定义一个指针变量来存放这个的地址。那我们来看看下面这段代码。
通过上面的代码,我们可以知道p+i表示数组第i+1个元素的地址,再通过解引用操作,我们就可以对该元素赋值了。
拓展:访问数组元素的多种方式
8.二级指针
9.指针数组
我们知道,整型数组是存放整型的数组,字符数组是存放字符的数组。那么依次类推,指针数组就是存放指针的数组。我们可以通过下面代码来简单地认识一下指针数组。
不过指针数组经常不这么来使用,关于指针数组的高级使用,将在指针进阶里面向大家介绍。
二.指针进阶
1.字符指针
字符指针就是指向字符的指针,它跟整型指针类似。字符指针可以存放一个字符的地址,通过解引用操作可以将该字符修改。
那字符指针能不能指向一个字符串常量呢?其实是可以的,但并不是将整个字符串存储在字符指针里,而是将字符串首字符的地址存储在字符指针里。那我们通过下面的一段代码来学习一下。
字符指针指向字符串常量其实和字符数组差不多,数组名代表首元素的地址,也有和字符指针一样的功能,区别就在于字符数组中存放的是一个个字符。还有一个更一个更重要的区别,就是字符指针指向的是字符串常量,字符串的内容是不可以改的,而字符数组里面的内容是可以改的。
笔试题
大家可以看一下下面的这一段代码,想一想它的输出结果会是什么呢?
运行这段代码,我们就可以发现这段代码的输出结果为str1 and str2 are not same和str3 and str4 are same。那为什么会出现这样的结果呢?输出结果不应该是str1 and str2 are same和str3 and str4 are same吗?不着急!看完下图的解释,你就会明白为什么会这样了。
2.指针数组
指针数组本质上是一个数组,数组中存放的是指针(地址)。
#include <stdio.h> int main() { //指针数组 int* arr[3];//存放整型指针的数组,数组元素有三个 return 0; }
利用指针数组模拟实现二维数组。
3.数组指针
数组指针是C语言中非常重要的概念。那数组指针是指针还是数组呢?很明显,数组指针是指针。比如:整型指针是指向整型的指针,字符指针是指向字符的指针。那么数组指针就是指向数组的指针。
上面的代码就是数组指针的定义了。那我们怎样才能理解数组指针呢?首先parr是变量名,parr首先跟*结合(注:[]的优先级比*的优先级高,所以要用括号将*和parr括起来),说明parr是指针变量,再跟[]结合,说明parr指向的是数组,数组有5个元素,每个元素是int。现在有个存放着5个double型指针的数组d,如果我想取出d的地址,那我该如何定义一个指针变量来存放d的地址呢。 很明显,存放d的地址的指针变量应该这样定义:double* (*pd)[5] = &d !!!那我再来给大家分析一下这个指针变量。首先pd先和*结合,说明pd是一个指针变量;再与[]结合,说明pd指向的是数组;数组有5个元素,每个元素是double*。
4.&数组名vs数组名
我们都已经知道,数组名表示的是数组首元素的地址,那&数组名又表示什么呢?&数组名和数组名的区别在哪里?现在我们通过下面的代码来学习一下。
注意:数组名通常表示数组首元素的地址,但是有两个例外。第一个是sizeof(数组名),数组名表示的是整个数组,计算的是整个数组的大小,单位是字节;第二个时&数组名,数组名表示整个数组,取出的是整个数组的地址。除此之外,数组名通通表示数组首元素的地址。
数组指针的使用
数组指针通常用于二维数组,并不常用于一位数组(因为写法太麻烦了)。
用数组指针遍历一维数组(麻烦,不建议使用)。
用数组指针遍历二维数组。
学完了指针数组和数组指针,我们来一起回顾并看看下面代码的意思。
t arr[5]; 整型数组,数组5个元素,每个元素是int
int *parr1[10]; 整型指针的数组,数组10个元素,每个元素是int*
int (*parr2)[10]; 整型数组的指针,该指针指向一个数组,数组10个元素,每个元素是int
int (*parr3[10])[5]; parr3是一个存放数组指针的数组,该数组能够存放10个数组指针,每个 数组指针指向一个数组,数组5个元素,每个元素是int
注意:分析这些代码的意思有两个小技巧:第一,[]的优先级比*的优先级要高,所以变量名优先和[]结合;第二,将数组名和[]去掉,剩下的就是数组元素的类型。比如:将int (*parr3[10])[5]中的parr3[10]去掉得到的就是int (*)[5]--数组指针。
5.数组参数、指针参数
在写代码的时候,我们难免要把数组和指针传给函数,那么函数的参数应该如何去设计呢?接下来,我们就来学习一下数组参数和指针参数。
一维数组传参
二维数组传参
注意:二维数组传参,函数形参的设计只能省略第一个[ ]的数字。因为对于一个二维数组,可以不知道有多少行,但是必须知道有多少列。还有就是,二维数组名作为实参传过去的是数组指针,函数形参的设计不能是一级指针或者二级指针,形参和实参必须是对应的。
一级指针传参
一级指针传参相对来说比较简单,实参一级指针传过去,函数形参一级指针接收就好了。不过要注意的是,形参和实参的指针类型一定要对应起来。
二级指针传参
函数的形参设计为二级指针,那实参也必须传二级指针过来。二级指针有哪些呢?如下图所示:
6.函数指针
从名字上来看,我们很容易就知道函数指针是指向函数的指针,是存放函数地址的指针。先通过一个简单的程序来了解一下函数地址。
注意:函数名和&函数名的意义是一样的!!!
如果我们想要存储一个函数的地址,那我们怎么去定义一个函数指针去存放这个地址呢?定义一个函数指针变量需要注意那些问题呢?看下方的代码和解释,它会告诉你答案。
现在我们已经知道如何去定义一个函数指针变量了,那我们怎么通过这个函数指针变量去调用这个函数呢?同样,通过下方代码告诉你答案。
因为&函数名和函数名是等价的,所以上面的函数指针的定义也可以写成这样int (*pf) (int, int) =Add。那么也说明pf等价于Add。所以上面的代码就可以改写为:
对面这两段代码,也可以说明(*pf)和pf是完全等价的。其实这样说明pf前面的*是没有任何意义的,不管pf前面的*有多少的都是一样的。 前面的*只是为了我们能够更好地理解。
代码分析(这个比较困难,看不懂的话可以多看几遍)
7.函数指针数组
之前我们就学习过整型指针数组。整型指针数组是存放整型指针的数组,比如int* arr[5]。那函数指针数组就是存放函数指针的数组。那函数指针数组该怎么定义呢?我们现在来学习一下。
函数指针实现简单的计算器功能
无函数指针版本
#include <stdio.h> int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("****************************\n"); printf("****** 1.Add 2.Sub ******\n"); printf("****** 3.Mul 4.Div ******\n"); printf("******** 0. exit *********\n"); printf("****************************\n"); } int main() { int input = 0; //计算器-计算整型变量的加、减、乘、除 do { menu(); int x = 0, y = 0, ret; printf("请选择:>\n"); scanf("%d", &input); switch (input) { case 1: printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); ret=Add(x, y); printf("ret = %d\n", ret); break; case 2: printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); ret = Sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); ret = Mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); ret = Div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误,请重新选择\n"); break; } } while (input); return 0; }
我们可以发现,用无函数指针版本实现简单的计算器功能会有很多重复的语句。但是用函数指针版本来实现,这种情况就会减少。
函数指针版本
#include <stdio.h> int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("****************************\n"); printf("****** 1.Add 2.Sub ******\n"); printf("****** 3.Mul 4.Div ******\n"); printf("******** 0. exit *********\n"); printf("****************************\n\n"); } int main() { int input = 0; //计算器-计算整型变量的加、减、乘、除 do { menu(); int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };//转移表 //pfArr弄成五个元素是为了更好地对应起来 int x = 0, y = 0, ret = 0; printf("请选择:>\n"); scanf("%d", &input); if (input >= 1 && input <= 4) { printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); ret = (pfArr[input])(x, y); printf("ret = %d\n\n", ret); } else if (input == 0) { printf("退出程序\n"); } else { printf("选择错误,请重新选择:>\n"); } } while (input); return 0; }
代码运行结果
我们可以看到,用函数指针数组来实现简单的计算器功能,代码量将会减少且不会显得那么冗余。不过需要注意的是,使用函数指针数组时,要确保数组中的函数返回类型和参数都要相同。不然就不能使用函数指针数组了。
8.指向函数指针数组的指针
在前面的内容里,我们已经学习到了数组指针。比如整型数组指针int (*p)[5]。那这个指向函数指针数组的指针又是怎么一回事呢?我们来学习一下。
void test(const char* str) { printf("%s\n", str); } int main() { //函数指针pfun void (*pfun)(const char*) = test; //函数指针的数组pfunArr void (*pfunArr[5])(const char* str); pfunArr[0] = test; //指向函数指针数组pfunArr的指针ppfunArr void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0; } //去掉数组名和数组元素个数,剩下的就是数组元素类型
指向函数指针数组的指针这个内容只需了解即可,不要求熟练掌握。
9.回调函数
概念:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数实现简单的计算器功能
#include <stdio.h> int Add(int x, int y) { return x + y; } int Sub(int x, int y) { return x - y; } int Mul(int x, int y) { return x * y; } int Div(int x, int y) { return x / y; } void menu() { printf("****************************\n"); printf("****** 1.Add 2.Sub ******\n"); printf("****** 3.Mul 4.Div ******\n"); printf("******** 0. exit *********\n"); printf("****************************\n\n"); } int Calc(int (*pf)(int, int)) { int x = 0; int y = 0; printf("请输入两个操作数:>\n"); scanf("%d%d", &x, &y); return pf(x, y); } int main() { int input = 0; //回调函数实现简单的计算器功能 do { menu(); int ret = 0; printf("请选择:>\n"); scanf("%d", &input); switch (input) { case 1: ret = Calc(Add); printf("ret = %d\n\n", ret); break; case 2: ret = Calc(Sub); printf("ret = %d\n\n", ret); break; case 3: ret = Calc(Mul); printf("ret = %d\n\n", ret); break; case 4: ret = Calc(Div); printf("ret = %d\n\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误,请重新选择:>\n\n"); } } while (input); return 0; }
很明显,函数Add、Sub、Mul和Div就是回调函数,也就是A函数,而函数Calc就是B函数。通过回调函数,我们也能实现简单的计算机功能。