合迅科技 实习工作记录
我在合迅科技的实习工作记录。
1.15 - 1.26
迭代任务
- 走查代码
LStack,LQueue,LByteArray
代码走查问题
LStack,LQueue
二者是一样的设计,一样的问题,这里就统一写了
- 处理结果:刘治学已重构修改
关于使用公有继承的问题
- 代码中直接使用公有继承的方式,并且使用了目前不完善的
LList
。 - 建议改为
LVector
作为底层容器。
- 代码中直接使用公有继承的方式,并且使用了目前不完善的
1 | template<class T> |
- 关于使用继承还是复合的问题:经过讨论,决定保留
public
继承的方式,不做覆盖,因为Qt
也是这么做的,对于是隐藏父类不需要的功能还是保留,为了减少工作量,选择了保留,这个哲学问题留后续商榷
LByteArray
- 处理结果:钟老师已重构修改
内存设计极其不合理
- 未作合理的内存管理,未对内存的开销和释放做合理的设计,目前是有多少开多少,导致较大的性能消耗,
insert
函数就是个典型的例子;字节数组与普通的动态数组区别在于可以接受三种不同的字符串处理方式,经过编码和解码的转换之后,以字节为单位,本质上就是一个动态char *
,为了合理的管理内存,建议使用钟老师的LVector<char>
进行改造,钟老师的里面使用分配器allocator
作了合理的内存管理 - 一个
insert
的不合理设计例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23LByteArray& LByteArray::insert(const char* str, int len, int pos)
{
...
// 更新size的信息,开辟新内存
m_size = m_size + len;
unsigned char* lc = new unsigned char[m_size + 1];
// 将插入点以前的内容复制到新内存中
memcpy(lc, m_pByte, (pos) * sizeof(unsigned char));
// 复制要插入的内容到新内存中
for (int i = pos, j = 0; i < (pos + len); i++, j++)
{
unsigned char tempByte = 0;
tempByte = (unsigned char)*(str + j);
*(lc + i) = tempByte;
}
// 将插入点以后的内容复制到新内存中
memcpy(lc + pos + len, m_pByte + pos, (m_size - pos - len) * sizeof(unsigned char));
// 将字节数组存放的数据指向新内存
delete[] m_pByte;
m_pByte = lc;
return *this;
}- 未作合理的内存管理,未对内存的开销和释放做合理的设计,目前是有多少开多少,导致较大的性能消耗,
1.29 - 2.8
迭代任务
- 重构代码
LStack
,LQueue
- 走查代码
LObject
,LApplication
,LSignal
任务1
- 经过和钟老师商量,决定保留
public
继承的方式,不做覆盖,因为Qt
也是这么做的,对于是隐藏父类不需要的功能还是保留,为了减少工作量,选择了保留,这个哲学问题留后续商榷 - 目前已经完成初步重构,已经转测
任务2
LObject
- 这个类是所有类的基类,提供了
3种
非常重要的功能:对象树机制、动态属性功能、信号槽机制
对象树机制
学习的点
对象树机制
我们在构造对象的时候,为了方便内存的管理和释放,将所有的对象之间建立关系,每个对象都有父对象和子对象(当然最终的父对象没有),比如
LButton
显然就是LWindow
的子对象,如此以以来就形成了一颗多叉树用一张图理解
优点:能够做到很好的内存释放,我们确定的父对象,当父对象释放的时候,子对象也必须跟着释放,例如
LWindow
没了,那LButton
肯定也没了,对象树的功能就是父对象在释放的时候,会首先和他的父对象断开联系,然后释放以自己为根的这颗多叉树,从最下面的子对象开始依次向上释放,最终释放自身,有点Java
的gc
的感觉,如果不做处理的话,尤其是在堆上开辟的空间管理将会非常混乱;同时个人认为在GUI
编程中用的非常频繁在对象树当中,所有的对象为什么都必须处于同一个线程?
- 可能是跨线程的构造和释放不好管理吧(我也不是很清楚)
翻帖子的时候看到一个有意思的程序(重复析构)
1
2
3
4
5
6
7
8
9// main.cpp
int main()
{
// 用户编码行为不规范
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
}
判断对象是否是
new
出来的- 重载
new
运算符,对类重载new
运算符,在外部调用new
的时候会优先考虑重载的版本 - 在这里重载之后,将类内部的数据全部填充为
'L'
我们预留了一块内存判断区域,这时候
new
的实际操作顺序是,先调用我们的重载版本的new
,然后强转为本类指针被接受,然后调用构造函数,因此在构造的时候其他数据段会进行初始化,而内存判断区不会,问题解决
- 重载
RTTI
机制由于
C++
是静态类型语言,有关类的信息只在编译期被使用,编译后就不再保留,因此程序运行时无法获取类的信息。这时就需要使用「运行期类型信息」,即RTTI(Run-Time Type Information)
,运行时类型识别RTTI
主要是在用在多态里面,程序能使用基类的指针来识别实际使用的派生类类型,这个步骤在编译阶段是无法确定的typeid
- 对于非多态类型,没有虚函数表和虚函数指针的类型对象,
typeid
可以在编译期即完成计算,也就不存在RTTI
机制,dynamic_cast
更是没有必要,使用static_cast
即可满足需求 - 对于多态类型,在编译器无法确定基类指针真正指向的类型,其对象的
typeinfo
信息存储在该类的虚函数表中。在运行时刻,根据指针的实际指向,获取其typeinfo()
信息,从而进行相关操作 - 通过
dynamic_cast
进行的安全动态转换,会对应修改虚表中的相关信息,其中就包括typeinfo
的信息
- 对于非多态类型,没有虚函数表和虚函数指针的类型对象,
dynamic_cast
:将基类指针安全的转化为派生类指针从派生类指针转向基类指针,向上转换,是一个静态转换,并不是动态转换,因为子类一定包含基类
向下转换,编译器则需要沿着继承链寻找派生类是否匹配,查询
typeinfo
中的信息,匹配则转换,不匹配则按照失败处理- 这样会导致一个问题,如果继承链很庞大,效率就会低下,同时其中可能很多信息是没有用的,也消耗更多的空间成本
1
2
3
4
5
6
7
8
9
10
11
12// dynamic_cast
Animal* dog = new Dog;
// 检查dog的类型
// 如果能安全转换返回转换后的指针,不则返回空指针或者抛出异常
Dog* res = dynamic_cast<Dog*>(dog); // success
if (res)
std::cout << "Dog success.\n";
Cat* res2 = dynamic_cast<Cat*>(dog); // fail
if (res2)
std::cout << "Cat success.\n";typeinfo
类:typeinfo
类将构造函数,拷贝构造,移动构造等全部都删除了,因此不能实例化,其中含有方法name()
,==
,!=
,使用typeinfo
的唯一方式是通过关键字typeid
1
2
3
4
5
6
7
8
9
10
11
12
13
14int a = 1;
// 不同的编译器输出的结果不同,Ubuntu下这里返回的是 i
std::cout << typeid(a).name() << '\n';
// 但是尽管编译器的实现存在差异,typeid是可以进行==号和!=号的比较的
std::cout << (typeid(a) == typeid(int)) << '\n'; // true
std::cout << (typeid(a) == typeid(double)) << '\n'; // false
// typeid作用于自定义类
// 可以看出是有一定规律的,可以在构造函数的时候做一定解析存入类中
std::cout << typeid(Animal).name() << '\n'; // 6Animal
std::cout << typeid(Dog*).name() << '\n'; // P3Dog
std::cout << typeid(Cat**).name() << '\n'; // PP3Cat
代码走查的问题
Release
函数当中是释放子对象的过程感觉有点问题- 子对象可能也有自己的子对象列表,所以应该按照递归的方式去调用,当本对象没有子对象才做真正的释放,这样感觉合理一些
2.21
更新:我看错了,它delete
的是pChild
,这样下去就是递归调用,这一条没有问题
用
LList
替代std::list
LList
目前并未走查,也并未针对可能存在的问题修改或重构,代码健壮程度不如LVector
,但是选用LVector
的话由于二者接口名称的不同,可能需要改动的工作量较大
RTTI
机制我能想到的就是
typeinfo().name()
,它的返回值是有一定规律的,当然我们这里是用作自定义类类名的存储,做一个算法解析,然后在构造函数的时候调用即可具体见上面”学习的点”
动态属性功能
首先看
Qt
的属性机制,给我总的看法:我觉得很震惊,甚至感觉有点脱裤子放屁属性声明依托于
Q_PROPERTY
宏最核心的功能:为类内的成员属性很方便的设置一个
getter
和setter
方法,当然还有一些其他的附加方法我自己用
Qt
写了一个demo
做演示
动态属性和静态属性
- 除了类当中原本就存在的属性,在程序运行的时候还可以运行时插入新的属性,这就是动态属性和静态属性的区别
与我们的进行对比
- 个人认为
Qt
这么做的最大目的就是将属性的getter
和setter
方法做接口的统一,也就是使用property
和setProperty
,Qt
用了这个宏的方法实现了基础功能和更加复杂的多样化功能 - 但对于我们目前而言,我觉得能够做到
getter
和setter
就可以了,我们使用了LVector<PropertyStruct*>
来存储构成存储动态属性的数组
- 个人认为
信号槽机制
关于信号槽机制原理见
LSignal
中存储连接到本对象某个槽函数的所有信号列表
事件处理接口
event()
,可以处理定时器事件和信号槽事件,我关心信号槽事件,涉及到类LSignalEvent
,见下面
LApplication
学习的点
单例模式
不管有多少个
Window
,只能有一个Application
实例,这个实例在一个进程中有且只能只有一个简单的单例程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// singleton.h
class Singleton {
public:
static Singleton* getInstance();
void print();
protected:
Singleton() = default;
virtual ~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
static Singleton* m_instance;
};1
2
3
4
5
6
7
8
9
10
11
12
13
14// singleton.cpp
Singleton* Singleton::m_instance = nullptr;
Singleton* Singleton::getInstance() {
if (nullptr == m_instance)
m_instance = new Singleton();
return m_instance;
}
void Singleton::print() {
std::cout << "address: " << m_instance << '\n';
}
单例与多态结合
在构造
Application
的时候,构造单例平台相关接口,平台接口返回平台相关的一些信息,也是单例LPlatformInterface
派生出LLinuxInterface
和LWin32Interface
,并且LPlatformInterface
是一个抽象类,它内部的平台相关功能在派生类覆写,通用功能自己写了,并且它也是一个单例模式,因此派生类也是单例抽象类无法实例化,但是我们构造
Application
的时候已经实例化出平台相关的派生类单例对象了;这时候多态就派上用场了,由于单例指针的唯一性,我们通过基类方法获得的指针,进而调用的方法就是平台相关的派生类方法,这就是多态简单的多态程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
class Animal {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() { cout << "Dog speak." << endl; }
};
class Cat : public Animal {
public:
void speak() { cout << "Cat speak." << endl; }
};
int main() {
Animal *dog = new Dog();
dog->speak();
Animal *cat = new Cat();
cat->speak();
return 0;
}
事件机制(简单理解)
- 我们的程序是通过事件循环进行轮询,不断检查是否有新的事件发生;发生之后,添加到一个事件队列中,事件循环再依次处理
- 每个线程都有自己的事件循环,
Application
是我们的主程序框架,在这就是主线程和主事件循环,主事件循环主要处理与用户界面相关的事件,当然还有所有事件的管理和分发;其他工作线程处理各自的任务,这样可以极大的提高效率 - 但是事件机制(
event
)在我们的代码里面如何应用,目前我看不懂。。。- 跟海洋哥交流之后,说事件机制修改过几版,所以应该没有大问题,因此我只需要知道大致运作过程就好了
- 当然主程序框架为主事件循环提供了几个接口,例如
exec()
(其中主事件循环),quit()
(退出事件循环)等
代码走查的问题
LApplication
和lplatforminterface
的移动构造和移动赋值函数的参数应该不需要带上const
- 右值引用代表进来的是一个将亡对象,一般写移动的内部实现是将二者的数据区
swap
,这样原来的数据区就会被自动回收,这样可以减少拷贝的次数,所以应该没有const
- 右值引用代表进来的是一个将亡对象,一般写移动的内部实现是将二者的数据区
代码中的
TODO
LSignal
学习的点
信号槽机制
- 信号槽机制的作用是当将某个信号函数和某个响应的槽函数绑定在一起的时候,当我的信号被触发的时候,就会自动触发对应的槽函数进行响应,比如点击
button
,触发的信号是clicked
,可以关闭整个窗口,这里的槽函数就是close()
- 信号槽机制的作用是当将某个信号函数和某个响应的槽函数绑定在一起的时候,当我的信号被触发的时候,就会自动触发对应的槽函数进行响应,比如点击
LSignal
类信号类,这个类用作信号槽的管理,里面有方法
connect
,emit
,disconnect
等connect
函数做了两个重载,为了区分类对象的成员函数和普通函数(比如
lambda
)目前的实现无法处理带捕获的
lambda
,个人阅读LSignalEvent
的代码之后,觉得std::function
可能可以解决问题,由于槽函数的返回值是void
,因此在初始化的时候使用std::function<void()>
作为回调函数,然后绑定的时候赋值即可,这样带捕获的和不带捕获的就能统一,至于捕获的变量如何处理是用户的事情了测试样例在
./snippet/SignalEmitTest
中但是具体如何使用
std::function
替代函数指针并且做好类成员变量和普通函数(支持lambda
)的分别处理,需要一起讨论做进一步的设计
测试程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// ./snippet/SignalEmitTest/main.cpp
class Test : public LObject {
public:
Test() = default;
~Test() = default;
void run(int num) { std::cout << "class function: " << num << '\n'; }
};
void test1();
int main() {
test1();
return 0;
}
void test1() {
LSignal<int> s;
// 为该信号绑定槽函数
// 类成员对象函数
Test t;
s.connect(&t, &Test::run);
// 普通函数
s.connect([](int num) {
std::cout << "lambda: " << num << '\n';
});
s.emit(1); // 触发信号
}connect
函数的作用是把槽函数加入到自身的槽函数列表中
emit
函数的作用是发送信号,然后通过事件系统进行槽函数的调度disconnect
函数就很简单了,就是删除对应的槽函数
LSignalConnection
- 真正做工作的信号连接类,是将信号发出,调度槽函数执行
- 为了区分类的成员函数和普通函数,做了一个简单的多态
LSignalConnection
->
LSignalConnectionToMethod
(类的成员函数) 和LSignalConnectionToFunction
(普通函数)- 对于两种情况各自分别处理
代码走查的问题
关于
LSignalBase
类之前的想法
- 注释中写到:基类从实际的信号类中剥离泛型特性,方便使用统一的基类指针来实现信号列表的记录
- 但是实际上整个代码里面只有一个
LSignal
继承它,并且覆写了一些方法 - 在
LSignal
中存储LSignalConnection
指针来维护槽函数相关的信息,槽函数分为成员函数和普通函数,二者的使用具有一定的区别,因此在LSignalConnection
这里用到了多态,才是使用基类指针管理派生类 - 个人认为
LSignalBase
的存在有一些多余,当然目前能用
2.20
更新- 注意不是
LSignal
,是LSignal<... Args>
。LSignal<... Args>
不是【一个】类,是【一系列】类。剥离泛型特性的含义就是可以用统一的LSignalBase
指针处理任意LSignal<... Args>
的实例类。任一LSignal<... Args>
的实例,都以LSignalBase
作为基类 - 而之所以我之前会疑惑在
base
类中没有connect
方法,就是因为connect
方法需要接受... Args
参数,是模板类的成员函数,所以才需要base
指针统一管理,这又是多态
- 注意不是
阅读到
LSignalEvent
的代码时候,没有对类的成员函数做相应处理(参见LSignalConnection
),导致写demo
时加上注释这一段编译不通过,测试程序在./snippet/SignalEmitTest
,具体对比LSignal
对两种类型的槽函数的不同处理2.20
更新是我自己的原因,参数的放置有问题,第一个参数是成员函数指针,第二个参数是类对象指针,后面才是相应的参数
这是因为
LSignalEvent
里面使用了std::function
,std::function
很方便的能够把类的成员函数,普通函数,带捕获和不带捕获的lambda
统一起来,但是需要注意使用的参数规范但是关于上面提到的问题,个人的看法是,第二个重载感觉有点抽象,第一个重载可以处理普通函数,带捕获和不带捕获的
lambda
;第二个这个参数形式,一开始确实没办法让我想到可以适用于类的成员函数,因为没有给定第二个参数,相当于第二个类指针的参数隐藏在了rest...
中,这里建议做一些修改参照了这里的思路之后,关于
LSignal
无法处理带捕获的lambda
,个人认为就可以使用std::function
替代原本的函数指针了
会后总结
LStack,LQueue
- 自己修改后仍存在一系列代码规范问题,已按要求做相关修改
LObject
PropertyStruct
结构体中的name
和value
建议用pair
做相关替代MetadataStruct
结构体中存储的内容,考虑在后续引入RTTI
之后,可以直接放在Object
类中做成员而不用封装一层结构体- 代码中使用
std::list
的部分,后续建议替换为LVector
,问题在于接口名称统一以及工作量大小 - 代码中使用
std::set
的部分,后续建议替换为LSet
RTTI
机制,考虑是否引入?如何引入?
LApplication
- 代码中移动构造函数的右值引用不能带上
const
(已处理) - 代码中使用
std::string
的部分,功能完善之后改为LString
LSignal
- 代码中使用
std::list
的部分,同LObject
- 现在的信号槽机制无法处理带捕获的
lambda
,个人研究后建议可以使用std::function
对函数指针做统一的管理,具体用法见snippet/StdFunctionDemo
,尚需做后续研究(新功能,不影响原先的版本) LSignalEvent
中构造函数的第二个重载设计很不合适,已提相关bug
2.19 - 3.1
迭代任务
- 走查线程与同步相关内容,包括线程管理、线程数据、互斥锁、读写锁
学习的点
线程管理
lthreadinterface
lthreadinterface
又是一个提供平台接口的单例加多态的例子,这个单例涉及到多线程,需要说明一下
- 单例在多线程中的表现
- 单例,单例,顾名思义,意思是在整个程序当中内存中只允许出现一份,保存在静态存储区的,所有的线程共享这一份单例指针
- 类当中的接口都是需要不同线程返回不同的值
- 思考到这里,我就在想既然不同线程的结果需要不一样,那么单例设计是否正确呢?
- 在这里的线程设计是
ok
的,因为currentThreadId()
,sleepForMicroseconds()
,createThread()
这些接口,底层调用的方法都是线程相关的系统调用,这些方法在不同线程当中返回的值自然是不一样的,而我用统一的单例指针做了管理,这就是这个设计模式的整体思路,经过测试程序验证也是ok
的,因此没毛病
lposixthreadinterface
多线程私有数据
TSD(Thread Specific Data)
- 在多线程当中,所有线程共享程序段中的变量,例如一个全局变量,所有线程都可以使用,因此会出现线程同步的相关问题,也就有互斥锁、读写锁、条件变量、信号量等手段;但是现在我们需要一些数据,线程可以单独进行使用,其他线程无法访问,需要引入线程存储
pthread_key_t
,对应一个key
的变量,这个键在每个线程中可以映射到各自线程的数据区,可以存储对应的值,因此key
是共享的,起着索引的作用(理解这一点,后续理解销毁就轻松了),但是在各自线程中存储的数据是不一样的,这就是实现的大致思路,这也是线程存储的意义pthread_key_delete
,关于这个东西的销毁机制需要说明一下- 查阅资料之后得知,所有的线程都可以通过共享的
key
得到线程私有的value
,当线程内部调用pthread_key_delete
之后,当前线程key
与value
的连接断开,不能再通过key
获取到原先的value
,但是注意,原先的数据还在内存中,现在本线程再通过pthread_getspecific
获取value
得到的是空或者是系统设置的默认值,反正得不到之前的数据 - 那么之前的数据如何清理呢?这就需要
pthread_create
的第二个参数了,第二个参数传入一个函数指针,用于对value
的清理;当key
被delete
之后,系统会在线程结束的时候自动调用清理函数释放相关资源 - 注意,第二个参数的类型是
void*
,和线程的普遍设计一样,都给定void*
,然后自己进行特定类型指针的转化即可 - 值得提到的是,一定需要手动调用
pthread_key_delete
对存储键进行销毁,否则可能无法正确激活系统对清理函数的调用,因为系统可能认为存储键还绑定着,这样就可能导致内存泄漏
- 查阅资料之后得知,所有的线程都可以通过共享的
- 关于如何使用见
./snippet/PthreadKeyTest
pthread_once
:在多线程中让某些操作只执行一次1
2
3int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
// 功能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。- 多线程中,尽管
pthread_once
会出现在多个线程中,但是能保证init_routine
的函数只执行一次,once_control
作执行一次的标志作用,并且Linux
要求once_control
的初始值必须为PTHREAD_ONCE_INIT
,否则函数行为不正常
- 多线程中,尽管
有以上二者就能够理解代码中获取线程数据的设计了,代码中注释理解
reinterpret_cast
:reinterpret_cast
用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个bit
复制的操作。这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个int*
指针、函数指针或其他类型的指针转换成string*
类型的指针也是可以的,至于以后用转换后的指针调用string
类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22pthread_once_t LPosixThreadInterface::sm_onceFlag = PTHREAD_ONCE_INIT;
// 函数用作获取线程存储数据的指针
LThreadData *LPosixThreadInterface::currentThreadData()
{
// 用pthread_once保证,initThreadLocalStorage只执行一次
// initThreadLocalStorage函数用作初始化pthread_key_t,也就是存储键,可以让key共享,但是不同线程各自存储各自的数据
pthread_once(&sm_onceFlag, LPosixThreadInterface::initThreadLocalStorage);
// pthread_getspecific(),获取key对应的数据,这里就是线程数据;这样我的线程数据被更改之后就能通过这个接口通过key进行访问获得
LThreadData *pThreadData = reinterpret_cast<LThreadData *>(pthread_getspecific(sm_tlsKeyThreadData));
// 有则获得,无则创建返回
if (!pThreadData)
{
pThreadData = new LThreadData();
int res = pthread_setspecific(sm_tlsKeyThreadData, pThreadData);
if (res != 0)
{
throw LException("无法在线程本地存储中设置线程数据。");
}
}
return pThreadData;
}
lwin32threadinterface
设计同unix
平台一样,只是平台接口不同,具体可查看文档,我不是搞windows
的,所以这里不写了
lthread
关于
threadRoutine()
函数的设计threadRoutine()
是干嘛的?是目标线程的入口函数,用户使用LThread
类的时候,不能直接使用该类,因为本类中真正用户做的run()
是纯虚函数,所以需要派生覆写。但是LThread
类作为线程管理类,在执行线程任务的时候除了跑用户的函数,肯定需要做一些类似系统记录的东西,比如做一些赋值,发一些信号等等,因此threadRoutine()
才是真正传递给pthread_create()
的函数,并且由于需要作为pthread_create()
的参数,需要是静态的,执行函数需要的参数通过后面的void*
参数传入我们现在看一下
threadRoutine()
的具体实现- 在代码中,有且仅有
start()
中用到了threadRoutine
,并且将this
指针作为参数传入,至于为什么前面已经提到 - 具体的解读在代码注释中,这个设计真的很精妙
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46void LThread::start()
{
if (isStarted())
{
std::cerr << "线程已启动,无法再次启动。" << std::endl;
return;
}
// 将threadRoutine函数作为子线程真正执行的函数,this指针作为参数传入
m_threadId = LPlatformThreadInterface::instance()->createThread(LThread::threadRoutine, this);
}
// 注意,执行到这里的时候函数体内部已经是子线程中了
void *LThread::threadRoutine(void *pParam)
{
// 这里的pParam就是主线程中的this指针,子线程需要用到主线程中本类的相关内容,因此需要传入
if (!pParam)
{
throw LException("线程入口函数未能获取正确的参数。");
}
LThread *pThread = reinterpret_cast<LThread *>(pParam);
// 创建线程数据
LThreadData *pThreadData = LThread::currentThreadData();// 子线程的pThreadData
pThread->m_pTargetThreadData = pThreadData;// 通过this指针,更新主线程的类的成员
pThreadData->m_pThread = pThread; // 通过this指针,更新子线程的pThreadData的属性
// 下面的操作都需要主线程的this指针才能办到,也是执行子线程必不可少的管理步骤
// 发送线程起始信号
pThread->startSignal.emit();
// 调用线程任务函数
// 这里才是真正执行用户代码的地方
pThread->run();
// 清理
// pThread->cleanup();
// 发送线程结束信号
pThread->finishSignal.emit();
pThread->m_threadId = 0;
// 目前暂不处理线程返回
return nullptr;
}- 在代码中,有且仅有
互斥锁
lmutex
为了做到平台适配,定义了统一基类
LPlatformMutexContext
指针,针对不同平台派生,使用多态进行处理,在后续的lock(),trylock(),unlock()
中就很方便的使用LPlatformMutexContext
的接口即可- 在
LMutex
构造函数中,使用#ifdef、#elif、#else、#endif
宏初始化指针
1
2
3
4
5
6
7
8
9
10
11
12LMutex::LMutex()
{
m_pMutexContext = new LPosixMutexContext();
m_pMutexContext = new LWin32MutexContext();
throw LException("无法创建互斥锁上下文:无法检测当前操作系统平台。");
}- 在
lmutexcontext
- 是一个抽象基类,延申出
unix
和win32
的各自派生,在类当中的函数都是纯虚函数,等待子类的覆写,由于无法实例化,因此需要在上层在做一层包装,也就是在lmutex
中使用该类指针进行多态的管理,做到了平台无关
lposixmutexcontext
关于互斥锁属性中的普通锁和检错锁
互斥锁属性通过
pthread_mutex_init()
进行设置,第一个参数是互斥锁指针,第二个参数是属性指针普通锁属性:
PTHREAD_MUTEX_TIMED_NP
,默认值,线程加锁之后,实际上在锁这里形成了等待队列检错锁属性:
PTHREAD_MUTEX_ERRORCHECK_NP
,和普通锁的区别是检测到自身重复加锁之后不会阻塞,而是返回EDEADLK(35)
,这样可以一定程度上作加锁的安全检测
lwin32mutexcontext
win
平台下的互斥锁,官方文档:Synchapi.h 标头 - Win32 apps | Microsoft Learn
- 代码中使用
CRITICAL_SECTION
关键节对象代表互斥锁,值得注意的是win
平台自身也具有互斥锁,二者的区别在于关键节对象只能单个进程的线程使用,不支持跨进程共享 - 一些接口
InitializeCriticalSection
、DeleteCriticalSection
、EnterCriticalSection
、TryEnterCriticalSection
、LeaveCriticalSection
,CriticalSection
意为临界区,分别对应初始化、销毁、加锁、尝试加锁、解锁,其中只有尝试解锁trylock()
有返回值,其他的函数均没有返回值,均由操作系统自行处理,所以没有做错误和异常处理
读写锁
lreadwritelock、lreadwritelockcontext
整体设计和互斥锁一样,这里不再赘述
lposixreadwritelockcontext
锁的静态初始化和动态初始化(以读写锁为例)
- 静态初始化
1
pthread_rwlock_t m_rwlock = PTHREAD_RWLOCK_INITIALIZER;
- 动态初始化
1
2pthread_rwlock_t m_rwlock;
pthread_rwlock_init(&m_rwlock, nullptr);二者的区别:
- 静态初始化在编译期就初始化完毕;动态初始化在运行期
- 静态初始化不能设置锁的相关属性,只能使用默认的;动态初始化可以
- 静态初始化的锁存储在静态存储区;动态初始化在堆区
- 静态初始化使用完毕之后不需要手动
destroy
;动态初始化需要
读写锁的特别之处
- 为什么有读写锁?为了增加效率,不同线程可以通过读锁同时读取,共享数据,但是不能修改;需要修改的时候使用排他的写锁,写锁的表现行为和互斥锁是差不多的
- 在系统提供的
API
中,加锁有加读锁和加写锁两个,但是解锁只有一个,为什么?- 同一个线程不可能在持有读锁的同时,再去持有写锁,否则会导致死锁,程序阻塞,具体见
./snippet/RwlockTest
- 当然先加排他的写锁,再加共享的读锁是
ok
的,因为排他写锁首先能写,那必然读就隐含在里面了;只是不能持有共享读锁的情况下再加排他写锁 - 读写锁明确了读锁就只能读,不能写,如果每次读了就需要写,那还不如使用互斥锁
- 因此只有一个解锁接口
unlock()
- 同一个线程不可能在持有读锁的同时,再去持有写锁,否则会导致死锁,程序阻塞,具体见
lwin32readwritelockcontext
- 代码中使用
SRWLock
代表读写锁 - 一些接口
InitializeSRWLock
、AcquireSRWLockShared
、TryAcquireSRWLockShared
、AcquireSRWLockExclusive
、TryAcquireSRWLockExclusive
、ReleaseSRWLockShared
、ReleaseSRWLockExclusive
,其中只有trylock()
有返回值,其他函数均没有返回值,不用做异常处理
win32
下面,释放读写锁是分开的,没有统一的释放接口,只有各自释放的接口,共享读锁ReleaseSRWLockShared
和排他写锁ReleaseSRWLockExclusive
- 与互斥锁的临界区作对比
- 临界区中销毁临界区
DeleteCriticalSection
,解锁LeaveCriticalSection
,这两个是区分开的,对应unix
下面的pthread_mutex_destroy
和pthread_mutex_unlock
- 但是读写锁没有销毁函数,销毁操作通过读写锁的释放实现,即
ReleaseSRWLockShared
和ReleaseSRWLockExclusive
- 临界区中销毁临界区
代码走查问题
线程管理
lthreadinterface
currentThreadId()
接口注释没有@return
1
2
3
4
5
6
7/**
* @brief 获取当前线程的线程 ID。
* @details 在不同的平台下获取的 ID 数据类型:
* - Unix (POSIX) 平台: pthread_t 类型
* - Windows 平台:DWORD 类型,即 unsigned long
*/
virtual unsigned long currentThreadId() = 0;禁用了拷贝构造和赋值函数,按理同样应该禁用移动构造和赋值函数
- 当然在本类中这是单例将构造函数私有,这倒无所谓,但是其他类也有类似的问题,因此还是罗列出来
1
2
3
4
5
6
7
8
9/**
* @brief 禁止进行拷贝构造。
*/
LPlatformThreadInterface(const LPlatformThreadInterface &) = delete;
/**
* @brief 禁止进行赋值构造。
*/
LPlatformThreadInterface &operator=(const LPlatformThreadInterface &) = delete;
lposixthreadinterface
类最前面没有
doxygen
注释1
2
3
4
5
6
7
8
9
10
11
12
13
class LPosixThreadInterface : public LPlatformThreadInterface
{
public:
...
}currentThreadData()
中初始化线程键相关的东西,应放在构造函数中1
2
3
4
5
6LThreadData *LPosixThreadInterface::currentThreadData()
{
pthread_once(&sm_onceFlag, LPosixThreadInterface::initThreadLocalStorage);
...
}- 这里会引申出另一个问题,就是
key
的释放问题,代码中并没有给出pthread_key_delete
,也没有在pthread_create
的时候给出清理函数(第二个参数),这样可能会导致线程退出的时候线程数据没办法得到正确释放,从而导致内存泄漏 - 在析构函数中对键进行销毁,调用
pthread_key_delete
- 给出
pthread_create
的第二个参数,是一个函数指针,可以在代码里面设置一个静态的clean
函数,在实现里面直接delete
即可,会调用LThreadData
的析构函数,这个就不用担心了
- 这里会引申出另一个问题,就是
关于代码里面的所有线程
ID
类型,建议替换为系统给出的线程ID
类型pthread_t
2.26
更新- 为了保持平台无关适配,由于
win
平台不是pthread_t
,而是DWORD
,因此需要做一个统一,因此用unsigned long
,这条没问题 - 同理,
unix
下睡眠的useconds_t
,本质是unsigned int
,但是为了作适配,由于win
平台是unsigned long
,因此统一用unsigned long
- 为了保持平台无关适配,由于
1
2
3
4/**
* @brief 获取当前线程的线程 ID。
*/
unsigned long currentThreadId() override;currentThreadId()
接口注释没有@return
,同上createThread()
接口注释没有@param pParam
1
2
3
4
5
6/**
* @brief 创建并执行线程。若创建成功,返回新线程 ID。
* @param fpStarter 入口函数指针
* @return 新线程 ID。若失败则返回 0。
*/
unsigned long createThread(void *(*fpStarter)(void *), void *pParam) override;joinThread()
有两行代码可以写为一行1
2
3
4
5
6
7void LPosixThreadInterface::joinThread(unsigned long threadId)
{
pthread_t tid = (pthread_t)threadId;
int res = pthread_join(tid, nullptr); // 暂不处理返回值
...
};
lwin32threadinterface
类最前面没有
doxygen
注释,同lposixthreadinterface
currentThreadId()
接口注释没有@return
,同lposixthreadinterface
createThread()
接口注释没有@param pParam
,同lposixthreadinterface
构造函数中的进程只执行一次的设计同
unix
下的pthread_once()
是相同的功能,但是二者放的位置不同,两种位置均有说法,但是为了保持代码风格统一,可以考虑做一下统一1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27LWin32ThreadInterface::LWin32ThreadInterface()
{
InitOnceExecuteOnce(&sm_onceFlag, initThreadLocalStorage, nullptr, nullptr);
}
LThreadData *LWin32ThreadInterface::currentThreadData()
{
LThreadData *pThreadData = reinterpret_cast<LThreadData *>(TlsGetValue(sm_tlsKeyThreadData));
DWORD lastError = GetLastError();
if (!pThreadData && lastError == ERROR_SUCCESS)
{
// 如果线程本地存储的值为 nullptr,则创建新的 LThreadData 对象
pThreadData = new LThreadData();
if (!TlsSetValue(sm_tlsKeyThreadData, pThreadData))
{
// 设置线程本地存储的值失败
lastError = GetLastError();
delete pThreadData;
pThreadData = nullptr;
throw LException("无法在线程本地存储中设置线程数据。");
}
}
return pThreadData;
}代码中关于创建线程执行回调函数的设计,很抽象
- 第一,创建一个
LPair
对象干什么,还是new
出来的,还把函数指针包进去了 - 第二,
threadEntry
里面把函数指针和参数拿出来,然后手动执行,这一步没有必要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24unsigned long LWin32ThreadInterface::createThread(void *(*fpStarter)(void *), void *pParam)
{
unsigned long threadId = 0;
// 创建LPair对象,用于封装函数指针和参数
auto pParams = new LPair<void *(*)(void *), void *>(fpStarter, pParam);
// 创建并执行线程
HANDLE hThread = CreateThread(nullptr, 0, threadEntry, pParams, 0, &threadId);
...
}
...
DWORD WINAPI LWin32ThreadInterface::threadEntry(LPVOID pParameter)
{
auto pParams = static_cast<LPair<void *(*)(void *), void *>*>(pParameter);
void *(*fpStarter)(void *) = pParams->key();
void *pParam = pParams->value();
fpStarter(pParam);
delete pParams;
return 0;
}- 第一,创建一个
lthread
同样应禁用移动构造和赋值函数,同
lthreadinterface
目前注释了三个接口,
terminate(),setThreadFunction(),cleanup()
,是否考虑后续引入terminate()
接口未写@return
注释1
2
3
4// /**
// * @brief 尝试强行终止目标线程。
// */
// bool terminate();threadRoutine()
接口未写@return
注释1
2
3
4
5
6
7
8/**
* @brief 目标线程的入口函数。
* @details 遵循各平台下线程入口函数的约定,形参和返回值都为 void * 类型。
* 通过入口函数形参传入 \a this 指针,再在此入口函数中通过 this 指针来执行信号发送等操作。
* @param pParam 入口函数形参
* @note 注意该函数应执行于目标线程上。
*/
static void* threadRoutine(void *pParam);LThread
的构造函数可以内联1
2
3
4// lthread.cpp
LThread::LThread() : LObject()
{
}
线程数据
lthreaddata
同样应禁用移动构造和赋值函数,同
lthreadinterface
文件中做了
LDummyMainThread
的声明,但是本类中,甚至所有其它类中没有地方用到它1
2
3
4
5
6
7
8
9
10
11
12...
class LDummyMainThread;
...
class LThreadData
{
friend class LThread;
friend class LDummyMainThread;
...
}
互斥锁
lmutex
析构函数注释少写了句号(
。
)1
2
3
4/**
* @brief 析构函数
*/
~LMutex();lock()
接口注释不应该有@return
1
2
3
4
5/**
* @brief 进行加锁操作,使当前线程占有该锁。若另一线程已持有此锁,将阻塞当前线程。
* @return 是否成功加锁
*/
void lock();拷贝赋值函数少写了
const
,同时在代码里面未禁用移动构造和赋值,针对互斥锁当前的情况,应当禁用移动构造和赋值函数1
2
3
4
5
6
7
8
9/**
* @brief 禁止拷贝构造。
*/
LMutex(const LMutex &) = delete;
/**
* @brief 禁止赋值构造。
*/
LMutex &operator=(LMutex &) = delete;
lmutexcontext
<pthread.h>
头文件的引入,有点多余1
2
3
析构函数注释问题,同
lmutex
lposixmutexcontext
class
前面的注释内容@class
有误,是LPosixMutexContext
1
2
3
4
5
6
7
8/**
* @class LMutex
* @brief POSIX 平台互斥锁上下文。
*/
class LPosixMutexContext : public LPlatformMutexContext
{
...
}析构函数注释问题,同
lmutex
析构函数和
trylock()
部分错误处理使用了std::cerr
trylock()
这里可以理解,因为是试探能否加锁成功,不成功返回false
,通过标准错误输出信息也可以- 另外,
std::cerr
对应的标准错流是无缓冲的,没必要使用std::endl
再冲洗一次缓冲区,输出'\n'
即可 - 析构函数这里失败的话,建议使用异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22LPosixMutexContext::~LPosixMutexContext()
{
int res = pthread_mutex_destroy(&m_mutex);
if (res != 0)
{
// Handle error properly
std::cout << "pthread_mutex_destroy() gave error code: " << res << std::endl;
}
}
bool LPosixMutexContext::tryLock()
{
...
else if (res == EBUSY)
{
std::cerr << "LMutex::tryLock(): Target mutex is busy." << std::endl;
return false;
}
...
}在锁的构造函数当中,使用检错锁,但是在
lock()
当中未作重复加锁的针对性判断,具体见./snippet/MutexLockTest
1
2
3
4
5
6
7
8void LPosixMutexContext::lock()
{
int res = pthread_mutex_lock(&m_mutex);
if (res != 0)
{
throw LException(LString("互斥锁加锁失败,错误代码:").append(LString(res)));
}
}trylock()
的判断并无问题,经过验证普通锁和检错锁在trylock()
下加锁失败返回的都是EBUSY(16)
lwin32mutexcontext
- 析构函数注释问题,同
lmutex
std::cerr
问题,同lposixmutexcontext
读写锁
lreadwritelock
- 拷贝赋值函数问题,同
lmutex
lreadwritelockcontext
<pthread.h>
头文件的引入,同上
lposixreadwritelockcontext
构造函数使用了静态初始化,经研究动态和静态的区别之后认为使用动态初始化可能好一点
1
2
3
4LPosixReadWriteLockContext::LPosixReadWriteLockContext()
{
m_rwlock = PTHREAD_RWLOCK_INITIALIZER;
}- 静态初始化出来的锁不需要手动释放了,在析构函数中调用了
destroy
,可能会出现不可预测的运行时问题 - 互斥锁那边使用的是动态初始化,当然是因为需要设置检错锁必须这么做,但是个人认为为了保持代码风格的统一,建议使用动态初始化
- 静态初始化出来的锁不需要手动释放了,在析构函数中调用了
std::cerr
问题,同lposixmutexcontext
lwin32readwritelockcontext
std::cerr
问题,同lposixmutexcontext
代码风格统一问题,
trylock()
的判断,逻辑都是一样的,大致结构也应该一样1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41//互斥锁
bool LWin32MutexContext::tryLock()
{
// 尝试加锁操作
if (TryEnterCriticalSection(&m_mutex) != 0) //这里有 !=0
{
return true;
}
else
{
std::cerr << "无法获取互斥锁。" << std::endl;
return false;
}
}
//读写锁
bool LWin32ReadWriteLockContext::tryLockRead()
{
if (TryAcquireSRWLockShared(&m_rwlock)) //这里没有
{
return true;
}
else
{
std::cerr << "LWin32ReadWriteLockContext::tryLockRead(): Failed to acquire read lock." << std::endl;
return false;
}
}
bool LWin32ReadWriteLockContext::tryLockWrite()
{
if (TryAcquireSRWLockExclusive(&m_rwlock)) //这里没有
{
return true;
}
else
{
std::cerr << "LWin32ReadWriteLockContext::tryLockWrite(): Failed to acquire write lock." << std::endl;
return false;
}
}unlock()
接口,个人认为应当实现- 在
win
平台下,尝试释放一个为获取的读锁或者写锁,由于函数没有返回值,无法像unix
一样根据结果进行判断,但是win
平台调用内部做了相关处理,不用我们操心,在这里对应的结果就是函数执行会被忽略 - 因此,这里分别执行释放读锁和写锁就行
1
2
3
4void LWin32ReadWriteLockContext::unlock()
{
throw LException("Win32 unlock() 未实现,请使用 unlockRead() 或 unlockWrite() 。");
}- 在
3.4 - 3.15
迭代任务
- 走查画笔
LPen
,画刷LBrush
、LLinearGradient
,菜单LMenu
、LMenuItem
部分代码
学习的点
画笔
LPen
我以为这个类是拿来做画笔的相关绘制的,但其实设计并非我想的这样。这个类LPen
只是一个数据存储类,保存了画笔需要的一些信息,例如画笔宽度、画笔连接样式、画笔颜色等,真正管理绘画的类是绘画引擎类,对于LPen
是LDrawEngine
以及其下面的工具类,这些类里面保存了LPen
对象,并且针对不同的绘画做了不同的处理,包括下面的画刷LBrush
也是一样的道理
画刷
LBrush
也是一个数据存储类,具体思路见LPen
,不再赘述
LLinearGradient
大致设计思路:
这个类与
LBrush
类一起使用,以指定线形渐变笔刷,在类当中存储了渐变点起始坐标、结束坐标和标识的渐变点数组1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @brief 标识渐变起点坐标。
*/
LPoint m_start;
/**
* @brief 标识渐变终点坐标。
*/
LPoint m_finalStop;
/**
* @brief 标识渐变点数组,包含渐变比例参数和颜色,渐变比例参数范围为[0,1]。
*/
LVector<LPair<double, LColor>> m_stops;- 第一个参数是渐变比例参数,范围是
0~1
,如何理解? - 首先理解如何渐变,从起始点开始到结束点,连起来是一条线段,我们的
LBrush
是沿着这条线段进行颜色渐变,垂直线段的颜色是一样的; - 这个比例参数就是在线段上的比例对应位置想要设置的颜色,比如设置一个这个参数之后,左右的线段的渐变就被分为了两个区域,例如从黑到白变成黑到红到白,这两种的视觉效果是不一样的
- 第一个参数是渐变比例参数,范围是
更多接口的作用见上面,基本每个接口都或多或少存在问题,就写在上面了
可以通过一个测试程序看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, const char *argv[])
{
LWindowApplication app;
LDrawWindow dw;
LDrawContext *dc = dw.drawContext();
LLinearGradient line(LPoint(100, 0), LPoint(300, 0)); // 渐变从(100,0)到(300,0)
dc->setBrushGradient(line);
dc->drawRect(LRect(0, 0, 400, 400));// 画一个矩形
dw.show();
app.exec();
}执行下来大概就是这个结果
菜单
LMenu
整体设计
菜单的设计牵扯到窗体系统中的一大堆,这部分不归我走查,我也没有这么多精力走查,所以里面还是有一些一知半解的地方
LMenu
的菜单的含义为弹出式菜单,本质上是一个弹出式窗口,内部存在可点击的菜单项,还能添加分割线对菜单进行分组内部的成员变量
- 四个静态变量,默认菜单项宽度和高度、默认分割线区域高度很容易理解;默认(竖直方向)内边距是指菜单项与边框之间的距离,下面画出一个图就理解了(当然阅读了
repaint()
接口之后,这里其实不只是竖直方向,四边都留了间距) - 菜单项中存放了菜单项需要的一些属性,其中分割线是菜单项的一个派生类,绘图引擎会根据这个做不同的绘制
m_pointingItemIndex
是一个索引,用来表示当前鼠标指向的是哪一个菜单项,这个东西是通过当前鼠标的坐标与m_itemPos
数组结合确定的,这个数组存储了所有菜单项的底端y
坐标,找到范围即可明确- 当然还存储了一个字体类型,可以设置菜单项中的字体样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46class LMenu
{
...
private:
/**
* @brief 默认菜单项高度。
*/
static const int DefaultItemHeight;
/**
* @brief 默认分隔线区域高度。
*/
static const int DefaultSeparatorHeight;
/**
* @brief 默认菜单宽度。
*/
static const int DefaultWidth;
/**
* @brief 默认(竖直方向)内边距。
*/
static const int DefaultPadding;
/**
* @brief 菜单项容器。
*/
LVector<LMenuItem *> m_items;
/**
* @brief 记录各菜单项的竖直方向底端位置,用于判定当前鼠标指向的菜单项索引。
*/
LVector<int> m_itemPos;
/**
* @brief 鼠标当前指向的项索引。负数表示没有指向任何项。
* @todo 目前由鼠标悬停代表指向。后续可考虑键盘上下方向键选择指向。
*/
int m_pointingItemIndex = 0;
/**
* @brief 字体。
*/
LFont m_font;
};- 四个静态变量,默认菜单项宽度和高度、默认分割线区域高度很容易理解;默认(竖直方向)内边距是指菜单项与边框之间的距离,下面画出一个图就理解了(当然阅读了
repaint()
接口- 基类预留的可以覆写的接口,当我的
Menu
调用show()
函数的时候,就会进行重绘,绘出最终的结果,当然其中的逻辑需要派生类自己实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46void LMenu::repaint()
{
// 绘制背景
if (m_pDrawContext)
{
m_pDrawContext->clearFill(LColor(0xfafafa));
// 绘制菜单项和分隔线
int y = LMenu::DefaultPadding;
for (int i = 0; i < m_items.count(); i++)
{
LMenuItem *pItem = m_items[i];
// LMenuItemSeparator 是 LMenuItem 的一个派生类,这里通过 dynamic_cast 进行运行时类型转换,来判断是基类还是派生类,来实行不同的绘制逻辑
LMenuItemSeparator *pSeparator = dynamic_cast<LMenuItemSeparator *>(pItem);
if (pSeparator)
{
m_pDrawContext->setPenColor(LColor(0xdedede));
int separatorPosY = y + LMenu::DefaultSeparatorHeight / 2;
m_pDrawContext->drawLine(
0,
separatorPosY,
LMenu::DefaultWidth,
separatorPosY
);
y += LMenu::DefaultSeparatorHeight;
}
else
{
if (i == m_pointingItemIndex)
{
m_pDrawContext->setBrushColor(LColor(0xe6e6e6));
m_pDrawContext->fillRect(0, y, LMenu::DefaultWidth, LMenu::DefaultItemHeight);
}
m_pDrawContext->setPenColor(LColor(0x3c3c3c));
m_pDrawContext->drawText(
LRect(LMenu::DefaultPadding, y, LMenu::DefaultWidth - LMenu::DefaultPadding * 2, LMenu::DefaultItemHeight),
pItem->text(),
m_font,
Lark::AlignLeft | Lark::AlignVCenter
);
y += LMenu::DefaultItemHeight;
}
}
m_pDrawContext->flush();
}
}- 阅读了这个逻辑之后,我将整个绘图的范围画了一个图更清晰的展现,结合上面的代码就更容易理解了
- 基类预留的可以覆写的接口,当我的
事件处理机制
基类提供了几个处理事件的接口,包括窗口的,鼠标的,在派生类中进行重写已适应自身的功能
handleShowEvent()
- 处理窗体
show()
之后触发的事件的逻辑
1
2
3
4
5void LMenu::handleShowEvent(LShowEvent *e)
{
// 当菜单`show()`的时候触发,尝试获取鼠标独占,为当前窗口服务
grabMouse();
}- 处理窗体
handleMouseReleaseEvent()
- 处理鼠标释放触发的事件的逻辑
- 这里获取的
x()
和y()
是鼠标相对本窗体的坐标,而不是全局坐标,全局坐标另有接口globalX()
和globalY()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void LMenu::handleMouseReleaseEvent(LMouseEvent *e)
{
int x = e->x();
int y = e->y();
// 判断当前的鼠标位于哪个菜单项上,当我鼠标释放的时候,就是按下之后释放的那个释放,就会做如下的逻辑处理,分割线不管,菜单项则关闭窗体并发出信号,表示点击了某个菜单项,可以针对性的进行处理
// 这个函数要结合按下的那个事件一起看才能理解全部的过程,因为有两种极端情况,在里面按下,在外面释放;在外面按下,在里面释放,这两种情况的行为要符合感官认知才行
if (m_pointingItemIndex >= 0)
{
LMenuItem *pItem = m_items[m_pointingItemIndex];
if (dynamic_cast<LMenuItemSeparator *>(pItem))
{
return;
}
close();
pItem->clickSignal.emit(pItem->text());
}
}handleMousePressEvent()
- 处理鼠标按下触发的事件的逻辑
- 结合按下和释放的逻辑,那两种特殊情况
- 在里面按下,在外面释放,菜单不会关闭,这很正常
- 在外面按下,在里面释放;加入我现在已经有一个打开的菜单,当我立即在外面按下的时候,菜单就关闭了,根本不会有机会在里面释放,这也是符合正常逻辑的
1
2
3
4
5
6
7
8
9
10
11void LMenu::handleMousePressEvent(LMouseEvent *e)
{
int x = e->x();
int y = e->y();
// 如果在窗体外按下,则立即关闭窗体
if (x < 0 || x >= width() || y < 0 || y >= height())
{
releaseMouse();
close();
}
}handleMouseMoveEvent()
- 处理鼠标移动触发的事件的逻辑
- 上面其实一直存留着一个疑惑,就是鼠标按下的时候
m_pointingItemIndex
是如何做到实时更新的,答案就在这里,实时监测鼠标的移动,然后进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37void LMenu::handleMouseMoveEvent(LMouseEvent *e)
{
int x = e->x();
int y = e->y();
// 在外面即 -1
if (x < 0 || x >= width() || y < LMenu::DefaultPadding || y >= height() - LMenu::DefaultPadding)
{
m_pointingItemIndex = -1;
repaint();
return;
}
// TODO: 可考虑使用二分查找法压榨效率
// 在里面即计算得出下标
int i = 0;
while (i < m_itemPos.count() && y >= m_itemPos[i])
{
i++;
}
if (m_pointingItemIndex != i)
{
m_pointingItemIndex = i;
repaint();
}
// //鼠标进入菜单范围释放独占,独占时会自动触发一次进入事件,释放会自动触发移出,所以用移动事件处理
// LPoint pt(x, y);
// LRect bounds(0, 0, width(), height());
// if(bounds.contains(pt) && isGrabMouseFlagRaised())
// {
// // 释放独占会调用离开事件,给指定的释放独占添加标记防止再次独占
// m_isMoveRelease = true;
// releaseMouse();
// }
}handleMouseLeaveEvent()
- 目前实现注释掉了,我看不懂注释之前的逻辑
LMenuItem
这个东西就很简单了,就是一个菜单项,存储了一个菜单项的文本内容,一个信号,信号传递的参数是文本内容,可以被用户处理,这部分很简单,就简单略过了。
LMenuItemSeparator
LMenuItem
的派生类,代表是一个分割线,具体如何区分在上面repaint()
中具体阐述过了,不再赘述。
代码走查问题
画笔
LPen
JointType
线段交点样式,目前是只有一种方式,后续还有更多方式引入吗?- 如果有引入,那么构造函数对应也需要进行改造,构造函数见第
2
点
1
2
3
4
5
6
7
8/**
* @enum JointType
* @brief 线段交点样式定义,表示线段相交时,不同的连接方式。
*/
enum JointType
{
FlatJoint = 0 ///< 直接连接,没有拐点修饰
};- 如果有引入,那么构造函数对应也需要进行改造,构造函数见第
构造函数注释少写
@param color
m_width
不能直接赋值,因为有可能小于1
,需要调用setColor()
1
2
3
4
5/**
* @brief 构造函数,设置画笔宽度。
* @param width 画笔宽度
*/
explicit LPen(int width = 1, const LColor &color = LColor()) : m_width(width), m_color(color) {}拷贝构造函数、
width()
接口的@brief
没有句号1
2
3
4
5
6
7
8
9
10
11
12
13/**
* @brief 拷贝构造函数
* @param other 待拷贝对象
*/
LPen(const LPen &other) : m_width(other.width()), m_jointType(other.jointType()), m_color(other.color()) {}
...
/**
* @brief 返回画笔宽度
* @return 画笔宽度
*/
int width() const { return m_width; }setColor()
接口参数传常量引用更好一点1
2
3
4
5/**
* @brief 设置画笔颜色。
* @param color 画笔颜色
*/
void setColor(LColor color) { m_color = color; }m_width
注释错误1
2
3
4/**
* @brief 画笔连接样式。
*/
int m_width = 0;
画刷
LBrush
拷贝构造函数可以内联
1
2
3
4
5
6LBrush::LBrush(const LBrush &other) :
m_brushType(other.type()),
m_color(other.color())
{
m_pGradient = new LLinearGradient(*(other.gradient()));
}setBrushType
的@param
的单词与参数不对应1
2
3
4
5/**
* @brief 设置笔刷样式。
* @param brushTypes 笔刷样式
*/
void setBrushType(BrushType brushType);setGradient
的@param
与参数不对应- 单词不对应,描述也不对应
1
2
3
4
5/**
* @brief 设置渐变色属性。
* @param pGradient 渐变色类指针
*/
void setGradient(const LLinearGradient &gradient);代码中
LLinearGradient
结构存放的是指针,内存放在堆区,个人认为完全没有必要LLinearGradient
的成员变量如下,存这些东西哪里需要放在堆区?- 这里更改之后,源文件中关于指针
new
和delete
的东西都需要进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class LLinearGradient
{
...
private:
...
/**
* @brief 标识渐变起点坐标。
*/
LPoint m_start;
/**
* @brief 标识渐变终点坐标。
*/
LPoint m_finalStop;
/**
* @brief 标识渐变铺展效果,默认为PadMode。
*/
GradientMode m_mode = PadMode;
/**
* @brief 标识渐变点数组,包含渐变比例参数和颜色,渐变比例参数范围为[0,1]。
*/
LVector<LPair<double, LColor>> m_stops;
};- 同时构造函数的逻辑也需要进行修正
BrushType
根据不同的类型用到不同的成员变量,NoBrush
啥都用不到,SolidBrush
用到m_color
,LinearGradientBrush
用到m_pGradient
- 现在的构造函数逻辑有一些紊乱,用户没办法在构造函数中直接给出线性渐变相关的参数,这是不合适的,我认为修改的版本应给出三个,一个是默认的构造函数,对应
NoBrush
;第二个是针对LColor
的构造函数,对应SolidBrush
;第三个是针对LLinearGradient
的构造函数,对应LinearGradientBrush
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 原有的
// .h
/**
* @brief 构造函数。
* @param color 笔刷颜色
* @param brushType 笔刷样式
*/
explicit LBrush(const LColor &color = LColor(), BrushType brushType = LBrush::SolidBrush) : m_brushType(brushType), m_color(color) {}
// .cpp
LBrush::LBrush(const LBrush &other) :
m_brushType(other.type()),
m_color(other.color())
{
LLinearGradient *pGradient = other.gradient();
if (pGradient)
{
m_pGradient = new LLinearGradient(*pGradient);
}
}setBrushType()
接口逻辑混乱,设计不合理- 这个接口设置的初衷就是用户可以调用设置
brushType
,但是第一行判断如果是渐变,则无法设置,这就很扯淡了
1
2
3
4
5
6
7
8
9
10
11
12
13void LBrush::setBrushType(BrushType brushType)
{
if (brushType == LBrush::LinearGradientBrush)
{
throw LException("无法通过setBrushType设置渐变色,请使用setGradient");
}
m_brushType = brushType;
if (m_pGradient)
{
delete m_pGradient;
m_pGradient = nullptr;
}
}- 按理来说,
brushType
是能用户任意设置的,需要做判断的是不同brushType
的各自接口,比如setColor()
,setGradient()
,color()
,gradient()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// .h
/**
* @brief 返回笔刷颜色.
* @return 笔刷颜色
*/
LColor color() const { return m_color; }
/**
* @brief 获得该笔刷的渐变色属性。
* @return 渐变色属性
*/
LLinearGradient* gradient() const { return m_pGradient; }
/**
* @brief 设置笔刷颜色。
* @param color 笔刷颜色
*/
void setColor(LColor color) { this->m_color = color; }
// .cpp
void LBrush::setGradient(const LLinearGradient &gradient)
{
m_brushType = LBrush::LinearGradientBrush;
if (m_pGradient)
{
delete m_pGradient;
}
m_pGradient = new LLinearGradient(gradient);
}- 这个接口设置的初衷就是用户可以调用设置
LLinearGradient
头文件
<cmath>
的引入错误1
2
3
4
5enum
的格式不正确,少了@enum
,{
应该提行1
2
3
4
5
6
7/**
* @brief 铺展效果枚举。
*/
enum GradientMode {
PadMode, ///<默认铺展效果,没有被渐变覆盖的区域填充单一的起始颜色或终止颜色
RepeatMode ///<渐变在起点与终点的渐变区域外重复
};代码中有很多应该有空行,但没有空行的格式问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @class LLinearGradient
* @brief 渐变色填充类,与LBrush结合使用,以指定线形渐变笔刷。
* @details 默认初始起点颜色为黑色,终点颜色为白色。
*/
class LLinearGradient
{
public:
/**
* @brief 铺展效果枚举。
*/
enum GradientMode {
PadMode, ///<默认铺展效果,没有被渐变覆盖的区域填充单一的起始颜色或终止颜色
RepeatMode ///<渐变在起点与终点的渐变区域外重复
};
/**
* @brief 默认构造函数。
*/
LLinearGradient();
...
/**
* @brief 标识渐变起点坐标。
*/
LPoint m_start;
/**
* @brief 标识渐变终点坐标。
*/
LPoint m_finalStop;
/**
* @brief 标识渐变铺展效果,默认为PadMode。
*/
GradientMode m_mode = PadMode;
/**
* @brief 标识渐变点数组,包含渐变比例参数和颜色,渐变比例参数范围为[0,1]。
*/
LVector<LPair<double, LColor>> m_stops;
};代码中有很多内联函数,格式不正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64/**
* @brief 设置渐变起点位置。
* @param start 渐变起点坐标
*/
void setStart(const LPoint &start)
{
m_start = start;
}
...
/**
* @brief 设置渐变终点位置。
* @param finalStop 渐变终点坐标
*/
void setFinalStop(const LPoint &finalStop)
{
m_finalStop = finalStop;
}
...
/**
* @brief 设置铺展效果。
* @param mode 铺展效果
*/
void setMode(GradientMode mode)
{
m_mode = mode;
}
...
/**
* @brief 获取渐变起点。
*/
LPoint start() const
{
return LPoint(m_start);
}
/**
* @brief 获取渐变终点。
*/
LPoint finalStop() const
{
return LPoint(m_finalStop);
}
/**
* @brief 获取铺展效果。
*/
GradientMode mode() const
{
return m_mode;
}
/**
* @brief 获取渐变颜色数组。
*/
LVector<LPair<double, LColor>> stops() const
{
return m_stops;
}头文件中很多函数不能内联
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* @brief 设置渐变起点位置。
* @param x 渐变起点横坐标
* @param y 渐变起点纵坐标
*/
void setStart(int x, int y)
{
m_start.setX(x);
m_start.setY(y);
}
...
/**
* @brief 设置渐变终点位置。
* @param x 渐变终点横坐标
* @param y 渐变终点纵坐标
*/
void setFinalStop(int x, int y)
{
m_finalStop.setX(x);
m_finalStop.setY(y);
}
...
/**
* @brief 设置渐变颜色数组。
* @param stops 渐变颜色数组
*/
void setStops(const LVector<LPair<double, LColor>> &stops)
{
m_stops = stops;
if (m_stops.first().key() != 0)
{
m_stops.insert(0, LPair<double, LColor>(0, LColor(0,0,0)));
}
if (m_stops.last().key() != 1)
{
m_stops.append(LPair<double, LColor>(1, LColor(255,255,255)));
}
}源文件中有的函数可以内联
1
2
3
4
5
6
7
8LLinearGradient::LLinearGradient(int x1, int y1, int x2, int y2)
: LLinearGradient(LPoint(x1, y1), LPoint(x2, y2))
{
}
LLinearGradient::~LLinearGradient()
{
}代码中很多
LVector
的插入可以调用prepend()
或者append()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// .h
void setStops(const LVector<LPair<double, LColor>> &stops)
{
m_stops = stops;
if (m_stops.first().key() != 0)
{
m_stops.insert(0, LPair<double, LColor>(0, LColor(0,0,0)));
}
if (m_stops.last().key() != 1)
{
m_stops.append(LPair<double, LColor>(1, LColor(255,255,255)));
}
}
...
// .cpp
LLinearGradient::LLinearGradient()
{
...
m_stops.insert(0, LPair<double, LColor>(0, LColor(0,0,0)));
m_stops.insert(1, LPair<double, LColor>(1, LColor(255,255,255)));
}
LLinearGradient::LLinearGradient(const LPoint &start, const LPoint &finalStop)
{
...
m_stops.insert(0, LPair<double, LColor>(0, LColor(0,0,0)));
m_stops.insert(1, LPair<double, LColor>(1, LColor(255,255,255)));
}setColor()
接口代码冗杂,这下面的部分一行就能搞定- 接口作用:根据输入的渐变比例
pos
值插入到数组中;我们需要保证标识渐变点数组是按照渐变比例pos
递增的,因此可以采用二分的思路,这里查询的是不小于pos
的第一个元素下标 - 上面的二分思路,另一处代码中也用到了,查询的位置相同,可以封装出来进行复用
- 注释的
{
也有问题
1
2
3
4
5
6
7
8
9
10
11
12
13void LLinearGradient::setColor(double pos, const LColor &color) {
...
// 如果已经存在对应的pos值,则覆盖颜色值
if (index < m_stops.size() && m_stops[index].key() == pos)
{
m_stops[index].value() = color;
}
else
{
m_stops.insert(index, LPair<double, LColor>(pos, color));
}
}- 接口作用:根据输入的渐变比例
getColor()
接口,分了三种情况,经过数学推导,一个公式就能搞定推导过程
接口作用:根据输入点的坐标得出该点的颜色,本函数只负责将坐标转化为渐变比例
pos
(原始值),pos
的处理和计算颜色的具体逻辑交给getColorByPadMode()
和getColorByRepeatMode()
计算出
pos
之后,分铺展情况调用不同接口一行也能搞定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49LColor LLinearGradient::getColor(const LPoint &point) const
{
if (m_stops.size() < 2)
{
throw LException("尚未完整初始化颜色:至少提供两种颜色");
}
// 垂直渐变
if (m_start.x() == m_finalStop.x() && m_start.y() != m_finalStop.y())
{
// 竖向渐变
double pos = (point.y() - m_start.y()) / static_cast<double>(m_finalStop.y() - m_start.y());
if (m_mode == PadMode) // PadMode铺展
{
return getColorByPadMode(pos);
}
else // RepeatMode铺展
{
return getColorByRepeatMode(pos);
}
}
if (m_start.y() == m_finalStop.y() && m_start.x() != m_finalStop.x())
{
// 横向渐变
double pos = (point.x() - m_start.x()) / static_cast<double>(m_finalStop.x() - m_start.x());
if (m_mode == PadMode) // PadMode铺展
{
return getColorByPadMode(pos);
}
else // RepeatMode铺展
{
return getColorByRepeatMode(pos);
}
}
// 非垂直渐变
// c为起点与终点之间的距离
double c = sqrt(std::pow((m_start.x() - m_finalStop.x()),2) + std::pow((m_start.y() - m_finalStop.y()),2));
// 通过坐标旋转将非垂直渐变转变为垂直渐变
double revolve_x = ((point.x() - m_start.x()) * (m_finalStop.x() - m_start.x()) / c) -
((point.y() - m_start.y()) * (-(m_finalStop.y() - m_start.y())) / c);
double pos = revolve_x / c;
if (m_mode == PadMode) // PadMode铺展
{
return getColorByPadMode(pos);
}
else // RepeatMode铺展
{
return getColorByRepeatMode(pos);
}
}getColorByT()
接口代码冗杂,同上- 接口作用:这个函数才是根据真正输入的渐变比例
pos
值,找到其在升序数组中的位置,有人为设置则直接返回;没有则根据左右计算,具体逻辑在computeColorByT()
1
2
3
4
5
6
7
8
9
10
11
12
13LColor LLinearGradient::getColorByT(double t) const
{
...
// 如果渐变比例t刚好在m_stops中有对应值,则返回对应的颜色参数
if (m_stops[right].key() == t)
{
return m_stops[right].value();
}
// 计算新的渐变比例值
double target = (t - m_stops[left].key()) / (m_stops[right].key() - m_stops[left].key());
return computeColorByT(m_stops[left].value(), m_stops[right].value(), target);
}- 接口作用:这个函数才是根据真正输入的渐变比例
getColorByPadMode()
接口代码冗杂,同上- 接口作用:这个接口和下面那个的区别在于,一个是
PadMode
,一个是RepeatMode
,前者在超出区域使用边界值,后者在把超出区域视作另一个区域,进行重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15LColor LLinearGradient::getColorByPadMode(double t) const
{
if (t <= 1 && t >= 0)
{
return getColorByT(t);
}
else if (t < 0)
{
return LColor(m_stops.first().value());
}
else
{
return LColor(m_stops.last().value());
}
}- 接口作用:这个接口和下面那个的区别在于,一个是
getColorByRepeatMode()
接口代码冗杂,同上1
2
3
4
5
6
7
8
9
10
11
12
13LColor LLinearGradient::getColorByRepeatMode(double t) const
{
double pos;
if (t < 0)
{
pos = 1 - fabs(fmod(t, 1));
}
else
{
pos = fabs(fmod(t, 1));
}
return getColorByT(pos);
}computeColorByT()
接口代码冗杂,同上- 接口作用:这个接口是在确定
pos
落在哪两个设置的颜色之间,根据新计算出的pos
值得出最终的颜色
1
2
3
4
5
6
7LColor LLinearGradient::computeColorByT(const LColor &startColor, const LColor &finalStopColor, double t)
{
int r = (1 - t) * startColor.red() + t * finalStopColor.red();
int g = (1 - t) * startColor.green() + t * finalStopColor.green();
int b = (1 - t) * startColor.blue() + t * finalStopColor.blue();
return LColor(r, g, b);
}- 接口作用:这个接口是在确定
菜单
LMenu
部分接口可以内联
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// .cpp
LMenu::LMenu() : LPopupWindow(LMenu::DefaultWidth, LTopLevelWindow::MinHeight),
m_pointingItemIndex(-1)
{
}
LMenu::~LMenu()
{
clear();
}
...
bool LMenu::isEmpty() const
{
return m_items.count() == 0;
}
...
void LMenu::handleShowEvent(LShowEvent *e)
{
grabMouse();
}静态成员变量命名不规范
3.18
更新:Default
开头相关的表强调,使用的时候类似宏的感受,规定首字母大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* @brief 默认菜单项高度。
*/
static const int DefaultItemHeight;
/**
* @brief 默认分隔线区域高度。
*/
static const int DefaultSeparatorHeight;
/**
* @brief 默认菜单宽度。
*/
static const int DefaultWidth;
/**
* @brief 默认(竖直方向)内边距。
*/
static const int DefaultPadding;代码中的
TODO
,handleMouseMoveEvent()
m_itemPos
是升序的,可以二分,查找到对应的区间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void LMenu::handleMouseMoveEvent(LMouseEvent *e)
{
...
// TODO: 可考虑使用二分查找法压榨效率
int i = 0;
while (i < m_itemPos.count() && y >= m_itemPos[i])
{
i++;
}
if (m_pointingItemIndex != i)
{
m_pointingItemIndex = i;
repaint();
}
...
}repaint()
接口中,有一个地方没有看懂3.14
更新,理解了为什么要进行重新填充?对比了有这部分代码和没有这部分代码的
demo
效果,发现这是一个鼠标悬停效果,如下这样之后,
handleMouseMoveEvent()
就能解释了,实时判断鼠标位置来更新悬停效果引申思考:菜单的重绘制有两种情况,第一是
show()
,第二是handleMouseMoveEvent()
更新悬停效果,但是第二种情况还重绘整体效率会不会受很大的影响,个人认为可以将这两种情况区分开,第二种的时候就只管鼠标悬停,这样可能会好一点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32void LMenu::repaint()
{
// 绘制背景
if (m_pDrawContext)
{
m_pDrawContext->clearFill(LColor(0xfafafa));
// 绘制菜单项和分隔线
int y = LMenu::DefaultPadding;
for (int i = 0; i < m_items.count(); i++)
{
LMenuItem *pItem = m_items[i];
LMenuItemSeparator *pSeparator = dynamic_cast<LMenuItemSeparator *>(pItem);
if (pSeparator)
{
...
}
else
{
// 这个地方为什么当绘制到鼠标所在的菜单栏的时候需要把这一个区域重新填充?
if (i == m_pointingItemIndex)
{
m_pDrawContext->setBrushColor(LColor(0xe6e6e6));
m_pDrawContext->fillRect(0, y, LMenu::DefaultWidth, LMenu::DefaultItemHeight);
}
...
}
}
m_pDrawContext->flush();
}
}结合
repaint()
的逻辑,DefaultPadding
的注释描述不对了,应该是四边的内边距1
2
3
4/**
* @brief 默认(竖直方向)内边距。
*/
static const int DefaultPadding;handleMouseMoveEvent()
接口中,为什么每次都需要进行重绘?3.14
更新,见上面repaint()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35void LMenu::handleMouseMoveEvent(LMouseEvent *e)
{
int x = e->x();
int y = e->y();
if (x < 0 || x >= width() || y < LMenu::DefaultPadding || y >= height() - LMenu::DefaultPadding)
{
m_pointingItemIndex = -1;
repaint();
return;
}
// TODO: 可考虑使用二分查找法压榨效率
int i = 0;
while (i < m_itemPos.count() && y >= m_itemPos[i])
{
i++;
}
if (m_pointingItemIndex != i)
{
m_pointingItemIndex = i;
repaint();
}
// //鼠标进入菜单范围释放独占,独占时会自动触发一次进入事件,释放会自动触发移出,所以用移动事件处理
// LPoint pt(x, y);
// LRect bounds(0, 0, width(), height());
// if(bounds.contains(pt) && isGrabMouseFlagRaised())
// {
// // 释放独占会调用离开事件,给指定的释放独占添加标记防止再次独占
// m_isMoveRelease = true;
// releaseMouse();
// }
}
LMenuItem
类当中并没有用到
LMenu
相关内容,但是.h
中做了前置声明,.cpp
中做了头文件引入1
2
3
4
5
6
7
8
9
10
11
12
13
14// .h
class LMenu;
// .cpp构造函数可以内联
1
2
3
4
5// .cpp
LMenuItem::LMenuItem(const LString &text) : LObject(),
m_text(text)
{
}
LMenuItemSeparator
构造函数可以内联
1
2
3LMenuItemSeparator::LMenuItemSeparator() : LMenuItem(LString())
{
}
3.18 - 3.29
迭代任务
- 审核
LarkTestKit
部分代码,与陈冠杰同学对接,并且编写测试样例
总结
- 已经完成工程的搭建,包括项目本地运行、
conan
打包和gitlab CI
流程 - 和测试进行沟通,测试已经协调出两人进行测试,
bug
已上禅道 - 和陈冠杰进行沟通,确定
bug
细节和尚未完成的部分情况 - 问题陈述
- 未作
.h
和.cpp
声明和定义的文件分开,目前都放在.h
当中 - 功能实现不全,包括但不限于
- 值检查目前只支持整形
- 谓词断言目前只支持
1~5
个参数 - 目前仅有
TEST
宏而没有TEST_F
、TEST_P
等宏 - 键盘操作模拟为实现
KEY_RELEASE
相关宏 - 测试元语言目前暂未实现
- 个人感觉还缺少了很多宏,例如参照
GTEST
中的EXPECT_THROW
、EXPECT_NOTHROW
等(需求统一)
- 功能实现的很多细节存在问题,包括但不限于
- 测试的结果输出不合理,打印的
OK
过多,排版不合理等 - 执行失败的语句,未输出相关信息
- 测试的结果输出不合理,打印的
- 上面的问题以及相关
bug
,具体可见禅道
- 未作
4.7 - 4.19
迭代任务
LarkSDK-XML
代码走查(抽象,太抽象了)FileSystem
代码走查(包含LDir
和LFileInfo
,暂不包括LFile
)- 该部分的主要工作是重新明确语义,指定重构设计方案,故该部分的内容归纳在一个课题中
学习的点
XmlStream
xml
中的CDATA
,参考链接:https://www.w3school.com.cn/xml/xml_cdata.asp在
xml
文本中,所有文本均会被解析器解析,但CDATA
区段的文本会被忽略CDATA
部分由<![CDATA[
开始,由]]>
结束某些文本里面,需要放非法字符例如
<
和&
,但是又不能使用转移字符,例如传递代码片段,就可以使用CDATA
区段1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<script>
</script>补充:
xml
中预定义的实体引用
xml Token
,参考链接:https://xmlbeans.apache.org/docs/2.0.0/guide/conUnderstandingXMLTokens.html对于一个
xml
文件,可以根据起始标签、内容、结束标签、注释、文档开始、文档结束等,将整个文档进行划分,每个小块就是一个token
,当游标cursor
移动的过程中,每个位置就会对应一个tokenType
xml DTD
,参考链接:https://blog.csdn.net/gavin_john/article/details/51532756xml
的标签是用户自定义的,例如我们可以在标签内嵌套标签,例如<sport><ball>...</ball></sport>
,在这种情况下sport
中嵌套的标签肯定不可能是随意的,显然不能放math
,因此需要一个东西来进行约束,这个东西就是DTD
文件DTD
语法1
2
3
4
5
6
7
8<!DOCTYPE note
[
<!ELEMENT 班级 (学生+)>
<!ELEMENT 学生 (名字,年龄,介绍)>
<!ELEMENT 名字 (#PCDATA)>
<!ELEMENT 年龄 (#PCDATA)>
<!ELEMENT 介绍 (#PCDATA)>
]>在
xml
中引入DTD
文件,约束此xml
- 引入中写的
SYSTEM
,表示当前的DTD
文件是本地 - 如果写的是
PUBLIC
,则表示引入的DTD
文件是来自于网络
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--引入dtd文件,约束这个xml-->
<班级>
<学生>
<名字>周小星</名字>
<年龄>23</年龄>
<介绍>学习刻苦</介绍>
</学生>
<学生>
<名字>林晓</名字>
<年龄>25</年龄>
<介绍>是一个好学生</介绍>
</学生>
</班级>- 引入中写的
xml
中的PCDATA
PCDATA
,即Parsed Character Data
,就是xml
元素内部解析的文本数据,即作为元素的内容而存在的
关于
std::string
和std::wstring
的联系std::string
的底层是char
,std::wstring
的底层是wchar_t
,前者不必多言,后者是代表宽字节,根据平台及编译器的是实现不同可以是2
字节,也可以是4
字节,Linux
下的gcc
是4
字节- 显然,更多的字节代表可以表示更多的字符集;例如
std::wstring
主要用于utf-16
的编码字符,std::string
主要用于存储单字节的字符(ASCII
),也可以用来保存utf-8
的字符(注意,这里说的都是主要,当然可以表示其他编码的字符,至于情况如何或者会不会乱码我就不知道了) - 关于
unicode
unicode
统一了所有字符的编码,是一个Character Set
,也就是字符集- 但是
unicode
字符集只是给所有的字符一个唯一编号,却没有具体规定如何用二进制存储 - 因此有了不同的存储方式的设计,不同的编码方式就诞生了,最常见的就是
utf-32
,utf-16
,utf-8
- 具体如何进行编码的,参考链接:https://blog.csdn.net/hollis_chuang/article/details/110729762
在
c++
类中定义成员变量是一个引用,参考链接:https://blog.csdn.net/weixin_42579072/article/details/102618771以前听都没听过,今天搜了一下,发现这样在某些情况下,居然还真能通过编译,但是肯定不推荐这么用
c++
类内可以定义引用成员变量,但要遵循以下三个规则:- 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误
- 构造函数的形参也必须是引用类型
- 不能在构造函数里初始化,必须在初始化列表中进行初始化
构造函数分为初始化和计算两个阶段,前者对应成员初始化链表,后者对应构造函数函数体。引用必须在初始化阶段,也即在成员初始化链表中完成,否则编译时会报错(引用未初始化)
测试程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A
{
public:
A(int &target) : a(target)
{
std::cout << "构造函数" << std::endl;
}
void printA()
{
std::cout << "a is: " << a << std::endl;
}
private:
int &a;
};
int main()
{
int a = 20;
A r(a); // 依旧使用自定义的构造函数
r.printA();
int &b = a;
A r1(b);
r1.printA();
return 0;
}执行结果
FileSystem
linux
目录和windows
目录目录举例
linux
目录,例如/usr/bin/../local/bin
,./abc/../abc
windows
目录,例如c:\a\b\..\c
,.\a\b\..\c
相同点
- 都分为绝对路径和相对路径
.
表示本目录,..
表示上级目录
不同点
windows
的目录名和文件名可以有空格,linux
不行windows
下使用的是反斜杠\
,linux
使用的是正斜杠/
windows
由于具有盘符的概念,在根目录\
前面有一个盘符的标志,例如c:
linux
没有这个概念,最底层的根目录就是/
注意,
windows
下在c/c++
代码中经常会见到在字符串中使用路径,当使用反斜杠的时候,由于反斜杠本身具有特殊含义,是作为转义字符的起始标志,因此需要在前面再加上一个反斜杠代表后面的反斜杠是其原本的意思;而/
就不会有这个烦恼1
std::string path = "c:\\abc\\..\def";
代码走查问题
XmlStream
- 整体代码冗杂,三个类的分工不同,建议放在三个文件中,而不是像现在冗杂放在一个文件中
- 整体的设计全部都是硬解析,一口气读取所有的数据,然后从头开始以字节为单位进行硬解析;按照目前的规划应当使用开源
expat
库,使用其提供的接口封装成为我们自己的解析器Parser
,进而衍生出Reader
和Writer
Parser
由于接口太多了,加上这个代码不用想,肯定是要重构的,因此问题的个数不用那么精确了,由于我怕忘了它的逻辑,所以就把逻辑也放在这里面了(这些逻辑一般都有问题。。。)
代码中的英文字符串前后未与中文字符想间隔一个空格,全篇都是,代码就不放了
- 同时,代码存在各种规范问题,例如换行不规范、注释不全、注释不规范,函数可以内联等等,统一列在这里,后续不赘述
代码中的
std::vector
,std::list
,std::map
均都替换成为LarkSDK
的对应类.cpp
文件中函数定义的开头总是有一行英文注释,在代码中也是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// .cpp
...
// low-level - match character and advance cursor.
bool Parser::parseMatch(const LString chPattern)
{
bool bOK = peekMatch(chPattern);
if (bOK)
m_iCursor += chPattern.length();
return bOK;
}
// get next character - but do not advance cursor.
LChar Parser::peek()
{
if (!eof())
return m_buffer[m_iCursor];
return LChar("");
}
...采用结构体
struct
类型,但是确包含private
成员,虽然合法,但是很抽象1
2
3
4
5
6
7
8
9
10
11
12
13
14struct Parser
{
...
private:
// 补充预读缓冲区
void more();
// 文档读到末尾标识符
bool m_bEOF;
// 预读缓冲器
std::vector<LChar> m_buffer;
// 当前缓冲区中的字符数
size_t m_iBuffer;
};m_buffer
读取的缓冲器建议使用LByteArray
类型1
2// 预读缓冲器
std::vector<LChar> m_buffer;构造函数可以内联
m_pStream
是LString
类型,但是却用nullptr
赋值
1
2
3
4
5// .cpp
Parser::Parser() :
m_bEOF(false), m_pStream(nullptr), m_iCursor(0), m_iBuffer(0)
{
}close()
函数做的是清理的工作,命名不规范1
2
3
4
5
6
7
8// close parsing
void Parser::close()
{
m_pStream = LString();
m_bEOF = true;
m_iCursor = 0;
m_iBuffer = 0;
}peekMatch()
函数参数混乱,有const LString
,有const LString*
,应该统一用const LString&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* @brief 匹配文本但不前进光标。
* @param strPattern 需要匹配的文本
* @param iLen 模式长度引用
* @return true则匹配成功;否则失败
*/
bool peekMatch(const LString *strPattern, size_t &iLen);
/**
* @brief 匹配文本但不前进光标。
* @param strPattern 需要匹配的文本
* @return true则匹配成功;否则失败
*/
bool peekMatch(const LString *strPattern);
/**
* @brief 匹配字符但不推进光标。
* @param chPattern 需要匹配的字符
* @return true则匹配成功;否则失败
*/
bool peekMatch(const LString chPattern);peekMatch
的逻辑混乱,两个函数的逻辑实现不一样指针版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// low-level - match text but do not advance cursor.
bool Parser::peekMatch(const LString *strPattern, size_t &iLen)
{
size_t i = 0;
LString patternCopy = *strPattern; // 创建一个新的LString对象拷贝strPattern
while (true)
{
// 如果 i 超出 patternCopy 的大小,并且下面没有返回 false,则说明匹配成功
if (i >= patternCopy.length())
{
iLen = i;
return true;
}
// 如果 i 超出当前缓冲区的大小,则调用 more() 函数从 m_stream 中拿去更多的字节
if ((m_iCursor + i) >= m_iBuffer)
more();
// 如果扩充完,i 还超出,就返回 false
if ((m_iCursor + i) >= m_iBuffer)
return false;
// 比较传入的字符串和缓冲区中的内容,如果有一个不满足比较条件则返回 false
// 一个字节一个字节比。。。
if (patternCopy.at(i) != LChar(m_buffer[m_iCursor + i]))
return false;
i++;
}
}- 为什么需要第二个参数
iLen
,并且需要传入引用呢?答案是为parseMatch()
函数服务
1
2
3
4
5
6
7
8
9
10
11// low-level - match text and advance cursor.
bool Parser::parseMatch(const LString *strPattern, size_t &iLen)
{
bool bOK = peekMatch(strPattern, iLen);
if (bOK)
m_iCursor += iLen; // 这里需要 iLen 移动游标的位置
return bOK;
}
// 剩下两个同理
...- 为什么需要第二个参数
传值版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16bool Parser::peekMatch(const LString chPattern)
{
bool match = false;
int n = chPattern.length();
LString now;
for (int i = 0; i < n; i++)
{
// 把缓存中的比较字符串拷贝到 now 中,还是一个一个拷贝,然后进行比较
// 和前面对比而言,这样写 i 如果超出 m_buffer 边界就会出问题了
now.append(m_buffer[m_iCursor + i]);
}
match = now == chPattern;
return !eof() && m_iCursor < m_iBuffer && match;
}
consume()
函数命名不规范,并且逻辑有误第一,名字为啥叫
consume
,这和前进有啥关系嘛?返回了
bool
值,但是在整个文件中,并没有找到一处使用了这个返回值,这倒没什么;其次,m_iCursor
超出边界以后,返回false
,为什么不把m_iCursor
重置到原来的位置呢?
1
2
3
4
5
6
7
8
9// consume count characters.
bool Parser::consume(size_t iCount)
{
size_t iRemains = m_iBuffer - m_iCursor;
if (iRemains < iCount) // 如果剩下的长度小于想前进的长度,则补充缓冲区内容
more();
m_iCursor += iCount;
return m_iCursor <= m_iBuffer;
}接口逻辑:
readText()
和readCDATAText()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// consume characters up to the delimiter.
bool Parser::readText(LString &strText, char chDelimiter)
{
// 查看是否到达末尾
while (!eof())
{
// 一个一个字符的判断,考虑了跳过 CDATA 段的逻辑
readCDATAText(strText);
LChar ch = peek();
if (ch == LChar(chDelimiter))
{
return true;
}
// 加入 strText 。。。
strText.append(LChar(ch));
consume(1);
}
return strText.length() > 0;
}
bool Parser::readCDATAText(LString &strText)
{
// 匹配 CDATA 段起始
if (peekMatch(LString("<![CDATA[")))
{
consume(9); // Consume "<![CDATA["
// 匹配 CDATA 段结束
while (!peekMatch(LString("]]>")))
{
strText.append(peek());
consume(1);
}
// End of CDATA block
consume(3); // Consume "]]>"
return true;
}
return false;
}接口逻辑:
skipSpace()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// skips / consumes whitespace.
int Parser::skipSpace()
{
int res = 0;
// 循环匹配这些字符,然后跳过
while (peekMatch(LString("\t")) || peekMatch(LString("\r")) || peekMatch(LString("\n")))
{
res++;
consume(1);
}
// (原注释)有点奇怪 一起放上面会有问题 会有时match上不该match的东西
// peekMatch 写的,一言难尽,所以才出现了这种问题
while (peekMatch(LString(" ")))
{
res++;
consume(1);
}
return res;
}接口逻辑:
more()
这个类里面的冗杂代码和边界条件的代码太多了,不放完整代码了
第一步:将
buffer
中cursor
以后的部分移动到buffer
头部
1
2
3
4
5
6
7
8
9
10
11
12
13// unprocessed data in buffer.
size_t iCarry = m_iBuffer - m_iCursor;
// left over data?
if (iCarry > 0)
{
// yes: carry forward unprocessed data.
LChar *buf = &m_buffer[0];
memcpy(buf, buf + m_iCursor, iCarry);
}
// reset cursor to start of buffer.
// 不需重置?
// 对啊,这里注释也写了,为什么不需重置?
// m_iCursor = 0;示意图:
第二步:从
m_pStream
中读取内容
1
2
3
4
5
6
7// 全都进来,what????结合Reader,是一口气把文件的所有内容读进来,如果太大了怎么办
// 其次,按照这个逻辑,红色部分的内容还没有被读取,直接就从 0 开始赋值?
for (int i = 0; i < m_pStream.length(); i++)
{
ch = m_pStream.at(i);
m_buffer[i] = ch;
}
LXmlReader
代码存在各种规范问题,例如换行不规范、注释不全、注释不规范,函数可以内联等等,统一列在这里,后续不赘述
代码中的
std::pair
,std::vector
,std::list
,std::map
均都替换成为LarkSDK
的对应类LEntry
元素实体结构,完全没有注释,根本不知道里面的成员变量是什么意思- 我只能猜测了
m_element
:元素内容m_children
:是否具有子元素m_skipped
:结合成员变量m_iSkipped
是整个文档跳过的子元素个数,推断为在该元素实体内跳过的子元素个数m_iComment
:元素内部注释内容m_commentIndex
:根据getComment()
函数可以猜测是当前注释的字符偏移量,也就是游标curSor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* @struct LEntry
* @brief 元素实体,维护元素内容数据。
*/
struct LEntry
{
LString m_element;
bool m_children;
size_t m_skipped;
LString m_iComment;
LString m_commentIndex;
LEntry() : m_children(false), m_skipped(0){};
LEntry(const LEntry ©)
: m_children(copy.m_children), m_element(copy.m_element), m_skipped(copy.m_skipped){};
};- 我只能猜测了
成员变量文档版本信息
m_nowVersion
在源文件中没有使用,可以删除1
2
3// xml文档版本信息
LString m_nowVersion;
LString m_Version;m_attributes
和m_attributes2
一个使用STL
的容器,一个使用SDK
的容器,应统一1
2
3// 最近的 XML 元素的属性列表。
std::list<std::pair<LString, LString>> m_attributes;
LList<LPair<LString, LString>> m_attributes2;所有的构造函数通过初始化序列初始化成员变量,但是在函数体中又重复做了一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 默认构造
LXmlReader::LXmlReader() : m_bStart(false), m_iSkipped(0)
{
// 这里又重复赋值一次
m_bStart = false;
m_iSkipped = 0;
// LEntry enty;
// m_stack.push_back(enty);
bool open = m_parser.open("");
if (!open)
{
std::cout << "格式不正确,构造失败" << std::endl;
}
}
// 其他的同理
...构造函数的使用参数
const LString&
,const LByteArray&
,const char*
时,可以代码复用,但是目前的实现冗杂1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39LXmlReader::LXmlReader(const LString &inputStream) : m_bStart(false), m_iSkipped(0)
{
m_bStart = false;
m_iSkipped = 0;
// LEntry enty;
// m_stack.push_back(enty);
bool open = m_parser.open(inputStream);
if (!open)
{
std::cout << "格式不正确,构造失败" << std::endl;
}
}
LXmlReader::LXmlReader(const LByteArray &inputStream) : m_bStart(false), m_iSkipped(0)
{
m_bStart = false;
m_iSkipped = 0;
// LEntry enty;
// m_stack.push_back(enty);
bool open = m_parser.open(inputStream.toString());
if (!open)
{
std::cout << "格式不正确,构造失败" << std::endl;
}
}
LXmlReader::LXmlReader(const char *inputStream) : m_bStart(false), m_iSkipped(0)
{
m_bStart = false;
m_iSkipped = 0;
// LEntry enty;
// m_stack.push_back(enty);
LString now(inputStream);
bool open = m_parser.open(now);
if (!open)
{
std::cout << "格式不正确,构造失败" << std::endl;
}
}LXmlReader
通过传入LFile&
参数构造读取器的时候,将整个文件的所有内容一次性读进来,放入m_parser
中- 按照
expat
库的思路,应该是通过流式进行处理,例如读取一行处理一行,这样能够避免在内存中开辟过大的内存,这才是stream
,而不是tree
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17LXmlReader::LXmlReader(LFile &inputStream) : m_bStart(false), m_iSkipped(0)
{
...
LString file;
while (!inputStream.end())
{
file.append(inputStream.readLine().toString());
}
bool open = m_parser.open(file);
if (!open)
{
std::cout << "构造失败" << std::endl;
}
...
}- 按照
addData()
接口同样可以复用,同构造函数- 并且函数可以内联
1
2
3
4
5
6
7
8
9
10
11
12
13
14void LXmlReader::addData(const LString &data)
{
m_parser.m_pStream.append(data);
}
void LXmlReader::addData(const LByteArray &data)
{
m_parser.m_pStream.append(data.toString());
}
void LXmlReader::addData(const char *data)
{
m_parser.m_pStream.append(data);
}接口逻辑:
parseToken()
- 局部变量
upIndex
没有什么作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// parse a token
bool LXmlReader::parseToken(LString &strToken)
{
LChar ch = m_parser.peek();
LString nowString;
int upIndex = 0;
//?? 是否需要ch>=0
// 一个字符一个字符判断,目的是卡好一个 token 的范围(至于功能上能不能卡好我也无从验证)
while (ch != LChar('=') && ch != LChar('/') && ch != LChar('>') && ch != LChar('"') && ch != LChar(' ') && ch != LChar('\''))
{
nowString.append(ch);
m_parser.consume(1);
upIndex++;
ch = m_parser.peek();
}
strToken = nowString;
return strToken.length() > 0;
}为什么传递参数
strToken
,还是引用?最后返回的还是一个bool
值;单从本函数的逻辑上来看,完全可以将nowString
返回,如果为空就代表失败- 查看了
parseToken()
调用的地方,就明白了,在源文件中有两处 - 无非就想把解析出的结果记录在
element
或者pair
中,那直接用返回值记录啊
- 查看了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// returns true start of an element is successfully consumed.
// if true, the element's attributes are available below through getAttribute.
bool LXmlReader::readStartElement()
{
LEntry element;
if (isStartElement() && parseToken(element.m_element))
{
...
}
...
}
// parse attribute=quoted-value sequence.
bool LXmlReader::parseAttribute()
{
std::pair<LString, LString> pair;
bool bOK = skipspace(true) && parseToken(pair.first);
...
}- 局部变量
接口逻辑:
skipspace()
- 接口命名不规范,应使用小驼峰
skipSpace
- 参数
bInside
:判断是在元素声明内还是外部- 在内部,跳过空格、注释等非正文内容,直到遇到正文内容或其他标记为止
- 在外部,除了上面的内容,还会处理一些文档声明,例如
XML
版本,编码信息,DTD
声明等
- 这里面的所有匹配都是硬匹配的,这个函数就是一个非常经典的例子,单从匹配的角度来讲,可以考虑正则表达式而不是硬匹配
- 在内部处理注释的时候,应匹配注释开始
<!--
和注释结束-->
而不是--
TODO
:bInside
为false
的时候处理注释的逻辑目前看不懂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88// skips / consumes whitespace.
// bInside - true if inside an element declaration eg. between '<' and '>'.
bool LXmlReader::skipspace(bool bInside)
{
if (bInside)
{
while (true)
{
// 1. 匹配空格
m_parser.skipSpace();
// 2. 匹配注释
if (m_parser.peekMatch("--"))
{
m_parser.consume(2);
// 在注释内部,跳过注释内容
while (!m_parser.eof() && !m_parser.peekMatch("--"))
m_parser.consume(1);
// 匹配到注释结尾
if (m_parser.peekMatch("--"))
m_parser.consume(2);
}
else
break;
}
}
else
{
while (true)
{
// 1. 匹配空格
m_parser.skipSpace();
// 2. 匹配注释
// 基本逻辑和上面是一样的,但是这里多了很多成员变量的修改
// 下面关于 m_Comment 和 m_stack 目前看不懂
if (m_parser.peekMatch("<!--"))
{
bool flag1 = (m_stack.size() != 0);
m_parser.consume(4);
if (flag1)
m_stack.back().m_iComment = LString("");
m_Comment = LString("");
while (!m_parser.eof() && !m_parser.peekMatch("-->"))
{
if (flag1)
m_stack.back().m_iComment.append(m_parser.peek());
m_Comment.append(m_parser.peek());
m_parser.consume(1);
}
if (m_parser.peekMatch("-->"))
m_parser.consume(3);
// m_stack.back().m_iComment.append(LString(" "));
m_parser.skipSpace();
if (flag1)
m_stack.back().m_commentIndex = LString::fromInt(characterOffset());
}
// 3. 匹配版本号和编码信息
// <?xml version="1.0" encoding="utf-8"?> 匹配固定模式,逻辑固定
else if (m_parser.peekMatch("<?"))
{
m_parser.consume(2);
while (!m_parser.eof() && !m_parser.peekMatch("?>"))
{
// 获取版本信息
if (m_parser.peekMatch("version"))
{
...
}
// 获取编码信息
else if (m_parser.peekMatch("encoding"))
{
...
}
}
if (m_parser.peekMatch("?>"))
m_parser.consume(2);
}
// 3. 匹配 DTD 声明
// <!DOCTYPE 班级 SYSTEM "myClass.dtd"> 匹配固定模式,逻辑固定
else if (m_parser.peekMatch("<!DOCTYPE"))
{
...
}
else
break;
}
}
return true;
}- 接口命名不规范,应使用小驼峰
接口逻辑:
parseAttribute()
- 关于
m_attributes
和m_attributes2
,上面提到过,一个用的是STL
,一个用的是SDK
,但是在这里两个东西存储的数据是一模一样的,因此有理由怀疑这两个东西其实是一个东西,可以删除一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// parse attribute=quoted-value sequence.
bool LXmlReader::parseAttribute()
{
std::pair<LString, LString> pair;
// 跳过空格并且解析 key
bool bOK = skipspace(true) && parseToken(pair.first);
if (bOK)
{
// 解析 = 号
bool FLag1 = skipspace(true) && m_parser.parseMatch(LString("=")) && skipspace(true);
// 由于属性可能被单引号或者双引号扩充起来,因此两个都进行判断
// 由于 parseMatch 只有成功才会修改游标 curSor,因此这里的逻辑是没问题的
bool Flag2 = m_parser.parseMatch(LString("\'")) && m_parser.readText(pair.second, '\'') && m_parser.parseMatch(LString("\'"));
bool Flag3 = m_parser.parseMatch(LString("\"")) && m_parser.readText(pair.second, '"') && m_parser.parseMatch(LString("\""));
bOK = FLag1 && (Flag2 || Flag3);
if (bOK)
{
m_attributes.push_back(pair);
LPair<LString, LString> pair2;
pair2.key() = pair.first;
pair2.value() = pair.second;
m_attributes2.append(pair2);
}
}
return bOK;
}- 关于
接口逻辑:
readStartElement()
- 两个重载版本,第一个的参数应该用
const LString&
- 当前元素是否具有子元素
m_children
,代码中是直接判断是能否解析到>
,从这个函数来看,这个逻辑是不合理的,应该判断这个起始标签的下一个位置是不是子元素的起始标签,这样才是合理的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52// returns true if start of named element is successfully consumed.
// if true, the element's attributes are available below through getAttribute.
bool LXmlReader::readStartElement(const LString *strElement)
{
LEntry element;
size_t iLen = 0;
// 如果传入字符串为空,则失败
if (strElement == nullptr || strElement->isEmpty())
return false;
// 判断当前是否为起始标签,并且是否能够成功解析
if (isStartElement() && m_parser.parseMatch(strElement, iLen))
{
// element.Element.assign(strElement, iLen);
//??
LString a = *strElement;
// 修改元素实体的内容
element.m_element = a.substr(0, iLen);
// 修改属性
m_attributes.clear();
m_attributes2.clear();
while (parseAttribute())
;
// 记录当前元素是否具有子元素,这个逻辑不对吧。。
element.m_children = m_parser.parseMatch(LString(">"));
// 加入嵌套的栈
m_stack.push_back(element);
m_bStart = false;
return true;
}
return false;
}
// returns true start of an element is successfully consumed.
// if true, the element's attributes are available below through getAttribute.
bool LXmlReader::readStartElement()
{
// 思路和前面基本一致
LEntry element;
// 只是这里改为直接解析 token 去了
if (isStartElement() && parseToken(element.m_element))
{
m_attributes.clear();
m_attributes2.clear();
while (parseAttribute())
;
element.m_children = m_parser.parseMatch(LString(">"));
m_stack.push_back(element);
m_bStart = false;
return true;
}
return false;
}- 两个重载版本,第一个的参数应该用
接口逻辑:
isStartElement()
TODO
:第一个函数的逻辑目前看不懂。。。- 第二个重载版本的参数同理应当使用
const LString&
1
2
3
4
5
6
7
8
9
10
11
12
13
14// returns true if start of an element.
bool LXmlReader::isStartElement()
{
// 看不懂。。。
...
}
// returns true if start of the named element.
bool LXmlReader::isStartElement(const LString *strElement)
{
if (strElement == nullptr)
return false;
return isStartElement() && m_parser.peekMatch(strElement);
}接口逻辑:
readUntilElement()
- 参数同理应当使用
const LString&
- 一来就来了一个
new
,我了个豆。。。 AllStream
,把目前缓冲区里面的数据全拿出来???
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43bool LXmlReader::readUntilElement(const LString strElement)
{
LString *s = new LString(strElement);
// 枚举了三种不同的标签
// 第三种代表了一个自闭合的标签,表示该元素存在但是没有内容
// <element ...>,<element>,<element/>
LString now("<");
now.append(strElement);
now.append(" ");
LString now1("<");
now1.append(strElement);
now1.append(">");
LString now2("<");
now2.append(strElement);
now2.append("/>");
// 判断是否读取到末尾
while (!atEnd())
{
LString AllStream = m_parser.getBuffer();
// 查找是否含有上面三种模式的标签
if (AllStream.contains(now) || AllStream.contains(now1) || AllStream.contains(now2))
{
// 如果当前游标位置的标签是目标标签,读取并返回 true
if (isStartElement(s))
{
readStartElement();
return true;
}
// 又开始顺着刚才的位置读,读取(跳过)自由节点的内容
readStartElement();
readPCData();
if (readEndElement(false))
{
// 如果读到了返回 true,否则循环再来。。。
if (readStartElement(s))
return true;
}
}
else
return false;
}
return false;
}- 参数同理应当使用
接口逻辑:
isEndElement()
- 一个元素就算没有子元素,也可以自身有内容啊,不一定就是自闭合标签,例如
<book>math</book>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// returns true if current element is self-closing OR no more nested content remains
// eg. cursor is positioned at the closing tag.
bool LXmlReader::isEndElement()
{
if (m_stack.size() > 0)
{
// 查看该元素是否具有子元素
bool bChildren = m_stack.back().m_children;
skipspace(!bChildren);
if (bChildren)
{
// 有子元素则取匹配子元素
LString strTail;
strTail = "</";
strTail.append(m_stack.back().m_element);
strTail.append(">");
return m_parser.peekMatch(strTail);
}
// 没有则匹配自闭合???这里逻辑感觉不对
return m_parser.peekMatch("/>");
}
return false;
}- 一个元素就算没有子元素,也可以自身有内容啊,不一定就是自闭合标签,例如
接口逻辑:
readEndElement()
- 三个重载版本,第二个重载版本参数应使用
const LString&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74bool LXmlReader::readEndElement(bool bSkip, const LString *element)
{
if (element == nullptr)
return false;
return m_stack.size() &&
m_stack.back().m_element == (*element) &&
readEndElement(bSkip);
}
// conclude self-closing element OR consume closing element.
bool LXmlReader::readEndElement(bool bSkip)
{
bool bOK = false;
int startSkip = m_iSkipped;
if (m_stack.size() > 0)
{
// 判断是否存在子元素
bool bChildren = m_stack.back().m_children;
skipspace(!bChildren);
if (bChildren)
{
LString strTail;
strTail = "</";
strTail.append(m_stack.back().m_element);
strTail.append(">");
// 不存在结尾元素,则返回 false
LString buffer = m_parser.getBuffer();
if (!buffer.contains(strTail))
return false;
// 否则就开始循环查找末尾元素标签
// 循环里面的逻辑没看懂。。。
do
{
bOK = m_parser.parseMatch(strTail);
if (!bOK && bSkip)
{
// what the hell is doing here???
readPCData();
if (readStartElement())
{
m_stack.back().m_skipped++;
m_iSkipped++;
getElementName();
readPCData();
readEndElement();
readPCData();
}
// else break;
}
else
break;
} while (!bOK && !m_parser.eof());
}
// 没有子元素就匹配自闭合???又匹配自闭合???
else
bOK = m_parser.parseMatch("/>");
if (bOK)
{
nowSkip = m_iSkipped - startSkip;
m_stack.pop_back();
}
}
if (m_parser.eof())
{
std::cout << "已读到末尾。" << std::endl;
}
return bOK;
}
void LXmlReader::readEndElement()
{
// 和第二个重载代码结构类似
...
}- 三个重载版本,第二个重载版本参数应使用
接口
readEndEleName()
,和readEndElement()
功能类似,可以复用- 一个返回解析的元素名,一个返回是否解析成功,完全可以复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* @brief 解析结束标签(end element)
* @param bSkip 控制是否跳过再解析,当 bSkip 为 true时,若解析当前标签的结束标签失败,则会跳过中间子节点直到解析到当前标签的结束标签;
* 如果bSkip为false,则表示要继续解析子节点,而不跳过内容。
* @return 如果函数返回 true,则表示找到了结束元素;如果函数返回 false,则表示没有找到当前标签的结束标签。
*/
bool readEndElement(bool bSkip);
/**
* @brief 解析结束标签并返回当前解析元素名
* @param bSkip 控制是否跳过再解析,当 bSkip 为 true时,若解析当前标签的结束标签失败,则会跳过中间子节点直到解析到当前标签的结束标签;
* 如果bSkip为false,则表示要继续解析子节点,而不跳过内容。
* @return 如果函数返回不为空的节点名,则表示找到了结束元素;如果函数返回空LString,则表示没有找到当前标签的结束标签。
*/
LString readEndEleName(bool bSkip);接口逻辑:
readUntilEndEle()
readUntilElement()
是碰到这个标签就可以了,readUntilEndEle()
还需要这个标签是结束标签- 参数应使用
const LString&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34bool LXmlReader::readUntilEndEle(const LString element)
{
if (element.isEmpty())
return false;
// 不存在结尾元素
LString strTail("</");
strTail.append(element);
strTail.append(">");
// 又来了,又是经典的 getBuffer()
LString buffer = m_parser.getBuffer();
if (!buffer.contains(strTail))
return false;
// 从栈里面取出一个一个的元素,去和给定的值比较
bool Flag = false;
if (m_stack.size() > 0)
{
for (const auto &ele : m_stack)
{
if (ele.m_element == element)
Flag = true;
}
}
// 如果有,就开始循环读取,直到找到目标
if (Flag)
{
while (m_stack.back().m_element != element)
{
readEndElement();
}
readEndElement();
return true;
}
return false;
}接口逻辑:
readPCData()
readPCDataW
()是返回std::wstring
的版本- 目前两个函数的实现分开,可以复用,通过标准的
LString
进行中转,然后各自导出为std::string
和std::wstring
- 目前两个函数的实现分开,可以复用,通过标准的
- 函数声明中提到自由文本节点的概念,但是官方好像并没有这个概念,这样是否合理?
1
2
3
4
5/**
* @brief 检索元素下的PC数据(即自由文本节点)
* @return 返回获取的PC数据,若为空则未获取到PC数据
*/
LString readPCData();- 函数定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// retrieve PC Data (free text nodes under an element).
LString LXmlReader::readPCData()
{
LString strData;
if (m_stack.size() > 0 && m_stack.back().m_children)
{
bool bOK = false;
LString strText;
// strText.reserve(128);
// 通过 Parser 的 readText() 函数从 < 开始读取文本内容
bOK = m_parser.readText(strText, '<');
if (bOK)
// 处理文本实体引用
readEntities(strText, strData);
// //未读取到pcdata
// if(strText.isEmpty()) bOK = false;
}
return strData;
}接口逻辑:
readStringElement()
readStringElementW()
是该函数返回std::wstring
的版本- 问题同上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// read text content: assumes element with text only - no child elements.
LString LXmlReader::readStringElement(const LString *strElement)
{
LString res;
LString now(*strElement);
int nowIndex = characterOffset();
// 如果现在读取的标签(m_stringName) 和传入参数匹配
if (m_stringName == *strElement)
{
// 读取文本内容
res = readPCData();
// 读取标签末尾
if (readEndElement(false, &res))
return res;
return LString("");
}
// 不匹配则一直读取直到找到目标
else if (readUntilElement(now))
{
// 逻辑同上
res = readPCData();
if (readEndElement(false, strElement))
{
return res;
}
else
{
// 获取预前进光标数,将光标退回
int upIndex = characterOffset() - nowIndex;
m_parser.m_iCursor = m_parser.m_iCursor - upIndex;
res = LString("");
return res;
}
}
return res;
}getAttribute()
和getAttributeW()
同样可以复用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// retrieve text for the named attribute.
LString LXmlReader::getAttribute(const LString *strAttribute)
{
// strValue.resize(0);
LString strValue;
if (strAttribute == nullptr)
return strValue;
// 遍历 m_attributes 获得结果
std::list<std::pair<LString, LString>>::const_iterator it = m_attributes.begin();
while (it != m_attributes.end())
{
if ((*it).first == (*strAttribute))
{
// expand entities only when value is requested.
// 实体引用的转换
readEntities((*it).second, strValue);
return strValue;
}
it++;
}
return LString("");
}
// 另一个的逻辑一摸一样enumAttributes()
接口不知道设计的目的是什么,经全局搜索也没有在其他接口中使用到1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// begin/end iterators for current element's attributes.
bool LXmlReader::enumAttributes(LList<LPair<LString, LString>>::iterator &itBegin, LList<LPair<LString, LString>>::iterator &itEnd)
{
// itBegin = m_attributes.begin();
// itEnd = m_attributes.end();
// return m_attributes.size() > 0;
LList<LPair<LString, LString>>::iterator m_begin(m_attributes2.begin());
LList<LPair<LString, LString>>::iterator m_end(m_attributes2.end());
// 把成员变量的首尾迭代器赋值给参数???
itBegin = m_begin;
itEnd = m_end;
// itBegin = m_attributes.begin();
// itEnd = m_attributes.end();
// 有数据就返回 true ???
return m_attributes.size() > 0;
}接口逻辑:
getComment()
b.toInt()
这一样没有用,可以删去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22LString LXmlReader::getComment()
{
// 防止影响其他接口
if (m_stack.size() == 0)
return LString("");
skipspace(false);
if (m_stack.size() > 0 && !m_stack.back().m_iComment.isEmpty())
{
// 判断当前偏移量和 m_stack 中记录的偏移量是否一致
LString a = LString::fromInt(characterOffset());
LString b = m_stack.back().m_commentIndex;
b.toInt();
if (a.toInt() == b.toInt())
{
// 获取注释内容
LString now = m_stack.back().m_iComment;
now.trim();
return now;
}
}
return LString("");
}
LXmlWriter
代码存在各种规范问题,例如换行不规范、注释不全、注释不规范,函数可以内联等等,统一列在这里,后续不赘述
成员变量
m_pStream
是一个引用(???),或许有这种用法,但是肯定不是在这里1
2// 输入流内容
LString &m_pStream;成员变量嵌套元素的栈应使用
LStack
类型- 等等,前面是
std::stack
,这里又是std::vector
??
1
2// 嵌套元素的堆栈。
std::vector<LEntry> m_stack;- 等等,前面是
接口逻辑:
adopt
()TODO
:没看懂。。。
1
2
3
4
5
6
7
8
9void LXmlWriter::adopt()
{
if (m_stack.size() && !m_stack.back().m_children)
{
m_stack.back().m_children = true;
writeString(">");
writeString("\n");
}
}writeString()
接口,提供了多个重载版本,但是均可以做到复用,可通过LString
进行统一- 下面的做法都是将封装类型
LString
或者std::wstring
转化为基础类型const char*
和const wchar_t
*在插入,个人认为虽然可行但不优雅,应像上面所说使用LString
进行统一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void LXmlWriter::writeString(const char *strText)
{
// size_t iLen = strlen(strText);
// size_t iWrote = 0;
// m_pStream->Write((unsigned char *)strText, iLen, iWrote);
m_pStream.append(strText);
}
void LXmlWriter::writeString(const wchar_t *strText)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
std::string strTran = converter.to_bytes(strText);
writeString(strTran.c_str());
}
void LXmlWriter::writeString(const LString &strText)
{
// 不是哥,你数据的类型是 LString ,你插入 LString 还先转化为 const char* ???
writeString(strText.toStdString().c_str());
}
void LXmlWriter::writeString(std::wstring &strText)
{
writeString(strText.c_str());
}- 下面的做法都是将封装类型
writeAttributeRaw()
和writeAttribute()
函数writeAttributeRaw()
的参数应使用const LString&
- 观察了这两个函数可以发现,
writeAttributeRaw()
函数是writeAttribute()
函数实际做写入操作的函数 - 并且经过全局搜索后发现
writeAttributeRaw()
函数只在writeAttribute()
函数中被调用,writeAttribute()
其实是有很多重载版本 - 那么完全可以将
writeAttributeRaw()
函数合并到writeAttribute()
中,做一个基础版本的writeAttribute()
,比如就是LString
,其他的重载都复用该版本即可 - 目前的重载版本只留了一个
LString
,其他的版本均注释掉,后续应考虑撤销注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38bool LXmlWriter::writeAttributeRaw(const LString *strAttribute, const LString *strValue)
{
// 真正调用 writeString() 做写入操作的函数
if (strAttribute == nullptr || strValue == nullptr)
{
return false;
}
writeString(" ");
writeString(*strAttribute);
writeString("=\"");
writeString(*strValue);
writeString("\"");
return true;
}
bool LXmlWriter::writeAttribute(const LString &strAttribute, const LString &strValue)
{
// 先做一些合法性的判断,然后调用 writeAttributeRaw() 进行实际的写入操作
// 判空
if (strAttribute.isEmpty() || strValue.isEmpty())
{
std::cout << "写入属性不能为空" << std::endl;
return false;
}
// 判断写入格式是否正确
if (!rightForm(strAttribute))
{
std::cout << "写入格式错误,写入" << strAttribute << "失败" << std::endl;
return false;
}
std::string strEntity;
// 将特殊符号替换为其对应的实体引用
insertEntities(strValue.toStdString().c_str(), strEntity);
const LString valueStr(strEntity);
// insertEntities(strValue, strEntity);
// return writeAttributeRaw(strAttribute, strEntity.c_str());
return writeAttributeRaw(&strAttribute, &valueStr);
}构造函数的实现
m_pStream
是一个LString
,初始化先new
一个LString
,再解引用,然后赋值???- 参数中
LString&
,LByteArray&
,LFile&
均是左值引用,应当加上const
以保证能够传入value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26LXmlWriter::LXmlWriter() : m_pStream(*(new LString(""))) {}
LXmlWriter::LXmlWriter(LString &inputStream) : m_pStream(inputStream) {}
LXmlWriter::LXmlWriter(LByteArray &inputStream) : m_pStream(*(new LString("")))
{
m_pStream = inputStream.toString();
}
LXmlWriter::LXmlWriter(const char *inputStream) : m_pStream(*(new LString(inputStream))) {}
LXmlWriter::LXmlWriter(LFile &inputStream) : m_pStream(*(new LString("")))
{
if (!inputStream.exists())
{
std::cout << "文件不存在" << std::endl;
}
inputStream.open(LFile::OpenMode::ReadOnly | LFile::OpenMode::Text);
LString file;
while (!inputStream.end())
{
file.append(inputStream.readLine().toString());
}
m_pStream = file; // 绑定引用成员变量与传入的对象
}接口逻辑:
writeStartElement()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21bool LXmlWriter::writeStartElement(const LString &strElement)
{
// 判空
if (strElement.isEmpty())
return false;
// 检查写入的值是否符合规范
if (!rightForm(strElement))
{
std::cout << "写入格式错误,写入" << strElement << "失败" << std::endl;
return false;
}
LEntry e;
e.m_element = strElement;
// call before pushing new element onto stack.
adopt();
m_stack.push_back(e);
// 执行写入操作
writeString("<");
writeString(strElement);
return true;
}接口逻辑:
writeEndElement()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19bool LXmlWriter::writeEndElement()
{
if (m_stack.size())
{
if (m_stack.back().m_children)
{
// 写入一个标签
writeString("</");
writeString(m_stack.back().m_element.toStdString().c_str());
writeString(">\n");
}
else
// 没有子元素,又写入自闭合标签???
writeString(" />\n");
m_stack.pop_back();
return true;
}
return false;
}接口逻辑:
writeStringElement()
- 两个重载版本,一个带属性s,一个不带属性
- 解释一下四个参数的含义
strElement
:元素标签名strValue
:元素文本内容attributeName
:属性名attributeValue
:属性内容
- 解释一下四个参数的含义
- 属性内容这里给的是泛型
T
,但是在函数内部写入的时候给他当成LString
直接用了,这是不合理的,可以提供多个版本的重载,或者规定只能传入LSring
,把数据类型转换的问题交给用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50template <typename T>
bool LXmlWriter::writeStringElement(const LString &strElement, const LString &strValue,
const LString &attributeName, T attributeValue)
{
// 判空
if (strElement.isEmpty())
{
std::cout << "元素名为空,写入失败" << std::endl;
return false;
}
if (attributeName.isEmpty())
{
std::cout << "写入属性名为空,写入失败" << std::endl;
return false;
}
// 判断格式是否正确
if (!rightForm(strElement))
{
std::cout << "写入格式错误,写入失败";
return false;
}
adopt();
// 写入起始标签
writeString("<");
writeString(strElement);
// 写入属性
if (!attributeName.isEmpty())
{
// 这个函数目前只有两个参数都是 LString 的重载版本,就直接把 T 类型的属性值当成 LString 传进去了???
writeAttribute(attributeName, attributeValue);
}
writeString(">");
// 写入文本内容
writeString(strValue);
// 写入结束标签
writeString("</");
writeString(strElement);
writeString(">\n");
return true;
}
bool LXmlWriter::writeStringElement(const LString &strElement, const LString &strValue)
{
// 逻辑类似
...
}- 两个重载版本,一个带属性s,一个不带属性
接口逻辑:
writePCData()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18bool LXmlWriter::writePCData(const LString &strPCData)
{
// 判空
if (strPCData.isEmpty())
{
std::cout << "写入内容为空" << std::endl;
return false;
}
std::string strEntity;
// insertEntities(strPCData, strEntity);
// 字符的实体引用替换
insertEntities(strPCData.toStdString().c_str(), strEntity);
adopt();
// 写入内容
writeString(strEntity.c_str());
writeString("\n");
return true;
}接口逻辑:
writeComment()
和writeCommentOrigin()
- 二者都是写入注释,只不过前者要求输入的是标准格式的注释,后者是输入的是纯注释内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 要求输入标准格式
bool LXmlWriter::writeComment(const LString &text)
{
LString comment = text;
// 对输入的字符串是否符合注释规范进行检测
// 是否包含注释标识
if (text.contains("<!--") && text.contains("-->"))
{
comment.trim();
// 注释标识的位置是否正确
if (comment.indexOf("<!--") == 0 && comment.indexOf("-->") == comment.length() - 3)
{
adopt();
comment.append("\n");
// 写入
writeString(comment);
}
else
{
std::cout << "所输入注释格式不正确" << std::endl;
return false;
}
}
return true;
}
// 输入纯注释内容
void LXmlWriter::writeCommentOrigin(const LString &text)
{
if (text.isEmpty())
{
return;
}
adopt();
// 构造注释标识然后写入
LString comment = LString("<!--");
comment.append(text);
comment.append("-->");
comment.append("\n");
writeString(comment);
}接口逻辑:
writeDTD()
- 在
xml
中引入DTD
文件
1
DTD
文件内容
1
2
3
4
5
6
7
8<!DOCTYPE note
[
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>- 代码
- 看起来代码里的实现是让
DTD
文件的内容是内嵌在xml
文件中 - 下面的逻辑根本不对啊,判断括号的个数是否匹配??那万一他们的位置信息不对呢???想得出来哦。。
- 看起来代码里的实现是让
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48bool LXmlWriter::writeDTD(const LString &dtd)
{
if (dtd.isEmpty())
return false;
LString nowDTD = dtd;
nowDTD.trim();
// 先找固定的开头
if (nowDTD.indexOf("<!DOCTYPE") == 0)
{
int left = 0;
int right = 0;
int mid = 0;
int mid2 = 0;
nowDTD = nowDTD.substr(8, -1);
// 一个一个匹配 ELEMENT 的格式
for (int i = 0; i < nowDTD.length(); i++)
{
// 由于最开始消耗了一个 < ,因此后续的 > 总会比 < 多一个
if (nowDTD.at(i) == LChar("<"))
{
i++;
if (nowDTD.at(i) == LChar("!"))
left++;
}
else if (nowDTD.at(i) == LChar(">"))
right++;
// 通过 mid 判断 ( 和 ) 个数一致
else if (nowDTD.at(i) == LChar("("))
mid++;
else if (nowDTD.at(i) == LChar(")"))
mid--;
// 同样,[ 和 ] 个数一致
else if (nowDTD.at(i) == LChar("["))
mid2++;
else if (nowDTD.at(i) == LChar("]"))
mid2--;
}
if (mid == 0 && left == right - 1 && mid2 == 0)
{
writeString(dtd);
writeString("\n");
return true;
}
}
std::cout << "格式有误,写入错误" << std::endl;
return false;
}- 在
writeStartDocument()
接口最后的return
可以删除- 整体逻辑没什么毛病
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void LXmlWriter::writeStartDocument(const LString &version, const LString &encoding)
{
if (!version.isEmpty() && !encoding.isEmpty())
{
LString versionadd("<?xml version=\"");
versionadd.append(version);
versionadd.append("\" encoding=\"");
versionadd.append(encoding);
versionadd.append("\"?>\n");
m_pStream = versionadd + m_pStream;
}
else if (encoding.isEmpty() && !version.isEmpty())
{
LString versionadd("<?xml version=\"");
versionadd.append(version);
versionadd.append("\"?>\n");
m_pStream = versionadd + m_pStream;
}
return;
}writeFileAdd()
和writeFileRewrite()
功能类似,可以考虑复用二者功能差不多,只不过一个是
append
,一个是从头write
最后的
puts()
那里,代码太冗杂了,拿一个返回值记录即可;并且如果写入失败,也没有做日志输出或者抛出异常等相关处理writeFileRewrite()
写入以前有trim()
去除首位空格操作,writeFileAdd()
却没有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34bool LXmlWriter::writeFileAdd(LFile &file)
{
if (!file.exists())
{
std::cout << "文件不存在,新建文件" << std::endl;
}
file.open(LFile::OpenMode::Append | LFile::OpenMode::Text);
// what???。。。
if (file.puts(m_pStream))
{
file.close();
return true;
}
file.close();
return false;
}
bool LXmlWriter::writeFileRewrite(LFile &file)
{
if (!file.exists())
{
std::cout << "文件不存在,新建文件" << std::endl;
}
file.open(LFile::OpenMode::WriteOnly | LFile::OpenMode::Text);
if (file.puts(m_pStream))
{
// 这里用了前面却不用
m_pStream.trim();
file.close();
return true;
}
file.close();
return false;
}
namespace
namespace
中的所有函数都声明的是静态函数,那为什么不直接声明成静态成员函数,就不用开命名空间了里面有三个
append()
函数的重载版本,但是在整个文件中没有地方用到,可以删除mbstate_t
和mbrtowc()
是标准C
库的函数,是跨平台的,这个没问题,由于这三个函数没啥用,就不细看了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 根据需要处理转码以将字节字符附加到字符串。
static void append(std::wstring &strValue, char ch, mbstate_t &state)
{
wchar_t dest[4] = {0};
if (mbrtowc(dest, &ch, 1, &state) > 0)
strValue += dest;
}
// 根据需要处理转码以将字节字符附加到字符串。
static void append(LString &strValue, char ch)
{
strValue.append(LChar(ch));
}
static void append(std::string &strValue, char ch, mbstate_t &state)
{
char dest[4] = {0};
size_t used = 0;
errno_t err = wcrtomb_s(&used, dest, sizeof dest, ch, &state);
if (err == 0)
strValue.append(dest, used);
// state;
strValue += (char)ch;
}接口逻辑:
readEntities()
- 函数的作用是把
xml
中的实体引用恢复成为其原本的文本的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74// 将五个标准 XML 实体扩展为其文本值。
// & < > ’ “
// 未识别的实体被保留。
static void readEntities(const LString &strValue, LString &strResult)
{
strResult = LString(""); // 测试是否正确
bool bEntity = false;
LString strEntity;
// 更改为接收所有utf8字符
// 一个一个字节判断???
for (int i = 0; i < strValue.length(); i++)
{
LChar ch = strValue[i];
// 实体引用开始
if (ch == LChar("&"))
{
bEntity = true;
}
// 实体引用结束
else if (ch == LChar(";"))
{
// 处理实体引用的转换
if (bEntity)
{
// &
if (strEntity.compare("amp") == 0)
strResult.append(LChar('&'));
// 小于
else if (strEntity.compare("lt") == 0)
strResult.append(LChar('<'));
// 大于
else if (strEntity.compare("gt") == 0)
strResult.append(LChar('>'));
// 双引号
else if (strEntity.compare("quot") == 0)
strResult.append(LChar('"'));
// 单引号
else if (strEntity.compare("apos") == 0)
strResult.append(LChar('\''));
// 都不是,表示不是实体引用
else
{
strResult.append(LChar('&'));
strResult.append(strEntity);
strResult.append(LChar(';'));
}
bEntity = false;
}
}
else
{
// 如果经过实体引用开始 & ,表示可能是实体引用,加入 strEntity,待后续转化
if (bEntity)
strEntity.append(ch);
// 加入结果字符串
else
{
strResult.append(ch);
}
}
}
}
// 这个是返回字符串是 std::wstring 的版本
static void readEntities(const LString &strValue, std::wstring &strResult)
{
size_t size = strValue.length();
LString strMulti;
readEntities(strValue, strMulti);
strResult.resize(size);
std::string a = strMulti.toStdString();
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
strResult = converter.from_bytes(a);
}- 函数的作用是把
接口逻辑:
insertEntities()
- 为什么一来要
reserve()
?append()
会自动扩容的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30static void insertEntities(const char *strText, std::string &strEntity)
{
strEntity.reserve(64);
while (*strText != '\0')
{
// 将实体文本内容换成实体引用
char ch = *strText++;
switch (ch)
{
case '\'':
strEntity += "'";
break;
case '&':
strEntity += "&";
break;
case '<':
strEntity += "<";
break;
case '>':
strEntity += ">";
break;
case '"':
strEntity += """;
break;
default:
strEntity += ch;
break;
}
}
}- 为什么一来要
4.22 - 4.30
迭代任务
- 完成
LFileSystemPath
和LFileSystemEntry
的编写,自测无误后已经转测 - 对比
std::map
和QMap
的接口,当然会延申出std::multimap
和QMultiMap
- 同理处理
std::unordered_map
和QHash
- 帮助陈冠杰处理部分
LSet
部分的编写
学习的点
关于
std::multimap
的模板参数中的cmp
- 可以传入自定义的排序规则的
functor
,但是由于自身内部是红黑树的设计,传入的cmp
谓词的类型必须是key
,而不能是其他的,比如pair<key, data>
,即只能通过key
来进行排序,没有办法key
相同的时候通过data
排序。因为内部对functor
的类型做了检查,必须是key
的类型才能编译通过,其实这样想也有道理,红黑树的结构需要保持,只通过key
显然是最好的;对于上面的需求,可以使用set
存储pair
,然后自定义cmp
谓词处理数据即可
- 可以传入自定义的排序规则的
关于
std::map
和std::multimap
的二元谓词的思考- 看到
less<int>
的内部实现,发现只是简单的return x < y
1
2
3
4
5
6
7
8
9/// One of the @link comparison_functors comparison functors@endlink.
template<typename _Tp>
struct less : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x < __y; }
};- 现在我们构想一个排序的过程,例如冒泡排序,最重要的过程就是根据两个数的大小关系,然后判断是否发生交换,例如如果是升序排序,那么第一个数大于第二个数的时候就会发生交换。但是对于
less<int>
而言,里面是x < y
,经过平时的使用我们知道,这个东西是对应升序的,那么x < y
返回true
,不是就应该交换了吗,也就是降序吗? - 想到这里,就需要牵扯到谓词的逻辑了,我们假设二元谓词是
cmp(int val1,int val2)
,交换操作是swap
,实际的逻辑用伪代码写就是这样
1
2
3
4if(cmp(...))
{
swap(...)
}- 显然,
cmp
返回true
的时候才会做后续的事情,不管是几元谓词的逻辑都是这样,因此,可以预见的是,在实际的cmp
参数里面,是这样的,cmp(val2, val1)
,第二个值在前面,第一个基准值在后面,这样才能保证正确的结果,也可以这样理解,后面的值需要同前面的值进行比较,所以排在前面 - 所以,对于自定义的二元谓词,小于对应升序,大于对应降序,这也是符合用户使用习惯的
- 看到
QMap
构造函数
std::map
提供了很多版本,但是很多都和用户的使用没有关系,比如传入迭代器,分配器等,因此下面只看QMap
- 默认构造:略
- 带参构造、拷贝构造和移动构造:通过
initializer_list
构造,通过std::map
和QMap
进行拷贝和移动构造
1
2
3
4
5QMap(std::initializer_list<std::pair<Key, T>> list);
QMap(const std::map<Key, T> &other);
QMap(std::map<Key, T> &&other);
QMap(const QMap<Key, T> &other);
QMap(QMap<Key, T> &&other);析构函数:略
迭代器:首尾,反转首尾
- 应该提供非常量迭代器和常量迭代器的版本,接口包括
begin()
、end()
、rbegin()
、rend()
1
2
3
4
5
6QMap<Key, T>::iterator begin();
QMap<Key, T>::const_iterator begin() const;
QMap<Key, T>::const_iterator cbegin() const;
QMap<Key, T>::const_iterator cend() const;
QMap<Key, T>::const_iterator constBegin() const; // begin() 返回常量迭代器的别名
QMap<Key, T>::const_iterator constEnd() const; // cend() 返回常量迭代器的别名- 应该提供非常量迭代器和常量迭代器的版本,接口包括
clear()
:略contains()
:判断key
是否在map
中1
bool contains(const Key &key) const;
count()
:返回指定key
的元素个数,在map
中key
无法重复,值只能是0
或1
- 第二个重载,返回整个
map
的大小,即size()
1
2QMap<Key, T>::size_type count(const Key &key) const;
QMap<Key, T>::size_type count() const;- 第二个重载,返回整个
empty()
:判空,略isEmpty()
是empty()
的别名
equal_range()
:找到和给定key
相同的迭代器的范围,满足左开右闭的规则- 个人认为对
map
而言,这个接口没有意义,因为key
是唯一的,因此匹配范围是没有意义的,对于multimap
才有这个需求
1
2std::pair<QMap<Key, T>::iterator, QMap<Key, T>::iterator> equal_range(const Key &key);
std::pair<QMap<Key, T>::const_iterator, QMap<Key, T>::const_iterator> equal_range(const Key &key) const;- 个人认为对
erase()
:删除元素- 第二个重载版本,由于
map
默认是会自动排序的,因此遍历的结果是有序的(unorder_map
的哈希的遍历结果不一定是有序的,因为经过插入或者删除的哈希桶结构会发生变化,因此两个unorder_map
的元素是相同的情况下遍历的结果也可能会不同,但是map
底层是红黑树,元素是有固定的顺序的,因此结果有序),因此可以通过迭代器删除区间的元素
1
2QMap<Key, T>::iterator erase(QMap<Key, T>::const_iterator pos);
QMap<Key, T>::iterator erase(QMap<Key, T>::const_iterator first, QMap<Key, T>::const_iterator last);- 第二个重载版本,由于
find()
:通过key
,查找并返回迭代器1
2QMap<Key, T>::iterator find(const Key &key);
QMap<Key, T>::const_iterator find(const Key &key) const;constFind()
:通过key
找到迭代器,返回常量迭代器,即find
返回常量迭代器的重载版本的别名1
QMap<Key, T>::const_iterator constFind(const Key &key) const;
first()
、firstKey()
:返回首元素或者首元素的value
和key
- 对应的有
last
系列函数
1
2
3
4
5
6
7T& first();
const T& first() const;
const Key& firstKey() const;
T& last();
const T& last() const;
const Key& lastKey() const;- 对应的有
insert()
:根据key
和value
插入到map
中- 第四个重载版本,个人认为没有必要,
const &
本身就是万能引用,既能接受左值,又能接受右值
1
2
3
4QMap<Key, T>::iterator insert(const Key &key, const T &value);
QMap<Key, T>::iterator insert(QMap<Key, T>::const_iterator pos, const Key &key, const T &value);
void insert(const QMap<Key, T> &map);
void insert(QMap<Key, T> &&map);- 第四个重载版本,个人认为没有必要,
keys()
:返回key
的列表- 第二个重载版本,返回指定
value
对应的key
列表,因为key
不重复,value
当然可能重复,所以返回的是一个列表
1
2QList<Key> keys() const;
QList<Key> keys(const T &value) const;- 第二个重载版本,返回指定
values()
:导出value
列表,略key()
:根据value
找到对应第一个的key
- 第二个参数,用户可以自己给定如果
value
不存在,返回的key
值,否则返回默认值 - 这个参数的作用在于用户可以通过这个接口判断指定的
value
对应的key
存不存在,因为直接返回默认值作为参考的话,如果用户当前存储的刚好就是默认值,那么就没有办法正确判断value
对应的key
是否存在了,因此提供了一个defaultKey
的参数,这个设计比较巧妙
1
Key key(const T &value, const Key &defaultKey = Key()) const;
- 第二个参数,用户可以自己给定如果
value()
:根据key
找到对应的第一个value
,第二个参数同上1
T value(const Key &key, const T &defaultValue = T()) const;
lowerBuond()
和upperBound()
:通过二分有序查找指定的元素例子:
1,2,2,2,3
(当然map
是去重的,这里只是明确语义),key=2
lowerBound()
:找到大于等于key
的第一个元素,这里就是第一个2
upperBound()
:个人认为是找到不大于key
的最后一个元素,也就是最后一个2
,但是根据标准库和迭代器的语义,返回的是3
,也就是大于key
的第一个元素对于
map
而言,这两个接口同样没有意义,因为key
是唯一的,该接口适用于multimap
1
2
3
4QMap<Key, T>::iterator lowerBound(const Key &key);
QMap<Key, T>::const_iterator lowerBound(const Key &key) const;
QMap<Key, T>::iterator upperBound(const Key &key);
QMap<Key, T>::const_iterator upperBound(const Key &key) const;remove()
和removeIf()
- 第二个重载版本,根据传入的谓词进行删除
1
2QMap<Key, T>::size_type remove(const Key &key);
QMap<Key, T>::size_type removeIf(Predicate pred);size()
:返回大小,略swap()
:与另一个map
对象进行交换,略take()
:删除指定key
对应的元素并返回对应的data
- 如果
key
不存在,那么返回默认的data
1
T take(const Key &key);
- 如果
toStdMap()
:导出为std::map
,略1
T value(const Key &key, const T &defaultValue = T()) const;
operator=()
:拷贝和移动赋值函数1
2QMap<Key, T>& operator=(const QMap<Key, T> &other);
QMap<Key, T>& operator=(QMap<Key, T> &&other);operator[]()
:中括号[]
运算符重载- 个人认为第二个返回值的版本没有意义,这两个在实际接受的时候没有办法进行合理区分,建议改为
const T&
1
2T& operator[](const Key &key);
T operator[](const Key &key) const;- 个人认为第二个返回值的版本没有意义,这两个在实际接受的时候没有办法进行合理区分,建议改为
key_iterator
Qt
提供了一个专门用于遍历key
的迭代器- 个人认为完全没有必要,提供最普通的迭代器返回一个
pair
就完事了
1
2QMap<Key, T>::key_iterator keyBegin() const;
QMap<Key, T>::key_iterator keyEnd() const;key_value_iterator
- 经查询,
Qt
提供了一个STL Style
的迭代器
1
2
3
4
5
6QMap<Key, T>::key_value_iterator keyValueBegin();
QMap<Key, T>::const_key_value_iterator keyValueBegin() const;
QMap<Key, T>::key_value_iterator keyValueEnd();
QMap<Key, T>::const_key_value_iterator keyValueEnd() const;
QMap<Key, T>::const_key_value_iterator constKeyValueBegin() const;
QMap<Key, T>::const_key_value_iterator constKeyValueEnd() const;- 经查询,
各种迭代器总结
iterator
:迭代器,解引用返回的是data
,即T
const_iterator
:iterator
的常量版本key_value_iterator
:解引用返回的是pair<key,T>
key_iterator
:解引用返回的是key
asKeyValueRange()
- 这个接口是在
Qt6.4
以后引入的,个人认为用不上 - 他让
for each
可以使用一个pair
去接受,我觉得不如使用迭代器
1
2
3
4
5
6
7
8
9
10QMap<QString, int> map;
map.insert("January", 1);
map.insert("February", 2);
// ...
map.insert("December", 12);
for (auto [key, value] : map.asKeyValueRange()) {
cout << qPrintable(key) << ": " << value << endl;
--value; // convert to JS month indexing
}- 这个接口是在
QMultiMap
绝大部分接口和Map
类似,下面只列出不同的几处。
构造函数
- 除了最基本的,
Qt6.0
以后可以支持从QMap
构造成为QMultiMap
1
2QMultiMap(const QMap<Key, T> &other);
QMultiMap(QMap<Key, T> &&other);- 除了最基本的,
map
中大部分传入key
做查询,包含等操作的接口,在multimap
中可以传入key
和value
- 例如:
find()
、constFind()
、contains()
、count()
、remove()
1
2
3
4
5QMultiMap<Key, T>::const_iterator find(const Key &key, const T &value) const;
QMultiMap<Key, T>::const_iterator constFind(const Key &key, const T &value) const;
bool contains(const Key &key, const T &value) const;
QMultiMap<Key, T>::size_type count(const Key &key, const T &value) const;
QMultiMap<Key, T>::size_type remove(const Key &key, const T &value);- 例如:
replace()
- 如果
multimap
中没有元素的key
和传入key
相同,则执行插入操作 - 如果有一个元素匹配,则将传入的
value
替换原元素的value
- 如果有多个元素匹配,则替换最后插入的那个元素
1
QMultiMap<Key, T>::iterator QMultiMap::replace(const Key &key, const T &value);
- 如果
uniqueKeys()
- 返回所有的去重版本的
keys()
1
QList<Key> QMultiMap::uniqueKeys() const;
- 返回所有的去重版本的
unite()
- 将两个
multimap
合并,重复的元素保留不做处理,也就是新multimap
的大小等于原两个multimap
的大小之和 - 第二个移动构造的重载版本,个人认为没有必要
1
2QMultiMap<Key, T> &QMultiMap::unite(const QMultiMap<Key, T> &other);
QMultiMap<Key, T> &QMultiMap::unite(QMultiMap<Key, T> &&other)- 将两个
QHash
大部分接口和QMap
是大同小异的,但是由于底层实现不同,QHash
是哈希表,QMap
是红黑树,因此细节上可能会有一些小区别,在下面会列出。
capacity()
:返回哈希桶的数量- 在哈希表数组加链表的实现思路中,数组称为哈希桶,数组的长度称之为哈希桶的数量,每个数组下会接有链表,如果链表较长,代表
hashCode
取模以后相同,就会引发哈希冲突,查询效率就会退化,这是不可接受的。因此在下面会有load_factor
负载因子的概念,负载因子需要保持在某个合理的范围才能够接受。当然实际的哈希表还有更多的细节
1
qsizetype capacity() const;
- 在哈希表数组加链表的实现思路中,数组称为哈希桶,数组的长度称之为哈希桶的数量,每个数组下会接有链表,如果链表较长,代表
load_factor()
:返回负载因子的值,即size() / capacity()
1
float load_factor() const;
reserve()
:手动重新申请指定大小的内存空间- 如果申请的空间比当前
size()
更大,那么相当于预申请了一块内存,后续就可以减少扩容的次数 - 如果申请的空间比当前
size()
更小,那么会申请能满足当前size()
的最小的capacity()
1
void reserve(qsizetype size);
- 如果申请的空间比当前
squeeze()
:在保持性能的前提下尽量缩减哈希表的内存,减少哈希桶的数量- 利用
reserve()
函数即可,重新申请特定大小的内存空间,并在当前存储数据的基础上尽量达到需求,也就是调用reserve(0)
即可
1
void squeeze();
- 利用
emplace()
:当data
是结构体,需要构造的情况下,通过传递右值引用,进行在位构造,减少一次拷贝的过程1
2QHash<Key, T>::iterator emplace(const Key &key, Args &&... args);
QHash<Key, T>::iterator emplace(Key &&key, Args &&... args);
QMultiHash
暂时不关心。
QSet
在底层实现中,unordered_map
中的索引和元素的值分别对应Key
和Data
,而value_type
对应的std::pair<const Key, Data>
,然后在哈希表中需要用到这个Key
和value_type
。这一点要分清,而在unordered_set
中,Key
和value_type
都是输入的元素类型T
,即key means value
。这一点在以红黑树为基础的map
和set
中是一样的,具体实现当然有更多的细节。
比较两个unordered_set
是否相等,准确来讲是比较底层的哈希表两个对象是否相等,是去看存储的元素序列是否相等,与哈希表自身的结构无关,也就是两个哈希表的容量capacity
不同的情况下,他们元素序列是一样的,他们就是相等的(当然,必须是模板实例化后相同的类才能作比较),这一点std::unordered_set
已经做好了。
contains()
:多了一个重载版本,判断当前的集合是否包含另一个集合1
2bool contains(const T &value) const;
bool contains(const QSet<T> &other) const;intersect()
:求两个集合的交集- 对应有重载
&=
运算符,可以考虑&
运算符
1
2
3
4QSet<T>& intersect(const QSet<T> &other);
bool intersects(const QSet<T> &other) const;
QSet<T>& operator&=(const QSet<T> &other);
QSet<T>& operator&=(const T &value);- 对应有重载
subtract()
:求两个集合的差集- 对应有重载
-=
运算符,可以考虑-
运算符
1
QSet<T>& subtract(const QSet<T> &other);
- 对应有重载
unite()
:求两个集合的并集- 对应有重载
+=
和|=
运算符,可以考虑+
和|
运算符
1
QSet<T>& unite(const QSet<T> &other);
- 对应有重载
重载
<<
运算符,等价于insert()
操作- 返回自身引用,满足链式调用关系
1
QSet<T>& operator<<(const T &value);
后续
后面的工作内容大多数都是以课题研究的性质为主,相关文章请点击这里跳转,不单独在此处赘述了。