Contents

从一条内裤说起

开源内裤,是OSCHINA的主题物品,也算极品程序员必备之内裤。其区别一般内裤的地方就在于存在一句名言“hello world!"。

本篇的目标,就是我们如何用C语言作为工具,在屏幕上打印出hello world!(目前我们尚没有自动化设备,能在内裤上打印,抱歉)。

要想在屏幕上打印出hello world!。我们需要一个程序,但非常明确的,此时和C语言没有任何关系。假设我们在linux的当前目录下存在这样的程序,hello。则在inux的命令行中,敲入./hello。回车。此时,我们希望屏幕有个"hello world!"语句出现(""不需要考虑).

这里我们不得不先说明一些环境,假设我们是在linux下,讨论C语言的开发(注意不是讨论C语言),那么相关linux命令行的命令,linux的文本编辑器(用于敲入C代码),GCC开发工具,都需要准备到位。(我会在参考部分,给出 ubuntu下的开发环境的配置说明)。

鬼话:

学习C语言,始终是为了利用C语言文本,转换成可执行的文件,并执行。重点在两头,一头是根据语法设计文本,切莫认为那一堆可以看懂的字符就是程序,这事惯性误区,经常大家叫,把程序上传,把程序更新,其实说的都是代码文本。而另一头,是我们如何将代码文本转换成真正可执行的程序,也即命令机器,在屏幕上打印出hello world。再次强调,目前你能找到的计算机,暂时无法做出开源内裤,这不属于我们讨论对范围。

在进入下面的C语言正式讨论前,希望你手上备有以下几本资料,无论电子还是纸纸,无论是偷是抢,这些我都不在乎,但没有他们,很多信息无法对应。

  1. ISO/IEC 9899:201X Programming languages -C。

    • 这是2011版的C国际标准。不要怀疑作为初学者,此为第一本资料。老师告诉你,看教科书,不妨我说一句,教科书算老几啊?你没有国际标准,你如何对规范有最权威的理解。学会使用国际标准是你要掌握的基本能力。教科书只能让你应付考试,而国际标准可以令你找到教科书的错误。
  2. GCC -Complete Reference(GCC完全参考手册)

    • GCC是个好东西。GCC是什么,一个编译工具的大集合。当然也包括对C语言的编译工具。前面说的是C语言的标准。但标准只是一个规范,并不是具体的工具,GCC是众多C语言编译器中,品质优良,使用广泛的一种。当然你可以用微软的编译器,也可以用intel的编译器,后者在intel的CPU上,编译能力更强,效果更好。但考虑到C是被绝大多数硬件平台作为必备支持语言,大多数硬件平台的C编译器来源或借鉴了GCC的内容和使用习惯,这两个事实,因此,不学GCC,是个损失。

以上两本属于首先要拥有的。如同你需要在linux下安装gcc一样。否则我们无法沟通。

先完成第一件事,代码文本的设计。如下:

#include <stdio.h>

int main(int argc,char *argv[]){
    printf("hello world!\n");
    return 0;
}

使用gedit,敲入,并做保存,保存为hello.c文件。 再完成第二件事,依次几个步骤如下:

gcc -c hello.c   
gcc hello.o -o  print_hello 

此时,ls下当前目录。查看是否有hello.o print_hello两个文件。如果有,则执行 ./print_hello。看是否能打印出我们想要看到的内容。

如果 hello.o 不存在,表示gcc -c hello.c 存在问题或者文本没有正确敲对。通常是你gcc没有安装好。而对于./print_hello 没有反应,或者报错,不是你的前面文本没有正确敲对,就是上述命令没有敲对,记得大小写是敏感的。也即,g和G不是一个含义。

此时,我们完成了利用C语言实现开源内裤之屏幕显示法。看似很简单。不过却有很多基础的内容在本篇需要展开讨论。

首先我们看下第一步骤。文本的输入部分。

说个题外话。为什么我选择gedit。答案两条,其他编辑器不熟练,gedit够简单。编辑器就是编辑器,完全根据你的熟练所长来选择,诸如VIM等也是利器,无非你是否愿意使用,这和你的C语言编程能力毛关系没有。不要纠结使用哪种编辑器更潮流。争比谁的编辑器更好,就如同你和文印店的小妹妹比拼WORD的熟练程度一样,对C语言设计毫无意义,因为无论WORD提供多么强大的编辑功能,你多么牛的自豪宣称熟练掌握一个编辑利器,回头还得按照最基本文本格式,提供给第二步骤。 牛有啥用?还是傻眼两只。

