10.指针进阶【c语言】【2.7w字长文@@@】


image-20220124121052292

0.前言

虚拟地址与物理地址

虚拟地址经过软硬件(MMU)转化为物理地址

printf("%p\n",&a);
//打印出来的地址是虚拟地址

CPU 产生32位/64位虚拟地址空间,转化为物理地址后,物理地址才能真实找到内存中存储的数据

《深入理解计算机系统》

image-20220116112424716

1.字符指针

两种使用方式

int main()
{
    char ch = 'w';
    char* p = &ch;
    return 0;
}
int main()
{
    char* p = "abcdef";//存储在常量区(只读数据区)
    //p里面放的是字符串首地址
    return 0;
}

image-20220116113033100

字符串abcdef是存储在只读数据区的,无法被修改

常量字符串无法被修改

    char* p = "abcdef";
    *p = 'w';//错误,程序会崩溃

最好加上const,防止误修改导致程序崩溃

//最好加上const修饰*p
int main()
{
    const char* p = "abcdef";
    return 0;
}

面试题

#include <stdio.h>
int main()
{
    char arr1[] = "hello bit.";
    char arr2[] = "hello bit.";
    const char *str1 = "hello bit.";
    const char *str2 = "hello bit.";
    if(arr1 == arr2)
 printf("str1 and str2 are same\n");
    else
 printf("str1 and str2 are not same\n");
       
    if(str1 == str2)
 printf("str3 and str4 are same\n");
    else
 printf("str3 and str4 are not same\n");
       
    return 0; 
}

image-20220116113654665

常量字符串无法被改,所以str1和str2指向的是同一片空间(提高内存利用率)
而arr1 和arr2占用的是两片空间

注意指针定义的写法

int main()
{
    int a,b;//a b都是int类型
    int* pa,pb;//只有pa是int*类型,而pb是int
    int *pa, *pb;//这样才是2个指针
    return 0;
}
//或者利用重定义
typedef int* pint
int main()
{
    pint pa,pb;//此时pa pb都是int*
    return 0;
}
//注意用define不行,#define是替换
#define PINT int*
int main()
{
    PINT pa,pb;//此时pb还是int类型
    //相当于 int* pa,pb
    return 0;
}

typedef是一个新的类型,而define是替换
最好不要连续定义多个指针

2.指针数组

存放指针的数组
数组每个元素都是指针

int* arr1[10]; //存放整形指针的数组
char *arr2[4]; //存放一级字符指针的数组
char **arr3[5];//存放二级字符指针的数组

image-20220116114749361

image-20220116114853029

int main()
{
    char* arr[] = {"abcdef","qwer","zhangsan"};
    int i = 0;
    int sz = sizeof(arr) / sizeof(arr[0]);

    for(i=0; i<sz; i++)
    {
        printf("%s\n",arr[i]);//取到首地址就能打印出来了
        //数组存的每一个字符串首地址
    }
    return 0;
}
//abcdef
//qwer
//zhangsa
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	int* arr[] = {arr1, arr2, arr3};
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);//*(*(arr+i)+j)
            //通过F10调试监视窗口,可以验证
            //arr+1类型是int**	第一次解引用得到的arr2的地址,**(arr+1)得到的是arr2的首元素,(*(arr+i)+1)跳过的是arr2中一个元素的大小,得到的arr2中第二个元素的地址,再次解引用才得到元素值
		}
		printf("\n");
	}

	return 0;
}
//有一种模拟了一个二维数组的感觉
如果&arr,会发现它的类型是int* [3]*
一个指向指针数组的指针
int*(*p)[3] = &arr;

image-20220120175807826

3.数组指针

定义

int* pint;//整型指针,指向整型数据的指针
float* pf;//浮点型指针,指向浮点型数据的指针
int(*p)[10] = &arr;//数组指针,指向数组的指针
数组的地址放到数组指针中去

&数组名VS数组名

int arr[10] = {0};
//数组名表示首元素地址
//&数组名表示整个数组

//&arr+1跳过整个数组
//arr+1跳过一个元素

int(*p)[10] = &arr;//数组指针,指向数组的指针,
前面的int表示数组里面实际放的元素类型
数组的地址放到数组指针中去
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("arr = %p\n", arr);
    printf("arr+1 = %p\n", arr + 1);//跳过4个字节

    printf("&(arr[0]) = %p\n", &(arr[0]));
    printf(" &(arr[0])+1) = %p\n", &(arr[0]) + 1);//跳过4个字节

    printf("arr+1 = %p\n", arr + 1);
    printf("&arr+1= %p\n", &arr + 1);
    system("pause");
    return 0;
}
// arr = 000000e0f57ffd20
// arr+1 = 000000e0f57ffd24
// &(arr[0]) = 000000e0f57ffd20
//  &(arr[0])+1) = 000000e0f57ffd24
// arr+1 = 000000e0f57ffd24
// &arr+1= 000000e0f57ffd48

