前言
指针是C语言重要的特色,也是C语言的重难点部分,对于很多人来说也是个痛点。刚接触指针的人,如果没有一个正确的学习方向,学起来是比较痛苦的。而对于接触过的人,有些知识点掌握的可能也是懵懵懂懂的状态。本篇文章共有13节,每一节我都尽可能的去解释好对应的知识点,以及对于一些知识点,我会用图解的方式进行辅助理解。 如果你是初学者,我希望你可以耐心的看完每个知识点,并且一定要理解前面的知识后才去学习后面的知识,千万不可着急。 如果你是接触过指针,可以自行选择想要观看的内容,或者温故而知新即可。
若在阅读过程中,有不理解的地方、或者错误的地方都可以评论或私信我!话不多说,接下来就开始指针之旅!
1、初识指针
我们知道,学校的宿舍楼、酒店的房间通常都有房间号。有了这些房间号,就可以很方便地进行住宿管理。计算机的内存空间就像一栋房,里面有连续的内存空间来存放数据。每个内存空间都有自己的编号,也称为地址。有了这些内存单元的地址,计算机系统就可以快速方便地存储和管理数据。 1.1数据的存储:
了解指针之前,我们须弄清楚数据在内存中是怎样存放的。 计算机中的内存是由很多的单元组成的,而这些单元存储的都是数据,这些单元都是以字节为单位,连续的单元(字节)就是一块内存空间。为了正确的访问这些内存单元(存储的数据),必须给每个内存单元编号,然后通过这些编号即可准确地找到该对应的内存单元。而这些编号也叫作地址,通常也称为指针。如下图: 上图中,数据20存放在a变量中的,当把a的地址赋给一个p变量,此时p变量就称为指针变量!我们可以通过操作p指针变量来间接操作a变量。
1.2指针变量的定义:
指针变量是专门用来存放地址的,所以我们可以通过&(取地址操作符)取出变量在内存的首地址,然后把该地址放到另一个变量中,那么这个变量就是指针变量,即存放地址的变量。
注意:存放在指针中的值都被当成地址处理!
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
int *pc = 10;
return 0;
}
1.3指针变量的使用:
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
*p = 20;
printf("%d",a);
return 0;
}
其中 * 表示间接寻址运算符,如*p就表示找到p所指向的地址,然后对该地址 * 解引用,就找到其内容。 &表示取地址运算符,即取出其地址。可以把 和& 想象成一个是来一个是去,当它们在一起时就会相互抵消。 我们需要注意的是,如果是在声明处 ,* 只是表示这是一个指针,而不是解引用。如下:
int a = 10;
int *p = &a;
1.4指针变量的编址:
一个小的单元是1个字节,且经过计算和权衡我们发现一个字节给一个对应的地址是比较合适的,如下图:
每个地址标识一个单元(1个字节),那我们就可以给 (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB ==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。
所以在32位机器上,一个地址是由32位0或1二进制序列组成的,而1Byte=8bit,那一个地址就需要4个字节的空间来存储。而一个指针变量只能存储一个地址,所以一个指针变量的大小就应该为4个字节。 ( 64位机器可自行计算,原理都是一样的,这里就不展示了)。
1.5总结:
指针变量是用来存放地址的,通过地址可以找到对应的内存单元。 指针的大小在32位平台是4个字节,在64位平台是8个字节。
2、指针类型
我们都知道,变量有不同的类型,如整形,浮点型等,而指针同样也有类型。
普通类型
int a;
float b;
char c;
通过观察就会发现,只要把变量名去掉,剩下的就是类型! 所以同样的,指针的类型我们只需要去掉变量名即可:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int*pa= &a;
int*pb= &b;
}
2.1指针类型的意义: 2.1.1指针类型决定了在对p解引用操作时可以访问多大字节(即可以修改多大字节的空间)。
2.1.2指针类型决定了地址向前( - ) / 向后( + )一次走多少个字节的空间。
无论是指针类型还是什么类型,去掉名字剩下的就是类型。当以后遇到指针或者其他变量、数组、函数等时,我们应该问一问自己类型是什么?。只有明白了类型之后,我们才能更好的使用指针去对其操作。并且类型的理解,对后面的内容体现的是很明显的。
3、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。
3.1未初始化指针
#include<stdio.h>
int main()
{
int* p;
*p =10;
return 0;
}
3.2指针越界访问
3.3指针指向已释放的空间
3.4如何规避野指针:
- 指针初始化:
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
int *p = NULL;
return 0;
}
- 注意指针越界
- 指针指向空间释放,及时置NULL(后面会详细讲更多相关内容)
- 避免返回局部变量(栈空间)的地址(因为栈空间上的一些地址,出了函数空间就被释放了)
- 指针使用之前检查有效性(重):
#include<stdio.h>
int main()
{
int *p=NULL;
printf("%d",*p);
int *p=NULL;
if(p != NULL)
{
printf("%d",*p);
}
int a = 10;
int* p = &a;
if(p != NULL)
{
printf("%d",*p);
}
int* p = NULL;
*p = 10;
int* p = NULL;
int a = 10;
p = &a;
*p = 20;
return 0;
}
以上几点在一定程度上可以避免野指针的出现,但关于如何避免野指针的问题不止这几点,只有我们在写代码的过程中不断的积累经验和学习,功底越来越厚之后,才能更好的避免野指针的出现。
4、指针运算
4.1指针± 整数
4.2指针的关系运算 若指针要进行关系运算,前提是同时指向一块空间。若不是同时指向同一块空间,比较将无意义!
#include<stdio.h>
int main()
{
int Arr1[5];
int Arr2[10];
int* p1 = &Arr1[0];
int* p2 = &Arr2[9];
if(p1<p2)
{
printf("0");
}
int a[5];
int* p1 = &a[0];
int* p2 = &a[4];
if(p1 < p2)
{
printf("1");
}
int arr[5]={1,2,3,4,5};
int *p = &arr[4];
for(p; p >= &arr[0];p--)
{
printf("%d ",*p);
}
return 0;
}
上述的例2,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
图解:
4.3指针 - 指针 1、需同时指向一块连续的空间,否则结果是无法预测的(结果取决于编译器)。 2、指针 - 指针的绝对值是指针和指针之间的元素个数。 3、指针 + 指针无意义,且程序会出现错误。
5、二级指针
指针变量也是变量,它也有自己的地址,而当指针变量的地址存放在另一个变量里时,此时该变量就是二级指针 。
6、指针数组
指针数组是数组,是用来存放指针的数组(存放地址的数组)。
6.1指针数组的声明:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int* parr[2] = {&a,&b};
return 0;
}
6.2指针数组的使用:
以上是指针初级主题的内容,当我们了解了以上内容之后,接下来才能更好的了解下面的高级主题内容。
7、字符指针
字符指针是个指针,是用来存放字符的指针。
7.1字符指针的声明: 7.1.1一般声明:
#include<stdio.h>
int main()
{
char ch = 'h';
char* pc = &ch;
}
7.1.2另外一种声明:
#include<stdio.h>
int main()
{
char* pc = "abcdef";
}
7.2字符指针的使用:
7.2.1一般使用:
#include<stdio.h>
int main()
{
char c = 's';
char* pc = &c;
*pc = 't';
printf("%c",c);
return 0;
}
7.2.2另外一种使用:
#include<stdio.h>
int main()
{
const char* pc = "hello";
printf("%c\n", *pc);
printf("%s", pc);
return 0;
}
图解:
不是说指针存放的是地址吗? 为什么这里存的是字符串?如上述的 pc = “hello”,其实C语言中编译器会给字符串常量分配地址,所以pc = "hello"就是个地址,即本质就是pc = “hello” = 0xffffff40。所以字符指针指向的是首字符的地址,而不是把整个字符串存放进指针。
7.3字符数组和字符指针的区别:
接下来我们来比较一下下面两个看起来很相似的代码。
char str1[] = "hello";
char *Str2 = "hello";
前者是声明str1是一个字符数组,后者是声明Str2是一个字符指针。 上面的这两个声明都可以用作字符串,但需要注意的是,不能错误地认为上面的str1和Str2可以互换或等价,字符数组和字符指针之间是有很大的差别的。
7.3.1区别1:
在声明数组时,就像任意数组元素一样,可以修改存储在数组中的字符。而在声明为字符指针时,指针指向的是字符串常量,而字符串常量是不可修改的。
#include<stdio.h>
int main()
{
char data[7] = "hello";
data[0] = 'E';
const char *Data = "hello";
*Data = "ABC";
return 0;
}
7.3.2区别2:
在声明为数组时,data是数组名(数组名是首元素地址)。在声明为指针时,Data是指针变量,而这个变量可以在程序执行期间指向其他字符串。
#include<stdio.h>
int main()
{
char data[] = "hello";
char *Data = "hello";
Data = "ABC";
return 0;
}
7.3.3区别3:
如果希望可以修改字符串,那么我们可以建立字符数组来存储字符串,或者使字符指针指向一个字符数组,而不是字符指针。
#include<stdio.h>
int main()
{
char data[7] = "abcdef";
char* Data = data;
*Data = "hello"
char* DATA = "AAAAA";
*DATA = "BBBBB";
return 0;
}
7.3.4区别4:
下面的声明虽然使编译器为指针变量p分配了足够的内存空间:
char* p = NULL;
但可惜的是,它不能为字符串分配空间。因为我们没有指明字符串长度,所以在使用指针p作为字符串之前,我们需要先让它指向一个字符数组。
#include<stdio.h>
int main()
{
char str[7] = "ABCDEF";
char* p = str;
return 0;
}
现在指针p指向了str的首字符A的地址,这时可以把指针p作为字符串使用了。另一种是让p指向一个动态分配的字符串(这里就不展示了)。 需要注意的是:使用未初始化的指针变量作为字符串是非常严重的错误。如下:
#include<stdio.h>
int main()
{
char* p;
p[0] = 'A';
p[1] = 'B';
p[2] = 'C';
p[3] = '\0'
return 0;
}
因为指针p没有被初始化,不知道指向了哪里,所以p是个野指针,而用野指针p把字符a、b、c、\0写入内存会导致未定义的行为。
8、数组指针
我们先来了解以下内容:
8.1数组名VS&数组名:
#include<stdio.h>
int main()
{
int arr[5] = {1,2,3,4,5};
printf("%p\n",arr);
printf("%p",&arr);
return 0;
}
注:数组名除了以下两种情况外,剩下的都表示首元素地址。
#include<stdio.h>
int main()
{
int arr[10];
&arr;
sizeof(arr);
}
8.2数组指针的声明:
我们已经熟悉:整型指针、字符指针以及指针数组:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int * i;
char* c;
int* parr[2]={&a,&b};
return 0;
}
那数组指针就是: 能够指向数组的指针,即存放数组地址的指针。(注:不要和指针数组搞混了)
#include<stdio.h>
int main()
{
int arr[5];
int (*pa)[5] = &arr;
return 0;
}
8.3数组指针的使用
8.3.1对一维数组的使用:
#include<stdio.h>
void test(int(*p)[5], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ",(*p)[i]);
}
}
int main()
{
int arr[5]= {1,2,3,4,5};
int sz = sizeof(arr) / sizeof(arr[0]);
test(&arr,sz);
return 0;
}
而数组指针一般很少用于一维数组,更多的是对二维数组的使用。
8.3.2对二维数组的使用:
#include<stdio.h>
void test(int (*p)[5],int r,int c)
{
for(int i=0; i<r,i++)
{
for(int j=0; j<c,j++)
{
printf("%d ",*(*(p+i)+j));
}
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};
test(arr,3,5)
}
图解:
以上就是关于数组指针的内容。接下来我们就来了解数组、指针参数的传参。
9、数组、指针参数的传参
我们在写代码的时候难免要把【数组】或者【指针】传给函数,而函数的参数又该如何设计?
9.1一维数组传参:
#include<stdio.h>
void test_1(int arr_1[3])
{
}
void test_1(int *arr_1)
{
}
void Test_2(int* Arr_2[5])
{}
void Test_2(int **Arr_2)
{
}
int main()
{
int arr_1[3] = {0};
int* Arr_2[5] = {0};
test_1(arr_1);
Test_2(Arr_2);
return 0;
}
如果你还不是很理解上面的内容,不妨格外注意:数组元素的类型,以及对该元素&(取地址)后,需要在该元素原本的类型上加个* 。理解了这句话之后在看一遍试试?
9.2二维数组传参:
#include<stdio.h>
void test(int arr[2][4])
{
}
void test(int arr[][4])
{
}
void test(int arr[][])
{
}
void test(int *arr)
{
}
void test(int* arr[4])
{
}
void test(int (*arr)[4])
{
}
void test(int **arr)
{
}
int main()
{
int arr[2][4]={0};
test(arr);
return 0;
}
9.3一级指针传参:
#include<stdio.h>
void test_1(int *p)
{
}
int main()
{
int n = 10;
int arr[5];
int* p1 = &n;
int* p2 = arr;
test_1(&n);
test_1(arr);
test_1(p1);
test_1(p2);
return 0;
}
9.4二级指针传参:
#include <stdio.h>
void test(int** ptr)
{
}
int main()
{
int a = 20;
int*p = &a;
int **pp = &p;
int *arr[5];
test(pp);
test(&p);
test(arr);
return 0;
}
以上就是关于数组、指针传参的内容,如果看完觉得很难理解,建议回到前面的知识在复习复习。
10、函数指针
我们已经知道,数组指针是用来存放数组地址的指针,那函数指针呢?如果从名字上去分析,那么函数指针就是用来存放函数地址的指针, 其实函数指针就是如此,就是用来存放函数地址的指针。
10.1函数指针的声明:
我们知道一个数组,它的数组名表示首元素地址,&数组名表示数组的地址,那一个函数它的函数和&函数名又是怎样的呢?
#include<stdio.h>
void test(int x, int y)
{
}
int main()
{
int arr[10];
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
printf("\n");
printf("test = %p\n", test);
printf("&test= %p\n", &test);
return 0;
}
图解: 因为函数不像数组会有多少元素,所以函数名是直接表示函数的地址的,所以我们就知道对于函数来说函数名和&函数名意义是一样的,只不过我们写&函数名只是为了让人更容易理解这是函数的地址而已,但两者其实意义都一样。所以我们就可以这样定义:
#include<stdio.h>
int test(char x,char y)
{
}
int main()
{
int (*p1)(char x,char y) = test;
int (*p2)(char,char) = test;
int (*p3)(char a,char b) =&test;
int (*p4)(char,char) = &test;
return 0;
}
我们观察就会发现,其实去掉指针名字,剩下的就是指针类型。去掉指针名字和*,那么剩下的就是函数类型(即所指向对象的类型)。
10.2函数指针的使用:
#include<stdio.h>
int test(int x,int y)
{
return x + y;
}
int main()
{
int sum = 0;
sum = test(2,3);
int (*p)(int,int) = test;
sum = (*p)(2,3);
sum = p(2,3);
printf("%d",sum);
return 0;
}
学习了以上内容后,那下面的函数,函数指针该如何定义?
int test (const int *x,float y)
{
}
解答:
#include<stdio.h>
int main()
{
int (*p)(const int*,float) = test;
int (*p)(const int* x,float y) = test;
}
11、函数指针数组
数组是一个存放相同类型数据的存储空间,去掉:“ 数组名[ ] ” ,剩下的就是数组元素的类型。 我们知道,指针数组是一个数组是:存放指针的数组。
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
int* parr[1] = {p};
return 0;
}
当我们把整型指针数组中的 parr[1](数组) 去掉后,就剩下 int *(指针),而 int *,就是该数组“元素的类型”。而函数指针数组也是如此。
我们需要知道,数组名和&数组名[0]虽然都表示首元素地址,这一特点和:函数名和&函数名都表示函数地址是一样的。虽然都表示是地址,它们的类型是有所不同的。千万不要以为它们都表示地址,那么类型就一样。如下所示:
#include<stdio.h>
int test(int x,int y)
{
}
int main()
{
int arr[10] = {0};
return 0;
}
图解: 学习了上面的内容之后,我们接下来就可以很好的理解函数指针数组了。
11.1函数指针数组的声明:
前面我们说过,指针数组是一个数组是:存放指针的数组。而函数指针数组,其实也是一个数组,是:存放函数指针的数组。 函数指针数组:是存放函数指针的数组,可以存放多个【参数相同、返回类型相同】的函数的地址。
#include<stdio.h>
int test_1(int x,int y)
{
return x + y;
}
int test_2(int n,int m)
{
return n - m;
}
int main()
{
int (*p1)(int x,int y) = test_1;
int (*p2)(int n,int m) = test_2;
int (*parr_1[2])(int x,int y) = {p1,p2};
int(*parr_2[2])(int,int) = {test_1,test_2};
return 0;
}
我们需要理清一点:函数指针数组 parr_2[2],是数组类型是int (*)(int ,int),而决定的元素只能是函数指针。而不是因为数组的元素是函数指针,而决定的数组类型。
11.2函数指针数组的使用:
函数指针数组是数组,所以我们就按照数组的使用方式去访问函数指针就可以了。
#include<stdio.h>
int test_1(int x,int y)
{
return x + y;
}
int test_2(int x,int y)
{
return x - y;
}
int main()
{
int sum = 0;
int(*parr[1])(int,int)={test_1,test_2};
sum = parr[0](3,3);
sum = parr[1](3,1);
printf("%d",sum);
return 0;
}
知道了如何使用后,这里我们就使用函数指针数组来实现一个小的案例:计算器
#include<stdio.h>
int add(int a, int d)
{
return a + d;
}
int sub(int s, int u)
{
return s - u;
}
int mul(int m, int u)
{
return m*u;
}
int div(int d, int i)
{
return d / i;
}
int main()
{
int x, y;
int input = 1;
int sum = 0;
int(*parr[5])(int x,int y) = { 0, add, sub, mul, div };
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
sum = (*parr[input])(x, y);
}
else
{
printf( "输入有误\n" );
}
printf( "sum = %d\n", sum);
}
return 0;
}
上述的案例我们通过函数指针数组来实现会方便和高效许多。以上就是关于函数指针数组的内容。我们接下来就简单的来了解,指向函数指针数组的指针。
12、指向函数指针数组的指针
还记得我们前面所学的数组指针吗?数组指针,也叫指向数组的指针,是存放数组的指针。 而我们知道,函数指针数组也是一个数组,当把函数指针数组存放进一个指针时,就称之为:指向函数指针数组的指针。 我们回顾一下数组指针的声明:
#include<stdio.h>
{
int arr[10];
int(*p1)[10] = &arr;
return 0;
}
我们需要理清楚: 去掉 p,剩下的就表示指针所指向的数组的类型。 去掉p1,剩下的就表示指针所指向的类型。
清楚之后我们就可以来定义,指向函数指针数组的指针了。
12.1指向函数指针数组的指针的声明:
#include<stdio.h>
int test_1(int x,int y)
{
return x + y;
}
int test_2(int x,int y)
{
return x - y;
}
int main()
{
int(*parr[1])(int ,int) = { &test_1, &test_2 };
int (*(*p)[1])(int,int) = &parr;
return 0;
}
如果还是看的不太明白,很大原因可能是因为对类型还不够理解,建议去前面的知识复习复习!
12.2指向函数指针数组的指针的使用:
#include<stdio.h>
int test_1(int x,int y)
{
return x + y;
}
int test_2(int x,int y)
{
return x - y;
}
int main()
{
int(*parr[2])(int ,int) = { &test_1, &test_2 };
int (*(*p)[2])(int,int) = &parr;
int sum = (*p)[0](2, 6);
int ret = (*p)[1](7, 4);
printf("%d\n", sum);
printf("%d\n", ret);
return 0;
}
以上就是关于本节内容,如果对于本节内容不是很理解(当然能理解最好),其实关系也不大,因为我们对指向函数指针数组的指针使用是很少的,也用不着太深究,所以我们这里就大概的知道有这个东西,以后见到不陌生就可以了。 不知道你有没有发现,我们连着的这3节都离不开:函数指针。对于前面函数指针的使用,我们只是简单的介绍和使用而已,而对于函数指针的真正使用,其实是回调函数!
13、回调函数
回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。简单理解就是:把一个x函数的地址作为另一个y函数的参数,当y函数内部调用其x函数时,此时x函数我们就称之为回调函数。
这里我们举个简单的计算器例子:
#include<stdio.h>
int add(int a, int d)
{
return a + d;
}
int sub(int s, int u)
{
return s - u;
}
int mul(int m, int u)
{
return m * u;
}
int div(int d, int i)
{
return d / i;
}
void cale(int(*p)(int, int))
{
int x, y;
scanf("%d %d",&x,&y);
int ret = p(x, y);
printf("%d\n",ret);
}
int main()
{
int input = 0;
scanf("%d", &input);
switch (input)
{
case 1:
cale(add);
break;
case 2:
cale(sub);
break;
case 3:
cale(mul);
break;
case 4:
cale(div);
break;
}
return 0;
}
例如这里的add,cale函数的参数是把add的地址传给指针函数p,我们在cale函数内部,调用p函数时,其实就是调用add函数,而此时add函数就称之为回调函数,其它函数也是同理。 而当我们了解了回调函数之后,我们就来学习一个函数:qsort函数。
13.1psort函数:
qsort是C语言标准库提供的排序函数, 它采用的是快速排序的思想。 我们所熟悉的冒泡排序(不熟悉的可以百度下),它只能排整形数据。而对于qsort函数它任何数据都可以排。这也是两者最大的区别。
我们需要明白,既然是排序,那数据肯定需要先放进一个数组里。 所以我们下面来了解qsort函数的参数和返回类型。
void qsort ( void* base, size_t num, size_t size, int (*compar)(const void*,const void*) );
图解:
qsort函数有4个参数,无返回类型。因为是排序函数,所以前三个参数都是关于数组的。而最后一个参数是函数指针,即函数的地址。所以是把compar函数的地址作为qsort函数的参数,当我们调用qsort函数时,会在qsort函数内部调用compar函数,此时的compar就是回调函数!
因为qsort函数可以排序任意类型,因为设计者不知道我们会排序什么类型的数据,所以在设计之初就设计一个指针来存放任意类型的地址以便我们使用,而void *就是该指针,void * 表示它是个无类型的指针,该指针可以存放任意类型的地址,当想要使用时,强制类型转换后,在解引用即可!如下:
一般情况:只存类型匹配的地址
#include<stdio.h>
int main()
{
int n = 10;
char c = 'r';
int* pa = &n;
char*pc = &c;
pa =&c;
pc =&n;
return 0;
}
void * 情况:可存不同类型的地址
#include<stdio.h>
int main()
{
int n = 10;
char ch = 'w';
double d = 1.11;
void* p1 = &n;
p1 = &ch;
p1 = &d;
*p1 = 3.33;
*(double*)p1 = 2.10;
return 0;
}
了解完每个参数之后,我们就可以来学习qsort函数的定义和使用了。
13.1.1qsort函数的声明和使用:
#include<stdio.h>
int cmp(const void* p1,const void* p2)
{
return (*(int*)p1 - *(int*)p2);
}
int main()
{
int arr[10] = {2,9,6,3,7,1,0,4,8,5};
int sz = sizeof(arr)/sizeof(arr[0]);
qsort(arr, sz, sizeof(int), cmp);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
图解:
以上就是关于int类型的数据的排序,接下来我们来实现对其他类型的数据进行排序。
对char类型排序:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int cmp(const void* p1, const void* p2)
{
return (*(char*)p1 - *(char*)p2);
}
int main()
{
char ch[7] = "yzxbac";
int len = strlen(ch);
qsort(ch, lne, sizeof(char), cmp);
for (int i = 0; i < len; i++)
{
printf("%c ", ch[i]);
}
return 0;
}
这里只是字符排序。想字符串排序的可以自己试试,原理都是一样的。
对结构体类型排序:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
char name[15];
int age;
};
int cmp(const void* p1, const void* p2)
{
return strcmp(((struct Stu*)p1)->name , ((struct Stu*)p2)->name);
}
int main()
{
struct Stu s[3] = { { "zhangsan",15}, {"lisi",20}, {"wangwu",18}};
int sz = sizeof(s) / sizeof(s[0]);
qsort(s,sz,sizeof(s[0]),cmp);
for (int i = 0; i < sz; i++)
{
printf("%s\n",s[i].name);
}
return 0;
}
图解:
以上内容只是举了我们都比较熟悉的类型进行排序,qsort可以排序的数据类型还有很多,但原理都是一样的,如果能理解上面内容,对未来遇到的任意类型的排序,就可以大胆去实现了。
理解了qsort的实现,接下来我们就来模拟实现qsort函数,让它也如同qsort一样对任意类型进行排序。
13.2模拟qsort函数:
对int类型排序:
#include<stdio.h>
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2 ;
}
void Swap(char* ps1,char* ps2,int sz)
{
for (int i = 0; i < sz; i++)
{
char tmp = *ps1;
*ps1 = *ps2;
*ps2 = tmp;
ps1++;
ps2++;
}
}
void My_qsort(void* pa, int All_ele, int sz, int cmp(const void* e1, const void* e2))
{
for (int i = 0; i < (All_ele - 1); i++)
{
for (int j = 0; j < (All_ele - 1 -i); j++)
{
if ( cmp( (char*)pa + j * sz ,(char*)pa + (j + 1) * sz) > 0 )
{
Swap( (char*)pa + j * sz, (char*)pa + (j + 1) * sz, sz);
}
}
}
}
void test()
{
int arr[] = { 4,2,9,8,10 };
int All_ele = sizeof(arr) / sizeof(arr[0]);
My_qsort(arr, All_ele, sizeof(arr[0]), cmp_int);
for (int i = 0; i < All_ele; i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
test();
return 0;
}
图解: 对结构体类型排序:
#include<stdio.h>
#include<string.h>
struct stu
{
char name[15];
int age;
};
int cmp_name(const void* p1, const void* p2)
{
return strcmp( ((struct stu*)p1)->name,((struct stu*)p2)->name );
}
void Swap(char* ps1, char* ps2, int sz)
{
for (int i = 0; i < sz; i++)
{
char tmp = *ps1;
*ps1 = *ps2;
*ps2 = tmp;
ps1++;
ps2++;
}
}
void My_qsort(void* pa, int All_ele, int sz, int cmp(const void* e1, const void* e2))
{
for (int i = 0; i < (All_ele - 1); i++)
{
for (int j = 0; j < (All_ele - 1 - i); j++)
{
if (cmp ((char*)pa + j * sz, (char*)pa + (j + 1) * sz) > 0)
{
Swap((char*)pa + j * sz, (char*)pa + (j + 1) * sz, sz);
}
}
}
}
void test()
{
struct stu s[3] = { {"zhangsan",16},{"lisi",14},{"wangwu",18} };
int All_ele = sizeof(s) / sizeof(s[0]);
My_qsort(s, All_ele, sizeof(s[0]), cmp_name);
for (int i = 0; i < All_ele; i++)
{
printf("%s\n", s[i].name);
}
}
int main()
{
test();
return 0;
}
图解: 以上就是关于模拟qsort的内容,还有很多类型可以模拟实现,原理都是大同小异的。只有当我们掌握了其核心内容,才能游刃有余的使用。关于指针的内容还有很多,当理解了本篇文章的指针后,遇到关于指针的内容,也大概可以看得懂什么意思。当然后续我也会更新一些关于指针的内容。 本篇文章到这里也就结束了,只有我们不断的积累知识,今后才能在在知识的海洋中畅游!
|