Linux下的C编程实战.docx
《Linux下的C编程实战.docx》由会员分享,可在线阅读,更多相关《Linux下的C编程实战.docx(42页珍藏版)》请在冰豆网上搜索。
Linux下的C编程实战
Linux下的C编程实战
Linux下的C编程实战
(一)
――开发平台搭建
1.引言
Linux操作系统在服务器领域的应用和普及已经有较长的历史,这源于它的开源特点以及其超越Windows的安全性和稳定性。
而近年来,
Linux操作系统在嵌入式系统领域的延伸也可谓是如日中天,许多版本的嵌入式Linux系统被开发出来,如ucLinux、RTLinux、ARM-Linux等等。
在嵌入式操作系统方面,Linux的地位是不容怀疑的,它开源、它包含TCP/IP协议栈、它易集成GUI。
鉴于Linux操作系统在服务器和嵌入式系统领域愈来愈广泛的应用,社会上越来越需要基于Linux操作系统进行编程的开发人员。
浏览许多论坛,经常碰到这样的提问:
“现在是不是很流行unix/linux下的c编程?
所以想学习一下!
但是不知道该从何学起,如何下手!
有什
么好的建议吗?
各位高手!
哪些书籍比较合适初学者?
在深入浅出的过程中应该看哪些不同层次的书?
比如好的网站、论坛请大家赐教!
不慎
感激!
”
鉴于读者的需求,在本文中,笔者将对Linux平台下C编程的几个方面进行实例讲解,并力求回答读者们关心的问题,以与读者朋友们进行交流
,共同提高。
在本文的连载过程中,有任何问题或建议,您可以给笔者发送email:
21cnbao@,您也可以进入笔者的博客参与讨论:
笔者建议在PC内存足够大的情况下,不要直接安装Linux操作系统,最好把它安装在运行VMWare虚拟机软件的Windows平台上,如下图:
在Linux平台下,可用任意一个文本编辑工具编辑源代码,但笔者建议使用emacs软件,它具备语法高亮、版本控制等附带功能,如下图
:
2.GCC编译器
GCC是Linux平台下最重要的开发工具,它是GNU的C和C++编译器,其基本用法为:
gcc[options][filenames]
options为编译选项,GCC总共提供的编译选项超过100个,但只有少数几个会被频繁使用,我们仅对几个常用选项进行介绍。
假设我们编译一输出“HelloWorld”的程序:
/*Filename:
helloworld.c*/
main()
{
printf("HelloWorld"n");
}
最简单的编译方法是不指定任何编译选项:
gcchelloworld.c
它会为目标程序生成默认的文件名a.out,我们可用-o编译选项来为将产生的可执行文件指定一个文件名来代替a.out。
例如,将上述名为
helloworld.c的C程序编译为名叫helloworld的可执行文件,需要输入如下命令:
gcc–ohelloworldhelloworld.c
-c选项告诉GCC仅把源代码编译为目标代码而跳过汇编和连接的步骤;
-S编译选项告诉GCC在为C代码产生了汇编语言文件后停止编译。
GCC产生的汇编语言文件的缺省扩展名是.s,上述程序运行如下命令:
gcc–Shelloworld.c
将生成helloworld.c的汇编代码,使用的是AT&T汇编。
用emacs打开汇编代码如下图:
-E选项指示编译器仅对输入文件进行预处理。
当这个选项被使用时,预处理器的输出被送到标准输出(默认为屏幕)而不是储存在文件里。
-O选项告诉GCC对源代码进行基本优化从而使得程序执行地更快;而-O2选项告诉GCC产生尽可能小和尽可能快的代码。
使用-O2选项编译的速度
比使用-O时慢,但产生的代码执行速度会更快。
-g选项告诉GCC产生能被GNU调试器使用的调试信息以便调试你的程序,可喜的是,在GCC里,我们能联用-g和-O(产生优化代码)。
-pg选项告诉GCC在你的程序里加入额外的代码,执行时,产生gprof用的剖析信息以显示你的程序的耗时情况。
3.GDB调试器
GCC用于编译程序,而Linux的另一个GNU工具gdb则用于调试程序。
gdb是一个用来调试C和C++程序的强力调试器,我们能通过它进行一
系列调试工作,包括设置断点、观查变量、单步等。
其最常用的命令如下:
file:
装入想要调试的可执行文件。
kill:
终止正在调试的程序。
list:
列表显示源代码。
next:
执行一行源代码但不进入函数内部。
step:
执行一行源代码而且进入函数内部。
run:
执行当前被调试的程序
quit:
终止gdb
watch:
监视一个变量的值
break:
在代码里设置断点,程序执行到这里时挂起
make:
不退出gdb而重新产生可执行文件
shell:
不离开gdb而执行shell
下面我们来演示怎样用GDB来调试一个求0+1+2+3+…+99的程序:
/*Filename:
sum.c*/
main()
{
inti,sum;
sum=0;
for(i=0;i<100;i++)
{
sum+ =i;
}
printf("thesumof1+2+...+is%d",sum);
}
执行如下命令编译sum.c(加-g选项产生debug信息):
gcc–g–osumsum.c
在命令行上键入gdbsum并按回车键就可以开始调试sum了,再运行run命令执行sum,屏幕上将看到如下内容:
list命令:
list命令用于列出源代码,对上述程序两次运行list,将出现如下画面(源代码被标行号):
根据列出的源程序,如果我们将断点设置在第5行,只需在gdb命令行提示符下键入如下命令设置断点:
(gdb)break5,执行情况如下图:
这个时候我们再run,程序会停止在第5行,如下图:
设置断点的另一种语法是break,它在进入指定函数(function)时停住。
相反的,clear用于清除所有的已定义的断点,clear清除设置在函数上的断点, clear则清除设置在指定行上的断点
。
watch命令:
watch命令用于观查变量或表达式的值,我们观查sum变量只需要运行watchsum:
watch为表达式(变量)expr设置一个观察点,一量表达式值有变化时,程序会停止执行。
要观查当前设置的watch,可以使用infowatchpoints命令。
next、step命令:
next、step用于单步执行,在执行的过程中,被watch变量的变化情况将实时呈现(分别显示Oldvalue和Newvalue),如下图:
next、step命令的区别在于step遇到函数调用,会跳转到到该函数定义的开始行去执行,而next则不进入到函数内部,它把函数调用语句当作
一条普通语句执行。
4.Make
make是所有想在Linux系统上编程的用户必须掌握的工具,对于任何稍具规模的程序,我们都会使用到make,几乎可以说不使用make的程序不具
备任何实用价值。
在此,我们有必要解释编译和连接的区别。
编译器使用源码文件来产生某种形式的目标文件(objectfiles),在编译过程中,外部的符号参考
并没有被解释或替换(即外部全局变量和函数并没有被找到)。
因此,在编译阶段所报的错误一般都是语法错误。
而连接器则用于连接目标文
件和程序包,生成一个可执行程序。
在连接阶段,一个目标文件中对别的文件中的符号的参考被解释,如果有符号不能找到,会报告连接错误
。
编译和连接的一般步骤是:
第一阶段把源文件一个一个的编译成目标文件,第二阶段把所有的目标文件加上需要的程序包连接成一个可执行文
件。
这样的过程很痛苦,我们需要使用大量的gcc命令。
而make则使我们从大量源文件的编译和连接工作中解放出来,综合为一步完成。
GNUMake的主要工作是读进一个文本文件,称为makefile。
这
个文件记录了哪些文件(目的文件,目的文件不一定是最后的可执行程序,它可以是任何一种文件)由哪些文件(依靠文件)产生,用什么命
令来产生。
Make依靠此makefile中的信息检查磁盘上的文件,如果目的文件的创建或修改时间比它的一个依靠文件旧的话,make就执行相应的
命令,以便更新目的文件。
假设我们写下如下的三个文件,add.h用于声明add函数,add.c提供两个整数相加的函数体,而main.c中调用add函数:
/*filename:
add.h*/
externintadd(inti,intj);
/*filename:
add.c*/
intadd(inti,intj)
{
returni+j;
};
/*filename:
main.c*/
#include"add.h"
main()
{
inta,b;
a=2;
b=3;
printf("thesumofa+bis%d",add(a+b));
};
怎样为上述三个文件产生makefile呢?
如下:
-------------------------
test:
main.oadd.o
gccmain.oadd.o-otest
main.o:
main.cadd.h
gcc-cmain.c-omain.o
add.o:
add.cadd.h
gcc-cadd.c-oadd.o
-----------------------
(注意分割符为TAB键)
上述makefile利用add.c和add.h文件执行gcc-cadd.c-oadd.o命令产生add.o目标代码,利用main.c和add.h文件执行gcc-cmain.c-o
main.o命令产生main.o目标代码,最后利用main.o和add.o文件(两个模块的目标代码)执行gccmain.oadd.o-otest命令产生可执行文件
test。
我们可在makefile中加入变量,另外。
环境变量在make过程中也被解释成make的变量。
这些变量是大小写敏感的,一般使用大写字母。
Make变
量可以做很多事情,例如:
i)存储一个文件名列表;
ii)存储可执行文件名;
iii)存储编译器选项。
要定义一个变量,只需要在一行的开始写下这个变量的名字,后面跟一个=号,再跟变量的值。
引用变量的方法是写一个$符号,后面跟(变量
名)。
我们把前面的makefile利用变量重写一遍(并假设使用-Wall-O–g编译选项):
OBJS=main.oadd.o
CC=gcc
CFLAGS=-Wall-O-g
test:
$(OBJS)
$(CC)$(OBJS)-otest
main.o:
main.cadd.h
$(CC)$(CFLAGS)-cmain.c-omain.o
add.o:
add.cadd.h
$(CC)$(CFLAGS)-cadd.c-oadd.o
makefile中还可定义清除(clean)目标,可用来清除编译过程中产生的中间文件,例如在上述makefile文件中添加下列代码:
clean:
rm-f*.o
运行makeclean时,将执行rm-f*.o命令,删除所有编译过程中产生的中间文件。
不管怎么说,自己动手编写makefile仍然是很复杂和烦琐的,而且很容易出错。
因此,GNU也为我们提供了Automake和Autoconf来辅助快速自动
产生makefile,读者可以参阅相关资料。
5.小结
本章主要阐述了Linux程序的编写、编译、调试方法及make,实际上就是引导读者学习怎样在Linux下编程,为后续章节做好准备。
Linux下的C编程实战
(二)
――文件系统编程
1.Linux文件系统
Linux支持多种文件系统,如ext、ext2、minix、iso9660、msdos、fat、vfat、nfs等。
在这些具体文件系统的上层,Linux提供了虚拟
文件系统(VFS)来统一它们的行为,虚拟文件系统为不同的文件系统与内核的通信提供了一致的接口。
下图给出了Linux中文件系统的关系:
--[if!
vml]-->
--[endif]-->
在Linux平台下对文件编程可以使用两类函数:
(1)Linux操作系统文件API;
(2)C语言I/O库函数。
前者依赖于Linux系统调用,
后者实际上与操作系统是独立的,因为在任何操作系统下,使用C语言I/O库函数操作文件的方法都是相同的。
本章将对这两种方法进行实例讲
解。
2.Linux文件API
Linux的文件操作API涉及到创建、打开、读写和关闭文件。
创建
intcreat(constchar*filename,mode_tmode);
参数mode指定新建文件的存取权限,它同umask一起决定文件的最终权限(mode&umask),其中umask代表了文件在创建时需要去掉的一些存取
权限。
umask可通过系统调用umask()来改变:
intumask(intnewmask);
该调用将umask设置为newmask,然后返回旧的umask,它只影响读、写和执行权限。
打开
intopen(constchar*pathname,intflags);
intopen(constchar*pathname,intflags,mode_tmode);
open函数有两个形式,其中pathname是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下面),flags可以去下面的一个值或者是几
个值的组合:
标志
含义
O_RDONLY
以只读的方式打开文件
O_WRONLY
以只写的方式打开文件
O_RDWR
以读写的方式打开文件
O_APPEND
以追加的方式打开文件
O_CREAT
创建一个文件
O_EXEC
如果使用了O_CREAT而且文件已经存在,就会发生一个错误
O_NOBLOCK
以非阻塞的方式打开一个文件
O_TRUNC
如果文件已经存在,则删除文件的内容
O_RDONLY、O_WRONLY、O_RDWR三个标志只能使用任意的一个。
如果使用了O_CREATE标志,则使用的函数是intopen(constchar*pathname,intflags,mode_tmode);这个时候我们还要指定mode标志,用
来表示文件的访问权限。
mode可以是以下情况的组合:
标志
含义
S_IRUSR
用户可以读
S_IWUSR
用户可以写
S_IXUSR
用户可以执行
S_IRWXU
用户可以读、写、执行
S_IRGRP
组可以读
S_IWGRP
组可以写
S_IXGRP
组可以执行
S_IRWXG
组可以读写执行
S_IROTH
其他人可以读
S_IWOTH
其他人可以写
S_IXOTH
其他人可以执行
S_IRWXO
其他人可以读、写、执行
S_ISUID
设置用户执行ID
S_ISGID
设置组的执行ID
除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以自己用数字来表示,Linux总共用5个数字来表示文件的各种权限:
第一位表示
设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;最后一位表示其他人的权限。
每个数字可以取1(执
行权限)、2(写权限)、4(读权限)、0(无)或者是这些值的和。
例如,要创建一个用户可读、可写、可执行,但是组没有权限,其他人可以读、
可以执行的文件,并设置用户ID位。
那么,我们应该使用的模式是1(设置用户ID)、0(不设置组ID)、7(1+2+4,读、写、执行)、0(没有权限)、
5(1+4,读、执行)即10705:
open("test",O_CREAT,10705);
上述语句等价于:
open("test",O_CREAT,S_IRWXU|S_IROTH|S_IXOTH|S_ISUID);
如果文件打开成功,open函数会返回一个文件描述符,以后对该文件的所有操作就可以通过对这个文件描述符进行操作来实现。
读写
在文件打开以后,我们才可对文件进行读写了,Linux中提供文件读写的系统调用是read、write函数:
intread(intfd,constvoid*buf,size_tlength);
intwrite(intfd,constvoid*buf,size_tlength);
其中参数buf为指向缓冲区的指针,length为缓冲区的大小(以字节为单位)。
函数read()实现从文件描述符fd所指定的文件中读取length个字
节到buf所指向的缓冲区中,返回值为实际读取的字节数。
函数write实现将把length个字节从buf指向的缓冲区中写到文件描述符fd所指向的文
件中,返回值为实际写入的字节数。
以O_CREAT为标志的open实际上实现了文件创建的功能,因此,下面的函数等同creat()函数:
intopen(pathname,O_CREAT|O_WRONLY|O_TRUNC,mode);
定位
对于随机文件,我们可以随机的指定位置读写,使用如下函数进行定位:
intlseek(intfd,offset_toffset,intwhence);
lseek()将文件读写指针相对whence移动offset个字节。
操作成功时,返回文件指针相对于文件头的位置。
参数whence可使用下述值:
SEEK_SET:
相对文件开头
SEEK_CUR:
相对文件读写指针的当前位置
SEEK_END:
相对文件末尾
offset可取负值,例如下述调用可将文件指针相对当前位置向前移动5个字节:
lseek(fd,-5,SEEK_CUR);
由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度:
lseek(fd,0,SEEK_END);
关闭
当我们操作完成以后,我们要关闭文件了,只要调用close就可以了,其中fd是我们要关闭的文件描述符:
intclose(intfd);
例程:
编写一个程序,在当前目录下创建用户可读写文件“hello.txt”,在其中写入“Hello,softwareweekly”,关闭该文件。
再次打开该
文件,读取其中的内容并输出在屏幕上。
#include
#include
#include
#include
#defineLENGTH100
main()
{
intfd,len;
charstr[LENGTH];
fd=open("hello.txt",O_CREAT|O_RDWR,S_IRUSR|S_IWUSR);/*创建并打开文件*/
if(fd)
{
write(fd,"Hello,SoftwareWeekly",strlen("Hello,softwareweekly"));/*写入Hello,softwareweekly字符串*/
close(fd);
}
fd=open("hello.txt",O_RDWR);
len=read(fd,str,LENGTH);/*读取文件内容*/
str[len]='"0';
printf("%s"n",str);
close(fd);
};
编译并运行,执行
3.C语言库函数
C库函数的文件操作实际上是独立于具体的操作系统平台的,不管是在DOS、Windows、Linux还是在VxWorks中都是这些函数:
创建和打开
FILE*fopen(constchar*path,constchar*mode);
fopen()实现打开指定文件filename,其中的mode为打开模式,C语言中支持的打开模式如下表:
标志
含义
r,rb
以只读方式打开
w,wb
以只写方式打开。
如果文件不存在,则创建该文件,否则文件被截断
a,ab
以追加方式打开。
如果文件不存在,则创建该文件
r+,r+b,rb+
以读写方式打开
w+,w+b,wh+
以读写方式打开。
如果文件不存在时,创建新文件,否则文件被截断
a+,a+b,ab+
以读和追加方式打开。
如果文件不存在,创建新文件
其中b用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux不区分二进制文件和文本文件。
读写
C库函数支持以字符、字符串等为单位,支持按照某中格式进行文件的读写,这一组函数为:
intfgetc(FILE*stream);
intfputc(intc,FILE*stream);
char*fgets(char*s,intn,FILE*stream);
intfputs(constchar*s,FILE*stream);
intfprintf(FILE*stream,constchar*format,...);
intfscanf(FILE*stream,constchar*format,...);
size_tfread(void*ptr,size_tsize,size_tn,FILE*stream);
size_tfwrite(constvoid*ptr,size_tsize,size_tn,FILE*stream);
fread()实现从流stream中读取加n个字段,每个字段为size字节,并将读取的字段放入ptr所指的字符数组中,返回实际已读取的字段数。
在读
取的字段数小于num时,可能是在函数调用时出现错误,也可能是读到文件的结尾。
所以要通过调用feof()和ferror()来判断。
write()实现从缓冲区ptr所指的数组中把n个字段写到流stream中,每个字段长为size个字节,返回实际写入的字段数。
另外,C库函数还提供了读写过程中的定位能力,这些函数包括
intfgetpos(FILE*stream,fpos_t*pos);
intfsetpos(FILE*stream,constfpos_t*pos);
intfseek(FILE*stream,longoffset,int