数组的地址存放起来,放到数组指针里面去
int (*p)[10] = &arr;
类型就是int (*)[10]

总结

  • arr表示的数组首元素地址,类型是int*

  • &arr 表示的是数组的地址,类型是int (*)[10]

  • &arr 的类型是: int(*)[10] ,是一种数组指针类型

  • 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。

int arr[10];
int* p = arr;
int (*p2)[10] = &arr;
p存放的是第一个元素的地址
p+1跳过一个int
*p访问的是一个int数据,4字节


p2存放的是arr数组的地址
p2+1跳过整个数组
*p2访问到的是数组,*p2等价于arr,那么*p2就是数组名,数组名又相当于首元素地址,所以*p2本质是arr数组第一个元素的地址

p 《-----*p2
p+1 === *p2+1

数组指针使用

#include<stdio.h>
int main()
{
	char arr[5];
	char(*pa)[5] = &arr;

	int* parr[6];//指针数组
	int* (*pp)[6] = &parr;//指向指针数组的指针
	//pp类型: int* (*)[6]
	//(*pp) *先与pp结合,说明pp是指针
	return 0;
}

image-20220117114305165

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int*p = arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//1 2 3 4 5 6 7 8 9 10
	}
	return 0;
}
//强行使用数组指针
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *((*p) + i));//1 2 3 4 5 6 7 8 9 10
	}
	//*p表示arr
	return 0;
}
//这么用数组指针非常别扭
//数组指针一般用于二维指针

二维数组

void print(int a[3][5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[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);
	return 0;
}
//传过去的是第一行地址 数组名表示首元素地址,二维数组首元素是第一行
//也就是一维数组的地址  也就是int[5]
void print(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++)
		{
			//*(p+i) 相当于拿到了二维数组的第i行,也相当于第i行的数组名,也就是第i行第一个元素的地址
			printf("%d ", *(*(p + i) + j));
			//               p[i][j]
             //*(p+i) == p[i]
			//p是指针,指针里面放的是第一行的地址
			//p+i是第i行的地址
			//*(p+i) 相当于第i行数组名,是第i行第一个元素的地址
		}
		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就是第一行地址

	return 0;
}

image-20220117115607370

所以传过去的是第一行的地址

*(p+i) == p[i];
arr[i] == *(arr+i) == p[i] == *(p+i);
int main()
{
	int arr[10];
	int i = 0;
	arr[i] == *(arr+i) == p[i] == *(p+i);
	int* p = arr;
	*(p + i);
	return 0;
}
int arr[5];//整型数组
int *parr1[10];//指针数组,有10个int*类型的元素的数组
int (*parr2)[10];//数组指针,指向一个数组,数组是10个元素,每个元素是int类型
int (*parr3[10])[5];//数组,数组每个元素是数组指针
//parr3先和[]结合,说明是一个数组,有10个元素
//元素类型 int (*)[5]  指针指向的数组有5个int类型元素

image-20220117123420547

4.数组参数、指针参数

一维数组传参

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

void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
    //arr2数组每个元素都是int*类型
    //arr2代表首元素地址,首元素类型是int*
    //int*的地址用int**接收
{}
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};//指针数组
    test(arr);
    test2(arr2);
}

二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err 列不能省略
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
void test(int *arr)//err
{}
void test(int* arr[5])//err  arr先与[]结合,说明是数组而不是指针
    //arr表示的第一行数组,传过去的第一行数组(一维数组)的地址,指针接收类型是int(*)[5]
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//err  二级指针是接收一级指针的
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);
    //arr是数组名,数组名本质是首元素地址;二维数组的首元素的第一行,所以二维数组的数组名arr就是第一行的地址

}

二维数组名本质上就是指向第一行的数组指针

int main()
{
    int arr[3][5] = {0};
    int (*p)[5] = arr;
    //第一行数组地址用数组指针接收
}

一级指针传参

#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] = {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 a = 10;
    int* ptr = &a;
    int arr[10] = {0};
    test(&a);
    test(ptr);
    test(arr);
    return 0;
}

二级指针传参

#include <stdio.h>
void test(int** ptr) 
{
    printf("num = %d\n", **ptr); 
}
int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0; 
}

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

void test(char** p)
{}
int main()
{
    char ch = 'w';
    char* p = &ch;
    char** pp = &p;
    char* arr[5];

    test(&p);
    test(pp);
    test(arr);//传指针数组  首元素地址,char*的地址用char**接收

    return 0;
}

