C语言之漫谈指针(下)

C语言之漫谈指针(下)

在上节我们讲到了一些关于指针的基础知识:

详见:C语言之漫谈指针(上)

本节大纲:

  • 零.小tips
  • 一.字符指针
  • 二.指针数组数组指针
  • 三.数组传参与指针传参
  • 四.函数指针及函数指针数组
  • 五.回调函数
  • 六.例题讲解

 

零.小tips

在正式开始下节之前,我们先来穿插两个小tips:

1.打印函数哪家强!

//假设有下面两个打印函数,我们应该首选哪个?
struct person
{
    char name[20];
    int age;
    char sex[5];
    char tele[13];
    char addr[20];
};


void print_1(struct person peo)
{
    printf("This is print_1\n");
    printf("%s\n", peo.name);
    printf("%d\n", peo.age);
    printf("%s\n", peo.sex);
    printf("%s\n", peo.tele);
    printf("%s\n", peo.addr);
    printf("\n");
}

void print_2(struct person* peo)
{
    printf("This is print_2\n");
    printf("%s\n", peo->name);
    printf("%d\n", peo->age);
    printf("%s\n", peo->sex);
    printf("%s\n", peo->tele);
    printf("%s\n", peo->addr);
    printf("\n");
}

int main()
{
    struct person peo = { "zhangsan",18,"male","12345678","hualalalala" };
    print_1(peo);
    print_2(&peo);
    return 0;
}

在上述两个打印函数,我们应该首选 print_2() 函数

我们先来看看两个函数的传参是什么:

void print_1(struct person peo):参数是整个结构体

void print_2(struct person* peo):参数是该结构体的地址

我们不妨想一想当我们要传递的值非常多时,如果采用传递变量自身的模式,

变量传递到函数里将会产生一份临时拷贝,那会将使用多少的内存,且该内存的使用毫无意义,仅仅只是为了打印,这并不划来

而方式二采用传地址的方式,内存不仅使用的更少了一些,而且效率也变得更高!

2.  *  与  [ ] 的优先级关系:

如图:

 

 

 我们看到 [ ]运算符的优先级是高于 * 的!

如 int* arr[3]   这个arr首先于[3]结合再与*结合。

 

一.字符指针

在上节的指针类型中,我们见到了一种为char*的指针,他指向的空间内容为 char 型,且解引用时只能解引用1个字节。

并且我们一般的用法如下:

int main()
{
    char ch = 'a';
    char* p = &ch;
    *p = 'w';
    return 0;
}

但是如果我们这样写 char* p = "abcdef"; 它算不算字符指针呢?

我们可以解引用打印一下: printf("%c\n", *p); 我们会发现它打印了一个字符 a 

所以 char* p = "abcdef"; 这种类型也算字符指针,它指向的是首元素的地址

也就是说把 a 的地址放在了 p 中

但是我们发现当我们这样写的时候 printf("%s\n", p); 运行结果会出现整个字符串,那这是为什么呢?

 

 

 那我们继续回到刚才的话题,既然 char* p = "abcdef";  是一个字符指针,那我们可不可以对放在里面的值进行修改呢?

我们试试看:

int main()
{

    char* p = "abcdef";
    printf("%s\n", p);

    *p = 'c';

    return 0;
}

我们运行时会发现编译器运行到一半会崩,并弹出: 写入访问权限冲突。的错误。

这又是为什么呢?

这又得回到我们上节所提到的计算机储存器的一些知识了

见图:

 

 

 所以我们要想修改,应该怎么做?

如果想要修改字符串的内容,就需要对它的副本进行操作。如果在存储器的非只读区域创建了字符串的副本,就可以修改它的字母了。

简而言之:创建一个字符数组来接收它即可

int main()
{

    char ch[] = "abcdef";
    printf("%s\n", ch);


    *ch = 'c';
    printf("%s\n", ch);
    return 0;
}

我们会发现运行结果为:

abcdef

cbcdef

我们同样再来看一下原理:

 

 

[tips]:  

  所以若我们要写出 char* p = "abcdef"; 这样的字符指针,最好在前面加一个 const 修饰符

  即: const char* p = "abcdef"; ,因为 p 指向的内容不可修改。

 

