C++核心编程(黑马程序员)

C++核心编程:面向对象

1 内存分区模型

C++在执行时,将内存划分为四个区域

  • 代码区:存放函数体二进制代码,由操作系统管理
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放,存放函数的参数值、局部变量
  • 堆区:由程序员分配和释放,若程序员不释放则操作系统回收

意义:不同区域存放的数据,赋予不同的生命周期,更大的灵活编程

1.1 程序运行前

在程序编译前,生成了exe可执行程序,未执行程序前分为两个区域
代码区
存放CPU执行的机器指令
代码区是共享的,对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,防止程序意外的修改了他的指令
全局区
全局变量和静态变量存放在此
全局区包含:常量区、字符串常量和其他常量
该区域在程序结束后由操作系统释放
注:const修饰的变量,若为全局变量则放在全局区,若为局部变量,则放在栈区

int g_a = 10; // 全局变量
int g_b = 20;
const int c_a = 12; // const修饰的全局常量
int main()
{
    // 创建普通局部变量
    int a = 10;
    int b = 10;
    // 静态变量
    static int s_a = 12;
    static int s_b = 10;
    // 常量
    string str = "abs"; // 字符串常量
    const int c_b = 12; // const修饰的局部常量

    cout << "局部变量a\t" << (long long)&a << endl; // 存放在栈区
    cout << "局部变量b\t" << (long long)&b << endl; // 存放在栈区
    cout << "全局变量g_a\t" << (long long)&g_a << endl;
    cout << "全局变量g_b\t" << (long long)&g_b << endl; // 全局变量和局部变量的地址段相隔较远,不在一个段
    cout << "静态变量s_a\t" << (long long)&s_a << endl;
    cout << "静态变量s_b\t" << (long long)&s_b << endl; // 静态变量和全局变量在一个段
    cout << "静态变量c_a\t" << (long long)&c_a << endl; // 存放在全局区
    cout << "静态变量c_b\t" << (long long)&c_b << endl; // 存放在栈区
    return 0;
}

1.2 程序运行后

栈区:注意:不要返回局部变量的地址,因为栈区由编译器管理

void func()
{
    int a = 10; // 局部变量,存放在栈区
    return &a;  // 返回局部变量的地址
}
int main()
{

    int *p = func(); // 不能运行,会报错
    cout << *p << endl;
    return 0;
}

堆区:由程序员分配释放,在c++中主要用new在堆区开辟内存

int *func()
{                         // 指针是一个局部变量,放在栈上,存放了一个地址编号(堆区的编号)
    int *p = new int(10); // 利用new关键字,将数据开辟到堆区
    return p;
}
int main()
{
    int *p = func(); //释放的是存放这个指针的内存地址,返回的是堆区的地址
    cout << *p << endl;
    return 0;
}

1.3 new操作符

new开辟
delete释放

int *func()
{                         // 指针是一个局部变量,放在栈上,存放了一个地址编号(堆区的编号)
    int *p = new int(10); // 利用new关键字,将数据开辟到堆区
    return p;
}
void test()
{
    // 在堆区开辟一个数组
    int *arr = new int[10]; // 中括号:十个元素
    for (int i = 0; i < 10; i++)
    {
        arr[i] = i + 100;
    }
    delete[] arr; // 释放数组需要加一个[],若不加只释放一个
}
int main()
{
    int *p = func();
    cout << *p << endl;
    // 释放堆区数据
    delete p;
    cout << *p << endl; // 内存已经被释放,再次访问是非法操作
    return 0;
}

2 引用

注意:&既是一个取地址符号也是一个引用符号,简单区别:等号左边(左值)是引用,等号右边(右值)是取地址

2.1 引用的基本

本质:给变量起别名
语法:数据类型 &别名=原名

int a=10;
int &b=a;
b=20;
cout<<a<<endl; //20

2.2 引用的注意事项

  • 引用必须初始化。不可以写int &b,没有初始化
  • 初始化后不可以改变
int main()
{
    int a = 10;
    int &b = a;
    int c = 25;
    b = c;             // 这是一个赋值操作,不是更改引用
    cout << a << endl; // 25
    return 0;
}

2.3 引用做函数参数

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

void swap03(int &a, int &b) // 引用传递,用别名操作原名
{
    int tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int a = 10;
    int b = 20;
    swap03(a, b);
    cout << "a\t" << a << endl;
    cout << "b\t" << b << endl;
    return 0;
}

2.4 引用做函数返回值

作用:引用可以作为函数的返回值存在
注意事项:如果这个函数的返回值是一个引用,则这个函数可以作为左值

// 1.不要返回局部变量的引用
// int &test01()
// {
//     int a = 10; // 局部变量,存放在栈区
//     return a;   // 希望返回的是一个a的别名,&在函数名定义
// }
// 2.函数的调用可以作为左值
int &test02()
{
    static int a = 10; // 静态变量,存放在全局区,在程序解释后由系统释放
    return a;          // 希望返回的是一个a的别名,&在函数名定义
}
int main()
{
    // int &ref = test01(); 非法操作,不能执行
    int &ref2 = test02();
    cout << "ref\t" << ref2 << endl;
    test02() = 1000; // 如果这个函数的返回值是一个引用,则这个函数可以作为左值
    cout << "ref\t" << ref2 << endl;
    return 0;
}

2.5 引用的本质

本质:引用的本质在c++内部就是一个指针常量

2.6 常量引用

作用:常量引用主要用于修饰形参,防止误操作,防止形参改变实参

void showValue(const int &val)
{
    // val = 1000; // 会改变外部的a的值,若不想被修改,在上面加入const
    cout << "val\t" << val << endl;
}

int main()
{
    // int a = 10;
    // int &ref = a; // 必须引用一块合法的内存
    // int & ref=10; //不合法,会报错
    const int &ref = 10; // 合法,等同于:int tmp=10; const int & ref =tmp;但是没有原名
    // 加入const之后,变为只读,不可以修改,ref=20;不合法

    int b = 100;
    showValue(b);

    cout << "ref\t" << ref << endl;
    return 0;
}

3 函数提高

3.1 函数默认参数

函数的形参可以是有默认值的
语法: 返回值类型 函数名 (参数=默认值){}
规则:
1.如果某个位置参数有默认值,那么从这个位置往后,必须都要有默认值
2.如果函数声明有默认值,那么函数实现的时候就不能有默认参数

int func(int a = 10, int b = 10, int c = 10); // 声明

int main()
{
    cout << func(10, 20, 30) << endl;
    return 0;
}
int func(int a, int b, int c) // 实现,此时不能再写默认值
{
    return a + b + c;
}

3.2 函数占位参数

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

