说明

这是一个完善了但又不完善的笔记,或许以后会更新

可以参考但请务必超越

源文件


Tools

VS2019
VScode
Typora

指针进阶

1.字符指针

2.数组指针

3.指针数组

4.数组传参和指针传参

5.函数指针

6.函数指针数组

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

8.回调函数

9.指针和数组面试题的解析

1.字符指针

面试题:

没有const修饰的指针

str1和str2指向的地址不同

有const修饰的指针

str3和str4指向同一个地址

2.指针数组

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

[]优先级比*高,应该不用加(),当然加了也没事

指针数组的数组名是用二级指针存放的吗?

3.数组指针

数组指针的定义:

数组指针是指针?还是数组?

答案是:指针。

我们已经熟悉: 整形指针: int pint; 能够指向整形数据的指针。 浮点型指针: float pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

数组指针的类型:

去掉数组名,剩下的就是类型

int (*)[10]

&数组名VS数组名

int* (*)[10] //指针类型

数组指针的使用

...

见code

判断:

int arr[5];//整型数组
int *parr1[10];//parr1是一个数组,10个元素,每个元素是int*的。所以parr1是一个存放指针的数组
int (*parr2)[10];//parr2是一个数组指针,改指针指向的数组又10个元素,每个元素是int
int (*parr3[10])[5];//parr3是一个数组,数组有10个元素
//每个元素是一个数组指针,改指针指向的数组有5个元素
//每个元素是int

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?
{}//因为每个元素都是int*,所以用int*接收也没问题
int main()
{
 int arr[10] = {0};
 int *arr2[20] = {0};//每个元素都是int*
 test(arr);//数组名是首元素地址,传的都是首元素地址
 test2(arr2);
}

二维数组的传参

二维数组的首元素,指的是第一行。

想象成一维数组就是,一行一个元素。

void test(int arr[3][8])//ok?
{}//数组传过来,用数组接收,没问题
void test(int arr[][])//ok?
{}//列不能省略,传了个寂寞
void test(int arr[][9])//ok?
{}//行可以省略,知道了一列有几个,才能顺序接收下来,不然不知道第二行从什么地方开始接收。
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}//不能用整型指针接收
void test(int* arr[5])//ok?
{}//指针数组,完全不搭边
void test(int (*arr)[5])//ok?
{}//只有这样可以,要传的话必须要传表示一维数组的指针,因为二维数组arr的首元素是一个一维数组。
void test(int **arr)//ok?
{}//二级指针也不行,而且传的也是整型
int main()
{
 int arr[3][10] = {0};
 test(arr);
}

一级指针传参

#include <stdio.h>
void print(int *p, int sz)//用一级指针接收,没问题
//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 test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?

int main()
{
    int a = 10;
    int* p1 = &a;
    int arr[10] = {0};
    
    test1(&a);//整型变量的地址
    test1(arr);//数组名,首元素
    test1(p1);//一级整型指针
    //test(NULL);考虑清楚,当然语法上也支持,但是没意义,传了个寂寞
    //当然char同理
}

二级指针传参

void  test(int **ppa)
{}

int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    
    int* arr[5];//指针数组
    
    test(paa);//二级指针
    test(&pa);//一级指针变量的地址
    test(arr);//数组名,首元素地址
}

5.函数指针

前提注意:

有人会有疑问,函数指针好像没什么卵用

当然,在太简单的代码中可能完全用不到(总不能写上个位数代码,为了用个函数指针而用函数指针吧),杀鸡焉用牛刀?

在这里提前说明:下面的函数指针数组,指向函数指针数组的指针,回调函数等,将阐述标明函数指针是非常重要的!

尤其是在大型的工程中,进行复杂的工程,非常有用!

函数指针变量

    //整型指针 - 指向整型
    //字符指针 - 指向字符
    //数组指针 - 指向数组
    //那么 函数指针变量 - 存放函数的地址

//&数组名 - 数组的地址
//数组名 - 数组首元素的地址
//函数名 == &函数名

    //函数的地址我们怎么存呢?
    int(*pf)(int,int) = &Add;//pf是用来存放函数的地址 - pf就是函数指针变量
    //返回类型(指针)(传入类型)= &函数名;
    //如果不加括号
    //int* pf(int,int) = &Add;err,pf就变成了函数名,返回类型就变成了int*
    //类比数组指针
    int arr[10] = &arr;
    int (*parr)[10] = &arr;//parr就是数组指针变量

