C语言复习第八天

C语言复习第八天

  • 函数与指针分析
    • 函数类型
    • 函数指针
    • 回调函数
    • 小结
  • 指针阅读技巧
    • 右左法则
    • 小结
  • 动态内存分配
    • 动态内存分配的意义
    • malloc和free
    • 内存泄漏检测模块
    • calloc和realloc
    • 小结
  • 程序中的三国天下
    • 程序中的栈
    • 函数调用栈上的数据
    • 程序中的堆
    • 程序中静态存储区
    • 小结
  • 程序的内存分布
    • 程序文件的一般布局
    • 程序和进程
    • 程序文件的一般布局
    • 程序的内存布局
    • 程序术语的对应关系
    • 小结

函数与指针分析

函数类型

  1. C语言中的函数有自己特定的函数类型
  2. 函数的类型由返回值,参数类型和参数个数共同决定
			int  add(int i,int j)的类型为 int(int,int) 
  1. C语言中通过typedef函数类型重命名
  typedef type name(parameter list)
例如:
	typedef int f(int ,int);
	typedef void p(int);

函数指针

  1. 函数指针用于指向一个函数
  2. 函数名是执行函数体的入口地址
  3. 可以通过函数类型定义函数指针:
				FuncType* pointor;
  1. 也可以直接定义:
			type(*pointor)(parameter list)
			——pointor 为函数指针名
			——type  为所指的函数的返回类型
			——parameter list 为所指函数的参数类型列表

tips:
面试小问题:
如何使用C语言直接跳转到某个固定的地址开始执行?
通过函数指针,因为一个函数名,即为函数的入口的地址,函数指针指向的其实就是一个地址。
示例:

#include 

typedef int(Func)(int);

int test(int n)
{
     
   return n*n;
}

void fun()
{
     
  printf("Call fun()\n");
}

int main(int argc,char*argv[])
{
     

    Func* pt=test;   
    void(*pf)()=NULL;
    if(argc>1)
    {
     
      pf=0x400576;    //函数指针指向固定地址
      printf(" void(*pf)()=0x400576\n");
    }
    else
    {
     
      pf=fun;
      printf(" void(*pf)()=fun\n");
    }
    printf("pf = %p\n", pf);
    printf("f = %p\n", fun);
    printf("&f = %p\n", &fun);
    
    pf();
    (*pf)();
    
    printf("Call function point:%d\n",pt(6));
    return 0;
}

C语言复习第八天_第1张图片

回调函数

  1. 回调函数是利用函数指针实现的一种调用机制
  2. 回调机制原理:
    ——调用者不知道具体事件发生时需要调用的具体函数
    ——被调函数不知道何时被调用,只知道需要完成的任务
    ——当具体事件发生时,调用者通过函数指针调用具体函数
  3. 回调机制中的调用者和被调函数互不依赖。

示例:

#include 

typedef  void(*Attack)(int );

void knife(int n)
{
     
    for(int i=0;i<n;i++)
    {
     
      printf("Boss attack by knife\n");
    }
    printf("Boss lose %d blood\n",n);
}

void jian(int n)
{
     
    for(int i=0;i<n*3;i++)
    {
     
      printf("Boss attack by jian\n");
    }
    printf("Boss lose %d blood\n",n*3);
}

void gun(int n)
{
     
    for(int i=0;i<n*5;i++)
    {
     
      printf("Boss attack by gun\n");
    }
    printf("Boss lose %d blood\n",n*5);
}

void fight(Attack at,int n)
{
     
    at(n);
}

int main()
{
     
    fight(gun,1);
    fight(jian,1);
    fight(knife,1);
    return 0;
}

C语言复习第八天_第2张图片

小结

  1. C语言中的函数都有特定的类型
  2. 可以使用函数类型定义函数指针
  3. 函数指针是实现回调机制的关键技术
  4. 通过函数指针可以在C程序中实现固定地址跳转

指针阅读技巧

右左法则

  1. 从最里层的圆括号中未定义的标识符看起
  2. 首先往右看,再往左看
  3. 遇到圆括号或方括号时可以确定部分类型,并调转方向
  4. 重复2,3步骤,直到阅读结束。

示例:

#include 

int main()
{
      
    int (*p1)(int*, int (*f)(int*));
    // p1是一个函数指针,返回值为int,参数:(int *,和一个参数为int * ,返回值为int 类型的函数指针。)
    
    int (*p2[5])(int*);
    // p2是一个数组,有五个元素为指针,指向函数,函数的返回值为int ,参数为int *
    int (*(*p3)[5])(int*);
    // p3是一个数组指针,指向的数组有5个元素,这5个元素为函数指针,指向的函数类型为int(int *)
    int*(*(*p4)(int*))(int*);
    //p4是一个指针,函数指针,参数为int*类型,返回值为int*(int *)
    int (*(*p5)(int*))[5];
	//p5为指针,函数指针,参数为int *,返回值为指针,指向数组,指向数组的类型为int[5]
    return 0;
}

简化示例:

 int (*(*p5)(int*))[5];
可以简化为:

typedef int (ArrayType)[5]
typedef ArrarType*(FuncType)(int *) 

 FuncType*p5; //和上述p5指针等价

小结

  1. 右左法则总结于编译器对指针变量的解析过程
  2. 指针阅读练习的意义在于理解指针的组合定义
  3. 可通过typedef简化复杂指针的定义

动态内存分配

动态内存分配的意义

  1. C语言中的一切操作都是基于内存的
  2. 变量和数组都是内存的别名
    ——内存分配由编译器在编译期间决定
    ——定义数组的时候必须指定数组的长度
    ——数组长度是在编译期就必须确定的
    需求:
    程序运行的过程中,可能需要使用一些额外的内存空间

malloc和free

  1. malloc 和 free用于执行动态内存分配和释放
  2. malloc所分配的是一块连续的内存
  3. malloc以字节为单位,并且不带任何的类型信息
  4. free用于将动态内存归还系统
		void* malloc(size_t size);
		void free(void* pointer);

tips

  1. malloc和free是库函数,而不是系统调用
  2. malloc分配的内存可能会比请求的多
  3. 不能依赖于不同平台下的malloc行为
  4. 当请求的动态内存无法满足时 ,malloc返回NULL
  5. 当free的参数为NULL时,函数直接返回

内存泄漏检测模块

//mleak.h

#ifndef _MLEAK_H_
#define _MLEAK_H_

#include 

#define MALLOC(n) mallocEx(n, __FILE__, __LINE__)
#define FREE(p) freeEx(p)

void* mallocEx(size_t n, const char* file, const line);
void freeEx(void* p);
void PRINT_LEAK_INFO();

#endif
//mleak.c
#include "mleak.h"

#define SIZE 256

/* 动态内存申请参数结构体 */
typedef struct
{
     
    void* pointer;
    int size;
    const char* file;
    int line;
} MItem;

static MItem g_record[SIZE]; /* 记录动态内存申请的操作 */

void* mallocEx(size_t n, const char* file, const line)
{
     
    void* ret = malloc(n); /* 动态内存申请 */
    
    if( ret != NULL )
    {
     
        int i = 0;
        
        /* 遍历全局数组,记录此次操作 */
        for(i=0; i<SIZE; i++)
        {
     
            /* 查找位置 */
            if( g_record[i].pointer == NULL )
            {
     
                g_record[i].pointer = ret;
                g_record[i].size = n;
                g_record[i].file = file;
                g_record[i].line = line;
                break;
            }
        }
    }
    
    return ret;
}

void freeEx(void* p)
{
     
    if( p != NULL )
    {
     
        int i = 0;
        
        /* 遍历全局数组,释放内存空间,并清除操作记录 */
        for(i=0; i<SIZE; i++)
        {
     
            if( g_record[i].pointer == p )
            {
     
                g_record[i].pointer = NULL;
                g_record[i].size = 0;
                g_record[i].file = NULL;
                g_record[i].line = 0;
                
                free(p);
                
                break;
            }
        }
    }
}

void PRINT_LEAK_INFO()
{
     
    int i = 0;
    
    printf("Potential Memory Leak Info:\n");
    
    /* 遍历全局数组,打印未释放的空间记录 */
    for(i=0; i<SIZE; i++)
    {
     
        if( g_record[i].pointer != NULL )
        {
     
            printf("Address: %p, size:%d, Location: %s:%d\n", g_record[i].pointer, g_record[i].size, g_record[i].file, g_record[i].line);
        }
    }
}
#include 
#include "mleak.h"

void f()
{
     
    MALLOC(100);
}

int main()
{
     
    int* p = (int*)MALLOC(3 * sizeof(int));
    
    f();
    
    p[0] = 1;
    p[1] = 2;
    p[2] = 3;
    
    FREE(p);
    
    PRINT_LEAK_INFO();
    
    return 0;
}


在这里插入图片描述

检测出有100字节的内存没有释放。

calloc和realloc

  1. malloc 的同胞兄弟
		void *calloc(size_t num,size_t size);
		void *relloc(void * pointer,size_t new_size);
  1. calloc的参数代表所返回内存的类型信息
    ——calloc会将返回的内存初始化为0
  2. relaloc用于修改一个原先已经分配的内存块大小
    ——在使用realloc之后应该使用其返回值
    ——当pointer的第一个参数为NULL,等价于malloc

示例:

#include 
#include 

#define SIZE 5

int main()
{
     
    int i = 0;
    int* pI = (int*)malloc(SIZE * sizeof(int)); 
    short* pS = (short*)calloc(SIZE, sizeof(short));
    
    for(i=0; i<SIZE; i++)
    {
     
        printf("pI[%d] = %d, pS[%d] = %d\n", i, pI[i], i, pS[i]);
    }
    
    printf("Before: pI = %p\n", pI);
    
    pI = (int*)realloc(pI, 2 * SIZE * sizeof(int));
    
    printf("After: pI = %p\n", pI);
    
    for(i=0; i<10; i++)
    {
     
        printf("pI[%d] = %d\n", i, pI[i]);
    }
    
    free(pI);
    free(pS);
    
    return 0;
}

