C++面向对象(下)

1.3.22 说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?

参考回答

  1. 纯虚函数不可以实例化,但是可以用其派生类实例化,示例如下:

    class Base { public: virtual void func() = 0; };

    #include<iostream> using namespace std; class Base {    public:    virtual void func() = 0; }; class Derived :public Base {    public:    void func() override   {        cout << "哈哈" << endl;   } }; int main() {    Base *b = new Derived();    b->func();    return 0; }

  2. 虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。

    即“纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”

    所以纯虚函数不能实例化。

  3. 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。

  4. 定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

1.3.23 说说C++中虚函数与纯虚函数的区别

参考回答

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。

  2. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

  3. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  5. 虚函数的定义形式:virtual{};纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

答案解析

  1. 我们举个虚函数的例子:

    class A { public:    virtual void foo()   {        cout<<"A::foo() is called"<<endl;   } }; class B:public A { public:    void foo()   {        cout<<"B::foo() is called"<<endl;   } }; int main(void) {    A *a = new B();    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!    return 0; }

    这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果。

  2. 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

    virtual void funtion1()=0

    为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

    在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

    为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。 声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

    纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

    定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

    纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

1.3.24 说说 C++ 中什么是菱形继承问题,如何解决

参考回答

下面的图表可以用来解释菱形继承问题。

  • 假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。因为上述图表的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。现在,我们将上面的图表翻译成具体的代码:

    /* *Animal类对应于图表的类A* */ class Animal { /* ... */ }; // 基类 {    int weight;    public:    int getWeight() { return weight; } }; class Tiger : public Animal { /* ... */ }; class Lion : public Animal { /* ... */ } class Liger : public Tiger, public Lion { /* ... */ }

    在上面的代码中,我们给出了一个具体的菱形继承问题例子。Animal类对应于最顶层类(图表中的A),Tiger和Lion分别对应于图表的B和C,Liger类(狮虎兽,即老虎和狮子的杂交种)对应于D。

    现在,问题是如果我们有这种继承结构会出现什么样的问题。

    看看下面的代码后再来回答问题吧。

     int main( ) { Liger lg; /*编译错误,下面的代码不会被任何C++编译器通过 */ int weight = lg.getWeight();   }

  • 在我们的继承结构中,我们可以看出Tiger和Lion类都继承自Animal基类。所以问题是:因为Liger多重继承了Tiger和Lion类,因此Liger类会有两份Animal类的成员(数据和方法),Liger对象"lg"会包含Animal基类的两个子对象。

    所以,你会问Liger对象有两个Animal基类的子对象会出现什么问题?再看看上面的代码-调用"lg.getWeight()"将会导致一个编译错误。这是因为编译器并不知道是调用Tiger类的getWeight()还是调用Lion类的getWeight()。所以,调用getWeight方法是不明确的,因此不能通过编译。

我们给出了菱形继承问题的解释,但是现在我们要给出一个菱形继承问题的解决方案。如果Lion类和Tiger类在分别继承Animal类时都用virtual来标注,对于每一个Liger对象,C++会保证只有一个Animal类的子对象会被创建。看看下面的代码:

class Tiger : virtual public Animal { /* ... */ }; class Lion : virtual public Animal { /* ... */ };

你可以看出唯一的变化就是我们在类Tiger和类Lion的声明中增加了"virtual"关键字。现在类Liger对象将会只有一个Animal子对象,下面的代码编译正常:

int main( ) { Liger lg; /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */ int weight = lg.getWeight(); }

1.3.25 请问构造函数中的能不能调用虚方法

参考回答

不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。

同样,进入基类析构函数时,对象也是基类类型。

所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。

1.3.26 请问拷贝构造函数的参数是什么传递方式,为什么

参考回答

  1. 拷贝构造函数的参数必须使用引用传递

  2. 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

    需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

1.3.27 说说类方法和数据的权限有哪几种