5.函数指针

指向函数的指针
函数也是有地址的,函数名(&函数名)就是函数的地址
用函数指针变量存函数的地址

int my_test(const char* s)
{
    printf("测试通过函数指针调用函数\n");
    return 0;
}
int main()
{
    int (*pf)(const char*) = my_test;
    (*pf)("hehe");
    pf("hehe");  //不加*也行的
    return 0;
}
#include <stdio.h>
void test()
{
    printf("hehe\n");
}
int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0; 
}
//拿到的都是函数的地址,函数没有首元素的概念
int Add(int x,int y)
{
    return x+y;
}
int main()
{
    int arr[5];
    int (*pa)[5] = &arr;//数组指针
    int (*pf)(int,int) = &Add;//函数指针
    int (*pf)(int,int) = Add;//这样写也对
    int (*pf)(int x, int y) = Add;//这样写也对

    int sum = (*pf)(2,3);//()函数调用操作符
    return 0;
}

int Add(int x,int y)
{
    return x+y;
}
int main()
{
    int (*pf)(int,int) = Add;//函数指针
    
    int sum = (pf)(2,3);//这样写也对
    int sum = pf(2,3);//()可以去掉
    
    int sum = (*pf)(2,3);//加上*有助于理解
    //不能写成*pf(2,3)  这样是pf先与()结合,然后函数返回值被解引用
    return 0;
}

练习

//代码1 
(*(void (*)())0)();//一次函数调用
void (*)()	函数指针类型
(void (*)())00强制类型转换void (*)()类型的函数指针
    //把0理解成一个数字就行,int类型
    
(*(void (*)())0)()	先解引用,然后再去调用0地址处这个参数为无参,返回类型为void的函数
    //注意:普通用户是没有权限访问0地址的
char * (*c[10]) (int *p)10个元素的数组,每个元素是一个函数指针
    
char (*(*c)[10])(int *p) 数组指针
char (*)(int *p)
一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是char,参数是int*

signal函数声明

//代码2
void (*signal(int , void(*)(int)))(int);//一个函数声明
signal一定先跟()先结合,所以signal是函数
函数后面的()没有传参,说明是一个函数声明 第一个参数类型是int,第二个参数类型是函数指针   
    函数名 参数类型 返回类型
void (*)(int)  这是signal函数的返回类型,也是函数指针
    该指针指向的函数参数是int,返回类型是void
    
    简化:
typedef void(*pfun_t)(int);//pfun_t是函数指针类型的名字
//typedef void(*)(int) pfun_t;这样看更好理解,但不能这么写
pfun_t signal(int, pfun_t);
typedef void(*p)(int);//这时的p是类型名
typedef 是类型重命名
void (*p)(int);//这时的p是函数指针变量名
typedef struct Stu
{
    char name[20];
    int age;
    float score;
}Stu;
//把这个struct Stu的类型重新取名为Stu

《C陷阱与缺陷》

siganl使用

void fun(int num)
{
    printf("fun--->%d\n",num);
}
int main()
{
    void(*signal(int,void(*)(int)))(int);//函数声明
    void(*pf)(int);//函数指针变量名
    pf = siganl(100,fun);
}

6.函数指针数组

指针数组
int* arr[4];//整型指针数组
char* arr[5];//字符指针数组
函数指针数组:
    存放函数指针的数组,每个元素都是函数指针类型
    也是一种指针数组
int (*pfArr[4])(int,int);//pfArr先与[]结合函数指针数组
int (*)(int,int);//函数指针类型

例子

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 (*pf1)(int,int) = Add;//pf1就是一个函数指针
    int (*pf2)(int, int) = Sub;
    int (*pf3)(int, int) = Mul;
    int (*pf4)(int, int) = Div;

    int (*pfArr[4])(int, int) = {Add, Sub, Mul, Div};//函数指针数组,数组的元素类型是函数指针int (*)(int, int)
    int i = 0;
    for (i = 0; i < 4; i++)
    {
        //int ret = (*pfArr[i])(8, 4);
        //通过解引用调用函数也行
        int ret = pfArr[i](8, 4);
        printf("%d\n", ret);
    }
    return 0;
}

计算器

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 menu()
{
    printf("**********************************\n");
    printf("*****  1. add     2. sub     *****\n");
    printf("*****  3. mul     4. div     *****\n");
    printf("*****  0. exit               *****\n");
    printf("**********************************\n");
}

