指针详解(内含assert断言、冒泡排序、多级指针、qsort函数的使用等等)

简介: 电脑运行是需要内存的,通俗来讲内存就相当于一个宿舍楼。电脑运行过程中,内存空间如何进行管理?电脑运行时会将内存划分为一个个房间即内存单元,每个内存单元的大小为一个字节

初始指针


内存

什么是内存?

电脑运行是需要内存的,通俗来讲内存就相当于一个宿舍楼。

电脑运行过程中,内存空间如何进行管理?

电脑运行时会将内存划分为一个个房间即内存单元,每个内存单元的大小为一个字节

Tips:计算机内存中常见的单位转换


1byte = 8bit                                    


1KB = 1024byte


1MB = 1024KB


1GB = 1024MB


1TB = 1024GB


1PB = 1024TB


byte表示字节,bit表示比特位,一字节=8比特位,每一个比特位可以存储一个二进制数字。

如何理解指针?

 我们在上面说过内存相当于一个宿舍楼,一个内存单元相当于一个房间,而指针就是房间的门牌号,我们可以通过门牌号来寻找这个房间。

下面是总结的四种对应关系,方便大家理解。

 内存                 ------     宿舍楼                                    


                                               地址(指针)       ------     门牌号                                                


                                               内存单元         ------     房间


                                               比特位             ------     房间床位

指针操作符及指针变量

取地址操作符&

在c语言中我们创建变量的过程其实就是在向内存申请一片内存空间,以int a = 10为例,我们可以看到a向内存申请了四个字节用于存放整数10。



 知道了a已经从内存处获得了四个地址,那我们就可以通过取地址操作符&来获取a的地址了,但是要注意的是取地址操作符取到的地址是所有地址中最小的那一个地址并不会取出所有的地址。当然,如果我们知道了最小的那个地址,其余地址只需要简单的逐个加一就可以得到。

指针变量

       在上述操作中,我们利用取地址操作符&得到了a的地址,接下来我们为了以后使用该地址,就需要将该地址先储存起来,指针变量就是我们给这个地址找的“家”。

#include <stdio.h>
int main()
{
        int a = 10;
        int* pa = &a;//取出a的地址并存储到指针变量pa中
        return 0;
}

至于为什么叫它指针变量,那是因为它是存放指针的变量,所以叫指针变量。我们还可以观察到的是指针变量pa前有int* 这样的内容。其中int 表示pa指向的对象是int类型,*是在说明pa是指针变量。


另外,如果是这种情况:

Char ch =’m’;

那么指针变量的形式就是:

Char* pa = &ch;

解引用操作符*

我们通过指针变量将地址储存后,想要找到并使用该地址就需要利用解引用操作符*。

我们通过一段代码来理解解引用操作符*:

#include <stdio.h>
int main()
{
int a = 100;    //a向内存申请一片空间,这个空间的值为100,同时也获取了这个空间的地址
int* pa = &a;  //将a申请空间的地址存放在指针变量p中   
*pa = 20;        //*pa就是获取了a申请的空间的地址,*pa=0;就是改变了该空间的值,但是   return 0;       //该空间的地址不发生改变
}

指针变量类型的意义

 指针变量的⼤⼩和它指向的类型⽆关。只要是指针变量,在同⼀个编译器中⼤⼩都是⼀样的,无论指针指向的数据类型是什么,指针变量本身的大小都是固定的。在大多数系统中,指针变量的大小通常是x86环境(32位)下的4个字节或者是x64环境(64位)下的8个字节。那么,为什么还要有各种各样的指针变量类型呢? 其实指针变量类型是有特殊意义的。  

指针的解引 

2段代码,观察在调试时内存的变化。

//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}

我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第个字节改为0。

结论:指针变量的类型决定了,对指针解引⽤的时候可以⼀次操作⼏个字节

我们再来段代码: 

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

代码运的结果如下:



       我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节这就是指针变量的类型差异带来的变化。

结论:指针变量的类型决定了指针向前或者向后⾛⼀步的距离

Const修饰指针

