常见手撕项目C++
设计模式
单例模式
单例模式是一种常用的软件设计模式,其目的是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
- 优点:
- 资源控制:单例模式能够确保一个类只有一个实例存在,这对于控制资源的使用非常有用,如配置文件的读取、数据库的连接等,可以避免由于多个实例造成的资源浪费或冲突。
- 全局访问点:单例对象可以被全局访问,方便其他对象对其进行访问,而无需持有单例类的引用。
- 数据共享:由于整个应用程序共享一个单例实例,它自然地提供了一个共享数据的环境,这在某些场合下是非常有用的。
- 缺点:
- 全局变量(同步)问题:单例模式本质上提供了一个全局可访问的实例,但全局变量(或对象)容易被误用,特别是涉及多个线程进行访问的时候,还会出现同步问题。
- 违背单一职责原则:单例类除了管理自己的实例外,还承担了业务逻辑的职责,违反了单一职责原则。
介绍完单例模式,我们来看看单例模式的两种实现方式,分别是饿汉模式与懒汉模式。
饿汉模式
饿汉模式指的是单例实例在程序启动时就立即创建(迫不及待的感觉)。这种方式避免了线程安全问题,但可能会增加程序的启动时间,同时如果实例最终未被使用,则会造成资源的浪费。
class EagerSingleton{
private:
// 将自己的实例化对象申明为静态资源
static EagerSingleton instance;
protected:
// 隐藏自己的构造函数以及析构函数,防止用户调用
EagerSingleton() = default; // 这里构造函数设置为默认
EagerSingleton(const EagerSingleton&) = default;
EagerSingleton& operator= (const EagerSingleton&) = default;
~EagerSingleton() = default;
public:
EagerSingleton& getInstance(){
return instance;
}
}
// 静态的私有成员变量可以在类外进行初始化(一般在main()函数之前进行初始化),在这里,你可以理解instance是类内成员,可以访问私有以及保护成员。
EagerSingleton EagerSingleton::instance();
懒汉模式
懒汉模式指的是单例实例在第一次被使用时才进行创建(不叫我,那我就懒,不创建)。这种方式可以减少资源的消耗,但需要考虑线程安全问题(例如多个线程同时是第一次使用,所以一般需要锁)。
#include <mutex>
class lazySingleton{
private:
static lazySingleton* instance; // 懒汉模式一般使用指针
static mutex my_mu; // 考虑到线程安全,需要有锁。
protected:
// 不给用户调用构造函数和析构函数的机会
lazySingleton() = default;
lazySingleton(const lazySingleton&) = default;
lazySingleton& operator=(const laySingleton&) = default;
~lazySingleton() = default;
public:
lazySingleton* getInstance(){
if(instance == nullptr){ // 第一次检查
std::lock_guard<std::mutex> lock(my_mu); //作用域锁,离开作用域后,自动解锁
if(instance == nullptr){ // 第二次检查
instance = new lazySingleton();
}
}
return *this;
}
// my_mu会在这里结束后,自动解锁
}
// 静态成员变量类外初始化
lazySingleton lazySingleton::instance = nullptr;
lazySingleton lazySingleton::my_mu; // 调用锁的自动初始化方法
这里可能会好奇,为什么需要两次判断instance == nullptr
?
- 第一次检查 (
instance == nullptr
)
第一次检查是在锁外进行的。这个检查的目的是避免在单例实例已经创建之后的每次调用都需要进行昂贵的锁操作。如果实例已经存在,就直接返回实例,这样大部分时间可以避免锁的开销。 - 获取锁
如果第一次检查发现实例为nullptr,即单例尚未被创建,那么就需要进入同步块(通过获取锁)来确保只有一个线程可以创建单例实例。这是必要的,因为可能有多个线程同时通过了第一次的nullptr检查。 - 第二次检查 (
instance == nullptr
)
即使线程成功获取了锁,仍然需要再次检查实例是否为nullptr。这是因为在当前线程等待锁的同时,可能有另一个线程已经获取了锁、创建了实例并释放了锁。第二次检查确保了即使在多个线程同时尝试创建单例实例的情况下,单例实例仍然是唯一的。
策略模式
策略模式是一种行为设计模式,它允许在运行时选择算法或行为的最佳策略。策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以互换。这种模式让算法的变化独立于使用算法的客户。
策略接口
首先,要定义一个策略接口,表示可以执行的操作
class Strategy{
public:
virtual ~Strategy(){};
virtual void excute() const = 0; // 操作定义为纯虚函数
}
实现具体的策略(虚函数重写)
然后,利用继承,实现不同且具体的策略
class StrategyA : public Strategy{
public:
void excute() const override{ //override 关键字表示一定是重写虚函数,避免出现覆盖的情况(如果有存在的话)
cout << "StrategyA" << endl;
}
}
class StrategyB : public Strategy{
public:
void excute() const override{ //override 关键字表示一定是重写虚函数,避免出现覆盖的情况(如果有存在的话)
cout << "StrategyB" << endl;
}
}
class StrategyB : public Strategy{
public:
void excute() const override{ //override 关键字表示一定是重写虚函数,避免出现覆盖的情况(如果有存在的话)
cout << "StrategyB" << endl;
}
}
定义上下文
接着,定义一个上下文类,用于从客户端接收策略,并使用它执行操作
#include <iostream>
#include <memory> // 智能指针共享库
using namespace std;
class Context{
public:
unique_ptr<Strategy> strategy; // 使用独占的智能指针
// 使用右值引用来接收一个unique_ptr,并提供默认参数(即调用这个函数的时候可以不用传入参数)
Context(unique_ptr<Strategy> &&strategy = {}): strategy(std::move(strategy)){}
// 设置接口到底执行哪一个函数
void setStrategy(unique_ptr<Strategy> &&strategy){
strategy = std::move(strategy);
}
// 执行具体函数
void excuteStrategy() const{
if(strategy){
strtegy->excute();
}
}
}
用户调用
int main(){
// 需要注意make_unique<StrategyA>()是一个不具名的右值,所以可以正确调用右值构造函数
Context context(make_unique<StrategyA>());
context.excuteStrategy(); // 输出:StrategyA
// 动态切换策略
context.setStrategy(make_unique<StrategyB>());
context.excuteStrategy(); // 输出:StrategyB
context.setStrategy(make_unique<StrategyC>());
context.excuteStrategy(); // 输出:StrategyC
return 0;
}
代码
最短路径算法
floyd算法,dijkstra算法和bellman ford算法
使用函数模板写冒泡排序
#include <iostream>
// 函数模板定义冒泡排序
template<typename T>
void bubbleSort(T arr[], int n) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
// 如果当前元素大于下一个元素,则交换它们
if (arr[j] > arr[j+1]) {
T temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
// 打印数组元素的函数
template<typename T>
void printArray(T arr[], int size) {
for (int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
bubbleSort(arr, n);
std::cout << "Sorted array: \n";
printArray(arr, n);
// 可以用相同的函数模板对不同类型的数组进行排序
double arrDouble[] = {4.3, 2.5, -0.9, 10.2, 3.0};
int m = sizeof(arrDouble)/sizeof(arrDouble[0]);
bubbleSort(arrDouble, m);
std::cout << "Sorted array of doubles: \n";
printArray(arrDouble, m);
return 0;
}
写一个类模板
#include <iostream>
using namespace std;
// Pair类模板定义
template <typename T1, typename T2>
class Pair {
private:
T1 first;
T2 second;
public:
Pair(T1 a, T2 b) : first(a), second(b) {}
void setFirst(T1 a) {
first = a;
}
void setSecond(T2 b) {
second = b;
}
T1 getFirst() const {
return first;
}
T2 getSecond() const {
return second;
}
void print() const {
cout << "(" << first << ", " << second << ")" << endl;
}
};
int main() {
// 使用类模板创建int和double类型的Pair
Pair<int, double> p1(6, 7.8);
p1.print();
// 使用类模板创建string和string类型的Pair
Pair<string, string> p2("Hello", "World");
p2.print();
// 使用类模板创建char和int类型的Pair
Pair<char, int> p3('A', 100);
p3.print();
return 0;
}
string replace详解
方法概览
string (1)
string& replace (size_t pos, size_t len, const string& str);
string& replace (const_iterator i1, const_iterator i2, const string& str);
substring (2)
string& replace (size_t pos, size_t len, const string& str, size_t subpos, size_t sublen);
c-string (3)
string& replace (size_t pos, size_t len, const char* s);
string& replace (const_iterator i1, const_iterator i2, const char* s);
buffer (4)
string& replace (size_t pos, size_t len, const char* s, size_t n);
string& replace (const_iterator i1, const_iterator i2, const char* s, size_t n);
fill (5)
string& replace (size_t pos, size_t len, size_t n, char c);
string& replace (const_iterator i1, const_iterator i2, size_t n, char c);
range (6)
template <class InputIterator> string& replace (const_iterator i1, const_iterator i2, InputIterator first, InputIterator last);
initializer list (7)
string& replace (const_iterator i1, const_iterator i2, initializer_list<char> il);
参数介绍
str
- 描述: 另一个字符串对象,其值被复制到当前字符串的指定位置。
- 类型:
std::string
pos
- 描述: 要替换的第一个字符的位置。从0开始计数。
- 条件: 如果这个位置大于字符串长度,会抛出
std::out_of_range
异常。 - 类型:
size_t
len
- 描述: 要替换的字符数量。如果字符串比这个短,就尽可能多地替换字符。
- 特殊值:
string::npos
表示直到字符串的末尾都被替换。 - 类型:
size_t
subpos
- 描述: 在
str
中作为替换内容的第一个字符的位置。 - 条件: 如果这个位置大于
str
的长度,会抛出std::out_of_range
异常。 - 类型:
size_t
sublen
- 描述: 要从
str
中复制的子字符串的长度。如果str
比这个短,就尽可能多地复制字符。 - 特殊值:
string::npos
表示直到str
的末尾的所有字符都被复制。 - 类型:
size_t
s
- 描述: 指向字符数组的指针(例如,一个C风格的字符串)。
- 用途: 从这个数组复制字符到字符串的指定位置。
- 类型:
const char*
n
- 描述: 要复制的字符数量。
- 用途: 与
s
参数联合使用,指定从s
指向的数组中复制多少个字符。 - 类型:
size_t
c
- 描述: 一个字符值,会被重复
n
次以构成替换的内容。 - 类型:
char
first, last
- 描述: 输入迭代器到一个范围的初始和最终位置。使用的范围是
[first,last)
,包括first
指向的字符,但不包括last
指向的字符。 - 要求: 函数模板参数
InputIterator
应该是指向可以转换为char
类型的元素的输入迭代器类型。
il
- 描述: 一个
initializer_list
对象。 - 用途: 自动从初始化列表声明构造。
代码示例
#include <iostream>
#include <string>
int main() {
std::string base = "this is a test string.";
std::string str2 = "n example";
std::string str3 = "sample phrase";
std::string str4 = "useful.";
// 使用位置
std::string str = base;
std::cout << str << '\n'; // 初始字符串: "this is a test string."
str.replace(9, 5, str2);
std::cout << str << " (1)\n"; // 第1次替换后: "this is an example string."
str.replace(19, 6, str3, 7, 6);
std::cout << str << " (2)\n"; // 第2次替换后: "this is an example phrase."
str.replace(8, 10, "just a");
std::cout << str << " (3)\n"; // 第3次替换后: "this is just a phrase."
str.replace(8, 6, "a shorty", 7);
std::cout << str << " (4)\n"; // 第4次替换后: "this is a short phrase."
str.replace(22, 1, 3, '!');
std::cout << str << " (5)\n"; // 第5次替换后: "this is a short phrase!!!"
// 使用迭代器
str = base; // 重置str为初始值
str.replace(str.begin(), str.end() - 3, str3);
std::cout << str << " (6)\n"; // 第6次替换后: "sample phrase!!!"
str.replace(str.begin(), str.begin() + 6, "replace");
std::cout << str << " (7)\n"; // 第7次替换后: "replace phrase!!!"
str.replace(str.begin() + 8, str.begin() + 14, "is coolness", 7);
std::cout << str << " (8)\n"; // 第8次替换后: "replace is cool!!!"
str.replace(str.begin() + 12, str.end() - 4, 4, 'o');
std::cout << str << " (9)\n"; // 第9次替换后: "replace is cooool!!!"
str.replace(str.begin() + 11, str.end(), str4.begin(), str4.end());
std::cout << str << " (10)\n"; // 第10次替换后: "replace is useful."
return 0;
}
实验结果:
多线程
请使用两个线程打印分别打印基数和偶数
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx; // 全局互斥锁
std::condition_variable cv; // 条件变量
bool ready = false; // 控制打印的条件变量
int count = 1; // 当前打印的数字
// 打印奇数的函数
void print_odd() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return count % 2 == 1 || count > 10; });
if (count > 10) break;
std::cout << "Thread A: " << count++ << std::endl;
cv.notify_all(); // 唤醒所有等待线程
}
}
// 打印偶数的函数
void print_even() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return count % 2 == 0 || count > 10; });
if (count > 10) break;
std::cout << "Thread B: " << count++ << std::endl;
cv.notify_all(); // 唤醒所有等待线程
}
}
int main() {
std::thread t1(print_odd);
std::thread t2(print_even);
t1.join();
t2.join();
return 0;
}
信号量解释
https://www.cnblogs.com/harlanc/p/8596211.html
https://blog.csdn.net/MoonWisher_liang/article/details/110689470