CS144 lab1 实验问题记录
文章目录
1.碎片可能交叉或重叠。
2.如果某次新碎片到达后字节流的开头部分被凑齐,那就应当立刻把凑齐的部分立刻写入到_output
中。即对应讲义中的:
When should bytes be written to the stream?
As soon as possible. The only situation in which a byte should not be in the stream is that when there is a byte before it that has not been “pushed” yet.
3.碎片可能是一个只包含EOF标志的空串
4.LAB0的顺序字节流和LAB1的流重组器各有各的容量限制。流重组器把字节流写满后,只有当字节流腾出空后才能继续写,相当于字节流满时流重组器出口被“堵住”了。同样当流重组器容量满了后自身也无法被写入新数据,此时到来的新碎片只能被丢弃掉。
第一反应联想到了操作系统里的进程内存管理,用一个二叉排序树来记录每个碎片的索引、长度,排序规则为按索引值升序,每次插入新碎片时判断能不能和前后碎片进行合并。流的内容则可以用一个数组来做缓冲区,或者干脆一块存储在二叉树的节点里。不过还是因为“Modern C++”的缘故,我再次退而求其次用std::list
代替之。等我哼哧哼哧花了好几个小时写完LAB1后,又哼哧哼哧得改了一众BUG后,**才想起std::set
底层就是用红黑树实现的,**可以直接拿来用。
用一个block_node
结构体来存放每个碎片的索引、长度、内容。又因为set
排序实现基于对应节点类型的小于运算符规则,所以我把**block_node
结构体的小于运算符重载为按索引值升序**。再简单说下我的push_substring
处理流程:
-
容量判断:满了就立刻返回。
- 处理子串的冗余前缀:如果子串包含已经被写入字节流的部分,就把这部分剪掉。
- 合并子串:运用
set
自带的lowerbound
快速确定插入位置,前后重复比较,用个自己写的子函数判断重叠的字顺便合并之。 - 写入字节流:如果流重组器头部非空,就把头部写入字节流,并更新指示头部的游标。
- EOF判断
class StreamReassembler {
private:
// Your code here -- add private members as necessary.
struct block_node {
size_t begin = 0;
size_t length = 0;
std::string data = "";
bool operator<(const block_node t) const { return begin < t.begin; }
};
std::set<block_node> _blocks = {};
std::vector<char> _buffer = {};
size_t _unassembled_byte = 0;
size_t _head_index = 0;
bool _eof_flag = false;
ByteStream _output; //!< The reassembled in-order byte stream
size_t _capacity; //!< The maximum number of bytes
//! merge elm2 to elm1, return merged bytes
long merge_block(block_node &elm1, const block_node &elm2);
//......
上面就是写的类的内容,其中block_node结构体重载了运算符号,结构体block_node
还定义了一个小于运算符(operator<
),用于比较两个block_node
对象的大小。该小于运算符是成员函数,它接受一个const block_node
类型的参数t
,并返回一个bool
类型的值。它的实现比较简单,仅仅是比较两个block_node
对象的begin
成员变量的大小,并返回比较结果。这个小于运算符可以用于对block_node
对象进行排序,例如在使用std::set
容器时,可以按照block_node
对象的begin
值进行排序。
问题1 实际工程中使用vector,set记得include 相关头文件
在实验中,使用vector和set在.h文件中没有报错,但是在.c文件中就无法链接相关成员变量
问题2 lower_bound到底是如何起作用的
在这段代码中,_blocks是一个std::set容器,用于存储block_node类型的元素。segment结构体定义了三个成员变量begin、length和data,并重载了小于号运算符,以便可以将block_node对象插入到std::set容器中。
lower_bound()函数的作用是在_blocks容器中查找第一个大于等于给定elm的元素,并返回指向该元素的迭代器。由于_blocks是一个std::set容器,它的元素默认按照小于号运算符进行排序,因此lower_bound()函数查找的是第一个大于等于给定elm的元素。
具体来说,lower_bound()函数会从容器的起始位置开始,依次比较每个元素和给定的elm,直到找到第一个大于等于elm的元素。在这个过程中,lower_bound()函数会利用block_node结构体中重载的小于号运算符进行比较。如果给定的elm小于容器中的某个元素,那么这个元素就是第一个大于等于elm的元素,lower_bound()函数返回指向这个元素的迭代器。
因此,iter = _blocks.lower_bound(elm)语句的作用是在_blocks容器中查找第一个大于等于elm的元素,并将指向该元素的迭代器赋值给iter。然后程序将使用iter指向的元素进行操作,例如与elm合并或从容器中删除等。
问题3 如何解决新来的串的重叠
do {
// merge next
long merged_bytes = 0;
//lower_bound()函数的作用是在_blocks容器中查找第一个大于等于给定elm的元素
auto iter = _blocks.lower_bound(elm);
//? 为什么要做循环?
//因为到来的子串头部后方可能横跨多个子串
// 4|5|6|7|8|9|10
//0|1|2|3|4| |6|7| |10|11|12|13|14
//例如上面这个例子我合并了|6|7|之后后方还有一个需要合并的
while (iter != _blocks.end() && (merged_bytes = merge_block(elm, *iter)) >= 0) {
_unassembled_byte -= merged_bytes;
_blocks.erase(iter);
iter = _blocks.lower_bound(elm);
}
// merge prev
//遍历前面的子串看看前方的合并情况, 这里就没有办法了,只能把前面的子串全部遍历一遍看看能否合并(因为对应lower_bound只能找到begin)
if (iter == _blocks.begin()) {
break;
}
iter--;
while ((merged_bytes = merge_block(elm, *iter)) >= 0) {
_unassembled_byte -= merged_bytes;
_blocks.erase(iter);//删除iter指向的segment
iter = _blocks.lower_bound(elm);//?因为被删除了所以重新指定一下位置
if (iter == _blocks.begin()) {
break;
}
iter--;
}
} while (false);
_blocks.insert(elm);
问题4 迭代器作为函数参数传入的要点
long StreamReassembler::mergeTwoSegment(segment& str1, segment& str2)
mergeTwosegment(elm, *iter)
为什么我不能这样调用函数,必须把函数的第二个参数加上const修饰符才行
如果 mergeTwoSegment 函数的定义为 long StreamReassembler::mergeTwoSegment(segment& str1, segment& str2),那么在调用函数时,参数 str2 会被视为非常量引用,*而 iter 是一个迭代器,它所指向的对象是一个常量对象。所以,将 *iter 作为第二个参数传递给 mergeTwoSegment 函数时,会产生一个编译错误,提示无法将常量对象转换为非常量引用。
为了解决这个问题,需要将 mergeTwoSegment 函数的第二个参数声明为 const segment& 类型,这样就可以接受常量对象作为参数了。因此,将 mergeTwoSegment 函数的定义修改为 long StreamReassembler::mergeTwoSegment(segment& str1, const segment& str2),就可以将 *iter 作为第二个参数传递给函数了。
需要注意的是,在修改 mergeTwoSegment 函数的定义之后,如果函数中需要修改 str2 对象的值,就需要先将其复制一份,在副本上进行修改,而不能直接修改原对象的值,因为原对象是一个常量引用。
问题5 忽略了两个报文段之间不一定非要交叉或者重叠才merge,两个报文段之间紧挨着也要merge到一起
不能是大于号,必须是>=号
问题6 对goto语句的理解错误
#include<iostream>
#include<vector>
using namespace std;
int main(){
cout << 12 << endl;
int a = 1;
if (a == 2)
goto JUDGE;
else {
cout << 1 << endl;
}
cout << 32 << endl;
JUDGE:
cout << "pitch" << endl;
上面的示例代码,无论goto语句是否执行都会输出“pitch”
同理
if (index + data.size() <= _head_index) {
goto JUDGE_EOF;
}
新来的报文段可能含有EOF但是前面的报文段还没发送
所以在运行完合并等操作之后,还会将eof_flag设为true;
问题7 括号运算优先级
(merged_bytes = merge_block(elm, *iter) >= 0))
(merged_bytes = merge_block(elm, *iter)) >= 0)
我写的代码是上面的形式 >=的优先级比赋值运算符高,所以merged_bytes 总是等于1,也就是>=运算符得到的结果为TRUE
问题8 迭代器寻找的错误写法
while ((merged_bytes = mergeTwoSegment(elm, *iter)) >= 0) { //?為什麼要加=呢,經過debug之后发现merge两个不同的段,这两个段可能没有重叠,返回的merged_bytes当然是0但是肯定有合并操作
_unassembled_byte -= merged_bytes;
//!!!!!!!!!!!!!!!
auto temp = iter;
_segments.erase(temp);
iter--;
if (iter == _segments.begin()) {
break;
}
// _segments.erase(iter);
// iter = _segments.lower_bound(elm);
// if (iter == _segments.begin()) {
// break;
// }
// iter--;
}
iter
和 temp
都指向同一个元素。当您使用 _segments.erase(temp)
从 _segments
中删除该元素时,iter
迭代器仍然指向原来的位置,但是这个位置现在已经是无效的了,因为元素已经被删除了。(调试发现元素是从segment里面删掉了,但是iter指向的仍然是原来的元素没有变化,导致又会执行一次错误的merge,再次去segment里面删除temp时,发现里面并没有这个迭代器,导致删除错误)这种情况下,使用 iter--
来更新 iter
的值是不安全的,因为它可能会导致指向无效内存地址的未定义行为。
为了避免这种情况,可以在调用 erase()
函数之前将 iter
复制到另一个迭代器中,然后在删除元素后使用该迭代器来更新 iter
的值。例如,您可以这样修改代码:
代码的巧妙写法
while ((merged_bytes = mergeTwoSegment(elm, *iter)) >= 0) { //?為什麼要加=呢,經過debug之后发现merge两个不同的段,这两个段可能没有重叠,返回的merged_bytes当然是0但是肯定有合并操作
_unassembled_byte -= merged_bytes;
//!!!!!!!!!!!!!!!
_segments.erase(iter);
iter = _segments.lower_bound(elm);//
if (iter == _segments.begin()) {
break;
}
iter--;
}
显然前面我们已经做过了后方的交叉和重叠的融合操作,这时候使用lower_bound 就定位到了当前报文段不重叠的下一个报文段,或者定位到segment.end();
这时候在执行iter-- 就跳到了前面的元素,在判断前面的元素和当前报文段是否有重叠。
代码优化
iter--;
if ((merged_bytes = mergeTwoSegment(elm, *iter)) >= 0) { //?為什麼要加=呢,經過debug之后发现merge两个不同的段,这两个段可能没有重叠,返回的merged_bytes当然是0但是肯定有合并操作
_unassembled_byte -= merged_bytes;
//!!!!!!!!!!!!!!!
_segments.erase(iter);
// iter = _segments.lower_bound(elm);//
// if (iter == _segments.begin()) {
// break;
// }
// iter--;
}
执行过后方的交叉和融合合并之后,iter的位置指向了与当前报文段不相交的一个后方的segment,使用iter–便到了前方的segment,这个segment要么有交叉重叠,要么没有,不需要进行while循环。所以直接将原来代码中的while变成了if