C语言基础之【内存管理】
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++全栈开发
的知识,如果你感兴趣请多多关注博主。
相关文章:

C语言基础之【内存管理】
C语言基础之【内存管理】 存储类型作用域普通局部变量静态局部变量普通全局变量静态全局变量全局函数和静态函数 内存布局内存分区存储类型与内存四区内存操作函数memset()memcpy()memmove()memcmp() 堆区内存分配和释放malloc()free() 内存分区代码分析返回栈区地址返回data区…...

C盘清理技巧分享:释放空间,提升电脑性能
目录 1. 引言 2. C盘空间不足的影响 3. C盘清理的必要性 4. C盘清理的具体技巧 4.1 删除临时文件 4.2 清理系统还原点 4.3 卸载不必要的程序 4.4 清理下载文件夹 4.5 移动大文件到其他盘 4.6 清理系统缓存 4.7 使用磁盘清理工具 4.8 清理Windows更新文件 4.9 禁用…...

每天一道算法题【蓝桥杯】【两两交换链表中的节点】
思路 本质问题可以分成若干个子问题 即把前两个链表交换,并与后面的链表相连 故实现函数功能调用自身递归即可 #define _CRT_SECURE_NO_WARNINGS 1 struct ListNode {int val;ListNode *next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), nex…...

mIoU Class与mIoU Category的区别
mIoU(mean Intersection over Union)是语义分割任务中常用的评估指标,用于衡量模型预测的分割结果与真实标签之间的重叠程度。mIoU Class 和 mIoU Category 的区别主要体现在计算方式和应用场景上: 1. mIoU Class 定义ÿ…...

深入解析 C 语言中含数组和指针的构造体与共同体内存计算
在 C 语言中,构造体(struct)和共同体(union)允许我们将多种数据类型组合到一起。除了常见的基本数据类型之外,经常还会在它们中嵌入数组和指针。由于数组的内存是连续分配的,而指针的大小与平台…...

【C++模板】:开启泛型编程之门(函数模版,类模板)
📝前言: 在上一篇文章C内存管理中我们介绍了C的内存管理,重点介绍了与C语言的区别,以及new和delete。这篇文章我们将介绍C的利器——模板。 在C编程世界里,模板是一项强大的特性,它为泛型编程奠定了坚实基础…...

HEC-HMS水文建模全解析:气候变化与极端水文、离散化流域单元精准刻画地表径流、基流与河道演进过程
一、技术革新:数字流域的精密算法革命 在全球气候变化与极端水文事件频发的双重压力下,HEC-HMS模型凭借其半分布式建模架构与多尺度仿真能力,已成为现代流域管理的核心工具。该模型通过离散化流域单元精准刻画地表径流、基流与河…...

具备多种功能的PDF文件处理工具
软件介绍 在日常办公和学习场景中,PDF文件使用极为频繁,而一款功能强大的PDF编辑软件能大幅提升处理效率。 今天要介绍的Adobe Acrobat Pro DC 2024.005.20414,就具备像编辑Word文档一样便捷编辑PDF的能力。 PDF文档在学习和工作中广泛应用…...

【SpringMVC】SpringMVC的启动过程与原理分析:从源码到实战
SpringMVC的启动过程与原理分析:从源码到实战 SpringMVC是Spring框架中用于构建Web应用的核心模块,它基于MVC(Model-View-Controller)设计模式,提供了灵活且强大的Web开发能力。本文将深入分析SpringMVC的启动过程、核…...

转自南京日报:天洑软件创新AI+仿真技术变制造为“智造
以下文章来源:南京日报 进入3月,南京天洑软件有限公司(以下简称天洑软件)董事长张明更加忙碌。“公司强调工业软件在数字经济与先进制造业融合中的关键作用,并已广泛应用在能源、电力和航空等领域。”他说,…...

golang dlv调试工具
golang dlv调试工具 在goland2022.2版本 中调试go程序报错 WARNING: undefined behavior - version of Delve is too old for Go version 1.20.7 (maximum supported version 1.19) 即使你go install了新的dlv也无济于事 分析得出Goland实际使用的是 Goland安装目录下dlv 例…...

LSTM方法实践——基于LSTM的汽车销量时序建模与预测分析
Hi,大家好,我是半亩花海。本实验基于汽车销量时序数据,使用LSTM网络(长短期记忆网络)构建时间序列预测模型。通过数据预处理、模型训练与评估等完整流程,验证LSTM在短期时序预测中的有效性。 目录 一、实验…...

微服务——网关、网关登录校验、OpenFeign传递共享信息、Nacos共享配置以及热更新、动态路由
之前学习了Nacos,用于发现并注册、管理项目里所有的微服务,而OpenFeign简化微服务之间的通信,而为了使得前端可以使用微服务项目里的每一个微服务的接口,就应该将所有微服务的接口管理起来方便前端调用,所以有了网关。…...

【数据结构】二叉搜索树、平衡搜索树、红黑树
二叉搜索树(Binary Search Tree) 二叉搜索树是一种特殊的二叉树,它用来快速搜索某个值,对于每个节点都应该满足以下条件: 若该节点有左子树,那么左子树中所有节点的值都应该小于该节点的值。若该节点有右…...

