c++笔记07---纯虚函数和抽象类/纯抽象类,虚函数表和动态/后期绑定

1.    纯虚函数和抽象类/纯抽象类
    形如:
        virtual 返回类型 成员函数名(行参表) = 0;
    的虚函数被称为纯虚函数;这里等于零只是一个标记,没有任何意义,跟赋值/初始化等都没关系;

    一个包含纯虚函数的类称为抽象类;抽象类不能实例化对象;
    如果一个类继承自抽象类,但是并没有为其抽象基类中的全部纯虚函数提供覆盖,那么该子类也是一个抽象类;
    
    除了构造和析构以外,所有的函数都是纯虚函数,称为纯抽象类;(静态成员不考虑)
        class A {
        public:
            virtual void foo() = 0;
            virtual void bar() = 0;
            virtual void fun() = 0;
        };
        class B : public A {
        private:
            void foo() {...}
        };
        class C : public B {
            void bar() {...}
        };
        class D : public C {
            void fun() {...}
        };
        此时 A 是纯抽象类;
        B 里有纯虚函数 bar 和 fun,所以 B 也是抽象类;
        C 里有纯虚函数 fun,C 也是抽象类;
        D 不是抽象类,因为里面没有纯虚函数;
        
2.    虚函数表和动态绑定/后期绑定
    1)虚函数表
        虚函数表是个数组,里面保存着成员函数的地址;
    2)动态绑定
        当编译器看到通过指向子类对象的基类指针或者引用子类对象的基类引用,
        调用基类中的虚函数时,并不急着生成函数代码,相反,会在该函数调用处生成若干指令;
        这些指令在程序的运行阶段被执行,完成如下动作:
            根据指针或引用的目标对象找到相应虚函数表的指针呢;
            根据虚函数表指针,找到虚函数的地址;
            根据虚函数地址,执行虚函数代码;
    
    由此可见,对虚函数的调用,只有在运行阶段才能够确定;所以也叫后期绑定或运行时绑定;
        class A {
            public:
                virtual void foo() {...};
                virtual void bar() {...};
        };
        class B : public A {
            public:
                void foo() {...}
        };
        int main() {
            A* pa = new B;    // 多态
            pa->foo();        // B::foo();
            pa->bar();        // A::bar();
            
            A a;
            void(**vft)() = *(void(***)())&a;                    // A 的虚函数表
            cout << (void*)vft[0] << (void*)vft[1] << endl;
            
            B b;
            vft = *(void(***)())&b;                                // B 的虚函数表
            cout << (void*)vft[0] << (void*)vft[1] << endl;
            
            A* pa = new A;
            pa->foo();        // A::foo();
            pa->bar();        // A::bar();
        }
        A* pa = new B; 这条语句执行顺序:
        编译器生成必要代码;
        运行期分四步:
            pa 首先找到其创建对象 B;(B 里面保存着虚函数表的首地址)
            通过 B 找到其虚函数表;(虚函数表是个数组,里面保存着成员函数的地址)
            通过虚函数表找到 foo 地址;
            最后运行 foo;
        重点:虚函数表是建立在对象基础上的,如果没对象,就根本没有虚函数表;

    3)动态绑定优点是实现了多态,缺点是对程序的性能会造成不利影响;
        所以如果不需要多态,就不要使用虚函数;

