1,C进程内存布局
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。俗话所说知己知彼方能百战百胜,因此我们需要研究内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大的方便内核管理不同的进程。例如三个完全不相干的进程p1,p2,p3,他们很显然会占据不同区域的物理内存,但是经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
-
PM:Physical Memory,物理内存。
-
VM:Virtual Memory,虚拟内存
栈内存
-
什么东西存储在栈内存中?
-
环境变量 bin目录里面的文件
-
命令行参数 ./a.exe abc 123
-
局部变量(包含形参)
-
-
栈内存有什么特点?
-
空间有限,尤其在嵌入式环境下,尤其不可以用来存储尺寸太大的变量,在Linux栈内存大小为8M
-
每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量
-
每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
-
栈空间申请的变量随着函数结束,空间自动释放
-
-
注意:
-
栈内存的分配和释放都是由系统规定的,我们无法干预。
-
代码如下:
-
void func(int a, int *p) // 在函数 func 的栈内存中分配
{
double f1, f2; // 在函数 func 的栈内存中分配,局部变量
... // 退出函数 func 时,系统的栈向上缩减,释放内存
}
int a;// 全局变量
int main(void)
{
int m = 100; // 在函数 main 的栈内存中分配
func(m, &m); // 调用func时,系统的栈内存向下增长
}
数据段与代码段
-
数据段细分成如下几个区域:
-
.bss 段:存放未初始化的静态数据,它们将被系统自动初始化为0
-
.data段:存放已初始化的静态数据
-
.rodata段:存放常量数据
-
-
代码段细分成如下几个区域:
-
.text段:存放用户代码
-
.init段:存放系统初始化代码
-
int a; // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中
int main(void)
{
static int c; // 未初始化的静态局部变量,放置在.bss 中
static int d = 200; // 已初始化的静态局部变量,放置在.data 中
// 以上代码中的常量100、200防止在.rodata 中
}
注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预,所以尽量不要使用数据段,除非没办法。
局部变量与栈内存
-
局部变量概念:凡是被一对花括号包含的变量,称为局部变量
-
局部变量特点:
-
某一函数内部的局部变量,存储在该函数特定的栈内存中
-
局部变量只能在该函数内可见,在该函数外部不可见
-
当该函数退出后,局部变量所占用的内存立即被系统回收,因此局部变量也称为临时变量
-
函数的形参虽然不被花括号包含,但依然属于该函数的局部变量
-
-
栈内存特点:
-
每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量
-
每当一个函数有退出时,系统将自动回收其栈内存
-
系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配原则
-
-
示例代码:
int max(int x, int y)// 变量x和y存储在max()函数的栈中
{
int z;//变量z存储在max()函数的栈中
z = x > y ? x : y;
return z; // 函数退出后,栈中的x、y和z被系统回收
}
int main(void)
{
int a = 1;// 变量a存储在main()函数的栈中
int b = 2;// 变量b存储在main()函数的栈中
int m;// 变量m存储在main()函数的栈中,未赋值所以其值为随机值
m = max(a,b);
}
2,函数
const关键字----只读
int a = 100;
const int *p = &a;
//*p = 10;
int const *p1 = &a;
//*p1 = 10;
int b = 200;
int *const q = &a;
//q = &b;
静态数据
int a; // 全局变量,退出整个程序之前不会释放
void f(void)
{
static int b; // 静态局部变量,退出整个程序之前不会释放
printf("%d\n", b);
b++;
}
int main(void)
{
f();
f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
-
为什么需要静态数据
-
全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数之间访问的数据提供操作上的方便。
-
static修饰的全局变量,只能在本文件使用,如果未被static修饰的全局变量,所有的文件都能使用,会出现命名污染。
-
当我们希望一个函数退出后依然能保留局部变量的值,以便于下一次调用时还能用,静态局部变量可以帮助实现这样的功能。
-
-
注意1:
-
若定义时未初始化,则系统会将所有的静态数据自动初始化为0
-
静态数据初始化语句,只会执行一遍。
-
静态数据从程序开始运行时便已存在,直到程序退出时才释放。
-
-
注意2:
-
static修饰局部变量:使之由栈内存临时数据,变为静态数据
-
static修饰全局变量:使之由个文件可见的静态数据,变成为本文件可见的静态数据
-
static修饰的函数:使之由各文件可见的函数,变成为本文件可见的静态函数
-
堆内存(重点,一定要掌握)
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
-
堆内存基本特征:
-
相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
-
相比栈内存,堆内存从下往上增长。
-
堆内存是匿名的,只能由指针来访问。
-
自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
-
相关API:
-
申请堆内存:malloc() / calloc()
-
清零堆内存:bzero()
-
释放堆内存:free()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 申请堆空间,可以存放100个char类型的数据
char *str = malloc(sizeof(char)*100);
// str指向字符串常量,写法异常
//str = "jack";
// 将字符串进行拷贝
memcpy(str,"jack",5);
str[2] = 'g';
printf("%s\n",str);
return 0;
}
注意:
-
malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 戳中memset()来清零。
用malloc开辟二维数组
1. 使用二级指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void func(int **ptr, int row, int col)
{
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
printf("%d\t",ptr[i][j]);
}
printf("\n");
}
}
int main(int argc, char const *argv[])
{
// 数组[]表示数据的个数,总大小是数据个数*类型
int buf[3];// 3*4
// malloc实参是以字节为单位,相当于一维数组
int *ptr = malloc(3*sizeof(int));
for(int i = 0; i < 3; i++)
{
ptr[i] = i;
}
// 将mallc转为二维数组
// 3行
int **ptr1 = (int **)malloc(3*sizeof(int));
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
ptr1[i] = (int *)malloc(4*sizeof(int));
}
}
int count = 0;
int i,j;
for(i = 0; i < 3; i++)
{
for(j = 0; j < 4; j++)
{
ptr1[i][j] = ++count;
}
}
func(ptr1,i,j);
free(ptr1);
ptr1 = NULL;
return 0;
}
calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 申请堆空间
char *p = calloc(5,sizeof(char));
if(p == NULL)
{
perror("calloc failed:");
return -1;
}
// 错误,不能直接赋值,会改变p所指向的地址
//p = "jack";
//printf("%s\n",p);
// 清空堆空间
memset(p,0,5);
strcpy(p,"rose");
printf("%s\n",p);
// 清空堆空间
memset(p,0,5);
memcpy(p, "ken",3);
printf("%s\n",p);
// 释放空间
free(p);
p = NULL;
return 0;
}
realloc()申请的堆内存,在原来内存基础上可进行扩容。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 申请堆空间
char *p = calloc(1,sizeof(char));
if(p == NULL)
{
printf("calloc failed:");
return -1;
}
char buf[] = "afjoiajfajflajlfjalflafjslfjsljfls";
// 扩容,如果不扩容会溢出
char *ptr = realloc(p,100);
strcpy(ptr,buf);
printf("%s\n",ptr);
return 0;
}
释放内存的含义:
-
释放内存意味着将内存的使用权归还给系统。
-
释放内存并不会改变指针的指向。
-
释放内存并不会对内存做任何修改,更不会将内存清零
静态函数
-
背景知识:普通函数都是跨文件可见,即在文件a.c中定义的函数可以在b.c中使用。
-
静态函数:只能在定义的文件内可见的函数,称为静态函数。
-
语法:
-
// 在函数头前面增加关键字static,使之成为静态函数
static void f(void)
{
// 函数体
}
函数指针(非常重要一定要掌握)与指针函数数组
-
概念
-
函数指针变量,就是用来保存函数地址的变量
-
函数名相当于这个函数的首地址
-
int func(int a, int b);
// 函数指针,指向的函数类型为返回值为int 参数为(int,int)的函数
int (*pfunc)(int a, int b);
//demo:
#include <stdio.h>
typedef int int32_t;
// 给函数指针该别名,方便使用,增加指针的易用性
// 此时fptr就相当于void (*fptr)(int *a, int *b)的类型
typedef void (*fptr)(int *a, int *b);// 类似于 int类型 int a = 10
typedef int (*fMaxPtr)(int,int);// 函数指针的参数可以只写变量类型
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int max(int a, int b)
{
return a > b ? a : b;
}
int main(int argc, char const *argv[])
{
int a = 10 , b = 20;
// 定义函数指针指向swap
// 注意pfunc指针的类型与所指向的函数类型一致
// 将函数名去掉,剩下的部分为函数的类型
void (*pfunc)(int *a, int *b) = swap;
printf("%p\n",swap);
printf("%p\n",pfunc);
// 2. 将函数指针改别名
fptr p = swap;
printf("%p\n",p);
// 通过函数指针执行swap函数
//p(&a,&b);
pfunc(&a,&b);
printf("%d,%d\n",a,b);
// 定义函数指针指向max函数,实现比较最大值
fMaxPtr fmax = max;
printf("max = %d\n",fmax(19,29));
return 0;
}
函数指针数组
函数指针数组,数组存放的是指向返回值为 int 参数为int int类型的函数地址
int (*pfbuf[]) (int,int);
//dmeo:
#include <stdio.h>
#include <string.h>
// 设计函数指针数组类型,增加易用性
typedef int (*fpbuf[2]) (int,int);
int Max(int a, int b)
{
return a > b ? a : b;
}
int Min(int a, int b)
{
return a < b ? a : b;
}
int main(int argc, char const *argv[])
{
// 定义函数指针数组
int (*pbuf[2]) (int,int) = {Max,Min};
// 输出最小值
printf("min = %d\n",pbuf[1](10,20));
fpbuf fbuf = {Max,Min};
// 输出最小值
printf("Max = %d\n",fbuf[0](10,20));
return 0;
}
回调函数(钩子函数)
-
概念:函数实现方不调用该函数,而由函数接口提供方间接调用的函数,称为回调函数。
-
示例:系统中的信号处理,是一个典型的利用回调函数的情形。
要点:
-
示例中函数 sighandler 是回调函数。
-
signal() 将函数回调函数传递给内核,使得内核可以在恰当的时机回调 sighandler。
-
应用开发者和内核开发者只要约定好回调函数的接口,即可各自开发,进度互不影响
内联函数 inline
-
内联的特性 :以空间换时间
-
当编译器发现某段代码有inline关键字的时候就会将这段代码插入到当前的位置,加快运行效率,但是也会消耗一定的运行空间
-
什么时候用inline
-
函数需要频繁被调用,代码最好不要超过5行
-
-
inline注意事项
-
内联函数在头文件实现,其它函数不要在头文件实现
-
函数声明和函数实现都需要添加关键字inline,如果函数声明没有添加extern 和 inline 关键字,会报错
-
// 创建add.h头文件
#ifndef _ADD_H
#define _ADD_H
#include <stdio.h>
// 内联函数声明
extern inline int add(int a, int b);
#endif
// 创建main.c
#include "add.h"
inline int add(int a, int b)
{
return a+b;
}
int main(int argc, char const *argv[])
{
printf("%d\n",add(10,20));
return 0;
}
递归函数(自己调用自己)
-
递归概念:如果一个函数内部,包含了对自身的调用,则该函数称为递归函数。
自己调用自己,注意需要有结束条件,否则会出现内存溢出(段错误)
什么时候用到递归 : 需要满足有规律递减条件,递减到某个程度是可以退出的
func()
{
func()
}
#include <stdio.h>
void func(int n)
{
// 递归退出条件
if(n == 6)
return;
func(++n);
printf("%d\n",--n);
}
int main(int argc, char const *argv[])
{
int n = 1;
func(n);
return 0;
}
要点:
-
只有能被表达为递归的问题,才能用递归函数解决
-
递归函数必须有一个可直接退出的条件,否则会无限递归,直到内存溢出
-
递归函数包含两个过程,一个逐渐递进的过程,和一个逐渐回归的过程。
函数strlen
char *s = "www.csdn.com.cn";
printf("csdn的长度是:%d\n", strlen(s));
函数strcat与strncat
注意:
-
这两个函数的功能,都是将 src 中的字符串,复制拼接到 dest 的末尾。
-
strcat() 没有边界控制,因此可能会由于 src 的过长而导致内存溢出。
-
strncat() 有边界控制,最多复制 n+1 个字符(其中最后一个是 ‘\0’ )到 dest 的末尾。
char s1[10] = "abc";
strcat(s1, "xyz");
printf("%s\n", s1); // 输出 "abcxyz"
char s2[10] = "abc";
strcat(s3, "123456789"); // 此处操作内存溢出,可能会发生内存崩溃
char s[10] = "abc";
strncat(s, "123456789", sizeof(s)-strlen(s)-1);
printf("%s\n", s); // 输出 "abc123456",两个字符串被拼接到了一起,且不会溢出
注意:strncat()是具备边界检查的安全版本,推荐使用。
函数strcpy与strncpy
注意:
-
这两个函数的功能,都是将 src 中的字符串,复制到 dest 中。
-
strcpy() 没有边界控制,因此可能会由于 src 的过长而导致内存溢出。
-
strncpy() 有边界控制,最多复制 n+1 个字符(其中最后一个是 ‘\0’ )到 dest 中。
char s1[5] = "abc";
strcpy(s1, "xyz);
printf("%s\n", s1); // 输出 "xyz",原有的"abc"被覆盖
char s2[5] = "abc";
strcpy(s2, "123456789"); // 此处操作内存溢出,可能会发生内存崩溃
char s[5] = "abc";
strncpy(s, "123456789", sizeof(s)-1);
printf("%s\n", s); // 输出 "1234",有边界保护,不会溢出
注意:strncpy()是具备边界检查的安全版本,推荐使用。