【C语言】指针

0.参考内容

指针系列目录
C语言指针基础知识点(一)–指针及指针变量
C语言指针基础知识点(二)–指针变量的引用
C语言指针基础知识点(三)–指针变量作为函数参数
C语言指针基础知识点(四)–通过指针引用数组
C语言指针基础知识点(五)–用数组名作函数参数
C语言指针基础知识点(六)–通过指针引用多维数组
C语言指针基础知识点(七)–通过指针引用字符串
C语言指针基础知识点(八)–返回指针值的函数
C语言指针基础知识点(九)–指针数组和多重指针
C语言指针基础知识点(十)–动态内存分配与指向它的指针变量

1.指针

1.1 为什么需要指针

指针解决了一些编程中基本的问题。

第一,指针的使用使得不同区域的代码可以轻易的共享内存数据。当然你也可以通过数据的复制达到相同的效果,但是这样往往效率不太好,因为诸如结构体等大型数据,占用的字节数多,复制很消耗性能。但使用指针就可以很好的避免这个问题,因为任何类型的指针占用的字节数都是一样的(根据平台不同,有4字节或者8字节或者其他可能)。
第二,指针使得一些复杂的链接性的数据结构的构建成为可能,比如链表,链式二叉树等等。
第三,有些操作必须使用指针。如操作申请的堆内存。还有:C语言中的一切函数调用中,值传递都是“按值传递”的,如果我们要在函数中修改被传递过来的对象,就必须通过这个对象的指针来完成。


1.2 指针是什么

指针就是变量,用来存放地址,地址唯一标识一块内存空间
指针的大小是固定的4/8个字节(32位平台/64位平台)

int a = 10;
int*b = &a;
a是一个整形变量,值为100;
b是一个指针变量,值是变量a的地址。
变量和指针的类型必须相同。

利用指针变量如何读写地址中的值
1、首先来假设一个场景:交换a和b两个变量的值。看下面的例子:

#include<stdio.h>

void swap(int x,int y){
	int tmp = x;
	x = y;
	y = tmp;
}

int main(){
	int a = 10;
	int b = 20;
	printf("交换前:a = %d  b = %d\n",a,b);
	swap(a,b);
	printf("交换后:a = %d  b = %d\n",a,b);

	return 0;
}

结果a,b的值并没有交换成功。
原因是:C程序在调用函数时使用“按值调用”

2、C通过创建指针的方式解决上述问题。

#include<stdio.h>