3.    运行时类型信息(RTTI)
    1)tpypeid 操作符,获得运行过程中,对象类型;
        返回 typeinfo 类型的对象的常引用;
        需要包含头文件:#include <typeinfo>
        typeinfo::name()      -->  以字符串的形式返回类型名称;
        typeinfo::operator==  -->  用于类型一致判断;
        typeinfo::operator!=  -->  用于类型不一致判断;
        例 1:
        int main() {
            cout << typeid(int).name();                            // 输出:i
            cout << typeid(unsigned int).name();                // 输出:j
            cout << typeid(double[10]).name();                    // 输出:A10_d(A 为数组,d 为 double)
            cout << typeid(char[3][4][5]).name();                // 输出:A3_A4_A5_c
            char*(*p[5])(int*, short*);
            cout << typeid(p).name();                            // 输出:A5_PFPcPiPsE(F 为函数,E 不用管,有的编译器不显示 E)
            cout << typeid(const char* const* const).name();    // 输出:PKPKc(P 为指针,K 为 const,c 为 char)
        }
        
        例 2:
        class A {};
        class BBB {};
        int main() {
            cout << typeid(A).name();                            // 输出:1A(1 为数组名单词个数,A 为数组名)
            cout << typeid(BBB).name();                            // 输出:3BBB
        }
        
        例 3:
        class A { virtual void foo() {} };                        // 或者写:virtual void foo() = 0;
        class B : public A {};
        void print(A* pa)                                         // 用于测试下面两个 new 调用对象;
            if(!strcmp(typeid(*pa).name(), "1A"))                // 如果等于 1A,则输出下面语句;strcmp 为真返回 0;
            // 也可以写成:if(typeid(*pa) == typeid(A))
                cout << "A对象";
            else if(!strcmp(typeid(*pa).name(), "1B"))
                cout << "B对象";
        }
        int main() {
            A* pa = new B;                                        // 多态
            cout << typeid(*pa).name();                            // 输出:1B
            cout << typeid(pa).name();                            // 输出:P1A
            print(new A);                                            // 输出:A对象
            print(new B);                                            // 输出:B对象
        }

    2)dynamic_cast 动态类型转换
        class A {
            public:
                virtual void foo(){}
        };
        class B : public A {};
        class C : public B {};
        class D {};
        int main() {
            B b;
            A* pa = &b;
            cout << pa;                        // 0xbf5412c5(因为 pa 指向 B 对象,转换成功)
            B* pb = dynamic_cast<B*>(pa);
            cout << pb;                        // 0xbf5412c5(转换成功)
            C* pc = dynamic_cast<C*>(pa);
            cout << pc;                        // 0(pa 没有指向 C 和 D 对象,返回 0,转换失败)
            D* pd = dynamic_cast<D*>(pa);    // 0(返回 0,转换失败)
            
            pb = static_cast<B*>(pa);        // 因为 B 是 A 的子类,基类任何时候都可以转换为子类,所以转换成功;
            cout << pb;                        // 0xbf5412c5(转换成功)
            pc = static_cast<C*>(pa);        // 同上,虽然转换成功,但是这个转换是危险的;
            cout << pc;                        // 0xbf5412c5(转换成功)
            pd = static_cast<D*>(pa);        // 转换失败,编译报错
            
            pb = reinterpre_cast<B*>(pa);    // 0xbf5412c5(转换成功)
            cout << pb;
            pc = reinterpre_cast<C*>(pa);    // 0xbf5412c5(转换成功)
            cout << pc;
            pd = reinterpre_cast<D*>(pa);    // 0xbf5412c5(转换成功)
            cout << pd;
            
            A& ra = b;
            C& rc = dynamic_cast<&C>(ra);    // 和指针一样,会转换失败,不是返回 0,而是抛出异常;
            return 0;
        }
        总结:针对指针:
        动态类型转换可以发现错误,如果转换失败,返回 0;
        静态类型转换危险,如果转换失败,编译器报错;
        重解释类型转换完全不做任何检查,危险;
        
        如果是引用,动态转换失败就会抛出异常,杀死进程;
        
        动态类型转换必须用在多态类型,否则编译错误;

