从0开始音视频开发:C数据类型和内存模型
有过语言基础的同学都知道C语言是面向过程的,C++是C的超集,有过C、Java或Android基础的同学可以直接跟随本专题学习,如果基础语言不好C语言入门教程可以看链接的教程对C语言学习精通.
基本类型
每种语言都有基本的数据类型,表达方式都是一样的,需要注意在C语言中存在有符号和无符号的区别. 需要注意的是Java 中的long类型在C语言中是long long
是8个字节
需要注意在C99标准以前,C语言里面没有bool,C++里面才有,在C99标准里面定义了bool类型需要引入头文件#include ,在C/C++中if遵循一个规则,非0为true,非null为true.
bool sb = true;
signed int i = 10;//有符号 char int
unsigned int s = 10;//无符号
uint32_t ui1 = -1;
printf("ui1:%u",ui1);//0 - 4294967295 变成最大值
long ll = 10;
long long sss = 11;//Java中的long
long int ss = 11;//=long java中的long = long long,在规定中int 至少要和short一样长,long至少和int一样长
//C bool 非0即true,非null为true
printf("%lu",sizeof(ll));// long类型 8个字节 sizeof获取字节数
printf("Hello, World!n");
| 整型 | 字节 | 取值范围 | 占位 | | :------------- | ---- | ------------------------------- | ---- | | int | 4 | -2,147,483,648 到 2,147,483,647 | %d | | unsigned int | 4 | 0 到 4,294,967,295 | %u | | short | 2 | -32,768 到 32,767 | %hd | | unsigned short | 2 | 0 到 65,535 | %hu | | long | 4 | -2,147,483,648 到 2,147,483,647 | %ld | | unsigned long | 4 | 0 到 4,294,967,295 | %lu | | char | 1 | -128 到 127 | %c | | unsigned char | 1 | 0 到 255 | %c |
在上表中,在某些平台的字节大小并不一样,可以使用sizeof(type)得到准确的类型的存储字节的大小
注意:long int 类型其实就是长整型 = long 可以省去int,在标准中,规定int至少和short一样长,long至少和int一样长.
数组类型
在C/C++ 语言中,定义数组必须给定数组长度,变量的声明是存储在栈中,在Window或Linux系统中栈的大小都是有限制的,在Linux中栈的大小是8192kb,如果超过这个大小就会栈异常 stack overflow
int a[10];//c中的数组必须给定长度
int b[] = {1,2,3,4};
char a[8192*1024];//linux 栈大小是8192kb stack overflow 栈中的大小存在限制 如果超出大小会stack overflow
查询Linux系统栈的大小: 命令 ulimit -a,找到stack size 就是栈的大小
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
file size (blocks, -f) unlimited
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n) 256
pipe size (512 bytes, -p) 1
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 2784
virtual memory (kbytes, -v) unlimited
include
在C/C++语言中的include和java中的import相似,但是又存在着一些区别.
如:声明Header2.h头文件
#ifndef Header2_h
#define Header2_h
#include <stdio.h>
void test3();
#endif /* Header2_h */
新建一个.cpp文件Header2.cpp
#include "Header2.h"
void test3(){
printf("nTest3");
}
在新建一个Header1.h头文件,include Header2.h
#include "Header2.h"
#ifndef Header1_h
#define Header1_h
void test();
void test1(){
printf("ntest1");
}
#endif /* Header1_h */
我们在main.cpp中include Header1.h,就可以调用Header2.h的方法实现
```C
include
include "Header1.h"//Header1.h文件includeHeader2.h,可以直接调用Header2.h的方法
using namespace std;
void test(){ printf("test"); }
int main(int argc, const char * argv[]) { test(); test1(); test3(); }
### 动态内存申请
在上述中,数组类型中 栈的大小是有限制的,我们可以去堆中去申请内存来使用.
#### malloc
malloc 没有初始化内存的内容,一般需要调用memset来初始化这部分内存空间
```C
size_t size = 8192*1024;
//常用的malloc申请内存
void *jj= malloc(size);//申请大小8192*1024 大小的堆内存空间0
memset(jj, 0, size);//将jj指向的内存初始化为0 长度为size
calloc
申请内存并将初始化内存数据为null,calloc 和malloc 逻辑是相同的.
//第一个参数大小:10 * 4 = 40个字节的长度
int *cal = (int*)calloc(10, sizeof(int));
//相当于 void *jj= malloc(sizeof(int)*10); memset(jj, 0, sizeof(int)*10);
realloc
对malloc申请的内存进行大小调整
char *a = (char*)malloc(10);
realloc(a, 20);
free
free 释放malloc和calloc申请的内存空间,并且需要将指针置为0否则可能会悬空指针
free(cal);
cal = 0;
alloca 在栈中申请内存,很少会用到
int *p_alloca= (int*)alloca(sizeof(int)*10);
alloca无需释放内存空间,因为栈中的变量,在不会使用该变量后,会自动释放内存空间.
上述讲解了常见的动态内存申请,非常简单的API调用,但是我们需要更加深入的去了解内存模型,内存中是如何运行的? 作为C/C++的开发者,必须要掌握内存,因为在非常多的时候都需要去申请内存和释放内存,了解内存模型跟有助于对内存的使用.
Linux下的内存模型
Linux下的内存模型如下,主要有内核空间、栈、动态链接库、堆、全局数据区、常量区、代码区、保留区域.
各个内存区域说明
| 内存区域 | 说明 | | ---------- | ------------------------------------------------------------ | | 程序代码区 | 存放函数体的二进制代码,一个C语言程序由多个函数构成,程序的执行就是函数之间的调用. | | 常量区 | 存放一般常量、字符串常量.这块内存区域,只有读取权限没有写入权限,它们的值在程序运行期间不能改变. | | 全局数据区 | 全局变量、静态变量.这块内存有读写权限,它们的值可以在程序运行任意之间可以改变. | | 堆区 | 堆区一般由程序员进行分配和释放,如果程序员不释放,在程序运行结束时,会由操作系统回收.malloc calloc free,操作的就是这块区域内存,也是最重要的部分. | | 动态链接库 | 用于在程序运行期间加载和卸载动态链接库. | | 栈区 | 存放函数的参数值和局部变量的值等. |
程序代码区、常量区、全局数据区,在程序加载到内存后就分配好了,并在程序运行期间一直存在,不能对其销毁和增加(大小固定),只能等到程序运行结束后由操作系统回收,所以他们在任何地方都能够访问,因为它们在内存中一直存在.
在函数被调用时,会将参数、局部变量、返回地址等与函数信息压入栈中,函数执行结束后,这些信息都将被销毁.局部变量和参数只在当前函数中有效,不能传递到函数外部,因为在函数执行结束后,它们已经在内存中不在了.
常量区、全局数据区、栈上的内存都是由系统自动分配和释放的,不能由程序员控制.程序员唯一能控制的区域就是: 堆区.堆区是巨大的一块内存空间,常常占据整个虚拟空间的绝大部分,在这一片的空间内,可以由程序员申请一块内存,自由的存入数据和使用.堆内存在程序主动结束前会一直存在,并不随函数的结束而释放.在函数内部产生的数据只要放在堆中,就可以在函数外部调用.
char *str1 = "jakeprim.cn";//字符串在常量区,str1在全局数据区
int n;//全局数据区
char *func(){
char *str = "func";//字符串在常量区,str在栈区
return str;
}
/**
*加深对内存理解的实例
*/
void eg_ram(){
int a;//栈区
char *str2 = "str2";//字符串在常量区 str2在栈区
char arr[20] = "arr[]";//字符串和arr都在栈区
char *pstr = func();//栈区
int b;//栈区
printf("str1:%#Xnpstr:%#Xnstr2:%#Xn",str1,pstr,str2);
puts("------------------------");
printf("&str1: %#Xn &n: %#Xn", &str1, &n);
puts("--------------");
printf(" &a: %#Xn arr: %#Xn &b: %#Xn", &a, arr, &b);
puts("--------------");
printf("n: %dna :%dnb: %dn", n, a, b);
puts("--------------");
printf("%sn", pstr);
}
输出:
str1:0XEDE
pstr:0XEEA
str2:0XEEF
------------------------
&str1: 0X2020
&n: 0X2028
--------------
&a: 0XEFBFF55C
arr: 0XEFBFF560
&b: 0XEFBFF544
--------------
n: 0
a :32766
b: 0
d: 0
--------------
func
函数 func() 中的局部字符串常量"func"
也被存储到常量区,不会随着 func() 的运行结束而销毁,所以最后依然能够输出。
字符数组 arr[20] 在栈区分配内存,字符串"arr[]"
就保存在这块内存中,而不是在常量区,大家要注意区分。
全局变量的内存在编译时就已经分配好了,它的默认初始值是 0(它所占用的每一个字节都是0值),局部变量的内存在函数调用时分配,它默认初始值是不确定的,由编译器决定,一般是垃圾值.如:n,a,b的值.
内存分配原理
进程分配内存主要由两个系统调用完成:brk和mmap
- brk是将_edata(指带堆位置的指针)往高地址推;
- mmap是找一块空闲的虚拟内存分配;
brk和mmap的详细讲解
malloc小于128k的内存,使用brk分配内存,将_edata往高地址推,大于128k使用m map.
情况1 malloc分配小于128k的内存
为了方便大家理解,我画了如下图的形式.进程调用A = malloc(20K),malloc函数会调用brk系统调用,将_edata指针往高地址推20K,完成虚拟内存分配.调用B = malloc(30K) 又会将_edata指针往高地址推30K.
情况2,malloc分配大于128K的内存
如下图进程调用D = malloc(200K),这时申请的内存大于128K,malloc函数不会在调用brk去推_edata指针了,而是调用mmap系统调用,在堆和栈之间找一块空闲的虚拟内存进行分配.
那么问题来了,既然brk推指针很方便为什么又要引入mmap呢? 其实主要是因为:brk分配的内存需要等到高地址内存释放后才能释放,例如在B释放之前A是不可能释放的,这就是产生内存碎片的原因,而mmap是单独分配一块内存可以单独释放.当然mmap在性能上要比brk差,因为mmap需要查找空闲的虚拟内存.在具体的可以去glibc查看一下malloc源码
情况3.free释放内存
调用free(D),由于D是单独的一块内存可以直接释放掉虚拟内存和物理内存.
调用free(B),B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,不可能往回推,如果往回推那么C该怎么办呢?当然B这块内存是可以重用的,如果再来一个请求30K的内存空间,malloc很可能把B这块内存直接返回.
当调用free(C),B和C链接起来130K的内存空闲,当最高地址的内存空闲超过128K,会执行内存收紧操作,于是变成了第二个图.
mmap是需要查找一块空闲的虚拟内存进行分配,而brk是在当前的内存位置往上推所需要的内存空间大小,不需要进行查找操作,brk在性能上要比mmap的性能高.
malloc申请内存后,为什么要使用memset?
当有内存释放会存在内存碎片,brk 有可能是之前释放的内存,如上图的B,存在脏数据,memset 会重置内存空间的数据, 一切从0开始.
如果i1 i2 申请40k的字节,值都是100,这时候释放i2,申请i3,同样40k的字节,这时会复用i2的内存碎片,但是如果不使用memset 复用i2的内存碎片di3的值那么都是100.