首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言基础之【内存管理】

C语言基础之【内存管理】

作者头像
序属秋秋秋
发布2025-12-18 15:13:11
发布2025-12-18 15:13:11
1410
举报

往期《C语言基础系列》回顾: 链接: C语言基础之【C语言概述】 C语言基础之【数据类型】(上) C语言基础之【数据类型】(下) C语言基础之【运算符与表达式】 C语言基础之【程序流程结构】 C语言基础之【数组和字符串】(上) C语言基础之【数组和字符串】(下) C语言基础之【函数】 C语言基础之【指针】(上) C语言基础之【指针】(中) C语言基础之【指针】(下)


存储类型

存储类型(Storage Class):用于定义变量或函数的存储位置生命周期作用域以及链接属性 C语言提供了四种存储类型关键字autoregisterstaticextern

存储类型总结:

存储类型

作用域

生命周期

存储位置

特点

auto

局部作用域

代码块内

栈区

默认存储类型,通常省略

register

局部作用域

代码块内

寄存器或栈区

建议存储在寄存器中,提高访问速度

static

局部或文件作用域

整个程序运行期间

全局区/静态区

静态局部变量保留值,静态全局变量隐藏

extern

整个程序

整个程序运行期间

全局区/静态区

引用其他文件中定义的全局变量或函数

作用域

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

  • 代码块作用域
  • 函数作用域
  • 文件作用域

普通局部变量

普通局部变量:是在函数或代码块内部定义的变量,其作用域仅限于定义它的函数或代码块。

  • 局部变量的默认存储类型是auto
  • 局部变量也叫auto自动变量 (auto可写可不写)

普通局部变量的特点:

  1. 作用域
    • 局部变量的作用域仅限于定义它的函数或代码块。
    • 在函数或代码块外部无法访问。
  2. 生命周期
    • 局部变量的生命周期从函数或代码块开始执行时创建,到函数或代码块结束时销毁。
    • 每次函数调用时,局部变量都会重新初始化。
  3. 存储位置
    • 局部变量通常存储在 栈(stack) 中。
  4. 默认值
    • 局部变量不会自动初始化,其初始值是未定义的(垃圾值)

代码示例:普通局部变量

代码语言:javascript
复制
#include <stdio.h>

void func() 
{
    int x = 10;  // 局部变量
    printf("x = %d\n", x);
    x++;
}

int main() 
{
    func();  // 输出: x = 10
    func();  // 输出: x = 10
    return 0;
}
  • 在每次调用 func() 时,局部变量 x 都会重新创建并初始化为 10

静态局部变量

静态局部变量:是在函数内部定义的变量,使用 static 关键字修饰。

  • 类似于普通局部变量,区别在于其使用了 static 关键字修饰。
  • 它的作用域仍然是局部的,但其 生命周期存储位置 与普通局部变量不同。

静态局部变量的特点:

  1. 作用域
    • 静态局部变量的作用域仅限于定义它的函数或代码块。
    • 在函数或代码块外部无法访问。
  2. 生命周期
    • 静态局部变量的生命周期从程序开始运行时创建,到程序结束时销毁。
    • 即使函数调用结束,静态局部变量的值也会保留,下次调用时不会重新初始化。
  3. 存储位置
    • 静态局部变量存储在 静态存储区(static storage area),而不是栈中。
  4. 默认值
    • 静态局部变量若未赋以初值,则由系统自动赋值:
      • 数值型变量自动赋初值0
      • 字符型变量自动赋初值空字符

代码示例:静态局部变量

代码语言:javascript
复制
#include <stdio.h>

void func()
{
    static int x = 10;  // 静态局部变量
    printf("x = %d\n", x);
    x++;
}

int main()
{
    func();  // 输出: x = 10
    func();  // 输出: x = 11
    func();  // 输出: x = 12
    return 0;
}
  • 在第一次调用 func() 时,静态局部变量 x 初始化为 10
  • 之后的每次调用 func() 时,x 的值会保留并递增

普通局部变量与静态局部变量的对比:

特性

普通局部变量

静态局部变量

作用域

仅限于定义它的函数或代码块

仅限于定义它的函数或代码块

生命周期

函数或代码块执行期间

整个程序运行期间

存储位置

栈(stack)

静态存储区(static storage area)

初始化

未初始化时为垃圾值

自动初始化为 0

多次调用时的行为

每次调用重新初始化

保留上次调用的值

普通全局变量

普通全局变量:是在所有函数之外定义的变量,通常位于文件的顶部。


普通全局变量的特点:

作用域:从定义点开始,直到文件结束。

生命周期:程序启动时创建,程序结束时销毁。

链接性:具有 外部链接性,可在其他文件中通过extern声明访问。

代码语言:javascript
复制
// file1.c
int globalVar = 10;   // 全局变量
                  
// file2.c
extern int globalVar; // 声明全局变量,定义在file1.c中
//声明一个变量globalVar,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义

extern关键字:用于声明一个在其他文件中定义的全局变量。

  • extern声明不会分配内存,只是引用已经存在的全局变量。

代码示例:普通全局变量

代码语言:javascript
复制
#include <stdio.h>

int globalVar = 10;  // 全局变量

