WebGL Tutorial
and more

Sturcture

撰写时间:2024-09-06

修订时间:2024-09-12

声明与定义

声明

struct Phone { char *brand; int price; }; struct Phone phone; phone.brand = "iPhone"; phone.price = 9999; printf("%s\n", phone.brand); printf("%d\n", phone.price);

使用时,应带有struct关键字。

也可以在声明类型时紧跟变量名称:

struct Phone { char *brand; int price; } phone; phone.brand = "iPhone"; phone.price = 9999;

此时,还可不用指定类型:

struct { char *brand; int price; } phone; phone.brand = "iPhone"; phone.price = 9999;

但因没有声明类型,因此以后无法利用特定类型来动态声明该类型的新的变量。

初始化

struct Phone { char *brand; int price; } Phone; struct Phone phone = {"iPhone", 9999};

初始化时,需按声明顺序来初始赋值。

也可通过指定成员的方式来初始化。

struct Person { char *name; int age; }; struct Person person = {.age=25, .name="Mike"};

这种方式无需考虑成员声明的顺序。

使用typedef定义

typedef struct { char *brand; int price; } Phone; Phone phone1 = {"iPhone", 9999}; Phone phone2 = {"HuaWei", 2999};

使用typedef来定义后,声明变量时无需引用struct关键字。

存储状况

对于下面的struct

struct Node { int n1; short int n2; } node;

先查看intshort int的字长:

printf("%zd\n", sizeof(int)); // 4 printf("%zd\n", sizeof(short int)); // 2

根据相应的字长,为node的成员赋值:

node.n1 = 0x11111111; node.n2 = 0x2222;

查看node的字长:

printf("%zd\n", sizeof(node)); // 8

打印出node的存储状况:

dump_mem(&node, 8, 1, false);

显示:

00 01 02 03 04 05 06 07 -- -- -- -- -- -- -- -- 0XB2EA37D8 11 11 11 11 22 22 00 00 ....""..

再分别打印其成员的内存状况:

dump_mem(&node.n1, 4, 1, false); dump_mem(&node.n2, 2, 1, false);

显示:

00 01 02 03 -- -- -- -- 0XB2EA37D8 11 11 11 11 .... 00 01 -- -- 0XB2EA37DC 22 22 ""

可以看出,在内部存储上,struct的成员连续存储在一起,struct的地址即是其第一个成员的地址。因此,在内部存储上,struct与数组的方式是一样的。我们称struct的内部成员为成员,称数组的内部成员为元素。

注意到node的成员的字长不一样,n1字长为4n2字长为2。这就涉及到当成员字长不一样时,struct如何为其成员分配内存地址的问题。我们为node再添加一个字长为4的成员:

struct Node { int n1; short int n2; int n3; } node; node.n1 = 0x11111111; node.n2 = 0x2222; node.n3 = 0x33333333; dump_mem(&node, sizeof(node), 1, false);

显示:

00 01 02 03 04 05 06 07 08 09 0A 0B -- -- -- -- -- -- -- -- -- -- -- -- 0XB96BE7D0 11 11 11 11 22 22 00 00 33 33 33 33 ....""..3333

发现node的总字长为12,且n3并没有直接排在n2之后,这两个地址之间空出了没有使用的2个字节。原因是,为提高寻址效率,struct在存储内部成员时自动进行了对齐alignment),从而导致struct的内部出现了未实际使用的内存空间。

如果最后一个成员的字长较短,则会导致两个相同的struct在内存地址上未能紧密排在一起。

struct Node { int n1; short int n2; } node1, node2; node1.n1 = 0x11111111; node1.n2 = 0x2222; node2.n1 = 0x33333333; node2.n2 = 0x4444; dump_mem(&node2, sizeof(node2), 2, false);

显示:

00 01 02 03 04 05 06 07 -- -- -- -- -- -- -- -- 0XB966C7D0 33 33 33 33 44 44 00 00 3333DD.. 0XB966C7D8 11 11 11 11 22 22 00 00 ....""..

因此,不管struct在内部如何对齐,我们总是可以通过sizeof操作符来求出其总字长。

