C语言指针进阶

目录

0.指针初阶回顾:

1.字符指针

2.指针数组

3.数组指针

(1)数组指针的定义:

(2)数组名和&数组名

(3)数组指针的使用

(4)数组指针的数组:

4.数组传参和指针传参

一维数组传参:

二维数组传参:

一级指针传参:

二级指针传参:

5.函数指针

(1)函数的地址:

(2)函数指针的存放:

(3)使用存放函数指针的函数指针变量来调用函数:

(4)两个有趣的代码:

代码一:

代码二:

6.函数指针数组

(1)概念:

(2)函数指针数组的用途:转移表,减少代码的冗余性。

7.指向函数指针数组的指针(了解)

8.回调函数

(1)概念

qsort函数

9.指针和数组的面试题

(1)数组名的理解

(2)strlen函数的理解

(3)二维数组的数组名理解

(4)几个易错的题:

(5)sizeof的特点

10.指针的笔试题

笔试题1

笔试题2

笔试题3

笔试题4

笔试题5

笔试题6

笔试题7

笔试题8


0.指针初阶回顾:

  • 指针就是个变量,用来存放地址,指针唯一标识一块内存空间。

  • 指针的大小固定为4/8个字节(32/64位平台)。

  • 指针有类型的,指针的类型决定了指针+-整数的步长,指针解引用操作时的权限。

  • 指针的运算

1.字符指针

字符指针 char*

#include <stdio.h>
​
int main()
{
    char str1[] = "hello bit";
    const char* str2 = "hello bit";
​
    printf("%s\n", str1);
    printf("%s\n", str2);
​
    return 0;
}

字符指针除了表示字符的指针,还可以表示字符串

const char* pstr = "hello bit.";
  • 字符串可以用字符数组和字符指针来表示。

  • 字符指针表示的字符串是常量字符串

  • 这里的const在有的编译器可以不加,有的编译器要求严格必须加上。

  • 字符指针表示字符串不是把整个字符串放入字符指针中,本质是把字符串“hello bit“的首字符h的地址放入字符指针pstr中

内存分区:

​​

栈,堆,数据段(全局变量和静态变量),代码段(可执行代码,常量)

2.指针数组

指针数组顾名思义,就是存放指针的数组。

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

3.数组指针

(1)数组指针的定义:

数组指针就是指向数组的指针。

int *p1[10]; 
int (*p2)[10];
  • [ ]操作符的优先级是大于 * 操作符的。

  • p1先和[ ]结合,是一个数组,数组的每个元素都是int*的指针。

  • p2先和*结合,说明是一个指针,指针指向的类型是int [10],说明p2是指向数组的指针,指向的数组是一个大小为10的整型数组。

  • p1是指针数组,p2是数组指针

(2)数组名和&数组名

arr和&arr的值是一样的,但是他们的意义是不一样的。

arr是数组首元素的地址,&arr是数组的地址,也就是数组指针

&arr+1跳过的就是整个数组的大小。

数组指针解引用,其实就是数组首元素的地址。(这句话非常重要,以后都要用)

(3)数组指针的使用

数组指针一般很少在一维数组中使用

比如这样使用:

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

我们会发现这样使用毫无意义,就是在脱kz放p!

其实数组指针一般在二维数组中应用的比较广泛:

void print_arr1(int arr[3][5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
​
void print_arr2(int(*arr)[5], int row, int col)
{
    for (int i = 0; i < row; i++)
    {
        for (int j = 0; j < col; j++)
        {
            printf("%d ", arr[i][j]);//*(*(arr+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_arr1(arr, 3, 5);
    print_arr2(arr, 3, 5);
​
    return 0;
}

注意:

  • 数组传参,传的其实都是指针,尽管形参的形式还是个数组,但其实是为了增强代码的可读性。

  • 数组名是数组首元素的地址,二维数组的首元素是二维数组第一行的一维数组所以二维数组的数组名是第一行一维数组的地址,也就是数组指针(这是重点哦)

  • arr[i][j]在底层上就是* (*(arr+i)+j):

解析 arr[i][j],即* (*(arr+i)+j):

arr是二维数组第一行数组的地址。

arr+i是二维数组第i行数组的地址。

*(arr+i):前面说数组指针解引用就是数组首元素的地址,那么*(arr+i)就是第i行数组首元素的地址。

*(arr+i)+j:第i行数组第j个元素的地址。

*(*(arr+i)+j):第i行数组第j个元素

(4)数组指针的数组:

int (*parr1)[10];//parr1是数组指针,数组是大小为10的整形数组

int (*parr2[10])[5];数组指针数组:

parr2[10] : parr2是数组,该数组里面有10个元素

int (*)[5] : 每个元素都是数组指针,指针指向的数组是大小为5的整形数组

4.数组传参和指针传参

一维数组传参:

普通数组传参:

void test(int arr[ ]);

void test(int arr[10]);

void test(int* arr);

指针数组传参:

void test(int* arr[ ]);

void test(int* arr[10]);

void test(int** arr);

二维数组传参:

void test(int arr[3][5]);

void test(int arr[ ][5]);

void test(int arr[ ][ ]); ​ 错误!!

void test(int (*arr)[5]); ​ 二维数组的数组名是数组指针

一级指针传参:

一级指针

变量的地址

普通数组(不是指针数组)的数组名

二级指针传参:

二级指针

一级指针变量的地址

指针数组的数组名

5.函数指针

(1)函数的地址:

int Add(int a, int b)
{
    return a + b;
}
​
int main()
{
    printf("%p\n", Add);
    printf("%p\n", &Add);
​
    return 0;
}
Add 和 &Add都是函数的地址

(2)函数指针的存放:

int (*pfun)(int , int) = Add(或者&Add);

pfun是函数指针变量:pfun先和*结合,说明pfun是指针。

int (*)(int,int)是函数指针类型

注意:语法上是不允许这么写的:int (*)(int,int)pfun;

(3)使用存放函数指针的函数指针变量来调用函数:

一般调用:int a = Add(3,5);

int b = (*pfun)(4,5); - - - > &Add

int c = pfun(5,5); - - - > Add

两种方式都是可以的。

(4)两个有趣的代码:

代码一:

(*((void (*) ())0))();
  1. void (*) ()函数指针类型

  2. ​ void (*)() ​)0 :将0强制类型转换成函数指针类型

  3. *((​ void (*)() ​)0 ) 对函数指针类型解引用,就是调用该函数。

  4. 调用函数:(*((​ void (*)() ​)0 ) )()

意思就是调用0地址处的函数,函数无参,返回值void

代码二:

void (*signal(int,void(*)(int)))(int);
  1. signal先和()结合,说明signal是函数名。

  2. signal函数的第一个参数是int,第二个参数是函数指针void(*)(int),该函数指针指向一个参数为int,返回类型为void的函数。

  3. signal函数的返回类型也是一个函数指针void(*)(int);​去掉函数名和参数,剩下的就是返回类型:void(*)(int)

  4. 这是一个signal函数的声明

代码二的简化:使用typedef:

这种形式容易理解,但是语法上不允许,会报错

简化:

注意:

对函数指针类型void(*)(int)重命名,在语法上也要这么写:typedef void(*pfun_t)(int),不能typedef void(*)(int)pfun_t

6.函数指针数组

(1)概念:

函数指针数组顾名思义就是一个指针数组。

前面学习了数组指针数组,也就是int (*parr2[10])[5];

那么函数指针数组就是这么定义的:

int (*pfArr[4]) (int, int);
  • 注意操作符的优先级,[ ]的优先级比*高,pfArr先和[ ]结合,说明pfArr是一个数组。

  • 补充:对于一个数组的声明,去掉数组名和[ ],剩下的就是数组元素的类型;对于一个函数声明,去掉函数名和参数,剩下的就是返回类型。

    pfArr是数组名,去掉这个数组名和[ ],剩下的int (*)(int, int)是一个函数指针类型,说明pfArr是函数指针的数组。

  • 对于int (*parr2[10])[5];parr先和[ ]结合,是一个数组;去掉数组名parr2和[ ]。剩下的是int ( * )[5],是一个数组指针,说明parr2是数组指针的数组。

(2)函数指针数组的用途:转移表,减少代码的冗余性。

#define _CRT_SECURE_NO_WARNINGS 1
#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;
}
​
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 x, y;
    int input = -1;
    int ret = 0;
​
    int (*pfArr[5])(int, int) = { NULL,add,sub,mul,div };//函数指针数组
    while (input)
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        if (input >= 1 && input <= 4)
        {
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = pfArr[input](x, y);
        }
        else
        {
            printf("输入错误!\n");
        }
        printf("ret=%d\n", ret);
    }
​
    return 0;
}

我们想要实现一个计算器,如果不使用函数指针数组,免不了要使用switch语句来分开调用四个计算的函数,这样的话每个case语句就会有重复的代码出现,这样就造成了代码的冗余,而函数指针数组实现的转移表可以解决这个缺点。

7.指向函数指针数组的指针(了解)

很明显这是个指针,该指针指向的是函数指针数组。

如何定义?

int ( * (*p)[5])(int ,int) = &pfArr;

p先和*结合,说明p是指针, *p后面接[5],说明是数组指针,数组的元素类型是int ( * ) (int ,int),是函数指针类型。

8.回调函数

(1)概念

回调函数就是一个通过函数指针调用的函数。

