From: 爱编程的大丙
Author: 爱编程的大丙
原文链接:八大排序算法

一、冒泡排序

  1. 冒泡排序是稳定排序,原因是相等的值不交换,且只与相邻元素交换,不会打乱原有顺序
  2. 做法:每次都从待排序的部分挑选出一个最大值放在最后(挑选的方式是相邻元素都比较一次)
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
void bubble_sort(std::vector<int>& arr)
{
int length = arr.size() - 1;
// isSwap用于减少循环轮数,防止已经排好序后继续排序
bool isSwap = false;
// i 控制循环轮数
for (int i = 0; i < length; ++i)
{
isSwap = false;
// j不能从j=i+1开始,一次完整的内循环只能将最大的数字放在相对最后的位置,并没有对左边的数字进行排序
for (int j = 0; j < length - i; ++j)
{
if (arr[j] > arr[j + 1])
{
std::swap(arr[j], arr[j + 1]);
isSwap = true;
}
}
if (!isSwap)
{
break;
}
}
}

二、选择排序

  1. 选择排序是不稳定排序 原因是会出现长距离交换,会打乱原有的顺序
  2. 做法:每次都从待排序的部分找到最大或最小值的位置然后交换放在相对最前的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void selected_sort(std::vector<int>& arr)
{
int length = arr.size() - 1;
// i 控制循环轮数
for (int i = 0; i < length; ++i)
{
int minPos = i;
// 找到最大或最小值的位置
for (int j = i + 1; j <= length; ++j)
{
if (arr[minPos] > arr[j])
{
minPos = j;
}
}
if (minPos != i)
{
std::swap(arr[i], arr[minPos]);
}
}
}

三、插入排序

  1. 插入排序是稳定排序, 原因是我们是根据待排序数组的顺序进行排序的,且与相邻元素比较,不会出现长距离交换的情况
  2. 做法:从待排序的数组中选出第一个元素A,让这个元素和已排好序的数组的最后一个进行比较B,如果A<B,则将A后移一位覆盖数据,从此往复,知道找到小于B的元素,然后将B插入到该位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void insert_sort(std::vector<int>& arr)
{
int length = arr.size() - 1;
// i表示待排序的数组
for (int i = 1; i <= length; ++i)
{
// 使用一个临时的变量存储待排序的第一个元素,方便后续插入数据和移动覆盖数据
int temp = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > temp)
{
arr[j + 1] = arr[j];
--j;
}
arr[j + 1] = temp;
}
}

四、 希尔排序

  1. 插入排序的优化版 但并不是稳定排序 原因是:交换的元素并不是相邻的元素,存在长距离交换的情况
  2. 做法:利用插入排序趋向排好序的数组排序速度快的特点,将一个大数组分为一个个小数组对其进行插入排序,又因为每一个小数组都趋向于有序,所以排序速度很快,最后会导致整个大数组都趋向于有序,这样就能提高排序速度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void shell_sort(std::vector<int>& arr)
{
int length = arr.size();
for (int gap = length / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < length; ++i)
{
int temp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > temp)
{
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = temp;
}
}
}

五、快速排序

  1. 快速排序是不稳定排序,原因是因为交换的元素并不是相邻的元素,会出现长距离交换元素的情况
  2. 做法:快速排序采用分治法,通常在中间取一个基准数,然后将比基准数小的元素放在左边,比基准数大的元素放在右边,如果左边的元素比基准数大,右边的元素比基准数小,则交换这两个元素的位置,直到所有元素都排好序,即left>right停止,然后重新划分子数组,通过不断递归的方式,当所有的子数组都排好序后,整个数组也就排好序了
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
31
32
33
34
35
36
37
38
void quick_sort(std::vector<int>& arr)
{
if (arr.empty())
{
return;
}
quick_sort(arr, 0, arr.size() - 1);
}
void quick_sort(std::vector<int>& arr, int left, int right)
{
if (left >= right)
{
return;
}
// 基准数 取中间元素作为基准数,left + (right - left) / 2是为了防止left+right溢出
int pivotValue = arr[left + (right - left) / 2];

int begin = left - 1, end = right + 1;
while (begin < end)
{
do
{
begin++;
} while (arr[begin] < pivotValue);
do
{
end--;
} while (arr[end] > pivotValue);

if (begin < end)
{
std::swap(arr[begin], arr[end]);
}
}
quick_sort(arr, left, end);
// 下面这个的left参数不能使用begin+1,否则会导致一个元素会排序两次
quick_sort(arr, end + 1, right);
}