4.    虚析构:将基类的析构函数声明为虚函数;
    例 1:
    class A {
        public:
            A() { cout << "A 构造" << endl; }
            ~A() { cout << "A 析构" << endl; }
    };
    class B : public A {
        public:
            B() { cout << "B 构造" << endl; }
            ~B() { cout << "B 析构" << endl; }
    };
    int main() {
        B* pb = new B;
        delete pb;
        return 0;
    }
    输出:
    A 构造
    B 构造
    B 析构
    A 析构
    
    例 2:
    class A {
        public:
            A() { cout << "A 构造" << endl; }
            ~A() { cout << "A 析构" << endl; }
    };
    class B : public A {
        public:
            B() { cout << "B 构造" << endl; }
            ~B() { cout << "B 析构" << endl; }
    };
    int main() {
        A* pa = new B;
        delete pa;
        return 0;
    }
    输出:
    A 构造
    B 构造
    A 析构
    这个例子中,B 的析构函数没有被执行;
    delete 一个指向子类对象的基类指针,实际被执行的是基类自己的析构函数;由指针类型决定;
    此时,子类并未被析构,会造成内存泄露;
        解决办法一:delete static_cast<B*>(pa);                        // 调用 B 的析构函数;
        解决办法二:virtual ~A() { cout << "A 析构" << endl; }        // 基类虚构定义为:虚析构函数

    通过上面两个办法,delete 一个指向子类对象的基类指针,实际被执行的将是子类的析构函数;
    而子类的析构函数可以自动调用基类的析构函数;
    进而保证了子类特有的资源和基类子对象中的资源都能够被释放,防止内存泄露;
    如果基类中存在虚函数,那么需要为其定义一个虚析构函数,即使该函数什么也不做;

    问:虚函数可以内联吗 ?
    答:虚函数、递归函数、复杂的函数都不能作内联优化;
    
    问:构造函数可以被定义为虚函数吗 ?
    答:不可以;虚函数调用依赖于虚函数表,虚函数表存在于对象里,而调用构造的时候,还没对象呢;
    
    问:一个类的静态成员函数可以被定义为虚函数吗 ?
    答:不可以;静态成员函数属于类成员,不属于对象成员;

    问:全局函数可以被定义为虚函数吗 ?
    答:不可以;全局不属于类,无对象;
        
    问:用成员函数方式实现的操作符重载函数可以被定义为虚函数吗 ?
    答:可以;
    
    小结:虚函数表依赖于对象,凡是和对象无关的,都无法用虚函数,例如:构造、静态、全局等;
    或者说:动态分配是后期执行,而构造、静态、全局等是在前期(编译期)执行的;

5.    异常
    为什么要选择异常 ?
    1)通过返回值表达错误;局部对象都能正确析构;
        缺点:需要层层判断返回值,流程繁琐;
        举例:多级调用
        int func3() {
            FILE* fp = fopen("none", "r");    // 打开失败返回 0
            if(!fp)
                return -1;
            // . . .
            fclose(fp);
            return 0;
        }
        int func2() {
            if(func3() == -1)
                return -1;
            // . . .
            return 0;
        }
        int func1() {
            if(func2() == -1)
                return -1;
            // . . .
            return 0;
        }
        int main() {
            if(func1() == -1) {
                cout << "error";
                return -1;
            }
            // . . .
            cout << "ok";
            return 0;
        }
        简单分析:按顺序调用:f1->f2->f3,
        再安顺序返回,最后判断 main 里面 if;
        
    2)通过 setjmp/longjmp 远程跳转;一步到位,流程简单;
        缺点:局部对象会失去被析构的机会,因为直接跳转,不执行右花括号;
        举例 2:
        #include <cstdio>
        #include <csetjmp>
        jmp_buf g_fuc
        class A {
            public:
                A() {}
                ~A() {}
        };
        void func3() {
            A a;
            FILE* fp = fopen("none", "r");
            if(!fp)
                longjmp(g_env, -1);
            // . . .
            fclose(fp);
        }
        void func2() {
            A b;
            func3();
            // . . .
        }
        void func1() {
            A c;
            func2();
            // . . .
        }
        int main() {
            if(setjmp(g_env) == -1) {
                cout << "error";
                return -1;
            }
            func1();
            // . . .
            cout << "ok";
            return 0;
        }
        简单分析:先执行 setjmp,给 g_env 赋初值 0;
        然后按顺序调用:f1->f2->f3,
        当调到 f3,如果文件打开成功,正常执行,
        如果文件打开失败,执行 longjmp ,直接调到 setjmp,
        不执行右花括号,也就不执行析构;
        

    3)异常处理:
        局部对象能正确析构,同时流程也简单;
    异常抛出:
        throw 异常对象;
        异常对象可以是基本类型变量,也可以是类类型对象;
        当程序执行错误分支时,抛出异常;
    异常捕获:
        try {可能抛出异常的语句块;}
        catch(异常类型1 异常对象1){处理异常类型1的语句块;}
        catch(异常类型2 异常对象2){处理异常类型2的语句块;}
        ***
        catch(...)
        {处理其他异常类型的语句块;}

        class A {
            public:
                A() {}
                ~A() {}
        };
        void func3() {
            A a;
            FILE* fp = fopen("none", "r");
            if(!fp)
                throw -1;
            // . . .
            fclose(fp);
        }
        void func2() {
            A b;
            func3();
            // . . .
        }
        void func1() {
            A c;
            func2();
            // . . .
        }
        int main() {
            try {
                func1();
            }
            catch(int ex) {
                if(ex == -1) {
                    cout << "error";
                    return -1;
                }
            }
            func1();
            // . . .
            cout << "ok";
            return 0;
        }
        简单分析:打开成功,catch 不执行;
        打开失败后,throw 沿着右花括号逐层返回;
        保证所有局部对象都能够被析构,然后执行对应 catch 分支;

