day08 变量作用域生命周期与内存

一、变量的作用域

  C语言变量的作用域分为:

  (1)代码块作用域(代码块是{}之间的一段代码)

  (2)函数作用域

  (3)文件作用域

二、局部变量:

  概念:局部变量也叫auto自动变量(auto可写可不写)。定义在函数内部的变量。 或者说定义在代码块内,代码块结束了作用域也就结束了。

  作用域:从定义位置开始,到包裹该变量的第一个右大括号结束(到代码块结束)。
在这里插入图片描述

三、全局变量:

  概念:定义在函数外部的变量。

  作用域:从定义位置开始,默认到本文件内部。 其他文件如果想使用,其他的文件可以通过声明方式(加上extern)将作用域导出。

  (1)这个项目中有那么多的文件。
在这里插入图片描述
  (2)在第 8 个文件中定义了 a 变量,如下图
在这里插入图片描述
  (3)如果我想要在 第9个文件中使用 第8个文件中的 a 变量。就需要在第9个文件中用 extern 来声明a 变量是外来的变量。如下图:
在这里插入图片描述

四、static全局变量:

1.定义语法:

   在全局变量定义之前添加 static 关键字。 static int a = 10;

2.作用域:

  被限制在本文件内部,不允许通过声明(extern)导出到其他文件。就像是被关在了房间里面,不能跑出去。

3.测试:

  (1)在第8个文件中加上static关键字:
在这里插入图片描述
  (2)在第9个文件中依旧用 extern 声明变量 a。
在这里插入图片描述
  (3)执行结果错误。
在这里插入图片描述

五、static局部变量:

1.定义语法:

  在局部变量定义之前添加 static 关键字。

2.特性:

  静态局部变量只定义一次。在全局位置, 通常用来做计数器。

3.作用域:

  从定义位置开始,到包裹该变量的第一个右大括号结束。

4.测试

4.1 测试局部变量

(1)测试代码:

#include <stdio.h>

void test()
{
        int b = 10;
        printf("b=%d\n",b++);
}
int main()
{
        int i = 5;
        while( i )
        { 
          test();
          i--;
        }
        return 0;
}

(2)测试结果
在这里插入图片描述
(3)分析结果:
  每次调用 test() 函数,就会重新定义b变量的值为10。或者说每次调用完了 test() 这个函数,那么b 这个变量就会被释放了,如果再次调用,就再次定义 b 变量的值为10。

4.2 测试静态局部变量(加上static)

(1)测试代码:

#include <stdio.h>

void test()
{
        static int b = 10;
        printf("b=%d\n",b++);
}

(2)测试结果
在这里插入图片描述
(3)分析结果:
  一共调用了5次调用 test() 函数,每次b变量的值都不同。这是因为每次调用完了 test() 这个函数,b 这个变量没有被释放,b 一直保留这上一次调用后的值,在上一次调用后的值基础上进行运算。

  也可以这样说:调用了5次 test() 函数,本应该是调用一次函数就定义1次 b 这个变量的,一共定义了5次。但是加上了 static 关键字就执行了一次 b 变量的定义,后面几次调用就不执行定义b变量这句代码了 。相当于只是在 test() 函数外定义了一次。

int b = 10;
void test()
{
        printf("b=%d\n",b++);
}

  这个就像是本来干完活了就可以走人了,但是干完了活还不能走人要等下一个人来接手,把工作交付给另一个人,直到所有的工作结束了才能走。

六、全局函数: 函数

  定义语法: 函数原型 + 函数体。其实就是我们定义的普通函数。

void test()
{
        printf("b=%d\n",b++);
}

七、static函数:

  定义语法:static + 函数原型 + 函数体

  static 函数 只能在本文件内部使用。 其他文件即使声明也无效。就和静态变量一样,锁死在本文件内,锁在房间里。

八、生命周期:

  注意生命周期和作用域的区别。生命周期就是能活多久(什么时候被释放),作用域就是能在哪里活动(在哪里使用有效)。

1.局部变量:

  从变量定义开始,函数调用完成。 — 函数内部。

  为什么局部变量的生命周期是到函数调用完成?在day07 里面有说过:当函数调用时,系统会在 stack 空间上申请一块内存区域,用来供函数调用,主要存放 形参 和 局部变量(定义在函数内部)。当调用完成后,就释放这片空间。

2.全局变量:

  程序启动开始,程序终止结束。 — 程序执行期间。

  全局变量的生命周期早于函数,函数都没有开始执行,全局变量已经编译好,存储好了。

3.static局部变量:

  程序启动开始,程序终止结束。 — 程序执行期间。静态局部变量是在编译时赋初值的,并且只赋初值一次,在程序运行时它已有初值。在以后的每次调用函数时,不再重新赋初值,而是保留上一次函数调用结束时的值。

  就像是一出生就被关在监狱里面了,活得久,但是活动范围小。

4.static全局变量:

程序启动开始,程序终止结束。 — 程序执行期间。

5.全局函数:

程序启动开始,程序终止结束。 — 程序执行期间。

6.static函数:

程序启动开始,程序终止结束。 — 程序执行期间。

九、内存布局:

1.内存分区

  C代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。用size 列出一个二进制可执行文件的基本情况:
在这里插入图片描述
  通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)数据区(data)未初始化数据区(bss) 3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。

1.1 代码区

  存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

1.2 全局初始化数据区/静态数据区(data段)

  该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

1.3未初始化数据区(又叫 bss 区)

  存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

  程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。这就说明了为什么常量不可用修改,为了防止修改常量而导致全局区的大小改变。那为什么 const 定义的常量是可以修改呢?它是伪常量,不是放在全局区里面的,而是放在了栈区里面。

  然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区

