[C语言]指针进阶详解

心死翼未伤 2024-06-15 10:35:02 阅读 84

指针是C语言的精髓所以内容可能会比较多,需要我们认真学习


目录

1、字符指针

2、指针数组

3、数组指针

3.1数组指针的定义

3.2&数组名vs数组名

3.3数组指针的使用 

4、数组传参和指针传参

4.1一维数组传参

4.2二维数组传参

4.3一级指针传参

4.4二级指针传参

5、函数指针

6、函数指针数组

7、指向函数指针数组的指针

8、回调函数

8.1回调函数定义

8.2qsort库函数的用法

1、字符指针

在指针的类型中我们知道一种指针类型为字符指针char*

一般使用:

int main()

{

  char ch='w';

  char* pc=&ch;//*表示pc是指针,char表示pc指向的对象ch他的类型是char

  return 0;

}

#include<stdio.h>int main(){ char* pc="abcdef"; printf("%s\n",pc); return 0;}

看这样一个代码的运行结果:

 这里需要注意pc里面放的并不是整个字符串,而是a的地址,非要放在pc(4字节)中是放不下的,而打印的是字符串(%s),我们只要知道首字符a的地址就能向后访问得到整个字符串的地址在char* 前面加上const会更加好一些。看这样一道题:

#include<stdio.h>int main(){const char* p1 = "abcdef";const char* p2 = "abcdef";char arr1[] = "abcdef";char arr2[] = "abcdef";if (p1 == p2)printf("p1==p2\n");elseprintf("p1!=p2");if (arr1 == arr2)printf("arr1==arr2");elseprintf("arr1!=arr2");return 0;}

 这说明p1和p2指向同一个字符a,字符串abcdef是常量字符串,放在内存中的只读内存区,不能改变它的值,没有必要存在多份,只在内存中存一份,所以p1和p2都指向a的地址,p1就等于p2.而arr1和arr2不相等,是因为arr1[ ]和arr2[ ]是两个独立的数组,,每一个都在内存中开辟了一份独立的空间,所以地址肯定是不相同的。

2、指针数组

顾名思义指针数组就是存放指针的数组(本质是一个数组)

int* arr1[10];//整型指针的数组

char* arr2[10];//一级字符指针的数组

char **arr[5];//二级字符指针的数组 

#include<stdio.h>int main(){int arr1[] = { 1,2,3,4 };int arr2[] = { 2,3,4,5 };int arr3[] = { 3,4,5,6 };int* arr[3] = { arr1,arr2,arr3 };//每个数组的首元素地址,每个元素的类型是int*int i = 0;//代表arr的每个元素的下标for (i = 0; i < 3; i++){int j = 0;//j代表的是arr1、arr2、arr3中每个元素的下标for (j = 0; j < 4; j++){printf("%d ", *(arr[i] + j));//arr[i]代表各个数组的数组名,即每个数组的首元素地址,加上j并解引用就拿到了所有元素 //因为*(p+i)等价于arr[i],所以有可以写成这样的形式 //printf("%d ",*(*(arr+i)+j));或者 //printf("%d ",arr[i][j]);}printf("\n");}return 0;}

用了一个指针数组把三个一维数组关联起来了,模拟实现二维数组,但本质上并不是二维数组,因为这三个数组在内存中并不是连续存放的但二维数组在内存中是连续存放的。

3、数组指针

3.1数组指针的定义

数组指针的本质是指针。

整型指针:int* p;能够指向整形数据的指针

浮点型指针:float* p;能够指向浮点型数据的指针

所以说数组指针就是能够指向数组的指针。比如:

int (*p)[10];(*p)代表p是指针,指向的是整型数组中的10个元素,每个元素是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[0]);printf("%p\n", &arr);return 0;}//数组名通常表示的是数组首元素的地址,但有两个例外://1.sizeof(数组名),这里的数组名表示的是整个数组//2.&数组名,这里的数组名依然表示的是整个数组,所以&arr取出的是整个数组的地址

这里我们会发现运行结果显示,三个地址是完全相同的,但是&arr的步长和arr的步长是不同的,arr+1(类型是int*)跳过4个字节,但&arr+1跳过28(16进制)字节,即40字节。那么应该怎么存放整个数组的地址呢?

存放数组首元素的地址:int *p=arr;

存放整个数组的地址:int (*p)[10];(数组指针用来存放整个数组的地址)。它的类型为int (*)[10]。

