C++核心编程

本文章是本人黑马程序员 C++| 匠心之作 从0到1入门学编程的学习笔记


前置文章:


C++核心编程

本阶段主要针对C++面向对象编程技术做详细讲解,探讨C++中的核心和精髓。

1 内存分区模型

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

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

四区意义:

不同区域存放的数据,赋予不同的生命周期给我们更大的灵活编程

1.1 程序运行前

在程序编译后,生成了(exe)可执行程序,未执行该程序前分为两个区域

代码区:

  • 存放CPU执行的机器指令

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

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

全局区:

  • 全局变量静态变量存放在此

  • 全局区还包含了常量区,字符串常量和其他常量也存放在此

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

#include <iostream>
using namespace std;

//全局变量
int g_a = 10;
int g_b = 20;

//const修饰的全局变量
const int c_g_a = 15;
const int c_g_b = 25;

int main()
{
  //全局变量
  cout << "全局变量 g_a 的地址为:\t" << &g_a << endl;
  cout << "全局变量 g_b 的地址为:\t" << &g_b << endl << endl;
  
  //局部变量
  int a = 100;
  int b = 200;
  cout << "局部变量 a 的地址为:\t" << &a << endl;
  cout << "局部变量 b 的地址为:\t" << &b << endl << endl;
  
  //静态变量
  static int s_a = 1000;
  static int s_b = 2000;  
  cout << "静态变量 s_a 的地址为:\t" << &s_a << endl;
  cout << "静态变量 s_b 的地址为:\t" << &s_b << endl << endl;
  
  //常量
    //字符创常量
  cout << "字符串常量的地址为:\t" << &"Hello world!" << endl;
    //const修饰的变量
        //const修饰的全局变量
  cout << "全局常量c_g_a的地址为:\t" << &c_g_a << endl;
  cout << "全局常量c_g_b的地址为:\t" << &c_g_b << endl;
        //const修饰的局部变量
  const int c_a = 10000;
  const int c_b = 20000;
  cout << "局部常量c_a的地址为:\t" << &c_a << endl;
  cout << "局部常量c_b的地址为:\t" << &c_b << endl;
  
  system("pause");
  
  return 0; 
}
程序的内存模型-内存四区-全局区

总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放const修饰的全局常量和字符串常量

1.2 程序运行后

栈区:

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

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

#include <iostream>
using namespace std;

int * func(int b)   //形参数据也被放在栈区
{
  int b = 100;
  int a = 10;   //局部变量
  //不要返回局部变量的地址
  //局部变量存放在栈区,栈区的数据在程序执行完后自动释放
  return &a;
}

int main()
{
  //接收func()函数的返回值
  int * p = func(1);
  cout << *p << endl;       //第一次可以打印正确的数字,是因为编译器做了保留
  cout << *p << endl;       //第二次这个数据就不再保留,输出乱码
  
  system("pause");
  
  return 0;
}

堆区:

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

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

#include <iostream>
using namespace std;

int * func()
{
  //在堆区开辟数据
  //new 数据类型(初始值)
        //指针本质也是局部变量
        //指针保存的数据放在堆区
  int * p = new int(10);        //利用new关键字可以将数据开辟到堆区
  return p;
}

int main()
{
  int * p = func();
  
  cout << *p << endl;
  cout << *p << endl;
  cout << *p << endl;
  cout << *p << endl;
  
  system("pause");
  
  return 0;
}

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存

1.3 new 关键字

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

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

语法:new 数据类型

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

示例1:基本语法

#include <iostream>
using namespace std;

int * func()
{
  //new返回的是该数据类型的指针
  int * p = new int(10);
  return p;
}

int main()
{
  int * p = func();
  cout << *p << endl;
  cout << *p << endl;
  
  //释放堆区的数据
  delete p;
  cout << *p << endl;       //内存已经被释放,再次访问时非法操作
  
  system("pause");
  
  return 0;
}

示例2:在堆区开辟数组

#include <iostream>
using namespace std;

int main()
{
  //创建10个整形数据的数组
  int * arr = new int[0];   //10代表数组有10个数据
  
  for(int i = 0; i < 10; I++)
  {
    arr[i] = i + 100;
  }
  
  for(int i = 0; i < 10; I++)
  {
    cout << arr[i] << endl;
  }
  
  //释放堆区数组
  //释放数组的时候,要使用[]
  delete[] arr;

  system("pause");
  
  return 0;
}

2 引用

2.1 引用的基本使用

作用:给变量起别名

语法:数据类型 &别名 = 原名;

示例

#include <iostream>
using namespace std;

int main()
{
  int a = 10;
  //创建引用
  int &b = a;
  
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  
  b = 100;
  
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  
  system("pause");
  
  return 0;
}

2.2 引用的注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以更改

示例

#include <iostream>
using namespace std;

int main()
{
  int a = 10;
  int b = 20;
  //int &c;         //错误:必须初始化
  int &c = a;       //一旦初始化后,就不可以更改
  c = b;                //这是赋值操作,不是更改引用
  
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  cout << "c = " << c << endl;
  
  system("pause");
  
  return 0;
}

2.3 引用做函数参数

作用:函数传参时;可以运用引用的技术让形参修饰实参

优点:可以简化指针修改实参

示例

#include <iostream>
using namespace std;

//值传递
void mySwap01(int a, int b)
{
  int temp = a;
  a = b;
  b = temp;
}

//地址传递
void mySwap02(int * a, int * b)
{
  int temp = *a;
  *a = *b;
  *b = temp;
}

//引用传递
void mySwap03(int &a, int &b)
{
  int temp = a;
  a = b;
  b = temp;
}

int main()
{
  int a = 10;
  int b = 20;
  
  //值传递,形参不会修饰实参
  mySwap01(a, b);
  cout << "值传递:\ta = " << a << ", b = " << b << endl;
  
  //地址传递,形参会修饰实参
  a = 10;
  b = 20;
  mySwap02(&a, &b);
  cout << "地址传递:\ta = " << a << ", b = " << b << endl;
  
  //引用传递,形参也会修饰实参
  a = 10;
  b = 20;
  mySwap03(a, b);
  cout << "引用传递:\ta = " << a << ", b = " << b << endl;
  
  system("pause");
  
  return 0;
}

总结:通过引用参数产生的效果同按地址传递的效果是一样的。引用的语法更清楚简单

2.4 引用做函数的返回值

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

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

用法:函数调用作为左值

示例

#include <iostream>
using namespace std;

//返回局部变量引用
int& test01()
{
  int a = 10;   //局部变量,存放在栈区
  return a;
}

//返回静态变量引用
int& test02()
{
  static int a = 20;    //全局区,程序结束后释放
  return a;
}

int main()
{
  //引用局部变量
  int & ref = test01();
  cout << "ref = " << ref << endl;      //编译器做一次保留
  cout << "ref = " << ref << endl;      //内存已经是释放
  
  int & ref2 = test02();
  cout << "ref2 = " << ref2 << endl;
  cout << "ref2 = " << ref2 << endl;
  
  //函数调用作为左值
  test02() = 1000;          //等同于a = 1000;
  cout << "ref2 = " << ref2 << endl;
  cout << "ref2 = " << ref2 << endl;
  
  system("pause");
  
  return 0;
}

2.5 引用的本质

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

示例

#include <iostream>
using namespace std;

//发现是引用,转换为 int * const ref = &a
void func(int & ref)
{
  ref = 100;        //ref是引用,转换为*ref = 100
}

