C++一些新属性以及简单的使用例子

(1)线程
执行处理器调度的基本单位。程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。
(2)进程 资源分配的基本单位,也可能作为调度运行的单位。进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。


C++11 新特性
C++11新标准多线程支持库
< thread > : 提供线程创建及管理的函数或类接口;
< mutex > : 为线程提供获得独占式资源访问能力的互斥算法,保证多个线程对共享资源的同步访问;
< condition_variable > : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行;
< future > : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常;
< atomic > : 为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。

#####################################STL
std::vector
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。
因此能高效的进行随机存取,时间复杂度为o(1);
但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

std::list
list是由双向链表实现的,因此内存空间是不连续的。
只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);
但由于链表的特点,能高效地进行插入和删除。

注意:
vector容器有动态扩容的功能,每当容器容量不足时,vector就会进行动态扩容,动态扩容不是在原来的空间后面追加空间,而是在寻找一段新的更大的空间,把原来的元素复制过去。
但是这样一来,容器存储元素的位置就改变了,原来的迭代器还是指向原来的位置,因此每次进行动态扩容后原来的迭代器就会失效。

begin()和end()
顾名思义,begin()就是指向容器第一个元素的迭代器 如果你是初学者,你可能会猜到 end()是指向容器最后一个元素的迭代器, 但事实并非如此,实际上,end()是指向容器最后一个元素的下一个位置的迭代器

vector<类型>().swap(队列变量名) //清空队列内存

 

 


#############线程 (编译时要带 -lpthread)
(1). std::thread
std::thread(方法, ...) // 后面匹配的带参跟线程函数的参数个数以及类型一致
例如:
void print(){}
std::thread(print); // 创建一个线程,线程的运行方法是print, 无参数

int print(int num, void *arg){ return 0; }
int num = 8;
std::thread(print, num, &num); // 创建一个线程,线程的执行方法是print, thread()的参数跟print的参数的类型要一致


#############互斥锁
(2). std::mutex
lock_guard (在构造函数里加锁,在析构函数里解锁), 不支持手动解锁, 只能等待作用域执行完成后自动解锁, 轻量级锁
unique_lock 自动加锁、解锁, 可支持手动解锁, 锁住的作用域可选择; 重量级锁(支持的功能多)
lock_guard 是不可移动的(moveable),即不能拷贝、赋值、移动,只能通过构造函数初始化和析构函数销毁,unique_lock 是可移动的,可以拷贝、赋值、移动。

例如:
std::mutex mutex;
{
// 上锁,临界区资源操作, {}作用域完成后自动解锁
lock_guard<std::mutex> gLock(mutex);
}

{
// 上锁,临界区资源操作, {}作用域完成前可手动解锁,作用域完成后即使没有解锁也会自动解锁
unique_lock<std::mutex> uniqueLock(mutex);
uniqueLock.unlock();

}


####################条件变量
std::condition_variable
条件变量结合互斥锁使用, 提高多线程的效率, 线程在等待时, 进入休眠状态, 等到唤醒之后再从CPU调度出来
常用方法:
wait(); // 结合unique_lock锁使用, 表示改线程进入休眠等待
notify_all(); //唤醒全部线程
notiyfy_one(); // 唤醒默认等待的线程, 不确定是唤醒哪一个

例如:
std::mutex m_mutex;
std::condition_variable m_cv;
std::atomic<bool> m_atomic(false);

void workThread(int id)
{
std::unique_lock<std::mutex> uqLock;
while ( !m_atomic.load() )
{
m_cv.wait(unlock); // 阻塞等待
}

std::cout <<" Thread " << id << " Run.\n";
}

int main(void)
{
std::thread th[5];
for ( int i=0; i<5; i++ )
{
th[i] = std::thread(workThread, i);
}

std::this_thread::sleep_for(std::chrono::milliseconds(5));
// 唤醒全部线程
m_cv.notify_all();

for ( auto &obj: th )
{
obj.join();
}
return 0;
}

 

#############原子操作(多线程并发操作变量, 数据保护处理, 同步处理, 可避免互斥锁的使用,提高效率)
std::atomic_flag
atomic_flag 一种简单的原子布尔类型,只支持两种操作,test-and-set 和 clear。
如果在初始化时没有明确使用 ATOMIC_FLAG_INIT初始化,那么新创建的 std::atomic_flag 对象的状态是未指定的(unspecified)(既没有被 set 也没有被 clear。)
另外,atomic_flag不能被拷贝,也不能 move 赋值。
ATOMIC_FLAG_INIT: 如果某个 std::atomic_flag 对象使用该宏初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。

std::atomic<T>
大家可以把原子操作理解成一种:不需要用到互斥量加锁(无锁)技术的多线程并发编程方式
原子操作:在多线程中 不会被打断的 程序执行片段;原子操作,比互斥量效率上更胜一筹。
互斥量的加锁一般是针对一个代码段(几行代码),而原子操作针对的一般是一个变量,而不是一个代码段;
原子操作: 一般都是指"不可分割的操作";也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态;

