C++是一门面向对象的语言,而C++面向对象有三大特性:封装,继承,多态。这篇近俩万字的博客将带你走入C++的对象篇。
C++认为万事万物皆为对象,对象有其属性和行为。
具有相同性质的对象,我们可以把它抽象为类。人属于人类,车属于车类。
文章目录
- 封装
- 继承
- 多态
(一)封装
1.意义:(1)将属性和行为作为一个整体,表现生活中的事物。
(2)将属性和行为加以权限控制
2.意义解剖:
(1)设计类的时候,将属性和行为作为一个整体
语法:class circle{};//class代表一个类,类后面跟着类名称
//求圆的周长
#include<iostream>
using namespace std;
const double PI=3.14;
class circle{
//访问权限
//公共权限
public:
//属性:
//半径:
int m_r;
//行为:
//获取圆的周长
double calculate()
{
return 2*pi*m_r;
}
};
int main()
{
//实地化:通过一个类来创建一个对象的过程
//通过圆类来创建具体的圆
Circle c1;
//给圆对象进行赋值操作
c1.m_r=10;
//访问行为
cout<<c1.calculate()<<endl;
}
实例练习:
//设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
class stu{
public:
string pname;
int id;
void show()
{
cout<<pname<<id<<endl;
}
//给姓名赋值
void setname(string name)
{
pname=name;
}
};
int main()
{
//创建一个具体的学生 实例化对象
stu s1;
//给s1进行赋值
s1.pname="张三”;
//s1.setname("张三”);
//打印
s1.show();
stu s2;
s2.pname="李四”;
s2.id=2;
s2.show();
}
类中的属性和行为称为成员。属性叫做成员属性和成员变量,行为叫做成员函数和成员方法、
(2)类在设计的时候,我们可以把属性放在不同的权限下,加以控制。
访问权限有三种:
public公共权限 成员在类内可以访问类外也可以访问
protected保护权限 成员类内可以访问 类外不可以访问 儿子也可以访问父亲中保护的内容
保护权限和私有权限的内容,在类外访问不到。
private私有权限 成员类内可以访问 类外不可以访问 儿子不可以访问父亲的私有内容
3.struct和class的区别:默认的访问的访问权限不同
struct:公有 class:私有
class c1
{
int a=10;//私有
};
struct c2
{
int b=10;//公共
};
4.成员属性设置为私有化
1)将所有成员属性设置为私有,可以自己控制读写权限。
2)对于写权限,我们可以检测数据的有效性。
class person{
public:
void setname(string name)
{
pname=name;
}
string getname()
{
return pname;
}//可读可写
int getage()
{
return page;
}//只读
//设置年龄
void setage(int age)
{
if(age<0||age>150)
{
cout<<"你是老妖精”<<endl;
return;
}
page=age;
}
void lover(string lover)
{
lo=lover;
}//只写
private:
//姓名 可读可写
string pname;
//年龄 只读
int page;
string lo;
};
int main()
{
person p;
p.setname("张三");
p.age=18;
}
实例练习1:
1)求出立方体的面积和体积,用全局函数和成员函数判断俩个立方体是否相等
成员函数只用传入一个参数,全局函数传入俩个参数
/*立方体设计:
创建立方体类
设计属性
设计行为:获取立方体的面积和体积
利用成员函数,判断是否相等*/
class cube{
public:
//设置长
void setl(int l)
{
m_l=l;
}
//获取长
int getl()
{
return m_l;
}
//设置宽
void setl(int w)
{
m_w=w;
}
//获取宽
int getw()
{
return m_w;
}
//设置高
void seth(int h)
{
m_l=h;
}
//获取高
int geth()
{
return m_h;
}
//获取立方体面积
int ca()
{
return 2*(l*h+l*w+w*h);
}
int v()
{
return l*h*w;
}
bool issame(cube &c)
{
if(m_l==c.getL()&&m_h==c.geth()&&m_w==c.getw()
{
return true;
}
else return false;
}
private:
int m_l;
int m_w;
int m_h;
}
int main()
{
cube c1;
c1.setl(10);
c1.setl(20);
c1.setl(30);
cube c2;
c2.setl(10);
c2.setl(20);
c2.setl(30);
bool ret=IsSame(c1,c2);
if(ret)
{
cout<<"全等”<<endl;
}
else{
cout<<"不全等”<<endl;
}
}
bool IsSame(cube &c1,cube &c2)
{
if(c1.l==c2.l&&c1.h==c2.h&&c1.w==c2.w)
{
return ture;
}
else return false;
}
实例练习2:表示点和圆的位置关系。
class point{
public:
void setx(int x)
{
m_x=x;
}//获取
int getx()
{
return m_X;
}//设置
void sety(int y)
{
m_y=y;
}//获取
int gety()
{
return m_y;
}//设置
private:
int m_x;
int m_y;
};
class Circle
{
public:
void setr(int r)
{
m_R=r;
}
int getr()
{
return m_R;
}
void setc(int c)
{
m_Center=c;
}
int getr()
{
return m_Center;
}
private:
int m_R;
point m_Center;
};
//判断点和园关系的函数
void isincircle(Circle &c,Point &p)
{
int dis=(c.getc().getX()-p.getx())*c.getc().getX()-p.getx()-c.getc().getY()-
p.getY()*c.getc().getX()-p.getx();
int rdis=c.getR()*c.getR();
if(dis==rdis) cout<<"点在圆上"<<endl;
if(dis>rdis) cout<<"点在园外"<<endl;
if(dis<rdis) cout<<"点在园内"<<endl;
}
3.对象的初始化和清除
1)构造函数和析构函数
这个是由编译器自动调用的,完成对象的初始化和清理工作。对象的初始化和清理工作是编译器强制我们需要做的事情,因此如果不提供构造和析构,编译器会提供。编译器提供的构造函数和析构函数是空实现的。
2)构造函数语法:类名(){}
没有返回值也不写void
函数名和类名相同
构造函数可以有参数,因此可以发生重载
程序在调用对象的时候会自动调用构造,无须手动调用,只会调用一次。
3)析构函数:~类名(){}
没有返回值也不写void
函数名和类名相同,前面加~
构造函数不可以有参数,因此不可以发生重载
程序在调用对象的时候会自动调用构造,无须手动调用,只会调用一次。
构造和析构都是必须有的操作,自己不弄,系统自己也会搞
//构造函数初始化
person()
{
cout<<"爱"<<endl;
}
//析构函数清除
~person()
{
cout<<"ni"<<endl;
}
4)构造函数的分类和调用
俩种分类方式:按参数分为(有参构造和无参构造(默认构造)
按类型分为(普通构造和拷贝构造)
三种调用方式:括号法 显示法 隐式转换法
//拷贝构造函数
person(const person &p)
{
//将传入的人的属性都传到自己身上来
age=p.a;
}
//调用方式
{
//括号法
person p1;//调用无参
person p2(10);//调用有参
person p3(p1);//调用构造拷贝函数
//注意用默认参数构造的时候不要加入()因为编译器会认为是一个函数声明,不会认为创建对象
//显示法
person p1;
person p2=person(10);//有参构造
person p3=person(p2);//拷贝构造
person(10);//匿名对象 当前行执行完后,系统会立刻回收
person(p3);//不要用拷贝构造函数,初始化匿名对象。编译器会认为是一个对象声明 person p3
//隐式转换法
person p4=10;//相当于写了person p4=person(10);
person p5=p4;
}
5)拷贝构造函数的调用时机
以值方式返回局部对象
class person
{
public:
person()
{
cout<<"person的默认函数构造"<<endl;
}
person(int age)
{
cout<<"person的有参函数构造"<<endl;
m_age=age;
}
~person()
{
cout<<"person的析构函数调用”<<endl;
}
person(const person &p)
{
cout<<“person的拷贝构造函数"<<endl;
}
int m_age;
};
使用一个已经创建完毕的对象来初始化一个对象
void test01()
{
person p1(20);
person p2(p1);
cout<<p2.m.age<<endl;
}
值传递的方式给函数参数传值
void doWork(person p)
{
}
void test02()
{
person p;
dowork(p);
}
值方式返回局部对象
person dowork()
{
person p1;
return p1;
}
void test03()
{
person p=dowork();
}
6)构造函数的调用规则
默认情况下,编译器会给一个类添加三个函数
1)默认构造函数:无参,函数体为空
2)默认析构函数:无参,函数体为空
3)默认拷贝构造函数:对属性进行值拷贝
person(int age)
{
m_age=age;
}
void test()
{
person p(10);
person p2(p);//编译器会临时拷贝,会给p2赋值为18也就是m_age=p.m_age;
}
规则:
1)如果用户定义有参构造函数,c++不再提供默认无参构造,但会提供拷贝构造
2)如果用户定义拷贝构造函数,c++不再提供其他构造函数
4.深拷贝和浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区申请空间,进行拷贝操作
class{
public:
int *m_height;
person(int height)
{
m_height=new int(height);
}
~person()//析构函数:将堆区开辟的内存释放干净
{
if(m_height!=NULL)
{
delete m_height;
m_height=NULL;
}
}
};
void test01()
{
person p1(160);
person p2(p1);
cout<<*p2.m_height<<endl;
}
在堆区开辟一块内存空间后,会将地址原封不动的移动到p2.当执行析构函数的时候,根据先进后出的原则,p2先被释放,p1后被释放,是一个非法操作。 浅拷贝的问题就是堆区的内存重复释放。
解决方法:采用深拷贝构造
//自己实现一个拷贝构造函数
person(const person&p)
{
cout<<"person的拷贝函数调用"<<endl;
// m_height=p.m_height;编译器默认实现
m_height =new int(*p.m_height);//深拷贝在堆区重新创建一块内存
}
总结:如果有属性在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
5,初始化列表
作用:初始化属性。
语法:构造函数(): 属性1(值1),属性2(值2)... {}
class person{
public:
/*传统初始化操作
Person(int a,int b,intc)
{ m_A=a;
m_B=b;
m_C=c;
}*/
//初始化列表来初始化属性
person():m_A(10),m_B(20),m_C(30)
{
}
//灵活传入
person(int a,int b,int c):m_A(a),m_B(b),m_C(c)
{
}
int m_A;
int m_B;
int m_C;
};
void test()
{
person p(30,20,10);
}
6 类对象作为类的成员
C++类中的成员可以是另一个类的对象,我们称为对象成员
class A{}
class B{
A a;
}//B类中有对象A作为成员,A为对象成员
当创建B对象的时候,A与B的构造和析构顺序是什么?
//手机类
class Phone
{
public:
Phone(string pName)
{
m_PName=pName
}
string m_pname;
};
//人类
class Person{
public:
Person(string name,string phone):m_Name(name),m_Phone(PName)//相当于写Phone m_Phone=PName
{
}
string m_Name;
phone m_Phone;
};
当其他类的对象作为本类的成员的时候, 构造的时候先构造类对象,然后再去构造自身。
析构的顺序与构造相反。
7 静态成员:在成员变量和成员函数前面加上static
静态成员变量:所有对象共享一份数据 在编译阶段分配内存 类内声明,类外初始化
静态成员函数:所有对象共享同一个函数 静态成员函数只能访问静态成员变量,不可以访问非静态成员变量,因为没有办法区分是哪个对象的非静态成员变量。而m_A是共享的,只有一份。
class Person
{
public:
static void func()
{
cout<<"static void func()的调用"<<endl;
}
static int m_A;
private://私有作用域下静态函数也是有访问权限的,类外访问不到
static void func2()
{
}
};
int Person::m_A=0;
//有俩种访问方式
void test01()
{
//1.通过对象调用
Person p;
p.func();
//2.通过类名访问
Person:func();
}
8 C++对象模型和this指针
1)成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
class Person()
{
};
class pp
{
int m_A;
};
class ppp
{
int m_A;
static int m_B;
void func(){}//非静态成员函数
};
int person::m_B=0;
void test01()
{
person p;
cout<<sizeof(p)<<endl;//空对象占用的内存空间为1
//C++编译器会给每个空对象也分配一个字节的空间,是为了区分对象占内存的位置
//每个空对象也应该有一个独一无二的内存地址
pp p2;
cout<<sizeoof(p2)<<endl;//非静态成员变量位于类的对象上,则求得4
ppp p3;
cout<<sizeof(p3)<<endl;//求得还是4 静态成员变量和非静态成员函数不属于类的对象上
}
2)this指针概念
每一个非静态成员函数只会诞生一份函数实例, 也就是多个同类型的对象共用一块代码。
问题是:这一块代码如何区分那个对象如何调用自己?
this 指针指向被调用的成员函数所属的对象。
this指针隐含在每一个非静态成员函数内的一种指针。不需要定义,直接引用就可以。
用途:当形参和成员变量重名的时候,可以用this来区分。
在类的非静态成员函数中返回对象本身,可以使用return*this(链式编程)
如果传的是person的值,每次返回的都是一个新的对象。引用的方式返回回到p2.
class person{
public:
person(int age)
{
age=age;
}
person& personadd(person &p)
{
this->age+=p.age;//this指p2
return *this//*this指向p2这个对象的本体
}
int age;//写成员变量int m_age
};
void test01()
{
person p1(18);
}
void test02()
{
person p1(10);
person p2(10);
p2.personadd(p1);//20
}
//解决名称冲突
class person{
public:
person(int age)
{
this->age=age;//this指向p1
}
int age;
};
//返回对象本身用*this
3) 空指针访问成员函数
如果成员体的内部用到this指针,需要判断代码的健壮性。
this指针是一个指针常量,指针指向的对象不可以修改,但是指向的值可以修改。
class person
{
public:
void showclassname()
{
cout<<"this is Person class"<<endl;
}
void showpersonalage()
{
//可以加入下面的句子
if(this==NULL)
return;
cout<<"age"<<m_age<<endl;
}
int m_age;
};
void test01()
{
person *p=NULL;
p->showclassname();//没有错误
p-> void showpersonalage();//err 因为m_age的前面默认加了一个this->,但是空指针,没有确切的实体
}
4)const修饰成员函数——>常函数
常函数内不可以修改成员属性。成员属性的声明时加入关键字mutable之后,在常函数之中仍然可以修改。
在成员函数的后面加上一个const修饰的是this的指向,让指针指向的值也不可以修改。
class person
{
public:
void showPerson()
{
m_A=100;//可以修改 实际上this->m_A
//this相当于一个person*const this则this指针不允许修改他的指向
}
void showPerson() const
{
m_A=100;//不可以修改 相当于const person*const this
this->m_B=100;
}
int m_A;
mutable int m_B;//特殊变量加入mutable。即使在常函数中,它的属性还是可以被修改的
};
常对象:声明对象前面加入const称该对象为常对象
常对象只能调用常函数,不可以调用普通成员函数,因为普通成员函数可以修改属性。
void test02()
{
const person p;
p.m_A=100;//err
p.m_B=100;//可以修改
//常对象只能够调用常函数
p.showPerson();
}
8,友元
某些私有属性让类外的一些特殊函数或者类进行访问。
目的是为了让一个函数或者类访问另一个类中的私有成员
关键字是friend
三种实现:
1)全局函数做友元
class Build
{
friend void goodguy(Build*build);
//goodGay全局函数是Build的好朋友,可以访问Build中的私有成员
public:
bulid()
{
m_sittingroom="客厅";
m_bedroom="卧室";
}
public:
string m_sittingroom;
private:
string m_bedroom;
};
//全局函数
void goodguy(Build*build)
{
cout<<build.m_sittingroom<<endl;
cout<<build.m_bedroom<<endl;
}
void test01()
{
Build build;
goodguy(&build);
}
2)类做友元
#include<iostream>
using namespace std;
//类作为友元
class Build
{
friend clsss GoodGuy;//GoodGuy是本类中的好朋友,可以访问本类中的私有成员
public:
string m_settingroom;
private:
string m_bedroom;
};
//类外写成员函数
Build::Build(){
m_settingroom="客厅”;
m_bedroom="卧室";
}
clsss GoodGuy
{
public:
void visit();//参观函数,访问Build中的属性
Build*build
};
GoodGuy::GoodGuy()
{
//创建建筑物的对象
build=new Build;
}
void GoodGuy::visit()
{
cout<<building->m_Setting<<endl;
}
void test01()
{
GoodGuy gg;
gg.visit();
}
int main()
{
test01();
system("pause");
return 0;
}
3)成员对象做友元
class building:
{ //告诉编译器,GoodGuy类下的成员函数作为本类的好朋友,可以访问私有成员
friend void GoodGuy::visit()
public:
building();
string m_sittingroom;
private:
string m_bedroom;
};
//类外实现成员函数
building::building()
{
m_sittingroom="客厅";
m_bedroom="卧室”;
}
void GoodGuy::visit()
{
cout<<building.m_sittingroom<<endl;
}
void GoodGuy::visit2()
{
cout<<building.m_sittingroom<<endl;
}
class GoodGay
{
public:
GoodGuy():
void visit();//让visit可以访问building中的私有成员
void visit1();//让visit1不可以访问building中的私有成员
building*build;
};
9.运算符重载
对运算符重新定义,赋予其另外一种功能,以适应不同的数据类型。
1)加号运算符重载
作用:实现俩个自定义数据类型相加的运算
class person{
public:
int m_A;
int m_B;
}
person p1;
p1.m_A=10;
p1.m_B=10;
person p2;
p2.m_A=10;
p2.m_B=10;
person p3=p1.operator+(p2);//简化为person p3=p1+p2;
//通过自己写一个成员函数实现俩个对象相加属性后返回新的对象
person operator+(person &p)
{
person t;
t.m_A=this->m_A+p.m_A;
t.m_B=this->m_B+p.m_B;
return t;
}
//通过全局函数重载+
person operator+(person &p1,person &p2)
{
person t;
t.m_A=this->m_A+p2.m_A;
t.m_B=this->m_B+p2.m_B;
return t;
}
运算符重载也可以实现函数重载
person operator+(person &p1,int num)
{
person t;
t.m_A=this->m_A+num;
t.m_B=this->m_B+num;
return t;
}
person p3=p1+10;
对于内置的数据类型表达式的运算符是不可以改变的
不要滥用运算符重载
2)左移运算符重载
不会利用成员函数来重载左移运算符,因为无法实现cout在左侧,只能利用全局函数
ostream& operator<<(ostream &cout,p)//本质operator<<(cout,p)简化cout<<p ostream标准输出流对象
{
cout<<p.m_A<<p.m_B<<endl;
return cout;//链式编程思想可以无限追加
}
void test01()
{
person p;
p.m_A=10;
p.m_B=10;
cout<<p;
}
左移运算符配合友元可以实现输出自定义数据类型
3)递增运算符的重载
class s{
public:
integer()
{
m_num=0;
}
//前置递增,先进行++的运算,再自身返回
//返回引用是为了一直对同一个数据做递增
integer& operator++()
{
m_num++;
return *this;
}
//重载后置++递增
int operator+(int)//用int占位参数区分前置和后置递增
{
//先记录当时结果
integer t=*this;
//后递增
m_Num++;
//最后把记录结果作返回
return t;
}
}
4)赋值运算符重载
C++编译器为一个类至少添加的第四个函数是operator=,对属性进行值拷贝。
如果有属性指向堆区,做赋值操作可能也会引起深浅拷贝。
class person{
public:
person(int age)
{
m_A=new int(age);
}
~person()
{
if(m_age!=NULL)
{
delete m_age;
a_age=NULL;
}
}//堆区的内存重复释放,程序崩溃。使用深拷贝
person& operator=(person &p)
{
//编译器提供浅拷贝
//先判断是否有属性再堆区,有的话先释放干净
if(m_age!=NULL);
{
delete m_age;
m_age=NULL;
}
//深拷贝
m_age=new int(*p.m_age);
return *thisl
}
}
int *m_age;
};
void test01()
{
person p1(18);
cout<<p1.m_age<<endl;
person p2(20);
p2=p1;
}
5)关系运算符的重载
bool operator==(person &p)
{
if(this->name==p.name)
return true;
return false;
}
6)函数调用运算符()重载
由于重载后使用的方式非常像函数的调用,称为仿函数
仿函数没有固定的写法,非常灵活。
void operator()(string test)
{
cout<<test<<endl;
}
void test01
{
myprintf("hello world");
}
//匿名函数对象
cout<<myadd()(100,100)<<endl;
//myadd创建一个匿名对象,创建了完全被释放;重载了(),匿名函数对象
(二)继承
定义类的时候,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候用继承,减少重复的代码。
class basepage
{
public:
void header()
{
cout<<"首页.公开课"<<endl;
}
void footer()
{
cout<<"帮助中心,交流合作"<<endl;
}
void left()
{
cout<<"JAVA,python"<<endl;
}
};
class java:public basepage
{
public:
void content()
{cout<<"JAVA学科视频<<endl;}
};
class python:public basepage
{
public:
void content()
{cout<<"python学科视频<<endl;}
};
语法: class子类 :继承方式父类{}
子类:派生类 父类:基类
派生类中的成员,包含俩部分:从基类继承过来,自己增加成员。
从基类继承而来的有共性,自己增加的成员表现个性。
继承方式:公共继承 保护继承 私有继承
class base{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class son1:public base
{
public:
void test01()
{
m_A=10;//父类中的公共权限依然是公共权限
m_B=10;//父类中的保护权限依然是保护权限
m_C=10;//父类中的私有权限 子类访问不到
}
};//公共继承
class son2:protected base
{
public:
void test02()
{
m_A=10;//父类中的公共权限变为保护权限 类外访问不到
m_B=10;//父类中的保护权限子类中仍然保护 类外访问不到
m_C=10;//父类中的私有权限 子类访问不到
}
};//保护权限
class son3:private base
{
public:
void test02()
{
m_A=10;//父类中的公共权限变为子类私有 类外访问不到
m_B=10;//父类中的保护权限变为子类私有 类外访问不到
m_C=10;//父类中的私有权限 子类访问不到 类外访问不到
}
};//保护权限
2)继承中的对象模型
class base{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class son:public base
{
int m_D;
}
void test()
{
cout<<sizeof(son)<<endl;//结果16,父类所有非静态成员属性都会在子类继承下去,子类的也会保留一份。父类中的私有成员属性 被编译器隐藏 因此访问不到 但确实是被继承了
}
3)子类中的析构和构造顺序
子类继承父类之后,当创建子类对象的时候,也会调用父类构建函数。
构造:先有父类 再有子类 析构顺序与构造相反
4)继承同名成员处理方式
问题:当子类和父类出现同名的成员,如何通过子类对象,访问到子类或者父类中的同名数据?
访问子类同名的成员,直接访问即可
访问父类同名的成员,需要加作用域
1.同名成员属性
class base
{
public:
base()
{
m_A=100;
}
int m_A;
};
class son:public base
{
public:
son()
{
m_A=200;
}
int m_A;
};
void test01()
{
son s;
cout<<s<<endl;//200 访问的是自身的数据
cout<<s<<s.base::m_A<<endl;//加一个作用域可以通过子类对象 访问到父类的同名成员
}
2.同名成员函数
void test02()
{
son s;
s.func();//直接调用 子类中的同名成员
s.base::func();//调用父类 加入作用域
s.base::func(100);//占位也一样 如果一个子类出现和父类相同的名字,子类就会隐藏掉父类所有的函数。
//如果要访问到被子类隐藏的父类,需要加入作用域
}
3.继承同名静态成员的处理方式
静态成员和非静态成员出现同名,处理方式一样
访问子类直接访问
访问父类加入作用域
class base:
{
public:
static int m_A;
};
int base::m_A=100;
class son:public base
{
public:
static int m_A;
};
int base::m_A=200;
//同名静态成员属性
void test01{
//通过对象访问
son s;
cout<<son.m_A<<endl;
cout<<son,base::m_A<<endl;
//通过类名访问
cout<<son::m_A<<endl;
cout<<son::base::m_A<<endl;//第一个::代表通过类名的方式访问,第二个表示访问父类作用域下的m_A
}
4.多继承问题:允许一个类继承多个类
class 子类:继承方式1 父类1,继承方式2 父类2
多继承问题也可能出现同名成员,需要使用作用域访问
class base1{
public:
m_A=100;
int m_A;
};
class base2{
public:
m_B=200;
int m_B;
};
//子类继承
class son:public base1,public base2
{
public:
son()
{
m_C=300;
m_D=400;
}
int m_C;
int m_D;
};
void test()
{
son s;
cout<<sizeof(s)<<endl;//16
cout<<s.base1::m_A<<endl;
cout<<s.base2::m_A<<endl;
}
5菱形继承(钻石继承):
俩个派生类继承同一个基类
又有某个类同时继承俩个派生类
class animal(){};
class sheep:public animal{};
class tuo:public animal{};
class sheeptuo:public sheep,public tuo{};
利用虚继承解决菱形继承中的问题 继承之前加上virtual animal称为虚基类,使得这份数据只有一份,即使s.m_A也不会出现不知道是谁的。
vbptr 虚继承指针会指向一个虚积列表
(三)多态
1.分类:静态多态(函数重载和运算符重载,常用函数名)
动态多态(派生类和虚函数实现运行时多态)
区别:静态多态的函数地址是早绑定的:编译阶段就能确定函数地址
动态多态的函数地址迟绑定的:运行阶段确定函数地址
class Animal{
public:
//虚函数
virtual void speak()
{
cout<<"动物说话"<<endl;
}
};
class cat:public animal
{
public:
void speak()
{
cout<<"猫猫在说话"<<endl;
}
};
//地址早绑定,在编译阶段就确定了函数地址
//如果要执行让猫说话,这个函数地址就不能早绑定,需要在运行阶段绑定,也就是地址晚绑定
void dospeak(Animal &animal)//Animal &animal=cat;父类的引用直接指向子类对象
{
animal.speak;
}
void test01()
{
cat cat;
dospeak(cat);
}
2.动态多态满足条件:有继承关系+子类要重写父类中的虚函数
重写:函数返回值类型 函数名 参数列表完全相同
使用动态多态:父亲的指针或者引用 指向子类对象
class Animal{
public:
//虚函数
virtual void speak()
{
cout<<"动物说话"<<endl;
}
};//大小为4
class Animal{
public:
void speak()
{
cout<<"动物说话"<<endl;
}
};//大小为1 空类 函数变量分开存储
animal的内部结构:vfptr 虚函数记录表记录虚函数的地址
子类会继承父类的虚函数,但子类中的虚函数表会替换成子类的虚函数地址,当父类的指针或者引用指向子类对象的时候,发生多态。Animal animal=cat; //从猫的虚函数地址中去找猫说话 animal speak();
3.计算机类:
在真实的开发中,提倡开闭原则,对拓展进行开发,对修改进行关闭。
/*多态的优点:
代码组织结构清晰
可读性强
利用前期和后期的拓展以及维护*/
//实现计算机的抽象类
class Abstract
{
public:
virtual int gerResult()
{
return 0;
}
int num_1;
int num_2;
};
class Addcal:public Abstract
{
public:
int getResult()
return m_Num1+m_Num2;
};
class subcal:public Abstract
{
public:
int getResult()
return m_Num1-m_Num2;
};
class mulcal:public Abstract
{
public:
int getResult()
return m_Num1*m_Num2;
};
void test02()
{
//多态使用
//父类指针或者引用指向子类对象
//加法运算:
Abstract*abc=new addcal;
abc->m_Num1=10;
abc->m_Num2=10;
cout<<abc->m_Num1<<"+"<<abc_num2<<"="<<endl;//用完之后记得销毁
delete abc;
}
4.纯虚函数和抽象类
纯虚函数:virtual 返回值类型 函数名 (参数列表)=0;
当类中有纯虚函数的时候,这个类叫做抽象类。
1)无法实例化对象
2)子类必须重写抽象类中的纯虚函数,否则子类也属于抽象类
class base
{
public:
//纯虚函数
virtual void func()=0;
};
void test01()
{
base b;//抽象类无法实例化对象
new base;//err
son s;
};
class son:public base
{
public:
virtual void func(){};//子类必须重写父类中的纯虚函数,否则会报错
};
5,制作饮品
煮水->冲泡->倒入杯中->加入辅料
利用多态技术实现此案例,提供抽象制作饮品的基类,提供子类制作咖啡和茶叶。
class drinking
{
public:
//煮水
virtual void Boil()=0;
//冲泡
virtual void Brew()=0;
//倒入杯中
virtual void pourinvup()=0;
//加入足够佐料
virtual void putsome()=0;
//制作饮品
void makrdrink()
{
Boil();
Brew();
pourinvup();
putsome();
}
};
class coffee:public drinking
{
public:
//煮水
virtual void Boil();
{
cout<<"煮恒河水"<<endl;
}
//冲泡
virtual void Brew();
{
cout<<"冲泡咖啡"<<endl;
}
//倒入杯中
virtual void pourinvup();
//加入足够佐料
virtual void putsome();
};
6.虚析构和纯虚析构
多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放的时候无法调用子类中的析构代码。堆区的数据会造成内存泄漏。
解决方式:将父类中的析构函数改成纯虚构或者纯虚析构。
纯虚构或者纯虚析构的共性:
1)可以解决父亲指针释放子类对象
2)都需要有具体的函数实现
纯虚构或者纯虚析构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
class animal
{
public:
animal()
{
cout<<"animal函数的调用"<<endl;
}
//纯虚函数:
virtual void speak()=0;
virtual ~animal()
{
cout<<"animal析构函数调用"<<endl;
}//虚析构可以解决 父类指针释放子类对象时不干净的情况
//纯虚析构也能解决这个问题,但是必须要有代码具体实现
virtual ~animal()=0;
};
Animal::~Animal()
{
cout<<"animal纯虚析构函数的调用"<<endl;
}
class cat:public animal
{
public:
Cat(string name)
{
m_Name=new string(name);
}
virtual void test()
{
cout<<"小猫在说话"<<endl;
}
~Cat()
{
if(m_Name!=NULL)
{
cout<<"Cat析构函数的调用”<<endl;
delete m_Name;
m_Name=NULL;
}
}
string *m_Name;
};
void test01()
{
Animal*animal=new cat("Tom");
animal->speak();
//父类指针在析构的时候,不会调用子类中的析构函数,导致子类如果有堆区属性,会出现内存泄露问题
delete animal;
}
有了纯虚析构之后,这个类属于抽象类,无法实例化对象。