[C++/pedia]类与对象

1 对象特征

1.1 封装

这个概念其实没啥,体会就好,抽象出一个类,然后在这个类中对对象进行操作。其实是从过程到对象的一个转变的一个概念。

#include <iostream>
using namespace std;

class Circle
{
    private:
        float pi = 3.1115926;
        float r;
    public:
        Circle(){r=0.000;}
        Circle(float _r){r = _r;}
        float area();
};

float Circle::area()
{
    return (pi*r*r);
}


int main() {
    Circle c;
    cout<<c.area()<<endl;
    Circle c(3);
    cout<<c.area()<<endl;

    return 0;
}

1.1.1 权限

namescope
public可以被所有东西访问
private只能被同个类的成员访问
protected可以被同个类的成员访问,也可以被派生类访问,其余不可访问
这个不理解暂时没关系。

1.2 对象的初始化和清理

因为通常来说,我们希望我们的数据是private的,不能被随意修改的,因此如果我们要修改我们必须在public处设置对应的初始化函数。你当然可以写那种float set_pi();float set_r();的函数,但是C++提供了一中更为简便的方法:构造函数(construction fun)和析构函数(destruction fun)

class Circle
{
    private:
        float pi = 3.1115926;
        float r;
    public:
        Circle(){r=0.000;}//构造
        Circle(float _r){r = _r;}//构造,重载
        float area();
        ~Circle();//析构
};
  • 两个函数都不用返回;
  • 两个的命名需要跟类一致,析构要加个~;
  • 构造函数(Circle::Circle)可以重载;
  • 析构函数不允许重载(Circle::~Circle),且不需要参数。

1.2.1 构造函数的分类和调用

无参,有参

  public:
        Circle(){r=0.000;}
        Circle(float _r){r = _r;}
        float area();
        ~Circle();

拷贝构造

//假设已经有Circle这个类了
//!!!拷贝的,无修改的统统用const!!!!
//!!!注意,如果是值传递,内存需要开辟一片空间,
//既然已经写了const了,那最好直接用Circle &c,不需要额外的开销
class Circle
{
	public:
		Circle(const Circle c);
		Circle(const Circle &c);
};
Circle::Circle(const Circle c);
Circle::Circle(const Circle &c);

调用的方法,括号,显示,隐式

// Online C++ compiler to run C++ program online
#include <iostream>
using namespace std;

class Circle
{
private:
    float pi = 3.1115926;
    float r;

//public一般是用来做声明的,对于一些比较小的函数可以写在这里,然后会自动inline
public:
    Circle() { r = 0.000; }
    Circle(float _r) { r = _r; }
    Circle(const Circle &c) { r = c.r; }
    float area();
    ~Circle() { cout << "out"; }
};

int main()
{

    // 1.括号调用(建议用这个,简单,不易出错)
    //1.1对于默认的,如果写成 Circle c1();
    //编译器以为你在搞函数声明,参数为空,返回一个Circle
    Circle c1;
    Circle c2(2.0);
    Circle c3(c1);
    
    //2.显式调用
    //2.1这种调用因为会创建临时的匿名对象然后赋值给对应的变量
    //所以,当匿名对象销毁时会调用3次~Circle()
    Circle c1;
    Circle c2 = Circle(2.0);
    Circle c3 = Circle(c1);
    // 2.2不要在一个匿名对象里,以拷贝的形式,创建一个已经声明的对象
    //如:Circle(c3);编译器以为你在

    //3.隐式,别搞这种看起来有点莫名其妙
    Circle c2 = 3.0;
    Circle c3 = c2;

    system("pause");
    return 0;
}

1.2.2 拷贝函数的调用

#include <iostream>
using namespace std;

class Circle
{
private:
    float pi = 3.1115926;
    float r;

public:
    Circle() { r = 0.000; cout<<"default is used"<<endl;}
    Circle(float _r) { r = _r; }
    Circle(const Circle &c) { r = c.r;cout<<"copy is used"<<endl; }
    float area();
    ~Circle() {cout<<"destructor is used"<<endl;}
};

//1.值传递,这里会触发拷贝函数创造一个临时对象c,这个临时对象的生命周期只在test里面
void test(Circle c)
{
	cout<<"eeee"<<endl;
}

