0.指针简单介绍
1.什么是指针
1.指针是内存中最小单元的编号,也就是地址。地址是唯一标识一块空间的。
2.口语中的指针通常指的是指针变量,也就是存放内存的变量
2.指针变量的大小
在32位机器上,地址需要由32个二进制序列完成,所以地址需要四个字节的空间来存储,所以指针的大小在32位平台上就是四字节。但如果是64位机器,则需要翻倍,即64个二进制序列,八个字节。
3.指针类型的意义
1.指针的类型决定了指针解引用时的访问权限。即解引用后指针能从所指的位置向后访问几个字节。
2.指针的类型决定了指针+(-)整数时的步长。即+1向后跳过几个字节,-1向前移动几个字节。
4.指针的运算
1.指针+(-)整数:指针移动整数个指针类型大小
2.指针-指针:得到指针之间的元素个数
3.指针间关系运算:比较两个指针大小
5.野指针和规避方法
野指针的成因主要有:
1.指针未初始化
2.指针越界访问
3.指针指向的空间被释放
规避方法:
1.定义指针时同时初始化,如果不知道初始化何值就置为NULL。
2.避免指针越界
3.释放指针指向的空间的同时把该指针置为NULL
4.牢记局部变量离开局部区域就会被销毁,避免返回局部变量的指针。
5.使用指针前检查其有效性
1.字符指针
字符指针就是用来存放字符地址的指针,类型为char*
字符指针的两种使用方法:
第一种:
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
第二种:
int main() { const char* pstr = "hi boy.";//这里是把一个字符串放到pstr指针变量里了吗? printf("%s\n", pstr); return 0; }
这里对第二种使用方法简单介绍一下:虽然我们把常量字符串"hi boy "作为初始值赋给字符指针pstr,但是**实际上pstr只是把这个常量字符串的首地址,即’h’的地址给存储起来了。**后续我们可以用%s的方式打印整个字符串。
在这里再来解释一下什么是常量字符串:存储在字符常量区,并且不能修改的字符,就叫常量字符串。但是指针似乎是可以解引用修改指向空间的值,因此为了防止这种误操作,我们应该用const关键字来修饰该指向常量字符串的指针。
1.const修饰指针:在*的左边,p指向的对象不能通过p来改变,但是p本身还是可以改变的。
2.const在*的右边,p指向的对象可以通过p来修改,但是p本身不能被修改。
笔试题练习
判断下面程序的输出结果:
int main() { char str1[] = "hello world"; char str2[] = "hello world"; const char *str3 = "hello world"; const char *str4 = "hello world"; 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; }
代码解析
str1和str2是两个独立数组,数组的空间在栈区开辟,内存会给两个数组分配不同的空间并把空间里的内容初始化为"hello world"。数组名代表首元素地址,空间都不同,地址肯定不会相同。
但对于str3和str4来说,由于"hello world"是常量字符串不可修改,因此它只会在字符常量区中存一份。也就是说str3和str4 指向的是同一块空间的同一个字符’h’的地址,结果当然是相等。
2.指针数组
指针数组是一个数组,这个数组是用来存放指针变量的。
指针数组的定义:
int* arr[10]; # arr的类型:int* [10] //去掉变量名剩下的就是变量类型 # arr先和[10]结合,表示arr是一个数组,数组里面有10个元素,每个元素的类型是int* char* str[10]; # str的类型 char* [10] //去掉变量名剩下的就是变量类型 # str先和[10]结合,表示str是一个数组,数组里面有10个元素,每个元素的类型是char*
指针数组的使用:
//使用指针数组可以实现升维操作,一维数组变二维数组 int main() { int a[] = { 1,2,3 }; int b[] = { 3,4,5 }; int c[] = { 4,5,6 }; int* arr[3] = { a, b, c }; int i = 0; for (i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", arr[i][j]); } printf("\n"); } return 0; }
3.数组指针
数组指针是指针,这个指针指向数组。
数组指针的定义:
int (*arr)[10]; # arr的类型:int (*)[10] //去掉变量名剩下的就是变量类型 # arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是int; char* (*arr)[10]; # arr的类型:int* (*)[10] //去掉变量名剩下的就是变量类型 # arr首先和*结合,表示它是一个指针,然后和[10]结合,表示它指向的是一个数组,数组里面有10个元素,每个元素的类型是char*;
数组名和&数组名的区别
我们知道arr是数组名,数组名表示数组首元素的地址。那&arr数组名到底是啥? 我们先看一段代码:
#include <stdio.h> int main() { int arr[10] = { 0 }; printf("arr = %p\n", arr); printf("&arr= %p\n", &arr); printf("arr+1 = %p\n", arr + 1); printf("&arr+1= %p\n", &arr + 1); return 0; }
看运行结果我们发现,数组名和&数组名的地址是一致的。但是数组名+1只是跳过四个字节,而&数组名+1却跳过了四十个字节。
这说明:数组名只是首元素地址,而&数组名取出的是整个数组的地址。至于为什么数组名和取地址数组名相同是因为指针中存储的是一个类型的起始地址。
通常来说数组名都是代表首元素的地址,但也有两个例外:
1.那就是sizeof(数组名),当sizeof中单独放数组名时,此时数组名代表首元素地址。
2.&数组名,得到的是整个数组的地址。
数组指针的使用(通常用于二维数组)
void print_arr(int(*arr)[5], int row, int col) { int i = 0; int j = 0; for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { //arr[i] 相当于 *(arr+i) //所以 arr[i][j] 相当于 *(*(arr+i)+j) //arr[i] 找到二维数组具体的某一行,而行号代表那一行,同时行号又表示那一行首元素的地址 //所以 arr[i][j] 就可以找到二维数组中具体某一行的具体某一个元素 printf("%d ", arr[i][j]); } printf("\n"); } } int main() { int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 }; //数组名arr,表示首元素的地址 //但是二维数组的首元素是二维数组的第一行 //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址 //一维数组的地址用数组指针来接收 print_arr(arr, 3, 5); return 0; }
4.数组,指针传参
一维数组传参
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); }
二维数组传参
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? //不行。 一维数组的地址应该用一个数组指针来接收。 void test(int(*arr)[5]){}//ok? //这不就是心心念念的数组指针吗 void test(int** arr){}//ok? //不行。二级指针是用来接收一级指针变量地址的指针 int main() { int arr[3][5] = { 0 }; test(arr); }
一级指针传参
void print(int* p, int sz) //一级指针用指针变量来接收 { int i = 0; for (i = 0; i < sz; i++) { printf("%d\n", *(p + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9 }; int* p = arr; //数组名代表首元素地址,即整形的地址 int sz = sizeof(arr) / sizeof(arr[0]); print(p, sz); //将一级指针p传给函数 return 0; }
思考:当一个函数的参数部分为一级指针时,函数可以接收什么参数?
1.同级别的指针(一级指针或者二级指针解引用)
2.一维数组的数组名
3.整形等类型的地址
二级指针传参
void print(int* p, int sz) //一级指针用指针变量来接收 { int i = 0; for (i = 0; i < sz; i++) { printf("%d\n", *(p + i)); } } int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9 }; int* p = arr; //数组名代表首元素地址,即整形的地址 int sz = sizeof(arr) / sizeof(arr[0]); print(p, sz); //将一级指针p传给函数 return 0; }
思考:当一个函数的参数部分为二级指针时,函数可以接收什么参数?
1.二级指针
2.一级指针的地址
3.指针数组的数组名
5.函数指针
什么是函数指针
首先看一段代码:
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("%p\n", test); printf("%p\n", &test); return 0; }
我们已经知道,所有类型的变量都要在内存中开辟空间,为了能精确的识别每一块空间,因此每一块空间都有自己的编号,即地址。上面那段代码证明:函数同样也是有地址的,存放函数地址的变量就是函数指针。
函数名也是函数的地址。
函数指针的定义
int (*p1)(int, int) = &Add; int (*p1)(int, int) = Add; # 我们上面已经知道了,函数名和&函数名都代表函数的地址,所以上面这两种写法其实是一样的,都是把Add函数的地址赋给了函数指针p1; # p1的类型:int (*)(int, int) //去掉变量名剩下的就是变量类型 # p1首先和*结合,表示它是一个指针,然后和(int, int)结合,表示它指向的是一个函数,函数的参数是int,int,返回值也是int;
函数指针的使用
int Add(int x, int y) { return x + y; } int main() { int (*p1)(int, int) = &Add; //int (*p1)(int, int) = Add; int ret1 = Add(2, 3); int ret2 = (*Add)(2, 3); printf("%d %d\n", ret1, ret2); int ret3 = p1(2, 3); int ret4 = (*p1)(2, 3); int ret5 = (*********p1)(2, 3); printf("%d %d %d\n", ret3, ret4, ret5); return 0; }
我们之前在使用函数时,是函数名+函数传参。而函数指针是指针,我们是否需要先解引用再进行传参呢?前面有提到,函数名也是函数的地址,而函数指针存储的也是函数的地址。因此就算不对函数指针进行解引用操作也可以直接使用,上面的代码就已经证明。但如果要加解引用操作,其实写多少个都无所谓,没有影响,就像上面的ret5。但一定要注意,要将解引用操作符用括号将其和变量名括起来。
两段有趣的代码
(*(void (*)())0)();
首先可以发现void (* )()是一个函数指针类型,该指针指向的函数参数为空返回值为void,即不需要返回值。
我们知道将函数类型放在括号里的操作是强制类型转换。即(int*)10,是把原本为整形的10强制类型转换为整形指针。那么同理可得(void( *)())0是将0强制类型转换为函数指针。
(* void(* )()0)()是对函数指针进行解引用操作,也就是说这是一次函数调用。调用了0地址处,参数为空返回值也为空的函数。
上面这段代码出自《C陷阱和指针》的第二章,该书对本问题的描述如下:
这说明这种看起来花里胡哨的代码还是有实际意义的,并不是为了装逼的产物,只是一般很少写。(书中提到的子例程是函数的意思)
void (*signal(int , void(*)(int)))(int);
signal(int,void(*)(int)),这好像是一个还没写出返回类型的函数。恭喜你答对了,signal是这个函数的函数名,该函数有两个参数,第一个参数是整形。第二个参数是函数指针类型,该函数指针指向的函数参数为整形,返回值为空。
我们把signal(int,void( * )(int)抽离出去,得到void(* )(int),不难发现这是一个函数指针类型。对于函数而言,除去函数名和参数,剩下的就是函数的返回值。因此signal函数的返回值是一个函数指针类型,该指针指向的函数参数为整形,返回值为空。
综上所述,这是一次函数声明。声明的函数的参数为整形和参数为整形,返回值为空的函数指针,返回值也是一个参数为整形,返回值为空的函数指针。
这个问题同样出自《C陷阱和缺陷》第二章,紧挨着上一个函数调用:
上面的代码太过复杂,能不能简化一下呢?
用typedef关键字,给一个类型起别名。将参数为整形,返回值为空的函数指针重命名即可简化上面的代码。
typedef void(*pfun_t)(int); pfun_t signal(int, pfun_t);
6.函数指针数组
数组是用来存储相同数据类型的存储空间,因此函数指针数组就是用来存储函数指针的数组。本质上还是数组,只是数组的每一个元素都是函数指针。
函数指针数组的定义
int (*parr1[10])(int); # parr1的类型:int (*[10])(int) //去掉变量名剩下的就是变量类型 # parr1和[10]结合,表示它是一个数组,数组里面有10个元素,每个元素的类型是一个函数指针,该指针指向的函数的参数为int,返回值为int;
7.函数指针和函数指针数组的用途
在学了函数指针时,我们就可能会有这种疑惑:为什么明明能直接使用函数名来调用函数,为什么还要用函数指针来调用?是多此一举吗?C语言在20世纪七十年代初问世,从【K&R C】发展到C99,如果是真的没有意义的东西,早就被删除了。那么函数指针存在的意义到底是什么?
实际上,函数指针是特别C语言中特别高明的存在,在用C语言完成大型工程时,函数指针会被经常使用。而函数指针最常用的两个用途就是回调函数和转移表。
举个栗子:
int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } 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 x, y; int input = 1; int ret = 0; do { menu(); printf("请选择:"); scanf("%d", &input); switch (input) { case 0: printf("退出程序\n"); break; case 1: printf("输入操作数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("ret = %d\n", ret); break; case 2: printf("输入操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输入操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输入操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
这样的代码对于已经学到指针的我们来说,是不成问题的。但是我们发现这段代码中由大量的重复的代码
除了中间的函数调用不同以外,其它的代码都是一样的。大量相同代码,你是不是想到用封装函数的办法来解决?但是如何封装却成为了一个很大的问题。因为每个case中使用的函数都不相同,如果每个case封装一个函数,代码同样是重复的。这个时候就体现函数指针的妙用了。