void func(int a, int) // 第二个int为占位参数
{
    cout << "func" << endl;
}

int main()
{
    func(10, 10); // 必须要多输入一个10,才能调用
    return 0;
}

3.3 函数重载

3.3.1 概述

作用:函数名可以相同,提高复用性
函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者个数不同 顺序不同
    注意:函数的返回参数不可以作为重载的条件,即不可以用返回值类型不同来区别函数
void func()
{
   cout << "func" << endl;
}
void func(int a)
{
   cout << "func(int a)" << endl;
}
void func(int a, double b)
{
   cout << "func(int a double b)" << endl;
}
void func(double a, int b)
{
   cout << "func(double a int b)" << endl;
}
int main()
{
   int a = 10;
   double b = 10;
   func();
   func(10);
   func(a, b);
   func(b, a);
   return 0;
}
//func
//func(int a)
//func(int a double b)
//func(double a int b)

3.3.2 注意事项

  • 引用作为函数重载的条件
  • 函数重载碰到默认参数
void func(int &a) // 引用
{
   cout << "func(int &a)" << endl;
}
void func(const int &a)
{
   cout << "func(const int &a)" << endl;
}
void func2(int a)
{
   cout << "func2(int a)" << endl;
}
// void func2(int a, int b = 10) // 重载失败,编译器不能识别,二义性
// {
//     cout << "func2(int a,int b=10)" << endl;
// }
int main()
{
   int a = 10;
   const int b = 10;
   func(a);  // 无const
   func(b);  // const
   func(10); // const
   func2(10);
   return 0;
}

4 类和对象

c++面向对象的三大特性:封装、继承、多态
c++认为万物皆为对象

4.1 封装

4.1.1 封装的意义

(1)将属性和行为作为一个整体,来表现生活中的事物,
属性和行为统一称为成员,

const double PI = 3.14; // 圆周率
class Circle
{
    // 访问权限:公共权限
public:
    // 属性
    int m_r;
    // 行为
    double calculateZC()
    {
        return 2 * PI * m_r;
    }
};
int main()
{
    Circle cl;   // 创建类
    cl.m_r = 10; // 赋值属性
    cout << "周长\t" << cl.calculateZC() << endl;

    return 0;
}

(2)将属性和行为加以权限控制

  • public:公共,类内类外可以访问
  • protected:保护,类内可以访问,类外不可以访问,在继承关系中,儿子可以访问父亲中的保护内容
  • private:私有,类内可以访问,类外不可以访问,儿子不可以访问父亲的私有内容
class Person
{
public: // 公共权限
   string m_name;

protected: // 保护权限
   string m_car;

private: // 私有权限
   int m_password;

public:
   void func()
   {
       m_name = "zhangsan";
       m_car = "拖拉机";
       m_password = 123456;
   }
};
int main()
{
   Person P1; // 创建类
   P1.m_name = "wangwu";
   // P1.m_car="奔驰"; //会报错,不能访问
   // P1.m_password = 23456; // 报错,不能访问
   P1.func();
   return 0;
}

4.1.2 struct 和 class的区别

struct 默认权限为公共
class 默认权限为私有

class Person
{
    string m_name; // 默认权限,private
};
struct Student
{
    int c; // 默认权限,public
};

4.1.3 成员属性设置为私有

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

class Person
{
public:
    // 姓名可读可写
    void setName(string name)
    {
        m_name = name;
    }
    // 获取姓名
    string getName()
    {
        return m_name;
    }
    // 年龄只读
    int getAge()
    {
        m_Age = 0; // 初始化
        return m_Age;
    }
    // 情人只写
    string setLover(string lover)
    {
        m_lover = lover;
    }

private:
    string m_name;
    int m_Age;
    string m_lover;
};

int main()
{
    Person P1; // 创建类
    P1.setName("张三");
    P1.setLover("美女");
    cout << "name\t" << P1.getName() << endl;
    cout << "age\t" << P1.getAge() << endl;
    return 0;
}

4.1.4 案例:点和圆的关系判断

1.代码示例

class Point
{
public:
    void setX(int X)
    {
        m_X = X;
    }
    void setY(int Y)
    {
        m_X = Y;
    }
    int getX()
    {
        return m_X;
    }
    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 setCenter(Point center)
    {
        m_Center = center;
    }
    Point getCenter()
    {
        return m_Center;
    }

private:
    int m_R;
    Point m_Center;
};

