C++ 学习笔记8

man 查看库函数

帮助文档的使用

man 级别 命令或函数

显示帮助的界面可以用vi的命令,q退出。

man的级别:

1-用户命令;2-系统接口;3-库函数;4-特殊文件,比如设备文件;5-文件;

6-游戏;7-系统的软件包;8-系统管理命令;9-内核。

编译

gcc/g++ 选项 源代码文件1 源代码文件2 源代码文件n...

常用选项:

-o:指定输出文件名。

-g:若想对源代码进行调试,则加。

-On:推荐用 -O2O2优化增加了编译时间的基础上,提高了生成代码的执行效率。

-c:只编译,不链接成为可执行文件,通常用于把源文件编译成静态库或动态库。

-std=c++11:支持 c++ 11 标准。

静态库和动态库

把通用的函数和类分文件编写,称之为库。在其它的程序中,可以使用库中的函数和类。相当于加密了,库文件是被编译为二进制文件。

如果动态库和静态库同时存在,编译器将优先使用动态库。

静态库

程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接。

如果多个程序中用到了同一静态库中的函数或类,就会存在多份拷贝。

项目目录结构:项目目录结构是两个文件夹,一个是 include 文件夹包含了 public.h 头文件,另一个是 src 文件夹包含了 main.cpp 和 public.cpp 源文件,那么我在 src 目录下。

  1. 制作静态库:g++ -c -o lib库名.a 源代码文件清单,如:g++ -c -o libpublic.a public.cpp

    如果库的源文件和头文件不在同一文件夹,则可以指定头文件目录,如:g++ -c -o libpublic.a public.cpp -I../include

  2. 使用静态库: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 的库名。

静态库的特点

  • 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。

  • 目标程序的可执行文件比较大,浪费空间。

  • 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译。

动态库

程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入。

如果多个进程中用到了同一动态库中的函数或类,那么在内存中只有一份,避免了空间浪费问题。

  1. 制作动态库:g++ -fPIC -shared -o lib库名.so 源代码文件清单,如:g++ -fPIC -shared -o libpublic.so public.cpp

  2. 使用动态库,即与其它源文件一同编译: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

更便捷的生成目标文件。

  1. all:指代要编译生成的目标文件,如果有多个文件则以空格隔开,要换行则用反斜杠 \
  2. 接着给出要编译的文件 所依赖的文件 以及 编译命令。注意:命令前面是一个 tab 不是空格。
  3. 最后的 clean 通过 make clean 命令执行,清除编译生成的目标文件。

生成库的 makefile:

该 makefile 一般处于库的源文件(和头文件)处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 指定编译的目标文件是libpublic.a和libpublic.so
all: libpublic.a \
libpublic.so

# 编译libpublic.a需要依赖public.h和public.cpp
# 如果被依赖文件内容发生了变化,将重新编译libpublic.a
libpublic.a: ./include/public.h ./src/public.cpp
g++ -c -o libpublic.a ./src/public.cpp -I./include

libpublic.so: ./include/public.h ./src/public.cpp
g++ -fPIC -shared -o libpublic.so ./src/public.cpp -I./include

# clean用于清理编译目标文件,仅在make clean才会执行。
clean:
rm -f libpublic.a libpublic.so

生成项目的可执行文件的makefile:

可以定义变量,类似 #define

1
2
3
4
5
6
7
8
9
10
11
INCLUDEDIR=-I../include
LIBDIR=-L/home/ran/codeProj/lib

all: main

main: main.cpp
# g++ -o main main.cpp -L/home/ran/codeProj/lib -lpublic -I../include
g++ -o main main.cpp $(INCLUDEDIR) $(LIBDIR) -lpublic

clean:
rm -f main

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也是进不去的,因为没有源代码,如果是自定义的函数,只要有源码就可以进去。
print 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文件的步骤如下:

  1. ulimit -a查看当前用户的资源限制参数;
    78c1fc41dd89f4050b30061c27cb0b04.png

  2. ulimit -c unlimited把 core file size 改为unlimited

  3. 运行程序,产生 core 文件;

  4. 运行 gdb 程序名 core 文件名;

  5. 在 gdb 中,用 bt 查看函数调用栈。

    函数调用栈(backtrace):
    当一个函数被调用时,会将函数的参数、局部变量以及函数调用后需要返回的地址等信息压入调用栈中。这样,程序就可以在函数执行完毕后返回到调用该函数的位置继续执行。
    每次函数调用时,系统会为该函数分配一块内存空间用来存储函数的参数和局部变量,这块内存空间就称为栈帧(Stack Frame)。栈帧包含了函数的参数、局部变量、返回地址以及其他与函数调用相关的信息。
    当函数执行完毕时,系统会从调用栈中弹出该函数的栈帧,并将程序控制返回到调用该函数的位置。这样,程序就可以继续执行下一个函数调用或者返回到主程序。

    如下,gdb 调试 core 或者 可执行文件,bt 显示函数调用栈,由 main 到 func() ,推测 func() 中出现问题。

