2019年5月

一、蓝牙模式

第一步:打开鼠标电源开关。

第二步:长按蓝牙键3秒以上,红灯慢闪,鼠标在2分钟内将处于可被搜索状态。

第三步:在设备上进行蓝牙配对。

配对第二个设备

短按蓝牙键0.5秒切换信道,然后按照上面的步骤重新配对新设备。

二、接收器模式

将接收器插入到电脑即可。

三、当前工作模式

拿起鼠标时,如果红灯常亮65秒,当前连接的是蓝牙设备1;如果红灯慢闪,当前连接的是蓝牙设备2;如果指示灯熄灭,当前连接的是2.4GHz接收器连接的设备。

如果使用鼠标时,红灯会每隔2秒快速闪烁2次。

一、测试文件状态

shell中的测试,如果成功返回0,否则返回1。

操作符描述
-e FILE测试文件是否存在
-f FILE文件存在且是一个常规的文件为真
-d FILE文件存在且是一个目录为真
-[r,w,x] FILE文件是否可读、可写或者可执行

二、字符串比较

操作符描述
-z STR字符串为空返回真
-n STR字符串不为空返回真
STR1 = STR2字符串相等返回真
STR1 != STR2字符串不相等返回真
STR1 [<,>] STR2字符串STR1小于或者大于STR2返回真,按字典序比较

使用[]判断时,><符号需要加下划线:

> [ "abcde" \> "abcd" ] && echo "yes"
yes

使用[[]]判断时,><符号不需要加下划线:

> [[ "abcde" > "abcd" ]] && echo yes
yes

三、整数比较

操作符描述
INT2 -eq INT2两个整数相等返回真
INT1 -ne INT2两个整数不相等返回真
INT1 -[gt,ge,lt,le] INT2整数大于、大于等于、小于以及小于等于判断

四、注意事项

><符号只能用于字符串比较,不能用于整数比较。对整数使用大于小于符号比较的时候,会被当成字符串:

> [[ 53 -gt 153 ]] && echo yes
> [[ 53 > 153 ]] && echo yes
yes

来源:力扣(LeetCode)

链接:234. 回文链表

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

一、题目描述

请判断一个链表是否为回文链表。

示例 1:

输入:1->2
输出:false

示例 2:

输入:1->2->2->1
输出:true

进阶:

你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

二、题解

2.1 暴力法

算法:

  1. 遍历链表并复制一份
  2. 同时把复制出来的链表反转
  3. 使用2个指针分别遍历两个链表对比每个节点

image4dda4285d4a090df.png

算法复杂度

  • 时间复杂度:O(n),需要遍历两次链表。
  • 空间复杂度:O(n),需要拷贝一份链表。

2.2 快慢指针

判断是否回文数核心思想是先找到链表的中间节点,然后从中间节点开始往两边对比、或者从两边往中间节点对比,以此判断是否为回文数。

算法:

使用2个指针,快指针每次走一步,慢指针每次走两步。当快指针到链表末尾的时候,慢指针在链表中间。这个时候反转后半部分链表,再使用两个指针从链表两端朝中间遍历即可判断出是不是回文。

图示:

当快指针走到末尾的时候,慢指针刚好走到一半,中间节点就在p1和p1前一个节点之间:

imageca687ca20d2e92c6.png

以下是节点个数为奇数时,快慢指针从开始到结束时候的状态,中间节点是p1的前一个节点:

imaged7b93ca12e0ffb2f.png

可以看到不管链表个数是奇数还是偶数,中间节点都是在慢指针p1附近。

优化

上面的快慢指针算法的执行步骤:

  1. 快指针遍历到末尾(遍历了一半的链表)。
  2. 反转慢指针到快指针这一段(遍历了一半的链表)。
  3. 从两端往中间遍历(遍历了一半的链表)。

整个时间复杂度是O(n),虽然是O(n),但是实际上链表遍历了1.5次。因为要对比整个链表,所以算法的时间复杂度不可能低于O(n),O(n)最理想的情况就是只遍历一次,因此可以想办法优化掉多余的0.5次遍历。