从上述文本第一行说起。#include是什么?这是一个预处理的关键字。后面跟的是文本文件名。有两种写法,一种是"",一种是<>。参考资料1中,6.10.2(164页)有明确说明。记得跟着野鬼学习C,要有两种方式,其一,自己看官方资料,相信我,你的老师,只是为了课时费,而不是官方代言人,我也不是。其二、听听野鬼的鬼话,仅算是一个侧面的口述理解。如果有冲突,鬼话不足信,资料更正确。资料的内容自己找字典翻译理解,此处说句鬼话:

#include你可以看做从当前行,将后续所指引的文件内的文本内容全部插入这个位置,仅此而已。我大约学习C用了2,3年时间才理解这个道理。或许下面的例子,能让你别走我这2,3年的弯路。

1、用gedit编辑一个文件,内容如下

printf("hello world!\n");

保存文件为 aa.bb 。特别怪异的名字,目的是让你知道#include和.h毫无关系。

2、修改上述程序代码如下

#include <stdio.h>

int main(int argc,char *argv[]){
    #include "aa.bb"    
    return 0;
}

保存,执行上述第二步骤。看./print_hello有什么异常?为了确保实验成功,你在执行第二步骤前,将hello.o print_hello给rm掉。

鬼话:十几年前,我这个猪脑子,一直有这样的直观印象,#include 后面一定跟的是.h文件,是程序都需要#include <stdio.h> 很悲摧的是当时我自学的是C语言教材,没人告诉我应该去读读C国际标准,通常认为只有真正的高手,才应该阅读。相信我。如果有所谓的高手或老师,阻止你阅读国际标准,特别是以“以你目前的水平,还没有达到读国际标准的程度”,通常是因为他们能力差到,没有读过,或读了也无法理解标准描述,所以防止你这个小牛犊闯进瓷器店。

再次强调,你可以不相信本书的任何鬼话,但一定要相信标准和官方资料,例如gcc参考大全等和亲自动手实践,(其他的书或许是真正的书,本书更希望是个实践讨论集),而绝不要简单的对教科书唯真理论。这么多年的经验教训使得我绝对是个反教材的主。

当然你完全可以将aa.bb文件,放到环境变量所指定的标准库的路径对应的文件夹内,这样你可以使用<aa.bb>。这里引发了一个问题,如果在本地目录下和标准库目录下,都有相同的文件,那么该引用哪个?哈。不告诉你,其实参考资料1,6.10.2.3说的很清楚,自己看。

说下 int main(int argc,char *argv[]){ .... return XXX; }

这是一个标准的函数。也是一个特殊函数。参考资料1,5.1.2.2.

1中说的非常清楚关于main函数的重要性。简单的鬼话总结如下:

  1. main 有两种写法。其实别图省事,保持良好习惯,坚持使用 int argc ,char *argv[],而不是void。这是你整个程序在启动前,与外部信息对接的接口,留着总有用,何必void掉。

  2. 最左边的int,是一个数据类型,通常是32位宽,注意从现在开始,位宽,地址的概念都要启动了。int后面会展开讨论。此时你关注到,该函数需要返回,返回是一个32位宽的数值,同时由于是有符号类型(参考下面的讨论),因此 232 等数值你就不要考虑了,返回了也不会正确告知别人。

  3. int argc ,char *argv[],这表示一个函数的入口参数。你可以想像一下,当你老婆塞给你张纸条,上面列有一堆采购清单,随后一脚把你踹进超市,那么这个清单,可以比喻做上述入口参数,而这个超市及后续采购发生的故事,算做该函数内部的逻辑,至于你老婆?不好意思,就是把你调入运行的OS主。

  4. {}表示这个函数的代码起始结束。总要有个标记,否则编译器(参考下面的讨论)如何理解,这个函数到那结束呢? 5、由于函数存在返回,因此我们需要使用return XX;你可以想象XX就是超市打印的小票,用于给予你老婆大人,交差用的。

  5. 每个执行程序所对应的一堆C代码,至少要有一个main,参考文献1 ,5.1.2.2.1.1

