C++Linux编程基础

动态库和静态库

当动态库和静态库同时存在的时候,会优先使用动态库

静态库

1. 制作静态库

g++ -c -o lib库名.a 源文件代码清单

-c表示只编译,-o则是说明需要指定文件名

2. 使用静态库
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名
3. 库文件的概念

程序在编译时,会将库文件的二进制代码链接到目标程序中,这种方式称为静态编译
如果多个程序中用到了同一个静态库中的函数,就会存在多份拷贝。

4. 静态库的特点
  • 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
  • 目标程序的可执行文件比较大,浪费空间
  • 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译

动态库

1. 制作动态库
g++ -fPIC -shared -o lib库名.so 源代码文件清单
2. 使用动态库
g++ 选项 源代码文件名清单 -l库名 -L库文件所在的目录名

需要注意的是:运行可执行程序的时候,需要提前设置LD_LIBRARY_PATH环境变量。

3. 动态库的概念

程序在编译时不会把库的二进制代码链接到目标程序中,而是在运行的时候才被载入。
如果多个程序中用到了同一动态库中的函数,那么在内存中只有一份,避免了空间浪费问题。

4. 动态库的特点
  • 程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存
  • 可以实现进程之间的代码共享,因此动态库也称为共享库
  • 程序升级比较简单,不需要重新编译程序,只需要更新动态库就行

makefile

makefile是一个编译规则文件,用于实现自动化编译
[[…/杂项/Makefile|Makefile]]中有写

main函数的参数

main函数有三个参数,分别是argc、argv和envp:

int main(int argc, char* argv[], char* envp[]){ }
  • argc:存放了程序参数的个数,包括程序本身
  • argv:字符串数组,存放了每个参数的值,包括程序名本身
  • envp:字符串数组,存放了环境变量,数组的最后一个元素是空

什么叫包括程序本身?
在Linux中,我们想要运行这个程序,就需要在终端中使用指令:

./程序名

其实这就相当于将程序名作为一个参数传递给main函数,因此不管什么时候,argc最小都为1,但是我们在终端输入的时候可能还有别的情况:

./程序名 Hello World

此时这个main函数就接收了三个参数,即:argc = 3,此时的argv为:

argv[0] = "./程序名"
argv[1] = "hello"
argv[2] = "world"

操作环境变量

1. 设置环境变量

使用函数setenv():(这个函数是POSIX提供的,因此只能够在Linux系统中使用

int setenv(const char* name, const char* value, int overwrite);
  • name:环境变量名
  • value:环境变量的值
  • overwrite:这个变量的值有两种情况:0和非0
    • 0:如果环境变量不存在,则增加新的环境变量;如果环境变量已经存在,不替换它的值
    • 非0:如果环境变量不存在,则增加新的环境变量;如果环境变量已经存在,替换它的值
  • 返回值:0(成功),-1(失败)
注意事项

此函数设置的环境变量只对本进程有效,不会影响shell的环境变量
也就是说,如果执行了setenv()函数后关闭了该程序,上次的设置失效。

获取环境变量的值

char* getenv(const char* name);

这个函数就更简单了,好像也没什么好说的。
但是这个函数与setenv不同,getenv()是C/C++库提供的,在stdlib.h(cstdlib)中

gdb常用命令

gdb(GNU symbolic debugge)是C/C++最常用的调试工具,gdb通常需要手动安装。

1. 安装gdb

sudo apt install gdb

2. 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模式

Linux的时间操作

UNIX操作系统根据计算机产生的年代把1970年1月1日作为UNIX的纪元时间,1970年1月1日是时间的中间点,将从1970年1月1日起经过的秒数用一个整数存放

time_t

time_t用于表示事件类型,它是long类型的别名,在头文件time.h中定义,用于表示1970年1月1日到0时0秒到现在的秒数

time()

time函数用于获取操作系统的当前时间,需要使用头文件time.h
它有两种使用方法:

  1. 将空地址传给time(),并将time的返回值赋值给now:
    #include 
    
    time_t now = time(0);
    
  2. 将变量的地址作为参数传递给time():
    #include 
    time_t now;
    time(&now);
    

tm结构体,localtime()和mktime()

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);

该函数主要用于时间的计算

gettiemofday()

该函数用于获取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);              // 单位是微秒

目录操作函数

1. 获取当前目录函数getcwd()和get_current_dir_name()

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进行内存分配,因此我们在释放的时候也要使用freenew、delete、malloc、free不能混用!混用可能会导致问题

2. 切换工作目录chdir()、创建目录mkdir()和删除目录rmdir()

切换工作目录chdir()

切换工作目录函数chdir需要包含头文件unistd.h

#include 

int chdir(const char* path);

若是返回值为0则表示切换成功,若非0则失败(目录不存在或没有权限)

创建目录mkdir()

创建目录的函数名就是Linux中创建目录的命令名mkdir,它需要使用头文件sys/stat.h

#include 

int mkdir(const char* pathname, mode_t mode);

