[C++进阶]容易被忽视的细节之-基础篇

365次阅读
没有评论

目录

从头开始

在初学C++时,由于学到的东西还少,所以写的一些东西暂时视作固定写法。这里回过头来看看初学内容的细节。

命名空间

标准命名空间

在初学C++时,我们在文件的顶部加上这些代码:

#include <iostream>
using namespace std;

int main(){
    cout << "Hello World" << endl;
}

这样写会导入std中的所有内容,但如果我们只用std里面的一部分内容,又担心命名冲突,此时我们可以去掉using namespace std;,改用std::xxx的形式来使用std中的内容:

#include <iostream>

int main(){
    std::cout << "Hello World" << std::endl;
}

命名空间的存在是为了避免不经意间的命名冲突,如果感觉这样写很麻烦,而且只需用到std的一部分内容(比如cout和endl),可以这样写:

#include <iostream>
using std::cout;
using std::endl;

int main(){
    cout << "Hello World" << endl;
}

到了C++17,两个using可改为using std::cout, std::endl;

你可能不知道的命名空间

using并不是只能在全局范围内使用:

int main(){
    using namespace my_namespace;
    if(my_namespace::b){
        using std::cout;
        cout << my_namespace::s;
    }
}

命名空间可以嵌套定义:

namespace first{
    namespace second{
        int a = 1;
        void func1(){
            std::cout << "1" << std::endl;
        }
        void func2();
        void func3();
    }
    void second::func2(){
        std::cout << "2" << std::endl;
    }
}
void first::second::func3(){
    std::cout << "3" << std::endl;
}
int main(){
    int b = first::second::a;
    first::second::func1();
    first::second::func2();
    first::second::func3();
}

命名空间可以起别名

(这是C++11后的特性)

int main(){
    namespace haha = std;
    haha::cout << "Hello" << haha::endl;
}

你可能不知道的作用域

我们都知道,一个局部变量只在当前块作用域内有效,在当前块作用域内嵌套的其他块中也有效。允许在嵌套的内作用域中重新定义外作用域已有的名字:

int a = 1;
int main(){
    std::cout << a << std::endl;  // 输出1
    int a = 2;
    if(a > 0){
        int a = 3;
        std::cout << a << std::endl;  // 输出3
    }
    std::cout << a << std::endl;  // 输出2
    //要想用全局的a,可以在前面加上::
    std::cout << ::a << std::endl;  // 输出1
}

当然,不建议这样给变量起名,因为这样容易造成混乱,导致代码维护困难。

using的其他作用

C++11后,using可以用来代替typedef给某个类型起别名:

using int32 = int;
int32 a = 1;

对于函数式编程,定义一个可以指向某一类函数的函数指针:

double (*f)(double, double);

这段代码指的是,变量f是指向某一类返回类型为double,有两个类型为double的形参的函数指针。

为了能做到符合知觉的func f;,func为函数,f为变量名,使用typedef时:

typedef double (*func)(double, double);
func f;

使用using时:

using func = double(*)(double, double);
func f;

这一部分也有很多坑,比如漏括号、漏星号等,都会有不同的效果,都会让你在debug时看着谜语人般的报错时叫苦连天。感兴趣的可以阅读我的另一篇文章:你真的会声明C++的函数吗?

using和typedef都不会创建新的数据类型,他们仅创建现有类型的同义词(synonyms),或者说给现有类型“起别名”。

using和typedef的区别是,typedef不能给模板类起别名,而using可以。

template 
using VectorT = std::vector;

标准IO的缓冲区

在写下cout << "Hello World" << endl;时,这个endl有换行的作用。但\n也能表示换行,难道endl = '\n'?实际上不然。endl并不是一个字符,也不是字符串,我们称他为操作符(manipulator),如果查看它的源码会发现它是一个函数模板,作用是换行并刷新缓冲区,即把缓冲区的内容写入到输出设备中并清空缓冲区,相当于std::cout << '\n' << std::flush;

那么,缓冲区是什么呢?为什么要先将输出的内容写入到缓冲区,然后再把内容输出到输出设备,而不直接一步到位呢?

这是因为,CPU与IO设备的速度是不同的,大部分情况下IO设备的速度很慢,假设需要向输出设备写入一万次数据,如果每输出一次都需要向输出设备做一次写操作,那么就会导致程序很多时间用在等待IO设备上。为了解决这一问题,可以先划定一定的内存空间,先把数据写入到这个空间中,等到这片空间满了、设备空闲了或者程序手动写入,才把这个内存中的数据全部写入到输出设备上,这样不仅可以加快程序运行的速度,还能减少写入的次数,延长输出设备的寿命。而这片内存区域,就被称为缓冲区。

