当前位置:网站首页>指针的进阶(C语言超详细指针介绍)
指针的进阶(C语言超详细指针介绍)
2022-06-10 03:03:00 【云朵c】
前言
关于指针这一主题,我在初阶->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型就可以了。
边栏推荐
- 三個月GMV6000w+,盤點家紡行業打造爆款的關鍵
- Luogu p2678 stone jumping
- 17 orthogonal matrix and gram Schmidt orthogonalization
- Educational Codeforces Round 129 (Rated for Div. 2)(A-D)
- Robustness problem -- a work of Enlightenment
- protobuf 基本介绍安装和使用
- 脚本bash
- qiankun 如何提取出公共的依赖库
- TS 23.122
- 自学脚手架——“Data-Driven Science and Engineering” by steven L. brunton(Chapter 5.0 - 5.4)
猜你喜欢

21. dynamic planning and arrangement

Yum Usage Summary

取消打印Tensorflow中的无用信息,如tensorflow:AutoGraph could not transform <*> and will run it as-is、加载CUDA信息等

Analysis of the meaning of autocorrelation function / autocovariance function in different fields

Pandas connection database read / write file

架构的演变

Vscade C language code ctrl+ left key cannot jump to definition

Tidb experience sharing 01

Technology dry goods | linkis practice: analysis of new engine implementation process

Timestamp transform to standard time format
随机推荐
uwsgi loading shared libraries:libicui18n.so.58 异常处理
cmake记录
22.对于BFS的思考
JS内存泄漏
Lua's modules and packages
Disgruntled Judge(扩欧+枚举)
Dataframe date data processing
Robustness problem -- a work of Enlightenment
多线程的使用场景
bad interpreter:No such file or directory
npm 报错 Class extends value undefined is not a constructor or null
P1082 [NOIP2012 提高组] 同余方程
取消打印Tensorflow中的无用信息,如tensorflow:AutoGraph could not transform <*> and will run it as-is、加载CUDA信息等
Sql Server sql语句创建索引
Esp32 intrinsic function / variable cannot jump to definition
Multithread concurrency
单条视频播放量破8000w,硬核做饭原来如此上头
M3u8 label and attribute description in m3u8 file
TiDB经验分享01
Luogu p1902 assassinating Ambassador (two points | BFS)