void func()
{
	printf("全局变量: %d\n", globalVar);
}

int main()
{
	func();
	globalVar = 20;  // 修改全局变量
	func();
	return 0;
}
  • 全局变量在整个程序中都是可见的,可以被程序中的任何函数访问和修改。

输出:

代码语言:javascript
复制
全局变量: 10
全局变量: 20

静态全局变量

静态全局变量 :是指在函数外部使用 static 关键字声明的变量。


静态全局变量的特点:

作用域:仅限于定义它的文件,其他文件无法访问该变量。

生命周期:程序启动时创建,程序结束时销毁。

链接性:具有 内部链接性,无法在其他文件中访问。

代码语言:javascript
复制
// file1.c
static int staticGlobalVar = 20;  // static全局变量,仅在file1.c中可见
                  
// file2.c
extern int staticGlobalVar;   // 错误,无法访问file1.c中的static全局变量

代码示例:静态全局变量

代码语言:javascript
复制
#include <stdio.h>

static int staticGlobalVar = 30;  // 静态全局变量

void func()
{
	printf("static全局变量: %d\n", staticGlobalVar);
}

int main()
{
	func();
	staticGlobalVar = 40;  // 修改静态全局变量
	func();
	return 0;
}

输出:

代码语言:javascript
复制
static全局变量: 30
static全局变量: 40

普通全局变量和静态全局变量对比总结:

特性

普通全局变量

静态全局变量

作用域

文件内全局

文件内全局

生命周期

程序运行期间

程序运行期间

链接性

外部链接

内部链接

跨文件访问

可以

不可以

全局函数和静态函数

在这里插入图片描述
在这里插入图片描述

全局函数:是在所有函数外部定义的函数。(默认的函数类型)


全局函数的特点:

  • 全局可见性:全局函数具有全局的作用域。
    • 这使得不同源文件中的代码可以方便地复用该函数的功能。
  • 外部链接属性:全局函数具有外部链接属性。
    • 这意味着在一个源文件中定义的全局函数可以在其他源文件中通过函数声明来调用。

代码示例:全局函数

代码语言:javascript
复制
//file1.c
#include <stdio.h>

// 全局函数的定义
int subtract(int a, int b) 
{
    return a - b;
}
代码语言:javascript
复制
//file2.c
#include <stdio.h>

// 声明 file1.c 中的全局函数
extern int subtract(int a, int b);

int main() 
{
    int result = subtract(8, 3);
    printf("两数相减的结果: %d\n", result); //输出:两数相减的结果: 5
    return 0;
}
  1. 如果需要在其他文件中调用全局函数,需要在调用文件中声明该函数
  2. 通常使用extern关键字,但extern可以省略,因为函数声明默认具有外部链接属性

extern关键字:用于声明一个在其他文件中定义的全局函数。

  • 它告诉编译器该函数的定义在别处,通常在另一个源文件中。
  • extern声明不会定义函数,只是引用已经存在的全局函数。

静态函数:是使用static关键字修饰的函数,其定义形式和普通函数类似,只是在函数返回类型前加上static关键字。


静态函数的特点:

  • 局部可见性:静态函数的作用域仅限于定义它的源文件。
    • 在一个源文件中定义的静态函数,在其他源文件中无法直接调用。
  • 内部链接属性:静态函数具有内部链接属性。
    • 这意味着它只能在定义它的源文件中被调用,其他源文件无法访问该函数。

代码示例:静态函数

代码语言:javascript
复制
// file1.c
#include <stdio.h>

// 静态函数定义
static void staticFunc() 
{
    printf("这是 file1.c 中的静态函数\n");
}

//全局函数定义
void publicFunc() 
{
    printf("从同一个文件中调用静态函数:\n");
    staticFunc();  // 静态函数只能在当前文件中调用
}
代码语言:javascript
复制
// file2.c
// 声明全局函数
void publicFunc();

int main() 
{
    publicFunc();  // 调用全局函数
    // staticFunc();  // 错误:静态函数无法在其他文件中访问
    return 0;
}

输出:

从同一个文件中调用静态函数: 这是 file1.c 中的静态函数

全局函数和静态函数的对比:

特性

全局函数

静态函数

作用域

整个程序

定义它的文件

链接属性

外部链接(external linkage)

内部链接(internal linkage)

可见性

所有文件可见

仅定义文件可见

关键字

无(默认)

static

用途

提供公共接口

隐藏实现细节,模块化设计

内存布局

内存分区

在 Windows 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:

在这里插入图片描述
在这里插入图片描述

