目录
在C语言初阶⑦(指针初阶)知识点+(操作符作业)_GR_C的博客-优快云博客接触过了指针,指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型的,指针的类型决定了指针的 + - 整数步长,指针解引用操作时的权限。
4. 指针的运算。
1. 字符指针
在指针的类型中有一种指针类型为字符指针 char* ;
定义:字符指针,常量字符串,存储时仅存储一份(为了节约内存)

一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式如下:
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);//本质是把字符串 hello bit. 首字符h的地址放到了pstr中。
return 0;
}
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
那就有可这样的面试题:(剑指offer第49页)
#include <stdio.h>
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;
}
解析:
在内存中有两个空间,一个存 arr1,一个存 arr2,当两个起始地址在不同的空间上的时候,这两个值自然不一样,
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,(常量字符串不能被修改,没必要分为两个内存)当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。
所以str1和str2不同,str3和str4相同。
2. 指针数组
在前面我们也学了指针数组,指针数组是一个存放指针(地址)的数组(所以可以存放数组名)

[ ] 优先级高,先与 p 结合成为一个数组,再由 int* 说明这是一个整型指针数组,
它有 n 个指针类型的数组元素。这里执行 p+1 时,则 p 指向下一个数组元素。

下面指针数组是什么意思?
int* arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
少用:
#include<stdio.h>
int main()
{
int a = 10, b = 20, c = 30;
int* arr[] = { &a,&b,&c };
for (int i = 0;i < 3;i++)
{
printf("%d\n", *arr[i]);
}
return 0;
}
常用:(模拟二维数组)
#include<stdio.h>
int main()
{
int a[] = { 1,2,3,4,5,6 };
int b[] = { 1,9,8,7,6,5 };
int c[] = { 0,3,0,8,2,3 };
int* arr[] = { a,b,c };
for (int i = 0;i < 3;i++)
{
for (int j = 0;j < 6;j++)
{
//printf("%d ", *(arr[i] + j));
printf("%d ", arr[i][j]);//[j]转化为+j并解引用
}
printf("\n");
}
return 0;
}
3. 数组指针
3.1 数组指针的定义
数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
字符指针 - char* pc;是指向字符的指针
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释:
int (*p2)[10];
//解释:p2先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p2是一个指针,指向一个数组(数组的元素的类型是int),叫数组指针。而前者是一个指针数组,
//这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2 &数组名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.(十六进制的28是40)
3.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 print_arr1(int arr[3][5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
//printf("%d ", *(*(arr + i) + j));//arr+i,跳过i个数组
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

4. 数组传参和指针传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
4.1 一维数组传参
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? //arr2是存放int *元素的数组,*arr取出一级指针的地址就是二级指针
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
//都是对的

4.2 二维数组传参
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);
}
//除了后面写对的都是错的
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算
4.3 一级指针传参
#include <stdio.h>
void print_arr(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print_arr(p, sz);
return 0;
}
思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
一级指针传参,一级指针接收:
void test1(int* p)
{}
void test2(char* p)
{}
int main()
{
int a = 10;
int* pa = &a;
test1(&a); // √
test1(pa); // √
char ch = 'w';
char* pc = &ch;
test2(&ch); // √
test2(pc); // √
return 0;
}
4.4 二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
// 两种写法,都是二级指针
test(pp);
test(&p); // 取p指针的地址,依然是个二级指针
return 0;
}
思考:当函数的参数为二级指针的时候,可以接收什么参数?
void test(int** p) // 如果参数时二级指针
{
;
}
int main()
{
int* ptr;
int** pp = &ptr;
test(&ptr); // 传一级指针变量的地址 √
test(pp); // 传二级指针变量 √
//指针数组也可以
int* arr[10];
test(arr); // 传存放一级指针的数组,因为arr是首元素地址,int* 的地址 √
return 0;
}
5. 函数指针
指针数组:存放指针的数组。数组指针:指向数组的指针,
函数指针:指向函数的指针,存放函数地址的指针。
取函数指针地址:函数也是有地址的,取函数地址可以通过 &函数名 或者 函数名 实现。
注意事项:
① 函数名 == &函数名 (这两种写法只是形式上的区别而已,意义是一模一样的)
② 数组名 != &数组名
取函数地址:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 函数指针 - 存放函数地址的指针
// &函数名 - 取到的就是函数的地址
printf("%p\n", &Add);
printf("%p\n", Add);
return 0;
}
输出的是两个地址,这两个地址是 test 函数的地址。
那函数的地址要想保存起来,怎么保存?
看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参
数(有参数要写上参数),返回值类型为void。