因此,使用cout来调试程序时(特别是在定位程序崩溃的位置时),应保证及时刷新流,防止输出调试信息前程序就已经崩溃了,导致无法定位崩溃的位置。在写入文件时,不应一个一个字符地写入,使用缓冲区可以提高写入速度。

标准错误流

除了cout这个标准输出流以外,还有cerrclog这两个标准错误流,用来输出错误或警告信息。

cerrclog的区别是,cerr不经过缓冲区,会直接写入到输出设备上。这是为了在紧急情况下(比如内存已满,没空间用来做缓冲区了,或者程序崩溃时),提供一个输出信息的可能性。

输出流的重定向

coutcerrclog都可以被重定向,比如将cout重定向到文件:

#include <iostream>
#include <fstream>

int main(){
    std::ofstream out("test.txt");
    std::cout.rdbuf(out.rdbuf());
    std::cout << "test" << std::endl;
}

当然,你也可以直接写入文件而不重定向:

std::ofstream out("test.txt");
out << "test" << std::endl;

重定向是有作用范围限制的,比如:

if(true){
    std::ofstream out("test.txt");
    std::cout.rdbuf(out.rdbuf());
    std::cout << "1" << std::endl;
}
std::cout << "2" << std::endl;

运行以上代码,你会发现test.txt中只有“1”没有“2”。如果需要让“2”也能输出到文件里,需要把out写在if的外面:

std::ofstream out("test.txt");
if(true){
    std::cout.rdbuf(out.rdbuf());
    std::cout << "1" << std::endl;
}
std::cout << "2" << std::endl;

重定向之后如何恢复呢:

std::ofstream out("out.txt");
std::streambuf* old = std::cout.rdbuf(out.rdbuf());
std::cout << "test1" << std::endl;
std::cout.rdbuf(old);
std::cout << "test2" << std::endl;

运行以上代码,可以发现out.txt中有test1,屏幕上输出test2

如果既要输出到屏幕,又要写入文件中呢?一般这是为了做日志。虽然你可以自定义一个cout来实现它,但不建议再折腾cout了。你可以自己写一个日志框架,或者使用第三方日志框架,比如log4cpp

cout中也许有用的内容

我们可以修改输出整数的形式:以十六进制(hex)、十进制(dec)、八进制(oct)形式输出。

比如十六进制:

std::cout << std::hex;

在此之后输出的整数就是十六进制形式了。同理,把上面的hex改为dec、oct就可以改为十进制、八进制形式。

对于整数,可以使用std::showbase来显示进制的前缀,使用std::noshowbase不显示(默认),如:

std::cout << std::showbase << std::hex << 15;
std::cout << ", "<< std::oct << 8;

输出:0xf, 010

使用std::uppercase,可以在输出16进制时a-f变为A-F输出16进制前缀时0x0X,输出科学记数法时eE。默认情况为std::nouppercase

对于浮点数,需要控制输出的精度(包括整数位),可以使用std::setprecision(n),需要引入头文件iomanip,如:

#include <iostream>
#include <iomanip>
int main(){
    double pi = 3.1415926
    std::cout << std::fixed << std::setprecision(3) << pi << std::endl;
}

输出:3.14

这个包括整数位是什么意思呢?比如setprecision(4),那么1.23456 -> 1.23412.3456 -> 12.34
也可以使用std::cout.precision(n)来设置精度。精度默认为6。

对于浮点数,如果刚好是整数,又想输出后面的.0时,可以使用std::showpoint

double d = 4.0;
std::cout << d << ", " << std::showpoint << d << std::endl

输出:4, 4.0

对于浮点数,可以使用std::fixed设置小数部分自动补0,直到符合精度:

std::cout << std::setprecision(5) << std::fixed << 3.14 << std::endl;

输出: 3.1400

想要取消掉std::fixed等设定,可以使用std::cout.unsetf(s);

对于浮点数,使用std::scientific设为科学计数法输出。

对于布尔值,默认情况下输出truefalse时会显示为1和0。为了能够输出true或者false,你可以使用std::boolalpha

std::cout << std::boolalpha << true;

如果需要输出指定长度的内容,不足时补齐,可以引入头文件iomanip并使用std::setfill(char)std::setw(int),其中setw是暂时性的,只会对下一次输出起作用:

std::cout << std::setfill('x') << std::setw(5) << 123 << "\n" << 456;

输出:

xx123
456

标准输入流

用户输入错误数据

我们常用标准输入流cin来让用户输入数字,一般都是这样写:

int a;
std::cin >> a;
std::cout << a << std::endl;

如果用户输入的不是数字呢?
如果输入的是abc,输出是0
如果输入的是123abc,输出的是123
我们可以这样判断输入的是否是数字:

int a;
if(std::cin >> a){
    std::cout << "输入了" << a << std::endl;
} else {
    std::cout << "输入的不是数字" << std::endl;
}

不过,如果输入的是123abc,那么输出会是输入了123

如果程序中有多个cin,当输入的数据错误时(比如上面的应输入整数却输入了字符串,或是输入的整数超过int最大限制等),会导致后面的cin都失效:

int a, b;
std::cout << "请输入a:";
std::cin >> a;
std::cout << "请输入b:";
std::cin >> b;
std::cout << a << "\n" << b << std::endl;

运行以上代码,并输入abc并回车,会发现并没有让我们第二次输入,且输出结果是:

请输入a:abc
请输入b:
0
0

这是因为错误的输入让cin处于错误状态,必须消除它的错误状态才行:

if(std::cin.fail()){
    std::cin.clear();
    std::cin.sync();
}

std::cin重载了运算符!,即!std::cin返回std::cin.fail()
于是你便可以在用户输入错误时不断让用户输入新数据:

int a;
while(!(std::cin >> a)){
    std::cout << "输入的不是数字,请重新输入" << std::endl;
    std::cin.clear();
    std::cin.sync();
}
std::cout << "输入的是" << a << std::endl;

单字符读取和getline

char c1, c2;
std::cin >> c1;
std::cin >> c2;
std::cout << c1 << "\n" << c2 << std::endl;

如果用户输入的不止一个字符,比如abcd,会是怎样呢?
答案是用户只需输入一次,输出为:

a
b

cin中有一个储存字节的流,类似于缓冲区,每当给字符赋值时,会从中拿出最前面的一个字符,直到它被清空。在它被清空之前,不会再让用户输入。

我们同样可以使用std::cin.get()来从中读取字符,即上述代码可改为:

char c1, c2;
c1 = std::cin.get();
c2 = std::cin.get();
std::cout << c1 << "\n" << c2 << std::endl;

利用这个特性,我们可以写一个求和程序:

int sum = 0, input;
while(std::cin >> input)
    sum += input;
std::cout << sum << std::endl;

输入:1 3 5 7 8回车,再输入2 4 6 9回车,然后按键盘上的Ctrl+D(Unix)或Ctrl+Z(Windows),以输入一个文件结束符(end-of-file)。
输出:45

std::cin >> input返回std::cin本身,当std::cin作为条件时,是检测流的状态,若流正常,则是true,若流遇到错误,或者接收到了一个文件结束符(end-of-file),则为false。

std::cin.getline(char*, int, char='\n')可以获取一行文本(字符串):

char str[128];
std::cin.getline(str, sizeof(str));
std::cout << str << std::endl;

该函数的三个参数分别是:需要赋值的字符串对应的字符指针,字符串长度,结束标识符(默认为换行\n)。
同样,在cin中的数据被清空之前,不会再让用户输入: 比如将小写字母z作为结束符:

char str1[128], str2[128];
std::cin.getline(str, sizeof(str1), 'x');
std::cin.getline(str, sizeof(str2), 'y');
std::cout << str1 << "\n" << str2 << std::endl;

输入:abcxdefyghi
输出:

abc
efh

clear()和sync()

std::cin.clear()第一眼看起来像是清空cin的数据流,但实际上并不是,它的作用是去除cin中的错误。

int a;
char str[128];
std::cin >> a;
std::cin.clear();
std::cin >> str;
std::cout << str << std::endl;

输入:123a456 输出:a456

如果去掉上上述代码中的std::cin.clear(),再输入123a456,将不会有任何输出。

std::cin.sync()的作用才是清空数据流。clear()sync()一起使用,就可以去除cin的错误并清空数据流,前面已经提到过了,这里不再复述。

为什么要先声明再定义?为什么要写一个头文件用来存放声明?

我们在定义函数时,一般要先在前面声明函数,然后再定义函数,如:

void func();  // 声明函数
int main(){
    func();  // 调用函数
}
void func(){  // 定义函数
    // do something
}

一般还把声明放到头文件中(.h)。

