UNIX_C 高级编程<一>
课程阶段简介
Unix/linux系统的基础
=>主要学习常用的基本命令以及vi编辑器的使用
=>依赖于Unix/linux系统,部分命令和window相同
标准C语言的语法
=>主要学习C语言的基本语法
=>不依赖于具体的操作系统,支持Unix/Linux/Windows
数据结构和算法
=>主要学习常用数据结构的特性以及算法的思想
=>不依赖于具体的编程语言,支持c/c++/...
=>不依赖于具体的操作系统,支持Unix/linux/windows
Unix/Linux系统下的高级C编程
=>主要学习常用的API函数以及系统原理
(Application Programming Interface)应用程序接口
=>依赖于具体的操作系统,支持Unix/Linux
=>依赖于具体的编程语言,支持C语言的语法
目前主流的主机操作系统:Unix/Linux/Windows系列
目前主流的移动终端操作系统:android/ios/Windows
** C语言诞生于1972年,在编程语言排行榜占据重要地址
**Unix系统诞生于1970年,具有支持多用户,多任务以及多种处理器的特点
**Linux系统是一款自由免费开放源代码的类Unix系统
UNIX C 高级编程
==================================
UNIX体系结构:
应用程序
/ | \
shell | 公用函数库
| | |
(system call)系统调用
|
(kernel)内核
|
控制计算机硬件资源
gcc编译器
** 预处理 —— 主要实现头文件的扩展以及宏替换等 gcc -E 源文件 -o 预处理文件.i
** 编译 —— 实现从高级源代码到汇编语言的翻译 gcc -S 预处理文件.i --> 生成.s汇编文件
** 汇编 —— 实现汇编语言到机器语言指令的翻译 gcc -c 汇编文件.c --> 生成.o目标文件
** 链接 —— 实现目标文件和库文件的链接 gcc 目标文件.o --> 生成a.out可执行文件
需要掌握的选项
gcc/cc-E
gcc/cc-S
gcc/cc-c
gcc/cc-o 指定输出文件名
gcc/ccxxx.c 编译链接生成可执行文件a.out
熟悉
* gcc/cc -std 指定具体的C标准(C89 C99)
* gcc/cc -v 查看gcc的版本信息
* gcc/cc -Wall 尽可能显示所有的警告信息
* gcc/cc -Werror 将警告信息当作错误来处理
了解
gcc/cc-g 生成gdb相关的调试信息
gcc/cc-x 指定源代码的编译语言
gcc/cc-O 进行优化处理
.h —— 头文件,主要存放函数的声明等
.c —— 源文件,主要存放函数定义等
.a —— 静态库文件,实现对功能模块的打包
.so—— 共享库文件,实现对功能模块的打包
** 头文件的详细组成(重中之重)
1、头文件卫士
#ifndef……
#define ……
#endif
2、包含其它头文件
3、宏定义
4、结构体的定义以及对数据类型起别名
5、外部变量的声明或函数声明
Makefile批处理文件
1 circle.out: circle.o main.o //可执行文件.out:由那那些.o文件生成
2 gcc circle.o main.o -o circle.out //.out可执行文件是由什么命令的生成的
3 //+前面必须是1个tab键宽度
4 circle.o: circle.c //目标文件.o是由那个.c文件生成的
5 gcc -c circle.c //生成.o目标文件的命令
6
7 main.o: main.c
8 gcc -c main.c
9
10 clean: //敲击clean删除所有.o文件和可执行文件
11 mv *.o circle.out ~/tmp/
执行命令:make //运行Makefile文件生成指定的.out可执行文件
再次执行:make //如果想执行clean命令删除.o 和 可执行命令
再执行:make clean
vim~/.vimrc 文件,vim插件
setnu 显示行号
setcindent 表示自动缩进
setshiftwidth=4 自动缩进大小为4个空格
settabstop=4 设置tap键大小为4个空格
--------------------------------------------------------
day02_0630
常用预处理指令
#include 包含……
#define 定义……
#undef 取消宏定义……
#if 如果……
#ifndef 如果没有定义……
#ifdef 如果定义……
#elif 否则如果……
#else 否则……
#endif 结束如果
#line 整数n
=>用于将下一行的行号修改为第n行
=>用于在调试阶段确保错误信息的行号保持不变
#error 字符串
=>用于在调试阶段产生错误信息
#warning 字符串
=>主要用于在调试阶段产生警告信息
3#define VERSION 4
4 #if (VERSION >3)
5 #warning "版本过高"
6 #elif (VERSION <3)
7 #error "版本过低"
8 #endif
注意: #if 和 if 之间有何区别
** #if —— 用于在预处理阶段进行条件判断
** if —— 用于在运行阶段进行条件判断
* #pragma GCC dependency文件名
=>表示当前文件依赖与于面指定的文件名,如果指定文件名的最后一次修改时间晚于当前文件,则产生警告信息
3 //表示当前文件依赖于02line.c
4 #pragma GCC dependency "02line.c"
* #pragmaGCC poison 表示符 => 用于将后面指定的标识符设置为毒药,一旦使用产生错误信息;
eg: #pragma GCC poison goto
** #pragmapack(整数n) => 主要用于设置结构体的对齐和补齐方式;
** pragmapack(2) //整数n必须是2的次方,2的倍数
** 32位系统中结构体默认以4对齐
** pragmapack(8) //超过4的都是以4对齐
** 64位系统中结构体默认以8对齐
** 结构体的对齐补齐策略是为了提高存取数据的效率,但是带来的问题是浪费内存空间,
** +若需要节省内存空间,则可以调整对齐和补齐的方式
常用的与定义宏
* __FILE__表示获取当前宏所在的文件名 %s
* __LINE__表示获取当前宏所在的行号信息 %d
* __TIME__表示获取当前宏所在文件的最后一次编译时间 %s
* __DATE__表示获取当前宏所在文件的最后一次编译日期 %s
环境变量的基本概念和使用
环境变量一般是指在操作系统中用来指定操作系统运行环境相关参数的特殊变量也就用于存放
+和系统/软件相关的信息的变量;
Path/PATH本质上就是一个环境变量,用于存放应用程序的路径信息,定义/存放在Path环境
+变量中的路径可以被系统自动找到,因此可以直接通过应用程序名称来启动该程序,也就是说可
+以省略路径信息
PATH环境变量配置方法
windows环境下: 我的电脑-> 右键单击,选择属性->高级->环境变量->系统变量
->找到Path,点击编辑-> 在Path变量值的最后增加分号,再增加新路径
linux环境下: 打开一个终端执行以下指令:exportPATH=$PATH:. (只对当前终端有用)
vi~/.bashrc文件,在文件的最后增加上述指令
source~/.bashrc 使得配置文件立即生效
编程相关的常用环境变量
CPATH/C_INCLUDE_PATH -主要用于存放C语言头文件路径信息
CPLUS_INCLUDE_PATH -主要用于存放C++语言头文件路径信息
LIBRARY_PATH -主要用于存放库文件所在的路径信息
-主要用于解决编译链接时的问题
LD_LIBRARY_PATH -主要用于存放共享库所在的路径信息
-主要用于解决运行时的问题
查找头文件的主要方式
#include<> -在系统默认的路径中查找该头文件
#include"" -优先在当前工作目录中查找该头文件,若查找不到,则会去系统默认路径中查找
当前系统默认的路径是: /usr/include/...
使用whereis stdio.h命令来查找该头文件的位置
.c文件查找#include ""头文件 配置环境变量CPATH/C_INCLUDE_PATH
在终端中执行:export CPATH=$CPATH:.. 但当有多个项目/工程时,配置环境变量可能
会引起冲突问题
* 采用编译选项来指定头文件的路径信息
* gcc/cc xxx.c -I 头文件所在的路径信息
库文件的概念和使用
在大型项目开发中,如果每个函数都放在不同的.o文件中,则项目管理是灾难,使用库文件可能解决该问题;
一般来说,按照项目中功能模块的划分将多个.o文件打包成一个/多个库文件,编写者只需要提供库文件和头文件即可;
库文件主要分为两类:
静态库文件
静态库本质就是由若干个目标文件打包生存的.a文件
链接静态库的本质就是将被调用模块的代码指令复制到调用模块中,体现在最终的可执行文件中
优点:使用静态库在运行时不需要依赖于库文件并且执行的效率比较高
缺点:静态库占用空间比较大,库中的代码一旦修改则必须重新链接才能对可执行文件产生效果
静态库文件的生成
生成步骤: 1、编写源代码文件xxx.c eg:vim add.c
2、只编译不链接生成目标文件xxx.o eg:gcc -c add.c
3、生成静态库文件
ar-r lib库名.a 目标文件 eg:ar -r libadd.a add.o
注意:其中lib库名.a这个整体叫做静态库文件名,去掉lib前缀和.a后缀之后的内容叫库名
调用步骤: 1、编写测试源代码文件xxx.c eg:vim main.c
2、只编译不链接生成目标文件xxx.o eg:gcc -c main.c
3、链接静态库文件,链接的方式有三种
a、直接链接 ccmain.o libadd.a
b、编译选项链接 cc 目标文件 -l 库名 -L 库文件所在的路径
ccmain.o -l add -L .
c、环境变量进行链接 export LIBRARY_PATH=$LIBRARY_PATH:.
ccmain.o -l add
注意:使用ldda.out可以察看a.out 所依赖的共享库文件
使用gcc/cc -static xxx.c xxx.a ——表示强制要求链接静态库
共享库文件 —— 由若干个目标文件打包生成的.so文件
* 链接共享库和静态库最大的不同就是,链接共享库时并不需要将库中被调用的代码指令复
* +制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码指令在共享库中的相对地址;
* 优点:共享库占用空间小,即使修改了库中的代码,只要接口(相对地址)不变,则不需要
* +重新链接就可以对可执行文件生效;
* 缺点:使用共享库的代码在运行时需要依赖于共享库,并且需要跳转,因此执行效率比较低
* +目前主流的商业开发中,使用共享库比较多
共享库静态加载
** 生成步骤: 1、编写源代码文件xxx.c eg:vim add.c
* 2、只编译不链接生成目标文件xxx.o eg:gcc -c -fpic add.c
* -fpic--> 生成位置无关码
* 3、生成共享库文件 gcc -shared 目标文件 -o lib库名.so
* eg:gcc -shared add.o -o libadd.so
*
** 调用步骤: 1、编写测试源代码文件xxx.c eg:vim main.c
* 2、只编译不链接生成目标文件xxx.o eg:gcc -c main.c
* 3、链接共享库文件
*
* 环境变量进行链接 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
* ccmain.o libadd.so
*
注意:使用共享库文件时,需要配置环境变量 LD_LIBRARY_PATH来解决运行时找不到
+库文件的问题,配置方法 exportLD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
gcc/cc编译器缺省优先链接共享库,可以使用-static选项来要求编译链接静态库
静态库:gcc-c xxx.c 动态库: gcc -c xxx.c
ar -r libxxx.a xxx.o gcc-shared xxx.o -o libxxx.so
gcc -static main.c libxxx.a gcc main.c libxxx.so
exportLD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
共享库的动态加载
#include<dlfcn.h>
编译链接时加载选项: -ldl
void * handle = dlopen ( const char * filename, int flag );
constchar * filename: 字符串形式的共享库文件名
int flag: RTLD_LAZY—— 延迟加载
RTLD_NOW —— 立即加载
返回值:成功—— 返回该文件共享库文件的句柄(标识符) 失败 —— 返回NULL
函数功能:用于将第一个参数指定的共享库加载到内存中
(*fun) = ( void * ) dlsym ( void * handle,const char * symbol );
(void * ): 强制类型转换函数类型指针
(* fun ) : 函数指针,用于接受函数在静态库中的相对地址
void * handle: void * 类型的句柄,dlopen 函数的返回值
const char * symblo:字符串形式的符号,这里指函数名
返回值:成功—— 返回该函数在内存中的地址 失败—— 返回NULL
函数功能:根据参数指定的句柄和字符串形式的函数名去内存中查找
int dlclose( void * handle );
返回值:成功—— 0 失败 —— !0
函数功能:关闭参数指定的共享库,参数传递dlopen函数的返回值
char * dlerror( void );
返回值:主要用于获取dlopen()/dlsym()/dlclose()函数调用期间的错误
信息,如果有错误则返回字符串,如果没有错误则返回NULL;
------------------------------------------------04dynamic.c
9 //1、打开共享库文件libadd.so, dlopen函数
10 void * handle = dlopen("./add_shared/libadd.so", RTLD_NOW );
11
12 //2、判断是否出错,若出错打印出错信息
13 if( !handle )
14 {
15 printf("dlopen: %s \n", dlerror() );
16 return -1;
17 }
18
19 //3、找出共享库中名字为"add_int"的函数地址,使用dlsym函数
20 //+并准备函数指针来接受函数add_int的地址
21
22 int (* p_add_int) ( int, int);
23
24 p_add_int = ( int (*)(int,int ) ) dlsym (handle, "add_int" );
25 //4、盘都是否出错,若出错则打印结束
26 if(!p_add_int)
27 {
28 printf("dlsym:%s\n", dlerror() );
29 return -1;
30 }
31
32 //5、调用该函数,计算40和50的和,并打印
33 printf("a+b = %d\n", p_add_int(40,50));
34
35 //6、关闭共享库,使用dlclose函数
36 //7、判断是否出错,若出错则打印并结束
37 if( dlclose(handle) )
38 {
39 printf("dlclose:%s\n", dlerror());
40 }
*** 编译链接# gcc 04dynamic.c -ldl
c语言中错误处理
return0; //表示程序正常结束
return-1; //非正常结束
c语言中的错误表现形式(错了吗)
* 一般来说,c语言中是通过函数的返回值来表示是否出错的,而具体的表现形式如下:
* 1、对于返回值类型为int类型的函数来说,如果函数的计算结果不可能是负数时,一般
* +使用-1表示出错,使用其它数据表示正常结束;
* 2、对于返回值类型为int类型的函数来说,如果函数的结果可能是负数时,通常指针
* +作为函数的参数将函数的计算结果带出去,而函数的返回值专门用于表示是否出错,
* +一般使用-1表示出错,0表示正常结束
* 3、对于返回值类型为指针类型的函数来说一般使用返回NULL表示出错,否则返回有效地址;
* 4、如果不考虑函数出错的情况,则返回值类型使用void即可;
错误编号(为什么错了)
判断函数是否出错,需要依据函数的返回值;
当函数已经出错时,判断为什么错了,需要依据一个int 类型的全局变量errno;
#include<errno.h> - 包含了对变量errno的声明,以及对errno变量取值范围的说明等
/etc/passwd—— 存放了当前系统的帐号信息等
/etc/shadow—— 存放了当前系统帐号密码等信息
错误信息(对错误编号的翻译)
#include<string.h>
char* strerror (int errnum);
功能:主要用于将参数指定的错误编号翻译成对应的错误信息,并通过返回值返回
#include<stdio.h>
** voidperror (const char *s);
功能:主要用于打印系统/库函数调用期间产生的最后一个错误信息,如果参数不为
空,则原样打印, eg : perror("fopen:");
** 能否直接使用errno作为判断函数是否出错的依据呢?
* ——不能直接使用errno作为判断函数是否出错的依据,应为errno可能会保留之前的数值,
并且有些函数出错时可能不会修改errno的数值;
--------------------------------------------------------