当前函数的名称与返回地址

#include <stdio.h> void test() { printf("%s\n", __func__); printf("%p\n", __builtin_return_address(0)); } test(); /* "test" 0x400C75 */

除了__func__,还有:

printf("%s\n", __FUNCTION__); printf("%s\n", __FILE__); printf("%d\n", __LINE__);

在编译器内部,每行代码都有地址。

#include <stdio.h> void test() { printf("%p\n", __builtin_return_address(0)); } int main() { printf("main func address: %p\n", main); test(); } /* main func address: 0x400be8 func returned addr: 0x400c0a */

如果添加一行,则函数返回地址也会发生变化:

#include <stdio.h> void test() { printf("%p\n", __builtin_return_address(0)); } int main() { printf("main func address: %p\n", main); int a = 10; // added test(); } /* main func address: 0x400be8 func returned addr: 0x400c15 */

查看一条语句的字长:

void *get_returned_addr() { void *ptr = __builtin_return_address(0); printf("func returned address: %p\n", ptr); return ptr; } int main() { void *ptr1 = get_returned_addr(); // 0x400c26 void *ptr2 = get_returned_addr(); // 0x400c34 ptrdiff_t ptrOffset = ptr2 - ptr1; printf("%zd", ptrOffset); // 14 }

谁是调用者?

void *funcAddrBuffer[10] = {NULL}; void func1() { void *callerAddr = __builtin_return_address(0); printf("callerAddr: %p\n", callerAddr); for (int i = 0; i < 10; i++) { if (funcAddrBuffer[i] && callerAddr > funcAddrBuffer[i]) { printf("yes\n"); } } } void bridge() { func1(); } int main() { funcAddrBuffer[0] = bridge; funcAddrBuffer[1] = main; printf("func1: %p\n", func1); printf("bridge: %p\n", bridge); printf("main: %p\n", main); func1(); return 0; }

链表

定义节点

在声明struct时,成员类型可以指定自身。

struct Node { void *data; struct Node *next; }; struct Node node1, node2;

但这种方式,如前所述,无论是声明还是定义,均需使用关键字struct

struct可以提前声明,再引用。

typedef struct Node TNode; struct Node { void *data; TNode *next; }; TNode node1, node2;

这样,TNode即可成为其成员可引用自身类型的数据类型。

下面代码使用它:

int a = 10; int b = 25; node1.data = &a; node2.data = &b; node1.next = &node2; node2.next = NULL;

可改写为使用动态的内存:

int *a = malloc(sizeof(int)); int *b = malloc(sizeof(int)); node1.data = a; node2.data = b; node1.next = &node2; node2.next = NULL; free(node1.data); free(node2.data);

这种设计,成员data指向在堆上动态分配的内存地址,使用完后需要释放内存;成员next指向下一个TNode节点,可用于实现遍历算法。

digraph { rankdir=LR; node [shape=record]; heap [label="{heap area | int1 | int2}"]; node1 [label="node1 | { *data | *next}"]; node2 [label="node2 | { *data | *next}"]; node1:f0 -> heap:f0 [color = gray]; node2:f0 -> heap:f1 [color = gray]; node1:f1 -> node2; node2:f1 -> NULL [color = red]; }

这就是链表的基本原理:需要时就申请分配内存,并将前一个节点的next指向新节点。最后一个节点的next指向NULL。从而将所有的节点都能链接到一起。

忽略动态分配内存的细节,可简化为以下图表:

digraph { rankdir=LR; node [shape=record]; node1 [label="node1 | { *data | *next}"]; node2 [label="node2 | { *data | *next}"]; node1:f1 -> node2; node2:f1 -> NULL [color = red]; }

链表的职责与种类

上面实现了表示节点的TNode,该类型只负责两点:一是存储数据,二是指向下一节点。而要管理所有的节点,我们需要引入新的数据类型:链表。链表的职责在于统一管理所有节点的增删及遍历。

上面的图示是单链表,只实现了从前往后的一个方向的管理。除此之外,还可以实现双链表及循环列表。如果需要从当前节点往前访问上一节点,便可实现双链表。循环链表最后一个节点指向第一个节点。但单链表是最基本的实现。

单链表的实现

现在我们一步步实现单链表。

首先声明数据类型。

typedef struct Node TNode; struct Node { void *data; TNode *next; }; typedef struct { TNode *head; TNode *tail; } LinkedList;

LinkedList的成员,是两个类型为TNode、且分别指向链表第一个节点及最后一个节点的指针。

第二步,初始化链表。

void initLinkedList(LinkedList *pList) { pList->head = NULL; pList->tail = NULL; } void printList(const LinkedList *pList) { if (pList->head == NULL) { printf("Empty\n"); } else { } } void freeList(const LinkedList *pList) { TNode *node = pList->head; while(node != NULL) { node = node->next; } } int main() { LinkedList list; initLinkedList(&list); printList(&list); freeList(&list); }

定义了3个函数,分别用于初始化链表、打印链表,以及释放链表内存。

因涉及到动态内存的分配,因此我们需小心翼翼地跟踪内存分配的细节。我们准备使用malloc函数来申请分配内存。只要一调用此函数,则需立即考虑在合适的位置释放相应的内存。因此,在主程序的最后,我们调用freeList函数以完成内存清理工作。目前,因我们尚未实际分配内存,因此它仅是完成了最基本的遍历工作,尚未真正实现释放内存的功能。

此时状态:

digraph { rankdir=LR; node [shape=record]; linkedList [label="linkedList | *head | *tail"]; NULL [shape=point, width=0.15, fillcolor="red"]; linkedList:head, linkedList:tail -> NULL; }

下面实现添加节点的函数。

void addNode(LinkedList *pList, void *pData) { TNode *node = malloc(sizeof(TNode)); node->data = pData; node->next = NULL; if (pList->head == NULL) { pList->head = node; } else { pList->tail->next = node; } pList->tail = node; } int main() { LinkedList list; initLinkedList(&list); // codes added int data = 10; addNode(&list, &data); printList(&list); freeList(&list); }

此时内存状态如下图所示:

digraph { rankdir=LR; subgraph clusterStack { linkedList [shape=plain, fillcolor=invis, label=<
linkedList
*head
*tail
>]; localVariable [shape=plain, fillcolor=invis, label=<
data
10
>]; linkedList -> localVariable [style=invis]; label="Main Function Stack"; } subgraph clusterHeap { tnode [shape=plain, fillcolor=invis, label=<
node
*data
*next
>]; NULL [shape=point, width=0.15, fillcolor="red"]; tnode:next -> NULL [color=gray]; label="Heap"; } tnode:title -> linkedList:head [dir=back]; tnode:title -> linkedList:tail [dir=back]; tnode:data -> localVariable [color=gray]; }

addNode的主要任务,就是穿针引线。当原来的链表为空时,需要同时将headtail都指向新创建的节点。而节点与节点的关系仍旧由node来维护。

我们看到,用户所传进的参数pData,其存储在主程序的栈中,不属于在堆上动态分配的内存,因此不需要释放其内存。这里,只有node在堆上动态分配了内存,用完后需要释放内存。因此,我们需在freeList函数释放其内存。修改该函数如下:

void freeList(const LinkedList *pList) { TNode *node = pList->head; while(node != NULL) { printf("Freeing a node... \n"); TNode *currNode = node; TNode *nextNode = currNode->next; free(currNode); node = nextNode; } }

这里的要点是,在释放当前节点的内存之前,我们需要先将其所指向的下一节点的地址保存进另一指针变量中,然后再安全地释放当前节点的内存。由于while语句是根据node来遍历的,因此释放内存完毕后,应将node指向下一节点。同时,为确保程序是否清理了内存,函数中加入了打印信息的功能。

做好内存清理工作,再回头完善打印函数。

void printList(const LinkedList *pList) { if (pList->head == NULL) { printf("Empty\n"); } else { TNode *node = pList->head; while(node != NULL) { int *data = (int *)node->data; printf("%d\n", *data); node = node->next; } } }

我们看到,虽然链表被设计为可以存储任意类型的数据,但要打印这些数据时,需将它们转换回具体类型的指针才能正确打印其结果。

运行程序,终端输出:

10 Freeing a node...

添加的数据被打印了,且最后内存得到了清理。

现在,再添加第二个数据:

LinkedList list; initLinkedList(&list); int data1 = 10; addNode(&list, &data1); int data2 = 20; addNode(&list, &data2); printList(&list); freeList(&list);

内存示意图:

digraph { rankdir=LR; subgraph clusterStack { linkedList [shape=plain, fillcolor=invis, label=<
linkedList
*head
*tail
>]; localData1 [shape=plain, fillcolor=invis, label=<
data1
10
>]; localData2 [shape=plain, fillcolor=invis, label=<
data2
20
>]; linkedList -> localData1 -> localData2 [style=invis]; label="Main Function Stack"; } subgraph clusterHeap { tnode1 [shape=plain, fillcolor=invis, label=<
node1
*data
*next
>]; tnode2 [shape=plain, fillcolor=invis, label=<
node2
*data
*next
>]; NULL [shape=point, width=0.15, fillcolor="red"]; tnode1:next -> tnode2 [color=gray]; tnode2:next -> NULL [color=gray]; label="Heap"; } tnode1:title -> linkedList:head [dir=back, color=green]; tnode2:title -> linkedList:tail [dir=back, color=cyan]; tnode1:data -> localData1 [color=gray]; tnode2:data -> localData2 [color=gray]; }

这里可以更为清晰地看出,linkedList只负责维护第一个及最后一个节点,而各个节点之间的链路,则由各节点自行维护。

linkedList除了维护第一个节点之外,还有指向最后一个节点的指针,这对于在最后添加新节点是很有必要的,否则,每次添加新节点,都得从头开始遍历,顺藤摸瓜,费时费力才能定位到最后的节点。

在实现链表的过程中,我们也感受到了指针的强大威力所在。addNode函数的pData,如果将其数据类型改为int,则只能处理该数据类型,对其他的数据类型则无能为力了。而在我们将其数据类型设置为能接受任意类型的指针后,我们的链表就能动态地存储任意的数据类型。其原理是,不管是何数据类型,它们的指针的字长都是一样的。而指定并保存了指针,在需要时就可访问该指针所指向的地址并取出其数据。当然,如上所述,在具体打印数值时,需将void*指针转换为特定类型的指针。而这属于调用者的职责而非链表的职责。在下面我们将进一步通过函数指针来实现通用的打印功能。

上面的例子还有一个问题,即只能存储在函数栈帧中创建的数据,不能存储在堆上创建的数据。存在即是合理。当存在这种情况时,我们就不能在freeList函数中简单地一概释放pData的内存空间了。在上面的例子中,pData所指向的数据属于在栈中自动创建的变量,无需释放,也不能释放。同样,根据谁创建谁负责清理的原则,如果调用者需要存储在堆上动态创建的数据,则由调用者来负责释放其内存。这同样也可通过函数指针来实现。

在打印函数中加入函数指针

在上面的实现中,printList里面内容是linkedList要干的活,但它却被迫干了它不该干的活:

int *data = (int *)node->data; printf("%d\n", *data);

linkedList只负责存储所有的数据类型的数据,但它并不了解应为何数据类型。调用者负责提供数据,他才是真正的知情人。因此应由他来负责转换数据。

因此,我们需要在printList通过函数指针来实现一种回调机制,调用者向该函数提供一个函数指针,当printList遍历到相应数据后,将以此数据作为参数回调该函数。直白来说,就是"如何打印这个数据我不懂,既然你是数据提供者,就由你代办吧"。具体来说,我们需要分4步来改写上面的代码。

第一步,先声明一个函数指针。

typedef void (*PrintFuncPtr)(void *);

关键字typedef声明了一种名为PrintFuncPtr新的数据类型,它是一个函数指针,所指向的函数,没有返回值,参数类型为void *

函数原型确定后,其名字也明确了该函数指针的用途:将参数中的数据打印出来。

第二步,在printList函数中使用该函数指针。

void printList(const LinkedList *pList, PrintFuncPtr printPtr) { // modified if (pList->head == NULL) { printf("Empty\n"); } else { TNode *node = pList->head; while(node != NULL) { printPtr(node->data); // modified node = node->next; } } }

形参中添加了类型为PrintFuncPtr的函数指针,然后将数据node->data作为参数回调它。

下面轮到调用者干活了。

第三步,调用者根据PrintFuncPtr的原型来定义一个专门打印int类型的函数。

void printInt(void *data) { int *p = data; printf("%d\n", *p); }

第四步,将printInt这个函数的地址传给printList

int main() { LinkedList list; initLinkedList(&list); int data1 = 10; addNode(&list, &data1); int data2 = 20; addNode(&list, &data2); printList(&list, printInt); // modified freeList(&list); }

上面这段代码可清晰地看到,调用者原来存储的是int的数据类型,因此,由他来要求按int来打印出来,合情合理。

以后如果需要存储其他类型的数据,例如,数据类型为struct、数组等复合数据,只需再添加相应的函数再将其地址传给printList就行了。可见,函数指针的引入,为我们留出了充分的扩展接口。

仅是简单地声明并使用函数指针,并不会体现出函数指针有多大的价值。但如果在协调不同的协作者之间的职责及任务时,依据相应的设计模式,使用函数指针来编写代码,其强大威力就立即体现出来了。

C语言标准库也有许多这样的应用,较为典型的是stdlib.h中的qsort函数,我们向其传入一个规范了如何排序的函数指针,qsort函数就能依此排序规则对数组进行相应的排序。其结果就是,qsort函数能对任何数据进行排序。

在清理函数中加入函数指针

同样,链表中存储的数据是否动态分配的、是否需要释放其内存,均是调用者的职责。因此,在freeList函数中,我们也需要传入专门清理内存的函数指针。

第一步,先声明一个专用于清理内存的函数指针。

typedef void (*PrintFuncPtr)(void *); typedef void (*FreeFuncPtr)(void *); // added

第二步,在freeList函数中使用该函数指针。

void freeList(const LinkedList *pList, FreeFuncPtr freePtr) { TNode *node = pList->head; while(node != NULL) { TNode *currNode = node; TNode *nextNode = currNode->next; if (freePtr) { printf("Freeing dynamic data in node, at: %p\n", currNode->data); freePtr(currNode->data); } printf("Freeing node, at: %p\n", currNode); free(currNode); node = nextNode; } }

如前所述,如果要存储的数据是在栈上自动分配的,则不需要释放其内存。因此,上面加了一个条件语句以同时应对这两种场合。

有一点需注意,在层级上来讲,currNode持有要清理内存的变量,在释放内存时,需先释放子级的内存,再释放父级的内存。

第三步,定义一个释放内存的函数。

void freeMem(void *data) { free(data); }

函数内容这么简单,还需要大费周章来改写代码吗?有必要。你敢将free(data)直接放在原来的freeList函数中吗?不敢。因为上面的例子数据在栈中生成,因此不能释放。同样,这个决定应由调用者而不是LinkedList来作出。

第四步,将freeMem这个函数的地址传给freeList

int main() { LinkedList list; initLinkedList(&list); int data1 = 10; addNode(&list, &data1); int data2 = 20; addNode(&list, &data2); printList(&list, printInt); freeList(&list, NULL); }

对于在栈中分配的数据,不需要释放内存,因此只需将Null作为地址传递给freeList函数即可。在该函数中,只有此参数值为非空时,才会调用清理内存的函数:

if (freePtr) { ... freePtr(currNode->data); }

因为我们传入了空值,因此不会调用释放内存的函数。

下面我们可以安全地改写为使用动态分配内存的数据了。

int main() { LinkedList list; initLinkedList(&list); int *data1 = malloc(sizeof(int)); // heap data, need to be freed *data1 = 10; addNode(&list, data1); int *data2 = malloc(sizeof(int)); // heap data, need to be freed *data2 = 20; addNode(&list, data2); printList(&list, printInt); freeList(&list, freeMem); }

运行程序,终端显示:

10 20 Freeing dynamic data in node, at: 0x6000026b8040 Freeing node, at: 0x6000026b8050 Freeing dynamic data in node, at: 0x6000026b8060 Freeing node, at: 0x6000026b8070

所有动态分配的内存,最终均得到了释放。我们的程序不会造成内存泄露。我们毕竟是负责任的一方。

从上面的代码可以看出,如何解释数据,如何释放内存,均是调用者的职责。而使用了函数指针的printList函数及freeList函数提供了这样一个相互协同配合的机会:按调用者所提供的函数指针的规范来操作即可。因此调用者应承担起其相应的职责。

而另一方面,由于数据类型及动态分配的代码均集中于一个地方,作为调用者,我们能够快速地明确职责所在,因此职责履行起来也很轻松。

提炼为单独的文件

LinkedList.h文件的内容:

#ifndef linkedlist_h #define linkedlist_h #include <stdbool.h> typedef struct Node TNode; struct Node { void *data; TNode *next; }; typedef struct { TNode *head; TNode *tail; } LinkedList; typedef void (*PrintFuncPtr)(int, void *); typedef void (*FreeFuncPtr)(void *); typedef bool (*FilterFuncPtr)(void *); void initLinkedList(LinkedList *pList); void addNode(LinkedList *pList, void *pData); void addHead(LinkedList *pList, void *pData); void addTail(LinkedList *pList, void *pData); void printList(const LinkedList *pList, PrintFuncPtr printPtr); void freeList(const LinkedList *pList, FreeFuncPtr freePtr, bool isVerbose); void printInt(int, void *data); void freeMem(void *data); #endif /* linkedlist_h */

LinkedList.c文件的内容:

#include <stdio.h> #include <stdlib.h> #include "linkedlist.h" void initLinkedList(LinkedList *pList) { pList->head = NULL; pList->tail = NULL; } void printList(const LinkedList *pList, PrintFuncPtr printPtr) { if (pList->head == NULL) { printf("Empty\n"); } else { TNode *node = pList->head; int index = 0; while(node != NULL) { printPtr(index++, node->data); node = node->next; } } } void addHead(LinkedList *pList, void *pData) { TNode *node = malloc(sizeof(TNode)); node->data = pData; if (pList->head == NULL) { node->next = NULL; pList->head = node; pList->tail = node; } else { node->next = pList->head; pList->head = node; } } void addTail(LinkedList *pList, void *pData) { TNode *node = malloc(sizeof(TNode)); node->data = pData; node->next = NULL; if (pList->head == NULL) { pList->head = node; pList->tail = node; } else { pList->tail->next = node; pList->tail = node; } } void addNode(LinkedList *pList, void *pData) { TNode *node = malloc(sizeof(TNode)); node->data = pData; node->next = NULL; if (pList->head == NULL) { pList->head = node; } else { pList->tail->next = node; } pList->tail = node; } void freeList(const LinkedList *pList, FreeFuncPtr freePtr, bool isVerbose) { TNode *node = pList->head; int dataFreed = 0; int nodesFreed = 0; while(node != NULL) { TNode *currNode = node; TNode *nextNode = currNode->next; if (freePtr) { if (isVerbose) { printf("Freeing dynamic data in node, at: %p\n", currNode->data); } freePtr(currNode->data); dataFreed++; } if (isVerbose) { printf("Freeing node, at: %p\n", currNode); } free(currNode); nodesFreed++; node = nextNode; } printf("%d data are freed.\n", dataFreed); printf("%d nodes are freed.\n", nodesFreed); } void printInt(int index, void *data) { int *p = data; printf("Node %d: %d\n", index, *p); } void freeMem(void *data) { free(data); }

上面的实现,我们还添加了addHeadaddTail函数,即方便了链表的管理,同时也为下一步实现队列、栈的功能打下基础。此外,在释放内存时,freeList函数可根据需要打印出不同的信息。

我们的链表虽然目前为止仅实现了一些最基本的功能,还不够强大。但已有用武之地。在目前已经实现的基础上,我利用它来实现了一个内存自动管理的功能。详见自动内存管理

filter函数

下面的用例是在随机生成的整数中,筛选出符合特定条件的一组数字。

要点有2个。首先是通过筛选函数的指针来植入不同的筛选条件,这个前面已有涉及。其次是接收筛选结果的方式。这是此用例的重点。有两种方式。一是利用栈空间来接收函数返回的数据,二是接收在堆上动态生成的数据。

下面先实现第一种。

先看客户端代码:

void addRandInts(uint32_t upper_bound, uint32_t count) { for (int i = 1; i <= count; i++) { uint32_t *data = malloc(sizeof(unsigned int)); *data = arc4random_uniform(upper_bound) + 1; addTail(&auto_mem_list, data); } } bool filterFunc(void *data) { if (*((uint32_t *)data) >= 5) { return true; } return false; } int main() { init_mem_manager(); addRandInts(10, 5); printList(&auto_mem_list, print_uint32); const uint32_t BUFFER_SIZE = 5; uint32_t *buffer[BUFFER_SIZE]; int nums = filter(&auto_mem_list, filterFunc, (void *)buffer, BUFFER_SIZE); if (nums >= 0) { for (int i = 0; i < nums; i++) { uint32_t *value = (uint32_t *)buffer[i]; printf("%u\n", *value); } } }

生成5个整数,其范围是[1, 10]。filterFunc函数筛选出其值大于等于5的数值。而在main函数中,先在函数栈帧上开辟能存储5个元素的数组,用以接收从filter函数返回的数据。

由于C语言不会检测数组越界的问题,因此,对于专用于接收从函数返回值的数组,应同时传入数组元素个数以避免数组越界。

int filter(LinkedList *pList, FilterFuncPtr filterPtr, void *buffer[], uint32_t buffSize) { TNode *node = pList->head; uint32_t index = 0; void *localBuffer[buffSize]; while(node != NULL) { if (filterPtr(node->data)) { if (index < buffSize) { buffer[index++] = node->data; } else { printf("Buffer size too small. Failed.\n"); return -1; } } node = node->next; } return index; }

对于每个符合条件的数据,先检查是否数组越界,确定不越界后才写入传进来的buffer数组,且最后返回符合筛选条件的数量;而一旦发现数组越界,则直接返回-1

因此在客户端代码中,需根据函数返回值来判断是否成功。如果成功,才打印其结果。

这种方式有个弊端,虽然我们已经确保了不存在数组越界的风险,但为顺利接收结果,需事先声明足够大的数组空间,否则,就没有下文了。因此这种方式对客户端并不友好。

下面实现第二种方式,即接收在堆上动态生成的一组数据。由于我们已经实现了链表,因此,对于这种动态分配一组数据的情况,非常适合使用链表来存储并返回。

LinkedList *filter(LinkedList *pList, FilterFuncPtr filterPtr) { TNode *node = pList->head; LinkedList *list = NULL; while(node != NULL) { if (filterPtr(node->data)) { if (!list) { list = malloc(sizeof(list)); } addTail(list, node->data); } node = node->next; } return list; }

如果没有符合筛选条件的数据,不会申请内存空间;若有,才会为新链表申请新的内存空间。此时新链表中存储的数值,仍是原来链表中已经分配了内存空间的数据的地址,它们仍由原链表进行管理,因此不会对这些数据再次申请内存空间。

客户端调用代码如下:

int main() { init_mem_manager(); addRandInts(10, 5); printList(&auto_mem_list, print_uint32); LinkedList *list = filter(&auto_mem_list, filterFunc); if (list) { printf("result: \n"); printList(list, print_uint32); free(list); } }

这种方式,由于返回的是在堆上动态分配的数据,在使用完毕后,我们需要用时清理内存。但此时我们只需清理第二个链表自身的内存,不能清理它所存储的数据的内存。因为第二个链表中所存储的数据,指向了第一个链表,也即auto_mem_list链表所存储的数据,这些数据的内存空间会由auto_mem_list在程序退出时自动清理。

上面清理第二个链表时也可改写为:

if (list) { printf("result: \n"); printList(list, print_uint32); freeList(list, NULL, false); }

程序运行,其结果为:

Node 0: 7 Node 1: 3 Node 2: 10 Node 3: 2 Node 4: 1 result: Node 0: 7 Node 1: 10 0 data are freed. 2 nodes are freed. 5 data are freed. 5 nodes are freed. Good bye!

在随机生成的数据中,符合大于等于5的筛选条件的值共有710共两个。第一个链表list仅该链表所占用的内存空间得到了释放,而第二个链表auto_mem_list不仅释放了该链表自身的内存空间,同时也释放了该链表所存储数据所占用的内存空间。

可见,C语言给了我们很大的自由与权利,但权利与义务相对等,我们需要同时承担起相应的职责。如果我们知道我们在干什么,我们就可编写出灵活、强健、安全的代码。

虚拟函数表

#include <stdio.h> #include <stdlib.h> #include <string.h> #include "memutils.h" typedef struct _Object Object; typedef void (*ShowInfoFuncPtr)(Object *); typedef void (*SelfFuncPtr)(); struct _Object { SelfFuncPtr selfFunc; char *name; int age; ShowInfoFuncPtr showInfo; Object *this; }; typedef void (*ObjFuncPtr)(void); typedef ObjFuncPtr *PFuncPtr; typedef struct { Object *pObj; char *funcName; PFuncPtr pFuncPtr; } V_FUNC_TABLE; const uint32_t V_TABLE_BUFFER_SIZE = 10; V_FUNC_TABLE funcTable[V_TABLE_BUFFER_SIZE] = {0}; void _outerShow() { } void resolve() { printf("Hello, here\n"); } void selfFunc() { printf("in selfFuc:\n"); printf("func name: %s\n", __func__); for (int i = 0; i < V_TABLE_BUFFER_SIZE; i++) { if (funcTable[i].funcName) { if (strcmp(__func__, funcTable[i].funcName) == 0) { Object *pObj = funcTable[i].pObj; printf("pObj adress: %p\n", pObj); } } } } void show(Object *pObj) { printf("%s\n", pObj->name); printf("%d\n", pObj->age); } Object *newObj() { Object *pObj = malloc(sizeof(Object)); PFuncPtr pFuncPtr1 = malloc(sizeof(PFuncPtr)); *pFuncPtr1 = selfFunc; funcTable[0].pObj = pObj; funcTable[0].funcName = "selfFunc"; funcTable[0].pFuncPtr = pFuncPtr1; pObj->selfFunc = selfFunc; pObj->name = "Sarkuya"; pObj->age = 25; pObj->showInfo = _outerShow; pObj->this = pObj; return pObj; } void printVFuncTable() { for (int i = 0; i < V_TABLE_BUFFER_SIZE; i++) { if (funcTable[i].pObj) { PFuncPtr pFuncPtr = funcTable[i].pFuncPtr; printf("here: %p\n", pFuncPtr); printf("%p, %s, %p -> %p\n", funcTable[i].pObj, funcTable[i].funcName, pFuncPtr, *pFuncPtr); } } } void initVFuncTable() { for (int i = 0; i < V_TABLE_BUFFER_SIZE; i++) { funcTable[i].pObj = NULL; funcTable[i].funcName = NULL; funcTable[i].pFuncPtr = NULL; } } void freeObjs() { for (int i = 0; i < V_TABLE_BUFFER_SIZE; i++) { if (funcTable[i].pObj) { free(funcTable[i].pFuncPtr); free(funcTable[i].pObj); } } } int main() { initVFuncTable(); Object *pObj = newObj(); printVFuncTable(); printf("selfFunc address: %p\n", selfFunc); pObj->selfFunc(); freeObjs(); // goal: get the obj which invoke the method }

参考资源

  1. C11 Standard, §6.7.2.1, Structure and union specifiers, P112
  2. 什么叫做函数插入
  3. 百度:C自动植入执行