比如上面的计算器示例除了函数指针数组,也可以通过回调函数实现:

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("请输入两个操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);
    printf("ret = %d\n", ret);
}
​
int main()
{
    int input = 0;
    int x = 0;
    int y = 0;
    int ret = 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;
}
 

qsort函数

C语言库中也有一个回调函数,那就是qsort函数。

  • 所谓qsort就是quick sort,他的底层是快速排序。并且qsort排序适用于任何数据类型的排序

  • 我们发现qsort函数的参数很有意思,首先void*无类型的指针决定了这个函数适用于任何类型数据的排序,其次第四个参数明显是一个函数指针,参数也都是void *,这是用来比较两个数据的,无类型的指针也说明可以用于任何数据类型的比较。

  • num是数据个数,size是数据的大小(单位字节)。

  • 这个compar函数是需要用户自己去实现的,然后让qsort函数调用comp函数的指针。

  • 对于void*指针,他可以接受任何类型的指针,但是这种指针是不能进行解引用操作的,也不能进行指针运算。

‍qsort函数的使用

下面展示qsort函数的使用:

void printArr(int arr[], int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
​
//int类型数据排序的比较函数
int comp_int(void* p1, void* p2)
{
    return *(int*)p1 - *(int*)p2;
}
​
//int类型数据排序
void test1()
{
    int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), comp_int);
    printArr(arr, sz);
}
​
typedef struct Stu
{
    char name[20];
    int age;
}Stu;
​
//struct类型数据排序的比较函数
//按年龄排序
int comp_StuByAge(void* p1, void* p2)
{
    return ((Stu*)p1)->age - ((Stu*)p2)->age;
}
​
//按姓名排序
int comp_StuByName(void* p1, void* p2)
{
    return strcmp(((Stu*)p1)->name, ((Stu*)p1)->name);
}
​
//struct类型数据排序
void test2()
{
    Stu arr[3] = { {"Jack",18},{"Tom",22},{"Lucy",15} };
    int sz = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, sz, sizeof(arr[0]), comp_StuByAge);
​
}
​
int main()
{
    //test1();
    test2();
​
    return 0;
}

‍qsort函数的实现

下面我们通过冒泡排序模拟实现一下qsort排序:

//int类型数据排序的比较函数
int comp_int(void* p1, void* p2)
{
    return *(int*)p1 - *(int*)p2;
}
​
typedef struct Stu
{
    char name[20];
    int age;
}Stu;
​
//struct类型数据排序的比较函数
//按年龄排序
int comp_StuByAge(void* p1, void* p2)
{
    return ((Stu*)p1)->age - ((Stu*)p2)->age;
}
​
//按姓名排序
int comp_StuByName(void* p1, void* p2)
{
    return strcmp(((Stu*)p1)->name, ((Stu*)p1)->name);
}
​
void Swap(char* buf1, char* buf2, int size)//交换arr[j],arr[j+1]这两个元素,一个一个字节的交换
{
    int i = 0;
    char tmp = 0;
    for (i = 0; i < size; i++)
    {
        tmp = *buf1;
        *buf1 = *buf2;
        *buf2 = tmp;
        buf1++;
        buf2++;
    }
}
​
void bubble_sort(void* base, //base存放的是待排序数组中第一个元素的地址,void*说明可以对任何数据类型进行排序
                int size, //排序数据元素的个数
                int width, //数据的大小
                int (*cmp)(void* , void*))//比较函数的指针,比较函数用户自己实现。参数类型是void*,说明可以比较任何数据类型
{
    int  i = 0;
    //趟数
    for (i = 0; i < size - 1; i++)
    {
        int j = 0;
        //一趟内部比较的对数
        for (j = 0; j < size - 1 - i; j++)
        {
            //假设需要升序cmp返回>0,交换
            if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//两个元素比较,需要将arr[j],arr[j+1]的地址要传给cmp
            {
                //交换
                Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
            }
        }
    }
}

注意:

  • 通过cmp函数指针变量调用比较函数时,参数为什么要用char*?

因为char正好一个字节,(char* )base + j * width和(char*)base + (j + 1) * width 正好可以定位到arr[j],arr[j+1]的地址。

  • swap函数的参数为什么是char*?

char* 正好一个字节,可以用char*来一字节一字节的交换。

9.指针和数组的面试题


(1)数组名的理解

数组名是数组首元素的地址

但是有2个例外:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。sizeof里面必须要只有一个数组名,其他的都不行。

  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

除此之外所有的数组名都表示数组首元素的地址。包括sizeof(arr + 0)和sizeof(arr)也不一样。sizeof(arr + 0)里的arr表示数组首元素地址。


(2)strlen函数的理解

strlen函数接收的是指针参数,参数类型是const char* ,表示一个字符的指针,通过寻找‘\0’来计算字符串长度。


(3)二维数组的数组名理解

