C++核心编程

文章目录

C++核心编程(面向对象)

1 内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:

    存放函数的二进制代码,由操作系统进行管理

  • 全局区

    存放全局变量和静态变量以及常量

  • 栈区

    由编译器自动分配释放,存放函数的参数值,局部变量等

  • 堆区

    由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

1.1 程序运行前

  • 代码区

    存放CPU执行的机器指令

    代码区是共享的,共享的目的是对于频繁执行的程序,只需要在内存中有一份代码即可

    代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

  • 全局区

    全局变量和静态变量存放在此。

    全局区还包含了常量区,字符串常量和其他常量

    该区域的数据在程序结束后由操作系统释放

1.2 程序运行后

  • 栈区

    由编译器自动分配释放,存放函数的参数值,局部变量等

    注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

  • 堆区

    由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

    在C++中主要利用new在堆区开辟内存

    int *p = new int(10);
    

1.3 new操作符

  • C++中利用new操作符在堆区开辟数据

  • 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete

    //释放数据
    delete p;
    //释放数组
    delete[] arr;
    
  • 语法:new 数据类型

  • 利用new创建的数据会返回该数据对应的类型的指针

2 引用

2.1 引用的基本使用

  • 作用:给变量起别名
  • 语法:数据类型 &别名 = 原名

2.2 引用注意事项

  • 引用必须初始化
  • 引用一旦初始化,就不可以更改

2.3 引用做函数参数

  • 作用:函数传参时,可以利用引用的技术让形参修饰实参
  • 优点:可以简化指针修改实参

2.4 引用做函数返回值

  • 作用:引用是可以作为函数的返回值

  • 注意:不要返回局部变量引用

  • 用法:函数调用作为左值

    int& test()
    {
        static int a =10;
        return a;
    }
    int main()
    {
        int &ref = test();
        cout<<"ref = "<<ref<<endl;
        //如果函数的返回值是引用,这个函数调用可以作为左值
        test() = 1000;
        cout<<"ref = "<<ref<<endl;
    }
    

2.5 引用的本质

  • 本质:引用的本质在C++内部实现是一个指针常量

    //发现是引用,转换为 int* const ref = &a;
    void func(int& ref){
    	ref = 100;//ref是引用,转换为*ref = 100
    }
    
  • C++推荐引用计数,因为语法方便,饮用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用

  • 作用:常量引用主要用来修饰形参

    //函数中利用常量引用防止误操作修改实参
    void showValue(const int& v){
    	//v += 10;错误
    	cout << v << endl;
    }
    
    int main()
    {
    	//int& ref = 10;引用本身需要一个合法的内存空间,因此这行错误
    	//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
        const int& ref = 10;
    }
    

3 函数

3.1 函数默认参数

  • 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须要有默认值
  • 如果函数声明有默认值,函数实现的时候就不能有默认参数(只能一个有)

3.2 函数占位参数

  • C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
  • 语法:返回值类型 函数名(数据类型){}

3.3 函数重载

3.3.1 函数重载概述
  • 作用:函数名可以相同,提高复用性
  • 函数重载满足条件:
    • 同一个作用域下
    • 函数名称相同
    • 函数参数类型不同或者个数不同或者顺序不同
  • 注意:函数的返回值不可以作为函数重载的条件

3.3.2 函数重载的注意事项

  • 引用作为重载条件

    void func(int& ref){
    	cout << ref << end1;
    }
    void func(const int& ref){
    	cout << ref << endl;
    }
    int main()
    {
    	int a = 10;
    	func(a);//调用无const
    	func(10);//调用有const
    }
    
  • 函数重载碰到函数默认参数(尽量避免)

4 类和对象

封装继承多态是面向对象的三大特性

4.1 封装

封装是C++面向对象三大特性之一

4.1.1 封装的意义
  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装意义一:

  • 在设计类的时候,成员(属性和行为)写在一起,表现事物
  • 语法:class 类名{ 访问权限: 属性 / 行为};

封装意义二:

  • 访问权限
    • 公共权限 public 成员 类内可以访问 类外可以访问
    • 保护权限 protected 成员 类内可以访问 类外不可以访问 儿子可以访问父亲中的保护内容
    • 私有权限 private 成员 类内可以访问 类外不可以访问 儿子不可以访问父亲的私有内容