d5523f00a23b35722263093aa95b9c83.png

gdb 调试正在运行的程序

假设正在运行的程序名为:demo

  1. 执行命令:ps -ef | grep demo ,显示结果:第一列是 UID,第二列是 PID,第三列是 PPID。

  2. 执行命令: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);

有两种调用方法:

  1. time_t now=time(0); // 将空地址传递给time()函数,并将time()返回值赋给变量now。
  2. 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
2
3
4
5
6
7
8
9
10
11
12
struct tm
{
int tm_year; // 年份:其值等于实际年份减去1900
int tm_mon; // 月份:取值区间为[0,11],其中0代表一月,11代表12月
int tm_mday; // 日期:一个月中的日期,取值区间为[1,31]
int tm_hour; // 时:取值区间为[0,23]
int tm_min; // 分:取值区间为[0,59]
int tm_sec; // 秒:取值区间为[0,59]
int tm_wday; // 星期:取值区间为[0,6],其中0代表星期天,6代表星期六
int tm_yday; // 从每年的1月1日开始算起的天数:取值区间为[0,365]
int tm_isdst; // 夏令时标识符,该字段意义不大
};

localtime() 库函数

localtime()函数用于把time_t表示的时间转换为tm结构体表示的时间。

localtime()函数不是线程安全的,localtime_r()是线程安全的。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>
#include <time.h>

using namespace std;

int main(int argc, char* argv[])
{
time_t now1 = time(0);
long now2;
time(&now2);

cout << "now1:" << now1 << endl;
cout << "now2:" << now2 << endl;

tm tmnow;
localtime_r(&now1, &tmnow);

string time_str = to_string(tmnow.tm_year + 1900) + "/"
+ to_string(tmnow.tm_mon + 1) + "/"
+ to_string(tmnow.tm_mday) + " "
+ to_string(tmnow.tm_hour) + ":"
+ to_string(tmnow.tm_min) + ":"
+ to_string(tmnow.tm_sec);

cout << "current time: " << time_str << endl;

return 0;
}

mktime() 库函数

mktime()函数的功能与localtime()函数相反,用于把tm结构体时间转换为time_t时间。

函数声明:

1
time_t mktime(struct tm *tm);

该函数主要用于时间的运算,例如:把2022-03-01 00:00:25加30分钟。

思路:

  1. 解析字符串格式的时间,转换成tm结构体;
  2. mktime()函数把tm结构体转换成time_t时间;
  3. time_t时间加30*60秒;
  4. localtime_r()函数把time_t时间转换成tm结构体;
  5. tm结构体转换成字符串。

程序睡眠

头文件 <unistd.h>

如果需要把程序挂起一段时间,可以使用sleep()usleep()两个库函数。

1
2
unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usec);

linux 目录操作

获取当前目录

头文件:<unistd.h>

相当于 pwd 命令。

1
2
3
4
// 目录长度最大 255,因此要创建的 buf 大小为256,一位是字符0.
char *getcwd(char *buf, size_t size);
// 下列函数是 malloc 动态分配的内存,需要 free 释放。
char *get_current_dir_name(void);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
char path1[256]; // linux系统目录的最大长度是255。
getcwd(path1,256);
cout << "path1=" << path1 << endl;

if(chdir("/home/ran/codeProj") != 0) return -1;

char *path2=get_current_dir_name();
cout << "path2=" << path2 << endl;
free(path2); // malloc()分配,free()释放内存。 new分配,delete释放。
}

切换工作目录

头文件:<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>

  1. opendir()函数打开目录。

    1
    DIR *opendir(const char *pathname);

    成功-返回目录的地址,失败-返回空地址。

  2. readdir()函数循环的读取目录。

    1
    struct dirent *readdir(DIR *dirp);

    成功-返回struct dirent结构体的地址,失败-返回空地址。

  3. closedir()关闭目录。

    1
    int closedir(DIR *dirp);

数据结构:

DIR *目录指针变量名;

每次调用readdir(),函数返回struct dirent的地址,存放了本次读取到的内容。

1
2
3
4
5
6
7
8
struct dirent
{
long d_ino; // inode number 索引节点号。
off_t d_off; // offset to this dirent 在目录文件中的偏移。
unsigned short d_reclen; // length of this d_name 文件名长度。
unsigned char d_type; // the type of d_name 文件类型。
char d_name [NAME_MAX+1];// file name文件名,最长255字符。
};

重点关注结构体的d_named_type成员。
d_name:文件名或目录名。
d_type:文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-子目录(A directory),其它的暂时不关心。注意,d_name 的数据类型是字符,不可直接显示。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <unistd.h>
#include <dirent.h>

using namespace std;