通过上图可以得知: 程序没有运行时,也就是说程序没有加载到内存时,可执行程序内部已经分好3段信息 分别为:代码区(text)数据区(data)未初始化数据区(bss) (有些人直接把databss合起来叫做全局区静态区


程序没有运行时,代码区 和 全局区( data 和 bss ) 的大小就是固定的,程序运行期间不能改变。 然后,程序运行时,系统把程序加载到内存,内存上除了有根据可执行程序的信息分出代码区(text)数据区(data)未初始化数据区(bss)之外,还额外增加了 栈区堆区

在这里插入图片描述
在这里插入图片描述

在C语言中,程序运行时使用的内存通常分为以下几个分区(也称为内存段

C语言程序的内存通常分为以下四个主要区域:

  1. 代码区(Text Segment)
  2. 全局区/静态区(Data Segment)
    • 初始化的全局变量和静态变量(Data 段)
    • 未初始化的全局变量和静态变量(Bss段)
  3. 栈区(Stack Segment)
  4. 堆区(Heap Segment)

代码区(Text Segment)

  • 存放内容:程序的机器指令(即:编译后的二进制代码)
  • 特点
    • 由操作系统管理。
    • 在程序运行期间,代码区的大小是固定的。
    • 只读,不可修改。
  • 示例:函数体的代码等。

全局区/静态区(Data Segment)

全局区/静态区分为两部分:

初始化的全局变量和静态变量(初始化区)

存放内容:显式初始化的全局变量和静态变量。

特点

  • 在程序启动时分配内存,并初始化为指定的值。
  • 生命周期为整个程序运行期间。

示例

代码语言:javascript
复制
int globalVar = 10;          // 初始化的全局变量
static int staticVar = 20;   // 初始化的静态变量

未初始化的全局变量和静态变量(BSS区)

存放内容:未显式初始化的全局变量和静态变量。

特点

  • 在程序启动时分配内存,并初始化为0(或空指针)
  • 生命周期为整个程序运行期间。

示例

代码语言:javascript
复制
int globalVar;         // 未初始化的全局变量
static int staticVar;  // 未初始化的静态变量

栈区(Stack Segment)

存放内容

  • 局部变量(包括函数参数)
  • 函数调用的上下文(如:返回地址、寄存器状态等)

特点

  • 由编译器自动管理。
  • 栈区的大小有限,通常较小(默认几MB)
  • 内存分配和释放遵循“后进先出”(LIFO)原则。
  • 栈区的分配和释放速度快。

示例

代码语言:javascript
复制
void func() 
{
    int localVar = 30;  // 局部变量,存放在栈区
    printf("%d\n", localVar);
}

堆区(Heap Segment)

存放内容:动态分配的内存(通过malloccallocrealloc等函数分配)

特点

  • 由程序员手动管理(分配和释放)
  • 堆区的大小通常较大,受系统可用内存限制。
  • 需要显式释放内存(通过free函数),否则会导致内存泄漏。
  • 堆区分配和释放速度较慢。

示例

代码语言:javascript
复制
int *ptr = (int *)malloc(sizeof(int) * 10);  // 动态分配内存,存放在堆区
                  
if (ptr != NULL) 
{
    free(ptr);  // 释放内存
}

内存分区示意图:

代码语言:javascript
复制
+-----------------------+
|       栈区 (Stack)     |  <- 高地址
|-----------------------|
|          ↓            |
|                       |
|          ↑            |
|-----------------------|
|       堆区 (Heap)      |
|-----------------------|
|  未初始化数据区 (BSS)   |
|-----------------------|
| 初始化数据区 (Data)     |
|-----------------------|
|   代码区 (Text)        |  <- 低地址
+-----------------------+

内存四区的比较:

分区

存放内容

管理方式

生命周期

特点

代码区

程序指令常量字符串

操作系统

整个程序运行期间

只读、大小固定

全局区/静态区

初始化和未初始化的全局变量/静态变量

编译器

整个程序运行期间

初始化区存放显式初始化的变量BSS区存放未初始化的变量

栈区

局部变量函数调用上下文

编译器

函数调用期间

自动管理、大小有限

堆区

动态分配的内存

程序员

直到显式释放

手动管理、大小较大

存储类型与内存四区

代码示例

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int globalVar = 10;          // 全局变量,存放在全局区/静态区
static int staticVar = 20;   // 静态变量,存放在全局区/静态区
int uninitGlobalVar;         // 未初始化的全局变量,存放在BSS段

void func()
{
	int localVar = 30;             // 局部变量,存放在栈区
	static int staticLocalVar = 40;// 静态局部变量,存放在全局/静态区
	int* heapVar = (int*)malloc(sizeof(int));  // 动态分配内存,存放在堆区
	*heapVar = 50;

	printf("局部变量: % d\n静态局部变量: % d\n堆变量: % d\n", localVar, staticLocalVar, *heapVar);

	free(heapVar);
}

int main()
{
	func();
	return 0;
}

输出

代码语言:javascript
复制
局部变量:  30
静态局部变量:  40
堆变量:  50

各存储类型的变量或函数的总结:

类型

存储位置

普通局部变量

栈区

静态局部变量

初始化在data段,未初始化在BSS段

普通全局变量

初始化在data段,未初始化在BSS段

静态全局变量

初始化在data段,未初始化在BSS段

全局函数

代码区

静态函数

代码区

字符串常量

data段

寄存器变量

运行时存储在CPU寄存器

代码示例:各存储类型的变量所在内存分区的地址高低

代码语言:javascript
复制
#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;
}

输出

代码语言:javascript
复制
&a       000000E22E0FFCA4        //普通局部未初始化变量
&b       000000E22E0FFCC4        //普通局部初始化变量

&c       00007FF69C59D1D8        //静态局部 未 初始化变量
&d       00007FF69C59D050        //静态局部初始化变量
&e       00007FF69C59D1D0        //普通全局 未 初始化变量
&f       00007FF69C59D1D4        //静态全局 未 初始化变量
&g       00007FF69C59D048        //普通全局初始化变量
&h       00007FF69C59D04C        //静态全局初始化变量

i        000000E22E0FFCE4        //只读数据(文字常量区)

k        000002C1A6090100        //动态分配的内存

内存操作函数

内存操作函数:用于处理内存块的重要工具。

  • 这些函数可以对内存进行 填充复制比较查找 操作。
  • 通常定义在<string.h>头文件中。

内存操作函数的总结:

函数

功能

特点

memset

内存填充

按字节将内存块置为指定值,用于初始化

memcpy

内存复制

高效复制内存,不处理重叠情况

memmove

内存移动

复制内存,可处理重叠区域

memcmp

内存比较

按字节比较两块内存内容

memchr

内存查找

在内存块中查找首个指定字节值

memset()

函数的介绍: memset() :用于将指定内存区域的前若干个字节设置为特定的值。

  • 它定义在 <string.h> 头文件中。
  • 它按字节填充数据,不关心数据类型。
  • 常用于初始化内存、清空缓冲区或设置特定模式。

memset 函数将从地址 s 开始的连续 n 个字节的内存区域,全部设置为指定的值 c 注意

  • 必须确保指针 s 指向的内存区域足够大,能够容纳 n 个字节,否则会导致内存越界。
  • 必须确保填充值 cunsigned char 范围内。

函数的原型:

代码语言:javascript
复制
void *memset(void *s, int c, size_t n);
  • s:是指向要操作的内存区域的指针。
    • 这个内存区域可以是数组、结构体等任何类型的内存区域。
  • c:是要设置的值。
    • 虽然参数类型为 int,但实际上会被转换为 unsigned char 类型。
    • 因此通常是一个 0 - 255 之间的整数,代表一个字节的值。
  • n:是要填充的字节数。
    • 类型为 size_t,它是一个无符号整数类型,通常用于表示对象的大小。

函数的返回值:

  • memset() :返回指向目标内存区域 s 的指针,这样可以方便进行链式操作。

函数的使用:

1.初始化数组

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
       int arr[5];

       // 将数组初始化为 0
       memset(arr, 0, sizeof(arr)); //或者 memset(arr, 0, 5 * sizeof(int));
	
       printf("数组的元素为: ");
       for (int i = 0; i < 5; i++)
       {
           printf("%d ", arr[i]);
       }
       printf("\n");

       return 0;
}

