zoukankan      html  css  js  c++  java
  • 野指针

    讨论一

    什么是野指针?
      一个母亲有两个小孩(两个指针),一个在厨房,一个在卧室,(属于不同的代码块,其生存期不同)母亲让在厨房的小孩带一块蛋糕(指针指向的对象)给在卧室的小孩,这样在卧室的孩子才肯写作业。但这个在厨房的小孩比较淘气,他在走出厨房时自己将蛋糕吃了,没能带出来。而在卧室的没有吃到蛋糕,所以不肯完成他的作业。结果母亲却不知道卧室的孩子没有吃到蛋糕,还以为作业完了。结果第二天她就被老师召唤到办公室了。事情麻烦了。
      这样,那个在卧室的孩子就是野指针了,因为他没有得到应得的蛋糕,不能完成母亲交给他的作业。
      这就是c中所讲的野指针。上面的小剧本不过演示了一种最基本的野指针的形成过程。更容易出现的情形是coder在编码时,大意之下使用了已经free过的指针。
      对于年轻点的经验欠缺的coder来说是比较容易犯的错误,经验老到的程序员或者慎重采取成对编程的形式避免这种失误,或者使用引用计数器防止形成野指针。
      总之,在c中,野指针也许性子野,但是控制起来也是有章可循。然而事情在c++中出现了变化。
      coder们面临更大的麻烦了。c++程序员无可避免的要写很多这样那样的类。谁让c++是面向对象的呢?
      我们在写类的时候难免要用new给类的数据成员分配内存。这本来没什么,动态分配内存是一种很常见的基本操作,我们在学数据结构时经常这么做,不是么? 
      但是伙计,事情并非这么简单。类是一种高级的用户自定义数据类型,看起来和结构、枚举这样的用户自定义类型没啥太大差别。如果你这样认为....?那你会死的很惨。类太复杂了,普通情况下使用类的对象并没有太大的问题,但是,当你要复制一个对象时,问题就来了。
      比如我们知道,你要用一个对象初始化另一个对象时,c++是按位进行拷贝的,即在目标对象里创建了初始化对象的一个完全相同的拷贝。这在多数情况下已经足够了。但是,当你的类在创建时为每个对象分配内存,也就是说类中有new操作。当你的对象创建好后,类也为对象分配了一块内存。如果你用这个对象去初始化另一个对象时,被初始化的对象和初始化的对象完全一样。这意味着,他们使用同一块内存,而不是重新为被初始化的对象分配内存。
      这样麻烦就大了。如果一个对象销毁了,那么分配的内存也就销毁了(别忘了,类是有析构函数的,它负责在对象销毁时,释放动态分配的内存。难道你说你不在类中写上析构部分?那么可怜的孩子,那你就走向了另一个深渊,当你的程序运行数小时之后,系统会告诉你,内存不够用了。想象一下把你的程序用在腾讯的服务器上),另一个对象就残缺不全了,这就像一对连体婴儿,他们共用了一部分器官,心脏或者肝脏。要救活一个,就牺牲了另一个。一个得病了,另一个也要遭殃。
      可以说,这就是c++中更加变态的野指针。
      什么?你说我不用对象初始化对象?那么我们会不会将一个对象作为变元传递给函数呢?我们很多时候都这样做。有时我们不得不将对象按值传递给一个函数,但是你要知道,按值传递是什么意思?它的意思就是,把实参的一个拷贝传递给函数。这和刚才的初始化没什么两样,按位拷贝,函数体内的对象与外面的对象共用一块内存,即便在函数中的对象没有对这块内存进行过操作,但是当函数结束时。。。。析构函数将会被调用......
      还有一种与之相反的情况......, 当你想要把一个在函数内的对象值返回给外面的对象时,这时候,会自动产生一个临时对象,由它容纳函数的返回值,并在函数结束时把结果传给目标。那么这个临时对象迅速的被创建,并被迅速的释放。。。一块内存被释放了两次。其后果是不可预见的。 
      当你把一个对象的值赋给另一个对象时,如果你没有重载赋值运算符,那么也会导致按位拷贝。最终产生一个野指针(一个隐藏在类内的毒瘤),或者释放同一块内存多次。 
      看到了么?害怕了么?是不是感到C++到处都是陷阱呢?不但有陷阱,到处都是危险品。所有c中的疑难问题,到了c++就成了一般问题了。好了不废话了,我们继续讲讲解决之道。
      对于最后的这种赋值的情况,我们只有通过重载赋值运算符才能解决,也就是避免按位拷贝。
      至于前面的都属于初始化,概括下来就是三种情况:
      1.当一个对象初始化另一个对象时,例如在声明中;
      2.把所创建的对象拷贝(按值)传递给一个函数时;
      3.生成临时对象时,最常见的就是函数的返回值。
      解决初始化时的按位拷贝问题,我们通过创建拷贝构造函数来解决。
      基本的拷贝构造函数形式为:
    classname (const classname &o)
    {
    //body here
    }
      拷贝构造函数就是针对这个问题而设计的。

    讨论二

        野指针,也就是指向不可用内存区域的指针。通常对这种指针进行操作的话,将会使程序发生不可预知的错误。 
        “野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。野指针的成因主要有两种: 
        (1)、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。 
        (2)、指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。例: 
    char *p = (char *) malloc(100); 
    strcpy(p, “hello”); 
    free(p); // p 所指的内存被释放,但是p所指的地址仍然不变 
    if(p != NULL) // 没有起到防错作用 
    strcpy(p, “world”); // 出错 
        另外一个要注意的问题:不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放

        首先请诸位看以下一段“危险”的C++代码: 
    void function( void ) 

    char* str = new char[100]; 
    delete[] str; 
    // Do something 
    strcpy( str, "Dangerous!!" ); 

        之所以说其危险,是因为这是一段完全合乎语法的代码,编译的时候完美得一点错误也不会有,然而当运行到strcpy一句的时候,问题就会出现,因为在这之前,str的空间已经被delete掉了,所以strcpy当然不会成功。对于这种类似的情况,在林锐博士的书中有过介绍,称其为“野指针”。 
        那么,诸位有没有见过安全的“野指针”呢?下面请看我的一段C++程序,灵感来自CSDN上的一次讨论。在此,我只需要C++的“类”,C++的其余一概不需要,因此我没有使用任何的C++标准库,连输出都是用printf完成的。 
    #include <stdio.h> 
    class CTestClass 

    public: 
    CTestClass( void ); 
    int m_nInteger; 
    void Function( void ); 
    }; 
    CTestClass::CTestClass( void ) 

    m_nInteger = 0; 

    void CTestClass::Function( void ) 

    printf( "This is a test function. " ); 

    void main( void ) 

    CTestClass* p = new CTestClass; 
    delete p; 
    p->Function(); 

       OK,程序到此为止,诸位可以编译运行一下看看结果如何。你也许会惊异地发现:没有任何的出错信息,屏幕上竟然乖乖地出现了这么一行字符串: 
    This is a test function. 
        奇怪吗?不要急,还有更奇怪的呢,你可以把主函数中加上一句更不可理喻的: 
    ((CTestClass*)NULL)->Function(); 
        这仍然没有问题!! 
        我这还有呢,哈哈。现在你在主函数中这么写,倘说上一句不可理喻,那么以下可以叫做无法无天了: 
    int i = 888; 
    CTestClass* p2 = (CTestClass*)&i; 
    p2->Function(); 
        你看到了什么?是的,“This is a test function.”如约而至,没有任何的错误。 
        你也许要问为什么,但是在我解答你之前,请你在主函数中加入如下代码: 
    printf( "%d, %d", sizeof( CTestClass ), sizeof( int ) ); 
        这时你就会看到真相了:输出结果是——得到的两个十进制数相等。对,由sizeof得到的CTestClass的大小其实就是它的成员m_nInteger的大小。亦即是说,对于CTestClass的一个实例化的对象(设为a)而言,只有a.m_nInteger是属于a这个对象的,而a.Function()却是属于CTestClass这个类的。所以以上看似危险的操作其实都是可行且无误的。
        现在你明白为什么我的“野指针”是安全的了,那么以下我所列出的,就是在什么情况下,我的“野指针”不安全: 
        (1)在成员函数Function中对成员变量m_nInteger进行操作; 
        (2)将成员函数Function声明为虚函数(virtual)。 
        以上的两种情况,目的就是强迫野指针使用属于自己的东西导致不安全,比如第一种情况中操作本身的m_nInteger,第二种情况中变为虚函数的Function成为了属于对象的函数(这一点可以从sizeof看出来)。 
        其实,安全的野指针在实际的程序设计中是几乎毫无用处的。我写这一篇文章,意图并不是像孔乙己一样去琢磨回字有几种写法,而是想通过这个小例子向诸位写明白C++的对象实例化本质,希望大家不但要明白what和how,更要明白why。李马二零零三年二月二十日作于自宅。 
        关于成员函数CTestClass::Function的补充说明 :
        (1)这个函数是一个普通的成员函数,它在编译器的处理下,会成为类似如下的代码: 
    void Function( const CTestClass * this ) // ① 

    printf("This is a test function. "); 

        那么p->Function();一句将被编译器解释为: 
    Function( p ); 
        这就是说,普通的成员函数必须经由一个对象来调用(经由this指针激活②)。那么由上例的delete之后,p指针将会指向一个无效的地址,然而p本身是一个有效的变量,因此编译能够通过。并且在编译通过之后,由于CTestClass::Function的函数体内并未对这个传入的this指针进行任何的操作,所以在这里,“野指针”便成了一个看似安全的东西。
        然而若这样改写CTestClass::Function: 
    void CTestClass::Function( void ) 

    m_nInteger = 0; 

        那么它将会被编译器解释为: 
    void Function( const CTestClass * this ) 

    this->m_nInteger = 0; 

        你看到了,在p->Function();的时候,系统将会尝试在传入的这个无效地址中寻找m_nInteger成员并将其赋值为0,剩下的我不用说了——非法操作出现了。 
        至于virtual虚函数,如果在类定义之中将CTestClass声明为虚函数: 
    class CTestClass 

    public: 
    // ... 
    virtual void Function( void ); 
    }; 
        那么C++在构建CTestClass类的对象模型时,将会为之分配一个虚函数表vptr(可以从sizeof看出来)。vptr是一个指针,它指向一个函数指针的数组,数组中的成员即是在CTestClass中声明的所有虚函数。在调用虚函数的时候,必须经由这个vptr,这也就是为什么虚函数较之普通成员函数要消耗一些成本的缘故。以本例而言,p->Function();一句将被编译器解释为: 
    (*p->vptr[1])( p ); // 调用vptr表中索引号为1的函数(即Function)③ 
        上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。 
    备注: 
    ①关于函数的命名,我采用了原名而没有变化。事实上编译器为了避免函数重载造成的重名情况,会对函数的名字进行处理,使之成为独一无二的名称。 
    ②将成员函数声明为static,可以使成员函数不经由this指针便可调用。 
    ③vptr表中,索引号0为类的type_info。

    讨论三:

    先上代码,传说中的腾讯笔试题:
    #include 'stdafx.h'  
    #include <iostream> 
    #include <string> 
    using std::cout; 
    using std::endl; 
    class Test 

    public: 
    Test() 

    a = 9; 
    delete this; 

    ~Test() 

    cout<<'destructor called!'<<endl; 

    int a; 
    }; 

    int _tmain(int argc, _TCHAR* argv[]) 

    Test *mytest = new Test(); //mytest的值和this指针的值是一样一样的 
    cout<<mytest->a<<endl; 
    return 0; 

    请问运行结果如何?
        常见的回答,程序会报错,通不过编译。或者说编译通过,运行时报错,因为居然Test类的构造函数删除了this指针,相当于调用了Test类的析构函数,对象不再存在,所以访问成员变量a的时候出错。
        实际的结果是,程序可以通过编译,运行时不报错,只不过打印出a的值不是9,而是内存中一个随机垃圾值
        如果想让程序运行时出错,可以这样写main函数:
    Test mytest;
    cout<<mytest.a<<endl;
    return 0;
        这样mytest是局部对象,内存在栈上分配,delete this试图释放栈上的内存,因此会报错。
        下面的代码演示了这种情况。
    int a = 6;
    delete &a; //运行时报错
        继续上面的讨论,野指针是指在delete了一个指向动态对象的指针后,没有及时置为NULL,如果对该指针进行解除引用,就会产生垃圾值。
        一个铁的纪律,彻底杜绝野指针(这道题没办法,this不能做左值,况且即使改了this,mytest也是改不了的,不再考虑范围)delete了一个指向动态对象的指针后,及时置为NULL。相应的,对指针进行解除引用前,判断指针是否为NULL。

     
     
  • 相关阅读:
    第二十一章流 1流的操作 简单
    第二十章友元类与嵌套类 1友元类 简单
    第十九章 19 利用私有继承来实现代码重用 简单
    第二十章友元类与嵌套类 2嵌套类 简单
    第十九章 8链表类Node 简单
    第二十一章流 3用cin输入 简单
    第十九章 10 图书 药品管理系统 简单
    第十九章 11图书 药品管理系统 简单
    第二十一章流 4文件的输入和输出 简单
    第十九章 12 什么时候使用私有继承,什么时候使用包含 简单
  • 原文地址:https://www.cnblogs.com/skl374199080/p/4097164.html
Copyright © 2011-2022 走看看