1 内存与CPU关系
程序存储在硬盘中,CPU进行计算时,需要将程序从硬盘加载到内存中,然后CPU从内存读取到数据进行相应的运算,得出结果。
- 寄存器(Register)是CPU内部非常小、非常快速的存储部件。我们经常听说多少位的CPU,指的就是寄存器的的位数。寄存器在程序的执行过程中至关重要,不可或缺,它们可以用来完成数学运算、控制循环次数、控制程序的执行流程、标记CPU运行状态等。
- 缓存,CPU频繁的从内存读取数据效率还是不高,添加缓存可以提高读取效率。在CPU内部设置一个缓存,可以将使用频繁的数据暂时读取到缓存,对于不是很频繁的数据可以直接从内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。关于缓存的命中率又是一门学问,哪些数据保留在缓存,哪些数据不保留,都有复杂的算法。大家在购买CPU时,也会经常关心缓存容量,例如 Intel Core i7 3770K 的三级缓存为 8MB,二级缓存为 256KB,一级缓存为 32KB。容量越大,CPU越强悍。
2 内存管理作用域
- 代码块作用域
- 函数作用域
- 文件作用域
2.1 局部变量
局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是局部变量,特性如下:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句(语句块即{…})中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束
- 如果没有赋初值,内容为随机
2.2 全局变量
- 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明:
如extern int a;声明一个变量,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义。 - 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
2.3 静态变量(static)
2.3.1 静态局部变量
- static局部变量的作用域也是在定义的函数内有效
- static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
- static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
习题:
#include <stdio.h>
void fun1()
{
int i = 0;
i++;
printf("i = %d\n", i);
}
void fun2()
{
static int a;
a++;
printf("a = %d\n", a);
}
int main(void)
{
fun1();
fun1();
fun2();
fun2();
return 0;
}
2.3.2 静态全局变量
- 在函数外定义,作用范围被限制在所定义的文件中
- 不同文件静态全局变量可以重名,但作用域不冲突
- static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次
tips:注意与全局变量的区别,只有生命周期是和全局变量相同,作用域上,命名上都有差别
2.3.3 总结
局部变量和静态局部变量: 共同点:作用域,都是在函数定义内有效 区别:生命周期,静态是程序运行期间一直存在(这点很像全局变量,但是全局变量对所有函数可见,它只在自己函数内部可见),局部变量是函数运行完即消失 初始值上,局部是随机的,静态是0
全局变量和静态全局变量: 共同点:生命周期:程序运行期间都是一直存在的。 区别:作用域:如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。全局变量则是所有文件可以使用,且不能重复名称。
2.4 全局函数和静态函数
在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
对于不同文件中的staitc函数名字可以相同。 测试题: main.c
#include <stdio.h>
extern int va;
extern int getG();
extern int getO();
int main()
{
printf("va=%d\n", va);
printf("getO=%d\n", getO());
printf("getG=%d\n", getG());
printf("%d", va*getO()*getG());
return 0;
}
fun1.c
int va = 7;
int getG()
{
int va = 20;
return va;
}
fun2.c
static int va = 18;
static int getG()
{
return va;
}
int getO()
{
return getG();
}
注意:
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。
2.5 总结
3 内存布局
3.1 内存分区
总览: 详细:
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
-
代码区(text segment): 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 -
未初始化数据区(BSS): 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。 -
初始化数据区(data segment): 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程 -
栈区(stack): 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 -
堆区(heap): 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
举个例子:
#include <stdio.h>
#include <stdlib.h>
int e;
static int f;
int g = 10;
static int h = 10;
int main()
{
int a;
int b = 10;
static int c;
static int d = 10;
char *i = "test";
char *k = NULL;
printf("&a\t %p\t //局部未初始化变量\n", &a);
printf("&b\t %p\t //局部初始化变量\n", &b);
k = (char *)malloc(10);
printf("k\t %p\t //动态分配的内存\n", k);
printf("&e\t %p\t //全局未初始化变量\n", &e);
printf("&c\t %p\t //静态局部未初始化变量\n", &c);
printf("&f\t %p\t //全局静态未初始化变量\n", &f);
printf("&d\t %p\t //静态局部初始化变量\n", &d);
printf("&g\t %p\t //全局初始化变量\n", &g);
printf("&h\t %p\t //全局静态初始化变量\n", &h);
printf("i\t %p\t //只读数据(文字常量区)\n", i);
return 0;
}
3.2 存储类型总结
3.3 内存操作函数
3.3.1 memset()
void *memset(void *s, int c, size_t n);
功能:将s的内存区域的前n个字节以参数c填入
参数:
s:需要操作内存s的首地址
c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
n:指定需要设置的大小
返回值:s的首地址
int main()
{
int a =10;
memset(&a,0,sizeof(a));
char buf[10] = "";
strcpy(buf,"hello");
printf("%s\n",buf);
memset(buf,0,sizeof(buf));
printf("%s\n", buf);
memset(buf,'a',sizeof(buf)-1);
printf("%s\n", buf);
system("pause");
return 0;
}
常见用途,通过memset 0 来清空内存
3.3.2 memcpy()
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
参数:
dest:目的内存首地址
src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
n:需要拷贝的字节数
返回值:dest的首地址
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int b[10] = { 0 };
memcpy(b,a,sizeof(int)*5);
for (int i = 0; i < sizeof(b) / sizeof(b[0]); i++)
{
printf("%d ",b[i]);
}
system("pause");
return 0;
}
注意点:memcpy和strncpy的区别,memcpy遇到\0不会停止,而strncpy遇到\0后续就不会拷贝了
int main()
{
char str1[128] = "";
char str2[128] = "abc\0def\0dadfa";
memcpy(str1,str2,10*sizeof(char));
for (int i = 0; i < 10; i++)
{
printf("%c ",str1[i]);
}
system("pause");
return 0;
}
3.3.3 memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。
3.3.4 memcmp()
memcmp类似于strncmp
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
s1:内存首地址1
s2:内存首地址2
n:需比较的前n个字节
返回值:
相等:=0
大于:>0
小于:<0
int main()
{
char num1[] = { 1,0,3,4,5,6,7 };
char num2[] = { 1,0,3,6,5,6,7 };
char str1[] = "dbakf\0afnafa";
char str2[] = "dbakf\0bfnafa";
printf("%d\n", memcmp(num1,num2,7*sizeof(char)));
printf("%d\n", strncmp(num1, num2, 7 * sizeof(char)));
system("pause");
return 0;
}
总之: 内存操作函数遇到0会\0都不会结束操作,而str字符串处理函数遇到\0都会结束
3.4 堆区内存分配和释放
3.4.1 malloc 向堆区申请空间
#include <stdlib.h>
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
参数:
size:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL
int main()
{
int *p = (int *)malloc(sizeof(int)*10);
memset(p,0,sizeof(int )*10);
*p = 1000;
*(p + 5) = 2000;
for (int i = 0; i < 10; i++)
{
printf("%d ",*(p+i));
}
system("pause");
return 0;
}
3.4.2 free,释放堆内存
#include <stdlib.h>
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
参数:
ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
返回值:无
注意: free只能释放一次上次申请过的空间 free 参数 地址必须是上一次malloc申请过的,不能改变这个地址
int main()
{
char *p =(char *) malloc(1024);
memset(p,0,1024);
strcpy(p,"helloworld");
free(p);
system("pause");
return 0;
}
3.5 内存泄漏与内存污染
内存污染:没有申请内存,却写入数据
4 内存分区代码分析
4.1 返回栈区的地址
栈区变量的地址在函数运行完就销毁了,所以不能返回该地址再使用,会造成内存污染
栈区的局部变量的情况,返回是错误的
#include <stdio.h>
int *fun()
{
int a = 10;
return &a;
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = fun();
*p = 100;
return 0;
}
4.2 返回静态全局区地址
存储在静态全局区的变量是可以的
#include <stdio.h>
int *fun()
{
static int a = 10;
return &a;
}
int main(int argc, char *argv[])
{
int *p = NULL;
p = fun();
*p = 100;
printf("*p = %d\n", *p);
return 0;
}
只有普通局部变量的地址不可以返回,因为普通局部变量的地址在返回之前就已经释放了 静态局部、全局、静态全局,在程序运行期间内存一直存在,所以这些变量的地址是可以返回的
4.3 返回堆区地址
解决办法: 使用strcpy或者memcpy,不改变p指向的地址
4.4 值传递
要点:要使用p指针,就要找到p指针指向的内存块是否合法(如值传递,实际上p还是null),中途p指针指向的内存有没有被偷偷修改(p =“hello”,那么p就指向了常量区,再去释放p就有问题了)
|