// 判断关系
void isInCircle(Circle &c, Point &p)
{
    int dist = (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
    int rDist = c.getR() * c.getR();
    if (dist == rDist)
    {
        cout << "圆上" << endl;
    }
    else if (dist < rDist)
    {
        cout << "圆内" << endl;
    }
    else
    {
        cout << "圆外" << endl;
    }
}

int main()
{
    Circle c;
    c.setR(10);
    Point center;
    center.setX(10);
    center.setY(0);
    c.setCenter(center);
    Point p;
    p.setX(10);
    p.setY(1);
    isInCircle(c, p);
    return 0;
}

2.分文件放置
主函数main.cpp

#include <iostream>
#include <string>
#include "point.h"
#include "point.cpp"
#include "circle.h"
#include "circle.cpp"
using namespace std;
const double PI = 3.14; // 圆周率

// 判断关系
void isInCircle(Circle &c, Point &p)
{
    int dist = (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) + (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());
    int rDist = c.getR() * c.getR();
    if (dist == rDist)
    {
        cout << "圆上" << endl;
    }
    else if (dist < rDist)
    {
        cout << "圆内" << endl;
    }
    else
    {
        cout << "圆外" << endl;
    }
}

int main()
{
    Circle c;
    c.setR(10);
    Point center;
    center.setX(10);
    center.setY(0);
    c.setCenter(center);
    Point p;
    p.setX(10);
    p.setY(1);
    isInCircle(c, p);
    return 0;
}

point.h:头文件中仅保留声明

#pragma once // 防止头文件重复包含
#include <iostream>
using namespace std;
class Point // 头文件中仅保留声明
{
public:
    void setX(int X);
    void setY(int Y);
    int getX();
    int getY();

private:
    int m_X;
    int m_Y;
};

point.cpp:源文件中存放实现方式

#include "point.h"
void Point::setX(int X) // 限定作用域
{
    m_X = X;
}
void Point::setY(int Y)
{
    m_X = Y;
}
int Point::getX()
{
    return m_X;
}
int Point::getY()
{
    return m_Y;
}

circle.h

#pragma once // 防止头文件重复包含
#include <iostream>
using namespace std;
#include "point.h" //包含使用的类
class Circle
{
public:
    void setR(int r);
    int getR();
    void setCenter(Point center);
    Point getCenter();

private:
    int m_R;
    Point m_Center;
};

circle.cpp

#include "circle.h"
void Circle::setR(int r)
{
    m_R = r;
}
int Circle::getR()
{
    return m_R;
}
void Circle::setCenter(Point center)
{
    m_Center = center;
}
Point Circle::getCenter()
{
    return m_Center;
}

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数

对象的初始化和清理是2个非常重要的安全问题。
一个对象没有初始状态,使用后果也是未知的
若使用完成没有清晰,也会造成安全问题
构造函数和析构函数:若我们不提供,编译器会提供,但是是空实现。

  • 构造函数:创建对象时未对象的成员属性赋值,自动调用,无需手动调用
  • 析构函数:销毁前的清理工作

** 构造函数语法**:类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.自动调用,且只调用一次

析构函数语法 ~类名(){}
1.没有返回值,也不写void
2.名称为~类名
3.不可以有参数,因此不可以发生重载
4.程序在对象销毁前自动调用,且只调用一次

4.2.2 构造函数的分类与调用

两种分类方式:
按照参数:有参构造和无参构造
按照类型:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法

class Person
{
public:      // 限定作用域,如果想在外部调用,需要public
    Person() // 无参构造(默认构造)
    {
        cout << "调用无参构造函数" << endl;
    }
    Person(int a) // 有参构造
    {
        cout << "调用有参构造函数" << endl;
    }
    Person(const Person &p) // 拷贝构造,把别人的属性拷贝过来
    {
        age = p.age;
        cout << "拷贝构造" << endl;
    }
    ~Person()
    {
        cout << "调用析构函数" << endl;
    }
    int age;
};
// 调用
void test01()
{
    // 1.括号法
    Person p1;     // 无参构造,此时不要写小括号,若有小括号会被编译器认为是函数声明
    Person p2(10); // 有参构造
    Person p3(p2); // 拷贝构造
    // 2.显示法
    Person p4 = Person(10); // 有偿构造
    Person p5 = Person(p4); // 拷贝构造
    Person p6(10);          // 匿名对象,创建之后立刻构造析构,且不能用拷贝构造初始化匿名对象
    // Person(p6); 编译器会认为是 Person p6;相当于定义了两次p6,报错
    //  3.隐式转换法
    Person p7 = 10; // 相当于 Person p7=Person(10);
    Person p8 = p7; // 拷贝构造
}
int main()
{
    test01(); // 栈上的数据,执行完会释放对象
    // system("pause"); 若pause要等待全部执行完毕之后再释放
    return 0;
}

4.2.3 拷贝构造函数调用时机

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递方式给函数参数传值
  • 以值的方式返回局部对象
    示例:创建的类同上
void test01()
{
    Person p1(20);
    Person p2(p1); // 使用一个已经创建完毕的对象来初始化一个新对象
}
void doWork(Person p) // 值传递形式调用构造函数
{
}
Person doWork2()
{
    Person p1(10);
    return p1; // 返回的其实不是p1,是拷贝的新对象
}
void test03()
{
    Person p = doWork2();
}
int main()
{
    // Person p;
    // doWork(p); // 值传递形式调用构造函数
    // test01();
    test03();
    return 0;
}

4.2.4 构造函数的调用规则

默认情况下,c++编译器至少给一个类添加三个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝函数,对属性进行值拷贝

构造函数调用规则:

  • 如果用户定义有参构造,c++不提供默认无参构造,但提供默认拷贝构造
  • 如果用户定义拷贝构造,c++不会再提供其他构造函数

4.2.5 深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝,会带来堆区内存重复释放
深拷贝:在堆区重新申请空间,进行操作

class Person
{
public:
    Person()
    {
        cout << "默认构造" << endl;
    }
    Person(int a, int h)
    {
        age = a;
        height = new int(h); // 如果属性有在堆区开辟的,要自己提供拷贝函数,防止浅拷贝带来问题
        cout << "有参构造" << endl;
    }
    Person(const Person &p) // 解决浅拷贝带来的问题
    {
        cout << "拷贝构造" << endl;
        age = p.age;
        height = new int(*p.height); // 深拷贝
    }
    ~Person()
    {
        // 将堆区开辟的数据做释放操作
        if (height != NULL)
        {
            delete height;
            height = NULL;
        }
        cout << "析构函数" << endl;
    }
    int age;
    int *height;
};
// 调用
void test01()
{
    Person p1(20, 18);
    cout << "p1.age" << p1.age << endl;
    Person p2(p1);
    cout << "p2.age" << p2.age << endl;
}
int main()
{
    test01();

    return 0;
}

4.2.6 初始化列表

为类中的属性初始化
语法:构造函数():属性(值1),属性2(值2)…

class Person
{
public:
    int m_A;
    int m_B;
    int m_C;

    // Person(int a, int b, int c) //传统初始化操作
    // {
    //     m_A = a;
    //     m_B = b;
    //     m_C = c;
    //     cout << "默认构造" << endl;
    // }
    Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) // 初始化列表语法
    {
    }
    ~Person()
    {
        cout << "析构函数" << endl;
    }
};
// 调用
int main()
{
    Person p(10, 20, 30);
    return 0;
}

4.2.7 类对象作为类成员

C++类中的成员,可以是另一个类的对象

class Phone
{
public:
    string m_PName;
    Phone(string pName)
    {
        m_PName = pName;
    }
    ~Phone()
    {
        cout << "phone 析构" << endl;
    }
};

class Person
{
public:
    string m_Name;
    Phone m_Phone;
    Person(string name, string pName) : m_Name(name), m_Phone(pName) // 相当于隐式转换法:Phone m_Phone=pName
    {
    }
    ~Person()
    {
        cout << "Person析构函数" << endl;
    }
};
// 调用
int main()
{
    // 此时先构建Phone,再构建Person类。即,先构建类对象,再构造本身
    // 析构时,先析构自身(Person),再析构类对象(Phone)
    Person p("zhangsan", "iphone");
    cout << p.m_Name << "  " << p.m_Phone.m_PName << endl;
    return 0;
}

4.2.8 静态成员

静态成员就是加上关键字static
静态成员变量:

  • 所有成员共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