函数指针类型

//去掉名,就是类型
int a = 10;//int
int arr[10] = {0};//int [10]
int (*parr)[10] = &arr;//int (*)[10]
//函数指针一样的道理
int(*pf)(int,int) = &Add;//int(*)(int,int) = &Add;

函数指针的应用

    //我们有时候并不能直接拿到一个函数或变量,只能拿到他的地址,这时候就可以用指针,通过解引用指针就可以找到这个变量
    //下面函数指针数组应用中实现的计算器,就更好的证明了这一点
    int(*pf)(int,int) = &Add;    
    int ret = Add(2, 3);//通过函数调用
    printf("%d\n", ret);//5
    ret = (*pf)(4, 5);//通过函数指针调用
    printf("%d\n", ret);//9
    //注意!
    //首先函数名也是函数的地址
    int(*pf)(int, int) = Add;//去掉&也可以获取
    //能把Add赋给pf,而且什么警告都没有
    //说明Add == pf
    //是不是可以直接不解引用
    int ret = pf(2, 3);
    printf("%d\n", ret);//5
    ret = pf(4, 5);
    //而且多写几颗*都可以
    //ret = (****pf)(4, 5);多解几次也可以
    printf("%d\n", ret);//9
    //*在这里就是摆设,既然如此为什么还要放呢? - 让初学者更加容易理解

阅读两段有趣的代码

//代码1 
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

注 :推荐《C陷阱和缺陷》

这本书中提及这两个代码。

    //代码1拆分开
    //可以看到void(*)() - 是一个函数指针类型,0是一个int被强行转换成前面void(*)()类型的地址,*再解引用找回那个函数调用
    //代码是一次函数调用,解析:
    //1.代码中把0强制类型转换为void(*)()的一个函数的地址。
    //2.解引用0地址,就是去调用0地址处的这个函数,被调用的函数是无参,返回类型是void
    //在当前所有编译器上,0是个值,可能调用不了,但是对于硬件来说是可以用的。
    //代码2从里从外都可以开始解释,是一个有趣的套娃
    //这里从signal开始解释
    //代码是一次函数声明,解析:
    //1.声明的函数名是signal
    //2.signal函数有2个参数,第一个是int类型,第二个是void(*)(int)的函数指针类型
    //3.signal函数的返回类型依然是void(*)(int)的函数指针类型
    //可以把signal提出来理解
    //void(*)(int) signal(int, void(*)(int));当然这是错误的,err
    //我们可以给void(*)(int)这个类型使用typedef定义一个函数(别名)
    //就像typedef int int32 - int32就是int的别名
    //typedef void(* pfun_t)(int) - pfun_t就表示把pfun_t去掉后的void(*)(int),这句代码写在main()前
    //虽然很别扭,但这样就可以方便的理解,代码2就可以写成
typedef void(*pfun_t)(int);
int main()
{
    pfun_t signal2(int, pfun_t);
    return 0;
}

代码2太复杂,如何简化:

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

以上内容可直接在源代码中运行查看

6.函数指针数组

首先有加减乘除4个函数

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* arr[10];//整型指针的数组
    //函数指针数组 - 存放函数指针的数组(废话)
    int(*pf1)(int, int) = Add;
    int(*pf2)(int, int) = Sub;
    int(*pf3)(int, int) = Mul;
    int(*pf4)(int, int) = Div;
    //想把这4个函数统一管理起来到一个数组中
    int(*pfArr[4])(int, int) = { Add,Sub,Mul,Div };//pfArr就是一个函数指针的数组

函数指针数组应用

实现一个计算器

(源代码可查看运行)

简单打印个菜单

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

使用input变量选择功能,设定x和y为需要运算的数字,sum为结果

    int input = 0;
    int x = 0;
    int y = 0;
    int sum = 0;
麻烦的使用Switch