6.    异常处理使用方法:
    1)抛出基本类型异常,用不同的值代表不同的错误;
    2)抛出类类型异常,用不同的类型来表示不同的错误;
        可以直接在 thow 后面定义匿名变量,如:
            throw FileError();        // 这里不能 throw 局部变量的地址;
            catch(FileError& ex) {}    // 用引用接受,可以避免多调一次拷贝构造函数;
        注意:如果错误也分子类和基类,那么在写 catch 的时候,
        子类写在前面,基类在后面;因为 catch 使用最快原则,而不是最优原则;
    3)通过类类型的异常携带更多诊断信息;
    4)忽略异常和继续抛出异常;
        . . . throw . . .
        catch(XX& ex) {                // 必须是引用
            . . .
            throw;                        // 继续抛出异常,给下面的 catch
        }
        
7.    异常说明:
    在一个函数的行参表后面写如下语句:
        . . . 行参表)throw(异常类型1,异常类型2,. . .){ . . . }
    表示这个函数出现的所有异常里,只能捕获 throw 行参表里的异常;
    void foo() throw(int, double) {        // 异常说明
        throw 12.3;                        // 可以被捕获
        throw "hello";                    // 无法被捕获,虽然下面有字符串的 catch
    }
    int main() {
        try {
            foo();
        }
        catch(int& ex) {}
        catch(double& ex) {}
        catch(const char* ex) {}
        return 0;
    }

    极端类型:    throw()---表示这个函数抛出的任何异常都无法捕获;
                没有异常说明---表示这个函数抛出的任何异常都可以捕获;
    
    不能抛出比基类还多的异常:
    class A {
        public:
            virtual void foo() throw(int, double) {...}
            void bar() throw() {...}
    };
    class B : public A {
        public:
            void foo() throw(int, char) {...}        // ERROR
            void bar {...}                            // ERROR
    };

8.    标准库异常
    #include <stdexcept>

9.    构造函数中的异常
    构造函数可以抛异常,当构造函数初始化一半的时候出问题,还必须抛异常;
    因为构造函数无返回值,所以当出问题的时候,只能通过异常通知客户;
    如果在一个对象构造过程中抛出异常,这个对象就叫做不完整对象;
    不完整对象的析构函数永远不会执行,因此需要在 throw 之前,手动释放动态资源;

        #include <stdexcept>
        #include <cstdio>
        class FileError : public exception {
            private:
                const char* what() const throw() {}
        };
        class B {
            public:
                B() {}
                ~B() {}
        };
        class C {
            public:
                C() {}
                ~C() {}
        };
        class A : public C {
            public:
                A() : m_b(new B) {
                    FILE* fp = fopen("none", "r");
                    if(!fp)
                        throw FileError();
                    fclose(fp);
                }
                ~A() { delete m_b; }
            private:
                B* m_b;
                C m_c;
        };
        int main() {
            try {
                A a;
            }
            catch(exception& ex)
            {
                cout << "error";
            }
        }
        这里不会调用 A 的析构函数,解决办法是在抛出异常前析构它:
        if(!fp) {
            delete m_b;
            throw FileError();
        }
    
10.    析构函数中的异常:
    永远不要在析构函数中抛出异常!!!
    由此可能会导致未定义的后果;
        class A {
            public:
                ~A() { throw -1; }
        };
        try {
            A a;
            a.foo();
        }
        catch(...) {...}
        死循环。
    
    如果析构函数中的调用函数可能出现异常,
    那么要自我消化:
        ~A() {
            try {...foo(){} }
            catch(...) {...}
        }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值