有了以上的知识,我们来看一道题:

#include <stdio.h>
int main()
{
    char str1[] = "hello world.";
    char str2[] = "hello world.";
    char* str3 = "hello world.";
    char* str4 = "hello world.";

    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;
}

这道题的结果是什么呢?

我们来分析一下:

 

 

 所以,运行结果会是:

str1 and str2 are not same
str3 and str4 are same

我们来验证一下:

 

 

 

二.指针数组与数组指针

1.指针数组

在上节中我们便提到了指针数组的概念,我们再次来复习一下

字符数组:存放字符的数组

整形数组:存放整形的数组

指针数组:存放指针的数组

比如:

int main()
{
    int a = 1;
    int b = 2;
    int c = 3;

    int* arr[3] = { &a,&b,&c };//arr便是一个指针数组

    return 0;
}

对于一个单纯的指针数组并没有太多的知识

2.数组指针

数组指针,末尾两字为“指针”,所以它就是个指针,用来指向数组的指针。

那它怎么表示呢? int (*p2)[10]; 

我们可以在这解释一下:

 

 

 我们还可以这样理解:

 

 

 3.数组名与&数组名

在上节课中,我们举过这样的一个例子:

#include <stdio.h>
int main()
{

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

    printf("%p\n", arr);

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

    return 0;
}

我们当时发现他们俩的地址相同,于是我们下了一个结论:数组名表示的就是数组首元素地址

那么数组名与&数组名呢,我们看看下面这个例子

#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

我们发现结果也是相同的,但我们能不能所这两个东西是一样的?

我们将上面的代码微做调整:(我们分别再进行加一)

#include <stdio.h>
int main()
{

    int arr[10] = { 0 };

    printf("arr = %li\n", arr);
    printf("&arr= %li\n", &arr);

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

    return 0;
}

注:为了方便观察地址值的差异,笔者在这用 %li 来打印

 

我们会发现,结果并不一样,这就说明,它们俩并不是一个东西;

那它俩究竟有何不同呢?

  &arr 表示的是数组的地址,而不是数组首元素的地址,加1就跳过整个数组

  arr表示的是数组首元素的地址,加1跳过1个元素,到数组的第2个元素

所以我们来看看结果:

 

 

 arr与arr+1刚好差4个字节

而&arr与&arr+1刚好差 我们所定义的 一个有10个整形的数组的大小即40个字节

所以:到这大家明白它俩的区别了吗?

4.指针[整数]与  *(指针±整数)

对此我们看一个例子:

int main()
{
    int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
    int* p = arr;
    int i = 0;
    printf("指针[整数]---- *(指针±整数)\n");
    for (i = 0; i < 10; i++)
    {
        printf("     %d    ----      %d   \n",p[i],*(p+i));
    }

    return 0;
}

运行结果:

 

 

 我们发现    指针[整数]  与  *(指针±整数)  的效果相同

 

此外再提一点,指针地址的强制类型转换:

  我们都知道,指针的类型决定了指针一次可以解引用几个字节,所以指针的强制类型转换只是改变了该指针一次可以解引用几个字节!

如:

#include<stdio.h>

int main()
{
    double a = 1;//首先 dp  ip  cp 储存的地址相同

    double* dp = &a;//只是 dp 解引用可以访问8个字节,所以dp+1跳过8个字节

    int* ip = (int*)&a;// ip 解引用可以访问4个字节,ip+1跳过4个字节

    char* cp = (char*)&a;// cp 解引用可以访问1个字节,cp+1跳过一个字节

    return 0;// 此外,并无区别
}

 

对上述知识我们先来看一道题:

int main()
{

    int a[5] = { 1, 2, 3, 4, 5 };

    int* ptr = (int*)(&a + 1);

    printf("%d,%d",*(a + 1), *(ptr - 1));

    return 0;
}
//程序的结果是什么?

  1.首先a是一个数组名,也就是数组的首元素地址,加一,跳过一个元素,指向数组的第二个元素,此时再进行解引用,所获得的值就是数组的第二个元素

   所以*(a+1)的值就是 2;

  2.ptr=int*(&a+1),首先这个数组名加了 & 符号,所以它加一就跳过整个数组然后将其转为int*,最后再减一,此时指针的类型为 int * ,所以指向的位置就再往前走一个int 的位置,

  指向数组的最后一个元素,所以*(ptr - 1)的值就为 5;

 