这是因为,函数的作用域是从它被定义时开始的,虽然你也可以把定义写到前面去,但是这样不太美观。为了能把主函数放到上面,因此先声明,然后是主函数,然后再定义前面声明的函数。
另外,如果函数间存在互相调用,比如:

void func1(){
    func2();
}
void func2(){
    func1();
}

那么无论哪个在前哪个在后,上述代码都会报错。因此先声明再定义,可以让你免去函数互相调用时调整函数位置的痛苦。

另外,当多个文件需要调用某个函数时,在每一个文件都定义一遍显然是不智慧的选择,为了能把函数的定义只写在一个文件里,你可以在调用它的地方声明这个函数就行了:

// 这里是b.cpp
// a.cpp里有一个func(),我想调用它:
void func();  // 声明这个函数
void fb(){
    func();
}

当这样做还是很不智慧,因为它并不能直接看出func是哪来的。为了能做到“导入一个模块”,我们可以把func写到头文件中,然后在需要用到它的地方包含这个头文件:

// a.h
void func();  // 声明函数
// a.cpp
#include "a.h"
void func(){  // 定义函数
    // do something
}
// b.cpp
#include "a.h"  // “导入了a这个库”,相当于把a.h中对func的声明加进来
void fb(){
    func();  // 调用函数
}

先声明再定义还有一个好处是,对于一个大型合作项目,你可以先声明一系列函数,然后用注释标注这些函数有什么用,接着你就可以以整体的思想去构思整个项目,而不必先一个个实现他们,或者将这个头文件交给合作者,让他们去编写函数的实现,而你就可以直接调用这些函数来实现具体的内容了,即便现在他们还没有定义。比如,你想编写一个把大象装进冰箱的程序,需要三步:打开冰箱门,把大象放进冰箱,关闭冰箱门。于是需要三个函数,你可以先声明这些函数,然后调用这三个函数,完成整体的内容,然后再编写细节,即这三个函数的具体实现。

对于某些企业的项目或者一些商业项目,需要提供一些函数。但这些函数可能涉及到机密,不能直接把这些函数的具体实现给到对方,此时你就可以只把这些函数的声明写在头文件中,然后把头文件给对方,这样对方就可以知道有哪些函数供他们使用,但无法看到函数的具体实现。

编译和链接

编译和链接是大家一定会用到但很少重视的步骤,这是因为大部分的集成开发环境(IDE)已经把它们封装好了,帮我们隐藏掉了这些细节。如果离开IDE,要怎么对代码进行编译呢?

这里以Linux环境为例,测试环境为ubuntu 20.04。

多文件的编译

在实际的开发中,我们会把不同代码放在不同的文件中。

// lib.cpp
int add(int a, int b){
    return a + b;
}
// main.cpp
#include <iostream>
int add(int, int);
int main(){
    int b = add(1, 2);
    std::cout << b << std::endl;
    return 0;
}

编译这两个文件:
g++ -c lib.cpp
g++ -c main.cpp

此时会生成两个文件:lib.omain.o,他们被称为目标文件,是不能直接执行的。
比如main.o中,函数add只是一个声明,具体的实现是在lib.o中,所以在main.o中函数add的跳转暂时设为0,需要将这些文件链接起来后,才会修正函数add的地址。

链接这两个文件:
g++ main.o lib.o -o target

运行这段指令,会生成一个文件target,运行它:
./target

输出:

3

但这样做效率实在太低了,实际的项目可能会有非常多的文件,一个个手动编译会非常慢。

Makefile

Make是一个常用的构建工具,它除了能够完成程序的自动化构建外,还有其他的奇技淫巧。

makefile的格式是:

目标: 所需文件1 所需文件2 ...
    指令

目标: 所需文件1 所需文件2 ...
    指令

...

前面例子的makefile就是:

# 可以使用井号作为注释。
# 如果要在makefile中表示井号而不作为注释,可以使用转义:\#
all: target clean

target: main.o lib.o
    g++ main.o lib.o -o target

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

lib.o: lib.cpp
    g++ -c lib.cpp

clean:
    rm -f *.o

执行指令make target,意思就是构建目标target,此时终极目标就是这个target。
因为target所需文件是main.o和lib.o,这两个文件还未被构建,因此会去找这两个文件。
main.o所需文件是main.cpp,存在这个文件,于是执行指令g++ -c main.cpp,生成了main.o。lib.o同理。
此时目标target所需文件已经有了,于是执行指令g++ main.o lib.o -o main,得到目标文件target。

