什么是多态?你对多态的认识?
简单来说就是同一事物在不同的场景下表现出的多种形态。
栗子:网上的例子,见人说人或话,见鬼说鬼话。
一个人见到不同身份的人说话的态度不一样。
多态的实现条件:
在继承的体系下
基类中必须有虚函数,被virtual修饰的成员函数(注意一定是成员函数),友元函数不是虚函数
在派生类中必须要对基类中的虚函数进行重写
对于虚函数的调用:必须使用基类的指针或者引用调用多态。多态的发生就是在基类的指针或者引用指向子类的对象的时候。
体现多态性:在代码运行时,基类指针指向哪个类的对象,就调用哪个类的虚函数
多态的实现:
汉语命名好玩,但是感觉好乱
class 人类
{
public:
人类(string 名字,string 性别,int 年龄)
:_名字(名字)
, _性别(性别)
, _年龄(年龄)
{}
virtual void 买票()
{
cout << "全价票" << endl;
}
protected:
string _名字; //姓名
string _性别; //性别
int _年龄; //年龄
};
class 学生类 : public 人类
{
public:
学生类(string 名字, string 性别, int 年龄, int 学号)
:人类(名字, 性别, 年龄)
,_学号(学号)
{}
virtual void 买票()
{
cout << "半价票" << endl;
}
protected:
int _学号; //学号
};
class 军人类 : public 人类
{
public:
军人类(string 名字, string 性别, int 年龄, string 军衔)
:人类(名字, 性别, 年龄)
, _军衔(军衔)
{}
virtual void 买票()
{
cout << "免票" << endl;
}
protected:
string _军衔; //学号
};
int main()
{
//必须通过基类的指针或引用来调用
//多态的体现就是:代码编译的时候不能确定到底调用哪个类的虚函数,要在代码运行时才可以确定下来
军人类 张超伦("张超伦", "男", 28,"大佐");
学生类 小董("小董", "男", 20, 1740);
人类& 九一刘先生 = 张超伦; //发生多态
//根据基类指向的派生类的对象,才能确定调用哪一个类的虚函数
人类& 小王 = 小董; //发生多态
九一刘先生.买票();
小王.买票();
return 0;
}
发生多态后打印结果:
重写:派生类重写基类的某个虚函数,派生类虚函数必须要和基类中虚函数的原型完全一致,原型完全一致指的是,返回值类型,函数名,参数类型,完全一致。
重写:一定是派生类重写基类的虚函数
1.一个函数在基类中,一个函数在子类中
2.基类中的成员函数必须是虚函数:基类的成员函数前必须增加virtual的关键字,
3.子类同名成员函数前virtual关键字是否田间都可以。子类加不加都可以,在子类中就算不加virtual关键字,但是实际上它还是虚函数,所以说建议最好加上。
4.基类虚函数必须要与派生类虚函数的原型完全一致(返回值类型,函数名,参数列表)。
关于原型完全一致也是存在**两个例外(协变,析构函数)**的:
协变:
基类中虚函数返回的是基类的引用或者指针,子类的虚函数返回子类的引用或者指针
此时就算在基类和派生类中虚函数原型不一致,这照样可以形成重写,也可以发生多态。
利用协变发生多态:
class Base
{
public:
virtual Base* test() 在基类中返回基类的指针或者引用
{
cout << "base()" << endl;
return this;
}
};
class Dri : public Base
{
public:
virtual Dri* test() //在子类中返回子类的指针或者引用
{
cout << "Dri()" << endl;
return this;
}
};
void test(Base* p)
{
p->test();
}
int main()
{
Base b;
Dri d;
test(&b);
test(&d);
return 0;
}
关于协变的用法:
举例说名:
class A
{
public:
virtual A* test()
{
cout << "A" << endl;
return this;
}
};
class B : public A
{
public:
virtual A* test() //返回值是基类的指针
{
cout << "B" << endl;
return this;
}
};
class C : public B
{
public:
virtual B* test() //返回的是基类的指针
{
cout << "C" << endl;
return this;
}
};
void test(A* p)
{
p->test();
}
int main()
{
A a;
B b;
C c;
test(&a);
test(&b);
test(&c);
return 0;
}
上面代码的关系:
2.析构函数:
class A
{
public:
A()
{
cout << "A" << endl;
}
virtual ~A() //注意这里将析构函数给成了虚函数
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
B() :p(new int[10])
{
cout << "B" << endl;
}
~B()
{
delete[] p;
cout << "~B()" << endl;
}
int* p;
};
void test()
{
A* p = new B;
delete p;
}
int main()
{
test();
return 0;
}
打印结果:
假如不把A类的析构函数给成虚函数的话。
打印结果就变成了:
说明并没有调用子类的析构 ,也就是没有释放子类申请的空间
所以析构函数也是一个例外,虽然基类和派生类析构函数名不一样,但是照样发生了重写。
析构函数和构造函数都没有办法被继承,
构造函数可以被重载,析构函数不可以被重载
析构函数能作为虚函数,构造函数不可以作为虚函数
添加:
析构函数不可以声明为const
C++11auto是在编译时器进行推演,所以不可以推演函数参数和数组类型,
this指针可以为空,也就是说,自定义类型的指针指向空之后,照样存在有this指针。
析构函数不能被重载,构造函数可以被重载
析构函数和构造函数都不可以被继承
析构函数可以是虚函数,构造函数不可以是虚函数
static成员可以被继承
static成员不能被作为虚函数
static成员大小不包含在类的成员大小中
友元函数不可以被继承
一个类不想被继承可以使用final,
一个类不想实例化出对象,可以在类中的虚函数后面 添加=0,则此函数为纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象
派生类不充写抽象类中的纯虚函数的话也无法实例化对象
基类对象使用同一张虚表,虚表地址相同
派生类和基类永远不会使用同一张虚表,哪怕派生类并没有重写积累的虚函数,只是照搬下来,此时基类的派生类虚表地址不同(说明不是同一张虚表),但是内容一致
构造函数和析构函数可以抛出异常么?
首先是析构函数。
一. 析构函数
参照《Effective C++》中条款08:别让异常逃离析构函数。
总结如下:
1. 不要在析构函数中抛出异常!虽然C++并不禁止析构函数抛出异常,但这样会导致程序过早结束或出现不明确的行为。
2. 如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。
3. 如果析构函数中异常非抛不可,那就用try catch来将异常吞下,但这样方法并不好,我们提倡有错早些报出来。
二. 构造函数
总结如下:
1. 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
2. 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
3. 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。(如何保证???使用auto_ptr???)
最后总结如下:
-
构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!
-
不要在析构函数中抛出异常!