T. W. Burger (twburger@bigfoot.com), 老板, Thomas Wolfgang Burger Consulting 公司
2000 年 9 月 01 日
您是否做过这样一个项目,它要求您在内存中保存数目不定的若干不同对象?对于某些情况,二叉树是最佳选择,但在通常情况下,更简单的链表是显而易见的选择。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
一个简化的问题示例
链表的难点在于必须复制链表处理函数来处理不同的对象,即便逻辑是完全相同的。例如:
两个结构类似的链表
struct Struct_Object_A { int a; int b; Struct_Object_A *next; } OBJECT_A; typedef struct Struct_Object_B { int a; int b; int c; Struct_Object_B *next; } OBJECT_B;
|
上面定义的两个结构只有很小的一点差别。OBJECT_B 和 OBJECT_A 之间只差一个整型变量。但是,在编译器看来,它们仍然是非常不同的。必须为存储在链表中的每个对象复制用来添加、删除和搜索链表的函数。为了解决这个问题,可以使用具有全部三个变量的一个联合或结构,其中整数 c 并不是在所有的情况下都要使用。这可能变得非常复杂,并会形成不良的编程风格。
C 代码解决方案:虚拟链表
此问题更好的解决方案之一是虚拟链表。虚拟链表是只包含链表指针的链表。对象存储在链表结构背后。这一点是这样实现的,首先为链表节点分配内存,接着为对象分配内存,然后将这块内存分配给链表节点指针,如下所示:
虚拟链表结构的一种实现
typedef struct liststruct { liststruct *next; } LIST, *pLIST; pLIST Head = NULL; pLIST AddToList( pLIST Head, void * data, size_t datasize ) { pLIST newlist=NULL; void *p; // 分配节点内存和数据内存 newlist = (pLIST) malloc( datasize + sizeof( LIST ) ); // 为这块数据缓冲区指定一个指针 p = (void *)( newlist + 1 ); // 复制数据 memcpy( p, data, datasize ); // 将这个节点指定给链表的表头 if( Head ) { newlist->next = Head; } else newlist->next = NULL; Head = newlist; return Head; }
|
链表节点现在建立在数据值副本的基本之上。这个版本能很好地处理标量值,但不能处理带有用 malloc 或 new 分配的元素的对象。要处理这些对象,LIST 结构需要包含一个一般的解除函数指针,这个指针可用来在将节点从链表中删除并解除它之前释放内存(或者关闭文件,或者调用关闭方法)。
一个带有解除函数的链表
typedef void (*ListNodeDestructor)( void * ); typedef struct liststruct { ListNodeDestructor DestructFunc; liststruct *next; } LIST, *pLIST; pLIST AddToList( pLIST Head, void * data, size_t datasize, ListNodeDestructor Destructor ) { pLIST newlist=NULL; void *p; // 分配节点内存和数据内存 newlist = (pLIST) malloc( datasize + sizeof( LIST ) ); // 为这块数据缓冲区指定一个指针 p = (void *)( newlist + 1 ); // 复制数据 memcpy( p, data, datasize ); newlist->DestructFunc = Destructor;
// 将这个节点指定给链表的表头 if( Head ) { newlist->next = Head; } else newlist->next = NULL; Head = newlist; return Head; } void DeleteList( pLIST Head ) { pLIST Next; while( Head ) { Next = Head->next; Head->DestructFunc( (void *) Head ); free( Head ); Head = Next; } } typedef struct ListDataStruct { LPSTR p; } LIST_DATA, *pLIST_DATA; void ListDataDestructor( void *p ) { // 对节点指针进行类型转换 pLIST pl = (pLIST)p; // 对数据指针进行类型转换 pLIST_DATA pLD = (pLIST_DATA) ( pl + 1 ); delete pLD->p; } pLIST Head = NULL; void TestList() { pLIST_DATA d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "Hello" ); Head = AddToList( Head, (void *) d, sizeof( pLIST_DATA ), ListDataDestructor ); // 该对象已被复制,现在删除原来的对象 delete d; d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "World" ); Head = AddToList( Head, (void *) d, sizeof( pLIST_DATA ), ListDataDestructor ); delete d; // 释放链表 DeleteList( Head ); }
|
在每个链表节点中包含同一个解除函数的同一个指针似乎是浪费内存空间。确实如此,但只有链表始终包含相同的对象才属于这种情况。按这种方式编写链表允许您 将任何对象放在链表中的任何位置。大多数链表函数要求对象总是相同的类型或类。虚拟链表则无此要求。它所需要的只是将对象彼此区分开的一种方法。要实现这 一点,您既可以检测解除函数指针的值,也可以在链表中所用的全部结构前添加一个类型值并对它进行检测。当然,如果要将链表编写为一个 C++ 类,则对指向解除函数的指针的设置和存储只能进行一次。
C++ 解决方案:类链表
本解决方案将 CList 类定义为从 LIST 结构导出的一个类,它通过存储解除函数的单个值来处理单个存储类型。请注意添加的 GetCurrentData() 函数,该函数完成从链表节点指针到数据偏移指针的数学转换。
一个虚拟链表对象
// 定义解除函数指针 typedef void (*ListNodeDestructor)( void * ); // 未添加解除函数指针的链表 typedef struct ndliststruct { ndliststruct *next; } ND_LIST, *pND_LIST; // 定义处理一种数据类型的链表类 class CList : public ND_LIST { public: CList(ListNodeDestructor); ~CList(); pND_LIST AddToList( void * data, size_t datasize ); void *GetCurrentData(); void DeleteList( pND_LIST Head ); private: pND_LIST m_HeadOfList; pND_LIST m_CurrentNode; ListNodeDestructor m_DestructFunc; }; // 用正确的起始值构造这个链表对象 CList::CList(ListNodeDestructor Destructor) : m_HeadOfList(NULL), m_CurrentNode(NULL) { m_DestructFunc = Destructor; } // 在解除对象以后删除链表 CList::~CList() { DeleteList(m_HeadOfList); } // 向链表中添加一个新节点 pND_LIST CList::AddToList( void * data, size_t datasize ) { pND_LIST newlist=NULL; void *p; // 分配节点内存和数据内存 newlist = (pND_LIST) malloc( datasize + sizeof( ND_LIST ) ); // 为这块数据缓冲区指定一个指针 p = (void *)( newlist + 1 ); // 复制数据 memcpy( p, data, datasize ); // 将这个节点指定给链表的表头 if( m_HeadOfList ) { newlist->next = m_HeadOfList; } else newlist->next = NULL; m_HeadOfList = newlist; return m_HeadOfList; } // 将当前的节点数据作为 void 类型返回,以便调用函数能够将它转换为任何类型 void * CList::GetCurrentData() { return (void *)(m_CurrentNode+1); } // 删除已分配的链表 void CList::DeleteList( pND_LIST Head ) { pND_LIST Next; while( Head ) { Next = Head->next; m_DestructFunc( (void *) Head ); free( Head ); Head = Next; } } // 创建一个要在链表中创建和存储的结构 typedef struct ListDataStruct { LPSTR p; } LIST_DATA, *pND_LIST_DATA; // 定义标准解除函数 void ClassListDataDestructor( void *p ) { // 对节点指针进行类型转换 pND_LIST pl = (pND_LIST)p; // 对数据指针进行类型转换 pND_LIST_DATA pLD = (pND_LIST_DATA) ( pl + 1 ); delete pLD->p; } // 测试上面的代码 void MyCListClassTest() { // 创建链表类 CList* pA_List_of_Data = new CList(ClassListDataDestructor); // 创建数据对象
pND_LIST_DATA d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "Hello" ); // 创建指向链表顶部的局部指针 pND_LIST Head = NULL; //向链表中添加一些数据 Head = pA_List_of_Data->AddToList( (void *) d, sizeof( pND_LIST_DATA ) ); // 该对象已被复制,现在删除原来的对象 delete d; // 确认它已被存储 char * p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p; d = new LIST_DATA; d->p = new char[24]; strcpy( d->p, "World" ); Head = pA_List_of_Data->AddToList( (void *) d, sizeof( pND_LIST_DATA ) ); // 该对象已被复制,现在删除原来的对象 delete d; // 确认它已被存储 p = ((pND_LIST_DATA) pA_List_of_Data->GetCurrentData())->p; // 删除链表类,析构函数将删除链表 delete pA_List_of_Data; }
|
小结
从前面的讨论来看,似乎仅编写一个简单的链表就要做大量的工作,但这只须进行一次。很容易将这段代码扩充为一个处理排序、搜索以及各种其他任务的 C++ 类,并且这个类可以处理任何数据对象或类(在一个项目中,它处理大约二十个不同的对象)。您永远不必重新编写这段代码。
分享到:
相关推荐
在构建通用对象链表时,我们可以利用C/C++的模板和继承机制来实现。通过定义一个基类,包含链表节点的基本结构,然后让其他对象继承这个基类,可以创建一个能处理多种类型对象的链表。这样,尽管对象的类型各异,...
技巧:在 C-C++中如何构造通用的对象链表
复杂的C/C++声明并不是好的编程风格;这里仅仅是教你如何去理解这些声明。注意:为了保证能够在同一行上显示代码和相关注释,本文在至少1024x768分辨率的显示器上阅读。链表的难点在于必须复制链表处理函数来处理...
C++是在C的基础上发展起来的,引入了面向对象编程(OOP)的概念,如类、对象、继承、多态和封装。C++库函数在C的基础上增加了更多高级特性,如异常处理、流I/O和智能指针。C++的库函数通常在`iostream`、`fstream`等...
C++不仅继承了C的函数特性,还引入了类和对象的概念,使得函数可以作为成员函数存在于类中,增强了面向对象的编程能力。此外,C++还支持重载函数,即同一函数名可以根据不同的参数类型或数量有多个实现。 2. **c++...
C++,由Bjarne Stroustrup在1983年基于C语言发展而来,引入了面向对象编程(OOP)的概念,如类、对象、继承、多态和封装。这些特性使得C++更适合编写大型、复杂且易于维护的软件系统。C++还增加了模板、异常处理、...
### C/C++经典笔试题汇总知识点解析 #### 题目一:单向链表的反转 **知识点:** 1. **链表基础知识**:理解单向链表的基本结构(包含节点、节点间的链接关系等)。 2. **迭代反转算法**:掌握如何通过迭代方式实现...
在C/C++编程中,库函数和标准模板库(Standard Template Library, STL)是开发者不可或缺的工具。这些库提供了丰富的功能,使得程序员可以更高效、更便捷地编写代码。本资源"库函数以及文件大全(经典).chm"很可能...
在C语言中,标准库主要由ISO C99定义,它包含了多个头文件,如(I/O操作)、(字符串处理)、(数学函数)和(通用实用函数)。例如,`printf`和`scanf`用于格式化输出和输入,`strlen`用于获取字符串长度,`malloc`...
《C/C++软件编程思想策略》一书探讨的是在C/C++编程中如何采用高效、灵活的思维方式来设计和实现软件。尽管标题提及了JAVA,但主要焦点仍然是C/C++编程语言及其背后的策略。 在软件开发中,编程思想是至关重要的,...
C语言是一种静态类型、过程化编程的通用编程语言,而C++则在其基础上增加了面向对象的特性,使得程序设计更加灵活和高效。这两个语言在软件开发中都有着广泛的应用,特别是在系统编程、游戏开发、嵌入式系统等领域。...
C/C++ API参考文档是程序员在开发过程中不可或缺的资源,它涵盖了C和C++编程语言的核心元素和标准库。这份文档详细介绍了C++标准模板库(STL),C语言的关键字,预处理指令,转义字符,基本数据类型,以及ASCII码表...
C运行时库是C++中继承自C语言的部分,包含了一系列的函数,如内存管理(malloc、calloc、free等)、输入/输出(printf、scanf等)和数学运算(如sin、cos等)。这些函数在C++程序中起着基础支持的作用。 C++标准库...
C++语言支持更多的面向对象特性,因此在C++中实现设计模式更加直观和方便。以下是一些设计模式的简要介绍: - **设计模式概论**:概述了设计模式的基本概念和分类。 - **单例模式**:确保一个类只有一个实例,并...
C++则在C的基础上增加了面向对象编程(OOP)的概念,如类、对象、继承、多态和封装。这两个语言都强调内存管理,程序员需要手动分配和释放内存,这在学习过程中是非常重要的一个环节。 这些趣味小程序可能包括但不...
在C/C++编程语言的世界里,...这些都是C/C++面试中可能遇到的知识点,深入理解和熟练掌握这些概念对于在面试中脱颖而出至关重要。同时,了解最新的编程实践、设计模式以及如何解决实际问题的能力也是面试官关注的重点。
《JAVA C/C++ MFC 中文API chm》是一个综合性的编程参考资料,它涵盖了C++、C和Java这三种主流编程语言的关键API以及MFC(Microsoft Foundation Classes)库的相关知识。对于初学者和有经验的开发者来说,这份资料都...
函数在C/C++编程中扮演着核心角色,它们允许程序员将复杂任务分解为更小、更易管理的部分。函数可以接受参数,返回值,并具有局部作用域,这使得代码模块化,易于调试和重用。函数的定义、调用、参数传递、返回值...
在准备C/C++面试的过程中,理解并掌握一系列关键知识点至关重要。这份资料集合涵盖了大量面试可能会出现的问题,虽然可能有些重复,但无疑是全面复习的理想资源。以下是对这些知识点的详细阐述: 1. **基础语法**:...
在IT领域,特别是软件开发中,C和C++语言因其高效性和灵活性被广泛使用。而“算法总会 C/C++”这个主题,显然聚焦于利用这两种语言来理解和实现各种数据结构和算法。数据结构和算法是计算机科学的基础,它们是解决...