本文已参与「新人创作礼」活动,一起开启掘金创作之路
之前我们初步学习了指针的有关知识,也就是初阶指针,我们已经知道了指针的概念:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
今天,我们将继续探讨指针的高级主题
本章的重点是
- 字符指针
- 数组指针
- 指针数组
- 数组传参和指针传参
- 函数指针
- 函数指针数组
- 指向函数指针数组的指针
- 回调函数
- 指针和数组面试题的解析
目录
一.字符指针
二.指针数组
三.数组指针
四.数组参数,指针参数
五.函数指针
六.函数指针数组
七.指向函数指针数组的指针
八.回调函数
一.字符指针 在指针的类型中有一种指针类型——字符指针 char*
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
还有一种使用方法如下:
int main() { const char* pstr = "hello."; printf("%s\n", pstr); return 0; } 这里是把一个字符串放到pstr指针变量里了吗?
代码 const char* pstr = "hello."; 特别容易让人觉得是把字符串hello. 放到字符指针pstr里了
但是本质是把字符串 hello 的首字符h 的地址放到了 pstr里
还有一点需要说明的是 这里的 "hello" 是一个常量字符串 ,“ ” 在只读区为字符串申请了内存,用于存放字符串,所以这里的字符串是不能被修改的
char* p="abcded"; *p='w'; 这样写就是错误的,因为常量字符串不能被修改
下面我们来看一道来自《剑指offer》的面试题:
#include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char *str3 = "hello bit."; const char *str4 = "hello bit."; 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 用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。 它们是两块不同的空间,那它们的地址当然不同
其次,str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,它的地址只要存一份,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。
这里再看一个代码区分一下
这里的typedef 是给int* 这个类型名重命名为pint
而#define 只是单纯的替换
二.指针数组 指针数组是一个存放指针,数组元素是指针的数组
int* arr1[10]; //整形指针的数组 char *arr2[4]; //一级字符指针的数组 char **arr3[5];//二级字符指针的数组
由于数组名先与方括号[ ]结合 ,所以本质上是一个数组 ,*说明这是一个存放指针的数组
我们来看两个例子
1.字符指针数组
如果要打印出来
2.整型指针数组
#include<stdio.h> int main() { int arr1[] = { 1,2,3,4,5 }; int arr2[] = { 2,3,4,5,6 }; int arr3[] = { 3,4,5,6,7 }; int* arr[] = { arr1,arr2,arr3 }; int i = 0; int j = 0;
for (i = 0;i < 3;i++)
{
for (j = 0;j < 5;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
这里我们相当于模拟了一个二维数组
三.数组指针 1.数组指针的定义
数组指针是指针?还是数组? 答案是:指针。
我们已经熟悉: 整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够指向浮点型数据的指针。 那数组指针应该是:能够指向数组的指针
下面代码哪个是数组指针?
int *p1[10]; int (*p2)[10]; //p1, p2分别是什么
p1是一个指针数组,p2是一个指针数组
int (p)[10]; //解释:p先和结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。 这里要注意:[]的优先级要高于号的,所以必须加上()来保证p先和结合。
-
&数组名 vs 数组名
int arr[10]; 对于这个数组
arr 和 &arr 分别是什么
arr是数组名,数组名表示首元素地址
那么&arr呢?
我们先看一个代码
#include <stdio.h> int main() { int arr[10] = {0}; printf("%p\n", arr); printf("%p\n", &arr); return 0; }
可见数组名和&数组名打印的地址是一样的。 难道两个是一样的吗? 我们再看一段代码
#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; }
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。 本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
arr+1 跳过一个元素(4字节)
&arr+1 跳过整个数组(40字节)
这里我们再补充一个知识点:
数组名是数组首元素的的地址
但是有两个例外:
-
sizeof(数组名) ——> 数组名表示整个数组,计算的是整个数组的大小,单位是字节
-
&数组名 ——> 数组名表示整个数组,取出的是整个数组的地址
3.数组指针的使用
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,0}; int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p //但是我们一般很少这样写代码 return 0; } 其实我们很少这样写代码
让我们再看一个
#include<stdio.h> void print1(int arr[3][5], int r, int c) { int i = 0; int j = 0; for (i = 0;i < r;i++) { for (j = 0;j < c;j++) { printf("%d ", arr[i][j]); } printf("\n"); } } void print2(int(*p)[5],int r,int c) { int i = 0; int j = 0; for (i = 0;i < r;i++) { for (j = 0;j < c;j++) { printf("%d ", ((p + i) + j)); } printf("\n"); } } int main() { int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} }; print1(arr, 3, 5); print2(arr, 3, 5); return 0; }
//数组名arr,表示首元素的地址 //但是二维数组的首元素是二维数组的第一行 //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址 //可以数组指针来接收
学了指针数组和数组指针后我们再来看下面的概念
四.数组参数,指针参数 在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
一维数组传参
这些传参都是正确的
二维数组传参
五.函数指针 首先看一段代码
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("%p\n", test); printf("%p\n", &test); return 0; }
输出的结果是一样的,&函数名 和 函数名 拿到的都是函数的地址
那我们的函数的地址要想保存起来,怎么保存? 下面我们看代码:
void test() { printf("hehe\n"); } //下面pfun1和pfun2哪个有能力存放test函数的地址? void (*pfun1)(); void *pfun2(); 首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是: pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参 数,返回值类型为void
下面看两个有趣的代码,这两个代码都来自于《C陷阱和缺陷》
//代码1 ((void ()())0)(); //代码2 void (signal(int , void()(int)))(int); 我们先来分析一下第一个代码
下面再来分析第二个代码
如果觉得代码2太复杂,我们可以这样简化
typedef void(*pfun_t)(int); pfun_t signal(int, pfun_t); 我们把函数指针类型重命名为pfun_t
这里我们再区分一个概念
六.函数指针数组 数组是一个存放相同类型数据的存储空间
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组
那么函数指针数组该怎么定义呢?
int (parr1[10])(); parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int ()() 类型的函数指针
函数指针数组的用途:转移表 例子:计算器
#include <stdio.h> 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; } int main() { int x, y; int input = 1; int ret = 0; do { printf("\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf("\n"); printf("请选择:"); scanf("%d", &input); switch (input) { 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; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
我们可以发现,这样的代码太过于繁琐
此时我们就可以通过函数指针数组来简化代码
#include <stdio.h> 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; } int main() { int x, y; int input = 1; int ret = 0; int(p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表 while (input) { printf("**********************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf("***********************\n"); printf("请选择:"); scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf("输入操作数:"); scanf("%d %d", &x, &y); ret = (*p[input])(x, y); } else printf("输入有误\n"); printf("ret = %d\n", ret); } return 0;
}
七.指向函数指针数组的指针 指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?
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; }
八.回调函数 回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数
当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,
而是在特定的事件或条件发生时由另外的一方调用的,
用于对该事件或条件进行响应
首先,我们来认识一下qsort函数
这里我们第一次遇到 void* 类型的指针,我们先来了解一下吧
void* 是一种无类型的指针,无具体类型的指针
void* 的指针变量可以存放任意类型的地址
void* 的指针不能直接进行解引用操作
void* 的指针也不能直接加减整数
我们再来看一下void函数的返回值
e1指向的元素小于e2指向的数 返回值<0
e1指向的元素等于e2指向的数 返回值为0
e1指向的元素大于e2指向的数 返回值>0
那我们试着写一个用来比较整型数的大小的compare函数
由于e1,e2是void*类型的指针
而我们上面才说过void*类型的指针不能直接进行解引用操作,
所以这里我们需要将 e1,e2强制类型转换为int*类型的指针才可以进行解引用操作
大概了解了sqort函数之后,我们来测试一下用qsort函数排序结构体数据吧
#include <stdio.h> #include<stdlib.h> struct Stu { char name[20]; int age; float score; }; int cmp_stu_by_score(const void* e1,const void* e2) { if ( ((struct Stu*)e1)->score > ((struct Stu*)e2)->score ) { return 1; } else if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score) { return -1; } else { return 0; } } void test() { struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu",70,68.5f} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_score); int i = 0; for (i = 0;i < sz;i++) { printf("%10s %5d %5f\n", arr[i].name, arr[i].age, arr[i].score); } } int main() { test(); return 0; }
下面难度升级
我们即将用冒泡排序的方式实现一下qsort函数
函数能够对不同类型的元素进行排序
首先我们先使用这个函数对一个整型数组排序
#include<stdio.h> #include<stdlib.h> int cmp_int(void* e1,void* e2) { return ((int)e1) - ((int)e2); } void swap(charbuf1, charbuf2, int width) { int i = 0; for (i = 0;i < width;i++) { char tmp = buf1; buf1 = buf2; buf2 = tmp; buf1++; buf2++; } } void bubble_sort(void base,int sz,int width,int(cmp)(const voide1,const void e2)) { int i = 0; for (i = 0;i < sz-1;i++) { int j = 0; for (j = 0;j < sz - 1 - i;j++) { if (cmp((char)base + j * width, (char)base + (j + 1)width) > 0) { swap((char)base + j * width, (char*)base + (j + 1)width, width); } } } } void print(int arr, int sz) { int i = 0; for (i = 0;i < sz;i++) { printf("%d ", arr[i]); } } void test1() { int arr[5] = { 5,3,2,4,1 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]),cmp_int); print(arr,sz); } int main() { test1(); return 0; }
接下来我们再对其中一些知识点还有一些小细节做一些说明
(1)
我们将这里的base强制类型转换为char*
由于是char*类型的指针,每次+整数1的偏移量刚好是1个字节
所以只有转化成char*的指针我们才能准确地算出偏移量
所以对于其他类型元素的比较,都可以在此基础上+width来跳过一个元素
(char*)base 和 (char*)base+width 刚好是跳过了一个元素的字节指向下一个元素
(2)
比如:
好了,熟悉了整个流程之后,我们再用它来测试一下结构体数组把
#include<stdio.h> #include<stdlib.h> #include<string.h> struct Stu { char name[20]; int age; float score; }; int cmp_stu_by_socre(const void* e1, const void* e2) { if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score) { return 1; } else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score) { return -1; } else return 0; } int cmp_stu_by_name(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name); } void swap(charbuf1, charbuf2, int width) { int i = 0; for (i = 0;i < width;i++) { char tmp = buf1; buf1 = buf2; buf2 = tmp; buf1++; buf2++; } } void bubble_sort(void base, int sz, int width, int(cmp)(const voide1, const void e2)) { int i = 0; for (i = 0;i < sz - 1;i++) { int j = 0; for (j = 0;j < sz - 1 - i;j++) { if (cmp((char)base + j * width, (char)base + (j + 1)width) > 0) { swap((char)base + j * width, (char*)base + (j + 1)width, width); } } } } void print(struct Stu arr,int sz) { int i = 0; for (i = 0;i < sz;i++) { printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score); } } test2() { struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu", 10, 68.5f} }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_socre); print(arr,sz); } test3() { struct Stu arr[] = { {"zhangsan",20,87.5f},{"lisi",22,99.0f},{"wangwu", 10, 68.5f} }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), cmp_stu_by_name); print(arr, sz); }
int main() { test2(); printf("\n"); test3(); return 0; }