众所周知,GNU/GCC在标准的C/C++基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展. 多数情况下, 其应用在变长数组中, 其定义如下: struct Packet { int state; int len; char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持 }; 首先对 0 长度数组, 也叫柔性数组,做一个解释 : 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体; 用法 : 在一个结构体的最后,声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量 (注意 : 数组名永远都不会是指针!), 但对于这个数组的大小, 我们可以进行动态分配。 注意 :如果结构体是通过calloc、malloc或 者new等动态分配方式生成,在不需要时要释放相应的空间。 优点 :比起在结构体中声明一个指针变量、再进行动态分 配的办法,这种方法效率要高。因为在访问数组内容时,不需要间接访问,避免了两次访存。 缺点 :在结构体中,数组为 0 的数组必须在最后声明,使用上有一定限制。 对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量! 0 长度数组的用途: 我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路: 定长数据缓冲区, 设置一个足够大小MAX_LENGTH的数据缓冲区 设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间 我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟、释放和访问。 1、定长包(开辟空间, 释放, 访问): 比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度MAX_LENGTH为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费: 数据结构定义: // 定长缓冲区 struct max_buffer { int len; char data[MAX_LENGTH]; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char) * MAX_LENGTH 由于考虑到数据的溢出, 变长数据包中的data数组长度一般会设置得足够长足以容纳最大的数据, 因此max_buffer中的 data 数组很多情况下都没有填满数据, 因此造成了浪费。 数据包的构造:假如我们要发送CURR_LENGTH = 1024个字节, 我们如何构造这个数据包呢;一般来说, 我们会返回一个指向缓冲区数据结构max_buffer的指针: // 开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL) { mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data); } 访问:这段内存要分两部分使用;前部分 4 个字节p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是len的作用);而紧接其后的内存是真正的数据部分, 通过p->data, 最后, 进行一个memcpy()内存拷贝, 把要发送的数据填入到这段内存当中 释放:那么当使用完毕释放数据的空间的时候, 直接释放就可以了 // 销毁 free(mbuffer); mbuffer = NULL; 2、小结: 使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间MAX_LENGTH, 而实际使用过程中, 达到MAX_LENGTH长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的 但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作 3、 指针数据包(开辟空间, 释放, 访问): 如果你将上面的长度为MAX_LENGTH的定长数组换为指针, 每次使用时动态的开辟CURR_LENGTH大小的空间, 那么就避免造成MAX_LENGTH - CURR_LENGTH空间的浪费, 只浪费了一个指针域的空间: 数据包定义: struct point_buffer { int len; char *data; }; 数据结构大小:考虑对齐, 那么数据结构的大小 >=sizeof(int) + sizeof(char *) 空间分配:但是也造成了使用在分配内存时,需采用两步 // ===================== // 指针数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); /// 开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL) { pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL) { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data); } } 首先, 需为结构体分配一块内存空间;其次再为结构体中的成员变量分配内存空间。 这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它。 释放:相反, 释放时也是一样的: /// 销毁 free(pbuffer->data); free(pbuffer); pbuffer = NULL; 小结: - 使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费。 但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向struct point_buffer的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。 4、变长数据缓冲区(开辟空间, 释放, 访问) 定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢? GNU C的 0 长度数组, 也叫变长数组, 柔性数组就是这样一个扩展。对于 0 长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等: 数据结构定义: // 0长度数组 struct zero_buffer { int len; char data[0]; }; 数据结构大小:这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0];只是个数组名, 是不占用存储空间的: sizeof(struct zero_buffer) = sizeof(int) 开辟空间:那么我们使用的时候, 只需要开辟一次空间即可 /// 开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL) { zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data); } 释放空间:释放空间也是一样的, 一次释放即可 /// 销毁 free(zbuffer); zbuffer = NULL; 总结: // zero_length_array.c #include #include #define MAX_LENGTH 1024 #define CURR_LENGTH 512 // 0长度数组 struct zero_buffer { int len; char data[0]; }__attribute((packed)); // 定长数组 struct max_buffer { int len; char data[MAX_LENGTH]; }__attribute((packed)); // 指针数组 struct point_buffer { int len; char *data; }__attribute((packed)); int main(void) { struct zero_buffer *zbuffer = NULL; struct max_buffer *mbuffer = NULL; struct point_buffer *pbuffer = NULL; // ===================== // 0长度数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test1:%d\n",sizeof(struct zero_buffer)); /// 开辟 if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL) { zbuffer->len = CURR_LENGTH; memcpy(zbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", zbuffer->len, zbuffer->data); } /// 销毁 free(zbuffer); zbuffer = NULL; // ===================== // 定长数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test2:%d\n",sizeof(struct max_buffer)); /// 开辟 if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL) { mbuffer->len = CURR_LENGTH; memcpy(mbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", mbuffer->len, mbuffer->data); } /// 销毁 free(mbuffer); mbuffer = NULL; // ===================== // 指针数组 占用-开辟-销毁 // ===================== /// 占用 printf("the length of struct test3:%d\n",sizeof(struct point_buffer)); /// 开辟 if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL) { pbuffer->len = CURR_LENGTH; if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL) { memcpy(pbuffer->data, "Hello World", CURR_LENGTH); printf("%d, %s\n", pbuffer->len, pbuffer->data); } } /// 销毁 free(pbuffer->data); free(pbuffer); pbuffer = NULL; return EXIT_SUCCESS; } 长度为 0 的数组并不占有内存空间, 而指针方式需要占用内存空间. 对于长度为 0 数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 在申请空间时需分别进行, 释放时也需分别释放. 对于长度为 0 的数组的访问可采用数组方式进行 GNU Document中 变长数组的支持: 参考: 6.17 Arrays of Length Zero C Struct Hack – Structure with variable length array 在C90之前, 并不支持 0 长度的数组, 0 长度数组是GNU C的一个扩展, 因此早期的编译器中是无法通过编译的;对于GNU C增加的扩展,GCC提供了编译选项来明确的标识出他们: -pedantic选项,那么使用了扩展语法的地方将产生相应的警告信息 -Wall使用它能够使GCC产生尽可能多的警告信息 -Werror, 它要求GCC将所有的警告当成错误进行处理 // 1.c #include #include int main(void) { char a[0]; printf("%ld", sizeof(a)); return EXIT_SUCCESS; } 我们来编译: # 显示所有警告 gcc 1.c -Wall #none warning and error # 对GNU C的扩展显示警告 gcc 1.c -Wall -pedantic 1.c: In function ‘main’: 1.c:7: warning: ISO C forbids zero-size array ‘a’ # 显示所有警告同时GNU C的扩展显示警告, 将警告用 error 显示 gcc 1.c -Werror -Wall -pedantic cc1: warnings being treated as errors 1.c: In function ‘main’: 1.c:7: error: ISO C forbids zero-size array ‘a’ 0长度数组其实就是灵活地运用数组指向的是其后面连续的内存空间: struct buffer { int len; char data[0]; }; 在早期没引入 0 长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是: 定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费 指针的方式, 要求程序员在释放空间时必须进行多次的free操作, 而我们在使用的过程中往往在函数中返回了指向缓冲区的指针, 我们并不能保证每个人都理解并遵从我们的释放方式。 所以GNU就对其进行了 0 长度数组的扩展. 当使用data[0]的时候, 也就是 0 长度数组的时候,0长度数组作为数组名, 并不占用存储空间。 在C99之后,也加了类似的扩展,只不过用的是char payload[]这种形式(所以如果你在编译的时候确实需要用到-pedantic参数,那么你可以将char payload[0]类型改成char payload[], 这样就可以编译通过了,当然你的编译器必须支持C99标准的,如果太古老的编译器,那可能不支持了) // 2.c payload #include # include struct payload { int len; char data[]; }; int main(void) { struct payload pay; printf("%ld", sizeof(pay)); return EXIT_SUCCESS; } 使用-pedantic编译后, 不出现警告, 说明这种语法是 C 标准的 gcc 2.c -pedantic -std=c99 所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式,并且最后一个成员变量,也就刚好是数据内容了. GNU 手册还提供了另外两个结构体来说明,更容易看懂意思: struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; 我把 f2 里面的 2,3,4 改成了 5,6,7 以示区分。如果你把数据打出来。即如下的信息: f1.x = 1 f1.y[0] = 2 f1.y[1] = 3 f1.y[2] = 4 也就是f1.y指向的是{2,3,4}这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y指向的数据也就是正好f2.data的内容了。打印出来的数据: f2.f1.x = 1 f2.f1.y[0] = 5 f2.f1.y[1] = 6 f2.f1.y[2] = 7 如果你不是很确认其是否占用空间. 你可以用sizeof来计算一下。就可以知道sizeof(struct f1)=4,也就是int y[]其实是不占用空间的。但是这个 0 长度的数组,必须放在结构体的末尾。如果你没有把它放在末尾的话。编译的时候,会有如下的错误: main.c:37:9: error: flexible array member not at end of struct int y[]; ^ 到这边,你可能会有疑问,如果将struct f1中的int y[]替换成int *y,又会是如何?这就涉及到数组和指针的问题了. 有时候吧,这两个是一样的,有时候又有区别。 首先要说明的是,支持 0 长度数组的扩展,重点在数组,也就是不能用int *y指针来替换。sizeof的长度就不一样了。把struct f1改成这样: struct f3 { int x; int *y; }; 在 32/64 位下, int 均是 4 个字节,sizeof(struct f1)=4,而sizeof(struct f3)=16 因为int *y是指针, 指针在 64 位下, 是 64 位的,sizeof(struct f3) = 16;如果在32位环境的话,sizeof(struct f3)则是 8 了,sizeof(struct f1)不变. 所以int *y是不能替代int y[]的; 代码如下: // 3.c #include #include struct f1 { int x; int y[]; } f1 = { 1, { 2, 3, 4 } }; struct f2 { struct f1 f1; int data[3]; } f2 = { { 1 }, { 5, 6, 7 } }; struct f3 { int x; int *y; }; int main(void) { printf("sizeof(f1) = %d\n", sizeof(struct f1)); printf("sizeof(f2) = %d\n", sizeof(struct f2)); printf("szieof(f3) = %d\n\n", sizeof(struct f3)); printf("f1.x = %d\n", f1.x); printf("f1.y[0] = %d\n", f1.y[0]); printf("f1.y[1] = %d\n", f1.y[1]); printf("f1.y[2] = %d\n", f1.y[2]); printf("f2.f1.x = %d\n", f1.x); printf("f2.f1.y[0] = %d\n", f2.f1.y[0]); printf("f2.f1.y[1] = %d\n", f2.f1.y[1]); printf("f2.f1.y[2] = %d\n", f2.f1.y[2]); return EXIT_SUCCESS; } 0 长度数组的其他特征: 1、为什么 0 长度数组不占用存储空间: 0 长度数组与指针实现有什么区别呢, 为什么0长度数组不占用存储空间呢? 其实本质上涉及到的是一个 C 语言里面的数组和指针的区别问题。char a[1]里面的a和char *b的b相同吗? 《 Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说: “arr is defined to be identical to &arr[0]”. 也就是说,char a[1]里面的a实际是一个常量,等于&a[0]。而char *b是有一个实实在在的指针变量b存在。所以,a=b是不允许的,而b=a是允许的。两种变量都支持下标式的访问,那么对于a[0]和b[0]本质上是否有区别?我们可以通过一个例子来说明。 参见如下两个程序gdb_zero_length_array.c和gdb_zero_length_array.c: // gdb_zero_length_array.c #include #include struct str { int len; char s[0]; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if(f.a->s) { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n"); } return EXIT_SUCCESS; } \ // gdb_pzero_length_array.c #include # include struct str { int len; char *s; }; struct foo { struct str *a; }; int main(void) { struct foo f = { NULL }; printf("sizeof(struct str) = %d\n", sizeof(struct str)); printf("before f.a->s.\n"); if (f.a->s) { printf("before printf f.a->s.\n"); printf(f.a->s); printf("before printf f.a->s.\n"); } return EXIT_SUCCESS; } 可以看到这两个程序虽然都存在访问异常, 但是段错误的位置却不同 我们将两个程序编译成汇编, 然后diff查看他们的汇编代码有何不同 gcc -S gdb_zero_length_array.c -o gdb_test.s gcc -S gdb_pzero_length_array.c -o gdb_ptest diff gdb_test.s gdb_ptest.s 1c1 < .file "gdb_zero_length_array.c" --- > .file "gdb_pzero_length_array.c" 23c23 < movl $4, %esi --- > movl $16, %esi 30c30 < addq $4, %rax --- > movq 8(%rax), %rax 36c36 < addq $4, %rax --- > movq 8(%rax), %rax # printf("sizeof(struct str) = %d\n", sizeof(struct str)); 23c23 < movl $4, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); --- > movl $16, %esi #printf("sizeof(struct str) = %d\n", sizeof(struct str)); 从 64 位系统中, 汇编我们看出, 变长数组结构的大小为 4, 而指针形式的结构大小为 16: f.a->s 30c30/36c36 < addq $4, %rax --- > movq 8(%rax), %rax 可以看到有: 对于char s[0]来说, 汇编代码用了addq指令,addq $4, %rax 对于char *s来说,汇编代码用了movq指令,movq 8(%rax), %rax addq对%rax + sizeof(struct str), 即str结构的末尾即是char s[0]的地址, 这一步只是拿到了其地址, 而movq则是把地址里的内容放进去, 因此有时也被翻译为leap指令, 参见下一例子 从这里可以看到, 访问成员数组名其实得到的是数组的相对地址, 而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的): 访问相对地址,程序不会crash,但是,访问一个非法的地址中的内容,程序就会crash。 // 4-1.c #include #include int main(void) { char *a; printf("%p\n", a); return EXIT_SUCCESS; } 4-2.c #include #include int main(void) { char a[0]; printf("%p\n", a); return EXIT_SUCCESS; } $ diff 4-1.s 4-2.s 1c1 < .file "4-1.c" --- > .file "4-2.c" 13c13 < subl $16, %esp --- > subl $32, %esp 15c15 < leal 16(%esp), %eax --- > movl 28(%esp), %eax 对于char a[0]来说, 汇编代码用了leal指令,leal 16(%esp), %eax: 对于char *a来说,汇编代码用了movl指令,movl 28(%esp), %eax 2、地址优化: // 5-1.c #include #include int main(void) { char a[0]; printf("%p\n", a); char b[0]; printf("%p\n", b); return EXIT_SUCCESS; } img 由于 0 长度数组是 GNU C 的扩展, 不被标准库任可, 那么一些巧妙编写的诡异代码, 其执行结果就是依赖于编译器和优化策略的实现的. 比如上面的代码, a 和 b 的地址就会被编译器优化到一处, 因为a[0]和b[0]对于程序来说是无法使用的, 这让我们想到了什么? 编译器对于相同字符串常量, 往往地址也是优化到一处, 减少空间占用: // 5-2.c #include #include int main(void) { const char *a = "Hello"; printf("%p\n", a); const char *b = "Hello"; printf("%p\n", b); const char c[] = "Hello"; printf("%p\n", c); return EXIT_SUCCESS; }
1、程序框架的重要性 很多人尤其是初学者在写代码的时候往往都是想一点写一点,最开始没有一个整体的规划,导致后面代码越写越乱,bug不断。 最终代码跑起来看似没有问题(有可能也真的没有问题),但是要加一个功能的时候会浪费大量的时间,甚至导致整个代码的崩溃。 所以,在一个项目开始的时候多花一些时间在代码的架构设计上是十分有必要的。 代码架构确定好了之后你会发现敲代码的时候会特别快,并且在后期调试的时候也不会像无头苍蝇一样胡乱找问题。当然,调试也是一门技术。 在学习实时操作系统的过程中,发现实时操作系统框架与个人的业务代码之间的耦合性就非常低,都是只需要将业务代码通过一定的接口函数注册好后就交给操作系统托管了,十分方便。 但是操作系统的调度过于复杂,这里就使用操作系统的思维方式来重构这个时间片轮询框架。 实现该框架的完全解耦,用户只需要包含头文件,并且在使用过程中不需要改动已经写好的库文件。 2、程序实例 首先来个demo,该demo是使用电脑开两个线程: 一个线程模拟单片机的定时器中断产生时间片轮询个时钟,另一个线程则模拟主函数中一直运行的时间片轮询调度程序。 #include #include #include #include "timeslice.h" // 创建5个任务对象TimesilceTaskObj task_1, task_2, task_3, task_4, task_5; // 具体的任务函数void task1_hdl(){ printf(">> task 1 is running ...\n");} void task2_hdl(){ printf(">> task 2 is running ...\n");} void task3_hdl(){ printf(">> task 3 is running ...\n");} void task4_hdl(){ printf(">> task 4 is running ...\n");} void task5_hdl(){ printf(">> task 5 is running ...\n");} // 初始化任务对象,并且将任务添加到时间片轮询调度中void task_init(){ timeslice_task_init(&task_1, task1_hdl, 1, 10); timeslice_task_init(&task_2, task2_hdl, 2, 20); timeslice_task_init(&task_3, task3_hdl, 3, 30); timeslice_task_init(&task_4, task4_hdl, 4, 40); timeslice_task_init(&task_5, task5_hdl, 5, 50); timeslice_task_add(&task_1); timeslice_task_add(&task_2); timeslice_task_add(&task_3); timeslice_task_add(&task_4); timeslice_task_add(&task_5);} // 开两个线程模拟在单片机上的运行过程void timeslice_exec_thread(){ while (true) { timeslice_exec(); }} void timeslice_tick_thread(){ while (true) { timeslice_tick(); Sleep(10); }} int main(){ task_init(); printf(">> task num: %d\n", timeslice_get_task_num()); printf(">> task len: %d\n", timeslice_get_task_timeslice_len(&task_3)); timeslice_task_del(&task_2); printf(">> delet task 2\n"); printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2)); printf(">> task num: %d\n", timeslice_get_task_num()); timeslice_task_del(&task_5); printf(">> delet task 5\n"); printf(">> task num: %d\n", timeslice_get_task_num()); printf(">> task 3 is exist: %d\n", timeslice_task_isexist(&task_3)); timeslice_task_add(&task_2); printf(">> add task 2\n"); printf(">> task 2 is exist: %d\n", timeslice_task_isexist(&task_2)); timeslice_task_add(&task_5); printf(">> add task 5\n"); printf(">> task num: %d\n", timeslice_get_task_num()); printf("\n\n========timeslice running===========\n"); std::thread thread_1(timeslice_exec_thread); std::thread thread_2(timeslice_tick_thread); thread_1.join(); thread_2.join(); return 0;} 运行结果如下: 由以上例子可见,这个框架使用十分方便,甚至可以完全不知道其原理,仅仅通过几个简单的接口就可以迅速创建任务并加入到时间片轮询的框架中,十分好用。 3、时间片轮询框架 其实该部分主要使用了面向对象的思维,使用结构体作为对象,并使用结构体指针作为参数传递,这样作可以节省资源,并且有着极高的运行效率。 其中最难的部分是侵入式链表的使用,这种链表在一些操作系统内核中使用十分广泛,这里是参考RT-Thread实时操作系统中的侵入式链表实现。h文件: #ifndef _TIMESLICE_H#define _TIMESLICE_H #include "./list.h" typedef enum { TASK_STOP, TASK_RUN} IsTaskRun; typedef struct timesilce{ unsigned int id; void (*task_hdl)(void); IsTaskRun is_run; unsigned int timer; unsigned int timeslice_len; ListObj timeslice_task_list;} TimesilceTaskObj; void timeslice_exec(void);void timeslice_tick(void);void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len);void timeslice_task_add(TimesilceTaskObj* obj);void timeslice_task_del(TimesilceTaskObj* obj);unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj);unsigned int timeslice_get_task_num(void);unsigned char timeslice_task_isexist(TimesilceTaskObj* obj); #endif c文件: #include "./timeslice.h" static LIST_HEAD(timeslice_task_list); void timeslice_exec(){ ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (task->is_run == TASK_RUN) { task->task_hdl(); task->is_run = TASK_STOP; } }} void timeslice_tick(){ ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (task->timer != 0) { task->timer--; if (task->timer == 0) { task->is_run = TASK_RUN; task->timer = task->timeslice_len; } } }} unsigned int timeslice_get_task_num(){ return list_len(×lice_task_list);} void timeslice_task_init(TimesilceTaskObj* obj, void (*task_hdl)(void), unsigned int id, unsigned int timeslice_len){ obj->id = id; obj->is_run = TASK_STOP; obj->task_hdl = task_hdl; obj->timer = timeslice_len; obj->timeslice_len = timeslice_len;} void timeslice_task_add(TimesilceTaskObj* obj){ list_insert_before(×lice_task_list, &obj->timeslice_task_list);} void timeslice_task_del(TimesilceTaskObj* obj){ if (timeslice_task_isexist(obj)) list_remove(&obj->timeslice_task_list); else return;} unsigned char timeslice_task_isexist(TimesilceTaskObj* obj){ unsigned char isexist = 0; ListObj* node; TimesilceTaskObj* task; list_for_each(node, ×lice_task_list) { task = list_entry(node, TimesilceTaskObj, timeslice_task_list); if (obj->id == task->id) isexist = 1; } return isexist;} unsigned int timeslice_get_task_timeslice_len(TimesilceTaskObj* obj){ return obj->timeslice_len;} 4、底层侵入式双向链表 该链表是linux内核中使用十分广泛,也十分经典,其原理具体可以参考文章:https://www.cnblogs.com/skywang12345/p/3562146.htmlh文件: #ifndef _LIST_H#define _LIST_H #define offset_of(type, member) (unsigned long) &((type*)0)->member#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offset_of(type, member))) typedef struct list_structure{ struct list_structure* next; struct list_structure* prev;} ListObj; #define LIST_HEAD_INIT(name) {&(name), &(name)}#define LIST_HEAD(name) ListObj name = LIST_HEAD_INIT(name) void list_init(ListObj* list);void list_insert_after(ListObj* list, ListObj* node);void list_insert_before(ListObj* list, ListObj* node);void list_remove(ListObj* node);int list_isempty(const ListObj* list);unsigned int list_len(const ListObj* list); #define list_entry(node, type, member) \ container_of(node, type, member) #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) #endif c文件: #include "list.h" void list_init(ListObj* list){ list->next = list->prev = list;} void list_insert_after(ListObj* list, ListObj* node){ list->next->prev = node; node->next = list->next; list->next = node; node->prev = list;} void list_insert_before(ListObj* list, ListObj* node){ list->prev->next = node; node->prev = list->prev; list->prev = node; node->next = list;} void list_remove(ListObj* node){ node->next->prev = node->prev; node->prev->next = node->next; node->next = node->prev = node;} int list_isempty(const ListObj* list){ return list->next == list;} unsigned int list_len(const ListObj* list){ unsigned int len = 0; const ListObj* p = list; while (p->next != list) { p = p->next; len++; } return len;} 到此,一个全新的,完全解耦的,十分方便易用时间片轮询框架完成。
有一些人虽然工作了很多年,但工作表现就像刚入行的新人。他们几乎不学习软件开发的基础知识 。除了最初几年有所成长,后期一直停滞不前,而且他们不明白为什么。 与此同时,我也曾与一些只有几年工作经验的开发人员共事,他们表现出惊人的增长潜力。他们工作态度端正,并且明白如何避免不称职的行为。 根据开发人员的某些习惯,可以非常明显地分辨出谁更专业,谁更业余。让我们深入剖析下业余程序开发人员的几种表现,每个程序开发人员都应该引以为戒,这些错误会阻碍我们的职业发展。 一次性提交大量代码 回忆下,你是否碰到过一次性提交大量代码的人,你都不想给他做代码评审。是的,不专业的开发人员就会这样做。他们会在一次代码评审请求中包含多个模块的修改,而且会催促你优先评审他们的代码。 是啊,能不急吗,排到后边,还需要解决代码冲突的问题。这个问题在很多高级开发工程师中也存在,他们在功能开发期间不做任何提交,只有在功能彻底完工后,才会提交所有修改,于是代码评审中的任何意见都会引起大量的修改。 当我碰到这种代码评审请求时,我首先做的是要求提交者按功能模块将其拆分成多个小的请求。 我只会对 issues(任务管理系统)中的第一个功能需求评审,然后将其转回提交者。如果我有时间,我会和提交者连线进行代码实时评审。 你能做什么? 进行小的代码提交。一个好的做法是:每个工作日都进行代码提交。 不要提交没有编译或者会导致构建失败的代码。 代码写的很烂 缺乏经验的开发人员写不出漂亮的代码,他们写出的代码会很混乱,而且分布在代码库的各个部分。 当你尝试阅读这类代码时,会感觉自己身处一座迷宫之中。你会逐渐忘记自己是从什么地方开始的,要寻找什么以及这段代码完成了什么功能。 有经验的开发人员知道代码如何设计。除非要开发的功能显而易见,首先需要在纸上写出你对需求的理解并画出流程图(简化版的规格需求说明书),在脑海里对这段代码进行一个完整的构思。除非你彻底弄清楚了如何修改,否则不要开始代码编写。 如果你不遵守以上的规则,当你回顾自己完成的代码时会非常痛苦。以后如果需要修正问题或者增加功能,也会变得非常棘手。 你能做什么? 编写代码之前,对你要实现的功能有个清晰的了解。为了清楚地理解需求,你需要尽量多问问题。 让你的代码简洁而优雅。其他团队成员可以读懂代码并理解它打算做什么。 同时开展多项工作 缺乏经验的开发人员不知道什么时候开始一项任务、如何推进、什么时候结束。他们试图并行处理多项任务。他们不知道如何将一项大任务分解为小的模块,从而减轻实现的难度。 当他们收到一项任务时,并不是第一时间和上级确认需求,而是立刻就开始编程,而且在做任务期间,也不会和上级就任务进度进行沟通。 只有当任务完成时,他们才会向你反馈。到那个时候,你只能祈祷他们完成的功能就是你想要的。 缺乏经验的开发人员的另一个表现是同时推进多项任务,他们会同时处理多项事情,如:实现多个没有太大联系的功能点、解决生产环境问题、协助其他同事工作等。 最终,从他们那里得不到有效的产出。虽然他们的态度和出发点是好的,但对整个团队造成的后果是灾难性的,浪费了很多的时间,导致团队得日夜赶工。 你能做什么? 专注完成小的任务。将收到的任务分解为小块,明确需求的优先级,一小块一小块地完成。 领取一项任务,完成后再开始新的任务。 性格傲慢 对于缺乏经验的开发人员,傲慢是非常致命的。傲慢会导致他们不能接受别人的批评和建议。当你对他们的代码或者陈述给出意见时,他们会认为你是在质疑他们的能力。 许多新人由于无知,都会表现出这种傲慢。刚走出校门的他们充满自信,并没有意识到他们在学校学到的东西离社会要求还有很大差距。这些人中的聪明者会很快调整自己,以归零的心态,努力学习并适应公司文化。 其实不只是新人——一些有几年工作经验的开发人员也会表现出这种傲慢,一部分原因是其满足于个人获得的专业成就,另一部分可能的原因是其缺乏和优秀的人共事的机会,有点坐井观天。 此外,傲慢的行为也从另一方面证明这样的开发人员确实缺乏经验。这样的行为会对他们的职业发展造成很多阻碍,因为没有人喜欢和一个傲慢的人共事。当成长变慢时,他们不会从自身找原因,而是更多的归罪于别人。 你能做什么 在前行的路上保持谦卑。礼貌地对待别人会让你在软件开发职业生涯中走得更远。 尊重每一个人。出现分歧后,在你发表意见时,不管对方是什么身份,都要尊重对方。 不能从之前的错误中学到经验 我一直认为,对于软件开发人员,反馈机制是一个很有效的工具。来自他人的反馈,会让我们明白自己的短板是什么以及如何去改进。一个聪明的开发人员明白如何借助他人反馈来促进自己的成长。 根据一个开发人员对建设性意见的反应,你可以判断出他是否缺乏经验。缺乏经验的开发人员不接受任何建设性的建议,甚至代码评审中的评论,他都会认为是对他个人的一种攻击。 很多年前,我有一个同事给我写了很长的一封邮件,教我如何来评审代码,他对我给他代码的评论感到愤怒。他的主要观点是我不应该关注编码标准,因为他知道如何编码,我应该只关注代码能否满足功能需求。 如果一个开发人员因为别人对他代码给出的评论,而感觉被冒犯,只能表明他不具有真正的开发经验。他抱着做一天和尚撞一天钟的态度工作,却感慨没有遇到赏识自己的伯乐。 你能做什么? 对每个反馈保持积极的态度。对于每个反馈,你可以选择是接受还是拒绝,但拒绝之前要保持心平气和的态度。 从错误中学习。没有人能永远正确,保持终身学习才能让自己持续强大。 工作时间处理私人事务 日常工作中,总是发现团队里的一些成员在工作时间处理私人事务,如:看社交媒体,浏览购物网站,玩游戏。 我之前还有个团队成员,上班时间炒股。因为他需要不时地关注股票的 K 线走势,造成个人的产出质量不高。其他同事对他很有意见,因为他们需要花费更多的时间去赶工期。 当开发经理和这个开发人员谈话之后,他改变了一段时间,但是很快就故态复萌。最终,公司只能把他开除了。 工作时间处理私人事务,这是违反商业道德,并且表现了你的不专业。我们需要对工作敬业,毕竟我们要靠它谋生。 你能做什么? 工作时间尽量不要处理私人事务。当你需要离开几个小时去处理个人事情时,请向你的管理者请假。 使用休息时间浏览你的社交媒体。如果必须要点外卖或炒股,请利用午休时间。 盲目追逐技术潮流 开发人员缺乏经验的另一个表现是面对技术潮流的态度。你会发现他们总是在谈论技术潮流,当有一个新的潮流出现时,他们会立刻丢弃原来的潮流,投入新的怀抱。 缺乏经验的开发人员总是在学习教程。毫无疑问,教程是很有用的学习工具,但是,不进行任何实践而只是按照教程一步步操作无疑是浪费时间。 它会让你虚幻地觉得自己好像都掌握了,但是知识是否掌握了,需要通过真实的项目进行检验。 开发人员很少会用热门技术或者从教程中学到的知识来实现新的东西,他们学习热门技术或者教程很多是为了满足自己的虚荣心,或者担心自己会错过什么。 你能做什么? 花费时间和精力学习那些能在工作中或者实际项目中真正用到的技术。 从教程中学习并及时练习,相对于新手教程,自己实现一个功能能学到更多的东西。 缺乏经验的开发人员会因为自己的效率低下进而降低整个团队的效率。他对待自己工作的错误态度,会让其在职业发展中错失很多机会。 了解并避免这种错误的态度和工作方式,是聪明人的做法。如果你不幸染上了这些坏习惯,随着时间的推移,你会越来越难以摆脱。
以下是GObject的一些核心概念和使用方法。 源码:https://gitlab.gnome.org/GNOME/glib/ 教程:https://docs.gtk.org/gobject/index.html 1. GObject的核心概念 动态类型系统:GObject允许程序在运行时进行类型注册,这意味着可以使用纯C语言设计一整套面向对象的软件模块。 内存管理:GObject实现了基于引用计数的内存管理,这简化了内存管理的复杂性。 属性系统:GObject提供了通用的set/get属性获取方法,使得属性管理变得更加简单。 信号机制:GObject内置了简单易用的信号机制,允许对象之间进行通信。 2. GObject的使用示例 在GObject中,类和实例是两个结构体的组合。类结构体初始化函数一般被调用一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。 所有实例共享的数据保存在类结构体中,而对象私有的数据保存在实例结构体中。 GObject实例的结构体定义如下: typedef struct _GObject GObject; struct _GObject { GTypeInstance g_type_instance; /*< private >*/ guint ref_count; /* (atomic) */ GData *qdata; }; GObject类的结构体定义如下: struct _GObjectClass { GTypeClass g_type_class; /*< private >*/ GSList *construct_properties; /*< public >*/ /* seldom overridden */ GObject* (*constructor) (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties); /* overridable methods */ void (*set_property) (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); void (*get_property) (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); void (*dispose) (GObject *object); void (*finalize) (GObject *object); /* seldom overridden */ void (*dispatch_properties_changed) (GObject *object, guint n_pspecs, GParamSpec **pspecs); /* signals */ void (*notify) (GObject *object, GParamSpec *pspec); /* called when done constructing */ void (*constructed) (GObject *object); /*< private >*/ gsize flags; gsize n_construct_properties; gpointer pspecs; gsize n_pspecs; /* padding */ gpointer pdummy[3]; }; 以下是一个简单的示例,展示了如何创建和使用GObject实例: #include int main (int argc, char **argv) { GObject* instance1, *instance2; // 指向实例的指针 GObjectClass* class1, *class2; // 指向类的指针 instance1 = g_object_new (G_TYPE_OBJECT, NULL); instance2 = g_object_new (G_TYPE_OBJECT, NULL); g_print ("The address of instance1 is %p\n", instance1); g_print ("The address of instance2 is %p\n", instance2); class1 = G_OBJECT_GET_CLASS (instance1); class2 = G_OBJECT_GET_CLASS (instance2); g_print ("The address of the class of instance1 is %p\n", class1); g_print ("The address of the class of instance2 is %p\n", class2); g_object_unref (instance1); g_object_unref (instance2); return 0; } The address of instance1 is 0x55fb9141ad20 The address of instance2 is 0x55fb9141ad40 The address of the class of instance1 is 0x55fb9141a350 The address of the class of instance2 is 0x55fb9141a350 在这个示例中,g_object_new函数用于创建GObject实例,并返回指向它的指针。 G_TYPE_OBJECT是GObject基类的类型标识符,所有其他GObject类型都从这个基类型派生。 宏G_OBJECT_GET_CLASS返回指向参数所属类变量的指针。g_object_unref用于销毁实例变量并释放内存。 实例1与实例2的存储空间是不同的,每个实例都有自己的空间。 两个类的存储空间是相同的,两个GObject实例共享同一个类。 3. GObject的信号机制 GObject允许定义和使用属性,以及发出和连接信号。 这些特性使得GObject非常适合用于构建复杂的软件系统,尤其是在需要组件间通信和属性管理的场景中。 信号最基本的用途是实现事件通知。例如:创建一个信号,当调用文件写方法时,触发文件变化信号。 创建信号: file_signals[CHANGED] = g_signal_newv ("changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, NULL /* closure */, NULL /* accumulator */, NULL /* accumulator data */, NULL /* C marshaller */, G_TYPE_NONE /* return_type */, 0 /* n_params */, NULL /* param_types */); 带信号机制的文件写方法: void viewer_file_write (ViewerFile *self, const guint8 *buffer, gsize size) { g_return_if_fail (VIEWER_IS_FILE (self)); g_return_if_fail (buffer != NULL || size == 0); /* First write data. */ /* Then, notify user of data written. */ g_signal_emit (self, file_signals[CHANGED], 0 /* details */); } 用户回调处理函数连接到信号: g_signal_connect (file, "changed", (GCallback) changed_event, NULL); 4. 跨语言互通性 GObject被设计为可以直接使用在C程序中,并且可以封装至其他语言,如C++、Java、Ruby、Python和.NET/Mono等,这使得GObject具有很好的跨语言互通性。
一、异常处理实践在编写 C++ 代码时会遇到不可预期的错误和异常情况。为了让我们的代码更健壮和可靠,我们需要使用异常处理机制来处理这些情况。