如果使用switch,可以想象重复的代码和参数有很多

    do
    {
        menu();
        printf("请选择:");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("请输入2个操作数:");
            scanf("%d %d", &x, &y);
            sum = Add(x, y);
            printf("sum = %d\n", sum);
            break;
        case 2:
            printf("请输入2个操作数:");
            scanf("%d %d", &x, &y);
            sum = Sub(x, y);
            printf("sum = %d\n", sum);
            break;
        case 3:
            printf("请输入2个操作数:");
            scanf("%d %d", &x, &y);
            sum = Mul(x, y);
            printf("sum = %d\n", sum);
            break;
        case 4:
            printf("请输入2个操作数:");
            scanf("%d %d", &x, &y);
            sum = Div(x, y);
            printf("sum = %d\n", sum);
            break;
        case 0:
            printf("退出计算器\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);

可以看到,使用switch有至少三行代码一直在重复
假设,还要添加更多的运算功能
x&y x|y x>>y x<<y x^y ...只要是2个数能运算的都加上,
那这个代码简直是,冗长又难管
里面的代码大多数都是重复的,参数也都是重复的
所以,来改造!

优化(heal ?重点)
    do
    {
        menu();
        printf("请选择:");
        scanf("%d", &input);
        //这里把上面的函数指针数组应用下来
        //int(*pfArr[4])(int, int) = { Add,Sub,Mul,Div };
        //现在4个函数都有了下标,前面再加个0把下标挤过去
        int(*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };
        //使用下标调用函数           0  1   2   3   4
        if (input == 0)
        {
            printf("退出计算器\n");
        }
        else if(input>=1 && input<=4)
        {
            printf("请输入2个操作数:");
            scanf("%d %d", &x, &y);
            sum = pfArr[input](x, y);//input选择是几就进入下标为几的函数,x和y也传进去
            //注意!这里体现了,有时我们并不能使用函数名,而只能使用函数指针,函数指针可以选择运算函数,单写函数名就只是单独的一个运算功能
            //如果这里使用Add,Sub ... 就需要写更多的else
            printf("sum = %d\n", sum);
        }
        else
        {
            printf("选择错误\n");
        }
    } while (input);

如果以后还要添加的话,只需要扩大数组变更判断就可以了

int(*pfArr[n])(int, int) = { 0,Add,Sub,Mul,Div ... n };
else if(input>=1 && input<=n-1)
优化的使用Switch

之前switch的代码我们可以看到

相同的代码重复出现

思考一下我们写case语句是为了什么,不就是为了实现不同的功能吗。那么我们可以把所有的功能再放进一个函数中统一管理。

写一个Calc()函数把重复的内容放进去,把运算函数Add,Sub ... 也放进去使用指针调用。

void Calc(int (*pf)(int, int))
{
    int x = 0;
    int y = 0;
    int sum = 0;

    printf("请输入2个操作数:");
    scanf("%d %d", &x, &y);
    sum = pf(x, y);//pf == *pf == 运算函数名(Add,Sub ...)
    //当然,这里和前面一样只能使用函数指针来调用几个运算函数,而不能使用单独的一个函数名
    printf("sum = %d\n", sum);
}

代码就可以优化成

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

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

虽然感觉很绕,但其实很简单,也很好理解

类比数组指针和指针数组

int arr[10];
int (*p)[10] = &arr;
//p是一个指向整型数组的指针
int* arr[10];//整型指针的数组
int* (*p)[10] = &arr;//整数指针数组的地址
//p是一个指向(整型指针数组)的指针
int(*pf)(int,int) = &Add;//pf是函数指针
int(*pfArr[5])(int, int);//pfArr是一个函数指针的数组
int(*(*p)[5])() = &pfArr;
//p是一个指向函数指针数组的指针

可以看到因为都可以互相放进去,所以其实是能无限套娃的,但再往下套就太没有必要了。

所以,到此为止!

8.回调函数

首先:回调函数必须依赖函数指针

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

上面计算器的Calc()就是一个回调函数。

void Calc(int (*pf)(int, int))//传入运算函数的指针pf
{
    int x = 0;
    int y = 0;
    int sum = 0;

    printf("请输入2个操作数:");
    scanf("%d %d", &x, &y);
    sum = pf(x, y);//通过调用pf来运算
}

qsort函数

void qsort (void* base, size_t num, size_t size, int (*compar)(const void*, const void*));

排序的方法如我们所知:

冒泡排序

选择排序

插入排序

快速排序

qsort是C语言提供的一个快速排序的库函数,包含在C标准库<stdlib.h中>

可以看到qsort有4个参数

void* base,//void*类型的base
size_t num,//size_t类型的num
size_t size,//size_t类型的尺寸
int (*compar)(const void*,const void*)//函数指针,两个参数是const void*类型,返回类型是int

相比较冒泡排序:

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

void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
    //8 9 7 6 5 4 3 2 1 0
    //..
    //8 7 6 5 4 3 2 1 0 9
    //这是一趟冒泡排序,使9移动到了最后
    //接下里从8到0又一趟
    //7 6 5 4 3 2 1 0 8 9
    //10个元素每冒泡排序一趟只能处理一个元素,处理完需要9趟
    //且每一趟要处理的对数都不一样
    int sz = sizeof(arr) / sizeof(arr[0]);
    //升序
    bubble_sort(arr, sz); 
    //打印
    print_arr(arr, sz);
    
    return 0;
}

可以看到,这个冒泡排序是固定死的,固定了int类型
qsort的好处就是什么类型都可以

为什么呢?

举个栗子:

可以看到,把int*和float*赋给*虽然在我的编译器上运行了起来,但是会有不兼容的警告(ctrl+f7即可查看)

没有任何警告

void* - 无具体类型的指针

她里面可以放任何类型的指针,也就相当于一个通用类型,所以qsort的第一个参数不是写死的任何类型的指针,而是一个void*

优点:能够接收任意类型的地址

缺点:不能进行运算,不能+-整数,不能解引用

不能进行运算是什么意思呢?

这里int*类型的p1+1,可以得知意思是跳过一个整型int

但是void*不行

运算了个寂寞,p3++也就是p3+1。

size_t num - 待排序的元素个数

知道了要排序的类型,还要知道要排序的个数

有一些版本或者旧版本会使用nitems,完全不影响

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));