算法如下:

  1. 快慢指针分别遍历链表,遍历的过程中,把慢指针经过的节点反转。
  2. 当快指针走到链表末尾的时候,慢指针依旧在链表中间的位置,但是此时,慢指针前面的节点都已经反转了。
  3. 分别从满指针前后朝两端遍历,对比每个节点。

该算法把前面的步骤1和2合并成了一个操作,减少了一次遍历操作(需要遍历一半链表节点),最后只要遍历1次链表即可完成判断链表是否回文。

算法要注意区分奇数个节点和偶数个节点。

当链表节点个数是偶数个的时候,快指针在指向NULL时,p1前面的就是链表的前半段,从p1开始就是后半段:

image7579a979434ed70d.png

链表节点个数是奇数个的时候,p2不会走到NULL(p2要走2步,没有更多的节点了),此时p1在最中间的节点。p1之前是链表的前半段,p1之后就是链表的后半段:

image73a4094c407466c1.png

代码:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode *slow = head, *fast = head, *pre = NULL, *next = NULL;
        
        // 注意链表为空的时候也要返回true
        if (head == NULL || head->next == NULL) {
            return true;
        }

        // pre节点用于保存中间节点的前面一个节点
        while (fast && fast->next != NULL) {
            fast = fast->next->next;

            // 反转
            next = slow->next;
            slow->next = pre;
            pre = slow; // 备份节点
            slow = next;
        }

        // 奇数个节点的时候,fast指向最后一个节点,非NULL
        // 此时slow指向中间节点,slow下滑一个节点
        // 此时pre所指向的是前半段链表,slow所指向的是后半段链表
        if (fast != NULL) {
            slow = slow->next;
        }

        while (pre != NULL && slow != NULL) {
            if (pre->val != slow->val) {
                return false;
            }
            pre = pre->next;
            slow = slow->next;
        }

        return pre == NULL && slow == NULL;
    }
};
这里要注意的是pre指针的作用,从上面的图示中可以发现,不管是奇数还是偶数个节点,反转后都是从slow节点前分隔开的。因此pre的作用就是保存slow前面的节点,用于后续回文数比对。

imagec2b6b3c5714a7902.png

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

一、区别

区别一:MyISAM是非事务安全的,InnoDB是事务安全的

  • 事务安全的特点为更安全,遇到问题会自动恢复或从备份加事务日志回复,如果更新失败,你的所有改变都变回原来。
  • 非事务安全的优点为更快,所需的磁盘空间更小,执行更新时需要的内存更小,但是所有发生的改变都是永久的。
InnoDB用于事务处理,具有ACID(原子性,一致性,隔离性,持久性)事务支持等特性,如果在应用中大量使用insert和update操作,应选用InnoDB。

区别二:MyISAM锁的粒度是表级的,而InnoDB支持行级锁

数据库引擎具有多粒度锁定,允许一个事务锁定不同类型的资源。 为了尽量减少锁定的开销,数据库引擎自动将资源锁定在适合任务的级别。 锁定在较小的粒度(例如行)可以提高并发度,但开销较高,因为如果锁定了许多行,则需要持有更多的锁。 锁定在较大的粒度(例如表)会降低了并发度,因为锁定整个表限制了其他事务对表中任意部分的访问。 但其开销较低,因为需要维护的锁较少。

区别三:MyISAM支持全文类型索引,而InnoDB(以前)不支持全文索引

  • 主键索引:主键是一种唯一性索引,但它必须指定为PRIMARY KEY,每个表只能有一个主键。
  • 唯一索引:索引列的所有值都只能出现一次,即必须唯一,值可以为空。
  • 普通索引:基本的索引类型,值可以为空,没有唯一性的限制。
  • 全文索引:全文索引的索引类型为FULLTEXT。全文索引可以在varchar、char、text类型的列上创建。可以通过ALTER TABLE或CREATE INDEX命令创建。对于大规模的数据集,通过ALTER TABLE(或者CREATE INDEX)命令创建全文索引要比把记录插入带有全文索引的空表更快。MyISAM支持全文索引,InnoDB在mysql5.6之后支持了全文索引。

