c/c++ 复习笔记 第六天 文件系统

文件系统

一、sync/fsync/fdatasync

  1. 大多数磁盘I/O都通过缓冲进行,
    写入文件其实只是写入缓冲区,直到缓冲区满,
    才将其排入写队列。

  2. 延迟写降低了写操作的次数,提高了写操作的效率,
    但可能导致磁盘文件与缓冲区数据不同步。

  3. sync/fsync/fdatasync用于强制磁盘文件与缓冲区同步。

  4. sync将所有被修改过的缓冲区排入写队列即返回,
    不等待写磁盘操作完成。

  5. fsync只针对一个文件,且直到写磁盘操作完成才返回。

  6. fdatasync只同步文件数据,不同步文件属性。

     #include 
     
     void sync (void);
     
     int fsync (
         int fd
     );
     
     成功返回0,失败返回-1。
     
     int fdatasync (
         int fd
     );
    

    成功返回0,失败返回-1。

c/c++ 复习笔记 第六天 文件系统_第1张图片

二、fcntl

#include

int fcntl (
int fd, // 文件描述符
int cmd, // 操作指令
… // 可变参数,因操作指令而异
);

对fd文件执行cmd操作,某些操作需要提供参数。

  1. 常用形式

#include

int fcntl (int fd, int cmd);
int fcntl (int fd, int cmd, long arg);

成功返回值因cmd而异,失败返回-1。

cmd取值:

F_DUPFD - 复制fd为不小于arg的文件描述符。
若arg文件描述符已用,
该函数会选择比arg大的最小未用值,
而非如dup2函数那样关闭之。

范例:dup.c

#include
#include
#include
int main(void){
        int fd1=open("fdup1.txt",O_WRONLY|O_CREAT|O_TRUNC,0664) ;
        if(fd1==-1){
                perror("fopen");
                return -1;
        }
        printf("fd1=%d\n",fd1);
        int fd2=open("fdup2.txt",O_WRONLY|O_CREAT|O_TRUNC,0664) ;
        if(fd2==-1){
                perror("fopen");
                return -1;
        }
        printf("fd2=%d\n",fd2);
        int fd3=fcntl(fd1,F_DUPFD,fd2);
        if(fd3==-1){
                perror("fcntl");
                return -1;
        }
        printf("fd3=%d\n",fd3);
        const char* text="123";
        if(write(fd1,text,strlen(text)*sizeof(text[0]))==-1){
                perror("write");
                return -1;
        }
        text="456";
        if(write(fd2,text,strlen(text)*sizeof(text[0]))==-1){
                perror("write");
                return -1;
        }
        text="789";
        if(write(fd3,text,strlen(text)*sizeof(text[0]))==-1){
                perror("write");
                return -1;
        }
        close(fd1);
        close(fd2);
        close(fd3);
        return 0;
}

~/uc/day05$ gcc fdup.c
~/uc/day05$ ./a.out
fd1=3
fd2=4
fd3=5
~/uc/day05$ cat fdup1.txt
123789~/uc/day05$ cat fdup2.txt
456~/uc/day05$

F_GETFD - 获取文件描述符标志。

F_SETFD - 设置文件描述符标志。

目前仅定义了一个文件描述符标志位FD_CLOEXEC:

0 - 在通过execve()函数所创建的进程中,
该文件描述符依然保持打开。

1 - 在通过execve()函数所创建的进程中,
该文件描述符将被关闭。

F_GETFL - 获取文件状态标志。
不能获取O_CREAT/O_EXCL/O_TRUNC。

F_SETFL - 追加文件状态标志。
只能追加O_APPEND/O_NONBLOCK。

范例:flags.c