参考回答

  1. C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。

    关键字

    权限

    public

    可以被任意实体访问

    protected

    只允许子类及本类的成员函数访问

    private

    只允许本类的成员函数访问

  2. 下面介绍一个例子。

    父类:

    class Person { public:    Person(const string& name, int age) : m_name(name), m_age(age)   {   }    void ShowInfo()   {        cout << "姓名:" << m_name << endl;        cout << "年龄:" << m_age << endl;   }     protected:    string m_name;     //姓名 private:    int m_age;      //年龄 };

    子类:

    class Teacher : public Person { public: Teacher(const string& name, int age, const string& title) : Person(name, age), m_title(title) { } void ShowTeacherInfo() { ShowInfo(); //正确,public属性子类可见 cout << "姓名:" << m_name << endl; //正确,protected属性子类可见 cout << "年龄:" << m_age << endl; //错误,private属性子类不可见 cout << "职称:" << m_title << endl; //正确,本类中可见自己的所有成员 } private: string m_title; //职称 };

    调用方:

    void test() { Person person("张三", 22); person.ShowInfo(); //public属性,对外部可见 cout << person.m_name << endl; //protected属性,对外部不可见 cout << person.m_age << endl; //private属性,对外部不可见 }

1.3.28 如何理解抽象类?

参考回答

  1. 抽象类的定义如下:

    纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。

  2. 抽象类有如下几个特点:

    1)抽象类只能用作其他类的基类,不能建立抽象类对象。

    2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。

    3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

1.3.29 什么是多态?除了虚函数,还有什么方式能实现多态?

参考回答

  1. 多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)

  2. 多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,代码如下:

    class A { public: void do(int a); void do(int a, int b); };

1.3.30 简述一下虚析构函数,什么作用

参考回答

  1. 虚析构函数,是将基类的析构函数声明为virtual,举例如下:

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

  2. 虚析构函数的主要作用是防止内存泄露。

    定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对象,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,所谓先构造的后释放);如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。

    如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。

答案解析

  • 我们创建一个TimeKeeper基类和一些及其它的派生类作为不同的计时方法

    class TimeKeeper { public: TimeKeeper() {} ~TimeKeeper() {} //非virtual的 }; //都继承与TimeKeeper class AtomicClock :public TimeKeeper{}; class WaterClock :public TimeKeeper {}; class WristWatch :public TimeKeeper {};

  • 如果客户想要在程序中使用时间,不想操作时间如何计算等细节,这时候我们可以设计factory(工厂)函数,让函数返回指针指向一个计时对象。该函数返回一个基类指针,这个基类指针是指向于派生类对象的

    TimeKeeper* getTimeKeeper() { //返回一个指针,指向一个TimeKeeper派生类的动态分配对象 }

  • 因为函数返回的对象存在于堆中,因此为了在不使用时我们需要使用释放该对象(delete)

    TimeKeeper* ptk = getTimeKeeper(); delete ptk;

  • 此处基类的析构函数是非virtual的,因此通过一个基类指针删除派生类对象是错误的

  • 解决办法: 将基类的析构函数改为virtual就正确了

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

  • 声明为virtual之后,通过基类指针删除派生类对象就会释放整个对象(基类+派生类)

1.3.31 说说什么是虚基类,可否被实例化?

参考回答

  1. 在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:

    class A class B1:public virtual A; class B2:public virtual A; class D:public B1,public B2;

  2. 虚继承的类可以被实例化,举例如下:

    class Animal {/* ... */ }; class Tiger : virtual public Animal { /* ... */ }; class Lion : virtual public Animal { /* ... */ }

    int main( ) {     Liger lg;     /*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */     int weight = lg.getWeight(); }

1.3.32 简述一下拷贝赋值和移动赋值?

参考回答

  1. 拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创建的对象。

  2. 移动赋值是通过移动构造函数来赋值,二者的主要区别在于

    1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;

    2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。

1.3.33仿函数了解吗?有什么作用

参考回答

仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,举个例子:

class Func{ public:     void operator() (const string& str) const {         cout<<str<<endl;     } }; Func myFunc; myFunc("helloworld!"); >>>helloworld!

仿函数既能想普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子:

假设有一个vector<string>,你的任务是统计长度小于5的string的个数,如果使用count_if函数的话,你的代码可能长成这样:

bool LengthIsLessThanFive(const string& str) { return str.length()<5; } int res=count_if(vec.begin(), vec.end(), LengthIsLessThanFive);

其中count_if函数的第三个参数是一个函数指针,返回一个bool类型的值。一般的,如果需要将特定的阈值长度也传入的话,我们可能将函数写成这样:

bool LenthIsLessThan(const string& str, int len) {     return str.length()<len; }

这个函数看起来比前面一个版本更具有一般性,但是他不能满足count_if函数的参数要求:count_if要求的是unary function(仅带有一个参数)作为它的最后一个参数。如果我们使用仿函数,是不是就豁然开朗了呢:

class ShorterThan { public:     explicit ShorterThan(int maxLength) : length(maxLength) {} bool operator() (const string& str) const { return str.length() < length; } private: const int length; };

1.3.34 C++ 中哪些函数不能被声明为虚函数?

参考回答

常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

为什么C++不支持普通函数为虚函数?

  1. 为什么C++不支持构造函数为虚函数?

    这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

    构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数

  2. 为什么C++不支持内联成员函数为虚函数?

    其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数)

    内联函数是在编译时期展开,而虚函数的特性是运行时才动态联编,所以两者矛盾,不能定义内联函数为虚函数

  3. 为什么C++不支持静态成员函数为虚函数?

    这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。

    静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别

  4. 为什么C++不支持友元函数为虚函数?

    因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

1.3.35 解释下 C++ 中类模板和模板类的区别

参考回答

  1. 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数

  2. 模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。

答案解析

  1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template <class T1,class T2>class someclass{…};在定义对象时分别代入实际的类型名,如   someclass<int,double> obj;

  2. 和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。

  3. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

1.3.36 虚函数表里存放的内容是什么时候写进去的?

参考回答

  1. 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入

  2. 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值