可以看到该函数有两个参数:

  • pathname:目录名
  • mode:访问权限的数字写法,如:0755(不能省略前置的0,因为权限数字是八进制

返回值和chdir()一样。

删除目录rmdir()

使用过rmdir()需要包含头文件unistd.h

int rmdir(const char* path);

path就是要删除的目录的路径,返回值和chdir()也是一样的。

获取目录中文件的列表

这一系列的操作都需要使用头文件dirent.h(dir event),一共有三个步骤:

步骤一:用opendir()打开目录
DIR* opendir(const char* pathname);

若是成功,返回目录的地址;若是失败,返回空地址。

步骤二:用readdir()读取目录
struct dirent* readdir(DIR* dirp);

若是成功过,返回struct dirent结构体的地址;若是失败,返回空地址。

步骤三:用closedir()关闭目录
int closedir(DIR* dirp);

相关的数据结构DIR

在上面的函数中,我们使用了目录指针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_named_type

  • d_name是文件名或目录名
  • d_type是文件类型,有多种取值,这里我们只关注两种:
    • 8:常规文件
    • 4:目录

Linux的系统错误

在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实还有一个整型的全局变量errno,存放了函数调用过程中产生的错误代码。
如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。
使用errno需要包含头文件errno.h(或cerrno),配合strerror()和perror()两个库函数,可以差点出错的详细信息。

strerror()

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()

perror()在头文件stdio.h中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大:

void perror(const char* s);

注意事项

1. 调用库函数失败不一定会设置errno

并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(不属于系统调用的函数不会设置errno,即:操作系统(OS)提供的库才会设置errno

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

errno的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动得置为0。
在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。

目录和文件的更多操作

access()

access()用于判断当前用户对目录或文件的存取权限,需要包含头文件unistd.h

int access(const char* pathname, int mode);
  • pathname:目录或文件名
  • mode:需要判断的存取权限,在unistd.h中存在如下宏定义:
      #define R_OK 4 // 判断是否有读权限
      #define W_OK 2 // 判断是否有写权限
      #define X_OK 1 // 判断是否有执行权限
      #define F_OK 0 // 判断是否存在
    
  • 返回值:若是pathname满足mode权限就返回0;不满足就返回-1,并设置errno(这也说明unistd.h是Linux提供的库

stat()与stat结构体

(略)

rename()

rename()函数在头文件stdio.h中,用于重命名目录或文件,相当于操作系统的mv命令

int rename(const char* oldpath, const char* newpath);
  • oldname:原目录或文件名
  • newpath:目标目录或文件名
  • 返回值:0(成功),-1(失败,并设置errno)

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

remove()

remove()函数在头文件stdio.h中,用于删除目录或文件,相当于操作系统的rm命令

#include 

int remove(const char* pathname);
  • pathname:待删除的目录或文件名
  • 返回值:0(成功),-1(失败,并设置errno)

Linux中的信号

信号的基本概念

信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。
信号产生的原因有很多,在Shell中,可以用killkillall命令发送信号:

kill -信号的类型 进程编号
killall -信号的类型 进程名

信号处理、

进程对信号的处理方法有三种:

  1. 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程
  2. 设置终端的处理函数,收到信号后,由该函数来处理
  3. 忽略某个信号,对该信号不做处理,就像未发生过一样

主要是通过signal函数来设置对信号的处理方式,需要包含头文件signal.h

sighandler_t signal(int signum, sighander_t handler);
  • signum:信号的编号,在Linux中默认有64种编号(0~63,其中很大一部分属于自定义信号,默认是终止进程
  • handle:信号的处理方式
    • SIG_DFL:恢复参数signum信号的处理方式为默认行为
    • 一个自定义的处理很好的函数,函数的形参是信号的编号
    • SIG_IGN:忽略参数signum所指的信号
      这里给出一段示例:
// 如果接收到信号1,就执行func函数中的内容
signal(1, func);

发送信号

可以使用kill库函数发送信号:

int kill(pid_t pid, int sig);
  • pid:指定的进程
  • sig:所指定的需要发送的信号

其他内容后续再来补充

进程终止

一共有八种方式可以终止进程,其中5种为正常终止:

  1. main()中使用return返回
  2. 在任意函数中调用exit()
  3. 在任意函数中调用_exit()或_Exit()
  4. . 最后一个线程中其启动例程(线程主函数)用return返回
  5. 在最后一个线程中调用pthread_exit()返回

还有3种异常终止:

  1. 调用abort()终止
  2. 接收到一个信号
  3. 最后一个线程对取消请求做出响应

进程终止的状态

main()中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0
在Shell中,查看进程的终止状态:

echo &?

正常终止进程的三个函数:

  • exit()
  • _Exit()
  • _exit()

其中,前两个是ISO C说明的,_exit()是POSIX说明的:

void exit(int status);
void _exit(int status);
void _Exit(int status);

status即为进程终止的状态。
如果进程不是正常终止,打印的终止状态为非0

调用可执行程序

Linuz提供了system()和exec()函数族,在C++程序中,可以执行其他的程序(二进制文件,操作系统命令或Shell脚本)

system()

system()提供了一种简单的执行程序的方法,需要使用头文件stdlib.h,把需要执行的程序和参数用一个字符串传给system()就行了。

int system(const char* string);

system()的返回值比较麻烦:

  • 如果函数执行失败,system()返回值非0
  • 如果程序执行成功,并且被执行的程序终止状态是0,此函数的返回值即为0
注意事项

在使用此函数的时候,传递的参数最好使用全路径,这样可以避免环境变量的问题

exec函数族

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[]);

你可能感兴趣的:(Linux,玩转C++,c++,linux)