一、变量的作用域
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中用