//2.值方式返回局部对象,这里返回也会触发拷贝函数,
//因为c会被销毁,然后编译器会自动的开辟空间创建一个有c内容的Circle对象
Circle test2()
{
	Circle c(3.0);
	return c;
}

int main()
{
	Circle c1;	
	//3.这里会触发拷贝函数,
	//若数据成员是public的,改变c2数据成员的值不会改变c1里面的值
	Circle c2(c1);
	Circle c3 = test2();
	return 0;
}

1.2.3 构造函数的调用

  • C++在创建类时,至少自动创建了3个函数:1、构造(空实现);2、析构(空实现);3、拷贝构造(值拷贝)
  • 如果写了构造,但没写拷贝,则不自动生成构造,但拷贝构造会生成
  • 如果写了拷贝,则其他构造函数不再自动生成(包括拷贝)

1.2.4 浅拷贝和深拷贝

  • 浅拷贝:简单的赋值拷贝,基本就是系统的默认拷贝函数
  • 深拷贝:在堆区重新申请空间,进行拷贝操作

浅拷贝及其问题

#include <iostream>
using namespace std;

class Circle
{
private:
    float pi = 3.1115926;

public:
    float r;
    int *x; //随便设定一个数,下面的赋值拷贝会有所体现
    //如果都是要给r,*x初始化的话,可以在有参那里使用缺省的输入形式
    Circle()
    {
        r = 0.000;
        cout << "default is used" << endl;
    }
    Circle(float _r, int _x)
    {
        r = _r;
        //向堆区申请空间,这里写得不规范,应该判断是否分配好了
        x = new int(_x);
    }
    // Circle(const Circle &c) { r = c.r;cout<<"copy is used"<<endl; }
    float area();
    //因为我们这里使用了new,所以要在析构这里delete掉
    ~Circle()
    {
        delete x;
        //不规范,如果没创建成功会释放一块不知道哪里的内存,报错
        cout << "destructor is used" << endl;
    }
};

