Linux中的C语言程序设计
C语言:是Linux中最常用的系统编程语言之一。功能极其强大的程序设计语言。包含直接操作内存和硬件的底层计算语句,也包含高级语言所具有的数据结构、控制语句、函数调用等功能。是一门编译语言,而不是脚本语言。可使用Linux函数库和系统调用。
如何编写和执行C语言程序:使用任何文本编辑器编写C语言程序源文件.c。使用C语言编译器gcc编译源文件,生成独立可执行文件。在遇到错误时,编译器会给出错误信息停止编译,不会生成目标文件。
gcc编译格式:
gcc 源文件名 #生成的目标文件名为默认文件名a.out
gcc 源文件名 -o 目标文件名
gcc -o 目标文件名 源文件名
C语言程序构成:预处理器指令+程序主体。
预处理器指令:在文件开头以#开始的指令,不会被C语言编译器采用。常用来引用C语言头文件.h或设定宏。
#include <stdio.h>
#define PI 3.14
程序主体:变量声明、函数声明、函数定义、main主程序。
变量和常数:变量名必须以字母或_开头,其余部分可以包含字母、数字和_。每个变量必须声明类型后才能使用。变量在使用前必须初始化。
基本类型:int, char, float, double
派生类型:数组和指针
用户定义类型:结构,枚举,typedef
常量:
普通常量:const 类型 常量名=常量值
符号常量:#define 常量名 常量值
类型转换:
自动转换:数值类型自动转换,将一个较低类型变换为较高类型。表达式的值自动转换成赋值左边的类型,有可能造成信息丢失。char和short类型会自动变换为int类型。
float pi = 22/7.0;
隐式变换:常量使用后缀:L,F。强制类型转换:(类型)表达式
数组:数组声明:type array[n]={val1,val2,...,valn}; 数组下标从0开始,如长度为n,则最后一个数组下标为n-1。C编译器不做数组边界检查,可以访问未声明的数组下标项,但很危险。数组的实质是指向一组连续内存空间的指针。数组名表示的是数组中第一个元素的内存位置。字符串就是一个char类型的数组,最后一个元素为NULL(\0)。
控制结构语句:与awk和perl的控制结构语句格式相似。
switch语句
switch (exp) {
case val1: statements1; break;
case val2: statements2; break;
...
default: statementsn;
}
do-while语句
do {
statements;
} while (exp)
函数:
函数声明:
void function(void);
type function(type1 var1, type2 var2, ...);
函数定义:定义局部变量和函数体。
函数调用:
function()
var=function(exp1,exp2,...)
return 结束函数并返回值
函数参数传递:传值:实参的值赋给独立的形参。传址:实参的地址赋给形参的指针。
指针:单个内存位置的数值地址。指针变量:存储指针的变量。用途:直接操作内存地址、函数传址调用。
声明:type *p;
内存地址:&p
存储数据:*p
指针变量赋值:p=&a; *p=val;
指针运算:指针类型的大小不同于其指向的数据类型。指针的大小是固定的。指针之间的算术运算按照指针指向的存储空间大小来进行。对于未初始化的指针进行访问和操作都是危险的。任何类型的指针都可以指向NULL,void类型的指针可以指向任何类型。
int *p; //sizeof(p)=8; sizeof(*p)=4
int *p=&a;
p+n 等价于 p+n*sizeof(p)
*(array+n)等价于array[n]
*(p++) //把指针往后挪了一个步长,然后取值
*(p)++ //把p值取出,把值+1
&array[m]-&array[n]等于m-n //返回的不是地址差值,是差几个单元
指针与字符数组:
char st1[] = {'H','e','l','l','o','\0'}; //最后一位是NULL字符。
char st2[] = "Hello"; //给字符数组赋值字符串时会自动添加NULL
char *st3 = "Hello"; //给char指针赋值字符串时会自动添加NULL
可以通过printf的%s来输出st1,st2,st3
st1和st2是常量,不能进行赋值操作
st3可以进行指针运算,可以指向别的地址,也可以指向其他数组
C语言中不能直接比较两个字符串(st1 == st2 是非法的)
C语言命令行参数:在main函数定义中给出形参。在执行时,命令行参数存储在字符串数组argv中,字符数组的指针。参数个数存储在整型变量argc中。形参名字可以自定义。
int main(int argc, char *argv[ ])
C语言编译器和相关工具
C语言程序开发工具:编译器gcc,编译器更新make,生成库ar,调试工具gdb,版本控制工具。
gcc编译器:gcc命令包含预处理.i、编译.s、汇编(不可直接运行)和连接(加上头文件/库文件)4个步骤,最终生成可执行代码。gcc接受多种文件类型并依据用户指定的选项做相应处理。
格式:gcc [options] filenames
文件类型 | 对应程序 |
.c | C语言源程序 |
.i | 经过预处理的源程序 |
.a | 编译后的库文件 |
.s | 汇编语言源程序 |
.h | 预处理文件(头文件) |
.o | 目标文件 |
选项 | 含义(得到4个步骤的中间结果) |
-o file | 将目标文件保存到file |
-E | 只做预处理,生成经过预处理的.i文件 |
-S | 只做预处理和编译阶段,生成汇编代码.s文件 |
-c | 不做连接步骤,生成.o目标文件 |
-I dir | 在头文件搜索路径中加入dir |
-L dir | 在库文件搜索路径中加入dir |
-l lib | 连接库文件lib |
-O[0-3] | 根据指定级别进行编译优化 |
gcc编译过程:①预处理(符号常量换成设定的值):预处理器将所有预处理指令替换为编译器可以理解的形式。②编译:编译器将C源代码转换为汇编语言代码。③汇编:汇编器(as)将汇编代码转换成目标代码,但这些代码并不完整。④链接:链接器(ld)将目标代码和系统函数库文件和其他目标文件链接起来,生成单一可执行文件。
C语言程序源文件:
头文件.h | 系统函数声明等stdio.h 用户自定义函数声明 |
源文件.c | main函数定义 头文件中声明的函数的定义部分 |
库文件.a .so | 系统函数库文件,包括静态链接库.a和动态链接库.o gcc source -o target -l libfile 库文件必须以lib开头,执行-l引用时可以省略lib和.a .so |
编译多个源文件:
gcc -c file1.c file2.c ... //编译多个.c文件生成相应的.o目标文件
gcc -o file file1.o file2.o ... //链接.o文件生成可执行目标文件file
保留目标文件.o的意义:假设c文件的编译依赖于a,b文件,当a文件有更新时,只需重新编译a文件,而不需要重新编译未修改的b文件,只需要在最后阶段链接已有的b.o文件即可。可以合并一组.o文件,构成一个库,供将来使用。
如何发现某个文件已被更新?可以比较.o文件和其源文件.c的修改时间。如果.c文件新于.o文件,则需要重新编译该文件,并重新编译所有依赖于该文件的文件。
makefile:指定每个文件的依赖关系,并在依赖文件有更新时执行相应的操作。当一个源文件被修改时,所有依赖于该源文件的文件都需要进行重新编译。使用make命令来检查依赖文件的更新状态并执行makefile中设定的操作。
makefile文件格式:
包含一组规则,每个规则有如下形式:
target: dependency_list //目标文件以及其依赖于那些文件
command_list //注意前面是制表符,如更新,重新编译
//当dependency_list中的任何文件修改时间晚于target,则执行command_list
当目标文件与依赖项的基名称相同,则不需要command_list。
(例如:quit.o: quit.h quit.c 不需要指定gcc -c quit.c)
可以在依赖项中省略同名源文件本身。
(例如:quit.o: quit.h quit.c 中可以省略quit.c)
make命令支持用-f选项指定makefile文件,支持使用target作为参数执行指定规则。(冒号之前的)
cat makefile
#Makefile
interest: interest.o arg_check.o quit.o
gcc -o interest interest.o arg_check.o quit.o -lm -g
interest.o: interest.c arg_check.h quit.h
gcc -c interest.c -g
quit.o: quit.h quit.c
gcc -c quit.c -g
arg_check.o: arg_check.h arg_check.c
gcc -c arg_check.c -g
//判断冒号之后的list文件有无更新 有更新则执行第二行的gcc命令
//如果.o后面的list更新 则重新编译list里的.c文件
//修改interest.c后
make
gcc -c interest.c -g //interest.o更新 所以interest更新
gcc -o interest interest.o arg_check.o quit.o -lm -g
//修改arg_check.c后
make
gcc -c arg_check.c -g //quit.o更新 所以interest更新
gcc -o interest interest.o arg_check.o quit.o -lm -g
makefile中的变量:makefile支持声明和使用变量。变量分为环境变量、预定义变量和自动变量。变量的声明:在makefile开头以var=value来定义。变量的使用:在规则中用$(var)来使用。make命令会将变量替换为其当前值。(相当于shell命令,字符串赋给一个变量,如CC=gcc)
预定义变量和自动变量:
变量名称 | 含义 |
CC | C编译器名称,默认为cc |
AS | 汇编器名称,默认为as |
RM | 文件删除命令名称,默认为rm -f |
AR | 库文件管理命令名称,默认为ar |
$* | 不包含扩展名的目标文件名称 |
$+ | 所有依赖文件,以空格分开,可能出现重复 |
$< | 第一个依赖文件名称 |
$? | 所有时间戳比target文件晚的依赖文件 |
$@ | 目标文件的完整名称 |
$^ | 所有不重复的依赖文件 |
ar:生成静态库命令:将.o文件打包成一个静态库,便于管理。类似于tar命令。静态库文件命名方式:lib+name+.a
ar [option] libname.a file_list
-r 添加文件到库中,或替换已有文件
-q 在库中追加文件
-x 从库中提取某文件
-d 删除库中的某文件
-t 显示库内容
-v 显示操作内容
ar -rv libit.a quit.o arg_check.o
生成动态共享库:动态共享库是在程序执行时去链接的库。共享库命名规则:lib+name+.so
生成共享库:
gcc -shared libname.so file1.o file2.o ...
使用共享库:
编译时声明:gcc -o main main.c -lname -Ldir //告诉动态链接库的位置dir
程序运行时动态共享库的路径指定:①将.so文件复制到/lib或/usr/lib目录下(需要root权限)。②设置环境变量LD_LIBRARY_PATH
静态库与共享库:①使用-l参数链接库文件。②对于符合命名规则的库文件,可以直接使用-lname。③默认库文件路径:/lib和/usr/lib。④gcc使用-L选项指定库文件目录。⑤-l参数会优先选择共享库(动态),然后是静态库。
gcc interest.c -lit -L. -lm
//-L. 当前目录
//-lm 系统库文件
静态库 | 动态库 |
.a | .so |
在链接时插入目标文件 | 在运行时调用 |
运行时不需要库文件 | 运行时需要指定库文件路径 |
可执行文件占用空间大 | 可执行文件占用空间小 |
在makefile中管理库文件:在makefile中使用ar_name(file_name)的方式来访问库文件。执行make命令时,会调用ar命令对库中的文件进行更新。
CC=gcc
interest: interest.o libit.a(quit.o) libit.a(arg_check.o)
$(CC) -o $@ $< libit.a -lm
interest.o: quit.h arg_check.h
libit.a(quit.o): quit.h
libit.a(arg_check.o): arg_check.h
//如果源文件有更新,则更新在库文件中
调试器:调试器时控制另一个程序运行的程序。可以捕获任意时刻的程序状态;可以指定断点,暂停程序,观察变量的状态;可以修改程序的错误,并重新运行程序。使用gcc -g选项编译程序才能使用调试器。
gdb调试器:普及度最高的调试器。支持C,C++,Fortran,Java。可以在所有Linux系统上运行。
使用模式:①调试模式:运行程序,允许监控每条语句的执行。②附加模式:将自身附加到任意正在运行的进程,获取该程序所用的内存信息。③事后模式:当程序崩溃时,可以查看被转储的core文件,以判断崩溃原因。
gdb调试器命令:
gdb [options] [可执行程序[core文件|进程ID]]
-c corefile 检查指定的core文件
-q 禁止显示介绍信息
使用方法:
用gcc -o main main.c -g 编译程序
用gdb main 启动调试器
在提示符(gdb)下输入调试命令
用quit退出gdb
命令 | 含义 |
list | 显示10行源代码 //list5 以第五行为中心的上下10行 |
list m,n | 显示m到n行源代码 |
run | 执行程序,直到下一个断点 |
cont | 继续执行 |
next | 执行下一条语句,函数体看成一条语句 |
step | 执行下一条语句,进入函数体(函数体第一条语句之后停) |
break n | 在第n行设置断点 |
break function | 在调用函数function的行设置断点 |
break n function | condition为真时,在第n行设定一个断点 |
watch x | 设定对变量或表达式x的观察,在每次x值变化时暂停 |
whatis x | 显示变量或表达式x的类型 |
display/print x | 显示变量或表达式x的值 |
quit | 退出gdb调试环境 |
Linux的C程序系统函数库
数学函数:头文件:#include <math.h> 函数库:libm.a/so ==> lm
pow 乘方函数
double pow(double x,double y)
sqrt 开平方根函数
double sqrt(double x)
随机数函数:头文件:#include <stdlib.h>
rand:产生随机数
int rand(void)
返回值:0~RAND_MAX之间的随机数
通常使用 1+(int)(n*rand()/(RAND_MAX+1.0)) 来产生1~n之间的随机数
需要先设置随机数种子
srand:设置随机数种子
void srand(unsigned int seed)
通常使用time函数返回当前时间作为种子(数值大且每次都不同)
#include<stdlib.h>
#include<stdio.h>
int main()
{
int i,j,k;
printf("The seed is %d.\n",(int)time(0));
srand((int)time(0));
for (i=0;i<10;i++)
{
j=1+(int)(10.0*rand()/(RAND_MAX+1.0));//1-10之间的随机数
k=rand()%50+1;//1-50之间的随机数
printf("%d %d ", j,k);
}
printf("\n");
}
字符函数:头文件:#include <ctype.h>
函数 | 功能 |
bool isalnum(char c) | 是否为字母或数字 |
bool isalpha(char c) | 是否为字母 |
bool isdigit(char c) | 是否为数字 |
bool islower(char c) | 是否为小写字母 |
bool isupper(char c) | 是否为大写字母 |
bool isspace(char c) | 是否为空格 |
bool ispunct(char c) | 是否为特殊符号 |
bool isxdigit(char c) | 是否为十六进制数字 |
系统时间和日期函数:#include <time.h>
struct tm
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
}
time:返回当前时间
time_t time(time_t *t) //time_t可与整数相互转换
返回值存储在t指针指向的地址
成功则返回1970年1月1日开始的秒数,失败则返回-1
localtime:取得当前时间和日期,以本地格式返回
struct tm * localtime(const time_t *timep)
输入time_t类型指针,输出tm结构类型指针
#include<time.h>
#include<stdio.h>
int main()
{
time_t timep;
char *wday[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
struct tm *p;
time(&timep);//返回值存入timep变量里
printf("The current time by time function is %d \n", timep);
printf("The current time in GMT is %s \n",asctime(gmtime(&timep)));
//gmt=标准时间格式 asc 用通常方式显示
p=localtime(&timep);
printf("The current local time is %d/%d/%d ",1900+p->tm_year,1+p->tm_mon,p->tm_mday);//返回值是2位 前面加1900
printf("%s %d:%d:%d\n", wday[p->tm_wday],p->tm_hour,p->tm_min,p->tm_sec);
return 0;
}
环境控制函数:头文件:#include <stdlib.h>
getenv:获取环境变量值
char* getenv(const char *name)
输入环境变量名称,输出环境变量值,若环境变量不存在则返回NULL
setenv:设置环境变量值
int setenv(const char *name, const char *value, int overwrite)
name为环境变量名,value为设置值,overwrite=1会覆盖原有值
成功返回0,不成功返回-1
内存分配函数:头文件:#include <stdlib.h>
malloc:分配一个动态内存空间
void *malloc(size_t size)
分配一段大小为size的动态内存空间
成功则返回指向该内存地址的指针,失败返回NULL
calloc:分配多个动态内存空间
void *calloc(int num, size_t size)
分配num个连续的大小为size的动态内存空间,并初始化为0
成功则返回指向该内存地址的指针,失败返回NULL
free:释放动态内存空间
void free(void *p)
释放p指针指向的内存空间,只能释放已分配的动态内存空间
内存操作函数:
memset:初始化指定内存空间
void *memset(void *buffer, int c, int count)
buffer是指定内存空间的指针,c为初始化内容,count是初始化字节数
将buffer所指内存区域的前count个字节设置为c
memcpy:复制内存空间
void *memcpy(void *dest, void *src, int count)
dest为目标内存区,src为源内存区,count为要复制的字节数
将src内存区中的前count字节复制到dest内存区
memcmp:比较两个内存空间的字符
int memcmp(void *buf1, void *buf2, int count)
buf1,buf2为两个内存区,count为要比较的字节数
比较内存区域的前count个字节,根据ASCII码表顺序比较
buf1<buf2, return <0; buf1=buf2, return=0; buf1>buf2, return>0
system函数:执行shell命令。头文件:#include <stdlib.h>
int system(const char *string)
输入字符串string将作为shell命令由/bin/sh执行
执行成功返回shell命令的返回值
Linux的C程序系统调用
C语言系统调用:系统调用是一种例程,可以由C程序调用来访问系统资源。(文件I/O、分配内存、进程创建)。系统调用的使用方式类似于函数调用。系统调用的很多命令 与shell命令同名。执行时需要切换到内核模式,执行完毕之后切回用户模式。内核代理操作,用户无法管理内部操作。
系统调用与库函数的区别:可以使用 man 2+系统调用名字 命令查看系统调用的文档。可以使用 man 3 命令查看库函数的文档。
系统调用 | 库函数 |
系统资源低层接口 | 高层应用接口,对低层封装 |
操作精细程度高 | 操作精细程度低 |
有时效率较低 | 效率较高 |
执行模式需要切换,开销大 | 开销较低 |
错误处理:系统调用在发生错误时返回-1(int)。错误代码——全局变量errno用一个正整数记录错误的类型信息。
通过perror命令显示与errno相关联的错误信息
#include <sys/errno.h>
void perror(const char *str)
输出str: errno错误信息 //str内容可以自定义
符号常量 | errno | 消息 |
EPERM | 1 | Operation not permitted |
ENOENT | 2 | No such file or directory |
ESRCH | 3 | No such process |
EINTR | 4 | Interrupted system call |
EIO | 5 | I/O error |
EACCES | 13 | Permission denied |
EEXIST | 17 | File exists |
ENOTDDIR | 20 | Not a directory |
EISDIR | 21 | Is a directory |
ENOSPC | 28 | No space left on device |
ESPIPE | 29 | Illegal seek |
EROFS | 30 | Read only file system |
open:打开和创建文件。头文件:#include <fcntl.h>
int open(const char* path, int oflag, int smode)
输入参数:
path 文件路径
oflag 打开模式 O_RDONLY, O_WRONLY, O_RDWR, 中间用|分隔
smode 创建文件时设定权限,可用8进制数表示,前面加0前缀
返回值:一个标识符fd
文件说明符,通常从3开始 //0-标准输入流 1-标准输出流 2-标准错误流
错误时返回-1
close:关闭文件。close会释放指定的文件说明符,供下一个open使用(释放文件资源)。当由多个open打开同一个文件时,只有所有指向该文件的说明符都释放后文件才会关闭。可以使用close关闭标准输入(0),标准输出(1),标准错误(2)。
int close(int fd)
输入参数:fd 文件说明符(open的返回值)
返回值:成功时返回0
read:读取文件。
ssize_t read(int fd, void *buf, size_t nbyte)
输入参数:
fd 文件说明符
buf 指向通用缓冲区的指针 //读的内容放在缓冲区内(固定byte大小)
nbyte 缓冲区本身的大小,也是读取的字符数
返回值:
在遇到EOF之前,返回nbyte
遇到EOF时,返回所读字符数,下一次执行read时返回0 //可用于判断是否读完
write:写入文件。把缓冲区里的东西写入文件流。
ssize_t write(int fd, const void *buf, size_t nbyte)
输入参数:每次调用都将buf中的nbyte个字节写入文件说明符fd中
返回值:
返回所写字符数 //实际写了多少字符
在磁盘已满或文件大小超出限制时返回-1
缓冲区大小:
写入设备
内核缓冲区大小
#include<fcntl.h>
//文件相关
#include<sys/stat.h>
//系统状态
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#define BUFSIZE 4096
//缓冲区大小
#define OFLAGS O_WRONLY | O_CREAT | O_TRUNC
#define SMODES S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH
//user的r和w权限 group的r和w权限 other的r权限
//相当于权限664
int main()
{
int fd1,fd2;
int n;
char buf[BUFSIZE];
if ((fd1=open("/etc/passwd", O_RDONLY))==-1)//相当于输入流
{
perror("open1");
exit(1);
}
if ((fd2=open("passwd.bak", OFLAGS, SMODES))==-1)//可以理解为输出流 可创建一个文件
{
perror("open2");
exit(2);
}
while ((n=read(fd1,buf,BUFSIZE)) > 0)//从fd1中读出东西存在buff里 一次读4096 跳出循环 读=0或<0
{
if (n!=write(fd2,buf,n))//写入fd2 写入n个大小 读多少写多少
//正常情况write返回值=n 错误则返回错误并退出
{
perror("write");
exit(3);
}
}
close(fd1);
close(fd2);
exit(0);
}
lseek:移动文件指针到指定位置。
off_t lseek(int fd, off_t offset, int whence)
输入参数:
whence 指针基准位置:
SEEK_SET 文件开头 开头+offset
SEEK_END 文件结尾 结尾+offset(offset为负)
SEEK_CUR 当前位置 offset可正可负
offset 相对于基准位置的偏移量,正整数或者负整数
返回值:返回指针到文件开头的距离
#include<fcntl.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include"quit.h"
#include"arg_check.h"
int main(int argc, char **argv)
{
int size, fd;
char buf;
arg_check(2,argc,"Not enough arguments!\n",1);//正常给两个参数
if ((fd=open(argv[1], O_RDONLY))==-1)
quit("open",1);
lseek(fd,1,SEEK_END);//指针移动到文件结尾再往后挪一个
while (lseek(fd,-2,SEEK_CUR) >= 0)//当前往前偏2个==读文件最后一个字符
{
if (read(fd,&buf,1)!=1)//正常 = 1 指针往后移动一个
quit("read",2);
if (write(STDOUT_FILENO, &buf, 1)!=1)//stdout为标准输出的符号常量 把该字符写到buf里
quit("write",3);
}//循环结束即是把文件倒着输出一遍
//read和write指针都会动,但是流不同
buf='\n';
write(STDOUT_FILENO, &buf, 1);
close(fd);
exit(0);
}