1 什么是多态?
定义:“多种状态”,指的是相同对象收到不同消息(静态多态),或者不同对象收到相同消息(动态多态),产生不同的结果。
1)静态多态:指相同对象收到不同消息,产生不同的结果--》函数重载
2)动态多态:指的是不同对象收到相同消息,产生不同的结果-->虚函数
代码实现:
#include <iostream>
using namespace std;
class base
{
public:
int num1;
int num2;
base(int a,int b):num1(a),num2(b)
{
cout<<"base()--------------\r\n";
}
~base()
{
cout<<"~base()---------------\r\n";
}
};
class sub:public base
{
public:
int num3;
sub(int a,int b,int c):base(a,b),num3(c)
{
cout<<"sub()------------\r\n";
}
~sub()
{
cout<<"~sub()------------\r\n";
}
};
int main()
{
base obj1(1,2);
sub obj2(10,20,30);
base *p=&obj1;
cout<<"p->num1="<<p->num1<<endl;
cout<<"p->num2="<<p->num2<<endl;
cout<<"--------------------\r\n";
//基类的指针在不进行显示类型转换的情况下可以指向派生类对象,
//通过基类的指针可以访问派生类继承自基类的成员,但是不能访问派生类新增的成员
p=&obj2;
cout<<"p->num1="<<p->num1<<endl;//10
cout<<"p->num2="<<p->num2<<endl;//20
// cout<<"p->num3="<<p->num3<<endl;
//基类的引用在不进行显示类型转换的情况下可以引用派生类对象,
//通过基类的引用可以访问派生类继承自基类的成员,但是不能访问派生类新增的成员
base &obj=obj2;
cout<<"obj.num1="<<obj.num1<<endl;
cout<<"obj.num2="<<obj.num2<<endl;
//cout<<"obj.num3="<<obj.num3<<endl;
//sub *q=&obj1;//子类的指针无法指向基类的对象
//sub &obj3=obj1;//子类的引用无法引用基类的对象
return 0;
}
2 多态是如何实现的?
c++的动态多态性是通过虚函数实现的
*什么是虚函数?
指在基类的函数声明前加virtual关键字,在派生类中可以对继承过来的。该虚函数进行函数重写,运行函数时,系统会根据对象的实际类型来调用相应的函数。如果对象是派生类对象,就调用派生类的重写该虚函数;如果对象是基类对象,就调用基类的虚函数。
*虚函数的工作原理?
含有虚函数的类,类的大小会比不含虚函数的类的大小多出4个字节,多出的4个字节存放的是虚函数表指针。每一个含有虚函数的类都有一个虚函数表。
*虚函数表指针(vptr)?
指向类中虚函数表的指针
*虚函数表:
是存放类中所有虚函数地址的一张表(存放顺序和虚函数的声明顺序一致)c++编译器会给含有虚函数的类编译生成一张虚函数表,并生成一个虚函表指针指向该虚函数表。当我们该类定义对象时,对象的地址会自动赋给虚函数表指针,所以对象可以直接调用虚函数;当我们用基类的指针指向派生类对象时,基类指针的值就是派生类对象的地址,所以基类指针p可以通过vptr访问子类对象的虚函数(重写后的)。
*虚函数virtual的使用限制:
1)virtual不能修饰类外的普通函数,只能修饰类中的成员函数(普通成员函数和析构函数);
2)不能修饰构造函数;
3)不能修饰静态成员函数;
代码实现:
#include <iostream>
using namespace std;
class B
{
public:
int b;
void func()
{
cout<<"B::func()--------------\r\n";
}
};
class B1:virtual public B
{//虚基类指针4
private:
int b1;
};
class B2:virtual public B
{
private:
int b2;
};
class C:public B1,public B2
{
private:
int c;
void func2()
{
B1::func();//加作用域
cout<<"C::func2()------------\r\n";
}
#if 0
public:
void func() //同名覆盖
{
cout<<"C::func()------------\r\n";
}
#endif
};
int main()
{
C obj;
obj.B2::func();//B::func() 加作用域
obj.func();//B::func()
cout<<"B:size="<<sizeof(B)<<endl;//4
cout<<"B1:size="<<sizeof(B1)<<endl;//12
cout<<"B2:size="<<sizeof(B2)<<endl;//12
cout<<"C:size="<<sizeof(C)<<endl;//24 12+12+4-4=24
return 0;
}
3 多态的作用
作用:对外提供高度统一的接口,实现接口复用,提高代码的可扩展性和可维护性
4 实现多态的条件
条件:1) 必须要有继承;
2) 在派生类中重写继承自基类的虚函数;
3) 通过基类的指针或引用指向派生类对象,通过基类指针或引用调用虚函数;
5 函数重写:
就是重新定义,子类继承了基类的虚函数,子类可以对该虚函数进行重新定义,子类的该虚函数的函数头和基类的函数头一致,但是函数体和基类的不一样。
代码实现:
#include <iostream>
using namespace std;
class base
{
public:
base()
{
cout<<"base()------------\r\n";
}
~base()
{
cout<<"~base()--------------\r\n";
}
virtual void func()
{
cout<<"base::func()--------------\r\n";
}
};
class sub:public base
{
public:
sub()
{
cout<<"sub()--------------\r\n";
}
~sub()
{
cout<<"~sub()-------------\r\n";
}
void func()
{
cout<<"sub::func()-----------\r\n";
}
};
int main()
{
sub obj;
obj.func();//调用的是重写后的
//obj.base::func();//子类函数重写后,继承的基类的该虚函数还隐式存在,要使用必须加作用域显示调用
base *p=&obj;
p->func();//一定调用的是重写后的
return 0;
}
6 关于函数重载 函数重写 函数隐藏相关概念
*函数重载:是指函数名相同,参数不同(个数、类型),与返回值值无关
函数重载特点:A 相同的作用域 B 函数名相同 C 参数不同 D 没有virtual关键字E 返回值可以相同,也可以不同
*函数重写:成员函数覆盖(指派生类重新定义继承自基类的虚函数)
函数重写特点:A 不同的作用域(分别位于基类和派生类中) B 函数名相同 C 参数相同 D 函数体不同 E 基类的函数前必须有virtual关键字,子类的该函数可有可无 F 返回值相同 G 重写函数的访问限定符可以和基类的访问限定符不同
*函数隐藏:同名覆盖,指的是成员函数隐藏
函数隐藏特点:A 不同的作用域(分别位于基类和派生类中) B 函数名相同,函数名前没有virtual关键字 C 返回值可相同,也可以不同 D 参数可以相同,也可以不同。
7 纯虚函数和抽象类
*纯虚函数:基类中的虚函数,只有声明,没有函数体。例如:virtual void move()=0;
*抽象类:含有纯虚函数的类就是抽象类,一般抽象类作为接口类使用,一般用于基类。抽象类无法实例化对象。
8 虚析构函数
基类的指针指向派生类对象,当通过基类的指针要释放派生类对象内存的时候(delete p),系统只会调用基类的析构函数去析构子类对象中的基类部分,不会调用子类的析构函数去析构子类新增部分。那么子类成员所占的内存空间就不会被释放,会造成内存泄漏或堆内存空间浪费。那么如何解决这个问题呢?用虚析构函数,将基类的析构函数设计成虚析构函数。(在基类的析构函数前加virtual关键字)。
注意:只要将基类的析构函数设计成虚析构函数,通过基类的指针去释放子类对象的内存时,就能将子类对象的所有空间释放掉
建议:若要将一个类作为基类,那么就把该类的析构函数设计成虚析构函数
代码实现:
#include <iostream>
using namespace std;
class base
{
private:
int bnum;
public:
base(int val=0)
{
bnum=val;
cout<<"base()----------\r\n";
}
virtual ~base()
{
cout<<"~base()------------\r\n";
}
};
class sub:public base
{
private:
int snum;
public:
sub(int a,int b):base(a),snum(b)
{
cout<<"sub()-------------\r\n";
}
~sub()
{
cout<<"~sub()--------------\r\n";
}
};
int main()
{
cout<<"sub size="<<sizeof(sub)<<endl;//8
base *p=new sub(1,2);
delete p;
return 0;
}
9 c++关键字
9.1 explicit关键字
作用:用来防止单参数的构造函数发生隐式类型转化(把基本数据类型转化成类类型)
特点:1) 只能修饰类中的构造函数 ;2) explicit关键字只对一个参数的构造函数有效,对多参数的构造函数无效(多参数的构造函数本身也不会发生隐式类型转化)
代码示例:
#include <iostream>
using namespace std;
class demo
{
public:
explicit demo(int val=0)
{
ival=val;
cout<<"demo()----------------\r\n";
}
~demo()
{
cout<<"~demo()-------------\r\n";
}
void getint()
{
cout<<"getint():ival="<<ival<<endl;
}
private:
int ival;
};
int main()
{
// demo obj=10;//隐式类型转换 等价于 demo a(10);demo obj=a;
obj.getint();
return 0;
}
9.2 override关键字
作用:若在父类中定义了虚函数,在子类中要对继承的该虚函数做函数重写,override关键字会帮你检查重写的虚函数名和基类的虚函数名是否一致?若不一致,编译会报错。
代码示例:
#include <iostream>
using namespace std;
class base
{
public:
virtual void func()
{
cout<<"base::func()----------\r\n";
}
virtual void func1()
{
cout<<"base::func1()------------\r\n";
}
};
class sub:public base
{
public:
virtual void func() override
{
cout<<"sub:func()-----------\r\n";
}
};
int main()
{
base *p=new sub;
p->func();
return 0;
}
9.3 final关键字
作用:当不希望某个类被继承,或者不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加后,此类就是一个final类(final类不能派生子类),此虚函数就是一个final虚函数(final虚函数不能被重写)。
代码示例:
#include <iostream>
using namespace std;
class base
{
public:
void func()
{
cout<<"base()----------\r\n";
}
};
class sub:public base
{
public:
void func1()
{
cout<<"sub::func1()-------\r\n";
}
};
class sua final:public sub
{
public:
void func2()
{
cout<<"sua::func2---------\r\n";
}
};
#if 0
class suc:public sua //final类不能派生新的子类
{
public:
void func3()
{
cout<<"suc::func3()-----------\r\n";
}
};
#endif
class A
{
public:
virtual void func()
{
cout<<"A::func()---------\r\n";
}
};
class B:public A
{
public:
virtual void func() final //最后一次被重写
{
cout<<"B::func()----------\r\n";
}
};
class C:public B
{
public:
void func()
{
cout<<"C::func()--------------\r\n";
}
};
int main()
{
sua obj;
obj.func();
return 0;
}
9.4 inline关键字
作用:inline用来修饰c++类中的函数,被修饰的函数称为内联函数,内联函数是c++的函数宏在c++中,常量宏用const表示,函数宏用内联函数表示(使用函数宏比使用函数的系统开销小,所以建议将简短的频繁调用的成员函数设计成内联函数)
Inline内联函数的特点:1) 函数体功能简单、代码简短、调用频繁; 2) 内联函数是宏函数,是宏,使用时只做宏替换,所以系统开销小;
注意:
1) inline必须和函数的定义一起使用,如果只是和函数的声明放一起,则inline不起作用;
2) 如果函数体代码过长或函数体内有循环语句、if语句或switch语句或递归算法语句,那么不宜将此函数设计成内联函数;
3) 内联函数只是对编译的一个建议,是否能真正内联,取决于编译器;
代码示例:
#include <iostream>
using namespace std;
class demo
{
public:
#if 0
inline void func() //内联函数
{
cout<<"demo::func()-----------\r\n";
}
#endif
inline void func();
};
inline void demo::func()
{
cout<<"demo::func()-----------\r\n";
}
int main()
{
demo obj;
obj.func();
return 0;
}
10 异常处理
标准异常处理机制:异常处理分成三部分:try检查异常-------throw抛出异常------catch捕获异常。
语法格式为:
try
{
//检查语句
if(错误)
throw 异常
}
catch(异常类型比较)
{处理异常语句;}
catch(异常类型)
{处理异常语句;}
。。。。
代码示例:
#include <iostream>
#include <stdexcept>
using namespace std;
int func(int a,int b)
{
if(0==b)
{
throw(invalid_argument("div is 0"));
}
return a/b;
}
int main()
{
try
{
int a=10,b=0;
int ret=func(a,b);
cout<<"ret="<<endl;
}
catch(invalid_argument&err)
{
cout<<"error "<<err.what()<<endl;//打印err的错误信息
}
catch(int err)
{
cout<<"......"<<endl;
}
return 0;
}
11 转化函数
c++提供类型转化函数,用来将一个类的对象转化成另一类型的数据。转化函数的本质就是运算符重载,只是重载的不是内置的运算符,而是类名这个特殊的数据类型。
有两种转换函数:
1>自定义转化函数
语法格式:operator 类型名(){实现转化的语句块;}
转化函数的基本规则:
1) 转换函数只能是类的成员函数,没有返回值,没有入参;
2) 转化函数不能将类类型转化成void类型,也不能转化成数组和函数;
3) 转化函数通常定义成const形式,原因是它并不该变数据成员的值;
2> 标准转化函数
c++标准库提供了标准转化函数:
1) reinterpret_cast:可以做不同类型对象之间的转化,还可以将指针类型转化成整型类型(可以做不同数据类型之家的指针和引用之间的转化);
2) const_cast:可以将常指针或常引用转化成非常指针或非常引用;
3) static_cast:可以做基本数据类型之间的转化,也可以做具有继承关系的对象之间的转化;
4) dynamic_cast:可以做具有继承关系的指针或引用的转化;
代码示例:
#include <iostream>
using namespace std;
class demo
{
public:
demo(int a):ival(a)
{
cout<<"demo()------------\r\n";
}
~demo()
{
cout<<"~demo()-----------\r\n";
}
operator int() const
{
return this->ival;
}
private:
int ival;
};
class demo1
{
public:
demo1(int a,int b):val(a),val2(b)
{
cout<<"demo1()--------------------\r\n";
}
~demo1()
{
cout<<"~demo1()--------------------\r\n";
}
operator double() const
{
return ((double)val/val2);
}
private:
int val;
int val2;
};
int main()
{
demo obj(100);
int tmp=obj;
cout<<"tmp==="<<tmp<<endl;//100
demo1 obj1(3,5);
double d=5+obj1;
cout<<"d==="<<d<<endl;//5.6
return 0;
}
12 智能指针(代表一个类作用域)
12.1为什么要使用智能指针?
c++没有自动内存回收机制,当我们写一个new语句时,一般也会把delete语句写上,单我们不能避免程序还为执行到delete时就跳转了 或函数还没有执行到delete时就返回了,那么此时就会造成内存泄露。所以为了避免内存泄露,引入了智能指针,因为智能指针就是一个类作用域,当超过了类的作用域时,系统会自动调用析构函数来释放内存资源。
12.2本质
让智能指针的生命周期和堆内存的生命周期保持一致,释放智能指针时,自动释放堆内存空间
12.3 c++提供了4种常见的智能指针(类模板)
1)auto_ptr:自动智能指针(新版c++已废弃);
2)shared_ptr:共享性智能指针,可以允许多个智能指针指向同一块内存空间,可以自动把堆内存空间释放(最后一个智能指针没了,空间才会被释放);
3)weak_ptr:弱型智能指针,不能单独存在,必须和共享型智能指针一起使用(依附 型智能指针),弱型智能指针的增加和消亡不会影响堆内存的释放;
4)unique_ptr:独享型智能指针,同一时刻只能有一个独享型智能指针指向一块内存;
注意: reset()函数释放智能指针(让智能指针不在指向new demo);expired函数检查new的空间是否被释放了?若释放了,返回真;若未释放,返回假。
代码示例:
#include <iostream>
#include <memory>
using namespace std;
class demo
{
public:
demo()
{
cout<<"demo()------------\r\n";
}
~demo()
{
cout<<"~demo()----------\r\n";
}
};
int main()
{
shared_ptr<demo> p(new demo);//p是智能共享性指针,指向new demo
shared_ptr<demo> q=p;//q也指向new demo
cout<<q.use_count()<<endl;//2 查看堆内存空间的所有者个数
p.reset();//释放智能指针P,p不再指向new demo
weak_ptr<demo> r=q;//r是弱性智能指针,r指向new demo
cout<<q.use_count()<<endl;//1
q.reset();//释放智能指针q
if(r.expired())
{
cout<<"堆内存空间被释放了----\r\n";
}
else
{
cout<<"堆内存空间未被释放-------\r\n";
}
return 0;
}