文件IO编程一:文件概念及系统IO

一、文件IO概念

1、什么是文件

        在Linux下,一切皆文件。

        除了我们平时常见的文件,如:1.txt 、2.jpg 、3.bmp ...... 这些文件之外,Linux系统中还会将硬件设备当做是文件,比如:LED灯、蜂鸣器、LCD液晶屏、触摸屏...这些硬件设备在Linux眼里都是文件。

2、什么是IO

        IO ---》inoput / output -->输入和输出

        所谓的文件IO ---》对文件数据的输入和输出 ---》将数据写入到文件(输入)    从文件中读取数据出来(输出)

3、如何实现文件读取/写入?

        不需要用户自定义函数实现,因为linux中,已经有现成的函数来实现。

        访问文件的方式有两种:

        (1)系统IO

        open   read write
        系统IO ---》系统调用 ---2   System calls (functions provided by the kernel) --->系统IO接口 

        (2)标准IO

        fopen fread fwrite fclose
        标准IO --->标准C库的接口 ---3   Library calls (functions within program libraries)

4、系统IO 与 标准IO 的区别

      (1)系统IO是 linux内核提供给应用层开发的接口,标准IO 是标准C库里面的接口。实际上,标准IO是 系统IO的封装。

        (2)  系统IO处理文件,没有缓冲区,直接按字节来处理。标准IO 处理文件,有缓冲区,数据按块来处理。

        (3)作用对象:

        访问硬件设备(lcd液晶屏、触摸屏、蜂鸣器...) ----》使用系统IO来处理。

        比如: 触摸屏 --》使用系统IO接口来处理 触摸屏的数据

        访问普通文件(1.txt 2.jpg 3.bmp...)--->使用标准IO(接口功能丰富)来处理

        比如:访问 :1.txt文件  ---》使用 标准IO

5、文件类型(7种)

        -   普通文件  ---》标准IO
        d   目标文件  ---》标准IO中的目录IO
        l   链接文件
        p   管道文件  ---》系统IO  进程之间的通信方式
        s   套接字文件 --》 网络编程
        c   字符设备文件 ---》系统IO
        b   块设备文件 - ---系统IO 

二、如何使用系统IO来访问文件

1、打开文件(open)

打开文件 ---->man 2 open

头文件:
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

函数原型:
       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

函数作用:
        打开一个文件或者创建一个文件
函数参数:
        pathname --》你要打开文件的路径名   路径+名字      路径:相对路径   绝对路径
                    ./1.txt   /home/gec/1.txt 
        flags   ---》你要以什么方式打开这个文件
                     只能三选一 
                     
                     O_RDONLY  readonly  只读
                     O_WRONLY  writeonly  只写
                     O_RDWR.   readwrite  可读可写

比如: 以可读的方式 打开当前目录下的1.txt 
        open("1.txt",O_RDONLY);
                     
函数返回值:
       成功返回  一个文件描述符fd(这个文件描述符就是文件的代号,用来代表这个打开的文件)
       失败返回  -1 

2、关闭文件 --回收文件描述符资源(close)

头文件:
      #include <unistd.h>
函数原型:
       int close(int fd);
函数参数:
        fd ---》你要关闭哪个文件,将这个文件的文件描述符传递进来即可

函数返回值:       
    成功返回  0
    失败 返回  -1

3、相关编程练习

        (1)练习1:自己用编程实现,打开一个文件,并且关闭。如何判断 是否成功打开一个文件或者关闭文件,使用返回值进行判断。

        注意: open函数 什么时候会打开失败??

            1)你要打开的这个文件不存在
            2)如果文件本身的权限不允许,那么操作权限不对,也会失败。
                比如 1.txt -wx-wx-wx    没有可读权限,此时以O_RDONLY的方式打开会失败