int main()
{
    int input = 0;
    int x = 0;
    int y = 0;
    int ret = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input)
        {
            case 1:
                printf("输入2个操作数:>");
                scanf("%d %d", &x, &y);
                ret = Add(x, y);
                printf("ret = %d\n", ret);
                break;
            case 2:
                printf("输入2个操作数:>");
                scanf("%d %d", &x, &y);
                ret = Sub(x, y);
                printf("ret = %d\n", ret);
                break;
            case 3:
                printf("输入2个操作数:>");
                scanf("%d %d", &x, &y);
                ret = Mul(x, y);
                printf("ret = %d\n", ret);
                break;
            case 4:
                printf("输入2个操作数:>");
                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;
}

缺点:如果函数达到几十个呢?那岂不是要写几十个case语句?

//改进;利用函数指针数组作为转移表
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 menu()
{
	printf("**********************************\n");
	printf("*****  1. add     2. sub     *****\n");
	printf("*****  3. mul     4. div     *****\n");
	printf("*****  0. exit               *****\n");
	printf("**********************************\n");
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;

	int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};//pfArr是一个函数指针的数组,也叫转移表
	
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("ret = %d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);

	return 0;
}

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

int Add(int x, int y)
{
	return x + y;
}
int main()
{
    int (*pa)(int,int) = Add;//函数指针
    int (*pfA[4])(int,int);//函数指针数组
    int (*(*ppfA)[4])(int,int);//ppfA是一个指针,指向存放函数指针的数组
    (*ppfA)[4]说明这是一个数组指针,数组存放的元素类型是int (*)(int,int)
    return 0;
}
//可以继续这样套娃下去,指针又可以放在数组里面
//。。。。。。

计算器

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 menu()
{
	printf("**********************************\n");
	printf("*****  1. add     2. sub     *****\n");
	printf("*****  3. mul     4. div     *****\n");
	printf("*****  0. exit               *****\n");
	printf("**********************************\n");
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("输入2个操作数:>");
			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;
}
image-20220117151113218

非常冗余的代码,非常相似,但又不能封装成一个函数(目前做不到)

//通过calc函数改进
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 menu()
{
    printf("**********************************\n");
    printf("*****  1. add     2. sub     *****\n");
    printf("*****  3. mul     4. div     *****\n");
    printf("*****  0. exit               *****\n");
    printf("**********************************\n");
}

void calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int ret = 0;
    printf("输入2个操作数:>");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("ret = %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;
}
//通过一个函数实现多个功能

8.回调函数

定义

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

上面的calc函数就是利用了回调函数机制

冒泡排序

