当前位置 : 主页 > 编程语言 > 其它开发 >

4小时彻底掌握C指针

来源:互联网 收集:自由互联 发布时间:2022-06-06
本文依据已故的印度程序员Harsha Suryanarayana图文讲解整理而来。 B站视频链接:https://www.bilibili.com/video/BV1bo4y1Z7xf 指针的基本介绍 关键词组: 内存图示(体系架构)、数据所在内存区域

本文依据已故的印度程序员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
上一篇:Vue 2.x 响应式原理(一)
下一篇:没有了
网友评论