void swap(int* x,int* y){
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int main(){
	int a = 10;
	int b = 20;
	printf("交换前:a = %d  b = %d\n",a,b);
	swap(&a,&b);
	printf("交换后:a = %d  b = %d\n",a,b);

	return 0;
}

结果a,b的值交换成功。
原因是:C程序在调用函数时使用“按址调用”

3、函数栈帧(链接)


1.3 指针与变量的关系

用来保存 指针 的变量,就是指针变量。如果指针变量p1保存了变量 num的地址,则就说:p1指向了变量num,也可以说p1指向了num所在的内存块 ,这种指向关系,在图中一般用 箭头表示。

指针变量也是一种程序数据,那么它也有自己的指针,这样就会形成指针的指针(二级指针)。

int a = 10;
int *pa  = &a;
int **ppa = &pa;

在这里插入图片描述


1.4 指针与指针类型

指针是有类型,指针的类型决定了指针的步长(字节数),一般指针变量的类型要和它指向的数据的类型匹配。

#include<stdio.h>
int main(void)
{
    int num = 97;
    int *p1  = &num;
    char* p2 = (char*)(&num);
    printf("%d\n",*p1);    //输出  97
    putchar(*p2);          //输出  a
    return 0;
}

由于num的地址是0028FF40,因此p1 和 p2的值都是0028FF40

*p1 : 将从地址0028FF40 开始解析,因为p1是int类型指针,int占4字节,因此向后连续取4个字节,并将这4个字节的二进制数据解析为一个整数 97。

*p2 : 将从地址0028FF40 开始解析,因为p2是char类型指针,char占1字节,因此向后连续取1个字节,并将这1个字节的二进制数据解析为一个字符,即’a’。

指针的大小

#include<stdio.h>
int main(){
    printf("%d\n",sizeof(char*));   //4
    printf("%d\n",sizeof(short*));  //4
    printf("%d\n",sizeof(int*));    //4
    printf("%d\n",sizeof(double*)); //4

    return 0;
}

在32位平台下,指针的大小占4字节;
在64位平台下,指针的大小占8字节

#include<stdio.h>
int main(){
    int a = 0x11223344;
    int* pa = &a;
    char* pc = &a;
    printf("%p\n",pa);
    printf("%p\n",pc);

    return 0;
}
#include<stdio.h>
int main(){
    int a = 0x11223344;
    int* pa = &a;
    *pa = 0;
    // char* pc = &a;
    // *pc = 0;
    //指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
    //int* p;       *p能够访问4个字节
    //char* p;      *p能够访问1个字节
    //double* p;    *p能够访问8个字节

    return 0;
}

指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
char* p; *p能够访问1个字节
int* p; *p能够访问4个字节
double* p; *p能够访问8个字节

#include<stdio.h>
int main(){
    int a = 0x11223344;
    int* pa = &a;
    char* pc = &a;
    printf("%p\n",pa);
    printf("%p\n",pa+1);

    printf("%p\n",pc);
    printf("%p\n",pc+1);

    //指针类型决定了:指针走一步走多远(指针步长)
    //int* p;       //p+1-->4
    //char *p;      //p+1-->1
    //double *p     //p+1-->8

    return 0;
}
#include<stdio.h>
int main(){
    int arr[10] = {0};
    int* p = arr;   //数组名-首元素的地址
    //char* p = arr;    //每次只改变1字节
    int i = 0;
    for(i = 0; i < 10; i++){
        *(p + i) = 1;
    }
    for(i = 0; i < 10; i++){
        printf("%d ",arr[i]);
    }

    return 0;
}

//(int* p = arr)1 1 1 1 1 1 1 1 1 1
//(char* p = arr)16843009 16843009 257 0 0 0 0 0 0 0

在这里插入图片描述

#include <stdio.h>

int main(){
    int arr[] = {1,2,3,4,5};
    short* p = (short*)arr;
    int i = 0;
    for(i = 0; i < 4; i++){
        *(p + i) = 0;
    }

    for(i = 0; i < 5; i++){
        printf("%d ",arr[i]);	//0 0 3 4 5 
    }

    return 0;
}
#include <stdio.h>

int main(){
    int a = 0x11223344;
    char* pc = (char*)&a;
    *pc = 0;
    printf("%x\n",a);		//11223300

    return 0;
}

1.5 野指针

野指针就是指针指向的位置是不可知的(随机的、不明确的、没有明确限制的)

#include<stdio.h>
int main(){
    int a;  //局部变量不初始化,默认是随机值
    int *p; //局部指针变量不初始化,默认也是随机值

    return 0;
}

指针越界导致野指针问题

#include<stdio.h>
int main(){
    int arr[10] = {0};
    int* p = arr;
    int i = 0;
    for(i = 0; i < 12; i++){
        *(p++) = i;
    }
    //指针越界也会导致野指针问题

    return 0;
}

指针被指向的空间释放导致野指针问题

int* test(){	//函数调用是通过栈实现的,调用完后栈被释放	
	int a = 10;
	return &a;
}

int main(){
	int* p = test();//所以变量a的地址被销毁,指向的内存空间不再是a
	*p = 20;			
}

如何规避野指针

  • 指针初始化
  • 小心指针越界
  • 指针指向空间释放即使置NULL
  • 指针使用之前检查有效性
#include<stdio.h>
int main(){
    int a = 10;
    int* pa = &a;   //初始化
    int* p = NULL;  //NULL - 用来初始化指针的,会给指针赋值

    return 0;

2.数组传参和指针传参

#include<stdio.h>

//参数是数组的形式
void print(int arr[3][5],int x,int y){
    int i = 0;
    int j = 0;
    for(i = 0; i < x; i++){
        for(j = 0; j < y; j++){
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
}

//参数是指针的形式
void print2(int (*p)[5],int x,int y){
    int i = 0;
    int j = 0;
    for(i = 0; i < x; i++){
        for(j = 0; j < y; 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}};
    print(arr,3,5);     //arr - 数组名 - 数组名就是首元素地址
    printf("\n");
    print2(arr,3,5);

    return 0;
}

2.1 数组传参

一维数组传参

#include <stdio.h>
void test(int arr[]) {}
// void test(int arr[10]) {}
// void test(int *arr) {}

void test2(int *arr[20]) {}
// void test2(int **arr) {}

int main(){
    int arr[10] = {0};
    int *arr2[20] = {0};
    test2(arr2);
}

二维数组传参

#include <stdio.h>
void test(int arr[3][5]) {}
void test2(int arr[][5]) {}

int main()
{
    int arr[3][5] = {0};
    test(arr); //二维数组传参
    test2(arr);

    return 0;
}

2.2 指针传参

一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d\n", *(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(p, sz);
    return 0;
}

二级指针传参

void test(int **p) {}

int main()
{
    int *ptr;
    int **pp = &ptr;
    test(&ptr);
    test(pp);
    //int *arr[10];
    //test(arr); 	//指针数组也可以
    return 0;
}

C语言中实参变量和形参变量之间的数据传递是单向的值传递方式。指针变量做函数参数同样要遵循这一规则。

总结:
不可能通过执行调用函数来改变实参指针变量的值,但是可以改变实参指针变量所指变量的值。


3.指针与数组

#include <stdio.h>

int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);//地址-首元素的地址
    printf("%p\n",arr+1);

    printf("%p\n", &arr[0]);
    printf("%p\n", &arr[0]+1);

    printf("%p\n",&arr);
    printf("%p\n",&arr+1);

    // 1.&arr-&数组名 - 数组名不是首元素的地址-数组名表示整个数组~&数组名取出的是整个数组的地址
    // 2. sizeof(arr) - sizeof(数组名)- 数组名表示的整个数组- sizeof(数组名)计算的是整个数组的大小
   
    return 0;
}
#include <stdio.h>

int main()
{
    int arr[10] = {0};
    int *p = arr;
    int i = 0;

    for(i = 0; i < 10; i++){
        *(p+i) = i;
    }
    for(i = 0; i < 10; i++){
        printf("%d ",arr[i]);
        // printf("%d ",*(p+i));
    }

    // for (i = 0; i < 10; i++)
    // {
    //     printf("%p  ======  %p\n", p + i, &arr[i]);
    // }

    return 0;
}

通过指针引用数组元素
引用一个数组元素,可以用以下两种方法:
1.下标法,如 a[i] 形式;
2.指针法,如 *(a+i)或 *(p+i)。其中 a 是数组名,p是指向数组元素的指针变量,其初值为 p=a;

//1.下标法
# include<stdio.h>

int main()
{
	int i,a[5];

    printf("Please enter 5 integer numbers:\n");
	for(i=0;i<5;i++)
		scanf("%d",&a[i]);
	for(i=0;i<5;i++)
		printf("%d ",a[i]);
	printf("\n");

	return 0;
}
//2.通过数组名计算数组元素地址,找出元素的值
# include<stdio.h>

int main()
{
	int i,a[5];

	printf("Please enter 5 integer numbers:\n");
	for(i=0;i<5;i++)
		scanf("%d",&a[i]); // or scanf("%d",a+i)
	for(i=0;i<5;i++)
		printf("%d ",*(a+i));
	printf("\n");

	return 0;
}
//3.用指针变量指向数组元素
# include<stdio.h>

int main()
{
	int i,a[5],*p;
	p = a; // or  p=&a[0]

	printf("Please enter 5 integer numbers:\n");
	for(i=0;i<5;i++)
		scanf("%d",p+i);
	for(i=0;i<5;i++)
		printf("%d ",*(p+i));
	//for(p = a; p < (a+5); p++){
		printf("%d ",*p);
	}
	printf("\n");

	return 0;
}