4.1.2 struct和class区别

在C++中struct和class唯一的区别就在于默认的访问权限不同

  • struct 默认权限为公共
  • class 默认权限为私有
4.1.3 成员属性设置为私有
  • 优点1:将所有成员属性设置为私有,可以自己控制读写权限
  • 优点2:对于写权限,我们可以检测数据的有效性

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数
  • 对象的初始化和清理也是两个非常重要的安全问题
    • 一个对象或者变量没有初始状态,对其使用后果是未知
    • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
  • C++利用构造函数析构函数解决上述问题,这两个函数编译器会自动调用,完成对象初始化和清理工作,对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
  • 编译器提供的构造函数和析构函数是空实现
  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
    • 语法:类名(){}
      • 构造函数,没有返回值也不写void
      • 函数名称与类名相同
      • 构造函数可以有参数,因此可以发生重载
      • 程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
    • 析构函数语法: ~类名(){}
      • 析构函数,没有返回值也不写void
      • 函数名称与类名相同,在名称前加上符号~
      • 析构函数不可以有参数,因此不可以发生重载
      • 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
4.2.2 构造函数的分类及调用
  • 两种分类方式

    • 按参数分为:有参构造和无参构造(默认构造)

    • 按类型分为:普通构造和拷贝构造

      class Person{
      public:
          //无参(默认)构造函数
          Person(){}
          //有参构造函数
          Person(int a)
          {
              age = a;
          }
      	//拷贝构造
          Person(const Person& p)
          {
              age = p.age;
          }
          //析构函数
          ~Person(){}
      public:
          int age;
      };
      
  • 三种调用方式

    • 括号法

      Person p1;//默认
      Person p2(10);//有参
      Person p3(P2);//拷贝
      
    • 显示法

      Person p1;//默认
      Person p2 = Person(10);//有参
      Person p3 = Person(P2);//拷贝
      Person(10);//匿名对象,特点:当前执行结束后,系统会立即回收掉匿名对象
      
    • 隐式转换法

      Person p2 = 10;//有参, 相当于写了Person p2 = Person(10);
      Person p3 = p2;//拷贝
      
  • 注意事项

    • 调用默认构造函数,不要加()

      //编译器会认为这是一个函数的声明,不会认为在创建对象
      Person p1();
      
    • 不要利用拷贝构造函数来初始化一个匿名的对象

      //编译器会认为 Person (p3) == Person p3
      Person (p3);
      
4.2.3 拷贝构造函数调用时机
  • C++中拷贝构造函数调用时机通常有三种情况

    • 使用一个已经创建完毕的对象来初始化一个新对象

    • 值传递的方式给函数参数传值

      //拷贝一个副本
      void doWork(Person p){}
      void test()
      {
      	Person p;
      	doWork(p);//实参传进去调用
      }
      
    • 以值方式返回局部对象

      Person doWork2()
      {
      	Person p1;
      	return p1;//调用拷贝构造函数
      }
      void test02()
      {
      	Person p = doWork2();
      }
      
4.2.4 构造函数调用规则
  • 默认情况下,C++编译器至少给一个类添加3个函数
    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数,对属性进行值拷贝
  • 构造函数调用规则如下:
    • 如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
    • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
4.2.5 深拷贝与浅拷贝
  • 浅拷贝:简单的赋值拷贝操作
    • 问题:编译器提供的浅拷贝会出现重复释放堆区的问题,深拷贝解决该问题
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
    • 自己编写拷贝构造函数
  • 总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
4.2.6 初始化列表
  • 作用:C++提供了初始化列表语法,用来初始化属性

  • 语法:构造函数(): 属性1(值1), 属性2(值2) …{}

    class Person{
    public:
    	Person(int a, int b, int c) :A(a), B(b), C(c)
    	{
    
    	}
    public:
    	int A;
        int B;
        int C;
    };
    
4.2.7 类对象作为类成员
  • C++类中的成员可以是另一个类的对象,我们称该成员为对象成员

    class A{};
    class B
    {
    	A a;
    }
    
  • B类中有对象A作为成员,A为对象成员

  • 那么当创建B对象时,A与B的构造和析构的顺序谁先谁后

    • 构造时先构造类对象,再构造自身
    • 析构时先析构自身,再析构类对象
