文章目录
前言
关于指针这一主题,我在初阶->C语言指针详解这篇博客中已经介绍过相关的内容:
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32/64位平台)。
- 指针是有类型的,指针的类型决定了指针的
+-
整数的步长,指针解引用操作的时候的权限。- 指针的运算。
本篇博客,探讨指针的更深层的内容。
字符指针
指针类型有一种为字符指针char*
//demo
char c = 'a';//将常量字符c赋给新创建的字符变量c
char* pc = &c;//将字符变量c的地址赋给新创建的字符指针pc
*pc = 'b';//解引用字符指针pc,然后修改字符变量c的内容
还有一种使用方法如下:
//demo
const char* ps = "hello world!";
printf("%s\n", ps);
注意该方式只是把常量字符串首元素的地址赋给了指针ps
,而不是把整个字符串的内容放入了指针ps里,因为指针存放的是地址,并且,字符串中的一个字符占一个字节,而指针只能是4/8个字节。
综上所述:
字符指针可以存放一个字符的地址
字符指针可以存放一个字符串的首元素的地址
来一道经典题目练练手
#include <stdio.h>
int main()
{
const char* ps1 = "hello world";
const char* ps2 = "hello world";
char ps3[] = "hello world";
char ps4[] = "hello world";
if(ps1 == ps2){
printf("ps1 == ps2");
}
else{
printf("ps1 != ps2");
}
if(ps3 == ps4){
printf("ps3 == ps4");
}
else{
printf("ps3 != ps4");
}
return 0;
}
原因是:
- 由于ps1和ps2只存放地址,而又由于常量字符串相同,所以C/C++会把常量字符串存储到一个单独的一个内存区域,当不同的指针指向该字符串时,实际会指向同一块内存。所以ps1和ps2的值相等。
- 但是用相同的常量字符串去初始化不同的数组时,会开辟出不同的内存块,因为数组是真实存放字符串的。由于两个数组分别开辟了内存块,所以ps3和ps4的值不相等。
指针数组
顾名思义,就是一个存放指针的数组
int* arr1[10]; //存放十个整形指针
char* arr2[10]; //存放十个字符指针
char** arr3[10];//存放十个二级字符指针
数组指针
数组指针的定义
指针数组是数组,那么数组指针就是指针啦。
前面已经讲过指向整型数据的整型指针int* pi
指向浮点型数据的浮点型指针float* pf
指向字符数据的字符指针char* pc
那么数组指针就该是指向数组的指针
char* p1[10];//一个数组,存放十个字符指针
char (*p2)[10];//一个指针,指向一个含有十个char型数据的数组
//解释
由于[]的优先级高于*,所以p1先与[]结合,成为数组,存放十个字符指针
由于有括号的存在,p2先与*结合,成为指针变量,然后指向一个含有十个char型数据的数组,所以称为数组指针
&数组名vs数组名
int arr[10];
前面讲过arr是数组名,数组名是首元素的地址(除了两种情况:sizeof(arr)和&arr)
那么&arr究竟是什么呢?
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
答案是一样的,继续往下看
printf("arr = %p\n", arr);
printf("arr = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr = %p\n", &arr + 1);
前面讲过,指针+-
整数,可以访问该地址的下一个地址(+)或上一个地址(-),而这里我们访问下一个地址,答案却发生了不同,经过计算,arr + 1
比arr
大4,这是因为arr
中的数据类型是int
,而&arr + 1
比&arr
大40,而该数组的大小就为40。
所以,我们就发现arr
和&arr
看似值相同,但是其背后的意义是不同的
实际上:arr
代表的是数组首元素的地址,而&arr
代表的是数组的地址
而&arr
的类型是int(*)[10]
,是一种数组指针类型
arr
的地址+1跳过一个数组元素,而&arr + 1
跳过一个数组
数组指针的使用
int arr[3][4] = { {1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6} };
int(*pa)[4] = arr;
既然arr
是数组首元素地址,那么在二维数组中,首元素就是一个数组,arr
就是一个首数组的地址,所以用数组指针来接收
回顾一下
int a[10];//a是一个整型数组,存放10个int整型
int* b[10];//b是一个指针数组,存放10个int*指针
int(*c)[10];//c是一个数组指针,指向一个数组,该数组存放10个int整型
int(*d[10])[5];//d是一个数组指针数组,该数组存放10个指针,这10个指针分别指向10个数组,这10个数组分别存放5个int整型
数组参数、指针参数
一维数组传参
#include <stdio.h>
void test1(int arr[10]);//可行,形参的类型和实参的类型相同
void test1(int arr[]); //可行
//实际上我们应该已经清楚数组名传参,传的是首元素地址,而这里的arr实际上是一个指向int的指针,所以写不写10无意义
void test1(int* arr); //可行
void test2(int* arr[10]);//可行,形参的类型和实参的类型相同,这里的arr实际上是一个指向int*的指针,来接收指针的地址没问题
void test2(int** arr);//可行,首元素是一个指针,一个指针的地址交给一个二级指针没有问题
int main()
{
int arr1[10] = { 0 };
int* arr2[10] = { NULL };
test1(arr1);
test2(arr2);
return 0;
}
二维数组传参
#include <stdio.h>
void test(int arr[3][4]);//可行,形参实参类型相同
void test(int arr[][4]);//可行,二维数组可以省略第一个下标
void test(int arr[][]);//不行,二维数组不可以省略第一个下标
void test(int* arr);//不可行,形参和实参类型不同,形参接收一个int的地址,而实参传的是一个数组的地址
void test(int* arr[4]);//不可行,形参接收二级地址
void test(int(*arr)[4]);//可行,数组指针来接收数组的地址,完全没问题
void test(int** arr);//不可行,形参二级指针来接收二级地址,而实参传的是数组的地址
int main()
{
int arr[3][4] = { 0 };
test(arr);
return 0;
}
一级指针传参
#include <stdio.h>
void print(int* p, int size){
for(int i = 0; i < size; i++){
printf("%d ", *(p + i));
}
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int size = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
print(arr, size);
return 0;
}
当一个函数的参数部分为一级指针的时候,函数能接收什么参数呢?
void test(int* p);//能接收一个整型的地址,一个整型数组首元素的地址
void test(char* p);//能接收一个字符的地址,一个字符数组首元素的地址,一个字符串首元素的地址
二级指针传参
#include <stdio.h>
void test(int** p){
printf("%d\n", **p);
}
int main()
{
int n = 10;
int* pn = &n;
int** ppn = &pn;
test(ppn);
test(&pn);
return 0;
}
当一个函数的参数部分为二级指针的时候,函数能接收什么参数呢?
#include <stdio.h>
void test(char** p);
int main()
{
char c = 'a';
char* pc = &c;
char** ppc = &pc;
test(&pc);
test(ppc);
char s[] = "hello world!";
char* ps = s;
char** pps = &ps;
test(&ps);
test(pps);
char* arr[10];
test(arr);
return 0;
}
总结:
写了这么多,其实可以总结出只要形参、实参类型相同,那么就可以进行传参!
函数指针
顾名思义就是指向函数的指针,没错,函数是有地址的,其实在内存中的任何数据都有地址!!!
函数名可以直接输出地址,也可以取地址再输出
怎么样理解这两者呢?可以暂时这样理解:
test
是函数的地址,它的类型是void()
&test
是指向函数这个对象的地址,它的类型是void(*)()
好了,现在地址有了,那怎么保存呢?
一个整形的地址需要一个整形指针,一个字符的地址需要一个字符指针,那么一个函数的地址,就需要一个函数指针
#include <stdio.h>
int Sub(int x, int y){
return x + y;
}
int main()
{
int(*p)(int, int) = ⋐
return 0;
}
仔细看函数指针的定义和函数的定义,可以发现,只需要把函数名称Sub
换成(*p)
,而变量只写类型就可以了。
其实就是要求操作数两边的指针类型一致。
注意:()
一定不能少!!!
int(*p)(int,int)//这是一个函数指针
int*p(int,int)//这是一个函数声明
摘自《C陷阱与缺陷》的两处代码
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
解释:
代码1:将0强制转换成函数指针,然后解引用取到该函数。这实际上是一次函数调用。
代码2:signal是函数名,参数类型分别是整形和函数指针,返回类型函数指针。这实际上是一个函数声明。
代码2也可以进行简化,便于更好的理解
typedef void(*pfun_t)(int);//将函数指针类型重命名为pfun_t
pfun_t signal(int, pfun_t);//signal函数的参数类型分别是int和pfun_t,返回类型是pfun_t
函数指针的一个用途:
最基本的计算器的模板是这样的(计算器的功能实现不重要,重要的是函数指针的实现方法)
#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");
}
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 input = 0;
int x = 0, y = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Add(x, y));
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Sub(x, y));
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Mul(x, y));
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", Div(x, y));
break;
case 0:
printf("退出\n");
break;
default:
printf("请重新选择\n");
break;
}
} while (input);
return 0;
}
可以看到在case
语句那里,有非常多的代码冗余,而函数指针则能解决这一问题。
函数指针的实现方法:
#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");
}
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 calc(int(*p)(int, int)) {
int x = 0, y = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", p(x, y));
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2:
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
printf("退出\n");
break;
default:
printf("请重新选择\n");
break;
}
} while (input);
return 0;
}
函数指针数组
顾名思义就是存放函数指针的数组了
int arr[10];//存放十个int
int* arr[10];//存放十个int*
int(*arr[10])(int, int);//存放十个函数指针,函数指针指向的函数的参数类型分别是int, 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");
}
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 input = 0;
int x = 0, y = 0;
int(*p[5])(int, int) = { 0, Add, Sub, Mul, Div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4) {
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
printf("%d\n", p[input](x, y));
}
else if(0 == input){
printf("退出\n");
break;
}
else {
printf("请重新选择\n");
}
} while (input);
return 0;
}
指向函数指针数组的指针
看名字,最后是指针,那它就是一个指针了。
该指针
指向一个数组
,该数组
的元素是函数指针
。
void(*arr[5])();//arr是一个数组,该数组有五个元素,元素类型是函数指针
void(*(*parr)[5])();//parr是一个指针,指向一个数组,该数组有五个元素,元素类型是函数指针
回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这时回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时,由另外的一方调用的,用于对该事件或条件进行响应。
qsort
函数就是一个典型的例子,它是C的一个库函数,该函数可以排序任意类型的数据,但是排序的顺序需要用户自己定义。
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
int cmp(const void* e1, const void* e2) {
return (*(int*)e1 - *(int*)e2);
}
int main()
{
int arr[] = { 1,3,2,4,6,5,8,7,0,9 };
int size = sizeof(arr) / sizeof(arr[0]);
printf("排序前:\n");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
qsort(arr, size, sizeof(int), cmp);
printf("排序后:\n");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
qsort
函数的第一个参数是需要排序的数组的首元素地址,这个没什么解释的
第二个参数是需要排序的元素的数量,这也没什么解释的
第三个参数是需要排序的元素类型的大小,因为qsort
函数是用来排序任何类型的,所以为了能够支持这一目的,必须传一下元素类型的大小,能够让qsort
函数知道,此时此刻在排序什么类型的数据
第四个参数是cmp
函数,这个函数是自定义的,该函数是来确定排序的顺序的。
cmp
函数的规则是:
前者-
后者为升序排序,结果>0
进行排序,<0 || <=0
不排序
后者-
前者为降序排序,结果>0
进行排序,<0 || <=0
不排序
cmp
函数是用户定义的,但不是用户调用的,而是qsort
函数调用的,所以cmp
函数就是一个回调函数。
上面是来排序整型数据的,我们接下来使用qsort
函数排序结构体数据
根据年龄来排序
#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
typedef struct Student {
char name[20];
int age;
}Student;
int cmpStudentAge(const void* e1, const void* e2) {
return (((Student*)e1)->age - ((Student*)e2)->age);
}
int main()
{
Student s[] = { { "zhangsan", 20 }, { "lisi", 60 }, { "wangwu", 40 } };
int sSize = sizeof(s) / sizeof(s[0]);
printf("排序前:\n");
for (int i = 0; i < sSize; i++) {
printf("%s %d\n", s[i].name, s[i].age);
}
printf("\n");
qsort(s, sSize, sizeof(Student), cmpStudentAge);
printf("排序后:\n");
for (int i = 0; i < sSize; i++) {
printf("%s %d\n", s[i].name, s[i].age);
}
printf("\n");
system("pause");
return 0;
}
根据姓名来排序
#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
#include <string.h>
typedef struct Student {
char name[20];
int age;
}Student;
int cmpStudentName(const void* e1, const void* e2) {
return strcmp(((Student*)e1)->name, ((Student*)e2)->name);
}
int main()
{
Student s[] = { { "zhangsan", 20 }, { "lisi", 60 }, { "wangwu", 40 } };
int sSize = sizeof(s) / sizeof(s[0]);
printf("排序前:\n");
for (int i = 0; i < sSize; i++) {
printf("%s %d\n", s[i].name, s[i].age);
}
printf("\n");
qsort(s, sSize, sizeof(Student), cmpStudentName);
printf("排序后:\n");
for (int i = 0; i < sSize; i++) {
printf("%s %d\n", s[i].name, s[i].age);
}
printf("\n");
system("pause");
return 0;
}
综上所述:
qsort
函数需要调用一个cmp
函数,cmp
函数就是回调函数。
qsort
函数的使用很简单,只需要自定义一个cmp
函数,前-
后是升序,后-
前是降序。
用冒泡排序模拟实现qsort函数
#include <stdio.h>
#include <Windows.h>
int cmp(const void* e1, const void* e2) {
return (*(int*)e1 - *(int*)e2);
}
void sort(char* e1, char* e2, int width) {
for (int i = 0; i < width; i++) {
char tmp = *e1;
*e1++ = *e2;
*e2++ = tmp;
}
}
void bsort(void* base, int size, int width, int(*cmp)(const void* e1, const void* e2)) {
for (int i = 0; i < size - 1; i++) {
int flag = 1;
for (int j = 0; j < size - 1 - i; j++) {
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
sort((char*)base + j * width, (char*)base + (j + 1) * width, width);
flag = 0;
}
}
if (1 == flag) {
break;
}
}
}
int main()
{
int arr[] = { 1,3,2,4,6,5,8,7,0,9 };
int size = sizeof(arr) / sizeof(arr[0]);
printf("排序前:\n");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
bsort(arr, size, sizeof(int), cmp);
printf("排序后:\n");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
system("pause");
return 0;
}
关于qsort
函数的参数介绍,上面段落已经讲到了,这里讲一下函数内部的核心部分:交换数据
void sort(char* e1, char* e2, int width) {
for (int i = 0; i < width; i++) {
char tmp = *e1;
*e1++ = *e2;
*e2++ = tmp;
}
}
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
sort((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
为使qsort
函数能够排序任意类型数据,只能将第一个参数设置成char
型,然后配合传入的数据的width
一起来确定每个数据的大小,从而间接确定数据的类型。
真正在交换数据时,实际上时以char
为单位来进行交换的,假设交换两个int
型,只需要连续交换四次char
型就可以了。