原文:技巧:在 C/C++中如何构造通用的对象链表

虚拟链表和类链表可以很好地实现这一点

您是否做过这样一个项目,它要求您在内存中保存数目不定的若干不同对象?对于某些情况,二叉树是最佳选择,但在通常情况下,更简单的链表是显而易见的选择。

一个简化的问题示例

链表的难点在于必须复制链表处理函数来处理不同的对象,即便逻辑是完全相同的。例如:

两个结构类似的链表
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++中如何构造通用的对象链表[转]的更多相关文章

  1. 【Unity3D技巧】在Unity中使用事件/委托机制(event/delegate)进行GameObject之间的通信 (二) : 引入中间层NotificationCenter

    作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 一对多的观察者模式机制有什么缺点? 想要查看 ...

  2. 在ASP.NET非MVC环境中(WebForm中)构造MVC的URL参数

    目前项目中有个需求,需要在WebForm中去构造MVC的URL信息,这里写了一个帮助类可以在ASP.NET非MVC环境中(WebForm中)构造MVC的URL信息,主要就是借助当前Http上下文去构造 ...

  3. Swift难点-继承中的构造规则实例具体解释

    关于继承中的构造规则是一个难点. 假设有问题,请留言问我. 我的Swift新手教程专栏 http://blog.csdn.net/column/details/swfitexperience.html ...

  4. java中的构造,封装

    今天给大家讲一下面向对象中的构造,封装: 构造:构造方法有以下几个特点:1.方法名和类名一致.2.无返回类型.接下来的几种构造样式,直接上代码吧: //这是一个宠物类 有一个属性:名字(name) p ...

  5. loadrunner 技巧-模拟Run Logic中的随机Action运行

    技巧-模拟Run Logic中的随机Action运行 by:授客 QQ:1033553122   可以这样做,Run-time Settings,删除Action7,然后在其它Action比如Acti ...

  6. 在ASP.NET非MVC环境中(WebForm中)构造MVC的URL参数,以及如何根据URL解析出匹配到MVC路由的Controller和Action

    目前项目中有个需求,需要在WebForm中去构造MVC的URL信息,这里写了一个帮助类可以在ASP.NET非MVC环境中(WebForm中)构造MVC的URL信息,主要就是借助当前Http上下文去构造 ...

  7. VB的一些项目中常用的通用方法-一般用于验证类

    1.VB的一些项目中常用的通用方法: ' 设置校验键盘输入值,数字 Public Function kyd(key As Integer) As Integer Dim mychar mychar = ...

  8. C++ 类的继承三(继承中的构造与析构)

    //继承中的构造与析构 #include<iostream> using namespace std; /* 继承中的构造析构调用原则 1.子类对象在创建时会首先调用父类的构造函数 2.父 ...

  9. C++ 构造中调用构造

    //构造中调用构造 #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; class Point{ ...

随机推荐

  1. type(),dir(),getattr(),hasattr(), isinstance()用法

    1.type(变量)  --->输出变量的类型int.float.str or others: 2.dir()   ----> dir() 函数不带参数时,返回当前范围内的变量.方法和定义 ...

  2. Web前端开发CSS基础

    CSS 层叠样式表(英文全称:Cascading Style Sheets),是一种用来表现HTML(标准通用标记语言的一个应用)或XML(标准通用标记语言的一个子集)等文件样式的计算机语言.CSS不 ...

  3. Codeforces 1238F. The Maximum Subtree

    传送门 考虑构造一些区间使得树尽可能的 "大" 发现这棵树最多就是一条链加上链上出去的其他边连接的点 构造的区间大概长这样(图比较丑请谅解..$qwq$,图中每一个 "└ ...

  4. Vuex是什么?

    记得去年公司招聘前端工程师的时候,我要负责准备面试题去考验面试者,让我记忆深刻的有一件事,我看大多数面试者简历上都写了熟练掌握Vuex,然而当我问起的时候,大部分回答都支支吾吾,解释不清,而当我提起与 ...

  5. 【weixin】微信支付---PC网站微信支付

    一.PC网站支付 微信支付支持完成域名ICP备案的网站接入支付功能.PC网站接入支付后,可以通过JSAPI支付或Native支付,自行开发生成二维码,用户使用微信“扫一扫”来完成支付. 二.支付产品介 ...

  6. python:常用模块 知识整理

    time模块 time.time() # 时间戳:1487130156.419527 time.strftime("%Y-%m-%d %X") #格式化的时间字符串:'2017-0 ...

  7. IPC之msgutil.c源码解读

    // SPDX-License-Identifier: GPL-2.0-or-later /* * linux/ipc/msgutil.c * Copyright (C) 1999, 2004 Man ...

  8. python常用模块:sys、os、path、setting、random、shutil

    今日内容讲了3个常用模块 一.sys模块二.os模块三.os下path模块四.random模块五.shutil模块 一.sys模块 import sys #环境变量 print(sys.path) # ...

  9. linux 重定向类型 超级块 i节点

    超级块:定义文件系统的元数据(总大小.块大小.空闲.......):在格式化的时候确定 查看超级块信息:tune2fs  -l   目录 i节点:定义文件的元数据(名称.大小.存放位置.权限.修改时间 ...

  10. js去掉url后某参数【函数封装】

    function delParam(paramKey) { var url = window.location.href; //页面url var urlParam = window.location ...