【C语言进阶】详谈指针

目录

前言:

一、字符指针

1.字符指针的使用:

2.常量字符串:

3.相关面试题分析:

二、指针数组

三、数组指针

1.数组指针的定义:

2.&数组名vs数组名

3.数组指针的使用

四、数组参数和指针参数

1.一维数组传参:

2.二维数组传参:

3.一级指针传参

4.二级指针传参

五、函数指针:

1.函数指针:

2.函数指针数组:

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

4.回调函数:

六、总结:



前言:

        在C语言初阶中,我们已经对指针已经有了初步了解,今天我们将更加深入的探讨指针,希望对大家能有所帮助。

一、字符指针

1.字符指针的使用:

         在一众指针类型中,我们知道存在这字符指针这种类型,并且我们一般会这样去使用它:

int main()
{
    //第一种形式
    char ch='w';
    char* p=&ch;
    *p='w';
    printf("%c\n",*p);

    //第二种形式
    const char* pstr="hello world";
    printf("%s\n",pstr);
    
    return 0;
}

        ps:这里第二种形式的本质是把常量字符串'hello world'的首字符'h'的地址放到pstr中。

2.常量字符串:

        我们要注意,上面所说到的常量字符串作为常量,它们是不可以被修改的:

int main()
{
    char *p="abcde";
//使指针指向常量区的常量字符串abcde
    *p='a';
//按照我们的理解,*p中存放的是首元素a的地址,我们尝试改变它
//我们运行起来发现无法完成编译运行,程序会直接卡住
//于是我们可以得出,当时使用常量字符串时,常量字符串abcde不可修改
    return0;
}

        这些常量字符串中的常量字符,是原本就储存在常量区(只读数据区)的一些常量,不同于需要我们输入字符变量。所以当使用字符指针指向常量字符串时,可以直接进行调用但无法对其修改。

        当出现这种错误时,程序往往是可以正常编译运行,但是会卡住而得不出结果,我们往往很难发现问题所在,会给我们代码书写造成很大得困扰。为了尽可能避免这种情况得发生,我们通常在指向常量字符串时,使用const对指针变量进行修饰,进行这样得书写操作之后,当我们试图对常量字符串进行修改时,编译器就会直接报错而无法运行,便于我们发现问题所在:

3.相关面试题分析:

通过上面讲解,我们来分析一下下面的面试题一下:

int main()
{
    //创建两个数组,两个字符型指针变量
    char arr1[]="hello bit.";
    char arr2[]="hello bit.";
    
    const char* str1="hello bit.";
    const char* str2="hello bit.";

    //判断数组内容是否相同
    if(arr1==arr2)
    {
        printf("arr1 and arr2 are same\n");
    }
    else
    {

        printf("arr1 and arr2 are not same\n");
    }
    
    //判断字符指针内容是否相同
    if(str1==str2)
    {
        printf("str1 and str2 are same\n");
    }
    else
    {

        printf("str1 and str2 are not same\n");
    }
    return 0;
}

         将两个字符型数组进行对比和两个字符型指针进行对比,目的是为了验证它们在存储时内存的开辟;

         这里的str1和str2指向的时同一常量字符串。C/C++会把常量字符传存储到单独的一个内存区域(在我们内存中存在着常量区,用于存放一些常量),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟不同的内存块,这就解释了arr1和arr2为什么不同了。

二、指针数组

         在初阶指针章节中,我们已经对指针数组有所了解,所以我们这里简单回顾一下。指针数组本质还是数组,是用于存放指针变量的数组:

int main()
{
    //创建变量
    int a=0;
    int b=1;
    int c=2;

    //创建指针数组
    int* arr[]={&a,&b,&c};
    
    return 0;
}

三、数组指针

1.数组指针的定义:

        我们一定要区分指针数组,数组指针的本质是指针。不同于其他指针,数组指针指向的不是地址而是数组:

int main()
{
    int* p[10];
    //p是数组,指针数组

    int(*p1)[10];
    //p1是指针,数组指针
    
    return 0;
}

解释:p1先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10的整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。 

这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。 

2.&数组名vs数组名

        我们知道,出一些特殊情况外,大多数的数组名都表示数组首元素的地址,那么既然数组名已经是地址了,&数组名又有什么呢?我们来看看这段代码:

int main()
{
    int arr[10] = { 0 };
    printf("arr: %p\n", arr);
    printf("&arr: %p\n", &arr);
    return 0;
}

输出结果如下图:

         我们会发现arr和&arr输出的结果是一样,难道他们是一回事吗?我们再来看看下面这一段代码:

int main()
{
    int arr[10] = { 0 };
    printf("arr = %p\n", arr);
    printf("&arr= %p\n", &arr);

    printf("\n");

    printf("arr+1 = %p\n", arr + 1);
    printf("&arr+1= %p\n", &arr + 1);
    return 0;
}

        我们让arr和&arr同时+1,这时我们看到不一样的结果:

         我们可以发现,&ar表示的是整个数组的地址,而arr仅表示首元素的地址,这就导致了我们+1之后,他们向后走的步伐不同,&arr跨过一个数组的大小,而arr仅仅跨过一个元素大小而已。

3.数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;
    return 0;
}

把数组arr的地址赋值给数组指针变量p,但是我们一般很少这样写代码。更多的是在我们进行函数调用们进行数组传参时,可以通过数组指针来进行接收:

void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
 
//使用数组指针来进行接收:
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", 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,其实相当于第一行的地址,是一维数组的地址
    printf("\n");
 
    //可以使用数组指针来进行接收:
    print_arr2(arr, 3, 5);
    return 0;
}

输出结果:

         ps:数组名arr,表示首元素的地址但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址。

四、数组参数和指针参数

1.一维数组传参:

        我们知道,在对数组进行传参时并不会真的在内存中创建临时数组,数组名的意义是作为数组内首元素的地址,因此当函数参数为数组名时,实际上传递的是数组中首元素的地址,于是我们可以发现下面三种传参方式都是可行的:

//方式一:完整的传递数组内容
void test1(int arr[3])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", arr[i]);
    }
}

//方式二:省略数组大小
void test2(int arr[])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", arr[i]);
    }
}

//方式三:扩大形参数组大小
void test3(int arr[100])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    int arr[3] = { 1,2,3 };

    test1(arr);
    test2(arr);
    test3(arr);

    return 0;
}

        这三种将形参写作数组形式的方式都是可行,但是为了避免出现错误推荐大家尽可能的使用第一种方式,其次是第二种方式。第三种方式虽然也可行,但是为了避免出现错误,不推荐大家使用。

        同时,以上三种将形参写为数组的方式,也可改为将形参写为指针的方式:

//方式一:一级指针
void test1(int* p)
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", *(p+i));
    }
}

//方式二:二级指针
void test3(int** p)
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%d ", **(p + i));
    }
}

int main()
{
    int arr[3] = { 1,2,3 };
    int arrA[3] = { 1,2,3 };
    int arrB[3] = { 4,5,6 };
    int arrC[3] = { 7,8,9 };
    int* arrD[3] = { &arrA,&arrB,&arrC };

    test1(arr);
    test3(arrD);

    return 0;
}

2.二维数组传参:

        我们在前面初阶的部分学习二维数组传参是就知道了,二维数组在进行传参时可以不知道有多少行但是必须知道有多少列,这样计算机才可以知道什么时候进行换行。如此只要知道什么时候进行换行,对于行数就不需要进行强制要求了,所以二维数组传参时,允许写成一下三种形式:

//方式一:二维数组接收
void test1(int arr[3][3])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

//方式二:省略行数
void test2(int arr[][3])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

//方式三:行数超出原数上限
void test3(int arr[20][3])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };

    test1(arr);
    test2(arr);
    test2(arr);

    return 0;
}

        同样的,二维数组除了可以用数组作为函数参数以外,也可以使用指针作为函数参数进行传参,区别在于一维数组传参时传的时首元素地址,而二维数组是首行地址:

void test(int (*p)[3])
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", (*p+i*3)[j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };

    test(arr);

    return 0;
}

        但是要格外注意,二维数组与一维数组不同,不可以使用二级指针进行传参。其原理是:二级指针的作用是用于存储一级指针的地址,而传递过来的参数是二维数组第一行(这里可以简单理解为一个以为数组,但本质上不是)地址,无法使用二级指针。

3.一级指针传参

        当我们在函数调用,并使用一级指针作为参数时,很容易理解:一级指针p中存放的是数组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;
}

4.二级指针传参

        首先最基础的用法很好理解,无非就是传递二级指针,就是用二级指针接受,无需过多阐述。直接上代码:

#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 test1(int *p)
{
    printf("%d "*p);
}
int main()
{
    //方式一:
    int a=10;
    test1(&a);
    
    //方式二:
    int *p=&a;
    test1(p);
    
    //方式三:
    int arr[1]={1};
    test1(arr);
    return 0;
}

        接着我们来看看二级指针:

void test(char **p)
{
 
}
int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
//方式一: 
test(&pc);
//方式二:
 test(ppc);
//方式三:
 test(arr);
 return 0;
}

        我们都知道二级指针是用来存储一级指针的地址,我们在前面也学过一个可以用来存储地址的——指针数组。也就是上面二级指针方式三的arr。

五、函数指针:

1.函数指针:

        我们都知道,在我们的程序中,各种值和组成成分都有自己的一片空间,我们的自定函数也不例外:

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 a = 10;
    int b = 20;
    int ret = Add(a, b);
    printf("a+b=%d\n", ret);

    int(*p)(int,int) = Add;
    //Add函数的返回类型为int类型,
    // 函数指针名为p
    //两个参数类型分别为int类型
    printf("%p\n", p);
    return 0;
}

        我们可以很清楚看到,通过使用函数指针就可以将函数的地址存储起来:

         并且我们可以通过使用函数指针优化我们的代码,大幅度提升我们代码可读性:

首先我们来看看这段代码:

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

        很明显,这段代码的可读性非常差,理解起来非常麻烦,于是我们可以通过使用函数指针来提升我们代码的可读性:

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

2.函数指针数组:

        数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:

int *arr[10];
//数组的每个元素是int*

        那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

答案是:parr1,parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。       

int Add(int x, int y)
{
    return x + y;
}
int Sub(int x, int y)
{
    return x - y;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
    int ret2 = Sub(a, b);

    printf("Add%d Add%d\n", ret,ret2);

    int(*p)(int,int) = Add;
    int(*Sp)(int,int) = Sub;

    //函数指针数组
    int(*pp[2])(int, int) = { p,Sp };

    int i = 0;
    for (i = 0; i < 2; i++)
    {
        printf("指针p[%d]中存放的地址为:%p\n", i, pp[i]);
    }

    //通过函数指针数组调用函数;
    for (i = 0; i < 2; i++)
    {
        int RET = pp[i](a, b);
        printf("%d\n", RET);
    }
    
    
    return 0;
}

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

        指向函数指针数组的指针是一个 指针,指针指向一个 数组,数组的元素都是函数指针;它的定义格式:

void test(const char* str)
{
 printf("%s\n", str);
}
int main()
{
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

示例:

int Add(int x, int y)
{
    return x + y;
}
int Sub(int x, int y)
{
    return x - y;
}
int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
    int ret2 = Sub(a, b);

    //使用函数指针存放函数地址:
    int(*Ap)(int,int) = Add;
    int(*Sp)(int,int) = Sub;

    //函数指针数组
    int(*p[2])(int, int) = { p,Sp };


    //使用指针指向函数指针数组:
    //即pp为指向函数指针数组的指针
    int* pp = &p;

    //上面这种形式是为了便于我们理解
    //而在面试和工作中更多的会写成下面这种形式

    int(*(*p_p))(int,int)=&p;
    printf("%p\n", &p);
    printf("%p\n", pp);
    printf("%p\n", p_p);
   
    
    
    return 0;
}

4.回调函数:

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

       在实际使用中,如果你把函数的地址(即使用指针)传递给另一个函数,且当这个指针调用了他指向的函数时我们就把这样的使用方式称为回调函数:

示例:

        

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
  for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

六、总结:

        至此我们算是给我们进阶指针画上了一个句号。我们学习了字符指针,指针数组,数组指针,数组参数和指针参数还有函数指针,同时也举一些例子。本文仍有许多不足之处,欢迎各位小伙伴们随时交流,批评指正。

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值