int main()
{
  int a = 10;
  
  //自动转换为 int * const ref = &a; 指针常量是指指针指向不改,也说明为什么引用不可更改
  int & ref = a;
  ref = 20;     //内部发现ref是引用,自动帮我们转换为:*ref = 20;
  
  cout << "a =\t" << a << endl;
  cout << "ref =\t" << ref << endl;
  
  func(a);
  cout << "a =\t" << a << endl;
  cout << "ref =\t" << ref << endl;
  return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用

作用:主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参

示例

#include <iostream>
using namespace std;

//引用使用的场景,通常用来修饰形参
void showValue(const int & val)
{
  //v += 10;
  cout << "val = " << v << endl;
}

int main()
{
  //int & ref = 10;     //引用本身需要一个合法的内存空间,因此这行错误
  //加入const就可以了,编译器优化代码,int temp = 10; const int & ref = temp;
  const int & ref = 10;
  cout << "ref = " << ref << endl;
  //ref = 20;       //加入const之后变为只读,不可以修改
  
  //函数中加入常量引用防止误操作修改实参
  int a = 100;
  showValue(a);
  
  system("pause");
  
  return 0;
}

3 函数的提高

3.1 函数的默认值

在C++中,函数的形参列表中的形参是可以有默认值的

语法:返回值类型 函数名(参数 = 默认值) {函数体}

示例

#include <iostream>
using namespace std;

int func(int a, int b = 10, int c = 10)
{
  return a + b + c;
}

//1、如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//int wrongFunc(int a = 10, int b, int c, int d);

//2、如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 20);  //函数声明有默认值
int func2(int a, int b)                         //函数实现的时候就不能有默认参数
{
  return a + b;
}

int main()
{
  cout << "func(10) = \t" << func(10) << endl;
  //如果我们自己传入数据,就用自己的数据;如果没有,那么用默认值
  cout << "func(10, 20) = \t" << func(10, 20) << endl;
  
  cout << "func2() = \t" << func2() << endl;
  
  system("pause");
  
  return 0;
}

3.2 函数占位参数

C++的形参列表里可以有占位参数,用来占位、调整函数时必须填补该位置

语法:返回值类型 函数名(数据类型) {函数体}

在现价段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

示例

#include <iostream>
using namespace std;

//函数占位参数,占位参数也可以有默认参数
void func(int a, int = 10)
{
  cout << "This is func. " << endl;
}

int main()
{
  func(10, 10); //占位参数必须填补
  func(10)          //占位参数可以有默认参数,可以不用传默认参数
  
  system("pause");
  
  return 0;
}

3.3 函数重载

3.1 函数重载概述

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同,或者个数不同或者顺序不同

注意:函数的返回值不可以作为函数重载的条件

示例

#include <iostream>
using namespace std;

//函数重载需要函数都在同一个作用域下
void func()
{
  cout << "这是func()的调用。" << endl;
}

//个数不同
void func(int a)
{
  cout << "这是函数func(int a)的调用。" << endl;
}

//类型不同
void func(double a)
{
  cout << "这是函数func(double 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 func(double a, int b)
//{
//  cout << "这是函数int func(double a, int b)的调用。" << endl;
//  return a + b;
//}

int main()
{
  func();
  //个数不同
  func(10);
  //类型不同
  func(1.0);
  //顺序不同
  func(10, 1.0);
  func(1.0, 10);
  
  system("pause");

  return 0;
}

3.3.2 函数重载注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认值

示例

#include <iostream>
using namespace std;

//函数重载注意事项
//引用作为重载条件
void func(int &a)       //int &a = 10; 不合法
{
  cout << "func(int &a)" << endl;
}

void func(const int &a) //const int &a = 10; 合法
{
  cout << "func(const int &a)" << endl;
}

//函数重载碰到默认参数
void func2(int a)
{
  cout << "func(int a)" << endl;
}

void func2(int a, int b = 10)
{
  cout << "func(int a, int b = 10)" << endl;
}

int main()
{
  int a = 10;
  func(a);      //调用的是func(int &a)
  
  const int b = 10;
  func(b);      //调用的是func(const int &a)
  func(10);     //调用的是func(const int &a)
  
  //func2(10);  //当函数重载碰到默认参数,出现二义性,报错,尽量避免这种情况
  
  system("pause");
  
  return 0;
}

4 类和对象

C++面向对象的三大特性为:封装、继承、多态

C++认为万事万物都皆为对象,对象上有其属性和行为

例如:

​ 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...

​ 车也可以作为对象,属性有轮胎、方向盘车灯...,行为有载人、放音乐、放空调...

​ 具有相同性质的对象,我们可以抽象称为,人属于人类,车属于车类

4.1 封装

4.1.1 封装的意义

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

封装的意义:

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

封装意义一:

​ 在设计类的时候,属性和行为写在一起,表现事物

语法:class 类名{ 访问权限: 属性/行为 };

示例1:设计一个圆类,求圆的周长

#include <iostream>
using namespace std;

//圆周率
const double PI = 3.1415926535897932384626;

//1、封装的意义
//属性和行为作为一个整体,用来表现生活中的事物
class Circle
{
  //访问权限
public:     //公共权限
  //属性
  int m_r;  //半径
  
  //行为
  //获取圆的周长
  double calcCircum()
  {
    return 2 * PI * m_r;
  }
};

int main()
{
  //通过圆类创建具体的圆(对象)
  //实例化:通过一个类创建一个对象的过程
  Circle c1;
  //给圆对象的属性进行赋值
  c1.m_r = 10;
  cout << "圆的周长是:" << c1.calcCircum() << endl;
  
  system("pause");
  
  return 0;
}

示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

#include <iostream>
#include <string>
using namespace std;

class Student
{
public:
  //类中的属性和行为,我们统一称为成员
  //属性      成员属性、成员变量
  //行为      成员函数、成员方法
  string m_name;
  string m_ID;
  
  //给姓名辅助
  void setName(string name)
  {
    m_name = name;
  }
  
  //给学号赋值
  void setID(string ID)
  {
    m_ID = ID;
  }
  
  void showStudent()
  {
    cout << "姓名:" << m_name << "\t学号:" << m_ID << endl;
  }
};

int main()
{
  //实例化
  Student s1;
  //赋值
  s1.setName("张三");     //等同于 s1.m_name = "张三";
  s1.setID("00001");        //等同于 s1.m_ID = "00001";
  
  s1.showStudent();
  
  system("pause");
  
  return 0;
}

封装意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

  1. public 公共权限
  2. protected 保护权限
  3. private 私有权限

示例

#include <iostream>
#include <string>
using namespace std;

//三种权限
//公共权限  public      类内可以访问  类外可以访问
//保护权限  protected   类内可以访问  类外不可以访问     子类可以访问
//私有权限  private     类内可以访问  类外不可以访问     子类不可以访问

class Person
{
public:
  //公共权限
  string m_name;
protected:
  //保护权限
  string m_car;
private:
  //私有权限
  string m_password;

public:
  void func()
  {
    //类内可以访问
    m_name = "张三";
    m_car = "拖拉机";
    m_password = "1234567890"
  }
};

int main()
{
  //实例化具体对象
  Person p1;
  system("pause");
  
  p1.m_name = "李四";
  //p1.m_car = "奔驰";                //保护权限的内容,在类外访问不到
  //p1.m_password = "0000"   //私有权限的内容,在类外访问不到
  
  return 0;
}

4.1.2 struct 和 class 区别

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

区别:

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

示例

#include <iostream>
using namespace std;

class C1
{
  int m_A;      //默认是私有权限
};

struct C2
{
  int m_A           //默认是公共全权限
};

int main()
{
  C1 c1;
  //c1.m_A = 100;       //错误,class中默认权限是private,不可以访问
  
  struct C2 c2;         //正确,struct中默认权限是private,可以访问
  c2.m_A = 100;
  
  system("pause");
  
  return 0;
}

4.1.3 成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限

优点2:对于写权限,我们可以检测数据的有效性

示例

#include <iostream>
using namespace std;

class Person
{
public:
  //姓名设置可读可写
    //设置姓名
  void setName(string name)
  {
    m_Name = name;
  }
    //读取姓名
  string getName()
  {
    return m_Name;
  }
  
  //年龄设置为只读
    //获取年龄
  int getAge()
  {
    m_Age = 99;
    return m_Age;
  }
  
  //情人设置为只写
        //设置情人
  void setLover(string lover)
  {
    m_Lover = lover;
  }
  
private:
  string m_Name;
  int m_Age;
  string m_Lover;
};

int main()
{
  Person p1;
  
  p1.setName("张三");
  cout << "姓名为:" << p1.getName() << endl;
  
  //p1.m_Age = 18;      //只读权限,数据不可以更改
  cout << "年龄为:" << p1.getAge() << endl;
  
  p1.setLover("情人");
  //cout << "情人为:" << p1.m_Lover << endl;       //只写权限,数据不可以访问
  
  system("pause");
  
  return 0;
}

练习案例1:设计立长体类

设计立长体类(Cuboid)

求出立方体的面积和体积

分别用全局函数和成员函数判断两个立方体是否相等

Cube
#include <iostream>
using namespace std;

class Cuboid
{
public:
  //长
  void setL(int l) {m_L = l;}  
  int getL() {return m_L;}
  //宽
  void setW(int w) {m_W = w;}  
  int getW() {return m_W;}
  //高
  void setH(int h) {m_H = h;}  
  int getH() {return m_H;}
  
  //计算表面积
  int calcSA() {return 2 * (m_L * m_W + m_L * m_H + m_W * m_H);}
  //计算体积
  int calcVol() {return m_L * m_W * m_H;}
  
  //利用成员函数判断两个立方体是否相等
  bool isSameByClass(Cuboid &c)
  {
    return ((m_L == c.getL())&&(m_W == c.getW()) && (m_H == c.getH())) ? true : false;
  }
  
private:
  int m_L;      //长
  int m_W;      //宽
  int m_H;      //高
};

//利用全局函数判断两个立方体是否相等
bool isSame(Cuboid &c1, Cuboid &c2)
{
  return ((c1.getL() == c2.getL())&&(c1.getW() == c2.getW()) && (c1.getH() == c2.getH())) ? true : false;
}

int main()
{
  Cuboid c1;
  c1.setL(10);
  c1.setW(11);
  c1.setH(12);
  cout << "c1的面积为:" << c1.calcSA() << endl;
  cout << "c1的体积为:" << c1.calcVol() << endl;
  
  Cuboid c2;
  c2.setL(10);
  c2.setW(11);
  c2.setH(12);
  
  //判断c1、c2是否相等
    //利用全局函数判断
  string sameString = isSame(c1, c2) ? "" : "不";
  cout << "全局函数判断:c1和c2是" << sameString << "相等的" << endl;
    //利用成员函数判断
  sameString = c1.isSameByClass(c2) ? "" : "不";
  cout << "成员函数判断:c1和c2是" << sameString << "相等的" << endl;
  
  system("pause");
  
  return 0;
}

练习案例2:圆和点的关系

设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系

思路:用点到圆心的距离 d\sqrt{(x_1-x_2)^2+(y_1-y_2)^2} )和半径 m\_R 作对比

  • d > m\_R,点在圆外
  • d = m\_R,点在圆上
  • d < m\_R,点在圆内
//point.h
#pragma once        //防止头文件重复包含
#include <stdio.h>
using namespace std;

class Point
{
public:
  void setX(int x);
  int getX();
  void setY(int y);
  int getY();
private:
  int m_X;
  int m_Y;
};
//point.cpp
#include "point.h"

//需要加入 Point:: 告知是Point作用域下的成员函数
void Point::setX(int x) {m_X = x;}
int Point::getX() {return m_X;}
void Point::setY(int y) {m_Y = y;}
int Point::getY() {return m_Y;}
//circle.h
#pragma once        //防止头文件重复包含
#include <stdio.h>
#include "point.h"
using namespace std;

class Circle
{
public:
  //半径
  void setR(int r);
  int getR();
  //圆心
  void setC(Point c);
  Point getC();
private:
  int m_R;              //半径
  //在类中可以让另一个类作为本类中的成员
  Point m_Centre;   //圆心
};
//circle.cpp
#include "circle.h"

void Circle::setR(int r) {m_R = r;}
int Circle::getR() {return m_R;}
void Circle::setC(Point c) {m_Centre = c;}
Point Circle::getC() {return m_Centre;}
#include <iostream>
using namespace std;

#include "point.h"
#include "circle.h"

void calcRelation(Circle &c, Point &p)
{
  //两点间距离的平方
  int d = ((c.getC().getX() - p.getX()) * (c.getC().getX() - p.getX())) + 
    ((c.getC().getY() - p.getY()) * (c.getC().getY() - p.getY()));
  //半径的平方
  int r_d = c.getR() * c.getR();
  //判断关系
  if (d > r_d) {cout << "点在圆外" << endl;}
  else if (d == r_d) {cout << "点在圆上" << endl;}
  else {cout << "点在圆内" << endl;}
}

int main()
{
  Circle c;
  c.setR(10);
  Point centre;
  centre.setX(0);
  centre.setY(0);
  c.setC(centre);
  
  Point p;
  p.setX(0);
  p.setY(10);
  
  calcRelation(c, p);
  
  system("pause");
  
  return 0;
}

Compile: g++ main.cpp point.cpp circle.cpp -o main

4.2 对象的初始化和清理

  • 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
  • C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置

4.2.1 构造函数和析构函数

对象的初始化清理也是两个非常重要的安全问题

​ 一个对象或者变量没有初始状态,对其使用后果是未知

​ 同样的使用完一个对象或变量,没有及时清理也会造成一定的安全问题

C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

编译器提供的构造函数和析构函数是空实现。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名() {}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法:~类名() {}

  1. 析构函数,没有返回值也不写``void`
  2. 函数名称与类名相同在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include <iostream>
using namespace std;

//函数的初始化和清理
class Person
{
public:
  //1、构造函数,进行初始化操作
  Person()
  {
    cout << "Person构造函数的调用" << endl;
  }
  
  //2、析构函数,进行清理操作
  ~Person()
  {
    cout << "Person析构函数的调用" << endl;
  }
};

//构造和析构都是必须有的实现,如果我们自己不操作,编译器会提供一个空实现的构造和析构
void test01()
{
  Person p;     //在栈区的数据,test01()执行完毕后,自动释放这个对象
}

int main()
{
  test01();
  
  system("pause");
  
  return 0;
}

4.2.2 构造函数的分类及调用

两种分类方式:

​ 按参数分为:有参构造和无参构

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

三种调用方式:

​ 括号法

​ 显示法

​ 隐式转换法

示例

#include <iostream>
using namespace std;

//分类
class Person
{
public:
  //无参构造(默认构造)
  Person()
  {
    cout << "Person的无参构造函数调用。" << endl;
  }
  //有参构造
  Person(int a)
  {
    age = a;
    cout << "Person的有参构造函数调用。" << endl;
  }
  
  //拷贝构造函数
  Person(const Person &p)
  {
    //将传入的人身上的所有属性,拷贝到我的身上
    age = p.age;
    cout << "Person的拷贝构造函数调用、" << endl;
  }
  
  ~Person()
  {
    cout << "Person的析构函数调用。" << endl;
  }
  
  int age;
};

//调用
void test01()
{
  //括号法
  cout << "括号法"
  Person p1_1;              //默认构造函数调用
  Person p2_1(10);      //有参构造函数调用
  Person p3_1(p2_1);    //拷贝构造函数调用
  cout << "p2的年龄为:" << p2_1.age << endl;
  cout << "p3的年龄为:" << p3_1.age << endl;
  //调用默认构造函数的时候不要加小括号
  //Person p1();        //编译器会认为这行代码是函数的声明,不会认为在创建对象
  
  //显示法
  cout << "显示法:" << endl;
  Person p1_2;                                  //默认构造
  Person p2_2 = Person(10);     //有参构造
  Person p3_2 = Person(p2_2);   //拷贝构造
  Person(10);       //匿名对象 特点:当前行执行结束后,系统会自动回收掉匿名对象
  //不要利用拷贝构造函数,初始化匿名对象
  //Person(p3); //错误,编译器会认为"Person(p3);"等价于"Person p3;",即p3的重定义
  
  //隐式转换法
  cout << "隐式转换法:" << endl;
  Person p1_3;
  Person p2_3 = 10;         //有参构造,相当于 Person p2_3 = Person(10);
  Person p3_3 = p2_3;       //拷贝构造,相当于 Person p3_3 = Person(p2_3);
  
  cout << endl;
}

int main()
{
  test01();
  
  system("pause");
  
  return 0;
}

4.2.3 拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况:

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
#include <iostream>
using namespace std;

class Person
{
public:
  Person() {cout << "Person默认构造函数调用。" << endl;}
  
  Person(int age)
  {
    cout << "Person有参构造函数调用。" << endl;
    m_Age = age;
  }
  
  Person(const Person &p)
  {
    cout << "Person拷贝构造函数调用。" << endl;
    m_Age = p.m_Age;
  }
  
  ~Person() {cout << "Person析构函数调用。" << endl;}
  
  int m_Age;
};

//使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
  Person p1(10);
  Person p2(p1);
  cout << "p2的年龄为:" << p2.m_Age << endl;
}

//值传递的方式给函数参数传值
void doWork(Person p) {}
void test02()
{
  Person p;     //默认构造函数
  doWork(p);    //拷贝构造函数
}

//以值方式返回局部对象
Person doWork2()
{
  Person p1;
  cout << &p1 << endl;
  return p1;
}
void test03()
{
  Person p = doWork2();
  cout << &p << endl;
}

int main()
{
  test01();     //使用一个已经创建完毕的对象来初始化一个新对象
  test02();     //值传递的方式给函数参数传值
  test03();     //以值方式返回局部对象
  
  system("pause");
  
  return 0;
}

4.2.4 构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

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

示例:

#include <iostream>
using namespace std;

class Person
{
public:
  //无参(默认)构造函数
  Person()
  {
    cout << "无参构造函数!" << endl;
  }
  //有参构造函数
  person(int a)
  {
    m_Age = a;
    cout << "有参构造函数!" << endl;
  }
  
  //拷贝构造函数
  Person(const Person& p)
    {
        m_Age = p.m_Age;
        cout << "Person拷贝构造函数!" << endl;
    }
  
  //析构函数
    ~Person()
    {
        cout << "Person析构函数!" << endl;
    }
  
  int m_Age;
}

void test01()
{
    Person p;
    p.m_Age = 18;

    Person p2(p);
    cout << "p2的年龄为:" << p2.m_Age << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.2.5 深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

#include <iostream>
using namespace std;

class Person
{
public:
    //无参(默认)构造函数
    Person()
    {
        cout << "Person的无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int age, int height)
    {
        m_Age = age;
        m_Height = new int(height);
        cout << "Person的有参构造函数!" << endl;
    }
    //自己实现拷贝构造函数,解决浅拷贝带来的问题
    Person(const Person& p)
    {
        m_Age = p.m_Age;
        //m_Height = p.m_Height;    //编译器默认实现的浅拷贝
        //深拷贝
        m_Height = new int(*p.m_Height);
        cout << "Person的拷贝构造函数!" << endl;
    }
    //析构函数
    ~Person()
    {
        //析构代码,将堆区开辟的数据释放
        if (m_Height != NULL)
        {
            delete m_Height;
            m_Height = NULL;
        }
        cout << "Person析构函数!" << endl;
    }

    int m_Age;
    int* m_Height;
};

void test01()
{
    Person p1(18, 160);
    cout << "p1的年龄为" << p1.m_Age << "身高为" << *p1.m_Height << endl;

    Person p2(p1);
    cout << "p2的年龄为" << p2.m_Age << "身高为" << *p2.m_Height << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

4.2.6 初始化列表

作用:C++提供了初始化列表语法,用来初始化属性

语法:构造函数(): 属性1(值1); 属性2(值2); ... {函数体}

#include <iostream>
using namespace std;

class Person
{
public:
    /*
    //传统方式初始化
    Person(int a, int b, int c)
    {
        m_A = a;
        m_B = b;
        m_C = c;
    }
    */

    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 test01()
{
    //Person p(10, 20, 30);
    Person p(30, 20, 10);
    cout << "m_A:" << p.m_A << "\t\tm_B:" << p.m_B << "\t\tm_C:" << p.m_C << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.2.7 类对象作为类成员

C++类中的成员可以是另一个类中的对象,我们称该成员为对象成员

例如:

class A {}
class B 
{
  A a;
}

B类中有对象A作为成员,A为成员对象

那么当创建B对象时,AB的构造与析构顺序是谁先谁后?

示例:

#include <iostream>
#include <string>
using namespace std;

//类对象作为类成员

class Phone
{
public:
    Phone(string pName) : m_PName(pName)
    {
        cout << "Phone的构造函数调用" << endl;
    }

    ~Phone()
    {
        cout << "Phone的析构函数调用!" << endl;
    }
    string m_PName; //品牌 
};

class Person
{
public:
    //Phone m_Phone = pName     隐式转换法
    Person(string name, string pName) : m_Name(name), m_Phone(pName)
    {
        cout << "Person的构造函数调用!" << endl;
    }

    ~Person()
    {
        cout << "Person的析构函数调用!" << endl;
    }

    string m_Name;  //姓名
    Phone m_Phone;  //手机
};


//当其他类的对象作为本类的成员,在构造的时候,先构造其他类对象,再构造自身
//析构的顺序与构造相反
void test01()
{
    Person p("张三", "iPhone 11 Pro Max");
    cout << p.m_Name << "拿着" << p.m_Phone.m_PName << "。\n";
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.2.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字``static`,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

示例1:静态成员变量

#include <iostream>
using namespace std;

class Person
{
public:
  //静态成员函数特点
  //1. 所有对象共享同一个函数
  //2. 静态成员函数只能访问静态成员变量
    static void func()
    {
        m_A = 100;      //静态成员函数可以访问静态成员变量
        //m_B = 100;    //静态成员函数不可以访问非静态成员变量。无法区分到底是哪个对象的成员变量
        cout << "static void func()的调用!" << endl;
    }

    static int m_A;     //静态成员变量
    int m_B;                    //非静态成员变量
    
    //静态成员函数也有访问权限
private:
    static void func2()
    {
        cout << "static void func2()的调用!" << endl;
    }
};

int Person::m_A = 0;

void test01()
{
    //通过对象进行访问
    Person p;
    p.func();

    //通过类名进行访问
    Person::func();
    //Person::func2();  //类外访问不到私有的静态成员函数
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.3 对象模型和this指针

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

在C++中,类内的成员变量和成员函数分开储存

只有非静态成员变量才属于类的对象上

#include <iostream>
using namespace std;

class Person
{
public:
    Person()
    {
        m_A = 0;
    }
    //非静态成员变量占对象空间
    int m_A;
    //静态成员变量不占对象空间
    static int m_B;

    //函数也不占对象空间,所有函数共享一个函数实例
    void func()
    {
        cout << "m_A = " << this->m_A << endl;
    }
};

int Person::m_B = 0;

class Empty
{

};

void test01()
{
    //空对象占用内存空间为1
    //C++编译器会给每一个空对象也分配一个字节空间,是为了区分空对象占内存的位置
    //每个空对象也应该有一个独一无二的内存地址
    Empty empty;
    cout << "sizeof(empty) = " << sizeof(empty) << " 字节.\n";

    Person p;
    cout << "sizeof(p) = " << sizeof(p) << " 字节.\n";
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.3.2 this指针概念

通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分那个对象调用自己的呢?

C++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

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

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用 return *this
#include <iostream>
using namespace std;

class Person
{
public:
    Person(int age)
    {
        //解决名称冲突 - 当形参和成员变量重名时,可以用this指针来区分
        //this指针指向被调用的成员函数所属的对象
        this->age = age;
    }

    Person& addAge(Person& p)       //返回整体要用引用的方式做返回
    {
        this->age += p.age;
        return *this;       //返回成员本身
    }

    int age;
};

void test01()
{
    Person p(18);
    cout << "p1的年龄是" << p.age << "岁\n";
}


//返回对象本身用 *this
void test02()
{
    Person p1(10);
    Person p2(10);

    //链式编程思想
    p2.addAge(p1).addAge(p1).addAge(p1);

    cout << "p2的年龄是" << p2.age << "岁\n";
}


int main()
{
    test01();

    test02();

    system("pause");

    return 0;
}

4.3.3 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保障代码的健壮性

示例:

#include <iostream>
using namespace std;

class Person
{
public:
    void showClassName()
    {
        cout << "This is Person class! " << endl;
    }

    void showAge()
    {
        if (this == NULL)
        {
            //如果为空指针就返回,提高代码健壮性
            return;
        }
        cout << "Age is " << m_Age << endl;     //相当与 this->m_Age
    }

    int m_Age;
};

void test01()
{
    Person *p = NULL;

    p->showClassName();
    p->showAge();       //报错原因是因为传入的指针是为NULL
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.3.3 const修饰成员函数

常函数

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

常对象

  • 声明对象前加const,称该对象为常对象
  • 常对象只能调用常函数
#include <iostream>
using namespace std;

class Person
{
public:
    void showPerson() const     //在成员函数后加const,修饰的是this指针的指向,让指针指向的值也不可以修改
    {
        //this指针的本质一个指针常量,指针的指向是不可以修改的
        //this = NULL;      //this指针指向的对象不可以修改 - Person * const this
        //m_A = 100;        //const Person * const this
        this->m_B = 100;
        cout << "m_A = " << this->m_A << endl;
        cout << "m_B = " << this->m_B << endl;
    }

    void func()
    {
        m_A = 100;
        cout << "func()" << endl;
    }

    int m_A = 0;
    mutable int m_B = 0;        //特殊变量,即使在常函数中,也可以修改这个值
};

void test01()
{
    Person p;
    p.showPerson();
}

void test02()
{
    //常对象
    const Person p;
    //p.m_A = 100;
    p.m_B = 100;        //m_B是一个特殊的变量,在常变量下也可以修改

    //常对象只能调用常函数 
    //p.func();         //常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
    p.showPerson();
}

int main()
{
    test01();

    test02();

    system("pause");

    return 0;
}

4.4 友元

生活中你的家有客厅(Public),有你的卧室(Private

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好闺蜜好基友进去。

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类访问另一个类中私有成员

友元的关键字为friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.4.1 全局函数做友元

#include <iostream>
#include <string>
using namespace std;

class Building
{
    //goodFriend是Building的好朋友,可以访问Building的私有成员
    friend void goodFriend(Building* building);
  
public:
    Building() :m_SittingRoom("客厅"), m_BedRoom("卧室") {}
    string m_SittingRoom;   //客厅

private:
    string m_BedRoom;       //卧室
};

//全局函数
void goodFriend(Building* building)
{
    cout << "好朋友全局函数正在访问" << building->m_SittingRoom << endl;

    cout << "好朋友全局函数正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    Building b;
    goodFriend(&b);
}
int main()
{
    test01();

    system("pause");

    return 0;
}

4.4.2 类做友元

#include <iostream>
#include <string>
using namespace std;

class Building;

//类做友元
class GoodFriend
{
public:
    GoodFriend();
    void visit();       //参观函数访问building中的属性
    Building* building;
};

class Building
{
    //类做友元
    friend class GoodFriend;
public:
    Building();
    string m_SittingRoom;
private:
    string m_BedRoom;
};

//类外写成员函数
Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodFriend::GoodFriend()
{
    //创建建筑物对象
    building = new Building;
}

void GoodFriend::visit()
{
    cout << "好朋友正在访问" << building->m_SittingRoom << endl;
    cout << "好朋友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    GoodFriend gFriend;
    gFriend.visit();
}
int main()
{
    test01();

    system("pause");

    return 0;
}

4.4.3 成员函数做友元

#include <iostream>
#include <string>
using namespace std;

class Building;

class GoodFriend
{
public:
    GoodFriend();

    void visit();       //让visit()可以访问Building中的私有成员
    void visit2();      //让visit2()不可以访问Building中的私有成员

    Building* building;
};

class Building
{
    //成员函数做友元
    friend void GoodFriend::visit();
public:
    Building();
    string m_SittingRoom;
private:
    string m_BedRoom;
};

Building::Building()
{
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
}

GoodFriend::GoodFriend()
{
    building = new Building;
}

void GoodFriend::visit()
{
    cout << "visit函数正在访问" << building->m_SittingRoom << endl;
    cout << "visit函数正在访问" << building->m_BedRoom << endl;
}

void GoodFriend::visit2()
{
    cout << "visit2函数正在访问" << building->m_SittingRoom << endl;
    //cout << "visit2函数正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    GoodFriend gFriend;
    gFriend.visit();
    gFriend.visit2();
}
int main()
{
    test01();

    system("pause");

    return 0;
}

4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

4.5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算

类和对象-C++运算符重载-加号运算符重载
#include <iostream>
using namespace std;

class Person
{
public:
    Person() {};
    Person(int a, int b)
    {
        this->m_A = a;
        this->m_B = b;
    }

    //成员函数实现 + 运算符重载
    Person operator+(const 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;
};

//全局函数重载 + 运算符
Person operator+(Person& p1, Person& p2)
{
    Person temp;
    temp.m_A = p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}

//运算符重载也可以发生函数重载
Person operator+(Person& p1, int num)
{
    Person temp;
    temp.m_A = p1.m_A + num;
    temp.m_B = p1.m_B + num;
    return temp;
}

void test01()
{
    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 = operator+(p1, p2);    //全局函数重载本质调用
    Person p3 = p1 + p2;
    cout << "p3.m_A = " << p3.m_A << "\np3.m_B = " << p3.m_B << endl;

    //运算符重载也可以发生函数重载
    Person p4 = p3 + 10;        //Person + int
    cout << "p4.m_A = " << p4.m_A << "\np4.m_B = " << p4.m_B << endl;
}
int main()
{
    test01();

    system("pause");

    return 0;
}

总结1:对于内置的数据类型的表达式的运算符是不可能修改的

总结2:不要滥用运算符重载

4.5.2 左移运算符重载

作用:可以输出自定义数据类型

#include <iostream>
using namespace std;

class Person
{
    friend ostream& operator<<(ostream& cout, Person& p);
    friend void test01();
public:
    Person() {};
    Person(int a, int b)
    {
        this->m_A = a;
        this->m_B = b;
    }

    //利用成员函数重载 << 运算符,p.operator<<(cout) 简化为 p << cout,不是想要的结果
    //不会利用成员函数重载运算符,因为无法实现cout在左侧
    //void operator<<(cout) {}

private:
    int m_A;
    int m_B;
};

//只能利用全局函数重载左移运算符
//本质:operator<<(cout, p),简化为cout << p
//ostream对象只能有一个
ostream& operator<<(ostream& cout, Person& p)
{
    cout << "m_A = " << p.m_A << "\nm_B = " << p.m_B;
    return cout;
}

void test01()
{
    Person p;
    p.m_A = 10;
    p.m_B = 10;

    cout << p << endl;
}
int main()
{
    test01();

    system("pause");

    return 0;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型

4.5.3 递增运算符重载

作用:通过重载递增运算符,实现自己的整形数据

#include <iostream>
using namespace std;

class MyInteger
{
    friend ostream& operator<<(ostream& cout, MyInteger mInt);
public:
    MyInteger()
    {
        m_Num = 0;
    }

    //重载 前置++ 运算符;返回引用是为了一直对一个数据进行操作
    MyInteger& operator++()
    {
        //先进行++运算
        this->m_Num++;
        //再将自身返回
        return *this;
    }
    //重载 后置++ 运算符;返回的是局部变量,所以不能返回引用
    MyInteger operator++(int)       //这个int代表占位参数,可以用于区分前置和后置递增
    {
        //先记录当前结果
        MyInteger temp = *this;
        //再进行++运算
        this->m_Num++;
        //先将自身返回
        return temp;
    }

private:
    int m_Num;
};

//重载 << 运算符
ostream& operator<<(ostream& cout, MyInteger mInt)
{
    cout << mInt.m_Num;
    return cout;
}

void test01()
{
    MyInteger mInt;
    cout << "mInt = " << mInt << endl;
    cout << "++(++mInt) = " << ++(++mInt) << endl;
    cout << "mInt = " << mInt << endl;
    cout << "mInt++ = " << mInt++ << endl;
    cout << "mInt = " << mInt << endl;
}
int main()
{
    test01();

    system("pause");

    return 0;
}

总结:前置递增但会引用,后置递增返回值

4.5.4 赋值运算符重载

C++编译器至少给一个类添加4个函数:

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符operator=,对属性进行值拷贝

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

#include <iostream>
using namespace std;

class Person
{
public:
    Person(int age)
    {
        m_Age = new int(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;
    }

    int* m_Age;
};

void test01()
{
    Person p1(18);
    Person p2(20);
    Person p3(30);

    cout << "p1的年龄为" << *p1.m_Age << "岁" << endl;
    cout << "p2的年龄为" << *p2.m_Age << "岁" << endl;
    cout << "p3的年龄为" << *p3.m_Age << "岁" << endl;

    p3 = p2 = p1;   //赋值操作
    cout << "p1的年龄为" << *p1.m_Age << "岁" << endl;
    cout << "p2的年龄为" << *p2.m_Age << "岁" << endl;
    cout << "p3的年龄为" << *p3.m_Age << "岁" << endl;
}
int main()
{
    test01();

    system("pause");

    return 0;
}

4.5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

#include <iostream>
#include <string>
using namespace std;

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; }
        else { return false; }
    }

    bool operator!=(Person& p)
    {
        if (this->m_Name != p.m_Name || this->m_Age != p.m_Age) { return true; }
        else { return false; }
    }

    string m_Name;
    int m_Age;
};

void test01()
{
    Person p1("Tom", 18);
    Person p2("Tom", 18);
    Person p3("Jerry", 18);

    if (p1 == p2) { cout << "p1与p2相等" << endl; } else { cout << "p1与p2不等" << endl; }
    if (p1 != p3) { cout << "p1与p3不等" << endl; }    else { cout << "p1与p3相等" << endl; }


}
int main()
{
    test01();

    system("pause");

    return 0;
}

4.5.6 函数调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
#include <iostream>
#include <string>
using namespace std;

class MyPrint
{
public:
    //重载函数调用运算符
    void operator()(string text)
    {
        cout << text << endl;
    }
};

//仿函数非常灵活,没有固定的写法
class MyAdd
{
public:
    int operator()(int num1, int num2) { return num1 + num2; }
};

void test01()
{
    //重载的()运算符,也称为仿函数
    MyPrint mPrint;
    mPrint("Hello world!");

    MyAdd mAdd;
    cout << "mAdd(100, 100) = " << mAdd(100, 100) << endl;

    //匿名函数对象
    cout << "MyAdd()(100, 100) = " << MyAdd()(100, 100) << endl;    //匿名对象运行完本行立即销毁
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.6 继承

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

有写类与类之间存在特殊的关系,例如下图中:

类和对象-继承

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

4.6.1 继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同

接下来我们分析利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处

普通实现

#include <iostream>
using namespace std;

//Java页面
class Java
{
public:
    void header() { cout << "首页\t公开课\t学习路线\t登录\t注册\t(公共头部)" << endl; }
    void footer() { cout << "帮助中心\t交流合作\t站内地图\t(公共底部)" << endl; }
    void left() { cout << "Java\tPython\tC/C++\t大数据\tLinux\t...\t(公共分类列表)" << endl; }
    void content() { cout << "Java学科视频" << endl; }
};

//Python页面
class Python
{
public:
    void header() { cout << "首页\t公开课\t学习路线\t登录\t注册\t(公共头部)" << endl; }
    void footer() { cout << "帮助中心\t交流合作\t站内地图\t(公共底部)" << endl; }
    void left() { cout << "Java\tPython\tC/C++\t大数据\tLinux\t...\t(公共分类列表)" << endl; }
    void content() { cout << "Python学科视频" << endl; }
};

//C/C++页面
class Cpp
{
public:
    void header() { cout << "首页\t公开课\t学习路线\t登录\t注册\t(公共头部)" << endl; }
    void footer() { cout << "帮助中心\t交流合作\t站内地图\t(公共底部)" << endl; }
    void left() { cout << "Java\tPython\tC/C++\t大数据\tLinux\t...\t(公共分类列表)" << endl; }
    void content() { cout << "C/C++学科视频" << endl; }
};

void test01()
{
    cout << "Java下载视频界面如下:" << endl;
    Java j; j.header(); j.left(); j.content(); j.footer();
    cout << "----------------------------------------------------------------" << endl;
    cout << "Python下载视频界面如下:" << endl;
    Python p; p.header(); p.left(); p.content(); p.footer();
    cout << "----------------------------------------------------------------" << endl;
    cout << "C/C++下载视频界面如下:" << endl;
    Cpp c; c.header(); c.left(); c.content(); c.footer();
}

int main()
{
    test01();

    system("pause");

    return 0;
}

继承实现

#include <iostream>
using namespace std;

//Java页面
class BasePage
{
public:
    void header() { cout << "首页\t公开课\t学习路线\t登录\t注册\t(公共头部)" << endl; }
    void footer() { cout << "帮助中心\t交流合作\t站内地图\t(公共底部)" << endl; }
    void left() { cout << "Java\tPython\tC/C++\t大数据\tLinux\t...\t(公共分类列表)" << endl; }
};

//Java页面
class Java : public BasePage
{
public:
    void content() { cout << "Java学科视频" << endl; }
};

//Python页面
class Python : public BasePage
{
public:
    void content() { cout << "Python学科视频" << endl; }
};

//C/C++学科视频
class Cpp : public BasePage
{
public:
    void content() { cout << "C/C++学科视频" << endl; }
};


void test01()
{
    cout << "Java下载视频界面如下:" << endl;
    Java j; j.header(); j.left(); j.content(); j.footer();
    cout << "----------------------------------------------------------------" << endl;
    cout << "Python下载视频界面如下:" << endl;
    Python p; p.header(); p.left(); p.content(); p.footer();
    cout << "----------------------------------------------------------------" << endl;
    cout << "C/C++下载视频界面如下:" << endl;
    Cpp c; c.header(); c.left(); c.content(); c.footer();
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:

继承的好处:减少重复的代码

语法:class 子类 : 继承方式 父类

子类:也称派生类

父类:也称基类

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

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

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

4.6.2 继承的方式

继承的语法:class 子类 : 继承方式 父类

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承
类和对象-继承-继承方式
#include <iostream>
using namespace std;

class Base
{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

//公共继承
class Son1 : public Base
{
public:
    void func()
    {
        m_A = 10;       //父类中的公共权限成员,子类中依旧是公共权限
        m_B = 10;       //父类中的保护权限成员,子类中依旧是保护权限
        //m_C = 10;     //父类中的私有权限成员,子类中访问不到
    }
};

class Son2 : protected Base
{
    void func()
    {
        m_A = 10;       //父类中的公共权限成员,子类中变为保护权限
        m_B = 10;       //父类中的保护权限成员,子类中依旧是保护权限
        //m_C = 10;     //父类中的私有权限成员,子类中访问不到
    }
};

class Son3 : private Base
{
    void func()
    {
        m_A = 10;       //父类中的公共权限成员,子类中变为私有权限
        m_B = 10;       //父类中的保护权限成员,子类中变为私有权限
        //m_C = 10;     //父类中的私有权限成员,子类中访问不到
    }
};

class GrandSon3 : public Son3
{
    void func()
    {
        //m_A = 10;     //私有权限,访问不到
        //m_B = 10;     //私有权限,访问不到
    }
};

void test01()
{
    Son1 s1;
    s1.m_A = 10;
    //s1.m_B = 10;

    Son2 s2;
    //s2.m_A = 10;      //保护权限
    //s2.m_B = 10;      //保护权限

    Son3 s3;
    //s3.m_A = 10;      //保护权限
    //s3.m_B = 10;      //保护权限
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.6.3 继承中的对象模型

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

示例:

#include <iostream>
using namespace std;

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) = " << sizeof(Son) << "字节" <<endl;     //16字节
}

int main()
{
    test01();

    system("pause");

    return 0;
}

查看对象模型布局:

  1. 打开开发人员命令提示符(Developer Command Prompt for VS)
  2. 切换到文件目录
  3. cl /dl reportSingleClassLayout类名 文件名
cd "C:\Users\lance\Desktop\Visual Studio 2019\Learning_Cpp\Learning_Cpp"

cl /d1 reportSingleClassLayoutSon main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class Son       size(16):
        +---
 0      | +--- (base class Base)
 0      | | m_A
 4      | | m_B
 8      | | m_C
        | +---
12      | m_D
        +---

总结:父类中私有成员也是被子类继承下去了,只是有编译器给隐藏后访问不到

4.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

示例:

#include <iostream>
using namespace std;

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

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

void test01()
{
    Son s;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:继承中,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

4.6.5 继承中同名成员处理方式

问题:当子类与父类出现同名成员时,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

示例:

#include <iostream>
using namespace std;

class Base
{
public:
    Base() { m_A = 100; }
    void func() { cout << "Base\tfunc()函数调用!" << endl; }
    void func(int a) { cout << "Base\tfunc(int a)函数调用!" << endl; }
    int m_A;
};

class Son : public Base
{
public:
    Son() { m_A = 200; }
    void func() { cout << "Son\tfunc()函数调用!" << endl; }
    int m_A;
};

void test01()
{
    Son s;
    //同名成员属性
    cout << "子类中m_A = " << s.m_A << endl;
    cout << "父类中m_A = " << s.Base::m_A << endl;
    //同名成员函数
    s.func();
    s.Base::func();
    //如果子类出现和父类同名的成员函数,子类的同名成员函数会隐藏父类中所有的同名成员函数
    //s.func(100);
    s.Base::func(100);
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问父类同名成员
  3. 当子类和父类拥有同名的成员函数,子类的同名成员函数会隐藏父类中所有的同名成员函数;加作用域可访问到父类中同名函数

4.6.6 继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

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

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
#include <iostream>
using namespace std;

class Base
{
public:
    static void func() { cout << "Base\tstatic void func()函数调用!" << endl; }
    static void func(int a) { cout << "Base\tstatic void func(int a)函数调用!" << endl; }
    static int m_A;
};
int Base::m_A = 100;

class Son : public Base
{
public:
    static void func() { cout << "Son\tstatic void func()函数调用!" << endl; }
    static int m_A;
};
int Son::m_A = 200;

void test01()
{
    //通过对象访问
    cout << "通过对象访问" << endl;
    Son s;
    cout << "子类中m_A = " << s.m_A << endl;
    cout << "父类中m_A = " << s.Base::m_A << endl;
    s.func();
    s.Base::func();
    s.Base::func(100);
    //通过类名访问
    cout << "\n通过类名访问" << endl;
    cout << "子类中m_A = " << Son::m_A << endl;
    cout << "父类中m_A = " << Base::m_A << endl;
    Son::func();
    Base::func();
    Son::Base::func(100);
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名

4.6.7 多继承语法

C++允许一个类继承多各类

语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ...

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

示例:

#include <iostream>
using namespace std;

class Base1
{
public:
    Base1() { m_A = 100; }
    int m_A;
};

class Base2
{
public:
    Base2() { m_A = 200; }
    int m_A;        //与Base1中m_A重名,产生不明确性
};

//子类,继承Base1和Base2
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(Son) = " << sizeof(s) << "字节" << endl;      //16字节
    cout << "s.Base1::m_A = " << s.Base1::m_A << endl;
    cout << "s.Base2::m_A = " << s.Base2::m_A << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:多继承中如果父类中出现了同名情况,子类使用的时候要加作用域

4.6.8 菱形继承

菱形继承概念:

​ 两个派生类继承同一个基类

​ 又有某个派生类同时继承这两个派生类

​ 这种继承被称为菱形继承,或者钻石继承

类和对象-继承-菱形继承

菱形继承问题

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性
  2. 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以
#include <iostream>
using namespace std;

//动物类
class Animal
{
public:
    int m_Age;
};

//继承之前加上关键字virtual,变为虚继承
//Animal类称为 虚基类

//羊类
class Sheep :virtual public Animal {};
//驼类
class Camel :virtual public Animal {};
//羊驼类
class Alpaca :public Sheep, public Camel {};

void test01()
{
    Alpaca alpaca;
    alpaca.Sheep::m_Age = 18;
    alpaca.Camel::m_Age = 28;
    //菱形继承时,两个父类拥有相同的数据,需要加以作用域区分
    //这份数据我们只需要一份,菱形继承导致数据有两份,造成资源浪费
    //利用虚继承解决菱形继承的问题
    cout << "alpaca.Sheep::m_Age = " << alpaca.Sheep::m_Age << endl;
    cout << "alpaca.Camel::m_Age = " << alpaca.Camel::m_Age << endl;
    cout << "alpace.m_Age = " << alpaca.m_Age << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

不使用虚继承:cl /d1 reportSingleClassLayoutAlpaca main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class Alpaca    size(8):
        +---
 0      | +--- (base class Sheep)
 0      | | +--- (base class Animal)
 0      | | | m_Age
        | | +---
        | +---
 4      | +--- (base class Camel)
 4      | | +--- (base class Animal)
 4      | | | m_Age
        | | +---
        | +---
        +---

使用虚继承:cl /d1 reportSingleClassLayoutAlpaca main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class Alpaca    size(12):
        +---
 0      | +--- (base class Sheep)
 0      | | {vbptr}
        | +---
 4      | +--- (base class Camel)
 4      | | {vbptr}
        | +---
        +---
        +--- (virtual base Animal)
 8      | m_Age
        +---

Alpaca::$vbtable@Sheep@:
 0      | 0
 1      | 8 (Alpacad(Sheep+0)Animal)

Alpaca::$vbtable@Camel@:
 0      | 0
 1      | 4 (Alpacad(Camel+0)Animal)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
          Animal       8       0       4 0

vbptr - Virtual Base Pointer,虚基类指针

vbptr会指向``bvtable` - Virtual Base Table,虚基类表

cl /d1 reportSingleClassLayoutAlpaca main.cpp

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

4.7 多态

4.7.1 多态的基本概念

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

多态分为两类

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

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

下面通过案例进行讲解多态

#include <iostream>
using namespace std;

//动物类
class Animal
{
public:
    void virtual 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();
}

void test01()
{
    Cat cat;
    Dog dog;
    doSpeak(cat);       //Animal& animal = cat;
    doSpeak(dog);
}

int main()
{
    test01();

    system("pause");

    return 0;
}
动态多态满足条件:
  • 有继承关系
  • 子类重写父类的虚函数(重写:函数返回值 函数名 参数列表 完全相同
动态多态使用:
  • 父类的指针或者引用指向子类的对象
多态原理:
多态原理.png

使用virtual关键字前,Animal类:

cl /d1 reportSingleClassLayoutAnimal main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class Animal    size(4):
        +---
 0      | {vfptr}
        +---

Animal::$vftable@:
        | &Animal_meta
        |  0
 0      | &Animal::speak

使用virtual关键字后,Animal类:

cl /d1 reportSingleClassLayoutAnimal main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class Animal    size(4):
        +---
 0      | {vfptr}
        +---

Animal::$vftable@:
        | &Animal_meta
        |  0
 0      | &Animal::speak

Cat类未重写时:

cl /d1 reportSingleClassLayoutCat main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class _s__CatchableType size(28):
        +---
 0      | properties
 4      | pType
 8      | _PMD thisDisplacement
20      | sizeOrOffset
24      | copyFunction
        +---

class _s__CatchableTypeArray    size(4):
        +---
 0      | nCatchableTypes
 4      | arrayOfCatchableTypes
        +---

class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr}
        | +---
        +---

Cat::$vftable@:
        | &Cat_meta
        |  0
 0      | &Animal::speak

Cat类重写后:

cl /d1 reportSingleClassLayoutCat main.cpp

用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.26.28806 版
版权所有(C) Microsoft Corporation。保留所有权利。

main.cpp

class _s__CatchableType size(28):
        +---
 0      | properties
 4      | pType
 8      | _PMD thisDisplacement
20      | sizeOrOffset
24      | copyFunction
        +---

class _s__CatchableTypeArray    size(4):
        +---
 0      | nCatchableTypes
 4      | arrayOfCatchableTypes
        +---

class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr}
        | +---
        +---

Cat::$vftable@:
        | &Cat_meta
        |  0
 0      | &Cat::speak

4.7.2 多态案例一 - 计算器类

案例描述:

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

多态的优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

示例:

//普通实现:
#include <iostream>
#include <string>
using namespace std;

//普通写法
class Calculator
{
public:
    double 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; }
        else if (oper == "/") { return m_Num1 / m_Num2; }
    }
    //如果想拓展新的功能,则需要修改源代码
    //在实际开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭
    double m_Num1;      //操作数1
    double m_Num2;      //操作数2
};

void test01()
{
    //创建计算器对象
    Calculator c;
    c.m_Num1 = 10;
    c.m_Num2 = 20;
    cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
    cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
    cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
    cout << c.m_Num1 << " / " << c.m_Num2 << " = " << c.getResult("/") << endl;
}

int main()
{
    test01();

    system("pause");

    return 0;
}
//多态实现:
#include <iostream>
using namespace std;

//实现计算器抽象类
class AbstractCalc
{
public:
    virtual int getResult() { return 0; }
    int m_Num1;
    int m_Num2;
};

//加法计算器类
class AddCalc :public AbstractCalc
{
public:
    int getResult() { return m_Num1 + m_Num2; }
};

//减法计算器类
class SubCalc :public AbstractCalc
{
public:
    int getResult() { return m_Num1 - m_Num2; }
};

//乘法计算器类
class MultCalc :public AbstractCalc
{
public:
    int getResult() { return m_Num1 * m_Num2; }
};

//除法计算器类
class DivCalc :public AbstractCalc
{
public:
    int getResult() { return m_Num1 / m_Num2; }
};

void test01()
{
    //加法运算
    AbstractCalc* abc = new AddCalc;
    abc->m_Num1 = 10;
    abc->m_Num2 = 20;
    cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
    //用完后记得销毁
    delete abc;

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

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

    //除法运算
    abc = new DivCalc;
    abc->m_Num1 = 10;
    abc->m_Num2 = 20;
    cout << abc->m_Num1 << " / " << abc->m_Num2 << " = " << abc->getResult() << endl;
    delete abc;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:C++开发提倡利用多态设计程序架构,因为多态有点很多

4.7.3 纯虚函数和抽象类

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

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

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

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

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

示例:

#include <iostream>
using namespace std;

//纯虚函数和抽象类
class Base
{
public:
    virtual void func() = 0;        //这是一个纯虚函数
};

class Son1 :public Base
{
public:
};

class Son2 :public Base
{
    void func()
    {
        cout << "func()函数的调用!" << endl;
    }
};

void test01()
{
    //Base b;   //抽象类无法实例化对象
    //new Base; //抽象类无法实例化对象
    //Son1 s1;  //抽象类的子类必须重写父类中的纯虚函数,否则也属于抽象类
    Base* base = new Son2;
    base->func();
    delete base;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.7.4 多态案例二 - 制作饮品

索例描述:

制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

#include <iostream>
using namespace std;

class AbsBeverage
{
public:
    virtual void boil() = 0;    //烧水
    virtual void brew() = 0;    //冲泡
    virtual void pour() = 0;    //倒入杯中
    virtual void condiments() = 0;  //加入辅料
    void makeBeverage()
    {
        boil();
        brew();
        pour();
        condiments();
    }
};

class Coffee :public AbsBeverage
{
public:
    void boil() { cout << "烧水" << endl; }
    void brew() { cout << "冲泡咖啡" << endl; }
    void pour() { cout << "倒入精致的咖啡杯" << endl; }
    void condiments() { cout << "加入糖和牛奶" << endl; }
};

void doWork(AbsBeverage* abs)
{
    abs->makeBeverage();
    delete abs;
}

class Tea :public AbsBeverage
{
public:
    void boil() { cout << "烧水" << endl; }
    void brew() { cout << "冲泡茶叶" << endl; }
    void pour() { cout << "倒入高雅的茶杯" << endl; }
    void condiments() { cout << "加入枸杞" << endl; }
};

void test01()
{
    //制作咖啡
    doWork(new Coffee);
    cout << "----------" << endl;
    //制作茶
    doWork(new Tea);
}

int main()
{
    test01();

    system("pause");

    return 0;
}

4.7.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

虚析构语法:

virtual ~类名() {}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名() {}

示例:

#include <iostream>
#include <string>
using namespace std;

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)
    {
        cout << "Cat的构造函数调用!" << endl;
        m_Name = new string(name);
    }
    //重写虚构函数时,可以加或者不加virtual关键字
    virtual void speak()
    {
        cout << *this->m_Name << "叫了一声:“喵呜~”" << endl;
    }

    ~Cat()
    {
        if (m_Name !=NULL)
        {
            cout << "释放Cat的内存!" << endl;
            delete m_Name;
            m_Name = NULL;
        }
    }

    string* m_Name;
};

void test01()
{
    Animal* animal = new Cat("Kitty");
    animal->speak();
    //父类指针析构的时候不会调用子类析构函数,导致如果子类有堆区数据,造成内存泄漏
    delete animal;
}

int main()
{
    test01();

    system("pause");

    return 0;
}

总结:

  1. 虚析构或纯虚析构就是来解决通过父类指针释放子类对象
  2. 如果子类中没有堆区数据,可以不写为虚析构或者纯虚析构
  3. 拥有虚析构函数的类也属于抽象类

4.7.6 多态案例三 - 电脑组装

索例描述:

电脑主要组成部件为CPU (用于计算),显卡(用于显示) ,内存条(用于存储)

将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel商和AMD厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装两台不同的电脑进行工作

示例:

#include <iostream>
using namespace std;

//抽象CPU类
class CPU
{
public:
    //抽象计算函数
    virtual void calc() = 0;
};

//抽象GPU类
class GPU
{
public:
    //抽象计算函数
    virtual void display() = 0;
};

//抽象RAM类
class RAM
{
public:
    //抽象计算函数
    virtual void store() = 0;
};

//抽象电脑类
class Computer
{
public:
    Computer(CPU* cpu, GPU* gpu, RAM* ram)
    {
        m_CPU = cpu;
        m_GPU = gpu;
        m_RAM = ram;
    }

    void doWork()
    {
        m_CPU->calc();
        m_GPU->display();
        m_RAM->store();
    }

    //提供析构函数,释放三个电脑零件
    ~Computer()
    {
        if (m_CPU != NULL)
        {
            delete m_CPU;
            m_CPU = NULL;
        }

        if (m_GPU != NULL)
        {
            delete m_GPU;
            m_GPU = NULL;
        }

        if (m_RAM != NULL)
        {
            delete m_RAM;
            m_RAM = NULL;
        }
    }

    CPU* m_CPU;     //CPU零件指针
    GPU* m_GPU;     //GPU零件指针
    RAM* m_RAM;     //RAM零件指针
};

//具体厂商
class IntelCPU :public CPU
{
    virtual void calc()
    {
        cout << "Intel CPU开始计算!" << endl;
    }
};

class AMD_CPU :public CPU
{
    virtual void calc()
    {
        cout << "AMD CPU开始计算!" << endl;
    }
};

class NVIDIA_GPU :public GPU
{
    virtual void display()
    {
        cout << "NVIDIA GPU开始渲染!" << endl;
    }
};

class AMD_GPU :public GPU
{
    virtual void display()
    {
        cout << "AMD GPU开始渲染!" << endl;
    }
};

class CorsairRAM :public RAM
{
    virtual void store()
    {
        cout << "Corsair RAM开始缓存!" << endl;
    }
};

class GSkillRAM :public RAM
{
    virtual void store()
    {
        cout << "G.Skill RAM开始缓存!" << endl;
    }
};

void test01()
{
    //第一台电脑的零件
    CPU* c1_IntelCPU = new IntelCPU;
    GPU* c1_NVIDIA_GPU = new NVIDIA_GPU;
    RAM* c1_CorsairRAM = new CorsairRAM;

    //创建第一台电脑
    cout << "第一台电脑开始工作" << endl;
    Computer* computer1 = new Computer(c1_IntelCPU, c1_NVIDIA_GPU, c1_CorsairRAM);
    computer1->doWork();
    delete computer1;

    cout << "--------------------" << endl;

    //创建第二台电脑
    cout << "第二台电脑开始工作" << endl;
    Computer* computer2 = new Computer(new AMD_CPU, new AMD_GPU, new GSkillRAM);
    computer1->doWork();
    delete computer1;
}

int main()
{
    test01();

    //system("pause");

    return 0;
}

5 文件操作

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

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

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

文件类型分为两种:

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

操作文件的三大类

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

5.1 文本文件

5.1.1写文件

写文件步骤如下:

  1. 包含头文件
    #include <fstream>
  2. 创建流对象
    ofstream ofs;
  3. 打开文件
    ofs.open("文件路径", 打开方式);
  4. 写数据
    ofs << "写入的数据";
  5. 关闭文件
    ofs.close();

文件打开方式:

打开方式 解释
ios::in 为读文件而打开文件
ios::out 为写文件而打开文件
ios::ate 初始位置:文件尾
ios::app 追加方式写文件
ios::trunc 如果文件存在,先删除,再写文件
ios::binary 二进制方式

注意:文件打开方式可以配合使用,利用|操作符

例如:用二进制方式写文件 ios::binary | ios::out

示例:

#include <iostream>
#include <fstream>  //1、包含头文件
using namespace std;

//文本文件 写文件
void test01()
{
  //2、创建流对象
  ofstream ofs;
  //3、打开文件,指定打开方式
  ofs.open("test.txt", ios::out);
  //4、写数据
  ofs << "姓名:张三" << endl;
  ofs << "性别:男" << endl;
  ofs << "年龄:18" << endl;
  ofs << "地址:北京市" << endl;
  //5、关闭文件
  ofs.close();
}

int main()
{
  test01();

  system("pause");

  return 0;
}

总结:

  • 文件操作必须包含头文件fstream
  • 文件可以利用ofstream,或者fstream
  • 开文件时候需要指定操作文件的路径,以及打开方式
  • 利用<<可以向文件中写数据
  • 操作完毕,要关闭文件

5.1.2 读文件

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

读文件步骤如下:

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

示例:

#include <iostream>
#include <fstream>      //1、包含头文件
#include <string>
using namespace std;

//文本文件 读文件
void read01(ifstream &ifs);
void read02(ifstream &ifs);
void read03(ifstream &ifs);
void read04(ifstream &ifs);

void test01()
{
  //2、创建流对象
  ifstream ifs;
  //3、打开文件并判断文件是否打开成功
  ifs.open("test.txt", ios::in);
  if (!ifs.is_open()) {
    cout << "文件打开失败!" << endl;
    return;
  }
  //4、读数据
  //第一种
  read01(ifs);
  //第二种
  read02(ifs);
  //第三种
  read03(ifs);
  //第四种 - 不推荐
  read04(ifs);
}

void read01(ifstream &ifs)
{
  char buf01[1024] = {0};
  while (ifs >> buf01) { cout << buf01 << endl; }
}

void read02(ifstream &ifs)
{
  char buf02[1024] = {0};
  while (ifs.getline(buf02, sizeof(buf02))) { cout << buf02 << endl; }
}

void read03(ifstream &ifs)
{
  string buf03;
  while (getline(ifs, buf03)) { cout << buf03 << endl; }
}

void read04(ifstream &ifs)
{
  char c;
  while ((c = ifs.get()) != EOF) { cout << c; }  //EOF: End Of File
}

int main() 
{
  test01();

  system("pause");

  return 0;
}

总结:

  • 读文件可以利用ifstream,或者fstream
  • 利用is_open()函数可以判断文件是否打开成功
  • close关闭文件

5.2 二进制操作

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

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

5.2.1 写文件

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

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

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

示例

#include <iostream>
#include <fstream>
using namespace std;

class Person
{
public:
  char m_Name[64];    //姓名
  int m_age;          //年龄
};

void test01()
{
  Person p = { "张三", 18};
  ofstream ofs("person.txt", ios::out | ios::binary);
  ofs.write((const char *) &p, sizeof(Person));
  ofs.close();
}

int main()
{
  test01();

  system("pause");

  return 0;
}

总结:文件输出流对象可以通过write()函数,以二进制的方式读写数据

5.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:ostream& read(char* buffer, int len)

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

示例:

#include <iostream>
#include <fstream>
using namespace std;

class Person
{
  public:
  char m_Name[64];
  int m_Age;
};

void test01()
{
  ifstream ifs;
  ifs.open("person.txt", ios::in | ios::binary);
  if(!ifs.is_open())
  {
    cout << "文件打开失败!" << endl;
    return;
  }
  Person p;
  ifs.read((char *) &p, sizeof(Person));
  cout << "姓名:" << p.m_Name << "\n年龄:" << p.m_Age << endl;

  ifs.close();
}

int main()
{
  test01();

  system("pause");

  return 0;
}

总结:文件输入流对象可以通过read()函数,以二进制方式读数据

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348