操作系统-内存、文件管理
一、内存管理的层次关系
用户层 | ||
---|---|---|
STL | 自动分配、自动释放 | 调用C++ |
C++ | new /delete 、构造/析构 |
调用C |
C | malloc \ calloc \ realloc \ free |
调用POSIX\Linux |
POSIX | sbrk \ brk |
调用Kernal |
Linux | mmap \ munmap |
调用Kernal |
系统层 | ||
Kernal | kmalloc \ vmalloc |
调用驱动Driver |
Driver | get_free_page |
... |
二、进程映像
程序与进程:
- 程序是存储在磁盘上的可执行文件,当程序被运行时,系统会把程序从磁盘加载到内存中运行,正在运行中的程序称为进程,一个程序可以同时被加载多次,形成多个进程,每个进程相互独立,由操作系统管理
进程映像:进程在内存空间中的分布使用情况就称为进程映像,从低地址到高地址分别是:
- 代码段
text
- 存储二进制指令、字面值常量、被
const
修饰过的原data
段的数据 - 权限
r--
或者r-x
权限只读
- 存储二进制指令、字面值常量、被
- 数据段
data
- 存储初始化过的全局变量和静态变量
- 静态数据段
BSS
- 存储未初始化过的全局变量和静态变量
- 进程一旦加载,此内存段会被操作系统自动清零,默认值是0
- 如果初始化的值给0,依然还在BSS
- 堆区
heap
- 要程序员动态分配、动态释放,从低地址向高地址扩招
- 使用
malloc
系列函数进行内存管理 - 而
malloc
系列函数底层调用操作系统的API(brk
\sbrk
\mmap
\munmap
)
- 栈区
stack
- 存储非静态局部变量、块变量,包括函数的参数(除了main函数的参数)、返回值
- 内存扩展从高地址向低地址扩展
- 栈区与堆区中间有一段预留空间,一个作用为了预留,第二个是让共享库的内存以及共享内存使用此段内存
- 命令行参数与环境变量表
- 里面存储环境变量表以及命令行传给main的参数内容
三、系统调用(系统API)
- 由操作系统向应用程序提供的程序接口信息,本质上就是应用程序与操作系统之间交互的接口。
- 操作系统的主要功能是为了管理硬件资源和为应用软件的开发人员提供一个良好的环境,使得应用程序具有更好的兼容性,为了达到这个目的,内核提供一套统一的具有一定功能的内核接口函数,称为系统调用\系统函数,以C语言函数的格式提供给用户
- 操作系统负责把应用程序的请求传给内核,由内核调用具体内核功能完成所需请求,结束后把结果通过操作系统的接口函数的返回值传递给调用者
UNIX
\Linux
大部分系统中的系统功能都是通过系统调用来实现的,相当于调用系统函数,但是它们不是真正意义的函数,当调用它们时,进程立即进入内核态,当调用结束,会重新转入回用户态
普通函数与系统函数(系统调用)的区别:
普通函数的调用步骤:
- 调用者会把要传递的参数压入栈内存
- 根据函数名也就是该函数的代码段地址,跳转到该函数代码段中执行
- 从栈内存中弹出传递的参数数据
- 定义的相关局部变量会入栈到该函数的栈内存进行扩展,并执行相关代码
- 返回执行结果
- 销毁该函数的栈内存
- 返回调用语句处继续执行
系统函数的调用过程:
- 当执行到系统函数的位置时,会触发软件中断机制
- 然后进程会转入内核态执行,由内核负责把参数从用户空间拷贝到内核空间
- 然后内核根据中断编号来执行相应的操作
- 等执行完毕后,内核再把执行结果从内核空间再拷贝回用户空间中
- 返回到中断触发位置,转换回用户态继续执行进程
四、文件
- 在
UNIX
\Linux
系统中,操作系统把所有服务、设备都抽象成了文件,因为这样可以给各种设备、服务提供同一套统一而简单的操作接口,程序就可以像访问普通文件一样,控制串口、网络、打印机等设备 - 因此在
UNIX
\Linux
系统中对文件就具有特别重要的意义,一切皆文件,所以大多数情况下,只需要五个基本系统函数操作open
/close
/read
/write
/ioctl
,既可以完成对各种设备的输入、输出控制
文件分类:
文件类型 | 标识符 | 描述 |
---|---|---|
普通文件 | - |
包括文本文件、二进制文件、各种压缩包文件等 |
目录文件 | d |
类似Windows的文件夹 |
块设备文件 | b |
用于保存大块数据的设备,例如磁盘 |
字符设备文件 | c |
用于对字符处理的服务、设备,例如 键盘 |
链接文件 | l |
类似于Windows的快捷方式 |
管道文件 | p |
用于早期进程通信 |
Socket文件\套接字文件 | s |
用于网络通信 |
文件描述符
- 文件描述符是一种非负的整数,用于表示一个打开了的文件
- 由系统调用(
open
\creat
)返回值,在后续操作文件时可以被内核空间引用去操作对应的文件 - 它代表了一个内核对象(类似于
FILE*
),因为内核不能暴露它的内存地址,因此不能返回真正的文件地址给用户 - 内核中有一张表格,记录了所有被打开的文件对象,文件描述符就是访问这张表格的下标,文件描述符也叫做句柄,是用户操作文件的凭证
- 内核只要启动,一定会给每个进程打开三个文件描述符,并且是一直默认打开,除非自己手动关闭
// 在<unistd.h> 定义了三个宏
#define STDIN_FILENO 0 // 标准输入 文件指针 stdin
#define STDOUT_FILENO 1 // 标准输出 文件指针 stdout
#define STDERR_FILENO 2 // 标准输入 文件指针 stderr
文件描述符与文件指针:
- 在
Linux
系统中打开文件后,内存中(内核区间)就会有一个内核对象,也就是记录了该文件相关信息的结构变量,但是内核为了自己的安全不能把它的地址返回给用户,而且内核区间用户是无法直接访问的,就算返回也无权限访问。 - 而且一个进程可以同时打开多份文件,所以操作系统会在内核区间创造一份索引表,表中的每一项都指向了一个打开的文件内核对象,而用户拿到的文件描述符就是该索引表的下标(主键),因此不同进程之间直接交互文件描述符是没有意义的。
- C语言标准函数中,使用文件指针代表文件,文件指针指向的区间是进程的用户 区间的一个结构变量(文件结构变量
FILE
类型),里面记录了该文件的文件描述符还有一些文件缓冲区,因此某种程度上可以理解文件指针就是文件描述符,只是不同的形式,给不同的对象使用而已
/**
* 功能:把文件指针转换成文件描述符
*/
int fileno(FILE *stream);
/**
* 功能:把文件描述符转换成文件指针
*/
FILE *fdopen(int fd, const char *mode);
文件的创建与打开
/**
* 功能: 打开文件
* @pathname: 文件路径
* @flags: 打开文件的方式
* O_WRONLY 只写权限
* O_RDONLY 只读权限
* O_RDWR 读写权限
* O_APPEND 打开文件后位置指针指向末尾
* O_CREAT 文件不存在会创建
* O_EXCL 如果文件存在则创建失败,如果没有该flags,文件存在直接打开
* O_TRUNC 清空内容打开
* O_ASYNC 当文件描述符可读/可写时,会向调用进程发送信号SIGIO
* 返回值: 文件描述符
*/
int open(const char *pathname, int flags);
/**
* 功能: 打开或创建文件
* @pathname: 文件路径
* @flags: 打开文件的方式
* @mode: 创建文件时的权限,提供一个三位八进制数表示权限,当flags为O_CREAT时必须提供
* 宏名 权限码
* S_IRWXU 00700 用户权限码
* S_IRUSR 00400 读权限
* S_IWUSR 00200 写权限
* S_IXUSR 00100 执行权限
* S_IRWXG 00070 同组其它用户权限
* S_IRGRP 00040
* S_IWGRP 00020
* S_IXGRP 00010
* S_IRWXO 00007 除了同组的用户外,其它用户的权限
* S_IROTH 00004
* S_IWOTH 00002
* S_IXOTH 00001
* 返回值: 成功返回文件描述符,失败返回负数
*/
int open(const char *pathname, int flags, mode_t mode);
/**
* 功能: 专门用于创建文件,但是基本不用,因为open可以覆盖它的功能
*/
int creat(const char *pathname, mode_t mode);
/**
* 功能:关闭文件,成功0 失败-1
*/
int close(int fd);
注意:open
\ creat
所返回的一定是当前未使用过的最小的文件描述符
注意:一个进程可以同时打开多个文件描述符,最大的数量受limit.h
中宏控制数量,在不同的系统标准中不一样,POSIX中不低于16,传统UNIX63个,现代的Linux系统255个
C语言定义重名函数的情况:
- 情况1:在不同的源文件中,
static
声明的函数可以与其他源文件中的普通函数重名 - 情况2:在函数内定义的函数可以与普通函数重名
系统调用可以重名的原因:因为系统调用本身就不是真正的函数,而是借助了软中断实现内核执行对应的系统操作,而决定执行哪个系统调用操作是由中断编号决定的,而不是名字
标准库函数中的r
\ r+
\ w
\ w+
\ a
\ a+
对应系统调用中的flags的标志:
参数 | 对应标志 |
---|---|
r |
O_RDONLY |
r+ |
O_RDWR |
w |
O_WRONLY|O_CREAT|O_TRUNC 0666 |
w+ |
O_RDWR|O_CREAT|O_TRUNC 0666 |
a |
O_WRONLY|O_CREAT|O_APPEND 0666 |
a+ |
O_RDWR|O_CREAT|O_APPEND 0666 |
文件读写
/**
* 功能:写入文件内容
* @fd: 文件描述符
* @buf:要写入的数据的内存首地址
* @count: 要写入的字节数
* 返回值: 成功写入的字节数
*/
ssize_t write(int fd, const void *buf, size_t count);
/**
* 功能:从文件中读取数据到内存
* @fd: 文件描述符
* @buf: 存储读取到的数据的内存首地址
* @count: 想要读取的字节数,一般就是buf的大小
* 返回值:成功读取到的字节数
*/
ssize_t read(int fd, void *buf, size_t count);
注意:它们与标准C的 fwrite
/ fread
很像,但是它们更纯粹直接,速度比标准C的慢
五、系统IO与标准IO
- 当系统调用被执行时,需要从用户态转换成内核态,执行完毕后又要转回用户态,如果频繁来回切换会导致性能丢失
- 在标准IO中,内部维护一个缓冲区(1k,1024字节),要写入的数据先存储到缓冲区中,只有当满足特定条件时才会把缓冲区中数据全部通过系统调用write写入文件,从而降低了系统调用的使用频率,减少了状态的切换,因此标准IO的效率要比直接使用系统IO要快
- 如果想要提高系统IO的速度,可以尝试自己维护一个缓冲区,先把数据存储到缓冲区,等满后再调用write,这样提高速度
- 普通情况下建议使用标准IO,因为更快,如果对速度还有更高的要求,可以自己使用系统IO+大缓冲区
UNIX/Linux
只有一套读写文件的系统调用,没有像标准C中的文本读写 ,那么可以先把数据转换成字符串(sprintf
/sscanf
),然后再通过write
\read
读写,从而实现文本读写的效果
六、文件位置指针
- 与标准IO的文件读写位置指针一样,系统IO时也会有一个表示位置的指针在移动,会随着读写操作的执行向后自动移动
- 当需要随机位置进行读写操作时,那么需要移动位置指针的位置
/**
* 功能:调整文件指针的位置
* @fd: 要调整的文件描述符
* @offset: 偏移值
* @whence: 基础位置
* SEEK_SET 文件开头
* SEEK_CUR 当前位置
* SEEK_END 文件末尾
* 返回值: 返回当前位置指针的位置 从文件开头计算该位置的字节数
*/
off_t lseek(int fd, off_t offset, int whence);
注意:系统IO中不需要ftell
函数的系统调用,因为lseek
就能够完成获取位置指针位置的功能
- 当超过文件末尾的位置再写入数据时,原末尾与最后的数据之间会有一个“数据黑洞”,但是黑洞不占用磁盘大小,但是会计算成文件的大小,不会影响后序的读写
七、文件同步
- 大多数磁盘
I/O
都有缓冲区机制,写入文件其实先写入缓冲区,直到缓冲区满才将其排入写队列。降低写操作的次数,提高写操作效率,但是可能会导致磁盘文件与缓冲区数据不同步,可以借助系统调用来强制让磁盘与缓冲区的内容同步:
/**
* 功能: 将所有被修改过的缓冲区中的数据排入写队列,立即返回,不等待写入磁盘的完成
*/
void sync(void);
/**
* 功能: 只针对文件fd,并且会等待写入完成才返回
*/
int fsync(int fd);
/**
* 功能: 只同步文件fd的数据,不同步文件属性,等待写入完成才返回
*/
int fdatasync(int fd);
八、文件描述符的状态标志
/**
* 功能:设置或获取文件描述符的状态标志
* @cmd:
* F_GETFD 获取文件描述符状态标志
* F_SETFD 设置文件描述符状态标志
* 其中 O_CREAT\O_EXCL\O_TRUNC 获取不了
* F_GETFL 获取文件状态标志
* F_SETFL 追加文件状态标志
*/
int fcntl(int fd, int cmd, ... /* arg */ );
九、文件锁
文件锁的意义:当多个进程同时访问同一个文件时,就有可能造成文件的数据读写混乱,为了解决这一问题可以在读写文件前给文件尝试并加锁
文件锁的限制:一般情况下,系统提供的文件锁都是劝解锁,内核主要负责文件的加锁和检查是否上锁,而不直接参与锁的控制以及协同操作,这类锁就需要程序员每次用之前都要检查是否被别的进程加锁,再实现并发操作
/**
* 功能: 对文件的某一部分进行锁操作
* @cmd:
* F_GETLK 测试lock所表示的锁能否加锁
* 如果可以加则讲lock.l_type设置为F_UNLCK
* 否则会通过lock.l_type返回当前锁信息
* F_SETLK 设置文件的锁定状态为lock.l_type
* 成功返回0,失败返回-1,如果有其他进程持有该锁导致加锁失败,返回EACCES or EAGAIN
* F_SETLKW 设置文件的锁定状态为lock.l_type
* 成功返回0,否则一直等待,除非被其它信号打断返回-1
*/
int fcntl(int fd, int cmd, flock* lock);
struct flock {
short l_type; /* 锁的类型:
F_RDLCK,读锁
F_WRLCK, 写锁
F_UNLCK 解锁*/
short l_whence; /* 偏移起点 SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* 偏移值 锁区起始位置:l_whence+l_start */
off_t l_len; /* 锁区长度 0表示锁到文件末尾*/
pid_t l_pid; /* 加锁的进程id -1表示让内核自动设置 */
};
十、文件描述符
/**
* 功能: 复制文件描述符
* @oldfd: 已经打开的要复制的文件描述符
* 返回值: 返回一个新的文件描述符,是当前可用的文件描述符中最小值,失败-1
*/
int dup(int oldfd);
/**
* 功能: 复制oldfd文件描述符成指定的文件描述符newfd, 如果newfd原来已经被占用,则会把它关闭重新复制
* 返回值: 成功0 失败-1
*/
int dup2(int oldfd, int newfd);
注意:复制成功后,相当于两个文件描述符对应同一个打开的文件
复制文件描述符的意义:
- 复制成功后,相当于两个文件描述符对应同一个打开的文件,可以以此实现很多奇特的操作,例如重定向文件读写、重定向命令
ls >> file
- 通过把一个已经打开了的文件描述符
fd
,通过dup2
重定向为标准输入0或者标准输出1,此时就会先把0、1文件关闭,然后0\1指向我们刚刚的文件fd,此后,通过输出语句执行时,相当于把本来要输出到屏幕的内容,直接写如到文件fd
中,相当于执行write
,如果执行输入语句,相当于把本来要从键盘中输入的内容,直接从文件fd
中读取,相当于执行了read
操作
十一、获取文件属性
/**
* 功能:根据文件路径获取该文件的属性
*/
int stat(const char *pathname, struct stat *buf);
/**
* 功能:根据文件描述符获取该文件的属性
*/
int fstat(int fd, struct stat *buf);
/**
* 功能:根据文件路径获取链接文件的属性
*/
int lstat(const char *pathname, struct stat *buf);
struct stat {
dev_t st_dev; /* 文件的设备ID */
ino_t st_ino; /* 文件的inode节点号 */
mode_t st_mode; /* 文件的类型和权限 */
nlink_t st_nlink; /* 硬链接数量 */
uid_t st_uid; /* 属主ID */
gid_t st_gid; /* 属组ID */
dev_t st_rdev; /* 特殊设备ID */
off_t st_size; /* 文件的总字节数 */
blksize_t st_blksize; /* 文件的IO块数量 */
blkcnt_t st_blocks; /* 以512字节为一块,该文件占了几块 */
struct timespec st_atim; /* 最后访问时间 */
struct timespec st_mtim; /* 最后内容修改时间 */
struct timespec st_ctim; /* 最后文件状态属性修改时间 */
#define st_atime st_atim.tv_sec /* 时间换算成总秒数 */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
/**
* @st_mode: 记录了文件类型和权限
* S_IFMT 0170000 获取文件类型的掩码
* S_IFSOCK 0140000 socket文件
* S_IFLNK 0120000 软链接文件
* S_IFREG 0100000 普通文件
* S_IFBLK 0060000 块设备文件
* S_IFDIR 0040000 目录
* S_IFCHR 0020000 字符设备文件
* S_IFIFO 0010000 FIFO 管道文件
*
* 或者借助提供的宏函数来判断文件类型:
* S_ISREG(m) 是否是普通文件
* S_ISDIR(m) 目录
* S_ISCHR(m) 字符设备文件
* S_ISBLK(m) 块设备文件
* S_ISFIFO(m) 管道文件
* S_ISLNK(m) 软链接文件
* S_ISSOCK(m) socket文件
*
* 判断权限:
* S_IRWXU 00700 判断属主的读写执行权限的权限码
* S_IRUSR 00400 owner has read permission
* S_IWUSR 00200 owner has write permission
* S_IXUSR 00100 owner has execute permission
*
* S_IRWXG 00070 判断属组其他用户的读写执行权限的权限码
* S_IRGRP 00040 group has read permission
* S_IWGRP 00020 group has write permission
* S_IXGRP 00010 group has execute permission
*
* S_IRWXO 00007 判断其它用户的读写执行权限的权限码
* S_IROTH 00004 others have read permission
* S_IWOTH 00002 others have write permission
* S_IXOTH 00001 others have execute permission
*/
十二、文件的权限
测试文件的权限:
/**
* 功能: 测试当前用户对该文件的权限
* @pathname: 要测试的文件路径
* @mode:
* R_OK 读权限
* W_OK 写权限
* X_OK 执行权限
* F_OK 测试文件是否存在
* 返回值: 存在权限返回0 否则返回-1
*/
int access(const char *pathname, int mode);
修改文件权限:
/**
* 功能: 修改文件的权限为mode,mode可以使用提供的宏,或者直接使用一个八进制数表示三组权限
*/
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
注意:权限都可以修改
文件的权限屏蔽码:当使用open
\ creat
创建文件时,无论给什么权限创建都会成功,但是系统中记录有一个权限屏蔽码会对用户创建的文件的权限进行过滤屏蔽,最终创建出来的文件权限要除去文件屏蔽码
// 可以通过命令 umask 查看当前用户的权限屏蔽码
// 可以通过命令 umask 0xxx 修改当前终端这一次的权限屏蔽码为0xxx
/**
* 功能: 给当前进程设置权限屏蔽码
* mask: 新的屏蔽码
* 返回值: 旧的屏蔽码
*/
mode_t umask(mode_t mask);
注意:只对当前进程有效
注意:屏蔽码只影响open
、creat
,对于chmod
、fchmod
不受影响
十三、修改文件大小
/**
* length: 想要修改成的文件字节数
* 成功返回0,失败返回-1
* 截短末尾丢弃,加长末尾添0
*/
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
十四、链接文件
Linux的文件系统会把磁盘分区成主要的两大部分:
inode
信息块- 默认128B,里面主要记录文件的权限、大小、所有者、修改时间等基本信息
block
数据块- 默认4Kb,记录了文件名和真正的文件数据内容
- 每个文件必须拥有一个唯一的
inode
以及若干个block
组成,读取文件需要借助文件所在目录的block
中记录的文件inode
号,找到该文件的inode
,inode
中记录了该文件的block
位置,从而最终读取文件
硬链接文件:硬链接文件没有自己inode
和block
,只是在不同的目录下复制了一份源文件的inode
信息,可以通过该inode
找到同一份源文件的block
软链接文件:软链接文件会创建自己的新的inode
和block
,它的inode
也是为了找到自己的block
,而在它的block
中存储的是链接源文件的文件名和inode
信息
区别:
- 删除源文件,只是删除源文件的
inode
块,但是硬链接文件不受影响,而软链接文件就无法访问了 - 当一个文件的硬链接数删除成0时,文件才被真正的删除
- 修改硬链接文件内容,源文件也会被修改;而修改软链接的
block
,不会改变源文件的内容,反而会让软链接无法找到源文件 - 硬链接不能链接目录,软链接可以
硬链接文件的创建和删除:
/**
* 功能: 创建硬链接文件
* 与命令 link \ ln 功能一样
*/
int link(const char *oldpath, const char *newpath);
/**
* 功能: 删除文件的硬链接,文件的硬链接数-1
*/
int unlink(const char *pathname);
/**
* 功能:与unlink一致,都可以删除普通文件以及硬链接文件
*/
int remove(const char *pathname);
注意:如果删除的文件正在被打开,则会等待文件关闭后删除
注意:如果删除的是软链接文件,则会只删除软链接文件本身,而不会对源文件有任何影响,而且没有任何一个可以借助软链接来删除链接对象文件的函数
软链接文件的创建与读取:
/**
* 功能: 创建软链接文件
* 对应命令:ln -s
*/
int symlink(const char *target, const char *linkpath);
/**
* 功能: 获取到软链接文件自身的block内容,也就是它链接对象的文件名,而不会获取到链接对象的文件内容
* 如果想要读取链接对象的文件内容,还是通过read\write进行
*/
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
十五、工作目录
- 工作目录指的是当前进程所在的目录,它是相对路径的起点,在操作文件时,如果没有提供文件的绝对路径信息,那么会操作工作目录下的文件,一般默认下工作目录就是当前进程的目录
/**
* 功能: 获取当前进程的工作路径,相当于命令 pwd
*/
char *getcwd(char *buf, size_t size);
/**
* 功能:修改工作路径,相当于cd
*/
int chdir(const char *path);
int fchdir(int fd);
创建、删除、读取目录:
/**
* 功能: 创建空白目录
* @mode: 目录的权限,目录必须有执行权限次才能进入
*/
int mkdir(const char *pathname, mode_t mode);
/**
* 功能: 删除目录,只能删除空白目录
*/
int rmdir(const char *pathname);
/**
* 功能: 打开目录文件
* 返回值: 成功返回目录流指针,失败返回NULL
*/
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
/**
* 功能: 从目录流对象中读取一条条目信息
* 注意: 读取完一条条目后,会自动的往后移动,只需要再次调用该函数,即可以读下一条目录
* 返回值: 返回该条目录信息的结构指针或者NULL(读取失败或者读到了目录末尾结束)
*/
struct dirent *readdir(DIR *dirp);
// 结构体 struct dirent里面存储了目录中某个文件的信息
struct dirent {
ino_t d_ino; /* inode节点号*/
off_t d_off; /* 下一条条目的偏移量 注意是磁盘偏移量,而非内存地址 */
unsigned short d_reclen; /* 当前条目的长度 */
unsigned char d_type; /* 文件类型 */
char d_name[256]; /* 文件名 */
};
/**
* d_type的取值:
* DT_BLK 块设备文件
* DT_CHR 字符设备文件
* DT_DIR 目录
* DT_FIFO 管道文件
* DT_LNK 软链接文件
* DT_REG 普通文件
* DT_SOCK socket文件
* DT_UNKNOWN 未知
*/
/**
* 功能: 复位目录流,设置目录流的位置指针回到开头
*/
void rewinddir(DIR *dirp);
/**
* 功能: 获取当前目录流的位置
*/
long telldir(DIR *dirp);
/**
* 功能: 设置目录流的读取位置,这样可以进行任意条目的获取
*/
void seekdir(DIR *dirp, long loc);
/**
* 功能: 关闭目录文件
*/
int closedir(DIR *dirp);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)