六、归并排序

  1. 归并排序是稳定排序, 原因是:交换的是相邻的元素且是有序的,相等的元素不会交换位置
  2. 通过不断递归将数组分到不可再分为止,最后每一个子数组都是一个元素,然后将具有同一个分支的两个子数组进行合并,合并成一个有序数组最后,当所有的子数组都合并成一个有序数组后,整个数组也就排好序了
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void merge_sort(std::vector<int>& arr)
{
if (arr.empty())
{
return;
}
std::vector<int> temp(arr.size());
merge_sort(arr, temp, 0, arr.size() - 1);
}
void merge_sort(std::vector<int>& arr, std::vector<int>& temp, int left, int right)
{
if (left >= right)
{
return;
}
int mid = left + (right - left) / 2;
// 递归分割数组
merge_sort(arr, temp, left, mid);
merge_sort(arr, temp, mid + 1, right);
// 合并同一个分支上的两个有序数组(开始时每一个子数组都是1个元素)
int i = left, j = mid + 1, index = 0;
while (i <= mid && j <= right)
{
if (arr[i] < arr[j])
{
temp[index++] = arr[i++];
}
else
{
temp[index++] = arr[j++];
}
}
// 将剩余的元素放入临时数组,只会有一个while循环执行
while (i <= mid)
{
temp[index++] = arr[i++];
}
while (j <= right)
{
temp[index++] = arr[j++];
}

// 将临时数组中的元素放入原数组
for (int k = 0; k < index; ++k)
{
arr[left + k] = temp[k];
}
}

七、堆排序

  1. 堆排序是不稳定排序,因为顶部元素和最后一个非叶子节点交换时是长距离交换,会打乱顺序
  2. 做法:先构建大根堆,通过将第一个元素和最后一个元素交换后,最后一个元素就是最大的元素,然后将剩下的元素进行再次堆排序,直到所有的元素都排好序为止
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void head_sort(std::vector<int>& arr, int length, int parent)
{
// 不需要构建满二叉树,我们默认数组就是满二叉树
int maxIndex = parent;
int leftChild = 2 * parent + 1;
int rightChild = 2 * parent + 2;
// 找到最大值的索引
if (leftChild < length && arr[leftChild] > arr[maxIndex])
{
maxIndex = leftChild;
}
if (rightChild < length && arr[rightChild] > arr[maxIndex])
{
maxIndex = rightChild;
}
// 如果最大值不是父节点,则交换父节点和最大值的值
if (maxIndex != parent)
{
std::swap(arr[parent], arr[maxIndex]);
// 继续递归调整交换后的子树,继续比较其与其子树的大小
head_sort(arr, length, maxIndex);
}
}
void head_sort(std::vector<int>& arr)
{
if (arr.empty())
{
return;
}
// 由于大根堆的性质,父节点的值大于等于子节点的值,所以我们从最后一个非叶子节点开始调整
// 逐个向上比较全部的非叶子节点,直到将最大的非叶子节点调整到根节点,此时根节点就是最大值
// 然后将根节点和最后一个叶子节点交换(将最大值放在最后),然后重新构建大根堆
// 循环这个过程,直到所有的非叶子节点都调整完毕

//arr.size()/2-1就是最后一个元素的父节点,构建大根堆
for (int i = arr.size() / 2 - 1; i >= 0; --i)
{
head_sort(arr, arr.size(), i);
}
//开始排序操作
for (int i = arr.size() - 1; i > 0; --i)
{
// 交换根节点和最后一个叶子节点,i就是最后一个叶子节点的索引
std::swap(arr[0], arr[i]);
// 重新构建大根堆,length是去掉最后一个叶子节点的长度,即去掉节点i后的长度
head_sort(arr, i, 0);
}
}

