C++从入门到精通【DAY5】面向对象(下)

一、继承

        继承是面向对象三大特性之一

        有些类除了有自己的特性,还与其他类存在共同的属性,比如学生和老师,他们都是属于同一个学校、同一个班等等。

        这个时候我们可以考虑利用继承,减少重复代码。例如在很多网站中,它们都有共同的头部、底部等内容,它们都是由继承技术实现的。

1.继承的基本语法

基本语法:

class 父类名(基类) {
    父类(基类)的成员变量和成员函数
};

class 子类(派生类)名 : 继承方式 父类名 {
    子类(派生类)的成员变量和成员函数
};

例:

class Student
{
public:
    string schoolname;
    void print()
    {
        cout << "学校名字 :" << schoolname << endl;
    }
private:
    int student_id;
};
class Teacher : public Student
{
private:
    int teacher_id;
};
int main()
{   
    Teacher t;
    t.schoolname = "清华大学";//子类对象可以访问父类对象的属性
    t.print();
    return 0;
}

        C++中允许一个类继承多个类,多继承语法:

class 子类: 继承方式 父类1 , 继承方式 父类2 ······

        C++实际开发中不建议使用多继承,可能会引发有同名成员出现,需要加作用域区分,后面我们会学习到。

2.继承方式

        继承方式也有三种:公共继承(public),保护继承(protected),私有继承(private)

        public公共继承:可以继承父类的public和protected成员,且在子类中父类的public成员权限仍为public,父类的protected成员权限仍为protected;不可以访问父类的private成员,但仍会继承

        private私有继承:可以继承父类的public和protected成员,且在子类中父类的public和protected成员的权限都变为private;不可以访问父类的private成员,但仍会继承

        protected保护继承:可以继承父类的public和protected成员,且在子类中父类的public和protected成员的权限都变为protected;不可以访问父类的private成员,但仍会继承

例:

class ABC; //声明一个ABC类,作为父类
class a; class b; class c;  //声明三个类a , b , c作为子类
class ABC
{
public:
    int PublicMembers;
protected:
    int ProtectedMembers;
private:
    int PrivateMember;
};
class a:public ABC    //公有继承
{
public:
    int PublicMembers; //父类的公有成员在a中仍为公有成员
protect:
    int ProtectedMembers; //父类的保护成员在a中仍为保护成员
};
class b:private ABC    //私有继承
{
private:
    int PublicMembers;
    int ProtectedMembers;  //父类的公有成员和保护成员在b中都为私有成员
};
class c:protected ABC    //保护继承
{
protected:
    int PublicMembers;
    int ProtectedMembers;  //父类的公有成员和保护成员在b中都为保护成员
};

        由上可知,三种继承方式子类都是无法访问父类的private成员,但是子类仍会继承,只是被隐藏了。

例:

class A
{
public:
    int PublicMembers;
protected:
    int ProtectedMembers;
private:
    int PrivateMember;
};

class a : public A    //公有继承
{
public:
    int a;
};
int main()
{   
    cout<<"size of class_a "<<sizeof(a)<<endl;  //输出16
    return 0;
}

输出结果为16,可知子类a继承了父类A的三类成员加上它自身的成员一共有4个整型数据成员,所以sizeof(a)的结果得到16

3.继承中的构造和析构顺序

例:

class Father
{
public:
    Father()
    {
        cout << "调用父类构造函数" << endl;
    }
    ~Father()
    {
        cout << "调用父类析构函数" << endl;
    }
};
class Son : public Father
{
public:
    Son()
    {
        cout << "调用子类构造函数" << endl;
    }
    ~Son()
    {
        cout << "调用子类析构函数" << endl;
    }
};
void test_01() //测试函数,这样我们就可以在里边看到析构函数的结果
{
    Son s;
}
int main()
{   
    test_01();
    return 0;
}

输出结果:

由结果我们可以知道,构造函数的调用顺序是先父后子,析构函数的调用顺序是先子后父。

4.继承的同名成员处理

        面对父类和子类的同名成员,为了在调用时区分它们,我们在调用父类同名成员时可以使用父类名+指定作用域符号(::)来调用。

例:

class Father
{
public:
    int a;
    Father()
    {
       a = 10;
    }
};
class Son:public Father
{
public:
        int a = 20;
};
int main()
{   
    Son s;
    cout<<" 调用Son 下的 a成员变量:"<<s.a<<endl;
    cout<<" 调用Father 下的 a成员变量:"<<s.Father::a<<endl;
    return 0;
}

        对于同名的静态成员,我们也是以同样的方式处理。

