今天是学习《重学C++ 重构你的C++知识体系》 的第四天,重点在 C++进阶编程。

五、C++进阶编程

(一)STL标准库简介

STL 算法是泛型的(generic),不需要创建特定的数据结构和对象。

1. 容器

序列式容器:元素都是可以排序的。比如:vector、list、deque等序列式容器,而stack、queue、priority_queue则是容器适配器。

关联式容器:每个数据元素都是由一个key和value组成。比如:set、multiset、map、multimap。

(二)仿函数(functor)

仿函数一般不会单独使用,主要是为了搭配STL算法使用。

在C++中,仿函数(Functor)是一种行为类似于函数的对象,它可以被像函数一样调用。仿函数是通过重载函数调用操作符(operator())来实现的,使得对象可以像函数一样被调用。

仿函数常用于算法和容器中,可以用于自定义排序、比较、转换等操作。它提供了更灵活的方式来定制函数行为,比普通函数更具扩展性。

下面是一个简单的示例,展示了如何定义和使用一个仿函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

// 定义一个仿函数
struct Adder {
int operator()(int a, int b) {
return a + b;
}
};

int main() {
Adder add;

int result = add(3, 4); // 调用仿函数
std::cout << "Result: " << result << std::endl;

return 0;
}

在上述示例中,我们定义了一个名为Adder的仿函数结构体。它重载了函数调用操作符operator(),接受两个int类型的参数,并返回它们的和。

main函数中,我们创建了一个Adder对象add,然后通过将其当作函数一样调用,传入参数3和4,得到了结果7。

通过仿函数,我们可以像使用函数一样使用add对象,这给了我们更大的灵活性。我们可以在仿函数中实现更复杂的逻辑,甚至可以在构造函数中存储一些状态,使得仿函数更加强大和可定制。

需要注意的是,仿函数可以具有不同的形式,可以是结构体、类、函数对象等,只要它们重载了函数调用操作符operator()即可。这使得我们可以根据具体的需求选择不同的仿函数形式。

(三)STL算法

STL中算法大致分为四类:包含于

    1. 非可变序列算法:指不直接修改其所操作的容器内容的
    1. 可变序列算法:指可以修改它们所操作的容器内容的算法
    1. 排序算法:包括对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作
    1. 数值算法:对容器内容进行数值计算。

1. std::sort 对容器中的元素进行排序。

std::vector<int> numbers = {4,2,7,1,5};

std::sort(numbers.begin(),numbers.end());

for(const auto& number : numbers) {
    std::cout << number << " ";
}

std::cout << std::endl;

2. std::find 在容器中查找指定的元素

std::vector<int> numbers = {4,2,7,1,5};

auto it = std::find(numbers.begin(),numbers.end(),7);
if(it != numbers.end()) {
    std::cout << "Found:" << *it << std::endl;
}else{
    std::cout << "Not found" << std::endl;
}

3. std::transform 对容器中的元素进行转换操作

std::vector<int> numbers = {4,2,7,1,5};
std::vector<int> doubledNumbers;

std::transform(numbers.begin,numbers.end(),std::back_inserter(    doubledNumbers),[](int number)) {
    return number * 2;
}

for (const auto& number : doubledNumbers) {
    std::cout << number << " ";
}
std::cout << std::endl;

4. std::accumulate 对容器中的元素进行累加

std::vector<int> numbers = {1,2,3,4,5};

int sum = std::accumulate(numbers.begin(),numbers.end().0);

5. std::copy 将一个容器的元素复制到另一个容器中

std::vector<int> source = {1,2,3,4,5};
std::vector<int> destination(source.size());

std::copy(source.begin(),source.end(),destination.begin());

6. std::count 计算容器中某个值出现的次数

std::vector<int> source = {1,2,3,4,5};

int count = std::count(numbers.begin,numbers.end,2)

7. std::replace 将容器中的某个值替换为另一个值

std::vector<int> source = {1,2,3,4,5};
std::replace(numbers.begin(),numbers.end(),2,10);

8. std::reverse 反转容器中的元素顺序

std::vector<int> source = {1,2,3,4,5};
std::reverse(numbers.begin(), numbers.end());

(四)Lambda表达式

Lambda表达式是C++11引入的一种匿名函数语法,它允许我们在需要函数的地方直接定义函数,而无需显式地定义一个函数对象或函数指针。

Lambda表达式的基本语法如下:

1
2
3
[capture-list](parameters) -> return-type {
// 函数体
}

其中,capture-list是捕获列表,用于指定Lambda表达式中的外部变量的访问方式。parameters是函数的参数列表,可以为空。return-type是函数的返回类型,可以省略。

int main() {
    
    int x = 5;
    int y = 2;

    auto multiplyAndPrint = [x,&y](int num) {
        y++; // 修改外部变量y的值
        std::cout << "Result:" << num * x * y << std::endl;
    }

    multiplyAndPrint(3); // 调用Lambda表达式

    return 0;
}

(四)迭代器(iterator)

iterator 是一种 smart pointer ,用于访问顺序容器和关联容器中的元素,相当于 容器和操纵容器算法 之间的中介。

[ChatGPT] 在C++中,迭代器(iterator)是一种用于遍历和访问容器元素的对象。迭代器提供了一种统一的方式来访问容器中的元素,无论容器的类型是什么。迭代器可以被视为指向容器元素的指针,它可以移动到容器中的下一个元素,或者反向移动到前一个元素。

C++标准库中的容器(如vector、list、set等)都提供了迭代器,可以通过迭代器来遍历容器中的元素。迭代器具有类似指针的行为,可以使用解引用操作符(*)来访问元素的值,使用递增操作符(++)来移动到下一个元素。

以下是一些常见的迭代器操作示例:

  1. 使用迭代器遍历容器:
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用迭代器遍历容器
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 使用迭代器修改容器元素:
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用迭代器修改容器元素
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        *it *= 2;
    }

    // 输出修改后的容器元素
    for (const auto& number : numbers) {
        std::cout << number << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 使用迭代器插入和删除容器元素:
#include <iostream>
#include <list>

int main() {
    std::list<int> numbers = {1, 2, 3, 4, 5};

    // 使用迭代器在指定位置插入元素
    auto it = numbers.begin();
    ++it;  // 移动到第二个元素的位置
    numbers.insert(it, 10);

    // 使用迭代器删除指定位置的元素
    it = numbers.begin();
    ++it;  // 移动到第二个元素的位置
    numbers.erase(it);

    // 输出修改后的容器元素
    for (const auto& number : numbers) {
        std::cout << number << " ";
    }
    std::cout << std::endl;

    return 0;
}

上述示例展示了使用迭代器遍历容器、修改容器元素以及插入和删除容器元素的常见操作。迭代器提供了一种通用的方式来访问和操作容器中的元素,使得我们可以更加灵活地处理容器数据。

(五)Boost库

Boost是一个由C++社区开发和维护的开源库集合,它提供了许多高质量、可移植且广泛使用的C++库。Boost库扩展了C++标准库的功能,提供了一系列的工具和组件,用于各种领域的开发,包括但不限于网络编程、多线程、图形图像处理、数学计算、文件系统操作等。

Boost库的特点和优势包括:

  1. 高质量和可移植性:Boost库经过广泛的测试和验证,具有高质量的代码和可移植性,可以在不同的操作系统和编译器上使用。

  2. 广泛的功能:Boost库提供了丰富的功能和组件,涵盖了许多领域的需求,如智能指针、正则表达式、日期时间处理、容器、算法、并发编程等。

  3. 增强的性能和效率:Boost库在设计和实现上注重性能和效率,提供了许多优化的算法和数据结构,以提升程序的执行效率。

  4. 对C++标准的扩展:Boost库在C++标准库的基础上进行了扩展,填补了一些标准库中的空白或不足之处,为C++开发者提供了更多的选择和工具。

以下是一些常用的Boost库模块:

  • Boost.Asio:网络和异步编程库,提供了网络编程和异步操作的支持。
  • Boost.Thread:多线程编程库,提供了线程、互斥锁、条件变量等多线程相关的功能。
  • Boost.Filesystem:文件系统操作库,提供了对文件和目录的操作和管理。
  • Boost.Regex:正则表达式库,提供了对字符串的模式匹配和替换功能。
  • Boost.Serialization:序列化库,提供了对象的序列化和反序列化功能。
  • Boost.Math:数学计算库,提供了许多数学函数和算法。

(六)多线程

std::thread是C++标准库中提供的用于多线程编程的类。它允许我们创建和管理线程,执行并发的任务。std::thread提供了一种方便的方式来启动新线程,并在不同线程之间进行数据共享和通信。

以下是std::thread的基本用法示例:

#include <iostream>
#include <thread>

// 线程函数
void threadFunction(int n) {
    std::cout << "Hello from thread! Number: " << n << std::endl;
}

int main() {
    // 创建新线程并启动
    std::thread t(threadFunction, 42);

    // 主线程继续执行其他任务

    // 等待新线程执行完毕
    t.join();

    return 0;
}

在上述示例中,我们定义了一个线程函数threadFunction,它接受一个整数参数并打印一条消息。在主函数中,我们创建了一个新线程t,并将线程函数和参数传递给std::thread的构造函数。然后,主线程继续执行其他任务,而新线程在后台执行。最后,我们使用t.join()等待新线程执行完毕。

通过std::thread,我们可以创建多个线程来执行并发的任务。线程之间可以共享数据,但需要注意对共享数据的访问进行同步,以避免竞态条件和数据竞争。

以下是一个使用std::thread的例子,模拟了一个简单的并发任务处理场景:

#include <iostream>
#include <thread>
#include <vector>

// 并发任务函数
void processTask(int taskId) {
    std::cout << "Processing task: " << taskId << std::endl;
    // 执行任务的代码
}

int main() {
    std::vector<std::thread> threads;

    // 创建并启动多个线程来处理任务
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(processTask, i);
    }

    // 等待所有线程执行完毕
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

在这个例子中,我们使用std::vectorstd::thread来存储多个线程对象。然后,通过循环创建并启动多个线程,每个线程处理一个任务。最后,使用循环和t.join()等待所有线程执行完毕。

std::thread提供了一种方便的方式来创建和管理线程。它是C++多线程编程中的重要工具,可以帮助我们实现并发的任务处理和线程间的协作。同时,需要注意对共享数据的访问进行同步,以确保线程安全性。

在C++中,t.join()是std::thread类的成员函数,用于等待线程的执行完成。当调用t.join()时,当前线程会被阻塞,直到被调用的线程(在这个例子中是t所代表的线程)执行完毕。

具体来说,t.join()的作用是将当前线程(通常是主线程)与被调用的线程进行同步。它会阻塞当前线程,直到被调用的线程执行完毕,然后当前线程才会继续执行后续的代码。

在上述例子中,我们使用t.join()来等待所有的子线程执行完毕。通过循环遍历线程对象的vector,并对每个线程对象调用t.join(),我们确保主线程在所有子线程执行完毕之前不会退出。

需要注意的是,一旦调用了t.join(),就不能再次使用该线程对象执行其他任务。如果希望在主线程继续执行其他任务而不阻塞,可以使用t.detach()函数将线程对象分离,使其在后台独立执行。

总之,t.join()是用于等待被调用线程执行完毕的函数,它在多线程编程中用于同步和协调线程的执行顺序。

章节延伸题思考

(一)给我介绍一下 vector、list、deque、stack、queue、priority_queue、set、multiset、map、multimap 和常用接口使用方法,用简单的代码举例。

  1. vector : 动态数组,支持随机访问、在尾部进行插入和删除操作

    std::vector vec = {1,2,3};
    vec.push_back(4); // 在尾部插入元素
    vec.pop_bacl(); // 删除尾部元素
    int value = vac[1]; // 随机访问元素

  2. list : 双向链表,支持在任何位置进行插入和删除操作,但不支持随机访问。

    std::list lst = {1,2,3};
    lst.push_back(4); // 在尾部插入元素
    lst.pop_front(); // 删除头部元素
    int value = lst.front(); // 访问头部元素

  3. deque : 双端队列,支持随机访问、在头部和尾部进行插入和删除操作。

    std::deque deq = {1,2,3}
    deq.push_back(4); // 在尾部插入元素
    deq.push_front(0); // 在头部插入元素
    int value = deq[1]; // 随机访问元素

  4. stack : 后进先出的数据结构,只允许在顶部进行插入和删除操作

    std::stack stk;
    stk.push(1); // 压入元素
    int value = stk.top(); // 访问顶部元素
    stk.pop(); // 弹出顶部元素

  5. queue : 先进先出的数据结构,只允许在尾部插入元素,在头部删除元素

    std::queue que;
    que.push(1); // 入队元素
    int value = que.front(); // 访问队首元素
    que.pop();

  6. priority_queue : 优先队列,元素按照优先级顺序存储。默认清空下,优先级由元素的 < 操作符确定。

    std::priority_queue pq;
    pq.push(1);
    pq.push(3);
    pq.push(2);
    int highest_priority_value = pq.top(); // 访问优先级最高的元素(3)

  7. set : 关联容器,用于存储唯一元素,元素按照排序顺序存储

std::set s = {1,2,3};
s.insert(4); // 插入元素
s.erase(1); // 删除元素

  1. multiset: 和set类似,但允许存储重复元素

std::multiset ms={1,2,2,3}
ms.insert(2); // 插入重复元素

  1. map : map是一种关联容器,键是唯一的,按照排序顺序存储。

    #include
    #include

    int main() {

     std::map<int, std::string> myMap;
    
     myMap.insert({3, "three"});
     myMap.insert({1, "one"});
     myMap.insert({2, "two"});
    
     for (const auto& pair : myMap) {
         std::cout << pair.first << ": " << pair.second << std::endl;
     }
    
     return 0;
    

    }

  2. multimap

    std::multimap<int, std::string> myMultimap;

    // 插入元素
    myMultimap.insert({3, “three”});
    myMultimap.insert({1, “one”});
    myMultimap.insert({2, “two”});
    myMultimap.insert({1, “one”});

    // 遍历元素
    for(const auto& pair : myMultimap) {

    std::cout << pair.first << ":" << pair.second << std::endl;
    

    }

    // 查找元素
    auto range = myMultimap.equal_range(3)
    for (auto it = range.first; it != range.second; ++it) {

    std::cout << it->first << ":" << it->second << std::endl;
    

    }

    // 删除元素
    myMultimap.erase(2);

    // 按断元素是否存在
    if(myMultimap.find(1) != myMultimap.end()) {

    std::cout << "Key 1 is found" << std::endl;
    

    }else{

    std::cout << "Key 1 is not found" << std::endl;
    

    }

(二)list和deque有什么区别? 使用的时候有什么区别?

省流:可以理解成 deque = list + vector 。 也就是集成了 list的频繁插入的效率,也实现了vector的随机访问

可以理解成std::dequestd::liststd::vector的结合体,它同时具有std::list的频繁插入和删除的效率,以及std::vector的随机访问的效率。

在内部实现上,std::deque通常被实现为一个由多个缓冲区组成的序列,每个缓冲区都是一个连续的存储区域,元素按照块的形式存储。这种设计使得在队头和队尾进行插入和删除操作的效率较高,同时支持随机访问。

std::list相比,std::deque具有更好的随机访问性能,因为它支持通过下标访问元素,而不需要遍历整个容器。与std::vector相比,std::deque具有更好的插入和删除性能,特别是在队头和队尾进行操作时,因为它不需要移动大量的元素。

因此,如果需要同时具备频繁插入和删除的效率以及随机访问的效率,可以选择使用std::deque。它是一个非常灵活和高效的容器,适用于各种场景。

(三)使用priority_queue时,怎么给入队对象设置优先级?

(四)C++中set中的元素是有顺序吗? 元素怎么保证自己的顺序?

省流:初始化set的时候传入一个排序operator

在C++中,std::set是一个有序的容器,它按照元素的键值进行排序,并且不允许重复的元素。

std::set中的元素是根据元素的键值来进行排序的,默认情况下,元素会按照升序进行排序。如果需要按照自定义的排序规则来排序元素,可以通过自定义比较函数或使用自定义的比较类来实现。

下面是一个示例,展示了如何使用std::set来保持元素的顺序:

#include <iostream>
#include <set>

// 自定义比较函数,按照元素的长度从小到大排序
struct Compare {
    bool operator()(const std::string& a, const std::string& b) const {
        return a.length() < b.length();
    }
};

int main() {
    // 使用默认的排序规则
    std::set<int> mySet1;

    mySet1.insert(3);
    mySet1.insert(1);
    mySet1.insert(2);

    for (const auto& num : mySet1) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 使用自定义的排序规则
    std::set<std::string, Compare> mySet2;

    mySet2.insert("abc");
    mySet2.insert("de");
    mySet2.insert("f");

    for (const auto& str : mySet2) {
        std::cout << str << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上述示例中,我们首先创建了一个默认的std::set对象mySet1,并将元素3、1和2依次插入。由于默认的排序规则是升序,所以输出的结果为1 2 3。

接着,我们定义了一个自定义的比较类Compare,它重载了函数调用运算符(),按照元素的长度从小到大进行排序。然后我们创建了另一个std::set对象mySet2,并通过比较类Compare来设置排序规则。我们将字符串”abc”、”de”和”f”依次插入,由于自定义的排序规则是按照长度从小到大排序,所以输出的结果为”f de abc”。

通过自定义比较函数或比较类,我们可以根据自己的需求来设置元素的排序规则,从而保证std::set中元素的顺序。

(五)C++中map中的元素是有顺序吗? 元素怎么保证自己的顺序?

std::map中的元素是根据键值来进行排序的,默认情况下,元素会按照键值的升序进行排序。如果需要按照自定义的排序规则来排序元素,可以通过自定义比较函数或使用自定义的比较类来实现。

下面是一个示例,展示了如何使用std::map来保持元素的顺序:

#include <iostream>
#include <map>

// 自定义比较函数,按照元素的键值从大到小排序
struct Compare {
    bool operator()(int a, int b) const {
        return a > b;
    }
};

int main() {
    // 使用默认的排序规则
    std::map<int, std::string> myMap1;

    myMap1.insert({3, "three"});
    myMap1.insert({1, "one"});
    myMap1.insert({2, "two"});

    for (const auto& pair : myMap1) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    // 使用自定义的排序规则
    std::map<int, std::string, Compare> myMap2;

    myMap2.insert({3, "three"});
    myMap2.insert({1, "one"});
    myMap2.insert({2, "two"});

    for (const auto& pair : myMap2) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

在上述示例中,我们首先创建了一个默认的std::map对象myMap1,并将键值对{3, “three”}、{1, “one”}和{2, “two”}依次插入。由于默认的排序规则是按照键值的升序进行排序,所以输出的结果为1: one、2: two、3: three。

接着,我们定义了一个自定义的比较类Compare,它重载了函数调用运算符(),按照键值的降序进行排序。然后我们创建了另一个std::map对象myMap2,并通过比较类Compare来设置排序规则。我们将键值对{3, “three”}、{1, “one”}和{2, “two”}

(六)仿函数在平时开发都应用在什么情况下,举例说明

iOS中的和C++仿函数类似的代码:

struct Student {
    let name: String
    let age: Int
}

let students = [
    Student(name: "Alice", age: 20),
    Student(name: "Bob", age: 18),
    Student(name: "Charlie", age: 22)
]

let sortedStudents = students.sorted { (student1, student2) -> Bool in
    return student1.age < student2.age
}

for student in sortedStudents {
    print("\(student.name) \(student.age)")

}

C++中的仿函数就类似iOS中的sort operator算子。

[ChatGPT] 仿函数在平时的开发中有很多应用场景,以下是一些常见的示例:

  1. 自定义排序规则:在使用算法或容器进行排序时,可以通过自定义仿函数来指定排序的规则。例如,可以定义一个仿函数来实现按照字符串长度进行排序:

    #include
    #include
    #include
    #include

    struct StringLengthComparator {

     bool operator()(const std::string& str1, const std::string& str2) {
         return str1.length() < str2.length();
     }
    

    };

    int main() {

     std::vector<std::string> strings = {"apple", "banana", "cherry", "date"};
    
     std::sort(strings.begin(), strings.end(), StringLengthComparator());
    
     for (const auto& str : strings) {
         std::cout << str << " ";
     }
     std::cout << std::endl;
    
     return 0;
    

    }

  1. 转换操作:仿函数可以用于对容器中的元素进行转换操作。例如,可以定义一个仿函数来将字符串转换为大写:
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <cctype>

struct ToUpper {
    std::string operator()(const std::string& str) {
        std::string result = str;
        std::transform(result.begin(), result.end(), result.begin(), [](unsigned     char c) {
            return std::toupper(c);
        });
        return result;
    }
};

int main() {
    std::vector<std::string> strings = {"apple", "banana", "cherry", "date"};

    std::transform(strings.begin(), strings.end(), strings.begin(), ToUpper());

    for (const auto& str : strings) {
        std::cout << str << " ";
    }
    std::cout << std::endl;

    return 0;
}
  1. 自定义比较规则:仿函数可以用于自定义容器中元素的比较规则。例如,可以定义一个仿函数来实现按照学生年龄进行比较:
#include <iostream>
#include <set>
#include <string>

struct Student {
    std::string name;
    int age;

    Student(const std::string& n, int a) : name(n), age(a) {}

    bool operator<(const Student& other) const {
        return age < other.age;
    }
};

int main() {
    std::set<Student> students;

    students.insert(Student("Alice", 20));
    students.insert(Student("Bob", 18));
    students.insert(Student("Charlie", 22));

    for (const auto& student : students) {
        std::cout << student.name << " " << student.age << std::endl;
    }

    return 0;
}

通过自定义的仿函数,我们可以根据具体的需求来定制函数行为,使得代码更加灵活和可扩展。仿函数可以应用于各种算法、容器和数据结构中,根据需要来实现不同的功能。

(七)使用C++进行开发时,有哪些业务可能会使用到多线程?

在C++开发中,有许多业务场景可能会使用到多线程来提高并发性和响应性。以下是一些常见的业务场景:

  1. 并行计算:当需要同时处理大量计算密集型任务时,可以使用多线程来并行执行这些任务,以提高计算性能。例如,图像处理、数据分析、科学计算等。

  2. 网络编程:在网络应用中,多线程可以用于处理并发的客户端请求,以提供更好的响应速度和吞吐量。例如,Web服务器、聊天应用、下载管理器等。

  3. 数据库操作:当需要对数据库进行大量读写操作时,可以使用多线程来提高数据库访问的效率。例如,批量数据导入、数据备份、并发查询等。

  4. 图形界面应用:在图形界面应用中,多线程可以用于响应用户交互事件,同时处理后台任务,以保持界面的流畅性和响应性。例如,图形编辑器、游戏应用、音视频播放器等。

  5. 并发任务处理:当需要同时处理多个独立的任务时,可以使用多线程来并行执行这些任务,以提高整体的处理效率。例如,任务调度器、批量处理任务、并行算法等。

  6. 数据采集与处理:在数据采集和处理过程中,多线程可以用于同时从不同的数据源获取数据,并进行并行处理和分析。例如,传感器数据采集、实时监控系统等。

需要注意的是,在使用多线程时,需要考虑线程安全性、数据共享与通信、同步机制等问题,以避免竞态条件、死锁和数据竞争等多线程问题。合理地使用多线程可以提高程序的性能和响应性,但也需要仔细设计和测试,以确保正确和可靠的多线程行为。

(八)使用C++进行开发处理数据库操作时,如何在非主线程IO避免卡住主线程?举例说明

[ChatGPT] 在C++中处理数据库操作时,可以使用多线程来避免阻塞主线程。通过将数据库操作放在一个单独的线程中执行,可以确保主线程的运行不会被数据库操作所阻塞,从而提高程序的响应性。

以下是一个简单的示例,展示了如何在非主线程中执行数据库操作,以避免卡住主线程:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <vector>

// 模拟数据库操作
void databaseOperation(int id) {
    // 模拟数据库操作的耗时
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Database operation completed: " << id << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    std::mutex mtx;
    std::condition_variable cv;

    // 创建并启动多个线程来执行数据库操作
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([&]() {
            std::unique_lock<std::mutex> lock(mtx);
            // 等待主线程通知开始执行数据库操作
            cv.wait(lock);

            // 执行数据库操作
            databaseOperation(i);
        });
    }

    // 通知所有线程开始执行数据库操作
    cv.notify_all();

    // 主线程继续执行其他任务

    // 等待所有线程执行完毕
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

在这个示例中,我们使用了一个条件变量(std::condition_variable)来实现线程间的同步。首先,我们创建了一个std::mutexstd::condition_variable对象,用于线程间的同步和通信。

然后,我们创建了多个线程,并将每个线程的执行逻辑封装在lambda表达式中。在每个线程的逻辑中,我们使用std::unique_lock对互斥锁进行加锁,并调用cv.wait(lock)来等待主线程的通知。

在主线程中,我们调用cv.notify_all()来通知所有线程开始执行数据库操作。然后,主线程可以继续执行其他任务,而数据库操作将在各个线程中并行执行。

通过使用条件变量和互斥锁,我们可以实现主线程与数据库操作线程之间的同步,从而避免阻塞主线程。这样,主线程可以继续执行其他任务,而数据库操作线程可以在后台执行,提高程序的并发性和响应性。

(九)使用C++进行开发时,我如何让一个方法一定只能在主线程调用?

[ChatGPT] 在C++中,可以使用std::this_thread::get_id()函数来获取当前线程的ID。通过比较当前线程的ID与主线程的ID,可以确定一个方法是否在主线程中被调用。

以下是一个示例,展示了如何确保一个方法只能在主线程中调用:

#include <iostream>
#include <thread>

// 主线程ID
std::thread::id mainThreadId;

// 初始化主线程ID
void initMainThreadId() {
    mainThreadId = std::this_thread::get_id();
}

// 检查当前线程是否为主线程
bool isMainThread() {
    return std::this_thread::get_id() == mainThreadId;
}

// 只能在主线程中调用的方法
void mainThreadOnlyMethod() {
    if (isMainThread()) {
        std::cout << "Method called from main thread" << std::endl;
    } else {
        std::cout << "Method can only be called from main thread" << std::endl;
    }
}

int main() {
    // 初始化主线程ID
    initMainThreadId();

    // 在主线程中调用方法
    mainThreadOnlyMethod();

    // 在子线程中调用方法
    std::thread t([&]() {
        mainThreadOnlyMethod();
    });
    t.join();

    return 0;
}

在这个示例中,我们定义了一个initMainThreadId()函数来初始化主线程的ID,并定义了一个isMainThread()函数来检查当前线程是否为主线程。

然后,我们定义了一个mainThreadOnlyMethod()方法,该方法会检查当前线程是否为主线程,并根据结果输出相应的信息。

main()函数中,我们首先调用initMainThreadId()来初始化主线程的ID。然后,我们在主线程中调用mainThreadOnlyMethod()方法,由于是在主线程中调用,所以输出的信息会显示方法被主线程调用。

接着,我们创建了一个子线程,并在子线程中调用mainThreadOnlyMethod()方法。由于子线程不是主线程,所以输出的信息会显示方法只能在主线程中调用。

通过使用线程ID来判断当前线程是否为主线程,我们可以在方法内部进行条件判断,确保方法只能在主线程中调用。这样可以提高代码的可靠性和安全性,避免在非主线程中调用不适合的方法。

(十)std::vector emplace_back 是什么意思?

[ChatGPT] std::vector::emplace_back()是C++标准库中std::vector类的成员函数之一,用于在容器的末尾插入一个新元素。与push_back()函数不同,emplace_back()函数直接在容器中构造新元素,而不是通过拷贝或移动已有的对象。

具体来说,emplace_back()函数接受可变数量的参数,并使用这些参数在容器的末尾构造一个新元素。这些参数将被传递给元素类型的构造函数,用于初始化新元素的成员变量。

以下是一个示例,展示了emplace_back()函数的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <vector>

class MyClass {
public:
MyClass(int value) : m_value(value) {
std::cout << "MyClass constructor called with value: " << m_value << std::endl;
}

private:
int m_value;
};

int main() {
std::vector<MyClass> myVector;

// 使用 emplace_back() 在容器末尾插入新元素
myVector.emplace_back(42);

return 0;
}

在这个示例中,我们定义了一个名为MyClass的类,它有一个带有一个整数参数的构造函数。在main()函数中,我们创建了一个std::vector<MyClass>容器,并使用emplace_back()函数在容器末尾插入一个新元素。

当调用emplace_back(42)时,emplace_back()函数会直接在容器中构造一个新的MyClass对象,使用参数42来初始化该对象的成员变量。在输出中,我们可以看到构造函数被调用,并显示了传递的参数值。

相比于push_back()函数,emplace_back()函数避免了额外的拷贝或移动操作,直接在容器中构造新元素,因此可以提高代码的效率。它特别适用于构造函数参数较多或复杂的情况。

(十一)std::vector emplace_back 和 push_back 具体有什么不同?详细说明

[ChatGPT] std::vector::emplace_back()std::vector::push_back()都是std::vector类的成员函数,用于在容器的末尾插入新元素。它们的主要区别在于元素的构造方式和参数传递方式。

  1. 构造方式:

    • emplace_back():在容器的末尾直接构造一个新元素,而不是通过拷贝或移动已有的对象。
    • push_back():将已有的对象拷贝或移动到容器的末尾,作为新元素。
  2. 参数传递方式:

    • emplace_back():接受可变数量的参数,并将这些参数传递给元素类型的构造函数,用于初始化新元素的成员变量。
    • push_back():接受一个参数,该参数是元素类型的实例,它会被拷贝或移动到容器中。

下面是一个更详细的示例,展示了emplace_back()push_back()的不同之处:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int value) : m_value(value) {
        std::cout << "MyClass constructor called with value: " << m_value <<     std::endl;
    }

    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "MyClass copy constructor called with value: " << m_value <<     std::endl;
    }

    MyClass(MyClass&& other) noexcept : m_value(other.m_value) {
        std::cout << "MyClass move constructor called with value: " << m_value <<     std::endl;
    }

private:
    int m_value;
};

int main() {
    std::vector<MyClass> myVector;

    // 使用 emplace_back() 在容器末尾插入新元素
    myVector.emplace_back(42);

    std::cout << "-----------------------------" << std::endl;

    // 使用 push_back() 在容器末尾插入新元素
    MyClass obj(24);
    myVector.push_back(obj);

    return 0;
}

在这个示例中,我们定义了一个名为MyClass的类,它有一个带有一个整数参数的构造函数,并且还定义了拷贝构造函数和移动构造函数。

首先,我们使用emplace_back(42)来调用emplace_back()函数,在容器中直接构造一个新的MyClass对象,并将参数42传递给构造函数。在输出中,我们可以看到只调用了构造函数。

然后,我们使用push_back(obj)来调用push_back()函数,将已有的MyClass对象obj拷贝到容器中。在输出中,我们可以看到调用了拷贝构造函数。

通过emplace_back()函数的使用,我们直接在容器中构造新元素,避免了额外的拷贝或移动操作,从而提高了效率。而push_back()函数则需要通过拷贝或移动已有的对象来插入新元素。

总结起来,emplace_back()适用于直接在容器中构造新元素,并且参数数量较多或构造过程较复杂的情况。而push_back()适用于已有对象的拷贝或移动操作,或者在构造函数参数较少且简单的情况下。

(十二)在C++中,一般什么样的操作会放到子线程执行,什么样的操作一定要在主线程执行?

在C++中,一般将以下类型的操作放到子线程中执行:

  1. 长时间运行的任务:如果某个操作需要花费较长时间来完成,例如大量计算、网络请求、磁盘读写等,可以将这些操作放到子线程中执行,以避免阻塞主线程,保持程序的响应性。

  2. 并行计算:当需要同时处理多个计算密集型任务时,可以将这些任务分配给不同的子线程进行并行计算,以提高整体的计算性能。

  3. IO操作:对于一些需要进行IO操作的任务,例如文件读写、网络通信等,可以将这些操作放到子线程中执行,以避免阻塞主线程,保持程序的流畅性。

  4. 后台任务:一些后台任务,例如数据处理、数据同步、定时任务等,可以放到子线程中执行,以不影响主线程的正常运行。

另外,有一些操作一定要在主线程中执行,包括但不限于:

  1. 用户界面操作:与用户界面相关的操作,例如更新UI、处理用户输入、显示提示信息等,必须在主线程中执行,以保证界面的响应性和正确性。

  2. UI元素的创建和销毁:涉及到UI元素的创建、销毁、修改属性等操作,必须在主线程中执行,以避免UI线程安全问题。

  3. 系统调用:某些系统API要求在主线程中调用,例如Windows中的消息循环、Android中的UI线程等,必须在主线程中执行以确保正确的系统交互。

需要注意的是,多线程编程需要谨慎处理线程安全、数据共享与通信、同步机制等问题,以避免竞态条件、死锁和数据竞争等多线程问题。合理地使用多线程可以提高程序的性能和响应性,但也需要仔细设计和测试,以确保正确和可靠的多线程行为。