成员函数:
store 用非原子参数替换原子对象的值
load 获取原子对象的值
exchange 交换两个原子对象的值

例如:
// 两个线程交替给全局变量赋值
std::atomic<int> m_atoNum(0);
int thread3(int num)
{
for ( int i=0; i<8; i++ )
{
m_atoNum++;
printf("thread %d - m_atoNum: %d\n", num, m_atoNum.load());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}

int main(void)
{
std::thread th1(thread3, 0);
std::thread th2(thread3, 1);
th1.join();
th2.join();
return 0;
}

#########################RALL机制(将资源的生命周期与对象的生命周期所绑定(构造获取资源/析构释放资源,利用了栈上的变量在离开作用域的时候会析构的特性)
RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,
最后在对象析构的时候,释放构造时获取的资源
我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。
由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了.

1. 资源创建
2. 资源使用
3. 资源销毁

关于互斥锁和条件变量:
互斥量可以保护共享数据的修改,如果线程正在等待共享数据的某个条件出现,仅用互斥量的话就需要反复对互斥对象锁定解锁,以检查值的变化,这样将频繁查询的效率非常低。
条件变量可以让等待共享数据条件的线程进入休眠,并在条件达成时唤醒等待线程,提供一种更高效的线程同步方式。条件变量一般和互斥锁同时使用,提供一种更高效的线程同步方式。

#######################智能指针(智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确的销毁动态分配的对象,防止内存泄漏)
头文件#include <memory>
unique_ptr特性 (属性为对象, 而不是指针, 管理指针)
(1).基于排他所有权模式:两个指针不能指向同一个资源
无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
在容器中保存指针是安全的
注意: 一个裸指针在丢给一个unique_ptr对象之前,该unique_ptr认为对该指针所有权是唯一的,当多个unique_ptr对象引用同一个裸的指针是无法检查该指针被谁引用

 

 

(2). unique_ptr做为STL容器的元素时,不能作为类的成员变量; share_ptr可以作为类成员嵌套使用
例如:
class b{
private:
vector<unique_ptr<int>> temp; //错误
vector<unique_ptr<a>> tmp; //错误
}

(3). unique_ptr做为STL容器的元素时,不能直接进行传递,因为不可以进行拷贝和赋值操作.
vector<unique_ptr<int>> a;
a.push_back(new int(20)); //错误
a.emplace_back(new int(20)); //错误
a.emplace_back(make_unique<int>(20)); //错误

(4). unique_ptr 初始化不能传递栈上的数据,否则最后释放指针时, 会释放无效的指针;
例如<1>:
int num= 3;
unique_ptr<int> ptr(&num); //错误,此处不能赋值栈的信息, 虽然能编译过,但是在智能指针释放时会报释放无效的指针;

例如<2>:
T *pAbs = new T;
// 初始化智能指针ptr2
unique_ptr<T> ptr2;
ptr2.reset(pAbs);
printf("ptr2.get: %p\n", ptr2.get());

// 初始化智能指针ptr3
unique_ptr<T> ptr3;
ptr3.reset(pAbs);
printf("ptr3.get: %p\n", ptr3.get());

if ( ptr2 == ptr3 )
{
printf("ptr2 = ptr3\n");
printf("ptr2.point: %p\n", ptr2.get());
}

注意: 当两个对象引用同一个裸指针时, 无法检测该内存给那个对象使用, 因此当作用域结束后, 会造成指针的二次释放问题(double free)

修改如下:
unique_ptr<T> ptr3;
ptr3.reset(ptr2.release()); // 此时ptr2对象已经跟裸指针解绑, 并且该对象被释放, 转交裸指针的控制权给ptr3
printf("ptr3.get: %p\n", ptr3.get());


unique_ptr的函数:
1. get(); // 返回对象管理的裸指针,带有风险性
2. release(); // 释放,调用后智能指针和其所指向对象的联系再无联系,但是该内存仍然存在有效。它会返回裸指针,但是该智能指针被置空。
返回的裸指针我们可以手工delete来释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值。
3. reset();
reset()不带参数情况:释放智能指针所指向的对象(释放因为它是独占,而不像shared_ptr还需要考虑引用计数),并将智能指针置空。
reset()带参数时:释放智能指针所指向的对象,并将该智能指针指向新对象。

4. swap(); // 交换智能指针

注意, 以下赋值方法禁止, 因此禁止拷贝和赋值;
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

 

 

 

例如:
unique_ptr<string> pointer(new string("123456"));
unique_ptr<string> pointer2(new string("888888"));

pointer = pointer2; // 非法, 禁止左值赋值操作
unique_ptr<string> pointer3(pointer2); // 禁止左值赋值构造

unique_ptr<string> p3(std::move(p1)); // 合法, std::move()将左值改变成右值
p1 = std::move(p2); // 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

 

posted @   蔡头一枚  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示