C语言复习第八天_第3张图片

小结

  1. 动态内存分配是C语言的强大功能
  2. 程序能够在需要的时候有机会使用更多的内存
  3. malloc单纯的从系统中申请固定字节大小的内存
  4. calloc能以类型大小为单位申请内存并初始化为0
  5. realloc用于重置内存大小

程序中的三国天下

程序中的栈

  1. 栈是现代计算机程序里最为重要的概念之一
  2. 栈在程序中用于维护函数调用上下文
  3. 函数中的参数和局部变量存储在栈上
  4. 栈保存了一个函数调用所需要的维护信息

栈的参数如下:C语言复习第八天_第4张图片
函数调用的栈变化一:
C语言复习第八天_第5张图片
变化二
C语言复习第八天_第6张图片
变化三:

C语言复习第八天_第7张图片

函数调用栈上的数据

  1. 函数调用时,对应栈空间在函数返回前是专用的
  2. 函数调用结束后,栈空间将被释放,数据不再有效
    示例:
#include 

int* g()
{
     
    int a[10] = {
     0};
    
    return a;
}

void f()
{
     
    int* pointer = g();
    int b[10]={
     0,1,2,3,4,5,6,7,8,9};
    /*for(int i=0;i<10;i++)
    {
      b[i]=pointer[i];
    }
    */
    for(int i=0;i<10;i++)
    {
     
      printf("%d\n",pointer[i]);
    }
}

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

内存访问出错,因为内存已经被释放掉,无法进行访问了。
在这里插入图片描述

程序中的堆

  1. 堆是程序中一块预留的内存空间,可由程序自由使用
  2. 堆中被程序申请使用的内存在被主动释放前将一直有效

问题: 为什么有了栈还需要堆呢?
栈上的数据在函数返回后就会被释放掉无法传递到函数外部,如:局部数组

  1. C语言程序中通过库函数的调用获得堆空间
    ——头文件: malloc.h
    ——malloc --以字节的方式动态申请堆空间
    ——free --将堆空间归还给系统

C语言复习第八天_第8张图片

程序中静态存储区

  1. 静态存储区随着程序的运行而分配空间
  2. 静态存储区的生命周期直到程序运行结束
  3. 在程序的编译期静态存储区的大小就已经确定
  4. 静态存储区主要用于保存全局变量和静态局部变量
  5. 静态存储区的信息最终会保存到可执行程序中

示例:验证静态存储区的存在

#include 

int g_v = 1;

static int g_vs  = 2;

void f()
{
     
    static int g_vl = 3;
    
    printf("%p\n", &g_vl);
}

int main()
{
     
    printf("%p\n", &g_v);
    
    printf("%p\n", &g_vs);
    
    f();
    
    return 0;
}

在这里插入图片描述
最后可以发现:它们地址是连在一起,所以可以判断静态存储区的存在。

小结

栈,堆,静态存储区是程序中三个基本的数据存储区
——栈区主要用于函数调用的使用
——区主要用于内存的动态申请和归还
——静态存储区用于保存全局变量和静态变量

程序的内存分布

程序文件的一般布局

C语言复习第八天_第9张图片

程序和进程

  1. 程序和进程不同
    ——程序是静态的概念,表现形式为一个可执行文件
    ——进程是动态的概念,程序由操作系统加载运行后得到进程
    ——每个程序可以对应多个进程
    ——每个进程只能对应一个程序

例如:浏览器的多开,浏览器为一个程序,当双击之后被操作系统加载,成为了进程。多开对应多个进程.

  • 小问题:包含脚本代码的文本文件是一种类型的可执行程序吗?如果是,对应什么样的进程呢?

C语言复习第八天_第10张图片

程序文件的一般布局

C语言复习第八天_第11张图片

程序的内存布局

  • 各个段的作用
    ——堆栈段在程序运行后才正式存在,是程序运行的基础
    ——.bss段存放的是未初始化的全局变量和静态变量。
    ——.text段存放的程序的可执行代码
    ——.data段保存的是已经初始化的全局变量和静态变量
    ——.rodata段存放程序中的常量值,如字符串常量

程序术语的对应关系

  • 静态存储区通常指的是.bss段和.data段
  • 只读存储区指的是.rodata段
  • 局部变量所占空间为栈上的空间
  • 动态空间堆上的空间
  • 程序可执行代码存放于.text段
    小问题:
    同是全局变量静态变量,为什么初始化和未初始化的保存在不同段中?
    仅是个人推测,效率问题,程序在运行时,需要将已经初始化的变量一个个在内存中赋值,但是如果是未出初始化的变量,即可批量操作去赋值为0,大大提升了效率

小结

  • 程序源码在编译后对应可执行程序中的不同存储区
  • 程序和进程不同,程序是静态概念进程是动态概念
  • 堆栈段是程序运行的基础,只存在于进程空间中
  • 程序可执行代码存放于.text段,是只读的
  • .bss段.data段用于保存全局变量静态变量

你可能感兴趣的:(C语言温故,指针,c语言)