在了解const修饰指针时,我们先引入一个概念:

 我们知道创建临时变量后内存会分出一个内存单元即房间给这个临时变量,而值就是我们在房间中放的东西

然后,我们看这样一段代码:

Int m = 20;
m = 0;                  //输出结果m=0;
在这里我们并未运用指针的只是,只是单纯的对m进行重新赋值,所以
m的地址(门牌号)没有发生改变,但是它的值(空间m中的东西)发生了改变

然后我们再分别运行这样两段代码:

int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//no
p = &m; //ok
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;//ok
p = &m; //no

我们会发现当const处于*不同侧时会有两种不同的情况:

  • 当const在*的左边时,*p=20报错,p=&m正常。此时,不可以修改n的值,但是可以修改所指向的地址,通俗的来讲就是我不能进去n空间但是我可以进去m空间。
  • 当const在*右侧时,我们发现,*p=20正常,p=&m报错。此时,可以修改它的值,但是不可以修改所指向的地址,通俗来讲就是你想动我房间里面的东西是可以的,但是你不能背着我进另一个房间。

!!!只要位于*左侧或者右侧即可,并不要求具体位置!!!

结论: const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变,但是指针变量本⾝的内容可变即可以指向不同的空间。 const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改即不可以指向不同空间,但是指针指向的内容可以通过指针改变。

指针运算

指针+- 整数

这里就引用指针+整数的两个例子,-整数的例子可以自行研究:

 #include <stdio.h>
 int main()
 {
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int *p = &arr[0];
 int i = 0;
 int sz = sizeof(arr)/sizeof(arr[0]);
         for(i=0; i<sz; i++)
                 {
                 printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
                 }
 return 0;
 }
 //在*(p+i)中,随着i的不断++,每次++后都会跳到新的数组元素的所在地址
//至于为什么可以这样,是因为int*类型的指针变量每次加一,就会跳过四个字节也就是一个地址,而数组元素随着下标的不断增大地址是由低到高的,一次跳四个字节并打印就是遍历打印数组元素
//整型数组中每个元素的大小通常是4个字节,具体取决于编程语言和操作系统的规范。在大多数现代计算机体系结构中,整型数据通常使用4个字节来表示,这对应于32位的二进制数。然而,一些编程语言和平台也支持其他大小的整型数据,如8位、16位或64位。因此,确切的整型元素大小可能会有所不同。

同理,对于字符类型指针也是这样的


#include <stdio.h>
int main()
{
    char arr[] = "abcdef";
    char* pc = &arr[0];
    while (*pc != '\0')
    {
        printf("%c ", *pc);
        pc++;
    }
    return 0;
}

指针-指针

   其实,(指针-指针)=(地址-地址),且两个指针必须指向同一空间,得到的值的绝对值,是指针和指针元素之间的个数。

# include <stdio.h>
int my_strlen ( char *s)
{
char *p = s;
while (*p != '\0' )
        {
                p++;
                return p-s;    //计算个数
        }
int main ()
{
printf ( "%d\n" , my_strlen( "abc" ));    //打印my_strlen函数的返回值,结果为3
return 0 ;
}     

指针的关系运算

#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];   //p中存储的初始地址是数组的首元素的地址1
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较,当地址小于arr+sz这个地址时就结束循环 
{
printf("%d ", *p);     
p++;                //指针++,对应的地址也++
}
return 0;
}
/*
 p:变量本身,存的是地址
*p:p指向的对象
*/

野指针

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

野指针的成因

指针未初始化

#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}      //结果报错

指针越界访问

#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}

指针指向的空间释放

#include <stdio.h>
int* test()   //因为返回的是一个地址,所以返回类型应该是int*类型
{
int n = 100;   
/
.....               //一系列操作
/
return &n;     
}
int main()
{
int*p = test();   //用指针变量p接收返回回来的地址
*p=200;//报错
//n出函数释放内存空间,但是p指针仍然指向n的地址,这时如果再使用*p就会出问题,相当于你今天开了个住一晚的酒店房间,但是你第二天走后告诉另一个人这个房间还可以住,你让你朋友去住那个房间。
printf("%d\n", *p);
return 0;
}

