混搭下的C与C++内存操作

源自最近遇到一个的问题,先介绍一下背景。项目中混用了C与C++编程范式,鉴于项目成员背景不一,每个模块的负责人可以自行2选1。同时为了提高效率,C范式的模块被允许使用STL库的部分容器(其实也就仅仅大量使用了vector而已)。开发环境是visual studio 2005 wiht sp1。

那么问题来了,在部分模块中,纯C结构体和包含C++类的结构体共存,但它们的内存布局是不同的,所需要的初始化方式、内存操作函数均不同(malloc、new、memset....)。

巧合的是,在vs2005下包含vector类的结构体可以使用C的内存操作函数,而不会出错。比如用malloc申请结构体内存,用memset清空整个结构体都没有问题,程序可以正确运行。所以使用C规范的模块很开心的无脑使用malloc,且都是全局变量也不涉及free的问题。

悲剧的是因为外部原因,开发环境要升级到vs2010,上面的巧合不复存在,程序会崩溃,并且因为各模块大量使用了memset的方式来初始化包含vector的结构体,还需要解决这类结构体的初始化问题,比如非vector的结构体成员要求清零。

从程序的角度来说,这是一个POD问题,以下是维基百科的POD介绍:Plain old data structure, 缩写为POD, 是C++语言的标准中定义的一类数据结构,POD适用于需要明确的数据底层操作的系统中。POD通常被用在系统的边界处,即指不同系统之间只能以底层数据的形式进行交互,系统的高层逻辑不能互相兼容。比如当对象的字段值是从外部数据中构建时,系统还没有办法对对象进行语义检查和解释,这时就适用POD来存储数据。

简单来说就是POD类型在源代码兼容于ANSI C时非常重要。POD对象与C语言的对应对象具有共同的一些特性,包括初始化、复制、内存布局、寻址。

我们的目标不仅仅是清除现有隐患,而且要建立一个机制避免后续类似问题,因为一旦将来有人误用内存操作,由于错误地点和崩溃地点完全不同,定位会非常麻烦。总的来说有以下几点需求:

  1. 清理现有的POD内存申请和释放操作,并为POD内存申请提供检查机制,在误用POD内存申请的时候报错,比如用malloc申请了非POD内存。
  2. 清理现有的memset操作,并为memset提供检查机制,不允许对非POD内存执行memset操作。
  3. 支持对非POD内存块的内存清零操作

具体操作的思路是清理所有使用malloc的地方,替换为新封装的内存申请函数,然后回归测试,失败的地方肯定就是有非法POD操作,视具体情况逐个解决即可。

  • C内存布局(POD类型)的操作

1)动态申请和释放

封装的MemAlloc函数会检查申请的类型是否符合POD要求。释放操作比较简单,封装的MemFree函数直接调用C语言的free。

在vs2005和2008的标准库中没有is_pod函数,此时可以使用boost库的is_pod函数替代。

template<typename T>  
T* MemAlloc(size_t a = 1)  
{     
    assert(std::is_pod<T>::value == true && "MemAlloc POD error");  
    T* mem = (T*)malloc(a*sizeof(T));  
    return mem;  
}

例:
TEST_STRU* pPdu = MemAlloc<TEST_STRU>(); //申请单个MAC_PDU内存块
TEST_STRU* pPduS = MemAlloc<TEST_STRU>(10); //申请10个MAC_PDU内存块
MemFree(pPdu);

MemFree(pPduS);

2)内存置位操作

用封装的置位函数替换标准memset函数,新函数会检查操作的类型是否符合POD要求。采用宏替换,简单粗暴,注意控制宏的生效范围。

template<typename T>  
void pod_memset(T* p,int val, size_t size)  
{  
    assert(std::is_pod<T>::value== true && "pod_memset POD error");
    ::memset(p, val, size);  
}  
#define memset pod_memset

  • C++内存布局(非POD类型)的操作

1)动态申请与释放

对于需要内存清零的申请操作,分别被封装为MemNew函数和MemNewMulti函数,函数中使用了不太常用的operator new和placement new,相关知识点这里就不介绍了。相应的释放函数为MemDel和MemDelMulti。

对于不需要内存清零的动态内存操作,请使用语言自带的new和delete。

template<typename T>  
T* MemNew()  
{  
    T *p = (T*)operator new(sizeof(T));  
    ::memset(p,0,sizeof(T));  
    new (p) T;    
    return p;  
}  
  
template<typename T>  
void MemDel(T* p)  
{  
    p->~T();  
    operator delete(p);  
}  
  
template<typename T>  
T* MemNewMulti(size_t cnt)  
{  
    T *p = (T*)operator new(sizeof(T)*cnt);  
    ::memset(p,0,sizeof(T)*cnt);  
    for(size_t i=0; i < cnt; ++i)  
    {  
        new (&p[i]) T;   
    }  
    return p;  
}  
  
template<typename T>  
void MemDelMulti(T* p, size_t cnt)  
{  
    for(size_t i=0; i < cnt; ++i)  
    {  
        p[i].~T();  
    }  
  
    operator delete( p);  
}

例:
TEST_STRUCT* pUser = MemNew<TEST_STRUCT>(); //申请单个TEST_STRUCT内存块,并清零
TEST_STRUCT* pUsers = MemNewMulti<TEST_STRUCT>(3); //申请3个TEST_STRUCT内存块,并清零
MemDel(pUser);
MemDelMulti(pUsers, 3); //释放3个USER内存块,注意需要额外输入释放的个数

2)内存置位操作

因为上面已经禁用了非POD的置位操作,误用替换后的memset会触发断言保护。

对于动态申请的非POD类型的内存清零用上面的申请函数即可,对于栈上定义的非POD结构体可以用以下方式来完成结构体成员的清零操作。

