未完每日更新
https://blog.csdn.net/yawdd/article/details/80010148
C++中的内存管理
寄存器:
CPU内部主要由控制器、运算器和寄存器组成。
控制器负责指令的读取和调度,运算器负责指令的运算执行,寄存器负责数据的存储,它们之间通过CPU的内部总线连接在一起。
寄存器拥有非常高的读写速度,可以看作数据在CPU内的一个临时存储的单元。
寄存器的制作难度大,选材精,而且是集成到芯片内部,所价格高。而内存的成本则相对低廉,而且从工艺上来说,我们不可能在CPU内部集成大量的存储单元。
高度缓存Cache
当程序在运行时,就可以预先将部分在内存中要执行的指令代码以及数据复制到高速缓存中去,而CPU则不再每次都从内存中读取指令而是直接从高速缓存依次读取指令来执行,从而加快了整体的速度。
高速缓存又分为一级Cache和二级Cache,一级缓存集成在CPU内部,二级缓存以前焊在主板上,现在也都集成在CPU内部。
Cache成本比寄存器低,但是比内存的制造成本高,容量要比寄存器大,但是比内存的容量小很多。
内存
分为只读存储器(ROM)、随机存储器(RAM)和高速缓存存储器(cache)。
内存具有“掉电信息全部消失”的特性,而外存则具有“掉电信息也不会丢失”的特性。
空间换时间
在软件设计上有一个所谓的空间换时间的概念,就是当两个对象之间进行交互时因为二者处理速度并不一致时,我们就需要引入缓存来解决读写不一致的问题。
比如文件读写或者socket通信时,因为IO设备的处理速度很慢,所以在进行文件读写以及socket通信时总是要将读出或者写入的部分数据先保存到一个缓存中,然后再统一的执行读出和写入操作。
阻塞 与 非阻塞IO
CPU层次:
现代操作系统通常使用异步非阻塞方式进行IO,即发出IO请求之后,并不等待IO操作完成,而是继续执行下面的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式来通知IO操作完成结果。
线程层次:
操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read,write等)以同步的方式展现出来。
而以同步展现的IO又会带来新的问题,即为:
同步阻塞的IO会使线程挂起(后面的指令都等着IO),
同步非阻塞的IO会消耗CPU资源在轮询上
为解决这一问题,有新的解决方案:
多线程(同步阻塞);
IO多路复用(select,poll,epoll)(同步非阻塞,严格地来讲,是把阻塞点改变了位置);
一、内存管理
堆栈是内存中的一个数据结构!!!
1.1、内存布局
栈:
局部变量,函数参数等存储在该区,由编译器自动分配和释放。函数执行结束时这些存储单元自动被释放。
栈的内存空间是连续的,效率很高,但栈的内存空间有限。堆:
需要程序员手动分配和释放,一个new就要对应一个delete。
如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。自由存储区:
由malloc等分配的内存块,和堆十分相似的,不过它是用free来结束自己的生命的。全局/静态存储区:
全局变量和静态变量被分配到同一块内存中,它们共用一块存储区。
全局变量,静态变量分配到该区,到程序结束时自动释放。
包括DATA段(全局初始化区)与BBS段(全局未初始化段):在程序执行前BBS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0。常量存储区
存放的是常量,不允许修改。
举例
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456{post.content}在常量区,p3在栈上
}
1.2、堆和栈的区别
管理方式不同:手动——自动
空间大小不同:不连续,但几乎没有限制——连续,但是有限
产生碎片不同:
生长方向不同:对于堆来说,是向着内存地址增加的方向;对于栈来讲,是向着内存地址减小的方向增长。
分配方式不同:堆都是动态分配的,没有静态分配的堆。
栈的静态分配是编译器完成的,比如局部变量的分配。
栈的动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率不同:
栈是机器系统提供的数据结构,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,它的机制很复杂,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间。
1.3、内存分配的问题
- a.内存分配未成功,却使用了它 :在使用内存之前检查指针是否为NULL。
如果指针p是函数的参数,那么在函数的入口处用assert()
如果是用malloc或new来申请内存,应该用if(p==NULL) 进行防错处理
补充:assert()作用是如果它的条件返回错误,则终止程序执行,一般不使用。
b. 内存分配虽然成功,但是尚未初始化就引用它:创建数组,别忘了赋初值
c. 操作越过了内存的边界:注意下标
d. 忘记了释放内存:malloc与free的使用次数一定要相同(new/delete同理),否则肯定有错误。
e. 释放了内存却继续使用它:用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
1.3、指针与数组
数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”。
若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。
比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。
2 多线程
2.1 Linux环境下的C++多线程编程
什么是临界区
临界区:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
2.1.1 noncopyable
同意程序实现一个不可复制类。
定义一个类时 C++会默认生成 复制构造函数和复制赋值操作符。
class empty_class{
public:
empty_class(const empty_class &){...}
empty_class & operator=(const empty_class &){...}
};
原因:有些对象是独一无二的,作备份不合逻辑。
2.1.2 pthread_mutex_t
linux线程互斥量pthread_mutex_t,当另一个线程也要访问这个变量时,发现这个变量被锁住了,无法访问,它就会一直等待。
2.1.2 pid_t
pid_t是一个typedef定义类型,实际上就是一个int类型。用它来表示进程id类型。
2.1.3 nullptr与NULL
在C++中,一个空指针要么是一个字面值整形,要么是一个std::nullptr_t。
C++中的NULL,其实就是一个0,这会导致很多问题,func(NULL)会去调用void func(int),所以引入nullptr。
2.1.4 boost::bind
TODO:
2.1.5 pthread_cond_t
pthread_cond_t表示多线程的条件变量,用于控制线程等待和就绪的条件。
pthread_cond_destroy用于销毁一个条件变量
2.1.6 __thread
__thread 关键字表示每一个线程都有一份独立的实体,且互不干扰。
只能修饰:基本变量、指针变量、不带自定义构造函数和析构函数的类。
2.1.7 extern
a.C++ primer 4 -- 669 指定使用其他语言编写的函数
b.如果想声明一个变量而非定义它,就在变量名前添加extern关键字
2.1.8 timespec
struct timespec 和 struct timeval 是Linux环境下的两个时间struct,主要用于由函数int clock_gettime(clockid_t, struct timespec *)
获取特定时钟的时间,其中的clockid_t
用以下几种时钟:
- CLOCK_REALTIME 统当前时间,从1970年1.1日算起
- CLOCK_MONOTONIC 系统的启动时间,不能被设置
- CLOCK_PROCESS_CPUTIME_ID 本进程运行时间
- CLOCK_THREAD_CPUTIME_ID 本线程运行时间
timespec有两个成员,一个是秒,一个是纳秒, 所以最高精确度是纳秒。
2.1.9 跨平台的数据格式
数据类型特别是int相关的类型在不同位数机器的平台下长度不同,为了保证平台的通用性,程序中尽量不要使用long数据库型 。
可以用int64_t
来表示C++中的long long
2.1.10 static_cast
static_cast是一个强制类型转换操作符。强制类型转换,也称为显式转换。
double a = 1.999;
int b = static_cast<double>(a);
使用static_cast可以找回存放在void*
指针中的值。
double a = 1.999;
void * vptr = & a;
double * dptr = static_cast<double*>(vptr);
static_cast也可以用在于基类与派生类指针或引用类型之间的转换。然而它不做运行时的检查,不如dynamic_cast安全。
2.1.11 c++ 时间类型
Unix时间戳是一种时间表示方式,定义为从格林威治时间1970年01月01日00时00分00秒起至现在的总秒数。
time_t 这种类型(struct)就是用来存储从1970年到现在经过了多少秒。
2.1.13 静态断言
static_assert(常量表达式,提示字符串)
如果第一个参数常量表达式的值为真(true或者非零值),那么static_assert不做任何事情,就像它不存在一样,否则会产生一条编译错误,错误位置就是该static_assert语句所在行,错误提示就是第二个参数提示字符串。
2.1.14 std::is_same
std::is_same 判断类型是否一致,两个一样的类型会返回true。
2.1.15 栈追踪
获取当前线程的调用堆栈,获取的信息将会被存放在buffer中
int backtrace(void **buffer,int size)
,参数 size 用来指定buffer中可以保存多少个 void*
元素
char ** backtrace_symbols (void *const *buffer, int size)
将从backtrace函数获取的信息转化为一个字符串数组,size是该数组中的元素个数
2.1.16
2.1.50 内联函数 inline
大多数机器上,调用函数要做很多工作,调用前要先保存寄存器,并且在返回时恢复。程序还必须转向一个新的位置。
内联函数保证他在每一个调用节点上,内联地展开,适用于小但是调用很频繁的函数
2.2 强引用与弱引用
2.2.1 weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象.
不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
a.升级
如果对象存在,weak_ptr 的 lock()函数返回一个指向共享对象的shared_ptr,此时如果原来的shared_ptr被销毁,则该对象的生命期将被延长至这个临时的shared_ptr同样被销毁为止。
如果weak_ptr指向的对象被销毁,否则返回一个空shared_ptr。
b.打破循环引用
class ClassA
{
public:
ClassA() {}
~ClassA() {}
private:
weak_ptr<ClassB> pb; // 在A中引用B
};
class ClassB
{
public:
ClassB() {}
~ClassB() {}
private:
weak_ptr<ClassA> pa; // 在B中引用A
};
int main() {
shared_ptr<ClassA> spa = make_shared<ClassA>();
shared_ptr<ClassB> spb = make_shared<ClassB>();
// 因为没改变shared_ptr的引用计数,此时引用计数为1,超过作用域后自动释放
}
因为没改变shared_ptr的引用计数,此时引用计数为1,超过作用域后自动释放。
2.2.2 enable_shared_from this
int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);
此时这两个shared_ptr指针,所以当其中一个在析构时有可能资源已经被释放了。
Similarly, if a member function needs a shared_ptr object that owns the object that it's being called on, it can't just create an object on the fly:
struct S : enable_shared_from_this<S>
{
shared_ptr<S> not_dangerous()
{
return shared_from_this();
}
};
When you do this, keep in mind that the object on which you call shared_from_this must be owned by a shared_ptr object.
2.2.3 临界区
临界区指的是一个访问共用资源的程序片段
2.2.4 copy on write——COW
有多个调用者使用相同的资源时,他们会共同获取相同的指针指向相同的资源。内核此时并不复制。
直到某个调用者试图修改资源的内容时,内核才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
这是一种种延时懒惰策略。
2.2.5 unique_ptr
unique_ptr 独占所指向的对象, 同一时刻只能有一个 unique_ptr 指向给定对象,在 unique_ptr 离开作用域时释放该对象。
在下列两者之一发生时用关联的删除器释放对象:
- 销毁了管理的
unique_ptr
对象 - 通过
operator=
或reset()
赋值另一指针给管理的unique_ptr
对象。
shared_ptr的unique函数:检查所管理对象是否仅由当前 shared_ptr 的实例管理
2.2.5 std::const_iterator
const_iterator 可遍历,不可改变所指元素
const iterator 不可遍历,可改变所指元素:iterator本身里面存的是指针,指针不能改变,也就是不能指向其他的位置
2.2.6 shared_ptr 的线程安全性
同一个shared_ptr对象可以被多线程同时读取。
不同的shared_ptr对象可以被多线程同时修改(即使这些shared_ptr对象管理着同一个对象的指针)
2.2.7 size_t
与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。
size_t在32位架构上是4字节,在64位架构上是8字节.
2.2.8 implicit_cast
struct top {}; //最顶层的父类
struct mid_a : top {};
struct mid_b : top {};
struct bottom : mid_a, mid_b {}; //最底层的派生类
之前的static_cast太强大了, 强大到可以进行”down-cast”. 于是编译器没有任何的警告, 就可以把一个top类型的引用给强制转换成了min_a的引用.
在C++世界的英文里, 我们说”convert”通常指”implicit convert”, 而”cast”指explicit cast(显式)
implicit cast——隐式cast
2.3 两个类互相引用
根本原因:定义A的时候,A的里面有B,所以就需要去查看B的占空间大小,但是查看的时候又发现需要知道A的占空间大小,造成死循环。
2.3.1 解决方案
#include <MutexLock.h>
#include <set>
class Request;
class Inventory {
public:
void add(Request *request) {
//
};
private:
mutable MutexLock mutexLock;
std::set<Request *> requests;
};
在Inventory.h中,采用的class Request的前置声明,但是在class Inventory的声明中只能定义Class Request的指针或引用。
原理:虽然在B的定义文件中并没有导入A的头文件,不知道A的占空间大小,但是由于在B中调用A的时候用的指针形式,B只知道指针占4个字节就可以,不需要知道A真正占空间大小。
2.3.2 前置声明
2.3.3 字节的占用
2.3.4 C++类的this
2.3.4 boost::function()
2.3.5 mangle和demangle
ABI是Application Binary Interface的简称。 C/C++发展的过程中,二进制兼容一直是个问题。不同编译器厂商编译的二进制代码之间兼容性不好,甚至同一个编译器的不同版本之间兼容性也不好。
C/C++语言在编译以后,函数的名字会被编译器修改,改成编译器内部的名字,这个名字会在链接的时候用到。
识别C++编译以后的函数名的过程,就叫demangle。
应用:打印异常日志
2.3.6 volatile
在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。
2.3.10 RTTI
2.X 之补充 — 移动右值引用 ( C++11特性 )
2.X.1右值引用
a.判断左值与右值
典型情况下左值和右值可以通过在赋值表达式中的位置进行判断,在等号左边的为左值、等号右边的为右值
另外一个判别方法是:可以取地址、有名字的就是左值,否则就是右值。b.左值与右值的区别
左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象
int b = 20; //这里b是左值 20是右值 ,因为这个表达式过后 20将不存在了 而b依然存在
c.右值引用的初始化
右值引用,是对临时对象的一种引用,它是在初始化时完成引用的,右值引用可以在初始化后改变临时对象的值。
int &&i = 1;
i绑定到了右值1
2.X.2 移动MOVE
int i = 1;
int&& rr = std::move(i);
移动操作窃取了对象资源的控制权,从而避免了不必要的拷贝。
2.X.3 四行代码的故事
第一行: int i = getVar();
这行代码会产生两种类型的值,一种是左值i,一种是函数getVar()返回的临时值,这个临时值在表达式结束后就销毁了,而左值i在表达式结束后仍然存在,这个临时值就是右值
第二行: T&& k = getVar();
getVar()产生的临时值不会像第一行代码那样,在表达式结束之后就销毁了,而是会被“续命”,他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。
第三行: T(T&& a) : m_val(val){ a.m_val=nullptr; }
移动构造函数
class A
{
public:
A() :m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
{
cout << "copy construct" << endl;
}
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
输出:
construct
move construct
move construct
输出结果表明,并没有调用拷贝构造函数,只调用了移动构造函数,它的参数是一个右值引用类型。
std::list< std::string> tokens;//省略初始化...
std::list< std::string> t = tokens; //这里存在拷贝
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens); //这里没有拷贝
使用move几乎没有任何代价,只是转换了资源的所有权。他实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的堆内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。
第四行:
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
processValue(std::forward<T>(val)); //照参数本来的类型进行转发。
}
void Testdelcl()
{
int i = 0;
forwardValue(i); //传入左值
forwardValue(0);//传入右值
}
输出:
lvaue
rvalue
按照参数的实际类型进行转发。
2.X.4 深拷贝与浅拷贝
浅拷贝是增加了一个指针,指向原来已经存在的内存。
PS:要注意浅拷贝的指针被析构两次。
而深拷贝是增加了一个指针,并新开辟了一块空间。
2.4 Reactor模式——一种高性能IO
2.4.1 原始的网络编程思想
while(true){
socket = accept();
new thread(socket);
}
while循环不断监听端口是否有新的套接字连接,配合多线程,每一个连接用一个线程处理。
缺点:如果连接数太高,系统无法承受,如果使用线程池,会导致线程的粒度太大。每一个线程把一次交互的事情全部做了。
2.4.2 基于事件驱动
应该把一次连接的操作分为更细的粒度,这些更细的粒度是更小的线程。整个线程池的数目会翻倍,但是线程更简单,任务更加单一。
这其实就是Reactor出现的原因,在Reactor中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。
2.5 无锁化编程
2.5.1 RAM与ROM
RAM:随机存取存储器random access memory,是与CPU直接交换数据的内部存储器,也叫内存。它可以随时读写,而且速度很快。当电源关闭时RAM不能保留数据。
ROM:ROM是Read Only Memory的缩写
2.5.2 原子操作
锁的缺点:lock锁的是FSB,前端串行总线,这个FSB是处理器CPU和RAM之间的总线,锁住FSB,就能阻止其他处理器从RAM获取数据。当然这种操作开销相当大,只能操作小的内存可以这样做,想想有memcpy,如果操作一大片内存,锁内存,那么代价太大了。
原子操作:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
_sync_fetch_and_add,先fetch,然后自加,返回的是自加以前的值。
以count = 4为例,调用__sync_fetch_and_add(&count,1)
之后,返回值是4,然后,count变成了5.
__sync_bool_compare_and_swap
等
三、IO多路复用
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
后面的fd均指文件描述符
3.1、流
不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
3.2、阻塞与非阻塞 (线程层次)
可以简单理解为调用一个IO操作能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了
3.3、同步与异步
同步IO操作将导致请求的进程一直被blocked,直到IO操作完成。从这个层次来,阻塞IO、非阻塞IO操作、IO多路复用都是同步IO。
3.4、多路复用IO
阻塞I/O有一个比较明显的缺点是在I/O阻塞模式下,一个线程只能处理一个流的I/O事件。
多路复用IO也是阻塞IO,只是阻塞的方法是select/poll/epoll。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理是select/epoll这个函数会不断轮询所负责的IO操作,当某个IO操作有数据到达时,就通知用户进程。然后由用户进程去操作IO。
3.6、Linux的用户空间(User space)和内核空间( Kernel space)
x86 CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。
Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。
3.6、select
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。
仅仅知道有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
1024- 32 || 2048 -64
3.7、poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历。
如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。
它没有最大连接数的限制,原因是它是基于链表来存储的
3.8、epoll
Epoll最大的优点就在于它只管“活跃”的连接,而跟连接总数无关。
3.9、
3.9.1、__builtin_expect
指令的写法为:__builtin_expect(EXP, N)
,意思是:EXP==N的概率很大。
__builtin_expect() 是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。
例子:GCC编译的指令会优先读取 y = -1
int x, y;
if(unlikely(x > 0))
y = 1;
else
y = -1;
3.9.1、GCC
GCC(GNU Compiler Collection,GNU编译器套件),是由 GNU 开发的编程语言编译器。
四、网络编程
4.1 UNIX环境高级编程
4.1.1 readv和writev函数
#include<sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
//若成功则返回已读,写的字节数,若出错则返回-1。
4.1.2 C++中的字节序
字节序的含义:大于一个字节类型的数据在内存中的存放顺序。比如short 或者int在不同的字节序存储结果是不一样的。
大字节序(Big-Endian):高位字节排放在内存的低地址端,低位字节排放在内存的高地址端
小字节序(Little-Endian):低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
4.1.2 char 与 uint8_t
一个是字符类型,一个是超短无符号整型,他们唯一一样的地方就是占内存大小一样。
序列化:将对象或数据结构转为字节序列的过程。
4.1.3 reinterpret_cast
Reinterpret_cast:编译器不会做任何检查,截断,补齐的操作,只是把比特位拷贝过去.
所以 reinterpret_cast 常常被用作不同类型指针间的相互转换,因为所有类型的指针的长度都是一致的(32位系统上都是4字节),按比特位拷贝后不会损失数据.
2.6 event loop
所有同步任务都在主线程上执行
异步任务指的是,不进入主线程、而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
主线程从"任务队列"中读取事件,这个过程是循环不断的。
为什么不能用if来等待条件变量?spurious wakeup
event loop
计算机的内存回收算法
HTTP中的GET和POST
C++ 头文件引用的库在cpp文件里可以引用吗
new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
important C++的编译问题
tcpdump WireShark