class Person
{
public:
    static int m_A;

private:
    static int m_B; // 私有权限外部无法访问
};
// 调用
int Person::m_A = 100; // 类外初始化
int Person::m_B = 200;
void test1() // 公用同一份内存
{
    Person p;
    cout << p.m_A << endl;
    Person p2;
    p.m_A = 200;
    cout << p.m_A << endl;
}
void test2() // 静态成员的两种访问方式
{
    // 1.通过对象进行访问
    Person p;
    cout << p.m_A << endl;
    // 2.通过类名进行访问
    cout << Person::m_A << endl;
}
int main()
{
    test2();
    return 0;
}

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
class Person
{
public:
   static void func() // 静态成员函数,有访问权限,若在private下面这不能在外部调用
   {
       m_A = 100; // 静态成员函数可以访问静态成员变量
       // m_B=200;静态成员函数不可以访问非静态成员变量,无法区分这个属性属于那个对象
       cout << "static void func调用" << endl;
   }
   static int m_A;
   int m_B;
};
int Person::m_A = 0;
// 调用
void test1()
{
   // 1.通过对象访问
   Person p;
   p.func();
   // 2.通过类名访问
   Person::func();
}
int main()
{
   test1();
   return 0;
}

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

4.3.1 成员变量和成员函数分开存储

在c++中,类内成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
简而言之:只有非静态成员变量属于类的对象

class Person
{
};
class Person1
{
    int a; // 非静态成员变量,属于类对象
};
class Person2
{
    int a;
    static int b;  // 静态成员变量,不属于类对象
    void func() {} // 非静态成员函数,不属于类对象
};

// 调用
void test1()
{
    // 空对象占用内存为1,c++会给每个空对象分配一个字节空间,区分空对象占内存的位置(person内部为空)
    // 每个空对象也有独一无二的内存地址
    Person p;
    cout << "size of p=" << sizeof(p) << endl;
    Person1 p1;
    cout << "size of p1=" << sizeof(p1) << endl;
    Person2 p2;
    cout << "size of p2=" << sizeof(p2) << endl;
}

int main()
{
    test1();
    return 0;
}

4.3.2 this指针

特殊的对象指针:this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用

this指针用途:
当形参和成员变量同名时,可以用this指针来区分
在类的非静态成员函数中,返回对象本身,可以使用return *this

class Person
{
public:
    // Person(int age) //这样的age,形参和成员对象名称一样,返回的age会是一个随机值
    // {
    //     age = age;
    // }
    Person(int age) // 解决名称冲突
    {
        this->age = age; // this指针指向被调用的成员函数所属的对象(p1)
    }
    void PersonAddAge(Person &p)
    {
        this->age += p.age; // 把别人的年龄加到自身身上
    }
    Person &PersonAddAge1(Person &p)
    {
        this->age += p.age; // 把别人的年龄加到自身身上
        return *this;       // this指向p2的指针,而*this指向的就是p2这个对象的本体
    }
    Person PersonAddAge2(Person &p) // 不加引用,值传递
    {
        this->age += p.age; // 把别人的年龄加到自身身上
        return *this;       // this指向p2的指针,而*this指向的就是p2这个对象的本体
    }
    int age;
};

// 调用
void test1()
{
    // 空对象占用内存为1,c++会给每个空对象分配一个字节空间,区分空对象占内存的位置(person内部为空)
    // 每个空对象也有独一无二的内存地址
    Person p1(18);
    cout << "p1.age=" << p1.age << endl;
}
void test2()
{
    Person p1(10);
    Person p2(10);
    // p2.PersonAddAge(p1);
    // cout << "p2.age=" << p2.age << endl; //10
    // 若希望多加几次年龄,不能写p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);因为返回的是void,而不是p2
    // p2.PersonAddAge1(p1).PersonAddAge1(p1).PersonAddAge1(p1); // 链式编程思想
    // cout << "p2.age=" << p2.age << endl;// 40
    p2.PersonAddAge2(p1).PersonAddAge2(p1).PersonAddAge2(p1); // 每次返回的是一个新的对象
    cout << "p2.age=" << p2.age << endl;                      // 20
}

int main()
{
    test1();
    test2();
    return 0;
}

4.3.3 空指针访问成员函数

c++中空指针也可以调用成员函数,但是要注意有没有用到this指针
如果用到this指针需要加以判断保证代码的健壮性

class Person
{
public:
    void showClassName()
    {
        cout << "this is Person" << endl;
    }
    void showPersonAge()
    {
        if (this == NULL) // 为增强健壮性而添加
        {
            return;
        }
        cout << "age=" << m_Age << endl;
    }
    int m_Age;
};

// 调用
void test1()
{
    Person *p = NULL;
    p->showClassName();
    // p->showPersonAge(); //若不注释,会报错,因为传入的指针为空
}
int main()
{
    test1();
    return 0;
}

4.3.4 const修饰成员函数

常函数:

  • 成员函数后加const后我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
class Person
{
public:
   void showPerson()
   {
       m_A = 100;
       this->m_A = 200; // this指针的指向不可以修改,但是指向的值可以修改
   }
   void showPerson1() const // 加了const之后,常函数内不可以修改对象属性,this指向的值也不可以修改
   {
       this->m_B = 10; // 因为加了mutable
   }
   int m_A;
   mutable int m_B; // 特殊变量即使在常函数中也可以修改
};

// 调用
void test1()
{
   Person p;
   p.showPerson();
   // p->showPersonAge(); //若不注释,会报错,因为传入的指针为空
}
void test2()
{
   const Person p{}; // 常对象,不允许修改对象的属性
   // p.m_A = 10;       // 不允许修改普通变量
   p.m_B = 20;
   // p.showPerson(); // 报错,若可以调用普通的函数,可以存在对象属性被修改的情况,这与常对象的定义相悖
   p.showPerson1(); // 常对象只能调用常函数
}
int main()
{
   test1();
   test2();
   return 0;
}

4.4 友元

友元的目的是让一个函数或者类访问另一个类的私有成员
三种实现方法:

4.4.1 全局函数做友元

class Building
{
    friend void goodGay(Building *building); // 声明为友元,可以访问私有成员

public:
    Building()
    {
        m_SittingRoom = "客厅";
        m_BedRoonm = "卧室";
    }
    string m_SittingRoom; // 客厅
private:
    string m_BedRoonm; // 卧室
};

// 调用
void goodGay(Building *building)
{
    cout << "好基友正在访问:" << building->m_SittingRoom << endl;
    cout << "好基友正在访问:" << building->m_BedRoonm << endl;
}
void test1()
{
    Building building;
    goodGay(&building);
}
int main()
{
    test1();
    return 0;
}

4.4.2 类做友元 and 4.4.3 成员函数做友元

——区别只在于定义友元的是类还是类的成员函数
1.如何类作为友元
2.类外定义成员函数
3.在一个类内访问另一个类,用指针指向对象类