//1-open.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
	//打开当前目录下的1.txt 
	int fd = open("./1.txt", O_RDONLY);
	if(fd != -1)//说明打开成功
	{
		printf("open 1.txt succuss\n");
	}else{
		printf("open 1.txt error\n");
	}
	
	printf("fd:%d\n",fd);
	
	//关闭文件
	int ret = close(fd);
	if(ret != -1)
	{
		printf("close 1.txt succuss\n");
	}else{
		printf("close 1.txt error\n");
	}
	
	return 0;
}

        gcc编译后的运行结果如下:

        (2)打开多个文件,自己打印,看看文件描述符的值是多少???

//2-open.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
	 //打开当前目录下的1.txt 
	int fd1 = open("./1.txt", O_RDONLY);
	if(fd1 != -1)//说明打开成功
	{
		printf("open 1.txt succuss\n");
	}else{
		printf("open 1.txt error\n");
	}
	int fd2 = open("./2.txt", O_RDONLY);
	if(fd2 != -1)//说明打开成功
	{
		printf("open 1.txt succuss\n");
	}else{
		printf("open 1.txt error\n");
	}
	
	printf("fd1:%d\n",fd1); //fd1 = 3
	printf("fd2:%d\n",fd2); //fd2 = 4
	
	//关闭文件
	int ret1 = close(fd1);
	if(ret1 != -1)
	{
		printf("close 1.txt succuss\n");
	}else{
		printf("close 1.txt error\n");
	}
	
	//关闭文件
	int ret2 = close(fd2);
	if(ret2 != -1)
	{
		printf("close 1.txt succuss\n");
	}else{
		printf("close 1.txt error\n");
	} 
	
	return 0;
}

        编译运行,结果如下:

        (3)实现打开文件时,当文件不存在时,去创建这个文件

//3-open.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
	//打开当前目录下的a.txt,如果该文件不存在,则创建
	//如果该文件已经存在,则清空里面的内容
	int fd = open("./a.txt",O_RDWR|O_CREAT|O_TRUNC,0777);
	if(fd == -1)
	{
		printf("open a.txt error");
		return -1;
	}
	
	close(fd);
	
	return 0;
}

        编译运行,结果如下所示:

4、思考:为什么会是这个值?

        下面文件描述符的分配会讲解这个值是怎么来的。

三、文件描述符的分配

1、什么是文件描述符?

        文件描述符fd是open函数的返回值,当open()函数执行成功后,就会返回 一个非负、最小的、没有使用过的整数。

        如:

        3 = open("1.txt");
        4 = open("2.txt");

        结论:将来处理文件时,我们不需要提供文件的名字,只需要提供文件对应的文件描述符即可。

2、访问文件时 ,fd从 3开始分配,说明 0/1/2已经被占用,到底是谁用了?

        其实在系统启动的时候,就会默认打开3个文件,分别是 “标准输入”  “标准输出” "标准出错"

        这些其实就是 宏定义 ,在/usr/include/unistd.h 头文件中:

/* Standard file descriptors.  */
#define	STDIN_FILENO	0	/* Standard input.  */    “标准输入”  ---》对象:键盘
#define	STDOUT_FILENO	1	/* Standard output.  */   “标准输出”   ---》对象:屏幕
#define	STDERR_FILENO	2	/* Standard error output.  */  "标准出错" ---》对象:屏幕

        可以理解为  : 0  = open("标准输入");

        fd是 一个非负、最小的、没有使用过的整数。
        结论: 
        打开一个文件,得到一个文件描述符
        关闭一个文件,这个文件对应的文件描述符可以被别人使用

3、研究文件描述符的最大值

while(1)
{
	int fd = open("./1.txt", O_RDONLY);
	if(fd == -1)
	{
		printf("open error:%d\n",fd);
		break;
	}
	printf("fd:%d\n",fd);
}

        运行结果:

        范围: 0-1023  //其中 0  1  2 是已经被系统占用了

        记住:文件描述符的资源有限,打开 文件之后,记得一定要关闭。

