罗剑锋的C++实战笔记学习(二):容器、算法库、多线程

4、容器

1)、容器的通用特性

所有容器都具有的一个基本特性:它保存元素采用的是值(value)语义,也就是说,容器里存储的是元素的拷贝、副本,而不是引用

容器操作元素的很大一块成本就是值的拷贝。所以,如果元素比较大,或者非常多,那么操作时的拷贝开销就会很高,性能也就不会太好

一个解决办法是,尽量为元素实现转移构造和转移赋值函数,在加入容器的时候使用std::move()来转移,减少元素复制的成本:

#include <iostream>
#include <vector>class Point {
public:Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {std::cout << "Point constructor called." << std::endl;}Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {std::cout << "Copy constructor called." << std::endl;}Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {std::cout << "Move constructor called." << std::endl;}private:int x_, y_;std::string data; // 假设这是一个昂贵的资源
};int main() {// 不使用移动语义,直接拷贝{Point p(1, 2);std::vector<Point> v;v.push_back(p); // 这里会调用拷贝构造函数std::cout << "---- After normal push_back ----" << std::endl;}// 使用移动语义{Point p(3, 4);std::vector<Point> v;v.push_back(std::move(p)); // 这里会调用移动构造函数std::cout << "---- After move semantics push_back ----" << std::endl;}return 0;
}

输出:

Point constructor called.
Copy constructor called.
---- After normal push_back ----
Point constructor called.
Move constructor called.
---- After move semantics push_back ----

也可以使用C++11为容器新增加的emplace操作函数,它可以就地构造元素,免去了构造后再拷贝、转移的成本,不但高效,而且用起来也很方便:

#include <iostream>
#include <vector>
#include <string>class Point {
public:Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {std::cout << "Point constructor called." << std::endl;}Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {std::cout << "Copy constructor called." << std::endl;}Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {std::cout << "Move constructor called." << std::endl;}private:int x_, y_;std::string data; // 假设这是一个昂贵的资源
};int main() {std::vector<Point> v;v.reserve(2);  // 预分配足够的空间,避免内部扩展// 使用emplace_back直接在容器内构造对象v.emplace_back(1, 2); // 这里不会调用拷贝或移动构造函数v.emplace_back(3, 4);// 当std::vector需要扩容时,会触发之前对象的移动构造函数v.emplace_back(5, 6);return 0;
}

输出:

Point constructor called.
Point constructor called.
Point constructor called.
Move constructor called.
Move constructor called.

还可以在容器里存放元素的指针,来间接保存元素。这里建议使用智能指针unique_ptr/shared_ptr,让它们帮你自动管理元素,一般情况下,shared_ptr是一个更好的选择,它的共享语义与容器的值语义基本一致

2)、顺序容器

顺序容器就是数据结构里的线性表,一共有5种:array、vector、deque、list、forward_list

按照存储结构,这5种容器又可以再细分成两组

  • 连续存储的数组:array、vector和deque
  • 指针结构的链表:list和forward_list

数组:

array和vector直接对应C的内置数组,内存布局与C完全兼容,所以是开销最低、速度最快的容器它们两个的区别在于容量能否动态增长。array是静态数组,大小在初始化的时候就固定了,不能再容纳更多的元素。而vector是动态数组,虽然初始化的时候设定了大小,但可以在后面随需增长,容纳任意数量的元素

#include <array>
#include <vector>int main() {std::array<int, 2> arr;assert(arr.size() == 2);std::vector<int> v(2);for (int i = 0; i < 10; i++) {v.emplace_back(i);}assert(v.size() == 12);return 0;
}

deque也是一种可以动态增长的数组,它和vector的区别是,它可以在两端高效地插入删除元素,而vector则只能用push_back在末端追加元素

#include <deque>int main() {std::deque<int> d;d.emplace_back(9); // 末端添加一个元素d.emplace_front(1); // 前端添加一个元素assert(d.size() == 2);return 0;
}

链表:

vector和deque里的元素因为是连续存储的,所以在中间的插入删除效率就很低,而list和forward_list是链表结构,插入删除操作只需要调整指针,所以在任意位置的操作都很高效

链表的缺点是查找效率低,只能沿着指针顺序访问,这方面不如vector随机访问的效率高。list是双向链表,可以向前或者向后遍历,而forward_list是单向链表,只能向前遍历,查找效率就更低了

链表结构比起数组结构还有一个缺点,就是存储成本略高,因为必须要为每个元素附加一个或者两个的指针,指向链表的前后节点

扩容机制:

vector/deque和list/forward_list都可以动态增长来容纳更多的元素,但它们的内部扩容机制却是不一样的

当vector的容量到达上限的时候(capacity),它会再分配一块两倍大小的新内存,然后把旧元素拷贝或者移动过去。这个操作的成本是非常大的,所以,在使用vector的时候最好能够预估容量,使用reserve提前分配足够的空间,减少动态扩容的拷贝代价

deque、list会按照固定的步长(例如N个字节、一个节点)去增加容量。但在短时间内插入大量数据的时候就会频繁分配内存,效果反而不如vector一次分配来得好

如何选择:

如果没有什么特殊需求,首选的容器就是array和vector,它们的速度最快、开销最低,数组的形式也令它们最容易使用,搭配算法也可以实现快速的排序和查找

剩下的deque、list和forward_list则适合对插入删除性能比较敏感的场合,如果还很在意空间开销,那就只能选择非链表的deque了

3)、有序容器

顺序容器的特点是,元素的次序是由它插入的次序而决定的,访问元素也就按照最初插入的顺序。而有序容器则不同,它的元素在插入容器后就被按照某种规则自动排序,所以是有序的

标准库里一共有四种有序容器:set/multiset和map/multimap(底层是通过红黑树实现)。有multi前缀的容器表示可以容纳重复的key

在定义有序容器的时候必须要指定key的比较函数。只不过这个函数通常是默认的less,表示小于关系,不用特意写出来

C++里的int、string等基本类型都支持比较排序,但很多自定义类型没有默认的比较函数,需要重载<或者自定义模板参数

比如说有一个Point类,它是没有大小概念的,但只要给它重载<操作符,就可以放进有序容器里了:

#include <iostream>
#include <set>class Point {
public:Point(int x, int y) : x_(x), y_(y) {}bool operator<(const Point &other) const {if (x_ != other.x_) {return x_ < other.x_;} else {return y_ < other.y_;}}friend std::ostream &operator<<(std::ostream &os, const Point &p) {os << "(" << p.x_ << ", " << p.y_ << ")";return os;}private:int x_;int y_;
};int main() {std::set<Point> points;points.emplace(7, 2);points.emplace(3, 5);for (const auto &point: points) {std::cout << point << std::endl;}return 0;
}

另一种方式是编写专门的函数对象或者lambda表达式,然后在容器的模板参数里指定。这种方式更灵活,而且可以实现任意的排序准则:

#include <iostream>
#include <set>template<typename Iter>
void printRangeWithCommas(Iter begin, Iter end) {if (begin == end) return;for (Iter it = begin; it != end; ++it) {std::cout << *it;if (std::next(it) != end) {std::cout << ",";}}std::cout << "\n";
}int main() {std::set<int> s = {7, 3, 9};printRangeWithCommas(s.begin(), s.end()); // 调用函数打印集合,输出: 3,7,9auto comp = [](auto a, auto b) {return a > b;};std::set<int, decltype(comp)> gs(comp);std::copy(s.begin(), s.end(), std::inserter(gs, gs.end()));printRangeWithCommas(gs.begin(), gs.end()); // 再次调用函数打印另一个集合,输出: 9,7,3return 0;
}
4)、无序容器

无序容器也有四种,名字里也有set和map,只是加上了unordered(无序)前缀,分别是unordered_set/unordered_multiset、unordered_map/unordered_multimap

