V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yiouejv
V2EX  ›  C++

关于面向对象的面试知识点整理(一)

  •  
  •   yiouejv · 2021-02-25 22:43:52 +08:00 · 1865 次点击
    这是一个创建于 1401 天前的主题,其中的信息可能已经有所发展或是发生改变。

    例 1: C++ 中的空类默认产生哪些类成员函数?

    对于一个空类,编译器默认产生 4 个成员函数:默认构造函数、 析构函数、 拷贝构造函数和赋值函数。

    例 2: structure 是否可以拥有 constructor / destructor 及成员函数?如果可以, 那么 structure 和 class 还有区别么?

    区别是 class 中变量默认是 private, struct 中的变量默认是 public 。struct 可以有构造函数,析构函数,之间也可以继承,等等。C++中的 struct 其实和 class 意义一样, 唯一不同的就是 struct 里面默认的访问控制是 public, class 中默认的访问控制是 private 。C++中存在 struct 关键字的唯一意义就是为了让 C 程序员们有个归属感, 是为了让 C++编译器兼容以前用 C 开发的项目。

    例 3: 现有以下代码, 则编译时会产生错误的是?

    struct Test
    {
        Test(int) {}
        Test() {}
        ~Test() {}
        void fun() {}
    };
    
    int main()
    {
        Test a(1);
        a.fun();
        Test b();
        b.fun();
        return 0;
    }
    

    Test b() 这个语法等同于声明了一个函数,函数名为 b, 返回值为 Test, 传入参数为空。但是实际上,代码作者是希望声明一个类型为 Test,变量名为 b 的变量,应该写成Test b;, 但程序中这个错误在编译时是检测不出来的。出错的是b.fun(),它是编译不过去的。

    例 4: 下面程序的打印出的结果是什么?

    #include <iostream>
    using namespace std;
     
    class Base
    {
    public:
        Base(int i) : m_j(i), m_i(m_j) {}
        Base() : m_j(0), m_i(m_j) {}
        ~Base() {}
        int get_i() const { return m_i; }
        int get_j() const { return m_j; }
        
    private:
        int m_i;
        int m_j;
    };
    
    
    int main()
    {
        Base obj(98);
        cout << obj.get_i() << endl;
        cout << obj.get_j() << endl;
        return 0;
    }
    

    本题想要得到的结果是"98,98"。 但是成员变量的声明是先 m_i, 然后是 m_j ;初始化列表的初始化变量顺序是根据成员变量的声明顺序来执行的, 因此 m_i 会被赋予一个随机值。更改一下成员变量的声明顺序可以得到预想的结果。 如果要得到 "98,98"的输出结果,程序需要修改如下

    int m_j;
    int m_i;
    

    补充变量未初始化的值

    1. 未初始化的全局数组为 0 ;
    2. 未初始化的局部数组为随机值;
    3. 初始化部分的全局数组与局部数组,初始化部分为初始化值,未初始化部分都为 0 ;(不管全局还是局部)
    4. 全局变量未初始化:
    #include <iostream>
    using namespace std;
     
    int i;
    char c;
    float f;
    double d;
    int* p;
    
    int main()
    {
        cout << i << endl; // 0
        cout << c << endl; // '\0'
        cout << f << endl; // 0
        cout << d << endl; // 0
        cout << p << endl; // 0
        return 0;
    }
    
    1. 局部变量未初始化为随机数。
    #include <iostream>
    using namespace std;
     
    
    int main()
    {
        int i;
        char c;
        float f;
        double d;
        int* p;
        cout << i << endl;
        cout << c << endl;
        cout << f << endl;
        cout << d << endl;
        cout << p << endl;
        return 0;
    }
    
    1. 类的成员变量未初始化的值

      1. 对象在全局作用域或为静态局部对象时,类的内置成员变量被初始化为 0.

      2. 对象在局部作用域定义时,类的内置成员变量不被初始化为 0 。

      3. 对于类类型成员按照其自身的默认构造函数进行初始化。

        如果类显式提供了带参数的构造函数,则编译器不会再为其生成空参数的构造函数。这时候就不能用空参数来定义类类型变量

        class A{
        public:
            int value;
            A (int i):value(i){}
        };
        
        int main(){
            A a;  // error
            return 0;
        }
        

    参考代码:

    #include <iostream>
    using namespace std;
     
    class A
    {
    public:
        int i;
        float f;
        double d;
        char c;
        int* p;
        void print() {
            cout << i << endl;
            cout << c << endl;
            cout << f << endl;
            cout << d << endl;
            cout << p << endl;
            cout << "-------------------" << endl;
        }
    };
    
    A a0;
    
    int main()
    {
        a0.print();
        A a;
        a.print();
        return 0;
    }
    

    例 5: MFC 类库中,CObject 类的重要性不言自明。在 CObject 的定义中,我们看到一个有趣的现象,即 CObject 的析构函数是虚拟的。为什么 MFC 的编写者认为虚拟的析构函数是必要的?

    #include <iostream>
    using namespace std;
     
    class Base
    {
    public:
        Base() { cout << "Base" << endl; }
        ~Base() { cout << "~Base" << endl; }
    };
    
    class Derived : public Base
    {
    public:
        Derived() { cout << "Derived" << endl; }
        ~Derived() { cout << "~Derived" << endl; }
    };
    
    int main()
    {
        Base *b = new Derived();
        delete b;
        return 0;
    }
    

    我们将一个父类的指针指向子类的对象,当我们使用delete b 去释放 b 指向的内存,会调用父类的析构函数(静态联编),但是却不会调用子类的析构函数。

    如果子类对象在构造函数内申请了一块堆内存,最后根据上述情况则会造成内存泄漏,我们在 ~Base() { cout << "~Base" << endl; } 前加上 virtual, delete b 会先调用子类析构,再调用父类析构,自然就把子类申请的内存释放掉啦!


    补充知识: C++静态联编和动态联编

    根据: https://blog.csdn.net/erlian1992/article/details/44262843 所修改

    联编是指一个程序自身彼此关联的一个过程。 按照联编所进行的阶段不同,可分为静态联编和动态联编两种。

    静态联编: 静态联编是指在程序编译链接阶段进行联编。这种联编又称为早期联编,这是因为这种联编工作是在程序运行之前完成的。

    编译时所进行的联编又称为静态束定。束定是指确定所调用的函数与执行该函数代码之间的关系。 其优点是效率高,但灵活性差。

    下面来看一个静态联编的程序例题:

    #include <iostream>
    using namespace std;
    class Point
    {
        public:
        Point(double i,double j)//基类的构造函数
        { x=i;y=j; }
        double Area() const//定义的常成员函数
        { return 0.0; }
    private:
        double x,y;
    };
    class Rectangle:public Point//公有继承的派生类
    {
    public:
        Rectangle(double i,double j,double k,double l);//声明派生类的构造函数
        double Area() const
        { return w*h; }
    private:
        double w,h;
    };
    Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)//派生类构造函数的函数体
    {
        w=k;
        h=l;
    } 
    void fun(Point &s)//定义的类外函数
    {
        cout<<s.Area()<<endl;
    }
    int main()
    {
        Rectangle rec(3.5,15.2,5.0,28.0);//定义的派生类的带参数的对象
        fun(rec);//调用函数 fun()
    }
    

    输出的结果为:0

    程序分析:从输出的结果来看,该程序执行了 Point::Area()的结果。派生类 Rectangle 的对象 rec 作为函数 fun()的实参,而该函数的形参是类 Point()的对象的引用 s 。在程序编译阶段,对象引用 s 所执行的 Area()操作被联编到 Point 类的函数上。因此在执行函数 fun()中的 s.Area()操作时,返回值为 0 。

    通过对象指针进行的普通成员函数的调用,仅仅与指针的类型有关,而与此刻指针正指向什么对象无关。要想实现当指针指向不同对象时执行不同的操作,就必须将基类中相应的成员函数定义为虚函数,进行动态联编。

    动态联编

    动态联编是指在程序运行时进行的联编,这种联编又称为晚期联编。 动态联编的优点是灵活性强,但效率低。

    动态联编要求在运行时解决程序中的函数调用与执行该函数代码间的关系,又称为动态束定。在上述的例题中,由于是静态联编,函数 fun()中的参数 s 所引用的对象被联编到类 Point 上。那么实行动态联编,则 s 所引用的对象将被联编到类 Rectangle 上。由此可见,对于同一个对象的引用,采用不同的联编方式将会被联编到不同类的对象上,即不同联编可以选择不同的实现,这便是多态性。实际上是对于函数 fun()的参数的多态性选择。联编是一种重要的多态性选择。

    实现动态联编需要同时满足以下三个条件:

    1. 必须把动态联编的行为定义为类的虚函数。
    2. 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来。
    3. 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。

    上述的例题可以改为:

    #include <iostream>
    using namespace std;
    class Point
    {
        public:
        Point(double i,double j)//基类的构造函数
        { x=i;y=j; }
        virtual double Area() const//定义的虚函数
        { return 0.0; }
    private:
        double x,y;
    };
    class Rectangle:public Point//公有继承的派生类
    {
    public:
        Rectangle(double i,double j,double k,double l);//声明派生类的构造函数
        virtual double Area() const//派生类的虚函数
        { return w*h; }
    private:
        double w,h;
    };
    Rectangle::Rectangle(double i,double j,double k,double l):Point(i,j)//派生类构造函数的函数体
    {
        w=k;
        h=l;
    } 
    void fun(Point &s)//定义的类外函数
    {
        cout<<s.Area()<<endl;
    }
    int main()
    {
        Rectangle rec(3.5,15.2,5.0,28.0);//定义的派生类的带参数的对象
        fun(rec);//调用函数 fun()
    }
    

    输出的结果为:140.

    程序分析:该程序中说明了虚函数,fun()函数的对象引用参数 s 被动态联编,该函数体内调用的 Area()函数是在运行中确定的,而不是在编译时确定的,因此在运行时,实参 rec 为类 rectangle 的对象,于是 Area()函数被确定为类 Rectangle 的 Area()函数。

    例 6: 析构函数可以为 virtual 型,构造函数则不能。那么为什么构造函数不能为虚呢?

    虚函数采用一种虚调用的办法。虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。

    例 7: 如果虚函数是非常有效的,我们是否可以把每个函数都声明为虚函数?

    不行,这是因为虚函数是有代价的:由于每个虚函数的对象都必须维护一个虚表,因此在使用虚函数的时候都会产生个系统开销。如果仅是个很小的类,且不想派生其他 类,那么根本没必要使用虚函数。

    例 8: 析构函数可以是内联函数吗?

    析构函数可以是内联函数。

    例 9: 重载和覆盖有什么不同?

    override 是指派生类重写基类的虚函数, overload 约定成俗地被翻译为 “重载",是指编写一个与已有函数同名,但是参数表不同的函数。 例如一个函数既可以接收整型数作为参数,也可以接收浮点数作为参数。 重载不是一 种面向对象的编程, 而只是一种语法规则, 重载与多态没有什么直接关系。

    例 10: 在 C ++中, 你不应该从以下哪个抛出异常?

    A. Constructor (构造函数)
    B. Destructor (析构函数)
    C. Virtual function (虚方法)
    D. None of the above (以上答案都不对)
    

    构造函数中抛出异常是有一定必要的,试想如下情况:

    构造函数中 有两次 new 操作,第一次成功了,返回了有效的内存,而第二次失败,此时因为对象构造尚未完成,析构函数是不会调用的,也就是 delete 语句没有被执行,第一次 new 出的内存就悬在那儿了(发生内存泄露), 所以异常处理程序可以将其暴露出来。

    构造函数中遇到异常是不会调用析构函数的, 一个对象的父对象的构造函数执行完毕,不能称之为构造完成,对象构造是不可分割的,要么完全成功, 要么完全失败,C++保证这一点。 对于成员变量,C++遵循这样的规则,即会从异常的发生点按照成员变量的初始化的逆序释放成员。 举例来说,有如下初始化列表:

    A:: A() : ml(), m2(), m3(), m4(), m5() () {};
    

    假定 m3 的初始化过程中抛出异常,则会按照 m2,ml 的顺序调用这两个成员的析构函数。 在{}之间发生的未捕捉异常,最终会导致在栈的开解时析构所有的数据成员。

    处理这样的问题, 使用智能指针是最好的, 这是因为auto_ptr成员是一个对象而不是指针。 换句话说,只要不使用原始的指针,那么就不必担心构造函数抛出异常而导致资源泄露。

    所以在 C++中,资源泄露的问题一般都用 RAII (资源获取即初始化)的办法:把需要打开/关闭的资源用简单的对象封装起来(这种封装可以同时有多种用处,比如隐藏底层 API 细节,以利于移植)。 这可以省去很多的麻烦。

    如果不用 RAII,即使当前构造函数里获取的东西在析构函数里都释放了,如果某天对类有改动,要新增加一种资源,构造函数里一般能适当地获取,但记不记得要在析构函数里相应地释放呢?失误的比例很大。 如果考虑到构造函数里抛出异常,就更复杂了。 随着项目的不断扩大和时间的推移,这些细节不可能都记得住,而且,有可能会由别人来实施这样的改动。

    1. C++中通知对象构造失败的唯一方法,就是在构造函数抛出异常
    2. 当对象发生部分构造异常时,已经构造完毕的子对象将会逆序地被析构(即异常发生点前面的对象; 而还没有开始构建的子对象将不会被构造了(即异常发生点后面的对象, 当然它也就没有析构过程了; 还有正在构建的子对象和对象自己本身将停止继续构建(即出现异常的对象),并且它的析构是不会被执行的。

    析构函数抛异常

    Effective C++建议,析构函数尽可能地不要抛出异常。 设想如果对象出了异常, 现在异常处理模块为了维护系统对象数据的一致性,避免资源泄漏,有责任释放这个对象的资源,调用对象的析构函数,可现在假如析构过程又再出现异常,那么请问由谁来保证这个对象的资源释放呢?而且这新出现的异常又由谁来处理呢?不要忘记前面的一个异常目前都还没有处理结束,因此陷入了一个矛盾之中,或者说处于无限的递归嵌套之中。 所以 C++标准就做出了这种假设。

    4 条回复    2021-02-26 14:06:35 +08:00
    alazysun
        1
    alazysun  
       2021-02-25 22:56:35 +08:00
    空类在不被需要时不生成任意函数,sizeof(className) == 1,
    编译器还生成 operator&,const operator&和 this

    底下的没看
    nightwitch
        2
    nightwitch  
       2021-02-26 00:07:53 +08:00
    例 1: C++ 中的空类默认产生哪些类成员函数?
    至少还要包括移动构造和移动赋值。

    “动态联编,晚期联编”
    反应了半天,我不知道 binding 还能翻译成联编。还不如翻译成动态绑定和延迟绑定

    “处理这样的问题, 使用智能指针是最好的, 这是因为 auto_ptr 成员是一个对象而不是指针。“
    auto_ptr 是一个失败的设计,已经被弃用了。

    这篇文章读起来像是 10 年前的东西。
    yiouejv
        3
    yiouejv  
    OP
       2021-02-26 09:49:52 +08:00
    @nightwitch 有没有相关的文章,我学习一下
    auto8888
        4
    auto8888  
       2021-02-26 14:06:35 +08:00
    bind 一般翻译成绑定吧

    重载是编译期多态

    其他的学习了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2602 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 05:23 · PVG 13:23 · LAX 21:23 · JFK 00:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.