1、作用域
C 语言变量的作用域主要有三种:
- 代码块作用域(代码块是 {} 之间的一段代码)
- 函数作用域
- 文件作用域
1.1、局部变量
局部变量也叫 auto 自动变量(auto 可以不写),一般情况下代码块 {} 内部定义的变量都是自动变量,具有如下特点:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束,局部变量的生命周期也就结束了
int main()
{
//定义变量,局部变量,只在main函数内有效
//作用域:main函数之内
//生命周期:从创建到函数结束
auto int a=10; //auto可以省略不写
return 0;
}
1.2、全局变量
- 在函数外定义,可被本文件及其他文件中的函数所共用,若其他文件中的函数调用此变量,须用 extern 声明
- 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
- 全局变量存储在数据区
- 全局变量可以和局部变量同名,使用的时候采用就近原则
- 不同文件中的全局变量不可以重名
main.c
#include<stdio.h>
//全局变量:在函数外部定义的变量
//作用域,整个项目中所有文件,如果在其他文件中使用,需要声明
//生命周期,从程序创建到程序销毁
int a=10;
void func()
{
a=100;
printf("%d\n",a);
}
int main()
{
printf("%d\n",a); //10
int a=123; //数据在操作时,采用就近原则
//匿名内部函数
{
int a=456;
printf("%d\n",a); //456
}
//匿名内部函数
{
a=456;
printf("%d\n",a); //456
}
printf("%d\n",a); //456
func(); //100
func2(); //1000
return 0;
}
test.c
#include<stdio.h>
//声明全局变量
extern int a;
//定义全局变量
int b=10; //全局变量不可以重名
int b=10; //err,全局变量不可以重名
void func2()
{
a=1000;
printf("%d\n",a);
}
1.3、静态局部变量
- static 局部变量的作用域也是在定义的函数内有效
- static 局部变量的生命周期和程序的运行周期一样,同时 static 局部变量的值只能初始化一次,但可以赋值多次
- static 局部变量若未赋初值,则由系统自动赋值,数值型变量自动赋初值 0,字符型变量赋空字符
- static 局部变量只能在其作用域内使用,即当前函数中使用
- static 局部变量在数据区存储,不会随着栈区的变化而变化
//静态局部变量
void func()
{
printf("%d\n",b); //err,未定义变量b
}
int main()
{
//静态局部变量
static int b=10;
printf("%d\n",b); //10
return 0;
}
//静态局部变量
void func()
{
// printf("%d\n",b); //err,未定义变量b
static int b=10; //静态局部变量只会初始化一次,可以多次赋值,作用域只在函数内,只能在当前函数中使用,生命周期与程序生命周期相同
b++;
printf("%d\n",b);
}
int main()
{
//如果在 func() 中定义的是局部变量,会输出十次 11
for(int i=0;i<10;i++)
{
func(); //十次输出,分别输出:11,12,13,14,15,16,17,18,19,20
}
return 0;
}
1.4、静态全局变量
- 静态全局变量,作用域为本文件,只能在本文件中使用,不能在其他文件中使用
- 生命周期:从程序创建到程序销毁
- 存储在数据区
main.c
//静态全局变量,作用域为本文件,只能在本文件中使用,不能在其他文件中使用
//生命周期:从程序创建到程序销毁
//存储在数据区
static int a=10;
void func()
{
a=20;
printf("%d\n",a); //20
}
int main()
{
printf("%d\n",a); //10
func();
func06();
return 0;
}
test.c
extern int a; //err,静态全局变量的作用域为本文件,因此这里无法使用
void func06()
{
printf("%d\n",a); //20
}
- 变量作用域
变量类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
局部变量 | 函数内部 | 从局部变量创建到函数结束 | 栈区 |
全局变量 | 项目中所有文件 | 从程序创建到程序销毁 | 数据区 |
静态局部变量 | 函数内部 | 从程序创建到程序销毁 | 数据区 |
静态全局变量 | 定义所在的文件中 | 从程序创建到程序销毁 | 数据区 |
1.5、未初始化的数据
- 局部变量未初始化,值为任意值(乱码)
- 未初始化的全局变量,值为0
- 未初始化的静态局部变量,值为0
- 未初始化的静态全局变量,值为0
//未初始化的全局变量,值为0
int abc;
int main()
{
/*
//局部变量未初始化,值为任意值(乱码)
int abc;
printf("%d\n",abc); //在 vs 中 err,在其他环境中可以输出,但是为乱码
*/
printf("%d\n",abc);
}
1.6、全局函数和静态函数
- 在 C 语言中,函数默认都是全局的,使用 static 关键字可以使函数变成静态函数
- static 静态函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数也无法使用
- 对于不同文件的 static 函数的名字可以相同
- 在 C 语言中,不支持函数的重载
- 全局函数的名称在作用域中是惟一的
- 全局函数的作用域:在这各个项目的所有文件中都可以使用
- 函数可以调用自己:递归函数。递归函数一定要有出口
- 静态函数可以和全局函数重名,在当前文件中,使用的是当前文件中定义的函数
- 静态函数和全局函数的生命周期相同,从程序创建到程序销毁
main.c
//全局函数声明
void buble();
int main()
{
buble(); //err,静态函数,作用域仅限于定义这个函数的文件
return 0;
}
test.c
//静态函数,作用域仅限于定义这个函数的文件
static void buble()
{
printf("buble\n");
}
- 函数作用域
函数类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
全局函数 | 项目中所有文件 | 从程序创建到程序销毁 | 代码区(未唤醒),栈区(唤醒之后) |
静态函数 | 定义所在的文件 | 从程序创建到程序销毁 | 代码区 |
1.7、extern 全局变量声明
- extern 用于声明一个在别的文件中已经定义的全局变量,这里只是声明,不是定义
注意:
- 语序在不同的函数中使用相同的变量名,它们代表了不同的对象,分配不同的单眼,互不干扰
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
- 所有的函数默认是全局的,那么函数名不能重名;不同文件的 static 函数是可以重名的
2、内存布局
2.1、内存分区
- 局部常量不安全,全局常量是安全的
//未初始化全局变量
int a1;
//初始化全局变量
int b1=10;
//未初始化静态全局变量
static int c1;
//初始化静态全局变量
static int d1=10;
int main()
{
//初始化局部变量
int e1=10;
//未初始化静态局部变量
static int f1;
//初始化静态局部变量
static int g1=10;
//常量字符串
char* p="hello world";
//数组
int arr[]={1,2,3,4};
//指针变量
int *pp=arr;
//指针地址 &pp
return 0;
}
2.2、内存模型
应用程序的内存四区模型:
- 代码区:程序执行的二进制码(程序指令)
- 共享的,形同内容只存放一份
- 只读的
- 数据区(与程序同生共死)
- 初始化数据区,data 段
- 未初始化数据区,bss 段
- 常量区
- 栈区:系统为每一个程序分配一个临时的空间,要用就分配,用完就销毁
- 局部变量,函数信息,函数参数,数组
- 栈区大小:1MB,在 windows 中可以拓展到 10MB,在 Linux 中,可以拓展到 16MB
- 堆区:
- 堆区大小:没有上限限制
- 存储大数据,图片,音频视频文件
- 需要手动开辟,手动释放。开辟方式:malloc,colloc,realloc,free
2.3、内存分区概述
- 在程序没有运行前,即程序没有加载到内存前,可执行程序内部已经分好了 3 段信息:代码区(text)、数据区(data)、未初始化数据区(bss),其中 data 和 bss 合起来统称静态区或者全局区。
- 程序在加载到内存前,代码区和全局区(data 段和 bss 区)的大小是固定的,程序运行期间是不能改变的。
- 运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出的代码区(text)、数据区(data)、未初始化数据区(bss)之外,还额外增加了栈区、堆区。
- 内存中,从低地址区到高地址区的方向,依次分别是:代码区、初始化数据区(离低地址区较近的部分是常量区,离高地址区较近的部分是 data 段)、未初始化数据区(bss)、堆区、栈区(栈区内部从高地址区往低地址区)。
程序运行之前:
1)代码区
- 存放 CPU 执行的机器指令。
- 代码区通常是可共享的(即其他的可执行程序也可以调用它)。使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
- 代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。
- 另外,代码区还规划了局部变量的相关信息。
2)全局初始化数据区 / 静态数据区(data 段)
- data 段包含了在程序中明确被初始化的全局变量、已经被初始化的静态变量(包括全局静态变量和局部静态变量)、常量数据(例如字符串常量)
3)未初始化数据区(bss 区)
- 存入的是全局未初始化变量和外初始化静态变量
- 未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者是空(NULL)
程序运行之后:
1)代码区(text segment)
- 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的
2)未初始化数据区(bss)
- 加载的是可执行文件 bss 段,位置可以分开亦可以紧靠数据段,存储在数据段的数据(全局未初始化、静态未初始化数据)的生存周期为整个程序运行过程
3)全局初始化数据区 / 静态数据区(data segment)
- 加载的是可执行文件的数据段,存储于数据段(全局初始化、静态初始化的数据,文字常量(只读))的数据的生存周期为整个程序运行过程
4)栈区(stack)
- 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。
- 在程序运行过程中,实时加载和释放,因此局部变量的生存周期为申请到释放该段栈空间
5)堆区(heap)
- 堆是一个大容器,其容量远远大于栈,但没有栈那样先进后出的顺序。
- 用于动态内存分配。
- 堆在内存中位于 bss 区和栈区之间。
- 一般由程序员分配和释放,若程序员未释放,程序结束时由操作系统回收。
2.4、栈区存储模型
1)普通变量存储:从高地址区往低地址区存储
2)数组:开辟一段连续的内存,数组从前往后是从低地址区往高地址区存储
3)栈区:从高地址区向低地址区生长。先进后出,后进先出
4)堆区:从低地址区向高地址区生长
3、堆空间开辟和使用
3.1、栈区的大小
int main()
{
//栈区的大小
int arr[820000]={0}; //err,数组太大了,栈区内存不够 820000*4/1024/1024>31MB,远大于栈区的大小,栈区大小为1MB
}
- 栈区的大小可以拓展,windows 中可以拓展至10 MB,在 Linux 中,可以拓展到 16MB
3.2、开辟堆空间存储数据
1)malloc() 函数
#include<srdlib.h>
void* malloc(size_t size);
- 功能:在内存的动态存储区(堆区)中分配一块长度为 size 字节的连续区域,用来存放类型说明符指定的类型,分配的内存空间内容不确定,需要使用 memset 进行初始化
- 参数:size,需要分配的内存大小(单位,字节)
- 返回值:
- 成功:分配空间的起始地址
- 失败:NULL
2)free() 函数
#include<srdlib.h>
void free(void* ptr);
- 功能:释放 ptr 所指向的一块内存空间,ptr 是一个任意类型的指针变量,指向被释放区域的首地址,对同一内存空间多次释放会出错
- 参数:ptr,需要释放空间的首地址,被释放区应该是由 maclloc 函数所分配的区域
- 返回值:无
int main()
{
//开辟堆空间存储数据
int* p=(int*)malloc(sizeof(int));
printf("%p\n",p);
printf("%d\n",*p); //乱码
//使用堆空间
*p=123;
printf("%d\n",*p); //123
//释放堆空间
free(p);
printf("%p\n",p); //还是输出 p 的地址,但是此时 p 指向的是一个未知空间,p 为野指针
*p=456;
printf("%d\n",*p); //456
p=NULL;
return 0;
}
为了防止所使用的堆空间指针释放后成为野指针,应该让其指向空指针。
3)可以开辟多大的堆空间
- 理论上没有上限,不超过最大剩余内存即可。但是由于开辟的是连续空间,因此开辟空间过大,当内存不存在这么大的连续空间,会开辟失败。
3.3、堆空间存储数据
- 在堆空间的开辟和释放的时候,应该保证释放的指针和开辟时的指针相同
#define MAX 10
int* p=(int*)malloc(sizeof(int)*10);
for(int i=0;i<10;i++)
{
p[i]=i;
}
for(int i=0;i<10;i++)
{
printf("%d\n",*(p+i));
}
free(p);
srand((size_t)time(NULL));
int* p=(int*)malloc(sizeof(int)*MAX);
for(int i=0;i<MAX;i++)
{
p[i]=rand()%100;
printf("%d\n",*(p+i));
}
for(int i=0;i<MAX;i++)
{
printf("%d\n",*p);
p++;
}
free(p); //err,这里会报错,因为指针在叠加过程中,正在不断改变,最后释放的指针和开辟的空间首地址不同,会导致前面开辟的堆空间成为无主空间,只有等程序结束才能释放
//在堆空间的开辟和释放的时候,应该保证释放的指针和开辟时的指针相同
3.4、内存操作函数
1)memset()
#include<string.h>
void* memset(void* s,int c,size_t,n);
- 功能:将 s 的内存区域的前 n 个字节以参数 c 填入
- 参数:
- s:需要操作的内存的首地址
- c:填充的字符,c 为 int 型,但必须是 unsigned char,范围为 0-255
- n:指定需要设置的大小
- 返回值:s 的首地址
int* p=(int*)malloc(sizeof(int)*10);
//未初始化时,原始数据,乱码
for(int i=0;i<10;i++)
{
printf("%d\n",*(p+i));
}
//memset() 重置内存空间的值
memset(p,0,40);
//memset(p,1,40),这样无法使内部每个整型都变成 1,实际上是将四十个字节分别设置为 00000001,最后再将 0000001000000100000010000001 以整型读出,就和预想的不一样了
//字符数组可以使用 memset() 重置,但是不能用于字符串形式输出
for(int i=0;i<10;i++)
{
printf("%d\n",*(p+i)); //输出十个 0
}
free(p);
2)memcpy()
#include<string.h>
void* memcpy(void* dest,const void* src,size_t,n);
- 功能:拷贝 src 所指的内存内容的前 n 个字节到 dest 所指的内存地址上
- 参数:
- dest:目的内存首地址
- src:源内存首地址
- n:需要拷贝的字节数
- 返回值:dest的首地址
注:dest 和 src 所指的内存空间不可以重叠,否则可能会导致程序报错。当他们重叠的时候,使用 memmove
int arr[]={1,2,3,4,5,6,7,8,9,10};
int* p=(int*)malloc(sizeof(int)*10);
memcpy(p,arr,sizeof(int)*10);
for(int i=0;i<10;i++)
{
printf("%d\n",*(p+i)); //输出:1,2,3,4,5,6,7,8,9,10
}
free(p);
//dest 和 src 所指的内存空间重叠,可能会导致报错
memcpy(&arr[5],&arr[3],20);
strcpy() 和 memcpy() 的区别:
- strcpy():遇到 \0 停止
- memcpy():拷贝的内容和字节数有关,和拷贝内容无关
3)memmove()
memmove() 功能用法和 memcpy() 一样,区别在于:/dest 和 src 所指的内存空间重叠时,memmove() 仍然能够处理,不过执行效率比 memcpy() 低。
int arr[]={1,2,3,4,5,6,7,8,9,10};
//dest 和 src 所指的内存空间重叠,使用 memmove() 更安全,不会报错
memmove(&arr[5],&arr[3],20);
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
int arr1[]={1,2,3,4,5,6,7,8,9,10};
int arr2[]={1,2,3,4,5};
memcmp(arr1,arr2,20); // 0,相同
char str1[]="hello\0 world";
char str1[]="hello\0 world";
memcmp(str1,str2,13); // 0,相同
strcmp() 和 memcmp() 的区别:
- strcmp():遇到 \0 停止
- memcmp():拷贝的内容和字节数有关,和拷贝内容无关
3.5、内存常见问题
1)数组下标越界
//数组下标越界
char ch[11]="hello world";
//堆空间越界
char* p=(char*)malloc(sizeof(char)*11);
strcpy(p,"hello world");
printf("%s\n",p); //输出:hello world
free(p); //err,开辟了11字节,使用和释放了12个
2)开辟内存大小和释放内存大小不一致
int* p=(int*)malloc(0);
printf("%p\n",p); //会输出一个地址
*p=100;
printf("%d\n",*p); //输出:100
free(p); //err,由于分配了 0 字节空间,p 相当于是野指针,但是释放了 4 字节,开辟内存大小和释放内存大小不一致
3)开辟空间和类型不对应
int* p=(int*)malloc(10);
p[0]=123;
p[1]=456;
//p[2]=789;
printf("%p\n",p); //会输出一个地址
printf("%d\n",*p); //输出:123
printf("%d\n",*(p+1)); //输出:456
//printf("%d\n",*(p+2)); //err,堆空间越界了
4)堆空间不允许多次释放
int* p=(int*)malloc(sizeof(int)*10);
free(p); //释放 p,没问题
//free(p); //此时 p 是野指针,再次释放会报错
//在释放完之后,应该让 p 指向空指针
p=NULL;
//空指针允许多次释放
free(p);
free(p);
free(p);
free(p);
free(p);
5)释放的指针应该和开辟空间时得到的指针相同
int* p=(int*)malloc(sizeof(int)*10);
for(int i=0;i<10;i++)
{
*p=0;
//指针叠加,不断改变指针方向,释放会出错
p++;
}
free(p); //err,指针叠加,不断改变指针方向,释放会出错
p=NULL;
4)值传递和地址传递
void func(int* p) //本质上还是值传递,传递的是指针变量
{
p=(int*)malloc(sizeof(int)*10);
}
voidfunc1(int** p) //这个才是地址传递
{
*p=(int*)malloc(sizeof(int)*10);
}
int main()
{
int *p=NULL;
func(p);
//func1(&p);
for(int i=0;i<10;i++)
{
p[i]=i; //err,这里 p 依然是空指针,操作空指针会报错
}
free(p);
}
//解决这里的问题可以使用地址传递,或者是使用函数的返回值
3.6、二级指针对应的堆空间
//开辟二级指针对应的堆空间,实际上就是一个 5*3 的二维整型数组,int arr[5][3]
int** p=(int**)malloc(sizeof(int*)*5); //在堆空间中开辟
p[0]=(int*)malloc(sizeof(int)*3);
p[1]=(int*)malloc(sizeof(int)*3);
//开辟一级指针对应的堆空间
for(int i=0;i<5;i++)
{
p[i]=(int*)malloc(sizeof(int)*3); //在堆空间中开辟,但是 p 和 p[i]之间不一定是连续的
}
//内存使用
for(int i=0;i<5;i++)
{
for(int j=0;j<3;j++)
{
scanf("%d",&p[i][j]);
}
}
for(int i=0;i<5;i++)
{
for(int j=0;j<3;j++)
{
printf("%d ",p[i][j]);
}
printf("\n");
}
//空间释放,需要一级一级地释放,从内层向外层释放
for(int i=0;i<5;i++)
{
free(p[i]);
}
f