推荐链接:六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序

‘xxx’ has not been declared

原因是头文件互相包含

1
2
3
[build] D:/a-mycode/C++/DDZ-NET/client-ddz/thread/include/Communication.h:87:5: error: 'DataManager' has not been declared
[build] DataManager::getInstance()->getCommunication()->setCards(cards, last3Cards);
[build] ^~~~~~~~~~~

首先我们需要知道为什么会有头文件互相包含这种错误,假如我们有两个类,类A和类B,在类A的头文件中包含类B的头文件,这意味着,类B是先于类A出现的,即类B需要先于类A被定义。如果类B也包含了类A的头文件,这就出现问题了,因为类A要求类B先出现,而类B又要求类A先出现,这就出现了循环,使编译器无法正确解析类型定义。

具体原因是:C++预处理器会将头文件内容直接插入到包含它的文件中,当存在循环依赖时,编译器会在某一点找不到完整的类型定义

解决方案:删除其中一个头文件,防止头文件互相包含

一、懒汉模式

懒汉模式存在线程安全问题

1、双重检测锁(一坨)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#ifndef DOUBLE_CHECKED_SINGLETON_H
#define DOUBLE_CHECKED_SINGLETON_H

#include <iostream>
#include <mutex>
#include <atomic>
#include <memory>

class DoubleCheckedSingleton {
public:
// 禁用复制构造和赋值运算符
DoubleCheckedSingleton(const DoubleCheckedSingleton&) = delete;
DoubleCheckedSingleton& operator=(const DoubleCheckedSingleton&) = delete;

// 获取单例实例
static DoubleCheckedSingleton* getInstance() {
DoubleCheckedSingleton* p = instance.load(std::memory_order_acquire);
if (p == nullptr) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mutex);
p = instance.load(std::memory_order_relaxed);
if (p == nullptr) { // 第二次检查(有锁)
p = new DoubleCheckedSingleton();
instance.store(p, std::memory_order_release);
}
}
return p;
}

// 释放单例(在程序结束时调用)
static void destroyInstance() {
std::lock_guard<std::mutex> lock(mutex);
if (instance != nullptr) {
delete instance.load();
instance = nullptr;
}
}

// 析构函数
~DoubleCheckedSingleton() {
std::cout << "双重检测锁单例被销毁" << std::endl;
}

private:
// 私有构造函数,防止外部实例化
DoubleCheckedSingleton() = default;

static std::atomic<DoubleCheckedSingleton*> instance;
static std::mutex mutex;
};

// 静态成员变量初始化
std::atomic<DoubleCheckedSingleton*> DoubleCheckedSingleton::instance{nullptr};
std::mutex DoubleCheckedSingleton::mutex;

#endif // DOUBLE_CHECKED_SINGLETON_H

双重检测锁在C++中存在内存顺序问题。例如,instance = new DoubleCheckedSingleton()这行代码在编译器优化下可能会重排序为:

  1. 分配内存
  2. 将指针赋值给instance变量
  3. 构造对象

如果执行顺序变为1→2→3,在进行到2时实例已经非空,但实例还未构建,如果此时有第三个线程想要获取实例,就会返回一个空的实例!

2、静态局部变量(最推荐的方式)

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
#ifndef STATIC_LOCAL_SINGLETON_H
#define STATIC_LOCAL_SINGLETON_H

#include <iostream>