class Building; // 预先声明
class GoodGay
{
public:
    GoodGay();
    void visit(); // 参观函数,访问building中的属性
    Building *building;
};
class Building
{
    friend class GoodGay; // 类作为友元,声明为友元,可以访问私有成员
  //  friend void GoodGay::visit(); //成员函数作为友元, 声明为友元,可以访问私有成员
public:
    Building();
    string m_SittingRoom; // 客厅
private:
    string m_BedRoonm; // 卧室
};
// 类外写成员函数
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoonm = "卧室";
}
GoodGay::GoodGay()
{
    building = new Building; //新建一个对象,放在堆区,如果不用new,在GoodGay gg;之后,building将被释放,在调用就会出现访问的空指针
}

// 调用
void GoodGay::visit()
{
    cout << "好基友正在访问:" << building->m_SittingRoom << endl;
    cout << "好基友正在访问:" << building->m_BedRoonm << endl;
}
void test1()
{
    GoodGay gg;
    gg.visit();
}
int main()
{
    test1();
    return 0;
}

4.5 运算符重载

对已有的运算符重新定义,赋予其另一种功能,适应不同的数据类型
对于内置的数据类型,编译器知道如何运算,这样的运算符是不可以改变的

int a=10;
int b=20;
int c=a+b;

4.5.1加号运算符重载

class Person
{
public:
    int m_A;
    int m_B;
    // Person operator+(Person &p) // 通过成员函数重载加号
    // {
    //     Person tmp;
    //     tmp.m_A = this->m_A + p.m_A;
    //     tmp.m_B = this->m_B + p.m_B;
    //     return tmp;
    // }
};
Person operator+(Person &p1, Person &p2) // 如果不加引用,则是值传递,不能改变实参
{
    Person tmp;
    tmp.m_A = p1.m_A + p2.m_A;
    tmp.m_B = p1.m_B + p2.m_B;
    return tmp;
}
void test1()
{
    Person p1;
    p1.m_A = 10;
    p1.m_B = 10;
    Person p2;
    p2.m_A = 20;
    p2.m_B = 20;
    // Person p3 = p1.operator+(p2); // 成员函数重载,可以简化为下一行
    Person p5 = operator+(p1, p2); // 全局函数重载
    Person p4 = p1 + p2;
    cout << p4.m_A << endl;
}
int main()
{
    test1();
    return 0;
}

4.5.2 左移运算符重载

作用:输出自定义的类型
Person p;
cout<<p<<endl; 希望实现这样的功能

class Person
{
    friend ostream &operator<<(ostream &cout, Person &p);

public:
    int m_A;
    int m_B;
    // 通常不会利用成员函数重载运算符,无法实现cout在左侧
    // Person operator<<(){}
private:
    int p_A; // 默认初始化值为0,实际上需要写一个构造函数来为其赋值
};
// 只能利用全局函数来重载,本质是operator<<(cout,p)简化cout<<p
ostream &operator<<(ostream &cout, Person &p) // cout 是ostream 类
{
    cout << "m_A" << p.m_A << "\tm_B" << p.m_B;
    cout << "\tp_A" << p.p_A; // 若还希望访问私有部分,则需要将该函数作为友元
    return cout;              // 满足链式编程法则,这样在外部还可以继续输出,并且,这里的cout只是一个别名,还可以换成别的名字不影响,本质都是cout
}
void test1()
{
    Person p1;
    p1.m_A = 10;
    p1.m_B = 10;
    cout << p1 << endl;
}
int main()
{
    test1();
    return 0;
}

4.5.3 递增运算符重载

作用:实现自己的整型数据
前置递增返回引用,后置递增返回值

class MyInteger
{
    friend ostream &operator<<(ostream &cout, MyInteger myint);

public:
    MyInteger()
    {
        m_Num = 0;
    }
    // 重载前置++运算符
    MyInteger &operator++() // 引用的目的是返回自己的地址,一直对同一个数据进行处理,若不加引用会返回一个新的值
    {
        m_Num++;
        return *this; // 返回对象自身
    }
    // 重载后置++运算符
    MyInteger operator++(int) // int代表占位参数,可以用于区分前置和后置递增
    {
        // 先记录当时的结果
        MyInteger tmp = *this;
        // 递增
        m_Num++;
        // 最后返回记录的结果
        return tmp; // 后置递增返回的是值,因为如果返回对象的话,他永远都不能+1
    }

private:
    int m_Num;
};
// 只能利用全局函数来重载,本质是operator<<(cout,p)简化cout<<p
ostream &operator<<(ostream &cout, MyInteger myint)
{
    cout << myint.m_Num;
    return cout;
}
void test1()
{
    MyInteger myint;
    cout << ++(++myint) << endl;
    cout << myint << endl;
    cout << myint++ << endl;
}
int main()
{
    test1();
    return 0;
}

4.5.4 赋值运算符重载

c++编译器至少给一个类添加四个函数
1.默认构造函数
2.默认析构函数
3.默认拷贝函数,对属性进行值拷贝
4.赋值运算符 operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时会出现深浅拷贝的问题、

class Person
{
public:
    Person(int age)
    {
        m_Age = new int(age); // 创建在堆区
    }
    int *m_Age;
    ~Person() // 若不采用深拷贝,会导致堆区内存被重复释放
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
    }
    Person &operator=(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;
    }
};
void test1()
{
    Person p1(18);
    Person p2(20);
    p2 = p1; // 赋值运算
    cout << "p1.age:" << *p1.m_Age << endl;
    cout << "p2.age:" << *p2.m_Age << endl;
    Person p3(10);
    p3 = p2 = p1;
    cout << "p3.age:" << *p3.m_Age << endl;
}
int main()
{
    // 对于内置的数据类型,编译器支持连等
    int a = 10;
    int b;
    int c;
    b = c = a;
    cout << a << b << c << endl;
    test1();
    return 0;
}

4.5.5 关系运算符重载(<、>…)

作用:让两个自定义类型对象进行对比操作

class Person
{
public:
    Person(int age, string name)
    {
        m_Age = age; // 创建在堆区
        m_Name = name;
    }
    int m_Age;
    string m_Name;
    ~Person() // 若不采用深拷贝,会导致堆区内存被重复释放
    {
    }
    // 重载等号
    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 false;
        }
        return true;
    }
};
void test1()
{
    Person p1(18, "Tom");
    Person p2(18, "Tom");
    if (p1 == p2)
    {
        cout << "p1=p2" << endl;
    }
    else
    {
        cout << "p1!=p2" << endl;
    }
    if (p1 != p2)
    {
        cout << "p1!=p2" << endl;
    }
    else
    {
        cout << "p1=p2" << endl;
    }
}
int main()
{
    test1();
    return 0;
}

4.5.6 函数调用运算符重载

函数调用运算符()也可以重载
由于重载后的方法非常像函数的调用,也称为仿函数
仿函数没有固定写法,非常灵活
类名()(): 暗示仿函数