3.3数组指针的使用 

#include<stdio.h>int main(){int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int(*p)[10] = &arr;int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(*p + i));//因为p指向的是整个数组,(不包含数组大小),*p表示的是整个数组的内容(数组名)即首元素地址//也可以这样说:*p表示从p所指向的内存地址开始的数组内存的布局。这通常被理解为该数组本身,实际上它只是一个别名或引用,指向已经存在的数组//简单理解就是如果int* p=arr,那*p就是找到arr的内容,如果int (*p)arr[10]=&arr,则*p就是找到*&arr即arr(数组名)}return 0;}

这样用我们会感到相当别扭,很不舒服,所以很不建议这样用! 建议大家用在二维数组上。

#include<stdio.h>void print1(int arr[3][5], int r, int c){int i = 0;for (i = 0; i < r; i++){int j = 0;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;for (i = 0; i < r; i++){int j = 0;for (j = 0; j < c; j++){printf("%d ",*(*(p+i)+j));//p相当于是第一行的地址,解引用相当于首元素地址}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);//传过去的是二维数组的首元素地址,那二维数组的首元素地址是什么呢?//其实是二维数组的第一行,所以传过去的其实是数组内容为5个整型的一维数组的地址return 0;}

int (*parr[10])[5];parr先和[5]配对说明是个数组数组的类型是int (*)[5]是个数组指针。所以parr就是存放数组指针的数组 。

4、数组传参和指针传参

写代码时难免会把数组或指针传给函数,那么函数的参数该如何设计呢?

4.1一维数组传参

#include<stdio.h>

void test1(int arr1[]) {

}

void test1(int arr1[10]) {

}

void test1(int* arr1) {

}

void test2(int *arr2[10]) { 

} //10可以省略

void test2(int** arr2) {

}

int main()

{

    int arr1[10] = { 0 };

    int* arr2[10] = { 0 };

    test1(arr1);

    test2(arr2);

    return 0;

}

4.2二维数组传参

#include<stdio.h>

void test(int arr[3][5]) {

}

void test(int arr[][5]) {

}//二维数组传参,函数的形参设计只能省略行,因为对于一个二维数组,可以不知道有多少行,但是必须知道一行有多少个元素

void test(int (*p)[5]) {

}//二维数组传参传的是第一行元素的地址,需要拿一个数组指针才能接收

int main()

{

    int arr[3][5] = {0};

    test(arr);

    return 0;

}

4.3一级指针传参

#include<stdio.h>

void print(int* p, int sz)

{

    int i = 0;

    for (i = 0; i < sz; i++)

    {

        printf("%d ", *(p + i));

    }

}

int main()

{

    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

    int* p = arr;

    int sz = sizeof(arr) / sizeof(arr[0]);

    print(p, sz);//p是一级指针变量

    return 0;

}

4.4二级指针传参

#include<stdio.h>

void print(int** ptr)

{

    printf("%d\n", **ptr);

}

int main()

{

    int n = 10;

    int* p = &n;

    int** pp = &p;

    print(&p);

    return 0;

}

当函数的参数为二级指针的时候,可以接收什么参数?

可以说指向一级指针的变量,也可以是指向一级指针的数组,二级指针变量本身

5、函数指针

指向函数的指针就叫做函数指针,用来存放函数的地址

#include<stdio.h>int Add(int x, int y){return x + y;}int main(){printf("%p\n", &Add);return 0;}

 由此可见打印出来的值就是Add函数的地址。对于函数来说,&函数名和函数名都是函数的地址

那怎么用指针把函数地址存起来呢?

拿此例子来说:int (*pf)(int,int)=&Add;

#include<stdio.h>int Add(int x, int y){return x + y;}int main(){int (*pf)(int, int) = &Add;int ret = (*pf)(2, 3);//解引用就相当于找到了这个函数,*是可以省略的printf("%d\n", ret);return 0;}

 int main()

{

    (*(void(*) () )0)();//把0强制转换成函数指针类型(我们可以认为0就是一个地址),解引用调用这个函数,但什么参数都没有传,所以本质上是一次函数调用,调用的是0作为地址处的函数

    void(* signal(int,void(*)(int) ) )(int);

    //signal与括号先结合是函数名,其中void(*)(int)是函数指针类型,去掉

    signal(int,void(*)(int) )我们会发现剩下部分也是返回也是一个函数返回类型。也就是说这个代码是一次函数声明。声明的signal函数第一个参数的类型是int,第二个参数的类型是函数指针。该函数指针指向的函数参数是int,返回类型是void;而signal函数的返回类型也是一个函数指针,该函数指针指向的参数是int,返回类型也是void。

    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 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;int y = 0;int ret = 0;printf("请输入两个操作数:>\n");scanf("%d%d", &x, &y);ret = (*p)(x, y);printf("%d\n", ret);}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;}

6、函数指针数组

把存放函数的地址存到一个数组中,那么这个数组就叫做函数指针数组,如何定义函数指针数组?

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 (*arr[4])(int,int)={add,sub,mul,div};//arr先于[4]结合表明本质是个数组,数组每个元素的类型为int (*)(int,int)即函数指针类型

    int i = 0;

   for (i = 0; i < 4; i++)

   {

      int ret = arr[i](8, 4);

      printf("%d ", ret);

    }

    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 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 x = 0;int y = 0;int input = 0;int ret = 0; //转移表int (*pfarr[5])(int, int) = { 0,add,sub,mul,div };//之所以第一个元素放0,是为了,与为了调用时和菜单选项对应起来,比如输入下标为1是调用的是add函数do{menu();printf("请选择:>");scanf("%d", &input);if (input == 0){printf("退出程序\n");}else if (input >= 1 && input <= 4){printf("请输入两个操作数:>\n");scanf("%d%d", &x, &y);ret = pfarr[input](x, y);printf("%d\n", ret);}else{printf("选择错误请重新选择\n");}} while (input);return 0;}

这个代码现在就变得清爽了许多,非常的简洁。 

7、指向函数指针数组的指针

指向函数指针数组的指针本质上就是一个指针,指针指向一个数组,数组里面的元素都是函数指针

int main()

{

    int (*pfarr[5])(int, int) = { 0,add,sub,mul,div };

    int (*(*ppfarr)[5])(int,int) = &pfarr;

    return 0;

}

但用的相对并不多,以后会详细介绍 

8、回调函数

8.1回调函数定义

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

8.2qsort库函数的用法

qsort是C标准库<stdlib.h>中的一个函数,用于对数组进行快速排序。它接受一个指向要排序的数组的指针、数组中元素的数量、每个元素的大小(字节为单位)以及一个比较函数作为参数。

//qsort的声明

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

base:指向要排序的数组的第一个元素的指针(数据的起始位置)

nmemb:数组中元素的数量

size:数组中每个元素的大小

compar:一个指向比较函数的指针,该函数用于确定元素的排序顺序 

比较函数应该接受两个只想要比较的元素的指针,并返回一个整型,表示他们的相对顺序。如果第一个元素排在第二个元素之前返回负数(第一个数小于第二个数),反之返回整数,相等则返回0.

qsort具体要怎么用呢?现在我们对整型数组arr[10]={10,9,8,7,6,5,4,3,2,1}排成升序之前我们学过冒泡排序的思想,现在我们换种方法来实现

void print(int arr[],int sz)//打印数组内容{int i = 0;for (i = 0;i < sz;i++){printf("%d ", arr[i]);}printf("\n");}//比较两个整型元素,e1指向一个整数,e2也指向一个整数int cmp_int(const void* e1, const void* e2){return (*(int*)e1 - *(int*)e2);//强制类型转换成int*类型}//如果我们像排成降序只需要把e1改成e2,把e2改成e1,逻辑相反int main(){int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);print(arr, sz);return 0;}//void* 是无具体类型的指针,不能解引用操作,也不能加减整数

qsort不仅仅可以排序整型数据也可以排结构体数据,也可以排字符数据

我们来看对结构体数据排序的实例:

#include<stdio.h>#include<string.h>#include<stdlib.h>struct Stu{char name[20];int age;};int cmp_stu_by_name(const void* e1, const void* e2){return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//e1是指针变量//strcmp函数如果第一个字符串比第二个大返回正数,小于返回负数,否则返回0}int main(){struct Stu s[] = { {"张三",15},{"李四",16},{"王五",18}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);int i = 0;for (i = 0; i < sz; i++){printf("name=%s age=%d\n",(s+i)->name,(s+i)->age);//s虽然本身不是指针,是数组名,但会"退化为"指向第一个元素的指针。这是为了与期望接受指针的函数(qsort)兼容//也可以用s[i].name,s[i].age的形式来访问结构体成员}return 0;}}

 

 

 

 

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。