class StaticLocalSingleton {
public:
// 禁用复制构造和赋值运算符
StaticLocalSingleton(const StaticLocalSingleton&) = delete;
StaticLocalSingleton& operator=(const StaticLocalSingleton&) = delete;

// 获取单例实例
static StaticLocalSingleton& getInstance() {
// C++11保证静态局部变量的初始化是线程安全的
static StaticLocalSingleton instance;
return instance;
}

// 析构函数
~StaticLocalSingleton() {
std::cout << "静态局部变量单例被销毁" << std::endl;
}

private:
// 私有构造函数
StaticLocalSingleton() = default;
};

#endif // STATIC_LOCAL_SINGLETON_H

二、饿汉模式

在程序加载时就创建,天然线程安全,无需加锁

1、引用方式

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
31
32
33
#ifndef EAGER_SINGLETON_H
#define EAGER_SINGLETON_H

#include <iostream>

class EagerSingleton {
public:
// 禁用复制构造和赋值运算符
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;

// 获取单例实例
static EagerSingleton& getInstance() {
return instance;
}

// 析构函数
~EagerSingleton() {
std::cout << "饿汉单例被销毁" << std::endl;
}

private:
// 私有构造函数
EagerSingleton() = default;

// 静态实例(在程序加载时就创建)先创建对象,不管用与不用
static EagerSingleton instance;
};

// 静态成员变量初始化
EagerSingleton EagerSingleton::instance;

#endif // EAGER_SINGLETON_H

2、指针方式

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
31
32
33
34
35
36
37
38
39
40
41
#ifndef EAGER_SINGLETON_PTR_H
#define EAGER_SINGLETON_PTR_H

#include <iostream>

class EagerSingletonPtr {
public:
// 禁用复制构造和赋值运算符
EagerSingletonPtr(const EagerSingletonPtr&) = delete;
EagerSingletonPtr& operator=(const EagerSingletonPtr&) = delete;

// 获取单例实例
static EagerSingletonPtr* getInstance() {
return instance;
}

// 释放单例资源(解决内存泄漏问题)
static void destroyInstance() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}

// 析构函数
~EagerSingletonPtr() {
std::cout << "饿汉单例被销毁" << std::endl;
}

private:
// 私有构造函数
EagerSingletonPtr() = default;

// 静态实例(在程序加载时就创建)先创建对象,不管用与不用
static EagerSingletonPtr* instance;
};

// 静态成员变量初始化
EagerSingletonPtr* EagerSingletonPtr::instance = new EagerSingletonPtr();

#endif // EAGER_SINGLETON_PTR_H

三、补充

C++单例类初始化顺序会导致一些问题,详细可以查看这篇文章

一、安装redis

GitHub地址:https://github.com/redis/redis

下载压缩包:https://github.com/redis/redis/archive/refs/tags/7.4.2.tar.gz

1
2
3
4
tar -zxvf 7.4.2.tar.gz
cd redis-7.4.2/
make -j$(nproc)
sudo make install
  • 测试

    • 启动服务器
    1
    redis-server
    • 连接服务器
    1
    redis-cli

二、安裝hiredis

GitHub地址:https://github.com/redis/hiredis

下载压缩包:https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz

1
2
3
4
tar -zxvf v1.2.0.tar.gz
cd hiredis-1.2.0
make -j$(nproc)
sudo make install

三、安装redis plus plus

GitHub地址:https://github.com/sewenew/redis-plus-plus

下载压缩包:https://github.com/sewenew/redis-plus-plus/archive/refs/tags/1.3.13.tar.gz

1
2
3
4
5
6
tar -zxvf 1.3.13.tar.gz
cd redis-plus-plus-1.3.13
mkdir build && cd build
cmake -G "Unix Makefiles" ..
make -j$(nproc)
sudo make install

四、CMake使用

1
2
3
4
#添加Redis++
include_directories(/usr/local/include/sw/redis++)
link_directories(/usr/local/lib)
target_link_libraries(${PROJECT_NAME} redis++ hiredis)

问题:C++使用fstream打开相对路径文件发生错误,只能使用绝对路径,下面是项目结构

image-20250318231044365