class MyPrint
{
public:
    // 重载函数调用运算符
    void operator()(string test)
    {
        cout << test << endl;
    }
};
class MyAdd
{
public:
    int operator()(int num1, int num2)
    {
        return num1 + num2;
    }
};
void test1()
{
    MyPrint myPrint;
    myPrint("hello world");
}
void test2()
{
    MyAdd myadd;
    int c = myadd(10, 20);
    cout << c << endl;
    // 匿名函数对象
    cout << MyAdd()(100, 100) << endl;
}
int main()
{
    // test1();
    test2();
    return 0;
}

4.6 继承:面向对象的三大特性之一

4.6.1 继承的基本语法

目的:减少重复代码
语法: class 子类: 继承方式 父类
子类:派生类
父类:基类

class Java
{
public:
    void header()
    {
        cout << "首页、公开课" << endl;
    }
    void footer()
    {
        cout << "help" << endl;
    }
    void left()
    {
        cout << "学科" << endl;
    }
};
// 继承实现页面
class BasePage
{
public:
    void header()
    {
        cout << "首页、公开课" << endl;
    }
    void footer()
    {
        cout << "help" << endl;
    }
    void left()
    {
        cout << "学科" << endl;
    }
};
class Python : public BasePage
{
public:
    void content()
    {
        cout << "python" << endl;
    }
};
class CPP : public BasePage
{
public:
    void content()
    {
        cout << "CPP" << endl;
    }
};
int main()
{
    Java J;
    J.header();
    J.footer();
    J.left();
    CPP C;
    C.header();
}

4.6.2 继承方式

1.private的父类权限均不可以访问
2.public保留原有属性
3.protected各类属性变为protected
4.private各类属性变为private
在这里插入图片描述

4.6.3 继承中的对象模型

从父类继承过来的成员,哪些属于子类对象中
父类中所有的非静态成员属性都会被子类继承下去
父类中的私有属性被隐藏了,但确实被继承下去了

class Base
{
public:
    int m_A;

protected:
    int m_B;

private:
    int m_C;
};
class Son : public Base
{
public:
    int m_D;
};

void test01()
{
    cout << sizeof(Son) << endl; // 16字节
}
int main()
{
    test01();
}

4.6.4 继承中的构造和析构顺序

子类继承父类后, 当创建之类对象时,也会调用父类的构造函数
先构造父亲再构造儿子,先析构儿子再析构父亲

class Base
{
public:
    int m_A;
    Base()
    {
        cout << "base的构造函数" << endl;
    }
    ~Base()
    {
        cout << "Base的析构函数" << endl;
    }

protected:
    int m_B;

private:
    int m_C;
};
class Son : public Base
{
public:
    int m_D;
    Son()
    {
        cout << "Son的构造函数" << endl;
    }
    ~Son()
    {
        cout << "Son的析构函数" << endl;
    }
};

void test01()
{
    Son s;
    cout << sizeof(Son) << endl; // 16字节
}
int main()
{
    test01();
}
#base的构造函数
#Son的构造函数
#16
#Son的析构函数
#Base的析构函数

4.6.5 继承同名成员处理方式

当子类与父类出现同名的成员
访问子类同名成员,直接访问
访问父类同名成员,需要加作用域,哪怕函数名重载也必须添加作用域

class Base
{
public:
    int m_A;
    Base()
    {
        m_A = 100;
    }
    void func()
    {
        cout << "base_func" << endl;
    }
    void func(int a)
    {
        cout << "base_func_int_a" << endl;
    }

protected:
    int m_B;

private:
    int m_C;
};
class Son : public Base
{
public:
    int m_A;
    Son()
    {
        m_A = 200;
    }
    void func()
    {
        cout << "son_func" << endl;
    }
};

void test01()
{
    Son s;
    cout << s.m_A << endl;       // 200
    cout << s.Base::m_A << endl; // 100
}
void test02()
{
    Son s;
    s.func();
    s.Base::func();
    s.Base::func(100);
}
int main()
{
    test02();
}

4.6.6 继承同名静态成员处理

静态成员与非静态成员出现同名,处理方式一致(通过对象和通过类名)

  • 访问子类同名成员,直接访问
  • 访问父类同名成员,加上作用域(会被子类隐藏)
class Base
{
public:
   static int m_A;
   static void func()
   {
       cout << "base func" << endl;
   }
};
int Base::m_A = 100;
class Son : public Base
{
public:
   static int m_A;
   static void func()
   {
       cout << "son func" << endl;
   }
};
int Son::m_A = 200;
void test01()
{
   // 1.通过对象访问
   Son s;
   cout << s.m_A << endl;       // 200,访问的是son的m_A
   cout << s.Base::m_A << endl; // 100,访问的是base的m_A
   // 2.通过类名访问
   cout << Son::m_A << endl;       // 200
   cout << Son::Base::m_A << endl; // 100
}
void test02()
{
   Son s;
   s.func();
   s.Base::func();
   Son::func();
   Son::Base::func();
}
int main()
{
   // test01();
   test02();
}

4.6.7 多继承语法

c++允许一个类继承多个类
语法: calss 子类: 继承方式 父类1, 继承方式 父类2 ...
C++实际开发中不建议用多继承

class Base1
{
public:
    Base1()
    {
        m_A = 100;
    }
    int m_A;
};
class Base2
{
public:
    Base2()
    {
        m_A = 200;
    }
    int m_A;
};
class Son : public Base1, public Base2
{
public:
    Son()
    {
        m_C = 300;
        m_D = 400;
    }
    int m_C;
    int m_D;
};

void test01()
{
    Son s;
    cout << sizeof(s) << endl;    // 16字节
    cout << s.Base1::m_A << endl; // 两个父类都有m_A,需要加作用域
    cout << s.Base2::m_A << endl;
}

int main()
{
    test01();
}

4.6.8 菱形继承

菱形继承概念:
两个派生类继承同一个基类
又有某个类同时继承了两个派生类
这种继承被称为菱形继承,或者钻石继承

存在的问题:
继承了两份数据,但其实只需要一份

class Animal
{
public:
    int m_Age;
};
class Sheep : virtual public Animal
{
};
class Tuo : virtual public Animal
{
};
class SheepTuo : public Sheep, public Tuo
{
};

void test01()
{
    SheepTuo st;
    st.Sheep::m_Age = 18;
    st.Tuo::m_Age = 28;
    // 菱形继承,两个父类拥有相同的数据需要加以作用域区分
    cout << "sheep age " << st.Sheep::m_Age << endl; // 都是28
    cout << "tuo age " << st.Tuo::m_Age << endl;
    cout << st.m_Age << endl;
    // 这份数据只要有一份就可以,菱形数据导致有两份,资源浪费
    // 利用虚继承,解决菱形继承问题 ,在继承之前加上关键字Virtual, animal就变成虚基类
}