四、open函数的拓展参数

int open(const char *pathname, int flags, mode_t mode);


flags   ---》你要以什么方式打开这个文件
                     只能三选一 
                     
                     O_RDONLY  readonly  只读
                     O_WRONLY  writeonly  只写
                     O_RDWR.   readwrite  可读可写
            以下还有一些参数 可以跟上面使用 位或 | 并存

            O_CREAT  --》如果你打开的那个文件不存在,那么就会创建。
                        ---》如果flags 有O_CREAT,那么第三个参数一定要填。
                                mode:文件的权限 0777  0666
                        ---》如果flags 没有O_CREAT,第三参数 你填了没用。

比如:
    //打开当前目录下的a.txt,如果该文件不存在,则创建
    int fd = open("./a.txt",O_RDWR|O_CREAT,0777);

            O_TRUNC ---》如果文件已经存在并且是一个普通文件,而且打开的方式必须是(O_WRONLY或者是O_RDWR)
                        那么这个文件 就会打开的时候会被清空


比如: 

    //打开当前目录下的a.txt,如果该文件不存在,则创建
    //如果该文件已经存在,则清空里面的内容
    int fd = open("./a.txt",O_RDWR|O_CREAT|O_TRUNC,0777);


            O_APPEND---》 以追加的方式打开文件。也就是说,默认打开文件的时候,文件光标位置是在开头,
            那么使用了这个宏O_APPEND,在打开文件的同时,把文件光标的位置偏移到文件的尾部。

五、文件数据的输出、输出

1、如何读取文件的数据?(read)

        man 2 read

头文件:
 #include <unistd.h>

 函数原型:
       ssize_t read(int fd, void *buf, size_t count);

函数作用:从文件fd中读取数据,存储缓冲区buf,读取 的大小为count

参数:
        fd--->你要读取的那个文件的文件描述符
        buf--> 读取的数据,存储在这里buf
        count-->尝试读取的大小,以字节为单位

返回值:
        成功返回  从文件中读取到的字节数
        失败 返回  -1


验证: 打开一个文件之后,读取文件的数据,接着再次读取,这个时候是从头开始读 还是 说 接着 后面继续读取呢????    

答案:继续读取。因为文件光标会进行偏移。

//4-read.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>

int main()
{
	//打开a.txt文件,从文件中读取数据
	int fd = open("a.txt",O_RDONLY);
	if(fd == -1)
	{
		printf("open error\n");
		return -1;
	}
	
	char buf[1024];
	int r_ret;
	
	while(1)
	{
		bzero(buf,1024);//清空数组内存空间
		r_ret = read(fd,buf,10);
		if(r_ret == 0)//已经读完了
		{
			printf("\nfile read end\n");
			break;
		}
		printf("r_ret:%d\n",r_ret);
		printf("%s\n",buf);
	}
	
	close(fd);
	
	return 0;
}

运行结果如下:


注意: 

1)从任何的文件中读取数据出来,都是以字符串的形式存在的。
2)如果一个文件很大,可以写一个循环每次获取指定的字节数,当read返回值 为 0时,说明这个文件已经读完了,则退出。

2、如何将数据写入文件中?(write)

        man 2  write

头文件:
        #include <unistd.h>
函数原型:
       ssize_t write(int fd, const void *buf, size_t count);

函数作用:将指定的数据buf 写入到指定的文件fd中,写入的数据为count字节

参数:
        fd ---》你要写入哪个文件,将这个文件的文件描述符传递进来
        buf ---》你要写入的数据
        count--》你写入数据的大小
返回值:
        成功返回 写入的字节数 
        失败返回  -1
    
注意: 

1)有多少个字节的数据,第三个参数count就写多少。如果你写入的字节数count比真实的数据要大,那么文本的后面会有乱码。(文本不够,乱码来凑)