可以发现,make是根据一个“依赖树”递归地去构建文件,依赖不存在,那就先去构建依赖,依赖的依赖不存在,就先去构建依赖的依赖,直到完成终极目标。当然,如果最后的依赖不存在,比如上面的main.cpp不存在或lib.cpp不存在,则会报错并退出。

构建完后,再次执行指令make target,会得到提示“make: 'target' is up to date.”,意思是所有文件都已存在并且是是最新的了,不需要重复构建。如果我们修改了main.cpp再执行make target,则会只更新main.o和目标文件target,lib.cpp没有被修改所以无需重复构建,可见它也可以节省构建所需的时间。

上面构建的是目标target,但如果我们要构建文件中的目标“all”,即make all,则是这样:
目标all需要的是target和clean两个目标,目标target上面已经讲过了。目标target完成后是目标clean,于是执行指令rm -f *.o,即删除所有后缀名为.o的文件。当然,这个终极目标由实际情况而定,并不一定需要清理文件。

makefile第一个目标会被视作默认目标,因此上面的指令make all可改为make

目标clean后面没有依赖,因此你可以执行make clean来清理.o文件,即执行目标clean,对应的指令为rm -f *.o

可是,当文件多起来之后,每次添加或去除一个.o文件,都需要在目标文件后面的所需文件和指令中写两遍,麻烦且容易出错,此时我们就可以使用变量

# 文件很多时,可以用反斜杆换行
objects = main.o lib.o a.o b.o c.o \
          d.o e.o

target: $(objects)
    g++ $(objects) -o target

...

实际上,gnu的make还可以自动推导,如果没有特殊需求,make可以推导出指令,而且,假设需要构建main.o,它也会把main.cpp自动加入依赖。假设main还需要abc.h和xyz.h,那么makefile就可以简化成这样:

target: main.o lib.o
    g++ main.o lib.o -o target

main.o: abc.h xyz.h

没错,main.o不需要加上main.cpp,而且lib.o的依赖干脆就不需要写了。
甚至你还能:

objects = a.o b.o c.o
libs = lib1.h lib2.h lib3.h
target: $(objects)
    g++ $(objects) -o target
$(objects): $(libs)

当然,奇技淫巧是要慎用的,上面的代码很偷懒,但容易出错误,而且也不容易看清依赖关系。

另外,每一个makefile都建议加一个clean用来清理.o文件和构建出的可执行文件,而且最好放在最后面,以便清理并重新构建。而且,clean建议这样写:

.PHONY: clean
clean:
    -rm target $(objects)

.PHONY表示clean是一个“伪目标”,因为clean没有依赖文件,而且也不需要生成一个文件clean。
rm $(objects)前还加了一个减号-表示忽略某些文件的错误。

使用变量,你还可以:

PROJECT_NAME = MyProject
VERSION = 1.0.0
TARGET = $(PROJECT_NAME)-$(VERSION)

OBJS = ...

$(TARGET): $(OBJS)
    g++ $(OBJS) -o $(TARGET)

...

makefile实际上像是一个脚本语言,它可以做很多复杂的事情。除了最基本的构建,你还能实现提交代码、备份代码、自动化部署程序等功能。
如果你还想知道makefile的详细内容,可以查阅:

CMake

CMake是一个开源的,跨平台的构建工具。大部分IDE都支持CMake。

首先需要创建一个CMakeLists.txt,对于一个最简单的项目,CMakeList只需三行:

cmake_minimum_required(VERSION 3.10)
project(ProjectName)
add_executable(ProjectName main.cpp)

第1行指定了所需的CMake最低版本,第2行指定项目的名称,第三行指定可执行文件的名称和它由那些文件编译而成。

在项目所在目录下创建新目录build,并把cmake生成的文件放在这里:

mkdir build
cd build
cmake ..

运行以上指令后,便会在build下生成相关文件和Makefile,接着只需输入make并回车,即可生成可执行文件target。

对于多文件工程,则需修改第3行代码,如:

add_executable(ProjectName main.cpp lib.cpp)

一个个添加太麻烦了,那么我们可以:

file(GLOB SRC_FILES
        "${PROJECT_SOURCE_DIR}/*.h"
        "${PROJECT_SOURCE_DIR}/*.cpp")

add_executable(${CMAKE_PROJECT_NAME} ${SRC_FILES})

${CMAKE_PROJECT_NAME}指的是project()中定义的项目名,我们通过file()将项目目录下所有以.h.cpp结尾的文件都添加到变量SRC_FILES中,并在add_executable()使用它。

