GCC
GNU(GNU C Compiler GNU C语言编译器 --> GNU Compiler Collection)
安装命令 sudo apt install gcc g++
查看版本 gcc/g++ -v/--version
ctrl+l :清空目录
编程语言的发展
计算机 --> 机器语言 --> 汇编语言 --> 高级语言
gcc test.c :
gcc test.c -E -o test.i -- 预处理
gcc test.i -S -o test.s -- 编译
gcc test.s -C -o test.o -- 汇编
gcc test.o -o test.out -- 链接成可执行文件
gcc常用参数
gcc 和 g++ 的区别
gcc和g++都是GNU的一个编译器
后缀为.c的gcc会把它当作C程序,而g++会把它当作C++程序;后缀为.cpp的,两者都会认为是C++程序,C++的语法规则更加严谨一些
编译阶段,g++会调用gcc,对于C++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库连接,所以通常用g++来完成链接。为了统一起见,干脆编译/链接统统用g++了
编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++
gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但是在编译阶段,g++会自动调用gcc,二者等价
静态库
什么是库?
库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,提供给使用者一些可以直接拿来用的变量、函数或类
库是一种特殊的程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行
库文件有两种:静态库和动态库(共享库)。静态库在程序的链接阶段被复制到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用
库的好处是:1、代码保密;2、方便部署和分发
静态库的命名规则
Linux:libxxx.a
(lib—>固定前缀;xxx—>库的名字,自定义;.a—>固定后缀)
Windows:libxxx.lib
静态库的制作
gcc 获得.o文件
gcc -c xxx.c xxx.c
将.o文件打包,使用ar工具(archive)
ar rcs libxxx.a xxx.o xxx.o
r — 将文件插入备存文件中
c — 建立备存文件
s — 索引
静态库的使用
其中,app是生成的可执行文件(gcc main.c -o app -I ./include/ -l calc -L ./lib)(第一个是大写的i,第二个是小写的L)(填入要指定的库时,只需要填写库的名称即可(calc))----------------------------------------在该文件下,include是用来放置头文件的,lib是放置库文件(静态库的制作),mian函数是测试文件,src是源代码文件(source)-----------------------------------静态库的制作:1. gcc -c add.c sub.c mult.c div.c -I ../include/ (该目录下没有头文件,得去上级目录下的include文件夹中寻找头文件)。2. ar rcs libcalc.a add.o div.o mult.o div.o(通过ar指令对.o文件进行打包处理)3.mv libcalc.a ../lib/ (将libcalc.a 移动到lib文件夹中)
动态库
动态库的命名规则
Linux:libxxx.so
在Linux下是一个可执行文件
Windows:libxxx.dll
动态库的制作
gcc 得到 .o文件,得到和位置无关的代码fpic
gcc -c -fpic/-fpIC a.c b.c
-fpic 用于编译阶段,产生的代码没有绝对地址,全部用相对地址,这正好满足了共享库的要求,共享库被加载时地址不是固定的。如果不加-fpic ,那么生成的代码就会与位置有关,当进程使用该.so文件时都需要重定位,且会产生成该文件的副本,每个副本都不同,不同点取决于该文件代码段与数据段所映射内存的位置。
gcc 得到动态库
gcc -shared a.o b.o -o libcalc.so
动态库加载失败的原因
在运行一个使用动态库文件时,按照动态库的制作会出现一些问题
动态库的工作原理
原因是静态库和动态库的工作原理不同:静态库在GCC进行链接时,会把静态库中代码打包到可执行程序中;而动态库在GCC进行链接时,动态库的代码不会被打包到可执行程序中。程序启动后,动态库会被动态加载到内存中,通过ldd (list dynamic dependecies) 命令检查动态库的依赖关系
如何定位共享库文件? -- 当系统加载可执行代码时,能够知道其所依赖的库的名字,但是还需知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。对于elf格式的可执行程序,是由ld-linux.so来完成的,它先后搜索elf文件的 DT_RPATH段 --> 环境变量LD_LIBRARY_PATH --> /etc/ld.so.cache文件列表 --> /lib/, /usr/lib目录找到库文件后将其载入内存
一、对于修改LD_LIBRARY_PATH的三种方法
解决方案一:在终端中用export命令在LD_LIBRARY_PATH中添加库文件的绝对路径(缺点:在终端关闭后该环境变量会消失)
解决方案二:(用户级别永久配置环境变量)
vim .bashrc (在home目录下,有一个.bashrc文件,直接在该文件最末行(shift+g)插入环境变量),再 . .bashrc(第一个.相当于source,所以也可以用source .bashrc) 使文件生效
解决方案三:(系统级别永久配置环境变量)
系统级别则需要sudo权限,在etc目录下的profile文件内添加环境变量,source一下使文件生效(如果source不能使用则使用.命令是一样的)
二、对/etc/ld.so.cache文件列表进行配置的方法
三、在 /lib/, /usr/lib目录下加入动态库文件
不推荐使用,因为本身就有很多系统自带的库文件,可能会出现重名替换问题(防止误操作)
静态库和动态库的对比
静态库 | 动态库 | |
优点 | 打包到应用程序中加载速度快; 发布程序无需提供静态库,移植方便 | 可以实现进程间的资源共享(共享库) 更新、部署、发布简单 可以控制何时加载动态库 |
缺点 | 消耗系统资源,浪费内存; 更新、部署、发布麻烦 | 加载速度比静态库慢 发布程序时需要提供依赖的动态库 |
Makefile
什么是Makefile?
一个工程中的源文件不计其数,按类型、功能、模块分别放在若干个目录中,Makefile文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile文件就像一个Shell脚本一样,也可以执行操作系统的命令。
Makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高的软件开发的效率。make是一个命令工具,是一个解释Makefile文件中指令的命令工具,一般来说,大多数的IDE都有这个命令。
Makefile文件命名和规则
文件命名:makefile 或 Makefile
一个Makefile文件中可以有一个或者多个规则,任何其他规则都是为第一个规则所服务的
1、打开Makefile进行编写(vim Makefile):
2、 然后执行make指令,会自动帮我们寻找当前目录下的Makefile文件
工作原理
命令在执行之前,需要先检查规则中的依赖是否存在:
存在,则执行命令
不存在,则向下检查其他的规则,检查有没有一个规则是用来生成这个依赖的,如果找到了,则执行该规则中的命令
检查更新,在执行规则的命令时,会比较目标和依赖文件的时间:
如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行
变量
模式匹配
函数
wildcard:
patsubst:
GDB调试
什么是GDB?
由GNU软件系统社区提供的调试工具,同GCC配套组成了一套完整的开发环境,GDB是Linux和许多类Unix系统中的标准开发
一般来说,GDB主要帮助完成以下四方面的功能:1、启动程序,可以按照自定义的要求随心所欲的运行程序;2、可让被调试程序在所指定的调置的断点处停住(断点可以是条件表达式);3、当程序被停住时,可以检查此时程序中所发生的事;4、可以改变程序,将一个BUG产生的影响修正从而测试其他BUG
准备工作
通常情况下,在为调试而编译时,我们会()关掉编译器的优化选项(`-O`),并打开调试选项(`-g`);另外,`-Wall`在尽量不影响程序行为的情况下选项打开所有warning,也可以发现许多问题,避免一些不必要的BUG
gcc -g -Wall program.c -o program
`-g` --> 在可执行文件中加入源代码信息。比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件
GDB命令
启动 -- gdb 可执行程序 退出 -- quit/q
给程序设置参数 -- set args 10 20 获得设置参数 -- show args
GDB 使用帮助 -- help
查看当前文件代码 -- list/l(从默认位置显示);list/l 行号(从指定的行显示--该行号在中间位置);list/l 函数名(从指定的函数显示)
查看非当前文件代码 -- list/l 文件名:行号;list/l 文件名:函数名
设置显示的行数 -- show list/listsize;show list/listsize 行数
设置断点 -- b/break 行号;b/break 函数名;b/break 函数名;b/break 文件名:行号;b/break 文件名:函数
查看断点:i/info b/break
删除断点:d/del/delete 断点编号
设置断点无效:dis/disable 断点编号
设置断点生效:ena/enable 断点编号
设置条件断点(一般用在循环的位置):b/break 10 if i==5
常用GDB调试命令
运行GDB程序:start(程序停在第一行);run(遇到断点才停)
继续运行,到下一个断点停:c/continue
向下执行一行一行代码(不会进入函数体):n/next
变量操作:p/print 变量名(打印变量值);ptype 变量名(打印变量类型)
向下单步调试(遇到函数进入函数体):s/step;finish(跳出函数体:不能有断点)
自动变量操作:display num(自动打印指定变量的值);i/info display;
underplay 编号
其他操作:set var 变量名=变量值;until(跳出循环:不能有断点&&在该循环的最后一句)
文件IO
在C标准库中的IO函数:可跨平台:
虚拟地址空间
内存空间大小由电脑的CPU决定。以32位内存空间为例,虚拟内存空间为2的32次方(约4G) (64位为2的48次方);
虚拟地址空间氛围0~3G用户区和3~4G的内核区。虚拟地址空间的数据最终会被cpu中的逻辑管理单元MMU映射到真实的物理内存上;
内核区的数据普通用户没有权限操作;若要操作内核中的数据,则需要进行系统调用(调用Linux系统的API)
文件描述符
用来定位磁盘上的文件位置,位于内核区,由内核进行管理(具体是由PCB进程控制块管理,PCB中有一个文件描述符表,是一个数组,用于存储文件描述符)
文件描述符表(该数组默认大小为1024)中,前三个是默认被占用的(标准输入、标准输出、标准错误)指向的是当前终端。若文件描述符被占用,则将去文件描述符表中找一个最小的文件描述符去使用(由内核维护)
Linux系统IO函数
在Linux中输入“man 2 open”能看到Linux的open手册,其中要声明三个头文件:
#include <sys/types.h>
#include <sys/stat.h>
//定义的宏(flags)放在了以上两个头文件中
#include <fcntl.h> //open函数声明在该头文件中
int open(const char *pathname, int flags); //打开一个已经存在的文件
参数:
pathname -- 要打开的文件路径
flags -- 对文件的操作权限设置还有其他的设置(只读、只写、可读可写;三者互斥)
返回值:返回一个新的文件描述符(若成功,则返回文件描述符的号码;失败返回-1)
//errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号
perror:打印errno对应的错误描述;
参数s:用户描述,比如hello,最终输出的内容是 hello:xxx(xxx指的是实际的错误描述
int open(const char *pathname, int flags, mode_t mode); //创建一个新文件
与之前的int open(const char *pathname, int flags);不是函数重载(C语言中没有函数重载),是通过可变参数实现了一个同名的效果
参数:
pathname -- 要创建的文件路径
flags -- 对文件的操作权限设置还有其他的设置,用“|(按位或)”增加可选项
必选项:只读、只写、可读可写;三者互斥
可选项:O_APPEN(向文件内追加)O_ASYNC(同步)...O_CREAT(文件不存在,则创建新文件)
mode -- 八进制数,表示用户对创建出的新的操作权限,比如:0777,0为八进制的数。
最终权限 = (mode & ~umask),如umask=0002(取反结果为0775),mode=0777,则进行运算:0775(会少了某些权限)umask的作用是抹去某些权限,让文件权限更合理一些
int close(int fd);
关闭一个文件描述符使得它不能再指向任何一个文件;且该文件描述符还能被重用
ssize_t read(int fd, void *buf, size_t count); //从文件中读取数据到内存中
参数:
fd --> 文件描述符,open得到的,通过这个文件描述符来操作某个文件
buf --> 需要读取数据存放的地方,数组的地址,(传出参数)
count --> 指定的数组的大小
返回值:若成功,则会返回实际读取到字节的数量;若返回0(也为成功),则表示已经读到了文件末尾(读完了);若出现错误,则返回-1(null),并把errno设置为合适的值
ssize_t write(int fd, const void *buf, size_t count); //把内存中的数据写到文件中
参数:
buf --> 要往磁盘写入的数据,一般是数组
count --> 要写的数据的实际的大小
返回值:若成功,则返回实际的写入的字节数;若为0,则表示没有写入任何内容;若为-1,则表示写入失败
off_t lseek(int fd, off_t offset, int where); //对标标准C库中的fseek
fd --> 通过open得到的文件描述符,通过fd去操作某个文件
offset --> 偏移量
whence --> 指定一些标记
SEEK_SET 设置文件指针的偏移量
SEEK_CUR 设置偏移量:当前位置+第二个参数offset的值
SEEK_END 设置偏移量:文件的大小+第二个参数offset的值
返回值:返回最终文件指针的位置
lseek函数的作用:
1、移动文件指针到头文件:lseek(fd, 0, SEEK_SET);
2、获取当前文件指针的位置: lseek(fd, 0, SEEK_CUR);
3、获取文件长度: lseek(fd, 0, SEEK_END);
4、拓展文件的长度: lseek(fd, 100, SEEK_END); (如,当前文件10b,要拓展成110b,增加了100个字节:【注意】在拓展文件长度之后再写入一个空数据(拓展文件后需要写入一次数据才会生效),查看文件时会发现该文件从10b变成了111b( write(fd, " ", 1); )),拓展的字符(100b)都是空字符
int stat(const char *pathname, struct stat *statbuf); //获取文件的信息
作用:获取文件的相关信息
在Linux终端中,可以输入stat指令,后面跟上想要查看的文件名查看
参数:
pathname --> 操作的文件的路径
statbuf --> 结构体变量,传出参数,用于保存获取到的文件信息
返回值:成功则返回0,失败则返回-1,设置errno
其中,mode_t st_mode;一共16位,用标志位变量记录文件的类型和存储权限如下:
若想要判断的权限,需要将所求权限与原先的二进制码做与的操作;若想判断文件类型,则需要将原先的数值和掩码进行与操作【st_mode & S_IFMT】
int lstat(const char *pathname, struct stat *statbuf); //获取软链接文件的信息
若想获取软链接的信息,则使用lstat函数:当一个文件指向另一个文件时(软链接),用stat得到的是另一个文件的信息,用lstat才能得到软链接文件本身的信息
文件属性操作函数
int access (const char *pathname, int mode); //判断文件的权限或者判断文件是否存在
参数:
pathname --> 判断的文件路径
mode --> 要判断的权限(F_OK:测试文件是否存在;R_OK/ W_OK/ X_OK:测试文件是否有读/写/可执行权限)
返回值:成功返回0;失败返回-1
int chmod (const char *filename, int mode); //修改文件权限
参数:
pathname --> 需要修改的文件的路径
mode--> 需要修改的权限值,八进制的数
返回值: 成功返回0;失败返回-1
int chown (const char *path, uid_t owner, gid_t group); //修改文件所有者或所在组
在 etc/passwd里面可以看到 --> 用户名:所有者id:所在组id;在 /etc/group 查看所有组
int truncate (const char *path, off_t length); //缩减或扩展某个文件尺寸至指定大小
参数:
path --> 需要修改的文件的路径
length --> 需要最终文件变成的大小
返回值: 成功返回0;失败返回-1
目录操作函数
int mkdir (const char *pathname, mode_t mode); //创建目录
参数:
pathname --> 创建的目录路径
mode --> 权限,八进制的数
返回值: 成功返回0;失败返回-1
int rmdir (const char *pathname); //删除一个空文件
in rename (const char *oldpath, const char *newpath); //对目录进行重命名
int chdir (const char *path); //修改进程的工作路径/目录
参数:
path --> 需要修改的工作路径
char *getcwd (char *buf, size_t size); //获取当前进程工作的路径/目录
参数:
buf --> 存储的路径,指向的是一个数组
size --> 数组的大小
返回值: 返回的指向的一块内存,这个数据就是第一个数组
目录遍历函数
以下三个函数可以实现 读取某个目录下普通文件的个数
DIR *opendir (const char * name); //打开一个目录
参数:
name --> 需要打开的目录
返回值:
DIR * 类型 --> 理解为目录流,其实是一个结构体
错误返回NULL
struct dirent *readdir (DIR *dirp); //读取目录里的内容
调用一次dirent,就会指向下一个实体(会向后移)
参数:
dirp --> 是opendir返回的结果
返回值:
若读取到末尾或者失败了,就返回NULL
struct dirent --> 结构体,代表读取到的文件信息
int closedir (DIR dirp); //关闭一个目录
文件描述符操作相关函数
int dup (int oldfd); //复制一个新的文件描述符
参数及返回值:
oldfd --> 旧的文件描述符,它和新的文件描述符都指向同一个文件(如,fd=3, int fd1 = dup(fd),其中fd指向的是a.txt,fd1也指向a.txt),从空闲的文件描述符表中找一个最小 ,作为新的拷贝的文件描述符
int dup2 (int oldfd, int newfd); //重定向文件描述符
将oldfd拷贝给newfd。如,有一个oldfd指向a.txt,newfd指向b.txt。调用函数成功后,newfd 和 b.txt 做close,newfd 指向了 a.txt(即newfd指向了和oldfd相同的文件)
oldfd必须是一个有效的文件描述符;
若oldfd和newfd值相同,相当于什么都没做;
int fcntl (int fd, int cmd, ... /* arg */ );
参数:
fd --> 需要操作的文件描述符
cmd --> command传递的一个命令(一些定义的宏)
F_DUPFD:复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述度(通过返回值)
F_GETFL:获取指定的文件描述符文件状态flag(文件状态flag 的 我们通过open函数传递的flag是同一个东西
F_SETFL:设置文件描述符文件状态flag(必选项O_RDONLY,O_WRONLY,O_RDWR不可以被修改;可选项有O_APPEND,O_NUNBLOCK等)
--> O_APPEND表示追加数据;O_NUNBLOCK表示设置成非阻塞
阻塞和非阻塞:描述的是函数调用的行为。 阻塞行为是指在调用一个函数的过程中、该函数的返回值还没有返回之前,其他进程/线程被挂起;非阻塞行为是指调用一个函数后立马就能返回(不管是否得到想要的结果),不会影响其他进程/线程的情况
... --> 可变参数,可以没有,也可以有多个参数