C++实现一个简易的内存池分配器

闲来无事突发奇想写个简单的内存分配器, 考虑到C++ 中的new 操作符可以很轻易的分配内存, delete释放也很方便,但是如果使用C++编写单片机的代码,就不能这样轻易使用new/delete了。其实基于C++的强大,完全可以自己实现new/delete的重载,使用到自己的内存分配。 使用自己的内存分配的好处是,可以在某个已知可用的内存块上随意的进行小块内存的动态分配与释放,这样的话,不仅可以是内存的使用更灵活,也更高效

此篇博文中所涉及的内存分配算法并不保证完全的绝对的高效,但是在平时动态使用内存块的时候却也能提供一定的方便(即使new/delete可用)

此内存块内存分配算法的时间与已分配内存块数目和需要分配内存块大小有关, 释放时间复杂度O(1)

struct MNode
{
    unsigned size;
    MNode *prev;
    MNode *next;
    char data[0];
};
class MMU
{
public:
    MMU(void * base, unsigned len): base((int *)base), length(len) { init();}
    void *alloc(unsigned size) {
        unsigned nsize = sizeof(MNode); //节点大小
        unsigned bsize = (size + nsize + 3) & (~0x03u);   //要分配的内存块大小, 保证四字节对齐
        MNode *ph = head;
        while(ph->next != nullptr) {  //没有到末尾则循环查找
            unsigned fsize = (unsigned long)ph->next - (unsigned long long)ph; //计算两个节点中间的空间
            fsize -= nsize + ph->size;  //除去MNode节点本身大小和分配的大小总和后剩下的空间,为空闲空间
            if(fsize > bsize) break;  //空闲空间找到了,停止循环
            ph = ph->next; // 继续找下一个区间
        }

        if(ph->next == nullptr) { //查到了末尾,表示无法再分配空间
            return nullptr;
        }

        MNode *ret = (MNode *)(((char *)ph) + nsize + ph->size);
        ret->size = size;
        ret->prev = ph;
        ret->next = ph->next;

        ret->next->prev = ret;
        ret->prev->next = ret;

        ++(*base);
        return ret->data;
    }

    void free(void *ptr) {
        MNode *node = (MNode *)((char *)ptr - sizeof(MNode));
        if(node <= head || node >= tail) { //如果内存数据不在管理范围内, 跳过
            return;
        }
        if(node->prev->next == node->next
            && node->next->prev == node->prev) { // 如果已经释放过, 跳过
            return;
        } 
        node->prev->next = node->next;
        node->next->prev = node->prev;
        --(*base);
    }
private:
    void init() {
        *base = 0;
        head = (MNode *)(base + 1);
        tail = (MNode *)(base + length / sizeof(int)) - 1;

        head->size = 0;
        head->next = tail;
        head->prev = nullptr;

        tail->size = 0;
        tail->prev = head;
        tail->next = nullptr;
    }



private:
    int *base;
    unsigned length;
    MNode *head;
    MNode *tail;
};

以下是测试代码