size_t size - 一个元素的大小,单位是字节

有一些版本或者旧版本会使用width(宽度),当然不影响

void qsort (void* base, size_t num, size_t width, int (*compar)(const void*, const void*));

关键点注意

仅仅有前三个参数是不能进行排序的,前三个参数只是获得了要排序的元素,并没有比较,大于小于等等。

而且比较规则方法也不确定。如果要排序结构体的话,假设一个学生成绩名单:

struct Stu
{
    char name[20];
    int age;
    float score;
}
//有3个学生数据
{
    {"张三",20,99.5f},
    {"李四",30,65.5f},
    {"王五",55,76.5f},
}

这时如果要用qsort排序,这要怎么比呢?是按名字、年龄、分数还是身高体重或者各种奇怪的数据呢?

这时 --

int (*compar)(const void*,const void*) - 指向排序时比较2个元素的函数

qsort抽象出来一个函数:compar(cmp)

有一些版本或者旧版本会使用compare,当然不影响

void qsort(void* base, size_t num, size_t width, int(__cdecl*compare)(const void* elem1, const void* elem2));
int (*compar)(const void* elem1,const void* elem2)//这个地方的elem1和elem2是要比较的2个元素的地址

光说不练假把式

这里的第四个参数内容是这样设计的

  • 当elem1小于elem2的时候,返回一个小于0的数字(负数)
  • 当elem1等于elem2的时候,返回0
  • 当elem1大于elem2的时候,返回一个大于0的数字(正数)

解释比较2个整型函数的代码

int cmp_int(const void* e1, const void* e2)
{
    return *(int*)e1 - *(int*)e2;
    //e1和e2都是void*类型的不能直接用
    //强制类型转换成int* - int* e1 和 int* e2
    //解引用 - *(int*)e1 和 *(int*)e2
    //相减返回值就行了
}

//打印
void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
}

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

回到回调函数的定义:

在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

在以上的例子中,cmp_int就是一个回调函数,把指向cmp_int的指针传给qsort作为compar参数使用

多种测试

使用名字排序
struct Stu 
{
    char name[20];
    int age;
};

