C语言内存管理
内存分配的类型
在C/C++中内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。
静态内存分配:编译时分配,包括:全局、静态全局、静态局部三种变量;
动态内存分配:运行时分配,包括:栈(stack):局部变量;堆(heap):C语言中用到的变量被动态的分配在内存中(malloc或calloc、realloc、free函数)。
变量的内存分配
栈区(stack):指那些由编译器在需要的时候分配,不需要时自动清除的变量所在的存储区,如函数执行时,函数的形参以及函数内的局部变量分配在栈区,函数运行结束后,形参和局部变量去栈(自动释放)。栈内存分配运算内置于处理器的指令集中,效率高但是分配的内存空间有限。
堆区(heap):指那些由程序员手动分配释放的存储区,如果程序员不释放这块内存、内存将一直被占用,知道程序运行结束由系统自动回收,C语言中使用malloc和free申请和释放空间。
静态存储区(static):全局变量和静态变量的存储是放在一块的,其中初始化的全局变量和静态变量在一个区域,这块空间当程序运行结束后由系统释放。
常量存储区(const):常量字符串存储区域,如“ABC”字符串等,存储在常量区的只读不可写。const修饰的全局变量存储在常量区,const修饰的局部变量依然存储在栈上。
程序代码区:存放程序的二进制代码。
指针
CPU访问内存空间的方式,指针即内存空间的地址、门牌号的代名词
指针变量:在内存中开辟一个空间,存放指针
C语言编译器对于指针这个特殊概念,有2个疑问:
1.分配一个盒子 ,盒子要多大?
在32bit系统中,指针就4个字节
2.盒子里存放的地址 所指向 内存的读取方法是什么?
例如:char *p; //p指针一次读取1个字节
int *p; //p指针一次读取4个字节
例如:
int main()
{
int *p1;
char *p2;
printf("the p1 is %u,the p2 is %u\n,"sizeof(p1),sizeof(p2))
}//输出结果the p1 is 4,the p2 is 4
指针指向内存空间,一定要保证合法性
int main()
{
int a = 0x12345678;
char *p1;
p1 = &a;
printf("the p1 is %x\n", *p1);//输出 the p1 is 78,从低地址开始读1个字节,8位
}
指针+修饰符:const、volatile、typdef
const:常量、只读
常量指针是指针指向的内容是常量,可以有一下两种定义方式
指针p可以指向任意内存空间,但p指向的内存空间里的内容是只读的
const int *p;
int const *p;
int main()
{
char *p = "hello world!\n"; //不可改变,,相当于const char *p
char buf[] = {"hello world!\n"};
char *p2 = buf;
printf("the one is %x\n",*p);
*p2 = 'a';
printf("the %s\n",p2);//输出the aello world!
}
指针常量是指指针本身是个常量,不能在指向其他的地址
指针指向的内存空间里的内容是可以改变的
int *const p;
volatile:防止优化指向内存地址
volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如 果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
volatile char *p1; //仍会从指向的地址中读取数据,不会进行优化
*p1 = 0x10;
while(*p1 == 0x10)
{
xxx ;
}
typedef
char *name_t;//name_t是一个指针,指向了要给char类型的内存
typedef char *name_t; //name_t是一个指针类型的名称,指向了一个char类型的内存
name_t abc;// 相当于char *abc
指针+运算符:++、–、+、-
int *p = 0x12;
p+1; // [0x12+1*(sizeof()*p)],以p为基地址
//指针的加法减法运算实际上加的是一个单位,单位的大小可以使用sizeof(p[0])
p++;//更新地址,相当于p = p+1;
p--;
p[n]; //以p为基地址,标签访问,非线性访问
变量分配从内存地址的高到低分配
int main()
{
int a = 0x12345678;
int b = 0x99991199;
int *p1 = &b;
char *p2 = (char *)&b; //强制类型转换
printf("the p1+1 is %x,%x,%x\n",*(p1+1),p1[1],*p1+1);//输出结果为the p1+1 is 12345678,12345678,9999119a
printf("the p2+1 is %x\n",*(p2+1));//读取到从低到高(右到左)的第2个字节,输出结果为the p2+1 is 11
}
多级指针
存放地址的地址空间;
int **p;
p[m] == NULL; //二维指针结束标志
#include<stdio.h>
int main(int argc,char **argv)//int argc指传递参数的个数,char **argv是二维指针
//等同于 int main(int argc,char *argv[])
//argv内存空间中每个空间内都存放一个指针,每个指针都指向一段内存,以char类型读取
{
int i;
}
int main(int argc, char **argv)
{
int i = 0;
while(argv[i] != NULL) //二维指针常用结构判断
{
printf("the argv[%d] is %s\n", i, argv[i]);
i++;
}
return 0;
}
数组
内存分配的一种形式
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的。
数组的存储空间,不是在静态区就是在栈上。
定义一个空间:1.大小 2.读取方式
数据类型 数组名[m] //通过方括号的形式将数组名升级为一个连续空间的名称,m的作用域是在申请的时候,数组名是一个常量符号,一定不要放到 = 的左边
例如:buf = “hello world”; 是一种经典错误,标签不能改变
例如:int a[100];
数组的使用方法与指针相似,例如:a[n]
数组空间的初始化
空间的赋值:按照标签逐一处理
int a[10];
a[0] = xx;
a[1] = yy;
空间定义时就告知编译器的初始情况,空间的第一次赋值,初始化操作
int a[10] = {10,20,30};
数组空间的初始化和变量的初始化本质不同,尤其在嵌入式裸机开发中,空间的初始化往往需要库函数的辅助
char buf[10] = {'a','b','c'};//buf当成普通内存看,没有问题,当成字符串来看,最后需要加上一个'\0'
char buf[10] = {"abc"}; //把buf当成字符串,即内存中有4个字节
char buf[10] = "abc"; //把abc逐个拷进buf内存空间中,不能对abc进行改变
// 区别于char *p = "abc";//指的是abc\0的地址
数组进行第二次内存初始化赋值,需要逐一处理
例如:buf[0] = ‘h’ buf[1] = ‘e’ … buf[n] = ‘d’ buf[n+1] = 0
strcpy
strcpy()函数是C语言中的一个复制字符串的库函数
C 库函数 char *strcpy(char *dest, const char *src)
把 src 所指向的字符串复制到 dest。
需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况
dest – 指向用于存储复制内容的目标数组。
src – 要复制的字符串。
#include<string.h>
#include <stdio.h>
void main()
{
char a[20], c[] = "I am a teacher!";
strcpy(a, c);
printf(" c=%s\n", c);
printf(" a=%s\n", a);
char buf[] = "abc";
strcpy(buf,"hllo world");
}
字符空间:ASCII码编码来解码的空间,给人看的 //字符类
非字符空间:数据采集 0x00 - 0xff 8bit
开辟一个存储这些数据的盒子:unsigned char buf[10]; //data
、char buf[10]; //string
非字符空间只管逐一拷贝,用定义个数的方法结束
拷贝三要素:1、src 需要拷的内容 2、dest 需要拷贝到的地址 3、数据个数
memcpy函数是一个用于内存复制的函数,声明在 string.h 其原型是:
void *memcpy(void *destin, void *source, unsigned n);
作用是:以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。函数有三个参数,第一个是目标地址,第二个是源地址,第三个是数据长度。数据长度(第三个参数)的单位是字节(1byte = 8bit)。
int buf[10];
int sensor_buf[100];
memcpy(buf,sensor_buf,10*sizeod(int));//需要注意数据的单位
unsigned char buf[10];
unsigned char sensor_buf[100];
strncpy(buf,sensor_buf,10);
memcpy(buf,sensor_buf,10*sizeof(unsigned char));
指针与数组
指针数组:
它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素
int *arr[8];
sizeof(arr) = 8*4;// 一个指针大小是4个字节
//优先级问题:[]的优先级比*高
//说明arr是一个数组,而int*是数组里面的内容
//这句话的意思就是:arr是一个含有8和int*的数组
int a[10];
int b[5] [6];//二维数组,一行有六个
int *p1 = a;
int **p2 = b;//此行会出错,因为二维指针与二维数组读取内存方式不同,二维数组按照一块一块读,按行加累加;二维指针是存储地址的地址存储区域
int (*p2)[6] = b; //正确表示方法,通过数组指针读取内存
int c[2][3][4];
int (*p)[3][4] = c;
**数组指针:**它实际上是一个指针,该指针指向一个数组。
int (*arr)[8];
//由于[]的优先级比*高,因此在写数组指针的时候必须将*arr用括号括起来
//arr先和*结合,说明p是一个指针变量
//这句话的意思就是:指针arr指向一个大小为8个整型的数组
结构体
有时候我们需要其中的几种一起来修饰某个变量,例如一个学生的信息就需要学号(字符串),姓名(字符串),年龄(整形)等等,这些数据类型都不同但是他们又是表示一个整体,要存在联系,那么我们就需要一个新的数据类型——结构体,他就将不同类型的数据存放在一起,作为一个整体进行处理。
结构体声明:
1、先定义结构体类型再定义结构体变量
如先定义结构体类型:
struct student
{undefined
int StuNo;//学号
char StuName[20]; //学生姓名
char Sex; //学生性别
int Agel //年龄
}
struct student stu1,stu2;//再定义结构体变量
2、定义结构体类型的同时定义结构体变量
struct student
{undefined
int StuNo;//学号
char StuName[20]; //学生姓名
char Sex; //学生性别
int Agel //年龄
} stu1,stu2;
typedef可以声明新的类型名来代替已有的类型名,但却不能增加新的类型。
typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
在编程中使用typedef目的一般有两个,一个是给变量提供一个易记且意义明确的新名字(类型有新别名,方便变量的定义),另一个是简化一些比较复杂的类型声明。
typedef struct
{
unsigned char *StuNo;//学号
char *StuName; //学生姓名
char *Sex; //学生性别
int Agel; //年龄
} stu;
int main()
{
stu stu1;
stu1.StuNo = "201811250101";
stu1.StuName = "Angle";
stu1.Sex = "Man";
stu1.Agel = 23;
printf("%s\n%s\n%s\n%d\n", stu1.StuNo,stu1.StuName,stu1.Sex,stu1.Agel);
}