Linux下C++基础篇

以前没系统学过linux下C++的一些基本开发方法,所以抽空在网易云课堂上听了个课程简单学习一下基础,本文记录一下听课中的笔记。

课程前几章介绍开发环境、linux基础操作、配置SAMBA共享目录啥的就不进行记录了,毕竟听这个课是为了补一下linux下如何进行C++编程的基础知识。

最简单的两个命令记录一下:

g++ -c main.cpp -o main.o:表示将main.cpp编译成main.o中间文件。

g++ main.o -o helloworld:表示将main.o链接成可执行文件helloworld。

也可直接:

g++ main.cpp -o helloworld

第五章 makefile

makefile的基本规则在另一篇笔记里记录了,这里只记录几个新的点。

增量编译

增量编译其实在记录makefile的那篇笔记里也提过,就是当某个文件改变时,只增量编译受影响的文件,而不是把整个项目重新编译。

增量编译是靠文件时间来判断的,查看文件时间可以用:ls -ls --full-time

所谓的比较时间,规则如下:

  1. 如果target不存在,则执行make,否则转2;
  2. 如果dependencies的时间比target时间新,则执行make,否则转3;
  3. 输出已经是最新的了,不执行规则。

优化makefile

这个教程里的优化写的比较简单,但也可以学习:

  • 使用变量、通配符
1
2
%.o:%.cpp
g++ ‐c $< ‐o $@
  • 自动罗列*.o文件
1
2
CXX_SOURCES=$(wildcard*.cpp)
CXX_OBJECTS=$(patsubst%.cpp,%.o,$(CXX_SOURCES))
  • 头文件依赖

使用编译选项-MMD,比如g++ -c -MMD main.cpp -o main.o则会在生成main.o的同时生成main.d文件,该文件里的内容就是所需要的依赖,然后如下操作:

1
2
3
4
5
6
DEP_FILES=$(patsubst%.o,%.d,$(CXX_OBJECTS))
%.o:%.cpp
g++ ‐c‐MMD $< ‐o $@

#使用‐include指令,将所有的.d文件包含进来
include$(DEP_FILES)
  • 利用foreach函数进行对子目录的支持