无序容器用法上与有序容器几乎是一样的,区别在于内部数据结构:它不是红黑树,而是散列表(也叫哈希表,hash table)

因为它采用散列表存储数据,元素的位置取决于计算的散列值,没有规律可言,所以就是无序的

#include <iostream>
#include <unordered_map>int main() {using map_type =std::unordered_map<int, std::string>;map_type dict;dict[1] = "one";dict.emplace(2, "two");dict[10] = "ten";for (auto &x: dict) { // 遍历顺序不确定,既不是插入顺序,也不是大小序std::cout << x.first << "=>"<< x.second << ",";}return 0;
}

无序容器要求key具备两个条件,一是可以计算hash值,二是能够执行相等比较操作。第一个是因为散列表的要求,只有计算hash值才能放入散列表,第二个则是因为hash值可能会冲突,所以当hash值相同时,就要比较真正的key值

#include <iostream>
#include <unordered_set>class Point {
public:Point(int x, int y) : x_(x), y_(y) {}int getX() const { return x_; }int getY() const { return y_; }// 重载operator==用于比较bool operator==(const Point &other) const {return x_ == other.x_ && y_ == other.y_;}// 为了使用Point作为std::unordered_set或std::unordered_map的键,需要定义哈希函数struct Hash {std::size_t operator()(const Point &p) const noexcept {std::size_t h1 = std::hash<int>{}(p.getX());std::size_t h2 = std::hash<int>{}(p.getY());return h1 ^ (h2 << 1);}};// 重载operator<<以便可以直接输出Point对象friend std::ostream &operator<<(std::ostream &os, const Point &p) {return os << "(" << p.x_ << ", " << p.y_ << ")";}private:int x_, y_;
};int main() {std::unordered_set<Point, Point::Hash> pointSet;pointSet.insert(Point(1, 2));pointSet.insert(Point(3, 4));// 尝试插入重复的点,不会成功pointSet.insert(Point(1, 2));for (const auto &point: pointSet) {std::cout << point << std::endl;}return 0;
}
5)、小结
  1. 标准容器可以分为三大类,即顺序容器、有序容器和无序容器
  2. 所有容器中最优先选择的应该是array和vector,它们的速度最快,开销最低
  3. list是链表结构,插入删除的效率高,但查找效率低
  4. 有序容器是红黑树结构,对key自动排序,查找效率高,但有插入成本
  5. 无序容器是散列表结构,由hash值计算存储位置,查找和插入的成本都很低
  6. 有序容器和无序容器都属于关联容器,元素有key的概念,操作元素实际上是在操作key,所以要定义对key的比较函数或者散列函数

5、算法库

1)、迭代器

迭代器常用的函数如下:

  • begin()end():得到表示两个端点的迭代器
  • distance():计算两个迭代器之间的距离
  • advance():前进或者后退 N 步
  • next()prev():计算迭代器前后的某个位置
#include <array>int main() {std::array<int, 5> arr = {0, 1, 2, 3, 4}; // array静态数组容器auto b = begin(arr); // 全局函数获取迭代器,首端auto e = end(arr); // 全局函数获取迭代器,末端assert(std::distance(b, e) == 5); // 迭代器的距离auto p = std::next(b); // 获取下一个位置assert(std::distance(b, p) == 1); // 迭代器的距离assert(std::distance(p, b) == -1); // 反向计算迭代器的距离std::advance(p, 2); // 迭代器前进两个位置,指向元素3assert(*p == 3);assert(p == std::prev(e, 2)); // 是末端迭代器的前两个位置return 0;
}
2)、for_each
#include <iostream>
#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10};for (const auto &x: v) { // range for循环std::cout << x << ",";}std::cout << "\n";auto print = [](const auto &x) // 定义一个lambda表达式{std::cout << x << ",";};for_each(cbegin(v), cend(v), print); // for_each算法std::cout << "\n";for_each( // for_each算法,内部定义lambda表达式cbegin(v), cend(v), // 获取常量迭代器[](const auto &x) // 匿名lambda表达式{std::cout << x << ",";});std::cout << "\n";return 0;
}
3)、排序算法