然后,我们再来看这一道题:

int main()
{
    int a[4] = { 1,2,3,4 };

    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);

    printf("%x %x\n", ptr1[-1], *ptr2);
    return 0;
}

它的结果是什么呢?

  1.首先&a+1就跳过整个数组,然后ptr[-1]就是指跳过整个数组之后,再向前移一位,也就是数组元素4,

   所以ptr[-1]就是 4 

  2.对于ptr2来说,我们先看  int(a) + 1,这部分,首先a是一个数组名,那它就是数组首元素的地址,总之就是一个地址,现在将他强制类型转换为 int ,再加一,此时就是简简单单的加一

   然后又将其强制类型转换为 int* 型,应为再强转之前加了一,所以现在指向的是数组首元素的第二个字节,然后按照你编译器的大小端模式所储存的内存,再往后读取3个字节,

   再按找大小端模式拿出来,就是*ptr2的值

 

对于第二点的一些疑问,笔者整理了一张图:

 

 

 这样就应该容易理解第二点了。对于大小端的储存模式详见:C语言之数据在内存中的存储

 

5.数组指针的使用

再学习了概念之后,我们就开始正式使用了:

#include <stdio.h>
int main()
{

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

    int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p

    //但是我们一般很少这样写代码

    return 0;
}

 

使用,这里我们用到的例子是:依据  C语言之三字棋的实现及扩展 改编的一些函数;

如我们的棋盘打印函数:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)//写成这种形式,大家肯定都能明白,但是写成下面那样呢?
{

    int i = 0;
    int j = 0;

    for (i = 0; i < row; i++)
    {

        for (j = 0; j < col; j++)
        {

            printf("%d ", arr[i][j]);
        }

        printf("\n");
    }
}
void print_arr2(int(*arr)[5], int row, int col)//首先arr是一个数组指针,它指向的是一个有五个 int 元素的数组,所以现在的arr就相当于二维数组第一行的地址
{//那么,arr+1 便表示第二行首元素的地址,以此类推

    int i = 0;
    int j = 0;

    for (i = 0; i < row; i++)
    {

        for (j = 0; j < col; j++)
        {
       //printf("%d ", *(*(arr+i)+j));//他们俩效果相同
            printf("%d ", arr[i][j]);//这里之前已经说了,指针[整数] = *(指针±整数)
        }

        printf("\n");
    }
}
int main()
{

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

    print_arr1(arr, 3, 5);

    //数组名arr,表示首元素的地址

    //但是二维数组的首元素是二维数组的第一行

    //所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址

    //可以数组指针来接收

    print_arr2(arr, 3, 5);

    return 0;
}

 在此之后,我们在辨认一下下面分别都是声明类型

int arr[5];//数组,大小为5,内容为int
int* parr1[10];//数组,大小为10,内容为int*
int(*parr2)[10];//指针,指向一个大小为10,内容为int的数组
int(*parr3[10])[5];//对于这个,似乎就麻烦了一点
                   //我们把它分开来看,int(*)[5] 是类型,指元素为5个元素指针数组
                   // parr3[10]就是它的名称
            //换种写法就是  
//typedef int(*foo)[5];
//foo parr3[10];

而且,在这有一个问题:我们可以这样写吗? parr2 = &parr1; 

我们再来看一下:

parr1里放的是int*,而parr2里放的是int,两者类型不一样,所以当然不可以这样写

要是parr2这样写便可以了 int*(*parr2)[10] 

 

三.数组传参与指针传参

接下来就是传参了,

要是只在主函数里这样改改去去,那多没意义;我们要做的就是写一个函数,在函数里进行改变。

1.一维数组传参

C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。

注意数组传参主函数里要传数组名

函数里可以写成指针的形式也可以写为数组的形式,注意[ ]里可以不写大小,

实际传递的数组大小与函数形参指定的数组大小没有关系。

例:

#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 };

    test(arr);

    test2(arr2);
}

 

 

2.二维数组传参

C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。

这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维再也不可改写。