可以用数组的名字作为指向数组第一个元素的指针。但数组名是指针常量,不能赋值。

多维数组的元素在内存中是依序存储的,获得第一个元素的指针,就可以通过重复自增指针的方式访问到每一个元素。

C语言中n维数组是被当成一个一维数组,每个数组元素是一个n-1维数组,依次类推。因此对于数组int a[N1][N2]…[Nm]来说,a是一个一维数组的指针,其元素是一个n维数组,因此a指针增减的移动单位是n维数组,a[N1]是一个一维数组的指针,其元素是一个n-1维数组,因此a[N1]指针增减的移动单位是n-1维数组,依次类推,a[N1][N2]…[Nm-1]是一个一维数组的指针,其指针增加的移动单位是一个int型长度,指针指向的是int型的数组元素。

#include <stdio.h>

#define N 10

void ReversePrint(int a[])
{
    int *p = a + N - 1;
    for (; p >= a; p--)
        printf("%d ", *p);
}

int Sum(int *a)
{
    int *p = a;
    int sum = 0;
    for (; p < a + N; p++)
        sum += *p;
    return sum;
}

int main()
{
    int a[N];
    printf("请输入%d个整数", N);
    int i = 0;
    for (; i < N; i++)
        scanf("%d", &a[i]);

    printf("10个数的反序排列为:");
    ReversePrint(a);
    printf("\n");
    printf("这%d个整数的和为%d\n", N, Sum(a));

    return 0;
}

在这里插入图片描述


4.指针的算术运算

当C语言中的指针执行数组元素时,C语言允许对指针进行算术运算

  • 指针 加/减 整数
    • 指针p加上整数j表示指针向后移动j个单位(每个单位大小为指针p的类型所占的字节数),指向p原先指向的元素后的第j个元素。若p指向数组a[i],则p+j指向a[i+j]。
    • 指针p减去整数j表示指针向前移动j个单位,指向p原先指向的元素前的第j个元素。若p指向数组a[i],则p-j指向a[i-j]。
  • 指针 减 指针
    • 两个指针相减,得到的是指针之间元素的个数。若指针p指向a[i],指针q指向a[j],则p-q等于i-j。
  • 关系运算符进行指针比较
    • 指针的比较依赖于指针所指向的两个元素的相对位置。若指针p指向a[i],指针q指向a[j],p和q的比较结果由i与j的大小决定。

因此适用于指针运算的运算符包括算术运算符+、-赋值运算符复合赋值运算符(=,+=,’-=’,++,–)和所有的关系运算符

#include<stdio.h>
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    int* p = arr;
    for(i = 0; i < sz; i++){
        printf("%d ",*p);
        p++;
    }

    return 0;
}
//1.指针 减 整数
#include<stdio.h>
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int sz = sizeof(arr)/sizeof(arr[0]);
    int* p = &arr[9];
    for(i = 0; i < 5; i++){
        printf("%d ",*p);
        p-=2;	//指针减整数
    }

    return 0;
}
//2.指针 减 指针
#include<stdio.h>
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("%d\n",&arr[9] - &arr[0]);	//指针-指针

    return 0;
}
//3.关系运算符进行指针比较
#include<stdio.h>

int my_strlen(char* str){
    char* start = str;
    char* end = str;
    while(*end != '\0'){	//关系比较
        end++;
    }
    return end - start;
}

int main(){
    char arr[] = "bit";
    int len = my_strlen(arr);
    printf("%d\n",len);

    return 0;
}

标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
在这里插入图片描述

C语言的间接寻址运算符*常和++或- -运算符一起使用,具有以下四种不同的形式:

表达式含义
*p++或 *(p++)首先计算表达式的值为*p,然后p自增1
(*p)++首先计算表达式的值为*p,然后*p自增1
*++p或 *(++p)首先将p自增1,然后计算表达式的值为*p
++*p或 ++(*p)首先将(*p)自增1,然后计算表达式的值为(*p)
#include <stdio.h>

#define N 10

int main()
{
    int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int temp = 0;

    int *p = a;
    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = *p++;
    printf("*p++的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = *(p++);
    printf("*(p++)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = (*p)++;
    printf("(*p)++的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = *++p;
    printf("*++p的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = *(++p);
    printf("*(++p)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = ++*p;
    printf("++*p的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    printf("p的初始值为%p, *p值为%d\n", p, *p);
    temp = ++(*p);
    printf("++(*p)的值为%d, 运算后p的值为%p, *p的值为%d\n\n", temp, p, *p);

    return 0;
}

在这里插入图片描述


5.指针类别

5.1 字符指针

#include<stdio.h>

int main(){
    char arr1[] = "abcdef";
    char arr2[] = "abcdef";
    char* p1 = "abcdef";
    char *p2 = "abcdef";

    // if(arr1 == arr2){
    //     printf("hehe\n");
    // }else{
    //     printf("haha\n");	//打印haha
    // }

    if(p1 == p2){
        printf("hehe\n");	//打印hehe
    }else{
        printf("haha\n");
    }

    return 0;

}

5.2 数组指针

数组的指针:是一个指针,什么样的指针呢?指向数组的指针。

然后,需要明确一个优先级顺序:( )>[ ]> *,所以:
(*p)[n]:根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即数组指针;

*p[n]:根据优先级,先看[],则p是一个数组,再结合,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即指针数组。

根据上面两个分析,可以看出,p是什么,则词组的中心词就是什么,即数组“指针”和指针“数组”。

int *p1[5]//指针的数组
int (*p2)[5]//数组的指针

首先,对于语句int* p1[5],因为“[]”的优先级要比 * 要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 所示,因此,它是一个指针数组。
在这里插入图片描述其次,对于语句int (* p2)[5],“()”的优先级比“[]”高,“*”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。
在这里插入图片描述由此可见,对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在 32 位系统下永远占 4 字节,至于它指向的数组占多少字节,这个不能够确定,要看具体情况。

数组指针(*p)[n]

#include<stdio.h>
 
int main()
{
	//一维数组
	int a[5] = { 1, 2, 3, 4, 5 };
	//步长为5的数组指针,即数组里有5个元素
	int (*p)[5];
	//把数组a的地址赋给p,则p为数组a的地址,则*p表示数组a本身
	p = &a;
 
	//%p输出地址, %d输出十进制
	//\n回车
	//在C中,在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址,它的类型取决于数组元素的类型。
	printf("%p\n", a); //输出数组名,一般用数组的首元素地址来标识一个数组,则输出数组首元素地址
	printf("%p\n", p); //根据上面,p为数组a的地址,输出数组a的地址
	printf("%p\n", *p); //*p表示数组a本身,一般用数组的首元素地址来标识一个数组
	printf("%p\n", &a[0]); //a[0]的地址
	printf("%p\n", &a[1]); //a[1]的地址
	printf("%p\n", p[0]); //数组首元素的地址
	printf("%d\n", **p); //*p为数组a本身,即为数组a首元素地址,则*(*p)为值,当*p为数组首元素地址时,**p表示首元素的值1
	printf("%d\n", *p[0]); //根据优先级,p[0] 表示首元素地址,则*p[0]表示首元素本身,即首元素的值1
	printf("%p\n", &p[1]); //为野指针,*p[1]的地址为*p[0]的地址加上4*5,其中4为int整型大小,5为数组个数
	printf("%p\n", &p[2]);
    return 0;
}

结果为:
0061FF08
0061FF08
0061FF08
0061FF08
0061FF0C
0061FF08
1
1
0061FF1C
0061FF30
#include<stdio.h>
 
int main()
{
	//将二维数组赋给指针
	int b[3][4];
	int(*pp)[4]; //定义一个数组指针,指向含4个元素的一维数组
	pp = b; //将该二维数组的首地址赋给pp,也就是b[0]或&b[0],二维数组中pp=b和pp=&b[0]是等价的
	pp++; //pp=pp+1,该语句执行过后pp的指向从行b[0][]变为了行b[1][],pp=&b[1]
 
    return 0;
}   

根据上面二维数组可以得出,数组指针也称指向一维数组的指针,所以数组指针也称行指针。

5.3 指针数组

指针的数组:是一个数组,什么样的数组呢?装着指针的数组。

指针数组*p[n]

#include<stdio.h>

int main(){
    int arr[5] = {0};  //整型数组
    char ch[5] = {0};   //字符数组
    int* parr[5];   //存放整型指针的数组 - 指针数组
    char* pch[5];   //存放字符指针的数组 - 指针数组

    return 0;
}

在这里插入图片描述

一维指针数组

#include<stdio.h>

int main(){
    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;
    int* arr[4] = {&a,&b,&c,&d};
    int i = 0;
    for(i = 0; i < 4; i++){
        printf("%d ",*(arr[i]));
    }

    return 0;
}

二维指针数组

#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* parr[] = {arr1,arr2,arr3};
    int i = 0;
    for(i = 0; i < 3; i++){
        int j = 0;
        for(j = 0; j < 5; j++){
            printf("%d ",*(parr[i] + j));
        }
        printf("\n");
    }

    return 0;
}

在这里插入图片描述

#include <stdio.h>

int main()
{
    //将二维数组赋给指针数组
    int *pp[3]; //一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值
    int c[3][4];
    for (int i = 0; i < 3; i++)
        pp[i] = c[i];

    return 0;
}

在这里插入图片描述

最后,从上文来看:
数组指针是一个指针变量,占有内存中一个指针的存储空间;
指针数组是多个指针变量,以数组的形式存储在内存中,占有多个指针的存储空间。


5.4 函数指针

1. 如何声明一个指向函数的指针?

void print(int num); // 声明函数
void (*func)(int) = print; 
// 声明指向函数的指针, 该函数接受一个int参数, 返回void, 
// 并用print函数的地址初始化
//数组指针-指向数组的指针
//函数指针-指向函数的指针 - 存放函数地址的一个指针
#include <stdio.h>

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 10;
    int b = 20;
    int arr[10] = { 0 };
    //数组指针
    int (*p)[10] = &arr;
    //&arr
    //arr

    printf("%d\n", Add(a, b));
    //&函数名 和 函数名 都是函数的地址
    printf("%p\n",&Add);
    printf("%p\n",Add);

    //函数指针
    int (*pa)(int,int) = Add;
    printf("%d\n",(*pa)(2,3));


    return 0;
}

2. 如何通过指向函数的指针调用函数?

print(10); // (1) 使用函数名调用print函数
(*funptr)(10); // (2) 使用指向函数指针解引调用print函数`
funptr(10); // (3) 使用指向函数指针直接调用print函数
#include<stdio.h>

void Print(char*str){
    printf("%s\n",str);
}

int main(){
    void (*p)(char*) = Print;
    p("hello world\n");     
    //(*p)("hello world\n");
    //Print("hello world\n")
}

3. 声明 “指向函数的指针” 的指针
指向函数的指针本质上仍然是一种指针, 我们可以按照二级指针声明指向它的指针.

void print(int num); // 声明函数
void (*funptr)(int) = &print; // 声明指向函数的指针
void (**funpptr)(int) = &funptr; // 声明 "指向函数的指针" 的指针
(**funpptr)(10); // 通过二级指针调用print

4. 声明元素类型为 “指向函数的指针” 的数组
同样, 如果具有多个类型一样的指向函数的指针, 我们可以将它们放在一个数组中.这种技巧也称为转移表.

void print(int num); // 声明函数
void (*funptr)(int) = &print; // 声明指向函数的指针
void (*funptrarray[])(int) = funptr; // 声明元素类型为"指向函数的指针"的数组, 数组大小为1, funptrarray[0]用funptr初始化
(*funptrarray[0])(10); // 调用print
(**funptrarray)(10); // 等价的调用print

5.5 指向函数指针的数组

char* my_strcpy(char* dest, const char* src);
//1.写一个函数指针 pf,能够指向my_strcpy
char* pf(char*,const char*);

//2.写一个函数指针数组 pfArr,能够存放4个my_strcpy函数的地址
char* (*pfArr[4])(char*,const char*);
//计算器
#include<stdio.h>

void menu()
{
    printf("\n****************************\n");
    printf("****1.add         2. sub****\n");
    printf("****3. mul        4. div****\n");
    printf("********   0. exit   *******\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;
    int y = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d",&input);

        switch(input){
            case 1:
                printf("请输入两个操作数:>");
        		scanf("%d%d",&x,&y);
                printf("%d + %d = %d\n",x,y,Add(x,y));
                break;
            case 2:
            	printf("请输入两个操作数:>");
        		scanf("%d%d",&x,&y);
                printf("%d - %d = %d\n",x,y,Sub(x,y));
                break;
            case 3:
            	printf("请输入两个操作数:>");
        		scanf("%d%d",&x,&y);
                printf("%d * %d = %d\n",x,y,Mul(x,y));
                break;
            case 4:
            	printf("请输入两个操作数:>");
        		scanf("%d%d",&x,&y);
                printf("%d / %d = %d\n",x,y,Div(x,y));
                break;
            case 0:
                printf("退出\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    }while(input);

	return 0;
}
//计算器(优化)
#include<stdio.h>

void menu()
{
    printf("\n****************************\n");
    printf("****1.add         2. sub****\n");
    printf("****3. mul        4. div****\n");
    printf("********   0. exit   *******\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;
    int y = 0;
    //函数指针数组
    //用途(转移表)
    int (*pfArr[])(int,int) = {0 , Add , Sub , Mul , Div};
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d",&input);
        if(input >= 1 && input <= 4){
            printf("请输入两个操作数:>");
            scanf("%d%d",&x,&y);
            int ret = pfArr[input](x,y);
            printf("%d\n",ret);
        }
        else if(input == 0){
            printf("退出\n");
        }
        else{
            printf("选择错误\n");
        }
    }while(input);
}

5.6 指向函数指针数组的指针

指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针

int arr[10] = { 0 };
int (*p)[10] = &arr;//数组指针

int(*pf)(int, int);//函数指针
int(*pfArr[4])(int,int); // pfArr是一个数组-函数指针的数组
int(*(*ppfArr)[4])(int,int) = &pfArr;
// ppfArr 是一个数组指针,指针指向的数组有4个元素
// 指向的数组的每个元素的类型是一个函数指针int(*)(int, int)

5.7 小结

int Add(int x,int y){
    return x + y;
}

//指针数组
int* arr[10];
//数组指针
int* (*pa)[10] = &arr;
//函数指针
int (*pAdd)(int, int) = Add; //&Add
//int sum = (*pAdd)(1,2);
//int sum = pAdd(1,2);
// Add(1,2);
// printf( "sum = %d\n", sum);
//函数指针的数组
int(*pArr[5])(int,int);
//指向函数 指针数组 的指针
int(*(*ppArr)[5])(int, int) = &pArr;

6.回调函数

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

//计算器
#include<stdio.h>

void menu()
{
    printf("****************************\n");
    printf("****1.add         2. sub****\n");
    printf("****3. mul        4. div****\n");
    printf("********   0. exit   *******\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 (*pf)(int,int)){
    int x = 0;
    int y = 0;
    printf("请输入两个操作数:>");
    scanf("%d%d",&x,&y);
    printf("%d\n",pf(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;
}
#include<stdio.h>

void print(char *str)    //回调函数
{
    printf("hehe:%s", str);
}

void test(void (*p)(char *))
{
    printf("test\n");
    p("bit");
}

int main()
{
    test(print);
    return 0;
}

7. 排序

冒泡排序只适用于对一个整数数组的排序,无法对字符串、结构体等进行排序,而库函数中的qsort函数可以对各种类型的数据进行比较个排序

#include<stdio.h>

void bubble_sort(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz - 1; i++)
    {
        //一趟冒泡排序
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);

    bubble_sort(arr, sz);

    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

拓展:(void指针总结)
在 C 语言中,void 指针可以指向任何类型的数据。所以 void 指针一般被称为通用指针或者泛指针,也可以叫做万能指针。在 C 语言中在任何时候都可以用 void 类型的指针来代替其他类型的指针,void 指针可以指向任何数据类型的变量。
如果要通过 void 指针去获取它所指向的变量值时候,需要先将 void 指针强制类型转换成和变量名类型相匹配的数据类型指针后再进行操作。任何类型的指针都可以赋值给 void 指针,无需进行强制类型转换。

qsort函数的使用
第一个参数:待排序数组的首元素地址
第二个参数:待排序数组的元素个数
第三个参数:待排序数组的每个元素的大小-单位是字节
第四个参数:是函数指针,比较两个元素的所用函数的地址-这个函数需要使用者自己实现(函数指针的两个参数是:待比较的两个元素的地址)

整数排序

int cmp_int(const void *e1, const void *e2)
{
    //比较两个整型值
    return *(int *)e1 - *(int *)e2;
}
void test1()
{
    int arr[10] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}

浮点数排序

int cmp_float(const void *e1, const void *e2)
{
    //比较两个浮点型
    return ((int)(*(float *)e1 - *(float *)e2));
}
void test2()
{
    float f[5] = {5.0, 4.0, 3.0, 2.0, 1.0};
    int sz = sizeof(f) / sizeof(f[0]);
    qsort(f, sz, sizeof(f[0]), cmp_float);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%f ", f[i]);
    }
}

结构体排序

struct Stu
{
    char name[20];
    int age;
};
int cmp_stu_by_name(const void *e1, const void *e2)
{
    //字符串比较不能直接用> < =来比较,应该用strcmp函数
    return strcmp(((struct Stu *)e1)->name, ((struct Stu *)e2)->name);
}
int cmp_stu_by_age(const void *e1, const void *e2)
{
    return (((struct Stu *)e1)->age - ((struct Stu *)e2)->age);
}
void test3()
{
    struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 10}};
    int sz = sizeof(s) / sizeof(s[0]);
    // qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}

qsort原理

void Swap(char *buff1, char *buff2, int width)
{
    int i = 0;
    for (i = 0; i < width; i++)
    {
        char tmp = *buff1;
        *buff1 = *buff2;
        *buff2 = tmp;
        buff1++;
        buff2++;
    }
}
void bubble_sort(void *base, int sz, int width, int (*cmp)(void *e1, 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 test()
{
    int arr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    test();
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的冯猪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值