sort()是经典的快排算法,示例如下:

#include <iostream>
#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10};auto print = [](const auto &x) // lambda表达式输出元素{std::cout << x << ",";};std::sort(begin(v), end(v)); // 快速排序for_each(cbegin(v), cend(v), print);return 0;
}

一些常见问题对应的算法:

  • 要求排序后仍然保持元素的相对顺序,应该用stable_sort,它是稳定的
  • 选出前几名(TopN),应该用partial_sort
  • 选出前几名,但不要求再排出名次(BestN),应该用nth_element
  • 中位数(Median)、百分位数(Percentile),还是用nth_element
  • 按照某种规则把元素划分成两组,用partition
  • 第一名和最后一名,用minmax_element
#include <iostream>
#include <vector>template<typename Iter>
void printRangeWithCommas(const std::string &prefix, Iter begin, Iter end) {if (begin == end) return;std::cout << prefix;for (auto it = begin; it != end; ++it) {std::cout << (it == begin ? "" : ",") << *it;}std::cout << '\n';
}int main() {std::vector<int> v = {3, 5, 1, 7, 10};// top3std::partial_sort(begin(v), next(begin(v), 3), end(v)); // 取前3名printRangeWithCommas("top3: ", v.begin(), next(begin(v), 3));// best3std::nth_element(begin(v), next(begin(v), 3), end(v)); // 最好的3个printRangeWithCommas("best3: ", v.begin(), next(begin(v), 3));// medianauto mid_iter = // 中位数的位置next(begin(v), v.size() / 2);std::nth_element(begin(v), mid_iter, end(v)); // 排序得到中位数std::cout << "median: " << *mid_iter << std::endl;// partitionauto pos = std::partition( // 找出所有大于9的数begin(v), end(v), [](const auto &x) // 定义一个lambda表达式{return x > 9;});printRangeWithCommas("values > 9: ", v.begin(), pos);// min/maxauto [minIt, maxIt] = std::minmax_element( // 找出第一名和倒数第一cbegin(v), cend(v));std::cout << "min value: " << *minIt << ", max value: " << *maxIt << std::endl;return 0;
}

在使用这些排序算法时,对迭代器要求比较高,通常都是随机访问迭代器(minmax_element除外),所以最好在顺序容器array/vector上调用

4)、查找算法

binary_search:在已经排好序的区间里执行二分查找,但它只返回一个bool值,告知元素是否存在

#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};std::sort(begin(v), end(v)); // 快速排序// 二分查找,只能确定元素在不在bool found = binary_search(cbegin(v), cend(v), 7);assert(found);return 0;
}

想要在已序容器上执行二分查找,要用算法lower_bound,它返回第一个大于或等于值的位置

#include <vector>int main() {std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};std::sort(begin(v), end(v));// 找到第一个>=7的位置auto pos = std::lower_bound(cbegin(v), cend(v), 7);bool found = (pos != cend(v)) && (*pos == 7); // 可能找不到,所以必须要判断assert(found); // 7在容器里// 找到第一个>=9的位置pos = std::lower_bound(cbegin(v), cend(v), 9);found = (pos != cend(v)) && (*pos == 9); // 可能找不到,所以必须要判断assert(!found); // 9不在容器里return 0;
}

lower_bound的返回值是一个迭代器,需要做判断才能知道是否真的找到了。判断的条件有两个,一个是迭代器是否有效,另一个是迭代器的值是不是要找的值

upper_bound算法:返回第一个大于值的元素(也是在已序容器上执行二分查找)

对于有序容器set/map,就不需要调用这三个算法了,它们有等价的成员函数find/lower_bound/upper_bound,效果是一样的