如何规避野指针 

指针初始化

如果明确知道指针指向哪就直接赋值地址,如果不知道指针应该指向哪,可以给指针赋值NULLNULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。

初始化如下:

#include <stdio.h>
int main()
{
int num = 10;
int*p1 = &num;
int*p2 = NULL;
*p2 = 100;     //报错
return 0;
}

!!!⼩⼼指针越界!!!

       ⼀段程序向内存申请了哪些空间,通过指针就只能访问这些空间,不能超出范围访问,超出了就是越界访问。

  指针变量不再使⽤时,及时设置NULL,指针使⽤之前检查有效性,当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。


       我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。  

int main ()
{
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
int *p = &arr[ 0 ];
int i = 0;
for (i= 0 ; i< 10 ; i++)
{
         printf("%d ",*p);
         p++;
}
// 此时 p 已经越界了,p指向数组元素10所在地址,如果再使用p++就会造成越界访问,所以将把 p 置为 NULL
p = NULL ;
//...
//...
// 下次使⽤的时候要将NULL更改为想要的,然后判断 p 不为 NULL 的时候再使⽤
p = arr;
if (p != NULL ) 
{
//...
}
return 0 ;
}

Assert断言

格式:assert(表达式)

使用环境:⼀般我们可以在debug版本中使⽤,在release版本中选择禁⽤assert就⾏,在VS的release版本中,直接被优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响⽤⼾使⽤时程序的效率。



关于头文件:


       使用assert断言需要添加头文件#include <assert.h>,如果已经确认程序没有问题,不需要做断⾔,就在#include<assert.h> 前定义一个 NDEBUG。 然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除或者注释掉它,再次运行代码就重新启⽤了 assert() 语句。

#define NDEBUG
#include <assert.h>

作用:反应出错的具体位置

运行条件:括号内表达式结果为真,程序继续运行;表达式结果为假,程序报错终止运行

优点:⾃动标识⽂件和出问题的⾏号

缺点:因为引⼊了额外的检查,增加了程序的运⾏时间,效率降低

适用案例:assert(str != NULL)      ----str为指针

指针的使用和传址调用

 传值调用和传址调

传值调

学习指针的的是使指针解决问题,那什么问题,指针不可呢?

例如:写个函数,交换两个整型变量的值

#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

当我们运代码,结果如下:



我们发现其实没产交换的效果,这是为什么呢?

调试一下试试:



我们发现在main函数内部,创建了a和b,a的地址是0x0000000b6a14ff694,b的地址是0x000000b6a14ff6b4,在调⽤Swap1函数时,将a和b的值传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x000000b6a14ff670,y的地址是0x000000b6a14ff678,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。这种传参就叫传值调用。


结论实参传递给形参的时候,形参相当于一份实参的复制品,形参有一个独立的空间,对形参的修改并不会影响实参

传址调用

       我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就需要使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap函数⾥边通过地址间接的操作main函数中的a和b就好了。


具体代码如下:

#include <stdio.h>
void Swap(int*px, int*py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

输出结果为:


简单综合练习

#include <assert.h>
int my_strlen(const char * str)    //统计所求字符串长度函数,加上const修饰*str,表示不能修改这个字符串,防止修改str的内容,
{
int count = 0;
assert(str != NULL);    //确保了指针的有效性,str为空指针就报错了。
while(*str != ‘\0’)
        {
                count++;     //次数加一
                str++;       //地址加一
        }
return count;   //返回统计的个数
}
int main()
{
char arr[] = “hello bit”;
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}

对于数组名的理解

三种情况下数组名的意义:

*1、数组名就是数组⾸元素地址。

*2、 sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节。

*3、&数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)。

tips:

一、

*1为一般情况,*2、*3为特殊情况

编译器在遇到arr[i]的时候会将它转为*(arr+i)的形式进行计算,所以理论上有这种写法,但不推荐

arr[i]   ==   *(arr+i)    ==  *(i+arr)  ==  i[arr]