输出:

代码语言:javascript
复制
数组的元素为: 0 0 0 0 0 

2.初始化字符串

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
       char str[10];
       // 将字符数组 str 的前 9 个字节设置为 'A'
       memset(str, 'A', 9);
       str[9] = '\0';  // 添加字符串结束符
       printf("字符串为:%s\n", str);
       return 0;
}

输出:

代码语言:javascript
复制
字符串为: AAAAAAAAA

3.初始化结构体

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

typedef struct
{
       int id;
       char name[20];
} Person;

int main()
{
       Person p;

       // 将结构体初始化为 0
       memset(&p, 0, sizeof(Person));

       printf("ID: %d\n", p.id);
       printf("Name: %s\n", p.name);

       return 0;
}

输出:

代码语言:javascript
复制
ID: 0
Name: 
memcpy()

函数的介绍: memcpy():用于将一块内存区域的内容复制到另一块内存区域。

  • 它定义在 <string.h> 头文件中。
  • memcpy函数复制是按字节进行的,不关心数据类型。
    • 因此,它可以用于复制任意类型的数据(如数组、结构体等)。
  • memcpy函数不处理内存重叠的情况,当源内存区域和目标内存区域有重叠时,复制结果是未定义的。
    • 因此,如果内存区域可能重叠,应使用 memmove() 函数。

memcpy 函数的作用是从源内存区域 src 开始,拷贝连续的 n 个字节到目标内存区域 dest

  • 它保证拷贝操作是高效且连续的,
  • 它不会检查目标内存区域是否足够大。
  • 它不会检查源和目标内存区域是否重叠。

总结:使用memcpy函数时需确保目标内存足够大,且源和目标内存不重叠。

函数的原型:

代码语言:javascript
复制
void *memcpy(void *dest, const void *src, size_t n);
  • dest: 是指向目标内存区域的指针,即数据要被复制到的地方。
    • 这块内存区域的大小至少要能容纳从源地址复制过来的数据。
  • src:是指向源内存区域的指针,即数据的来源。
  • n:是要复制的字节数。
    • 类型为 size_t(无符号整数类型),它指定了从源内存区域复制到目标内存区域的数据量。

函数的返回值: memcpy():返回指向目标内存区域 dest 的指针,这样可以方便进行链式操作,比如连续调用多个内存操作函数。

函数的使用:

1.复制整型数组

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
       int src[] = { 1, 2, 3, 4, 5 };
       int dest[5];

       // 复制整型数组
       memcpy(dest, src, sizeof(src));

       printf("源数组: ");
       for (int i = 0; i < 5; i++)
       {
           printf("%d ", src[i]);
       }
       printf("\n");

       printf("目标数组: ");
       for (int i = 0; i < 5; i++)
       {
           printf("%d ", dest[i]);
       }
       printf("\n");

       return 0;
}

输出:

代码语言:javascript
复制
源数组: 1 2 3 4 5
目标数组: 1 2 3 4 5

2.复制字符串

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
     char src[] = "Hello, World!";
     char dest[20];
   
     // 复制字符串(包括 '\0')
     memcpy(dest, src, strlen(src) + 1);
   
     printf("源字符串: %s\n", src);
     printf("目标字符串: %s\n", dest);
   
     return 0;
}

输出:

代码语言:javascript
复制
源字符串: Hello, World!
目标字符串: Hello, World!

3.复制结构体

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

typedef struct
{
     int id;
     char name[20];
} Person;

int main()
{
     Person src = { 1, "Alice" };
     Person dest;

     // 复制结构体
     memcpy(&dest, &src, sizeof(Person));
   
     printf("源结构体: ID = %d, Name = %s\n", src.id, src.name);
     printf("目标结构体: ID = %d, Name = %s\n", dest.id, dest.name);
   
     return 0;
}

输出:

代码语言:javascript
复制
源结构体: ID = 1, Name = Alice
目标结构体: ID = 1, Name = Alice
memmove()

函数的介绍: memmove():用于将一块内存的内容复制到另一块内存中。

  • 它定义在 <string.h> 头文件中。
  • 它是按字节复制的,不关心数据的类型。因此,它可以用于复制任意类型的数据(如:数组、结构体等)
  • memcpy() 不同的是,memmove() 能够正确处理源内存区域和目标内存区域重叠的情况。
    • 如果重叠,它会选择一种安全的复制方式(例如:从后向前复制),以确保数据正确

函数的原型:

代码语言:javascript
复制
void *memmove(void *dest, const void *src, size_t n);
  • dest:是指向目标内存区域的指针,即数据要被复制到的位置。
    • 此内存区域需要有足够的空间来存储从源区域复制过来的数据。
  • src:是指向源内存区域的指针,即数据的来源。
  • n:是要复制的字节数。
    • 类型为 size_t(无符号整数类型),它明确了从源内存区域复制到目标内存区域的数据量。

函数的返回值: memmove():返回指向目标内存区域 dest 的指针,这样便于进行链式操作。

  • 例如:连续调用多个内存操作函数。

函数的使用:

内存无重叠情况:

1.复制整型数组

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
     int src[] = { 1, 2, 3, 4, 5 };
     int dest[5];

     // 复制整型数组
     memmove(dest, src, sizeof(src));

     printf("源数组: ");
     for (int i = 0; i < 5; i++)
     {
           printf("%d ", src[i]);
     }
     printf("\n");

     printf("目标数组: ");
     for (int i = 0; i < 5; i++)
     {
           printf("%d ", dest[i]);
     }
     printf("\n");

     return 0;
}

输出:

代码语言:javascript
复制
源数组: 1 2 3 4 5
目标数组: 1 2 3 4 5

2.复制字符串

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
     char src[] = "Hello, World!";
     char dest[20];

     // 复制字符串(包括 '\0')
     memmove(dest, src, strlen(src) + 1);
     //或者这样写:memmove(dest, src, sizeof(src));

     printf("源字符串: %s\n", src);
     printf("目标字符串: %s\n", dest);

     return 0;
}

输出:

代码语言:javascript
复制
源字符串: Hello, World!
目标字符串: Hello, World!

3.复制结构体

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

typedef struct
{
     int id;
     char name[20];
} Person;

int main()
{
     Person src = { 1, "Alice" };
     Person dest;

     // 复制结构体
     memmove(&dest, &src, sizeof(Person));

     printf("源结构体: ID = %d, Name = %s\n", src.id, src.name);
     printf("目标结构体: ID = %d, Name = %s\n", dest.id, dest.name);

     return 0;
}

输出:

代码语言:javascript
复制
源结构体: ID = 1, Name = Alice
目标结构体: ID = 1, Name = Alice

内存无重叠情况:

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
     char str[] = "Hello, World!";

     // 将字符串移动到自身内部
     memmove(str + 7, str, 13);

     printf("结果为: %s\n", str);

     return 0;
}

程序崩溃!!!


错误分析

代码语言:javascript
复制
char str[] = "Hello, World!";
memmove(str + 7, str, 14);   // 将字符串移动到自身内部

这两行代码尝试将 str 字符串的前 13 个字节复制到从 str + 7 开始的位置。

但是:

  1. str 数组的大小是由初始化字符串 "Hello, World!" 决定的,该字符串包含 14个字符(包括字符串结束符 '\0'),数组 str 的大小也是 14 个字节。
  2. 当执行 memmove(str + 7, str, 13); 时,试图从 str 复制 14个字节到从 str + 7 开始的位置,这会导致复制操作超出 str 数组的边界,从而引发未定义行为。
    • 因为复制的数据会覆盖 str 数组之外的内存区域,可能会破坏其他重要的数据。

修正建议:

如果想要实现字符串内部的移动操作,需要确保目标区域有足够的空间来容纳复制的数据。可以将 str 数组的大小扩大,以避免内存越界问题。

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
    // 扩大数组大小以避免越界
    char str[21] = "Hello, World!";

    // 将字符串移动到自身内部
    memmove(str + 7, str, 14);

    printf("结果为: %s\n", str);

    return 0;
}

输出

代码语言:javascript
复制
结果为: Hello, Hello, World!

memmove()memcpy() 的区别:

特性

memcpy()

memmove()

内存重叠

不处理内存重叠,行为未定义

处理内存重叠,保证数据正确

性能

通常更快(假设没有内存重叠)

稍慢(需要额外检查内存重叠)

使用场景

源和目标内存不重叠时使用

源和目标内存可能重叠时使用

memcmp()

函数的介绍: memcmp():用于按字节顺序比较两块内存区域的内容。

  • 它定义在 <string.h> 头文件中。
  • memcmp 函数是按字节比较的,不关心数据的类型。
    • 因此,它可以用于比较任意类型的数据(如数组、结构体等)

memcmp 函数逐字节比较两个内存区域的前 n 个字节,直到找到不相等的字节或比较完所有字节。

  • 比较时,字节被视为无符号字符(unsigned char

函数的原型:

代码语言:javascript
复制
int memcmp(const void *s1, const void *s2, size_t n);
  • s1:是指向第一个要比较的内存区域的指针。
  • s2:是指向第二个要比较的内存区域的指针。
  • n:是要比较的字节数。
    • 类型为 size_t(无符号整数类型),它明确了从两个内存块起始位置开始比较的字节数量。

函数的返回值: s1 所指向的内存块的内容在字典序上 与 s2 所指向的内存块的内容

  • 如果 s1 < s2,返回负值
  • 如果 s1 == s2,返回 0
  • 如果 s1 > s2,返回正值

函数的使用:

1.比较整型数组

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
       int arr1[] = { 1, 2, 3, 4, 5 };
       int arr2[] = { 1, 2, 3, 4, 5 };
       int arr3[] = { 1, 2, 3, 4, 6 };

       // 比较整型数组
       int result1 = memcmp(arr1, arr2, sizeof(arr1));  // 比较整个数组
       int result2 = memcmp(arr1, arr3, sizeof(arr1));  // 比较整个数组

       printf("Result1: %d\n", result1);  // 输出 0(相等)
       printf("Result2: %d\n", result2);  // 输出负值(arr1 < arr3)

       return 0;
}

输出:

代码语言:javascript
复制
Result1: 0
Result2: -1

2.比较字符串

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

int main()
{
       char str1[] = "Hello";
       char str2[] = "Hello";
       char str3[] = "World";

       // 比较字符串
       int result1 = memcmp(str1, str2, 5);  // 比较前5个字节
       int result2 = memcmp(str1, str3, 5);  // 比较前5个字节

       printf("Result1: %d\n", result1);  // 输出 0(相等)
       printf("Result2: %d\n", result2);  // 输出负值(str1 < str3)

       return 0;
}

输出:

代码语言:javascript
复制
Result1: 0
Result2: -1

3.比较结构体

代码语言:javascript
复制
#include <stdio.h>
#include <string.h>

typedef struct
{
       int id;
       char name[20];
} Person;

int main()
{
       Person p1 = { 1, "Alice" };
       Person p2 = { 1, "Alice" };
       Person p3 = { 2, "Bob" };

       // 比较结构体
       int result1 = memcmp(&p1, &p2, sizeof(Person));  // 比较整个结构体
       int result2 = memcmp(&p1, &p3, sizeof(Person));  // 比较整个结构体

       printf("Result1: %d\n", result1);  // 输出 0(相等)
       printf("Result2: %d\n", result2);  // 输出负值(p1 < p3)

       return 0;
}

输出:

代码语言:javascript
复制
Result1: 0
Result2: -1

堆区内存分配和释放

malloc()

函数的介绍: malloc():用于在运行时从堆(heap)中申请一块指定大小的内存空间。

  • 定义在 <stdlib.h> 头文件中。

注意事项

  1. 分配内存malloc() 会根据传入的大小分配一块连续的内存区域。
  2. 返回指针:返回的指针类型是 void*,需要根据实际用途进行类型转换。
  3. 内存未初始化malloc() 分配的内存不会自动初始化,内容可能是随机的。(一般使用memset初始化)
  4. 检查返回值:分配失败时返回 NULL,使用前应检查指针是否为 NULL

函数的原型:

代码语言:javascript
复制
void* malloc(size_t size);
  • size:要分配的内存块的大小。
    • 单位是字节,类型为 size_t(无符号整数类型),你需要根据实际需求指定所需内存的字节数。

函数的返回值:

  • 如果内存分配成功:函数会返回一个指向新分配内存块起始位置的指针。
    • 该指针的类型为 void *,意味着可以将其转换为任意类型的指针。
  • 如果内存分配失败:函数会返回空指针。
    • 使用前应检查指针是否为 NULL

函数的使用:

1.分配整数数组

代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> 
#include <string.h>

