• 4小时彻底掌握C指针


    本文依据已故的印度程序员Harsha Suryanarayana图文讲解整理而来。

    B站视频链接:https://www.bilibili.com/video/BV1bo4y1Z7xf

    指针的基本介绍

    关键词组:内存图示(体系架构)、数据所在内存区域(文本区、常量区、堆区、栈区),不同数据类型的内存分配情况、每行代码执行时在内存中的图示,指针,指针值&所占空间大小

    不同数据类型或变量在数据在内存中如何存储?

    首先内存是一块一块连续的地址,以字节为单位,一个字节为一个内存单元形如下图所示。

    内存根据实际情况分为不同的区段,当在代码中声明一个变量时,知晓其所在区段,此处我们在Main函数中声明一个int a;其会在栈区分配一块内存给变量a.

    不同编译器在对于不同的数据类型分配的字节数是不一样的。一般典型的编译器对于int char float数据类型分配情况如图所示,分配的字节数通过sizeof(数据类型)得到。

    代码行语句在内存中的运行情况:

    int a;

    char c;

    a=5;

    a++;

    当在Main函数中声明int a时,程序运行时首先会被分配在栈区,然后编译器根据其数据类型为整型随机分配4字节的地址为204~207,

    同样的,当声明char类型变量c时,分配字节大小为1,起始地址为209的内存空间给变量c.

    执行到a=5语句时,会将数据5(二进制00000000 00000000 00000000 00000110)以204为起始地址连续写入4字节到内存中,当然此处要注意数据写入时的大小端情况。

    同样的,当执行到a++语句时,会先找到变量a在内存中的起始地址,然后a自增,将数据6以二进制的方式写入到204~207中,在该语句其实就是找地址,修改(操作)该地址下的变量值。正好,C语言指针提供的这样的功能。

    指针变量:存储其他变量地址的变量。指针是强类型的,一定是指向特定的数据类型变量(指针值为该变量的起始地址),比如下图中的int *p其指向整型变量。

    p为地址,*p为解引用,即得到该地址下的值。

    以下面语句为例:

    int a;

    int *p;//声明整型指针p

    p=&a;//&符号位取地址符,p指向了整型变量a

    a=5;

    print p or &a //取得了整型a的地址

    *p=8;//通过指针修改地址下的值。

    当执行语句int *p时,首先会在栈区随机分配8字节(64位系统)固定大小的字节数用于存储变量a的地址值204.指针所占内存空间的大小与指针所指向的数据类型没有关系,指针始终是指向特定类型(int,char ,etc)的数据的首地址,即指针值为数据的首地址;

    而跟系统的寻址能力有关。32位机器为4字节,64位为8字节。

     

    指针代码示例

    关键词组:野指针、变量初始化、指针修改指向单元的值,指针运算

    野指针(wild Pointer):指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。野指针不是NULL空指针。

    成因一般有一下几点:

    指针变量未初始化

    指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针。

    在Debug模式下,VC++编译器会把未初始化的栈内存上的指针全部填成 0xcccccccc ,当字符串看就是 “烫烫烫烫……”;会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd,当字符串看就是 “屯屯屯屯……”。把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,使我们能够一眼就能确定我们使用了未初始化的野指针。在Release模式下,编译器则会将指针赋随机值,它会乱指一气。所以,指针变量在创建时应当被初始化,要么将其设置为NULL,要么让它指向合法的内存

    #include<stdio.h>
    int main()
    {
        int a;
        int* p;
        //p = &a;
        printf("%d\n", p);
        //printf("%d\n", *p);
        return 0;
    }

     此时p就是一个野指针。

    指针释放之后未置空

    有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

        int num = 6;
        int* p = &num;
        cout << *p << endl;
        free(p);//p所指向的堆内存单元已经释放
        cout << *p << endl;  /// p是野指针

    指针操作超越变量作用域

    不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

    class A {
    public:
      void Func(void){ cout << “Func of class A” << endl; }
    };
    class B {
    public:
      A *p;
      void Test(void) {
        A a;
        p = &a; // 注意a的生命期 ,只在这个函数Test中,而不是整个class B
      }
      void Test1() {
      p->Func(); // p 是“野指针”,Test函数指向完毕后,a在栈上的地址空间已经被清除掉
      }
    };

    定义变量要初始化,不然会产生随机值。

     通过指针修改指针指向的地址处的值。

     指针运算

    当指针指向了具体类型的数据后,可以对指针p进行运算,比如p++;p--。

    但是不能对指针运算过后的地址解引用取值( *(p+1) ),一般会得到随机数。

     

    指针的类型,算术运算,void指针

    关键词组:void指针、类型转换

     前面我们讲到指针是强类型,意味着需要用特定类型的指针变量来存放特定类型变量的地址。。如果我们想要一种通用类型的指针变量来存储所有类型变量,此时可以使用void指针。

    但是在将void指针进行类型转换成特定类型指针的时候,要特别的注意。不同的数据类型有不同的存储空间大小,可能会存在数据截断的情况。

    #include<stdio.h>
    /*
    * 
    知识点
    第一点:
    指针是强类型的,意味着:
    需要用特定类型的指针变量来存放特定类型变量的地址。
    int *-->int
    char*-->char
    那为什么不能用一个通用类型的指针变量来存储所有类型变量呢?-->void*的提出
    第二点:
    可以使用*来解引用,访问和修改这些地址对应的值。此处涉及到通用类型变量指针在解引用时候的情况处理
    不同的数据类型有不同的存储空间大小
    一般
    int  4 bytes;
    float 4 bytes
    char  1 byte
    
    */
    
    void TypeCaseTest()
    {
        int a = 1025;
        int* p;
        p = &a;
        printf("size of integer is %d bytes\n", sizeof(int));
        printf("address =%d, value=%d\n", p, *p);
        printf("address =%d,value=%d\n", p + 1, *(p + 1));
        char* p0;
        p0 = (char*)p;//type casting
        printf("size of char is %d bytes\n", sizeof(char));
        printf("address =%d,value=%d\n", p0, *p0);
        printf("address =%d,value=%d\n", p0 + 1, *(p0 + 1));
        //1025=00000000 00000000 00000100 00000001
    }
    //指针类型、类型转换、指针运算内容
    int main()
    {
        TypeCaseTest();
        return 0;
    }

     我们看到*p0值为1,即00000001,因为它是char指针解引用。系统只能取到1个字节的数据。*(p0+1)为00000100

    在使用void指针进行解引用(*p)或指针运算(p++)的时候,要特别的注意。可能存在报错的情况。

       int a = 1025;
        int* p;
        p = &a;
        printf("size of interger is %d bytes\n",sizeof(int));
        printf("address =%d,value=%d", p, *p);
        //Void pointer--Genric pointer
        void* p0;
        p0 = p;//此处不用进行强制转换p0=(int*)p
        printf("address=%d\n", p0);
        printf("address=%d\n", p0 + 1);//不知道p0具体error表达式必须包含指向 类 的指针类型,因为不知道P0指针指向的具体数据类型,所以没法其指针+1,不知道具体类型,有可能地址+1,+4
        printf("value=%d\n", *p0);//error 表达式必须包含指向 类 的指针类型,因为不知道P0指针指向的具体数据类型,所以没法对其解引用。

    指向指针的指针

     指针的套娃。

    理解几个形式:

    int x=6;

    int* p=&x;

    int ** q=&p;//q为一个指向指针的指针

    int ***r=&q;//r为一个指针的指针的指针

                     

                                             

     在对套娃的指针进行解引用的时候,一层一层通过*解引用即可。

        int x = 5;
        int* p;
        p = &x;
        *p = 6;//修改值
        int** q;
        q = &p;
        int*** r;
        r = &q;
        printf("%d\n", *p);
        printf("%d\n", *q);
        printf("%d\n", **q);
        printf("%d\n", **r);
        printf("%d\n", ***r);
        ***r = 10;
        printf("x=%d\n", x);
        **q = *p + 2;
        printf("x=%d\n", x);

    函数传值VS传引用

     关键词组:内存模型图、传值与传引用区别

    指针一个典型应用是作为函数参数使用。我们先以函数传值&传引用为例讲述两者区别。

    #include<stdio.h>
    void Increment(int a)
    {
        a = a + 1;//x=x+1;
        printf("address of variable a in increment =%d\n", &a);
    }
    void IncrementByReference(int* p)
    {
        *p = *p + 1;
    }
    int main()
    {
        int a = 10;
        Increment(a);
        printf("address of variable a in main =%d\n", &a);
        printf("value of a= %d\n", a);
    View Code

    上述运行在内存中运行过程如下图:

                                                           

     图中Code,static/Gloal和Stack空间都是固定大小,Heap空间是不固定的可以自行去分配和释放。

    执行主函数时,执行int a=10后会在main函数的栈区(stack frame)分配内存给局部变量(起始地址为300);

    执行到Increment(a)后系统产生中断,转而执行子程序Increment,内存分配另外的栈空间给此函数,其中会将主程序实参a的值拷贝给形参a(tips:两者地址不一样),如图红框处的栈空间。当Increment执行完毕后其栈空间内容清除掉,随后回到主函数往下执行打印函数。

    a的值未改变,仍然为10。

    传引用修改值:

    void IncrementByReference(int* p)//此处int* p是指针参数的声明形式
    {
        *p = *p + 1;
    }
    int main()
    {
        int a = 10;
        IncrementByReference(&a);
    }

    结果为11.

    内存运行情况分析:

                                                                                   

    实参将a地址(address=308)传给指针p后,在子函数内部指针解引用修改308地址处的值,该地址空间始终存在。

    最后a=11。

    对比传值跟传引用,会发现函数传值,内存会额外分配多的空间(如上例中increment的栈区空间都是),而传引用只会分配4或8字节的P指针的空间,如果参数数据类型更加复杂,increment栈区空间局部变量所占空间更大。

    指针和数组

    注意几点:

    1、指针在进行算术运行时(如增加1时,p+1),会产生野指针(因为p+1,即p+sizeof(特定类型),该地址下的值不知道是什么。。)

    2、数组可以在数组长度内进行算术运行

         int A[5]

        print A+1;A+2;...A+4

    3.数组索引i处的地址和值的表示:

       address:&A[I] or (A+i)

        value:   A[i] or  *(A+i)

    #include<stdio.h>
    int main()
    {
        int A[] = { 2,4,5,8,1 };
        int i;
        int* p = A;
        p++;
        //A++;//报错:表达式必须是可修改的左值(A此时是数组A的地址,是一个常量值,不能进行算术运算)
        printf("%d\n", A);
        printf("%d\n", &A[0]);
        printf("%d\n", A[0]);
        printf("%d\n", *A);
        for (int i = 0; i < 5; i++)
        {
            printf("address =%d\n",& A[i]);
            printf("address =%d\n", A+i);
            printf("value =%d\n", A[i]);
            printf("value =%d\n", *(A + i));
        }
        return 0;
    }
    View Code

     数组作为函数参数

    注意一点:数组作为函数参数时,整个数组不会被拷贝到子函数的栈空间中,编译器只是创建了一个同名的指针(而不是创建整个数组),将数组首地址拷贝给该特定类型(int,char etc)的指针。

     如以下求和的示例:

                                                   

     注意:main函数栈帧与SOE栈帧中A的不同。main中A为数组A(20字节),SOE中A为同名的整型指针(4字节)

    int main()
    {
        int A[] = { 1,2,3,4,5 };
        //第一种方法求解总和
        {
            int size = sizeof(A) / sizeof(A[0]);
            int total = SumOfElement(A, size);
        }
        //第二种得到的结果不对,
        int total = SumOfElement1(A);
        printf("Sum of elements=%d\n", total);
        printf("Main-size of A=%d,size of A[0]=%d\n", sizeof(A), sizeof(A[0]));
        return 0;
    }
    int SumOfElement(int A[],int size)//int *A  or intA[]  it's the same(编译器会隐式转换intA[] 为int *A
    {
        int sum = 0;
        for (int i = 0; i < size; i++)
        {
            sum += A[i];
        }
        return sum;
    }
    int SumOfElement1(int A[])
    {
        int sum = 0;
        int size = sizeof(A) / sizeof(A[0]);
        // sizeof(A)为4,因为编译器此处将并不会拷贝实参的整个数组值,而是数组指针(其实深入思考,如果数组很大,直接拷贝数组容量大小的数据也会浪费空间),int A[]等价于int *A,所以sizeof(A)为4
        printf("SOE-size of A=%d,sizeof A[0]=%d\n", sizeof(A), sizeof(A[0]));
        for (int i = 0; i < size; i++)
        {
            sum += A[i];
        }
        return sum;
    }
    View Code
    方法1 result=15;
    方法2 result=1;//结果错误
    void Double(int* A, int size)
    {
        for (int i = 0; i < size; i++)
        {
            A[i] = 2 * A[i];
        }
    }
    int main()
    {
        int A[] = { 1,2,3,4,5 };
        {
            int size = sizeof(A) / sizeof(A[0]);
            Double(A, size);
            for (int i = 0; i < size; i++)
            {
                printf("%d ", A[i]);
            }
        }
    View Code
    result: 2 4 6 8 10

     指针和字符数组

    关键词组:字符数组的几种声明;'\0';指针作为函数参数;常量指针(指针指向的内容只读,不能写);指针常量;

    如何存储字符串,以存储字符串"JOHN"为例

                                                                  

     C语言中没有字符串类型的概念,只有字符数组。此字符串长度为4,sizeof(C)字节长度为5,用字符数组存储数组长度要大于等于len+1;此处即5,char C[5],char[20]都是合法的。

    C语言的基本数据类型中并没有字符串类型,在使用的过程中通过指针或字符数组来实现。但是两者在内存中的位置还是有差别的。

    char str1[] = "abcd";

    char *str2 = "abcd";

    于str1在内存中的存放方式是{‘a’,‘b’,‘c’,‘d’,’\0’},是以字符数组的形式存放在内存中,在函数定义时存放在栈区,函数结束就释放。

    str2存放在字符常量区,即全局区,当程序结束才释放

    字符数组的几种声明方式:

        char C[5];//逐一赋值
        C[0] = 'J';
        C[1] = 'O';
        C[2] = 'H';
        C[3] = 'N';
        C[4] = '\0';
    int main()
    {
        char C[5];
        C[0] = 'J';
        C[1] = 'O';
        C[2] = 'H';
        C[3] = 'N';
        C[4] = '\0';
        int len = strlen(C);
        printf("%s\r\n", C);//JOHN
        printf("length is %d\r\n", len);//4
    }
    View Code
    char C[5] = "JOHN";//第二种写法:可以不用写结束符,但是字符数组空间要>=len+1
    char C[5] = "JOHN";//可以不用写结束符,但是字符数组空间要>=len+1
        
        printf("size of bytes =%d\n", sizeof(C));
        int len = strlen(C);
        printf("length =%d\n", len)
    result:
    size of bytes =5
    length =4
    char AnotherC[5] = { 'J','O','H','N','\0' };//第三种:另一种声明字符数组的写法,需要显示写上结束符\0

    字符数组与字符指针相关操作:

        char C1[6] = "HELLO";
        char* C2;
        C2 = C1;
        //print C2[1];//e
        C2[0] = 'A';//C2[i] is *(C2+i)
        C1=C2;//报错    E0137    表达式必须是可修改的左值    
        C1=C1+1;X C1是一个常量
    C2++是对的

    字符指针(假设p)作为函数参数时,会在该函数栈帧中存储p,且p=实参地址值

    void print(char* C)
    {
        int i = 0;
        while (C[i]!='\0')//C[i] equals *(C+i),so 也可以写成*(C+i)
        {
            printf("%c", C[i]);
            i++;
        }
        //下面的也是对的
        //while (*C!='\0')
        //{
        //    printf("%c", *C);
        //    C++;
        //}
        printf("\n");
    }
    int main()
    {
        char C[20] = "HELLO";
        print(C);
        return 0;
    }
    View Code

    程序会打印出:HELLO

    我们也可以在print函数中对字符数组值进行修改

        C[0] = 'A';//c[i] 等于*(c+i)
        while (*C!='\0')
        {
            printf("%c", *C);
            C++;
        }
        printf("\n");

    结果为:AELLO

    如果我们不想在print函数的时候数组值被修改,或者说函数是只读的,可以在指针参数前加上const关键字这样传入函数

    void print(const char* C)
    {
    ...
    }

    const char * c :表示c指针所指向的内存内容不能改变,是只读的。

    char * const c:表示c是一个常量指针,指针值不可变。

     指针和动态内存

    栈vs堆

     关键词组:内存模型;堆栈溢出(stack overflow);堆的引入,两者区别;

    如下图:

     内存被分为代码区、静态常量区、栈区、堆区。

    static变量以及全局变量会分配在静态常量区,会随着程序一直存在,程序结束,对应空间就进行释放了。

    函数以及函数的局部变量存储在栈区;如上图中main函数以及其下 的a,b;sos里面的x,y,z;sq里面的r变量,栈空间在程序刚开始时就已经分配好了,对于 x86 和 x64 计算机,默认堆栈大小为 1 MB。当程序一直嵌套调用,或者递归调用耗尽栈空间时,会出现stack overflow栈溢出。另外,栈空间都是固定大小的,如果我们想根据传入的参数n值来动态的分配大容量(超过1M)的数组时,很明显此时栈空间已经不符合我们的要求了。此时可以通过堆来存储大容量的数组。

    堆空间时自己申请,自己释放的。若程序员不释放的话,程序结束时可能由OS回收,但其与数据结构中的堆是两回事,分配方式倒是类似于数据结构的链表。

    堆空间一般是很大的一段地址空间。

    C语言中堆空间申请通过malloc申请,free进行释放。C++中配套使用new delete

    程序执行片段内存分析:

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
        int a;
        int* p;
        p = (int*)malloc(sizeof(int));
        *p = 10;
    free(p);
       p = (int*)malloc(sizeof(int));
    *p=20
    }

                                                         

     首先程序在执行main函数,在栈区申请一段空间存储main函数栈区,在该区域还有局部变量a,p;

    执行malloc语句申请一段4字节堆内存,堆内存的地址由指针p执行,即p赋值为该堆空间地址。

    此时地址200处还未填值,通过*操作解引用堆空间内容为10.

    赋值完成后,执行free,释放p指向的堆内存。

                                                                                         

    执行malloc重新申请一段空间,p指针指向这段还未赋值的空间。

    *p解引用将20填入该堆空间。

    当然也可以申请一段连续的空间

    p=(int*)malloc(20*sizeof(int));

    //p[0],p[1],p[2] or *p,*(p+1) becase p[i] equals *(p+i)

    malloc calloc realloc free

    malloc函数用于在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,其不初始化时内容为为随机值。 

    函数原型:void* malloc (size_t size);

    calloc() 函数用来动态地分配内存空间并初始化为 0,

    函数原型:void* calloc (size_t num, size_t size);

    realloc函数用来扩大已经开辟好的堆空间。

    void *realloc(*mem_addr,unsigned int newsize)

    含义是:(数据类型*)realloc(要扩大内存的指针名,新的内存大小)

    这里有2种情况:

    1、够开辟新的newsize,即mem_addr开始的空闲内存不小于newsize,则返回mem_addr。

    2、不够开辟的newsize,即mem_addr开始的空闲内存小于newsize,则会换一个新的地方重新开辟newsize大小的内存,并将mem_addr处开始的数据自动拷贝到新的地址,mem_addr开始的原来的内存也自动释放掉,不用手动free。返回新的首地址。

    int main()
    {
        int n;
        printf("enter size of array\r\n");
        scanf_s("%d", &n);
        int* A = (int*)malloc(n * sizeof(int));//malloc不初始化的时候为随机值
       //int *A = (int*)calloc(n, sizeof(int));//calloc函数不初始化的时候都为0
        for (int i = 0; i < n; i++)
        {
            A[i] = i + 1;
        }
        //free(A);
        int* B = (int*)realloc(A, n * sizeof(int));
        //int* B = (int*)realloc(A, 0);//equivalent to free(A)
        //int* B = (int*)realloc(NULL, n * sizeof(int));//equivalent to malloc(n*sizeof(int))
        printf("prev block address =%d,new address=%d\n", A, B);
        for (int i = 0; i < 2*n; i++)
        {
            printf("%d ", A[i]);
        }
        return 0;
    }

    输入5,结果为:

    enter size of array
    5
    prev block address =18497888,new address=18497888
    1 2 3 4 5 -33686019 -636223161 35591 18528192 18501808

    说明realloc够开辟新的newsize,直接在A指针原有的数据后面追加了newsize长度的内存空间。

    内存泄漏

    内存泄漏是指不当地使用动态内存或者内存的堆区,泄漏的内存在一段时间增长。内存泄漏总是因为堆中未使用和未引用的内存块发生的。栈空间会自动清除,顶多出现stackoverflow堆栈溢出。

                             

    代码变量的内存分布情况如上图所示:

    关键点解释一下:

    在TestMemoryOverflow函数堆栈中,存在指针C,指向堆中100字节的数组首地址,函数执行完毕后,C指针空间清除,堆中这100字节未使用,while一直执行,程序就内存泄漏了。【会看到随着程序运行,任务管理器中该程序使用内存一直变大】

    另外两个函数中的C变量一个是栈分配空间,自动清除;另一个是堆空间,但是会free手动释放。【会看到随着程序运行,任务管理器中该程序使用内存一直维持在一个稳定值】

    // ConsoleApplication2.cpp : 定义控制台应用程序的入口点。
    //
    
    #include "stdafx.h"
    #include<stdio.h>
    #include<stdlib.h>
    int GlobalCount = 0;
    void NormalMemoryAlloc()
    {
        char C[100];
        printf("normal memory\r\n");
    }
    void TestMemoryOverflow()
    {
        char* C =(char*) malloc(100 * sizeof(char));
        printf("MemoryOverflow\r\n");
    }
    void NoMemoryOverflow()
    {
        char* C = (char*)malloc(100 * sizeof(char));
        printf("MemoryOverflow\r\n");
        free(C);
    }
    
    int main()
    {
        while (true)
        {
            GlobalCount++;
            NormalMemoryAlloc();
            TestMemoryOverflow();
            NoMemoryOverflow();
        }
        
    }
    View Code

    函数返回指针

     关键词组:指针是一种类型、函数返回指针的使用场景、被调函数访问主函数变量、返回被调函数的局部变量给主调函数

     当我们进行Add操作时,如果传递值进行调用,可以查看在传递参数时是进行值拷贝,形参实参地址分别在不同栈帧空间下,如下:

    int Add(int a, int b)
    {
        printf("address of a in Add  =%d\n", &a);
        int c = a + b;
        return c;
    }
    int main()
    {
        int a = 10, b = 20;
        printf("address of a in main =%d\n", &a);
        //call by value
        int c = Add(a, b);  //value in a of main is cpoied to a of add;
                            //value in b of main is cpoied to b 
                                          of add;
    }
    View Code

    结果如下:

    address of a in main =5962472
    address of a in Add  =5962208

    引用传递时,结果如下:

    int AddByReference(int *a, int *b)
    {
        printf("address of a in AddByReference  =%d\n", a);
        int c = *a + *b;
        return c;
    
    }
    int main()
    {
        int a = 10, b = 20;
        printf("address of a in main =%d\n", &a);
        int c = AddByReference(&a, &b);
        printf("Sum=%d\n", c);
    }
    View Code
    address of a in main =8125180
    address of a in AddByReference  =8125180
    Sum=30

    如果被调函数返回指针呢?

    //此情况下主调函数无法访问到被调函数的局部变量(因为该栈空间在函数调用结束后就清除了)
    int* AddByRefReturnPointer(int *a, int *b)
    {
        int c = *a + *b;
        return &c;
    
    }
    int main()
    {
        int a = 10, b = 20;
        printf("sum =%d\n", *res);//打印随机数
    }
    View Code

    虽然结果sum=30是正确的,但是当我们在打印前调用PrintHelloWorld函数时,sum为随机值

    分析其内存分配情况:

    在执行AddByRefReturnPointer时,会在栈上分配AddByRefReturnPointer栈帧空间,里面有4字节的指针a,b,局部变量c(假设地址为144,值则为30)。

    当函数指向完毕时,该栈帧空间被清除掉,虽然c的地址以返回值的形式返回到主函数res指针,但是该指针所值的内存空间已经被清除掉。

    当执行PrintHelloWorld函数时,该地址值可能被重写,可能未分配值是随机数。

    void PrintHelloWorld()
    {
        printf("hello world\n");
    }
    
    int* res=AddByRefReturnPointer(&a,&b);
    PrintHelloWorld();
    printf("sum =%d\n", *res);//打印随机数
    hello world
    sum =-858993460

    如果想要正常返回结果呢?

    //次此情况下,主调函数可以访问到被调函数的局部变量(因为其
    int* AddByRefReturnPointer1(int *a, int *b)
    {
        int* c = (int*)malloc(sizeof(int));
        *c= *a + *b;
        return c;//函数执行完毕指针在该堆栈上的空间(4字节)释放了,但是堆上空间没有,同时堆上该空间的首地址作为返回值返回到了主函数
    }

    分析其内存分配情况:

    在执行AddByRefReturnPointer1时,会在栈上分配AddByRefReturnPointer1栈帧空间,里面有4字节的指针a,b,局部变量指针c(假设地址为144,则指向的地址的值为30)。

    当函数指向完毕时,该栈帧空间被清除掉,c的地址以返回值的形式返回到主函数res指针,其指向的堆地址空间也存在,故主函数可以对其进行访问

    当执行PrintHelloWorld函数时,该地址值也不会存在问题。

    被调函数执行时,主调函数能够确保还在栈内存中(依据栈的数据结构特点),所以被调函数此时还能访问主调函数局部变量(main函数中的变量地址对Add来讲是可以访问的)

    但是如果我们尝试返回一个被调函数的局部变量给主调函数时呢,就会出现问题。(因为被调函数的栈空间已经被释放了)

    一层一层递进,所以也就有了函数的入口函数main函数。

    函数指针

     关键词组:函数指针的引出(汇编中jump,函数指针指向函数的入口地址entrypoint)、定义以及使用

    函数指针是指向函数的指针变量。

    通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。

    函数指针可以像一般函数一样,用于调用函数、传递参数(回调函数)。

    函数指针变量的声明:

    typedef int (*fun_ptr)(int,int); // 声明一个指向int,int型的参数、int返回值的函数指针类型
    #include<stdio.h>
    void PrintHello(const char* name)
    {
        printf("hello %s\n", name);
    }
    int Add(int a, int b)
    {
        return a + b;
    }
    int main()
    {
        //两种书写方式:
        int c;
        int (*p)(int, int);//声明函数指针p,该指针所指向的函数返回值为int,形参为(int,int)
        //p = &Add;//指针建立指向
        //c = (*p)(2, 3);//p指针解引用获得函数,然后传入参数执行函数
        p = Add;//函数名也是函数的首地址,没毛病
        c = p(2, 3);//p指针解引用获得函数,然后传入参数执行函数
        printf("%d\n", c);
        void (*ptr)(const char*);
        ptr = &PrintHello;
        ptr("jack");
    
    }

    注意:int (*p)(int, int)的括号,没有括号,编译器将假定这p是一个普通的函数名,形参为int,int,并返回一个指向整数的指针。

    上面部分的函数指针主要用户函数调用的功能;下面讲解函数指针作为函数参数实现函数回调。

    函数指针的使用(回调函数)

    关键词组:回调函数的定义、回调函数三部分、回调函数使用场景(QuickSort)、事件

    维基百科定义:

    回调通常与原始调用者处于相同的抽象层

    在计算机程序设计中,回调函数,或简称回调(Callback),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。

    知乎:桥头堡 回答

    什么是回调函数?

    我们绕点远路来回答这个问题。

    编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。

    当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

    打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):

                                                               

    #include<stdio.h>
    void A()//A是回调函数,回调函数就是用来给别人调用的函数
    {
        printf("Hello");
    }
    void B(void(*ptr)())//function pointer as argument
    {
        ptr();//call back function that "ptr" points to
    }
    int main()
    {
        /*void(*p)() = A;
        B(p);*/
        //等价于下面的
        B(A);//A是回调函数(我理解回调函数的调用过程就是把该函数指针传入主调函数,然后主调函数B通过函数指针来回调它,
             //回调就是它作为B的参数,传入实参后进入B函数的函数体,结果反而回来被调用。
    }
    View Code

    关于回调函数的详细介绍,后续会单独出一篇。

    参考:

    https://www.cnblogs.com/kira2will/p/3477511.html#:~:text=%E5%9C%A8%20%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%20%E4%B8%AD%EF%BC%8C%20%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0%20%EF%BC%8C%E6%88%96%E7%AE%80%E7%A7%B0%20%E5%9B%9E%E8%B0%83%20%EF%BC%88Callback%EF%BC%89%EF%BC%8C%E6%98%AF%E6%8C%87%E9%80%9A%E8%BF%87%20%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0,%E5%BC%95%E7%94%A8%20%E3%80%82%20%E8%BF%99%E4%B8%80%E8%AE%BE%E8%AE%A1%E5%85%81%E8%AE%B8%E4%BA%86%20%E5%BA%95%E5%B1%82%20%E4%BB%A3%E7%A0%81%E8%B0%83%E7%94%A8%E5%9C%A8%E9%AB%98%E5%B1%82%E5%AE%9A%E4%B9%89%E7%9A%84%20%E5%AD%90%E7%A8%8B%E5%BA%8F%20%E3%80%82%20%E7%BB%B4%E5%9F%BA%E7%99%BE%E7%A7%91%E9%93%BE%E6%8E%A5%EF%BC%9Ahttp%3A%2F%2Fzh.wikipedia.org%2Fzh-cn%2F%25E5%259B%259E%25E8%25B0%2583%25E5%2587%25BD%25E6%2595%25B0

     https://www.zhihu.com/question/19801131/answer/17156023?utm_source=weibo&utm_medium=weibo_share&utm_content=share_answer&utm_campaign=share_button
  • 相关阅读:
    Leetcode-Minimum Depth of Binary Tree
    Leetcode-Path Sum II
    Leetcode-Path Sum
    Leetcode-Flatten Binary Tree to Linked List
    Leetcode-Populating Next Right Pointer in Binary Tree II
    Leetcode-Pascal's Triangle II
    Leetcode-Pascal's Triangle
    Leetcode-Triangle
    第10月第20天 afnetwork like MKNetworkEngine http post
    第10月第13天 xcode ipa
  • 原文地址:https://www.cnblogs.com/shuzhongke/p/16322385.html
Copyright © 2020-2023  润新知