两种思维理解

例如二维数组int arr[3][4]:

  • 二维数组的数组名arr是第一行一维数组的指针,即数组指针,而数组指针解引用就是数组名,*arr是第一行一维数组的数组名,*(arr+i)就是第i行一维数组的数组名。

  • 另一种思维理解:二维数组是一维数组的数组,对于arr[i][j],arr[i]可以看成第i个一维数组的数组名而arr[i]即*(arr+i)。


(4)几个易错的题:

int a[]={1,2,3,4};
pritnf("%d\n",sizeof(*&a));

sizeof(*&a)就是sizeof(a),里面只有一个数组名,表示整个数组,大小是16;


int a[3][4]={0};
pritnf("%d\n",sizeof(a[0]));

两种思维理解(针对a[i]):

  • a是二维数组的数组名,就是第一行数组的指针,a[i]就是*(a + i), ​ a + i就是第i行数组的指针,​ *(a + i)就是第i行数组的数组名(数组指针解引用就是数组名,sizeof里面只有一个数组名,就表示整个一维数组(值为16)。

  • 二维数组是一维数组的数组,对于arriarr[i]可以看成第i个一维数组的数组名sizeof里面只有一个数组名,就表示整个一维数组(值为16)。


(5)sizeof的特点

sizeof只关注类型

int a[3][4]={0};
ritnf("%d\n",sizeof(a[3]));

这里其实并不会实际去访问第四行(第四行越界)。

又比如:

int a = 7;
short b = 4;
printf("%d\n",sizeof(s = a + 2));

这里输出的是2(short类型的大小)

10.指针的笔试题

笔试题1


int main()
{
    int a[5] = { 1,2,3,4,5 };
    int* ptr = (int*)(&a + 1);
    printf("%d,%d", *(a + 1), *(ptr - 1));
​
    return 0;
}

&a:数组a的地址

答案:2和5

笔试题2


struct Test//20字节
{
    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;
}

注意:

  • 指针+/-整数:根据指针类型决定加多少字节。

  • 整数+整数:直接加整数个字节。

  • 十六进制的0x1就是数字1;

  • 内存中的每个内存单元都是1个字节,每个内存单元都有一个编号,就是地址,地址加1相当于跨一个字节

答案:

1.0x100014(加20)

2.0x100001(加1)

3.0x100004(加4)

笔试题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);
​
    return 0;
}

注意:

  • %x是以16进制打印。

  • ptr1是根据数组指针类型的来加1,加多少字节由数组大小决定。

  • ptr2转化成int再加1,直接加1个字节。

答案:4;02000000

笔试题4


int main()
{
    int a[3][2] = { (0,1),(2,3),(4,5) };
    int* p = a[0];
    printf("%d", p[0]);
​
    return 0;
}

注意:

  • 这里的二维数组是不完全初始化,因为是括号()而不是花括号{ },()里面的表达式是逗号表达式。所以数组元素是1 3 5 0 0 0。

  • p是第一行数组的数组名,也就是1的地址。

答案:1

笔试题5


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和%x的区别:两个都是打印十六进制数,%p一般打印地址。

  • 数据在内存中存放的都是补码。%d是以十进制打印。

答案:FFFFFFFFFFFFFFFC,-4

笔试题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));
​
    return 0;
}

注意:

ptr2:aa是二维数组的数组名,表示第一行一维数组的地址,aa+1就是第二行一维数组的地址,*(aa+1)就是第二行一维数组的数组名,也就是6的地址。

答案:10,5

笔试题7


int main()
{
    char* a[] = { "work","at","alibaba" };
    char** pa = a;
    pa++;
    printf("%s\n", *pa);
​
    return 0;
}

答案:at

笔试题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;
}

注意:

  • 运算符优先级:++/-- > ​ *(解引用操作符) > +/-(加减)。

  • 数组名是数组首元素的地址

  • **++cpp:cpp是c+3的地址,++后就是c+2的地址,*++cpp就是c+2,c+2是第三个char *的地址,** ++cpp就是 *(c+2),就是第三的char *,指向字符串“POINT”的首字符P。

  • *--*++cpp + 3:cpp是c+2的地址,++后再 *就是c+1,再--再 * 就是第一个char *,指向字符串“ENTER”的首字符E,再+3就指向了E。

  • *cpp[-2] + 3: ​转换成**(cpp-2)+3,cpp是c+1的地址,-2后再*就是c+3,再 *后就是第四个char *,指向字符串“FIRST”的首字符F,再+3就指向了S。

  • cpp[-1][-1] + 1转换成*( *(cpp-1)-1)+1,cpp是c+1的地址经过分析,*( *(cpp-1)-1)指向字符串NEW”**的首字符N,再+1指向E。

答案:POINT ER ST EW

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值