int main(int argc, char *argv[])
{
if(argc != 2) return -1;

DIR *dir = nullptr;

if (!(dir = opendir(argv[1])))
{
cout << "打开目录失败" << endl;
return -1;
}

struct dirent* stdinfo = nullptr;

while(true)
{
if(!(stdinfo = readdir(dir))) break;
cout << stdinfo->d_name << " " << (int)stdinfo->d_type << endl;
}

closedir(dir);

return 0;
}

access() 库函数

头文件:unistd.h

access()函数用于判断当前用户对目录或文件的存取权限。

1
int access(const char *pathname, int mode);

pathname:目录或文件名。

mode:需要判断的存取权限。在头文件<unistd.h>中的预定义如下:

1
2
3
4
#define R_OK  4   // 判断是否有读权限。
#define W_OK 2 // 判断是否有写权限。
#define X_OK 1 // 判断是否有执行权限。
#define F_OK 0 // 判断是否存在。

返回值:

pathname满足mode权限返回0,不满足返回-1,errno被设置。

在实际开发中,access()函数主要用于判断目录或文件是否存在。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <unistd.h>

int main() {
const char* filename = "example.txt";

// 检查文件是否可读
if (access(filename, R_OK) == 0) {
std::cout << "文件可读" << std::endl;
} else {
std::cout << "文件不可读" << std::endl;
}

return 0;
}

stat() 库函数

stat 结构体:

struct stat结构体用于存放目录或文件的详细信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct stat
{
dev_t st_dev; // 文件的设备编号。
ino_t st_ino; // 文件的i-node。
mode_t st_mode; // 文件的类型和存取的权限。
nlink_t st_nlink; // 连到该文件的硬连接数目,刚建立的文件值为1。
uid_t st_uid; // 文件所有者的用户识别码。
gid_t st_gid; // 文件所有者的组识别码。
dev_t st_rdev; // 若此文件为设备文件,则为其设备编号。
off_t st_size; // 文件的大小,以字节计算。
size_t st_blksize; // I/O 文件系统的I/O 缓冲区大小。
size_t st_blocks; // 占用文件区块的个数。
time_t st_atime; // 文件最近一次被存取或被执行的时间,
// 在用mknod、 utime、read、write 与tructate 时改变。
time_t st_mtime; // 文件最后一次被修改的时间,
// 在用mknod、 utime 和write 时才会改变。
time_t st_ctime; // 最近一次被更改的时间,在文件所有者、组、 权限被更改时更新。
};

重点关注st_modest_sizest_mtime成员。st_mtime是一个整数表示的时间,需要程序员自己写代码转换格式。

st_mode成员的取值很多,用以下两个宏来判断:

1
2
S_ISREG(st_mode)  // 是否为普通文件,如果是,返回真。 
S_ISDIR(st_mode) // 是否为目录,如果是,返回真。

stat() 库函数:

#include <sys/stat.h>

1
int stat(const char *path, struct stat *buf);

stat()函数获取path参数指定目录或文件的详细信息,保存到buf结构体中。

返回值:0-成功,-1-失败,errno被设置。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <iostream>
#include <cstdio>
#include <sys/stat.h>
#include <unistd.h>
using namespace std;

int main(int argc,char *argv[])
{
if (argc != 2) { cout << "Using:./demo 文件或目录名\n"; return -1; }

struct stat st; // 存放目录或文件详细信息的结构体。

// 获取目录或文件的详细信息
if (stat(argv[1],&st) != 0)
{
cout << "stat(" << argv[1] << "):" << strerror(errno) << endl; return -1;
}

if (S_ISREG(st.st_mode))
cout << argv[1] << "是一个文件(" << "mtime=" << st.st_mtime << ",size=" << st.st_size << ")\n";
if (S_ISDIR(st.st_mode))
cout << argv[1] << "是一个目录(" << "mtime=" << st.st_mtime << ",size=" << st.st_size << ")\n";
}

utime()库函数

#include <sys/types.h>

#include <utime.h>

utime()函数用于修改目录或文件的时间。

1
2
3
4
5
6
7
int utime(const char *filename, const struct utimbuf *times);

struct utimbuf
{
time_t actime;
time_t modtime;
};

utime()函数用来修改参数filenamest_atimest_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
2
char *strerror(int errnum);                       	// 非线程安全。
int strerror_r(int errnum, char *buf, size_t buflen); // 线程安全。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/stat.h>
using namespace std;

int main()
{
int iret=mkdir("/tmp/aaa",0755);

cout << "iret=" << iret << endl;
cout << errno << ":" << strerror(errno) << endl;
}

perror() 库函数

头文件:stdio.h

用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大。(对调试程序略有帮助)

1
void perror(const char *s);

注意事项

  1. 调用库函数失败不一定会设置errno
    答:并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(一般来说,不属于系统调用的函数不会设置errno,属于系统调用的函数才会设置errno)。

  2. errno不能作为调用库函数失败的标志

    答:只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动的置为 0。