#include 
#include 
#include 
void pflags (int flags) {
    printf ("文件状态标志(%08X):", flags);
    struct {
        int flag;
        const char* desc;
    }   flist[] = {
        {O_RDONLY,   "O_RDONLY"},   // 1000 0000 0000 0000 0
        {O_WRONLY,   "O_WRONLY"},   // 0100 0000 0000 0000 0
        {O_RDWR,     "O_RDWR"},     // 0010 0000 0000 0000 0
        {O_APPEND,   "O_APPEND"},   // 0001 0000 0000 0000 0
        {O_CREAT,    "O_CREAT"},    // 0000 1000 0000 0000 0
        {O_EXCL,     "O_EXCL"},     // 0000 0100 0000 0000 0
        {O_TRUNC,    "O_TRUNC"},    // 0000 0010 0000 0000 0
        {O_NOCTTY,   "O_NOCTTY"},
        {O_NONBLOCK, "O_NONBLOCK"},
        {O_SYNC,     "O_SYNC"},
        {O_DSYNC,    "O_DSYNC"},
        {O_RSYNC,    "O_RSYNC"},
        {O_ASYNC,    "O_ASYNC"}
    };
    size_t i;
    int first = 1;   //标志是否为第一个 要不要打 ”|“
    for (i = 0; i

**

2. 文件锁

**

#include

int fcntl (int fd, int cmd, struct flock* lock);

其中:

struct flock {
short int l_type; // 锁的类型:
// F_RDLCK/F_WRLCK/F_UNLCK
// (读锁/写锁/解锁)
short int l_whence; // 偏移起点:
// SEEK_SET/SEEK_CUR/SEEK_END
// (文件头/当前位置/文件尾)
off_t l_start; // 锁区偏移,从l_whence开始
off_t l_len; // 锁区长度,0表示锁到文件尾
pid_t l_pid; // 加锁进程,-1表示自动设置
};

cmd取值:

F_GETLK - 测试lock所表示的锁是否可加。
若可加则将lock.l_type置为F_UNLCK,
否则通过lock返回当前锁的信息。

F_SETLK - 设置锁定状态为lock.l_type,
成功返回0,失败返回-1。
若因其它进程持有锁而导致失败,
则errno为EACCES或EAGAIN。

F_SETLKW - 设置锁定状态为lock.l_type,
成功返回0,否则一直等待,
除非被信号打断返回-1。

  1. 既可以锁定整个文件,也可以锁定特定区域。

  2. 读锁(共享锁)、写锁(独占锁/排它锁)、解锁。

图示:flock.bmp

3) 文件描述符被关闭(进程结束)时,自动解锁。

  1. 劝谏锁(协议锁)、强制锁。

  2. 文件锁仅在不同进程间起作用。

  3. 通过锁同步多个进程对同一个文件的读写访问。
    范例:wlock.c

 #include 
#include 
#include 
#include 
#include 
// 写锁测试
// 返回
//  1 - 可加写锁
//  0 - 不可加写锁
// -1 - 系统错误
int wtest (int fd, off_t start, off_t len) {
    struct flock lock;
    lock.l_type   = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start  = start;
    lock.l_len    = len;
    lock.l_pid    = -1;
    if (fcntl (fd, F_GETLK, &lock) == -1)
        return -1;
    if (lock.l_type == F_UNLCK)
        return 1;
    return 0;
}
// 加写锁
int wlock (int fd, off_t start, off_t len, int wait) {
    struct flock lock;
    lock.l_type   = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start  = start;
    lock.l_len    = len;
    lock.l_pid    = -1;
    return fcntl (fd, wait ? F_SETLKW : F_SETLK,&lock);
}
// 解锁
int ulock (int fd, off_t start, off_t len) {
    struct flock lock;
    lock.l_type   = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start  = start;
    lock.l_len    = len;
    lock.l_pid    = -1;
    return fcntl (fd, F_SETLK, &lock);
}
int main (int argc, char* argv[]) {
    if (argc < 2) {
        fprintf (stderr,"用法:%s <字符串>\n",argv[0]);
        return -1;
    }
    int fd = open ("wlock.txt",
        O_WRONLY | O_CREAT | O_APPEND, 0664);
    if (fd == -1) {
        perror ("open");
        return -1;
    }
    /*
    int unlock = 0;
    do {
        if ((unlock = wtest (fd, 0, 0)) == -1) {
            perror ("wtest");
            return -1;
        }
        if (! unlock) {
            printf ("该文件已被锁定,稍后再试...\n");
            // 空闲处理
            // ...
        }
    }   while (! unlock);
    if (wlock (fd, 0, 0, 0) == -1) {
        perror ("wlock");
        return -1;
    }
    */
    while (wlock (fd, 0, 0, 0) == -1) {
        if (errno != EACCES && errno != EAGAIN) {
            perror ("wlock");
            return -1;
        }
        printf ("该文件已被锁定,稍后再试...\n");
        // 空闲处理
        // ...
    }
    /*
    if (wlock (fd, 0, 0, 1) == -1) {
        perror ("wlock");
        return -1;
    }
    */
    size_t i, len = strlen (argv[1]);
    for (i = 0; i < len; ++i) {
        if (write (fd, &argv[1][i],
            sizeof (argv[1][i])) == -1) {
            perror ("write");
            return -1;
        }
        sleep (1);
    }
    if (ulock (fd, 0, 0) == -1) {
        perror ("ulock");
        return -1;
    }
    close (fd);
    return 0;
}

三、stat/fstat/lstat

获取文件属性。

#include

int stat (
const char* path, // 文件路径
struct stat* buf // 文件属性
);

int fstat (
int fd, // 文件描述符
struct stat* buf // 文件属性
);

int lstat (
const char* path, // 文件路径
struct stat* buf // 文件属性
);

成功返回0,失败返回-1。

stat函数跟踪软链接,lstat函数不跟踪软链接。

struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // i节点号
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; // I/O块字节数
blkcnt_t st_blocks; // 占用块(512字节)数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态改变时间
};

