C语言指针全解
- 指针的定义
- 指针的大小
- 指针类型
- 指针类型意义
- 野指针
- 二级指针
- 字符指针
- 指针数组
- 数组指针
- &数组名 VS 数组名
- 应用
- 数组参数、指针参数
- 函数指针
- 函数指针数组
- 函数指针数组的使用 - 模拟计算器
- 指向函数指针数组的指针
- 回调函数
- 回调函数的使用 - qsort函数
指针的定义
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的内存单元,可以说地址指向该内存单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。
- 指针变量是用于存放地址的变量。(存放在指针中的值都将被当作地址处理)
#include<stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int* p = &a;//将a的地址取出,放到指针变量p中
return 0;
}
指针的大小
对于32位的机器,即有32根地址线,因为每根地址线能产生正电(1)或负电(0),所以在32位的机器上能够产生的地址信号就是32个0/1组成的二进制序列:一共 232 个地址。
同样的算法,在64位的机器上一共能产生 264 个不同的地址。
232 可以用32个bit位进行存储,而8个bit位等价于1个字节,所以在32位的平台下指针的大小为4个字节。
264 可以用64个bit位进行存储,所以在64位的平台下指针的大小为8个字节。
在32位平台下指针的大小为4个字节,在64位平台下指针的大小为8个字节。
指针类型
指针有哪些类型?
指针的定义方式是type + *
char * 类型的指针存放的是char类型的变量地址;
int * 类型的指针存放的是int类型的变量地址;
float * 类型的指针存放的是float类型的变量地址等。
指针类型意义
1.指针±整数
- 若指针类型为int * 的指针+1,那么它将跳过4个字节的大小指向4个字节以后的内容。
2.指针解引用
指针的类型决定了指针解引用的时候能够访问几个字节的内容。
- 若指针类型为int *,那么将它进行解引用操作,它将可以访问从指向位置开始向后4个字节的内容。
总结:
- 指针的类型决定了指针向前或向后走一步有多大距离。
- 指针的类型决定了指针在进行解引用操作时,能向后访问的空间大小。
野指针
概念:野指针就是指向位置是不可知的(随机的、不正确的、没有明确限制的)指针。
野指针的成因
1.指针未初始化
2.指针越界访问
3.指针指向的空间被释放
二级指针
我们知道,指针变量是用于存放地址的变量。但是指针变量也是变量,是变量就有地址,那么存放指针变量的地址的变量是什么呢?
其实,存放普通变量的地址的指针叫一级指针,存放一级指针变量的地址的指针叫二级指针,存放二级指针变量地址的指针叫三级指针,以此类推。
#include<stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
int** p2 = &p1;
return 0;
}
在这里,我们用一级指针p1存放了普通常量a的地址,用二级指针p2存放了一级指针p1的地址。
这时如果我们要得到a的值,就有两种方法:
方法一:对一级指针p1进行一次解引用操作即可得到a的值,即*p1。
方法二:对二级指针p2进行一次解引用操作即可得到p1的值,而p1的值就是a的地址,所以再对p2进行一次解引用操作即可得到a的值,也就是对二级指针p2进行两次解引用操作即可得到a的值,即**p2。
字符指针
例:
#include<stdio.h>
int main()
{
const char* p = "hello csdn.";
printf("%c\n", *p);//打印字符'h'
printf("%s\n", p);//打印字符串"hello csdn."
return 0;
}
- 代码中,字符指针p中存放的并非字符串"hello csdn.",字符指针p中存放的是字符串"hello csdn.“的首元素地址,即字符’h’的地址。
- 代码中的字符串"hello csdn."是一个常量字符串,存放在常量区,不可以被修改,我们顺便可以加上const修饰
- 我们对字符指针p进行解引用操作并以字符的形式打印时只能打印字符’h’。我们知道,打印一个字符串只需要提供字符串的首元素地址即可,既然字符指针p中存放的是字符串的首元素地址,那么我们只要提供p(字符串首地址)并以字符串的形式打印,便可以打印字符串"hello csdn.”。
注意:常量字符串与普通字符串最大的区别是,常量字符串是不可被修改的字符串,既然不能被修改,那么在内存中没有必要存放两个一模一样的字符串,所以在内存中相同的常量字符串只有一个。
指针数组
指针数组也是数组,是用于存放指针的数组。
int* arr3[5];//数组arr3包含5个元素,每个元素是一个一级整型指针。
举例:
#include<iostream>
using namespace std;
int main()
{
char a = 'm';
char* p =&a;
char* arr[1];
arr[0] = p;
cout << *arr[0] << endl;
return 0;
}
我们定义了一个p指针指向字符a,将p存到了指针数组arr[0]中,然后对arr[0]解引用取到了字符’m’。
数组指针
数组指针就是指向数组的指针
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
//()优先级高,说明表示指针,是一个数组指针
return 0;
}
&数组名 VS 数组名
对于一个数组的数组名,它什么时候代表数组首元素的地址,什么时候又代表整个数组的地址呢?
数组名代表整个数组的地址的情况其实只有两种:
- &数组名。
- 数组名单独放在sizeof内部,即sizeof(数组名)。
除此之外,所有的数组名都是数组首元素地址。
比如:
int arr[5] = { 1, 2, 3, 4, 5 };
对于该数组arr,只有以下两种情况数组名代表整个数组的地址:
&arr;
sizeof(arr);//arr单独放在sizeof内部
应用
#include<stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int* p1 = arr;//数组首元素的地址
int(*p2)[5] = &arr;//数组的地址
printf("%p\n", p1);
printf("%p\n", p2);
printf("%p\n", p1+1);
printf("%p\n", p2+1);
return 0;
}
- 因为代码中的arr是数组首元素地址,所以要用int * 的指针接收。而&arr是整个数组的地址,所以要用数组指针进行接收。
- 虽然一个是数组首元素地址,一个是数组的地址,但是它们存放的都是数组的起始位置的地址,所以将p1和p2以地址的形式打印出来发现它们的值一样。
- 数组首元素地址和数组的地址的区别在于,数组首元素地址+1只能跳过一个元素指向下一个元素,而数组的地址+1能跳过整个数组指向数组后面的内存空间。
数组参数、指针参数
一维数组传参
#include<stdio.h>
void test1(int arr[10])//数组接收
{}
void test1(int *arr)//指针接收
{}
void test2(int *arr[20])//数组接收
{}
void test2(int **arr)//指针接收
{}
int main()
{
int arr1[10] = { 0 };//整型数组
int *arr2[20] = { 0 };//整型指针数组
test1(arr1);
test2(arr2);
}
整型数组:
当向函数传入整型数组的数组名时,我们有以下几种参数可供接收:
- 数组传参数组接收,我们传入的是整型数组,那我们就用整型数组接收。
- 传入的数组名本质上是数组首元素地址,所以我们可以用指针接收。数组的元素类型是整型,我们接收整型元素的地址用int * 的指针即可。
整型指针数组:
当向函数传入整型指针数组的数组名时,我们有以下几种参数可供接收:
- 数组传参数组接收,我们传入的是整型指针数组,那我们就用整型指针数组接收。
- 指针接收,数组的元素是int * 类型的,我们接收int * 类型元素的地址用二级指针int ** 即可。
注意:一维数组传参,函数形参设计时[ ]内的数字可省略。
二维数组传参
#include<stdio.h>
void test(int arr[][5])//数组接收
{}
void test(int(*arr)[5])//指针接收
{}
int main()
{
int arr[3][5] = { 0 };//二维数组
test(arr);
}
当向函数传入二维数组的数组名时,我们有以下几种参数可供接收:
- 二维数组传参二维数组接收。
- 指针接收,二维数组的首元素是二维数组第一行的地址,即一维数组的地址,我们用数组指针接收即可。
注意:二维数组传参,函数形参的设计只能省略第一个[ ]内的数字。
一级指针传参
#include<stdio.h>
void print(int* p, int sz)//一级指针接收
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;//一级指针
print(p, sz);
return 0;
}
二级指针传参
#include<stdio.h>
void test(int** p)//二级指针接收
{}
int main()
{
int a = 10;
int* pa = &a;
int** paa = &pa;
test(paa);//二级指针
return 0;
}
当我们传入的参数为二级指针时,我们可以用二级指针的形参对其进行接收,那么当函数形参为二级指针的时候,我们可以传入什么样的参数呢?
#include<stdio.h>
void test(int** p)
{}
int main()
{
int a = 10;
int* pa = &a;
test(&pa);//可以传入一级指针的地址
int** paa = &pa;
test(paa);//可以传入二级指针
int* arr[10];
test(arr);//可以传入一级指针数组的数组名
//...
return 0;
}
总而言之,只要传入的表达式最终的类型是二级指针类型即可传入。
函数指针
函数指针的定义
函数指针就是指向函数的指针。
和学习数组指针一样,学习函数指针我们也需要知道三点:
- ( )的优先级要高于 * 。
- 一个变量除去了变量名,便是它的变量类型。
- 一个指针变量除去了变量名和 * ,便是指针指向的内容的类型。
例子:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*p)(int, int) = &Add;//取出函数的地址放在函数指针p中
return 0;
}
那么,函数指针p的类型我们是如何创建的呢?
- 首先,p是一个指针,所以必须先与 * 结合,而( )的优先级高于 * ,所以我们要把 * 和p用括号括起来,让它们先结合。
- 指针p指向的内容,即函数Add的类型是int (int,int),所以函数指针p就变成了int(*p)(int,int)。
- 去掉变量名p后,便是该函数指针的变量类型int( * )(int,int)。
函数指针的使用
知道了如何创建函数指针,那么函数指针应该如何使用呢?
1.函数指针的赋值
- 对于数组来说,数组名和&数组名它们代表的意义不同,数组名代表的是数组首元素地址,而&数组名代表的是整个数组的地址。
- 但是对于函数来说,函数名和&函数名它们代表的意义却是相同的,它们都代表函数的地址(毕竟你也没有听说过函数有首元素这个说法吧)。
所以,当我们对函数指针赋值时可以赋值为&函数名,也可以赋值为函数名。
int(*p)(int, int) = &Add;
int(*p)(int, int) = Add;
2.通过函数指针调用函数
方法一:我们知道,函数指针存放的是函数的地址,那么我们将函数指针进行解引用操作,便能找到该函数了,于是就可以通过函数指针调用该函数。
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int(*p)(int, int) = &Add;
int ret = (*p)(a, b);//解引用找到该函数
printf("%d\n", ret);
return 0;
}
方法二:我们在函数指针赋值中说到,函数名和&函数名都代表函数的地址,我们可以赋值时直接赋值函数名,那么通过函数指针调用函数的时候我们就可以不用解引用操作符就能找到函数了。
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int(*p)(int, int) = Add;
int ret = p(a, b);//不用解引用
printf("%d\n", ret);
return 0;
}
函数指针数组
函数指针数组的定义
我们知道,数组是一个存放相同类型数据的空间,我们已经认识了指针数组,比如:
int* arr[10];//数组arr有10个元素,每个元素的类型是int*
那如果要将一系列相同类型的函数指针存放到一个数组中,那么这个数组就叫做函数指针数组,比如:
int(*pArr[10])(int, int);
//数组pArr有10个元素,每个元素的类型是int(*)(int,int)
函数指针数组的创建只需在函数指针创建的基础上加上[ ]即可。
- 比如,你要创建一个函数指针数组,这个数组中存放的函数指针的类型均为int(*)(int,int),如果你要创建一个函数指针为该类型,那么该函数指针的写法为int(*p)(int,int),现在你要创建一个存放该指针类型的数组,只需在变量名的后面加上[]即可,int(*pArr[10])(int,int)。
函数指针数组的使用 - 模拟计算器
函数指针数组一个很好的运用场景,就是计算机的模拟实现:
#include<stdio.h>
void menu()
{
printf("|-----------------------|\n");
printf("| 1.Add 2.Sub |\n");
printf("| 3.Mul 4.Div |\n");
printf("| 0.exit |\n");
printf("|-----------------------|\n");
}//菜单
double Add(double x, double y)
{
return x + y;
}//加法函数
double Sub(double x, double y)
{
return x - y;
}//减法函数
double Mul(double x, double y)
{
return x*y;
}//乘法函数
double Div(double x, double y)
{
return x / y;
}//除法函数
int main()
{
int input = 0;
double x = 0;//第一个操作数
double y = 0;//第二个操作数
double ret = 0;//运算结果
double(*pArr[])(double, double) = { 0, Add, Sub, Mul, Div };
//函数指针数组-转移表
int sz = sizeof(pArr) / sizeof(pArr[0]);//计算数组的大小
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
if (input == 0)
printf("退出程序\n");
else if (input > 0 && input < sz)
{
printf("请输入两个操作数:>");
scanf("%lf %lf", &x, &y);
ret = pArr[input](x, y);
printf("ret=%lf\n", ret);
}
else
printf("选择错误,请重新选择!\n");
} while (input);//当input不为0时循环继续
return 0;
}
代码中,函数指针数组存放的是一系列参数和返回类型相同的函数名,即函数指针。将0放在该函数指针数组的第一位是为了让用户输入的数字input与对应的函数指针下标相对应。
该代码若不使用函数指针数组,而选择使用一系列的switch分支语句当然也能达到想要的效果,但会使代码出现许多重复内容,而且当以后需要增加该计算机功能时又需要增加一个case语句,而使用函数指针数组,当你想要增加计算机功能时只需在数组中加入一个函数名即可。
指向函数指针数组的指针
既然存在函数指针数组,那么必然存在指向函数指针数组的指针。
int(*p)(int, int);
//函数指针
int(*pArr[5])(int, int);
//函数指针数组
int(*(*pa)[5])(int, int) = &pArr;
//指向函数指针数组的指针
pa就是一个指向函数指针数组的指针,该函数指针数组中每个元素类型是int(*)(int, int)。
回调函数
回调函数的定义
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
举个简单的例子:
void test1()
{
printf("hello\n");
}
void test2(void(*p)())
{
p(); //指针p被用来调用其所指向的函数
}
int main()
{
test2(test1);//将test1函数的地址传递给test2
return 0;
}
在该代码中test1函数不是由该函数的实现方直接调用,而是将其地址传递给test2函数,在test2函数中通过函数指针间接调用了test1函数,那么函数test1就被称为回调函数。
回调函数的使用 - qsort函数
其实回调函数并不是很难见到,在用于快速排序的库函数qsort中便运用了回调函数。
void qsort(void*base,size_t num,size_t width,int(*compare)(const void*e1,const void*e2));
- qsort函数的第一个参数是待排序的内容的起始位置;第二个参数是从起始位置开始,待排序的元素个数;第三个参数是待排序的每个元素的大小,单位是字节;第四个参数是一个函数指针。qsort函数的返回类型为void。
- qsort函数的第四个参数是一个函数指针,该函数指针指向的函数的两个参数的参数类型均为const void*,返回类型为int。当参数e1小于参数e2时返回小于0的数;当参数e1大于参数e2时返回大于0的数;当参数e1等于参数e2时返回0。
列如,我们要排一个整型数组:
#include<stdio.h>
int compare(const void* e1, const void* e2)
{
return *((int*)e1) - *((int*)e2);
}//自定义的比较函数
int main()
{
int arr[] = { 2, 5, 1, 8, 6, 10, 9, 3, 5, 4 };
int sz = sizeof(arr) / sizeof(arr[0]);//元素个数
qsort(arr, sz, 4, compare);//用qsort函数将arr数组排序
return 0;
}
最终arr数组将被排为升序。
注意:qsort函数默认将待排序的内容排为升序,如果我们要排为降序可将自定义的比较函数的两个形参的位置互换一下即可。
在qsort函数中我们传入了一个函数指针,最终qsort函数会在其内部通过该函数指针调用该函数,那么我们的这个自定义比较函数就被称为回调函数。