前言
指针是C语言中较难的一部分,需要大家反复学习,思考,再学习,再思考。学好指针基本就能掌握C语言数据结构层面上的精髓。
一、字符指针
int main()
{
char* p = "abcde";//"abcde"是一个常量字符串
printf("%c\n", *p);//p存放的是常量字符串的首地址
printf("%s\n", p);
return 0;
}
int main()
{
char* p = "abcde";
*p = 'q';//常量字符串是不可以修改的
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
常量字符串是不可以修改的。虽然程序没有报错且正常生成,但是运行时程序崩溃。,我们通常再常量字符串前加 const修饰。
int main()
{
const char* p = "abcde";
*p = 'q';//常量字符串是不可以修改的
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
加 const修饰会防止我们对常量字符串进行有误操作,避免程序崩溃。
我们通过一个案例再深刻了解一下字符指针:
int main()
{
char ch1[] = "abcde";
char ch2[] = "abcde";
const char* p1 = "abcde";
const char* p2 = "abcde";
if (ch1 == ch2)
{
printf("比较的是数组的元素!\n");
}
else
{
printf("比较的是数组的地址!\n");
}
if (p1 == p2)
{
printf("比较的是指针的地址!\n");
}
else
{
printf("比较的是指针的元素!\n");
}
if (!strcmp(ch1, ch2))
{
printf("比较的是数组的元素!\n");
}
else
{
printf("比较的是数组的地址!\n");
}
return 0;
}
对比结果,我们可以看出当直接用数组名来比较时比较的是地址,数组名代表的是数组首元素的地址,所以打印的结果为比较地址,但直接比较指针发现,两个指针地址相同,因为两个指针指向的是一样的常量字符串,所以两个指针指向同一开辟的空间。要比较两个数组的元素,就要用到strcmp()函数,如果两个数组结果相同返回0,结果不同返回非0。
二、数组指针
数组指针顾名思义是数组,数组指针是用来存放指针的。
int main()
{
int arr[5] = {
0 };//整形数组,里面含有5个整形元素
char ch[5] = {
0 };//字符数组,里面含有5个字符元素
int* parr[5];//整形数组指针,里面含有5个整形指针
char* pch[5];//字符数组指针,里面含有5个字符指针
return 0;
}
一般用法如下:
int main()
{
int arr1[] = {
1,2,3 };
int arr2[] = {
4,5,6 };
int arr3[] = {
7,8,9 };
int* parr[] = {
arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", *(parr[i] + j));//j表示整形数组的元素
}
printf("\n");
}
return 0;
}
三、指针数组
指针数组顾名思义是指针,指针数组是指向数组的指针。
int main()
{
int* pi = NULL;//pi是整形指针,用来存放整形的地址,指向整形元素
char* pc = NULL;//pc是字符指针,用来存放字符的地址,指向字符元素
int arr[5] = {
1,2,3,4,5 };
int(*parr)[5] = &arr;//&arr是数组的地址,[]的优先级高与*,*和parr结合,代表parr是一个指针
return 0;
}
[ ]的优先级高与*,*和parr结合,代表parr是一个指针,再和[ ]结合,表示指向的是数组。
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//p指向arr数组的首元素
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));//指针偏移i个位置
printf("%d ", p[i]);
printf("%d ", *(arr + i));//arr代表首元素地址,偏移i个位置
printf("%d ", arr[i]);
printf("\n");
}
return 0;
}
我们看以看出,数组可以当成指针使用,指针也可以通过下标访问数组元素。
我们通过一个案例再深刻了解一下指针数组:
void print1(int arr[3][3])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);//j表示整形数组的元素
//printf("%d ", *(arr[i] + j));//数组当指针用
//printf("%d ", *(*(arr + i) + j));//数组当指针用
}
printf("\n");
}
}
void print2(int(*p)[3])
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", *(*(p + i) + j));
//p+i表示第几行元素,通过解引用获得这一行元素下标。
//+j表示找到这一行的第j个元素,通过解引用获得这一个元素。
//printf("%d ", *(p[i] + j));//指针当数组用
//printf("%d ", p[i][j]);//指针当数组用
}
printf("\n");
}
}
int main()
{
int arr[3][3] = {
{
1,2,3},{
4,5,6},{
7,8,9} };
int(*p)[3] = &arr;
print1(arr);//数组首元素地址,数组形式
printf("\n");
print2(arr);//指针数组形式
printf("\n");
return 0;
}
下面的这个代表什么含义呢?
int (*p[5])[10];
首先p先和 [ ] 结合,代表p是数组,把p[ ]拿出,结构变为int (* ) [10]。
上面代码的含义为:该数组有5个元素,每个元素为指针数组,每个指针数组指向含有10个整形元素的数组。
四、函数指针
函数指针是指向函数的指针
了解函数指针之前我们先看一下函数的地址:
int ADD(int x, int y)
{
return x + y;
}
int main()
{
printf("%p\n", &ADD);//对ADD取地址
printf("%p\n", ADD);//函数名
return 0;
}
我们可以发现取函数地址和函数名都代表函数地址。所以两种形式的函数调用起到相同结果。
下面我们正式的了解一下函数指针:
int ADD(int x, int y)
{
return x + y;
}
int main()
{
int* p1;//整形指针
int* p2[5];//数组指针
int(*p3)[5];//指针数组
int(*p4)(int ,int);//函数指针
p4 = &ADD;
printf("%d\n", (*p4)(2, 5));//通过解引用调用
printf("%d\n", p4(2, 5));//直接通过函数地址调用,和函数名调用类似
return 0;
}
对比我们所了解过的指针,我们可以发现:
int main()
{
int* p1;//整形指针
//p1和*结合,代表是一个指针,类型为int
int* p2[5];//数组指针
//[ ]的优先级高于*,所以p2先和[ ]结合,代表p2是一个数组,剩下int *,代表存储的类型,为整形指针。
int(*p3)[5];//指针数组
//p3先和*结合,代表一个指针,剩下的为int [5],代表一个含有5个整形元素的数组,p3指向这个数组
int(*p4)(int, int)=&ADD;//函数指针
//p4先和*结合,代表一个指针,剩下的为int (int,int),代表两个形参都为int类型,返回值也为int类型
}
单一使用函数指针来进行函数的调用过于麻烦,用法会在回调函数中提到。
五、函数指针数组
函数指针数组是指针数组,数组中用来存放函数的地址。
如何确定一个函数指针数组呢?
根据前面的结合来看我们只需要再指针名后面加[ ]。
int(*p5[5])(int, int);//函数指针数组
//p5先和[ ]结合,代表p5是一个数组,剩下的为int(*)(int, int),这个是不是很熟悉,剩下这个就是函数指针
我们通过一个简单的计算器来看函数指针数组的使用:
void test()
{
printf("1,相加,2,相减\n");
printf("3,相乘,4,相除\n");
printf(" 0,退出程序\n");
}
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;
}
int main()
{
int num = 0;
int(*p5[5])(int, int) = {
0, ADD,SUB,MUL,DIV };//数组下标从零开始,我们可以把数组下标为0的元素直接初始化为0
test();
do
{
int x, y;
printf("请输入要进行的操作:");
scanf("%d", &num);
switch (num)
{
case 0:
printf("程序即将退出\n");
break;
case 1:
printf("\n请输入要进行的操作的两个数:");
scanf("%d %d", &x, &y);
printf("%d\n", p5[num](x, y));
break;
case 2:
printf("\n请输入要进行的操作的两个数:");
scanf("%d %d", &x, &y);
printf("%d\n", p5[num](x, y));
break;
case 3:
printf("\n请输入要进行的操作的两个数:");
scanf("%d %d", &x, &y);
printf("%d\n", p5[num](x, y));
break;
case 4:
printf("\n请输入要进行的操作的两个数:");
scanf("%d %d", &x, &y);
printf("%d\n", p5[num](x, y));
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (num);
return 0;
}
我们可以用输入的数直接调用相应的函数,但我们发现这个代码中有部分代码出现冗余现象,我们可以把冗余的代码封装为一个函数。
六、指向函数指针数组的指针
指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针。
经过上面几个指针的推导,我们可以轻松的写出一个指向函数指针数组的指针:
int(*(*p6)[5])(int, int);//指向函数指针数组的指针
//p6先和*结合,代表p5是一个指针,剩下的为int(* [ ])(int, int)就是函数指针数组
//p6是一个数组指针,指向的的数组有5个元素。
//每个元素的类型是一个函数指针类型int(*)(int,int)
七、回调函数
回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这个是回调函数。回调函数不是由该函数的实现方法直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件的响应。
下面我们用自己的排序来模仿库函数中的qsort函数。
void Swap(char* p1, char* p2,int width)//交换两个元素的值
{
for (int i = 0; i < width; i++)
{
char ch = *p1;
*p1 = *p2;
*p2 = ch;
p1++;
p2++;
}
}
int cmp_int(void* p1,void* p2)//整形比较方法
{
return *((int*)p1) - *((int*)p2);
}
int cmp_float(void* p1, void* p2)//浮点型比较方法
{
if (*(float*)p1 - *(float*)p2 > 0)
{
return 1;
}
else if (*(float*)p1 - *(float*)p2 == 0)
{
return 0;
}
else
{
return -1;
}
}
void Bubble_sort(void *arr,int size,int width,int (*p)(const void *p1, const void* p2))//排序函数
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (p((char*)arr + j * width, (char*)arr + (j + 1) * width) > 0)
//把void*强制转化为char*,通过width来增加地址。
{
Swap((char*)arr + j * width, (char*)arr + (j + 1) * width, width);
//对要交换元素一个字节一个字节进行交换
}
}
}
}
void test1()
{
int arr[10] = {
1,5,9,7,3,8,4,2,6,0 };
int size = sizeof(arr) / sizeof(arr[0]);
Bubble_sort(arr,size,sizeof(int), cmp_int);
//第一个参数:待排序数组的首元素地址
//第二个参数:待排序的元素个数
//第三个参数:待排序数组中每个元素的大小
//第四个参数:函数指针,用来比较两个元素的所用函数的地址-需要我们自己实现。
//函数指针的两个参数是待比较的两个元素的地址
for (int i = 0; i < size; i++)
{
printf("%d\t", arr[i]);
}
printf("\n");
}
void test2()
{
float farr[10] = {
1.1,5.5,9.9,7.7,3.3,8.8,4.4,2.2,6.6,0.5 };
int size = sizeof(farr) / sizeof(farr[0]);
Bubble_sort(farr, size, sizeof(float), cmp_float);
for (int i = 0; i < size; i++)
{
printf("%.1f\t", farr[i]);
}
}
int main()
{
test1();//整形排序
test2();//浮点形排序
return 0;
}
我们通过一个我们自己的Bubble_sort函数实现了浮点形和整形排序。因为Bubble_sort函数不知道要排序的是何种类型的数组,所以我们排序的函数用void * 来接收(void * 可以接收任何类型地址,但void * 不可以直接解引用。所以在排序时要强制类型转化)。当我们使用我们的排序函数进行排序时,我们知道要比较的方法,所以我们要建立一个自己的比较函数cmp_int和cmp_float,如果我们想要排序字符型数组,我们可以写一个字符比较函数,通过Bubble_sort进行调用。调用比较函数时把void * 强制转化为char * ,通过width来增加地址,因为排序函数未知我们要排序的数据类型,我们可以通过char为字节为1,加上我们穿的要比较字符的大小,来确定每个元素的位置。
从上面的例子可以看出我们没有直接调用我们所写的比较函数,而是通过排序函数进行函数指针调用,这就是所谓的回调函数。
总结
下面看一个例子,更加深刻的了解指针的内容吧。
void (*test(int, void (*)(int)))(int);
//test是一个函数声明
//test函数的参数有2个,第一个是int,第二个是函数指针,该函数指针指向的函数的参数是int,返回值为void
//test函数的返回值也是一个函数指针,该函数指针指向的函数的参数为int,返回值为void
typedef void(*fun)(int);//对void(*)(int)重命名,fun要跟着*,不可以写在最后。
fun test(int, fun);//简化版本
日拱一卒终有尽,功不唐捐终入海。指针需要我们反复学习和思考。