如上图,原以为相对路径是相对于二进制文件(server-ddz.exe)的路径,所以使用相对路径../config/config.json,但是经过测试,还是相对路径不正确的问题。

下面的代码是查看程序运行时的路径

1
2
3
4
5
6
char buffer[256];
char* val = getcwd(buffer, sizeof(buffer));
if (val)
{
std::cout << buffer << std::endl;
}

经过测试,发现运行时的路径是这样的:/home/jianzhe/code/server-ddz,并不在bin目录,所以导致相对路径错误。

正确的相对路径应该是:config/config.json

‘xxx’ does not name a type

原因是由于头文件互相包含(循环依赖)导致的

1
2
3
/mnt/d/a-mycode/C++/DDZ-NET/server-ddz/tcp/include/TcpConnection.h:31:5: error: ‘Communication’ does not name a type
31 | Communication* m_reply = nullptr;
| ^~~~~~~~~~~~~

首先我们需要知道为什么会有头文件互相包含这种错误,假如我们有两个类,类A和类B,在类A的头文件中包含类B的头文件,这意味着,类B是先于类A出现的,即类B需要先于类A被定义。如果类B也包含了类A的头文件,这就出现问题了,因为类A要求类B先出现,而类B又要求类A先出现,这就出现了循环,使编译器无法正确解析类型定义。

具体原因是:C++预处理器会将头文件内容直接插入到包含它的文件中,当存在循环依赖时,编译器会在某一点找不到完整的类型定义

解决方案:删除其中一个头文件,防止头文件互相包含

1. 停止MySQL服务

1
sudo systemctl stop mysql

2. 卸载MySQL软件包

删除所有MySQL相关的软件包(根据你的安装版本调整包名):

1
sudo apt purge mysql-server mysql-client mysql-common mysql-server-core-* mysql-client-core-*

3. 删除残留文件和目录

手动删除MySQL的配置、数据和日志文件:

1
sudo rm -rf /etc/mysql /var/lib/mysql /var/log/mysql

4. 清理依赖和缓存

1
2
sudo apt autoremove  # 删除不再需要的依赖包
sudo apt autoclean # 清理软件包缓存

5. 检查是否彻底删除

验证是否还有残留的MySQL文件:

1
dpkg -l | grep mysql  # 检查是否有未卸载的包

如果仍有残留,手动删除相关文件

验证卸载是否成功

运行 mysql 命令:

1
mysql

提示 Command 'mysql' not found,则表示卸载成功。

记一次在windows+cmake+MinGW环境下使用openssl添加库文件错误的经历

一、库文件介绍

OpenSSL下载路径:https://slproweb.com/products/Win32OpenSSL.html

image-20250314170922728

安装后的库文件是这样的

  • bin目录

image-20250314171205286

  • lib目录

image-20250314171309001

可以看到,这是一个使用MSVC编译套件编译的库

二、遇到的问题

根据经验,添加库文件通常需要”掐头去尾”——即去掉lib前缀和.lib后缀。例如,对于libcrypto.lib,我尝试这样添加:

1
target_link_libraries(${PROJECT_NAME} crypto)

结果报错:cannot find -lcrypto

后面我尝试指定完整文件名

1
target_link_libraries(${PROJECT_NAME} libcrypto.lib)

依然报错:cannot find -lcrypto

三、问题分析

3.1 不同系统的库命名约定

  • MSVC (Windows): 库通常命名为crypto.liblibcrypto.lib
  • GCC/MinGW/Linux: 库通常命名为libcrypto.solibcrypto.a

3.2、CMake的库名处理机制

CMake会根据使用的生成器和编译器自动处理库名转换:

  1. 使用不带前缀和后缀的名称(crypto):
    • MSVC生成器会查找crypto.lib
    • GCC/MinGW生成器会查找libcrypto.alibcrypto.dll.a
  2. 使用带lib前缀的名称(libcrypto):
    • MinGW会查找libcrypto.alibcrypto.dll.a
    • MSVC会查找libcrypto.lib
  3. 使用完整文件名带扩展名(libcrypto.lib):
    • MinGW会错误地查找liblibcrypto.lib.aliblibcrypto.lib.dll.a