st_mode(0TTSUGO)为以下值的位或:

S_IFDIR - 目录
S_IFREG - 普通文件 |
S_IFLNK - 软链接 |
S_IFBLK - 块设备 > TT (S_IFMT)
S_IFCHR - 字符设备 |
S_IFSOCK - Unix域套接字 |
S_IFIFO - 有名管道 /

S_ISUID - 设置用户ID
S_ISGID - 设置组ID > S
S_ISVTX - 粘滞 /

S_IRUSR(S_IREAD) - 属主可读
S_IWUSR(S_IWRITE) - 属主可写 > U (S_IRWXU)
S_IXUSR(S_IEXEC) - 属主可执行 /

S_IRGRP - 属组可读
S_IWGRP - 属组可写 > G (S_IRWXG)
S_IXGRP - 属组可执行 /

S_IROTH - 其它可读
S_IWOTH - 其它可写 > O (S_IRWXO)
S_IXOTH - 其它可执行 /

  1. 有关S_ISUID/S_ISGID/S_ISVTX的说明
  1. 具有S_ISUID/S_ISGID位的可执行文件,
    其有效用户ID/有效组ID,
    并不取自由其父进程(比如登录shell)所决定的,
    实际用户ID/实际组ID,
    而是取自该可执行文件的属主ID/属组ID。
    如:/usr/bin/passwd

  2. 具有S_ISUID位的目录,
    其中的文件或目录除root外,
    只有其属主可以删除。

  3. 具有S_ISGID位的目录,
    在该目录下所创建的文件,继承该目录的属组ID,
    而非其创建者进程的有效组ID。

  4. 具有S_ISVTX位的可执行文件,
    在其首次执行并结束后,
    其代码区将被连续地保存在磁盘交换区中,
    而一般磁盘文件中的数据块是离散存放的。
    因此,下次执行该程序可以获得较快的载入速度。
    现代Unix系统大都采用快速文件系统,
    已不再需要这种技术。

  5. 具有S_ISVTX位的目录,
    只有对该目录具有写权限的用户,
    在满足下列条件之一的情况下,
    才能删除或更名该目录下的文件或目录:

    A. 拥有此文件;
    B. 拥有此目录;
    C. 是超级用户。

    如:/tmp

    任何用户都可在该目录下创建文件,
    任何用户对该目录都享有读/写/执行权限,
    但除root以外的任何用户在目录下,
    都只能删除或更名属于自己的文件。

  1. 常用以下宏辅助分析st_mode

S_ISDIR() - 是否目录
S_ISREG() - 是否普通文件
S_ISLNK() - 是否软链接
S_ISBLK() - 是否块设备
S_ISCHR() - 是否字符设备
S_ISSOCK() - 是否Unix域套接字
S_ISFIFO() - 是否有名管道

范例:stat.c

