当动态库和静态库同时存在的时候,会优先使用动态库。
g++ -c -o lib库名.a 源文件代码清单
-c表示只编译,-o则是说明需要指定文件名
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
程序在编译时,会将库文件的二进制代码链接到目标程序中,这种方式称为静态编译。
如果多个程序中用到了同一个静态库中的函数,就会存在多份拷贝。
g++ -fPIC -shared -o lib库名.so 源代码文件清单
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
需要注意的是:运行可执行程序的时候,需要提前设置LD_LIBRARY_PATH环境变量。
程序在编译时不会把库的二进制代码链接到目标程序中,而是在运行的时候才被载入。
如果多个程序中用到了同一动态库中的函数,那么在内存中只有一份,避免了空间浪费问题。
makefile是一个编译规则文件,用于实现自动化编译
[[…/杂项/Makefile|Makefile]]中有写
main函数有三个参数,分别是argc、argv和envp:
int main(int argc, char* argv[], char* envp[]){ }
什么叫包括程序本身?
在Linux中,我们想要运行这个程序,就需要在终端中使用指令:
./程序名
其实这就相当于将程序名作为一个参数传递给main函数,因此不管什么时候,argc最小都为1,但是我们在终端输入的时候可能还有别的情况:
./程序名 Hello World
此时这个main函数就接收了三个参数,即:argc = 3,此时的argv为:
argv[0] = "./程序名"
argv[1] = "hello"
argv[2] = "world"
使用函数setenv():(这个函数是POSIX提供的,因此只能够在Linux系统中使用)
int setenv(const char* name, const char* value, int overwrite);
此函数设置的环境变量只对本进程有效,不会影响shell的环境变量。
也就是说,如果执行了setenv()函数后关闭了该程序,上次的设置失效。
char* getenv(const char* name);
这个函数就更简单了,好像也没什么好说的。
但是这个函数与setenv不同,getenv()是C/C++库提供的,在stdlib.h(cstdlib)中。
gdb(GNU symbolic debugge)是C/C++最常用的调试工具,gdb通常需要手动安装。
sudo apt install gdb
如果希望程序可调试,编译的时候需要添加-g(gdb的缩写)选项,并且不能够使用-O选项进行优化。
在开始调试之前,需要输入指令:
gdb 目标程序
命令 | 简写 | 命令说明 |
---|---|---|
set args | 设置程序运行的参数,例如:set args 需要输入的参数 | |
break | b | 设置断点(可以有多个),例如:b 20,表示在第20行设置断点 |
run | r | 开始运行程序,或在程序运行结束后重新开始执行 |
next | n | 执行当前行语句,如果该语句为函数调用,不会进入函数内部 |
step | s | 执行当前行语句,如果该语句为函数调用,则会进入函数内部(有源码才能进) |
print() | p | 显示变量或者表达式的值,如果p后面是表达式,会执行这个表达式 |
continue | c | 继续运行程序,遇到下一个断点停止,如果没有遇到断点,程序将会一直运行 |
set var | 设置变量的值 | |
quit | q | 退出gdb模式 |
UNIX操作系统根据计算机产生的年代把1970年1月1日作为UNIX的纪元时间,1970年1月1日是时间的中间点,将从1970年1月1日起经过的秒数用一个整数存放
time_t用于表示事件类型,它是long类型的别名,在头文件time.h中定义,用于表示1970年1月1日到0时0秒到现在的秒数。
time函数用于获取操作系统的当前时间,需要使用头文件time.h
它有两种使用方法:
#include
time_t now = time(0);
#include
time_t now;
time(&now);
time_t是一个长整数,不符合人类的使用习惯,需要转换成tm结构体,tm结构体在头文件time.h中:
struct tm{
int tm_year; // 年份:其值等于实际年份减去1970
int tm_mon; // 月份:取值区间为[0, 11]
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;// 夏令时标识符(没啥用)
}
想要将time_h转换为tm结构体,需要使用库函数localtime,需要使用头文件time.h。
需要注意的是:loacaltime()不是线程安全的(因为它使用一个静态的结构来存储转换后的本地时间,并返回指向该结构的指针),而localtime_r()是线程安全的(它接受一个指向存储结构的指针作为参数,并将转换后的本地时间存储在该结构中,而不需要使用静态的存储)。
struct tm *localtime(const time_t* timep);
struct tm *localtime_r(const time_t* timep, struct tm* result);
若是要将tm结构体转换成time_t,就需要使用库函数mktime,它也在time.h中:
time_t mktime(struct tm* tm);
该函数主要用于时间的计算。
该函数用于获取1970年1月1日到现在的秒和当前秒钟已逝去的微妙数,可用于程序计时,该函数在头文件sys/time.h钟。
int gettimeofday(struct timeva* tv, struct timezone* tz);
struct timeval{
time_t tv_sec; // seconds
susenconds tv_usec; // microseconds
};
struct timezone{ // 时区
int tz_minuteswest; // minutes west of Greenwich
int tz_dsttime; // type of DST correction
};
如果需要将程序挂起一段时间,可以使用sleep()和usleep()两个库函数,需要使用头文件unistd.h:
unsigned int sleep(unsigned int seconds); // 单位是秒
int usleep(useconds_t usec); // 单位是微秒
getcwd()和get_current_dir_name(),这两个函数都在头文件unistd.h中:
char* getcwd(char* buf, size_t size);
char* get_current_dir_name(void);
这两个函数功能上没什么区别:
#include
#include
using namespace std;
int main(){
char path1[256]; // linux系统目录的最大长度嘶255
getcwd(path1, 256);
cout << "path1 = " << path1 << endl;
char* path2 = get_current_dir_name();
cout << "path2 = " << path2 << endl;
free(path2); // 注意释放内存
}
注意事项:get_currrent_dir_name()会动态分配内存,需要使用char*进行接收,并且这块内存需要我们进行手动释放,并且需要注意,get_current_dir_name()中使用的是malloc进行内存分配,因此我们在释放的时候也要使用free,new、delete、malloc、free不能混用!混用可能会导致问题。
切换工作目录函数chdir需要包含头文件unistd.h:
#include
int chdir(const char* path);
若是返回值为0则表示切换成功,若非0则失败(目录不存在或没有权限)。
创建目录的函数名就是Linux中创建目录的命令名mkdir,它需要使用头文件sys/stat.h:
#include
int mkdir(const char* pathname, mode_t mode);
可以看到该函数有两个参数:
返回值和chdir()一样。
使用过rmdir()需要包含头文件unistd.h:
int rmdir(const char* path);
path就是要删除的目录的路径,返回值和chdir()也是一样的。
这一系列的操作都需要使用头文件dirent.h(dir event),一共有三个步骤:
DIR* opendir(const char* pathname);
若是成功,返回目录的地址;若是失败,返回空地址。
struct dirent* readdir(DIR* dirp);
若是成功过,返回struct dirent结构体的地址;若是失败,返回空地址。
int closedir(DIR* dirp);
在上面的函数中,我们使用了目录指针DIR*,每调用一次readdir(),含税返回struct dirent的地址,存放了本次读取到的内容:
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字符(因为是Linux系统)
};
重点在d_name和d_type:
在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实还有一个整型的全局变量errno,存放了函数调用过程中产生的错误代码。
如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。
使用errno需要包含头文件errno.h(或cerrno),配合strerror()和perror()两个库函数,可以差点出错的详细信息。
strerror()在头文件string.h中声明,用于获取错误代码对应的详细信息。它有两个版本,一个线程安全,一个非线程安全:
char* strerror(int errnum); // 非线程安全
char* strerror_r(int errnum, char* buf, size_t buflen); // 线程安全
这里给出一段示例代码:
#include
#include
using namespace std;
int main(){
int ii;
for(ii=0; ii<150; ii++){ // gcc 8.3.1 一共有133个错误代码
cout << ii << ":" << strerror(ii) << endl;
}
}
运行这段代码,能看到0133都是有语句输出的,其中:==0表示程序正常运行,1133是错误信息==。
perror()在头文件stdio.h中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大:
void perror(const char* s);
并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(不属于系统调用的函数不会设置errno,即:操作系统(OS)提供的库才会设置errno)
errno的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动得置为0。
在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。
access()用于判断当前用户对目录或文件的存取权限,需要包含头文件unistd.h:
int access(const char* pathname, int mode);
#define R_OK 4 // 判断是否有读权限
#define W_OK 2 // 判断是否有写权限
#define X_OK 1 // 判断是否有执行权限
#define F_OK 0 // 判断是否存在
(略)
rename()函数在头文件stdio.h中,用于重命名目录或文件,相当于操作系统的mv命令:
int rename(const char* oldpath, const char* newpath);
在实际开发中,access()主要用于判断目录或文件是否存在。
remove()函数在头文件stdio.h中,用于删除目录或文件,相当于操作系统的rm命令:
#include
int remove(const char* pathname);
信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。
信号产生的原因有很多,在Shell中,可以用kill和killall命令发送信号:
kill -信号的类型 进程编号
killall -信号的类型 进程名
进程对信号的处理方法有三种:
主要是通过signal函数来设置对信号的处理方式,需要包含头文件signal.h:
sighandler_t signal(int signum, sighander_t handler);
// 如果接收到信号1,就执行func函数中的内容
signal(1, func);
可以使用kill库函数发送信号:
int kill(pid_t pid, int sig);
其他内容后续再来补充
一共有八种方式可以终止进程,其中5种为正常终止:
还有3种异常终止:
在main()中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0。
在Shell中,查看进程的终止状态:
echo &?
正常终止进程的三个函数:
其中,前两个是ISO C说明的,_exit()是POSIX说明的:
void exit(int status);
void _exit(int status);
void _Exit(int status);
status即为进程终止的状态。
如果进程不是正常终止,打印的终止状态为非0。
Linuz提供了system()和exec()函数族,在C++程序中,可以执行其他的程序(二进制文件,操作系统命令或Shell脚本)
system()提供了一种简单的执行程序的方法,需要使用头文件stdlib.h,把需要执行的程序和参数用一个字符串传给system()就行了。
int system(const char* string);
system()的返回值比较麻烦:
在使用此函数的时候,传递的参数最好使用全路径,这样可以避免环境变量的问题。
exec函数族提供了另一种在进程中调用程序(可执行文件或Shell脚本)的办法:
int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execcv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpr(const char* file, char* const argv[], char* const envp[]);