//假设parent下的ueInfo是包含C++类成员的结构体(TEST_STRUCT),需要将其中的其他POD成员初始化为0 
TEST_STRUCT temp={0}; //临时变量,要求保证TEST_STRUCT的第一个成员可以用0进行初始化,如果第一个成员也是结构体或者是数组,可以写成{{0}} 
parent.ueInfo = temp;

时间: 2024-11-01 11:46:58

混搭下的C与C++内存操作的相关文章

linux下查找进程及终止进程操作的相关命令

使用linux操作系统,难免遇到一些软件"卡壳"的问题,这时就需要使用linux下强大的kill命令来结束相关进程.这在linux系统下是极其容易的事情,你只需要kill xxx即可,这里xxx代表与此软件运行相关的进程PID号.首先,我们需要使用linux下另外一个命令ps查找与进程相关的PID号:ps aux | grep program_filter_word1)ps a 显示现行终端机下的所有程序,包括其他用户的程序.2)ps -A 显示所有程序.3)ps c 列出程序时,显示

【转】《深入理解计算机系统》C程序中常见的内存操作有关的典型编程错误

原文地址:http://blog.csdn.net/slvher/article/details/9150597 对C/C++程序员来说,内存管理是个不小的挑战,绝对值得慎之又慎,否则让由上万行代码构成的模块跑起来后才出现内存崩溃,是很让人痛苦的.因为崩溃的位置在时间和空间上,通常是在距真正的错误源一段距离之后才表现出来.前几天线上模块因堆内存写越界1个字节引起各种诡异崩溃,定位问题过程中的折腾仍历历在目,今天读到<深入理解计算机系统>第9章-虚拟存储器,发现书中总结了C程序中常见的内存操作有

Java并发编程--7.Java内存操作总结

主内存和工作内存 工作规则 Java内存模型, 定义变量的访问规则, 即将共享变量存储到内存和取出内存的底层细节  所有的变量都存储在主内存中,每条线程有自己的工作内存,工作内存中用到的变量, 是从主内存拷贝的副本,线程对变量的所有操作都在工作内存中进行, 线程间变量值得传递均需通过主内存来完成 内存间交互操作 1.luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态. 2.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其

Java API —— IO流(数据操作流 &amp; 内存操作流 &amp; 打印流 &amp; 标准输入输出流 &amp; 随机访问流 &amp; 合并流 &amp; 序列化流 &amp; Properties &amp; NIO)

1.操作基本数据类型的流 1) 操作基本数据类型 · DataInputStream:数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型.应用程序可以使用数据输出流写入稍后由数据输入流读取的数据. · DataOutputStream:数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中.然后,应用程序可以使用数据输入流将数据读入. package datastreamdemos; import java.io.*; /** * Created b

Windows下使用WSRM限制MongoDB内存

有个项目用到了MongoDB,我们是在WINDOWS 2008 64位环境下部署的,为啥不部署到linux下面呢,我们没那么多服务器,只能将就一下了. 大家都知道Mongodb吃内存太厉害了,如果不重启服务,内存一直蹭蹭地往上涨,定时重启MongoDB服务是能暂时的收回内存,但这也不是长久之计.如果不去限制MongoDB的内存那么系统有多少内存都能被它消耗掉,我们的服务器上还有IIS, SQL SERVER, Redis等其他服务,不能将内存全部分配给Mongodb使用,怎样限制MongoDB的

Java基础系列10:内存操作流,管道流,合并流,压缩流以及回退流

前言:这篇文章将对几个"非主流"的IO流进行简单的介绍 一 内存操作流 内存操作流的主要作用是完成内存的输入和输出.比如说在某些情况下需要生成一些临时信息,而将这些临时信息保存在文件中不仅要进行文件的读写而且在功能完成之后还需要删除这个临时文件,因此比较麻烦,这时或许就需要用到内存操作流了. 需要用到的API是:ByteArrayInputStream和ByteArrayOutputStream,分别表示输入流和输出流,示例代码如下: package javase.io; import

Linux下的内核编译与模块操作

Linux下的内核编译与模块操作 一:实验环境 1):虚拟机 2):linux系统 3):linux系统的硬盘的空余空间要大于7G 4):虚拟机的内存要大于2.5G以上 二:实验目标 1):源码编译Linux内核 2):使用Linux内核模块 3):实战-编译一个NTFS内核模块,实现linux挂载NTFS文件系统并实现读写功能 三:实验脚本 第一块 --源码编译Linux内核 linux系统与windows系统是两种截然不同的系统,windows系统中的软件都是需要付费的,而linux系统中的

Marshal 类的内存操作的一般功能

Marshal类 提供了一个方法集,这些方法用于分配非托管内存.复制非托管内存块.将托管类型转换为非托管类型,此外还提供了在与非托管代码交互时使用的其他杂项方法. 命名空间:System.Runtime.InteropServices Marshal 类中定义的 static 方法对于处理非托管代码至关重要.此类中定义的大多数方法通常由需要在托管和非托管编程模型之间提供桥梁的开发人员使用. 例如,StringToHGlobalAnsi 方法将 ANSI 字符从指定的字符串(在托管堆中)复制到非托

Spiceserver内存操作相关函数的使用

Spiceserver内存操作部分也是整个项目用的比较多的模块,它自己封装了一套内存操作的函数,要掌握Spiceserver必须要掌握这些函数的使用,本篇我主要介绍一下我在阅读和使用这些函数及宏的一些理解,可能不是很全面,甚至理解不是很到位,后面有新的理解和发现再对blog进行更新. 内存操作底层相关函数 void *spice_malloc(size_t n_bytes) SPICE_GNUC_MALLOC SPICE_GNUC_ALLOC_SIZE(1); void *spice_mallo