我们也可以使用set()来设置cmake或自定义的变量:

set(CMAKE_CXX_STANDARD 11)
set(MY_VARIABLE 666)

第一行表示构建用的C++的版本为C++11,第二行是我们的自定义变量。

当我们使用了第三方库时,需要链接库:

target_link_libraries($(CMAKE_PROJECT_NAME) 库名)

你还可以使用CMake生成Doxygen文档、自动化测试、自动部署等,甚至能当成一个编程语言使用,知乎上甚至有人用它写了个光追

CMake建议边用边学,因为你很难不结合项目一起使用。这里列举几个学习资源:

静态链接与动态链接

静态链接

以前面多文件编译为例,有两个文件:

// main.cpp
int add(int, int);
int main(){
    int b = add(1, 2);
}
// lib.cpp
int add(int a, int b){
    return a + b;
}

分别编译它们,生成main.o和lib.o两个文件,在main.o中,函数add的地址会暂时设定为0,在与lib.o链接的过程再把它重定位到add的正确地址。

强引用与弱引用

上面的代码中,add是一个强引用。如果链接过程中add没有被定义,则链接会报错。

我们也可以创建一个弱引用,它与强引用类似,但如果它没有被定义,则链接不会报错。

__attribute__((weakref)) void func();

attribute并不是只能放在开头:

void __attribute__((weakref)) func1();
void func2() __attribute__((weakref));

在现代编译器中,使用weakref还需要指定别名:

__attribute__((weakref, alias("y"))) static void func();

如果函数未被定义,那么它的地址将会是0,我们可以这样判断函数是否被定义了:

if(func){
    func();
}

这样一来,只有该函数被定义了,才会执行它。

变量也可以是弱引用:

__attribute__((weak)) extern int a;

不过要注意的是,__attribute__是给编译器看的,像#define那样。且并非所有编译器都支持weakref的特性。

弱引用可以给程序模块化提供方便。我们可以将扩展模块设定为弱引用,当扩展模块和主程序链接在一起时,就可以用到扩展模块的内容。如果没有扩展模块,就只使用主程序的部分功能。

动态链接

很多程序都需要用到iostream等内容,如果每个程序都把它们的实现代码都打包到最终的可执行文件中,就会导致每个程序都有一段重复的内容,势必会造成空间的浪费。

同样,我们在使用或给其他用户提供库时(特别是很大的库),也不希望它们被重复打包。如果能把它分离开来,保存成一个单独的文件,让每一个要用到的程序都去共享这个文件,不就可以避免重复了吗?

动态链接库很好的解决了这个问题。除了节省硬盘空间,由于它可以被多个程序共享,所以只需读取一份到内存中,减少了内存的浪费。因为动态链接库是一个单独的文件。所以我们也可以很方便地安装和更新他们。假设你发行了一个1GB甚至更大的程序,当程序需要更新时,也可以只下载被修改的动态链接库文件,避免了重新下载整个程序。另外,你也可以利用动态链接库的特性让你的程序支持插件功能。

动态链接库一般在Windows下是.dll后缀,在Linux下是.so后缀。

Windows下,使用动态链接库时,会先在当前目录下寻找,然后再搜索Windows/System和Windows/System32目录。
而Linux是,先搜索/lib、/lib64等,然后再搜索/usr/lib等,最后再搜索ld.so.conf配置下的路径。默认情况下不会搜索当前路径。
为了解决这个问题,你可以把库拷贝到/lib下,或者修改ld.so.conf的配置,再或者临时指定:

$ export LD_LIBRARY_PATH="$(pwd)"

第一个"$"指命令提示符。该命令只在当前terminal的会话中有效。

创建一个简单的动态链接库并调用(Linux环境):

#include <iostream>
// main.cpp
int add(int, int);
int main(){
    std::cout << add(1, 2) << std::endl;
}
// add.cpp
int add(int a, int b){
    return a + b;
}

编译和链接:

$ g++ -shared -fPIC add.cpp -o libadd.so  # 将add.cpp编译并打包为动态链接库
$ g++ main.cpp -ladd -L. -o target  # -l指定动态链接库名称,省略lib和.so,-L指定动态链接库位置,.指当前目录
$ export $LD_LIBRARY_PATH="$(pwd)"  # 临时设置,在当前目录下查找动态链接库
$ ./target  # 运行target
3

Lyzen
版权声明:本站原创文章,由 Lyzen 2022-08-16发表,共计1893字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码