1.4 栈区(stack)

  栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间

在这里插入图片描述

1.5 堆区(heap)

  堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

在这里插入图片描述

1.6 存储类型总结

在这里插入图片描述

1.7 测试变量的地址

  前面的文章,说了什么变量在哪片区域,那么现在就来测试一下,定义不同的类型变量,然后将他们的地址打出来。看他们的地址是否在同一片区,如果是放在同一片区中。

(1)测试代码

#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);

	printf("&c\t %p\t //静态局部未初始化变量\n", &c);
	printf("&d\t %p\t //静态局部初始化变量\n", &d);

	printf("&e\t %p\t //全局未初始化变量\n", &e);
	printf("&f\t %p\t //全局静态未初始化变量\n", &f);

	printf("&g\t %p\t //全局初始化变量\n", &g);
	printf("&h\t %p\t //全局静态初始化变量\n", &h);

	printf("i\t %p\t //只读数据(文字常量区)\n", i);

	k = (char *)malloc(10);
	printf("k\t %p\t //动态分配的内存\n", k);

	return 0;
}

(2)测试结果:将他们分类看,变量的地址是接近的。
在这里插入图片描述

2.开辟释放 heap 空间:

	void *malloc(size_t size); 
	 申请 size 大小的空间

	返回实际申请到的内存空间首地址。 【我们通常拿来当数组用】。

    因为不知道申请者是用这个内存空间存放什么类型的数据,所以返回的是泛型指针。

	void free(void *ptr);	释放申请的空间

		参数: malloc返回的地址值。

2.1 测试在stack区和在heap区定义数组

  在前面我们说,栈区的空间是比较小的,堆区的空间是很大的,当定义比较的大的数据时可以申请堆空间。

(1)在stack 定义数组:
在这里插入图片描述
测试结果:程序就崩了:
在这里插入图片描述
(2)申请堆区的空间定义数组:
在这里插入图片描述
测试结果:测试成功,可以定义,程序与下面 3.使用heap空间一样。
在这里插入图片描述

3.使用 heap 空间:

  空间是连续的。 当成数组使用。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  free后的空间,不会立即失效。 通常将free后的 地址置为NULL。free 地址必须 是 malloc申请地址。否则出错。
在这里插入图片描述

  如果malloc之后的地址一定会变化,那么使用临时变量tmp 保存。
在这里插入图片描述

  比如说想下面的使用方法,就会导致p的地址变换,所以我们应该用一个临时变量来存放申请得到的地址。或者不使用下面的方式,而是使用 *(p+i) 的方式
在这里插入图片描述

4.二级指针对应的 heap空间:

在这里插入图片描述

	申请外层指针: char **p = (char **)malloc(sizeof(char *) * 5);

	申请内层指针: for(i = 0; i < 5; i++)
			{
				p[i] = (char *)malloc(sizeof(char) *10);
			}

	使用: 不能修改 p 的值。

		 for(i = 0; i < 5; i++)
		{
			strcpy(p[i], "helloheap");
		}

	释放内层:

		 for(i = 0; i < 5; i++)
		{
			free(p[i]);
		}

	释放外层:

		free(p);


5.栈的存储特性:

  栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

6.内存操作函数:

6.1 memset()

#include <string.h>
void *memset(void *s, int c, size_t n);

  (1)功能:将s的内存区域的前n个字节以参数c填入,常用于初始化。

  (2)参数:

    1)s:需要操作内存s的首地址

    2) c:填充的字符,c虽然参数为int,但必须是unsigned char) , 范围为0~255

    3) n:指定需要设置的大小,注意单位是字节。

    4)返回值:s的首地址

6.1.1 测试:

(1)第一次测试,填充为0:
在这里插入图片描述
测试结果:只有两个被重置为0
在这里插入图片描述
修改测试代码:
在这里插入图片描述
再次打印结果:所有的被重置为0
在这里插入图片描述
(2)第二次测试,填充为1:
在这里插入图片描述

测试结果:
在这里插入图片描述

(3)分析结果:
  memset(p,1,40) 是往内存中的40个字节,每个字节都填充1的。就是使得那个字节的值为1,一个字节有8个比特位,使得一个字节的值等于1,那么就是一个字节的最低为1:
在这里插入图片描述
  如果是得4个字节的每个字节值为1,就是如下图:
在这里插入图片描述

  那么上面的这个二进制数,转换为十进制数输出就是:
在这里插入图片描述
  我们输出的数组是以十进制的方式输出的,每个整型数组占4个字节,输出的结果就是 16843009

6.2 memcpy()

#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);

  功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。

  参数:

  dest:目的内存首地址

  src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错

  n:需要拷贝的字节数

  返回值:dest的首地址

6.2.1 注意memcpy() 与strcpy() 的区别

  在拷贝字符串的时候,memcpy() 与 strcpy() 都可以使用。但是注意这两个函数的区别, 拷贝时strcpy() 是在源字符串遇到 ‘\0’ 为止。memcpy() 是拷贝完指定的内存字节数。

  memcpy() 的用途更广,strcpy() 只是用来拷贝字符串的。

6.3 memmove()

  memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。

6.4 memcmp()

#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2所指向内存区域的前n个字节
参数:
	s1:内存首地址1
	s2:内存首地址2
	n:需比较的前n个字节
返回值:
	相等:=0
	大于:>0
	小于:<0

7.内存常见问题:

1) 申请 0 字节空间

2)free空指针

3)越界访问

4)free ++后的地址

5)子函数malloc空间,main中用
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值