区别四:InnoDB支持外键,MyISAM不支持

区别五:MyISAM表保存成文件形式,跨平台使用更加方便

二、应用场景

MyISAM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和Innodb不同,MyISAM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。同时MyISAM提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作,应该选择MyISAM。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyISAM也是很好的选择。

Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。

一、三范式概念

第一范式:当关系模式R的所有属性都不能再分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。

第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。

第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF。

二、理解三范式

2.1 第一范式

第一范式要求:

  • 每一列的属性都是不可再分的属性
  • 属性相近或者相似的,尽量合并成同一列

例如保存一个学生对象的信息时,学生信息表的设计:

这里的地址信息是可以拆分的:xx省 xx市 xx区,按照第一范式的准则,应该设计为:

2.2 第二范式

第二范式要求每一行的数据只与一列数据有关,数据中不能出现重复的项目,如果有就要把表拆开来。

如学生所在的班级信息:

表中可能会出现多个学生对应的班级信息都是一样的情况,此时class_id就会重复。应该把class_id和学生信息单独抽出来放在新表中。

2.3 第三范式

第三范式要求所有的列都跟主键有直接关系而不是间接关系。例如学生所在的班级和班级所在的楼栋地址等关系,不应该设计成学生 - 班级 - 地址的关系,而应该是学生 - 班级班级 - 地址,两者相互独立,应该拆开。

一、红黑树

红黑树也是BST树的一种,它和AVL树不同,它不要求各个节点的高度都是完全一致,是一个近似平衡的树。

红黑树通过黑树节点增加颜色,对每一条根节点到叶子节点上面的各个节点颜色进行约束,确保没有一条路径会比其他路径长出2倍。

红黑树的性质:

  1. 每个节点的颜色要么是红色,要么是黑色
  2. 根节点是黑色的
  3. 每个叶子节点(NIL)是黑色
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的
  5. 对于每个节点,其到任意后代的叶子节点的路径上,都包含相同数量的黑色节点

和普通树不同的是,红黑树的叶子节点都是NIL节点,它是一个特殊的节点,颜色是黑色。

定理:一个有n各内部节点的红黑树的高度至多为2lg(n+1)。

下面就是一棵红黑树示例,省去了末尾的NIL叶子节点:

红黑树的操作和BST/AVL树相差不大,搜索、旋转也都是一模一样的代码。红黑树的操作:

  • RB_INSERT: 插入节点到红黑树
  • RB_DELETE: 从红黑树中删除节点

二、红黑树的添加操作

红黑树每个新插入的节点都是红色,节点添加操作分为以下两步:

  1. 按照BST的插入节点操作插入一个节点
  2. 调整节点颜色,以满足红黑树性质

过程中负责的步骤是第二部,也就是插入之后的调整,调整的规则有如下几种。

2.1 节点颜色调整规则

假设待插入节点为z,设其叔叔节点为y。插入节点后,有以下几种情况:

情况一:父节点是红色,叔叔节点也是红色

这种情况下,插入节点z会违反性质4,并且z的叔父节点也是红色,所以为了维持性质,需要修改其父亲节点和叔叔节点的颜色为黑色,修改其祖父节点的颜色为红色。并把z设置为祖父节点,继续判断z的属性,如果修改后z的叔叔节点还是红色,继续当前的操作。直到叔叔节点y是黑色了,情况一就转换成情况二,向下看。

情况二:父节点是红色,叔叔节点是黑色,当前节点是其父节点的右儿子

叔叔节点是黑色,判断当前节点是其父亲的左儿子还是右儿子,如果是左儿子,将满足情况三,按照情况三来处理。如果是右儿子,设置z指向其父节点,并对新的z节点左旋,把情况二转换成情况三。

