第4章 Linux下的C语言开发基础
4.1 C语言开发的基本步骤
C语言源程序开发的基本步骤如下:
- 根据项目需求划分功能模块
- 编辑。利用文本编辑器vi或gedit编写C源程序并保存,文件的后缀为.c(C++程序就是.cpp)
- 编译。将源程序进行编译,生成目标代码
- 链接。由gcc编译器将编译中生成的目标代码和库文件进行链接生成可执行文件。
- 执行产生的可执行文件,查看程序运行结果,如果能正常运行并得到预想的结果,则开发成功;否则根据错误提示,回去修改源代码。
4.1.1 gcc编译工具
上文提到,gcc编译工具主要完成c语言源程序到可执行文件的转换,那么gcc编译器到底如何使用呢。
首先来了解gcc命令的语法格式
gcc 语法格式:gcc [选项] 参数
gcc 命令的主要选项及作用:
[-o] 指定
目标
文件的名称[-g]
使生成的可执行程序中包含debug信息
[-c] 只编译不链接。(
c就是compile
)[-E] 只做
预编译
处理。[-S]
由C翻译成汇编
在选项省略的情况下,gcc是最简单的使用方式
案例:使用vi编辑器,新建一个"main.c"文件,代码如下
#include<stdio.h>
int main()
{
printf("my first C program\n");
return 0;
}
编辑完成后,在命令终端输入命令:gcc main.c 。这时会生成一个名为a.out的可执行文件,若想指定可执行文件名称为“first’’,则在命令终端输入命令:gcc -o first main.c
在终端输入./first 后按下Enter键,则会看到程序的运行结果。
4.1.2 gcc编译过程详解
gcc编译器编译c语言程序生成可执行文件的过程,看起来是经过一步完成的,实际上是经历了四个步骤。现在我们通过一个例子来对gcc编译的过程进行了解。
前提:首先编写一个源代码文件,main.c
#define PI 3.14
#include <stdio.h>
int main()
{
double r=1.0,s;
s=PI*r*r;
printf("s=%lf",s);
return 0;
}
gcc编译过程:
第1步:预编译阶段。
该阶段主要是处理源文件中所有的伪指令,包括宏定义、头文件包含等,gcc会将头文件及宏定义的内容全部展开到当前文件中。
命令:gcc -E -o main.i main.c
其中main.i就是预编译后生成的中间文件
,可以使用vi打开main.i,查看其内容。
第2步:编译阶段
在该阶段,编译器完成c语言到汇编语言的转换
命令:gcc -S -o main.s main.i 或者直接gcc main.i -S 也会生成main.s文件
(问题:直接gcc main.i -s会生成a.out文件,也可以正常执行)
注意:选项中S为大写,不能用小写
#测试:
[zsh@localhost dotest]$ gcc main.i -S
[zsh@localhost dotest]$ ls
main.c main.i main.s
这样在当前目录下就生成了一个main.s文件(文件内容为汇编语言
)。
第3步:汇编阶段
该阶段是将汇编语言翻译成二进制目标代码。
命令:gcc -c -o main.o main.s
执行命令后,会在当前目录下生成名为main.o的二进制文件
#查看二进制文件
od main.o
第4步:链接阶段
在该阶段链接器将多个目标代码文件(以后还可能和库文件)进行链接,最终生成可执行文件。
输入命令:gcc main.o ,会生成一个名为a.out的可执行文件
输入命令:gcc -o first main.o ,会生成一个名为first的可执行文件。
执行a.out或first文件,在终端输入./a.out或./first
通常情况下,我们将前三步统称为编译阶段,将最后一步成为链接阶段。
操作过程图:
编译全过程
$ ls
main.c
$ gcc -E -o main.i main.c
$ ls
main.c main.i
$ vi main.i
$ gcc -S -o main.s main.i
$ ls
main.c main.i main.s
$ vi main.s
$ gcc -c -o main.o main.s
$ ls
main.c main.i main.o main.s
$ od main.o (接下来会直接在终端输出一连串01代码)
$ ls
main.c main.i main.o main.s
$ gcc main.o
$ ls
a.out main.c main.i main.o main.s
$ gcc -o first main.o
$ ls
a.out first main.c main.i main.o main.s
$ ./a.out
s=3.140000
$ ./first
s=3.140000
另一种方式:
$ ls
main.c
$ gcc -E -o main.i main.c
$ ls
main.c main.i
$ gcc main.i -S (或者gcc -S main.i)
$ ls
main.c main.i main.s
$ gcc main.s -c (或者gcc -c main.s)
$ ls
main.c main.i main.o main.s
$ gcc main.o
$ ls
a.out main.c main.i main.o main.s
$ ./a.out
s=3.140000
总结
#gcc编译和链接命令的格式
gcc -E -o main.i main.c
说明:
-E 表示预编译
-o 表示将编译后的文件命名为XX,后面紧跟文件名字
4.1.3 gcc编译多文件
实际的项目功能比较复杂,往往由多个源文件组成。为了使代码结构更加合理,通常将主函数和其它函数放在不同的源文件中。除了主函数外,每个函数都有函数声明和函数实现两部分。函数的声明、宏定义、自定义类型、类型别名等内容通常放在头文件中(即.h文件),头文件中甚至还可以包含头文件。函数的实现放在.c文件(.cpp文件)中。
如果项目有多个源文件,基本上有两种编译方法,具体如下:
例4-2:使用gcc编译多个源文件
main.c内容:
#include<stdio.h>
#include "bank.h"
main()
{
int a=5,b=18,c;
c=max(a,b);
printf("a与b的最大值为%d",c);
}
m.c文件内容:
int max(int a,int b)
{
if(a>b)
return a;
else
return b;
}
头文件bank.h内容
int max(int a,int b); //或者int max(int ,int )
//在这个例子中bank.h文件包含max函数的声明
//m.c文件包含max函数的实现,文件main.c文件包含函数的调用。
现在要将上述文件进行编译
方法1:多个文件一起编译
#用法;gcc main.c m.c -o test
[zsh@localhost dotest]gcc main.c m.c -o test
#该方法实际上是将多个源文件分别编译后再链接成test可执行文件。
方法2:分别编译各个源文件,之后再对目标文件进行链接
#用法:
gcc -c main.c #将main.c编译成main.o
gcc -c m.c #将m.c编译成m.o
gcc main.o m.o -o test #将main.o和m.o链接成可执行文件test
总结:第一种方法需要全部编译,第二种方法可以因需编译。
4.2 头文件
C语言标准库中的头文件有15个之多,常用的4个头文件包括stdio.h,string.h,math.h,stdlib.h。
4.2.1 头文件的编辑和使用
除了C语言标准库头文件和Unix标准中通用的头文件,用户可以自定义头文件。
案例说明:在m.c文件中实现求两个学生成绩的最高成绩的功能,main.c中调用m.c中的函数max,然后输出两个学生中的最高成绩。步骤如下:
1.在bank.h头文件定义一个学生结构体类型,成员变量包括学号id、姓名name、成绩score。代码有几种写法:
C语言写法:
①struct student
{
int id;
char name[20];
float score;
};
//定义了一个结构体类型,类型名为struct student
②typedef struct student
{
int id;
char name[20];
float score;
}STU;
//STU为结构体的别名
C++写法:
class student
{
int id;
char name[20];
float score;
};
2.使用头文件,在main.c、m.c文件中的内容如下:
main.c文件
#include<stdio.h>
#include<string.h>
#include"bank.h"
main()
{
struct student stu1,stu2,stumax;
stu1.id=1;
strcpy(stu1.name,"zhangsan");
stu1.score=89.5;
stu2.id=2;
strpy(stu2.name,"lisi");
stu2.score=96;
stumax=max(stu1,stu2);
printf("stu1和stu2成绩最高分是%f\n",stumax.score);
}
m.c内容:
#include"bank.h"
struct student max(struct student a,struct student b) //此处struct student整体为一个类型名
{
if(a.score>b.score)
return a;
else
return b;
}
bank.h内容
struct student max(struct student a,struct student b);//函数声明
struct student
{
int id;
char name[20];
float score;
};
测试
$ gcc main.c m.c -o main
$ ls
bank.h main main.c Makefile m.c
$ ./main
stu1和stu2成绩最高分是96.000000
4.2.2 进一步理解头文件
头文件的包含有两种写法,对于C标准库的头文件和UNIX标准中通用的头文件用“<>”括起来,对于自定义的头文件用双引号括起来。如下:
#include<stdio.h> //标准库头文件
#include"bank.h" //自定义头文件
1.用“<>”括起来的头文件,编译器会自动从系统目录中寻找头文件
,系统目录
通常是指:
/usr/lib/gcc/i686-linux-gnu/4.6/include
/usr/local/include
/usr/include/i386-linux-gnu/
/usr/include/
我用的centos 7.0则是如下所示:
C标准库的头文件和UNIX标准中通用的头文件都存放在系统目录中,所以编译器自然能够找到。
我们可以做一个测试,使用find命令在系统目录中查找某个标准库头文件(stdio.h),操作步骤如下所示:
[zsh@localhost ~]$ sudo find /usr -name "stdio.h"
[sudo] zsh 的密码:
/usr/lib/x86_64-redhat-linux6E/include/bits/stdio.h
/usr/lib/x86_64-redhat-linux6E/include/stdio.h
/usr/include/bits/stdio.h
/usr/include/stdio.h
#由此可知stdio.h头文件确实存在于系统目录下
2.用双引号直接括起来的头文件与源文件在一个目录下。这样,编译器会先在该目录(当前工作目录)中搜索
,如果搜索不到再去系统目录下搜索。
特别提示:有时候为了规范地对项目进行管理,通常情况下C源文件和头文件不在同一个目录下
。那该怎么办呢?
处理办法如下:
①在C源文件中写法:#include"相对路径/xxx.h"
②在C源文件中写法:#include "xxx.h"
;然后在编译时写法:gcc -I 相对路径
(可以结合makefile的知识)
③将xxx.h移动到系统目录下。(不推荐)
4.2.3 头文件重复包含
在项目开发中经常会出现头文件重复包含的问题,可以用一个例子来说明。假设头文件A.h中包含头文件C.h,同时头文件B.h中也包含C.h,而在源文件中同时包含了A.h和B.h,这样编译器编译时就会出现头文件C.h重复包含的问题。如图所示:
实验:
C.h内容:
struct teacher
{
int id;
char name[20];
int age;
};
--------------------
A.h内容:
#define PI 3.14
#include<stdio.h>
#include"C.h"
--------------------
B.h内容:
#define x 3*4
#include"C.h"
--------------------
main.c源文件的内容:
#include "A.h"
#include "B.h"
int main()
{
printf("%f",PI*x);
return 0;
}
//编译时会出现重复定义
如果头文件中重复包含一些函数的声明,那么在编译时不会出现错误,但是会大大降低编译的效率(相当于头文件重复展开)
避免头文件重复包含的解决办法就是在头文件中使用条件编译进行控制。格式如下:
#ifndef _MY_H_
此处是任意合法标识符,通常使用大写,并且加上适当的下划线。MY一般是指头文件的主文件名的大写形式
#define _MY_H_
....//头文件中要包含的内容,如函数声明,结构体定义
#endif
对上述例子中的C.h使用条件编译:
#ifndef _C_H_
#define _C_H_
struct teacher
{
int id;
char name[20];
int age;
};
#endif
4.3 gdb调试工具
在windows开发中我们直接在IDE中进行调试,而在Linux下使用调试器gdb进行调试。
4.3.1 gdb调试基本命令
现在介绍gdb的常用命令和功能:
1.文件清单
命令:lsit/l
作用:列出产生执行文件的源代码的一部分。
举例:
(1)列出10到20行之间的源代码
list 10 20
(2)输出函数max前后的5行程序源代码
list max
2.执行程序
命令:run/r
作用:运行准备调试的程序
3.数据显示
命令:print/p
作用:print是gdb中功能很强的一个命令,利用它可以显示被调试的语言中任何有效的表达式。表达式除了包含程序中的变量外,还包含函数的调用。
举例:
(1)print p
(2)(gdb) print find_entry(1,0)
4.设置与清除断点
命令:break/b
作用:使程序恰好在执行给定行之前停止;使程序恰好在进入指定的函数之前停止。
举例:
(1)break line-number
(2)break function-name
以下是gdb调试的主要步骤及各个命令的使用:
//注意:下面gdb指令中step/s表示用全拼step,或者缩写s都可以
gcc -g mian.c //在目标文件加入源代码信息,默认生成目标文件a.out
gdb a.out
(gdb) start //开始调试
(gdb) n //一条一条执行
(gdb) step/s //执行一行源代码,如果此行代码中有函数调用,则进入该函数
(gdb) backtrace/bt //查看函数调用栈帧
(gdb) info/i locals //查看当前栈帧局部变量
(gdb) frame/f //选择栈帧,再查看局部变量
(gdb) print/p //打印变量的值
(gdb) finish //运行到当前函数返回
(gdb) set var sum=0 //修改变量的值
(gdb) list/l 行号或函数名 //列出源码(默认10行)
(gdb) display/undisplay sum //每次停下显示变量的值/取消跟踪
(gdb) break/b 行号或函数名 //设置断点
(gdb) continue/c //连续运行,直接执行完了
(gdb) info/i breakpoints //查看已经设置的断点
(gdb) delete breakpoints 2 //删除某个断点
(gdb) disable/enable breakpoints 3 //禁用/启用某个断点
(gdb) break 9 if sum!=0 //满足条件才激活断点
(gdb) run/r //重新从程序开头连续执行
(gdb) watch input[4] //设置观察点
(gdb) info/i watchpoints //查看设置的观察点
(gdb) x/7b input //打印存储器的内容,其中b表示每个字节组,7表示打印7组
(gdb) disassemble //反汇编当前函数或指定函数
(gdb) si //si命令类似s命令,所不同的是,si针对的是汇编指令,而s命令针对源代码
(gdb) info registers //显示所有寄存器的当前值
(gdb) x/20 $esp //查看内存中国开始的20个数
部分操作截图如下:
此处跳转上一篇文章 第3章 文本编辑器