比如:a[3][4][5]作为参数时可以被改写为(*p)[4][5]。二维数组传参是要注意函数形参的设计只能省略第一个[]的数字。

void test(int arr[3][5])//√
{}
void test(int arr[][])//×
{}
void test(int arr[][5])//√
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。

void test(int* arr)//×
{}
void test(int* arr[5])//×
{}
void test(int(*arr)[5])//√
{}
void test(int** arr)//×
{}
int main()
{

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

    test(arr);
}

 

3.一级指针传参

一级指针传参主要是用来传数组首元素的地址,以及需要改变的值(如之前提到的swap()交换俩整数的值)

所以我们遇到函数时就要想想它都可以传什么值进去

void test1(int* p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?

 

4.二级指针传参

 同样二级指针也是如此;

平常写函数时就要想想这种函数除了可以传我现在需要的变量类型,还可以传什么类型的变量,如此下去我们对于传参的理解肯定会愈来愈高!

四.函数指针及函数指针数组

1.函数指针

1.函数指针的定义

正所谓函数指针:那就是指向函数的指针变量

我们先来看一个函数指针长什么样:

char* (*fun1)(char* p1, char* p2);

fun是它的名字

char* 是它所指向函数的返回类型

(char* p1, char* p2)是它所指向的函数参数

 

2.函数指针的使用

现在我们知道了它长什么样子,那我们现在来使用一下它

void print()
{
    printf("hehe\n");
}

int main()
{
    void(*p)() = &print;//p是一个函数指针,所指向的函数无参无返回值

    (*p)();

    return 0;
}

我们发现,屏幕上出现了 hehe ,这就是它的一个基本使用

当然在这  void(*p)() = &print; 赋值的时候,可以不必写&号;

这是因为函数名被编译之后其实就是一个地址,所以这里两种用法没有本质的差别。

在这看一个例子:

void test()
{
    printf("hehe\n");
}

int main()
{
    printf("%p\n", test);
    printf("%p\n", &test);
    return 0;
}

我们发现他俩打印之后的结果相同;

同样,在函数调用的时候, (*p)(); 也可以不用写 * ;

(你见过谁调用函数的时候还带个 * )

即我们在写的时候,这两种其实都可以

void print()
{
    printf("hehe\n");
}

int main()
{
    void(*p)() = &print;
    void(*ch)() = print;

    p();
    (*ch)();

    return 0;
}

接下来我们再来看一个函数指针:

(*(void(*) ())0)()

乍一看,好复杂,我们来仔细分析一下

 

 

 这样是不是就清楚了许多呢

我们再来看一个:

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

我们再来分析一下:

这是一个函数声明

声明的函数叫signal,signal函数有2个参数,第一个参数类型是int,  第二个参数类型是一个函数指针,

该函数指针指向的函数参数是int,返回类型是void

signal函数的返回来类型是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

我们也可以把上面那个函数声明这样写:

typedef void(*pfun_t)(int);//类型为指向一个参数为int,无返回值的函数指针
pfun_t signal(int, pfun_t);//用上述类型,声明了一个函数

这样是不是明了了许多

2.函数指针数组

函数指针数组那然是储存函数指针的数组了啊

我们来看一个例子:

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

int Sub(int x, int y)
{
    return x - y;
}

int main()
{

    //函数指针的数组
    int (*pf1)(int, int) = Add;
    int (*pf2)(int, int) = Sub;
    int (*pf)(int, int);//函数指针

    int(* pfA[4])(int, int);//函数指针的数组

    //函数指针数组

    //pfArr2就是函数指针数组,数组的类型为 int(*)(int,int)
    int (* pfArr[2])(int, int) = { Add, Sub };

    return 0;
}

在此之上,我们再来回忆一下用多分支写出的一个简易计算器

//计算器 - 加、减、乘、除
void menu()
{
    printf("****************************\n");
    printf("**** 1. add   2. sub    ****\n");
    printf("**** 3. mul   4. div    ****\n");
    printf("**** 0. exit            ****\n");
    printf("****************************\n");
}

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

int Sub(int x, int y)
{
    return x - y;
}

int Mul(int x, int y)
{
    return x * y;
}

int Div(int x, int y)
{
    return x / y;
}

//函数传参-函数指针
//回调函数

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

我们会发现,在switch下出现了许多赘余的语句,我们来用函数指针来改写一下:

void menu()
{
    printf("****************************\n");
    printf("**** 1. add   2. sub    ****\n");
    printf("**** 3. mul   4. div    ****\n");
    printf("**** 0. exit            ****\n");
    printf("****************************\n");
}


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

int Sub(int x, int y)
{
    return x - y;
}

int Mul(int x, int y)
{
    return x * y;
}

int Div(int x, int y)
{
    return x / y;
}

int main()
{
    int input = 0;
    int x = 0;
    int y = 0;
    int ret = 0;
    //函数指针数组 - 转移表
    int (*pfArr[])(int, int) = { 0, Add, Sub, Mul, Div };

    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);//1
        if (0 == input)
        {
            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;
}

这样是不是简洁了不少呢

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

指向函数指针数组的指针,说白了,它就是个指针,所指向的内容为函数指针数组

int main()
{
    //函数指针
    int(*p)(int, int);
    //函数指针的数组,数组元素类型为 int(*)(int, int)
    int(*pArr[4])(int, int);
    //ppArr是指向函数指针数组的指针
    int(*(*ppArr)[4])(int, int) = &pArr;

    return 0;
}

这里给一个使用

//这里的函数作用都是讲传递的字符串给打印出来
char* fun1(char* p)
{
    printf("%s\n", p);
    return p;
}

char* fun2(char* p)
{
    printf("%s\n", p);
    return p;
}

char* fun3(char* p)
{
    printf("%s\n", p);
    return p;
}
int main()
{
    char* (*a[3])(char* p);//定义一个函数指针数组a,数组元素类型为char(*)(char*p)
    char* (*(*pf)[3])(char* p);//定义一个指向函数指针数组的指针,所指向的数组类型为 char*(*)(char*p)

    pf = &a;//将a的地址赋予pf

    //分别赋值
    a[0] = fun1;
    a[1] = &fun2;
    a[2] = &fun3;

    //分别使用
    (*pf)[0]("fun1");
    pf[0][1]("fun2");
    pf[0][2]("fun3");
    return 0;
}

注:

  函数指针不允许 ± 运算

五.回调函数

1.回调函数

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

 

如果学习过python装饰器的同学,或许对这个概念有点熟悉

我们先用python来写一个装饰器:

import time
def timing_func(f):
    def wrapper():
        start = time.time()
        f()
        stop = time.time()
        return (stop - start)
    return wrapper


def fun1():
    print("lalala")
    time.sleep(1)
fun1=timing_func(fun1)

@timing_func
def fun2():
    print("hehehe")
    time.sleep(1)

print(fun1())
print(fun2())

 

 

 

接下来我们再看 c 的回调函数的一个例子

#include<stdio.h>
#include<time.h>
#include <Windows.h>

void print()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", i);
        Sleep(100);
    }
    printf("\n");
}