情况三:父节点是红色,叔叔节点是黑色,当前节点是其父节点的左儿子

这种情况下,为了保持平衡,修改z的父颜色为黑色,z的祖父节点为红色,然后对祖父节点右旋。得到满足条件的红黑树:

情况四:父节点是黑色

如果父节点是黑色,无需调整,插入节点后树依旧是一棵红黑树,性质不会破坏。

2.2 插入节点代码

红黑树的插入代码前部分和BST插入一模一样,最后添加完成之后执行操作FIXUP对红黑树进行调整:

void rb_tree_insert(struct rb_tree_st *tree, struct rb_tree_node_st *node) {
    struct rb_tree_node_st *x = tree->root;
    struct rb_tree_node_st *y = NULL;
    struct rb_tree_node_st *z = node;

    while (x != NULL) {
        y = x;
        if (z->key < x->key)
            x = x->lchild;
        else
            x = x->rchild;
    }

    z->parent = y;
    if (y == NULL)
        tree->root = z;
    else if (y->key > z->key)
        y->lchild = z;
    else
        y->rchild = z;

    z->lchild = NULL;
    z->rchild = NULL;
    node->color = NODE_COLOR_RED;

    rb_tree_fixup(tree, node);
}

红黑树调整代码:

void rb_tree_fixup(struct rb_tree_st *tree, struct rb_tree_node_st *node) {
    struct rb_tree_node_st *z = node;
    struct rb_tree_node_st *y = NULL;
    
    while (z->parent && z->parent->color == NODE_COLOR_RED) {
        // 如果父节点是红色,那么必定有祖父节点,根据父节点的位置得到叔父节点
        if (z->parent == z->parent->parent->lchild)
            // 父节点是祖父节点的左子节点
            y = z->parent->parent->rchild;
        else
            y = z->parent->parent->lchild;

        if (y && y->color == NODE_COLOR_RED) {
            // 叔父节点是红色,把叔父节点和父节点全部置黑,不区分叔父节点是左儿子还是右儿子
            y->color = NODE_COLOR_BLACK;
            z->parent->color = NODE_COLOR_BLACK;
            // 祖父节点置红,然后重新处理祖父节点
            z->parent->parent->color = NODE_COLOR_RED;
            z = z->parent->parent;
            continue;
        }

        // 此时叔父节点必定是黑色,把y设为祖父节点,需要区分叔父节点是左儿子还是右儿子
        y = z->parent->parent;
        if (z->parent == y->lchild) {
            if (z == z->parent->rchild) {
                z = z->parent;
                left_rotate(tree, z);
            }
            z->parent->color = NODE_COLOR_BLACK;
            z->parent->parent->color = NODE_COLOR_RED;
            right_rotate(tree, z->parent->parent);
        } else {
            if (z == z->parent->lchild) {
                z = z->parent;
                right_rotate(tree, z);
            }
            z->parent->color = NODE_COLOR_BLACK;
            z->parent->parent->color = NODE_COLOR_RED;
            left_rotate(tree, y);
        }
    }
    // 设置根节点是黑色
    tree->root->color = NODE_COLOR_BLACK;
}

三、删除节点

和AVL中一样,删除节点总是比增加节点麻烦一些。因为红黑树也是一颗BST树,并且也是一个接近平衡的二叉树,因此它的删除过程也和AVL树一样。先删除节点,然后调整,步骤为:

  1. 删除或者替换节点,这一步有可能导致红黑树性质不满足
  2. 调整节点颜色,以满足红黑树性质

在AVL树中,删除节点并不是把这个节点直接删除,而是找到这个节点的后继节点来替代该节点。红黑树中的删除也是按照这种办法,假设删除节点为z,设定y表示替换z位置的节点,x表示实际上被删除的节点(即逻辑上被删除的叶子节点),wx的兄弟节点。删除节点x点有可能是z的儿子,也有可能是y的儿子,也有可能是NIL。