4.2.8 静态成员
  • 静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

  • 静态成员分为

    • 静态成员变量

      • 所有对象共享同一份数据

      • 在编译阶段分配内存

      • 类内声明,类外初始化

        int Person::m_A = 0;
        
    • 静态成员函数

      • 所有对象共享同一个函数
      • 静态成员函数只能访问静态成员变量
  • 静态成员不属于某个对象,所有对象都共享同一份数据因此静态成员变量有两种访问方式

    1. 通过对象访问

    2. 通过类名进行访问

      cout << Person::m_A << endl;
      
  • 静态成员也有访问权限,私有访问不到

4.3 C++对象模型和this指针

4.3.1 成员变量和成员函数分开存储
  • 在C++中类内的成员变量和成员函数分开存储
  • 只有非静态成员变量才属于类的对象上
  • 空对象占用内存空间为1个字节
4.3.2 this指针概念
  • 每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

  • 问题在于:这一块代码是如何区分哪个对象调用自己?

    • C++通过提供特殊的对象指针,this指针,解决该问题。this指针指向被调用的成员函数所属的对象
  • this指针是隐含每一个非静态成员函数内的一种指针

  • this指针不需要定义,直接使用即可

  • this指针的用途

    • 当形参和成员变量同名时,可以用this指针来区分

    • 当类的非静态成员函数中返回对象本身,可使用return *this

      class Person
      {
      public:
          Person(int age)
          {
              //解决名称冲突
              this->age = age;
          }
          
          Person& PersonAdd(Person P)
          {
              this->age += p.age;
              //返回对象本身
              return *this;
          }
          int age;
      }
      int main ()
      {
          Person p1(10);
          Person p2(5);
          //当返回对象本身时,链式编程思想
          p2.PersonAdd(p1).PersonAdd(p1).PersonAdd(p1);
          //典型链式
          cout << p2.age << endl;
      }
      
4.3.3 空指针访问成员函数
  • C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
  • 如果用到this指针,需要加以判断保证代码的健壮性
4.3.4 const修饰成员函数
  • 常函数

    • 成员函数后加const我们称这个函数为常函数

      void showPerson() const
      {
      }
      
    • 常函数内不可以修改成员属性

    • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

  • 常对象

    • 声明对象前加const称该对象为常对象
    • 常对象只能调用常函数

4.4 友元

  • 目的:让一个函数或者类 访问另一个类中私有成员
  • 关键字:friend
  • 三种实现
    • 全局函数做友元
    • 类做友元
    • 成员函数做友元
4.4.1 全局函数做友元
  • 在类中声明函数即可,加上friend关键字

    class Building
    {
    	//告诉编译器 goodGay全局函数是Person类的好朋友,可以访问类中的私有内容
    	friend void goodGay(Building &building);
    public:
        Building();
    public
        string m_SittingRoom;
    private:
    	string m_BedRoom;
    };
    
    void goodGay(Building &building)
    {
        cout << "正在访问: " << building.m_SittingRoom << endl;
        cout << "正在访问: " << building.m_BedRoom << endl;
    }
    //类外实现成员函数
    Building::Building()
    {
    	this->m_SittingRoom = "客厅";
    	this->m_BedRoom = "卧室";
    }
    
4.4.2 类做友元
  • 在要访问的类中声明类,在前面加上friend即可
4.4.3 成员函数做友元
  • 在类中声明

    class GoodGay;
    class Building
    {
    	//告诉编译器 GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员
    	friend void GoodGay::visit();
    };
    

4.5 运算符重载

  • 概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
4.5.1 加号运算符重载
  • 作用:实现两个自定义数据类型相加的运算

    class Person
    {
    public:
    	//1.成员函数重载+号
    	Person operator+(Person &p)
        {
            Person temp;
            temp.m_A = this->m_A + p.m_A;
            temp.m_B = this->m_B + p.m_B;
            return temp;
        }
        int m_A;
        int m_B;
    }
    //2.全局函数重载+号
    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;
    }
    //3.函数重载的版本
    Person operator+(Person &p1, int num)
    {
    	Person temp;
        temp.m_A = p1.m_A + num;
        temp.m_B = p1.m_B + num;
        return temp;    
    }
    void test()
    {
        Person p1;
        p1.m_A = 10;
        p1.m_B = 10;
        Person p2;
        p2.m_A = 10;
        p2.m_B = 10;
        Person p3 = p1 + p2;//实际就这么写
        //成员函数重载的本质调用
        Person p3 = p1.operator+(p2);
        //全局函数重载的本质调用
        Person p3 = operator+(p1, p2);
    }
    