#include <iostream>
#include <set>int main() {std::multiset<int> s = {3, 5, 1, 7, 7, 7, 10, 99, 42};auto pos = s.find(7); // 二分查找,返回迭代器assert(pos != s.end());auto lower_pos = s.lower_bound(7); // 获取区间的左端点auto upper_pos = s.upper_bound(7); // 获取区间的右端点auto print = [](const auto &x) {std::cout << x << ",";};// 输出7,7,7std::for_each(lower_pos, upper_pos, print);return 0;
}

标准库里还有一些查找算法可以用于未排序的容器,这些算法以find和search命名,其中用于查找区间的find_first_of/find_end

#include <vector>
#include <array>int main() {std::vector<int> v = {1, 9, 11, 3, 5, 7};// 查找算法,找到第一个出现的位置auto pos = std::find(begin(v), end(v), 3);assert(pos != end(v));// 查找算法,用lambda判断条件pos = std::find_if(begin(v), end(v),[](auto x) {return x % 2 == 0;});assert(pos == end(v));std::array<int, 2> arr = {3, 5};// 查找一个子区间pos = std::find_first_of(begin(v), end(v),begin(arr), end(arr));assert(pos != end(v));return 0;
}
5)、小结
  1. 算法是专门操作容器的函数,是一种智能for循环,它的最佳搭档是lambda表达式
  2. 算法通过迭代器来间接操作容器,使用两个端点指定操作范围,迭代器决定了算法的能力
  3. for_each算法是for的替代品,以函数式编程替代了面向过程编程
  4. 有多种排序算法,最基本的是sort,但应该根据实际情况选择其他更合适的算法,避免浪费
  5. 在已序容器上可以执行二分查找,应该使用的算法是lower_bound
  6. list/set/map提供了等价的排序、查找函数,更适应自己的数据结构
  7. find/search是通用的查找算法,效率不高,但不必排序也能使用

6、多线程

1)、仅调用一次

要先声明一个once_flag类型的变量,最好是静态、全局的(线程可见),作为初始化的标志。然后调用专门的call_once()函数,以函数式编程的方式,传递这个标志和初始化函数。这样C++就会保证,即使多个线程重入call_once(),也只能有一个线程会成功运行初始化

#include <iostream>
#include <thread>int main() {static std::once_flag flag; // 全局的初始化标志auto f = []() {std::call_once(flag, // 仅一次调用,注意要传flag[]() { // 匿名lambda,初始化函数,只会执行一次std::cout << "only once" << std::endl;});};// 使用vector管理线程,确保所有线程执行完毕后再退出mainstd::vector<std::thread> threads;for (int i = 0; i < 2; ++i) {threads.emplace_back(f);}// 等待所有线程完成for (std::thread &t: threads) {t.join();}return 0;
}
2)、线程局部存储

有thread_local标记的变量在每个线程里都会有一个独立的副本,是线程独占的

#include <iostream>
#include <thread>int main() {thread_local int n = 0; // 线程局部存储变量auto f = [&](int x) {n += x; // 使用线程局部变量,互不影响std::cout << n << std::endl;};// 使用vector管理线程,确保所有线程执行完毕后再退出mainstd::vector<std::thread> threads;threads.emplace_back(f, 10);threads.emplace_back(f, 20);// 等待所有线程完成for (std::thread &t: threads) {t.join();}return 0;
}

在程序执行后,可以看到两个线程分别输出了10和20,互不干扰

3)、原子变量

目前,C++只能让一些最基本的类型原子化,比如atomic_int、atomic_long等。这些原子变量都是模板类atomic的特化形式,包装了原始的类型,具有相同的接口,用起来和bool、int几乎一模一样,但却是原子化的,多线程读写不会出错

原子变量和原始的类型一个重要的区别是:原子变量禁用了拷贝构造函数,所以在初始化的时候不能用=的赋值形式,只能用圆括号或者花括