当删除节点是红色,删除时不会影响红黑树的性质,需要调整的直有删除节点是黑色的场景。

3.1 删除节点调整颜色规则

情况一:删除节点x的兄弟节点w是红色

操作:修改w和父节点的颜色,并对父节点左移。此时x的新兄弟节点是原w节点的左子节点,必定为黑色。情况一可以转换成下面的情况来处理。

情况二:x的兄弟节点w是黑色,且w的两个子节点都是黑色

操作:去掉w上的黑色,并使x为其父节点,继续循环处理新x节点。

情况三:x的兄弟节点w是黑色,w的左孩子是红色,w的右孩子是黑色

操作:修改w和其左儿子的颜色,并对w右旋,然后把w设置为其原来的左儿子。将情况三转换成情况四。

情况四:x的兄弟节点w是黑色,且w的右孩子是红色

第四种情况是我们最终想要转换的情况,因为只有这一种情况能通过旋转来彻底解决平衡。

操作:设置w的颜色为其父节点的颜色,w父节点设置为黑色,w的右儿子设置为黑色,对父节点进行左旋。

上面的描述过于抽象,可以用一个例子来理解。下面是一棵红黑树,想要删除节点60(60原先是黑色):

删除的步骤:

  1. 设置80的颜色为其父节点的颜色(红色)
  2. 设置80的父节点70颜色为黑色,设置80右儿子85的颜色为黑色
  3. 对70左旋,去掉节点60,得到右边的红黑树,此时红黑树的性质依旧保持

3.2 删除代码实现

删除红黑树节点的伪代码(来自算法导论):

修复节点伪代码:

一、HTTPS工作流程

HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

它的工作流程为:

  1. 客户端访问网站,先建立SSL连接,发送一个随机数A以及自己的加密套件等信息。这一步通常被成为Client Hello
  2. 服务端收到客户端的请求之后,生成一个随机数B,携带自己的证书(证书包含公钥、有效期等信息)一起发送给客户端,同时返回的还有加密协议信息。这一步通常称为Server Hello
  3. 客户端拿到证书后,通过公钥验证证书是否有效。如果有效的话,通过随机数A、随机数B以及公钥加密得到一个预主密钥,发送到服务端,这个密钥被称为Premaster Secret
  4. 服务端拿到客户端的数据后用私钥解开,通过加密校验,确认数据来自客户端。然后用随机数和预主密钥加密得到会话密钥,通过会话密钥加密一段握手消息,发送到客户端。
  5. 客户端收到之后解密数据,如果校验通过也发送一段握手消息到服务端。从这里开始所有后续的数据就会通过会话密钥来加密数据通信了。

图解流程为:

二、为什么需要多个随机数

避免客户端的随机数生成不是绝对随机的,多加一层随机数校验

一、vector迭代器失效

vector是先行存储的,大部分时候的插入删除操作都有可能导致迭代器失效。失效场景:

  • 执行插入操作时,end指针失效,如果此时重新分配内存了,所有的迭代器失效,否则其他迭代器可能不会失效。
  • 执行删除操作时,所有的迭代器都会失效

示例代码:

vector<int> data;
vector<int>::iterator it;
data.push_back(100);
data.push_back(200);
data.push_back(300);
data.push_back(400);

for (it = data.begin(); it != data.end(); it++) {
    cout << *it << endl;
    if (*it == 300)
        data.erase(it); // 删除之后将会失效
}

二、list迭代器失效

失效规则:

  • 插入操作(insert)和接合操作(splice)不会造成原有的list迭代器失效
  • 删除操作(erase)只有指向被删除元素的那个迭代器失效,其他迭代器不受影响

list不是线性存储的,链式存储的好处就是方便增加和删除,不会影响原有的内存空间。

三、deque迭代器失效

失效规则:

  • 在deque容器首部或者尾部插入元素不会使得任何迭代器失效,vs环境下测试会失效,但是g++没有失效
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效
  • 在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效