int buffer[4096];
int main(int argc, char **argv)
{

    MMU mmu(buffer, 4096 * sizeof(int));
    void *memory = mmu.alloc(64);
    std::cout << "新分配地址:" << memory << std::endl;
    void *memory1 = mmu.alloc(16);
    std::cout << "新分配地址:" << memory1 << std::endl;

    mmu.free(memory);
    void *memory2 = mmu.alloc(20);
    void *memory3 = mmu.alloc(16);
    std::cout << "新分配地址:" << memory2 << std::endl;
    std::cout << "新分配地址:" << memory3 << std::endl;
    mmu.free(memory2);
    mmu.free(memory3);
    mmu.free(memory1);
    return 0;
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
许多开发者对C/C++语言及其底层原理掌握不牢固,在面试过程中经常漏洞百出,无法取得好成绩。而招聘单位为了得到高素质的员工往往采用各种形式的面试考察求职者,这让面试难度大大增加。求职者要想成功应聘,不仅需要扎实的基本功,还需要经受情商和智商的考验。 本书通过380余个面试题,对企业招聘C/C++程序员需要掌握的知识进行了系统、全面的总结,以帮助读者进行充分的面试准备,在激烈的竞争中成功应聘。本书内容大多取材于各大IT公司的面试题,详细分析了应聘C/C++程序员职位的常见考点。本书主要内容包括:面试流程及准备、英语面试、电话面试、C/C++语言基础、流程控制、输入/输出、预处理、内存管理、指针、面向对象基础、类、多态、继承、函数、模板与STL、数据结构、软件工程、数据库、操作系统、计算机网络、经典算法题、数据库操作题、思维拓展、文字解答实战题、程序改错实战题、编写程序实战题、智力测试、逻辑测试等。 本书附带1张光盘,内容为本书所有面试题的多媒体教学视频及其他学习资料。 本书中的面试题不但以实例代码的形式对答案进行了详细解析,还对问题的相关知识点进行了扩展说明。希望通过本书,读者可以成功应聘,并提升综合素质。本书适合应聘C/C++职位的程序员阅读,也适合其他程序员作为拓展读物进行阅读。 目录: 第1篇 求职准备和技巧 第1章 了解面试——通过面试抓住机会 2 1.1 选择自己的应聘职位范围 2 1.1.1 针对初级C/C++工程师职位 2 1.1.2 针对中、高级C/C++开发工程师职位 3 1.1.3 C/C++的职位划分 4 1.2 制作专业化的应聘简历 5 1.2.1 简历的组成 5 1.2.2 一个简历的样本 6 1.4 掌握应聘的流程--步步为营 9 1.5 投递简历 10 1.5.1 招聘网站投递 10 1.5.2 E-mail投递 11 1.5.3 如何面对招聘会 12 1.6 面试+笔试+上机考试 13 1.6.1 面试 13 1.6.2 笔试 14 1.6.3 上机考试 15 1.7 面试后的终结邮件 16 第2章 英文面试(教学视频:60分钟) 18 2.1 英文面试过程和技巧 18 2.2 关于工作 19 面试题1 What salary would you expect for this job 19 面试题2 What new skills or ideas do you bring to the job that other candidates aren't likely to offer 20 面试题3 What would you like to accomplish that you weren't able to accomplish in your last position 20 面试题4 How have your career motivations changed over the past few years 20 面试题5 Why should I hire you 21 面试题6 What are your key skills 21 面试题7 What are your strengths 22 面试题8 How is your experience relevant to this job 22 面试题9 What skills do you think are most critical to this job 23 面试题10 What skills would you like to develop in this job 23 面试题11 If you had to stay in your current job, what would you spend more time on? Why 24 面试题12 How could you enrich your current job 24 2.3 关于个人 24 面试题13 Why do you want to work here 25 面试题14 Do you believe you're overqualified for this position 25 面试题15 Tell me about a time you didn't perform to your capabilities 26 面试题16 How do you manage stress in your daily work 26 面试题17 Considering your own resume,what are your weaknesses in relation to this job 26 面试题18 Describe your working relationship with your colleagues 27 面试题19 Describe your personality beneath the professional image 27 面试题20 How will you complement this department 27 2.4 关于发展 28 面试题21 Tell me what you learned from a recent book 28 面试题22 What's your most productive or ideal work setting 28 面试题23 Where do you want to be in five years 29 面试题24 What are your aspirations beyond this job 29 面试题25 How long do you think you'd continue to grow in this job 29 面试题26 What do you reasonably expect to earn within five years 30 第3章 电话面试(教学视频:13分钟) 31 3.1 电话面试简介 31 3.2 如何准备电话面试 32 3.3 电话面试常见问题 33 面试题27 为什么想进本公司 33 面试题28 你最喜欢这份工作的哪一点或者哪些方面 33 面试题29 请描述一下你自己的优缺点 34 面试题30 你对本公司的了解有多少 34 面试题31 对这份工作的期望与目标何在 34 面试题32 你为什么要离职 34 面试题33 选择这份工作的原因是什么 35 面试题34 你认为相关产业的发展会如何 35 面试题35 你希望的待遇为多少 35 面试题36 在工作中学习到了些什么 35 第2篇 C/C++基础知识 第4章 C/C++语言基础(教学视频:50分钟) 38 4.1 基本数据类型 38 面试题37 C++中有哪几种基本数据类型 38 面试题38 整型有哪几种形式?各种形式有什么区别 39 面试题39 C++中有哪些常量 40 面试题40 常量与变量有哪些区别 42 4.2 操作符 42 面试题41 操作符有哪些分类 43 面试题42 操作符优先级对运算结果有什么影响 43 面试题43 逻辑操作与其他操作的关系 45 面试题44 自增自减前操作与后操作的区别 46 面试题45 指针自增自减有什么不同 47 4.3 变量 47 面试题46 什么是左值和右值 48 面试题47 什么是变量 48 面试题48 变量有哪几种初始化方式 49 面试题49 变量默认初始化有什么规则 50 面试题50 什么是变量的声明和定义 50 面试题51 C++中有哪几种作用域 51 面试题52 局部变量与嵌套的作用域 52 面试题53 变量有哪几种存储类型 53 4.4 引用 53 面试题54 什么是引用 54 面试题55 如何使用const引用与非const引用 54 4.5 C与C++的联系 55 面试题56 C与C++有什么区别 55 4.6 编程规范 55 面试题57 什么是匈牙利命名法 55 面试题58 变量有什么命名规则 56 第5章 流程控制(教学视频:24分钟) 58 5.1 条件语句 58 面试题59 条件语句有哪几种形式 58 面试题60 条件语句如何嵌套?如何匹配else子句 59 5.2 循环语句 61 面试题61 for循环语句的计算顺序是什么 61 面试题62 while循环与do-while循环有什么区别 62 面试题63 典型循环语句 64 面试题64 break语句与continue语句有什么区别 64 5.3 switch语句 66 面试题65 switch语句的执行顺序是什么 66 面试题66 如何在switch语句内部定义变量 67 5.4 递归 68 面试题67 什么是递归 68 面试题68 汉诺塔问题 69 第6章 输入输出(教学视频:15分钟) 73 6.1 输入流与输出流 73 面试题69 什么是标准输入输出流 73 面试题70 如何重载输入输出 74 6.2 文件输入/输出 75 面试题71 如何读取一个文件 75 面试题72 文件输入输出有哪几种方式 76 6.3 异常与错误 77 面试题73 什么是异常 77 面试题74 如何抛出和捕捉异常 78 第7章 预处理以及内存管理(教学视频:28分钟) 80 7.1 宏定义 80 面试题75 宏定义与操作符的区别 80 面试题76 宏定义如何展开 81 7.2 include的使用 82 面试题77 include有哪几种使用方式?有什么区别 82 面试题78 包含头文件时如何查找头文件 83 7.3 内存分配 83 面试题79 C++中各数据类型的长度 84 面试题80 如何分配和释放存储空间 84 7.4 虚函数与纯虚函数 85 面试题81 虚函数与纯虚函数的区别 85 面试题82 如何使用纯虚函数 86 第8章 指针(教学视频:60分钟) 88 8.1 指针概述 88 面试题83 什么是指针 88 面试题84 如何初始化指针并对其赋值 89 面试题85 是否可以确定指针指向一个对象 90 面试题86 如何使用指针操作数组 90 面试题87 const对象的指针和const指针的区别 91 面试题88 数组指针与指针数组的区别 92 8.2 函数指针 93 面试题89 什么是函数指针?如何使用函数指针 93 面试题90 指针函数和函数指针的区别 95 8.3 this指针 96 面试题91 什么是this指针 96 面试题92 何时使用this指针 96 8.4 引用与值传递 97 面试题93 什么是值传递 97 面试题94 引用与值传递的区别 98 面试题95 指针和引用有什么区别 99 第9章 面向对象与类(教学视频:48分钟) 100 9.1 面向对象的基本知识 100 面试题96 面向对象与面向过程的区别 100 面试题97 面向对象的特征是什么 101 9.2 类 101 面试题98 类和结构有什么区别 102 面试题99 抽象类及它的用途 103 9.3 类成员 105 面试题100 成员变量有哪些访问方式 105 面试题101 成员变量有哪些访问控制方式 105 面试题102 如何访问静态成员 106 9.4 多态 108 面试题103 什么是多态?多态的作用 108 面试题104 在C++中如何实现多态 109 第10章 继承(教学视频:44分钟) 113 10.1 继承 113 面试题105 派生类与基类的转换 113 面试题106 什么是虚成员?有什么作用 115 面试题107 构造函数与析构函数的调用时机 116 10.2 访问控制 118 面试题108 有哪几种继承方式 118 面试题109 继承时访问级别如何变化 120 10.3 继承时的类作用域 121 面试题110 什么时候发生函数覆盖 121 面试题111 如何访问基类的成员 122 10.4 复制构造函数 123 面试题112 什么是深复制与浅复制 123 面试题113 什么是复制构造函数 124 10.5 类型转换构造函数 125 面试题114 类型转换有哪些分类 125 面试题115 什么是类型转换构造函数 126 第11章 函数(教学视频:49分钟) 128 11.1 函数的定义 128 面试题116 什么是函数 128 面试题117 形参与实参有什么区别 129 面试题118 C++支持参数个数不确定的函数吗 130 面试题119 什么是内联函数 131 11.2 函数参数的传递 132 面试题120 引用形参和非引用形参有什么区别 132 面试题121 使用引用形参有什么问题 133 面试题122 指针形参与引用形参有什么区别 134 11.3 类成员函数 135 面试题123 什么是类成员函数?有哪些特别的类成员函数 135 面试题124 什么是静态函数?如何使用静态函数 136 面试题125 静态函数能访问类的私有成员 137 面试题126 一个类可以访问另一个类的私有成员吗 137 11.4 函数重载 138 面试题127 函数重载与作用域 138 面试题128 如何进行函数重载的匹配 139 面试题129 函数重载时如何实现实参的类型转换 140 第12章 模板与STL(教学视频:46分钟) 141 12.1 模板 141 面试题130 什么是函数模板 141 面试题131 什么是类模板 143 12.2 容器 145 面试题132 什么是容器 145 面试题133 有哪几种顺序容器 147 面试题134 什么是迭代器的范围 149 面试题135 什么是关联容器 150 12.3 泛型编程 152 面试题136 什么是泛型编程 152 面试题137 C++如何实现泛型编程 152 第3篇 C/C++专业应用 第13章 数据结构(教学视频:100分钟) 156 13.1 表 156 面试题138 有哪几种表的实现方式 156 面试题139 链表有哪几种分类 157 13.2 队列 159 面试题140 什么是队列 159 13.3 栈 160 面试题141 什么是栈 160 面试题142 如何访问栈中的元素 162 13.4 树 162 面试题143 树的分类有哪些 162 面试题144 如何对树进行遍历 164 面试题145 如何对二叉树进行遍历 164 面试题146 如何计算二叉树的高度 166 面试题147 如何计算二叉树的结点数 167 13.5 图 167 面试题148 图的实现方式有哪几种 167 面试题149 如何进行图的搜索?搜索算法有哪几种方式 168 13.6 排序 169 面试题150 什么是冒泡排序 169 面试题151 鸡尾酒排序(改进的冒泡排序) 171 面试题152 什么是选择排序 172 面试题153 什么是直接插入排序 173 面试题154 什么是归并排序 174 面试题155 什么是快速排序 176 面试题156 什么是希尔(Shell)排序 177 面试题157 什么是堆排序 179 13.7 排序算法的总结 180 第14章 软件工程(教学视频:39分钟) 182 14.1 软件工程基础 182 面试题158 什么是软件工程 182 面试题159 什么是软件危机 183 14.2 软件的4大开发模型 184 面试题160 4大开发模型的区别 184 面试题161 如何选择开发模型 186 14.3 软件测试 187 面试题162 测试有哪些分类 187 面试题163 如何设计测试用例 188 14.4 UML语言概述 189 面试题164 什么是UML 189 面试题165 UML有哪些图 190 面试题166 UML中的动态图 191 面试题167 静态图有哪几种 192 14.5 设计模式 192 面试题168 什么是设计模式 192 面试题169 GOF的23个设计模式是如何进行分类的 193 第15章 数据库、操作系统、计算机网络(教学视频:65分钟) 195 15.1 数据库理论 195 面试题170 什么是关系数据库设计范式 195 面试题171 什么是数据库事务 196 面试题172 有哪几种数据模型 197 面试题173 什么是数据库管理系统 198 15.2 SQL语言 199 面试题174 什么是SQL语言 199 面试题175 使用SQL语句对表进行分页查询 199 15.3 操作系统概述 200 面试题176 什么是操作系统 200 面试题177 比较常用的操作系统有哪些 201 15.4 进程与线程 202 面试题178 操作系统的最小调度单位是什么 202 面试题179 资源的最小单位是什么 202 面试题180 进程与线程的区别 203 面试题181 如何实现多线程的同步 203 15.5 内存与作业调度 204 面试题182 内存管理方式有哪些 204 面试题183 作业调度算法有哪些 205 面试题184 作业一般有哪些状态 206 15.6 计算机网络结构 206 面试题185 OSI七层模型是什么 207 面试题186 OSI七层网络结构模型与TCP/IP四层结构模型的区别 207 面试题187 什么是TCP/IP协议 208 第16章 上机操作题(教学视频:25分钟) 210 16.1 经典算法问题 210 面试题188 斐波那契数列 210 面试题189 杨辉三角 212 面试题190 整数十进制转二进制 213 面试题191 素数问题 214 面试题192 字符串转换为整数 215 16.2 数据库操作题 216 面试题193 选课系统 216 第17章 思维拓展(教学视频:16分钟) 223 17.1 经典试题 223 面试题194 八皇后问题 223 面试题195 经典矩形 225 面试题196 汉诺塔 229 面试题197 新娘和新郞问题 230 面试题198 大数乘法 230 17.2 面试经验分享 232 17.2.1 面试经过 232 17.2.2 由面试想到的 234 17.3 群体面试 235 第4篇 C/C++面试题实战解析 第18章 文字解答实战题(教学视频:113分钟) 238 面试题199 介绍STL,详细说明STL如何实现vector(华为面试题) 238 面试题200 分析Visual C++程序出错的原因 239 面试题201 继承和多态有何区别 240 面试题202 指针和引用有何区别?传引用比传指针安全,为什么 240 面试题203 参数传递的方式与多态参数传递的实现(摩托罗拉面试题) 241 面试题204 怎样应用设计模式的理念 241 面试题205 介绍一下对设计模式的理解 241 面试题206 C++和C定义结构的区别是什么(摩托罗拉笔试题) 242 面试题207 关于构造函数和析构函数 242 面试题208 对拷贝构造函数的深拷贝、浅拷贝和临时对象的理解 242 面试题209 基类中有一个虚函数,子类还需要申明为virtual吗 242 面试题210 C++的类有什么优点 242 面试题211 如何实现多态?父类和子类的继承关系如何 243 面试题212 为什么要引入抽象基类和纯虚函数(摩托罗拉笔试题) 243 面试题213 介绍一下模板和容器,如何实现 243 面试题214 什么是MVC?简单举例说明其应用 243 面试题215 列举几种进程的同步机制(华为面试题) 243 面试题216 进程之间通信的途径 244 面试题217 进程死锁的原因 244 面试题218 死锁的4个必要条件 244 面试题219 死锁的处理 244 面试题220 操作系统中进程调度策略有哪几种 245 面试题221 类的静态成员和非静态成员有何区别 245 面试题222 纯虚函数如何定义?使用时应注意什么 245 面试题223 数组和链表的区别 245 面试题224 关于ISO七层模型的典型问题 245 面试题225 内存的分配方式及其区别 246 面试题226 struct和class的区别 246 面试题227 一个类所占内存空间的问题(Autodesk面试题改编) 246 面试题228 在8086汇编中,逻辑地址和物理地址如何转换(Intel面试题) 247 面试题229 比较C++中的4种类型转换方式 247 面试题230 面向对象的3个基本特征是什么 247 面试题231 重载(overload)和重写(overried)的区别 248 面试题232 多态的作用 248 面试题233 ADO与ADO .NET有何异同 248 面试题234 New delete与malloc free有何联系与区别 249 面试题235 #define DOUBLE(x) x+x,i = 5*DOUBLE(5),求i 249 面试题236 哪几种情况只能用intialization list而不能用assignment 249 面试题237 C++是不是类型安全的 249 面试题238 main()函数执行前还会执行什么代码 249 面试题239 static有什么用途(至少说明两种) 250 面试题240 描述实时系统的基本特性 250 面试题241 全局变量和局部变量在内存中是否有区别?说明原因 250 面试题242 什么是平衡二叉树 250 面试题243 定义int **a[3][4],则变量占用的内存空间为多少 250 面试题244 堆栈溢出一般是由什么原因导致的 251 面试题245 什么函数不能声明为虚函数 251 面试题246 冒泡排序算法的时间复杂度是什么 251 面试题247 写出float x与“零值”比较的if语句 251 面试题248 Internet采用哪种网络协议?介绍该协议的主要层次结构 252 面试题249 Internet物理地址和IP地址转换采用什么协议 252 面试题250 IP地址的编码分为哪两部分 252 面试题251 不能做switch()的参数类型是什么数据类型 252 面试题252 尽可能多地说出static和const关键字的作用 252 面试题253 什么是预编译 253 面试题254 写一个标准宏,使其输入两个参数,返回较小的一个 254 面试题255 嵌入式系统中如何用C语言编写死循环 254 面试题256 int (*s[10])(int)表示什么 254 面试题257 下列表达式哪些会被编译器禁止(华为面试题) 254 面试题258 换两个变量的值而不使用第3个变量,即a=3,b=5,交换之后a=5,b=3 255 面试题259 C和C++中的struct有什么不同 255 面试题260 如何让程序跳转到绝对地址0x100000去执行 255 面试题261 已知一个数组table,用一个宏定义求出数据的元素个数 256 面试题262 关于线程和进程的典型问题 256 面试题263 什么是引用?说明申明和使用引用要注意的问题 256 面试题264 将引用作为函数的参数有哪些特点 256 面试题265 什么时候需要使用常引用 257 面试题266 给出将“引用”作为函数返回值类型的格式及其优点和需遵守的规则 257 面试题267 “引用”与多态的关系 259 面试题268 如何判断一段程序是由C编译还是由C++编译的 259 面试题269 结构与联合有何区别 259 面试题270 下面代码的输出结果是什么 259 面试题271 一个32位的机器的指针是多少位(华为面试题) 260 面试题272 类成员函数的重载、覆盖和隐藏的区别 260 面试题273 const与#define相比有何优点 261 面试题274 h头文件中的ifndef/define/endif有何作用 261 面试题275 #include与#include "file.h"有何区别 261 面试题276 C++中调用被C编译器编译后的函数为何要加extern "C" 261 面试题277 关联、聚合(Aggregation)及组合(omposition)有何区别 264 面试题278 多重继承如何消除向上继承的二义性(摩托罗拉笔试题) 265 第19章 程序改错实战题(教学视频:31分钟) 266 面试题279 找出下面代码中的所有错误 266 面试题280 找出下面代码中的所有错误 266 面试题281 找出下面代码的错误 267 面试题282 找出下面代码的错误 267 面试题283 找出下面代码的错误 268 面试题284 找出下面代码的错误 269 面试题285 找出下面代码的错误 270 面试题286 找出下面代码的错误 270 面试题287 找出下面代码的错误 271 面试题288 以下代码有什么问题 271 面试题289 下面程序执行后有什么错误或结果(华为面试题) 272 面试题290 改正下面代码的错误 272 面试题291 以下代码中的两个sizeof用法有问题吗(朗讯面试题) 273 面试题292 指出下面程序中的错误 273 面试题293 指出下面程序中的错误 273 面试题294 指出下面程序中的错误 274 面试题295 下面程序有什么错误 274 面试题296 下面程序有什么错误 274 面试题297 下面程序有什么错误 275 面试题298 以下代码有什么错误 275 面试题299 求一个数的平方,请找出代码错误(华为面试题) 276 面试题300 找出下面这段程序的错误(华为面试题) 276 第20章 编写程序实战题(教学视频:79分钟) 277 面试题301 写出删除一个单项链表某一个结点的算法 277 面试题302 用1分钱、2分钱、5分钱组成1元钱的方式 278 面试题303 奶牛生子的问题(腾讯面试题) 278 面试题304 写一个函数将链表逆序(Intel面试题) 279 面试题305 将两个链表合并成一个链表依然有序 280 面试题306 用递归方法将两个链表合并成一个链表依然有序(Autodesk面试题) 281 面试题307 给出bool、int、float指针变量与“零值”比较的if 语句 281 面试题308 计算Windows NT下32位C++程序的sizeof值 282 面试题309 写一个“标准”宏MIN,输入两个参数,返回较小的 283 面试题310 标准头文件的结构问题(微软面试题) 283 面试题311 编写一个函数,把一个char组成的字符串循环右移n位 284 面试题312 以适当的数据结构组织WAV文件头并解析文件信息 285 面试题313 编写类String的构造函数、析构函数和赋值函数 286 面试题314 写一个C函数,判断不同模式处理器的返回值 287 面试题315 写一个函数返回1+2+3+…+n的值 288 面试题316 输入一个n,在屏幕上打印出N×N的矩阵(华为面试题) 288 面试题317 找出两个数中最大的一个(华为面试题) 290 面试题318 打印出当前源文件的文件名及当前行号 290 面试题319 main()主函数执行完毕后是否会再执行一段代码 290 面试题320 一个关于指针的问题 291 面试题321 按要求输出时间 292 面试题322 一个关于字符串函数的问题 293 面试题323 尝试写出类的成员函数的实现 294 面试题324 简述数组与指针的区别 295 面试题325 关于C/C++编译器的问题 295 面试题326 一个关于排序和文件输出的问题(华为面试题) 296 面试题327 找出一个整数数组中的第二大数(微软面试题) 297 面试题328 判断一个单链表有环 297 面试题329 求函数返回值 298 面试题330 一个关于如何处理溢出的问题 298 面试题331 如何用C语言将一个字符转换成整型 299 第5篇 智力测试与逻辑测试 第21章 智力测试(教学视频:54分钟) 302 21.1 数学智力测试 302 面试题332 现代的斯芬克斯(美国AT&T公司2000年面试题) 302 面试题333 找出次品乒乓球 302 面试题334 有几顶黑帽子 303 面试题335 如何分配蛋糕 303 面试题336 如何切会最多 303 面试题337 怎样烙饼 304 面试题338 如何最快送到情报 304 面试题339 他们是怎么比赛的 304 面试题340 如何称大米 305 面试题341 100美元哪里去了(欧洲某著名IT公司2005年面试题) 305 面试题342 击鼠标比赛(欧洲某著名IT公司2005年面试题) 305 面试题343 女儿哪里错了(印度某著名IT公司2003年面试题) 306 面试题344 原来是什么硬币(日本某著名公司1998年面试题) 306 面试题345 怎样会有满满的一瓶(欧洲某著名IT公司2006年面试题) 306 21.2 推理智力测试 307 面试题346 蜗牛出井(欧洲某著名IT公司2003年面试题) 307 面试题347 他们如何过河 307 面试题348 为什么上当(中国某大型IT公司2008年面试题) 307 面试题349 判断几率 308 面试题350 诚实的人(微软1999年应届生面试题) 308 面试题351 到达的顺序(中国某大型IT公司2009年面试题) 308 面试题352 下列数列中少了一个什么数(中国某大型IT公司2009年面试题) 308 21.3 综合智力测试 309 面试题353 飞机票如何分配 309 面试题354 聪明的农民 309 面试题355 求最短的时间(中国某大型IT公司2007年面试题) 309 面试题356 不能称到的最轻重量(中国某大型IT公司2007年面试题) 310 面试题357 小机灵的办法(中国某大型IT公司2006年面试题) 310 面试题358 池塘取水(美国某大型IT公司2003年面试题) 310 面试题359 1=5,2=15,3=215,4=2145。那么5=?(日本某大型IT公司2006年 面试题) 311 面试题360 如何打开房间 311 面试题361 聪明人怎么发财 311 面试题362 请问有几条生病的狗 312 第22章 逻辑测试(教学视频:41分钟) 313 22.1 文字逻辑测试 313 面试题363 H是什么人种(北美电信2006年应届生面试题) 313 面试题364 推断血液、脂肪蛋白和胆固醇的关系(中国某教育集团2008年面试题) 314 面试题365 学籍、学生会干部和奖学金(中国某大型IT公司2003年面试题) 314 面试题366 数学院的规定(中国某著名研究院2007年面试题) 315 面试题367 老师的结论(中国某著名公司2007年面试题) 315 面试题368 是哪一张牌 316 22.2 图形逻辑测试 317 面试题369 选择正确的图形(欧洲某著名IT公司2003年面试题) 317 面试题370 选择正确的图形(欧洲某著名IT公司2004年面试题) 318 面试题371 选择正确的图形(雅虎中国公司2005年面试题) 318 面试题372 选择正确的图形(中国某大型IT公司2007年面试题) 319 面试题373 选择正确的图形(中国某大型IT公司2002年面试题) 319 面试题374 选择正确的图形(亚洲某著名IT公司2006年面试题) 320 22.3 规律题测试 321 面试题375 规律题1 321 面试题376 规律题2 321 面试题377 规律题3 322 面试题378 规律题4 322 面试题379 规律题5 322 面试题380 规律题6 323 面试题381 规律题7 323 面试题382 规律题8 323
文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。 追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。 不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求: 确定您是否有足够的内存来处理数据。 从可用的内存中获取一部分内存。 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。 实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。 回页首 C 风格的内存分配程序 C 编程语言提供了两个函数来满足我们的三个需求: malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。 free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。 物理内存和虚拟内存 要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。 只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。 在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。) 基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用: brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。 mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。 如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。 实现一个简单的分配程序 如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。 要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。 在大部分操作系统中,内存分配由以下两个简单的函数来处理: void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。 void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。 malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量: 清单 1. 我们的简单分配程序的全局变量 int has_initialized = 0; void *managed_memory_start; void *last_valid_address; 如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量: 清单 2. 分配程序初始化函数 /* Include the sbrk function */ #include void malloc_init() { /* grab the last valid address from the OS */ last_valid_address = sbrk(0); /* we don't have any memory to manage yet, so *just set the beginning to be last_valid_address */ managed_memory_start = last_valid_address; /* Okay, we're initialized and ready to go */ has_initialized = 1; } 现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义 struct mem_control_block { int is_available; int size; }; 现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。 在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 清单 4. 解除分配函数 void free(void *firstbyte) { struct mem_control_block *mcb; /* Backup from the given pointer to find the * mem_control_block */ mcb = firstbyte - sizeof(struct mem_control_block); /* Mark the block as being available */ mcb->is_available = 1; /* That's It! We're done. */ return; } 如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述: 清单 5. 主分配程序的伪代码 1. If our allocator has not been initialized, initialize it. 2. Add sizeof(struct mem_control_block) to the size requested. 3. start at managed_memory_start. 4. Are we at last_valid address? 5. If we are: A. We didn't find any existing space that was large enough -- ask the operating system for more and return that. 6. Otherwise: A. Is the current space available (check is_available from the mem_control_block)? B. If it is: i) Is it large enough (check "size" from the mem_control_block)? ii) If so: a. Mark it as unavailable b. Move past mem_control_block and return the pointer iii) Otherwise: a. Move forward "size" bytes b. Go back go step 4 C. Otherwise: i) Move forward "size" bytes ii) Go back to step 4 我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码: 清单 6. 主分配程序 void *malloc(long numbytes) { /* Holds where we are looking in memory */ void *current_location; /* This is the same as current_location, but cast to a * memory_control_block */ struct mem_control_block *current_location_mcb; /* This is the memory location we will return. It will * be set to 0 until we find something suitable */ void *memory_location; /* Initialize if we haven't already done so */ if(! has_initialized) { malloc_init(); } /* The memory we search for has to include the memory * control block, but the users of malloc don't need * to know this, so we'll just add it in for them. */ numbytes = numbytes + sizeof(struct mem_control_block); /* Set memory_location to 0 until we find a suitable * location */ memory_location = 0; /* Begin searching at the start of managed memory */ current_location = managed_memory_start; /* Keep going until we have searched all allocated space */ while(current_location != last_valid_address) { /* current_location and current_location_mcb point * to the same address. However, current_location_mcb * is of the correct type, so we can use it as a struct. * current_location is a void pointer so we can use it * to calculate addresses. */ current_location_mcb = (struct mem_control_block *)current_location; if(current_location_mcb->is_available) { if(current_location_mcb->size >= numbytes) { /* Woohoo! We've found an open, * appropriately-size location. */ /* It is no longer available */ current_location_mcb->is_available = 0; /* We own it */ memory_location = current_location; /* Leave the loop */ break; } } /* If we made it here, it's because the Current memory * block not suitable; move to the next one */ current_location = current_location + current_location_mcb->size; } /* If we still don't have a valid location, we'll * have to ask the operating system for more memory */ if(! memory_location) { /* Move the program break numbytes further */ sbrk(numbytes); /* The new memory will be where the last valid * address left off */ memory_location = last_valid_address; /* We'll move the last valid address forward * numbytes */ last_valid_address = last_valid_address + numbytes; /* We need to initialize the mem_control_block */ current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; } /* Now, no matter what (well, except for error conditions), * memory_location has the address of the memory, including * the mem_control_block */ /* Move the pointer past the mem_control_block */ memory_location = memory_location + sizeof(struct mem_control_block); /* Return the pointer */ return memory_location; } 这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。 运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数): 清单 7. 编译分配程序 gcc -shared -fpic malloc.c -o malloc.so 该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。 在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下: 清单 8. 替换您的标准的 malloc LD_PRELOAD=/path/to/malloc.so export LD_PRELOAD LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。 如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。 我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括: 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。 它没有实现很多其他的内存函数,比如 realloc()。 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。 分配程序不是线程安全的。 分配程序不能将空闲空间拼合为更大的内存块。 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。 我确信还有很多其他问题。这就是为什么它只是一个例子! 其他 malloc 实现 malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括: 分配的速度。 回收的速度。 有线程的环境的行为。 内存将要被用光时的行为。 局部缓存。 簿记(Bookkeeping)内存开销。 虚拟内存环境中的行为。 小的或者大的对象。 实时保证。 每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。 还有其他许多分配程序可以使用。其中包括: Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。 BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。 Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。 众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。 在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。 基于 malloc() 的内存管理的缺点 不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。 因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。 回页首 半自动内存管理策略 引用计数 引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。 在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。 这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。 要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。 一个示例引用计数函数集可能看起来如下所示: 清单 9. 基本的引用计数函数 /* Structure Definitions*/ /* Base structure that holds a refcount */ struct refcountedstruct { int refcount; } /* All refcounted structures must mirror struct * refcountedstruct for their first variables */ /* Refcount maintenance functions */ /* Increase reference count */ void REF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount++; } /* Decrease reference count */ void UNREF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount--; /* Free the structure if there are no more users */ if(rstruct->refcount == 0) { free(rstruct); } } REF 和 UNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。 当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则: UNREF 分配前左端指针(left-hand-side pointer)指向的值。 REF 分配后左端指针(left-hand-side pointer)指向的值。 在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则: 在函数的起始处 REF 每一个指针。 在函数的结束处 UNREF 第一个指针。 以下是一个使用引用计数的生动的代码示例: 清单 10. 使用引用计数的示例 /* EXAMPLES OF USAGE */ /* Data type to be refcounted */ struct mydata { int refcount; /* same as refcountedstruct */ int datafield1; /* Fields specific to this struct */ int datafield2; /* other declarations would go here as appropriate */ }; /* Use the functions in code */ void dosomething(struct mydata *data) { REF(data); /* Process data */ /* when we are through */ UNREF(data); } struct mydata *globalvar1; /* Note that in this one, we don't decrease the * refcount since we are maintaining the reference * past the end of the function call through the * global variable */ void storesomething(struct mydata *data) { REF(data); /* passed as a parameter */ globalvar1 = data; REF(data); /* ref because of Assignment */ UNREF(data); /* Function finished */ } 由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。 在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处: 实现简单。 易于使用。 由于引用是数据结构的一部分,所以它有一个好的缓存位置。 不过,它也有其不足之处: 要求您永远不要忘记调用引用计数函数。 无法释放作为循环数据结构的一部分的结构。 减缓几乎每一个指针的分配。 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。 需要额外的内存来处理引用。 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。 在多线程环境中更慢也更难以使用。 C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。 内存池 内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。 在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。 要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。 下面的假想代码列表展示了如何使用 obstack: 清单 11. obstack 的示例代码 #include #include /* Example code listing for using obstacks */ /* Used for obstack macros (xmalloc is a malloc function that exits if memory is exhausted */ #define obstack_chunk_alloc xmalloc #define obstack_chunk_free free /* Pools */ /* Only permanent allocations should go in this pool */ struct obstack *global_pool; /* This pool is for per-connection data */ struct obstack *connection_pool; /* This pool is for per-request data */ struct obstack *request_pool; void allocation_failed() { exit(1); } int main() { /* Initialize Pools */ global_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(global_pool); connection_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(connection_pool); request_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(request_pool); /* Set the error handling function */ obstack_alloc_failed_handler = &allocation_failed; /* Server main loop */ while(1) { wait_for_connection(); /* We are in a connection */ while(more_requests_available()) { /* Handle request */ handle_request(); /* Free all of the memory allocated * in the request pool */ obstack_free(request_pool, NULL); } /* We're finished with the connection, time * to free that pool */ obstack_free(connection_pool, NULL); } } int handle_request() { /* Be sure that all object allocations are allocated * from the request pool */ int bytes_i_need = 400; void *data1 = obstack_alloc(request_pool, bytes_i_need); /* Do stuff to process the request */ /* return */ return 0; } 基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。 使用池式内存分配的益处如下所示: 应用程序可以简单地管理内存。 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。 有非常易于使用的标准实现。 池式内存的缺点是: 内存池只适用于操作可以分阶段的程序。 内存池通常不能与第三方库很好地合作。 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。 回页首 垃圾收集 垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。 收集器的类型 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。 Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。 垃圾收集的一些优点: 您永远不必担心内存的双重释放或者对象的生命周期。 使用某些收集器,您可以使用与常规分配相同的 API。 其缺点包括: 使用大部分收集器时,您都无法干涉何时释放内存。 在多数情况下,垃圾收集比其他形式的内存管理更慢。 垃圾收集错误引发的缺陷难于调试。 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。 回页首 结束语 一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。 表 1. 内存分配策略的对比 策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好 定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现 简单分配程序 内存使用少时较快 很快 差 容易 高 否 否 GNU malloc 中 快 中 容易 高 否 中 Hoard 中 中 中 容易 高 否 是 引用计数 N/A N/A 非常好 中 中 是(取决于 malloc 实现) 取决于实现 池 中 非常快 极好 中 中 是(取决于 malloc 实现) 取决于实现 垃圾收集 中(进行收集时慢) 中 差 中 中 否 几乎不 增量垃圾收集 中 中 中 中 中 否 几乎不 增量保守垃圾收集 中 中 中 容易 高 否 几乎不 参考资料 您可以参阅本文在 developerWorks 全球站点上的 英文原文。 Web 上的文档 GNU C Library 手册的 obstacks 部分 提供了 obstacks 编程接口。 Apache Portable Runtime 文档 描述了它们的池式分配程序的接口。 基本的分配程序 Doug Lea 的 Malloc 是最流行的内存分配程序之一。 BSD Malloc 用于大部分基于 BSD 的系统中。 ptmalloc 起源于 Doug Lea 的 malloc,用于 GLIBC 之中。 Hoard 是一个为多线程应用程序优化的 malloc 实现。 GNU Memory-Mapped Malloc(GDB 的组成部分) 是一个基于 mmap() 的 malloc 实现。 池式分配程序 GNU Obstacks(GNU Libc 的组成部分)是安装最多的池式分配程序,因为在每一个基于 glibc 的系统中都有它。 Apache 的池式分配程序(Apache Portable Runtime 中) 是应用最为广泛的池式分配程序。 Squid 有其自己的池式分配程序。 NetBSD 也有其自己的池式分配程序。 talloc 是一个池式分配程序,是 Samba 的组成部分。 智能指针和定制分配程序 Loki C++ Library 有很多为 C++ 实现的通用模式,包括智能指针和一个定制的小对象分配程序。 垃圾收集器 Hahns Boehm Conservative Garbage Collector 是最流行的开源垃圾收集器,它可以用于常规的 C/C++ 程序。 关于现代操作系统中的虚拟内存的文章 Marshall Kirk McKusick 和 Michael J. Karels 合著的 A New Virtual Memory Implementation for Berkeley UNIX 讨论了 BSD 的 VM 系统。 Mel Gorman's Linux VM Documentation 讨论了 Linux VM 系统。 关于 malloc 的文章 Poul-Henning Kamp 撰写的 Malloc in Modern Virtual Memory Environments 讨论的是 malloc 以及它如何与 BSD 虚拟内存交互。 Berger、McKinley、Blumofe 和 Wilson 合著的 Hoard -- a Scalable Memory Allocator for Multithreaded Environments 讨论了 Hoard 分配程序的实现。 Marshall Kirk McKusick 和 Michael J. Karels 合著的 Design of a General Purpose Memory Allocator for the 4.3BSD UNIX Kernel 讨论了内核级的分配程序。 Doug Lea 撰写的 A Memory Allocator 给出了一个关于设计和实现分配程序的概述,其中包括设计选择与折衷。 Emery D. Berger 撰写的 Memory Management for High-Performance Applications 讨论的是定制内存管理以及它如何影响高性能应用程序。 关于定制分配程序的文章 Doug Lea 撰写的 Some Storage Management Techniques for Container Classes 描述的是为 C++ 类编写定制分配程序。 Berger、Zorn 和 McKinley 合著的 Composing High-Performance Memory Allocators 讨论了如何编写定制分配程序来加快具体工作的速度。 Berger、Zorn 和 McKinley 合著的 Reconsidering Custom Memory Allocation 再次提及了定制分配的主题,看是否真正值得为其费心。 关于垃圾收集的文章 Paul R. Wilson 撰写的 Uniprocessor Garbage Collection Techniques 给出了垃圾收集的一个基本概述。 Benjamin Zorn 撰写的 The Measured Cost of Garbage Collection 给出了关于垃圾收集和性能的硬数据(hard data)。 Hans-Juergen Boehm 撰写的 Memory Allocation Myths and Half-Truths 给出了关于垃圾收集的神话(myths)。 Hans-Juergen Boehm 撰写的 Space Efficient Conservative Garbage Collection 是一篇描述他的用于 C/C++ 的垃圾收集器的文章。 Web 上的通用参考资料 内存管理参考 中有很多关于内存管理参考资料和技术文章的链接。 关于内存管理和内存层级的 OOPS Group Papers 是非常好的一组关于此主题的技术文章。 C++ 中的内存管理讨论的是为 C++ 编写定制的分配程序。 Programming Alternatives: Memory Management 讨论了程序员进行内存管理时的一些选择。 垃圾收集 FAQ 讨论了关于垃圾收集您需要了解的所有内容。 Richard Jones 的 Garbage Collection Bibliography 有指向任何您想要的关于垃圾收集的文章的链接。 书籍 Michael Daconta 撰写的 C++ Pointers and Dynamic Memory Management 介绍了关于内存管理的很多技术。 Frantisek Franek 撰写的 Memory as a Programming Concept in C and C++ 讨论了有效使用内存的技术与工具,并给出了在计算机编程中应当引起注意的内存相关错误的角色。 Richard Jones 和 Rafael Lins 合著的 Garbage Collection: Algorithms for Automatic Dynamic Memory Management 描述了当前使用的最常见的垃圾收集算法。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.5 节“Dynamic Storage Allocation”中,描述了实现基本的分配程序的一些技术。 在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.3.5 节“Lists and Garbage Collection”中,讨论了用于列表的垃圾收集算法。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 4 章“Small Object Allocation”描述了一个C++ 标准分配程序效率高得多的一个高速小对象分配程序。 Andrei Alexandrescu 撰写的 Modern C++ Design 第 7 章“Smart Pointers”描述了在 C++ 中智能指针的实现。 Jonathan 撰写的 Programming from the Ground Up 第 8 章“Intermediate Memory Topics”中有本文使用的简单分配程序的一个汇编语言版本。 来自 developerWorks 自我管理数据缓冲区内存 (developerWorks,2004 年 1 月)略述了一个用于管理内存的自管理的抽象数据缓存器的伪 C (pseudo-C)实现。 A framework for the user defined malloc replacement feature (developerWorks,2002 年 2 月)展示了如何利用 AIX 中的一个工具,使用自己设计的内存子系统取代原有的内存子系统。 掌握 Linux 调试技术 (developerWorks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、内存溢出、内存泄漏和挂起。 在 处理 Java 程序中的内存漏洞 (developerWorks,2001 年 2 月)中,了解导致 Java 内存泄漏的原因,以及何时需要考虑它们。 在 developerWorks Linux 专区中,可以找到更多为 Linux 开发人员准备的参考资料。 从 developerWorks 的 Speed-start your Linux app 专区中,可以下载运行于 Linux 之上的 IBM 中间件产品的免费测试版本,其中包括 WebSphere® Studio Application Developer、WebSphere Application Server、DB2® Universal Database、Tivoli® Access Manager 和 Tivoli Directory Server,查找 how-to 文章和技术支持。 通过参与 developerWorks blogs 加入到 developerWorks 社区。 可以在 Developer Bookstore Linux 专栏中定购 打折出售的 Linux 书籍。 关于作者 Jonathan Bartlett 是 Programming from the Ground Up 一书的作者,这本书介绍的是 Linux 汇编语言编程。Jonathan Bartlett 是 New Media Worx 的总开发师,负责为客户开发 Web、视频、kiosk 和桌面应用程序。您可以通过 johnnyb@eskimo.com 与 Jonathan 联系。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逗神大人

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值