5.菱形继承(钻石继承)和 虚继承

        菱形继承概念:

                两个子类继承同一个父类,又有某个类同时继承这两个子类,此时它们之间的关系图像一个菱形,所以称为菱形继承或钻石继承。      

菱形继承的问题:

        子类Aa和Ab会继承父类A的成员数据,而子子类Aaa同时继承了子类Aa和Ab,此时就会产生二义性,即子子类Aaa就会继承了两份一样的父类A的成员数据,造成资源浪费,其实我们只需要一份就足够了。此时我们可以用到虚继承的方式来解决。

虚继承语法:在继承方式前加上关键字virtual

class 子类: virtual 继承方式 父类 {};

二、多态

多态是C++面向对象的三大特性之一

多态又分为两类:

①静态多态:函数重载 和 运算符重载 属于静态多态 ,复用函数名

②动态多态:子类和虚函数实现运行时多态

两者的区别:

···静态多态的函数地址在编译阶段已经确定和绑定(早)

···动态多态的函数地址在运行阶段才确定和绑定(晚)

1.静态多态

静态多态的运算符重载在【Day4】中我们已经学习过了,今天我们来了解函数重载。

        函数重载就是让同名但是参数不同的函数能发挥不同的功能,它有三个条件:

···同名函数必须在一个作用域下

···函数名称相同

···函数参数必须不同,可以是参数类型不同、个数不同、参数传入顺序不同等

例:

//在全局作用域下
void func()
{
    cout<<" func的调用 "<<endl;
}
void func(int a)
{
    cout<<" !func(int a)的调用!"<<endl;
}
int func(int a)
{
    cout<<" !func(int a)的调用! "<<endl; //错误!这个函数与上面的func(int a)产生冲突
}

需要注意的是,函数的数据类型不同不能作为函数重载的条件。

2.动态多态

        由于静态多态的函数地址在编译阶段已经确定和绑定,我们在通过父类地址调用子类的函数时就会失败,只能调用父类的函数。

动态多态的条件:

···有继承关系

···子类要重写父类的虚函数

动态多态的使用:

···使用父类的指针或者引用,执行子类对象

例:猫类是动物类的一个子类,我们想让猫和动物的对象能正确执行说话speak函数

class Animal
{
public:
    void speak()
    {
        cout<<" 动物在说话 "<<endl;
    }
};
class Cat:public Animal
{
public:
    void speak()
    {
        cout<<" 猫在说话 "<<endl;
    }
};

void doSpeak(Animal &animal) //使用父类的指针或者引用,指向子类对象
{
    animal.speak();
}

int main()
{
    Cat cat;
    doSpeak(cat);  //结果为 动物在说话
    return 0;
}

        得到的结果是因为函数的地址在编译阶段早就绑定了,调用了父类speak函数造成的,解决这一问题,我们可以采用动态多态,即采用虚函数,使函数的地址晚绑定。

例:

class Animal
{
public:
    virtual void speak() //设置为虚函数
    {
        cout<<" 动物在说话 "<<endl;
    }
};
class Cat:public Animal
{
public:
    (virtual) void speak() //子类重写父类的虚函数 子类中virtual可写可不写
    {
        cout<<" 猫在说话 "<<endl;
    }
};

void doSpeak(Animal &animal) //使用父类的指针或者引用,指向子类对象
{
    animal.speak();
}

int main()
{
    Cat cat;
    doSpeak(cat);  //结果为 猫在说话
    return 0;
}

此时,我们就能输出我们想要的结果了。

3.多态的原理剖析

        如上述例子,当Animal类里有一个虚函数,那么它的内存空间就会记录一个虚函数指针(vfptr),这个指针指向的是虚函数表(Vtable),表中记录的是Animal类里的虚函数地址。

        而对于子类Cat,它会继承父类Animal的内容,但因为重写了父类虚函数,子类Cat虚函数表(Vtable)里记录的会是Cat类里的虚函数地址。

多态技术的优点:

···代码组织结构清晰

···可读性强

···利于前期和后期的扩展和维护

4.纯虚函数和抽象类

在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:

virtual 返回值类型 函数名(参数) = 0;

当类中有了纯虚函数,这个类也称为抽象类,抽象类有两个特点:

···无法实例化对象

···子类必须重写抽象类中的纯虚函数,否则也属于抽象类

例:

class Animal
{
public:
    virtual void speak()=0;
};
class Cat:public Animal
{
public:
    void speak()  //重写父类中的纯虚函数
    {
        cout<<" 猫在说话 "<<endl;
    }
};
void doSpeak(Animal &animal) //仍可以用父类的指针调用子类对象
{
    animal.speak();
}
int main()
{   
    Animal animal;//错误,无法实例化抽象类的对象
    new Animal;  //错误,无法实例化抽象类的对象
    return 0;
}