#include 
#include 
#include 
#include 
const char* mtos (mode_t m) {
    static char s[11];
    if (S_ISDIR (m))
        strcpy (s, "d");
    else
    if (S_ISLNK (m))
        strcpy (s, "l");
    else
    if (S_ISBLK (m))
        strcpy (s, "b");
    else
    if (S_ISCHR (m))
        strcpy (s, "c");
    else
    if (S_ISSOCK (m))
        strcpy (s, "s");
    else
    if (S_ISFIFO (m))
        strcpy (s, "p");
    else
        strcpy (s, "-");
    strcat (s, m & S_IRUSR ? "r" : "-");
    strcat (s, m & S_IWUSR ? "w" : "-");
    strcat (s, m & S_IXUSR ? "x" : "-");
    strcat (s, m & S_IRGRP ? "r" : "-");
    strcat (s, m & S_IWGRP ? "w" : "-");
    strcat (s, m & S_IXGRP ? "x" : "-");
    strcat (s, m & S_IROTH ? "r" : "-");
    strcat (s, stm & S_IWOTH ? "w" : "-");
    strcat (s, m & S_IXOTH ? "x" : "-");
    if (m & S_ISUID)
        s[3] = (s[3] == 'x' ? 's' : 'S');
    if (m & S_ISGID)
        s[6] = (s[6] == 'x' ? 's' : 'S');
    if (m & S_ISVTX)
        s[9] = (s[9] == 'x' ? 't' : 'T');
    return s;
}
const char* ttos (time_t t) {
    static char s[20];
    struct tm* lt = localtime (&t);
    sprintf (s, "%04d-%02d-%02d %02d:%02d:%02d",
        lt->tm_year + 1900,
        lt->tm_mon + 1,
        lt->tm_mday,
        lt->tm_hour,
        lt->tm_min,
        lt->tm_sec);
    return s;
}
int main (int argc, char* argv[]) {
    if (argc < 2)
        goto usage;
    struct stat st;
    if (argc < 3) {
        if (stat (argv[1], &st) == -1) {
            perror ("stat");
            return -1;
        }
    }
    else if (! strcmp (argv[2], "-l")) {
        if (lstat (argv[1], &st) == -1) {
            perror ("lstat");
            return -1;
        }
    }
    else
        goto usage;
    printf ("           设备ID:%lld\n", st.st_dev);
    printf ("          i节点号:%ld\n", st.st_ino);
    printf ("             模式:%s\n",
        mtos (st.st_mode));
    printf ("         硬链接数:%u\n", st.st_nlink);
    printf ("           属主ID:%u\n", st.st_uid);
    printf ("           属组ID:%u\n", st.st_gid);
    printf ("       特殊设备ID:%lld\n", st.st_rdev);
    printf ("         总字节数:%ld\n", st.st_size);
    printf ("      I/O块字节数:%ld\n", st.st_blksize);
    printf ("占用块(512字节)数:%ld\n", st.st_blocks);
    printf ("     最后访问时间:%s\n",
        ttos (st.st_atime));
    printf ("     最后修改时间:%s\n",
        ttos (st.st_mtime));
    printf (" 最后状态改变时间:%s\n",
        ttos (st.st_ctime));
    return 0;
usage:
    fprintf (stderr, "用法:%s <文件> [-l]\n",argv[0]);
    return -1;
}
 

四、access

#include

int access (
const char* pathname, // 文件路径
int mode // 访问模式
);

  1. 按实际用户ID和实际组ID(而非有效用户ID和有效组ID),
    进行访问模式测试。

  2. 成功返回0,失败返回-1。

  3. mode取R_OK/W_OK/X_OK的位或,
    测试调用进程对该文件,
    是否可读/可写/可执行,
    或者取F_OK,测试该文件是否存在。

范例:access.c

五、umask

可以用umask命令查看/修改当前shell的文件权限屏蔽字:

umask

0022

umask 0033

umask

0033

#include

mode_t umask (
mode_t cmask // 屏蔽字
);

  1. 为进程设置文件权限屏蔽字,并返回以前的值,
    此函数永远成功。

  2. cmask由9个权限宏位或组成(直接写八进制整数形式亦可,
    如022 - 屏蔽属组和其它用户的写权限):