int main()
{
    test01();
}

vbptr virtual base pointer虚基类指针

4.7 多态:三大特性之一

4.7.1 多态的概念

分类:

  • 静态多态:函数重载和运算符重载,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态
    区别:
  • 静态多态的函数地址早绑定,编译阶段确定函数地址
  • 动态多态的函数地址晚绑定,运行阶段确定函数地址
    动态多态的满足条件:
  • 1.存在继承关系
  • 2.子类重写父类中的虚函数(virtual xxx),区别于重载,重写的函数返回值、名称、参数列表完全相同
    动态多态的使用:
  • 父类的指针或引用,执行子类对象
class animal{
   public: 
   virtual void speak()
   {
      cout<<"animal speak"<<endl;
   }
};
class Cat: public animal{
   public:
   void speak()
   {
      cout<<"cat speak"<<endl;
   }
};
class Dog: public animal{
   public:
   void speak()
   {
      cout<<"dog speak"<<endl;
   }
};
void doSpeak(animal &animal) //父类的引用,指向子类 animal & animal=cat
{
   animal.speak();
}
void test01()
{
   Cat cat;
   doSpeak(cat); //现在是让动物说话,如果想要猫说话,需要在speak前面加virtual
   Dog dog;
   doSpeak(dog);//这个函数的地址不能提前确定
}
void test02()
{
   cout<<sizeof(animal)<<endl; //去掉virtual关键字,1字节,若有关键字,vs x86编译器是8字节,即一个指针
}
int main()
{
   test02();
   return 0;
}

原理:
animal 内部结果:
vfptr 虚指针——>指向vftable 虚函数表,表内部记录虚函数的地址

4.7.2 多态案例一:计算器类

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

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护
// 普通写法
class Calculator
{
public:
   int getResult(string oper)
   {
      if (oper == "+")
      {
         return m_Num1 + m_Num2;
      }
      else if (oper == "-")
      {
         return m_Num1 - m_Num2;
      }
      else if (oper == "*")
      {
         return m_Num1 * m_Num2;
      }
      return 0;
   }
   int m_Num1;
   int m_Num2;
};
void test01()
{
   Calculator c;
   c.m_Num1 = 10;
   c.m_Num2 = 20;
   cout<<"num1+num2="<<c.getResult("+")<<endl;
   cout<<"num1-num2="<<c.getResult("-")<<endl;
   cout<<"num1*num2="<<c.getResult("*")<<endl;
}
int main()
{
   test01();
   return 0;
}
// 对于普通写法,需要修改源码,而在开发中,提倡开闭原则
// 开闭原则:对扩展进行开发,对修改进行关闭
// 多态开发:1.组织结构清晰;2.可读性强;3.前期和后期扩展以及维护方便
// 首先实现一个计算器的抽象类
class AbstractCalculator
{
public:
   virtual int getResult()
   {
      return 0;
   }
   int m_Num1;
   int m_Num2;
};
class AddCalculator : public AbstractCalculator
{
public:
   int getResult()
   {

      return m_Num1 + m_Num2;
   }
};
class SubCalculator : public AbstractCalculator
{
public:
   int getResult()
   {

      return m_Num1 - m_Num2;
   }
};
class MulCalculator : public AbstractCalculator
{
public:
   int getResult()
   {

      return m_Num1 * m_Num2;
   }
};
void test01()
{
   //多态使用条件————父类指针或者引用指向子类对象
   AbstractCalculator *abc=new AddCalculator;
   abc->m_Num1 = 10;
   abc->m_Num2 = 20;
   cout << "num1+num2=" << abc->getResult() << endl;
   //用完需要销毁,销毁只是释放堆区的数据,但是指针类型没有变
   delete abc;

   //减法运算
   abc= new SubCalculator;
   abc->m_Num1 = 10;
   abc->m_Num2 = 20;
   cout << "num1-num2=" << abc->getResult() << endl;
   delete abc;

     //乘法运算
   abc= new MulCalculator;
   abc->m_Num1 = 10;
   abc->m_Num2 = 20;
   cout << "num1*num2=" << abc->getResult() << endl;
   delete abc;

}
int main()
{
   test01();
   return 0;
}

4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此,可以将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名(参数列表)=0
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数,因此,这个类也是抽象类
   virtual void func()=0;
};
class Son: public Base{
   public:
   //2.子类必须重写纯虚函数,不然也属于抽象类
   virtual void func() {
      cout<<"func run"<<endl;
   };
};

void test01()
{
   //Base B;//报错,1.不允许实例化对象
   //一般调用
   Son s;
   s.func();
   //指针调用
   Base *base = new Son;
   base->func();
   
}
int main()
{
   test01();
   return 0;
}

4.7.4 多态案例二:制作饮品

案例描述:
音频制作流程:煮水-冲泡-倒入杯中-加入辅料

利用多态实现案例,抽象类制作饮品基类,子类制作茶(茶叶+柠檬),咖啡(咖啡+糖和牛奶)

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
{
   // 煮水
   virtual void Boil()
   {
      cout << "煮水" << endl;
   }
   // 冲泡
   virtual void Brew()
   {
      cout << "冲泡咖啡" << endl;
   }
   // 导入杯中
   virtual void PourInCup()
   {
      cout << "倒入杯中" << endl;
   }
   // 加入佐料
   virtual void PutSomething()
   {
      cout << "加入糖和牛奶" << endl;
   }
};
class Tea : public AbstractDrinking
{
   // 煮水
   virtual void Boil()
   {
      cout << "煮矿泉水" << endl;
   }
   // 冲泡
   virtual void Brew()
   {
      cout << "冲泡茶叶" << endl;
   }
   // 导入杯中
   virtual void PourInCup()
   {
      cout << "倒入杯中" << endl;
   }
   // 加入佐料
   virtual void PutSomething()
   {
      cout << "加入柠檬" << endl;
   }
};
void doWork(AbstractDrinking *abc) //用指针,不用引用,引用会修改内容?
{
   abc->makeDrink();
   delete abc;
}
void test01()
{
   doWork(new Coffee);
   cout<<"---------------"<<endl;
   doWork(new Tea);
}
int main()
{
   test01();
   return 0;
}

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

区别:

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

虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名()=0
类名::~类名(){}

class animal
{
public:
   virtual void speak() = 0;
   animal()
   {
      cout << "animal 构造" << endl;
   }
   //   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); // 创建在堆区
      cout << "cat 构造" << endl;
   }
   void speak()
   {
      cout << *m_Name << "cat speak" << 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;
}
int main()
{
   test01();
   return 0;
}

