二级网站怎样被百度收录澳门seo推广
C语言基础之【内存管理】
- 存储类型
- 作用域
- 普通局部变量
- 静态局部变量
- 普通全局变量
- 静态全局变量
- 全局函数和静态函数
- 内存布局
- 内存分区
- 存储类型与内存四区
- 内存操作函数
- memset()
- memcpy()
- memmove()
- memcmp()
- 堆区内存分配和释放
- malloc()
- free()
- 内存分区代码分析
- 返回栈区地址
- 返回data区地址
- 值传递示例1:被调函数中进行动态分配
- 值传递示例2:主调函数中进行动态分配
- 关于值传递的提问?
- 返回堆区地址
往期《C语言基础系列》回顾:
链接:
C语言基础之【C语言概述】
C语言基础之【数据类型】(上)
C语言基础之【数据类型】(下)
C语言基础之【运算符与表达式】
C语言基础之【程序流程结构】
C语言基础之【数组和字符串】(上)
C语言基础之【数组和字符串】(下)
C语言基础之【函数】
C语言基础之【指针】(上)
C语言基础之【指针】(中)
C语言基础之【指针】(下)
存储类型
存储类型(Storage Class)
:用于定义变量或函数的存储位置
、生命周期
、作用域
以及链接属性
C语言提供了四种存储类型关键字:
auto
、register
、static
和extern
存储类型总结:
存储类型 | 作用域 | 生命周期 | 存储位置 | 特点 |
---|---|---|---|---|
auto | 局部作用域 | 代码块内 | 栈区 | 默认存储类型,通常省略 |
register | 局部作用域 | 代码块内 | 寄存器或栈区 | 建议存储在寄存器中,提高访问速度 |
static | 局部或文件作用域 | 整个程序运行期间 | 全局区/静态区 | 静态局部变量保留值,静态全局变量隐藏 |
extern | 整个程序 | 整个程序运行期间 | 全局区/静态区 | 引用其他文件中定义的全局变量或函数 |
作用域
C语言变量的作用域分为:
代码块作用域
函数作用域
文件作用域
普通局部变量
普通局部变量
:是在函数或代码块内部定义的变量,其作用域仅限于定义它的函数或代码块。
局部变量的默认存储类型是
auto
局部变量
也叫auto自动变量
(auto可写可不写)
普通局部变量的特点:
作用域
:
- 局部变量的作用域仅限于定义它的函数或代码块。
- 在函数或代码块外部无法访问。
生命周期
:
- 局部变量的生命周期从函数或代码块开始执行时创建,到函数或代码块结束时销毁。
- 每次函数调用时,局部变量都会重新初始化。
存储位置
:
- 局部变量通常存储在
栈(stack)
中。
默认值
:
- 局部变量不会自动初始化,其初始值是未定义的(垃圾值)
代码示例:普通局部变量
#include <stdio.h>void func() {int x = 10; // 局部变量printf("x = %d\n", x);x++; }int main() {func(); // 输出: x = 10func(); // 输出: x = 10return 0; }
- 在每次调用
func()
时,局部变量x
都会重新创建并初始化为10
静态局部变量
静态局部变量
:是在函数内部定义的变量,使用static
关键字修饰。
类似于普通局部变量,区别在于其使用了
static
关键字修饰。它的作用域仍然是局部的,但其
生命周期
和存储位置
与普通局部变量不同。
静态局部变量的特点:
作用域
:
- 静态局部变量的作用域仅限于定义它的函数或代码块。
- 在函数或代码块外部无法访问。
生命周期
:
- 静态局部变量的生命周期从程序开始运行时创建,到程序结束时销毁。
- 即使函数调用结束,静态局部变量的值也会保留,下次调用时不会重新初始化。
存储位置
:
- 静态局部变量存储在
静态存储区(static storage area)
,而不是栈中。
默认值
:
- 静态局部变量若未赋以初值,则由系统自动赋值:
- 数值型变量自动赋初值
0
- 字符型变量自动赋初值
空字符
代码示例:静态局部变量
#include <stdio.h>void func() {static int x = 10; // 静态局部变量printf("x = %d\n", x);x++; }int main() {func(); // 输出: x = 10func(); // 输出: x = 11func(); // 输出: x = 12return 0; }
- 在第一次调用
func()
时,静态局部变量x
初始化为10
- 之后的每次调用
func()
时,x
的值会保留并递增
普通局部变量与静态局部变量的对比:
特性 | 普通局部变量 | 静态局部变量 |
---|---|---|
作用域 | 仅限于定义它的函数或代码块 | 仅限于定义它的函数或代码块 |
生命周期 | 函数或代码块执行期间 | 整个程序运行期间 |
存储位置 | 栈(stack) | 静态存储区(static storage area) |
初始化 | 未初始化时为垃圾值 | 自动初始化为 0 |
多次调用时的行为 | 每次调用重新初始化 | 保留上次调用的值 |
普通全局变量
普通全局变量
:是在所有函数之外定义的变量,通常位于文件的顶部。
普通全局变量的特点:
作用域
:从定义点开始,直到文件结束。
生命周期
:程序启动时创建,程序结束时销毁。
链接性
:具有外部链接性
,可在其他文件中通过extern
声明访问。// file1.c int globalVar = 10; // 全局变量// file2.c extern int globalVar; // 声明全局变量,定义在file1.c中 //声明一个变量globalVar,这个全局变量在别的文件中已经定义了,这里只是声明,而不是定义
extern关键字
:用于声明一个在其他文件中定义的全局变量。
extern
声明不会分配内存,只是引用已经存在的全局变量。代码示例:普通全局变量
#include <stdio.h>int globalVar = 10; // 全局变量void func() {printf("全局变量: %d\n", globalVar); }int main() {func();globalVar = 20; // 修改全局变量func();return 0; }
- 全局变量在整个程序中都是可见的,可以被程序中的任何函数访问和修改。
输出:
全局变量: 10 全局变量: 20
静态全局变量
静态全局变量
:是指在函数外部使用static
关键字声明的变量。
静态全局变量的特点:
作用域
:仅限于定义它的文件,其他文件无法访问该变量。
生命周期
:程序启动时创建,程序结束时销毁。
链接性
:具有内部链接性
,无法在其他文件中访问。// file1.c static int staticGlobalVar = 20; // static全局变量,仅在file1.c中可见// file2.c extern int staticGlobalVar; // 错误,无法访问file1.c中的static全局变量
代码示例:静态全局变量
#include <stdio.h>static int staticGlobalVar = 30; // 静态全局变量void func() {printf("static全局变量: %d\n", staticGlobalVar); }int main() {func();staticGlobalVar = 40; // 修改静态全局变量func();return 0; }
输出:
static全局变量: 30 static全局变量: 40
普通全局变量和静态全局变量对比总结:
特性 | 普通全局变量 | 静态全局变量 |
---|---|---|
作用域 | 文件内全局 | 文件内全局 |
生命周期 | 程序运行期间 | 程序运行期间 |
链接性 | 外部链接 | 内部链接 |
跨文件访问 | 可以 | 不可以 |
全局函数和静态函数
全局函数
:是在所有函数外部定义的函数。(默认的函数类型)
全局函数的特点:
全局可见性
:全局函数具有全局的作用域。
- 这使得不同源文件中的代码可以方便地复用该函数的功能。
外部链接属性
:全局函数具有外部链接属性。
- 这意味着在一个源文件中定义的全局函数可以在其他源文件中通过函数声明来调用。
代码示例:全局函数
//file1.c #include <stdio.h>// 全局函数的定义 int subtract(int a, int b) {return a - b; }
//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); //输出:两数相减的结果: 5return 0; }
如果需要在其他文件中调用全局函数,需要在调用文件中声明该函数
通常使用
extern
关键字,但extern
可以省略,因为函数声明默认具有外部链接属性
extern关键字
:用于声明一个在其他文件中定义的全局函数。
- 它告诉编译器该函数的定义在别处,通常在另一个源文件中。
extern
声明不会定义函数,只是引用已经存在的全局函数。
静态函数
:是使用static
关键字修饰的函数,其定义形式和普通函数类似,只是在函数返回类型前加上static
关键字。
静态函数的特点:
局部可见性
:静态函数的作用域仅限于定义它的源文件。
- 在一个源文件中定义的静态函数,在其他源文件中无法直接调用。
内部链接属性
:静态函数具有内部链接属性。
- 这意味着它只能在定义它的源文件中被调用,其他源文件无法访问该函数。
代码示例:静态函数
// file1.c #include <stdio.h>// 静态函数定义 static void staticFunc() {printf("这是 file1.c 中的静态函数\n"); }//全局函数定义 void publicFunc() {printf("从同一个文件中调用静态函数:\n");staticFunc(); // 静态函数只能在当前文件中调用 }
// file2.c // 声明全局函数 void publicFunc();int main() {publicFunc(); // 调用全局函数// staticFunc(); // 错误:静态函数无法在其他文件中访问return 0; }
输出:
从同一个文件中调用静态函数:
这是 file1.c 中的静态函数
全局函数和静态函数的对比:
特性 | 全局函数 | 静态函数 |
---|---|---|
作用域 | 整个程序 | 定义它的文件 |
链接属性 | 外部链接(external linkage) | 内部链接(internal linkage) |
可见性 | 所有文件可见 | 仅定义文件可见 |
关键字 | 无(默认) | static |
用途 | 提供公共接口 | 隐藏实现细节,模块化设计 |
内存布局
内存分区
在 Windows 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知:
程序没有运行时
,也就是说程序没有加载到内存时,可执行程序内部已经分好3段信息分别为:
代码区(text)
、数据区(data)
和未初始化数据区(bss)
(有些人直接把
data
和bss
合起来叫做全局区
或静态区
)
程序没有运行时
,代码区 和 全局区( data 和 bss ) 的大小就是固定的,程序运行期间不能改变。然后,
程序运行时
,系统把程序加载到内存,内存上除了有根据可执行程序的信息分出代码区(text)
、数据区(data)
和未初始化数据区(bss)
之外,还额外增加了栈区
、堆区
在C语言中,程序运行时使用的内存通常分为以下几个
分区
(也称为内存段
)C语言程序的内存通常分为以下四个主要区域:
- 代码区(Text Segment)
- 全局区/静态区(Data Segment)
初始化
的全局变量和静态变量(Data 段)未初始化
的全局变量和静态变量(Bss段)- 栈区(Stack Segment)
- 堆区(Heap Segment)
代码区(Text Segment)
存放内容:程序的机器指令(即:编译后的二进制代码)
特点:
- 由操作系统管理。
- 在程序运行期间,代码区的大小是固定的。
- 只读,不可修改。
示例:函数体的代码等。
全局区/静态区(Data Segment)
全局区/静态区分为两部分:
初始化的全局变量和静态变量(初始化区)
存放内容:显式初始化的全局变量和静态变量。
特点:
- 在程序启动时分配内存,并初始化为指定的值。
- 生命周期为整个程序运行期间。
示例:
int globalVar = 10; // 初始化的全局变量 static int staticVar = 20; // 初始化的静态变量
未初始化的全局变量和静态变量(BSS区)
存放内容:未显式初始化的全局变量和静态变量。
特点:
- 在程序启动时分配内存,并初始化为0(或空指针)
- 生命周期为整个程序运行期间。
示例:
int globalVar; // 未初始化的全局变量 static int staticVar; // 未初始化的静态变量
栈区(Stack Segment)
存放内容:
- 局部变量(包括函数参数)
- 函数调用的上下文(如:返回地址、寄存器状态等)
特点:
- 由编译器自动管理。
- 栈区的大小有限,通常较小(默认几MB)
- 内存分配和释放遵循“后进先出”(LIFO)原则。
- 栈区的分配和释放速度快。
示例:
void func() {int localVar = 30; // 局部变量,存放在栈区printf("%d\n", localVar); }
堆区(Heap Segment)
存放内容:动态分配的内存(通过
malloc
、calloc
、realloc
等函数分配)特点:
- 由程序员手动管理(分配和释放)
- 堆区的大小通常较大,受系统可用内存限制。
- 需要显式释放内存(通过
free
函数),否则会导致内存泄漏。- 堆区分配和释放速度较慢。
示例:
int *ptr = (int *)malloc(sizeof(int) * 10); // 动态分配内存,存放在堆区if (ptr != NULL) {free(ptr); // 释放内存 }
内存分区示意图:
+-----------------------+ | 栈区 (Stack) | <- 高地址 |-----------------------| | ↓ | | | | ↑ | |-----------------------| | 堆区 (Heap) | |-----------------------| | 未初始化数据区 (BSS) | |-----------------------| | 初始化数据区 (Data) | |-----------------------| | 代码区 (Text) | <- 低地址 +-----------------------+
内存四区的比较:
分区 | 存放内容 | 管理方式 | 生命周期 | 特点 |
---|---|---|---|---|
代码区 | 程序指令 常量字符串 | 操作系统 | 整个程序运行期间 | 只读、大小固定 |
全局区/静态区 | 初始化和未初始化的全局变量/静态变量 | 编译器 | 整个程序运行期间 | 初始化区存放显式初始化的变量 BSS区存放未初始化的变量 |
栈区 | 局部变量 函数调用上下文 | 编译器 | 函数调用期间 | 自动管理、大小有限 |
堆区 | 动态分配的内存 | 程序员 | 直到显式释放 | 手动管理、大小较大 |
存储类型与内存四区
代码示例:
#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; }
输出:
局部变量: 30 静态局部变量: 40 堆变量: 50
各存储类型的变量或函数的总结:
类型 | 存储位置 |
---|---|
普通局部变量 | 栈区 |
静态局部变量 | 初始化在data段,未初始化在BSS段 |
普通全局变量 | 初始化在data段,未初始化在BSS段 |
静态全局变量 | 初始化在data段,未初始化在BSS段 |
全局函数 | 代码区 |
静态函数 | 代码区 |
字符串常量 | data段 |
寄存器变量 | 运行时存储在CPU寄存器 |
代码示例:各存储类型的变量所在内存分区的地址高低
#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;
}
输出:
&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
个字节,否则会导致内存越界。- 必须确保填充值
c
在unsigned char
范围内。
函数的原型:
void *memset(void *s, int c, size_t n);
s
:是指向要操作的内存区域的指针。
- 这个内存区域可以是数组、结构体等任何类型的内存区域。
c
:是要设置的值。
- 虽然参数类型为
int
,但实际上会被转换为unsigned char
类型。- 因此通常是一个 0 - 255 之间的整数,代表一个字节的值。
n
:是要填充的字节数。
- 类型为
size_t
,它是一个无符号整数类型,通常用于表示对象的大小。
函数的返回值:
memset()
:返回指向目标内存区域s
的指针,这样可以方便进行链式操作。
函数的使用:
1.初始化数组
#include <stdio.h> #include <string.h>int main() {int arr[5];// 将数组初始化为 0memset(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; }
输出:
数组的元素为: 0 0 0 0 0
2.初始化字符串
#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; }
输出:
字符串为: AAAAAAAAA
3.初始化结构体
#include <stdio.h> #include <string.h>typedef struct {int id;char name[20]; } Person;int main() {Person p;// 将结构体初始化为 0memset(&p, 0, sizeof(Person));printf("ID: %d\n", p.id);printf("Name: %s\n", p.name);return 0; }
输出:
ID: 0 Name:
memcpy()
函数的介绍:
memcpy()
:用于将一块内存区域的内容复制到另一块内存区域。
它定义在
<string.h>
头文件中。memcpy函数复制是按字节进行的,不关心数据类型。
- 因此,它可以用于复制任意类型的数据(如数组、结构体等)。
memcpy函数不处理内存重叠的情况,当源内存区域和目标内存区域有重叠时,复制结果是未定义的。
- 因此,如果内存区域可能重叠,应使用
memmove()
函数。
memcpy
函数的作用是从源内存区域src
开始,拷贝连续的n
个字节到目标内存区域dest
- 它保证拷贝操作是高效且连续的,
- 它不会检查
目标内存区域
是否足够大。- 它不会检查
源和目标内存区域
是否重叠。总结:使用
memcpy
函数时需确保目标内存足够大,且源和目标内存不重叠。
函数的原型:
void *memcpy(void *dest, const void *src, size_t n);
dest
: 是指向目标内存区域的指针,即数据要被复制到的地方。
- 这块内存区域的大小至少要能容纳从源地址复制过来的数据。
src
:是指向源内存区域的指针,即数据的来源。n
:是要复制的字节数。
- 类型为
size_t
(无符号整数类型),它指定了从源内存区域复制到目标内存区域的数据量。
函数的返回值:
memcpy()
:返回指向目标内存区域dest
的指针,这样可以方便进行链式操作,比如连续调用多个内存操作函数。
函数的使用:
1.复制整型数组
#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; }
输出:
源数组: 1 2 3 4 5 目标数组: 1 2 3 4 5
2.复制字符串
#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; }
输出:
源字符串: Hello, World! 目标字符串: Hello, World!
3.复制结构体
#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; }
输出:
源结构体: ID = 1, Name = Alice 目标结构体: ID = 1, Name = Alice
memmove()
函数的介绍:
memmove()
:用于将一块内存的内容复制到另一块内存中。
- 它定义在
<string.h>
头文件中。- 它是按字节复制的,不关心数据的类型。因此,它可以用于复制任意类型的数据(如:数组、结构体等)
- 与
memcpy()
不同的是,memmove()
能够正确处理源内存区域和目标内存区域重叠的情况。
- 如果重叠,它会选择一种安全的复制方式(例如:从后向前复制),以确保数据正确
函数的原型:
void *memmove(void *dest, const void *src, size_t n);
dest
:是指向目标内存区域的指针,即数据要被复制到的位置。
- 此内存区域需要有足够的空间来存储从源区域复制过来的数据。
src
:是指向源内存区域的指针,即数据的来源。n
:是要复制的字节数。
- 类型为
size_t
(无符号整数类型),它明确了从源内存区域复制到目标内存区域的数据量。
函数的返回值:
memmove()
:返回指向目标内存区域dest
的指针,这样便于进行链式操作。
- 例如:连续调用多个内存操作函数。
函数的使用:
内存无重叠情况:
1.复制整型数组
#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; }
输出:
源数组: 1 2 3 4 5 目标数组: 1 2 3 4 5
2.复制字符串
#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; }
输出:
源字符串: Hello, World! 目标字符串: Hello, World!
3.复制结构体
#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; }
输出:
源结构体: ID = 1, Name = Alice 目标结构体: ID = 1, Name = Alice
内存无重叠情况:
#include <stdio.h> #include <string.h>int main() {char str[] = "Hello, World!";// 将字符串移动到自身内部memmove(str + 7, str, 13);printf("结果为: %s\n", str);return 0; }
程序崩溃!!!
错误分析:
char str[] = "Hello, World!"; memmove(str + 7, str, 14); // 将字符串移动到自身内部
这两行代码尝试将
str
字符串的前 13 个字节复制到从str + 7
开始的位置。但是:
str
数组的大小是由初始化字符串"Hello, World!"
决定的,该字符串包含 14个字符(包括字符串结束符'\0'
),数组str
的大小也是 14 个字节。当执行
memmove(str + 7, str, 13);
时,试图从str
复制 14个字节到从str + 7
开始的位置,这会导致复制操作超出str
数组的边界,从而引发未定义行为。
- 因为复制的数据会覆盖
str
数组之外的内存区域,可能会破坏其他重要的数据。
修正建议:
如果想要实现字符串内部的移动操作,需要确保目标区域有足够的空间来容纳复制的数据。可以将
str
数组的大小扩大,以避免内存越界问题。#include <stdio.h> #include <string.h>int main() {// 扩大数组大小以避免越界char str[21] = "Hello, World!";// 将字符串移动到自身内部memmove(str + 7, str, 14);printf("结果为: %s\n", str);return 0; }
输出:
结果为: Hello, Hello, World!
memmove()
与 memcpy()
的区别:
特性 | memcpy() | memmove() |
---|---|---|
内存重叠 | 不处理内存重叠,行为未定义 | 处理内存重叠,保证数据正确 |
性能 | 通常更快(假设没有内存重叠) | 稍慢(需要额外检查内存重叠) |
使用场景 | 源和目标内存不重叠时使用 | 源和目标内存可能重叠时使用 |
memcmp()
函数的介绍:
memcmp()
:用于按字节顺序比较两块内存区域的内容。
- 它定义在
<string.h>
头文件中。memcmp
函数是按字节比较的,不关心数据的类型。
- 因此,它可以用于比较任意类型的数据(如数组、结构体等)
memcmp
函数逐字节比较两个内存区域的前n
个字节,直到找到不相等的字节或比较完所有字节。
- 比较时,字节被视为无符号字符(
unsigned char
)
函数的原型:
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.比较整型数组
#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; }
输出:
Result1: 0 Result2: -1
2.比较字符串
#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; }
输出:
Result1: 0 Result2: -1
3.比较结构体
#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; }
输出:
Result1: 0 Result2: -1
堆区内存分配和释放
malloc()
函数的介绍:
malloc()
:用于在运行时从堆(heap)中申请一块指定大小的内存空间。
- 定义在
<stdlib.h>
头文件中。
注意事项:
- 分配内存:
malloc()
会根据传入的大小分配一块连续的内存区域。- 返回指针:返回的指针类型是
void*
,需要根据实际用途进行类型转换。- 内存未初始化:
malloc()
分配的内存不会自动初始化,内容可能是随机的。(一般使用memset初始化)- 检查返回值:分配失败时返回
NULL
,使用前应检查指针是否为NULL
。
函数的原型:
void* malloc(size_t size);
size
:要分配的内存块的大小。
- 单位是字节,类型为
size_t
(无符号整数类型),你需要根据实际需求指定所需内存的字节数。
函数的返回值:
- 如果内存分配成功:函数会返回一个指向新分配内存块起始位置的指针。
- 该指针的类型为
void *
,意味着可以将其转换为任意类型的指针。- 如果内存分配失败:函数会返回空指针。
- 使用前应检查指针是否为
NULL
函数的使用:
1.分配整数数组
#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;}//将申请到空间清0memset(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.分配结构体
#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()
函数的主要功能是将之前动态分配的内存块归还给操作系统,使得这部分内存可以被后续的内存分配请求再次使用。
释放内存
可以避免内存泄漏
,即:程序占用了不再使用的内存,导致系统可用内存逐渐减少。
函数的原型:
void free(void* ptr);
ptr
:是指向需要释放的内存块的指针。
- 这个指针必须是由
malloc()
、calloc()
或realloc()
返回的指针。- 如果:
ptr
为NULL
,则函数不执行任何操作。
函数的使用:
注意事项:
避免野指针:释放内存后,指针仍然指向原来的地址,但该地址已无效。如果不将指针置为
NULL
,可能会导致野指针问题。free(ptr); ptr = NULL; // 推荐做法
不能释放栈内存:
free()
只能释放堆内存,不能释放栈上的变量。int x = 10; free(&x); // 错误!x 是栈上的变量
不能重复释放:对同一块内存多次调用
free()
会导致程序崩溃。int *ptr = (int*)malloc(sizeof(int)); free(ptr); free(ptr); // 错误!重复释放
释放空指针是安全的:如果指针是
NULL
,free()
不会执行任何操作。int *ptr = NULL; free(ptr); // 安全,不会报错
内存泄漏:如果动态分配的内存没有通过
free()
释放,会导致内存泄漏。int *ptr = (int*)malloc(sizeof(int)); // 忘记调用 free(ptr);
内存分区代码分析
返回栈区地址
#include <stdio.h>
int *fun()
{int a = 10;return &a;//函数调用完毕,a释放
}int main(int argc, char *argv[])
{int *p = NULL;p = fun();*p = 100; //操作野指针指向的内存,errreturn 0;
}
返回data区地址
#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; //okprintf("*p = %d\n", *p);return 0;
}
值传递示例1:被调函数中进行动态分配
#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
指向的值。但由于函数参数传递的方式问题,代码中存在错误。
程序崩溃!!!
(程序崩溃的主要原因是我们对空指针进行解引用的操作)
代码示例:
#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; }
输出:
*p = 10
失败分析:(为什么fun函数不能修改p指向的值)
值传递问题:
- 在 C 语言中,函数参数是按值传递的。这意味着
fun(p)
传递的是指针p
的值(即:NULL
),而不是指针p
本身。- 在
fun
函数内部,tmp
是一个局部变量,它的修改不会影响main
函数中的p
- 因此,
p
在main
函数中仍然是NULL
空指针解引用:
- 在
main
函数中,p
仍然是NULL
,而printf("*p = %d\n", *p);
试图解引用空指针,这是未定义行为,通常会导致程序崩溃。
修正方法:
方法 1:
传递指针的指针
- 将
p
的地址传递给fun
函数,从而在fun
中修改p
的值。#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; }
输出:
*p = 100
方法 2:
返回动态分配的指针
- 让
fun
函数返回动态分配的指针,并在main
函数中接收返回值。#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; }
输出:
*p = 100
总结:
代码中的问题是由于函数参数按值传递导致的,
fun
函数内部的修改不会影响main
函数中的p
修正方法有两种:
- 传递指针的指针(
int **tmp
),在fun
中修改p
的值。- 让
fun
函数返回动态分配的指针,并在main
中接收返回值。
值传递示例2:主调函数中进行动态分配
#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
指向的值。与之前的代码不同,这段代码是正确的,并且能够正常运行。
输出:
*p = 100
关于值传递的提问?
疑问:上面的两个代码示例都是值传递,但是为什么一个fun函数可以修改成功,而另一个不可以修改的情况呢?
失败原因分析:
- 值传递机制:函数调用时会将实参的值复制一份给形参。在
main
函数中调用fun(p)
时,p
的值(即:变量 a 的地址
)被复制给了形参tmp
。此时,p 和 tmp 都指向变量 a
- fun函数内部操作:在
fun
函数中,tmp = (int*)malloc(sizeof(int));
这行代码将tmp
指向了新分配的一块动态内存,而不再指向变量a
。然后*tmp = 100;
是将新分配内存中的值设置为 100- 对实参的影响:由于是值传递,
tmp
的改变不会影响p
,p
仍然指向变量a
。所以printf("*p = %d\n", *p);
输出的是变量a
的值,而不是新分配内存中的值
成功原因分析:
- 值传递机制:同样是值传递,在
main
函数中调用fun(p)
时,p
的值(即:动态分配内存的地址
)被复制给了形参tmp
。此时,p 和 tmp 都指向同一块动态分配的内存
- fun函数内部操作:在
fun
函数中,*tmp = 100;
是对tmp
所指向的内存中的值进行修改,也就是修改了动态分配内存中的值。- 对实参的影响:虽然
tmp
是p
的副本,但它们指向同一块内存。所以对*tmp
的修改实际上就是对*p
的修改。因此,printf("*p = %d\n", *p);
输出的是修改后的值 100。
总结:
第一个代码中
fun
函数修改的是形参tmp
本身的指向,而不是tmp
所指向的内存中的值,由于值传递机制,形参的改变不会影响实参第二个代码中
fun
函数修改的是形参tmp
所指向的内存中的值,因为p
和tmp
指向同一块内存,所以修改操作会反映到p
所指向的内存中
返回堆区地址
#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; //okprintf("*p = %d\n", *p);//堆区空间,使用完毕,手动释放if (p != NULL){free(p);p = NULL;}return 0;
}
👨💻 博主正在持续更新C语言基础系列中。
❤️ 如果你觉得内容还不错,请多多点赞。⭐️ 如果你觉得对你有帮助,请多多收藏。(防止以后找不到了)
👨👩👧👦
C语言基础系列
持续更新中~,后续分享内容主要涉及C++全栈开发
的知识,如果你感兴趣请多多关注博主。