类和对象
基本概念
1. 类的封装性
有三种权限:private, protected, public
只有public能够被外部访问,默认为private
class Data{
public:
int a;
private:
int c;
}
声明类时不要初始化,因为此时没有分配空间
类相当于一个类型,需要实例化之后再使用
2. 示例
#include <iostream>
#include "fun.h"
using namespace std;
class Cube{
private:
int c;//长
int k;//宽
int g;//高
public:
void cubeInit(int cl, int kl, int gl){
c = cl;//注意不能同名,同名需要用this
k = kl;
g = gl;
}
int getS(void){
return (c*k+c*g+k*g)*2;//求面积
}
int getV(void){
return c*k*g;//获取体积
}
};
int main(void){
Cube ob;
ob.cubeInit(10,20,30);
cout<<ob.getS()<<endl;
cout<<ob.getV()<<endl;
return 0;
}
比较两个立方体
bool cubeCompare(Cube &ob1, Cube &ob2){
//if(ob1.c == ob2.c) //报错,私有变量不允许外部访问
//需要在类内部实现一个返回长的函数
if(ob1.getC() == ob2.getC()){}
}
3. 成员函数在类外实现
内部声明,外部实现
class Data{
private:
int mA;
public:
void setA(int a);
int getA(void);
};
void Data::setA(int a){//加上作用域修饰
mA = a;
}
4. 类在其他源文件中实现
data.h(C++ Class)
#ifndef DATA_H
#define DATA_H
class Data{
private:
int mA;
public:
void setA(int a);
int getA(void);
};
#endif // DATA_H
data.cpp(alt+enter)
#include "data.h"
void Data::setA(int a){
mA = a;
}
int Data::getA(){
return mA;
}
然后就可以在main函数中使用了
构造函数
1. 概述
构造函数和析构函数用于完成对象初始化和对象清理,由编译器自动调用
如果用户不提供构造函数,编译器会自动添加一个默认的构造函数(空函数);用户添加构造函数,编译器就不再设置默认构造函数
构造函数和类名相同,没有返回值类型,可以有参数,可以重载
先给对象开辟空间(实例化),然后调用构造函数(初始化)
构造函数在public区
class Data{
public:
int mA;
public:
Data(){
mA = 0;//无参构造函数
}
Data(int a){
mA = a;//有参构造函数
}
}
2. 调用构造函数
int main(void){
//隐式调用构造函数
Data ob1;
Data ob2(10);
//显式调用构造函数
Data ob3 = Data();
Data ob4 = Data(10);
//构造函数隐式转换(类中只有一个数据成员)
Data ob5 = 100;//相当于ob4
return 0;
}
Data()是一个匿名对象(存活周期为当前语句),在这里申请空间,然后执行构造函数,然后给ob3接管
析构函数
当对象生命周期结束后,系统自动调用析构函数
先调用析构函数进行清理,再释放空间
析构函数和类名相同,再函数名前加~(为了和构造函数区别),没有返回值,没有参数,不能重载
class Data{
public:
Data(){
cout<<"构造函数"<<endl;
}
~Data(){
//析构函数
cout<<"析构函数"<<endl;
}
};
Data ob1;//报错
void test(){
Data ob2();
{
Data ob3();
}
Data ob4();
}
采用栈的原理,先构造的对象后析构
一般情况下,空的析构函数就可以,但是如果类中有指针成员,必须写析构函数释放指针成员所指空间
释放时释放的是对象自身的空间,不会释放成员变量指向的堆空间
#include <iostream>
#include <string.h>
using namespace std;
class Data{
public:
char *name;
Data(){}
Data(char *str){
name = new char[strlen(str)+1];
strcpy(name, str);
cout<<"有参构造"<<endl;
}
~Data(){
if(name != NULL) delete []name;
cout<<"析构函数"<<endl;
}
};
int main(void){
Data ob("hello world");
cout<<ob.name<<endl;
return 0;
}
拷贝构造函数
Data ob1(10);
Data ob2 = ob1;//一次构造,一次拷贝构造,两次析构
1. 概述(浅拷贝)
拷贝构造:本质是构造函数
拷贝构造的调用时机:旧对象 初始化 新对象 才会调用拷贝构造函数,取别名&不算拷贝构造
用户不提供拷贝构造,编译器会提供一个默认的拷贝构造,完成赋值动作——浅拷贝
Data(const Data &ob){//拷贝构造的定义形式
//一旦自定义拷贝构造,则会使系统默认的失效。此时必须完成赋值操作
mA = ob.mA;
}
2. 拷贝构造,无参构造,有参构造
如果定义了拷贝构造和有参构造,会屏蔽系统默认的无参构造。
也就是说,如果只定义了有参构造和拷贝构造,再执行Data ob1会报错,因为此时无法找到系统默认的无参构造,因为被屏蔽了
如果定义了无参构造或有参构造,不会屏蔽拷贝构造
3. 拷贝构造的几种调用形式
(1)旧对象给新对象初始化,调用拷贝构造
Data ob2 = ob1;//一次构造,一次拷贝构造,两次析构
(2)给对象取别名,不会调用拷贝对象
Data &p = ob;
(3)普通对象作为函数参数,调用函数时会发生拷贝构造
(4)函数返回普通值对象
Data ob2 = getObject();//返回一个Data对象ob1
ob1开辟空间,触发构造函数。getObject函数结束时,ob1的值放在临时空间(返回值小于4字节放寄存器,大于4字节放临时空间)
在临时区域申请一个匿名对象,再把ob1的值赋值过去——在这个过程中调用拷贝构造
ob2与拷贝构造毫无关系,即便不用ob2去接,也会调用拷贝构造
ob2将占用了匿名空间,这一过程不会发生拷贝构造(在vs中)
(5)QTcreator、linux不会发生拷贝构造
Data getObject(void){
Data ob1(10);
return ob1;
}
void test(){
Data ob2 = getObject(); //仅一次构造,一次析构;无其他
}
QT和linux进一步优化,ob1构造,ob2析构,不再经过中间的匿名对象;
ob2直接接管ob1的空间,并导致作用范围改变
4. 拷贝对象的浅拷贝和深拷贝
如果没有指针成员,不必实现拷贝构造和析构函数
如果有指针成员,且指向堆区空间,必须实现析构释放堆区空间,必须实现拷贝构造完成深拷贝动作(否则会导致多次释放)
Data(const Data &ob){
name = new char[strlen(ob.name)+1];
strcpy(name, ob.name);//深拷贝的拷贝构造
}
初始化列表
1. 对象成员
在类中定义的对象,先调用对象成员的构造函数,再调用本身的构造函数;先构造的后析构
B中有A的对象变量,那么依次调用A类构造 - B类构造 - B类析构 - A类析构
类会自动调用对象成员的无参构造
2. 初始化列表
类想调用对象成员 有参构造 必须使用 初始化列表
class B{
int mb;
A oa;
B(int a, int b){
oa.ma = a;
}
}
B ob(10,20);
(1)B(int a, int b)构造函数希望将a传给oa中的ma,b传给自己的mb;
(2)执行B ob(10, 20);时,先初始化A oa;此时会调用A的构造函数。
(3)由于B的构造函数B(int a, int b)在A构造之后才会执行,所以参数a=10无法在A执行构造函数时传过去创建oa时先执行一边A的
(4)现在希望在构造A的时候,在构造B之前,将a传过去,此时就需要初始化列表
3. 初始化列表示例
class B{
int mb;
A oa;
B(int a, int b):oa(a){
oa.ma = a;
}
}
B ob(10, 20);
(1)B ob(10, 20); 找到构造函数 B(int a, int b),但此刻不会执行,因为需要先做A的构造
(2)加上:ob(a)表示初始化列表,B(int a, int b):ob(a)语句就是忽略掉B(int a, int b)的构造函数,先执行初始化列表ob(a)不加初始化列表时,默认调用无参构造;定义了初始化列表后,可以调用有参构造
explicit关键字
禁止构造函数进行的隐式转换,针对单参数的构造函数(或除第一个参数外其余参数都有默认值的多参构造)而言
class Data{
public:
explicit Data(int n){
cout<<n<<endl;
}
};
void test(){
//Data str = 1;//禁止这种转换,不加explicit可以运行
}
类的对象数组
A arr[5] = {A(10),A(20),A(30),A(40),A(50)};
动态对象创建
malloc和free在C++不能很好地完成初始化和析构函数
C++创建对象时:(1)分配内存空间;(2)调用构造初始化,调用析构清理
new动态分配
C++将创建对象所需要的操作结合在一个new的运算符中。
当new一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化
Person *person = new Person;
new能确定在调用构造函数初始化之前内存分配是成功的,存在内置的长度计算,类型转换和安全检查
detele释放动态空间
delete先调用析构函数,然后释放内存
Person *person = new Person("John",33);
delete person;
在栈上面的对象,所在作用域结束后,会自动执行析构函数,而new出来的在堆上的对象,不调用delete,即使它所在的作用域已经结束,也不会调用析构函数
类成员有vector也指向了堆上的内存,就需要在析构函数中同样使用delete释放这块内存
静态成员
static修饰的静态成员属于类,而不是对象,即所有对象共享一份静态成员数据
static修饰的成员,在定义类的时候,必须分配空间
static修饰的静态成员数据,必须类中定义,类外初始化
class Data{
public:
int a;//a为每个实例化对象独有
static int b;//b为所有实例化对象共有
};
//类外初始化
int Data::b = 100;
void test(){
//cout<<Data::a<<endl;//报错
cout<<Data::b<<endl;//输出:100
}
对b的修改是共有的
Data ob1;
Data ob2;
ob1.b = 200;
ob2.b = 400;
cout<<ob1.b<<endl;//输出:400
cout<<ob2.b<<endl;//输出:400
案例:统计创建对象的个数
class Data{
public:
int a;
static int cnt;
public:
Data(){
cnt++;
}
};
静态成员函数与静态成员相似,但是静态成员函数只能访问静态成员数据
同样需要类中定义,类外初始化
单例模式设计
一个类只实例化一个对象
1. 使不能实例化
class SingleTon{
//1. 防止该类在外界实例化对象,构造函数私有化
private:
SingleTon(){}
SingleTon(const SingleTon &ob){}
~SingleTon(){}
};
2. 单例模式
//单例模式类
class SingleTon{
//1. 防止该类在外界实例化对象,构造函数私有化
private:
SingleTon(){}
SingleTon(const SingleTon &ob){}
~SingleTon(){}
private:
//2. 定义一个静态的指针变量保存唯一实例的值
static SingleTon * const p;
public:
//3. 获得唯一的实例地址
static SingleTon * getSingleTon(void){
return p;
}
//4. 用户定义的任务函数
void printString(char *str){
cout<<str<<endl;
}
};
SingleTon * const SingleTon::p = new SingleTon;
int main(void){
//获取单例的地址
SingleTon *p1 = SingleTon::getSingleTon();
p1->printString("p1");
SingleTon *p2 = SingleTon::getSingleTon();
p1->printString("p2");
return 0;
}
this指针(面对对象模型)
成员变量和函数的存储
C++中非静态数据成员直接内含在类对象中,成员函数虽然内含在class声明之内,却不出现在对象中。每一个非内联成员函数只会诞生一份函数实例
也就是说非静态变量是对象独有的,函数是所有对象共有的
class Data{
public:
char a;
int b;
static int c;
void func(){};
};
//a和b为每个对象独有的,c和func是共享的
Data ob;//sizeof(ob) == 8,即便只有a和b也是8(可能是由于字节对齐的原因),不会将c和func的大小计算在内
//sizeof测量的是Data实例化对象的大小
this指针
1. 概述
成员函数是怎么知道是哪个对象调用自己的?this指针
class Data{
private:
int ma;
public:
void seta(int a){
ma = a;
}
};
Data ob1;
Data ob2;
Data ob3;
ob1.seta(10);//既然setA是公用的,那么setA怎么知道是谁调用的自己,并将a的值赋值到该对象中
实际上,在setA函数中,ma=a是this.ma = a的缩写,this中保存调用该成员函数的对象的地址,由编译器自动完成
this指针是一种隐含指针,隐含于每个类的非静态成员函数,无需定义,直接使用
注意:静态成员函数没有this指针,静态成员函数在对象出现之前出现,如果有this,在对象出来之前this不知道指向谁,并且静态成员函数只能操作静态成员变量
2. 形参和变量同名时可以使用this指针
class Data{
private:
int a;
public:
Data(int a){
this->a = a;
cout<<this->a<<endl;
}
};
Data ob1(10);//&ob1 == this
this指向ob1的地址,因此能够找到正确的对象
3. 链式操作
class Data{
public:
Data &myprintf(char *str){
cout<<str<<endl;
return *this;//返回调用该成员函数的对象(匿名对象)的别名
}
};
void test(){
Data().myprintf("one").myprintf("two").myprintf("three");
}
const修饰成员函数
const修饰的函数不允许对类中变量进行修改,mutable修饰的变量除外
class Data{
public:
int a;
int b;
mutable int c;
public:
Data(int a, int b, int c){
this->a = a;
this->b = b;
this->c = c;
}
void showData(void) const{
//a = 100;//报错
c = 100;//成功
}
};
友元
类的私有成员不允许在外部访问,友元将允许这种访问
使用关键字friend修饰
friend只出现在声明处,一个函数或一个类成为了另一个类的友元,就可以直接访问另一个类的私有数据
友元重要用在运算符重载上
1. 普通全局函数作为类的友元
class Data{
friend void visit(Data &data);
private:
int a;
public:
Data(int a){
this->a = a;
}
};
void visit(Data &data){
cout<<data.a<<endl;
}
void visit2(Data &data){
cout<<data.a<<endl;
}
int main(void){
Data data(10);
visit(data);
visit2(data);//报错:不能访问私有变量
return 0;
}
2. 类中的某个成员函数作为另一个类的友元
class B;//必须先声明
class A{
private:
int aa;
public:
A(int aa){
this->aa = aa;
}
public:
void visit(B &obB);
};
class B{
friend void A::visit(B &obB);
private:
int bb;
public:
B(int bb){
this->bb = bb;
}
};
void A::visit(B &obB){
cout<<obB.bb<<endl;
}
int main(void){
A obA(10);
B obB(20);
obA.visit(obB);
return 0;
}
如果要使得A的函数能够访问B中的私有变量,那么只需要在B中增加该函数的友元
3. 整个类作为另一个类的友元
如A要访问B的私有成员,那么A类要成为B类的友元。在B类中增加语句:
friend class A;
4. 友元的注意事项
友元不能被继承
友元关系是单向的
友元不具备传递性。B是A的朋友,C是B的朋友,不能得到C是A的朋友
运算符重载
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
语法: 定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算
符。
1. 全局函数实现
class Person{
friend ostream &operator<<(ostream &out, Person &ob);//必须设置友元
friend istream &operator>>(istream &in, Person &ob);
friend Person operator+(Person &ob1, Person &ob2);
private:
int num;
string name;
float score;
public:
Person(){}
Person(int num, string name, float score):num(num),name(name),score(score){}
};
//全局函数重载operator<<
//cout<<lucy<<endl;//报错,cout<<lucy无返回值,将变成void<<endl,没有这种运算符,因此报错。需要设置返回值
/*
void operator<<(ostream &out, Person &ob){
out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
}
*/
//cout<<lucy<<endl;成功
ostream &operator<<(ostream &out, Person &ob){
out<<ob.num<<" "<<ob.name<<" "<<ob.score;
return out;
}
//全局重载函数operator>>
istream &operator>>(istream &in, Person &ob){
in>>ob.num>>ob.name>>ob.score;
return in;
}
Person operator+(Person &ob1, Person &ob2){
Person tmp;
tmp.num = ob1.num+ob2.num;
tmp.name = ob1.name+ob2.name;
tmp.score = ob1.score+ob2.score;
return tmp;
}
int main(void){
Person lucy(100,"lucy",99.8f);
Person bob(100,"bob",88.0f);
cout<<lucy<<endl;//不定义运算符重载则报错
//相当于operator<<(cout, lucy)
Person tmp = lucy+bob;
cout<<tmp;//这样可以
//cout<<lucy+bob;//报错,因为operator<<的参数为Person &ob,而lucy+bob返回的是一个临时数据,不能取引用。可以将Person &ob改成Person ob
return 0;
}
可以重载的运算符
2. 成员函数实现(推荐)
class Person{
friend ostream &operator<<(ostream &out, Person ob);//不用设置operator+的友元
private:
int num;
string name;
float score;
public:
Person(){}
Person(int num, string name, float score):num(num),name(name),score(score){}
Person operator+(Person ob){
Person tmp;
tmp.num = num+ob.num;
tmp.name = name+ob.name;
tmp.score = score+ob.score;
return tmp;
}
};
ostream &operator<<(ostream &out, Person ob){
out<<ob.num<<" "<<ob.name<<" "<<ob.score;
return out;
}
int main(void){
Person lucy(100,"lucy",99.8f);
Person bob(100,"bob",88.0f);
//lucy+bob == lucy.operator+(bob)
//cout<<lucy+bob;
return 0;
}
3. 重载++/–运算符
++a:operator++(a)
a–:operator++(a, int);
Person operator++(int){//a没有传,因为this替代了
Person old = *this;
this->num++;
this->name = this->name+this->name;
this->score++;
return old;
}
cout<<lucy++<<endl;
cout<<lucy<<endl;
4. 重载函数调用运算符
class Print{
public:
//重载函数调用运算符
void operator()(char *str){
cout<<str<<endl;
}
};
int main(void){
Print ob;
//对象和()结合触发operator()调用;也称为仿函数
ob("hello");
Print()("world");//匿名对象结合()也可以触发
return 0;
}
智能指针
解决堆区空间的释放问题
class Data{
public:
Data(){
cout<<"Data的无参构造"<<endl;
}
~Data(){
cout<<"Data的析构函数"<<endl;
}
void func(){
cout<<"Data的func函数"<<endl;
}
};
class SmartPointer{
private:
Data *p;
public:
SmartPointer(){};
SmartPointer(Data *p){
this->p = p;
}
~SmartPointer(){
delete p;
}
//重载->运算符
Data *operator->(){
return p;
}
Data &operator*(){
return *p;
}
};
int main(void){
SmartPointer sp(new Data);
sp.operator ->()->func();
sp->func();
(*sp).func();
return 0;
}
继承
继承的定义
class 父类{};
class 子类:继承方式 父类名{
//新增子类数据
};
继承方式:private protected public(推荐)
public继承:父类的public为自己的public,protected为自己的protected,父类的private不可访问
protected继承:父类的public为自己的protected,protected为自己的protected,父类的private不可访问
private继承:父类的public为自己的private,protected为自己的private,父类的private不可访问
继承中的构造和析构
父类构造——内部对象构造——子类构造
子类析构——内部对象析构——父类析构
子类会默认调用成员对象、父类的默认构造
子类实例化对象时,必须使用初始化列表调用成员对象、父类的有参构造(父类写类名称,成员对象写对象名)
子类和父类的同名处理
最简单、最安全的处理方式:加作用域
1. 子类和父类同名成员数据
如父类中有int a,子类中也有int a;
子类优先访问自己的成员数据
要想访问父类的成员,需要加作用域
cout<<ob.a<<endl;//优先访问子类的变量
cout<<ob.Base::a<<endl;//作用域访问父类的变量,访问方法同理
2. 子类重定义父类的同名函数
子类一旦重定义父类的同名函数,父类的同名函数(重载函数)都将被屏蔽,但仍然可以通过作用域的方式访问
不能被继承的函数
构造函数、析构函数、operator=
如果子类没有自己创建这些函数,编译器会自动生成
多继承
class 父类1{};
class 父类2{};
class 子类:继承方式1 父类1, 继承方式2 父类2{
//新增子类数据
};
菱形继承
有公共祖先的继承,叫菱形继承
B和C都继承于A,D多继承于B和C,于是D就有多份A的成员,访问时会产生二义性。解决方法:加作用域
如果只想要一份公共数据:虚继承
虚继承
在继承方式前面加virtual继承,保留一份公共数据
class B:virtual public A{};
class C:virtual public A{}
class D:public B, public C{};
虚继承 会在子类中产生 虚基类指针(vbptr) 指向 虚基类表(vbtable), 虚基类表纪录的是通过该指针访问公共祖先的数据的偏移量。
注意:虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的。工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。
多态
静态多态(编译时多态,早绑定):函数重载、运算符重载、重定义
动态多态(运行时多态,晚绑定):虚函数
父类指针保存子类空间
需求:设计一个算法,可以操作父类派生的所有子类
方法:父类指针保存子类的空间地址
class Animal{
public:
void speak(void){
cout<<"动物在说话"<<endl;
}
};
class Dog:public Animal{
public:
void speak(void){
cout<<"狗在说话"<<endl;
}
};
int main(void){
Dog *p1 = new Dog;
p1->speak();//狗在说话
Animal *p2 = new Dog;
p2->speak();//动物在说话
Dog *p3 = new Animal;
p3->speak();//报错!
return 0;
}
p2输出“动物在说话”的原理:
一个指针变量能操作空间的大小是由它所指向的类型决定的。new Dog给出的空间是父类+自身,而p2只能有父类大小的空间,因此p2指向的是new Dog中父类的部分
虚函数
成员函数前加virtual修饰
//1. 在父类中定义虚函数
virtual void speak(void){
cout<<"动物在说话"<<endl;
}
//2. 在子类中重写函数;此时返回类型、函数名、参数必须完全一致,但这里的virtual可写可不写
virtual void speak(void){
cout<<"狗在说话"<<endl;
}
//3. 此时输出的是子类函数内容
Animal *p2 = new Dog;
p2->speak();//狗在说话
如果一个类中的成员函数被virtual修饰,那么这个函数就是虚函数。类就会产生一个虚函数指针(vfptr)指向了一张虚函数表(vftable)。如果这个类没有涉及到继承, 这时虚函数表中纪录的就是当前虚函数入口地址。
子类继承父类的虚函数指针,并且一旦子类重写父类虚函数,就会用子类的虚函数地址覆盖掉虚函数表中原来的入口地址
当执行Animal *p2 = new Dog时,p2仍然指向父类的空间,但是由于父类的虚函数表中的入口地址被覆盖,因此会执行子类函数
重载、重定义、重写的区别
重载:同一作用域,同名函数,参数的顺序、个数、类型不同 都可以重载。函数的返回值类型不能作为重载条件(函数重载、运算符重载)
重定义:有继承,子类 重定义 父类的同名函数(非虚函数), 参数顺序、个数、类型可以不同。子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)
重写(覆盖):有继承,子类 重写 父类的虚函数。返回值类型、函数名、参数顺序、个数、类型都必须一致。
纯虚函数
如果基类一定会派生出子类,且子类一定会重写父类的虚函数,那么父类中的函数体就显得无意义
class Animal{
public:
//纯虚函数
virtual void speak(void) = 0;
};
Animal p;//报错
有纯虚函数的类被称为抽象类,不能实例化对象
子类必须重写父类的所有纯虚函数。因为只要有一个纯虚函数,就是抽象类,抽象类不能实例化
虚函数不会导致父类变成抽象类,但纯虚函数会
纯虚函数案例 - 制作饮品
//抽象制作印品
class AbstractDrinking{
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void markDrink(){
this->Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee:public AbstractDrinking{
public:
//烧水
void Boil(){
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将咖啡倒入杯中" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入甲醛!" << endl;
}
};
//制作茶水
class Tea:public AbstractDrinking{
public:
//烧水
void Boil(){
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将茶水倒入杯中" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入乙醚!" << endl;
}
};
//业务函数
void doBussiness(AbstractDrinking *drink){
drink->markDrink();
delete drink;
}
int main()
{
doBussiness(new Coffee);
cout<<"-------------------"<<endl;
doBussiness(new Tea);
return 0;
}
虚析构
问题:只调用了父类的析构
原因:p执行父类的空间,析构也只能析构父类空间
需求:父类和子类都需要析构
Animal *p = new Dog;
p->speak();//动物在说话
delete p;
依次执行:Animal的构造 - Dog的构造 - speak函数 - Animal的析构
解决方法:在父类析构函数上加上virtual,就可以释放子类的空间
virtual ~Animal(){
cout<<"Animal的析构"<<endl;
}
此时依次执行:Animal的构造 - Dog的构造 - speak函数 - Dog的析构 - Animal的析构
原理:通过父类指针释放子类的所有空间。与虚函数类似,但并不是重写入口地址
-
父类的虚函数指针,指向虚函数表,这个表中记录的是父类的析构函数入口地址
-
子类继承时会吧虚析构指针和虚函数表继承过来,此时会修改使指向子类的析构函数(此时非重写)
-
delete p的时候,依然去父类空间,但指针此时指向的是子类析构函数,于是执行子类的析构
-
子类析构完之后,会自动调父类析构
纯虚析构
class Animal{
public:
virtual void speak(void) = 0;
//纯虚析构
virtual ~Animal() = 0;//可以在内部实现函数体,但不推荐。有的编译器不支持
}
Animal::~Animal(){
cout<<"Animal的析构函数"<<endl;
}
模板
C++面向对象编程:封装、继承、多态
C++泛型编程思想:模板
模板分类:函数模板、类模板
将功能相同,类型不同的函数(类)的类型抽象成虚拟的类型。当调用函数(类实例化对象)的时候,编译器自动将虚拟的类型具体化。这个就是函数模板(类模板)。
函数模板
模板关键字 template
//T只能对当前函数有效;T会自动推导数据类型
template<typename T> void swapAll(T &a, T &b){
T tmp = a;
a = b;
b = tmp;
return;
}
//也可以分行写
template<typename T>
void swapAll(T &a, T &b){}//作用范围仍然是swapAll
函数模板 会编译两次:
第一次:是对函数模板 本身编译
第二次:函数调用处 将T的类型具体化
函数模板目标:模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性。
函数模板的注意点
-
函数模板和普通函数(重名)都存在时,优先选择普通函数
-
强行调用模板函数时,可以使用<>
swapAll<>(a, b);//自动推导
swapAll<int>(a,b);//不必推导,已告知类型
- 函数模板自动类型推导时,不能对函数的参数进行自动类型转换
template<typename T> void myprint(T &a, T &b){}//函数模板
void myprint(int &a, int &b){}//普通函数
int main(void){
myprint(10,20);//普通函数
myprint('a','b');//函数模板
myprint(10,'b');//普通函数;T必须是一样的,模板函数类型推导失败(不能自动类型转换,但可以强制转换),因此会调用普通函数
myprint<int>(10,'b');//函数模板
}
函数模板的局限性
当函数模板推导出T为数组或其他自定义类型数据时,可能导致运算符不识别
解决方法一:运算符重载(推荐)
class Data{
friend ostream &operator<<(ostream &out, Data ob);
private:
int data;
public:
Data(){}
Data(int data){
this->data = data;
}
};
ostream &operator<<(ostream &out, Data ob){
out<<ob.data;
return out;
}
template<typename T> void myprint(T a){
cout<<a<<endl;
}
int main(void){
myprint(10);
Data ob(20);//如果不重写运算符,cout无法识别
myprint(ob);
return 0;
}
解决方法二:具体化函数模板
//依然先找myprint,但发现不支持Data,于是再去找有没有具体化的模板,然后找到<>printAll;
class Data{
private:
int data;
public:
Data(){}
Data(int data){
this->data = data;
}
int getData(void){
return data;
}
};
template<typename T> void myprint(T a){
cout<<a<<endl;
}
template<> void myprint<Data>(Data a){
cout<<a.getData()<<endl;
}
int main(void){
myprint(10);
Data ob(20);//如果不重写运算符,cout无法识别
myprint(ob);
return 0;
}
类模板
类模板不能自动推导类型,只能先指定
template <class T1, class T2> class Data{
private:
T1 a;
T2 b;
public:
Data(){}
Data(T1 a, T2 b){
this->a = a;
this->b = b;
}
void showData(){
cout<<a<<" "<<b<<endl;
}
};
//Data ob;//报错,推导不出类型
//Data ob(10,20);//报错,不能自动推导类型
Data<int,int> ob(10,20);
- 类模板的成员函数在类外实现
template <class T1, class T2> class Data{
private:
T1 a;
T2 b;
public:
Data(){}
Data(T1 a, T2 b);
void showData();
};
template <class T1, class T2> Data<T1,T2>::Data(T1 a, T2 b){
this->a = a;
this->b = b;
}
template <class T1, class T2> void Data<T1, T2>::showData(){
cout<<a<<" "<<b<<endl;
}
- 函数模板作为类模板的友元
template <class T1, class T2> class Data{
template<typename T3, typename T4> friend void myprint(Data<T3, T4> &ob);
private:
T1 a;
T2 b;
public:
Data(){}
Data(T1 a, T2 b);
void showData();
};
template<typename T3, typename T4> void myprint(Data<T3, T4> &ob){
cout<<ob.a<<" "<<ob.b<<endl;
}
int main(void){
Data<int, char> ob(100,'a');
myprint(ob);
return 0;
}
- 普通函数作为类模板的友元
friend void myprint(Data<int, char> &ob);
void myprint(Data<int, char> &ob){
cout<<ob.a<<" "<<ob.b<<endl;
}
- 模板头文件和源文件分离问题
模板文件需要二次编译,第一次是在编译阶段,第二次是在函数调用处。
头文件包含是在预处理阶段。当模板文件和源文件分离时,第一次编译能够引入模板文件,但第二次编译确定类型时就不会再去编译模板.cpp文件了。因此会报错undefine
因此对于模板文件,需要将template.h和template.cpp都include进来。
进一步地,可以将template.cpp中的内容复制到template.h中,并删除template.cpp,然后将template.h改为template.hpp
类模板所在文件一般用.hpp标识
数组类模板
问题:为什么第二遍输出会乱码?
推测:和string name的空间释放有关
main.cpp
#include<iostream>
#include<myarray.hpp>
#include<string>
using namespace std;
class Person{
friend ostream& operator<<(ostream &out, Person ob);
private:
int num;
string name;
float score;
public:
Person(){}
Person(int num, string name, float score){
this->num = num;
this->name = name;
this->score = score;
}
bool operator>(Person ob){
return score>ob.score;
}
};
//不识别某个类时,应该去该类中重载
ostream& operator<<(ostream &out, const Person ob){
out<<ob.num<<" "<<ob.name<<" "<<ob.score;
return out;
}
int main(void){
Person lucy = Person(100,"lucy",88.8f);
Person bob = Person(101,"bob",98.8f);
Person tom = Person(102,"tom",78.8f);
MyArray<Person> arr1;
arr1.pushback(lucy);
arr1.pushback(bob);
arr1.pushback(tom);
cout<<arr1;
cout<<arr1;//第二次报错的原因?
arr1.sortArray();
cout<<arr1;
return 0;
}
myarray.hpp
#ifndef MYARRAY_HPP
#define MYARRAY_HPP
#include <iostream>
#include <cstring>
using namespace std;
template<class T>
class MyArray{
template<typename T1> friend ostream& operator<<(ostream &out, MyArray<T1> ob);
private:
T *arr;
int size;
int capacity;
public:
MyArray();
MyArray(int capacity);
MyArray(const MyArray &ob);
~MyArray();
MyArray& operator=(MyArray &ob);
void pushback(T elem);
void sortArray();
};
#endif // MYARRAY_HPP
template<class T>
MyArray<T>::MyArray(){
this->size = 0;
this->capacity = 5;
this->arr = new T[capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::MyArray(int capacity){
this->capacity = capacity;
this->size = 0;
this->arr = new T[this->capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::MyArray(const MyArray &ob){
this->capacity = ob.capacity;
this->size = ob.size;
this->arr = new T[this->capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
memcpy(this->arr, ob.arr, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::~MyArray(){
delete[] arr;
}
//运算符重载,实现深拷贝
template<class T>
MyArray<T>& MyArray<T>::operator=(MyArray &ob){
//判断arr是否存在旧空间
if(this->arr != NULL){
delete[] this->arr;
this->arr = NULL;
}
this->capacity = ob.capacity;
this->size = ob.size;
this->arr = new T[this->capacity];
memset(this->arr, 0, sizeof(T)*this->capacity);
memcpy(this->arr, ob.arr, sizeof(T)*this->capacity);
//为了完成链式操作,即ob1=ob2=ob3,需要返回
return *this;
}
template<class T1>
ostream& operator<<(ostream &out, MyArray<T1> ob){
for(int i=0; i<ob.size; ++i) out<<ob.arr[i]<<" ";
out<<endl;
return out;
}
template<class T>
void MyArray<T>::pushback(T elem){
if(size == capacity){
capacity *= 2;
T *tmp = new T[capacity];
if(arr != NULL){
memcpy(tmp, arr, sizeof(T)*size);
delete[] arr;
}
arr = tmp;
}
arr[size] = elem;
size++;
}
template<class T>
void MyArray<T>::sortArray(){
if(size == 0){
cout<<"容器为空"<<endl;
return;
}
int i = 0, j = 0;
for(i=0; i<size-1; ++i){
cout<<"this is in before"<<endl;
cout<<arr;
for(j=0; j<size-i-1; ++j){
if(arr[j] > arr[j+1]){
T tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
cout<<"this is in after"<<endl;
}
cout<<"this is the end of sort"<<endl;
}
类模板的继承
子类继承类模板,那么子类也是类模板
template<class T1, class T2>
class Base{
private:
T1 a;
T2 b;
public:
Data(){}
Data(T1 a, T2 b);
void showData();
};
template<class T1, class T2, class T3>
class Son1:public Base<T1,T2>{
public:
T3 c;
public:
Son1(T1 a, T2 b, T3 c):Base<T1,T2>(a,b){
this->c = c;
}
};
类型转换
上行、下行转换
子类空间 > 父类空间 可以把大的转给小的
上行:安全。子类转为父类,父类指针保存子类空间,只访问子类的一部分,虽然有一部分不会再访问到,但不会越界
下行:不安全。父类转为子类,子类指针保存父类空间,父类只分配了自己空间,子类访问会造成越界
static_cast静态类型转换
- 基本类型
int num = static_cast<int>(3.14);//ok
- 上行转换
Base* p = static_cast<Base*>(new Son);//ok
- 下行转换
Son* p = static_cast<Son*>(new Base);//通过编译,使用出问题;
- 不相关类型转换
Son* p = static_cast<Son*>(new Other);//报错
dynamic_cast静态类型转换(推荐)
- 基本类型
int num = dynamic_cast<int>(3.14);//报错
- 上行转换
Base* p = dynamic_cast<Base*>(new Son);//ok
- 下行转换
Son* p = dynamic_cast<Son*>(new Base);//报错
- 不相关类型转换
Son* p = dynamic_cast<Son*>(new Other);//报错
const_cast常量转换
- 将const 转换为非const
const int *p1;
int *p2 = const_cast<int *>(p1);//必须加()
const int &ob = 10;
int &ob1 = const_cast<int &>(ob);//必须加()
- 将非const转换成const
int *p3;
const *p4 = const_cast<int *>(p3);//不用const_cast也能转换成功
int data = 10;
const int &ob2 = const_cast<const int &>(data);//不用const_cast也能转换成功
重新解释转换(reinterpret_cast)(最不安全)
- 基本类型,不支持
int num = reinterpret_cast<int>(3.14f);//报错
- 基本类型指针,支持
float *p;
int *q = reinterpret_cast<int *>(p);//ok
- 上行转换,支持
Base* p = reinterpret_cast<Base*>(new Son);//ok
- 下行转换,支持
Son* p = reinterpret_cast<Son*>(new Base);//通过编译
- 不相关类型转换:支持
Son* p = reinterpret_cast<Son*>(new Other);//通过编译
异常
C语言通过返回值提示异常
!!!异常是一个类
异常的抛出和捕获
try{
throw 异常值;
}catch(异常类型1 异常值1){
处理异常的代码1;
}catch(异常类型2 异常值2){
处理异常的代码2;
}catch(...){//任何异常都捕获
处理异常的代码3;
}
案例
try{
//throw 1;
throw 2.14f;//如果抛出异常而没有捕获,程序结束
}catch(int e){
cout<<"int异常值为:"<<e<<endl;
}catch(char e){
cout<<"char异常值为:"<<e<<endl;
}catch(...){//捕获所有异常
cout<<"其他异常值为:"<<endl;
}
!!如果抛出异常而没有捕获,程序结束。即上述代码如果没有catch(…)会报错
栈解旋
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋
try{
Data ob1;
Data ob2;
Data ob3;
throw 1;//这里抛出异常后,ob3和ob2和ob1依次自动释放,即栈解旋
}
栈解旋之后再捕获和处理
异常的接口声明
描述可以抛出哪些类型的异常
1. 默认可以抛出任意异常(推荐)
2. 只能抛出特定类型的异常
void func() throw(int, char){
throw "hello";//抛出,不能被捕获,于是系统接管,终止程序
}
3. 不能抛出任何异常
void func() throw(){
throw 1;
}
异常变量的声明周期
定义异常类
class MyException{
public:
MyException(){
cout<<"异常变量构造"<<endl;
}
MyException(const MyException &e){
cout<<"拷贝构造"<<endl;
}
~MyException(){
cout<<"异常变量析构"<<endl;
}
};
1. 普通变量接异常
void test01(){
try{
throw MyException();//抛出一个对象
}catch(MyException e){//普通对象e接异常,拷贝构造
cout<<"普通对象接异常"<<endl;
}
}
依次发生:异常变量构造 - 拷贝构造 - 异常变量析构 - 异常变量析构
2. 对象指针接异常
void test02(){
try{
throw new MyException;//抛出一个对象,这个对象在堆上
}catch(MyException *e){//指针e接异常,需要delete释放
cout<<"指针接异常"<<endl;
delete e;
}
}
依次发生:异常变量构造 - 异常变量析构
3. 引用接异常(推荐)
void test03(){
try{
throw MyException();//抛出一个对象
}catch(MyException &e){//引用e接异常,不需要delete释放
cout<<"引用接异常"<<endl;
}
}
依次发生:异常变量构造 - 异常变量析构
异常的多态
用父类异常去接所有的子类异常,类似于虚函数原理
//异常基类
class BaseException{
public:
virtual void printError(){};
};
//空指针异常
class NullPointerException:public BaseException{
public:
virtual void printError(){
cout<<"NullPointerException"<<endl;
}
};
//越界异常
class OutOfRangeException:public BaseException{
public:
virtual void printError(){
cout<<"OutOfRangeException"<<endl;
}
};
void dowork(){
//throw OutOfRangeException();
throw NullPointerException();
}
int main(void){
try{
dowork();
}catch(BaseException& e){
e.printError();
}
return 0;
}
C++标准异常
使用方法
try{
throw out_of_range("越界异常!");
}catch(exception &e){
cout<<e.what()<<endl;
}
编写自己的异常
需要基于标准异常基类,编写自己的异常
#include<exception>
//必须继承exception类
class NewException:public exception{
private:
string msg;
public:
NewException();
NewException(string msg){
this->msg = msg;
}
//重写父类的what
virtual const char *what() const throw(){//防止父类在子类前抛出标准异常
return this->msg.c_str();
}
~NewException(){}
};
int main(void){
try{
throw NewException("自定义异常!");//如果不加const throw,则输出std::exception
}catch(exception &e){
cout<<e.what()<<endl;
}
return 0;
}