前言
互联网技术面一般会涉及手撕算法,除了常见的力扣 Hot100、剑指 Offer 外,可能还会有一些常见的设计题,主要考察基本代码能力,本文整理了一些 C++ 语言常见的题目,大部分版本为简单的实现,面试的时候建议和面试官沟通确定是否需要进阶实现。个人手动整理,如有不足,欢迎批评指正。
单例模式
常见问题:
- 绝对线程安全的单例(Meyers Singeleton)
- 设计一个线程安全的单例模式
- 懒汉单例模式的线程安全问题
- 为什么要用锁加双重判断,只判断一次不可以吗?
什么是单例模式
在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。
如何实现一个单例模式
- 使用单例模式,首先要保证这个类的实例有且仅有一个,这个时候构造函数都需要设置为私有或删除的,因为不能通过构造函数来创建新的对象。
- 具体实现的时候,有两种实现方式:
- 饿汉式:饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。
- 懒汉式:是在类加载的时候不去创建这个唯一的实例,而是在需要使用的时候再进行实例化。
饿汉式单例模式实现代码
- 类的静态成员变量放在外面进行初始化,然后通过 getInstance() 函数返回实例
// 饿汉模式
class TaskQueue {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance() {
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
};
// 静态成员初始化放到类外部处理
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
int main() {
TaskQueue* obj = TaskQueue::getInstance();
}
懒汉式单例模式实现代码(基础版)
- 基本思路是在调用 getInstance() 函数的时候,判断类是否实例化了,没有的话,进行实例化;
- 但是下面的代码会有线程安全问题:
// 懒汉模式
class TaskQueue {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance() {
if(m_taskQ == nullptr) {
m_taskQ = new TaskQueue;
}
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
双重检查锁单例模式
为什么是双重检查?如果是只检查一次的话,先上锁然后在判断是否实例化,这样的话后续其他线程在调用 getInstance() 函数的时候,都会阻塞在这行,效率比较低下。
class TaskQueue {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance() {
if (m_taskQ == nullptr) {
m_mutex.lock();
if (m_taskQ == nullptr) {
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
C++ 双重检查锁单例模式一定是安全的吗?
- 不是,因为指令可能会重排,假设第一步是分配 TaskQueue 内存,第二步是在内存中构造 TaskQueue 对象,第三步是 m_taskQ 指针指向分配的内存;
- 重排后可能导致 m_taskQ 先指向分配的内存,之后在构造对象,而到了第二步,如果这个时候线程时间片耗尽,转而执行另外一个线程,另外一个线程在检查的时候,发现指针不为空,但是实际上这个时候对象还没初始化完成,这就出现问题了;
- 解决方法:
- 使用
call_once和once_flag来确保初始化只发生一次; - 另外一种是使用局部静态变量,因为 C++11 规定:如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待该变量完成初始化。所以局部静态变量也是线程安全的。
- 使用
class Singleton {
private:
static Singleton instance;
static std::once_flag onceFlag;
explicit Singleton() {
// 构造函数私有化,防止直接实例化
}
public:
static Singleton& getInstance() {
std::call_once(onceFlag, []() {
instance = Singleton();
});
return instance;
}
};
// 静态成员初始化
Singleton Singleton::instance;
std::once_flag Singleton::onceFlag;
// 示例使用
int main() {
Singleton& singleton = Singleton::getInstance();
// 使用singleton...
return 0;
}
以下是使用局部静态变量:
class TaskQueue {
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance() {
static TaskQueue taskQ;
return &taskQ;
}
void print() {
cout << "hello, world!!!" << endl;
}
private:
TaskQueue() = default;
};
int main() {
TaskQueue* queue = TaskQueue::getInstance();
queue->print();
return 0;
}
多线程交替打印 abc
问题:使用 3 个线程交替打印输出 abc 字符
主要考察多线程的使用,因为需要交替打印,这里需要对线程进行阻塞和等待,用一个全局的互斥锁,一个条件变量,以及一个变量 loop 来控制交替的轮次。代码如下:
// thread_abc.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
// 使用互斥锁确保线程安全
std::mutex mtx;
std::condition_variable cv;
int loop = 0;
// 打印函数
void printChar(int curr, int count) {
std::unique_lock<std::mutex> lock(mtx);
int i;
while (i < count) {
// 不是当前线程执行的时候,该线程进行阻塞等待
while (loop != curr) {
cv.wait(lock);
}
loop = (loop + 1) % 3; // 转移给下一个线程
char c = 'a' + curr;
std::cout << c;
++i;
cv.notify_all(); // 唤醒所有阻塞的线程
}
}
int main() {
// 创建三个线程,交替打印 'a', 'b', 'c' 共10次
std::thread threadA(printChar, 0, 10);
std::thread threadB(printChar, 1, 10);
std::thread threadC(printChar, 2, 10);
// 等待所有线程执行完毕
threadA.join();
threadB.join();
threadC.join();
std::cout << std::endl; // 输出换行以使结果更清晰
return 0;
}
生产者消费者模型
一个简单的生产者消费者模型,缓冲区大小为 10,生产者不断的往缓冲区中加入数据,直到容量满,消费者不断的从缓冲区中读取数据,一旦缓冲区为空就会阻塞。
#include <algorithm>
#include <iostream>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <queue>
using namespace std;
int N = 100;
const int MAX_SIZE = 10;
std::condition_variable cv;
std::mutex mtx;
std::queue<int> que;
void producer() {
for (int i = 0; i < N; ++i) {
std::unique_lock<mutex> lk(mtx);
cv.wait(lk, []() {
return que.size() <= MAX_SIZE;
});
std::cout << std::this_thread::get_id() << " 生成数据 " << i << endl;
que.push(i);
cv.notify_one();
}
}
void consumer() {
for (int i = 0; i < N; ++i) {
std::unique_lock<mutex> lk(mtx);
cv.wait(lk, []() {
return !que.empty();
});
std::cout << std::this_thread::get_id() << " 消耗数据 " << que.front() << endl;
que.pop();
cv.notify_one();
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
线程池
基础版线程池
一个比较简单的线程池,提交的任务函数类型为 std::function<void()>,实现如下:
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <thread>
#include <condition_variable>
#include <mutex>
using namespace std;
class ThreadPool {
using TaskType = std::function<void()>;
public:
ThreadPool(int n) : nthread(n) {
works.reserve(n);
for (int i = 0; i < n; ++i) {
works.push_back(thread([this]() {
while (true) {
std::unique_lock<mutex> lk(mtx);
cv.wait(lk, [this]() {
return !tasks.empty() || stop;
});
if (tasks.empty() && stop) {
return;
}
TaskType task = std::move(tasks.front());
tasks.pop();
task();
}
}));
}
}
~ThreadPool() {
{
std::unique_lock<mutex> lk(mtx);
stop = true;
}
cv.notify_all();
for (int i = 0; i < nthread; ++i) {
works[i].join();
}
}
void setStop(bool flag = true) {
{
std::unique_lock<mutex> lk(mtx);
stop = true;
}
cv.notify_all();
}
void addTask(TaskType task) {
{
std::unique_lock<mutex> lk(mtx);
tasks.push(task);
}
cv.notify_one();
}
private:
int nthread;
bool stop = false; // 默认是没退出
std::mutex mtx;
std::condition_variable cv;
std::vector<thread> works;
std::queue<TaskType> tasks;
};
int main() {
ThreadPool pool(10);
for (int i = 0; i < 100; ++i) {
pool.addTask([x = i]() { // 注意这里变量的捕获需要用值捕获
cout << std::this_thread::get_id() << " 正在处理任务 " << x << endl;
});
}
return 0;
}
进阶版线程池
更加复杂的线程池:提交的任务函数类型是任意的,最终通过 std::bind 等封装为 std::function<void()> 类型
#include <vector>
#include <thread>
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <functional>
#include <future>
#include <queue>
#include <memory>
using namespace std;
class ThreadPool{
using TaskType = std::function<void()>;
private:
std::vector<thread> pools;
std::queue<TaskType> tasks;
std::mutex mtx;
std::condition_variable cv;
bool flag = false;
public:
ThreadPool(int poolsize) {
for (int i = 0; i < poolsize; ++i) {
pools.push_back(thread([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, [this]() {
return !tasks.empty() || flag;
});
if (tasks.empty() && flag) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}));
}
}
~ThreadPool() {
stop();
}
template<typename F, class...Args>
auto submit(F &&func, Args&& ...args) -> std::future<decltype(func(args...))> {
using ReturnType = decltype(func(args...));
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(func), std::forward<Args>(args)...)
);
std::future<ReturnType> result = task->get_future();
{
std::unique_lock<std::mutex> lk(mtx);
tasks.emplace([task]() {
(*task)();
});
}
cv.notify_one();
return result;
}
void stop() {
{
std::unique_lock<std::mutex> lk(mtx);
flag = true;
}
cv.notify_all();
for (int i = 0; i < pools.size(); ++i) {
pools[i].join();
}
}
};
void exampleFunc(int n, int m) {
cout << "thread = " << std::this_thread::get_id() << " n = " << n << " m = " << m << endl;
}
int main() {
ThreadPool threadpool(3);
std::vector<std::future<void>> results;
for (int i = 0; i < 10; i++) {
results.emplace_back(threadpool.submit(exampleFunc, i, i * 10));
}
for (auto &result : results) {
result.get();
}
}
大小端判断
大端序、小端序是什么?
- 大端序:多字节数据的最高有效字节存放在最低的内存地址,而最低有效字节存放在最高的内存地址。例如:
0x1234,其中12在第一个字节,34在第二个字节。 - 小端序:多字节数据的最低有效字节存放在最低的内存地址,而最高有效字节存放在最高的内存地址。例如:
0x1234,其中34在第一个字节,12在第二个字节 - 大端序更加符合我们人类的读取习惯,如果地址从低往高来看的话,
0x1234就是按照我们从左往右的顺序读的,小端序更加符合计算机的习惯。
网络当中传输用的是什么字节序?
- 大端序
为什么字节序重要?
字节序的概念对于跨平台的数据交换非常重要,特别是在网络通信和文件传输中。网络协议通常规定使用大端序(例如在以太网和 IP 协议中),而不同的计算机架构可能使用不同的字节序。因此,当数据在不同系统之间传输时,可能需要进行字节序转换(也称为字节序标准化或网络字节序转换)以确保数据的正确解释。
如何判断系统是大端序还是小端序
通过强制类型转换将整型低位的内容转为字符串,然后进行判断:
#include <stdio.h>
int main() {
unsigned int x = 0x12345678;
char *c = (char*)&x;
if (*c == 0x78) {
printf("系统是小端序\n");
} else {
printf("系统是大端序\n");
}
return 0;
}
或使用 C 语言联合体的性质:
#include <stdio.h>
union {
unsigned int u;
unsigned char c[4];
} testend;
int main() {
testend.u = 0x12345678;
if (testend.c[0] == 0x12) {
printf("大端序(Big-Endian)\n");
} else if (testend.c[0] == 0x78) {
printf("小端序(Little-Endian)\n");
} else {
printf("无法确定字节序\n");
}
return 0;
}
智能指针的实现
简易版的智能指针实现如下:
unique_ptr 的实现
unique_ptr 比较简单,主要是几个方法要记住是干什么的,代码如下:
#include <iostream>
using namespace std;
template <typename T>
class UniquePtr {
private:
T* ptr; // 内部指针
public:
// 构造函数:接受一个指向 T 类型的指针
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
// 禁止拷贝构造和赋值操作
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// 移动构造函数
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 将源指针置为 nullptr
}
// 移动赋值操作符
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) { // 防止自我赋值
delete ptr; // 释放当前持有的资源
ptr = other.ptr; // 转移资源的所有权
other.ptr = nullptr; // 将源指针置为 nullptr
}
return *this;
}
// 重载解引用操作符
T& operator*() const {
return *ptr;
}
// 重载箭头操作符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
// 释放所拥有的指针并返回它
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
// 重置指针,释放当前指针并拥有新的指针
void reset(T* p = nullptr) {
if (ptr) {
delete ptr;
}
ptr = p;
}
// 析构函数,自动释放内存
~UniquePtr() {
if (ptr) {
delete ptr;
}
}
};
// 测试 unique_ptr 实现
int main() {
UniquePtr<int> p1(new int(42)); // 创建一个 unique_ptr
std::cout << *p1 << std::endl; // 输出 42
UniquePtr<int> p2 = std::move(p1); // 移动所有权
if (p1.get() == nullptr) {
std::cout << "p1 is now nullptr" << std::endl;
}
std::cout << *p2 << std::endl; // 输出 42
p2.reset(new int(100)); // 重置指针
std::cout << *p2 << std::endl; // 输出 100
return 0;
}
shared_ptr 的实现
核心是引用计数原理,注意细节部分:
- 拷贝赋值和移动赋值的时候需要将原来的指针进行释放;
- 移动赋值运算符参数不能加 const,返回值和拷贝赋值运算符一样是引用类型;
- 原子变量获得值的方法是 load()
#include <iostream>
#include <memory>
#include <atomic>
using namespace std;
template<typename T>
class SharedPtr {
private:
T* ptr; // 原始指针
std::atomic<int>* ref_count; // 引用计数
void release() {
if (ref_count) {
(*ref_count)--;
if (*ref_count == 0) {
delete ptr;
delete ref_count;
}
}
}
public:
SharedPtr() : ptr(nullptr), ref_count(nullptr) {}
explicit SharedPtr(T *p) : ptr(p), ref_count(new std::atomic<int>(1)) {}
~SharedPtr() {
release();
}
SharedPtr(const SharedPtr& rhs) : ptr(rhs.ptr), ref_count(rhs.ref_count) {
if (ref_count) {
(*ref_count)++;
}
}
SharedPtr(SharedPtr&& rhs) : ptr(rhs.ptr), ref_count(rhs.ref_count) {
rhs.ptr = nullptr;
rhs.ref_count = nullptr;
}
SharedPtr& operator=(const SharedPtr& rhs) {
if (this != &rhs) {
release(); // 释放当前指针
ptr = rhs.ptr;
ref_count = rhs.ref_count;
if (ref_count) {
(*ref_count)++;
}
}
return *this;
}
SharedPtr& operator=(SharedPtr&& rhs) {
if (this != &rhs) {
release();
ptr = rhs.ptr;
ref_count = rhs.ref_count;
rhs.ptr = nullptr;
rhs.ref_count = nullptr;
}
return *this;
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
int use_count() const {
return ref_count ? ref_count->load() : 0;
}
};
template<typename T, class...Args>
SharedPtr<T> makeShared(Args&&...args) {
return SharedPtr<T>(new T(std::forward<Args>(args)...));
}
class A {
public:
A(int a, int b) : ma(a), mb(b) {}
void print() {
cout << "a = " << ma << " b = " << mb << endl;
}
private:
int ma, mb;
};
// 测试 shared_ptr 实现
int main() {
SharedPtr<int> p1(new int(42)); // 创建一个 shared_ptr
std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 输出引用计数
{
SharedPtr<int> p2 = p1; // 拷贝构造
std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 输出引用计数
std::cout << "p2 use_count: " << p2.use_count() << std::endl; // 输出引用计数
} // p2 离开作用域,引用计数减 1
std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 输出引用计数
SharedPtr<int> p3 = std::move(p1); // 移动构造
std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 输出引用计数
std::cout << "p3 use_count: " << p3.use_count() << std::endl; // 输出引用计数
SharedPtr<A> pa = makeShared<A>(1, 2);
pa->print();
return 0;
}
内存池的实现
这里只实现了一个简单版本的内存池,基本思路,开辟一块大的空间,然后通过嵌入式指针的手法将每个块的前 8 个字节作为指针,存放下一块的地址,通过空闲链表串联起来。
class MemoryPool {
public:
MemoryPool(size_t size, size_t count) : blocksize(size), blockcount(count) {
memory = (char*)malloc(blocksize * blockcount);
freelist = memory; // 初始化,freelist指向内存池第一块内容
char *curr_block = memory;
for (size_t i = 0; i < count - 1; ++i) {
// 将每个块的前8个字节作为指针,存放下一个块的地址
*(void**)curr_block = (void*)(curr_block + blocksize);
curr_block += blocksize;
}
*(void**)curr_block = nullptr;
}
~MemoryPool() {
free(memory);
}
void* allocateBlock() {
if (freelist == nullptr) {
return nullptr;
}
void *block = freelist;
freelist = *(void**)block; // freelist指向下一个指针
return block;
}
void deallocate(void* ptr) {
// 检查内存是否在内存池的范围之内
if (!(ptr >= memory && ptr < memory + blocksize * blockcount)) {
return;
}
// 将块插入空闲链表的头部
*(void**)ptr = freelist;
freelist = ptr;
}
private:
char *memory = nullptr;
void *freelist = nullptr;
size_t blocksize;
size_t blockcount;
};
int main() {
{
MemoryPool pool(sizeof(int*), 1000);
int *p = (int*)pool.allocateBlock();
*p = 10;
int *p2 = (int*)pool.allocateBlock();
*p2 = *p;
std::cout << "&p = " << p << " *p = " << *p << std::endl;
std::cout << "&p2 = " << p2 << " *p2 = " << *p2 << std::endl;
}
}
线程安全的 HashMap
实现一个线程安全的 HashMap,这里采用的是开链法,整体不难实现,hash 函数那个可能要注意一下写法。这里没有进一步实现扩容操作了,如果需要实现整体会更加复杂。
#include <vector>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
using namespace std;
template<typename KeyType, typename ValueType>
class HashMap {
private:
struct Bucket {
std::list<std::pair<KeyType, ValueType>> ls;
std::mutex mtx;
};
std::vector<Bucket> table;
size_t hashFunction(const KeyType &key) {
return std::hash<KeyType>()(key) % table.size();
}
public:
HashMap(size_t size = 1001) : table(size) {}
void insert(const KeyType& key, const ValueType& value) {
size_t index = hashFunction(key);
auto &bucket = table[index];
std::unique_lock<std::mutex> lk(bucket.mtx);
for (auto &kv : bucket.ls) {
if (kv.first == key) {
kv.second = value;
return; // 键存在
}
}
bucket.ls.emplace_back(key, value); // 键不存在
}
bool get(const KeyType& key, ValueType& value) {
size_t index = hashFunction(key);
auto &bucket = table[index];
std::unique_lock<std::mutex> lk(bucket.mtx);
for (auto &kv : bucket.ls) {
if (kv.first == key) {
value = kv.second;
return true; // 键存在
}
}
return false; // 键不存在
}
bool erase(const KeyType& key) {
size_t index = hashFunction(key);
auto &bucket = table[index];
std::unique_lock<std::mutex> lk(bucket.mtx);
for (auto it = bucket.ls.begin(); it != bucket.ls.end(); ++it) {
if (it->first == key) {
bucket.ls.erase(it);
return true;
}
}
return false; // 键不存在
}
};
int main() {
HashMap<int, int> mp;
for (int i = 0; i < 5; i++) {
mp.insert(i, i * 10);
}
for (int i = 0; i < 5; i++) {
int value = -1;
if (mp.get(i, value)) {
cout << i << " key find, value = " << value << endl;
} else {
cout << i << " key not find" << endl;
}
}
for (int i = 0; i < 2; i++) {
mp.erase(i);
}
cout << "==================" << endl;
for (int i = 0; i < 5; i++) {
int value = -1;
if (mp.get(i, value)) {
cout << i << " key find, value = " << value << endl;
} else {
cout << i << " key not find" << endl;
}
}
}
实现 string 类
问题:写一个简单的 String 类,包含构造函数,拷贝构造函数,移动构造函数,拷贝赋值函数,析构函数,通过通过指针初始化,在这个基础上实现一个 append 函数
实现思路:这个问题主要是想考查几种构造函数的写法,然后 append 的函数的话,考虑采用 STL vector 的思想,提前预留一定的空间,当添加的字符串长度超过这个空间的时候进行扩容,下面是我的代码实现,仅供参考
有一些注意点:
- 1)采用 new 分配一个数组的时候,需要配套采用 delete[],析构函数当中需要释放内存;
- 2)拷贝构造函数传递的参数是 const 引用类型,而移动构造函数不是引用类型;
- 3)拷贝、移动赋值运算符需要进行特判,防止自己给自己赋值导致对象失效的问题;
#include <iostream>
#include <string.h>
using namespace std;
class String {
public:
String() : str_(nullptr), size_(0), capacity_(10) {}
String(const char *s) : capacity_(strlen(s) * 2), size_(strlen(s)) {
str_ = new char[capacity_];
strcpy(str_, s);
}
~String() {
if (str_) {
delete[] str_;
}
}
// 深拷贝
String(const String &rhs) : size_(rhs.size_), capacity_(rhs.capacity_) {
str_ = new char[rhs.size_];
strcpy(str_, rhs.str_);
}
String& operator=(const String &rhs) {
if (this == &rhs) {
return *this;
}
str_ = new char[rhs.size_];
strcpy(str_, rhs.str_);
size_ = rhs.size_;
capacity_ = rhs.capacity_;
return *this;
}
String(String &&rhs) : str_(rhs.str_), size_(rhs.size_), capacity_(rhs.capacity_) {
// 销毁rhs对象
rhs.str_ = nullptr;
rhs.capacity_ = 0;
rhs.size_ = 0;
}
String& operator=(String &&rhs) {
if (this == &rhs) {
return *this;
}
str_ = rhs.str_;
capacity_ = rhs.capacity_;
size_ = rhs.size_;
// 销毁rhs对象
rhs.str_ = nullptr;
rhs.capacity_ = 0;
rhs.size_ = 0;
return *this;
}
void append(const char *s) {
int len = strlen(s);
if (len + size_ < capacity_) {
// 注意添加的字符串可能比扩容2倍后还要大,所以这里用循环判断一下
while (len + size_ < capacity_) {
capacity_ *= 2;
}
char *newstr = new char[capacity_ * 2];
strcpy(newstr, str_);
delete[] str_; // 删除旧的字符串
str_ = newstr;
size_ += len;
} else {
strcpy(str_ + size_, s);
size_ += len;
}
}
int size() const { return size_; }
int capacity() const { return capacity_; }
void print() {
if (str_) {
cout << str_;
}
cout << endl;
}
private:
int capacity_ = 0;
int size_ = 0;
char *str_;
};
int main() {
const char *s = "hello ";
String str1(s);
str1.print();
str1.append("world! ");
str1.print();
str1.append("this is a massage");
str1.print();
std::cout << "===================" << std::endl;
String tmp = "this is a string";
String str2 = tmp; // 测试拷贝构造函数
str2.print();
String str3;
str3 = str2; // 测试拷贝赋值运算符
str3.print();
String str4 = std::move(str2); // 测试移动构造函数
str4.print();
cout << "str2 = ";
str2.print(); // str2应该为空
cout << "test str5" << endl;
String str5;
str5 = std::move(str4); // 测试移动赋值运算符
str5.print();
return 0;
}