同时这也是为什么我们有了数组首元素地址arr和一个[i]就可以通过for循环实现遍历数组元素

二、

1、&arr[0] 和 arr 都是⾸元素的地址。

2、&arr取的是数组的地址,+1 操作是跳过整个数组的。

⼀维数组传参的本质

首先先看一段代码:

void put(int arr[])
{
        int i = 0;
        int sz = sizeof(arr) / sizeof(arr[0]);
        for( i = 0;i<sz;i++)
        {
                printf("%d",arr[i]);
        }
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
put(arr);
return 0;
}

结果为:



   我们发现并不会遍历打印所有数组元素,这是因为我们数组传参的时候传的是首元素地址,至于为什么要形参要写成int arr[]这种数组的形式,是因为这是语法的规定,也是为了方便我们理解,但是实质还是首元素的地址即括号内本质上的形式是(int* arr)。


       在put函数中的sizeof(arr)求的是传过来的一个指针变量(首元素地址)的大小,sizeof(arr[0])计算的是下标为0的元素地址大小。


如果是x64环境下sizeof(arr)大小为8,x86环境下大小为4


所以应该在主函数求数组元素个数之后,将sz以参数的形式传递给Put函数。具体代码如下:


Void Put(int arr[],int sz)    //==  (int* arr,int sz)
{
For(int i = 0;i<sz;i++)
{
Printf(“%d ”,arr[i]);
}
Int main()
{
Int arr[] = {1,2,3,4,5,6,7,8,9,10};
Int sz = sizeof(arr) / sizeof(arr[0]);
Put(arr,sz);
Return 0;
}
}

总结:⼀维数组传参,传递的是数组首元素地址,形参的部分可以写成数组的形式,也可以写成指针的形式

冒泡排序

冒泡排序的核⼼思想就是:(从后往前或者从前往后)两两比较相邻元素的值,若为逆序即(arr[i-1]  >  arr[i] ),则交换它们的位置直至序列比较完,我们称这一过程为冒泡,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置....这样最多做n-1趟冒泡排序就能把所有元素排序。


具体代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdbool.h>
void sort(int arr[], int num)  //冒泡语法函数
{
      for (int i = 0; i <num - 1; i++)        //确定冒泡排序的次数
         {
               bool flag = true;  //设定完成标识
               for (int j = 0; j < num-1-i; j++)
                    {
                      //检测本次冒泡排序是否进行交换
                       if (arr[j] > arr[j+1])        //如果为逆序进行交换
                        {
                        int tmp = arr[j];         //中转变量tmp
                        arr[j] = arr[j + 1];
                        arr[j + 1] = tmp;
                        flag = false;                
                        }
                   }
                     if (flag == true)
                        {
                                break;
                        }
        }
}
void Print(int arr[], int sz)  //打印函数
{
        for (int i = 0; i < sz; i++)
        {
                printf("%d ", arr[i]);
        }
}
int main()
{
int arr[] = {1,2,34,9,6,10,11,3 };
int sz = sizeof(arr) / sizeof(arr[0]);
sort(arr, sz);
Print(arr, sz);
return 0;
}

⼆级指针

Int a = 10;

Int* p = &a;       一级指针变量p

一级指针存放变量的地址

Int** pp = &p;二级指针变量pp

二级指针存放一级指针变量的地址

第一个*表示p的类型:第二个*表示pp是一个指针变量

Int*** ppp = &pp;  三级指针变量ppp

三级指针存放二级指针变量的地址

第一个**表示:第二个*表示pp是一个指针变量

Int前面有几个*就是几级指针变量,一般来说三级变量就是极限了

通过画图可以有更好的理解:



指针数组

学习指针数组之前,我们要知道的是它与我们之前学过的各种数组有着类似的意思,即:

整型数组存放整型的数组  int arr[10]

  字符数组存放字符的数组  char arr[10]

  指针数组存放指针的数组  int * arr[10]

下面,我们通过例子更好的学习指针数组

实例:指针数组模拟⼆维数组