S_IRUSR(S_IREAD) - 属主可读
S_IWUSR(S_IWRITE) - 属主可写
S_IXUSR(S_IEXEC) - 属主可执行

S_IRGRP - 属组可读
S_IWGRP - 属组可写
S_IXGRP - 属组可执行

S_IROTH - 其它可读
S_IWOTH - 其它可写
S_IXOTH - 其它可执行

  1. 设上屏蔽字以后,此进程所创建的文件,
    都不会有屏蔽字所包含的权限。

范例:umask.c

六、chmod/fchmod

修改文件的权限。

#include

int chmod (
const char* path, // 文件路径
mode_t mode // 文件权限
);

int fchmod (
int fd, // 文件路径
mode_t mode // 文件权限
);

成功返回0,失败返回-1。

mode为以下值的位或(直接写八进制整数形式亦可,
如07654 - rwSr-sr-T):

S_ISUID - 设置用户ID
S_ISGID - 设置组ID
S_ISVTX - 粘滞

S_IRUSR(S_IREAD) - 属主可读
S_IWUSR(S_IWRITE) - 属主可写
S_IXUSR(S_IEXEC) - 属主可执行

S_IRGRP - 属组可读
S_IWGRP - 属组可写
S_IXGRP - 属组可执行

S_IROTH - 其它可读
S_IWOTH - 其它可写
S_IXOTH - 其它可执行

七、chown/fchown/lchown

chown :

修改文件的属主和属组。

#include

int chown (
const char* path, // 文件路径
uid_t owner, // 属主ID
gid_t group // 属组ID
);

int fchown (
int fildes, // 文件描述符
uid_t owner, // 属主ID
gid_t group // 属组ID
);

int lchown (
const char* path, // 文件路径(不跟踪软链接)
uid_t owner, // 属主ID
gid_t group // 属组ID
);

成功返回0,失败返回-1。

注意:

  1. 属主和属组ID取-1表示不修改。

  2. 超级用户进程可以修改文件的属主和属组,
    普通进程必须拥有该文件才可以修改其属主和属组。

八、truncate/ftruncate

修改文件的长度,截短丢弃,加长添零。

#include

int truncate (
const char* path, // 文件路径
off_t length // 文件长度
);

int ftruncate (
int fd, // 文件描述符
off_t length // 文件长度
);

成功返回0,失败返回-1。

范例:trunc.c、mmap.c

注意:对于文件映射,
私有映射(MAP_PRIVATE)将数据写到缓冲区而非文件中,
只有自己可以访问。
而对于内存映射,
私有(MAP_PRIVATE)和公有(MAP_SHARED)没有区别,
都是仅自己可以访问。

九、link/unlink/remove/rename

link: 创建文件的硬链接(目录条目)。

unlink: 删除文件的硬链接(目录条目)。
只有当文件的硬链接数降为0时,文件才会真正被删除。
若该文件正在被某个进程打开,
其内容直到该文件被关闭才会被真正删除。

remove: 对文件同unlink,
对目录同rmdir (不能删非空目录)。

rename: 修改文件/目录名。

#include

int link (
const char* path1, // 文件路径
const char* path2 // 链接路径
);

int unlink (
const char* path // 链接路径
);

#include

int remove (
const char* pathname // 文件/目录路径
);

int rename (
const char* old, // 原路径名
const char* new // 新路径名
);

成功返回0,失败返回-1。

注意:硬链接只是一个文件名,即目录中的一个条目。
软链接则是一个独立的文件,
其内容是另一个文件的路径信息。

十、symlink/readlink

symlink: 创建软链接。目标文件可以不存在,
也可以位于另一个文件系统中。

readlink: 获取软链接文件本身(而非其目标)的内容。
open不能打开软链接文件本身。

#include

int symlink (
const char* oldpath, // 文件路径(可以不存在)
const char* newpath // 链接路径
);

成功返回0,失败返回-1。

ssize_t readlink (
const char* restrict path, // 软链接文件路径
char* restrict buf, // 缓冲区
size_t bufsize // 缓冲区大小
);

成功返回实际拷入缓冲区buf中软链接文件内容的字节数,
失败返回-1。

范例:slink.c