4.5.2 左移运算符重载
  • 作用:可以输出自定义数据类型

    class Person
    {
        friend ostream& operator<<(ostream& out, Person& p);
    public:
    	Person(int a, int b)
        {
            m_A = a;
            m_B = b;
        }
    private:
        int m_A;
        int m_B;
    }
    //全局函数实现左移重载,不建议类内重载
    //ostream对象只能有一个
    ostream& operator<<(ostream& out, Person& p)
    {
        out << "a: " << p.m_A << " b:" << p.m_B;
        return out;
    }
    
    void test()
    {
        Person p1(10, 20);
        cout << p1 << "hello world" << endl;//链式编程
    }
    
  • 总结:重载左移运算符配合友元可以实现输出自定义数据类型

4.5.3 递增运算符重载
  • 作用:通过重载递增运算符,

    class MyInteger
    {
    	friend ostream& operator<<(ostream& out, Person& p);
    public:
    	MyInteger()
    	{
    		m_Num = 0;
    	}
        //重载前置++运算符 返回引用是为了一直对一个数据进行递增操作
        MyInteger& operator++()
        {
            //先进行++运算
            m_Num++;
            //再将自身做返回
            return *this;
        }
        //重载后置++运算符
        //int 代表占位参数, 可以用于区分前置和后置递增
        MyInteger operator++(int)
        {
            //先记录 当时结果
            MyInteger temp = *this;
            //后递增
            m_Num++;
       		//z最后将记录结果做返回
            return temp;
        }
    private:
    	int m_Num;
    };
    
    //全局函数实现左移重载,不建议类内重载
    //ostream对象只能有一个
    ostream& operator<<(ostream& out, Person& p)
    {
        out << myint.m_Num;
        return out;
    }
    
    void test01()
    {
    	MyInteger myInt;
    	cout << ++myInt << endl;
        cout << myInt << endl;
    }
    
    void test02()
    {
    	MyInteger myInt;
    	cout << myInt++ << endl;
        cout << myInt << endl;
    }
    
  • 总结:前置递增返回引用,后置递增返回值

4.5.4 赋值运算符重载
  • C++编译器至少给一个类添加4个函数

    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数,对属性进行值拷贝
    4. 赋值运算符 operator= 对属性进行值拷贝
  • 如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

    class Person{
    public:
    	Person(int age)
    	{
    		m_Age = new int(age);
    	}
    
        //重载 赋值运算符
        Person& operator=(const Person &p)
        {
            //编译器是提供浅拷贝,导致两个指针指向同一块内存,调用析构时,重复释放内存
            //m_Age = p.m_Age;
            
            //应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
            if (m_Age != NULL)
            {
                delete m_Age;
                m_Age = NULL;
            }
            //深拷贝
            m_Age = new int (*p.m_Age);
            //返回对象本身,支持链式编程
            return *this;
        }
        
        ~Person()
        {
            if(m_Age != NULL)
            {
                delete m_Age;
                m_Age = NULL;
            }
        }
        
    	int *m_Age;
    };
    
    void test01()
    {
        Person p1(18);
        Person p1(20);
        Person p1(30);
        p3 = p2 = p1;
        cout << "p1的年龄为: " << *p1.m_Age << endl;
        cout << "p2的年龄为: " << *p2.m_Age << endl;
        cout << "p3的年龄为: " << *p3.m_Age << endl;
    }
    