鬼话: main 函数的int argc ,char *argv[]实在太有用了,以至于我带的小朋友,谁写成(void)方式就算错。参考文献1,5.1.2.2.1.2详细解释了argc ,argv的用途和方法。我们下面做个例子就可以简单了解了。 用gedit修改最初的代码如下:

#include <stdio.h>

int main(int argc,char *argv[]){

    printf("argc is %d\n",argc);
    printf("%s \n",argv[0]);
    printf("%s \n",argv[1]);
    printf("hello world!\n");

    return 0;
}

仍然执行上面的第二部分的步骤。此时我的机器有如下打印: 1 ./print_hello (null) hello world! 如果你尝试这样再敲次命令,也即 print_hello后面跟一个参数

./print_hello param 则会输出 2 ./print_hello param hello world!

现在是否理解了?argc表示argv有几个字符串(后续讨论),而我们的程序名本身,就是第一个字符串,而后续的第一个参数是指第二字符串。记得计算机里,一切从0开始,哈,所以argv[0]是对应./print_hello ,argv[1]正好对应我们所理解的第一个参数,param。你若问,为什么这么巧呢?第一个参数(人的理解)正好对应了参数数组1。我只能回答,历史原因,哈。如果你程序后跟10个命令,则正好对应argv[1]到argv[10]这十个字符串。但记得,总计有11个。同时一定要明确,他始终是字符串,并不是什么数值。如果你希望传入数值给main函数,需要自己做转换(后续展开讨论)。 这里说来说去,没有讨论printf。printf是什么?仍然看资料,参考资料1 ,7.21.6.3,有明确的介绍。实际你应该看7.21.1,意思是说,printf是个函数,其属于stdio.h所对应的标准库。而实际你会发现printf,我们使用了,“”,%d,%s,\n等内容,在7.21.6.1中有明确说明。也即fprintf。

标准库,是C语言标准要求的基本函数所组成的集合。对应不同的目的和功能,整合出不同的子集合,形成对应的库文件,而stdio.h等头文件的目的是告诉编译器,对应函数的接口规范是什么,实际的执行代码是在对应的.o文件中(详细讨论参考下文)。

请相信我的鬼话:

标准库是唯一你能依赖,跨越任何平台均不怕的库。因为如果跨越了新的平台,不支持标准库,那么你大可以拿出标准辱骂它。任何计算机的硬件厂家,在设计一款新的CPU时,如果要支持C语言(基本属于必须支持的高级语言,这是C语言的魅力哈,不支持C都不好意思对客户说我这是CPU)则必须符合C语言的标准。因此标准库是要严格保证的。你想尽可能的另你的代码在跨越平台时,不需要二次修改,那么尽可能的使用标准库里的函数。C语言是难得一见的真正跨平台的语言,很少有CPU厂家敢说自己的芯片没有C语言库,但确实不是所有的CPU厂家都支持JAVA的虚拟机。

关于printf鬼话总结:

  1. \n是个好东西。\n表示行结尾。而且通常对应的库函数在输出给外部模块时,显示模块,\n可以强制将buffer内容传输出去。没有\n,则你得等到buffer满后,所有数据才能被输出到指定位置。这个buffer是什么?其实就是用来暂时缓存你要输出的内容,在参考资料 1,7.21.6.1.15中明确写名,如果你是支持C标准的系统,这个buffer至少要4095个。

  2. 如果你善用printf,并不代表你真正走火入魔,真正牛的C程序员,更喜欢使用fprintf(stdout,"...");在参考资料 1,7.21.6.3.2中明确写明了。C语言对外输出,更多把外部接口资源看作文件,如同饭堂垃圾桶边上的阿姨,眼睛中更多看到的是盘子,并不在意你在吃什么。而显示模块,在C里面正常用stdout,即标准输出来描述。stdout是什么?是在stdio.h中定义的一个值而已,至于数据是多少。自然你可以去查,但重点是,搞清楚,他不是语法中的什么保留字。stdout和stdin一样,都是一个宏定义,看看一个搞笑的注释如下文件路径为 /usr/include/stdio.h:

/* Standard streams.  */
extern struct _IO_FILE *stdin;        /* Standard input stream.  */
extern struct _IO_FILE *stdout;        /* Standard output stream.  */
extern struct _IO_FILE *stderr;        /* Standard error output stream.  */
/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr

这是GCC对标准的嘲笑。#define 和extern struct 等等在后续会做讲解,这里的大概意思是说,stdout, stdin是一个标准的输入输出流,而C标准说他得是一个宏,所以我们就自己定义自己,另他成为一个宏。省得说我不支持标准。哈,现在看到标准的强大了吧。无论你是否嘲笑,你也得跟着标准来。

鬼话: 说说这里的流。实话,10多年前,我很猪头的把流看的很神秘。充满了敬意。其实你可以这么想,通常的流,就是一个先进先出的BUF文件。文件的意思如同上面解释,C里面特别是很多操作系统,把各个设备的接口都当作文件,如同饭堂的阿姨,而BUF的意思是,设备与设备,模块与模块之间的对接时,通常需要有个临时存储区。简单的举例,码头把非洲的木材运到中国,在实际可以销售使用前,需要先在码头的仓库里堆放一下,这算做物流系统的一个对接。而流,就是货物一个个的顺着排放。你可以想想,这个仓库其实就是一个传送带。 货物得一个一个的按顺序传入,而另一端,也是一个一个的接受入库(进入BUF)。

让我们看另一个解释,这里隆重介绍一个网站 http://stackoverflow.com 除了我所提出的参考文献外,一些问答网站也是手上的必备资源。如果 http://stackoverflow.com 没逛过,很难说你是C的大牛。当你在被面试时,针对学习C回答“我经常在 http://stackoverflow.com获取答案”,相信我一句鬼话: 在意你,而刮目相看的,是有水平的。听不懂的,你还是洗洗睡吧,落他手上,迟早把你废了,如果他是C程序员而且将来作为你的领导的话。

参考 http://stackoverflow.com/questions/4056026/fflushstdout-in-c ,这里描述了 MinGW GCC 3.4.5 的一段定义,更为清晰。

typedef struct _iobuf
{
    char*   _ptr;
    int _cnt;
    char*   _base;
    int _flag;
    int _file;
    int _charbuf;
    int _bufsiz;
    char*   _tmpfname;
} FILE;

// oversimplify declaration of _iob[] here for clarity:
extern FILE _iob[FOPEN_MAX];    /* An array of FILE imported from DLL. */
//...
#define STDIN_FILENO    0
#define STDOUT_FILENO   1
#define STDERR_FILENO   2
#define stdin   (&_iob[STDIN_FILENO])
#define stdout  (&_iob[STDOUT_FILENO])
#define stderr  (&_iob[STDERR_FILENO])

iob 是一个文件接口的数组(参考后续讨论),而stdout 是iob[1]这个单元的地址。记得stdout不是指针,是个地址。你可全当城市的某个公共厕所的门牌号码,有什么需要发送的,找到对应门牌,输出就行了。当然别搞错,_iob[0]就是厕所隔壁的快餐店,那是管你上面入口的。落多计算机里,通常stdin就是直键盘输入,stdout就是指屏幕输出。

鬼话:重复一边,野鬼只能教你如何学习,实际内容,需要你更多的阅读相关资料和查找相关网站的相关讨论。这是一个非常有效的套话,遇到问题,请找相关部门,但确实很有用。

现在我们展开讨论下第二步骤。

gcc -c 是什么。gcc -o是什么。首先要明确一个概念。C是面向模块和过程的。针对过程。我们可以看到是一个函数执行,算一个过程,而该函数可以调用另一个函数,如同main 调用printf一样,同时从第三个例子,你也可以看到,C语言可以依次在一个函数里,调用其他函数。这就是过程,我们把一个复杂的过程,拆解为各个独立步骤,用函数实现,此时,整体就可以简单的通过函数掉函数,完成。这也是过程可细分为步骤,每个步骤又可看作一个独立的过程的思维方式。

那么面向模块是什么概念?如果你有螺丝刀,完全可以把你的电脑拆成N多块,当然是否能拼回去并正常使用,就不是我的事情了。至少说明一个问题,一个系统,可以分成不同部分。而之所以要分成不同部分,是因为,不同系统或整体设备可能包含相同的组成。例如你买个鼠标,台式机可以,笔记本也可以。只要接口一致。这可以有效的提高生产效率,并保证设计质量。硬件如此,软件也如此。如果没有面向模块的概念,那么我们会增加很多设计难度。