// 首尾添加元素,在vs环境下测试失效,g++正常
void deque_f() {
    deque<int> q;
    deque<int>::iterator it;

    q.push_back(100);
    q.push_back(200);
    q.push_back(300);
    q.push_back(400);

    for (it = q.begin(); it != q.end(); it++) {
        cout << *it << endl;
        if (*it == 200) // 不会失效
            q.push_front(50);
        if (*it == 300) // 不会失效,且最后会输出500
            q.push_back(500);
    }

}

四、map和set

map和set底层都是红黑树,不是线性存储,它们的失效规则:

  • 执行插入操作时,原有的迭代器不会失效
  • 删除操作时,其他的迭代器不会失效,被删除元素的迭代器失效

一、普通多继承时子类的内存布局

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
};

class CTestC : virtual public CTestA {
};

class CTestD : public CTestB, public CTestC {
};

内存布局:

1>class CTestD    size(8):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1>    | +---
1> 4    | +--- (base class CTestC)
1> 4    | | +--- (base class CTestA)
1> 4    | | | m_data
1>    | | +---
1>    | +---
1>    +---

CTestD的大小是8,分别包含了一份CTestA和CTestB,他们分别是4个字节,总共8字节。

1.2 在每个类中添加私有变量

class CTestA {
    int m_data;
};

class CTestB : public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

CTestD的大小为20,其中包含CTestA的8个字节和CTestB的8个字节以及自定义的m_data_d的四个字节:

1>class CTestD    size(20):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | +--- (base class CTestA)
1> 0    | | | m_data
1>    | | +---
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---

1.3 总结

多继承,如果没有使用virtual关键字,子类的大小为每个继承的父类的大小之和。其内存分布在变量的最开始部分,先排布父类的内存。

二、使用虚继承

2.1 单个父类虚继承

class CTestA {
    int m_data;
};

class CTestB : virtual public CTestA {
    int m_data_b;
};

class CTestC : public CTestA {
    int m_data_c;
};

class CTestD : public CTestB, public CTestC {
    int m_data_d;
};

内存排布:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | +--- (base class CTestA)
1> 8    | | | m_data
1>    | | +---
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

此时CTestD包含了以下几个部分:

  • CTestB: B采用了虚继承,其父类A的m_data不会占用D中的空间,但它内部多了一个vbptr指针,总共占用8字节
  • CTestC: C没有采用虚继承,其内部的m_data也还被D继承,没有vbptr指针,算上C自己的m_data_c共占用8个字节
  • m_data_d: D本身自己的成员变量,占用4字节
  • vitrual base CTestA: 虚继承于CTestA,包含CTestA的数据m_data,占用4个字节

CTestD共占用24个字节。

2.2 父类都使用虚继承

将CTestC也改成虚继承的方式:

class CTestC : virtual public CTestA {
    int m_data_c;
};

此时CTestD的内存分布情况为:

1>class CTestD    size(24):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vbptr}
1> 4    | | m_data_b
1>    | +---
1> 8    | +--- (base class CTestC)
1> 8    | | {vbptr}
1>12    | | m_data_c
1>    | +---
1>16    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>20    | m_data
1>    +---
1>

CTestD依旧占用24个字节的内存空间,不过和上面不同的是,D中属于C的8个字节空间的内容变了,其中原本属于A的m_data内存变成了vbptr指针。

2.3 总结

当子类(D)的多个父类(A,B)都继承于同一个基类(A)时,且继承时都添加了virtual关键字(属于虚继承时),子类(D)只会保存一份来自基类(A)的内存。每个父类(A,B)中会添加一个vbptr指针指向公共的基类。

三、vbptr指针

3.1 vbptr的内容

当使用虚继承时,类中会生成vbptr指针,指向公共基类的位置。vbptr中包含两个偏移量,第一个是vbptr指针在当前类中的偏移量,第二个是公共的基类在当前类中的位置。例如上面的CTestD类,其内存布局为:

