• 高质量C /C编程指南第7章 内存管理


    第7章 内存管理

        接待进入内存这片雷区。巨大的Bill Gates 曾经失口:

    640K ought to be enough for everybody

    — Bill Gates 1981

    序次递次员们但凡编写内存管理序次递次,每每胆战心惊。假定不想触雷,独一的处置办法就是发明扫数窜伏的地雷并且断根它们,躲是躲不了的。本章的内容比普通教科书的要深切得多,读者需细心阅读,做到真正地知晓内存管理。

    7.1内存分派编制

    内存分派编制有三种:

    (1)       从静态存储地区分派。内存在序次递次编译的时分就曾经分派好,这块内存在序次递次的整个运转期间都存在。譬喻全局变量,static变量。

    (2)       在栈上成立。在测验考试函数时,函数外部门变量的存储单位都可以在栈上成立,函数测验考试终了时这些存储单位自动被释放。栈内存分派运算内置于处置赏罚器的指令集合,效率很高,但是分派的内存容量无限。

    (3)       从堆上分派,亦称静态内存分派。序次递次在运转的时分用malloc或new请求随意几何的内存,序次递次员自己负责在何时用free或delete释放内存。静态内存的生计期由我们决意,运用极度灵动,但成果也最多。

    7.2罕见的内存错误及其对策

           发生内存错误是件极度费事的事情。编译器不能自动发明这些错误,但凡是在序次递次运转时才干捕捉到。而这些错误大多没有清晰的症状,时隐时现,添加了改错的难度。偶尔用户肝火冲发地把你找来,序次递次却没有发生任何成果,你一走,错误又发生负气了。

    罕见的内存错误及其对策如下:

    u       内存分派未乐成,却运用了它。

    编程熟手外行常犯这种错误,由于他们没有看法到内存分派会不乐成。常用途置办法是,在运用内存之前反省指针能否为NULL。假定指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)中断反省。假定是用malloc或new来请求内存,应该用if(p==NULL) 或if(p!=NULL)中断防错处置赏罚。

     

    u       内存分派虽然乐成,但是尚未初始化就引用它。

    犯这种错误首要有两个缘故起因:一是没有初始化的观念;二是误认为内存的缺省初值全为零,招致引用初值错误(譬喻数组)。

    内存的缺省初值理想后果是什么并没有同等的尺度,虽然有些时分为零值,我们宁可托其无不可托其有。所以无论用何种编制成立数组,都别忘了赋初值,即就是赋零值也不可省略,不要嫌费事。

     

    u       内存分派乐成并且曾经初始化,但应用越过了内存的边界。

    譬喻在运用数组时但凡发生下标“多1”也许“少1”的应用。特别是在for轮回语句中,轮回次数很随便搞错,招致数组应用越界。

     

    u       遗忘了释放内存,构成内存透露。

    含有这种错误的函数每被挪用一次就丧失一块内存。刚入手下手时琐屑的内存充沛,你看不到错误。终有一次序次递次突然物化失,琐屑出现提示:内存耗尽。

    静态内存的请求与释放必需配对,序次递次中malloc与free的运用次数一定要相反,否则一定有错误(new/delete同理)。

     

    u       释放了内存却持续运用它。

    有三种情况:

    (1)序次递次中的东西挪用关连过于严重,其实难以搞体会理睬某个东西理想后果能否曾经释放了内存,此时应该重新谋划数据机关,从基础内幕上处置东西管理的紊乱场合排场。

    (2)函数的return语句写错了,细心不要前往指向“栈内存”的“指针”也许“引用”,由于该内存在函数体终了时被自动销毁。

    (3)运用free或delete释放了内存后,没有将指针设置为NULL。招致发生“野指针”。

     

    l         【纪律7-2-1用malloc或new请求内存之后,应该立地反省指针值能否为NULL。抗御运用指针值为NULL的内存。

    l         【纪律7-2-2不要遗忘为数组和静态内存赋初值。抗御将未被初始化的内存作为右值运用。

    l         【纪律7-2-3抗御数组或指针的下标越界,特别要留神发生“多1”也许“少1”应用。

    l         【纪律7-2-4静态内存的请求与释放必需配对,抗御内存透露。

    l         【纪律7-2-5用free或delete释放了内存之后,立地将指针设置为NULL,抗御发生“野指针”。

    7.3指针与数组的相比

           C /C序次递次中,指针和数组在不少中心可以互相交换着用,让人发生一种错觉,认为两者是等价的。

           数组要么在静态存储区被成立(如全局数组),要么在栈上被成立。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只罕见组的内容可以转变。

    指针可以随时指向随意规范的内存块,它的特征是“可变”,所以我们常用指针来应用静态内存。指针远比数组灵动,但也更损伤。

    下面以字符串为例相比指针与数组的特征。

     

    7.3.1 编削内容

           示例7-3-1中,字符数组a的容量是6个字符,其内容为hello\0。a的内容可以转变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被编削的。从语法上看,编译器并不认为语句p[0]= ‘X’有什么不妥,但是该语句妄想编削常量字符串的内容而招致运转错误。

     

    char a[] = “hello”;

    a[0] = ‘X’;

    cout << a << endl;

    char *p = “world”;     // 细心p指向常量字符串

    p[0] = ‘X’;             // 编译器不能发明该错误

    cout << p << endl;

    示例7-3-1 编削数组和指针的内容

     

    7.3.2 内容复制与相比

        不能对数组名中断间接复制与相比。示例7-3-2中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将发生编译错误。应该用尺度库函数strcpy中断复制。同理,相比b和a的内容能否相反,不能用if(b==a) 来剖断,应该用尺度库函数strcmp中断相比。

        语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p请求一块容量为strlen(a) 1个字符的内存,再用strcpy中断字符串复制。同理,语句if(p==a) 相比的不是内容而是地址,应该用库函数strcmp来相比。

     

        // 数组…

        char a[] = "hello";

        char b[10];

        strcpy(b, a);           // 不能用   b = a;

        if(strcmp(b, a) == 0)   // 不能用  if (b == a)



        // 指针…

        int len = strlen(a);

        char *p = (char *)malloc(sizeof(char)*(len 1));

        strcpy(p,a);            // 不要用 p = a;

        if(strcmp(p, a) == 0)   // 不要用 if (p == a)



    示例7-3-2 数组和指针的内容复制与相比

     

     

    7.3.3 角力盘算争论内存容量

        用运算符sizeof可以角力盘算争论出数组的容量(字节数)。示例7-3-3(a)中,sizeof(a)的值是12(细心别忘了’\0’)。指针p指向a,但是sizeof(p)的值倒是4。这是由于sizeof(p)取得的是一个指针变量的字节数,相等于sizeof(char*),而不是p所指的内存容量。C /C语言没有办法晓得指针所指的内存容量,除非在请求内存时记取它。

    细心当数组作为函数的参数中断传递时,该数组自动退步为同规范的指针。示例7-3-3(b)中,不论数组a的容量是几何,sizeof(a)不休就是sizeof(char *)。

     

        char a[] = "hello world";

        char *p  = a;

        cout<< sizeof(a) << endl;   // 12字节

        cout<< sizeof(p) << endl;   // 4字节

    示例7-3-3(a) 角力盘算争论数组和指针的内存容量

          

        void Func(char a[100])

        {

            cout<< sizeof(a) << endl;   // 4字节而不是100字节

    }

    示例7-3-3(b) 数组退步为指针

    7.4指针参数是如何传递内存的?

           假定函数的参数是一个指针,不要指望用该指针去请求静态内存。示例7-4-1中,Test函数的语句GetMemory(str, 200)并没有使str取得希冀的内存,str模仿仍是是NULL,为什么?

     

    void GetMemory(char *p, int num)

    {

        p = (char *)malloc(sizeof(char) * num);

    }

    void Test(void)

    {

        char *str = NULL;

        GetMemory(str, 100);    // str 仍旧为 NULL 

        strcpy(str, "hello");   // 运转错误

    }

    示例7-4-1 试图用指针参数请求静态内存

     

    方向出在函数GetMemory中。编译器老是要为函数的每个参数制造且自正本,指针参数p的正本是 _p,编译器使 _p = p。假定函数体内的序次递次编削了_p的内容,就招致参数p的内容作相应的编削。这就是指针可以用作输入参数的缘故起因。在本例中,_p请求了新的内存,只是把_p所指的内存地址转变了,但是p丝毫未变。所以函数GetMemory并不能输入任何器械。理想上,每测验考试一次GetMemory就会透露一块内存,由于没有用free释放内存。

    假定非得要用指针参数去请求内存,那么应该改用“指向指针的指针”,见教例7-4-2。

     

    void GetMemory2(char **p, int num)

    {

        *p = (char *)malloc(sizeof(char) * num);

    }

    void Test2(void)

    {

        char *str = NULL;

        GetMemory2(&str, 100);  // 细心参数是 &str,而不是str

        strcpy(str, "hello");  

        cout<< str << endl;

        free(str); 

    }

    示例7-4-2用指向指针的指针请求静态内存

     

    由于“指向指针的指针”这个欠好看念不随便体会,我们可以用函数前往值来传递静态内存。这种办法越发复杂,见教例7-4-3。

     

    char *GetMemory3(int num)

    {

        char *p = (char *)malloc(sizeof(char) * num);

        return p;

    }

    void Test3(void)

    {

        char *str = NULL;

        str = GetMemory3(100); 

        strcpy(str, "hello");

        cout<< str << endl;

        free(str); 

    }

    示例7-4-3 用函数前往值来传递静态内存

     

    用函数前往值来传递静态内存这种办法虽然好用,但是但凡有人把return语句用错了。这里夸大不要用return语句前往指向“栈内存”的指针,由于该内存在函数终了时自动消亡,见教例7-4-4。

     

    char *GetString(void)

    {

        char p[] = "hello world";

        return p;   // 编译器将提出劝诫

    }

    void Test4(void)

    {

    char *str = NULL;

    str = GetString();  // str 的内容是渣滓

    cout<< str << endl;

    }

    示例7-4-4 return语句前往指向“栈内存”的指针

     

    用调试器慢慢跟踪Test4,发理论验str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是渣滓。

    假定把示例7-4-4改写成示例7-4-5,会如何样?

     

    char *GetString2(void)

    {

        char *p = "hello world";

        return p;

    }

    void Test5(void)

    {

        char *str = NULL;

        str = GetString2();

        cout<< str << endl;

    }

    示例7-4-5 return语句前往常量字符串

     

    函数Test5运转虽然不会出错,但是函数GetString2的谋划欠好看念倒是错误的。由于GetString2内的“hello world”是常量字符串,位于静态存储区,它在序次递次生命期内恒定不变。无论什么时分挪用GetString2,它前往的不休是统一个“只读”的内存块。

     

    7.5 free和delete把指针如何啦?

    别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放失,但并没有把指针自己干失。

    用调试器跟踪示例7-5,发明指针p被free以后其地址仍旧不变(非NULL),只是该地址对应的内存是渣滓,p成了“野指针”。假定此时不把p设置为NULL,会让人误认为p是个正当的指针。

    假定序次递次相比长,我们偶尔记不住p所指的内存能否曾经被释放,在持续运用p之前,但凡会用语句if (p != NULL)中断防错处置赏罚。很遗憾,此时if语句起不到防错陶染,由于即便p不是NULL指针,它也不指向正当的内存块。

     

        char *p = (char *) malloc(100);

        strcpy(p, “hello”);

        free(p);        // p 所指的内存被释放,但是p所指的地址仍旧不变

        …

        if(p != NULL)   // 没有起到防错陶染

        {

           strcpy(p, “world”);  // 出错

    }

    示例7-5  p成为野指针

    7.6 静态内存会被自动释放吗?

           函数体内的部门变量在函数终了时自动消亡。许多人误认为示例7-6是切确的。起因是p是部门的指针变量,它消亡的时分会让它所指的静态内存一起倒台。这是错觉!

     

        void Func(void)

    {

        char *p = (char *) malloc(100); // 静态内存会自动释放吗?

    }

    示例7-6 试图让静态内存自动释放

     

        我们发明指针有一些“貌同实异”的特征:

    (1)指针消亡了,并不暗示它所指的内存会被自动释放。

    (2)内存被释放了,并不暗示指针会消亡也许成了NULL指针。

    这表达释放内存并不是一件可以轻率对待的事。也许有人不佩服,一定要找出可以轻率行事的起因:

        假定序次递次停止了运转,齐全指针都会消亡,静态内存会被应用琐屑采取。既然云云,在序次递次临终前,就可以不必释放内存、不必将指针设置为NULL了。终于可以偷懒而不会发生错误了吧?

        想得美。假定别人把那段序次递次取出来用到别的中心如何办?

    7.7 根绝“野指针”

    “野指针”不是NULL指针,是指向“渣滓”内存的指针。人们普通不会错用NULL指针,由于用if语句很随便剖断。但是“野指针”是很损伤的,if语句对它不起陶染。

    “野指针”的成因首要有两种:

    (1)指针变量没有被初始化。任何指针变量刚被创顿时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在成立的同时应当被初始化,要么将指针设置为NULL,要么让它指向正当的内存。譬喻

        char *p = NULL;

        char *str = (char *) malloc(100);

     

    (2)指针p被free也许delete之后,没有置为NULL,让人误认为p是个正当的指针。参见7.5节。

     

    (3)指针应用跨越了变量的陶染领域。这种情况让人防不胜防,示例序次递次如下:

        >

    {  

    public:

        void Func(void){ cout << “Func of >” << endl; }

    };

        void Test(void)

    {

        A  *p;

            {

                A  a;

               p = &a;    // 细心 a 的生命期

    }

            p->Func();      // p是“野指针”

    }

     

    函数Test在测验考试语句p->Func()时,东西a曾经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运转这个序次递次时居然没有出错,这可以也许与编译器有关。

     

    7.8 有了malloc/free为什么还要new/delete ?

           malloc与free是C /C语言的尺度库函数,new/delete是C 的运算符。它们都可用于请求静态内存和释放内存。

    对于非外部数据规范的东西而言,光用maloc/free无法惬意静态东西的要求。东西在成立的同时要自动测验考试机关函数,东西在消亡之前要自动测验考试析构函数。由于malloc/free是库函数而不是运算符,不在编译器节制权限之内,不可以把测验考试机关函数和析构函数的义务强加于malloc/free。

           是以C 语言需求一个能完成静态内存分派和初始化任务的运算符new,以及一个能完成清理与释放内存任务的运算符delete。细心new/delete不是库函数。

    我们先看一看malloc/free和new/delete如何完成东西的静态内存管理,见教例7-8。

     

    >

    {

    public :

            Obj(void){ cout << “Initialization” << endl; }

    ~Obj(void){ cout << “Destroy” << endl; }

    void    Initialize(void){ cout << “Initialization” << endl; }

    void    Destroy(void){ cout << “Destroy” << endl; }

    };

    void UseMallocFree(void)

    {

        Obj  *a = (obj *)malloc(sizeof(obj));   // 请求静态内存

        a->Initialize();                        // 初始化

        //…

        a->Destroy();   // 断根任务

        free(a);        // 释放内存

    }

    void UseNewDelete(void)

    {

        Obj  *a = new Obj;  // 请求静态内存并且初始化

        //…

        delete a;           // 断根并且释放内存

    }

    示例7-8 用malloc/free和new/delete如何完成东西的静态内存管理

     

    类Obj的函数Initialize模拟告终构函数的成果,函数Destroy模拟了析构函数的成果。函数UseMallocFree中,由于malloc/free不能测验考试机关函数与析构函数,必需挪用成员函数Initialize和Destroy来完成初始化与断根任务。函数UseNewDelete则复杂得多。

    所以我们不要妄想用malloc/free来完成静态东西的内存管理,应该用new/delete。由于外部数据规范的“东西”没有机关与析构的历程,对它们而言malloc/free和new/delete是等价的。

        既然new/delete的成果完全掩饰笼罩了malloc/free,为什么C 不把malloc/free添加出局呢?这是由于C 序次递次但凡要挪用C函数,而C序次递次只能用malloc/free管理静态内存。

    假定用free释放“new成立的静态东西”,那么该东西因无法测验考试析构函数而可以也许招致序次递次出错。假定用delete释放“malloc请求的静态内存”,理论上讲序次递次不会出错,但是该序次递次的可读性很差。所以new/delete必需配对运用,malloc/free也一样。

    7.9 内存耗尽如何办?

           假定在请求静态内存时找不到充沛大的内存块,malloc和new将前往NULL指针,宣告内存请求失败。但凡有三种编制处置赏罚“内存耗尽”成果。

    (1)剖断指针能否为NULL,假定是则顿时用return语句停止本函数。譬喻:

    void Func(void)

    {

    A  *a = new A;

    if(a == NULL)

    {

        return;

        }


    }

     

    (2)剖断指针能否为NULL,假定是则顿时用exit(1)停止整个序次递次的运转。譬喻:

    void Func(void)

    {

    A  *a = new A;

    if(a == NULL)

    {

        cout << “Memory Exhausted” << endl;

        exit(1);

    }

        …

    }

     

    (3)为new和malloc设置十分处置赏罚函数。譬喻Visual C 可以用_set_new_hander函数为new设置用户自己定义的十分处置赏罚函数,也可以让malloc享用与new相反的十分处置赏罚函数。细致内容请参考C 运用手册。

     

           上述(1)(2)编制运用最广泛。假定一个函数内有多处需求请求静态内存,那么编制(1)就显得力所能及(释放内存很费事),应该用编制(2)来处置赏罚。

    许多人不忍心用exit(1),问:“不编写出错处置赏罚序次递次,让应用琐屑自己处置行不可?”

           不可。假定发生“内存耗尽”多么的事情,普通说来应用序次递次曾经无药可救。假定不必exit(1) 把坏序次递次杀物化,它可以也许会害物化应用琐屑。事理如同:假定不把坏人击毙,坏人在老物化之前会犯下更多的罪。

     

           有一个很首要的征象要通知大师。对于32位以上的应用序次递次而言,无论如何运用malloc与new,几乎不能够招致“内存耗尽”。我在Windows 98下用Visual C 编写了测试序次递次,见教例7-9。这个序次递次会无胁制地运转下去,基础内幕不会停止。由于32位应用琐屑支撑“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,Window 98曾经累得对键盘、鼠标毫无回响反映。

    我可以得出这么一个结论:对于32位以上的应用序次递次,“内存耗尽”错误处置赏罚序次递次毫无用途。这下可把Unix和Windows序次递次员们乐坏了:反正错误处置赏罚序次递次不起陶染,我就不写了,省了许多费事。

    我不想误导读者,必需夸大:不加错误处置赏罚将招致序次递次的质量很差,万万不可因小失大。

     

    void main(void)

    {

        float *p = NULL;

        while(TRUE)

        {

            p = new float[1000000];

            cout << “eat memory” << endl;

            if(p==NULL)

                exit(1);

        }

    }

    示例7-9试图耗尽应用琐屑的内存

    7.10 malloc/free 的运用要点

        函数malloc的原型如下:

            void * malloc(size_t size);

        用malloc请求一块长度为length的整数规范的内存,序次递次如下:

            int  *p = (int *) malloc(sizeof(int) * length);

    我们应当把详积极集合在两个要素上:“规范转换”和“sizeof”。

    u       malloc前往值的规范是void *,所以在挪用malloc时要显式地中断规范转换,将void * 转换成所需求的指针规范。

    u       malloc函数自己并不识别要请求的内存是什么规范,它只谅解内存的总字节数。我们但凡记不住int, float等数据规范的变量的稳当字节数。譬喻int变量在16位琐屑下是2个字节,在32位下是4个字节;而float变量在16位琐屑下是4个字节,在32位下也是4个字节。最好用以下序次递次作一次测试:

    cout << sizeof(char) << endl;

    cout << sizeof(int) << endl;

    cout << sizeof(unsigned int) << endl;

    cout << sizeof(long) << endl;

    cout << sizeof(unsigned long) << endl;

    cout << sizeof(float) << endl;

    cout << sizeof(double) << endl;

        cout << sizeof(void *) << endl;

       

        在malloc的“()”中运用sizeof运算符是杰出的气焰气宇,但要留神偶尔我们会昏了头,写出 p = malloc(sizeof(p))多么的序次递次来。

     

    u       函数free的原型如下:

    void free( void * memblock );

        为什么free函数不象malloc函数那样严重呢?这是由于指针p的规范以及它所指的内存的容量事前都是晓得的,语句free(p)能切确地释放内存。假定p是NULL指针,那么free对p无论应用几何次都不会出成果。假定p不是NULL指针,那么free对p持续应用两次就会招致序次递次运转错误。

    7.11 new/delete 的运用要点

           运算符new运用起来要比函数malloc复杂得多,譬喻:

    int  *p1 = (int *)malloc(sizeof(int) * length);

    int  *p2 = new int[length];

    这是由于new内置了sizeof、规范转换和规范平静反省成果。对于非外部数据规范的东西而言,new在成立静态东西的同时完成了初始化任务。假定东西有多个机关函数,那么new的语句也可以有多种方式。譬喻

    >

    {

    public :

        Obj(void);      // 无参数的机关函数

        Obj(int x);     // 带一个参数的机关函数


    }

    void Test(void)

    {

        Obj  *a = new Obj;

        Obj  *b = new Obj(1);    // 初值为1

        …

        delete a;

        delete b;

    }

    假定用new成立东西数组,那么只能运用东西的无参数机关函数。譬喻

        Obj  *objects = new Obj[100];    // 成立100个静态东西

    不能写成

        Obj  *objects = new Obj[100](1);// 成立100个静态东西的同时赋初值1

    在用delete释放东西数组时,留神不要丢了标志‘[]’。譬喻

        delete []objects;    // 切确的用法

    delete objects;    // 错误的用法

    后者相等于delete objects[0],漏失了别的99个东西。

    7.12 一些心得体会

    我熟习不少技术不错的C /C序次递次员,很少有人能拍拍胸脯说知晓指针与内存管理(网罗我自己)。我最后学习C语言时特别怕指针,招致我斥地第一个应用软件(约1万行C代码)时没有运用一个指针,全用数组来顶替指针,其实拙笨得过度。逃避指针不是办法,厥后我改写了这个软件,代码量减少到原先的一半。

    我的经历经历是:

    (1)越是怕指针,就越要运用指针。不会切确运用指针,一定算不上是合格的序次递次员。

    (2)必需养成“运用调试器慢慢跟踪序次递次”的习气,只需多么才干发明成果的本色。



    版权声明: 原创作品,批准转载,转载时请务必以超链接方式标明文章 原始出处 、作者信息和本声明。否则将清查执法责任。

  • 相关阅读:
    SQL第3课:具有约束的查询(第2部分)
    SQL第1课:SELECT查询
    idea快捷键
    Vue基础
    分布式基础
    数据结构-线性表
    常用算法
    数据结构-概述
    Django使用Jinja2模板引擎
    宿主机nginx使用容器php-fpm处理php请求
  • 原文地址:https://www.cnblogs.com/zgqjymx/p/1974604.html
Copyright © 2020-2023  润新知