#include <iostream>
#include <thread>void incrementAtomicInt(std::atomic_int &counter, int numIterations) {for (int i = 0; i < numIterations; ++i) {++counter;}
}void incrementAtomicLong(std::atomic_long &counter, long numIterations) {for (long i = 0; i < numIterations; ++i) {counter += 10;}
}int main() {const int numThreads = 4;const int iterationsPerThread = 100;std::atomic_int x{0};std::atomic_long y{1000L};std::vector<std::thread> threads;// 对x进行递增操作的线程for (int i = 0; i < numThreads; ++i) {threads.emplace_back(incrementAtomicInt, std::ref(x), iterationsPerThread);}// 对y进行递增操作的线程for (int i = 0; i < numThreads; ++i) {threads.emplace_back(incrementAtomicLong, std::ref(y), iterationsPerThread / 10); // 注意调整迭代次数以匹配期望的增量}// 等待所有线程完成for (std::thread &t: threads) {t.join();}std::cout << "final value of x: " << x << std::endl;std::cout << "final value of y: " << y << std::endl;assert(x == numThreads * iterationsPerThread);assert(y == 1000L + numThreads * (iterationsPerThread / 10) * 10);return 0;
}

除了模拟整数运算,原子变量还有一些特殊的原子操作,比如store、load、fetch_add、fetch_sub、exchange、compare_exchange_weak/compare_exchange_strong以及CAS(Compare And Swap)操作

4)、线程

C++标准库里有专门的线程类thread,使用它就可以简单地创建线程,在名字空间std::this_thread里,还有yield()get_id()sleep_for()sleep_until()等几个方便的管理函数

#include <iostream>
#include <chrono>
#include <thread>int main() {static std::atomic_flag flag{false};static std::atomic_int n;auto f = [&]() {auto value = flag.test_and_set(); // TAS检查原子标志量if (value) {std::cout << "flag has been set." << std::endl;} else {std::cout << "set flag by " << std::this_thread::get_id() << std::endl; // 输出线程id}n += 100; // 原子变量加法运算std::this_thread::sleep_for(std::chrono::milliseconds(n.load() * 10));std::cout << n << std::endl;};std::vector<std::thread> threads;for (int i = 0; i < 2; ++i) {threads.emplace_back(f);}for (std::thread &t: threads) {t.join();}return 0;
}

函数async()含义是异步运行一个任务,隐含的动作是启动一个线程去执行,但不绝对保证立即启动(也可以在第一个参数传递 std::launch::async,要求立即启动线程)

#include <iostream>
#include <chrono>
#include <thread>
#include <future>int main() {auto task = [](auto x) {std::this_thread::sleep_for(std::chrono::milliseconds(x));std::cout << "sleep for " << x << std::endl;return x;};auto f = std::async(task, 10);// 启动一个异步任务f.wait(); // 等待任务完成assert(f.valid());// 确实已经完成了任务std::cout << f.get() << std::endl; // 获取任务的执行结果return 0;
}

async()会返回一个future变量,如果任务有返回值,就可以用成员函数get()获取。不过要特别注意,get()只能调一次,再次获取结果会发生错误,抛出异常 std::future_error

这里还有一个很隐蔽的坑,如果不显式获取async()的返回值(即future对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数)。所以,即使不关心返回值,也总要用auto来配合async(),避免同步阻塞

5)、小结
  1. 多线程是并发最常用的实现方式,好处是任务并行、避免阻塞,坏处是开发难度高,有数据竞争、死锁等很多坑
  2. call_once()实现了仅调用一次的功能,避免多线程初始化时的冲突
  3. thread_local实现了线程局部存储,让每个线程都独立访问数据,互不干扰
  4. atomic实现了原子化变量,可以用作线程安全的计数器,也可以实现无锁数据结构
  5. async()启动一个异步任务,相当于开了一个线程,但内部通常会有优化,比直接使用线程更好

参考:

12 | 三分天下的容器:恰当选择,事半功倍

13 | 五花八门的算法:不要再手写for循环了

14 | 十面埋伏的并发:多线程真的很难吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/372174.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DBA 数据库管理