void print_time(void (*fun)())
{
    SYSTEMTIME tm;

    GetLocalTime(&tm);
    printf("函数开始前的时间为:\n%d-%d-%d %d:%d:%d:%d\n", tm.wYear, tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds);

    printf("\n函数执行中……\n");
    fun();

    GetLocalTime(&tm);
    printf("\n函数结束后的时间为:\n%d-%d-%d %d:%d:%d:%d\n", tm.wYear, tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds);
}

int main()
{
    print_time(print);

    return 0;
}

 

 

 

 

2.qsort

  qsort便是一个用到了回调函数的库函数

首先qsort是一个用来排序的函数:

我们来看看人家的声明:

 

 

 最后的 int(*compar)(const void*, const void*)便是一个我们使用时,需要传递的函数指针

 它的返回值为int ,参数为两个void*  ,使用我们设置函数是,要和它的类型一致

 接下来我们继续来看看它各个参数的含义:

 

 

 以及要注意的一点:

   使用时要包含 stdlib.h  这个头文件 

 接下来看他的一个使用:

#include <stdio.h>      
#include <stdlib.h>    

int values[] = { 40, 10, 100, 90, 20, 25 };

int compare(const void* a, const void* b)
{
    return (*(int*)a - *(int*)b);
}

int main()
{
    int n;
    qsort(values, 6, sizeof(int), compare);
    for (n = 0; n < 6; n++)
        printf("%d ", values[n]);
    return 0;
}