#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[3] = {arr1, arr2, arr3};//将三个整型数组存放在指针数组parr中 
int i = 0;
int j = 0;
        for(i=0; i<3; i++)    //i代表其余类型数组的个数
        {
                for(j=0; j<5; j++)    //j代表该类型数组中元素个数
                        {
                                printf("%d ", parr[i][j]);  // parr[i][j]  == >  *(*(parr+i)+j)
                        }
                printf("\n");
        }
 return 0;
}

二维数组两个括号内代表的意思分别为:

       [要拿地址为多少的那个数组?] [要拿这个数组的中地址为多少的数组元素?]

二维数组的画图演⽰:



上述的代码只是模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的

字符指针变量

我们先看一行代码:

const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?

事实上它的本质是把⼀个字符串常量hello bit的⾸字符 h 的地址存放到指针变量 pstr 中。

我们再通过一道练习题来更深入了解字符指针变量:

#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
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;
}

输出的结果是:



 这是因为C/C++会把常量字符串存储到单独的⼀个内存区域, 当⼏个指针指向同⼀个字符串的时候,他们实际会指向该同⼀块内存。suoyi1,指针变量str3和str4指向的是同⼀个常量字符串,但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。


数组指针变量

首先我们得明白一个概念:

数组指针变量  !=  指针数组

       指针数组是存放指针的数组,数组指针是存放整个数组的地址,而非数组首元素地址。

书写格式:数组类型(指针变量)[数组元素个数] = &数组名

它与字符指针和整型指针类似:

字符指针:指向字符的指针,存放的地址是字符的地址

Char ch = ‘w’;
   Char* pc = &ch;

整型指针:指向整型的指针,存放的地址是整型的地址

int n = 100;
int* p = &n;

数组指针:指向数组的指针,存放的是整个数组的地址

Int arr[10];
Int(*p)[10] = &arr;
//p通过括号先于*结合形成(*p),说明p是一个指针变量,指向的是一个大小为10个整型的数组。

!!!一定要加上(),否则就会变成int* p[10],这是指针数组而不是数组指针!!!

下面我们通过一段代码来更好的理解数组指针:

int main()
{
    int arr[6] = {0};
    int* p = arr;              //p指向数组首元素地址
    int(*ptr)[6] = &arr;    //*ptr是数组指针指向的数组地址,()后面的[]为指向数组的元素个数
    char* ch[8];              //一个char*类型的指针数组
    char* (*P2) = &ch;   //*p2是数组指针
    return 0;
}

⼆维数组传参的方式

实参和形参都是二维数组格式

#include <stdio.h>
void test(int a[3][5], int r, int c)
{
......
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
}

实参为二维数组格式,形参为指针变量格式

#include <stdio.h>
void test(int (*p)[5], int r, int c)        //p指向的就是第一行的一维数组的地址
{
int i = 0;
int j = 0;
for(i=0;i<r;i++)
        {
        for(j=0;j<c;j++)
                {
                        printf("%d ",*(*(p+i) +j)  );        //p增加一就相当于往后找第二个数组地址
                }                          //*(*(arr+i)+j)   ==   arr[i][j]
        }
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);//arr是数组名即首元素地址即第一行一维数组地址
return 0;
}

   对于第一种传参方式我们都能快速写出,但是对于第二种方式是基于我们刚了解过的数组指针的知识才能理解的。关于第二种传参方式的书写思路如下:


①⼆维数组的第一行的每个元素都是⼀个⼀维数组。

②⼆维数组的⾸元素即第⼀⾏是个⼀维数组。

③由于数组名是数组⾸元素的地址,所以⼆维数组的数组名表⽰的就是第一个⼀维数组的地址。

④我们用一个指针变量指向二维数组的数组名,当指针变量逐渐加一即指向的地址逐渐加一,这样就可以实现遍历每一个二维数组中的元素

       所以,⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏的⼀维数组的地址,那么形参也是可以写成指针形式的。


总结:⼆维数组传参本质上也是传递了地址(第一行的一维数组的地址),形参的部分可以写成数组,也可以写成指针形式

