模板元编程,斐波那契数列
定义一个模板结构体,模板参数是个常量,结构体有个成员 v,用于存储斐波那契数。
1 | template<int N> |
注意, constexpr 用于声明一个常量表达式,明确告知编译器,该表达式可以在编译时期计算出结果。
然后针对 Fib<1> 和 Fib<2> 进行模板特化
1 | template <> |
然后 Fib<N>::v
就是编译期确定的斐波那契数。
下面是完整的例子
1 |
|
定义一个模板结构体,模板参数是个常量,结构体有个成员 v,用于存储斐波那契数。
1 | template<int N> |
注意, constexpr 用于声明一个常量表达式,明确告知编译器,该表达式可以在编译时期计算出结果。
然后针对 Fib<1> 和 Fib<2> 进行模板特化
1 | template <> |
然后 Fib<N>::v
就是编译期确定的斐波那契数。
下面是完整的例子
1 |
|
本文章记录TCP的一些杂七杂八的知识点,比较零碎。
ICMP 可用于发现链路上的最小 MTU
一条 sock 连接有五元组(Proto, SourceIp, DestIp, SourcePort, DestPort) 组成,任意一个改变都是可以是一个新的连接。
比如两台机器都有 2个IP,2个端口,用于TCP连接。那么他们之间可以创建 (2222)2 = 32 个 TCP 连接。
传输层协议除了TCP 还有 UDP/SCTP/DCTP
TCP中的序号 是字节流编号,而不是报文的序号。比如第一个报文序号是0,然后该报文有300个字节的数据,那么第二个报文的序号就该是300
确认号表示期望收到的下一个报文的第一个字节编号是多少,比如面收到第一个报文之后,确认号就回设置为300,表示接下来希望收到第一个字节编号为300的报文
TCP的第一个序号通常假设为0, 但实际上可以随机的选择初始序号
如果发送端收到同一个序号的重复确认3次(冗余ACK)即可认为序号之后的报文已经丢失,可以进行快速重传
TCP 接受方重传的时候可以跳过那些已被选择确认的报文,需要接收方支持选择确认。(SACK,在TCP首部的 options 字段里)
TCP 接受方会维护一个 LastByteRead, 用户层已读取的最后一个字节的编号; LastByteRecv, 放入到缓存区的最后一个字节的编号。还有一个缓存区大小RecvBuffer。滑动窗口大小,就是缓存区大小(Buffer)减去已缓存的大小(Recv-Read)
TCP 发送方会维护一个 LatByteSent, 已发送的序号; LastByteAcked, 已被确认的序号。Sent-Acked 需要小于滑动窗口的大小。来保证接口方的缓存区不会被溢出。
如果接收方缓存区满了之后,窗口会设置为0;之后发送方会发送只有一个字节的报文段,用来 “轮询” 窗口更新。
TCP第三次握手确认的时候可以携带一些数据。
现在的主流操作系统都支持syn-cookie,在第三次握手之前,服务器并不维护客户端的信息, 可以有效的防御syn-flood攻击。
MSS 最大报文段长度,避免物理层分片,通常比MTU小一点。(在options里协商)
RTT 连接往返时间,即发出后到收到ACK的时间。
拥塞控制
TCP 字段
1 | 0 1 2 3 |
在RFC 3168 中 TCP 的 reserved 位置又使用了两位(CWE, ECE 用于处理拥塞控制和显式拥塞通知)
1 | 0 1 2 3 |
TCP与网络编程
C++中,锁是用来管理并发访问共享资源的工具,下面是与锁相关的一些概念。
互斥锁,最基本的锁类型之一,提供了最基本的锁操作,lock()
/unlock()
/try_lock()
;
共享锁,或者叫读写锁, C++17中引入的。允许多个线程同时读取共享锁。
对 mutex 进行了一层封装,更为抽象的封装,RAII风格,为 mutex 的管理类。
原子布尔类型,可用于实现自旋锁。提供有 test_and_set()
/clear()
方法
条件变量不是锁,而是与锁结合使用来实现复杂的线程同步机制。它允许一个线程在条件变量上等待,被唤醒之后先判断表达式的值,如果为真再尝试获取锁。
讲虚继承之前,先讲讲多继承,下面是一个多继承的示例, C 继承了 A 和 B。
1 |
|
先思考下面几个问题
A* a = new C();
, 那么 typeid(*a)
返回的会 A 的信息,还是 C 的信息?void process(A *a);
, 该函数内部,能否访问到 B::funB1
/ B::funB2
吗?如果你对上面的问题了如指掌,建议跳过本文章。
A,B,C 的内存布局与虚函数表如下
可以通过
g++ -fdump-lang-class -c base.cpp
来看到C++ 类的虚函数表和内存布局
我们先以class A 为例, 讲解下虚函数表的内容。
1 | -------------- |
在非虚继承当中,基类的内存布局要在派生类中保证完整性,比如示例中 C 的内存布局可以拆分成两块,一块用来表示子对象A,一块用来表示子对象B。上面的 Offset 原始对象指针的偏移。通常多继承的情况下,第一个子对象在内存布局的最顶部,所以 Offset 为 0,但是之后其它子对象的 Offset 就不为 0 了, 比如示例中的 子对象 B ,其 Offset 就为 -16,子对象B的指针向上偏移16就得到了原始对象的指针,该字段在基类向派生类转换的时候会用到。
首先要知道,派生类到基类的转换,百分百会成功,因为所谓的转换就是对指针进行调整,使其指向子对象的位置,这个动作编译器在编译期间就已经确定了。
1 | void fun(C *c){ |
但是当基类到派生类转换的时候,如果通过基类对象的地址找到其原本派生类对象的地址呢? 这就用到了前面提到的 Top Offset, 基类对象的地址,加上该偏移就得到了原始对象的地址。
1 | int main() { |
下面是一份通过内存布局访问虚函数的代码,在 Compiler Explore 上查看
1 |
|
如果 类 A 和类 B 都继承了一个 Base 类,那么 A 和 B 内部都有了 Base 类的成员。 那么 C 内部岂不是有两份 Base 的数据成员?怎么解决这个问题?这个就讲的了虚继承
虚继承和普通继承的区别,简单来说有两点
下面是一个虚继承下的内存布局与虚函数表示例
注意到 VTT 中有几处空白没有列出来,那几个是构造函数虚表,有兴趣可自行了解。
下面是一份通过内存布局访问虚函数的代码,在 Compiler Explorer上查看
1 |
|
C++ 的 shared_ptr
是什么呢?你真的理解吗?让我们试着回答以下问题。
1 | std::shared_ptr<int> p1; |
void func(Class *A);
的参数可以是 std::shared_ptr<A>
吗?shared_ptr<T>
是线程安全的吗?shared_ptr
?shared_ptr
的释放行为?在 C++ 中,shared_ptr
内部有一个指向动态分配对象的指针和一个指向控制块的指针。指向同一对象的共享指针共享这两个块。
1 | std::shared_ptr<int> p1; // 空对象,数据指针指向空,分配控制块内存,并计数 0 |
make_shared
只分配一次内存,效率更高,且能够有效避免内存碎片的产生。
std::shared_ptr<T>
构造函数中传入了一个对象指针。它同时提供了一个 get()
方法用来返回裸指针,但是在使用裸指针时要注意其生命周期。
shared_ptr
的引用计数是原子操作,因此是线程安全的。但是 shared_ptr
指向的对象以及 shared_ptr
自身并不是线程安全的。在多线程中修改 shared_ptr
指向的数据或修改 shared_ptr
自身的指向可能对其他线程的 shared_ptr
造成破坏。
考虑下面的代码会有什么问题。
1 |
|
运行这段代码会发现 A
会被析构两次。因为 std::shared_ptr<A>(this)
会增加一次引用计数,而返回的 shared_ptr
和之前的 shared_ptr
是没有关联的,所以会各自析构一次。下面的代码有同样的问题。
1 | void test(){ |
enable_shared_from_this
就是为了解决通过对象获取自身的智能指针的问题。
1 |
|
enable_shared_from_this
内部也是通过 weak_ptr
的原理来实现的。
1 |
|