int main()
{
	int count, *array, n;
	printf("请输入要申请数组的个数:\n");
	scanf("%d", &n);

	array = (int*)malloc(n * sizeof(int));
	if (array == NULL)
	{
		printf("申请空间失败!\n");
		return -1;
	}

	//将申请到空间清0
	memset(array, 0, sizeof(int) * n);

	for (count = 0; count < n; count++) /*给数组赋值*/
	{
		array[count] = count;
	}
	for (count = 0; count < n; count++) /*打印数组元素*/
	{
		printf("%2d", array[count]);
	}


	free(array);
	array = NULL;
	return 0;
}

2.分配结构体

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

typedef struct
{
       int id;
       char name[20];
} Person;

int main()
{
       // 分配能存储一个 Person 结构体的内存空间
       Person* p = (Person*)malloc(sizeof(Person));
       if (p == NULL)
       {
           printf("内存分配失败\n");
           return 1;
       }

       // 初始化结构体成员
       p->id = 1;
       strcpy(p->name, "Alice");

       // 输出结构体信息
       printf("ID: %d, Name: %s\n", p->id, p->name);

       // 释放分配的内存
       free(p);
       // 将指针置为 NULL,避免悬空指针
       p = NULL;
	
       return 0;
}
free()

函数的介绍: free():用于释放动态分配的内存,防止内存泄漏。

  • 定义在 <stdlib.h> 头文件中。

free() 函数的主要功能是将之前动态分配的内存块归还给操作系统,使得这部分内存可以被后续的内存分配请求再次使用。 释放内存可以避免内存泄漏,即:程序占用了不再使用的内存,导致系统可用内存逐渐减少。

函数的原型:

代码语言:javascript
复制
void free(void* ptr);
  • ptr:是指向需要释放的内存块的指针。
    • 这个指针必须是由 malloc()calloc()realloc() 返回的指针。
    • 如果: ptrNULL,则函数不执行任何操作。

函数的使用:

注意事项:

避免野指针:释放内存后,指针仍然指向原来的地址,但该地址已无效。如果不将指针置为 NULL,可能会导致野指针问题。

代码语言:javascript
复制
free(ptr);
ptr = NULL; // 推荐做法

不能释放栈内存free() 只能释放堆内存,不能释放栈上的变量。

代码语言:javascript
复制
int x = 10;
free(&x); // 错误!x 是栈上的变量

不能重复释放:对同一块内存多次调用 free() 会导致程序崩溃。

代码语言:javascript
复制
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // 错误!重复释放

释放空指针是安全的:如果指针是 NULLfree() 不会执行任何操作。

代码语言:javascript
复制
int *ptr = NULL;
free(ptr); // 安全,不会报错

内存泄漏:如果动态分配的内存没有通过 free() 释放,会导致内存泄漏。

代码语言:javascript
复制
int *ptr = (int*)malloc(sizeof(int));
// 忘记调用 free(ptr);

内存分区代码分析

返回栈区地址

代码语言:javascript
复制
#include <stdio.h>
int *fun()
{
	int a = 10;
	return &a;//函数调用完毕,a释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	*p = 100; //操作野指针指向的内存,err

	return 0;
}

返回data区地址

代码语言:javascript
复制
#include <stdio.h>

int *fun()
{
	static int a = 10;
	return &a; //函数调用完毕,a不释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	*p = 100; //ok
	printf("*p = %d\n", *p);

	return 0;
}

值传递示例1:被调函数中进行动态分配

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

void fun(int *tmp)
{
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	fun(p);   //值传递,形参修改不会影响实参
	printf("*p = %d\n", *p);//err,操作空指针指向的内存

	return 0;
}

这段代码的目的是通过函数 fun 动态分配内存并修改指针 p 指向的值。

但由于函数参数传递的方式问题,代码中存在错误。

程序崩溃!!!

(程序崩溃的主要原因是我们对空指针进行解引用的操作)

代码示例

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

void fun(int* tmp)
{
	tmp = (int*)malloc(sizeof(int));
	*tmp = 100;
}

int main(int argc, char* argv[])
{
	int* p = NULL;
	int a = 10;
	p = &a;
	fun(p);   //值传递,形参修改不会影响实参
	printf("*p = %d\n", *p);//err,操作空指针指向的内存

	return 0;
}

输出

代码语言:javascript
复制
*p = 10

失败分析:(为什么fun函数不能修改p指向的值)

  • 值传递问题
    • 在 C 语言中,函数参数是按值传递的。这意味着 fun(p) 传递的是指针 p 的值(即: NULL),而不是指针 p 本身。
    • fun 函数内部,tmp 是一个局部变量,它的修改不会影响 main 函数中的 p
    • 因此,pmain 函数中仍然是 NULL
  • 空指针解引用
    • main 函数中,p 仍然是 NULL,而 printf("*p = %d\n", *p); 试图解引用空指针,这是未定义行为,通常会导致程序崩溃。

修正方法:

方法 1:传递指针的指针

  • p 的地址传递给 fun 函数,从而在 fun 中修改 p 的值。
代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

void fun(int** tmp)
{
    *tmp = (int*)malloc(sizeof(int));  //---> p =
    **tmp = 100;   //---> *p = 
}