函数指针变量

函数指针变量指向的是函数的地址,与数组指针相同,函数名相当于函数的地址名

书写格式:函数类型(指针变量)(函数形参类型) =  函数名

我们接下来看这样一行代码:

Void (* signal(int ,void(*)(int) ) (int)

 首先我们可以理解的是signal是一个函数名,(int , void(*)(int))是signal函数的两个参数,一个是int型,另一个是函数指针类型,该函数指针类型指向的函数参数是int型,返回类型是void,signal(int,void(*)(int))就相当于对signal函数的声明,如果我们把这个函数声明的整体假设为m,那么这行代码就会变成void(* m)(int),*m就是一个新的函数指针,该函数指针指向的函数参数是int型,返回的类型为void型。

typedef关键字

typedef 是⽤来类型重命名的,可以将复杂的类型简单化。

⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:

typedef unsigned int uint;
//将unsigned int 重命名为uint

同样的我们也可以将指针类型重命名,将 int* 重命名为 ptr_t ,这样写:

Typedef  int* ptr_t;


但是对于数组指针和函数指针稍微有点区别,⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:


typedef int(*parr_t)[5]; //新的类型名必须在*的右边

函数指针类型的重命名也是⼀样的,⽐如将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:

typedef void(*pfun_t)(int);//新的类型名必须在*的右边

函数指针数组

什么是函数指针数组?

函数指针数组就是把多个函数的地址存放在一个数组中

书写格式:返回类型(*数组名[函数个数])(形参类型,形参类型) = (函数名,函数名)

通过一个例子我们来更好的理解函数指针数组:

int (*parr1[3])(int,int) = (Add,Sub);

其中,parr1 [3]表示一个有三个元素的数组,数组类型就int (*)(int,int) ,指向(int,int)返回值是int型的函数指针变量,数组元素为Add 和 Sub

为什么要使用函数指针数组?

当函数的返回值一样时为了简化代码可以将函数指针放在指向同一个指针类型的数组中,比如:

int Add(int x, int y)
{
        return x + y;
}
int Sub(int x, int y)
{
        return x - y;
}
int main()
{        
        int* arr[10];
        int (*pArr[4])(int, int) = { Add,Sub };
        return 0;
}

转移表

函数指针数组的⽤途:转移表

计算器的⼀般实现:

#include <stdio.h>
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, y;
int input = 1;
int ret = 0;
do
        {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
         printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        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>
int add(int a, int b)
{
        return a + b;
}
int sub(int a, int b)
{
        r
eturn a - b;
}
int mul(int a, int b)
{
        return a*b;
}
int div(int a, int b)
{
        return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[])(int x, int y) = { 0, add, sub, mul, div }; //转移表
        do
        {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf        (" 0:exit \n");
        printf("*************************\n");
        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("退出计算器\n");
                }
        else
                {
                        printf( "输⼊有误\n" );
               }
         }while (input);  
 return 0;
}

回调函数是什么?

回调函数就是⼀个通过函数指针调⽤的函数。

如果你把函数的地址作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。传递过去,使⽤函数指针接收,函数指针指向什么函数就调⽤什么函数,这⾥其实使⽤的就是回调函数的功能。


//使⽤回调函数改造前

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, y;
  int input = 1;
  int ret = 0;
  do
  {
    printf("******************************\n");
    printf("*    1:add          2:sub    *\n");
    printf("*    3:mul          4:div    *\n");
    printf("******************************\n");
    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;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}