int cmp_name(const void* e1, const void* e2)
{
    return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test()
{
    struct Stu s[3] = { {"张三", 15},{"李四", 30}, {"王五",10} };
    int sz = sizeof(s) / sizeof(s[0]);
    //假设按照名字排序
    qsort(s, sz, sizeof(s[0]), cmp_name);
}

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

这里就不直接运行了,简单写代码测试下

可以看到结构体已经创建好了,接下来排序

按英文字母z最大然后是w最小是l

李四,王五,张三。

使用年龄排序
struct Stu 
{
    char name[20];
    int age;
};

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

void test()
{
    struct Stu s[3] = { {"张三", 15},{"李四", 30}, {"王五",10} };
    int sz = sizeof(s) / sizeof(s[0]);
    //假设按照年龄排序
    qsort(s, sz, sizeof(s[0]), cmp_name);
}

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

稍微修改名字的函数然后调试

排序前

排序后

10<15<30。

模拟qsort

既然要模拟,那就使用冒泡排序模拟一下,快速排序当然也可以。

简单给定一组数,并且传参到自定义的BubbleSort中

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

重点来了,接下来会非常硬核

首先创建BubbleSort函数

//使用回调函数实现一个通用的冒泡排序函数
void BubbleSort(void* base, size_t num, size_t size, int(*cmp)(const void* e1, const void* e2))
{
    size_t i = 0;//注意int是有符号数,如果i和无符号数的num比较也需要写成size_t,下面的j同理
    //趟数
    for (i = 0; i < num - 1; i++)
    {
        size_t j = 0;
        //比较的对数
        for (j = 0; j < num - 1 - i; j++)
        {
            if (cmp((char*)base + j * size, (char*)base + (j + 1) * size > 0))//重点
            {
                //e1>e2,交换
                Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
            }
        }
    }

}

在此代码中可以看到,当两个数进行比较的时候,因为base是void*类型无法使用,强转什么类型都不是很合适,这个地方需要一个最细的力度,所以使用char*来进行比较,加1就跳过1个字符,加n就跳过n个字符,自然加size就跳过size个字符。

if (cmp((char*)base, (char*)base + size > 0))
{
    
}

这样做只是把下标为0和1的元素进行了比较,但是比较完第一对数后自然要比较第二对数,所以使用base直接加size也不行。于是,改为加一个j*size。那么相对的下一个元素就是(j+1)*size.

if (cmp((char*)base + j * size, (char*)base + (j + 1) * size > 0))
{
        
}

再然后如果大于0的话,需要交换,也是重点

思考一下,如果要交换的话,只有2个数也是不行的,不知道这2个元素多大,需要size。

Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);

知道了size的话就知道该交换多少个字节了,假设这里使用9,8,7,6,5,4,3,2,1,0。那么首先就是9和8的交换

由低地址到高地址,相对的一个字节一个字节的交换

void Swap(char* buf1, char* buf2, int size)//重点
{
    int i = 0;
    for ( i = 0; i < size; i++)
    {
        char tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}

到此位置完成了qsort的模拟。

当然结构体也可以排序,按名字或是年龄都可以,这里只展示名字排序:

排序前:

排序后:

9.指针和数组笔试题解析

有一堆要打印的,计算数组大小的值(注意x86和x64)

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));//数组名a单独放在sizeof内部,数组名表示整个数组,计算的是整个数组的大小
printf("%d\n",sizeof(a+0));//a表示首元素的地址,a+0还是首元素的地址,地址的大小是4/8字节
printf("%d\n",sizeof(*a));//a表示首元素的地址,*a就是首元素 ==> a[0],大小就是4
//*a <==> *(a+0) <==> a[0]
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 - int(*)[4]
//数组的地址也还是地址
printf("%d\n",sizeof(*&a));//*&a - &a是数组的地址,对数组的地址解引用拿到的是数组,整个数组的大小,所以大小是16
//取出来地址再解引用,那不就抵消了,就和第1条代码一样了 - printf("%d\n",sizeof(a));
printf("%d\n",sizeof(&a+1));//&a是数组的地址,&a+1是数组的地址+1,也就是跳过整个数组。虽然跳过了数组,还是个地址 - 4/8
printf("%d\n",sizeof(&a[0]));//取出了首元素地址 - 4/8
printf("%d\n",sizeof(&a[0]+1));//&a[0]是第一个元素的地址,&a[0]+1就是取出了第二个元素的地址 - 4/8

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6
printf("%d\n", sizeof(arr+0));//4/8
printf("%d\n", sizeof(*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));//随机值
printf("%d\n", strlen(arr+0));//随机值
printf("%d\n", strlen(*arr));//*arr - a - 97 - err
//strlen以为传进来的'a'的ascii码值97就是地址
printf("%d\n", strlen(arr[1]));//arr[1] - b - 98 - err
printf("%d\n", strlen(&arr));//随机值
printf("%d\n", strlen(&arr+1));//随机值
printf("%d\n", strlen(&arr[0]+1));//随机值
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7
printf("%d\n", sizeof(arr+0));//4/8
printf("%d\n", sizeof(*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));//6
printf("%d\n", strlen(arr+0));//6
printf("%d\n", strlen(*arr));//err
printf("%d\n", strlen(arr[1]));//err
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是一个指针变量,存放的是首元素a的地址 - 4/8
printf("%d\n", sizeof(p+1));//p+1是字符b的地址 - 4/8
printf("%d\n", sizeof(*p));//1
printf("%d\n", sizeof(p[0]));//p[0] -->*(p+0) --> *p - 1
printf("%d\n", sizeof(&p));//4/8
printf("%d\n", sizeof(&p+1));//4/8
printf("%d\n", sizeof(&p[0]+1));//4/8
printf("%d\n", strlen(p));//6
printf("%d\n", strlen(p+1));//5
printf("%d\n", strlen(*p));//err
printf("%d\n", strlen(p[0]));//err
printf("%d\n", strlen(&p));//随机值
printf("%d\n", strlen(&p+1));//随机值
printf("%d\n", strlen(&p[0]+1));//随机值

