在C中面向对象:#0x2 让我们来实现一个迭代器!
非常建议先看0x1
说起来我才发现,因为我超喜欢用波浪线,一大堆东西被替换成了删除线QAQ
前言
我们先来解决上一节中遗留下来的几个问题吧
UNUSED
由于在上一节中,我们强行规定了函数该长什么样。但是,有些时候我们可能并用不到某些参数,这个时候编译器就会报warning或error(取决你有没有开Wall、Werror、Wextra)。
为了保证编译器既不会报错,又能符合固定的函数原型,在此我们需要定义UNUSED
宏。
1 |
该宏的作用是:若编译器有__attribute__((unused))
选项时,则给该变量添加上unused
属性,并重命名为UNUSED_var
。
其示例用法如下:
1 | void foo(void *UNUSED(var1),int var2) //var1可以不被使用,var2则必须被使用 |
简单的获取指针指向的数据
由于为了泛型,void *
满天飞导致获取数据时需要大量的强制类型转换,对此我们可以加上个方便的宏:ptr2data
1 |
遗漏的operator
在上一期的operator里并没有自增和自减(主要因为作为参考的python里没有QAQ)
对此,我们在class定义里补上这几个函数
1 | void (*operator_inc)(void *_this); |
并在class.h
里补上这两个函数声明
1 | void inc(void *_this); |
关于为什么不直接将inc(_this)
链接到iadd(_this,1)
:说不定有人想重载呢对吧?
当然这里可以做一个fallback:若未定义operator_inc,就自动退化到iadd 1
用ioperator替换operator
ioperator:对自身进行操作
operator:复制一个新的对象并进行操作
我们发现,要调用operator,可以直接由操作符本身来复制对象,并对新对象来使用ioperator,这样我们实现类的时候又可以少实现一些了诶~!
但是我们这里还是保留最原始的用法吧~ 说不定会有人想重载呢?w~
目标
为了实现一部分的模板多态性,同时也是为了来基本的尝试一下使用,我们需要来实现一个iterator。
一个好的iterator必须具有以下的功能:
- 能自增一、自减一
- 能获取对应数据的地址
- 能比较是否相等(关于比较大小(先后):前面的蛆以后再来探索吧?x)
于是,我们可以列出如下的一个todo-list:
- [ ] constructor
- [ ] deep_copy
- [ ] destructor
- [ ] operator_add
- [ ] operator_iadd
- [ ] operator_min
- [ ] operator_imin
- [ ] operator_eq
- [ ] increase_iterator
- [ ] decrease_iterator
- [ ] iterator_data
实现class iterator
PS:在vs code上,不知道为什么直接将文件命名为iterator.h
会导致无法解析(和C++的冲突了……?).对此,名称改为my_iterator.h
让我们开始吧~!
iterator原型
首先,它需要继承class_base
其次,我们要提供一个将iterator自增、自减的模板函数,供子类来重载
最后,我们需要提供一个获取iterator指向的数据地址的模板函数,供子类来重载。
于是,iterator原型长这样:
1 | typedef struct ITERATOR_BASE_T |
暴露的接口函数
我们需要:获得iterator指向指针的函数void *it_data(void *_this);
我们还需要暴露一些写好的iterator类方法,供定义iterator的子类时使用,也就是
1 | void iterator_constructor(void *UNUSED(_this), va_list *UNUSED(ap_p)); |
这里可能会有些疑惑:为什么我们需要这么暴露呢?
举个栗子:我们现在定义了一个vector_iterator,显然,这个vector_iterator的operator_add/iadd/min/imin是应该直接使用它的父类iterator的,也就是:
但是,直接使用.operator_add=iterator->operator_add
是不可行的,因为iterator
并不是一个常量(哪怕你加满了const)
C语言并不将const视为常量!
所以,你必须暴露这些方法,以便让子类能很好的继承
这就是用C语言来写类的代价啊——
实现原型中的各个函数
data函数由子类实现
constructor和destructor
我们发现:其实new一个iterator对象并不需要构造函数(delete相似),因为一个iterator的模板里就没啥要初始化的
虽然我们可以留空(会自动赋值成null,因为函数原型在全局变量里),但最好我们还是实现一下,也就是用到UNUSED
1 | void iterator_constructor(void *UNUSED(_this), va_list *UNUSED(ap_p)) |
add和min
接下来的add就是直接重复调用inc就好(inc会被子类实现w),加上的数字可以从ap_p中获取;而iadd就是先copy一份自己,再调用add
1 | void iterator_operator_iadd(void *_this, va_list *ap_p) |
min的实现类似
实现获取data指针的接口
1 | void *it_data(void *_this) |
“注册”iterator类
我们首先需要一个已经赋值好的iterator原型
1 | class iterator_c = { |
接下来在.c文件中注册它
1 | class *iterator = &iterator_c; |
同时,我们需要在.h文件中用extern声明
1 | extern class * iterator; |
为什么这里一定要加extern?
C语言在include头文件时相当于直接复制。如果你不加extern的话,若最终代码中有多个代码都include了该头文件,就相当于该变量在多个文件中都被定义了,就会出现重定义的锅
而用了extern,就相当于告诉该变量是在别处定义。此时再在该.c文件中定义,就总体只会定义一次ouo
一件非常重要的事情!
你们或许会注意到:在operator(i.e. 非i的operator)内,都会将当前的拷贝一份,并返回新的
而拷贝的时候是会new一个新的对象的
同时原来的对象并不会被自动析构!
(与C++直接定义是不同的,毕竟C++不用new的话,每个对象都只是局部变量)
(而这里是每个都是new)
也就是说:a=add(a,b)的话,是会漏内存的!(原来的a并没有被析构
其实这也是C++中使用new会造成的事,只是一般很少在C++里的这种地方用new,就不会太注意罢了?
一个iterator的子类
由于这里的iterator类更像一个抽象类,为了方便理解,我这里给出vector的iterator实现,对照之下应该会更有益于理解~
vector_iterator原型(.h中)
1 | extern class *vector_iterator; |
vector_iterator内部函数实现:(.c中)
构造、析构函数
1 | void vector_iterator_constructor(void *_this, va_list *ap_p) |
这里提一句:一般而言,
构造函数:先调用父类的构造函数,再构造自己新增的部分
析构函数:先析构自己的部分,再调用父类的析构函数
operator实现
1 | void vector_iterator_inc(void *_this) //自增 |
data函数实现
1 | void *vector_iterator_data(void *_this) |
注册vector_iterator
1 | class vector_iterator_c = { |
尾声
有问题的话留言、发邮件、发推都是可以的~ 联系方式见主页哦
希望以上的东西能给你带来启发,要是能学到新的东西就再好不过了~
下一章,我们来尝试用一个很基础的容器练练手:vector