C语言复习第八天
函数与指针分析
函数类型
- C语言中的函数有自己特定的函数类型
- 函数的类型由返回值,参数类型和参数个数共同决定
int add(int i,int j)的类型为 int(int,int)
- C语言中通过typedef为函数类型重命名
typedef type name(parameter list)
例如:
typedef int f(int ,int);
typedef void p(int);
函数指针
- 函数指针用于指向一个函数
- 函数名是执行函数体的入口地址
- 可以通过函数类型定义函数指针:
FuncType* pointor;
- 也可以直接定义:
type(*pointor)(parameter list)
——pointor 为函数指针名
——type 为所指的函数的返回类型
——parameter list 为所指函数的参数类型列表
tips:
面试小问题:
如何使用C语言直接跳转到某个固定的地址开始执行?
通过函数指针,因为一个函数名,即为函数的入口的地址,函数指针指向的其实就是一个地址。
示例:
#include <stdio.h>
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;
}
回调函数
- 回调函数是利用函数指针实现的一种调用机制
- 回调机制原理:
——调用者不知道具体事件发生时需要调用的具体函数
——被调函数不知道何时被调用,只知道需要完成的任务
——当具体事件发生时,调用者通过函数指针调用具体函数 - 回调机制中的调用者和被调函数互不依赖。
示例:
#include <stdio.h>
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语言中的函数都有特定的类型
- 可以使用函数类型定义函数指针
- 函数指针是实现回调机制的关键技术
- 通过函数指针可以在C程序中实现固定地址跳转
指针阅读技巧
右左法则
- 从最里层的圆括号中未定义的标识符看起
- 首先往右看,再往左看
- 遇到圆括号或方括号时可以确定部分类型,并调转方向
- 重复2,3步骤,直到阅读结束。
示例:
#include <stdio.h>
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指针等价
小结
- 右左法则总结于编译器对指针变量的解析过程
- 指针阅读练习的意义在于理解指针的组合定义
- 可通过typedef简化复杂指针的定义
动态内存分配
动态内存分配的意义
- C语言中的一切操作都是基于内存的
- 变量和数组都是内存的别名
——内存分配由编译器在编译期间决定
——定义数组的时候必须指定数组的长度
——数组长度是在编译期就必须确定的
需求:
程序运行的过程中,可能需要使用一些额外的内存空间
malloc和free
- malloc 和 free用于执行动态内存分配和释放
- malloc所分配的是一块连续的内存
- malloc以字节为单位,并且不带任何的类型信息
- free用于将动态内存归还系统
void* malloc(size_t size);
void free(void* pointer);
tips
- malloc和free是库函数,而不是系统调用
- malloc分配的内存可能会比请求的多
- 不能依赖于不同平台下的malloc行为
- 当请求的动态内存无法满足时 ,malloc返回NULL
- 当free的参数为NULL时,函数直接返回
内存泄漏检测模块
//mleak.h
#ifndef _MLEAK_H_
#define _MLEAK_H_
#include <malloc.h>
#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 <stdio.h>
#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
- malloc 的同胞兄弟
void *calloc(size_t num,size_t size);
void *relloc(void * pointer,size_t new_size);
- calloc的参数代表所返回内存的类型信息
——calloc会将返回的内存初始化为0 - relaloc用于修改一个原先已经分配的内存块大小
——在使用realloc之后应该使用其返回值
——当pointer的第一个参数为NULL,等价于malloc
示例:
#include <stdio.h>
#include <malloc.h>
#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语言的强大功能
- 程序能够在需要的时候有机会使用更多的内存
- malloc单纯的从系统中申请固定字节大小的内存
- calloc能以类型大小为单位申请内存并初始化为0
- realloc用于重置内存大小
程序中的三国天下
程序中的栈
- 栈是现代计算机程序里最为重要的概念之一
- 栈在程序中用于维护函数调用上下文
- 函数中的参数和局部变量存储在栈上
- 栈保存了一个函数调用所需要的维护信息
栈的参数如下:
函数调用的栈变化一:
变化二
变化三:
函数调用栈上的数据
- 函数调用时,对应栈空间在函数返回前是专用的
- 函数调用结束后,栈空间将被释放,数据不再有效
示例:
#include <stdio.h>
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;
}
内存访问出错,因为内存已经被释放掉,无法进行访问了。
程序中的堆
- 堆是程序中一块预留的内存空间,可由程序自由使用
- 堆中被程序申请使用的内存在被主动释放前将一直有效
问题: 为什么有了栈还需要堆呢?
栈上的数据在函数返回后就会被释放掉,无法传递到函数外部,如:局部数组
- C语言程序中通过库函数的调用获得堆空间
——头文件: malloc.h
——malloc --以字节的方式动态申请堆空间
——free --将堆空间归还给系统
程序中静态存储区
- 静态存储区随着程序的运行而分配空间
- 静态存储区的生命周期直到程序运行结束
- 在程序的编译期静态存储区的大小就已经确定
- 静态存储区主要用于保存全局变量和静态局部变量
- 静态存储区的信息最终会保存到可执行程序中。
示例:验证静态存储区的存在
#include <stdio.h>
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;
}
最后可以发现:它们地址是连在一起,所以可以判断静态存储区的存在。
小结
栈,堆,静态存储区是程序中三个基本的数据存储区
——栈区主要用于函数调用的使用
——堆区主要用于内存的动态申请和归还
——静态存储区用于保存全局变量和静态变量
程序的内存分布
程序文件的一般布局
程序和进程
- 程序和进程不同
——程序是静态的概念,表现形式为一个可执行文件
——进程是动态的概念,程序由操作系统加载运行后得到进程
——每个程序可以对应多个进程
——每个进程只能对应一个程序
例如:浏览器的多开,浏览器为一个程序,当双击之后被操作系统加载,成为了进程。多开对应多个进程.
- 小问题:包含脚本代码的文本文件是一种类型的可执行程序吗?如果是,对应什么样的进程呢?
程序文件的一般布局
程序的内存布局
- 各个段的作用
——堆栈段在程序运行后才正式存在,是程序运行的基础
——.bss段存放的是未初始化的全局变量和静态变量。
——.text段存放的程序的可执行代码
——.data段保存的是已经初始化的全局变量和静态变量
——.rodata段存放程序中的常量值,如字符串常量
程序术语的对应关系
- 静态存储区通常指的是.bss段和.data段
- 只读存储区指的是.rodata段
- 局部变量所占空间为栈上的空间
- 动态空间为堆上的空间
- 程序可执行代码存放于.text段
小问题:
同是全局变量和静态变量,为什么初始化和未初始化的保存在不同段中?
仅是个人推测,效率问题,程序在运行时,需要将已经初始化的变量一个个在内存中赋值,但是如果是未出初始化的变量,即可批量操作去赋值为0,大大提升了效率
小结
- 程序源码在编译后对应可执行程序中的不同存储区
- 程序和进程不同,程序是静态概念,进程是动态概念
- 堆栈段是程序运行的基础,只存在于进程空间中
- 程序可执行代码存放于.text段,是只读的
- .bss段和.data段用于保存全局变量和静态变量