static的作用
? static修饰变量只能在本范围内可见(由external变为internal,作用域和链接属性并没有改变):修饰全局变量只能在本cpp文件中可见,修饰局部变量只能在该代码块内可见。修饰类的静态成员在类的对象中共享这一份数据。
? 其实就是一个类,当销毁指向的内存时,可以不用手动free内存,它会自动释放内存空间。
auto_ptr
unique_ptr
shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
? C++ 的全局对象的构造函数会在 main 函数之前先运行,其实在 c 语言里面很早就有啦,在 gcc 中可以使用 attribute 关键字指定如下(在编译器编译的时候就绝决定了)
? 它们的底层都是用红黑树实现的,关于红黑树和avl树的区别(首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!
? extern void memset(void buffer, int c, int count) ;
+ buffer:指针或者数组
+ c:赋给buffer的值
+ count:buffer的长度
? 一般用来给一段内存空间全部设置为某个字符。
? 管道、系统IPC(信号、信号量、共享内存、消息队列)、套接字socket
互斥量、信号量、临界区
每个进程的地址空间是独立的,位于一个进程的普通内存区域中的对象是无法被其它进程所访问的,能满足这一要求的内存区域是共享内存,因而同步对象要在进程的共享内存区域内创建。同步对象还可以放在文件中。同步对象可以比创建它的进程具有更长的生命周期。
1. std::lock_guard
2. std::unique_lock
3. std::condition_variable
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <mutex>
using std::thread;
using std::vector;
using std::cout;
using std::endl;
using std::mutex;
class Incrementer
{
private:
int counter;
mutex m;
public:
Incrementer() : counter{0} { };
void operator()()
{
for(int i = 0; i < 100000; i++)
{
this->m.lock();
this->counter++;
this->m.unlock();
}
}
int getCounter() const
{
return this->counter;
}
};
int main()
{
// Create the threads which will each do some counting
vector<thread> threads;
Incrementer counter;
threads.push_back(thread(std::ref(counter)));
threads.push_back(thread(std::ref(counter)));
threads.push_back(thread(std::ref(counter)));
for(auto &t : threads)
{
t.join();
}
cout << counter.getCounter() << endl;
return 0;
}
运行结果
修改其中代码
for(int i = 0; i < 100000; i++)
{
this->m.lock();
try
{
this->counter++;
this->m.unlock();
}
catch(...)
{
this->m.unlock();
throw;
}
}
#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
std::mutex g_mutex; // 用到的全局锁
std::condition_variable g_cond; // 用到的条件变量
int g_i = 0;
bool g_running = true;
void ThreadFunc(int n) { // 线程执行函数
for (int i = 0; i < n; ++i) {
{
std::lock_guard<std::mutex> lock(g_mutex); // 加锁,离开{}作用域后锁释放
++g_i;
std::cout << "plus g_i by func thread " << std::this_thread::get_id() << std::endl;
}
}
std::unique_lock<std::mutex> lock(g_mutex); // 加锁
while (g_running) {
std::cout << "wait for exit" << std::endl;
g_cond.wait(lock); // wait调用后,会先释放锁,之后进入等待状态;当其它进程调用通知激活后,会再次加锁
}
std::cout << "func thread exit" << std::endl;
}
int main() {
int n = 100;
std::thread t1(ThreadFunc, n); // 创建t1线程(func thread),t1会执行`ThreadFunc`中的指令
for (int i = 0; i < n; ++i) {
{
std::lock_guard<std::mutex> lock(g_mutex);
++g_i;
std::cout << "plus g_i by main thread " << std::this_thread::get_id() << std::endl;
}
}
{
std::lock_guard<std::mutex> lock(g_mutex);
g_running = false;
g_cond.notify_one(); // 通知其它线程
}
t1.join(); // 等待线程t1结束
std::cout << "g_i = " << g_i << std::endl;
}
首先,这在一个局部作用域内, std::lock_guard 在构造时,会调用 g_mutex->lock() 方法;
局部作用域代码结束后, std:;lock_guard 的析构函数会被调用,函数中会调用 g_mutex->unlock() 方法。
当线程调用 g_cond.wait(lock) 前要先手动调用 lock->lock() ,这里是通过 std::unique_lock 的构造方法实现的;
当线程调用 g_cond.wait(lock) 进入等待后,会调用 lock->unlock() 方法,所以这也是前面构造lock时使用了 std::unique_lock ;
通知使用的 g_cond.notify_one() ,这个可以通知一个线程,另外还有 g_cond.notify_all() 用于通知所有线程;
线程收到通知的代码放在一个while循环中,这是为了防止APUE中提到的虚假通知。
从 实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。所以,自旋锁一般用用多核的服务器。
int num = 0;
spin_mutex sm;
void thread_proc()
{
for(int i = 0; i < 100000; ++i) {
sm.lock();
++num;
sm.unlock();
}
}
int main()
{
std::thread td1(thread_proc), td2(thread_proc);
td1.join();
td2.join();
std::cout << num << std::endl;
return 0;
}
windows系统中临界区(Critical Section)、事件对象(Event)
nullptr
类型推导 auto(不能用于推导数组类型,不能用于函数传参) 和decltype关键字
有的时候我们只需要计算表达式得出的类型,不需要返回值
auto x = 1;
auto y = 2;
decltype(x+y) z;
拖尾返回类型、auto 与 decltype 配合,利用 auto 关键字将返回类型后置:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
初始化列表
struct A {
int a;
float b;
};
struct B {
B(int _a, float _b): a(_a), b(_b) {}
private:
int a;
float b;
};
A a {1, 1.1}; // 统一的初始化语法
B b {2, 2.2};
Lambda表达式
提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
[ caputrue ] ( params ) opt -> ret { body; };
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
int a = 0;
auto f = [=] { return a; };
a+=1;
cout << f() << endl; //输出0
int a = 0;
auto f = [&a] { return a; };
a+=1;
cout << f() <<endl; //输出1
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。
#include<iostream>
using namespace std;
class coord {
int x, y;
public:
coord(int i = 0, int j = 0)
{
x = i;
y = j;
}
friend ostream& operator<<(ostream &stream, coord &ob);//这里第二个参数采用了引用(&ob),
//是为了减少调用的开销,使用引用参数只需把对象的地址传进来就可以了,而不需把每个域分量逐一传进来
//而消耗内存和时间。所以不用普通的对象做参数,虽然结果一样。但是<<重载的函数返回值和第一个参数必须为输出流类ostream的的引用。
friend istream& operator>>(istream &input, coord &ob);//这里的第二个参数必须为引用,目的是函数体对参数a的修改能影响实参,因为从输入
//流输入的值要存入与a对应的实参中。注意重载输出<<时的作用并不是为了修改实参,此点不同。
};
ostream & operator<<(ostream &stream, coord &ob)
{
stream << ob.x << "," << ob.y << endl;//stream为ostream类的一个对象的引用,作为左操作数(cout也是一样,是C++中的两个流对象)
return stream;
}
istream& operator>>(istream &input, coord &ob)
{
cout << "Enter x and y value:";
input >> ob.x;
input >> ob.y;
return input;
}
int main()
{
coord a(55, 66), b(100, 220);
cout << a << b;
cin >> a;
cin >> b;
cout << a << b;
return 0;
}
分析:上面输出重载函数的形参stream是ostream类对象的引用,返回值也是ostream类对象的引用。在main中cout<<a;cout是ostream类对象,a是coord类对象,所以可以把其理解为operator<<(cout,a);
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
ostream& operator<<( ostream & os)
{
os<<"英寸:"<<feet<<"\n英尺:"<<inches;
return os;
}
};
int main ()
{
Distance d1(20,18);
d1<<cout;//相当于d1.operator<<(cout)
}
容器
vector向量:类似数组操作。
size 是当前 vector 容器真实占用的大小,也就是容器当前拥有多少个容器。
capacity 是指在发生 realloc 前能允许的最大元素数,即预分配的内存空间。
当然,这两个属性分别对应两个方法:resize() 和 reserve()。
使用 resize() 容器内的对象内存空间是真正存在的。
使用 reserve() 仅仅只是修改了 capacity 的值,容器内的对象并没有真实的内存空间(空间是"野"的)。
#include <iostream>
#include <vector>
using std::vector;
int main(void)
{
vector<int> v;
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
v.reserve(10);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
v.resize(10);
v.push_back(0);
std::cout<<"v.size() == " << v.size() << " v.capacity() = " << v.capacity() << std::endl;
return 0;
}
针对 capacity 这个属性,STL 中的其他容器,如 list map set deque,由于这些容器的内存是散列分布的,因此不会发生类似 realloc() 的调用情况,因此我们可以认为 capacity 属性针对这些容器是没有意义的,因此设计时这些容器没有该属性。
在 STL 中,拥有 capacity 属性的容器只有 vector 和 string。
+ 阻塞IO(Blocking IO)
? 在这个例子中,我们会通过UDP而不是TCP来举例,因为对于UDP来说,等待数据就绪这一步更加直观:要不就是收到了一个数据报,要不就是没收到一个数据报.但是对于TCP来说,还有很多额外的变量.
上图中的recvfrom是一个系统调用.当我们执行一次系统调用的时候,有一次从用户态到内核态的切换.
从上图中我们可以看到,进程调用recvfrom之后,这个系统调用并不会立即返回,它会等到数据报到达并且被拷贝到应用程序的缓冲区中,或者出现了一个错误,才会返回.我们称这个过程是阻塞的,应用程序只有在数据报被放入缓冲区之后,才能继续进行.
非阻塞IO(Nonblocking IO)
非阻塞IO和阻塞IO相对,它会告诉内核,"当我要你完成的IO操作不能完成时,不要让进程阻塞,你给我返回一个错误就行了".过程如下图所示:
在上面的三个recvfrom操作中,由于数据并没有就绪,所以内核返回了一个EWOULDBLOCK错误.在第四个recvfrom中,数据已经就绪了,并且已经被拷贝到我们的应用程序的缓冲区了,内核返回一个OK,然后我们的应用程序处理这些数据.
我们可以看到,在这种模型中,我们需要使用轮询的方式来确定数据到底是否就绪.尽管这会浪费CPU时间,但是仍然是比较常见的模型,一般是在系统函数中用到.
I/O复用(I/O Multiplexing)
在I/O多路复用中,我们会调用select()或者poll(),并且阻塞在这两个系统调用上.而不是阻塞在recvfrom这个实际的IO操作的系统调用上.下面是I/O多路复用模型的过程图:
从上图中,我们可以看到,我们会阻塞在select()这个系统调用上,并等待数据到达.当select()告诉我们数据到达时,再通过recvfrom系统调用将数据拷贝到应用程序的缓冲区.多了一次系统调用,确实是I/O多路复用模型的缺点.但是存在即合理,它也有优点.
它的优点在于,select可以同时监听多个文件描述符,以及感兴趣的事件.所以,我们可以在一个线程中完成之前需要好多个线程才能完成的事情.
比如,我们想要同时从一个接受来自Socket的数据,以及从文件中读数据.在阻塞IO模型中,我们会这么做:
1.创建一个线程A,在其中创建一个Socket Server,并通过它的accept()方法,等待客户端的连接并处理数据
2.创建一个线程B,在其中打开文件并且读数据.
这就需要两个线程,对吧?
而且我们又知道,线程之间的切换是有开销的,也是需要涉及到用户态到内核态的转换.
而我们在I/O多路复用模型中,可以这样做:
1.通过注册函数告诉系统,应用程序对于Socket的读事件以及文件的读事件感兴趣
2.通过轮询调用select()方法,查看哪些我们感兴趣的事件已经发生了
3.在同一个线程中,依次进行对应的操作
我们可以看到,在这里我们只需要用一个线程就可以做到在阻塞IO中我们需要两个线程才能做到的事情.这就是I/O复用中的复用的含义.
信号驱动IO(signal driven I/O)
信号驱动IO使用信号量机制,它告诉内核,当文件描述符准备就绪时,通过SIGIO信号通知我们.过程如下:
我们首先通过sigaction系统调用安装一个事件处理器.这个操作会立即返回.所以我们的应用程序会继续运行,而不会阻塞.当数据准备就绪时,内核会给我们的应用程序发出一个SIGIO信号,我们可以继续进行下面的处理:在信号处理器中,通过recvfrom系统调用将数据从内核缓冲区读取到应用程序缓冲区中,告诉应用程序从缓冲区读取数据并且处理.这种模型的优点是,在等待数据就绪时,应用程序并不会被阻塞.应用程序可以继续运行,只需要在数据就绪时,让时间处理器通知它即可.
异步IO(Asynchronous IO)
异步IO模型跟事件驱动IO模型类似,也是告诉内核,在一定情况下通知我们.但是它跟事件驱动IO模型不同的是,在事件驱动IO模型中,内核会在数据就绪,即数据被拷贝到内核缓冲区时,通知我们.而在异步IO中,内核会在整个操作都被完成,即数据从内核缓冲区拷贝到应用程序缓冲区时,通知我们.如下图所示:
原文:https://www.cnblogs.com/ustc-BlueSky/p/12535622.html