//二维数组
int a[3][29] = {0};
printf("%d\n",sizeof(a));//48
printf("%d\n",sizeof(a[0][0]));//4
printf("%d\n",sizeof(a[0]));//a[0]是第一行的数组名,数组名单独放在sizeof内部 - 16
printf("%d\n",sizeof(a[0]+1));//arr[0]是第一行的数组名,并没有单独放在sizeof内部,也没有&,所以arr[0]表示首元素的地址,就是第一行这个数组第一个元素的地址。所以a[0]+1就是第一行,第二个元素的地址 - 4
printf("%d\n",sizeof(*(a[0]+1)));//*(a[0]+1)就是第一行第二个元素 - 4
printf("%d\n",sizeof(a+1));//数组名a,并没有单独放在sizeof内部,也没有&,所以a表示首元素的地址。二维数组的首元素就是他的第一行。所以a+1就是第二行的地址。a他的指针类型就是int(*)[4] - 4/8
printf("%d\n",sizeof(*(a+1)));//*(a+1)就是第二行,*(a+1) --> a[1] - 16
printf("%d\n",sizeof(&a[0]+1));//a[0]是第一行的数组名,&a[0]拿到的是第一行的地址,&a[0]+1,就是第二行的地址,他的类型是int(*)[4] --> a+1 - 4/8
printf("%d\n",sizeof(*(&a[0]+1)));//解引用和取地址抵消 *(&a[0]+1)--> *(&a[1]) --> a[1],就是第二行 - 16
printf("%d\n",sizeof(*a));//a表示首元素的地址,二维数组首元素是第一行,*a就是第一行,也就是第一行的数组名a[0] - 16
printf("%d\n",sizeof(a[3]));//一共就3行,下标0,1,2。但是如果只是看一眼第四行,是可以的,他是有类型的,所以不会出问题。a[3]假设存在,就是第四行的数组名,sizeof(a[3]),就相当于把第四行的数组名单独放在sizeof内部 - 16
//二维数组的数组名是a
//第1行的数组名是a[0]
//第2行的数组名是a[1]
//第3行的数组名是a[2]
//sizeof(a)
//sizeof(a[0]) &a[0]
//sizeof(a[1]) &a[1]
//sizeof(a[2]) &a[2]
//在以上两种情况下,数组名表示整个数组,除此之外
//a - 二维数组的首元素(第一行)地址
//a[0] - 第1行第1个元素的地址
//a[1] - 第2行第1个元素的地址
//a[2] - 第3行第1个元素的地址
//sizeof根据类型计算,不会真实运算。类型也是可以放在sizeof内部的,就比如以前学过的
printf("%d\n",sizeof(int);

数组名的意义

很重要的内容,最好牢牢记住

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

10.指针笔试题

笔试题1:

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);//数组地址+1,跳过这组地址,地址类型是int(*)[5],强制类型转换int*。
    printf( "%d,%d", *(a + 1), *(ptr - 1));//首元素+1和数组地址加一过后的首元素+1。解引用访问到2,5
    return 0; 
}

笔试题2:

struct Test
{
 int Num;
 char *pcName;
 short sDate;
 char cha[2];
 short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知,结构体Test类型的变量大小是20个字节(32位,64位的话会不一样)
int main()
{
    p=(struct Test*)0x100000;//p的类型是结构体指针类型是struct Test* -- int,整型赋给指针会有类型的差异,需要强制类型转换
 printf("%p\n", p + 0x1);//p+0x1
 printf("%p\n", (unsigned long)p + 0x1);//p被强制类型转换成unsigned long后+0x1
 printf("%p\n", (unsigned int*)p + 0x1);//p被强制类型转换成unsigned int*后+0x1
 return 0; }

p结构体指针+0x1,就是跳过一个结构体,这里就是跳过了1x20个字节。强转unsigned long之后变成了整数,整数+0x1那就是+1。强转unsigned int*之后变成了整型指针,+0x1就是+4

p+0x1就是0x10000014(16进制的20),打印出来应该是00100014

(unsigned long)p + 0x1就是00100001

(unsigned int*)p + 0x1就是00100004

%p以地址的形式打印,默认不打印0x。可以自己加上0x,也可以用%#x以16进制的形式打印,会自动加0x。

笔试题3:

int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1);//取出整个数组的地址,加1跳过整个数组
    int *ptr2 = (int *)((int)a + 1);
    printf( "%x,%x", ptr1[-1], *ptr2);//ptr[-1] --> *(ptr+(-1)) --> *(ptr-1)。a强制类型转换成int,加1就是加了一个字节。
    //4 2000000
    return 0; 
}

笔试题4:

int main()
{
    int a[3][32] = { (0, 1), (2, 3), (4, 5) };//这里使用了小括号()可以说实际上放进去的只有1,3,5。int a[3][33] = { 1, 3, 5 };
    int *p;
    p = a[0];//首元素地址赋给指针p,指向首元素1的地址
    printf( "%d", p[0]);//*(p+0) --> *p 还是1
    return 0; 
}

笔试题5:

int main()
{
    int a[5][34];//a的类型是int(*)[5]
    int(*p)[4];//p的类型是int(*)[4]
    p = a;//虽然类型不一样但是还是强行放了进去
    printf( "%p,%d\n", &p[4][35] - &a[4][36], &p[4][37] - &a[4][38]);//p[4][39] --> *(*(p+4)+2),站在p的角度找到的就是p+4首元素地址+2。指针和指针相减得到的是他们之间的元素个数。低地址减高地址也就是-4。
    //10000000000000000000000000000000 -4的源码
    //11111111111111111111111111111011
    //11111111111111111111111111111100 -4的补码
    //%p打印地址,没有原反补概念,-4的补码就会打印成FFFFFFFC
    return 0; 
}

编译器报警

笔试题6:

int main()
{
    int aa[2][42] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    //1 2 3 4 5
    //6 7 8 9 10
    //本质上是连续存放的
    int *ptr1 = (int *)(&aa + 1);//取地址aa+1,跳过整个数组
    int *ptr2 = (int *)(*(aa + 1));//*(aa+1) --> aa[1],相当于第二行的首元素地址
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
    return 0; 
}

笔试题7:

int main()
{
 char *a[] = {"work","at","alibaba"};
 char**pa = a;//a现在是首元素地址,放到pa里去,pa指针变量存放的是char**类型的元素
 pa++;//跳过一个char*的变量
 printf("%s\n", *pa);//at
 return 0; 
}

笔试题8:

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,cp和cpp画出来

printf("%s\n", **++cpp);//++cpp首先移动,这时两次解引用,拿到的值其实就是POINT中P的地址。%s打印到\0其实就是打印了POINT

printf("%s\n", *--*++cpp+3);//前置++,先++,然后解引用,其实就是拿到了c+1。然后再--,就变成了c,再解引用就找到了ENTER。再+3也就是ENTER中第二个E的地址,打印出来就是ER。

printf("%s\n", *cpp[-2]+3);//*cpp[-2] --> *(*(cpp-2))。和上一步类似,但是没有移动回去,解引用得到F的地址,然后+3指向S。打印出来就是ST。

printf("%s\n", cpp[-1][-1]+1);//cpp[-1][-1] --> *(*(cpp-1)-1)。注意,首先cpp-1,解引用,指向的是POINT的首元素地址。然后再-1,解引用,指向的就是NEW的首元素地址。最后+1指向NEW的E。打印出来就是EW。

注意:这道题可能会有几个难点

1.++和--会自增和自减,移动位置。

2.*解引用和c[-1] = *(c-1)一定要搞清楚。

3.最后+3和+1,指的是加一个char*,因为前面都已经有2次*解引用了。

广告位招租
最后修改:2021 年 09 月 10 日 02 : 11 PM
如果觉得我的文章对你有用,请喂饱我!(理直气壮)