该章节最终的makefile如下,能支持简单使用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
######### 标准Makefile Lv1.0 ########
EXE=helloworld
SUBDIR=src object
#CXX_SOURCES=$(wildcard *.cpp)
CXX_SOURCES =$(foreach dir,$(SUBDIR), $(wildcard $(dir)/*.cpp))
CXX_OBJECTS=$(patsubst %.cpp, %.o, $(CXX_SOURCES))
DEP_FILES =$(patsubst %.o, %.d, $(CXX_OBJECTS))

$(EXE): $(CXX_OBJECTS)
g++ $(CXX_OBJECTS) -o $(EXE)

%.o: %.cpp
g++ -c -MMD $< -o $@

-include $(DEP_FILES)

clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)

test:
echo $(CXX_OBJECTS)

GDB

在compile这一步添加选项-g,如果没有用-g选项生成的可执行文件虽然也可以gdb运行,但是调试过程中不会显示调试信息,没什么用。

g++ -g main.cpp -o hello

b:添加断点

r:从头开始运行

n:下一步

c:程序继续运行直到下一处断点

p:显示表达式的值

info break:显示断点信息

del break n:删除编号n的断点

q:退出gdb

disp:监视变量的值

x:显示内存:x/16xb n,看16个单元,x表示16进制看,b表示每个单元多少字节,b是1个字节,h是2个字节,w是4个字节,g是8个字节。

bt:显示堆栈。

段错误

在Linux下,程序中如果进行了不正确的指针操作(如空指针访问、野指针访问),那么程序会崩溃,发生段错误(Segment Fault)。

很容易复现的话就可以直接用GDB来调试。

当错误不容易复现的时候,可以使用内存转储的手段。

内存转储步骤如下:

  1. ulimit -c unlimited
  2. 运行程序,程序发生段错误退出时会将信息转储到core.*文件中
  3. 用gdb来查看段错误的代码位置gdb exe core.*

一个程序生成时用了-g选项的话,生成的可执行文件会更大一些,因为包含了可调式信息。

如果要判断一个程序是否包含可调式信息,可以用objdump -h helloworld来判断,有debug就是有调试信息。

适用场合

  • 单元测试
  • 段错误的定位

不适用场合

  • 大型程序
  • 多线程

动态库和静态库

生成动态库

编译:g++ -c -fPIC example.cpp -o example.o

链接:g++ -shared example.o -o libexample.so

PIC:position independent code位置无关代码

动态库命名规范:libxxx.so

可以使用nm命令查看库中的符号。

使用动态库

包含头文件,调用里面的函数即可。

编译:g++ -c main.cpp -o main.o

链接:g++ main.o -o helloworld -L. -leample

-L.:指定库文件的位置

-leample:使用libexample.so这个动态库。

操作系统默认从标准位置寻找相应的库:/lib /usr/lib /usr/local/lib

如果没有就会去LD_LIBRARY_PATH环境变量里找。

所以要想使用自己的库,得做好上述设置。

查看依赖的库

readelf -d helloworld

通常目录结构是:

1
2
3
libxxx/
-lib/
-include/

常用CXXFLAGS表示C++编译选项,LDFLAGS表示链接选项。

静态库

命名标准:libxxx.a

第一步,编译得到.o文件

第二步,打包ar -rcs libxxx.a file1.o file2.o...

静态库本质就是将.o文件打个包而已,因此可以像.o文件一样使用。

当静态库和动态库同时存在的时候,会优先选择动态库进行链接,若要使用静调库,泽科使用全路径方式指定静态库。

c++兼容c的库

以前就学过,用external “C”即可,但比较好的做法是在.h文件中就写好兼容C和C++的版本。

1
2
3
4
5
6
7
#ifdef __cplusplus
extern "C"{
#endif
...
#ifdef __cplusplus
}
#endif

动态库的手工加载

使用dl库中的函数

#include<unistd.h>

#include<dlfcn.h>

具体再看教程吧,感觉用的不多,主要用dlopen、dlsym和dlclose方法。

Linux文件读写

简单来说有两种方式:

  1. ANSI C,使用stdio.h里的函数fopen、fclose、fwrite、fread。
  2. Linux API,open、close、write、read

推荐使用第一种吧,能被各平台支持。

使用第一种的时候,文件路径使用/,换行符Linux和windows下不同,linux是\n,windows是\n\r。

普通情况下均推荐使用ANSIC来操作文件。

仅当该文件表示一个外部设备时,才用LinuxAPI来操作。

线程

在Linux下,使用pthread库来创建和操作线程,pthread是Linux系统自带的一个库。

1
2
3
4
5
pthread_create(&handle,//指代一个线程对象,pthreat_t类型的实例
NULL,
function,//线程入口函数,用函数指针
context//线程参数
)

线程主函数退出时,线程自然终止。

pthread_join(handle,NULL):等待B线程自然退出,在B线程退出后回收B线程的系统资源。

pthread_cancel:用于终止取消一个正在运行的线程,不推荐使用。

互斥体

pthread_mutex_t hMutex;

pthread_mutex_init(&hMutext,NULL);

pthread_mutext_destroy(&hMutext);

pthread_mutext_lock(&hMutex);//不能获取锁的时候会阻塞,但trylock不能获取锁的时候就会直接返回。

pthread_mutext_unlock(&hMutex);

信号量Semaphore

1
2
3
4
5
6
7
8
#include<unistd.h>
#include<semaphore.h>

sem_t hSem;
sem_init(&hSem,1,initial_value);
sem_destroy(&hSem);
sem_wait(&hSem);
sem_post(&hSem);

进程相关

何为进程?

当helloworld文件被运行后,在操作系统内部创建一个进程对象。

查看进程:ps。

top:实时查看进程。

kill强制杀死进程。

前台进程和后台进程

关闭终端时会关闭前台进程。

以后台进程运行,在命令行后面加一个&符号。

前后台进程切换:ctrl+z暂停任务,bg将任务送到后台继续任务,fg将任务拿到前台运行。

进程间通信

IPC:inter-process communicating。

四种方式:管道、消息队列、共享内存、网络套接字。

管道:pipe,是Linux操作系统提供的一个消息传递机制。

  1. 系统中创建一个文件,其文件类型为管道mkfifo message
  2. 进程A打开该文件写入数据
  3. 进程B打开该文件读取数据

open是阻塞的,read也是阻塞的,管道是单向的,和普通文件有区别。

系统调用

查看可用的系统调用man syscalls

如果找不到对应的系统调用,可以在代码里直接调用命令行,用system().

int ret = system("rm -rf *.txt");

该函数会阻塞。

也可以用popen():调用某个命令行,并获取其标准输出。

该函数是linux特有的,windows下对应的叫_popen/_pclose

1
2
3
FILE* fp = popen("ifconfig","r");
int n = fread(buf,1,512,fp);
fclose(fp);

跨平台

尽量使用标准函数/类型/语法

可以用条件编译。