精华内容
下载资源
问答
  • C++虚析构函数及纯虚析构函数

    千次阅读 2016-09-14 23:40:49
    C++中析构函数可以为纯虚吗? 众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢? class CBase { public: CBase() { ...

    C++中析构函数可以为纯虚吗?

    众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢?
    class CBase
    {
    public:
        CBase()
        {
            printf("CBase()\n");
        }
        virtual ~CBase() = 0;
    };
    答案是可以,那么这样实现的目的是什么呢?当然是避免实例化。

    但因为派生类不可能来实现基类的析构函数,所以基类析构函数虽然可以标为纯虚,但是仍必须实现析构函数,否则派生类无法继承,也无法编译通过。

    下面详细讨论:


    一. 虚析构函数

    我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。
    如:
    class Base  
    {  
    public:  
        Base(){}  
        virtual ~Base(){}  
    };  
      
    class Derived: public Base  
    {  
    public:  
        Derived(){};  
        ~Derived(){};  
    }  
      
    void foo()  
    {  
       Base *pb;  
       pb = new Derived;  
       delete pb;  
    }   
    这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数。
    如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。

    因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base。


    二. 纯虚析构函数

    现在的问题是,我们想把Base做成抽象类,不能直接构造对象,需要在其中定义一个纯虚函数。如果其中没有其他合适的函数,可以把析构函数定义为纯虚的,即将前面的定义改成:
    class Base  
    {  
        public:  
        Base(){}  
        virtual ~Base() = 0  
    };  
    可是,这段代码不能通过编译,通常是link错误,不能找到~Base()的引用(gcc的错误报告)。这是因为,析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。也就是,Derived的析构函数里面隐含调用了Base的析构函数。而刚才的代码中,缺少~Base()的函数体,当然会出现错误。
    这里面有一个误区,有人认为,virtual f()=0这种纯虚函数语法就是没有定义体的语义。
    其实,这是不对的。这种语法只是表明这个函数是一个纯虚函数,因此这个类变成了抽象类,不能产生对象。我们完全可以为纯虚函数指定函数体。通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。这样,我们就有了一个纯虚析构函数的函数体,上面的代码需要改成:
    class Base  
    {  
    public:  
        Base(){}  
        virtual ~Base() = 0; //pure virtual  
    };  
    Base::~Base() //function body  
    {  
    }   
    从语法角度来说,不可以将上面的析构函数直接写入类声明中(内联函数的写法)。这或许是一个不正交化的地方。但是这样做的确显得有点累赘。
    这个问题看起来有些学术化,因为一般我们完全可以在Base中找到一个更加适合的函数,通过将其定义为没有实现体的纯虚函数,而将整个类定义为抽象类。但这种技术也有一些应用,如这个例子:
    class Base  //abstract class  
    {  
    public:  
        virtual ~Base(){};//virtual, not pure  
        virtual void Hiberarchy() const = 0;//pure virtual  
    };  
      
    void Base::Hiberarchy() const //pure virtual also can have function body  
    {  
       std::cout <<"Base::Hiberarchy";  
    }  
      
    class Derived : public Base  
    {  
    public:  
        Derived(){}  
        virtual void Hiberarchy() const  
        {  
            Base::Hiberarchy();  
            std::cout <<"Derived::Hiberarchy";  
        }  
        virtual void foo(){}  
    };  
      
    int main()  
    {  
       Base* pb=new Derived();  
       pb->Hiberarchy();  
       pb->Base::Hiberarchy();  
       return 0;  
    }   
    在这个例子中,我们试图打印出类的继承关系。在根基类中定义了虚函数Hiberarchy,然后在每个派生类中都重载此函数。我们再一次看到,由于想把Base做成个抽象类,而这个类中没有其他合适的方法成员可以定义为纯虚的,我们还是只好将Hiberarchy定义为纯虚的。(当然,完全可以定义~Base函数为纯虚的,这就和上面的讨论一样了。)
    另外,可以看到,在main中有两种调用方法,第一种是普通的方式,进行动态链接,执行虚函数,得到结果"Derived::Hiberarchy";第二种是指定类的方式,就不再执行虚函数的动态链接过程了,结果是"Base::Hiberarchy"。
    通过上面的分析可以看出,定义纯虚函数的真正目的是为了定义抽象类,而并不是函数本身。与之对比,在java中,定义抽象类的语法是 abstract class,也就是在类的一级作指定(当然虚函数还是也要加上abstract关键字)。是不是这种方式更好一些呢?在Stroustrup的《C++语言的设计与演化》中我找到这样一段话:
    “我选择的是将个别的函数描述为纯虚的方式,没有采用将完整的类声明定义为抽象的形式,这是因为纯虚函数的概念更加灵活一些。我很看重能够分阶段定义类的能力;也就是说,我发现预先定义一些纯虚函数,并把另外一些留给进一步的派生类去定义也是很有用的”。
    我还没有完全理解后一句话,我想从另外一个角度来阐述这个概念。那就是,在一个多层复杂类结构中,中间层次的类应该具体化一些抽象函数,但很可能并不是所有的。中间类没必要知道是否具体化了所有的虚函数,以及其祖先已经具体化了哪些函数,只要关注自己的职责就可以了。也就是说,中间类没必要知道自己是否是一个真正的抽象类,设计者也就不用考虑是否需要在这个中间类的类级别上加上类似abstract的说明了。
    当然,一个语言的设计有多种因素,好坏都是各个方面的。这只是一个解释而已。


    最后,总结一下关于虚函数的一些常见问题:

    1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 
    2) 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。 
    3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
    4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 
    5) 纯虚函数通常没有定义体,但也完全可以拥有。
    6) 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 
    7) 非纯的虚函数必须有定义体,不然是一个错误。 
    8) 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。 
    展开全文
  • C++中析构函数可以为纯虚吗?  众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢? class CBase {  public:  CBase...
     
       众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢?
    
    class  CBase
    {
        public :
            CBase()
           {
                printf("CBase()\n");
           }
         virtual  ~CBase() = 0;
    };
    答案是可以,那么这样实现的目的是什么呢?当然是避免实例化。

    但因为派生类不可能来实现基类的析构函数,所以基类析构函数虽然可以标为纯虚,但是仍必须实现析构函数,否则派生类无法继承,也无法编译通过。

    下面详细讨论:

    一. 虚析构函数
    我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。

    如:
    class Base
    {
    	public:
       		Base(){}
       		virtual ~Base(){}
    };
    
    class Derived: public Base
    {
    	public:
       		Derived(){};
       		~Derived(){};
    }
    
    void foo()
    {
       Base *pb;
       pb = new Derived;
       delete pb;
    } 

    这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数

    如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。
    因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base

    二. 纯虚析构函数
    现在的问题是,我们想把Base做出抽象类,不能直接构造对象,需要在其中定义一个纯虚函数。如果其中没有其他合适的函数,可以把析构函数定义为纯虚的,即将前面的CObject定义改成:
    class Base
    {
    	public:
       		Base(){}
       		virtual ~Base()= 0
    };

    可是,这段代码不能通过编译,通常是link错误,不能找到~Base()的引用(gcc的错误报告)。这是因为,析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。也就是,Derived的析构函数里面隐含调用了Base的析构函数。而刚才的代码中,缺少~Base()的函数体,当然会出现错误。

    这里面有一个误区,有人认为,virtual f()=0这种纯虚函数语法就是没有定义体的语义。
    其实,这是不对的。这种语法只是表明这个函数是一个纯虚函数,因此这个类变成了抽象类,不能产生对象。我们 完全可以为纯虚函数指定函数体  通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。这样,我们就有了一个纯虚析构函数的函数体,上面的代码需要改成:
    class Base
    {
    	public:
       		Base(){}
       		virtual ~Base() = 0; //pure virtual
    };
    Base::~Base()//function body
    {
    } 
    从语法角度来说,不可以将上面的析构函数直接写入类声明中(内联函数的写法)。这或许是一个不正交化的地方。但是这样做的确显得有点累赘

    这个问题看起来有些学术化,因为一般我们完全可以在Base中找到一个更加适合的函数,通过将其定义为没有实现体的纯虚函数,而将整个类定义为抽象类。但这种技术也有一些应用,如这个例子:
    class Base  //abstract class
    {
    	public:
       		virtual ~Base(){};//virtual, not pure
       		virtual void Hiberarchy() const = 0;//pure virtual
    };
    
    void Base::Hiberarchy() const //pure virtual also can have function body
    {
       std::cout <<"Base::Hiberarchy";
    }
    
    class Derived : public Base
    {
    	public:
       		Derived(){}
       		virtual void Hiberarchy() const
       		{
           		Base::Hiberarchy();
           		std::cout <<"Derived::Hiberarchy";
       		}
       		virtual void foo(){}
    };
    
    
    int main()
    {
       Base* pb=new Derived();
       pb->Hiberarchy();
       pb->Base::Hiberarchy();
       return 0;
    } 
    在这个例子中,我们试图打印出类的继承关系。在根基类中定义了虚函数Hiberarchy,然后在每个派生类中都重载此函数。我们再一次看到,由于想把Base做成个抽象类,而这个类中没有其他合适的方法成员可以定义为纯虚的,我们还是只好将Hiberarchy定义为纯虚的。(当然,完全可以定义~Base函数,这就和上面的讨论一样了。^_^)

    另外,可以看到,在main中有两种调用方法,第一种是普通的方式,进行动态链接,执行虚函数,得到结果"Derived::Hiberarchy";第二种是指定类的方式,就不再执行虚函数的动态链接过程了,结果是"Base::Hiberarchy"。

    通过上面的分析可以看出, 定义纯虚函数的真正目的是为了定义抽象类 ,而并不是函数本身。与之对比,在java中,定义抽象类的语法是 abstract class,也就是在类的一级作指定(当然虚函数还是也要加上abstract关键字)。是不是这种方式更好一些呢?在Stroustrup的《C++语言的设计与演化》中我找到这样一段话:

    “我选择的是将个别的函数描述为纯虚的方式,没有采用将完整的类声明定义为抽象的形式,这是因为纯虚函数的概念更加灵活一些。我很看重能够分阶段定义类的能力;也就是说,我发现预先定义一些纯虚函数,并把另外一些留给进一步的派生类去定义也是很有用的”。

    我还没有完全理解后一句话,我想从另外一个角度来阐述这个概念。那就是,在一个多层复杂类结构中,中间层次的类应该具体化一些抽象函数,但很可能并不是所有的。中间类没必要知道是否具体化了所有的虚函数,以及其祖先已经具体化了哪些函数,只要关注自己的职责就可以了。也就是说,中间类没必要知道自己是否是一个真正的抽象类,设计者也就不用考虑是否需要在这个中间类的类级别上加上类似abstract的说明了。

    当然,一个语言的设计有多种因素,好坏都是各个方面的。这只是一个解释而已。

    最后,总结一下关于虚函数的一些常见问题:

    1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。 

    2)  构造函数不能是虚函数 。而且, 在构造函数中调用虚函数,实际执行的是父类的对应函数 ,因为自己还没有构造好, 多态是被disable的。 

    3)  析构函数可以是虚函数 ,而且,在一个复杂类结构中,这往往是必须的。
     
    4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。 

    5)  纯虚函数通常没有定义体,但也完全可以拥有

    6)  析构函数可以是纯虚的,但 纯虚析构函数必须有定义体 ,因为析构函数的调用是在子类中隐含的。 

    7) 非纯的虚函数必须有定义体,不然是一个错误。 

    8) 派生类的override虚函数定义必须和父类完全一致。 除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生 。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。 

    转自:http://blog.csdn.net/fisher_jiang/article/details/2477577

    展开全文
  • C++中析构函数可以为纯虚函数吗? 众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢? class CBase { public: CBase() ...

    C++中析构函数可以为纯虚函数吗?

    众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢?

    class CBase
    {
        public:
            CBase()
           {
                printf("CBase()\n");
           }
        virtual ~CBase() = 0; // 析构函数是纯虚函数
    };
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    答案是可以,那么这样实现的目的是什么呢?当然是避免实例化

    但因为派生类不可能来实现基类的析构函数,所以基类析构函数虽然可以标为纯虚,但是仍必须实现析构函数,否则派生类无法继承,也无法编译通过

    下面详细讨论:

    虚析构函数

    我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。

    如:

    class Base  
    {  
        public:  
            Base(){}  
            virtual ~Base(){}  
    };  
    
    class Derived: public Base  
    {  
        public:  
            Derived(){};  
            ~Derived(){};  
    }  
    
    void foo()  
    {  
       Base *pb;  
       pb = new Derived;  
       delete pb;  
    } 
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数

    如果析构函数不加virtualdelete pb只会执行Base的析构函数,而不是真正的Derived析构函数。

    因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base

    纯虚析构函数

    现在的问题是,我们想把Base做出抽象类,不能直接构造对象,需要在其中定义一个纯虚函数。如果其中没有其他合适的函数,可以把析构函数定义为纯虚的,即将前面的CObject定义改成:

    class Base  
    {  
        public:  
            Base(){}  
            virtual ~Base()= 0;  
    }; 
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可是,这段代码不能通过编译,通常是link错误,不能找到~Base()的引用(gcc的错误报告)。

    error LNK2019: unresolved external symbol "public: virtual __thiscall Base::~Base(void)" (??1Base@@UAE@XZ) referenced in function "public: virtual __thiscall Derived::~Derived(void)" (??1Derived@@UAE@XZ)
    1>D:\C++\exercise\Debug\exercise.exe : fatal error LNK1120: 1 unresolved externals
     
    • 1
    • 2
    • 1
    • 2

    这是因为,析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。也就是,Derived的析构函数里面隐含调用了Base的析构函数。而刚才的代码中,缺少~Base()的函数体,当然会出现错误。

    这里面有一个误区,有人认为,virtual f()=0这种纯虚函数语法就是没有定义体的语义。

    其实,这是不对的。这种语法只是表明这个函数是一个纯虚函数因此这个类变成了抽象类,不能产生对象。我们完全可以为纯虚函数指定函数体 。通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。这样,我们就有了一个纯虚析构函数的函数体,上面的代码需要改成:

    class Base  
    {  
        public:  
            Base(){}  
            virtual ~Base() = 0; //pure virtual  
    };  
    Base::~Base()//function body  
    {  
    }  
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    从语法角度来说,不可以将上面的析构函数直接写入类声明中(内联函数的写法)。这或许是一个不正交化的地方。但是这样做的确显得有点累赘

    这个问题看起来有些学术化,因为一般我们完全可以在Base中找到一个更加适合的函数,通过将其定义为没有实现体的纯虚函数,而将整个类定义为抽象类。但这种技术也有一些应用,如这个例子:

    class Base  //abstract class  
    {  
        public:  
            virtual ~Base(){};//virtual, not pure  
            virtual void Hiberarchy() const = 0;//pure virtual  
    };  
    
    void Base::Hiberarchy() const //pure virtual also can have function body  
    {  
       std::cout <<"Base::Hiberarchy";  
    }  
    
    class Derived : public Base  
    {  
        public:  
            Derived(){}  
            virtual void Hiberarchy() const  
            {  
                Base::Hiberarchy();  
                std::cout <<"Derived::Hiberarchy";  
            }  
            virtual void foo(){}  
    };  
    
    
    int main()  
    {  
       Base* pb=new Derived();  
       pb->Hiberarchy();  
       pb->Base::Hiberarchy();  
       return 0;  
    }   
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这个例子中,我们试图打印出类的继承关系。在根基类中定义了虚函数Hiberarchy,然后在每个派生类中都重载此函数。我们再一次看到,由于想把Base做成个抽象类,而这个类中没有其他合适的方法成员可以定义为纯虚的,我们还是只好将Hiberarchy定义为纯虚的。(当然,完全可以定义~Base函数,这就和上面的讨论一样了。^_^)

    另外,可以看到,在main中有两种调用方法,第一种是普通的方式,进行动态链接,执行虚函数,得到结果"Derived::Hiberarchy";第二种是指定类的方式,就不再执行虚函数的动态链接过程了,结果是"Base::Hiberarchy"

    通过上面的分析可以看出,定义纯虚函数的真正目的是为了定义抽象类,而并不是函数本身。与之对比,在Java中,定义抽象类的语法是 abstract class,也就是在类的一级作指定(当然虚函数还是也要加上abstract关键字)。是不是这种方式更好一些呢?在Stroustrup的《C++语言的设计与演化》中我找到这样一段话:

    “我选择的是将个别的函数描述为纯虚的方式,没有采用将完整的类声明定义为抽象的形式,这是因为纯虚函数的概念更加灵活一些。我很看重能够分阶段定义类的能力;也就是说,我发现预先定义一些纯虚函数,并把另外一些留给进一步的派生类去定义也是很有用的”。

    我还没有完全理解后一句话,我想从另外一个角度来阐述这个概念。那就是,在一个多层复杂类结构中,中间层次的类应该具体化一些抽象函数,但很可能并不是所有的。中间类没必要知道是否具体化了所有的虚函数,以及其祖先已经具体化了哪些函数,只要关注自己的职责就可以了。也就是说,中间类没必要知道自己是否是一个真正的抽象类,设计者也就不用考虑是否需要在这个中间类的类级别上加上类似abstract的说明了。

    当然,一个语言的设计有多种因素,好坏都是各个方面的。这只是一个解释而已。

    最后,总结一下关于虚函数的一些常见问题:

    1. 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。

    2. 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

    3. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的

    4. 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象

    5. 纯虚函数通常没有定义体,但也完全可以拥有, 甚至可以显示调用。

    6. 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的

    7. 非纯的虚函数必须有定义体,不然是一个错误。

    8. 派生类的override虚函数定义必须和父类完全一致(C++11中使用override进行编译器检查)。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的(也就是说override并不会检查返回值类型)。


    展开全文
  • 我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。 如: class Base { public...
    一. 虚析构函数
    

    我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。

    如:

    class  Base
    {
    public :
       Base(){}
       
    virtual   ~ Base(){}
    };

    class  Derived:  public  Base
    {
    public :
       Derived(){};
       
    ~ Derived(){};
    }

    void  foo()
    {
       Base 
    * pb;
       pb 
    =   new  Derived;
       delete pb;
    这是正确的用法,会发生动态绑定,它会先调用Derived的析构函数,然后是Base的析构函数

    如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。
    因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base

    二. 纯虚析构函数
    现在的问题是,我们想把Base做出抽象类,不能直接构造对象,需要在其中定义一个纯虚函数。如果其中没有其他合适的函数,可以把析构函数定义为纯虚的,即将前面的CObject定义改成:
    class  Base
    {
    public :
       Base(){}
       
    virtual   ~ Base() =   0
    };
    可是,这段代码不能通过编译,通常是link错误,不能找到~Base()的引用(gcc的错误报告)。这是因为,析构函数、构造函数和其他内部函数不一样,在调用时,编译器需要产生一个调用链。也就是,Derived的析构函数里面隐含调用了Base的析构函数。而刚才的代码中,缺少~Base()的函数体,当然会出现错误。

    这里面有一个误区,有人认为,virtual f()=0这种纯虚函数语法就是没有定义体的语义。
    其实,这是不对的。这种语法只是表明这个函数是一个纯虚函数,因此这个类变成了抽象类,不能产生对象。我们 完全可以为纯虚函数指定函数体 (http://www.research.att.com/~bs/bs_faq2.html#pure-virtual)。通常的纯虚函数不需要函数体,是因为我们一般不会调用抽象类的这个函数,只会调用派生类的对应函数。这样,我们就有了一个纯虚析构函数的函数体,上面的代码需要改成:
    class  Base
    {
    public :
       Base()
       {
       }
       
    virtual   ~ Base()  =   0 // pure virtual
    };

    Base::
    ~Base () // function body
    {

    从语法角度来说,不可以将上面的析构函数直接写入类声明中(内联函数的写法)。这或许是一个不正交化的地方。但是这样做的确显得有点累赘

    这个问题看起来有些学术化,因为一般我们完全可以在Base中找到一个更加适合的函数,通过将其定义为没有实现体的纯虚函数,而将整个类定义为抽象类。但这种技术也有一些应用,如这个例子:
    class  Base   // abstract class
    {
    public :
       
    virtual   ~ Base(){}; // virtual, not pure
        virtual   void  Hiberarchy()  const   =   0 ; // pure virtual
    };

    void  Base::Hiberarchy()  const   // pure virtual also can have function body
    {
       std::cout 
    << " Base::Hiberarchy " ;
    }

    class  Derived :  public  Base
    {
    public :
       Derived(){}
       
    virtual   void  Hiberarchy()  const
       {
           CB::Hiberarchy();
           std::cout 
    << " Derived::Hiberarchy " ;
       }
       
    virtual   void  foo(){}
    };


    int  main(){
       Base
    *  pb = new  Derived();
       pb
    -> Hiberarchy();
       pb
    -> Base::Hiberarchy();
       
    return   0 ;


    在这个例子中,我们试图打印出类的继承关系。在根基类中定义了虚函数Hiberarchy,然后在每个派生类中都重载此函数。我们再一次看到,由于想把Base做成个抽象类,而这个类中没有其他合适的方法成员可以定义为纯虚的,我们还是只好将Hiberarchy定义为纯虚的。(当然,完全可以定义~Base函数,这就和上面的讨论一样了。^_^)

    另外,可以看到,在main中有两种调用方法,第一种是普通的方式,进行动态链接,执行虚函数,得到结果"Derived::Hiberarchy";第二种是指定类的方式,就不再执行虚函数的动态链接过程了,结果是"Base::Hiberarchy"。

    通过上面的分析可以看出, 定义纯虚函数的真正目的是为了定义抽象类,而并不是函数本身。与之对比,在java中,定义抽象类的语法是 abstract class,也就是在类的一级作指定(当然虚函数还是也要加上abstract关键字)。是不是这种方式更好一些呢?在Stroustrup的《C++语言的设计与演化》中我找到这样一段话:

    “我选择的是将个别的函数描述为纯虚的方式,没有采用将完整的类声明定义为抽象的形式,这是因为纯虚函数的概念更加灵活一些。我很看重能够分阶段定义类的能力;也就是说,我发现预先定义一些纯虚函数,并把另外一些留给进一步的派生类去定义也是很有用的”。

    我还没有完全理解后一句话,我想从另外一个角度来阐述这个概念。那就是,在一个多层复杂类结构中,中间层次的类应该具体化一些抽象函数,但很可能并不是所有的。中间类没必要知道是否具体化了所有的虚函数,以及其祖先已经具体化了哪些函数,只要关注自己的职责就可以了。也就是说,中间类没必要知道自己是否是一个真正的抽象类,设计者也就不用考虑是否需要在这个中间类的类级别上加上类似abstract的说明了。

    当然,一个语言的设计有多种因素,好坏都是各个方面的。这只是一个解释而已。

    最后,总结一下关于虚函数的一些常见问题:

    1) 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。

    2) 构造函数不能是虚函数。而且, 在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

    3) 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
     
    4) 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。

    5) 纯虚函数通常没有定义体,但也完全可以拥有

    6)  析构函数可以是纯虚的,但 纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

    7) 非纯的虚函数必须有定义体,不然是一个错误。

    8) 派生类的override虚函数定义必须和父类完全一致。 除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。


    假“虚拟构造函数”的用处


    从字面来看,谈论“虚拟构造函数”没有意义。当有一个指针或引用,但是不知道其指向对象的真实类型是什么时,可以调用虚拟函数来完成特定类型(type-specific)对象的行为。仅当还没拥有一个对象但是又确切地知道想要的对象的类型时,才会调用构造函数。那么虚拟构造函数又从何谈起呢?

    很简单。尽管虚拟构造函数看起来好像没有意义,其实它们有非常大的用处.例如,假设编写一个程序,用来进行新闻报道的工作,每一条新闻报道都由文字或图片组成。可以这样管理它们:

    class NLComponent { //用于 newsletter components
    public: // 的抽象基类
    ... //包含至少一个纯虚函数
    };
    class TextBlock: public NLComponent {
    public:
    ... // 不包含纯虚函数
    };
    class Graphic: public NLComponent {
    public:
    ... // 不包含纯虚函数
    };
    class NewsLetter { // 一个 newsletter 对象
    public: // 由NLComponent 对象
    ... // 的链表组成
    private:
    list<NLComponent*> components;
    };

    在NewsLetter中使用的list类是一个标准模板类(STL)。list类型对象的行为特性有些象双向链表,尽管它没有以这种方法来实现。对象NewLetter不运行时就会存储在磁盘上。为了能够通过位于磁盘的替代物来建立Newsletter对象,让NewLetter的构造函数带有istream参数是一种很方便的方法。当构造函数需要一些核心的数据结构时,它就从流中读取信息:
    class NewsLetter {
    public:
    NewsLetter(istream& str);
    ...
    };
    此构造函数的伪代码是这样的:
    NewsLetter::NewsLetter(istream& str)
    {
    while (str) { [Page]
    从str读取下一个component对象;
    把对象加入到newsletter的 components对象的链表中去;
    }
    }

    或者,把这种技巧用于另一个独立出来的函数叫做readComponent,如下所示:
    class NewsLetter {
    public:
    ...
    private:
    // 为建立下一个NLComponent对象从str读取数据,
    // 建立component 并返回一个指针。
    static NLComponent * readComponent(istream& str);
    ...
    };
    NewsLetter::NewsLetter(istream& str)
    {
    while (str) {
    // 把readComponent返回的指针添加到components链表的最后,
    // \"push_back\" 一个链表的成员函数,用来在链表最后进行插入操作。
    components.push_back(readComponent(str));
    }
    }

    考虑一下readComponent所做的工作。它根据所读取的数据建立了一个新对象,或是TextBlock或是Graphic。因为它能建立新对象,它的行为与构造函数相似,而且因为它能建立不同类型的对象,我们称它为虚拟构造函数。虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。虚拟构造函数在很多场合下都有用处,从磁盘(或者通过网络连接,或者从磁带机上)读取对象信息只是其中的一个应用。

    还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数――也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝。因为这种行为特性,虚拟拷贝构造函数的名字一般都是copySelf,cloneSelf或者是象下面这样就叫做clone。很少会有函数能以这么直接的方式实现它:
    class NLComponent {
    public:
    // declaration of virtual copy constructor
    virtual NLComponent * clone() const = 0;
    ...
    };
    class TextBlock: public NLComponent {
    public:
    virtual TextBlock * clone() const // virtual copy

    { return new TextBlock(*this); } // constructor
    ...
    };
    class Graphic: public NLComponent {
    public:
    virtual Graphic * clone() const // virtual copy
    { return new Graphic(*this); } // constructor [Page]
    ...
    };

    正如我们看到的,类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。因此“拷贝”的含义与真正的拷贝构造函数相同。如果真正的拷贝构造函数只做了简单的拷贝,那么虚拟拷贝构造函数也做简单的拷贝。如果真正的拷贝构造函数做了全面的拷贝,那么虚拟拷贝构造函数也做全面的拷贝。如果真正的拷贝构造函数做一些奇特的事情,象引用计数或copy-on-write,那么虚拟构造函数也这么做。

    注意上述代码的实现利用了最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。如果函数的返回类型是一个指向基类的指针(或一个引用),那么派生类的函数可以返回一个指向基类的派生类的指针(或引用)。这不是C++的类型检查上的漏洞,它使得有可能声明象虚拟构造函数这样的函数。这就是为什么TextBlock的clone函数能够返回TextBlock*和Graphic的clone能够返回Graphic*的原因,即使NLComponent的clone返回值类型为NLComponent*。

    在NLComponent中的虚拟拷贝构造函数能让实现NewLetter的(正常的)拷贝构造函数变得很容易:
    class NewsLetter {
    public:
    NewsLetter(const NewsLetter& rhs);
    ...
    private:
    list<NLComponent*> components;
    };
    NewsLetter::NewsLetter(const NewsLetter& rhs)
    {
    // 遍历整个rhs链表,使用每个元素的虚拟拷贝构造函数
    // 把元素拷贝进这个对象的component链表。
    for (list<NLComponent*>::const_iterator it =
    rhs.components.begin();
    it != rhs.components.end();
    ++it) {
    // \"it\" 指向rhs.components的当前元素,调用元素的clone函数,
    // 得到该元素的一个拷贝,并把该拷贝放到
    // 这个对象的component链表的尾端。
    components.push_back((*it)->clone());
    }
    }
    遍历被拷贝的NewsLetter对象中的整个component链表,调用链表内每个元素对象的虚拟构造函数。我们在这里需要一个虚拟构造函数,因为链表中包含指向NLComponent对象的指针,但是我们知道其实每一个指针不是指向TextBlock对象就是指向Graphic对象。无论它指向谁,我们都想进行正确的拷贝操作,虚拟构造函数能够为我们做到这点。

    虚拟化非成员函数
    就象构造函数不能真的成为虚拟函数一样,非成员函数也不能成为真正的虚拟函数。然而,既然一个函数能够构造出不同类型的新对象是可以理解的,那么同样也存在这样的非成员函数,可以根据参数的不同动态类型而其行为特性也不同。例如,假设你想为TextBlock和Graphic对象实现一个输出操作符。显而易见的方法是虚拟化这个输出操作符。但是输出操作符是operator<<,函数把ostream&做为它的左参数(left-hand argument)(即把它放在函数参数列表的左边),这就不可能使该函数成为TextBlock 或 Graphic成员函数。 [Page]
    (这样做也可以,不过看一看会发生什么:
    class NLComponent {
    public:
    // 对输出操作符的不寻常的声明
    virtual ostream& operator<<(ostream& str) const = 0;
    ...
    };
    class TextBlock: public NLComponent {
    public:
    // 虚拟输出操作符(同样不寻常)
    virtual ostream& operator<<(ostream& str) const;
    };
    class Graphic: public NLComponent {
    public:
    // 虚拟输出操作符 (不寻常)
    virtual ostream& operator<<(ostream& str) const;
    };
    TextBlock t;
    Graphic g;
    ...
    t << cout; // 通过virtual operator<<
    //把t打印到cout中。
    // 不寻常的语法
    g << cout; //通过virtual operator<<

    //把g打印到cout中。
    //不寻常的语法

    类的使用者得把stream对象放到<<符号的右边,这与输出操作符一般的用法相反。为了能够回到正常的语法上来,我们必须把operator<<移出TextBlock 和 Graphic类,但是如果我们这样做,就不能再把它声明为虚拟了。)

    另一种方法是为打印操作声明一个虚拟函数(例如print)把它定义在TextBlock 和 Graphic类里。但是如果这样,打印TextBlock 和 Graphic对象的语法就与使用operator<<做为输出操作符的其它类型的对象不一致了,这些解决方法都不很令人满意。我们想要的是一个称为operator<<的非成员函数,其具有象print虚拟函数的行为特性。有关我们想要什么的描述实际上已经很接近如何得到它的描述。我们定义operator<< 和print函数,让前者调用后者! [Page]
    class NLComponent {
    public:
    virtual ostream& print(ostream& s) const = 0;
    ...
    };
    class TextBlock: public NLComponent {
    public:
    virtual ostream& print(ostream& s) const;
    ...
    };
    class Graphic: public NLComponent {
    public:
    virtual ostream& print(ostream& s) const;
    ...
    };
    inline
    ostream& operator<<(ostream& s, const NLComponent& c)
    {
    return c.print(s);
    }

    具有虚拟行为的非成员函数很简单。编写一个虚拟函数来完成工作,然后再写一个非虚拟函数,它什么也不做只是调用这个虚拟函数。为了避免这个句法花招引起函数调用开销,当然可以内联这个非虚拟函数。

    现在你知道如何根据它们的一个参数让非成员函数虚拟化,你可能想知道是否可能让它们根据一个以上的参数虚拟化呢?可以,但是不是很容易。




    展开全文
  • 析构函数

    2020-03-27 19:07:00
    析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。 首先看一段示例代码: class A { public: ~A(){} }; class AX : public A { public: ~AX...
  • C++中析构函数可以为纯虚函数吗? 众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用。那么析构函数是否可以设为纯虚呢? class CBase { public: CBase() ...
  • 我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。 如: class Base { public: ...
  • 目录 0. 整体概述为什么要引入构造函数 1. 声明和定义构造函数 程序声明对象时,将自动调用构造函数 1.1 成员名和参数名 ...什么时候调用析构函数呢? 5 改进Stock类 stock10.h Stock10.cpp ...
  • 析构函数 (C++)

    2018-08-23 09:48:00
    最近发现自己对析构函数的认知有一定的问题,因为之前有在使用placement new时主动调用对象的析构函数,所以觉得析构函数只是个普通的成员函数,调用的时候只会执行自己方法体内的代码内容,而回收内存则是由于生命...
  • 文章目录析构函数概念析构函数特性要点总结: 析构函数概念 前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的? 与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作...
  • 这里我们主要讨论构造函数、析构函数、普通成员函数、虚函数,这几种函数说说自己的理解。 构造函数的总结 构造函数,我们先来看看如下的代码 #include using namespace std; class Base{ public: ...
  • 虚函数与构造函数、析构函数   1、构造函数能不能是虚函数:  1.1从存储空间角度 虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚...
  • 我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。 如: class Base { ...
  •  析构函数的作用在于完成对象销毁的一些“善后工作”,然而,某些不科学的设计会产生一些问题。  本文将说明其中的一种不科学设计 - "将异常处理机制设定在析构函数中" 会产生的问题,以及解决方案。 问题描述 ...
  • 析构函数的作用在于完成对象销毁的一些“善后工作”,然而,某些不科学的设计会产生一些问题。 本文将说明其中的一种不科学设计 - "将异常处理机制设定在析构函数中" 会产生的问题,以及解决方案。 问题描述 ...
  • 1、构造函数能不能是虚函数:  1.1从存储空间角度 虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。...虚函数主要用于在信息不全的情况下,能使重载的函数得到
  • 类类型的拷贝构造函数与析构函数,成员中含有动态内存指针时的处理,C++11标准以下讨论的符号约定得出以下结论的实验代码(建议先看后面的内容)tips:什么是拷贝构造为什么要自定义析构函数为什么要自定义拷贝构造函数...
  • 关于虚析构函数

    2011-06-27 16:46:00
    2)当有下面两件事情同时发生时就需要虚析构函数了:  a)有需要析构函数的事情发生。比如需要通过指针销毁对象时。(需要析构函数)  b)它发生在这样一种上下文中:指向一个基类的指针或者引用都有一个...
  • 然后又虚函数中最特殊的虚析构函数的运行情况进行了分析。  1、类的存储空间  在INTEL 32 CPU,VC6环境下,空类的一个实例占一个字节(特例);  一个C++类本身(注意:不是对象),在内存里是有信息的, 比如...
  • 文章目录C++入门04,类与对象面向对象程序设计的基本特点类和对象类的定义类的成员访问控制(权限限定)类的成员函数程序实例构造函数和析构函数构造函数默认构造函数委托构造函数拷贝构造函数析构函数对构造函数和...
  • 析构函数遇到多线程

    千次阅读 2015-03-14 00:11:41
    摘要 编写线程安全的类不是难事,用同步原语保护内部状态即可。但是对象的生与死不能由对象自身拥有的互斥器来保护。如何保证即将析构对象 x 的时候,不会有另一个...或者说,如何保证在执行 x 的成员函数期间,

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,496
精华内容 6,598
关键字:

下面对析构函数的正确描述是