//使⽤回调函数改造后

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;
}
void calc(int(*pf)(int, int))
{
  int ret = 0;
  int x, y;
  printf("输入操作数:");
  scanf("%d %d", &x, &y);
  ret = pf(x, y);
  printf("ret = %d\n", ret);
}
int main()
{
  int x, y;
  int input = 1;
  int ret = 0;
  do
  {
    printf("******************************\n");
    printf("*    1:add          2:sub    *\n");
    printf("*    3:mul          4:div    *\n");
    printf("******************************\n");
    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;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}

qsort函数

//使⽤回调函数改造前先qsort函数是一个库函数,它的具体内容如下:

                                    Void qsort(void* base,------base指向待排序的第一个元素

                                     Size_t num,------待排序的元素个数

                                     Size_t size,------待排序的数组元素大小,单位是字节

 Int (*compar)(const void*,const void*)------compar是一个函数指针,指针指向的函数是用来比较待排序数据中两个元素大小关系的

tips:Void*类型:是通用指针,可以接受任意类型的地址

优点:

1、Qsort函数是库函数,直接可以使用

2、Qsort函数可以排序任意类型的数据,内部使用的快速排序的方法

       现在我们已经基本了解了qsort库函数,我们来写一个基于qsort库函数的冒泡排序,使其能对任意类型进行排序。

//冒泡排序中的交换过程函数
void Swap(char* buf1, char* buf2, size_t width)
{
  int i = 0;
  for (i = 0; i < width; i++)
  {
    char tmp = *buf1;
    *buf1 = *buf2;
    *buf2 = tmp;
    buf1++;
    buf2++;
  }
}
//qsort比较函数
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(const void* e1, const 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);
      }
    }
  }
}
//比较整型元素大小
int cmp_int(const void* e1, const void* e2)
{
  return  *(int*)e1 - *(int*)e2;
}
void print_arr(int arr[], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//排序整型数组
void test1()
{
  int arr[] = { 3,2,5,6,8,7,10,9 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  print_arr(arr, sz);
  bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);   //函数套函数
  print_arr(arr, sz);
}
//声明结构体(以后会写到,现在先用着)
struct Stu {
  char name[20];
  int age;
};
//计算年龄差值于0的关系
int cmp_stu_by_age(const void* e1, const void* e2)
{
  return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//利用strcmp函数比较字符的ASCII码
int cmp_stu_by_name(const void* e1, const void* e2)
{
  return strcmp(((struct Stu*)e1)->name,((struct Stu*)e2)->name);
}
//排序其他类型数组(运行无结果,需要用调试观察arr2)
void test2()
{
  struct Stu arr2[] = { {"zhangsan",15},{"lisi",35},{"wangmazi",32} };
  int sz = sizeof(arr2) / sizeof(arr2[0]);
  qsort(arr2, sz, sizeof(arr2[0]),cmp_stu_by_age);
    //qsort(arr2, sz, sizeof(arr2[0]),cmp_stu_by_name);    
}
int main()
{
  //test1();
  test2();
  return 0;
}


相关文章
|
1月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
56 4
|
2月前
|
存储 C语言 C++
如何通过指针作为函数参数来实现函数的返回多个值
在C语言中,可以通过将指针作为函数参数来实现函数返回多个值。调用函数时,传递变量的地址,函数内部通过修改指针所指向的内存来改变原变量的值,从而实现多值返回。
|
2月前
|
存储 搜索推荐 C语言
如何理解指针作为函数参数的输入和输出特性
指针作为函数参数时,可以实现输入和输出的双重功能。通过指针传递变量的地址,函数可以修改外部变量的值,实现输出;同时,指针本身也可以作为输入,传递初始值或状态。这种方式提高了函数的灵活性和效率。
|
2月前
|
C++
指针中的回调函数与qsort的深度理解与模拟
本文详细介绍了回调函数的概念及其在计算器简化中的应用,以及C++标准库函数qsort的原理和使用示例,包括冒泡排序的模拟实现。
26 1
|
2月前
利用指针函数
【10月更文挑战第2天】利用指针函数。
22 1
|
2月前
|
C++
魔法指针 之 assert断言 传址调用 传值调用
魔法指针 之 assert断言 传址调用 传值调用
28 0
|
2月前
|
存储 C语言
操作多级(一、二、三级)指针才是我们的该有的姿态~
本文通过一道C语言编程题目,详细解析了多级指针的加减操作,包括二级指针和三级指针的使用,以及如何理解指针的地址计算过程,帮助读者巩固和理解指针概念。
69 0
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
117 13
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
37 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
141 4