两个vbtr内容的解析:

  1. CTestB.vbptr: 第一个偏移量=CTestB.vbptr - BTestB = 0,第二个偏移量=CTestA - CTestB = 22。
  2. CTestC.vbptr: 第一个偏移量=CTestC.vbptr - CTestC = 0,第二个偏移量=CTestA - CTestC = 16。

2.2 第一个偏移量的细节

上面的例子中,CTestB和CTestC的vbptr指针的第一个偏移量(以vbptr[0]表示)都是0,它表示的是这个偏移量对于当前类的偏移。因为类B和类C目前只有vbptr的指针,所有的类中,vbptr是在成员变量之前的,所以他们都是0。但是如果在类中加入一个虚函数,使类中产生虚指针,那么这个偏移量就不是0了。

修改类B:

class CTestB : virtual public CTestA {
    int m_data_b;
public:
    CTestB() {
        m_data_b = 2;
    }
    void print_data() {
        cout << m_data_b << endl;
    }
    virtual void hello() {
    }
};

打印出C的内存分布:

1>class CTestD    size(28):
1>    +---
1> 0    | +--- (base class CTestB)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | m_data_b
1>    | +---
1>12    | +--- (base class CTestC)
1>12    | | {vbptr}
1>16    | | m_data_c
1>    | +---
1>20    | m_data_d
1>    +---
1>    +--- (virtual base CTestA)
1>24    | m_data
1>    +---
1>

可以看到,类B中多了一个vfptr,它指向的是虚函数表的地址,此时vbptr放在第二位,CTestB.vbptr[1]的值就是-4。类B和类C的vbptr的细节:

1>CTestD::$vbtable@CTestB@:
1> 0    | -4
1> 1    | 20 (CTestDd(CTestB+4)CTestA)
1>
1>CTestD::$vbtable@CTestC@:
1> 0    | 0
1> 1    | 12 (CTestDd(CTestC+0)CTestA)

B的第1个偏移变成了-4,而C的没有变化。

一、TCP协议

TCP协议的首部:

字段解析:

  • 源目端口号:各2字节,short类型的端口号
  • 序号:标识当前TCP数据包在数据流中的序号,即平常的SYN和SEQ信息。
  • 确认号:收到数据报后,返回的ACK序号
  • 首部长度:TCP协议的首部长度,以四字节为单位,最多60字节。默认不带选项的情况下TCP首部是4个字节。
  • TCP选项:

    • URG: 紧急指针有效,很少使用
    • ACK: 确认选项
    • PSH: 推送选项,接收方收到后应该尽快被发送到应用层,选项没有被可靠的实现或用到
    • RST: 重置连接,经常是因为错误取消
    • SYN`: 开始连接,初始化一个连接的同步序号
    • FIN`: 结束连接
  • 窗口大小:2字节,滑动窗口协议中用来控制流量大小的字段
  • 校验和:计算方式和UDP一致,也会添加一个伪首部,计算整个头部和数据部分的内容
  • 紧急指针:只有在URG被设置时才有效,很少用到。
  • 其他选项

    • MSS: 最大段大小,表示连接希望收到的报文段的最大值

二、三次握手和四次挥手

参考TCP协议中的三次握手和四次挥手

三、TCP超时和重传

在连接无法收到响应后,TCP将会尝试重传数据,重传的时间间隔称为二进制指数退避。不过和CSMA/CD协议中的不同,这里的二进制指数退避是在之前的重传间隔上加倍。

0.2 - 0.4 - 0.8 - 1.6 - 3.2 - 6.4 - 12.8 - 25 - 50 - 100 - 200 - ...

linux中可以通过配置来设置重传的次数,重传涉及两个次数:第一个是TCP重新连接时尝试重传的次数,第二个是TCP放弃当前连接的时机。

net.ipv4.tcp_retries1
net.ipv4.tcp_retries2

3.1 RTT

RTT表示一个连接的往返时间,即数据发送时刻到接收到确认的时刻的差值。