//5-write.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
int main()
{
	//以可写的方式打开a.txt文件,如果这个文件不存在则新建,如果存在了则清空内容
	//int fd = open("a.txt",O_WRONLY|O_CREAT|O_TRUNC,0777);
	
	//以追加的方式打开文件,也就是文件光标的位置在文件的尾部
	int fd = open("a.txt",O_WRONLY|O_APPEND);
	if(fd == -1)
	{
		printf("open error\n");
		return -1;
	}
	int w_ret;
	char buf[ ] = "hello";
	
	w_ret = write(fd,buf,strlen(buf));
	
	strcpy(buf,"world");
	w_ret = write(fd,buf,strlen(buf));
	
	close(fd);
	
	return 0;
}

运行结果:


验证:打开一个文件之后,(第一次)将数据写入到文本中,接着(第二次)再将其他数据写入到文本中
     那么,第二次写入的数据,是从头开始写呢  还是接着后面写?? --接着后面写
     
    int w_ret;
    char buf[ ] = "hello";
    
    w_ret = write(fd,buf,strlen(buf));
    
    strcpy(buf,"world");
    w_ret = write(fd,buf,strlen(buf));
           

结论: open函数打开文件时,文件定位都是在开头,文件定位(文件光标的位置)随着 读取/写入字节 而往后面进行偏移。

六、练习题

1、练习题1

写一个程序,实现以下功能:
    注册:
        从键盘上输入账号,保存在login.txt文本中
        从键盘上输入密码,保存在passwd.txt文本中
    登录:
        输入账号的时候,验证与 login.txt文本 中的内容是否一样
            如果不一样,则提示账号输入错误
        输入密码的时候,验证与 passwd.txt文本 中的内容是否一样
            如果不一样,则提示密码输入错误2

//test1.c
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <stdbool.h>


enum {
	Register = 1,
	Login = 2,
	Exit  = 3,
};

#define ACOUNTFILE	"./login.txt"
#define PASSWDFILE	"./passwd.txt"


//#define DEBUG

/*
1、写一个程序,实现以下功能:
	注册:
		从键盘上输入账号,保存在login.txt文本中
		从键盘上输入密码,保存在passwd.txt文本中
	登录:
		输入账号的时候,验证与 login.txt文本 中的内容是否一样
			如果不一样,则提示账号输入错误
		输入密码的时候,验证与 passwd.txt文本 中的内容是否一样
			如果不一样,则提示密码输入错误
			如果不一样,则提示密码输入错误
*/

//注册功能
bool registerFunc()
{
	//0、从键盘上获取注册的账号和密码
	char registerAcount[256]={0};
	printf("请注册账号:");
	scanf("%s",registerAcount);
	
	char registerPasswd[256]={0};
	printf("请注册密码:");
	scanf("%s",registerPasswd);
	
	
	//1、先打开文本,如果文本不存在,则新建,如果存在了,清空
	int loginfd = open(ACOUNTFILE,O_WRONLY|O_CREAT|O_TRUNC,0777);
	if(loginfd == -1)
	{
		printf("open login.txt error\n");
		return false;
	}
	int passwdfd = open(PASSWDFILE,O_WRONLY|O_CREAT|O_TRUNC,0777);
	if(passwdfd == -1)
	{
		printf("open passwd.txt error\n");
		return false;
	}

	//2、将刚才从键盘上输入的账号和密码写入文本中
	write(loginfd,registerAcount,strlen(registerAcount));
	write(passwdfd,registerPasswd,strlen(registerPasswd));
	
	//3、关闭
	close(loginfd);
	close(passwdfd);
}
//登录功能 
bool loginFunc()
{
	//1、从键盘上获取输入的账号
	char loginAcount[256]={0};
	printf("请输入账号:");
	scanf("%s",loginAcount);
	
	//2、 打开存在login.txt文本,读取注册的账号
	int loginfd = open(ACOUNTFILE,O_RDONLY);
	if(loginfd == -1)
	{
		printf("open login.txt error\n");
		return false;
	}
	//从文本中 读取注册的账号
	char registerAcount[256]={0};
	read(loginfd,registerAcount,256);
	
	#ifdef DEBUG
		printf("loginAcount:%s\n",loginAcount);
		printf("registerAcount:%s\n",registerAcount);
	#endif
	
	//3、从键盘上获取的账号  和  文本中的账号 进行 比较,如果不相等,则提示账号错误
	if(strcmp(loginAcount,registerAcount) != 0)
	{
		printf("你输入的账号错误,登录失败\n");
		return false;
	}
	//1、从键盘上获取输入的密码
	char loginPasswd[256]={0};
	printf("请输入密码:");
	scanf("%s",loginPasswd);
	//2、 打开存在passwd.txt文本,读取注册的密码
	int passwdfd = open(PASSWDFILE,O_RDONLY);
	if(passwdfd == -1)
	{
		printf("open passwd.txt error\n");
		return false;
	}
	//从文本中 读取注册的密码
	char registerAasswd[256]={0};
	read(passwdfd,registerAasswd,256);
	
	//3、从键盘上获取的密码  和  文本中的密码 进行 比较,如果不相等,则提示账号错误
	if(strcmp(loginPasswd,registerAasswd) != 0)
	{
		printf("你输入的密码错误,登录失败\n");
		return false;
	}
	
	return true;
}

