C++面向对象的三大特性:封装、继承、多态
一、封装
1、语法
class 类名{访问权限:属性或行为};
class Person{
public:
string name;
int age;
};
2、三种权限
(1)公共权限public:类内可访问,类外也可以
(2)保护权限protected:类内可访问,类外不可以,子可以访问父的保护内容
(3)私有权限private:类内可访问,类外不可以,子不可以访问父的私有内容
3、struct与class的区别
struct的默认权限为public,class默认权限为private
4、成员属性设置为私有
优点:可以自己控制读写权限,对于写权限可以检测数据的有效性
class Person{
public:
//写入name
void setName(string name){
m_Name=name;
}
//获取name
void getName(){
return m_Name;
}
//检测数据的有效性
void setAge(int age){
if(age<0||age>150){
cout<<"error!"<<endl;
}
else{
m_Age=age;
}
private:
string m_Name;//可读可写
int m_Age;//只写
};
int main(){
Person p;
p.setName("Amy");
p.setAge(10);
cout<<"name:"<<p.getName()<<endl;
return 0;
}
5、函数中引用类
建议使用引用传递
bool cmp(Cube &c1,Cube &c2){}
6、类的封装性和信息隐蔽
(1)公用接口与私有实现的分离
当接口与实现分离时,只要类的接口没有改变,对私有实现的修改不会引起程序的其他部分的修改。
(2)类声明和成员函数定义的分离
一个C++程序是由三个部分组成:类声明头文件(.h),类实现文件(.cpp,包含类的定义),类的使用文件(.cpp,即主文件)
-
类声明头文件
#include<string> using namespace std; class Student { public: void display(); private: int num; string name; char sex; }
-
类实现文件
#include<iostream> #include"student.h" void Student::diaplay() { cout<<"num:"<<num<<endl; }
-
主文件
#include<iostream> #include"student.h" using namespace std; int main() { Student stu; stu.diaplay(); return 0; }
ps:可以认为类声明头文件时用户使用类库的公用接口
二、对象特性
1、析构函数
用于对对象的清理
(1)语法:~类名(){}
注意:
a.不可以有参数,不可发生重载
b.程序在对象销毁前会自动调用析构,无需手动调用,且只会调用一次
调用析构函数的具体情况:
- 如果在一个函数中定义了一个对象(假设是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数
- 静态(static)局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只有main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数
- 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数时),调用该全局对象的析构函数
- 如果new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数
class Person(){
public:
~Person(){
cout<<"析构函数"<<endl;
}
}
注意:如果直接调用,运行结果不会显示“析构函数”
但如果通过函数调用运行结果会输出“析构函数”
因为当函数执行完后会释放对象,所以会调用析构函数
而直接调用在代码执行完毕后才释放对象
如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它徒有析构函数的名称和形式,实际上什么操作都不执行。
2、构造函数
用于对对象的初始化
(1)语法
类名(){}
注意:
a.可以有参数,可发生重载,可以使用默认参数
b.程序在调用对象时会自动调用构造,无需手动调用,且只会调用一次
c.无返回值,作用只是对对象进行初始化,因此不需要在定义构造函数时声明类型
(2)分类
按有无参数:无参构造和有参构造
按类型:普通构造和拷贝构造
(3)调用方法
a.括号法
class Person{
public:
Person(){
cout<<"无参拷贝构造"<<endl;
}
Person(int a){
cout<<"有参拷贝构造"<<endl;
}
Person(const Person &p){
cout<<"拷贝构造"<<endl;
}
};
int main(){
Person p1;
Person p2(10);
Person p3(p2);
return 0;
}
b.显示法
Person p1;
Person p2=Person(10);
Person p3=Person(p2);
注:其中Person(10)是一个匿名对象,当前执行结束后系统会立即回收匿名对象。不要利用拷贝构造初始化匿名对象。
c.隐式转换法
Person p1;
Person p2=10;
Person p3=p2;
(4)调用规则
默认情况下,C++编译器至少会给一个类添加3个函数:
a.默认构造(无参空实现)
b.默认析构(无参空实现)
c.默认拷贝构造 对属性进行值拷贝
如果用户有定义有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造
如果用户有定义拷贝构造,C++不会再提供其他构造函数
(5)拷贝构造
class Person{
public:
int myage1,myage2;
Person(int age){
myage1=age;
}
Person(const Person &p){
myage2=p.myage1;
}
};
int main(){
Person p1(20);
Person p2(p1);
cout<<p2.myage2<<endl;
return 0;
}
拷贝函数的调用时机:
a.使用一个已创建的对象来初始化一个新对象时
b.通过值传递的方式给函数参数传值时
void test1(){
Person p3;
test2(p3);//在调用参数时也会调用拷贝构造函数
}
c.通过值传递方式返回局部对象时
class Person{
public:
int myage1,myage2;
Person(int age){
myage1=age;
}
Person(const Person &p){
myage2=p.myage1;
}
};
Person dowork(){
Person p3;
return p3;//此处会调用拷贝构造
}
void test3(){
Person p4=dowork();
}
int main(){
test3;
return 0;
}
深拷贝与浅拷贝:
浅拷贝:简单的拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作
注:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
(6)初始化列表
类并不是一个实体,而是一种抽象类型,并不占用存储空间,显然无处容纳数据,不能在类声明中对数据成员初始化。
如果一个类中所有成员都是公用的,则可以在定义对象时对数据成员进行初始化。但这一方法对类中有private和protected的成员不适用。
Person p1={0,0,0};
用构造函数初始化列表:
class Person{
public:
Person(int a,int b,int c){
A=a;
B=b;
C=c;
}
};
void test1(){
Person p(10,20,30);
}
在构造函数中用参数初始化列表对数据成员初始化:
class Person{
public:
int A,B,C;
Person(int a,int b,int c):A(a),B(b),C(c){
}
};
void test1(){
Person p(10,20,30);
}
也可以用一个类对象初始化另一个类对象
Person p1;
Person p2=p1;
(7)类对象作为类成员
当其他类对象作为本类成员,构造时先构造类对象,再构造自身,析构顺序相反
即先构造的后析构,后构造的先析构
class A{};
class B{
public:
A a;
};
3、静态成员
(1)静态成员变量
class Person{
public:
static int A;//类内声明
};
int Person::A=100;//类外初始化
int main(){
Person p;
cout<<p.A<<endl;//通过对象进行访问
cout<<Person::A<<endl;//通过类名进行访问
return 0;
}
特点:
a.所有对象共享同一份数据
Person p1;
p1.A=200;//通过p1将上述数据改为200
Person p2;
cout<<p2.A<<endl;//通过p2来访问,输出结果为200
b.在编译阶段就分配内存,内存分配在全局区
c.类内声明,类外初始化
d.静态成员变量也有访问权限,访问权限为private时也不可在类外访问
(2)静态成员函数
class Person{
public:
static int A;
static void func(){
cout<<Person::A<<endl;
}//静态成员函数
};
int Person::A=100;
int main(){
Person p;
p.func();//通过对象进行访问
Person::func();//通过类名进行访问
return 0;
}
特点:
a.所有成员共享同一个函数
Person p1;
p1.func();
Person p2;
p2.func();
其中p1,p2访问的是同一个函数
b.只能访问静态成员变量,若用普通变量,则会无法判别应该用哪个对象对应的变量
c.静态成员函数也有访问权限
4、C++对象模型和this指针
(1)成员变量和成员函数分开储存
空对象占用内存空间为1个字节:C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置。每个空对象也应该有一个独一无二的内存地址
class Person{
public:
int m_A;//非静态成员变量占用对象空间
static int m_B;//静态成员变量不占用对象空间
void show1(){
cout<<"非静态成员函数"<<endl;
}//非静态成员函数不占用对象空间
static void show2(){
cout<<"静态成员函数"<<endl;
}//静态成员变量不占用对象空间
};
int Person::m_B=100;
int main(){
Person p;
cout<<"对象占用空间为:"<<sizeof(p)<<endl;//输出结果为4
return 0;
}
(2)对象指针
1.指向对象的指针
一个对象存储空间的起始地址就是对象的指针。
可以定义一个指针变量,用来存放对象的地址,这就是指向对象的指针变量。
Student *stu;
(*stu).display();
stu->display();
2.指向对象成员的指针
-
指向对象数据成员的指针
int *p=&stu.num; cout<<*p<<endl;
-
指向成员函数的指针
指向普通函数的指针:
*类型名(指针变量名)(参数表列);
void (*p)();//p是指向void型函数的指针变量 p=func;//将func函数的入口地址赋给指针变量p,p就指向了func (*p)();//调用func函数
指向对象成员函数的指针:
*类型名(类名::指针变量名)(参数表列);
Student stu; void(Student::*p)(); p=&Student::display;//成员函数的入口地址写法:&类名::成员函数名 (stu.*p)();
3.指向当前对象的this指针
this指针指向被调用的成员函数所属的对象
this指针是隐含在每一个非静态成员函数内部的一种指针,不需定义,直接使用即可
用途:
a.当形参与成员变量同名时,可用this指针区分
class Person{
public:
Person(int age){
//age=age 无法输出正确结果
this->age=age;
}
int age;
};
int main(){
Person p(18);
cout<<"age="<<p.age<<endl;
return 0;
}
形参与成员变量不同名的情况:
class Person{
public:
Person(int age){
m_age=age;//其实也相当于this->m_age=age;
}
int m_age;
};
int main(){
Person p(18);
cout<<"age="<<p.age<<endl;
return 0;
}
b.返回对象本身用*this
class Person{
public:
Person showage(Person &p){
this->m_age+=p.m_age;
return *this;//返回对象本身
}
int m_age;
};
int main(){
Person p1;
p1.m_age=10;
Person p2;
p2.m_age=10;
p2.showage(p1);
cout<<"p2.m_age="<<p2.m_age<<endl;
return 0;
}
(3)空指针访问成员函数
C++中空指针是可以调用成员函数的,但是也要注意函数中有没有用到this指针
class Person{
public:
void showclassname(){
cout<<"this is Person class"<<endl;
}
void showage(){
cout<<"age="<<m_age<<endl;//m_age相当于this->m_age
}
int m_age;
};
int main(){
Person *p=NULL;
p->showclassname();//能运行
p->showage();//报错
return 0;
}
如果遇到this指针,要加以判断:
class Person{
public:
void showclassname(){
cout<<"this is Person class"<<endl;
}
void showage(){
if(this==NULL){
return;
}//如果遇到空指针则不运行下方程序
cout<<"age="<<m_age<<endl;//m_age相当于this->m_age
}
int m_age;
};
int main(){
Person *p=NULL;
p->showclassname();
p->showage();
return 0;
}
(4)const修饰成员函数
a.常函数
成员函数后加const
常函数内不能修改成员属性,但如果成员属性声明时加了mutable关键字,则这个成员属性在常函数中可以修改
class Person{
public:
//this指针的本质:Person *const this;指向不可修改,但指向的值可修改
//在成员函数后面加const修饰的是this的指向,让指针指向的值也不能改变
void show() const{
this->m_a=100;//报错
this->m_b=100;//可修改
}
int m_a;
mutable int m_b;
};
b.常对象
常对象不能修改成员属性,但如果成员属性声明时加了mutable关键字,则这个成员属性可以修改
const Person p;
p.m_a=100;//报错
p.m_b=100;//可修改
常对象只能调用常函数,因为普通成员函数可以修改成员属性
5、内置成员函数(inline成员函数)
为了减少时间开销,如果在类中定义的成员函数中不包括循环等控制结构,C++系统会自动地将它们作为内置(inline)函数来处理。
无论是否有inline声明,成员函数的代码都不占用对象的存储空间。inline函数只影响程序执行的效率。
6、对象数组
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
Student(int a,string b);
private:
int num;
string name;
};
Student::Student(int a,string b)
{
num=a;
name=b;
}
int main()
{
//定义对象数组
Student stu[3]={
Student(1,"John"), //调用构造函数,提供第一个元素的形参
Student(2,"Amy"),
Student(3,"Nancy")
};
return 0;
}
三、友元
关键字:friend
作用:让一个函数或者类访问另一个类中的私有成员
1、全局函数做友元
class Building{
//告诉编译器该全局函数是Building类的友元
friend void goodfriend(Building &b);
public:
string m_livingroom;
Building(){
m_livingroom="客厅";
m_bedroom="卧室";
}
private:
string m_bedroom;
};
void goodfriend(Building &b){
cout<<"正在参观"<<b.m_livingroom<<endl;
cout<<"正在参观"<<b.m_bedroom<<endl;
}
int main(){
Building b1;
goodfriend(b1);
return 0;
}
2、类做友元
class Building{
//告诉编译器该类为Building类的友元
friend class Goodfriend;
public:
string m_livingroom;
Building(){
m_livingroom="客厅";
m_bedroom="卧室";
}
private:
string m_bedroom;
};
class Goodfriend{
public:
Goodfriend();
void visit();
private:
Building *building;
};
//在类外写成员函数
Goodfriend::Goodfriend(){
//创建对象
building=new Building;
}//该函数运行时Building类的构造函数也会运行
void Goodfriend::visit(){
cout<<"正在参观"<<building->m_livingroom<<endl;
cout<<"正在参观"<<building->m_bedroom<<endl;
}
int main(){
Goodfriend g;
g.visit();
return 0;
}
3、成员函数做友元
class Building;//由于Goodfriend类中有用到Building类,所以需要先声明
class Goodfriend{
public:
Goodfriend();
void visit();
private:
Building *building;
};
class Building{
//告诉编译器该成员函数为Building类的友元
friend void Goodfriend::visit();
public:
string m_livingroom;
Building(){
m_livingroom="客厅";
m_bedroom="卧室";
}
private:
string m_bedroom;
};
//在类外写成员函数
//由于函数中含有Building类的东西,所以需要写在Building类定义之后
Goodfriend::Goodfriend(){
//创建对象
building=new Building;
}//该函数运行时Building类的构造函数也会运行
void Goodfriend::visit(){
cout<<"正在参观"<<building->m_livingroom<<endl;
cout<<"正在参观"<<building->m_bedroom<<endl;
}
int main(){
Goodfriend g;
g.visit();
return 0;
}
四、C++运算符重载
1、加号运算符重载
class Person{
public:
//通过成员函数实现
Person operator+(Person &p){
Person temp;
temp.m_a=p.m_a+this->m_a;
temp.m_b=p.m_b+this->m_b;
return temp;
}
int m_a,m_b;
};
//通过全局函数实现
Person operator+(Person &p1,Person &p2){
Person temp;
temp.m_a=p1.m_a+p2.m_a;
temp.m_b=p1.m_b+p2.m_b;
return temp;
}
//重载版本
Person operator+(Person &p,int num){
Person temp;
temp.m_a=p.m_a+num;
temp.m_b=p.m_b+num;
return temp;
}
int main(){
Person p1;
p1.m_a=10;
p1.m_b=20;
Person p2;
p2.m_a=10;
p2.m_b=20;
//成员函数本质调用:Person p3=p1.operator+(p2);
//全局函数本质调用:Person p3=operator+(p1,p2);
Person p3=p1+p2;
//运算符重载允许发生函数重载
Person p4=p1+10;
cout<<"p3.m_a="<<p3.m_a<<endl;
cout<<"p3.m_b="<<p3.m_b<<endl;
cout<<"p4.m_a="<<p4.m_a<<endl;
cout<<"p4.m_b="<<p4.m_b<<endl;
return 0;
}
2、左移运算符重载
class Person{
public:
//用成员函数实现
//相当于p.operator<<(cout),简化版本为p<<cout,由于cout不在左侧所以不常用
// ostream& operator<<(ostream &cout){
// cout<<this->m_name1<<" "<<this->m_name2<<endl;
// return cout;
// }
string m_name1;
string m_name2;
};
//用全局函数实现
//cout是一个标准输出流的对象,且全局只能有一个,所以用引用的方式
ostream& operator<<(ostream &cout,Person &p){
cout<<p.m_name1<<" "<<p.m_name2<<endl;
return cout;
}
int main(){
Person p;
p.m_name1="TOM";
p.m_name2="AMY";
cout<<p;
return 0;
}
3、递增运算符重载
(1)前置递增
class Person{
public:
//重载前置++运算符
//返回引用是为了一直对同一个数据进行递增
Person& operator++(){
this->m_a++;
this->m_b++;
return *this;
}
int m_a;
int m_b;
};
//重载<<运算符
ostream& operator<<(ostream& cout,Person &p){
cout<<"m_a="<<p.m_a<<" "<<"m_b="<<p.m_b<<endl;
return cout;
}
int main(){
Person p;
p.m_a=1;
p.m_b=10;
cout<<++p<<endl;
return 0;
}
(2)后置递增
class Person{
public:
//返回值不能用引用,不能返回一个局部对象的引用
//通常用一个占位参数区分前置和后置递增
Person operator++(int){
Person temp=*this;
m_a++;
m_b++;
return temp;
}
int m_a;
int m_b;
};
//重载<<运算符,注意此处p无引用,与++重载函数对应
ostream& operator<<(ostream& cout,Person p){
cout<<"m_a="<<p.m_a<<" "<<"m_b="<<p.m_b<<endl;
return cout;
}
int main(){
Person p;
p.m_a=1;
p.m_b=10;
cout<<p++<<endl;
cout<<p<<endl;
return 0;
}
4、赋值运算符重载
C++编译器至少给一个类添加4个函数:
(1)默认构造函数(无参,函数体为空)
(2)默认析构函数(无参,函数体为空)
(3)默认拷贝构造函数,对属性进行值拷贝
(4)赋值运算符operator=,对属性进行值拷贝
如果类中有属性指向堆区,做复制操作时也会出现深浅拷贝的问题
class Person{
public:
//在堆区开辟一个内存,new返回指针,用指针来接收
Person(int num){
m_age=new int(num);
}
//析构函数释放内存,堆区开辟的数据需要手动释放
//由于默认提供浅拷贝,所以两者指向的内存都是同一块,会造成内存重复释放的问题
~Person(){
if(m_age!=NULL){
delete m_age;
m_age=NULL;
}
}
//解决方案:提供深拷贝
Person& operator=(Person &p){
//浅拷贝
// m_age=p.m_age;
//深拷贝
m_age=new int(*p.m_age);
//要返回自身,返回引用才能返回自身,返回值只是返回拷贝的副本
return *this;
}
int *m_age;
};
int main(){
Person p1(18);
Person p2(20);
p2=p1;
cout<<"p1年龄:"<<*p1.m_age<<endl;
cout<<"p2年龄:"<<*p2.m_age<<endl;
return 0;
}
5、关系运算符重载
以==重载为例
#include<iostream>
#include<string>
using namespace std;
class Person{
//友元
friend bool operator==(Person &p1,Person &p2);
public:
Person(int age,int id){
m_age=age;
m_id=id;
}
private:
int m_age;
int m_id;
};
bool operator==(Person &p1,Person &p2){
if(p1.m_age==p2.m_age&&p1.m_id==p2.m_id){
return true;
}
else{
return false;
}
}
int main(){
Person p1(18,2022);
Person p2(18,2024);
if(p1==p2){
cout<<"p1与p2相等"<<endl;
}
else{
cout<<"p1与p2不相等"<<endl;
}
return 0;
}
6、函数调用运算符()重载
由于重载后使用的方式非常像函数调用,因此称为仿函数
仿函数没有固定写法,非常灵活
class Myprint{
public:
void operator()(string text){
cout<<text<<endl;
}
};
int main(){
Myprint myprint;
myprint("hello");
//匿名函数对象,执行完毕后立即被释放
Myprint()("hi");
return 0;
}
五、继承
作用:减少重复代码
1、基本语法
class 子类:继承方式 父类{};
子类也可以称为派生类,父类也可以称为基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
#include<iostream>
using namespace std;
class Base{
public:
void title(){
cout<<"公共标题"<<endl;
}
void bottle(){
cout<<"公共底部"<<endl;
}
};
class Python:public Base{
public:
void show(){
cout<<"python课程"<<endl;
}
};
class Cpp:public Base{
public:
void show(){
cout<<"C++课程"<<endl;
}
};
class Java:public Base{
public:
void show(){
cout<<"Java课程"<<endl;
}
};
int main(){
Python p;
p.title();
p.show();
p.bottle();
cout << "--------------------" << endl;
Cpp c;
c.title();
c.show();
c.bottle();
cout << "--------------------" << endl;
Java j;
j.title();
j.show();
j.bottle();
return 0;
}
2、继承方式
继承方式有:公有继承、保护继承、私有继承
3、继承中的对象模型
class Base{
public:
int m_a;
private:
int m_b;
protected:
int m_c;
};
class Python:public Base{
public:
void show(){
cout<<"python课程"<<endl;
}
};
class Son:public Base{
public:
int m_d;
};
int main(){
Son s;
cout<<"size="<<sizeof(s)<<endl;
return 0;
}
运行以上代码,输出结果为size=16
由此可知:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
4、继承中构造与析构的顺序
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
class Base{
public:
Base(){
cout<<"Base的构造函数"<<endl;
}
~Base(){
cout<<"Base的析构函数"<<endl;
}
};
class Son:public Base{
public:
Son(){
cout<<"Son的构造函数"<<endl;
}
~Son(){
cout<<"Son的析构函数"<<endl;
}
};
void test(){
Son s;
}
int main(){
test();
return 0;
}
5、继承中同名成员的处理
class Base{
public:
int m_a=20;
void func(){
cout<<"Base - func()调用"<<endl;
}
void func(int a){
cout << "Base - func(int a)调用" << endl;
}
};
class Son:public Base{
public:
int m_a=10;
void func(){
cout<<"Son中的func"<<endl;
}
};
void test(){
Son s;
cout<<"Son中的m_a="<<s.m_a<<endl;
cout<<"Base中的m_a="<<s.Base::m_a<<endl;
s.func();
s.Base::func();
s.Base::func(10);//
}
int main(){
test();
return 0;
}
总结:
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
6、继承中同名静态成员处理
静态成员和非静态成员出现同名,处理方式一致
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
只不过访问方式有两种:通过对象访问和通过类名访问
class Base {
public:
static void func()
{
cout << "Base - static void func()" << endl;
}
static void func(int a)
{
cout << "Base - static void func(int a)" << endl;
}
static int m_A;
};
int Base::m_A = 100;
class Son : public Base {
public:
static void func()
{
cout << "Son - static void func()" << endl;
}
static int m_A;
};
int Son::m_A = 200;
//同名成员属性
void test01()
{
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;
//通过类名访问
cout << "通过类名访问: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
//同名成员函数
void test02()
{
//通过对象访问
cout << "通过对象访问: " << endl;
Son s;
s.func();
s.Base::func();
cout << "通过类名访问: " << endl;
Son::func();
Son::Base::func();
//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::Base::func(100);
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
7、多继承语法
class 子类 :继承方式 父类1 , 继承方式 父类2…
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
class Base1 {
public:
Base1()
{
m_A = 100;
}
public:
int m_A;
};
class Base2 {
public:
Base2()
{
m_A = 200; //m_A会出现不明确
}
public:
int m_A;
};
class Son : public Base2, public Base1
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
public:
int m_C;
int m_D;
};
//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}
int main() {
test01();
system("pause");
return 0;
}
8、菱形继承
两个派生类继承同一个基类,又有某个类同时继承者两个派生类。这种继承被称为菱形继承,或者钻石继承
菱形继承的问题:
-
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
-
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
class Animal{
public:
int m_age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep:virtual public Animal{};
class Tuo:virtual public Animal{};
//虚继承后继承到两个指针,这两个指针指向同一个地址
class SheepTuo:public Sheep,public Tuo{};
int main(){
SheepTuo st;
st.Sheep::m_age=10;
st.Tuo::m_age=20;//与st.Sheep::m_age指向同一个数据
cout<<"size="<<sizeof(st)<<endl;
cout<<st.Sheep::m_age<<endl;
cout<<st.Tuo::m_age<<endl;
cout<<st.m_age<<endl;
return 0;
}
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
六、多态
1、基本概念
多态是C++面向对象三大特性之一
(1)多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
(2)静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal{
public:
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak(){
cout<<"动物在叫"<<endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout<<"喵喵喵"<<endl;
}
};
class Dog:public Animal{
public:
void speak(){
cout<<"汪汪汪"<<endl;
}
};
void test(Animal &animal){
animal.speak();
}
int main(){
Cat cat;
Dog dog;
//传入什么对象,那么就调用什么对象的函数
test(cat);
test(dog);
return 0;
}
如果函数地址在编译阶段就能确定,那么静态联编;如果函数地址在运行阶段才能确定,就是动态联编。
(3)多态使用:父类指针或引用指向子类对象
(4)多态满足条件:
-
有继承关系
-
子类重写父类中的虚函数
2、原理剖析
class Animal{
public:
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak(){
cout<<"动物在叫"<<endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout<<"喵喵喵"<<endl;
}
};
class Dog:public Animal{
public:
void speak(){
cout<<"汪汪汪"<<endl;
}
};
void test(Animal &animal){
animal.speak();
}
int main(){
Cat cat;
Dog dog;
cout<<"sizeof="<<sizeof(Animal)<<endl;//输出结果为一个指针的大小
return 0;
}
当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址。
3、优点
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
class Calculator{
public:
int m_a=10,m_b=12;
virtual int getans(){
return 0;
}
};
class Add:public Calculator{
public:
int getans(){
return m_a+m_b;
}
};
class Multiply:public Calculator{
public:
int getans(){
return m_a*m_b;
}
};
class Substract:public Calculator{
public:
int getans(){
return m_a-m_b;
}
};
int main(){
//多态使用:父类指针或引用指向子类对象
Calculator *c=new Add;
cout<<c->m_a<<"+"<<c->m_b<<"="<<c->getans()<<endl;
delete c;//堆区数据用完需手动销毁
c=new Substract;
cout<<c->m_a<<"-"<<c->m_b<<"="<<c->getans()<<endl;
delete c;
c=new Multiply;
cout<<c->m_a<<"*"<<c->m_b<<"="<<c->getans()<<endl;
delete c;
return 0;
}
4、纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
5、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象的问题
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
class Animal{
public:
virtual void speak()=0;
// virtual ~Animal(){
// cout<<"Animal的虚析构函数调用"<<endl;
// }
virtual ~Animal()=0;
};
Animal::~Animal(){
cout<<"Animal的纯虚析构函数的调用"<<endl;
}
class Cat:public Animal{
public:
Cat(string a){
m_name=new string(a);
}
string *m_name;
void speak(){
cout<<*m_name<<":喵喵喵"<<endl;
}
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//如果父类中没有虚析构函数或纯析构函数,此时子函数的析构函数不会执行
~Cat(){
if(m_name!=NULL){
cout<<"Cat的析构函数调用"<<endl;
delete m_name;
}
}
};
void test(Animal *animal){
animal->speak();
}
int main(){
test(new Cat("Tom"));
return 0;
}
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
列表)= 0 ;`
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
5、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象的问题
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
class Animal{
public:
virtual void speak()=0;
// virtual ~Animal(){
// cout<<"Animal的虚析构函数调用"<<endl;
// }
virtual ~Animal()=0;
};
Animal::~Animal(){
cout<<"Animal的纯虚析构函数的调用"<<endl;
}
class Cat:public Animal{
public:
Cat(string a){
m_name=new string(a);
}
string *m_name;
void speak(){
cout<<*m_name<<":喵喵喵"<<endl;
}
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//如果父类中没有虚析构函数或纯析构函数,此时子函数的析构函数不会执行
~Cat(){
if(m_name!=NULL){
cout<<"Cat的析构函数调用"<<endl;
delete m_name;
}
}
};
void test(Animal *animal){
animal->speak();
}
int main(){
test(new Cat("Tom"));
return 0;
}
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类