数据库&#xff1a;存储数据的仓库 数据库服务软件&#xff1a; 关系型数据库&#xff1a; 存在硬盘 &#xff0c;制作表格的 数据库的参数 [rootmysql50 ~]# cat /etc/my.cnf.d/mysql-server.cnf 主配置文件 [mysqld] datadir/var/lib/mysql 存放数据库目录…

浅谈信息技术高效课堂管理:策略、技巧与实践

引言&#xff1a; 在信息化教育的浪潮中&#xff0c;信息技术课程正逐渐成为学校教育体系中的重要组成部分。然而&#xff0c;信息技术课堂的特殊性——高互动性、高度依赖电子设备&#xff0c;给课堂管理带来了前所未有的挑战。如何在保证教学效率的同时&#xff0c;维护良好…

多端多商户自定义DIY商城小程序源码系统 前后端分离 带完整的安装代码包以及搭建教程

系统概述 在当今数字化时代&#xff0c;电子商务的发展日新月异&#xff0c;为了满足不断变化的市场需求和用户期望&#xff0c;我们自豪地推出了一款具有前瞻性的创新产品——多端多商户自定义 DIY 商城小程序源码系统。这款系统不仅具备前后端分离的先进架构&#xff0c;还配…

git 禁止dev合并到任何其他分支

创建 pre-merge-commit 钩子 导航到 Git 仓库的钩子目录&#xff1a; cd /path/to/your/repo/.git/hooks创建或编辑 pre-merge-commit 钩子&#xff1a; 也可以通过指令创建 nano pre-merge-commit在钩子文件中添加以下代码&#xff1a; #!/bin/sh# 获取当前分支名称 curr…

使用tcpdump抓取本本机的所有icmp包

1、抓取本机所有icmp包 tcpdump -i any icmp -vv 图中上半部分&#xff0c;是源主机tmp179无法ping通目标主机192.168.10.79&#xff08;因为把该主机关机了&#xff09;的状态&#xff0c;注意看&#xff0c;其中有unreachable 图中下半部分&#xff0c;是源主机tmp179可以p…

RIP环境下的MGRE网络

首先将LSP的IP地址进行配置 其他端口也进行同样的配置 将serial3/0/1配置25.0.0.2 24 将serial4/0/0配置35.0.0.2 24 将GE0/0/0配置45.0.0.2 24 进行第二步 R1与R5之间使用ppp的pap认证 在R5中进行配置 在aaa空间中创建账号和密码 将这个账号和密码使用在ppp协议中 然后…

什么是静态住宅代理?一文看懂它

静态住宅代理&#xff08;也称为 ISP 代理&#xff09;是最流行的代理类型之一。它们也是隐藏身份和在线匿名的最佳方式之一。但是您需要了解它们什么&#xff1f;是什么让它们如此特别&#xff1f;为什么您要使用住宅代理而不是仅仅使用常规代理服务&#xff1f;如果你感兴趣&…

移动的话费充值卡有什么用?

现在大部分人充话费都是直接用微信啊、支付宝啥的充 话费卡绑定麻烦不说&#xff0c;一些不太熟悉操作的人&#xff0c;对话费卡的使用方法更是头疼 这不前两天我刚用积分兑了几张移动的话费卡&#xff0c;绑定过程真的太麻烦了 最后还是在收卡云上卖出去了&#xff0c;这样…

周鸿祎数字分身亮相世界人工智能大会,应对“刁难提问”对答如流

近日&#xff0c;2024世界人工智能大会&#xff08;WAIC&#xff09;在上海举办&#xff0c;三六零集团&#xff08;601360.SH&#xff0c;下称“360”&#xff09;携多款大模型创新应用亮相全球顶尖盛会&#xff0c;以技术引擎、场景应用、安全赋能和产业联盟四大主题&#xf…

将QComboBox下拉项中的文本居中、居右

目录 1. 需求提出 2. 解决方法 1. 需求提出 QComboBox下拉项中的文本默认是居左的&#xff0c;如下&#xff1a; 有时需要将下拉项中的文本居中、居右。如何实现&#xff1f; 2. 解决方法 首先想到的是通过样式表来解决&#xff0c;但找遍Qt Assist和网络&#xff0c;都没这…