//冒泡排序
#include<stdio.h>
void BubbleSort(int arr[], int sz)
{
    int i = 0;
    int j = 0;
    //趟数 一趟处理一个数字
    for ( i = 0; i < sz-1; i++)
    {
        //每一趟冒泡排序的过程  
        //10元素 9躺 比较8对
        //需要比较的对数每一趟-1
        for (j = 0; j < sz-1-i; j++)
        {
            if (arr[j+1] < arr[j])//升序
                //如果是字符串排序,不能用><号 strcmp
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

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

只能排序整型数据,怎么改?

qsort函数

基于快速排序算法实现

//qsort函数原型
#include<stdlib.h>
void qsort(
    void* base,//数组起始地址
    size_t num,//元素个数
    size_t size,//元素大小
    int (*compare)(const void* e1, const void* e2)
);//函数指针,指向compar函数

void*

int main()
{
    int a = 10;
    char* p = &a;//编译会报警告,类型不兼容
    //应该这么写:
    void* p = &a;//void*是一种无具体类型的指针,void*的指针变量可以存放任意类型的地址
    //void* 缺点:不能直接进行解引用操作,也不能直接进行+-操作。
    //使用时应该强制类型转换为你需要的类型
}

compare原型

//compare函数原型
int compare(const void* e1, const void* e2);
不同数据比较方法不一样,所以需要特定的比较函数

image-20220117161043396

如果compare返回值小于0(< 0),那么e1所指向元素会被排在e2所指向元素的前面

如果compare返回值等于0(= 0),那么e1所指向元素与e2所指向元素的顺序不确定

如果compare返回值大于0(> 0),那么e1所指向元素会被排在e2所指向元素的后面

如果变量 a指向一个较小的负整型数,b指向一个较大的正整型数,
(*(int*)a - *(int*)b) 表达式会计算出一个正数,这是错误的。
因此,我们不能用减法来比较int的大小,这会产生溢出。取而代之地,我们可以使用大于、小于运算符来比较
 int compare (const void * a, const void * b)
 {
   if ( *(MyType*)a <  *(MyType*)b ) return -1;
   if ( *(MyType*)a == *(MyType*)b ) return 0;
   if ( *(MyType*)a >  *(MyType*)b ) return 1;
 }
//注意:需要将MyType换成实际数组元素的类型。

排序:

  1. 整型数组

  2. 字符数组

  3. 浮点型数组

  4. 结构体数组

整型数组

#include<stdio.h>
#include<stdlib.h>
//比较e1和e2指向的元素
int cmp_int(const void* e1, const void* e2)
{
	/*if (*(int*)e1 > *(int*)e2)
		return 1;
	else if (*(int*)e1 < *(int*)e2)
		return -1;
	else
		return 0;*/

	//可以简写成
	return *(int*)e1 - *(int*)e2;//默认是升序,如果e1和e2调换则能改成降序
}
int main()
{
	int arr[] = { 1,4,2,7,3,5,6,8,9,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

结构体数组

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[20];
	int age;
	float score;
};

//比较e1和e2指向的元素
int cmp_stu_by_score(const void* e1, const void* e2)
{
	//浮点数相减会出现问题

	if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
		return 1;
	else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
		return -1;
	else
		return 0;
}

int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}


int cmp_stu_by_name(const void* e1, const void* e2)
{
	//李四最小 l < w < z
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//strcmp比较的就是对应位置字符ASCII值
}

void print_stu(struct Stu arr[], int sz)
{
	int i = 0;
	for ( i = 0; i < sz; i++)
	{
		printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score);
	}
}
//测试qsort排序结构体数据
void test2()
{
	struct Stu arr[] = { {"张三",20,87.5f},{"李四",21,99.0f},{"王五",22,67.5f} };
	//假设按照成绩来排序
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_score);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	print_stu(arr, sz);
}
int main()
{
	test2();
	return 0;
}

strcmp的返回值
与comcapre的返回值保持一致

image-20220117163346488

字符串指针数组

int cmp_ptr_arr(const void* e1, const void* e2)
{
	//e1 e2是char*的地址,类型也就是char**
	char* a = *(char**)e1;
	char* b = *(char**)e2;
	int ret = strcmp(a, b);
	if (ret > 0)
	{
		return 1;
	}
	else if (ret < 0)
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

void test3()//字符指针数组
{
	char* arr[5] = { "i", "love", "c", "programming", "language" };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_ptr_arr);
	for (int i = 0; i < sz; i++)
	{
		printf("%s\n", arr[i]);
	}
}
int main()
{
	test3();
	return 0;
}

模拟实现qsort

整型数组

用宽度计算偏移量,不需要去管是什么类型的元素

//底层逻辑还是冒泡算法思想
void Swap(char* buf1, char* buf2, int width)
{
	//一对字节一对字节交换
	for (size_t i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

int cmp_int(const void* e1, const void* e2)
{
	/*if (*(int*)e1 > *(int*)e2)
		return 1;
	else if (*(int*)e1 < *(int*)e2)
		return -1;
	else
		return 0;*/
	//可以简写成
	return *(int*)e1 - *(int*)e2;//默认是升序,如果e1和e2调换则能改成降序
}

void bubble_qsort(void* base, int sz, int width, int(*cmp)(const void* e1,const void* e2))
    //非常依赖函数指针
{
	int i = 0;
	int j = 0;
	//趟数
	for ( i = 0; i < sz-1; i++)
	{
		//每一趟冒泡排序的过程  
		//10元素 9躺 比较8对
		//需要比较的对数每一趟-1
		for (j = 0; j < sz-1-i; j++)
		{
			if (cmp((char*)base + j*width, (char*)base + (j+1) * width) > 0)//升序
                //>0表明左边元素大于右边元素,交换了那就变成升序
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_qsort(arr, sz, sizeof(arr[0]), cmp_int);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	test3();
	return 0;
}
cmp((char*)base + j*width, (char*)base + (j+1) * width) > 0
void* base不能直接用,需强制转换,又作者不可能知道传进来的数组元素类型,只能强制转化为(char*)base,这样+1只会+1个字节,方便处理多种类型的数据
相当于把void* based 单位化了,然后才方便使用
传相邻2元素的'地址'到cmp函数里面,并通过cmp函数返回值决定升序or降序
Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
Swap交换函数,是一对字节一对字节进行交换的,就不需要知道数组中元素的类型,用宽度来限制最终交换多少对字节

结构体数组

void Swap(char* buf1, char* buf2, int width)
{
	//一对字节一对字节交换
	for (size_t i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void bubble_qsort(void* base, int sz, int width, int(*cmp)(const void* e1,const void* e2))
{
	int i = 0;
	int j = 0;
	//趟数
	for ( i = 0; i < sz-1; i++)
	{
		//每一趟冒泡排序的过程  
		//10元素 9躺 比较8对
		//需要比较的对数每一趟-1
		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);
			}
		}
	}
}

struct Stu
    //28字节
{
	char name[20];
	int age;
	float score;
};

int cmp_stu_by_score(const void* e1, const void* e2)
{
	//浮点数相减会出现问题

	if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
		return 1;
	else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
		return -1;
	else
		return 0;
}

int cmp_stu_by_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}


int cmp_stu_by_name(const void* e1, const void* e2)
{
	//李四最小 l < w < z
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void print_stu(struct Stu arr[], int sz)
{
	int i = 0;
	for ( i = 0; i < sz; i++)
	{
		printf("%s %d %f\n", arr[i].name, arr[i].age, arr[i].score);
	}
}
//测试qsort排序结构体数据
void test2()
{
	struct Stu arr[] = { {"张三",20,87.5f},{"李四",21,99.0f},{"王五",22,67.5f} };
	//假设按照成绩来排序
	int sz = sizeof(arr) / sizeof(arr[0]);
    //sizeof(arr[0])就是28z
	bubble_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_score);
	//bubble_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
	//bubble_qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	print_stu(arr, sz);
}

int main()
{
	test2();
	return 0;
}

改变cmp函数的返回值就可以改变升序降序

image-20220117172728712

image-20220117172756231

思考角度

排序的作者

1.不知道排序的数据类型是什么
bubble_qsort()
2.比较方法自然不同

使用者

1.待排序的数据类型
2.知道待排序的数据的比较方法

9.练习

sizeof、strlen

求变量(类型)所占空间的大小,单位是字节
sizeof只关注空间大小,不管\0,单位是字节
而strlen关注的是字符串中的\0为止,计算的\0之前出现了多少个字符
strlen只针对字符串
且strlen是库函数,而sizeof是操作符

数组名单独放到sizeof内部或者&数组名才表示整个数组,其他时候都是首元素地址

一维数组

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//整个数组大小 16字节
printf("%d\n",sizeof(a+0));//a表示首元素地址,a+0还是首元素是地址,地址大小是4/8字节
printf("%d\n",sizeof(*a));//a表示首元素地址,*a解引用表示首元素,大小4字节
printf("%d\n",sizeof(a+1));//a表示首元素地址,a+1是第二个元素地址,大小4/8字节
printf("%d\n",sizeof(a[1]));//a[1]表示数组第二个元素,大小是4字节
printf("%d\n",sizeof(&a));//&a数组地址,也是地址,大小就是4/8字节
printf("%d\n",sizeof(*&a));//*&抵消,相当于算的就是数组a的大小 16字节
//或者&a拿到数组地址,类型是数组指针  --》int(*)[4],再解引用,访问的就是一个4个int的数组,大小16字节
printf("%d\n",sizeof(&a+1));//取出整个数组地址,+1跳过整个数组,但指向的仍是地址 4/8字节
printf("%d\n",sizeof(&a[0]));//取出第一个元素地址,大小4/8字节
printf("%d\n",sizeof(&a[0]+1));//第一个元素地址+1,也就是第二个元素地址,大小4/8z
printf("%d\n",sizeof(a+0));//首元素地址

a表示首元素地址,a+0还是首元素是地址,地址大小是4/8字节

字符数组

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//计算整个数组大小 6
printf("%d\n", sizeof(arr+0));//首元素地址+0还是首元素地址,地址大小4/8
printf("%d\n", sizeof(*arr));//arr是首元素地址,解引用就表示首元素 大小1个字节
printf("%d\n", sizeof(arr[1]));//数组第二个元素,大小1个字节
printf("%d\n", sizeof(&arr));//数组地址,大小4/8字节
printf("%d\n", sizeof(&arr+1));//+1跳过整个数组,还是地址,大小4/8字节
printf("%d\n", sizeof(&arr[0]+1));//第一个元素+1,跳过1个字节,指向第二个元素地址,大小4/8字节


printf("%d\n", strlen(arr));//arr是数组首元素地址,找不到\0,计算的是随机值,
printf("%d\n", strlen(arr+0));//仍是随机值
printf("%d\n", strlen(*arr));//*arr是‘a’ 也就是97,strlen需要的是一个地址,strlen会把97认为是起始地址,向后访问,直到找到\0为止,非法访问,程序崩溃  普通用户不能访问地址97,类似访问0地址一样
printf("%d\n", strlen(arr[1]));//把‘b’传过去,同样会内存访问冲突,程序崩溃
printf("%d\n", strlen(&arr));//&arr取出是数组的地址,虽然是数组指针类型 char (*)[6],与strlen的参数类型有所差异,但传过去后就得按strlen的规矩来处理,计算结果还是随机值
printf("%d\n", strlen(&arr+1));//跳过整个数组,也是随机值
printf("%d\n", strlen(&arr[0]+1));//b的地址,也是随机值

strlen(*arr) 非法访问地址97

image-20220224220518303

char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7 包含\0
printf("%d\n", sizeof(arr+0));//地址 4/8字节
printf("%d\n", sizeof(*arr));//arr表示首元素地址,第一个元素大小 1字节
printf("%d\n", sizeof(arr[1]));//1字节
printf("%d\n", sizeof(&arr));//地址 4/8字节
printf("%d\n", sizeof(&arr+1));//跳过整个数组 地址4/8字节
printf("%d\n", sizeof(&arr[0]+1));//第二个元素地址 4/8字节

printf("%d\n", strlen(arr));//找得到\0 6
printf("%d\n", strlen(arr+0));//6  首元素地址
printf("%d\n", strlen(*arr));//‘a’ 程序崩溃 访问非法地址97
printf("%d\n", strlen(arr[1]));//‘b’ 程序崩溃
printf("%d\n", strlen(&arr));//6
printf("%d\n", strlen(&arr+1));//跳过整个数组 随机值
printf("%d\n", strlen(&arr[0]+1));//从第二个元素开始数 5
char *p = "abcdef";
printf("%d\n", sizeof(p));//p是指针变量,大小4/8字节
printf("%d\n", sizeof(p+1));//字符指针+1,跳过一个字节,指向b,仍是地址,4/8字节
printf("%d\n", sizeof(*p));//字符指针解引用访问1个字节,也就是‘a’,大小1字节
printf("%d\n", sizeof(p[0]));//p[0] --> *(p+0) --> *p 1个字节
printf("%d\n", sizeof(&p));//指针变量的地址,4/8字节 二级指针char**
printf("%d\n", sizeof(&p+1));//char* *pp 二级指针+1跳过的是一个char*元素,跳过4/8个字节,得到的仍是地址,大小是4/8字节
printf("%d\n", sizeof(&p[0]+1));//p[0]就是a,a的地址+1得到b的地址,地址大小4/8字节


printf("%d\n", strlen(p));//p里面存的首元素地址,p指向‘a’ 6
printf("%d\n", strlen(p+1));//p+1是b的地址,从b的地址开始求字符串长度,结果是5
printf("%d\n", strlen(*p));//‘a’ 把97传过去了,程序崩溃
printf("%d\n", strlen(p[0]));//‘a’ 程序崩溃
printf("%d\n", strlen(&p));//指针变量p的地址,二级指针,随机数
printf("%d\n", strlen(&p+1));//跳过一整个指针变量 随机值
printf("%d\n", strlen(&p[0]+1));//a的地址+1 得到b的地址,结果为5

image-20220224221552256

printf("%d\n", sizeof(&p+1));char* *pp 二级指针+1跳过的是一个char*元素,跳过 4/8个字节,得到的仍是地址,大小是 4/8字节
&p+1是p的地址+1,在内存中是跳过p变量后的地址

p[0] --> *(p+0) --> *p

二维数组

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));整个数组大小 12*4字节
printf("%d\n",sizeof(a[0][0]));第一个元素 4字节
    
printf("%d\n",sizeof(&a[0]+1));a[0]是第一行的地址,&a[0]还是第一行地址,+1得到就是第二行地址,地址大小4/8字节
printf("%d\n",sizeof(*(&a[0]+1)));相当于第二行,也就是a[1] 大小 4*4字节
printf("%d\n",sizeof(*a));a是二维数组数组名,没有&,没有单独放到sizeof内部,表示的就是首元素地址,也就是第一行元素地址,解引用得到的就是第一行 大小 16字节
*a --> *(a+0) --> a[0]
printf("%d\n",sizeof(a[0]));
a[0]表示第一行数组名,a[0]作为数组名单独放在sizeof内部,计算的第一行数组大小 4*4字节
printf("%d\n",sizeof(a[0]+1));a[0]作为第一行数组名,没有单独放到sizeof内部,因此a[0]表示的就是首元素地址,即a[0][0]的地址,+1就是第一行第二个元素地址,地址大小 4/8字节
printf("%d\n",sizeof(*(a[0]+1)));a[0][0]地址+1,跳过一个整型,也就指向第一行第二个元素,再次解引用,大小就是 4个字节

image-20220118163034318

printf("%d\n",sizeof(a+1));a是二维数组数组名,没有&,没有单独放在sizeof内部,表示首元素地址,即第一行元素地址,a+1就是第二行地址,类型是int(*)[4] 的数组指针,但也是地址,大小4/8字节
printf("%d\n",sizeof(*(a+1)));a+1是第二行地址,解引用找到第二行,也就是a[1],计算的就是第二行的大小, 4*4字节

image-20220118163335909

printf("%d\n",sizeof(a[3]));感觉越界了?但是没关系,sizeof计算时不会真的去计算,在编译时就算出结果了,不会真的去访问a[3],是抽象出类型 算的是int[4]  大小是 16 字节
    
    int b = 0;
    sizeof(int)
   	sizeof(a)
    sizeof a 都能通过运算

指针笔试题

1.

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));//2 5
    return 0; 
}
//程序的结果是什么?
&a+1跳过整个数组
类型是 int(*)[5],强制转化为 int*

