C++ 学习笔记8
man 查看库函数
帮助文档的使用
man 级别 命令或函数
显示帮助的界面可以用vi的命令,q退出。
man的级别:
1-用户命令;2-系统接口;3-库函数;4-特殊文件,比如设备文件;5-文件;
6-游戏;7-系统的软件包;8-系统管理命令;9-内核。
编译
gcc/g++ 选项 源代码文件1 源代码文件2 源代码文件n...
常用选项:
-o
:指定输出文件名。
-g
:若想对源代码进行调试,则加。
-On
:推荐用 -O2
,O2
优化增加了编译时间的基础上,提高了生成代码的执行效率。
-c
:只编译,不链接成为可执行文件,通常用于把源文件编译成静态库或动态库。
-std=c++11
:支持 c++ 11 标准。
静态库和动态库
把通用的函数和类分文件编写,称之为库。在其它的程序中,可以使用库中的函数和类。相当于加密了,库文件是被编译为二进制文件。
如果动态库和静态库同时存在,编译器将优先使用动态库。
静态库
程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接。
如果多个程序中用到了同一静态库中的函数或类,就会存在多份拷贝。
项目目录结构:项目目录结构是两个文件夹,一个是 include 文件夹包含了 public.h 头文件,另一个是 src 文件夹包含了 main.cpp 和 public.cpp 源文件,那么我在 src 目录下。
制作静态库:
g++ -c -o lib库名.a 源代码文件清单
,如:g++ -c -o libpublic.a public.cpp
如果库的源文件和头文件不在同一文件夹,则可以指定头文件目录,如:
g++ -c -o libpublic.a public.cpp -I../include
使用静态库:
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
,如:g++ -o demo01 demo01.cpp -L/home/wucz/tools -lpublic
。-L
:指定静态库所在的目录。-l
:指定需要链接的库名。同上,也可以指定头文件目录,如:
g++ -o hello hello.cpp -L/home/ran/codeProj/src -lpublic -I../include
如果要包含多个库,则上述命令要增加 -L 的库目录和 -l 的库名。
静态库的特点
静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
目标程序的可执行文件比较大,浪费空间。
程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译。
动态库
程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入。
如果多个进程中用到了同一动态库中的函数或类,那么在内存中只有一份,避免了空间浪费问题。
制作动态库:
g++ -fPIC -shared -o lib库名.so 源代码文件清单
,如:g++ -fPIC -shared -o libpublic.so public.cpp
使用动态库,即与其它源文件一同编译:
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
,如g++ -o demo01 demo01.cpp -L/home/wucz/tools -lpublic
注意:运行可执行程序的时候,需要提前设置
LD_LIBRARY_PATH
环境变量。该环境变量的用途是:指定动态文件库的目录。添加要使用的动态库的目录:
export LD_LIBRARY_PATH=$LD_LIBRARY_PTAH:/home/wucz/tools
如果要包含多个库,则上述命令要增加 -L 的库目录和 -l 的库名。
动态库的特点
程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存。
可以实现进程之间的代码共享,因此动态库也称为共享库。
程序升级比较简单,不需要重新编译程序,只需要更新动态库就行了。
意思是:在原本的库源文件中改动代码后,只需重新创建静态库,接着不需要重新编译整个项目文件,直接运行之前生成的可执行文件,即可完成更新。
makefile
更便捷的生成目标文件。
all
:指代要编译生成的目标文件,如果有多个文件则以空格隔开,要换行则用反斜杠\
。- 接着给出要编译的文件 所依赖的文件 以及 编译命令。注意:命令前面是一个
tab
不是空格。 - 最后的
clean
通过make clean
命令执行,清除编译生成的目标文件。
生成库的 makefile:
该 makefile 一般处于库的源文件(和头文件)处。
1 | # 指定编译的目标文件是libpublic.a和libpublic.so |
生成项目的可执行文件的makefile:
可以定义变量,类似
#define
1 | INCLUDEDIR=-I../include |
GDB 调试
sudo apt install gdb
如果希望程序可调试,编译时需要加
-g
选项,并且,不能使用-O
的优化选项(因为可能优化后,执行顺序与源代码不同)。例如:g++ -o demo demo.cpp -g
。
调试目标程序:gdb demo
命令 | 简写 | 命令说明 |
---|---|---|
set args | 设置程序运行的参数。 例如:./demo 张三 西施 我是一只傻傻鸟 设置参数的方法是: set args 张三 西施 我是一只傻傻鸟 |
|
break | b | 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。 |
run | r | 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。 |
next | n | 执行当前行语句,如果该语句为函数调用,不会进入函数内部。 VS的F10 |
step | s | 执行当前行语句,如果该语句为函数调用,则进入函数内部。 注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是自定义的函数,只要有源码就可以进去。 |
p | 显示变量或表达式的值,如果p后面是表达式,会执行这个表达式。 | |
continue | c | 继续运行程序,遇到下一个断点停止,如果没有遇到断点,程序将一直运行。 VS的F5 |
set var | 设置变量的值。 假设程序中定义了两个变量: int ii; char name[21]; set var ii=10 把ii的值设置为10; set var name=”西施”。 |
|
quit | q | 退出gdb。 |
gdb 调试 core 文件
内存泄漏等错误会导致操作系统(linux)在执行该程序时报出:段错误(
Segmentation fault (core dumped)
)。(默认不生成 core 文件)
调试core文件的步骤如下:
用
ulimit -a
查看当前用户的资源限制参数;用
ulimit -c unlimited
把 core file size 改为unlimited
;运行程序,产生 core 文件;
运行 gdb 程序名 core 文件名;
在 gdb 中,用
bt
查看函数调用栈。函数调用栈(backtrace):
当一个函数被调用时,会将函数的参数、局部变量以及函数调用后需要返回的地址等信息压入调用栈中。这样,程序就可以在函数执行完毕后返回到调用该函数的位置继续执行。
每次函数调用时,系统会为该函数分配一块内存空间用来存储函数的参数和局部变量,这块内存空间就称为栈帧(Stack Frame)。栈帧包含了函数的参数、局部变量、返回地址以及其他与函数调用相关的信息。
当函数执行完毕时,系统会从调用栈中弹出该函数的栈帧,并将程序控制返回到调用该函数的位置。这样,程序就可以继续执行下一个函数调用或者返回到主程序。如下,gdb 调试 core 或者 可执行文件,bt 显示函数调用栈,由 main 到 func() ,推测 func() 中出现问题。
gdb 调试正在运行的程序
假设正在运行的程序名为:demo
执行命令:
ps -ef | grep demo
,显示结果:第一列是 UID,第二列是 PID,第三列是 PPID。执行命令:
sudo gdb demo -p PID
注意:需要确保拥有足够的权限来附加到另一个进程,通常需要以 root 或者具有调试权限的用户身份运行
gdb
命令。如果没有足够的权限,可能无法成功附加到另一个进程。
linux 时间操作
使用需要包含
<time.h>
头文件。
time_t
time_t
用于表示时间类型,它是一个long类型的别名,在<time.h>
文件中定义,表示从1970年1月1日0时0分0秒到现在的秒数。
time() 库函数
time()
库函数用于获取操作系统的当前时间。
声明:
time_t time(time_t *tloc);
有两种调用方法:
time_t now=time(0);
// 将空地址传递给time()
函数,并将time()
返回值赋给变量now。time_t now; time(&now);
// 将变量now的地址作为参数传递给time()
函数。
tm 结构体
time_t
是一个长整数,不符合人类的使用习惯,需要转换成tm
结构体,tm
结构体在<time.h>
中声明,如:2022-10-01 15:30:25 Oct 1,2022 15:30:25
1 | struct tm |
localtime() 库函数
localtime()
函数用于把time_t
表示的时间转换为tm
结构体表示的时间。
localtime()
函数不是线程安全的,localtime_r()
是线程安全的。
使用示例:
1 |
|
mktime() 库函数
mktime()
函数的功能与localtime()
函数相反,用于把tm
结构体时间转换为time_t
时间。
函数声明:
1 | time_t mktime(struct tm *tm); |
该函数主要用于时间的运算,例如:把2022-03-01 00:00:25加30分钟。
思路:
- 解析字符串格式的时间,转换成
tm
结构体; - 用
mktime()
函数把tm
结构体转换成time_t
时间; - 把
time_t
时间加30*60秒; - 用
localtime_r()
函数把time_t
时间转换成tm结构体; - 把
tm
结构体转换成字符串。
程序睡眠
头文件
<unistd.h>
如果需要把程序挂起一段时间,可以使用sleep()
和usleep()
两个库函数。
1 | unsigned int sleep(unsigned int seconds); |
linux 目录操作
获取当前目录
头文件:
<unistd.h>
相当于
pwd
命令。
1 | // 目录长度最大 255,因此要创建的 buf 大小为256,一位是字符0. |
示例:
1 |
|
切换工作目录
头文件:
<unistd.h>
1 | int chdir(const char *path); |
返回值:0-成功;其它-失败(目录不存在或没有权限)。
创建目录
头文件:
<sys/stat.h>
1 | int mkdir(const char *pathname, mode_t mode); |
pathname:目录名。
mode:访问权限,如0755,不要省略前置的0。
返回值:0-成功;其它-失败(上级目录不存在或没有权限)。
删除目录
头文件:
<unistd.h>
1 | int rmdir(const char *path); |
path:目录名。
返回值:0-成功;其它-失败(目录不存在或没有权限)。
获取目录文件列表
文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表。
头文件:
<dirent.h>
用
opendir()
函数打开目录。1
DIR *opendir(const char *pathname);
成功-返回目录的地址,失败-返回空地址。
用
readdir()
函数循环的读取目录。1
struct dirent *readdir(DIR *dirp);
成功-返回
struct dirent
结构体的地址,失败-返回空地址。用
closedir()
关闭目录。1
int closedir(DIR *dirp);
数据结构:
DIR *目录指针变量名;
每次调用readdir()
,函数返回struct dirent
的地址,存放了本次读取到的内容。
1 | struct dirent |
重点关注结构体的d_name
和d_type
成员。
d_name:文件名或目录名。
d_type:文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-子目录(A directory),其它的暂时不关心。注意,d_name 的数据类型是字符,不可直接显示。
示例:
1 |
|
access() 库函数
头文件:
unistd.h
access()
函数用于判断当前用户对目录或文件的存取权限。
1 | int access(const char *pathname, int mode); |
pathname:目录或文件名。
mode:需要判断的存取权限。在头文件<unistd.h>
中的预定义如下:
1 |
返回值:
当pathname
满足mode
权限返回0,不满足返回-1,errno
被设置。
在实际开发中,access()
函数主要用于判断目录或文件是否存在。
示例:
1 |
|
stat() 库函数
stat 结构体:
struct stat
结构体用于存放目录或文件的详细信息
1 | struct stat |
重点关注st_mode
、st_size
和st_mtime
成员。st_mtime
是一个整数表示的时间,需要程序员自己写代码转换格式。
st_mode
成员的取值很多,用以下两个宏来判断:
1 | S_ISREG(st_mode) // 是否为普通文件,如果是,返回真。 |
stat() 库函数:
#include <sys/stat.h>
1 | int stat(const char *path, struct stat *buf); |
stat()
函数获取path参数指定目录或文件的详细信息,保存到buf结构体中。
返回值:0-成功,-1-失败,errno被设置。
示例:
1 |
|
utime()库函数
#include <sys/types.h>
#include <utime.h>
utime()
函数用于修改目录或文件的时间。
1 | int utime(const char *filename, const struct utimbuf *times); |
utime()
函数用来修改参数filename
的st_atime
和st_mtime
。如果参数times
为空地址,则设置为当前时间。
返回值:0-成功,-1-失败,errno被设置。
rename()库函数
#include <stdio.h>
rename()函数用于重命名目录或文件,相当于操作系统的mv命令。
1 | int rename(const char *oldpath, const char *newpath); |
参数说明:
oldpath:原目录或文件名。
newpath :目标目录或文件名。
返回值:0-成功,-1-失败,errno被设置。
remove()库函数
#include <stdio.h>
remove()函数用于删除目录或文件,相当于操作系统的rm命令。
1 | int remove(const char *pathname); |
参数说明:
pathname:待删除的目录或文件名。
返回值:0-成功,-1-失败,errno被设置。
linux 的系统错误
c++ 中,调用库函数可以通过函数的返回值判断调用是否成功。同时,还有一个整型的全局变量
errno
,存放了函数调用过程中产生的错误代码。如果调用库函数失败,可以通过
errno
的值来查找原因,这也是调试程序的一个重要方法。
errno
在<errno.h>
中声明。配合
strerror()
和perror()
两个库函数,可以查看出错的详细信息。
strerror() 库函数
头文件:
string.h
用于获取错误代码对应的详细信息。
1 | char *strerror(int errnum); // 非线程安全。 |
示例:
1 |
|
perror() 库函数
头文件:
stdio.h
用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大。(对调试程序略有帮助)
1 | void perror(const char *s); |
注意事项
调用库函数失败不一定会设置errno
答:并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(一般来说,不属于系统调用的函数不会设置errno,属于系统调用的函数才会设置errno)。errno不能作为调用库函数失败的标志
答:只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动的置为 0。