void test(char* str)
{
}
int main()
{
void (*pt)(char*) = &test;
return 0;
}
5.1函数指针的运用:
#include<stdio.h>
int add(int a,int b)
{
return a + b;
}
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
int (*p)(int, int) = &add;
//int (*p)(int, int) = add;也行
printf("%d\n", (*p)(a, b));
return 0;
}
那能不能把 (*pf) (3,5) 写成 *pf (3,5) 呢?
不行,这么写会导致星号对函数返回值进行解引用操作
字符指针 char* 型函数指针:
#include<stdio.h>
void Print(const char* str)
{
printf("%s\n", str);
}
int main()
{
void (*p)(const char*) = Print; // p先和*结合,是指针
(*p)("hello wrold"); // 调用这个函数
return 0;
}
阅读两段有趣的代码:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
《C陷阱和缺陷》这本书中提及这两个代码。
代码1图解:

代码2图解:

文字解析:
int main()
{
(*(void (*)())0)();
//()0 把0强制类型转换
//void (*)()是函数指针
(void (*)())0;//把0强制类型转化为函数指针类型
(*(void (*)())0);//解引用最后加()调用函数
//(*(void (*)())0)();所以这是调用0地址处的函数,该函数无参,返回类型是void
void (*signal(int, void(*)(int)))(int);
//按优先级signal和()结合,所以signal是函数名
//signal(int, void(*)(int))是函数名和函数类型,
//第一个类型是int 第二个类型是函数指针
//一个函数,函数名和函数类型讨论了,剩下是就是函数返回类型
//void (*)(int)就是函数的返回类型,为函数指针
//void (*signal(int, void(*)(int)))(int);所以这是一个函数的声明
//如果可以写成void (*)(int) signal(int, void(*)(int))就容易理解,但语法不支持
//但语法支持的太复杂,如何简化:
//对void (*)(int)重命名成pfun_t
//但语法又不能写成typedef void (*)(int) pfun_t
//所以写成以下格式
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
return 0;
}
6. 函数指针数组
函数指针数组就是存放函数指针的数组。
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。

int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int main()
{
int (*pf)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pfArr[2])(int, int) = { Add, Sub };//pfArr 就是函数指针数组
return 0;
}
函数指针数组的用途:转移表
6.1函数指针数组的应用
例子:(计算器)
#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 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 = 0, y = 0;
int input = 1;
int ret = 0;
do
{
menu();
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>
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 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 = 0, y = 0;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { NULL, add, sub, mul, div }; //转移表 《C和指针》
do
{
menu();
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序");
}
else
{
printf("选择错误,重新选择\n");
}
} while (input);
return 0;
}
解析:这就是函数指针数组的应用。接收一个下标,通过下标找到数组里的某个元素,这个元素如果恰好是一个函数的地址,然后去调用那个函数。它做到了一个 "跳板" 的作用,所以通常称这种数组叫做 转移表(转移表在《C和指针》这本书中有所提及)。
7. 指向函数指针数组的指针
指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?

int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr; // 取出数组的地址
int (*pfArr[4])(int, int); // pfArr是一个数组 - 函数指针的数组
// ppfArr是一个指向[函数指针数组]的指针
int (*(*ppfArr)[4])(int, int) = &pfArr;
// ppfArr 是一个数组指针,指针指向的数组有4个元素
// 指向的数组的每个元素的类型是一个函数指针 int(*)(int, int)
return 0;
}
7.1指针的总结

8. 回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个
函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,
用于对该事件或条件进行响应。

下面使用回调函数模拟实现qsort。
8.1 qsort函数介绍:
引入:我们以前自己实现的冒泡排序函数只能排序整型顺序,如果我们要排序字符串或者一个结构体,我们是不是要单独重新实现这个函数呢?而 qsort 函数可以帮我们排任意想排的数据类型。
说明:qsort 函数是C语言编译器函数库自带的排序函数( 需引入头文件 stdlib.h ),能够排序任意数据类型的数组其中包括整形,浮点型,字符串甚至还有自定义的结构体类型。
qsort函数定义:

注意:这里第一次使用 void* 的指针,void* 可以指向任意类型的地址,但是void * 类型的指针不能进行加减操作,也就无法移动
1.首元素地址base
我们要排序一组数据,首先我们需要找到这组数据在哪,因此我们直接将首元素的地址传给qsort函数来确定从哪开始排序。
2.元素个数num
我们知道了从哪开始,也要知道在哪结束才能确定一组需要排序的数据,但是我们不方便直接将结尾元素的地址传入函数,因此我们将需要排序的元素的个数传给qsort函数来确定一组数据。
3.元素大小width
我们知道qsort函数能排序任意数据类型的一组数据,因此我们用void * 类型的指针来接收元素,但是我们知道void * 类型的指针不能进行加减操作,也就无法移动,那么在函数内部我们究竟用什么类型的指针来操作变量呢?我们可以将void * 类型的指针强制类型转换成char * 类型的指针后来操作元素,因为char * 类型的指针移动的单位字节长度是1个字节,我们只需要再知道我们需要操作的数据是几个字节就可以操作指针从一个元素移动到下一个元素,因此我们需要将元素大小传入qsort函数。
4.自定义比较函数compar
第四个参数是一个函数指针
需要告诉qsort函数我们希望数据按照怎么的方式进行比较,比如对于几个字符串,可以比较字符串的大小(strcmp),也可以比较字符串的长度(strlen),因此要告诉qsort函数我们希望的比较方式,就需要传入一个比较函数compar,简写为cmp。
演示一下qsort函数的使用:
排序int 类型:
#include <stdio.h>
#include<stdlib.h>//qsort的头文件
//qsort函数的使用者得实现一个比较函数
int int_cmp(const void* p1, const void* p2)//类型要和qsort第四个参数一致
{
return (*(int*)p1 - *(int*)p2);//比较什么类型的元素就强制类型转化为什么类型的指针
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
排序结构体类型:写结构体的cmp函数(升序):
( 需求:结构体内容为 " 姓名 + 年龄 ",使用qsort,实现按年龄排序和按姓名排序 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_struct_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_struct_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void struct_sort()
{
// 使用qsort函数排序结构体数据
struct Stu s[3] = {
{"zhangsan", 20},
{"lisi", 40},
{"wangwu", 30}
};
int sz = sizeof(s) / sizeof(s[0]);
// 按照年龄排序
//qsort(s, sz, sizeof(s[0]), cmp_struct_age);
// 按照名字来排序
qsort(s, sz, sizeof(s[0]), cmp_struct_name);
}
int main()
{
struct_sort();
return 0;
}
现在是升序,如果我想实现降序呢?
很简单,只需要把 p1 - p2 换为 p2 - p1 即可:
8.2回调函数模拟实现qsort(采用冒泡的方式):
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[20];
char age;
};
// 模仿qsort实现一个冒泡排序的通用算法
void Swap(char* buf1, char* buf2, int width)
{
int i = 0;
for (i = 0; i < width; i++)
{
//一个字节一个字节交换
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort_q(
void* base, // 首元素地址
int sz, // 元素总个数
int width, // 每个元素的大小
int (*cmp)(const void* p1, const void* p2) // 两个元素的函数
)
{
// 确认趟数
int i = 0;
for (i = 0; i < sz - 1; i++)
{
// 一趟排序
int j = 0;
for (j = 0; j < sz - 1 - i; j++) {
// 两个元素比较 arr[i] arr[j+i]
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
{
//传参给用户传进来的cmp函数,但不知道比较的数据的类型
//把base强制类型转化为char*,一个字节一个字节交换
//把两个元素的地址传给cmp函数,升序,>0就交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
int cmp_struct_age(const void* p1, const void* p2)
{
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int cmp_struct_name(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
void struct_sort()
{
//使用qsort排序结构体数据
struct Stu s[3] = {
{"zhangsan", 20},
{"lisi", 40},
{"wangwu", 30}
};
int sz = sizeof(s) / sizeof(s[0]);
// 按照年龄排序
bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_age);
// 按照名字排序
bubble_sort_q(s, sz, sizeof(s[0]), cmp_struct_name);
}
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int cmp_int(const void* p1, const void* p2)
{
// 升序: p1 - p2
return *(int*)p1 - *(int*)p2;
}
void int_sort()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
// 排序
bubble_sort_q(arr, sz, sizeof(arr[0]), cmp_int);
// 打印
print_arr(arr, sz);
}
int main()
{
int_sort();
// struct_sort();
return 0;
}
本篇完。
指针和数组笔试题解析和课后作业放在下一篇。(穿越回来贴个链接:C语言进阶⑫(指针下)(指针和数组笔试题解析+课后作业)(杨氏矩阵)_GR_C的博客-优快云博客)