Unix/Linux C++应用开发-C++编程库简介
Linux软件开发中,编程库通常有两种创建方式:静态库与动态库。针对不同的应用需求,静态库与动态库创建的方式、实现的原理以及连接的方式都有着一定的区别。静态库要求在程序编译时,连接到应用程序的目标代码中去,程序运行时刻将不再需要静态库的存在。而动态库则正好相反。动态库参与程序编译时,并不会被连接到目标代码中,是在程序真正执行时才正式载入。为此,程序运行时,动态库必须存在。针对不同的处理情况,动态库
Linux软件开发中,编程库通常有两种创建方式:静态库与动态库。针对不同的应用需求,静态库与动态库创建的方式、实现的原理以及连接的方式都有着一定的区别。静态库要求在程序编译时,连接到应用程序的目标代码中去,程序运行时刻将不再需要静态库的存在。而动态库则正好相反。动态库参与程序编译时,并不会被连接到目标代码中,是在程序真正执行时才正式载入。为此,程序运行时,动态库必须存在。针对不同的处理情况,动态库提供了两种调用执行方式,分别为隐式调用与显式调用。
1.动态库隐式调用
动态库的隐式调用实际上是静态化的一种手段。应用程序执行时将动态库载入相应内存,当程序处理结束时自动将其卸载。但动态库的隐式调用与静态库还是存在区别的,它仅仅存在于应用程序的生命周期中,一旦应用程序运行停止,即进程(一次程序的执行过程)生命周期一结束也就跟着被卸载。
2.动态库显式调用
动态库的显式调用则严格遵守动态库原则,在应用程序编译期间不需要其存在,在实际调用时候再加载。从上述介绍的静态库和动态库的区别来看,静态库代码通常会加入应用程序编译的目标文件中,和动态库相比缺点是这样就相应的增加目标文件的代码量。
从调用处理效率来讲,由于应用场合不同,不能简单地对静态库与动态库作出效率比较的结论。在特殊的场合下,它们都可以做到高效处理。动态库相对静态库有更大的灵活性。例如软件系统需要在线升级,此时采用动态库可以支持在线升级的功能。这样就可以保持接口不变,更不需要重新编译应用程序。
从内存空间来看,使用动态库则意味着不同的应用程序共享使用了一份拷贝。这样就节省了内存空间。另外动态库是使用时加载的,这样对于不经常使用该库接口的场合,动态库更具有优势。
17.1.2 Linux下静态库与动态库操作
Linux系统中针对库的使用提供了相应的操作工具。这些工具可以帮助用户方便地将开发的组件等应用程序打包成相应的库,然后供其它模块使用。
1.静态库操作
对于静态库,Linux提供了ar命令用于创建静态库。该命令在Linux系统中通常由GNU提供。该命令附带一系列的选项作为辅助参数功能出现。初学者了解ar命令的常用选项操作即可,更多的操作选项可以在Linux系统中通过man ar方式来查询。ar命令基本语法定义如下:
ar [-X32_64] [-]p[mod [relpos] [count]] archive[member...]
下面分析上述语法的基本定义构成:
q 第一个中括号内选项为-X32_64,该选项表示使用平台是32位或者64位。一般的Linux主机平台通常该选项都是默认设置使用,开发者不需要关注具体的指定。
q 命令选项p指定了所使用的具体的选项操作:如果是选项d,表示从指定文件中删除模块,模块的名称由随后的参数member指定;如果是选项r,将随后参数member指定的成员加入到库中(如果存在同名则替换掉原来同名的成员);如果是选项s,表示创建库中归档的索引等。
静态库提供的命令操作会在后续实例中得到运用,具体参照该操作语法格式。
2.动态库操作
对于动态库的创建,Linux并没有提供特别的命令。通常动态库的创建是通过gcc编译命令添加-share等选项组合的命令完成的。Linux下通过gcc命令组合的基本语法如下:
gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list
gcc后第一个-shared选项用于标识创建动态库;第二个选项W1表明将后续使用逗号隔开的参数传递给链接器,如-soname等;第三个选项-o表示创建过程使用工程文件,其后是动态库名称以及相应加入动态库的工程文件列表等。
在动态库的操作过程中,需要注意动态库名称的构成。动态库名称的规范化有助于开发版本的管理。动态库名称必须以“lib”为前缀,“.so”以及相应的版本号为后缀,中间为库具体名称。例如,库名为libtest.so.1.0表示该库名为test,版本为1.0。动态库的创建与使用本小节暂不作介绍,后面章节将详细讲述具体的应用。
17.2 Linux下C++静态编程库
Linux下静态库是编程库方式的另一种实现形式。在应用程序编译时刻连接,将静态库中的库函数代码加入到对应的应用程序目标文件中,作为应用程序私有的一部分,供使用时调用。本章主要介绍Linux平台上C++应用程序如何创建和使用静态编程库。
17.2.1 C++静态库的创建
静态库前面已经介绍过,Linux系统下通过提供的ar工具来创建。通常创建静态库的前提是库的代码已经设计实现完毕,并且已经编译成相应的.o文件。而实际创建静态库只需要通过ar工具配合-r选项将这些.o文件放入到对应的静态库文件中即可。
下面根据前面封装实现的字符串处理操作函数接口,完整地演示从代码设计实现到创建静态库,以及调用静态库的整个过程。
1.准备实例
打开UE工具,创建新的空文件并且另存为chapter1701.cpp。该代码文件随后会同makefile文件一起通过FTP工具传输至Linux服务器端,客户端通过scrt工具访问操作。程序代码文件编辑如下所示。
/**
* 实例chapter1701
* 源文件chapter1701.h、chapter1701.cpp
* 字符串静态库实例
*/
//字符串操作头文件chapter1701.h
#include <iostream>
using namespace std;
/*
* 函数功能:拷贝字符串
* 函数参数:两个字符类型指针变量参数
* 返回值:目标字符指针
*/
char *strcpy(char *dest,char *src);
/*
* 函数功能:比较两个字符串大小
* 函数参数:两个字符类型数组参数
* 返回值:比较后的整型值
*/
int strcmp(char str1[],char str2[]);
/*
* 函数功能:连接两个字符串
* 函数参数:两个字符类型指针变量参数
* 返回值:拼凑后的字符串
*/
char *strcat(char *dest,char *src);
/*
* 函数功能:字符串长度求取
* 函数参数:需要求取长度的字符指针参数
* 返回值:字符串长度
*/
int strlen(char *str);
//字符串操作源文件chapter1701.cpp
#include " chapter1701.h"
char *strcpy(char *dest,char *src) //字符串拷贝函数定义
{
assert((dest!= NULL) && (src != NULL)); //断言判断处理输入的参数指针是否为空
char*temp = dest; //定义临时字符指针指向目标字符指针dest
while((*dest++= *src++) != '\0') //循环控制,将源字符拷贝进目标字符数组中,以\0为结束
;
returntemp; //返回临时字符指针
}
int strcmp(char *str1,char *str2) //字符串比较函数定义
{
inti = 0; //定义整型变量i并初始化为0
while(str1[i]== str2[i]) //首先比较字符串首字母是否相等
{
if(str1[i]== '\0') //如果相等,则判断首字符是不是结束符
{
return0; //如果是,则直接返回0,表示两个字符串相等
}
i++; //如果没有到结束符则i+1,继续比较下一位
}
return(str1[i] - str2[i]); //如果遇到字符不相等则直接返回两个字符ASCII相减值
}
char *strcat(char *dest,char *src) //字符串连接函数定义
{
inti,j; //定义两个整型变量,分别表示目标符数组和源字符数组下标
for(i= 0;dest[i] != '\0';i++); //通过本循环语句将目标字符串定位到尾部
for(j= 0;(dest[i]=src[j]) != '\0';) //当目标字符数组到达尾部时,将源字符数组字符依次存放到目标数组中
{
i++; //相应的目标字符数组下标后移继续存放源字符数组字符
j++; //源字符数组下标依次后移,逐位的将值存放到目标字符数组
}
returndest; //最后返回目标字符数组
}
int strlen(char *str) //求取字符串长度函数定义
{
inti; //定义整型值,用于统计传入的字符串的长度
for(i= 0;*str != '\0';str++) //循环控制判断,首先判断传入的字符数组是否到结束符
{
++i; //如果没到,则计数变量加1,继续判断对比
}
returni; //如果已经计算到最后结束符,则返回最终字符串长度
}
2.静态库创建过程
Linux平台下,静态库创建首先需要将库对应的源代码文件编译为.o的中间文件,编译命令如下所。
[developer@localhost src]$ g++ -O -cchapter1701.cpp
[developer @localhost src]$ ll *.o
-rw-r--r-- 1 billing oracle 2496 Mar 8 23:37chapter1701.o
上述编译命令中采用g++编译器编译源代码文件,配合-c命令,产生C++编译程序的中间.o文件;同时,增加了-O选项,用来优化代码。编译命令执行后,通过小写字母ll命令配合-lst选项或者通配符*组合命令,在当前目录下只选择出当前目录下产生的.o文件,从该命令执行结果来看此时已经生成StringOperator源代码文件的.o文件。
下面将会通过ar命令将生成的.o文件创建为对应的静态库,静态库的命名规则通常为lib[name].a的规则实现。此时静态库取名为stringOperator,则整个静态库的命名即为libstringOperator.a,目的是将工程文件chapter1701.o加入静态库生成libstringOperator.a文件。Linux下静态库操作工具ar需要配合-r选项,通过增加相应的中间文件到对应的静态库中,其操作命令如下所示。
[developer@localhost src]$ ar -rsvlibstringOperator.a chapter1701.o
ar: creating libstringOperator.a
a - chapter1701.o
[developer @localhost src]$ ll *a
-rw-r--r-- 1 billing oracle 2698 Mar 8 23:38libstringOperator.a
通过ar命令配合-r选项,随后跟着静态库的名称以及需要放入静态库的中间文件,ar命令操作创建完静态库之后,依然通过ll –lst或者“*”通配符显示查看是否创建静态库成功。
17.2.2 C++静态库的应用开发
一旦静态库编译成功后,应用程序中的应用就可以编译连接静态库,最后程序运行时可以调用静态库中的公用函数接口。首先软件系统依然是设计应用程序,并且在需要的地方调用静态库提供的方法接口。下面使用上节静态库设计的完整实例。通过该实例,帮助读者了解静态库编译连接以及调用的基本方法,代码编辑如下所示。
//静态库测试程序源文件testStringOperator.cpp
#include "StringOperator.h"
int main()
{
charstr1[100] = "hello"; //定义拥有100个元素的字符数组,使用字符串常量hello初始化
charstr2[100] = "welcom"; //定义拥有100个元素的字符数组,使用字符串常量welcom初始化
intflag = strcmp(str1,str2); //定义标记变量flag,调用字符串比较函数,将返回结果赋给flag
if(flag>0) //如果返回的比较值大于0
{
cout<<"str1large than str2!"<<endl; //打印提示信息,字符串str1大于str2
}
else if(flag == 0) //如果返回值的比较恒等于0,字符串str1等于str2
{
cout<<"str1 equalstr2!"<<endl; //打印字符串str1等于str2的提示信息
}
else
cout<<"str1 less thanstr2!"<<endl; //如果以上条件都不符合,则str1小于str2
cout<<"string'sstr1 length:"<<strlen(str1)<<endl; //调用求取字符串str1的长度函数,并打印结果
cout<<"string'sstr2 length:"<<strlen(str2)<<endl; //调用求取字符串str2的长度函数,并打印结果
cout<<"str1and str2:"<<strcat(str1,str2)<<endl; //调用连接字符串函数,连接str1、str2,并打印结果
cout<<"str1:"<<strcpy(str1,str2)<<endl; //调用字符串拷贝函数,将str2拷贝至str1并打印结果
return0;
}
上述源文件作为应用程序,在其中调用了静态库中的接口方法,同时包含了对应的头文件定义。该头文件包含方式采用用户自定义包含方式,即为#include "StringOperator.h"。这样是为了区别标准库头文件包含应用。由于上述源代码编译中需要连接已存在的静态库,生成可执行程序,所以编译命令如下。
[developer@localhost static]$ g++ -O -otestStringOperator testStringOperator.cpp ./libstringOperator.a
或
[developer@localhost static]$ g++ -O -o testStringOperatortestStringOperator.cpp -L./ -lstringOperator
上述编译命令中,g++编译命令首先通过-O优化程序;随后,-o将对应的源文件产生对应的可执行程序;同时,它还连接相应的静态库,最终生成可执行应用程序testStringOperator。静态库在编译连接时有两种方式,一种直接在当前目录下操作需要加上对应的静态库全称,见上述编译第一种方式。第二种通过-L选项。其中,-L选项表示可以在当前目录下查找库文件;-l后跟着库名称,最后连接生成可执行程序。
该应用程序中调用了字符串操作库的接口方法,可执行程序运行结果如下所示。
[developer@localhost static]$ ./testStringOperator
str1 less than str2!
string's str1 length:5
string's str2 length:6
str1 and str2:hellowelcom
str1:welcom
应用程序采用前面字符串操作实现进行修改而来。基本操作说明在程序注释中讲述的非常清楚,这里就不去过多的分析讲述。这里主要演示了静态库在整个应用程序中的操作实现方式。初学者可以从中总结出一般的静态库应用开发基本操作步骤,从而掌握静态库的基本开发方法。
17.3 Linux下C++动态编程库
应用系统中动态库的编程同样需要经历代码设计、动态库创建以及应用程序中连接调用几个步骤。下面将会按照上述这几个步骤来逐步讲述动态库的开发应用情况。
17.3.1 C++动态库的创建
本小节直接采用面向对象中类定义的实例来演示Linux下C++动态库的创建。通过该实例,读者可以了解整个动态库创建和使用等基本操作。
1.准备实例
打开UE工具,创建新的空文件并且另存为chapter1702.cpp。该代码文件随后会同makefile文件一起通过FTP工具传输至Linux服务器端,客户端通过scrt工具访问操作。程序代码文件编辑如下所示。
/**
* 实例chapter1702
* 源文件chapter1702.h、chapter1702.cpp
* 字符串动态库实例
*/
//头文件chapter1702.h
#ifndef CHAPTER1702_H_
#define CHAPTER1702_H_
#include <iostream>
using namespace std;
class StringOperate
{
public:
/*
* 函数功能:拷贝字符串
* 函数参数:两个字符类型指针变量参数
* 返回值:目标字符指针
*/
virtual char*strcpy(char *dest,char *src);
/*
* 函数功能:比较两个字符串大小
* 函数参数:两个字符类型数组参数
* 返回值:比较后的整型值
*/
virtual int strcmp(char str1[],char str2[]);
/*
* 函数功能:连接两个字符串
* 函数参数:两个字符类型指针变量参数
* 返回值:拼凑后的字符串
*/
virtual char *strcat(char *dest,char *src);
/*
* 函数功能:字符串长度求取
* 函数参数:需要求取长度的字符指针参数
* 返回值:字符串长度
*/
virtual int strlen(char *str);
};
#endif
//字符串操作源文件chapter1702.cpp
#include "chapter1702.h"
extern "C" StringOperate*create_object()
{
return new StringOperate;
}
extern "C" void destroy_object(StringOperate* object )
{
delete object;
}
char * StringOperate::strcpy(char *dest,char *src) //字符串拷贝函数定义
{
assert((dest != NULL)&& (src != NULL)); //断言判断处理输入的参数指针是否为空
char*temp = dest; //定义临时字符指针指向目标字符指针dest
while((*dest++= *src++) != '\0') //循环控制,将源字符拷贝进目标字符数组中,以\0为结束
;
return temp; //返回临时字符指针
}
int StringOperate::strcmp(char *str1,char *str2) //字符串比较函数定义
{
int i = 0; //定义整型变量i并初始化为0
while(str1[i]== str2[i]) //首先比较字符串首字母是否相等
{
if(str1[i]== '\0') //如果相等,则判断首字符是不是结束符
{
return0; //如果是,则直接返回0,表示两个字符串相等
}
i++; //如果没有到结束符则i+1,继续比较下一位
}
return(str1[i] - str2[i]); //如果遇到字符不相等则直接返回两个字符ASCII相减值
}
char * StringOperate::strcat(char *dest,char *src) //字符串连接函数定义
{
inti,j; //定义两个整型变量,分别表示目标符数组和源字符数组下标
for(i= 0;dest[i] != '\0';i++); //通过本循环语句将目标字符串定位到尾部
for(j= 0;(dest[i]=src[j]) != '\0';) //当目标字符数组到达尾部时,将源字符数组字符依次存放到目标数组中
{
i++; //相应的目标字符数组下标后移继续存放源字符数组字符
j++; //源字符数组下标依次后移,逐位的将值存放到目标字符数组
}
returndest; //最后返回目标字符数组
}
int StringOperate::strlen(char *str) //求取字符串长度函数定义
{
inti; //定义整型值,用于统计传入的字符串的长度
for(i= 0;*str != '\0';str++) //循环控制判断,首先判断传入的字符数组是否到结束符
{
++i; //如果没到,则计数变量加1,继续判断对比
}
returni; //如果已经计算到最后结束符,则返回最终字符串长度
}
该动态库源代码文件设计实现如上所示,主要通过直接封装实现基本字符串操作的类类型。字符串操作类StringOperate提供一系列字符串操作方法接口,应用程序中直接可以定义类StringOperate对象来使用其中的字符串操作接口。
Linux平台上动态库中编译生成中间文件需要使用-fpic选项,上述字符串封装类可以直接一条组合命令生成相应的动态库,代码编译如下。
[developer@localhost string]$ g++ -fPIC -sharedchapter1702.cpp -o libchapter1702.so
[developer @localhost src]$ ll -lst *.so
16 -rwxr-xr-x 1 developer oracle 8415 Jun 301:04 libchapter1702.so
通过-shared选项配合对应的-o,根据已经生成的中间文件chapter1702.o创建动态库文件libchapter1702.so。动态库文件是后缀为.so的库文件,区别于静态库文件命令规则。随后采用小写字母ll命令检查对应的动态库是否创建成功。
17.3.3 C++动态库的调用
Linux下动态库的使用方式与静态库稍有区别。本小节主要讲述动态库在应用程序中如何调用的过程。动态库的调用与静态库不同,需要配合一系列的动态库调用函数来配合使用。这些动态库调用函数都是标准系统库级的API,首先先介绍下动态库函数调用的基本函数方法。
#include <dlfcn.h> //动态链接标准头文件
void *dlopen(const char *filename, int flag);
char *dlerror(void);
void *dlsym(void *handle, const char *symbol);
int dlclose(void *handle);
1.dlopen函数
dlopen函数用于打开相应需要装载的动态库。该方法最终会返回一个动态库的句柄,该句柄为一个void类型的指针。动态库打开函数中包含两个参数:第一个参数filename为常量字符串类型,用于表示打开动态库的库名;第二个参数flag为打开动态库的标记值,用于表示了该方法内部实现功能的不同内容。该标记flag主要拥有三个值,分别为RTLD_LAZY、RTLD_NEW和RTLD_GLOBAL。
q 标记值RTLD_LAZY表示当动态库代码中定义类型为extern(即外部定义变量或者函数),不执行解析。
q 标记值RTLD_NEW表示,在dlopen函数返回动态库句柄之前,需要解析每个extern类型定义的变量或者函数地址。如果解析不到,则报出相应的无定义错误,同时dlopen方法会返回NULL值。
q 标记值RTLD_GLOBAL表示该动态库中解析的相关变量可以在其它的链接库(静态库或者动态库)中使用,即将解析的变量全局化。
2.dlerror函数
动态库的dlerror函数用于判断动态库链接打开是否成功。动态库调用dlopen方法之后,直接调用dlerror方法来检验。该函数调用后,返回一个字符指针类型的值。如果动态库链接成功后,则返回NULL值;否则,返回相应的错误信息。用户可以在程序中打印来检查问题。
3. dlsym函数
如果需要使用动态库中的具体方法,还需要通过调用dlsym函数来获取其相应的函数地址。该方法包含两个参数:第一个参数为动态库链接打开的句柄handle,为void类型指针;另一个为动态库中相应的函数名称。
4. dlclose函数
dlclose函数用于关闭动态库。该函数接口拥有一个参数,该参数为前面通过dlopen函数打开的动态库句柄handle。该函数调用之后,关闭传入的参数句柄相应的动态库。
通过前面动态库创建实例以及以上动态库使用的四个函数的讲解说明,下面将会通过一个实际应用例子,来讲解动态库在应用开发中的具体应用情况。相关代码实例如下所示。
//动态库测试程序源文件chapter1702_test.cpp
#include <dlfcn.h>
#include "chapter1702.h"
int main(int argc, char **argv)
{
void* handle =dlopen("libchapter1702.so", RTLD_LAZY);
StringOperate*(*create)();
void(*destroy)(StringOperate*);
create = (StringOperate*(*)())dlsym(handle, "create_object");
destroy = (void(*)(StringOperate*))dlsym(handle, "destroy_object");
StringOperate* stringOperate =(StringOperate*)create();
char str1[100] = "hello"; //定义拥有100个元素的字符数组,使用字符串常量hello初始化
charstr2[100] = "welcom"; //定义拥有100个元素的字符数组,使用字符串常量welcom初始化
int flag= stringOperate->strcmp(str1,str2); //定义标记变量flag,调用字符串比较函数,将返回结果赋给flag
if(flag>0) //如果返回的比较值大于0
{
cout<<"str1large than str2!"<<endl; //打印提示信息,字符串str1大于str2
}
else if(flag == 0) //如果返回值的比较恒等于0,字符串str1等于str2
{
cout<<"str1equal str2!"<<endl; //打印字符串str1等于str2的提示信息
}
else
cout<<"str1 less thanstr2!"<<endl; //如果以上条件都不符合,则str1小于str2
cout<<"string'sstr1 length:"<<stringOperate->strlen(str1)<<endl; //调用求取字符串str1的长度函数,并打印结果
cout<<"string'sstr2 length:"<<stringOperate->strlen(str2)<<endl; //调用求取字符串str2的长度函数,并打印结果
cout<<"str1and str2:"<<stringOperate->strcat(str1,str2)<<endl; //调用连接字符串函数,连接str1、str2,并打印结果
cout<<"str1:"<<stringOperate->strcpy(str1,str2)<<endl; //调用字符串拷贝函数,将str2拷贝至str1并打印结果
destroy( stringOperate );
}
本动态库调用实例,实现的思路与chanpter1701实例是一样的。仅仅将字符串的几个操作封装到StringOperate类类型中。之所以采用面向对象C++动态库调用作为演示,是因为面向对象中动态库的调用并不像C语言中那么简单。
而C语言中动态库的调用思路与上述C++应用中动态库调用大致相同,仅仅在面向对象类调用方式上不同而已。上述实例代码完成之后,在当前目录中编译链接生成的动态库libchapter1702.so。具体操作如下所示。
[developer@localhost src]$ g++chapter1702_test.cpp -ldl -o chapter1702_test
[developer @localhost src]$ ll
total 56
-rw-r--r-- 1 developer oracle 1894 Jun 301:03 chapter1702.cpp
-rw-r--r-- 1 developer oracle 1234 Jun 301:04 chapter1702.h
-rwxr-xr-x 1 developer oracle 8704 Jun 404:24 chapter1702_test
-rw-r--r-- 1 developer oracle 1581 Jun 301:03 chapter1702_test.cpp
-rwxr-xr-x 1 developer oracle 8415 Jun 301:04 libchapter1702.so
[developer @localhost src]$ cp libchapter1702.so../bin
[developer @localhost src]$ cp chapter1702_test../bin
[developer @localhost src]$ cd ../bin
[developer @localhost bin]$ ./chapter1702_test
str1 less than str2!
string's str1 length:5
string's str2 length:6
str1 and str2:hellowelcom
str1:welcom
当前src目录中,通过g++编译命令将测试动态库的应用程序文件chapter1702_test.cpp。g++编译命令通过-ldl选项,告诉编译器编译的代码源文件chapter1702_test.cpp,需要在当前目录连接相应的动态库。最后,通过-o命令生成可执行程序chapter1702_test。
随后,通过cp命令拷贝相应的可执行程序和动态库到bin目录下,然后执行可执行程序。程序加载动态库,并使用了动态库中函数。当然,用户也可以通过添加路径环境变量,来指定不同位置的动态库链接,而不是将动态库与可执行程序拷贝到同一个目录。
17.4 小结
本章主要介绍了Linux平台上C++应用开发编程库的相关方法,主要分为静态库和动态库两大部分。分别讲述了Linux系统C++应用程序中静态库和动态库创建和使用。初学者在重点掌握静态库、动态库基本概念后,能够通过实践开发自己的应用程序库。其中难点是能够根据日常开发中实际情况分清静态库与动态库应用场合,更好的为公司应用软件创造公共组件财富。下一章开始将会进入Linux平台上文件应用开发相关知识的讲解。
更多推荐
所有评论(0)