2.

struct Test
{
    int Num;
    char *pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节
int main()
{
    p = (struct Test*)0x100000;//p是一个指针而0x100000是一个整型,因此需要
    printf("%p\n", p + 0x1);//+1跳过20个字节
    //0x100014
    printf("%p\n", (unsigned long)p + 0x1);//正数+1就是正常的+1
    //0x100001
    printf("%p\n", (unsigned int*)p + 0x1);//+1跳过4个字节
    //0x100004
    return 0; 
}

image-20220118171738494

%p 以地址形式打印
%x 以地址形式打印,但会忽略前面的0

3.

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);//4,2000000
    return 0; 
}
ptr1[-1]
*(ptr1+(-1))
*(ptr1-1)
a数组,按小端存储,低位放在低地址
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00
数组名表示首元素地址
(int)a,把a的地址强制转换成'整型',整数+1就偏移1个字节
(int)a + 1偏移1个字节
所以ptr2指向的就是01后面的那个 00
00 00 00 02再以小端取出来
02 00 00 00
前面的0%x打印会省略,所以最终打印2000000

image-20220118172519038

如果以%p打印:

image-20220118172902416

4.

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    //里面是逗号表达式,注意是()而不是{}
    int *p;
    p = a[0];
    printf( "%d", p[0]);//1
 return 0; 
}

