C++类和对象篇

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;
}

有了纯虚析构之后,这个类属于抽象类,无法实例化对象。 

Guess you like

Origin blog.csdn.net/m0_63203388/article/details/121975457