int main(int argc, char* argv[])
{
    int* p = NULL;
    fun(&p); // 传递 p 的地址
    printf("*p = %d\n", *p); // 正确输出 100

    // 释放动态分配的内存
    free(p);

    return 0;
}

输出

代码语言:javascript
复制
*p = 100

方法 2:返回动态分配的指针

  • fun 函数返回动态分配的指针,并在 main 函数中接收返回值。
代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int* fun()
{
    int* tmp = (int*)malloc(sizeof(int));
    *tmp = 100;
    return tmp;
}

int main(int argc, char* argv[])
{
    int* p = NULL;
    p = fun(); // 接收返回值
    printf("*p = %d\n", *p); // 正确输出 100

    // 释放动态分配的内存
    free(p);

    return 0;
}

输出

代码语言:javascript
复制
*p = 100

总结

代码中的问题是由于函数参数按值传递导致的,fun 函数内部的修改不会影响 main 函数中的 p

修正方法有两种:

  1. 传递指针的指针(int **tmp),在 fun 中修改 p 的值。
  2. fun 函数返回动态分配的指针,并在 main 中接收返回值。

值传递示例2:主调函数中进行动态分配

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

void fun(int *tmp)
{
    *tmp = 100;
}

int main(int argc, char *argv[])
{
    int *p = NULL;
    p = (int *)malloc(sizeof(int));

    fun(p); // 值传递
    printf("*p = %d\n", *p); // ok,*p为100

    // 释放动态分配的内存
    free(p);

    return 0;
}

这段代码的目的是通过函数 fun 修改指针 p 指向的值。

与之前的代码不同,这段代码是正确的,并且能够正常运行。

输出

代码语言:javascript
复制
*p = 100
关于值传递的提问?

疑问:上面的两个代码示例都是值传递,但是为什么一个fun函数可以修改成功,而另一个不可以修改的情况呢?

失败原因分析:

  1. 值传递机制:函数调用时会将实参的值复制一份给形参。在 main 函数中调用 fun(p) 时,p 的值(即:变量 a 的地址)被复制给了形参 tmp。此时,p 和 tmp 都指向变量 a
  2. fun函数内部操作:在 fun 函数中,tmp = (int*)malloc(sizeof(int)); 这行代码将 tmp 指向了新分配的一块动态内存,而不再指向变量 a。然后 *tmp = 100; 是将新分配内存中的值设置为 100
  3. 对实参的影响:由于是值传递,tmp 的改变不会影响 pp 仍然指向变量 a。所以 printf("*p = %d\n", *p); 输出的是变量 a 的值,而不是新分配内存中的值

成功原因分析:

  1. 值传递机制:同样是值传递,在 main 函数中调用 fun(p) 时,p 的值(即:动态分配内存的地址)被复制给了形参 tmp。此时,p 和 tmp 都指向同一块动态分配的内存
  2. fun函数内部操作:在 fun 函数中,*tmp = 100; 是对 tmp 所指向的内存中的值进行修改,也就是修改了动态分配内存中的值。
  3. 对实参的影响:虽然 tmpp 的副本,但它们指向同一块内存。所以对 *tmp 的修改实际上就是对 *p 的修改。因此,printf("*p = %d\n", *p); 输出的是修改后的值 100。

总结: 第一个代码中 fun 函数修改的是形参 tmp 本身的指向,而不是 tmp 所指向的内存中的值,由于值传递机制,形参的改变不会影响实参 第二个代码中 fun 函数修改的是形参 tmp 所指向的内存中的值,因为 ptmp 指向同一块内存,所以修改操作会反映到 p 所指向的内存中

返回堆区地址

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

int* fun()
{
	int* tmp = NULL;
	tmp = (int*)malloc(sizeof(int));
	*tmp = 100;
	return tmp;//返回堆区地址,函数调用完毕,不释放
}

int main(int argc, char* argv[])
{
	int* p = NULL;
	p = fun();
	*p = 200;  //ok
	printf("*p = %d\n", *p);

	//堆区空间,使用完毕,手动释放
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}

	return 0;
}

👨‍💻 博主正在持续更新C语言基础系列中。 ❤️ 如果你觉得内容还不错,请多多点赞。 ⭐️ 如果你觉得对你有帮助,请多多收藏。(防止以后找不到了) 👨‍👩‍👧‍👦 C语言基础系列 持续更新中~,后续分享内容主要涉及 C++全栈开发 的知识,如果你感兴趣请多多关注博主。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 存储类型
    • 作用域
    • 普通局部变量
    • 静态局部变量
    • 普通全局变量
    • 静态全局变量
    • 全局函数和静态函数
  • 内存布局
    • 内存分区
    • 存储类型与内存四区
    • 内存操作函数
      • memset()
      • memcpy()
      • memmove()
      • memcmp()
    • 堆区内存分配和释放
      • malloc()
      • free()
  • 内存分区代码分析
    • 返回栈区地址
    • 返回data区地址
    • 值传递示例1:被调函数中进行动态分配
    • 值传递示例2:主调函数中进行动态分配
      • 关于值传递的提问?
    • 返回堆区地址
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档