3.3、问题根源

跨工具链使用导致的命名不匹配:我使用MinGW编译套件,但链接了MSVC构建的OpenSSL库。

  1. 当使用crypto引用时:

    • MinGW尝试查找libcrypto.alibcrypto.dll.a,但实际文件是libcrypto.lib

    • “掐头去尾”规则只适用于同一工具链

  2. 当使用libcrypto.lib引用时:

    • MinGW尝试查找liblibcrypto.lib.aliblibcrypto.lib.dll.a,这显然不存在

四、解决方案

  1. 方案一:使用不带扩展名的库名

    1
    target_link_libraries(${PROJECT_NAME} Qt5::Core libcrypto)

    MinGW会正确地查找libcrypto.alibcrypto.dll.a

  2. 方案二:使用绝对路径直接指定库文件

    1
    target_link_libraries(${PROJECT_NAME} ${OPENSSL_INSTALL_DIR}/lib/libcrypto.lib)

总结:当使用与库构建时不同的编译器工具链时,库命名约定可能不匹配,需要特别注意库的引用方式。

cannot found -lxxx动态库

找不到动态库文件,排查方向

  1. 在当前CMakeLists.txt文件下使用link_directories指定动态库路径

    1
    2
    set(PROTOBUF_PATH "D:/protobuf-cpp-3.21.12")
    link_directories(${PROTOBUF_PATH}/lib)
  2. 如果还是找不到,将link_directories的实现添加到顶层CMakeLists.txt文件中

    原因:你在子目录serialize中添加的上述的动态库路径,但还是报错,可能是其他同级子目录或者上层目录使用了serialize目录的相关代码,因此其他目录也需要链接该动态库,但是由于CMake添加的link_directories只在该目录及其子目录下生效,在其他同级目录和上层目录是不生效的,所以其他目录也会报出错误:cannot found -lxxx

  3. 确保库文件名正确,详细信息可以查看这篇文章:如何在CMake中正确的添加库文件:MSVC与MinGW混用陷阱

undefined reference to ‘xxx’函数

1
2
3
4
5
6
#情况1,库链接顺序不正确导致,明显特征是报错的函数是我们自己实现的函数,而不是底层函数
[build] D:/a-mycode/C++/DDZ-NET/client-ddz/window/login.cpp:186: undefined reference to `DataManager::getInstance()'
[build] D:/a-mycode/C++/DDZ-NET/client-ddz/window/login.cpp:187: undefined reference to `DataManager::setIp(QByteArray const&)'
#情况2,没有找到动态库文件,明显特征是报错的函数是底层函数,而不是我们自己实现的函数
[build] tcp/libtcp.a(TcpSocket.cpp.obj): In function `TcpSocket::TcpSocket(QObject*)':
[build] D:/a-mycode/C++/DDZ-NET/client-ddz/tcp/TcpSocket.cpp:11: undefined reference to `__imp_WSAStartup'

**原因是库文件找不到,**排查方向:

  1. 确保不是因为函数没有实现导致的

  2. 确保target_link_libraries函数设置的要链接的对象<target>

  3. 确保库链接顺序正确,需要在target_link_libraries修改摆放位置,左边库的依赖右边库

    例如,A库中用到了B库中的函数,这时就是A库依赖于B库,A库应该在左边,B库在A库右边,如果B库在A库左边,就会报undefined reference to 错误,而且有个明显特征是报错的函数是我们自己实现的函数,而不是底层函数

  4. 确保在target_link_libraries已经添加了导入库

  5. 确保target_link_libraries的lib库或dll.a导入库文件能被找到,使用绝对路径试试

  6. 确保dll或so动态库文件能找到,试试添加到环境变量或添加到build目录下(与exe文件同级目录)

0%