void test()
{
    Circle c1(3.6, 1);
    cout << "c1: " << c1.r << " " << (*c1.x) << endl;
    //没有定义拷贝函数,使用了默认的拷贝函数,
    //复制了c1的数据成员给c2,这里就是一个浅拷贝
    Circle c2(c1);

    cout << "c2: " << c2.r << " " << (*c2.x) << endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

浅拷贝可能会导致同个内存空间被多次释放。我们知道c1和c2的生命周期就是test的内部,当test执行到最后时,会调用c1,c2对应的析构函数,由于c1里面有个成员数据,int *x在浅拷贝中也给了c2,所以两个对象共同指向了同个内存区域,那么在调用析构函数时(由于我没有在堆区创建c1,c2),栈区的情况下,会先delete c2中的x,然后delete c1中的x,这样就delete了同个内存区域两次!

那怎么解决这个问题呢?这个就是深拷贝的问题,实际上就是说我自己定义一个拷贝函数再开辟一个空间放同样的值不就完了。因此我们只需要在我注释掉的拷贝构造函数里改成以下这样:

 Circle(const Circle &c)
    {
        r = c.r;
        //放同样的值,*c.x,这个值放到一个“new”的空间处
        x = new int(*c.x);
        cout << "copy is used" << endl;
    }

1.2.5 初始化列表

class Student
{
public:
    int a;
    int b;
    int c;

public:
    Student(int _a, int _b, int _c) : a(_a), b(_b), c(_c) {}
};

1.2.6 类作为成员数据

  • 就是类里面套个娃
  • 需要注意的是,当类1作为类2的数据成员时,类1的构造函数先被使用,然后才是类2的构造函数
  • 如果不是在堆区开辟空间创建类对象,那么(在栈区中,LIFO)析构函数的调用是先调用类2的析构,然后才是类1。

为什么会输出这样的结果?

#include <iostream>
using namespace std;

class A
{
public:
    int a;
    A(int _a)
    {
        a = _a;
        cout << "A con is called" << endl;
    }
    A(const A &b)
    {
        a = b.a;
        cout << "A copy is called" << endl;
    }
    ~A() { cout << "A de is called" << endl; }
};

class Student
{
public:
    int a;
    int b;
    int c;
    A d;

//A _d这里会调用copy,显然这个是形参,所以他会在con Stu完成后销毁
//d(_d)这里又调用copy
public:
    Student(int _a, int _b, int _c, A _d) : a(_a), b(_b), c(_c), d(_d)
    {
        cout << "Student con is called" << endl;
    }
    ~Student()
    {
        cout << "Student de is called" << endl;
    }
};

void test()
{
    A a(3);
    Student s(1, 2, 3, a);
}

int main()
{
    test();
    system("pause");
    return 0;
}

A con is called
A copy is called
A copy is called
Student con is called
A de is called
Student de is called
A de is called
A de is called

1.2.7 静态成员

静态数据成员

  • 被该类的所有成员所共用,那么他就成为了一个类的属性,而不是成员的属性
  • 必须在类外定义和初始化,不能在类里面初始化,用“::”来指明所属的类
  • 他的生存期与程序运行期相同
  • 在多线程开发中,谨慎使用

静态函数成员

  • 同样被所有成员所共用
  • 静态函数中只能包含静态变量!因为他是一个类的属性(Point::showCount();),如果我压根就没创建这个类的对象,那么这个函数就不能使用对象的数据成员。

1.3 C++对象模型和this指针

1.3.1 非静态成员变量的单独存储

#include <iostream>
using namespace std;

class Person
{

};

void test()
{
    Person P;
    cout << sizeof(P) << endl;
}
//--------输出的是1---------------
class  Person1
{
    int i;
};

void test1()
{
    Person1 P1;
    cout << sizeof(P1) << endl;
}
//----------输出的是1-------------
class Person2
{
    int i;
    static int j;
    void hehe();
    static void hehe1();
};

int Person2::j = 0;

void test2()
{
    Person2 P2;
    cout << sizeof(P2) << endl;
}
//--------输出的是也是1,这说明成员函数和静态数据成员不是存储在对象中的

int main()
{
    test();
    test1();
    test2();
    system("pause");
    return 0;
}

1.3.2 this(可以搞链式)

  • this本质跟引用一样也是一个指针常量
  • 他不需要定义,直接用

根据1.3.1的代码我们知道,创建对象时,只保存了这个对象的数据成员,而没保存他的函数成员,因为所有对象都将在另一块区域共同的调用这些函数成员,但是!编译器是怎么知道说,这个是a对象要调用成员函数,还是b对象要调用成员函数呢?

原因是this指针,他会直接指向被调用函数成员所属的对象,他不需要被定义。它的主要用途为:

  • 解决名称冲突
    #include <iostream>
    using namespace std;
    
    class Person
    {
    public:
    //直接寄!因为编译器认为你这三个age是完全一样的
        Person(int age)
        {
        //可以改成this->age = age;
        //实际上也可以写成 int _age
        //age = _age;
            age = age;
        }
        int age;
    };
    
    void test()
    {
        Person P(18);
        cout << P.age << endl;
    }
    
    int main()
    {
        test();
        system("pause");
        return 0;
    }
    
  • 返回对象本身 return *this
    #include <iostream>
    using namespace std;
    
    class Person
    {
    public:
        Person(int _age)
        {
            age = _age;
        }
        Person(const Person &p)
        {
            cout << "copy is used" << endl;
        }
        int age;
        Person &AddAge(const Person &P);
    };
    
    //要实现test里面的对P1.age的连加,需要返回P1本身!
    // *this返回的是P1,但如果这里使用 Person接收,
    //this所指向的东西会在函数结束后销毁
    //然后编译器通过复制函数创建一个新对象来保存this里面的数据
    //所以我们需要返回*this本身就需要引用Person&
    
    //此外因为*this是对象本身,不是指针,所以也不能用Person*来接收。
    //(其实也可以,只不过用起来很别扭)
    Person& Person::AddAge(const Person &P)
    {
        this->age = this->age + P.age;
        return *this;
    }
    
    void test()
    {
        Person P(18);
        Person P1(10);
        P1.AddAge(P).AddAge(P);
        cout << P1.age << endl;
    }
    
    int main()
    {
        test();
        system("pause");
        return 0;
    }
    

1.3.3 const与成员函数

  • 在成员函数后面加const,本质是在this指针那里加个const。
  • this指针本质类似于 Person *const this;如果加个const,变成 const Person *const this;他将似的无论是this指向的地址以及对应的值都是不可修改的。
  • 如果想在成员函数const里面修改成员数据,那么被修改的数据必须是mutable的

  • 常对象,const Person P;的效果跟成员函数const类似
  • 常对象只能调用常函数
#include <iostream>
using namespace std;

class Person
{
public:
    Person(int _age)
    {
        age = _age;
    }
    Person(const Person &p)
    {
        cout << "copy is used" << endl;
    }
    int age;
    //在show中修改b值是可以的
    mutable int b;
    //在成员函数后面加const,本质是在this指针那里加个const
	void show() const;
};


void test()
{
    Person P(18);
    Person P1(10);
    P1.AddAge(P).AddAge(P);
    cout << P1.age << endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

1.4 友元

他的作用是提高访问权限,比如一般来说private中不允许访问的数据成员,在friend下变得可以访问。

友元的三种实现:

  1. 全局函数作为友元
  2. 类作为友元
  3. 成员函数作为友元
#include <iostream>
#include <string.h>
using namespace std;

class Room;


class GoodGay1
{
private:
    int tmp;

public:
    GoodGay1(int _tmp);
    void FriendVisit(Room &R);
    void Friend1Visit(Room &R);
};

class Room
{
private:
    string LivingRoom;

public:
    string SittingRoom;
    Room(string _LR = "Living Room", string _SR = "Sitting Room") : LivingRoom(_LR), SittingRoom(_SR) {}
    friend void visit(Room &R);
    //友元类,但是这个会导致它内部的所有成员函数都可以访问Room里面的private
    //所以现在我要注释掉这个方法
    //我们采用友元成员函数的方法,但是这个方法很恶心,要考虑先后顺序的问题
    // friend class GoodGay1;
    friend void GoodGay1::FriendVisit(Room &R);
};

//友元全局函数
void visit(Room &R)
{
    cout << "Visit:" << R.LivingRoom << endl;
}

void GoodGay1::FriendVisit(Room &R)
{
    cout << "Your Friend is visiting:" << R.LivingRoom << endl;
}
GoodGay1::GoodGay1(int _tmp) { tmp = _tmp; }

int main()
{
    system("pause");
    return 0;
}

1.5 运算符重载

1.5.1 operator+

#include <iostream>
#include <string.h>
using namespace std;

class Person
{
public:
    int n1;
    int n2;
    Person(int _n1 = 10, int _n2 = 20) : n1(_n1), n2(_n2) {}
    Person operator+(const Person &p) const;
};

//类内部定义
Person Person::operator+(const Person &p) const
{
    Person tmp;
    tmp.n1 = p.n1 + this->n1;
    tmp.n2 = p.n2 + this->n2;
    return tmp;
}
//全局定义,由于他跟类内部定义在调用时是一样的,所以我这里注释掉了
// Person operator+(const Person &p1, const Person &p2)
// {
//     Person tmp;
//     tmp.n1 = p1.n1 + p2.n1;
//     tmp.n2 = p1.n2 + p2.n2;
//     return tmp;
// }

void test()
{
    Person p1;
    Person p2;
    Person p3 = p1 + p2;
    cout << p3.n1 << " " << p3.n2 << endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

1.5.2 operator<<

#include <iostream>
#include <string.h>
using namespace std;

class Person
{
public:
    int n1;
    int n2;
    Person(int _n1 = 10, int _n2 = 20) : n1(_n1), n2(_n2) {}
    
    // p.operator(os) p<<cout?? 不对,所以不应该在类成员函数实现
    // ostream &operator<<(ostream &os);

    //到时候就可以cout<<p;了
    //这里用friend是因为正常编写时,n1 n2是private的
    friend ostream &operator<<(ostream &os, const Person &p);
};

ostream &operator<<(ostream &os, const Person &p)
{
    cout << p.n1 << " " << p.n2 << endl;
    return os;
}

void test()
{
    Person p1;
    Person p2;
    // cout<<p1返回的是os,
    //即operator<<这个函数的第一个参数,所以可以继续传一个Person对象
    cout << p1 << p2;
}

int main()
{
    test();
    system("pause");
    return 0;
}

1.5.3 operator++

#include <iostream>
#include <string.h>
using namespace std;

class MyInt
{
private:
    int i;

public:
    MyInt(int _i) : i(_i) {}
    friend ostream &operator<<(ostream &os, MyInt I);
    MyInt &operator++();
    //使用占位参数,编译器就知道你这是在重载后置++
    MyInt operator++(int);
};
ostream &operator<<(ostream &os, MyInt I)
{
    cout << I.i;
    return os;
}

MyInt &MyInt::operator++()
{
    this->i += 1;
    return *this;
}

MyInt MyInt::operator++(int)
{
    MyInt tmp(0);
    tmp.i = this->i;
    this->i += 1;
    return tmp;
}

void test()
{
    MyInt i(10);
    cout << i << endl;
    cout << (++(++i)) << endl;
    cout << i++ << endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

1.5.4 operator=

如果类中没有创建这个函数,类会自动创建这个函数,其功能跟默认的复制构造函数一样,浅拷贝,容易造成重复释放空间。

这个是跟之前的深浅拷贝那里是联系的。

Circle &Circle::operator=(Circle &c)
 {
     if(this->x)
     {
         delete x;
         x = NULL;
     }

     this->r = c.r;
     this->x = new int(*c.x);
     return *this;
 }

1.5.5 operator==;operator!=

#include <iostream>
#include <string>
using namespace std;

class Person
{
private:
    string name;
    int ID;
    int age;

public:
    Person(string _name, int _ID, int _age) : name(_name), ID(_ID), age(_age) {}

    int operator==(Person &P) const;
    int operator!=(Person &P) const;
};

int Person::operator==(Person &P) const
{
    //默认不等于
    int flag = 0;
    //放一起太长了,返回用bool也行
    if (name == P.name)
        flag = 1;
    if (ID == P.ID)
        flag = 1;
    if (age == P.age)
        flag = 1;
    return flag;
}

void test()
{
    Person P1("Tom", 16, 2);
    Person P2("Jack", 1, 23);
    cout << (P1 == P2);
}

int main()
{
    test();
    system("pause");
    return 0;
}

1.5.6 operator()

不是很想写。

1.5.7 operator new()和operator delete()

new和delete是关键字,他们实际上各自都做了两件事,分配(释放)内存,初始化(反初始化)。分配(释放)就是基于operator new和operator delete实现的。当然new和delete的重载是很麻烦的,所以这里只是提一下说他们背后的一个函数是可以重载的。

在使用new和delete时,

  • 要么new,delete
  • 要么 new[],delete[]

不要乱搞,因为背后是依据operator new/delete记录元素个数之类的操作,而new和new[],记录的机理是各自对应delete和delete[]的机理的。这是一个非初级的内容。

2 继承

我们可以建立好多类,比如说:加菲猫类,波斯猫类等等,但是这些类有一些共同属性,那么这时候我们就可以建立一个基类/父类——猫,来获得这些共同属性,然后我们在建立加菲喵类时只需要注重他的特征(个性)而不用包括猫的特征(共性),这样我们能够节省代码量。

class Cat{};
class subCat1:public Cat{};
class subCat2:public Cat{};

2.1 继承方式

  • 父类A的private,子类永远访问不了,但是编译器是继承了过去了的,可以用sizeof(子类)的大小来确认
  • public继承的B类,除了父类的private,全部按照父类的访问属性访问
  • protected继承的B类,除了父类的private,全部改成protected
  • private继承的B类,除了父类的private,全部改成private

2.2 继承的con和de

跟栈一样,是FILO的,父类先con,子类后con;子类先de,父类de。

class A
{
private:
  int a;
  mutable int c;

public:
  A() { cout << "A con" << endl; }
  ~A() { cout << "A de" << endl; }
};

class B : public A
{
public:
  B() { cout << "B con" << endl; }
  ~B() { cout << "B de" << endl; }
};

void test()
{
  B b1;
}

int main()
{
  test();
  system("pause");
  return 0;
}

2.3 继承中的同名处理

  • 对于同名的数据成员,直接调用就是子类的数据,要访问父类的那个同名需要s.Base::m_A;即加个作用域
  • 对于同名的成员函数,也是加作用域
class A
{
private:
  int a;
  mutable int c;

public:
  A(int b = 2) : a(b) {}
  void add(int b) const { cout << "A" << endl; }
  void sub(int b) {}
  void div(int b) const { c = b; }
};

class B : public A
{
public:
//跟父类的add同名
  void add(int b) const { cout << "B" << endl; }
};

int main()
{
  B b;
  //加作用域
  b.A::add(2);

  system("pause");
  return 0;
}

虽然可以使用类似using的操作来实现,但是这种方法有很多局限性,故不讨论。

2.4 继承中的同名静态处理

处理方式跟5.3是一致的,但是这里会额外提供另一种方法。

#include <iostream>
using namespace std;
class Base
{
public:
    static int a;
};
int Base::a = 1;

class Son1 : public Base
{
};

int main()
{
	Son1 s;
	//5.3的方法
	cout<<s.Base::a<<endl;
	//另一种方法,这里可以不用创建对象就能访问
    cout << Son1::Base::a << endl;
    system("pause");
    return 0;
}

2.5 多继承

即一个类可以从多个类中派生,比如说中华田园犬,他是狗的一中,他当然也是动物的一中,也是宠物的一中,那么他可以

class Animal{};
class Dog{};
class Pet{};
class CNLocalDog:public Animal,public Dog, public pet{};

好像开发中建议不使用,同名问题会很头疼。

2.6 菱形继承

在这里插入图片描述
解决这个问题是在羊继承动物时加上virtual,class Sheep:virtual public Animal{};,驼也是

由于是多继承,所以也建议少用。

3 多态

多态是由继承发展的一中概念,他分为两种,一中是静态多态,另一种是动态多态。

  • 静态:函数重载,运算符重载,复用函数名。他们的函数地址早绑定了,在编译阶段就能确定函数地址。
  • 动态:派生类和虚函数的多态。他们函数地址晚绑定,需要在运行阶段才能确定函数地址。他需要在子类“重写“(函数名,返回值,参数列表完全相同)父类的虚函数时,才会触发。他在运行时是用父类的指针/引用指向子类对象就会使用多态。
#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void speak() { cout << "Animal is speaking!" << endl; }
	virtual ~Animal() { cout << "Animal de" << endl; };
};

class Cat : public Animal
{
public:
    void speak() { cout << "Cat says meow" << endl; }
    ~Cat() { cout << "Cat de" << endl; }
};

class Dog : public Animal
{
public:
    void speak() { cout << "Dog says woof" << endl; }
};

//当我们传入一个Animal的对象时,如果是静态编译,编译就会默认说他只接受Animal里面的成员函数
//但如果是动态编译,编译器在执行时,会看一下到底是Animal对象还是Animal的子类(Cat,Dog)对象
void toSpeak(Animal &A)
{
    A.speak();
}

//函数参数是父类指针/对象,但使用时用的是子类对象,这时候触发动态编译
void test()
{
    Dog D;
    toSpeak(D);
// # 3.4
    Animal *A = new Cat;
    (*A).speak();
    delete (A);
}

int main()
{
    test();
    system("pause");
    return 0;
}

以下都以这个例子开始吹水。

3.1 原理

去掉Animal中的virtual,sizeof(Animal)=1;

加上virtual,sizeof(Animal)=8(32位)。

这里面多存储了一个指针,vfptr,虚函数指针。这个指针指向一个vftable,虚函数表。表里面放的是父类的虚函数,子类也会继承这个表以及里面的内容,但是子类在重写这个父类的虚函数时,会覆盖掉子类vftable的内容。

3.2 纯虚函数,抽象类

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

在父类直接构造上面那种函数,这种函数是纯虚函数,而这个函数所在的类是抽象类。

  • 抽象类没办法实例化
  • 抽象类的子类必须重写父类的纯虚函数,否则也会变成抽象类(因为原理中的继承过去了)

3.3 虚析构和纯虚析构

像之前的代码,父类的指针,是没办法调用子类的析构函数,为了解决这种通过父类指针释放子类对象的问题,那么这时候我们考虑把析构函数设成虚就好了。否则会有内存泄漏问题。

纯虚析构必须要定义,在类外定义。用了纯虚,类变成抽象。

3.4 父类指针new子类对象

这是用来解决一种问题:只定义一个对象指针,然后能够调用父类以及各个子类的同名函数。

void test()
{
  Dog D;
  toSpeak(D);

  Animal *A = new Cat;
  (*A).speak();
  delete (A);
}

反过来,即子类指针new父类对象,是不可以的。

此时,在存在同名函数的情况下且同名函数是虚函数,A将会调用的是子函数的同名函数,除非加上::。

如果speak不是虚函数,那么这种指针只会调用父类的speak。

这里的

Animal *A = new Cat;

是动态绑定,即在运行阶段才能确定这个指针值绑定了哪种类型对象。这种操作,就是多态, 他的实现细节是要考虑vftable。

而如果是这种

Cat c;

这种就是静态绑定,虚函数是要结合动态绑定使用的!

3.5 RTTI,run time type identification

这是一个基于父类指针new子类对象所带来的问题,当我们添加下面一行

void test()
{
  Dog D;
  toSpeak(D);

  Animal *A = new Cat;
  //看这里!
  Animal &A1 = *A;
  (*A).speak();
  delete (A);
}

此时,A1表示的到底是父类对象还是子类对象是不清楚的。

又或者我们将Cat类加多一个他自身的一个成员函数:

class Cat : public Animal
{
public:
  virtual void speak() override { cout << "Cat says meow" << endl; }
  void move() { cout << "Cat is moving" << endl; }
  ~Cat() { cout << "Cat de" << endl; }
};

void test()
{
  Dog D;
  toSpeak(D);

  Animal *A = new Cat;
  // Animal &A1 = *A;
  (*A).speak();
  //error,因为move不是父类的虚函数
  (*A).move();
  delete (A);
}

此时因为不清楚 Animal *A到底是父类还是其多态子类,所以他会默认当做父类,但是父类没有move这个函数或者虚函数而只有子类是有的,为了解决这个问题C++提供RTTI。RTTI的实现通过以下两种方式:

  • dynamic_cast运算符,这本质是一个C++风格的类型转换符
  • typeid运算符,返回指针或引用所指对象的实际类型

以上运算符的使用必须基于“基类必须有一个虚函数”。因为要生成虚函数表。

3.5.1 dynamic_cast

void test()
{
  Animal *A = new Cat;
  // c风格的强制转换
  Cat *A1 = (Cat *)A;
  A1->move();
  // dynamic_cast
  Cat *A2 = dynamic_cast<Cat *>(A);
  if (A2 != nullptr)
  {
    cout << "dynamic_cast successed" << endl;
    A2->move();
  }
  else
  {
    cout << "dynamic_cast failed" << endl;
  }
  delete (A);
}

通过上面的方法进行类型转换,告诉编译器说A2,A1实际上是Cat子类而不是Animal父类,于是可以使用Cat的成员方法。注意dynamic_cast返回的是指针,但是可能会因为各种各样的原因他可能不会成功返回(比如说你乱转),所以要判断;而C风格的强制转换则没有这种保险。这里的的异常处理仅针对指针。

对于引用来说,需要进行如下的异常处理:

void test()
{
  Animal *A = new Cat;
  Animal &Ar = *A;
  try
  {
    Cat &c = dynamic_cast<Cat &>(Ar);
    c.move();
  }
  catch (bad_cast)
  {
    cout << "A不是cat类型" << endl;
  }

  delete (A);
}

记得一定要在多态的前提下使用。

3.5.2 typeid

  • typeid(指针/引用);typeid(表达式)
  • 拿到对象类型信息后,typeid会返回一个常量对象引用,这个常量对象是一个标准库类型 type_info。
void test()
{
  Animal *A = new Cat;
  Animal &Ar = *A;
  cout << typeid(*A).name() << endl;
  cout << typeid(Ar).name() << endl;
  cout << typeid(15).name() << endl;
  cout << typeid(16.1).name() << endl;
  cout << typeid(15 > 16).name() << endl;

  delete (A);
}

一般是这么用的,注意解引用。

void test()
{
  Animal *A = new Cat;
  Animal &Ar = *A;
  if (typeid(*A) == typeid(Cat))
  {
    cout << "A points to Cat" << endl;
  }
  delete (A);
}

仍然需要说的是,只有基类有虚函数时,上述所有操作才可以成立!否则,typeid返回的是静态类型而不是动态类型。

3.5.3 type_info类

这玩意儿是一个类,typeid返回的就是他,这里探讨的是一些方法。

  • .name();返回一个c风格字符串,告诉你()内部是什么类型
  • 类型相等与否用==,!=

4 其他

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值