5.虚析构和纯虚析构

        多态使用时,如果子类中有属性开辟到堆区(【Day3】中提到过内存分区模型),那么父类指针在释放时无法调用到子类的析构代码。为了解决这一问题,我们可以将父类中的析构函数改为虚析构或纯虚析构。

虚析构和纯虚析构共性:

···可以解决父类指针释放子类对象

···都需要有具体的函数实现

虚析构和纯虚析构区别:

···纯虚析构所在的类为抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名()= 0;

例:

class Animal
{
public:
    Animal()  //构造函数
    {
        cout<<"调用Animal构造函数"<<endl;
    }
    virtual void speak()=0;  //纯虚函数
    virtual ~Animal()  //虚析构函数
    {
        cout<<"调用Animal析构函数"<<endl;
    }
};
class Cat:public Animal
{
public:
    Cat(string name) //构造函数
    {
        cat_name = new string(name); //动态分配内存到堆区
    }
    void speak()
    {
        cout<<*cat_name<<" 猫在说话 "<<endl;
    }
    ~Cat()  //析构函数
    {
        if(cat_name!=NULL)
        {
            cout<<"调用Cat析构函数"<<endl;
            delete cat_name;  //释放内存
            cat_name = NULL; //释放指针
        }
    }
    string *cat_name;
};
void doSpeak(Animal &animal)
{
    animal.speak();
}
int main()
{   
    Animal * animal = new Cat("Tom");  //创建Tom猫对象
    animal->speak(); 
    delete animal;
    return 0;
}

输出结果:

如此,我们就能正常执行子类析构函数。

若我们没有设置父类析构函数为虚时,就调用不了子类析构函数,

错误结果则会输出:


三、文件操作

        程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

        通过文件我们可以将数据持久化

        C++对文件进行操作需要包含头文件<fstream>

文件类型分为两种:

①文本文件:文件以文本的ASCⅡ码形式存储在计算机中

②二进制文件:文件以文本的二进制形式存储在计算机中,用户很难读懂

操作文件的三大类:

①ofstream:写操作  ②ifstream:读操作  ③fstream:读写操作

1. 写文件

写文件的步骤如下:

        1.包含头文件:  #include<fstream>

        2.创建流对象:ofstream 文件名;

        3.打开文件:文件名.open("文件路径" , 打开方式);

        4.写数据:文件名<<"写入的数据" ;

        5.关闭数据:文件名.close() ;

文件打开方式:(初学者只需掌握in、out、binary就行了)

打开方式含义
ios::in为读文件而打开文件
ios::out为写文件而打开文件
ios::ate

初始位置:文件尾

ios::app追加方式写文件
ios::trunc如果文件存在先删除,在创建
ios::binary二进制方式

文件打开方式可以利用 | 或操作符来配合使用,例如:用二进制的方式写文件:

ios::bianry | ios::out

例:

#include "fstream" //头文件

int main()
{  
    ofstream file01; //创建流对象
    file01.open("test.txt",ios::out); //未指定路径的话会默认将文件存在项目文件夹下
    file01<<" Hello World "<<endl;
    file01<<" My name is qhl "<<endl;  //写文件
    file01.close(); //关闭文件
    return 0;
}

2.读文件

读文件的步骤如下:

        1.包含头文件:  #include<fstream>

        2.创建流对象:ifstream 文件名;

        3.打开文件并判断是否成功:文件名.open("文件路径" , 打开方式);

        4.读数据:有四种方式进行读取

        5.关闭数据:文件名.close() ;

例: 

#include "fstream" //头文件


int main()
{   
   ifstream file01; //创建流对象
   file01.open("test.txt",ios::in | ios::out);
   if( !file01.is_open() )  //判断文件是否打开
   {
       cout<<"文件打开失败"<<endl;
   }
   //第一种读取方法
   char ch[1024] = { 0 };
   while ( file01>>ch ) //读取文件存入数组
   {
        cout<<ch<<endl;
   }
   第二种读取方法
   char ch[1024] = {0} ; 
   while (file01.getline(ch,sizeof(ch)));
   {
        cout<<ch<<endl;
   }
   //第三种读取方法
   string ch;
   while( getline(file01,ch) )
   {
    cout<<ch<<endl;
   }
  //第四种读取方法
    char ch;
  while ( ( ch = file01.get( ) ) != EOF ) //EOF代表文件尾部位置
  {
    cout<<ch;
  }
    file01.close();
    return 0;
}

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值