5.

int main()
{
    int a[5][5];
    int(*p)[4];//一个数组指针,p能够指向的数组是4个元素
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0; 
}
p+116个字节,跳过4int
p[4][2] --*(*(p+4)+2)
p+4之后解引用找4int元素
地址相减得到的中间元素个数
小地址-大地址得到是负数,也就是-4%p形式打印,直接打印的就是'补码'
10000000 00000000 00000000 00000100
11111111 11111111 11111111 11111011
11111111 11111111 11111111 11111100
ff	     ff       ff       fc

"注意"此处无需管大小端存储问题,因为小端存进来还是得小端拿出来,拿出来的数和存进去的是一样的

image-20220118174522976

%p打印-4得到只是-4的16进制显示而已,与大小端无关

image-20220118180511178

6.

int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1);
    int *ptr2 = (int *)(*(aa + 1));
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
    return 0;
}
&aa+1跳过整个数组,ptr1指向的10后面的元素
aa+1跳过的第一行,再解引用得到第二行数组名
ptr2指向的就是6

7.

#include <stdio.h>
int main()
{
 char *a[] = {"work","at","alibaba"};
 char**pa = a;//数组名是首元素地址,char*的地址得用char**接收
 pa++;//指向第二个char*,第二个char*指向的是at
    //第2个char*里面存放的是at的地址
 printf("%s\n", *pa);//at
 return 0; 
}