C语言已经有了模块的约束方法。其实你已经面对了。C语言以每个C文件为一个独立的操作对象。参考文献1 5.1。这里有详细的介绍。每个C文件会被独立操作,完成翻译过程,就是我们说的编译。你可以看到正常的C代码中,有很多头文件,但通过#include的理解,这些头文件,仅是完成内容插入的工作。因此,下面的代码,和案例一,在编译后是完全一致的。

extern int printf (__const char *__restrict __format, ...);
int main(int argc,char *argv[]){
    printf("hello world!\n");
    return 0;
}

这里需要注意,如果你尝试直接使用C标准的函数声明如下: int printf(const char * restrict format, ...); gcc会给出错误。这是因为,gcc对一些相关关键字和约束进行了重新修正。其目的是为了适应环境。但不用担心,如果你只是#include <stdio.h>的话,对应的修正都会完整和正确。这也是为什么我们不使用 extern int printf (const char *restrict format, ...); 而使用#include <stdio.h>的一个原因。如果换了一个编译器,对const等有其他理解甚至不理睬,你就麻烦大了。

回到模块和编译。我们看到 gcc -c hello.c,实际生成了hello.o,这就是默认后缀名的力量。这算是潜规则。如果我们不给出输出文件名,而当你给入的输入文件名是.c后缀,此时gcc会自动认为你是要将hello.c编译为hello.o。

鬼话:做点错事是有好处的。你可以尝试 cp hello.c hello.t,然后执行 gcc -c hello.t。你会发现gcc报错。 先说下 -c,参考文献2 (ch4 70页)。-c则表示需要生成对象文件。当然你可以通过 -o文件来确定实际输出文件名,而不使用默认的方式。例如

gcc -c hello.c -o haha 
gcc haha -o haharun 
./haharun 

那么 gcc haha -o haharun 是什么?这是个链接动作。编译和链接的关联与差异下文描述。先说说 -o。-o其实很简单,就是为了重新拟定输出文件名。如果你不确定名称,那么会按照默认的规则来。而同时,gcc会通过 -c来判断,是只编译处理为对象文件,还是一直处理到执行文件。例如

gcc hello.c -o print_hello 
./print_hello 

假设你在执行上面两个命令前,将其他文件全部清除干净。此时你ls当前目录。会发现只有两个文件。hello.c 和 print_hello。参考文献说的很清楚,会一致执行到链接完成,并将中间的文件对应剔除,而不保存。也即hello.o这玩意没了。

之所以此处会编译,而不是如gcc hello.o -o print_hello 那样仅执行链接,是因为有.c这个后缀告诉gcc该做什么。

鬼话:继续做点错事。cp hello.c hellosource, 执行 gcc hellosource -o print_hello 此时gcc会说文件格式有问题。因为你不是c后缀啊。gcc直接折腾链接了。事情就是这么简单。所以,如果是对源代码即c文件进行处理,那么一定要加上后缀。如果你说,我的C源代码不是以c后缀为文件名怎么处理?你可以考虑自己对gcc的程序进行补充设计,使其可以自动识别文件是文本的源码,还是二进制的obj文件。如果你觉得这样修改gcc更麻烦,因为你要确保别人用你的代码时,你需要附带赠送一套你独特的GCC,那么你还是老实点,C的源代码,一定使用.c后缀。

当然,如前面说的,-o只是需要明确输出文件名。如果我们使用默认的也完全可以。对于编译,很简单,默认的是重名但后缀为.o的文件。可是linux下,从来没有说,通过后缀是.exe或 .com来确定文件是否可执行。那么如果 gcc 不仅仅需要编译,即我们不使用 -c,也不进行 -o指定名称输出,会有什么效果?

鬼话:虽然不算错 事,但实际是个很弱智的事情,我们也做一下。 gcc hello.c ls 一下,看生成了什么?多了个a.out。不要怀疑第二次会有b.out。始终都是a.out这一个默认文件名。也就是说,任何C代码,如果是默认进行链接的工作,那么都生成相同的可执行文件a.out。因此你是无法区分,究竟这个程序是做什么用的。那么准确的说,上述事情虽然弱智,但实际是个错误。或许教科书说,a.out生成了,但对于开发团队而言,谁用a.out,谁请回家,你可以犯错,但不能犯如此的低级错误。