【Python学习笔记】菜鸟教程Scrapy案例 + B站amazon案例视频

背景前摇&#xff08;省流可以跳过这部分&#xff09; 实习的时候厚脸皮请教了一位办公室负责做爬虫这块的老师&#xff0c;给我推荐了Scrapy框架。 我之前学过一些爬虫基础&#xff0c;但是用的是比较常见的BeautifulSoup和Request&#xff0c;于是得到Scrapy这个关键词后&am…

Linux环境部署Python Web服务

“姑娘&#xff0c;再见面就要靠运气了&#xff0c;可别装作不认识&#xff0c;那句“好久不见”可干万别打颤…” 将使用 Python 编写的后端 API 部署到 Linux 环境中&#xff0c;可以按照以下详细步骤操作。本文将涵盖环境准备、API 编写、使用 Gunicorn 作为 WSGI 服务器、配…

磁致伸缩液位计的应用领域

磁致伸缩液位计作为一种高精度、高稳定性的液位测量设备&#xff0c;在众多行业中都有着广泛的应用。接下来&#xff0c;我们将从多个角度详细探讨磁致伸缩液位计在不同领域的应用情况。 石油化工行业 在石油化工行业中&#xff0c;磁致伸缩液位计主要用于储罐、反应器和管道等…

celery执行任务报错ValueError: not enough values to unpack

背景 在做用户注册模块的时候需要对手机号验证的过程进行优化&#xff0c;目前想到的方式是通过celeryrabbitmq的方式进行异步处理&#xff0c;选择使用celery是因为使用方便、性能好、可分布式部署。 环境信息 目前使用地win11容器化启动 rabbitmq:3.13.2 python:3.6.8 cel…

Linux学习看这一篇就够了,超超超牛的Linux基础入门

引言 小伙伴们&#xff0c;不管是学习c还是学习其他语言在我们学的路上都绕不过操作系统&#xff0c;而且&#xff0c;老生常谈的Linux更是每个计算机人的必修&#xff0c;那么我们对Linux的了解可能只是从别人那听到的简单的这个系统很牛&#xff0c;巴拉巴拉的&#xff0c;但…

STM32介绍

本内容是基于江协科技STM32视频学习之后&#xff0c;并参考【重写】简析stm32启动过程-CSDN博客和STM32 最小系统_stm32最小系统-CSDN博客以及其他资料综合整理而成。 1. STM32 1.1 STM32简介 STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器&#xff1b;STM32常应用在…

【python学习】快速了解python基本数据类型

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 前言1. 整数&#xff08;int&#xff09;2. 浮点数&#xff08;float&#xff09;3. 布尔值&#xff08;bool&#xf…

[个人感悟] 消息队列应该考察哪些问题?

前言 消息队列. 不论是Java内部提供的LinkedBlockingQueue, 还是当下主流的中间件RabbitMQ, Kafka, RockMQ. 其本质上都是一个削峰填谷的工具. 我们都知道, 请求和流量都有可能瞬间很高, 或者很低. 所以, 很多时候, 我们需要请求存储起来, 或者使用异步的方式, 来匀速的处理过…

Qt QWizard新建向导实例

使用QWizard做新建向导&#xff0c;最简单的实例 class MyWizard : public QWizard { public: MyWizard(QWidget* parent nullptr); QWizardPage* createFirstPage(); QWizardPage* createSecondPage(); QWizardPage* createThirdPage(); }; MyWizard::MyWizard(QWidget* par…

python 10个自动化脚本

目录 &#x1f31f; 引言 &#x1f4da; 理论基础 &#x1f6e0;️ 使用场景与代码示例 场景一&#xff1a;批量重命名文件 场景二&#xff1a;自动下载网页内容 场景三&#xff1a;数据清洗 场景四&#xff1a;定时执行任务 场景五&#xff1a;自动化邮件发送 场景六…