int main()
{
	bool exitFlag = false;
	
	while(1)
	{
		int mode=0;
		printf("[1]注册 [2]:登录 [3]退出:");
		scanf("%d",&mode);
		
		#ifdef DEBUG
			printf("mode:%d\n",mode);
		#endif
		switch(mode)
		{
			case Register:
							registerFunc();
							break;
			case Login:
							if(loginFunc())
								printf("-----登录成功----\n");
							else 
								printf("-----登录失败-----\n");
							break;
			case Exit:
							exitFlag = true;	
							break;
		}
		if(exitFlag)
			break;
	}
	
	
	return 0;
}

2、练习题2

学生信息结构体
    struct student
    {
        char name[15];
        int num;
        char sex;
    };
    struct student lisi = {"lisi", 20, 'B'};
    

写两个接口:
1)保存  
    从键盘上获取一个学生的信息(结构体变量的数据),保存到info.txt文本
           
2)初始化读取   
    程序一开始运行的时候要进行一些初始化操作,这个时候从info.txt文本中读取数据,进行初始化

//test2.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define  INFOFILE	"./info.txt"

enum {
	Print = 1,
	Change = 2,
	Exit  = 3,
};

/* 2、学生信息结构体
	struct student
	{
		char name[15];
		int age;
		char sex;
	};
	struct student lisi = {"lisi", 20, 'B'};

写两个接口:
1)保存  
	从键盘上获取一个学生的信息(结构体变量的数据),保存到info.txt文本
	       
2)初始化读取   
	程序一开始运行的时候要进行一些初始化操作,
	这个时候从info.txt文本中读取数据,进行初始化 */

struct student
{
	char name[256];
	int age;
	char sex;
};

struct student lisi;

bool saveInfo();
bool initData();

void printFunc()
{
	printf("name:%s age:%d sex:%c\n",lisi.name,lisi.age,lisi.sex);
}
//修改功能
void changeFunc()
{
	printf("请输入姓名:");
	scanf("%s",lisi.name);
	
	printf("请输入年龄:");
	scanf("%d",&lisi.age);
	
	//吸收换行符   //  20\n
	getchar();
	
	printf("请输入性别:");
	scanf("%c",&lisi.sex);
		
	//保存
	saveInfo();
}