在这个例子中,我们要注意的就是在compare所指向的函数中,我们要将两个参数进行强制类型转换为我们要排序的类型

接下来就举几个例子:

#include<string.h>

struct stu  //假定一个结构体,来写它各成员类型的排序函数
{
    int num;
    char name[20];
    int score;
};

int int_compare(const void* _1, const void* _2)//进行 int 型的比较
{
    return (*(int*)_1) - (*(int*)_2);
}

int char_compare(const void* _1, const void* _2)// 进行char型的比较
{
    return (*(char*)_1) - (*(char*)_2);
}

//进行我所自定义结构体各成员元素的排序
int stu_cmp_num(const void* _1, const void* _2)//按照  num 来排序
{
    return ((struct stu*)_1)->num - ((struct stu*)_2)->num;
}

int stu_cmp_score(const void* _1, const void* _2) //按照 score 来排序
{
    return ((struct stu*)_1)->score - ((struct stu*)_2)->score;
}

int stu_cmp_name(const void* _1, const void* _2) // 按照 name 来排序
{
    return strcmp(((struct stu*)_1)->name, ((struct stu*)_2)->name);
}

 

六.例题讲解

接下来就到了我们的例题环节了,我们再来复习一下刚刚学过的东西

struct Test
{
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少? int main() { printf("%p\n", p + 0x1); printf("%p\n", (unsigned long)p + 0x1); printf("%p\n", (unsigned int*)p + 0x1); return 0; }

 在这用到了结构体内存对齐的知识,详见:C语言之结构体内存的对齐

简单得知该结构体在32位机器上的大小为20个字节

首先p 是一个结构体指针 p+1 = 0x100000+20 = 0x100014

(unsigned long)p + 0x1 = 0x100000 + 1 = 0x100001

(unsigned int*)p + 0x1 = 0x100000 + 4 = 0x100004

 

2.

#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int* p;
    p = a[0];
    printf("%d", p[0]);
    return 0;
}

 这要注意的就是数组元素为逗号表达式,逗号表达式的值为最后一个值 

所以数组初始化后的值为 {1,3,5,0,0,0}

又因为p指向的是a[0]的地址,也就是a[0]这一行的首元素地址

所以p[0]最后的值为 1

3.

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

 这里要注意的就是p所指向的是一个数组大小为4的整形数组

而我们的arr[5][5]是一行为5个整形元素的数组,共五行

又因为数组在内存中是顺序存储的

所以画出图:

 

 

 

 所以最后的结果为4

4.

#include <stdio.h>
int main()
{
  char *a[] = {"work","at","alibaba"};
  char**pa = a; 
  pa++;
  printf("%s\n", *pa);
  return 0;
}

 首先a里面分别存着三串字符串首字母的地址,现在pa指向了a,然后pa+1就指向了a首元素的下一位,也就是at中的a的地址

所以最后打印出来是at

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));
    return 0;
}

这里的&aa是直接跳过了一整个数组,而aa+1是跳过了一行也就是5个元素

所以最后的结果为 10,5

int main()
{
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    char** cp[] = { c + 3,c + 2,c + 1,c };
    char*** cpp = cp;
    
    printf("%s\n", **++cpp);
    printf("%s\n", *-- * ++cpp + 3);
    printf("%s\n", *cpp[-2] + 3);
    printf("%s\n", cpp[-1][-1] + 1);

    return 0;
}

 对于这道题就显得复杂了一些

c  里面存着,E、N、P、R的地址

cp 与 c 反了过来,存的是R、P、N、R的地址

cpp 指向了 cp ,cpp里存的是cp的首地址,即R的地址

解析如图:注意前置++是先加后用

 

 

 

 所以,最后的结果为:

 

 

 

 

 

 

 

|---------------------------------------------------

 到此,对于指针的讲解便结束了!

若有错误之处,还望指正!

 

posted @ 2021-03-25 22:04  HighLight_FanYa  阅读(401)  评论(0编辑  收藏  举报