第三章:C++面向对象(2)

目录

一、模板类是在什么时候实现?

二、类继承时派生类对不同关键字修饰的基类方法的访问权限

 三、移动构造函数是什么,哪些库用到该函数?

四、构造函数为什么不能被声明为虚函数?

五、常函数

六、虚继承

七、虚函数和纯虚函数

八、纯虚函数能实例化吗?派生类要实现吗?

九、拷贝构造函数的参数是什么传递方式?

十、抽象类

十一、什么是多态?除了虚函数,还有什么方式实现多态?

十二、虚析构函数

十三、虚基类是什么,是否可以被实例化?

十四、仿函数

十五、类模板和模板类的区别


一、模板类是在什么时候实现?

        模板实例化:模板的实例化分为显示实例化隐式实例化。前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板。无论是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的。

        模板具体化:当模板使用某种类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。

        代码示例:

#include <iostream>
using namespace std;

// #1 模板定义
template<class T>
struct TemplateStruct
{
    TemplateStruct()
    {
        cout<<sizeof(T)<<endl;
    }
};


// #2 模板显示实例化
template struct TemplateStruct<int>;

// #3 模板具体化
template<> struct TemplateStruct<double>
{
    TemplateStruct() {
        cout<<"--8--"<<endl;
    }
};

int main()
{
    TemplateStruct<int> intStruct;
    TemplateStruct<double> doubleStruct;

    // #4 模板隐式实例化
    TemplateStruct<char> llStruct;
}

运行结果:

4
--8--
1

二、类继承时派生类对不同关键字修饰的基类方法的访问权限

        类中的成员可以分为三种类型,分别为public成员protected成员public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员

        1. public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。

        2. protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。

        3. private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。

 三、移动构造函数是什么,哪些库用到该函数?

        移动也使用一个对象的值设置另一个对象的值。移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。

四、构造函数为什么不能被声明为虚函数?

        1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,无法调用。(悖论)

        2. 从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

        3. 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数。

五、常函数

        类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const, 而对于改变数据成员的成员函数不能加const。所以const关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加const还有什么好处呢?那就是常量(即 const)对象可以调用const成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样, 反之则不成立。

#include<iostream>
using namespace std;

class CStu
{
public:
    int a;
    CStu()
    {
        a = 12;
    }

    void Show() const
    {
        //a = 13; //常函数不能修改数据成员
        cout <<a << "I am show()" << endl;
    }
};

int main()
{
    CStu st;
    st.Show();
    system("pause");
    return 0;
}

六、虚继承

        虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:第一,浪费存储空间;第二,存在二义性问题。通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面提到的两个问题。

#include<iostream>
using namespace std;

class A{
public:
    int _a;
};

class B :virtual public A
{
public:
    int _b;
};

class C :virtual public A
{
public:
    int _c;
};

class D :public B, public C
{
public:
    int _d;
};
//菱形继承和菱形虚继承的对象模型

int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    cout << sizeof(D) << endl;
    return 0;
}

        分别从菱形继承和虚继承来分析: 菱形继承中A在B,C,D中各有一份,虚继承中,A共享。 上面的虚继承表实际上是一个指针数组。B、C实际上是虚基表指针,指向虚基表(存放相对偏移量,用来找虚基类)。

七、虚函数和纯虚函数

        1. C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

class Person{
    public:
        //虚函数
        virtual void GetName(){
            cout<<"PersonName:xiaosi"<<endl;
        };
};

class Student:public Person{
    public:
        void GetName(){
            cout<<"StudentName:xiaosi"<<endl;
        };
};

int main(){
    //指针
    Person *person = new Student();
    //基类调用子类的函数
    person->GetName();//StudentName:xiaosi
}

        虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

        2. 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” ,virtualvoid GetName() =0。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

//抽象类
class Person{
    public:
        //纯虚函数
        virtual void GetName()=0;
};

class Student:public Person{
    public:
        Student(){
        };
        void GetName(){
            cout<<"StudentName:xiaosi"<<endl;
        };
};

int main(){
    Student student;
}

八、纯虚函数能实例化吗?派生类要实现吗?

        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. 拷贝构造函数的参数必须使用引用传递

        2. 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。 需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。

十、抽象类

        1. 抽象类的定义如下:

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

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

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

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

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

十一、什么是多态?除了虚函数,还有什么方式实现多态?

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

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

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

十二、虚析构函数

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

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

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

十三、虚基类是什么,是否可以被实例化?

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

十四、仿函数

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

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


>>>helloworld!

        仿函数既能像普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。我们可以举个例子: 假设有一个 vector ,你的任务是统计长度小于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. 类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数。

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

        类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template class someclass{…};在定义对象时分别代入实际的类型名,如someclass obj。和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。模板可以有层次,一个类模板可以作为基类,派生出派生模板类。

  • 35
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿何试Bug个踌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值