十一、mkdir/rmdir

mkdir: 创建一个空目录。

rmdir: 删除一个空目录。

#include

int mkdir (
const char* path, // 目录路径
mode_t mode // 访问权限,
// 目录的执行权限(x)表示可进入
);

#include

int rmdir (
const char* path // 目录路径
);

成功返回0,失败返回-1。

十二、chdir/fchdir/getcwd

chdir/fchdir: 更改当前工作目录。
工作目录是进程的属性,只影响调用进程本身。

getcwd: 获取当前工作目录。

#include

int chdir (
const char* path // 工作目录路径
);

int fchdir (
int fildes // 工作目录描述符(由open函数返回)
);

成功返回0,失败返回-1。

char* getcwd (
char* buf, // 缓冲区
size_t size // 缓冲区大小
);

成功返回当前工作目录字符串指针,失败返回NULL。

十三、opendir/fdopendir/closedir/readdir/rewinddir/telldir/seekdir

opendir/fdopendir: 打开目录流。

closedir: 关闭目录流。

readdir: 读取目录流。

rewinddir: 复位目录流。

telldir: 获取目录流当前位置。

seekdir: 设置目录流当前位置。

#include
#include

DIR* opendir (
const char* name // 目录路径
);

DIR* fdopendir (
int fd // 目录描述符(由open函数返回)
);

成功返回目录流指针,失败返回NULL。

int closedir (
DIR* dirp // 目录流指针
);

成功返回0,失败返回-1。

struct dirent* readdir (
DIR* dirp // 目录流指针
);

成功返回下一个目录条目结构体的指针,
到达目录尾(不置errno)或失败(设置errno)返回NULL。

struct dirent {
ino_t d_ino; // i节点号
off_t d_off; // 下一条目的偏移量
// 注意是磁盘偏移量
// 而非内存地址偏移
unsigned short d_reclen; // 记录长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件名
};

d_type取值:

DT_DIR - 目录
DT_REG - 普通文件
DT_LNK - 软链接
DT_BLK - 块设备
DT_CHR - 字符设备
DT_SOCK - Unix域套接字
DT_FIFO - 有名管道
DT_UNKNOWN - 未知

图示:de.bmp
c/c++ 复习笔记 第六天 文件系统_第2张图片

练习:打印给定路径下的目录树。

void rewinddir (
DIR* dirp // 目录流指针
);

long telldir (
DIR* dirp // 目录流指针
);

成功返回目录流的当前位置,失败返回-1。

void seekdir (
DIR* dirp, // 目录流指针
long offset // 位置偏移量
);

c/c++ 复习笔记 第六天 文件系统_第3张图片

#include
#include
#include
#include
#include
int printdir(const char* path,int depth){
        DIR *dp=opendir(path);
        if(!dp){
                perror("opendir");
                return -1;
        }
        depth= depth+2;
        if(chdir(path)==-1){
                perror("chdir");
                return -1;
        }
        struct dirent* dir;
        errno=0;
        for(dir=readdir(dp);dir;dir=readdir(dp)){
                if(dir->d_type!=DT_DIR)
                        printf("%*s%s\n",depth,"|_",dir->d_name);
                else if(strcmp(dir->d_name,".")&&strcmp(dir->d_name,"..")){
                        printf("%*s\033[1;43;35m%s\033[0m\n",depth,"|_",dir->d_name);
                        printdir(dir->d_name,depth);
                }
        }
        if(errno){
                perror("readdir");
                return -1;
        }
        if(chdir("..")==-1){
                  perror("chdir");
                return -1;
        }
        closedir(dp);
        return 0;
}
int main(int argc,char* argv[]){
        if(argc<2){
                fprintf(stderr,"用法:%s <目录>\n",argv[0]);
                return -1;
        }
        return printdir(argv[1],0);
}

~/uc/day06/my$ gcc my_mulu.c
~/uc/day06/my$ ./a.out .
|_111
|_222
|_333
|_aac
|_aab
|_aa1
|_aaa
|_aab
|_a.out
|_my_mulu.c
|_my_dir.c
|_1111
|_2222
|_ddd
|_3333
|_ccc
|_aaa
|_bbb

你可能感兴趣的:(C,c++)