int main()
{
	bool exitFlag = false;
	
	//从文本中加载学生的信息
	initData();
	
	while(1)
	{
		int mode=0;
		printf("[1]显示 [2]:修改 [3]退出:");
		int ret = scanf("%d",&mode); //scanf 成功返回 获取到的变量的个数
		if(ret != 1)
		{
			//清空输入缓冲区
			while(getchar() != '\n');
			continue;
		}
			
		switch(mode)
		{
			case Print:
							printFunc();
							break;
			case Change:
							changeFunc();
							
							break;
			case Exit:
							exitFlag = true;	
							break;
		}
		if(exitFlag)
			break;
	}
	
	return 0;
}

//保存学生信息
bool saveInfo()
{
	//打开信息文本,如果存在了清空,如果不存在则创建
	int fd = open(INFOFILE,O_WRONLY|O_CREAT|O_TRUNC,0777);
	if(fd == -1)
	{
		printf("open %s error\n",INFOFILE);
		return false;
	}
	write(fd,&lisi,sizeof(struct student));
	
	close(fd);
}

//初始化显示
bool initData()
{
	int fd = open(INFOFILE,O_RDONLY);
	if(fd == -1)
	{
		printf("open %s error\n",INFOFILE);
		return false;
	}
	read(fd, &lisi, sizeof(struct student));  	
	
	close(fd);
}

3、练习题3

实现复制一个文件

类似于 ./mycp  old_file    new_file

//test3.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define  INFOFILE		"./info.txt"
#define  MALLOCSIZE 	(1024*1024*10)


/* 实现复制一个文件

类似于 ./mycp  old_file    new_file */

/*
getFileSize:得到文件的大小
参数:
	fd-->文件描述符
返回值:
	返回文件的大小,以字节为单位
*/
int getFileSize(int fd)
{
	int fileSize =0;
	
	//1、先保存此时文件的光标位置
	int pos = lseek(fd,0,SEEK_CUR);//current
	
	//2、将文件设置到末尾 ,该lseek函数返回值就是文件的大小
	fileSize = lseek(fd,0,SEEK_END);
	
	//3、恢复文件之前的光标位置(还原)
	lseek(fd,pos,SEEK_SET);
	
	return fileSize;
}

int main(int argc,char*argv[]) //./tes3  1.txt  2.txt
{
	float readByteCount=0;//记录读取的字节数
	
	if(argc<3)
	{
		printf("你输入的参数太少了,请把源文件 和 拷贝文件名传递进来\n");
		return -1;
	}
	//1、以可读的方式打开源文件 1.txt   argv[1]
	int srcfd = open(argv[1],O_RDONLY);
	if(srcfd == -1)
	{
		printf("open %s error\n",argv[1]);
		return -1;
	}
	//2、以可写的方式打开目标文件 2.txt ,如果目标文件不存在则新建,如果存在则清空
	int destfd = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0777);
	if(destfd == -1)
	{
		printf("open %s error\n",argv[2]);
		return -1;
	}
	//申请内存空间存储读取的数据
	char *buf = malloc(MALLOCSIZE);//申请1M的空间
	if(buf == NULL)
	{
		printf("malloc error\n");
		return -1;
	}
	//获取源文件的大小
	int srcfileSize = getFileSize(srcfd);
	printf("srcfileSize:%d\n",srcfileSize);
	
	while(1)
	{	
		//先清空
		bzero(buf,MALLOCSIZE);
		//3、读取源文件的数据 
		int r_byte = read(srcfd,buf,MALLOCSIZE);
		if(r_byte == 0)//读取完毕
		{
			printf("%s file read end\n",argv[1]);
			break;
		}
		//累加读取到的字节数
		readByteCount +=r_byte;
		//4、将读取到的源文件数据  写入到 目标文件中
		write(destfd,buf,r_byte);
		
		printf("已读取:%fM 进度:%f%%\n",readByteCount/1024/1024,readByteCount/srcfileSize*100); // 1M = 1024kb   1kb = 1024字节
	}
		
	//5、写完了,关闭两个文件
	close(srcfd);
	close(destfd);
	
	//释放内存空间
	free(buf);
	
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值