image-20220119143913066

8.

int main()
{
 char *c[] = {"ENTER","NEW","POINT","FIRST"};
 char**cp[] = {c+3,c+2,c+1,c};//c是char*,char*地址用char**存放
 char***cpp = cp;//cp是char**,char**的地址用char***存放
 printf("%s\n", **++cpp);//++之后cpp指向c+2,第一次解引用得到的c+2,c+2存的c中第三个元素地址,再次解引用得到就是c中第三个元素存的东西,也就是POINT\0的地址
//结果就是POINT
    
 printf("%s\n", *--*++cpp+3);//先算++cpp,此时cpp指向c+1,第一次解引用得到的cp中第三个元素的内容,也就是c+1,再执行*--,先--使得c+1变成c,指向的是c中第一个元素,,再解引用,得到c中第一个元素内容,也就是ENTER\0的首元素地址,最后再+3,得到的就是ER
    
 printf("%s\n", *cpp[-2]+3);//**(cpp-2)+3
    //cpp-2指向的就是c+3,第一次解引用得到的c+3,再次解引用得到的就是FIRST\0的地址,最后+3,指向的就是ST,最终打印的就是ST
    
 printf("%s\n", cpp[-1][-1]+1);//*(*(cpp-1)-1)+1
    //cpp-1指向的就是c+2,解引用得到c+2,再-1就变成c+1,再次解引用得到的就是NEW\0地址,+1指向的就是EW\0
//z
 return 0; 
}

image-20220119144940653

image-20220119145520389

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值