RTT的取值方式:为一个已发送但尚未确认的报文段做一次RTT采样,得到一个SampleRTT(不是为每一个发送的报文段都测量RTT),从而用这个SampleRTT来接近(代表)所有RTT。

  • 一个连接中,有且仅有一个测量定时器被使用。也就是说,如果TCP连续发出3组数据,只有一组数据会被测量。
  • TCP决不会为已被重传的报文段测量SampleRTT,仅仅为传输一次的报文段测量SampleRTT。
  • ACK数据报不会被测量,原因很简单,没有ACK的ACK回应可以供结束定时器测量。

由于链路负载的变化,很多时候RTT并不能完全代表网络中真正的状态,为了得到一个更合理更接近事实的RTT,TCP规范中通过一个低通过滤器来设置出平滑的RTT估计器:

SRTT = α(SRTT) + (1 - α)RTT

α一般设置为0.8-0.9,意思是SRTT的值的80%-90%来自前一个估计值,10%-20%来自当前的估计值。

3.2 RTO

RTO指重传超时时间,从数据发送时刻算起,超过这个时间便执行重传。

RTO的值依赖RTT,[RFC0793]中推荐的RTO计算公式为:

RTO = min(ubound, max(lbound, (RTT)β))

β是离散因子,推荐值为1.3-2.0,ubound是RTO的上边界(建议为1分钟)lbound是RTO的下边界(如1秒)。这种方法被称为经典方法,它使得RTO值为1秒或者2倍的SRTT。对于相对稳定的RTT来说,这种方法能取得不错的性能。运行在RTT波动较大的网络环境时,无法获得期望的效果。

3.3 TCP中的四个定时器

  • 重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间。即上面说的RTT和RTO。
  • 坚持定时器:为对付零窗口通知而设立的,具体见下面
  • 保活计时器:为保持TCP的keepalive设立的,一般为2小时
  • 时间等待计时器:关闭连接时需要等待的时间,即2MSL等待期

3.4 坚持定时器

当窗口大小为0时,如果一个确认丢失了,双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。

为防止这种死锁情况的发生,发送方使用一个坚持定时器(persist timer)来周期性地向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查(window probe)。

3.4 快重传

TCP发送端在观测到多个重复的ACK后,此时重传可能丢失的数据包分组。不要等到计时器超时,此时依旧可以发送新的数据。

四、拥塞避免

4.1 慢启动

一个TCP刚启动时,不知道外部网络环境状态,为了避免发送过多数据出现拥塞,TCP会维护一个从1开始的cwnd值,每次发送cwnd个数据包。当收到一个数据报的确认后,该字段值加1。这个过程被称为慢启动。

具体的过程为:

  1. 连接建好的开始先初始化cwnd = 1,表明可以传一个SMSS大小的数据
  2. 收到一个ACK,cwnd++,呈线性上升
  3. 过了一个RTT,cwnd = cwnd*2,呈指数上升
  4. 每当产生一个RTO超时重传,cwnd = 1, ssthresh减半

慢启动有一个门限值ssthresh,超过这个值之后会启动拥塞避免算法。

4.2 拥塞避免

cwnd超过ssthresh时,开始采用拥塞避免算法。因为cwnd不可能无限增加,拥塞避免算法的目的是使得cwnd处于更缓慢增长的模式。

假设cwnd=k*SMSS,当发送端收到一个包的ACK反馈的时候,按照cwnd=cwnd+(1/k)*SMSS。这样每经过一个RTT发送k个数据包的时候,cwnd增长了大约一个SMSS,通常上我们即描述为每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,使拥塞窗口 cwnd 按接近线性规律缓慢增长。这种增长方式叫做“加法增大”(Additive Increase)。总结如下:

  1. 收到一个ACK时,cwnd = cwnd + 1/cwnd
  2. 每过一个RTT时,cwnd = cwnd + 1

使用慢启动和拥塞避免算法产生的流量增长趋势图: