C++ 面向对象编程3 封装 继承 多态
封装
该隐藏的数据私有化(private),该公开的公有化(public),目的是为了分工合作,有助于数据的安全性和使用的方便性,防止不必要的扩展。
继承(inheritance)
1.作用
因为子类继承了父类的成员,能够实现代码和数据的复用,能在已有的代码和数据的基础上进行扩展。
凡是符合A is a B的情形,A与B之间就可以构成继承关系
比如:
动物 -----> 猫 狗
汽车 -----> SUV 货车 挖掘机
电话 -----> 手机 固话
2.继承的语法
class A{...};
class B:public A{....};//类B公有继承类A
//A叫做B的父类,B叫做A的子类(派生类)
3.继承方式
所谓的继承方式就是能够提供给子类的最大访问权限,实际权限小于等于继承方式,私有数据必然在子类中隐藏(隐藏不代表不存在)。
语法
公有继承 ----------- class B:public A{....};
保护继承 ----------- class B:protected A{....};
私有继承 ----------- class B:private A{....};
4.继承中的构造和析构函数
构造子类时,会自动调用父类的构造函数,析构子类时,自动调用父类的析构函数。
调用构造和析构的顺序是相反的,先构造父类再构造子类,先析构子类再析构父类。
&esmp;&esmp;父类的数据由父类构造和析构,子类新增的数据由子类构造和析构。如果父类中有多个构造函数(构造函数有参数),子类默认调用无参的构造函数,可以在子类构造函数的初始化参数列表中选择对应的父类构造函数。
5.继承的拷贝构造
类使用默认的拷贝构造函数,自动去调用父类的拷贝构造,但是重写子类的拷贝构造,默认不调用父类的拷贝构造。所以在子类拷贝构造函数中应该使用初始化参数列表去调用父类的拷贝构造函数(可以使用子类对象来代表父类对象)。
6.名字隐藏
如果子类中出现了与父类重名的成员,父类的同名成员将被隐藏,
如果希望去访问父类中被隐藏的成员,使用类名作用域符去访问。
7.实验程序
#include <iostream>
#include <cstring>
using namespace std;
class A{
public:
A()
{
//先构造父类再构造子类
cout<<"A()"<<endl;
this->p = new char[10];
memset(this->p,0,10);
}
//拷贝构造
A(const A &a)
{
cout<<"A(const A &a)"<<endl;
this->p = new char[10];
strcpy(this->p,a.p);
}
void show()
{
cout<<"A - show()"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
delete[] this->p;
}
private:
char *p;
};
class B:public A{
public:
B()
{
cout<<"B()"<<endl;
this->abc = new char[10];
memset(this->abc,0,10);
}
//子类拷贝构造 ----- 通过初始化参数列表调用父类拷贝构造,可以使用子类对象来代表父类对象
B(const B &b):A(b)
{
cout<<"B(const B &b)"<<endl;
this->abc = new char[10];
strcpy(this->abc,b.abc);
}
void show()
{
cout<<"B - show()"<<endl;
}
//先析构子类再析构父类
~B()
{
cout<<"~B()"<<endl;
delete[] this->abc;
}
private:
char *abc;
};
int main()
{
B b1;
B b2 = b1;////会先调用父类的构造拷贝函数
b1.show();
b1.A::show();//访问父类中被隐藏的成员
return 0;
}
输出结果:
多重继承
1.概念
C++允许一个类继承多个父类,当一个类中包含多个已有的类中的属性和功能时,可以使用多重继承。
2.语法
class 子类名:继承方式1 父类1,继承方式2 父类2,...{
......
};
如果继承的数据不冲突(无同名数据),所有的数据直接访问,如果发生冲突可以使用父类名加作用域符来区分。
3.多继承优化:虚继承
1).概念
多继承容易出现同名重复数据,造成代码冗余,可以使用以虚继承方法优化。虚继承对于单层继承来说,和普通的继承没有区别;区别在于多层继承,子类在拷贝数据时,如果该数据处于更高层的父类,子类不会从直接父类拷贝数据,而是从最高层父类获取数据,这样父类就不会拥有最高层父类的多份拷贝了。
2).语法
class 子类名:virtual 继承方式 父类名{
......
};
2).实验程序
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
//记录是哪里的人
class wfrom{
public:
wfrom(const char *p=NULL)
{
this->p = new char[strlen(p) +1];
strcpy(this->p, p);
}
//因为有指针类型所以需要使用拷贝构造函数
wfrom(const wfrom &n)
{
if(strlen(n.p)){ //需要判断是否为空字符串
this->p = new char[strlen(n.p) + 1];
strcpy(this->p, n.p);
}
else{//空串
this->p = new char[10];
memset(this->p, 0, 10);
}
}
//申请了堆空间所以需要虚构函数用来释放所占用的空间
~wfrom()
{
delete[] this->p;
}
const char *get_wf()
{
return this->p;
}
private:
char *p;
};
//记录姓名
class name:virtual public wfrom{ //虚继承
public:
name(string p_name, const char *p):wfrom(p)//必须传入参数构造最高父类的构造函数
{
this->p_name = p_name;
}
string get_name()
{
return this->p_name;
}
private:
string p_name;
};
//记录职业
class occupation:virtual public wfrom{
public:
occupation(string occut, const char *p):wfrom(p)
{
this->occut = occut;
}
string get_ocinfo()
{
return this->occut;
}
private:
string occut;
};
//记录个人信息
class personal_info:public name,public occupation{
public:
personal_info(int salary, string occut, string p_name, const char *p):
name(p_name, p),occupation(occut, p),wfrom(p) //注意这里需要再次调用最高父类的构造函数,父类的也不能漏,虽然参数重复了
{
this->salary = salary;
}
void show()
{
cout<<get_name()<<":"<<get_wf()<<":"<<get_ocinfo()<<":"<<"salary:"<<this->salary<<endl;
}
private:
int salary;
};
int main()
{
personal_info emp1(6000, "厨师", "小李", "广东人");
emp1.show();
return 0;
}
输出结果:
多态
1.虚函数
1).概念
虚函数就是在类的成员函数声明前加virtual,该成员函数就成为虚函数。一旦一个类有虚函数,编译器将为该类生成虚函数表,每一个虚函数都会在表中记录一个地址。
虚函数表中每一个元素记录一个虚函数的地址,该类构造的对象的前4(8)个字节记录虚函数表的地址。
如果父类中有虚函数,子类中可以对该虚函数进行重写(override),子类中重写的函数就会覆盖原虚函数表中虚函数的位置。
如果父类中的虚函数在子类中被重写,当使用父类类型记录子类对象时,调用该虚函数时就会去调用子类中被重写的函数,而不调用父类的原虚函数。普通的成员函数没有该特性。
使用虚函数可以实现用父类类型记录子类对象,通过该类型(指针/引用)访问对应的接口访问的事子类中的接口。可以大大提高编程的效率和灵活性,这种语法就叫多态。
2).虚析构
用父类类型记录子类对象,当对象的生命周期结束,系统将会自动调用父类的析构函数,而不调用子类对象的析构函数,这导致子类对象的占用的资源得不到释放,这可以使用虚析构函数,用法就是在析构函数前面加上virtual。
3).实验程序
#include <iostream>
using namespace std;
class Animal{
public:
//虚析构
virtual ~Animal()
{
cout<<"~Animal()"<<endl;
}
//虚函数
virtual void show()
{
cout<<"Animal show"<<endl;
}
virtual void run()
{
cout<<"Animal run"<<endl;
}
virtual void eat()
{
cout<<"Animal eat"<<endl;
}
};
class Cat:public Animal{
public:
//虚析构
virtual ~Cat()
{
cout<<"~Cat()"<<endl;
}
//虚函数重写,覆盖
virtual void show()
{
cout<<"Cat show"<<endl;
}
void run()
{
cout<<"Cat run"<<endl;
}
virtual void eat()
{
cout<<"Cat like fish"<<endl;
}
};
//传入一个Animal对象----- 多态
void animal_gogo(Animal *p)
{
p->show();
p->run();
p->eat();
}
int main()
{
//父类类型可以记录子类对象,父类的虚函数将被子类对象代替
Animal *pa = new Cat;
animal_gogo(pa);
delete pa;
//不使用父类类型记录子类对象,就直接调用本类的函数
pa = new Animal;
animal_gogo(pa);
delete pa;
return 0;
}
输出结果:
2.继承类和纯虚函数
1).概念
用virtual修饰,函数没有函数语句体,只有声明语句和(=0)。
2).语法
class A{
public:
virtual void show()=0;//纯虚函数
};
类中如果有纯虚函数,该类将不能实例化(不能构造对象),这种类就叫抽象类。
3).抽象类的应用
抽象类不是用来构造对象,是用来作为父类继承产生子类
子类在继承抽象类时,如果没有实现父类中的纯虚函数,那么该子类也是一个抽象类。
如果一个类中所有的成员函数都是纯虚函数,那么该类叫做纯抽象类。
C++中声明和实现的分离
C++代码的组成元素是类,类的声明写在头文件,类的声明包括成员变量的声明,成员函数的声明。成员变量的声明不用修改,类中的函数只保留声明语句。
类的函数的实现写到配对的源文件,实现类中函数时指定该函数属于哪个类。
头文件(xxx.h xxx.hpp)
class 类名{
成员变量的声明;
成员函数的声明;
构造函数,拷贝构造函数和析构函数的声明;
};
源文件(xxx.cpp)
成员函数的实现;
构造函数,拷贝构造函数和析构函数的实现;
//函数参数的默认值要写到声明中
//初始化参数列表必须写在源文件中
总结
1.父类的成员在子类中的访问权限只会收缩,不会扩大,在子类中的访问属性不会超过继承方式;
2.几种同名处理机制 ------ 函数重载(overload),名字隐藏(name hide),函数重写(override);
函数重载:自同一作用域,函数名相同,参数列表不同的函数构成重载关系
名字隐藏:子类中出现与父类同名的成员,父类的同名成员默认被隐藏
函数重写:子类中重写父类中的虚函数,父类的虚函数将被覆盖(虚函数表)
3.多态的基础是继承;虚函数是实现多态的关键;虚函数重写是多态的必要条件。