4.5.5 关系运算符重载
  • 重载关系运算符,可以让两个自定义类型对象进行对比操作

    class Person{
    public:
        Person(string name, int age)
        {
            m_Name = name;
            m_Age = age;
        }
        
        //重载 == 号
        bool operator==(Person &p)
        {
            if(this->m_Name == p.m_Name && this.m_Age == p.m_Age)
            {
                return true;
            }
            return false;
        }
        
        //重载 != 号
        bool operator!=(Person &p)
        {
            if(this->m_Name != p.m_Name || this.m_Age != p.m_Age)
            {
                return true;
            }
            return false;
        }
        string m_Name;
        int m_Age;
    };
    
    void test01()
    {
        Person p1("bob", 18);
        Person p2("jerry", 18);
        if (p1 == p2)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
4.5.6 函数调用运算符重载
  • 函数调用运算符()也可以重载

  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数

  • 仿函数没有固定写法,非常灵活

    class Myprint
    {
    public:
        void operator()(string text)
        {
            cout << text << endl;
        }
    };
    
    void test01()
    {
        //重载的 () 操作符 也成为仿函数
        Myprint myFunc;
        myFunc("hello,world");
    }
    
    class MyAdd
    {
    public:
    	int operator()(int v1, int v2)
        {
            return v1 + v2;
        }
    }
    
    void test02()
    {
        MyAdd add;
        int ret = add(10, 10);
        cout << "ret = " << ret << endl;
        
        //匿名对象调用,用完就销毁
        cout << MyAdd()(100, 100) << endl;
    }
    

4.6 继承

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

4.6.1 继承的基本语法
  • 继承的好处:可以减少重复的代码

  • 语法:class A:public B;

    A 类称为子类 或 派生类

    B 类称为父类 或 基类

  • 派生类中的成员,包含两大部分:

    一类是从基类继承过来的,一类是自己增加的成员。

    从基类继承过来的体现其共性,而新增的成员体现了其个性。

4.6.2 继承方式
  • 公共继承

    父类什么权限继承什么权限,父类私有不可访问。

  • 保护继承

    不管父类什么权限继承下来都是保护权限,父类私有不可访问。

  • 私有继承

    不管父类什么权限继承下来都是私有权限,父类私有不可访问。

4.6.3 继承中的对象模型
  • 问题:从父类继承过来的成员,哪些属于子类对象?

    父类中所有非静态成员属性都会被子类继承下去

    父类中私有成员属性 是被编译器给隐藏了, 因此是访问不到, 但是确实被继承下去了

4.6.4 继承中构造和析构顺序
  • 子类继承父类后,当创建子类对象时,也会调用父类的构造函数

  • 继承中的构造和析构顺序如下:

    先构造父类,再构造子类,析构的顺序与构造相反

4.6.5 继承同名成员处理方式
  • 问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

    访问子类同名成员,直接访问即可

    访问父类同名成员 需要加载作用域

  • 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员的函数,想访问加作用域即可

4.6.6 继承同名静态成员处理方式
  • 问题:继承中同名的静态成员在子类对象上如何进行访问

  • 静态成员和非静态成员出现同名,处理方式一致

    • 访问子类同名成员 直接访问即可

    • 访问父类同名成员 需要加作用域

      //同名静态成员属性
      void test01()
      {
          //1.通过对象访问
          Son s;
          cout << "son 下 m_A = " << s.m_A << endl;
          cout << "Base 下 m_A =" << s.Base::m_A << endl;
          
          //2.通过类名访问
          cout << "son 下 m_A = " << Son::m_A << endl;
          cout << "Base 下 m_A =" << Son::Base::m_A << endl;
      }
      //同名静态成员函数
      void test02()
      {
           //1.通过对象访问
          Son s;
          s.func();
          s.Base::func();
          
          //2.通过类名访问
          Son::func();
          Son::Base::func();
          
          //子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数
          //如果想访问父类中被隐藏同名成员,需要加作用域
      }
      
  • 总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

4.6.7 多继承语法
  • C++允许一个类继承多个类
  • 语法:class 子类 : 继承方式 父类1 , 继承方式 父类2…
  • 多继承可能会引发父类中有同名成员出现,需要加作用域区分
  • 实际开发并不建议用多继承
  • 注意事项
    • 当父类中出现同名成员,需要加作用域区分
4.6.8 菱形继承
  • 概念

    • 两个派生类继承同一个基类
    • 又有某个类同时继承两个派生类
    • 这种继承被称为菱形继承,或者钻石继承
  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义

    • 利用虚继承可以解决菱形继承问题(虚继承是继承指针而不是数据)

      class sheep :virtual public Animal {};
      

4.7 多态

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

4.7.1 多态的基本概念
  • 分类

    • 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
    • 动态多态:派生类和虚函数实现运行时多态
  • 静态多态和动态多态区别

    • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
    • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
    class Animal
    {
    public:
    	//speak函数就是虚函数
    	//函数前面加上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 DoSpeak(Animal &animal)
    {
        animal.speak();
    }
    
    //多态满足条件
    //1. 有继承关系
    //2. 子类重写父类中的虚函数
    //多态使用:
    //父类指针或引用指向子类对象
    
    void test01()
    {
        Cat cat;
        DoSpeak(cat);
        
        Dog dog;
        DoSpeak(dog);
    }
    
  • 总结

    • 多态满足条件
      • 有继承关系
      • 子类重写父类中的虚函数
    • 多态使用条件
      • 父类指针或引用指向子类对象
    • 重写:函数返回值类型 函数名 参数列表 完全一致称为重写
4.7.2 多态案例一-计算器类
  • 案例描述:

    分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

  • 多态的优点

    • 代码组织结构清晰

    • 可读性强

    • 利于前期和后期的扩展以及维护

      class Abstract_Cal
      {
      public:
      	virtual int getResult()
      	{
      	
      	}
          int num1;
          int num2;
      };
      
      class Add_Cal :public Abstract_Cal
      {
      public:
          int getResult()
          {
              return num1 + num2;
          }
      };
      
      class Sub_Cal :public Abstract_Cal
      {
      public:
          int getResult()
          {
              return num1 - num2;
          }
      };
      
      void test()
      {
          //多态使用条件 父类指针或引用指向子类对象
          Abstract_Cal *p = new Add_Cal;
          p->num1 = 10;
          p->num2 = 10;
          cout << p->num1 << "+" << p->num2 << " = " << p->getResult() << endl;
          delete p;//用完销毁
          
          //减法
          p = new Sub_Cal;
          p->num1 = 10;
          p->num2 = 10;
          cout << p->num1 << "-" << p->num2 << " = " << p->getResult() << endl;
          delete p;
      }
      
4.7.3 纯虚函数和抽象类
  • 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

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

    语法:virtual 返回值类型 函数名 (参数列表) = 0;

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

  • 抽象类特点

    • 无法实例化对象
    • 子类必须重写抽象类中的纯虚函数。否则也属于抽象类
4.7.2 多态案例二-制作饮品
  • 代码实现

    class AbstractDrinking
    {
    public:
    	//煮水
        virtual void Boil() = 0;
        
        //冲泡
        virtual void Brew() = 0;
        
        //倒入杯中
        virtual void PourIncup() = 0;
        
        //加入辅料
        virtual void PutSomething() = 0;
        
        //制作饮品
        void makeDrink()
        {
            Boil();
            Brew();
            PourIncup();
            PutSomething();
        }
    };
    //制作咖啡
    class Coffee :public AbstractDrinking
    {
    public:
    	//煮水
        virtual void Boil()
        {
            cout << "煮水" << endl;
        }
        
        //冲泡
        virtual void Brew()
        {
            cout << "冲泡咖啡" << endl;
        }
        
        //倒入杯中
        virtual void PourIncup()
        {
            cout << "倒入杯中" << endl;
        }
        
        //加入辅料
        virtual void PutSomething()
        {
            cout << "加入糖和牛奶" << endl;
        }
    };
    //制作茶叶
    class Tea :public AbstractDrinking
    {
    public:
    	//煮水
        virtual void Boil()
        {
            cout << "煮矿泉水" << endl;
        }
        
        //冲泡
        virtual void Brew()
        {
            cout << "冲泡茶叶" << endl;
        }
        
        //倒入杯中
        virtual void PourIncup()
        {
            cout << "倒入杯中" << endl;
        }
        
        //加入辅料
        virtual void PutSomething()
        {
            cout << "加入枸杞" << endl;
        }
    };
    //制作函数
    void doWork(AbstractDrinking *abs) //AbstractDrinking *abs = new Coffee;
    {
        abs->makeDrink();
        delete abs;//释放
    }
    
    void test01()
    {
        //制作咖啡
        doWork(new Coffee);
        
        cout << "--------------------------" << endl;
        //制作茶叶
        doWork(new Tea);
    }
    
4.7.5 虚析构和纯虚析构
  • 多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

    解决方式:将父类中的析构函数改为虚析构或者纯虚析构

  • 虚析构和纯虚析构共性:

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

    都需要具体的函数实现

  • 虚析构和纯虚析构区别:

    如果是纯虚析构,该类属于抽象类,无法实例化对象

  • 虚析构语法:

    virtual ~类名(){}

  • 纯虚析构语法:

    virtual ~类名() = 0;

    类名::~类名(){}

5 文件操作

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

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

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

文件类型分为两种:

  1. 文本文件 -文件以文本的ASCII码形式存储在计算机中
  2. 二进制文件 -文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件三大类:

  1. ofstream:写操作
  2. ifstream:读操作
  3. fstream:读写操作

5.1 文本文件

5.1.1 写文件
  • 写文件步骤:

    1. 包含头文件 #include

    2. 创建流对象

      ofstream ofs;

    3. 打开文件

      ofs.open(“文件路径”,打开方式);

    4. 写数据

      ofs << “写入的数据”;

    5. 关闭文件

      ofs.close();

  • 文件打开方式:

    打开方式解释
    ios::in为读文件而打开文件
    ios::out为写文件而打开文件
    ios::ate初始位置:文件尾
    ios::app追加方式写文件
    ios::trunc如果文件存在先删除,再创建
    ios::binary二进制方式
  • 注意:文件打开方式可以配合使用,利用|操作符

5.1.2 读文件
  • 读文件与写文件步骤相似。但是读取方式相对于比较多

  • 步骤

    1. 包含头文件

      #include

    2. 创建流对象

      ifstream ifs;

    3. 打开文件并判断文件是否打开成功

      ifs.open(“文件路径”, 打开方式);

      利用is_open函数可以判断文件是否打开成功

    4. 读数据

      四种方式读取

      //第一种方式
      char buf[1024] = {0};
      while(ifs >> buf)
      {
          cout << buf <<endl;
      }
      //第二种
      char buf[1024] = {0};
      while(ifs.getline(buf, sizeof(buf)))
      {
          cout << buf <<endl;
      }
      
      //第三种
      string buf;
      while (getline(ifs, buf))
      {
          cout << buf << endl;
      }
      
      char c;
      while((c = ifs.get()) != EOF)
      {
          cout << c;
      }
      
    5. 关闭文件

      ifs.close();

5.2 二进制文件

  • 以二进制的方式对文件进行读写操作

  • 打开方式要指定为 ios::binary

5.2.1 写文件
  • 二进制方式写文件主要利用流对象调用成员函数write

  • 函数原型: ostream& write(const char *buffer, int len);

    参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

    #include <fstream>
    #include <string>
    classs Person
    {
    public:
    	char m_Name[64];
    	int m_Age;
    };
    
    //二进制文件 写文件
    void test01()
    {
    	//1.包含头文件
    	
    	//2.创建输出流对象
    	ofstream ofs("person.txt", ios::out | ios::binary);//构造函数
    	
    	//3.打开文件
    	//ofs.open("person.txt", ios::out | ios::binary);
    	
    	Person p = {"张三", 18};
    	
    	//4.写文件
    	ofs.write((const char *)&p, sizeof(p));
        
        //5.关闭文件
        ofs.close();
    }
    
5.2.2 读文件
  • 二进制方式读文件主要利用流对象调用成员函数read

  • 函数原型:istream& read(char *buffer, int len);

    参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

    #include <fstream>
    #include <string>
    classs Person
    {
    public:
    	char m_Name[64];
    	int m_Age;
    };
    
    //二进制文件 读文件
    void test01()
    {
    	ifstream ifs("person.txt", ios::in | ios::binary);
        if (!ifs.is_open())
        {
            cout << "文件打开失败" <<endl;
        }
        
        Person p;
        ifs.read((char *)&p, sizeof(p));
        
        cout << "姓名:" << p.m_Name << " 年龄:" << p.m_Age << endl;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值