说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!
C
语言变量的作用域分为:
{}
之间的一段代码)生命周期:什么时候开辟空间(出生),释放空间(死亡),这个过程叫生命周期。
局部变量也叫auto
自动变量(auto
可写可不写),一般情况下代码块{}
内部定义的变量都是自动变量,它有如下特点:
示例1:
#include void test()
{//auto写不写是一样的//auto只能出现在{}内部auto int b = 20; return;
}int main(){//b = 100; //err, 在main作用域中没有b//int c;//printf("c=%d\n", c); // 未初始化的值 随机int* p = NULL;if (1){//在复合语句中定义,只在复合语句中有效int a = 10;int d = 30;p = &d;printf("a = %d\n", a);}//a = 10; //err 离开if()的复合语句,a已经不存在*p = 300;printf("%d", *p);return 0;
}
输出结果
a = 10
300
static
局部变量的作用域也是在定义的函数内有效static
局部变量的生命周期和程序运行周期一样,在执行main
函数之前就已经开辟空间,当程序结束之后才释放空间;同时static
局部变量的值只初始化一次,但可以赋值多次static
局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0
,字符型变量赋空字符示例1:
#include void fun1()
{int num1 = 1;num1++;printf("num1 = %d\n", num1);
}void fun2()
{//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次static int num2 = 1;num2++;printf("num2 = %d\n", num2);
}int main(void)
{static int n;printf("n = %d\n", n); // n = 0fun1(); // num1 = 2fun1(); // num1 = 2fun2(); // num2 = 2fun2(); // num2 = 3return 0;
}
输出结果
n = 0
num1 = 2
num1 = 2
num2 = 2
num2 = 3
extern
声明0
,字符型变量赋空字符示例1:
#include int num;void test01() {num = 10;printf("num = %d\n", num);}int main() {printf("num = %d\n", num); // 0test01(); // 10return 0;
}
示例2:
#include extern int num; // 声明num在其他文件定义过
int main() {num = 100;printf("num = %d\n", num);return 0;
}
static
全局变量的生命周期和程序运行周期一样,执行main函数之前就已经开辟空间,程序结束之后才释放空间;同时staitc
全局变量的值只初始化一次static
全局变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0
,字符型变量赋空字符示例1:
#include static int num2;void test02() {num2 = 20;printf("num2 = %d\n", num2);}int main() {printf("num2 = %d\n", num2); // 0test02(); // 20return 0;
}
示例2:
#include extern int num2; // error 静态全局变量不能进行声明,更不能在其他文件使用
int main() {num2 = 100;printf("num2 = %d\n", num2);return 0;
}
作用域: 局部变量(普通局部和静态局部)在{}
范围之内;普通全局变量作用域在整个工程;静态全局作用当前文件。
生命周期: 只有普通局部变量是运行至变量定义处时开辟,函数结束释放,其他变量都是执行main
函数之前就已经开辟空间,到程序结束之后才释放空间。
初始化的值: 只有普通局部未初始化的值为随机,其他为0
。
C语言中全局变量重定义的缺陷:
#include // 全局变量之所以能编译过去,是因为其中有三个默认为声明extern,因为extern可写可不写,所以最好声明加上 extern
int a;
int a;
int a;
int a;int main() {int b;//int b; // error 重定义return 0;
}
首先看看正确的全局变量分文件处理
main.c
文件
#include
#include "demo.h"// 声明全局变量cnum
//extern int cnum;
//extern void cfunc();
int main() {cfunc();printf("cnum = %d", cnum);return 0;
}
demo.c
文件
#include
// 定义全局变量
int cnum=10;
void cfunc() {cnum = 100;
}
demo.h
文件
#pragma once// 声明全局变量cnum
extern int cnum;
extern void cfunc();
现在将demo.h
头文件中的全局变量的声明注释掉,改为int cnum;
,这种情况下编译器是可以编译过去的,在不知道是定义还是声明的情况下,而在demo.c
文件中存在int cnum=10;
会当做定义,那么在main
函数中include "demo.h"
后,相当于在main
函数中存在int cnum;
会当做声明,即编译不报错。
那么如果修改demo.h
头文件中的int cnum;
为定义int cnum=20;
,那么就会报错重定义,所以在.h
文件中,全局变量只声明不定义,定义只放在.c
文件中
考虑作用域前提下就近原则:
示例1:
a.c
文件
#include static char* language = "java";void func3()
{printf("language = %s\n", language);
}
b.c
文件
#include // 不同作用域可以重名
char* language = "c";func2()
{printf("language = %s\n", language);}int main()
{func2();func3();char* language = "c++";printf("language = %s\n", language);if (1){char* language = "python";printf("language = %s\n", language);}printf("language = %s\n", language);return 0;
}
示例2:
func1.c
文件
int va = 7;
int getG(void)
{int va = 20;return va;
}
func2.c
文件
static int va = 18;
static int getG(void)
{return va;
}
int getO(void)
{return getG();
}
main.c
文件
#include extern int va;
extern int getG(void);
extern int getO(void);int main()
{printf("va=%d\n", va);printf("getO=%d\n", getO());printf("getG=%d\n", getG());printf("%d", va*getO()*getG());}
在C
语言中函数默认都是全局的,使用关键字static
可以将函数声明为静态,函数定义为static
就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
static
修饰的函数,静态函数只可以被当前文件函数调用全局函数:
#include static char* language = "java";
// 全局函数
void func3()
{printf("language = %s\n", language);
}
静态函数:
// 静态函数
static void func4()
{printf("language = %s\n", language);
}
如果非要调用静态函数,可以在静态函数所在.c文件中再定义一个全局函数,在这个全局函数中调用同文件的静态函数,那么我们就可以通过调用这个全局函数来调用静态函数。
// 静态函数
static void func4()
{printf("language = %s\n", language);
}
// 全局函数
void func5()
{ // 调用静态函数func4();
}
注意:
staitc
函数,那么作用域是文件级的,所以不同的文件static
函数名是可以相同的。类型 | 作用域 | 生命周期 |
---|---|---|
auto 变量 | 一对{ }内 | 当前函数 |
static 局部变量 | 一对{} 内 | 整个程序运行期 |
extern 变量 | 整个程序 | 整个程序运行期 |
static 全局变量 | 当前文件 | 整个程序运行期 |
extern 函数 | 整个程序 | 整个程序运行期 |
static 函数 | 当前文件 | 整个程序运行期 |
register 变量 | 一对{} 内 | 当前函数 |
全局变量 | 整个程序 | 整个程序运行期 |
C
代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。
在 Windows
下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data
和bss
合起来叫做静态区或全局区)。
代码区
存放 CPU
执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0
或者空(NULL
)。
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
未初始化数据区(BSS)
加载的是可执行文件BSS
段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS
区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
类型 | 作用域 | 生命周期 | 存储位置 |
---|---|---|---|
auto 变量 | 一对{} 内 | 当前函数 | 栈区 |
static 局部变量 | 一对{} 内 | 整个程序运行期 | 初始化在data 段,未初始化在BSS 段 |
extern 变量 | 整个程序 | 整个程序运行期 | 初始化在data 段,未初始化在BSS 段 |
static 全局变量 | 当前文件 | 整个程序运行期 | 初始化在data 段,未初始化在BSS 段 |
extern 函数 | 整个程序 | 整个程序运行期 | 代码区 |
static 函数 | 当前文件 | 整个程序运行期 | 代码区 |
register 变量 | 一对{} 内 | 当前函数 | 运行时存储在CPU 寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data 段 |
示例:
#include
#include 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;
}
输出结果
&a 003DFE9C //局部未初始化变量
&b 003DFE90 //局部初始化变量
&c 00088180 //静态局部未初始化变量
&d 00088014 //静态局部初始化变量
&e 000884A4 //全局未初始化变量
&f 0008817C //全局静态未初始化变量
&g 0008800C //全局初始化变量
&h 00088010 //全局静态初始化变量
i 00085060 //只读数据(文字常量区)
k 007104D0 //动态分配的内存
将一段内存空间填入某值:
#include
void *memset(void *s, int c, size_t n);
s
的内存区域的前n
个字节以参数c
填入s:
需要操作内存s
的首地址c:
填充的字符,c
虽然参数为int
,但必须是unsigned char
,范围为0~255
n:
指定需要设置的大小s
的首地址示例:
#include
#include // memset函数
int main()
{int a = 10;//a = 0; -> memset()memset(&a, 0, sizeof(a));printf("a=%d\n", a);char str[20] = "hellocdtaogang";printf("str=[%s]\n", str);memset(str, 0, sizeof(str));printf("str=[%s]\n", str);// 将前10个字符置为a字符memset(str, 'a', sizeof(str) - 10); // memset(str, 97, sizeof(str) - 10);printf("str=[%s]\n", str);return 0;
}
输出结果
a=0
str=[hellocdtaogang]
str=[]
str=[aaaaaaaaaa]
拷贝内存内容
#include
void *memcpy(void *dest, const void *src, size_t n);
src
所指的内存内容的前n
个字节到dest
所指的内存地址上。dest:
目的内存首地址src:
源内存首地址,注意:dest
和src
所指的内存空间不可重叠,可能会导致程序报错n:
需要拷贝的字节数dest
的首地址示例:
#define _CRT_SECURE_NO_WARNINGS
#include
#include // memcpy函数
int main()
{int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int b[10] = { 0 };// a = b;// error 常量不能修改// 将a数组中的前五个元素拷贝到b数组中memcpy(b, a, sizeof(int)*5);for (int i = 0; i < sizeof(b)/sizeof(b[0]); i++){printf("%d ", b[i]);}printf("\n");char str1[128] = "";char str2[128] = "abc\0def\0hellocdtaogang";char str3[128] = "";// 使用strncpystrncpy(str1, str2, sizeof(char) * 22);for (int i = 0; i < 22; i++){printf("%d ", str1[i]);}printf("\n");// 使用memcpymemcpy(str3, str2, sizeof(char) * 22);for (int i = 0; i < 22; i++){printf("%d ", str3[i]);}return 0;
}
输出结果
1 2 3 4 5 0 0 0 0 0
97 98 99 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
97 98 99 0 100 101 102 0 104 101 108 108 111 99 100 116 97 111 103 97 110 103
memmove()
功能用法和memcpy()
一样,区别在于:dest
和src
所指的内存空间重叠时,memmove()
仍然能处理,不过执行效率比memcpy()
低些。
比较内存内容
#include
int memcmp(const void *s1, const void *s2, size_t n);
s1
和s2
所指向内存区域的前n
个字节s1:
内存首地址1s2:
内存首地址2n:
需比较的前n个字节=0
>0
<0
示例:
#define _CRT_SECURE_NO_WARNINGS
#include
#include // memcmp函数
int main()
{int a[10] = { 1, 0, 2, 3, 5, 6, 2, 8, 9, 10 };int b[10] = { 1, 0, 4, 4, 5, 6, 7, 8, 9, 10 };int res1 = memcmp(a, b, sizeof(int));printf("res1 = %d\n", res1);int res2 = memcmp(a, b, sizeof(int)*10);printf("res2 = %d\n", res2);int res3 = strncmp(a, b, sizeof(int) * 10); // strncmp遇到0就结束了printf("res3 = %d\n", res3);char str1[] = "abcd\0abc";char str2[] = "abcd\0bbc";printf("%d\n", strncmp(str1, str2, sizeof(str1))); // strncmp遇到\0就结束了printf("%d\n", memcmp(str1, str2, sizeof(str1))); return 0;
}
输出结果
res1 = 0
res2 = -1
res3 = 0
0
-1
配置内存空间
#include
void *malloc(size_t size);
memset
初始化。size
:需要分配内存大小(单位:字节)NULL
示例:
#include
#include // malloc函数
int main()
{ //int a[10]; 直接是从栈区申请空间//申请一个数组,数组有10个元素,每个元素int类型,到堆区申请内存空间int* p = (int *)malloc(sizeof(int) * 10); // malloc函数的返回值未void *类型最好是强转下*p = 100;*(p + 5) = 200;for (int i = 0; i < 10; i++){printf("%d ", *(p + i));}
}
输出结果
100 -842150451 -842150451 -842150451 -842150451 200 -842150451 -842150451 -842150451 -842150451
释放原先配置的内存
#include
void free(void *ptr);
ptr
所指向的一块内存空间,ptr
是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。ptr:
需要释放空间的首地址,被释放区应是由malloc
函数所分配的区域。示例:
#include
#include
#include // free函数
int main()
{//申请一个字符数组,有1024元素char* p = (char *)malloc(1024);//将申请到空间清0memset(p, 0, 1024);strcpy(p, "hellocdtaogang");// 释放内存free(p);//free(p+1) // free 参数 地址必须是上一次malloc申请过的,不能去改变这个地址//printf("%s\n", p); // 释放完了再打印那么就不会是你想要的数据//free(p); // malloc申请的空间不可以释放两次,申请一次,释放一次return 0;
}
注意:free
只能释放一次上次申请过的空间;free
参数地址必须是上一次malloc
申请过的,不能改变这个地址;malloc
申请的空间不可以释放两次,申请一次,释放一次。
内存泄露:只申请而不释放
内存污染:向没有申请过的内存空间写入数据
示例:普通局部变量存在栈区,当函数调用完毕,就会释放,所以普通局部变量的地址是不可以返回操作的。
#include int* newfunc()
{int a = 10; a *= 10;return &a; // 函数调用完毕,a释放}int main()
{int* p = newfunc();//操作野指针指向的内存*p = 200; // error p所指向的空间已经被释放,printf("%d\n", *p);return 0;
}
示例1:静态局部变量存在静态全局区,已初始化的存在data区,只要程序不退出,就不会释放,所以这些变量的地址是可以返回操作。
#include int* newfunc()
{//int a = 10; static int a = 10;a *= 10;return &a; }int main()
{int* p = newfunc();*p = 200; // p所指向的空间没有释放(静态局部变量),则可以操作这块内存printf("%d\n", *p);return 0;
}
示例2:静态全局、全局变量存在静态全局区,已初始化的存在data区,只要程序不退出,就不会释放,所以这些变量的地址是可以返回操作。
#include //int a = 10; // 全局变量变量
static int a = 10; // 静态全局变量
int* newfunc()
{//int a = 10; //static int a = 10;a *= 10;return &a;}int main()
{int* p = newfunc();*p = 200; // p所指向的空间没有释放(静态全局变量、全局变量),则可以操作这块内存printf("%d\n", *p);return 0;
}
总之:只有普通局部变量的地址不可以返回,因为普通局部变量在所在的函数结束之后就被释放;而静态局部变量、全局变量、静态全局这些变量,只要程序不退出就不会释放,所以这些变量的地址是可以返回操作。
示例1:形参的本质就是局部变量,当函数调用完毕,就会释放,所以不可以返回形参的地址。
#include int* newfunc2(int num){ //形参的本质就是局部变量num += 100;return # // 函数调用完毕,num释放,不可以返回形参的地址}
int main()
{int num = 10;int *p = newfunc2(num); *p = 200;return 0;
}
示例2:定义函数返回k实参的地址,所以函数结束实参地址不会被释放。
#include int* newfunc3(int *k){int i = 100;*k = *k + i;return k; // 返回k指向的num的地址,所以函数结束num变量的地址没有被释放
}
int main()
{int num = 10;int* p = newfunc3(&num);return 0;
}
示例3:
示例:堆区的地址是可以返回的,堆区在函数结束后不会被释放;但不能直接赋值字符串常量,会导致空间丢失造成内存泄露
#include
#include char* newfunc4()
{char* q = malloc(100);return q; // 堆区的地址是可以返回的,函数结束不会被释放
}
int main()
{char* p = newfunc4();p = "hello";free(p); // error p并没有指向堆区,而是指向文字常量区"hello"return 0;
}
将p
赋值为整型,就不会出错了
使用strcpy(p, "hello");
就不会造成内存泄露
示例1: 通过函数的值传递,不能改变实参的值
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include void mem_p(char *q)
{q = malloc(1024);return;
}
int main()
{char* p = NULL;mem_p(p);strcpy(p, "hello");printf("p=%s\n", p);return 0;
}
如果直接编译运行,不会报错,但是没有打印数据,只能说编译器没有提示报错,但是可以通过打断点的方式来查看错误
导致以上错误就是p
指向的是NULL
,并没有指向堆区地址,就造成了内存污染,解决方法就是p
指向堆区开辟的地址即可。
char* mem_p(char *q)
{q = malloc(1024);return q;
}
int main()
{char* p = NULL;p = mem_p(p);strcpy(p, "hello");printf("p=%s\n", p);return 0;
}
示例2: 传实参的地址,可以在调用函数改变实参的值(实参为一级指针地址,形参为二级指针)
void mem_p2(char** k)
{*k = malloc(1024);return;
}
int main()
{char* p = NULL;mem_p2(&p);strcpy(p, "hello");printf("p=%s\n", p);return 0;
}