这也是为什么,编译,我们经常看不到-o,而链接都有-o的原因。

鬼话:曾经很猪头的认为, -c就是编译, -o就是链接。指导我看到gcc 参考大全。我猪头过,我不希望现在的新手同样猪头,因此,参考文献2是你在学习第一篇时,就需要看的。当然只要看ch4即可。 在继续讨论模块前,插入说明一下编译和链接。否则上面的讨论仍然云里雾里。其实你完全可以读懂参考文献1 5.1。你也可以尝试听一下

鬼话: 编译的作用是将每个模块(即C文件)翻译成机器指令。而连接的作用是将对应的多个模块之间的数据摆放整齐,并修正对应的一些地址数据。我们做个假设,A模块里有printf这个函数,而B模块里,有main这个函数。在编译时,main函数对应的一堆指令中,肯定有包含调用printf的内容。但是如果只是编译,实际调用的指令,并不清楚应该调用具体哪里的函数入口。而链接就包含明确这些信息的工作。链接还包含把数据放一块,代码放一块,有时还有常量表,目前你的阶段,可以简单理解,链接的作用是将你的电脑中每个模块的摆放位置进行确定并进行安装,算是系统架构组装的动作。而编译,则是根据设计图纸,实实在在的生产出各个电脑内部的模块。

我们讨论下模块,非常明确的,一个C文件,针对一个完整的编译工作,生成一个.o文件。这就是一个模块。如果一个模块调用了其他模块的函数,则需要使用函数接口声明。因为在这个C文件进行编译时,需要明确参数接口。如同main 有两种参数接口。不同的方式,对应的给入数据不一样。但模块不是库。参考文献2 68页 tab 4-1中第一个,.a后缀的文件,描述位库文件。库什么概念。就是仓库嘛。里面包含很多零件。而每个零件算是一个模块,可以简单看作.a文件就是对一堆.o文件的打包。

比如,我们有两个模块,一个是stdio对应的包含printf 函数的.o文件,一个是hello.c对应的hello.o。而printf对应的.o文件在哪呢?在libc.a这个库里。我们可以参考一下网络的资料 http://stackoverflow.com/questions/11654281/where-can-i-find-the-object-file-which-contains-the-definition-of-printf-function

我是64位的机器,所以对应使用: x86_64 system: $ ar -t /usr/lib/x86_64-linux-gnu/libc.a libc.a,表示C标准库。里面包含了各类标准库所要求的基本模块(对象文件)。所以缺少这玩意,那么你基本上等于无法用C语言进行开发工作。

鬼话:你可以尝试一下百度,无论用什么关键字,去想办法搜索 printf 在哪个库里。相信我,通常不会比在 http://stackoverflow.com 上找的快。全当我给stackoverflow打广告吧,这是一个值得免费推广的网站。比你所能捡到的学校老师管用多了,当然除了考试的分数。

ar的对应命令我就不多说了。自己查找 参考资料 2 ,ch4的相关内容。只要你能理解,库的作用是为了把各个近似相关的o文件收集起来,所以也叫归档动作。没啥特殊意义,就是方便你别眼花缭乱的索引查找一堆o文件。 对于标准库,不需要你自己定义位置,系统环境在安装gcc时会自动配好,至少ubuntu下的apt-get install可以方便的处理掉。而如果你创建和使用自己的库,需要ar来操作,并在生成执行文件的命令中,增加库的路径说明。只有在你所明确的库中无法找到相关函数,gcc才会在标准库中查找。具体操作。后续文章会继续展开讨论。

总结一下,我们为了在屏幕上显示hello world。采用C语言进行设计。代码不多,不过很多基础知识需要讲解。非常非常需要明确的是,代码写完不算完。代码和我们的目标其实毫无联系,我们的目标不是要写C代码,而是要显示hello world。任何一个程序开发团队的目标都是为了完成某个工作,而不是写出某个代码。而正是因为代码通过编译,链接,并执行,才有了,C代码设计,和设计目标之间的联系。因此,第二部分,恰恰是本篇的重点。至于C代码怎么设计,后续会详细展开。

上一篇:一切从0开始

下一篇:从 MVC 开始模块化编程(上)

comments powered by Disqus