Spring Boot 解析 LocalDateTime 失败?Uniapp 传输时间变 1970 的原因与解决方案
目录 前言1. 问题分析2. 时间戳(推荐,可尝试)3. 使用 JsonDeserialize & JsonSerialize(中立)4. 前端传 ISO-8601 格式(不推荐,可尝试)5. 用 String(中立)…...

Xilinx ZYNQ FSBL解读:LoadBootImage()
篇首 最近突发奇想,Xilinx 的集成开发环境已经很好了,很多必要的代码都直接生成了,这给开发者带来了巨大便利的同时,也让人错过了很多代码的精彩,可能有很多人用了很多年了,都还无法清楚的理解其中过程。博…...

mysql中in和exists的区别?
大家好,我是锋哥。今天分享关于【mysql中in和exists的区别?】面试题。希望对大家有帮助; mysql中in和exists的区别? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 MySQL 中,IN 和 EXISTS 都用于进行子查询,但它…...

oracle 数据导出方案
工作中有遇到需要将oracle 数据库表全部导出,还需要去除表数据中的换行符。 方案 shell 设计 封装函数 1 function con_oracle() 用于连接oracle 2 function send_file() 用于发送文件 3 主程序 使用循环将所有表导出并发送到数据服务器 主程序 程序代码 #!…...

Apache Commons Lang3 和 Commons Net 详解
目录 1. Apache Commons Lang3 1.1 什么是 Apache Commons Lang3? 1.2 主要功能 1.3 示例代码 2. Commons Net 2.1 什么是 Commons Net? 2.2 主要功能 2.3 示例代码 3. 总结 3.1 Apache Commons Lang3 3.2 Commons Net 3.3 使用建议 4. 参考…...
从0开始的操作系统手搓教程33:挂载我们的文件系统
目录 代码实现 添加到初始化上 上电看现象 挂载分区可能是一些朋友不理解的——实际上挂载就是将我们的文件系统封装好了的设备(硬盘啊,SD卡啊,U盘啊等等),挂到我们的默认分区路径下。这样我们就能访问到了ÿ…...

【Linux】36.简单的TCP网络程序
文章目录 1. TCP socket API 详解1.1 socket():打开一个网络通讯端口1.2 bind():绑定一个固定的网络地址和端口号1.3 listen():声明sockfd处于监听状态1.4 accept():接受连接1.5 connect():连接服务器 2. 实现一个TCP网络服务器2.1 Log.hpp - "多级日志系统"2.2 Daem…...

时序分析
1、基本概念介绍 1.1、 建立时间 T(su) 建立时间:setup time,它是指有效的边沿信号到来之前,输入端口数据保持稳定的时间。 1.1.1、 建立时间要求: 建立时间要求指的是 想要寄存器如期的工作,在有效时…...

doris:ClickHouse
Doris JDBC Catalog 支持通过标准 JDBC 接口连接 ClickHouse 数据库。本文档介绍如何配置 ClickHouse 数据库连接。 使用须知 要连接到 ClickHouse 数据库,您需要 ClickHouse 23.x 或更高版本 (低于此版本未经充分测试)。 ClickHouse 数据库的 JDBC 驱动程序&a…...

NLP常见任务专题介绍(1)-关系抽取(Relation Extraction, RE)任务训练模板
📌 关系抽取(Relation Extraction, RE)任务训练示例 本示例展示如何训练一个关系抽取模型,以识别两个实体之间的关系。 1️⃣ 任务描述 目标:从文本中提取两个实体之间的语义关系,例如 “人物 - 组织”、“药物 - 疾病”、“公司 - 创始人” 等。输入:句子 + 标注的实…...

大模型Transformer的MOE架构介绍及方案整理
前言:DeepSeek模型最近引起了NLP领域的极大关注,也让大家进一步对MOE(混合专家网络)架构提起了信心,借此机会整理下MOE的简单知识和对应的大模型。本文的思路是MOE的起源介绍、原理解释、再到现有MOE大模型的整理。 一…...

零基础掌握Linux SCP命令:5分钟实现高效文件传输,小白必看!
引言 “为什么我传个文件到服务器要折腾半小时?” 如果你也曾在Linux系统中为文件传输抓狂,今天这篇保姆级教程就是你的救星!SCP命令——一个基于SSH协议的高效传输工具,只需5分钟,彻底告别FTP客户端和繁琐操作&#…...
分类评价指标
基础概念解释 TP、TN、FP、FN 这里T是True,F是False,P为Positive,N为Negative TP:被模型正确地预测为正样本(原本为正样本,预测为正样本) TN:被模型正确地预测为负样本࿰…...

Python项目-基于Django的在线教育平台开发
1. 项目概述 在线教育平台已成为现代教育的重要组成部分,特别是在后疫情时代,远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台,包括系统设计、核心功能实现以及部署上线等关键环节。 本项…...

子数组问题——动态规划
个人主页:敲上瘾-CSDN博客 动态规划 基础dp:基础dp——动态规划-CSDN博客多状态dp:多状态dp——动态规划-CSDN博客 目录 一、解题技巧 二、最大子数组和 三、乘积最大子数组 四、最长湍流子数组 五、单词拆分 一、解题技巧 区分子数组&…...

linux设置pem免密登录和密码登录
其实现在chatgpt 上面很多东西问题都可以找到比较好答案了,最近换了一个服务器,记录一下。 如果设置root用户,就直接切换到cd .ssh目录下生成ssh key即可,不需要创建用户创建用户的ssh文件夹了 比如说我要让danny这个用户可以用p…...