4.7.6 多态案例三:电脑组装

案例描述:电脑主要组成部分为CPU(计算),显卡(显示),内存(存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产的不同零件,例如intel和Lenovo
创建电脑类让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作

class CPU
{
public:
   // 抽象计算函数
   virtual void calculator() = 0;
};
class VideoCard
{
public:
   // 抽象显示函数
   virtual void display() = 0;
};
class Memory
{
public:
   // 抽象存储函数
   virtual void storage() = 0;
};

class Computer
{
   // 构造函数中传入三个零件指针
   // 提供工作的函数{调用每个零件工作的接口}
public:
   Computer(CPU *cpu, VideoCard *vc, Memory *men)
   {
      m_cpu = cpu;
      m_vc = vc;
      m_men = men;
   }
   void work() // 工作函数
   {
      m_cpu->calculator();
      m_vc->display();
      m_men->storage();
   }
   ~Computer()
   {
      if (m_cpu != NULL)
      {
         delete m_cpu;
         m_cpu = NULL;
      }
      if (m_vc != NULL)
      {
         delete m_vc;
         m_vc = NULL;
      }
      if (m_men != NULL)
      {
         delete m_men;
         m_men = NULL;
      }
   }

private:
   CPU *m_cpu;
   VideoCard *m_vc;
   Memory *m_men;
};
// intel
class IntelCpu : public CPU
{
   virtual void calculator()
   {
      cout << "intel cpu" << endl;
   }
};
class IntelVideoCard : public VideoCard
{
public:
   // 抽象显示函数
   virtual void display()
   {
      cout << "intel videocard" << endl;
   }
};
class IntelMemory : public Memory
{
public:
   // 抽象存储函数
   virtual void storage()
   {
      cout << "intel memory" << endl;
   }
};
// lenove
class LenovoCpu : public CPU
{
   virtual void calculator()
   {
      cout << "Lenovo cpu" << endl;
   }
};
class LenovoVideoCard : public VideoCard
{
public:
   // 抽象显示函数
   virtual void display()
   {
      cout << "Lenovo videocard" << endl;
   }
};
class LenovoMemory : public Memory
{
public:
   // 抽象存储函数
   virtual void storage()
   {
      cout << "Lenovo memory" << endl;
   }
};
void test01()
{
   // 第一台电脑零件
   CPU *intelCpu = new IntelCpu;
   VideoCard *intelCard = new IntelVideoCard;
   Memory *intelMem = new IntelMemory;
   // 创建第一台电脑
   Computer *computer1 = new Computer(intelCpu, intelCard, intelMem);
   computer1->work();
   delete computer1;

   //创建第二台电脑
   Computer *computer2=new Computer(new LenovoCpu, new LenovoVideoCard,new LenovoMemory);
   computer2->work();
   delete computer2;
}
int main()
{
   test01();
   return 0;
}

5.文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
c++中对文件操作需要包含头文件< fstream >

文件类型:

  • 文本文件:文件以文本的ASCII码形式存储在计算机中
  • 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们

操作文件的三大类:

  • ofstream 写操作
  • ifstream 读操作
  • fstream 读写操作

5.1 文本文件

5.1.1 写文件

1.包含头文件
#include< fstream >
2.创建流对象
ofstream ofs;
3.打开文件
ofs.open(“文件路径”,打开方式)
4.写数据
ofs<<“写入的数据”;
5.关闭文件
ofs.close();
在这里插入图片描述
注意:文件打开方式可以配合使用,利用|操作符
例如:用二进制方式写文件ios::out|ios::binary


void test01()
{
    // 创建流对象
    ofstream ofs;
    // 指定打开方式
    ofs.open("test.txt", ios::out);
    // 写内容
    ofs << "姓名:张三" << endl; // endl表示换行
    ofs << "性别:男" << endl;
    // 关闭文件
    ofs.close();
}

int main()
{
    test01();
}

5.1.2 读文件

1.包含头文件
#include< fstream >
2.创建流对象
ifstream ifs;
3.打开文件并判断文件是否打开成功
ifs.open(“文件路径”,打开方式)
4.读数据
四种方法
5.关闭文件
ifs.close();

void test01()
{
    // 创建流对象
    ifstream ifs;
    // 指定打开方式
    ifs.open("test.txt", ios::in);
    if (!ifs.is_open())
    {
        cout << "文件打开失败" << endl;
        return;
    }
    // 读内容
    // // 第一种
    char buf[1024] = {0};
    while (ifs >> buf)
    {
        cout << buf << endl;
    }
    // // 第二种
    // char buf1[1024] = {0};
    // while (ifs.getline(buf1, sizeof(buf1)))
    // {
    //     cout << buf1 << endl;
    // }
    // // 第三种
    // string buf3;
    // while (getline(ifs, buf3))
    // {
    //     cout << buf3 << endl;
    // }
    // 第四种
    // char c;
    // while ((c = ifs.get()) != EOF) // 每次只读了一个字符
    // {
    //     cout << c;
    // }
    // 关闭文件
    ifs.close();
}

int main()
{
    test01();
}

5.2 二进制文件

以二进制的方式对文件进行读写操作
打开方式指定为ios::binary

5.2.1 写文件

二进制方式写文件主要利用流对象调用成员函数write
函数原型l:ostream& write(const char * buffer, int len)
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

class Person
{
public:
    char m_Name[64]; // 字符串尽量使用char
    int m_Age;
};
void test01()
{
    ofstream ofs;
    ofs.open("person.txt", ios::out | ios::binary);
    // 上述两步可以合并 ofstream ofs("person.txt", ios::out | ios::binary);
    Person p = {"张三", 18};
    ofs.write((const char *)&p, sizeof(p));//二进制的文件打开可能是乱码,但是只要能读取就行
    ofs.close();
}

int main()
{
    test01();
}

5.2.2 读文件

二进制方式读文件主要调用read函数
函数原型:istream& read(char *buffer,len int)

class Person
{
public:
    char m_Name[64]; // 字符串尽量使用char
    int m_Age;
};
void test01()
{
    ifstream ifs;
    ifs.open("person.txt", ios::in | ios::binary);
    // 上述两步可以合并 ofstream ofs("person.txt", ios::out | ios::binary);
    if (!ifs.is_open())
    {
        cout << "文件打开失败" << endl;
        return;
    }
    Person p;
    ifs.read((char *)&p, sizeof(p)); // 二进制的文件打开可能是乱码,但是只要能读取就行
    cout << p.m_Name << p.m_Age << endl;
    ifs.close();
}

int main()
{
    test01();
}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值