Unix/Linux下C/C++开发技术概览(下)
2. 基本开发流程在Unix下开发程序,不同于在Windows下开发,除了上面介绍的程序级的差别外,开发环境也有很大的差别。在Windows下面,大都使用集成开发环境进行开发,如MS Visual C++、Borland C++ Builder等。在Unix下面也有集成开发环境,如Sun 的Workshop、Linux下的KDevelop等,但是因为很多Unix开发都是移植,即现在Window
2. 基本开发流程
在Unix下开发程序,不同于在Windows下开发,除了上面介绍的程序级的差别外,开发环境也有很大的差别。在Windows下面,大都使用集成开发环境进行开发,如MS Visual C++、Borland C++ Builder等。在Unix下面也有集成开发环境,如Sun 的Workshop、Linux下的KDevelop等,但是因为很多Unix开发都是移植,即现在Windows基本完成,然后在移植到Unix下,所以需要的代码量就不是太大,所以经常采用Makefile的方法,而不用IDE开发环境来重新创建工程。本章的主要内容就是介绍在Unix下开发的基本过程和一些开发调试工具。
2.1 代码的编写
对于移植工程来讲,基本代码都在Windows下完成,只需要吧代码传到Unix下,然后在Unix下面组织源码目录即可。对于传到Unix代码进行编辑,可以使用Unix下的vi工具来完成,也可以通过UltraEdit以Ftp方式打开Unix下的文件进行编辑。vi是Unix下面最常用的一个文本编辑器,后面将会介绍vi的一些基本用法。
有一点值得注意,Windows下文本里面的回车符包含两个字符‘//n’和‘//r’,而Unix下的文本里面的回车符只包含一个字符‘//n’。这样,如果上传问文件的时候没有选择正确的方式,应该使用文本方式上传的使用了二进制方式,或者应该使用二进制方式上传的使用了文本方式,那么在unix下都会出现问题,打开的文本当中每一行的行尾就会出现一个‘^M’字符。可以通过vi的匹配替换功能(稍后会做介绍)或者重新按照正确的方式上传来解决。
2.2 编译
2.2.1 简单编译
对于简单的程序,如只有几个源文件,可以直接使用编译器进行编译,或者把几条编译命令写在一个脚本文件里面,通过执行脚本文件实现工程的编译和连接。比如只有一个hello.cpp文件的工程,可以通过如下命令编译:
CC –o hello hello.cpp
其中CC是编译器,不同的系统下面可能有不同的编译器。一般来说,大多数Unix系统下的C编译器都叫cc,而C++编译器叫CC。Linux下面带的C编译器为gcc,C++编译器为g++。-o参数用来指定输出的目标的名称,也就是编译后执行程序的名称。这种情况下编译和连接一步完成。
对于稍微负责一些的程序,包含多个源文件的,可以编写一个编译脚本,相当于windows下的批处理。如下:
工程中包含hello.cpp、func.cpp、other.cpp,我们可以用如下脚本来实现工程的编译。
CC –c hello.cpp
CC –c func.cpp
CC –c other.cpp
CC –o hello hello.o func.o other.o
多个文件情况下,把编译和连接分开执行,先逐个编译源文件,然后再进行链接,形成最终的可执行程序。参数-c就是声明只进行编译操作。
2.2.2 使用Makefile
当工程达到一定的规模的时候, 2.2.1 中的做法显然是不能满足要求的,如果非要那样做,将会带来很大的工作量,而且还非常容易出错。这是我们就要使用Makefile来帮助我们完成工程的编译工作。
Makefile文件相当于一个工程文件,文件中描述了工程中的源代码、额外需要的库文件及其路径、额外需要的头文件路径已经编译器类型、编译参数等。通过make命令来调入Makefile进行工程的编译。当执行make命令是,会在当前目录下搜索名称为“Makefile”或者“makefile”的文件,作为当前编译的工程文件,也可一指定其他的工程文件,如make –f MyMakefile。
一个简单的Makefile文件内容如下:
#Makefile for Linux(RedHat)
宏定义
#目标程序名称
BIN_NAME=demo
#编译器及编译参数
CC = gcc
CXX = g++
CXXFLAGS = -g
文件列表
#源代码列表
SRCS=//
demo.cpp //
func.cpp
#目标文件列表,通过源代码列表生成
OBJS=${SRCS:.cpp=.o}
#依赖关系
#depends
all:${BIN_NAME}
.depends : ${SRCS} #依赖规则
@echo Creating depend information,please wait ...
依赖关系
${CXX} -M ${SRCS} > .depends #执行的命令
-include .depends
#%.o:%.cpp
# @echo Compling file: $? please wait ...
# ${CXX} ${CXXFLAGS} -c $?
${BIN_NAME}:${OBJS}
@echo Linking file: $@ please wait ...
${CXX} -o $@ ${OBJS}
clean:
rm -f ${BIN_NAME} ${OBJS} *pure .depends
在Makefile文件中,大体来说分为“宏定义”、“文件列表”和“依赖关系”三个部分。宏定义中定义Makefile中可以使用的一些宏,定义好一个宏以后,在Makefile的后面部分可以通过宏引用来获得宏的值。像编译器、编译参数等通常都会定义成宏,然后通过修改宏的值就可以修改编译器类型及编译参数。在更复杂的Makefile中,宏定义中还会包含依赖的库文件、需要增加的库文件路径及头文件路径、程序中预定义的宏等。
其实文件列表也是一个宏,只是考虑到他的重要性,所以单独作为一个部分来介绍。在后面的依赖关系中,就会用到这个宏。文件列表中定义了工程包含的所有源文件和这些源文件编译以后生成的目标文件。目标文件可以通过源文件列表生成。上面我们使用${SRCS:.cpp=.o}来生成目标文件列表,意思就是说把源文件中所有扩展名为“.cpp”换成“.o”。
依赖关系部分是Makefile文件的关键,其中定义了多个依赖关系。每一个依赖关系又包括一个依赖规则和一组执行的命令。依赖规则由目标和依赖对象构成,也就是说这个目标依赖于指定的依赖对象。当执行make的时候,如果发现目标不存在或者目标的依赖对象的更新时间比目标还新,就会执行依赖关系中的命令,来完成对目标的编译。Makefile中可以定义多个目标,每个目标对应一个依赖关系和一组命令。执行make的时候,可以执行要编译的目标。如make clean表示检查执行目标clean。all是一个默认的目标,即执行make不加参数的时候,就会默认为要检查执行all这个目标。Makefile中可以为all目标指定依赖对象和要执行的命令。Makefile中,也可以只指定依赖关系,不指定要执行的命令,这种情况大多用于依赖对象也是一个定义的目标。Make在检查依赖关系的时候,如果发现依赖对象也是一个目标,就会先去检查这个目标的依赖关系,看是否需要重新编译这个目标,然后才会执行本依赖关系下面的命令。
为了实现源文件的编译,我们在依赖关系中需要定义源文件和目标文件之间的依赖关系,然后执行程序和这些目标文件之间建立一个依赖关系。这样就可以实现整个工程的编译了。上面的这个Makefile中,包含了两种定义这种依赖关系的方法,一种是通过编译器的“-M”命令来形成依赖关系文件,然后把这个文件的内容include到当前的依赖关系中,另一种是通过匹配替换的做法,直接通过源文件和目标文件的映射关系形成依赖关系。红色字体部分和蓝色字体部分分别代表这两种做法。“-M”参数是gcc/g++编译器特有的,其他编译器一般没有这个参数,应该使用第二种做法。
在不同的系统中,Makefile的语法会有一些细微的差别,在进行多平台移植的时候应该注意这个问题,分别对不同的系统编写不同的Makefile。
2.2.3 a utomake和autoconf
Makefile 基本结构虽然很简单,但是妥善运用这些规则就可以变换出许多不同的花样。却也因为这样,许多刚刚开始学习写Makefile 时会觉得没有规范可以遵循,每个人写出来的Makefile都不大一样,不知道从哪里下手,而且常常会受到自己的开发环境的限制,只要环境参数不同或者路径更改,可能 Makefile 就得跟着修改修改。虽然有 GNU Makefile Conventions (GNU Makefile惯例例)订出一些使用 GNU 程式设计时撰写 Makefile 的一些标准和规范,但是内容很长而且很复杂,并且经常作一些调整,为了减轻程序开发人员维护Makefile 的负担,因此出现了Automake。
程序设计者只需要写一些预先定义好的宏 (macro),提交给Automake处理后会产生一个可以供 Autoconf 使用的 Makefile.in文件。再配合利用 Autoconf产生的自动培植设置文件 configure 即可产生一份符合符合 GNU Makefile 惯例的 Makeifle 了。
automake的基本用法,是先用autoscan搜索当前目录,然后形成一个configure.scan文件,然后以configure.scan文件为蓝本,形成一个configure.in文件,这是最终需要的文件。然后使用aclocal处理一下本地的宏,再使用autoconf生成一个configure脚本文件。configure脚本文件用来形成最终的makefile。之后编译一个Makefile.am,定义automake需要的一些宏,包括源文件列表等。执行automake,形成Makefile.in文件。这样,需要的准备工作基本完成。编译的时候,通过执行configure脚本,会自动生成Makefile文件,然后再执行make就可以了。
关于automake的详细用法请查看参考资料。
2.3 运行和调试
跟Windows不统,Unix下面的文件只有有可执行的权限就可以执行,不像Windows那样依赖于扩展名。所以Unix下的执行程序大都没有扩展名,任何文件只要给了一个执行权限,就可以直接运行。可以通过chmod +x来给一个文件增加可执行的权限。编译后形成的目标文件,通常都已经有了执行的权限,所以可以直接执行。
2.3.1 设置运行环境
对于简单的程序来说,不用设置什么运行环境,直接运行即可。对于负责的程序来讲,可能执行程序和需要的共享库文件不在同一个目录下,就需要设置一些运行环境了。设置运行环境主要就是设置共享库的路径。Unix下的共享库,相当于Windows下面的动态库(dll文件),是一种以.so为扩展名的文件。系统的共享库存放在/usr/lib下面,程序运行的时候会自动去那个目录下面寻找需要的共享库。对于用户自己开发的共享库,一般不能放在/usr/lib目录下面,而是放在用户的程序目录下面。一个比较复杂的程序,其目录结构分的比较清晰,并不是把所有的文件都放在一个目录下面。下面列出了一个典型的目录结构:
mainpath 程序主目录
mainpath/bin 执行程序目录
mainpath/lib 共享库目录
这种情况下,就需要设置程序的运行环境,指定库文件目录。设置运行环境其实就是在运行程序前设置若干个环境变量,其中共享库的路径就是其中之一,也可一设置一些程序需要的其他的环境变量。Unix下面用LD_LIBRARY_PATH来表示用户共享库的路径。设置完环境变量,就可以运行程序了。
有一种更方便的方法,就是编写执行脚本。Unix下面的脚本相当于Windows下的批处理文件,就是执行一系列的命令。本质上来说,脚本文件就是一个文本文件,编辑完成后增加一个可执行权限即可。在Unix下面的终端窗口,都是基于外壳(shell)的。在UNIX中大家最常使用Bourne Shell以及C Shell ,系统默认采用的是Bourne Shell。不同的shell有不同的编程语言,也就是说编写shell脚本的语言,用来实现一些更高级的功能。在脚本中,可以指定本脚本采用的语言。关于shell编程的详细信息,请查阅参考资料。一般情况下,Bourne Shell脚本文件以.sh为扩展名,C Shell脚本文件以.csh为扩展名。这里我们以Bourne Shell为例。
一个典型的程序目录中,一般存在3个脚本:执行脚本(run.sh)、停止进程脚本(kill.sh)和设置环境脚本(setenv.sh)。脚本文件一般都放在程序的主目录下面,进入主目录可以直接通过这些脚本来执行程序。下面简单列出了这几个脚本的内容。
setenv.sh:
#!/bin/sh
path=`pwd`
LD_LIBRARY_PATH=$path/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
run.sh:
#!/bin/sh
path=`pwd`
LD_LIBRARY_PATH=$path/lib:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH
cd bin
./demo
cd ..
kill.sh:
#!/usr/bin/sh
for sid in `ps -e |grep demo |awk {'print $1'}`
do
kill $sid
done
值得注意的一点是,外壳其实也是一个进程,运行脚本以后会创建一个新的进程。环境变量依赖于进程,也就是说脚本里面设置的环境变量在脚本完成后将会消失,不回影响外壳进程和其他进程。也就是说,正常情况下我们的setenv.sh相当于不起任何作用。不过可以通过另一种执行方法来保留脚本执行过程中设置的环境变量,即“. setenv.sh”。其实就是在执行命令的前面加上一个‘.’和空格。这样脚本中设置的环境变量就会作用域执行这个脚本的外壳进程,也会影响到以后在这个外壳下面运行的所有进程。
2.3.2 使用调试工具进行调试
程序的调试,是程序开发中最重要的一部分,通过调试来找到程序中的bug。Unix下面的程序调试工具有很多,包括IDE环境下的调试工具和字符界面的调试工具。这里我们主要介绍字符界面的调试工具。常用的调是工具是dbx,大部分Unix系统下都有。在linux下面,调试工具为gdb,功能跟dbx类似。
跟Windows类似,要以调试方式运行程序,就必须按照debug版来编译程序。Unix下面的编译一一般通过-g参数来声明使用debug方式进行编译,这样编译后的目标文件中就包含了源代码的信息。下面以gdb为例,介绍一下Unix下面程序调试的基本方法。
1) 以调试方式运行程序
在执行程序所在的目录下,执行gdb,以程序名称为参数即可进入gdb调试界面。如:gdb demo
2) 查看源文件,设置断点
在gdb下,使用list命令可以查看主函数所在的文件的内容。默认情况下,一次显示10行内容,再次执行list可以显示后面的10行。
确定要在哪行上设置断点,然后用break命令进行断点设置。如:
break 10 表示在第10行上设置一个断点。
break命令还可以指定要设置断点的文件(如果不是主函数所在的文件),如:
break func.cpp:10 表示在func.cpp的第10行上设置一个断点。
除了在具体的行上设置断点外,还可以在某个函数上设置断点,只要把行号换成函数名即可。
clear命令用来清楚当前行的断点,delete命令可以删除所有设置的断点。
3) 运行程序
设置完断点后,可以在gdb下通过run命令运行程序,如果程序需要什么参数,直接放在run命令后即可,如:
run –start
其中-start是程序允许需要的一个参数。
4) 断点处程序分析
当程序执行到断点的时候,就会定下来等待用户进行处理,这是可以通过一些命令来查看程序当前的运行状态,这些命令包括:
print 打印变量的值,只需要用变量名做命令参数即可
where 显示程序当前的调用栈
set variable 改变变量的值,可能会影响到程序后面运行的结果
5) 单步调试
在断点处处理完成后,可以继续往下执行程序,next命令用来执行一行代码,step命令用来跟踪到即将执行的函数中。continue命令可以继续运行当前程序,直到遇到下一个断点。
上述流程中,简单介绍了一些如何使用使用gdb来进行程序的调试。gdb中的所有命令,都可以用简写来表示,只要写道能够区分处改命令的字母数即可,比如可以简单的输入n表示next命令。除了上面介绍的命令外,还有很多有用的命令。attach和detach命令,可以在程序正常运行的情况下把gdb帮定到一个进程中,进行调试,调试结束后恢复正常的程序运行,推出gdb对程序运行没有影响;pwd命令可以显示当前的工作目录,cd命令可以改变当前的工作目录;thread命令可以查看当前的线程ID。还有很多别的命令,可以通过help命令获得详细的帮助信息。
除了dbx和gdb外,Unix下面还有很多调试工具,详细信息请看参考资料。
2.3.3 core dump
对于Unix开发人员来说,core dump是再熟悉不过的了。再Unix下面,core dump就是程序bug的最严重的体现。core dump又叫核心转储,当程序运行过程中发生异常,程序以外退出的时候,系统会把程序当前的内存状况存储在一个core文件中,就叫core dump。
既然core文件记录了程序运行时的情况,就可以通过core文件来分析错误的原因。使用gdb/dbx 运行程序名 core就可以把程序恢复到发生错误的哪一时刻,可以看到程序在执行哪行代码,调用什么函数时发生了错误。这是,可以借助调试工具的其他命令进行更细致的分析。个人觉得,Solaris下实现的dbx远比gdb强大的多,可以看到更多的信息。
值得注意的是,在linux下面有一个系统限制,就是core文件的大小。如果这个限制设成0,则程序发生异常以后不回产生core文件。如果想得到core文件,请使用ulimit –c命令来调整这个参数。ulimit –a命令可以查看到系统当前的限制参数情况。
3. 常用命令介绍
Unix下面的常用命令,跟Windows下的控制台命令有很大差别。Unix下的命令,可能是一个执行程序,或者一个可以执行的脚本,系统的命令一般都可以在/usr/bin/下面找到。另外需要说明的一点,Unix下面严格区分文件名的大小写,包括命令的大小写。下面简单介绍一下Unix系统下常用的几个命令。
3.1 常用命令列表
ls 列出当前目录下的文件,相当于Windows的dir。
cd 切换目录,和Windows下的cd命令相同。
mkdir 创建一个目录。
rm 删除一个文件或者目录。-r参数表示删除的是目录,-f参数表示强制删除。
mv 移动或者修改文件/目录名称。
cp 拷贝文件或目录,-r参数表示拷贝的对象是目录。
ps 查看当前系统的进程情况,-e是最常用的一个参数,表示列出所有进程。
man 查看手册,这是一个最常用的命令,通过他可以查看指定命令或者函数的使用手册。比如man ps就可以产看ps命令的帮助手册,man fopen可以查看fopen函数的用法。
find 在指定目录下查找指定的文件。
grep 在指定文件中查找指定的内容。
vi 编辑文本文件,后面会做详细的介绍。
tar 文件打包和解包工具,通过cvf参数打包,通过xvf解包
gzip 压缩工具,可以把文件压缩成gz文件,通过-d参数进行解压缩,常于tar命令一起使用。
3.2 命令的组合
在Unix下面,可以把多个命令组合使用,可以把一个命令的执行结果作为另一个命令的输入。其中,最常跟别的命令组合使用的命令就是grep。在 2.3.1 中的那个kill.sh脚本中,我们就使用了这样一个组合命令“ps -e |grep demo |awk {'print $1'}”,用来列出当前系统中所有名称为demo的进程的进程号。这种用法是我们开发过程中最常用的方法。另外如果我们想知道我们的程序的网络情况,可以通过netstat命令和grep命令来组合使用,获得指定端口的状态情况,比如netstat –a | grep 43001,这个命令将列出当前系统中所有在端口43001上的连接情况。
通常的grep命令,只能查找当前目录下的文件,比如:grep ‘OPEN_MAX’ *.h 就是查找当前目录下面的所有头文件中是否含有“OPEN_MAX”这个词。而find命令可以查找指定路径下面的所有子目录,发现所有匹配的文件,那么,我们就可以用grep命令和find命令组合,来实现子目录下的文件内容查找。命令格式如下:
grep 'OPEN_MAX' `find /usr/include -name '*.h'`
这样可以查找所有在/usr/include及其子目录下的头文件,看是否包含“OPEN_MAX”。执行的时候,先执行find命令,形成一个文件列表,然后把这个文件列表作为grep的一个参数来执行grep命令。这是命令组合的另一种方式。
3.3 vi简介
vi是Unix下最常用的一个文本编辑器,小巧而且功能强大,一次我们把它单独拿出来做一下介绍。如果想了解更详细的信息,请执行man vi查看其联机帮助文档。
3.3.1 命令模式和编辑模式
vi的工作模式包括命令模式和编辑模式两种。命令模式下可以执行vi中定义的一些命令,这些命令跟一些特定的键相对应,命令模式下所有的键盘相应将会作为命令来解释。编辑模式就是编辑文档的模式,在编辑模式下所有键盘的相应都为作为文档输入的内容。通过ESC键可以从编辑模式切换到命令模式。通过一些编辑命名可以从命令模式进入编辑模式。
3.3.2 基本命令
基本命令指的是在命令模式下,通过敲键执行的命令,这里我们介绍几个常用的命令:
i 在当前开始插入,进入编辑模式
I 在行首开始插入,进入编辑模式
a 在当前字符后追加,进入编辑模式
A 在行尾追加,进入编辑模式
x 删除当前光标处的字符
X 删除当前光标前面的字符
D 删除从光标位置到行尾的所有字符
dd 删除当前行
dw 删除当前的单词
u 取消刚才的操作
G 跳到文件末尾
此外,在命令模式下,通过输入‘/’可以进行查询,通过输入‘:’可以输入一些其他命令。输入‘:’可以输入的命令包括:
q 退出
w 保存
wq 保存退出
q! 强制退出,不保存
w! 对受保护的文件强制写,包括只读文件
set number 显示行号
数字 跳到某一行
3.3.3 查找和匹配
vi的查找功能也非常强大,命令模式下通过输入‘/’就可以进入查找模式,可以输入要查找的关键字。然后可以通过‘n’来查找下一处。
在输入‘:’后,还可以输入一些匹配替换的命令——“%s”。命令的格式为“:%s /str1/str 2 ” ,执行之后将把当前文件中所有str1替换成str2。举一个典型的例子,前面我们说过,Windows下的文本文件如果以二进制的方式传到了Unix下,那么vi打开的时候每行的行尾就会出现一个“^M”,我们可以用vi打开这个文件,然后通过vi的匹配替换功能去掉这些“^M”。命令格式如下:
:%s /^M//
其中“^M”通过CTRL+V 和 CTRL+M来输入。
更多推荐
所有评论(0)