【黑马程序员匠心之作|C++教程】——核心编程

文章目录

C++核心编程

1 内存分区模型

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

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

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

1.1 程序运行前

在程序预处理 编译 汇编 链接后,生成了 可执行文件,未执行该程序前 C++内存空间分为两个区域:

  • 代码区:

    • 作用:存放CPU执行的机器指令
    • 特点:
      • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
      • 代码区是只读的,使其只读的原因是防止程序意外的 修改了它的指令
  • 全局区:

    • 作用:存放全局变量与静态变量,包含常量区(放字符串常量和const修饰的其它常量)
    • 该区域的数据在程序结束后 由操作系统释放

全局静态数据区(全局变量,静态变量,字符串常量,const修饰的全局常量), 代码区(共享 只读)

#include <iostream>
using namespace std;
//全局变量
int g_a = 1, g_b = 1;

//全局静态常量
static int s_g_a = 1,s_g_b = 1;

// 全局常量
const int c_g_a = 1, c_g_b = 1;


int main(){
    /*******************普通局部变量********************/
    int l_a = 1, l_b = 1;
    cout <<"局部变量l_a的地址为: "<< &l_a << endl;
    cout <<"局部变量l_b的地址为:" << &l_b << endl<< endl;
    
    /*******************全局变量********************/
    cout <<"全局变量g_a的地址为: "<< &g_a << endl;
    cout <<"全局变量g_b的地址为:" << &g_b << endl<< endl;

    /******************局部静态变量*******************/
    static int s_a = 1, s_b = 1;
    cout <<"局部静态变量s_a的地址为: "<< &s_a << endl;
    cout <<"局部静态变量s_b的地址为:" << &s_b << endl<< endl;
    
    /******************全局静态变量*******************/
    cout <<"全局静态变量s_g_a的地址为: "<< &s_g_a << endl;
    cout <<"全局静态变量s_g_b的地址为:" << &s_g_b << endl<< endl;
    

    /* ******************常量*******************
            常量: 字符串常量 + const修饰的变量
            const修饰的变量: const修饰的全局变量(在常量区),const修饰的局部变量(不在常量区)
    */
    cout <<"字符串常量的地址为:" << &("Hello World") << endl;

    cout <<"全局常量c_g_a的地址为: "<< &c_g_a << endl;
    cout <<"全局常量c_g_b的地址为: "<< &c_g_b << endl;

    const int c_l_a = 1, c_l_b = 1;
    cout <<"局部常量c_l_a的地址为: "<< &c_l_a << endl;
    cout <<"局部常量c_l_b的地址为: "<< &c_l_b << endl;

    system("pause");
    return 0;
}

在这里插入图片描述
在这里插入图片描述

总结:

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

1.2 程序运行后

栈区
  • 由编译器自动释放,存放函数的参数值,局部变量等
  • 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include<iostream>
using namespace std;

int * func(){ // 如果有形参,形参数据也会放在栈区
    int a = 0; // 局部变量 存放在栈区,栈区的数据在函数执行完后自动释放
    return &a;
}

int main(){
    int *p = func();
    cout << *p << endl; //解引用打印出局部变量a的值 
    cout << *p << endl;  
    cout << *p << endl; 
    
    system("pause");
    return 0;
}

/*
使用我的编译,在运行时候直接报错

老师的编译器可以运行成功,但只有第一次打印输出值是正确的,其它错误
- 第一次输出是正确的,因为编译器给我们做了一次保留
- 但是后面 第二次 第三次等,数据就不再保留

局部变量的内存在超出其函数作用域后,已经被释放
此时 *p 已经变成了 野指针
*/

在这里插入图片描述

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

  • 在C++ 中主要利用 操作符 new 在堆区中开辟内存

#include <iostream>
using namespace std;
int * func(){ 
    int *a = new int (10); 
    // 让指针p指向堆中申请的内存区域,内存区域大小是整形大小,存放的数据是10
    // 指针依旧是局部变量,也是放在栈上,指针保存的数据放在堆区
    return a;
}


int main(){
    int *p = func();
    cout << *p << endl; //解引用打印出局部变量a的值
    cout << *p << endl;  
    cout << *p << endl; 
    delete p; //释放堆中申请的资源
    system("pause");
    return 0;
}
/*
函数返回堆的内存地址
因为堆需要程序员手动释放,因此 在函数作用域外 堆内存地址依然存在

用p指针获得堆的内存地址,并用*p解引用 获得堆内存中存放的数据10
*/

1.3 new 操作符

new 操作符

  • C++ 利用new操作符在堆中开辟数据
  • 堆区中开辟的数据,需要由程序员手动开辟,手动释放,释放利用操作符delete
  • 语法:new 数据类型
  • 利用new创建的数据,会返回该数据对应的 类型指针
#include <iostream>
using namespace std;

int main(){
   
   int* p = new int(5); // 分配一个 int 对象并初始化为 5
    int* arr = new int[10]; // 分配一个包含 10 个 int 元素的数组

    // 使用分配的内存
    *p = 10;
    cout << *p << endl;
    for(int i = 0; i < 10; i++) arr[i] = i + 100;
    for(int i = 0; i < 10; i++) cout << arr[i] << " ";

    // 释放内存
    delete p;
    delete[] arr; // 不加上中括号,则只会释放一个数据,不会释放整个数组的元素

    system("pause");
    return 0;
}

2 引用

2.1 引用的基本使用

作用:给一个变量起别名

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

int a = 10; // 四个字节大小的内存,内存中存放的数据是10,用a去代表这块内存,方便操作内存
int &b = a; // 给这块内存取一个别名,为 b
printf("a = %d, b = %d\n", a, b); // 输出 a = 10, b = 10
b = 20;
printf("a = %d, b = %d\n", a, b); // 输出 a = 20, b = 20 , 因为原名a 和别名操作同一块内存

原名 和 别名,修改 操作的是同一块内存

2.2 引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以再发生改变
//int &b; //报错,引用 变量 "b" 需要初始值设定项

int a = 10, c = 20;
int &b = a;
//&b = c;// 报错,表达式必须是可修改的左值

b = c;// 这是赋值操作

在这里插入图片描述

2.3 引用作为函数参数

值传递,地址传递,引用传递

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

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

// 举例: 值传递,地址传递,引用传递 进行交换两个数
#include <iostream>
using namespace std;

void swap_Byvalue(int a, int b){
    int tmp = a;
    a = b;
    b = tmp;
}

void swap_Bypointer(int *a, int *b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void swap_Byreference(int &a, int &b){ // a是num1的别名,b是num2的别名
    int tmp = a;  // 原名和别名操作修改的是同一块内存
    a = b;
    b = tmp;
}
int main(){
    int num1 = 10, num2 = 20;
    swap_Byvalue(num1, num2);
    printf("num1 = %d, num2 = %d swap_Byvalue\n", num1, num2); // 10 20

    num1 = 10, num2 = 20;
    swap_Bypointer(&num1, &num2);
    printf("num1 = %d, num2 = %d swap_Bypointer\n", num1, num2); // 20 10

    num1 = 10, num2 = 20;
    swap_Byreference(num1, num2);
    printf("num1 = %d, num2 = %d swap_Byreference\n", num1, num2); // 20 10

    system("pause");
    return 0;
}

总结:通过引用参数 产生的效果桶地址传递是引用的,引用的语法更加清楚 更加简单

比较 值传递,传递地址(指针),传递引用 的优缺点

C++中,函数参数传递可以通过值传递、传递地址(指针)和传递引用三种方式进行。

下面是各自的特点

值传递

  • 缺点:

  • 无法修改实参值,只能修改函数参数副本 即形参

  • 如果传递的是大型对象,会产生额外的复制开销,影响性能

传递地址

  • 优点:

  • 可以修改实参值

  • 可以传递空指针作为参数,用于表示没有有效的对象

  • 可以通过传递 指针数组(数组每个元素都是指针) 等实现多个返回值

  • 缺点:

  • 修改数据需要函数内部解引用*a = 10;,可能会导致空指针引用和悬挂指针错误

  • 需要手动管理 指向动态分配内存的指针的生命周期,包括分配和释放动态内存

  • 指针操作,容易出错

传递引用

  • 优点:
  • 可以修改实参值
  • 可以直接访问和修改 传递的变量,类似于 对原始变量的操作
  • 不存在空引用,因为引用必须初始化为有效对象
  • 缺点:
  • 无法传递数组,因为引用的长度是固定的
  • 可能会意外修改传递的变量的值
  • 不支持空引用,则需要始终传递有效的对象

如果需要在函数内部修改实参,可以选择传递地址或者传递引用。

如果需要传递动态分配的内存,可以选择传递地址

如果需要频繁修改实参的值 且 不希望出现空引用问题,可以选择传递引用

如果不需要在函数内部修改实参的值,可以选择值传递

2.4 引用作为函数返回值

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

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

用法:函数调用作为左值

  • 案例:将局部变量的引用作为函数返回值,报错!

    #include <iostream>
    using namespace std;
    
    int& func(){
        int a = 10;  // 局部变量放在栈区 生命周期只在该函数内
        return a;
    }
    
    int main(){
        int &ref = func(); 
        cout << ref << endl; // 运行报错 warning: reference to local variable 'a' returned
        cout << ref << endl;
    
        system("pause");
        return 0;
    }
    /*
    老师的编译器同样可以输出,第一次结果正确
    后几次的输出结果随机,因为别名的内存已经被释放,第一次成功是因为编译器的暂时保留
    */
    
  • 举例:将函数调用作为等号左边的值,被操作

    #include <iostream>
    using namespace std;
    
    
    int& func1(){
        static int a = 10;  // 静态变量放在全局/静态数据区中,生命周期随程序,不随函数
        return a;
    }
    
    int main(){
        int &ref = func1();
        cout << ref << endl; // 10
        cout << ref << endl; // 10
    
        func1() = 100;  // 本质就是 a = 100
        cout << ref << endl; // 100   ref是a的别名,因此ref a func1() 都是对同一块内存的名字
        cout << ref << endl; // 100
    
        
        system("pause");
        return 0;
    }
    

2.5 引用的本质

本质:引用的本质在C++内部的实现是指针常量,int * const p 修饰的是p 的值,即 p 保存的地址值不能改变(p的指向不变),但是 p 保存的地址值指向数据的数据值可以改变

int a = 10;
int &ref = a; // 自动转换为 int * const ref = &a;
ref = 20; // 发现ref是引用,自动转换为 *ref = 20;

引用是对指针进行了封装,底层依然是通过指针实现,引用占用的内存和指针占用的内存长度一样,32位环境4 个字节,64位环境下8个字节,之所以不能获取引用的地址,是因为编译器进行了内部转换。

引用的本质,就是一个指针常量,因此引用(指针的朝向)一旦被初始化后,就不可以发生改变

引用和指针的区别
  1. 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定 义时不必赋值,以后也能指向任意数据。
  2. 指针可以用多级 int **p,但是引用只有一级int &&a不合法,可以int a = 10; int &b = a; int &c = b;
  3. 指针与引用的 自增自减 运算符意义不同,指针的++ -- 操作表示指向下一份数据,引用的++ -- 操作则表示 其所指代数据值得++ --

2.6 常量引用

作用:用来修饰形参,防止误操作时,形参改变实参

#include <iostream>
using namespace std;


void showValue(const int &val){
    // val = 30; // 报错:表达式必须是可修改的左值
    cout << val << endl;
}


int main(){
    
    // int &ref = 10; //报错:非常量引用的初始值必须为左值
    
    // 加上const之后,编译器将代码修改为int tmp = 10; const int &ref = tmp;
    const int &ref = 10; 
    // ref = 20; //报错:表达式必须是可修改的左值。加上const之后,不可以再被修改
   
    // 真正的使用场景 是用来修饰函数形参
    int a = 10;
    showValue(a);

    system("pause");
    return 0;
}
  • int &ref = 10; // 报错

  • 在函数中形参为const int &val的情况下,修改形参(实参的别名),报错

3 函数提高

3.1 函数默认参数 (形参的默认值)

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

语法:

返回值类型 函数名(参数=默认值){
    函数体...
    return 返回值
}

举例:

#include <iostream>
using namespace std;

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

/*
注意事项:
1. 如果某个位置设置了默认参数,那么从该位置往后的所有形参都必须有默认值
int func1(int a, int b = 10, int c, int d){

}

2. 如果函数声明有默认参数,函数实现就不能有默认参数
声明和实现只能有一个默认参数
*/

int func2(int a = 10, int b = 20);

int func2(int a = 10, int b = 20){ 
    return a + b;
}

int main(){
    cout << func(10, 20, 30) << endl;

    cout << func(10) << endl; // 可以只给一个值 即函数形参a的值,b和c使用形参默认值
    cout << func(10, 20) << endl; 
    // 如果在函数调用时传入了自己的数据,就用自己的数据,否则使用形参默认值;

    cout << func2(1,2) << endl; //运行报错 func2()定义处 重复定义默认参数
    system("pause");
    return 0;
}

注意事项1:如果某个位置设置了默认参数,那么从该位置往后的所有形参都必须有默认值

注意事项2:如果函数声明有默认参数,函数实现就不能有默认参数,声明和实现只能有一个默认参数

3.2 函数占位参数

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

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

#include <iostream>
using namespace std;

// 占位参数
void func(int a, int){
    cout <<"this is a func"<< endl;
}

int main(){
    // func(10); // 报错,函数调用中的参数太少
    func(10,10); // 占位参数必须填补, 函数调用的传入值无法在函数中被使用(后面课程中会用到
    system("pause");
    return 0;
}

3.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 <<"func(double a, int b)的调用" <<endl;
//     return a + b;
// }  //报错:无法重载仅按返回类型区分的函数,会产生歧义 

/*
注意事项:函数的返回值不可以作为函数重载的条件
*/
int main(){

    func();
    func(10); // 函数参数 个数不同
    func(3.14); // 函数参数 类型不同
    func(10,3.14);
    func(3.14,10);//函数参数 顺序不同
    system("pause");
    return 0;
}
  • 输出结果:
  • 不可以根据返回值类型 实现函数重载
3.3.2 函数重载的注意事项(有无const引用,默认参数)
  • 引用 作为重载条件 ,可以

    void func(int &a){ }
    void func(const int &a){}
    // 引用形参加不加 const 可以构成重载条件
    
    func(10);// 调用有const版本  int tmp = 10; const int &a = tmp;
    int a = 10;  func(a);//调用无const版本
    
  • 函数重载碰到 函数默认参数,不可以

    void func2(int a){ }
    void func2(int a, int b = 10){ }
    func2(10); //造成二义性,编译器报错
    

举例:

#include <iostream>
using namespace std;

// 同一个作用域下; 函数名称相同函数参数类型不同,或者个数不同 或者 顺序不同
void func(int &a){
    cout <<"func(int &a)的调用" <<endl;
}

void func(const int &a){
    cout <<"func(const int &a)的调用" <<endl;
}

/*****func(int &a) 和 func(const int &a) 构成重载,因为参数类型不同******/


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; 
    func(a); //重载会调用无const版本,因为a作为变量 应该可读可写
    
    func(10); // 重载会调用有const版本
    // int &a = 10; // 不合法
    // const int &a = 10; -> int tmp = 10; const &a = tmp;

    //func2(10); // 既可以调func2(int a),也可以调func2(int a, int b = 10)
    // 出现二义性,编译器报错:有多个 重载函数 "func2" 实例与参数列表匹配
    // 建议写函数重载的时候,不要使用默认参数

    func2(10,8);

    system("pause");
    return 0;
}
  • 答案:

  • 重载和默认参数,容易构成二义性

4 类和对象

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

C++ 认为万事万物 都皆为对象,对象上有属性(成员变量)和行为(成员方法)

4.1 封装

4.1.1 封装的意义

尽量隐藏类的内部实现,只想用户提供有用的成员函数。

封装的意义

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

语法: class 类名{ 访问权限: 成员变量/成员方法 };

类中的属性和行为,统一称为成员

属性:成员属性 成员变量

行为:成员函数 成员方法

成员变量大都以 m_开头,这是约定成俗的写法,不是语法规定的内容。以 m_开头既可以一眼看出这是成员变 量,又可以和成员函数中的形参名字区分开。

  • 意义一:将属性和行为作为一个整体,表现生活中的事物

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

      #include <iostream>
      using namespace std;
      const double PI = 3.14;
      
      // 设计一个圆类,求圆的周长:2 * PI * 半径
      class Circle{
      public:                     // 访问权限
          double m_r;             // 属性
          double calculateZC(){   // 行为
              return 2 * m_r * PI;
          }
      };
      
      int main(){
          // 实例化:通过一个类,创建一个对象的过程
          Circle c;     // 通过圆类,创建具体的圆(对象)
          c.m_r = 2.0;  // 给圆对象的属性进行赋值
          printf("圆的半径为%f,周长为%f\n",c.m_r,c.calculateZC()); 
          system("pause");
          return 0;
      }
      
    • 示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

      #include <iostream>
      using namespace std;
      
      class Student
      {
      private:
          string m_name;  // 姓名
          int m_id;       // 学号
      public:
          void set_name(string name){
              m_name = name;
          }
          void set_id(int id){
              m_id = id;
          }
          void showInfo(){
              printf("学生姓名为%s,学生学号为%d\n", m_name.c_str(),m_id);
          }
      
      };
      
      int main(){
         Student std;
         std.set_id(1);
         std.set_name("小红");
         std.showInfo();
      
         Student std1;
         std.set_id(2);
         std.set_name("小蓝");
         std.showInfo();
      
          system("pause");
          return 0;
      }
      
  • 意义2:类在设计的时候,可以把属性和方法放在不同的权限下,加以控制

    访问权限有三种:

    1. public 公共权限 类内可以访问 类外也可以访问
    2. protected 保护权限 类内可以访问 类外不可以访问 (继承的子类可以访问父类的protected成员)
    3. private 私有权限 类内可以访问 类外不可以访问 (子类也无法访问父类的private成员)

    如果不声明权限,则使用默认权限private

4.1.2 struct 和 class 的区别

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

  • 默认访问权限:
    • struct 作为数据结构的实现体,默认的数据访问控制是 public 的
    • class 作为对象的实现体,默认的成员变量访问控制是 private 的
  • 默认继承权限:class 默认 private, struct 默认是public
/**************** 此处说明struct和class的默认访问权限的不同 *******************/
#include <iostream>
using namespace std;

// struct T1 和 class T2 在定义属性的时候,没有给权限,则使用默认权限
struct T1{
    int a;         
};

class T2{
    int a;
};

int main(){
    T1 t1;
    T2 t2;
    t1.a = 10; // struct T1的默认权限 可以被访问 -> public
    //t2.a = 10; // class T2的默认权限,不可以被访问 -> private 
    // 报错:成员 "T2::a" (已声明 所在行数:10) 不可访问
    
    system("pause");
    return 0;
}
4.1.3 成员属性设置为私有
  • 将所有成员属性设置为私有,可以自己控制 成员属性 的控制读写权限
  • 对于写权限, 我们可以检测数据的有效性
#include <iostream>
using namespace std;

class Person{
private:
    string m_name;  // 可读可写
    int m_age = 0;      // 可读可写
    string m_lover; // 可写
public:
    /********* 对私有成员属性,提供公共控制接口 **********/
    // m_name 可读可写
    void setName(string name){
        m_name = name;
    }
    string getName(){
        return m_name;
    }

    // age 可读可写
    void setAge(int age){
        if(age >= 0 && age <= 150) m_age = age;
    }

    int getAge(){
        return m_age;
    }

    // lover 只写
    void setLover(string lover){
        m_lover = lover;
    }

};
int main(){
    Person p;
    p.setName("小红");
    p.setLover("小蓝");
    p.setAge(30);
    printf("姓名:%s,年龄:%d\n",p.getName().c_str(),p.getAge()); // p.m_lover 无法获取
    system("pause");
    return 0;
}
  • 练习案例1:设计立方体类,求立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等

    #include <iostream>
    using namespace std;
    
    class Cube{
    private:
        double m_L = 0;
        double m_H = 0;
        double m_W = 0;
    public:
        double calVolume(){
            return m_L*m_H*m_W;
        }
        double calArea(){
            return 2 * ( m_L * m_H + m_H * m_W + m_L * m_W);
        }
        bool isEqual(Cube &c){
            //也可以:if(m_L == c.getL() && m_H == c.getH() && m_W == c.getW())
            if(m_L == c.m_L &&m_H ==c.m_H && m_W == c.m_W ) return true;
            else return false;
        }
        
        void setL(double L){ m_L = L; }
        void setH(double H){ m_H = H; }
        void setW(double W){ m_W = W; }
        double getL(){ return m_L;  }
        double getH(){ return m_H;  }
        double getW(){ return m_W;  }
    };
    
    bool isEqual(Cube &a, Cube &b){ // 减少形参的拷贝,直接用引用
        if(a.getH() == b.getH() && a.getL() == b.getL() && a.getW() == b.getW()) 
            return true;
        return false;
    }
    // bool isEqual(Cube a, Cube b){
    //     if(b.m_L == b.m_L &&b.m_L ==b.m_H && b.m_L == b.m_W ) return true;
    //         else return false;
    // }  //成员 "Cube::m_L" (已声明 所在行数:6) 不可访问
    
    
    int main(){
        Cube a,b;
        a.setH(10), a.setL(10), a.setW(10);
        printf("Cube a,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",a.getL(),a.getW(),a.getH(),a.calVolume(),a.calArea());
    
        b.setH(10),b.setL(10),b.setW(10);
        printf("Cube b,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",b.getL(),b.getW(),b.getH(),b.calVolume(),b.calArea());
        cout << "Cube a 和 Cube b 是否相等(成员函数):" << b.isEqual(a) << endl;
        cout << "Cube a 和 Cube b 是否相等(全局函数):" << isEqual(a,b) << endl<< endl;
    
        b.setH(20),b.setL(10),b.setW(10);
        printf("Cube b,长%.2f宽%.2f高%.2f,体积%.2f 面积%.2f\n",b.getL(),b.getW(),b.getH(),b.calVolume(),b.calArea());
        cout << "Cube a 和 Cube b 是否相等(成员函数):" << b.isEqual(a) << endl;
        cout << "Cube a 和 Cube b 是否相等(全局函数):" << isEqual(a,b) << endl;
    
        system("pause");
        return 0;
    }
    

成员方法:bool isEqual(Cube &c) 中,可以访问c的私有成员变量,为什么?

C++ 允许同一个类的不同对象(实例)访问彼此的私有成员。

体现了面向对象的思想:封装是针对类 而不是对象,相同类之间所有的成员都是public,在类的成员函数中可以访问同类型实例对象的私有成员

  • 练习案例2:电和圆的关系。设计一个圆形Cirle类,和一个点Point类,设计点和圆的关系(圆内 圆上 圆外)

    点 到 圆心距离 == 半径, 点在圆上

    点 到 圆心距离 < 半径, 点在圆内

    点 到 圆心距离 > 半径, 点在圆外

    计算 点 到 圆心距离 :两点距离公式

    #include <iostream>
    using namespace std;
    
    class Point{
    private:
        double m_x;
        double m_y;
    public:
        void setX(double x){ m_x = x; }
        void setY(double y){ m_y = y; }
        double getX(){ return m_x; }
        double getY(){ return m_y;}
    };
    
    class Circle{
    private:
        Point m_center; // 在类中,让另一个类 作为本类的成员
        double m_r; // 圆的半径
    public:
        void setR(double r){m_r = r;}
        double getR(){ return m_r; }
        void setCenter(double x, double y){
            m_center.setX(x);
            m_center.setY(y);
        }
        void setCenter(Point center){
            m_center = center;
        }
        Point getCenter(){
            return m_center;
        }
    };
    
    void isInCircle(Point p, Circle c){
        double x1 = p.getX(), y1 = p.getY();
        double x2 = c.getCenter().getX(), y2 = c.getCenter().getY();
        double dis = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);   // 计算p和圆心距离 (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)  和 r*r 比较
        if(dis == c.getR()*c.getR()){
            cout << "点在圆上" <<endl;
        }else if(dis < c.getR()*c.getR()){
            cout << "点在圆内" <<endl;
        }else{
            cout << "点在圆外" <<endl;
        }
    }
    
    int main(){
        Circle c;
        c.setCenter(10,10);
        c.setR(3);
    
        Point p;
        p.setX(10.5), p.setY(11);
        isInCircle(p,c);
    
        p.setX(20), p.setY(20);
        isInCircle(p,c);
    
        p.setX(10), p.setY(7);
        isInCircle(p,c);
    
        system("pause");
        return 0;
    }
    

在类中,让另一个类 作为本类的成员

可以将声明 和 定义 分开写到.h 和 .cpp 文件中

以下是分开到不同的.h 和 .cpp 的形式:

/**************************point.h*******************************/
#pragma once //防止头文件重复包含
#include <iostream>
using namespace std;
class Point{
private:
    double m_x;
    double m_y;
public:
    void setX(double x);
    void setY(double y);
    double getX();
    double getY();
};
/**************************point.cpp*******************************/
#include "point.h"

// 需要告知是Point类下边的成员函数,不是一个普通函数

void Point::setX(double x){ m_x = x; }
void  Point::setY(double y){ m_y = y; }
double  Point::getX(){ return m_x; }
double  Point::getY(){ return m_y;}

/**************************circle.h*******************************/
#pragma once //防止头文件重复包含
#include "point.h"

class Circle{
private:
    Point m_center; // 在类中,让另一个类 作为本类的成员
    double m_r; // 圆的半径
public:
    void setR(double r);
    double getR();
    void setCenter(double x, double y);
    void setCenter(Point center);
    Point getCenter();
};
/**************************circle.cpp*******************************/
#include "circle.h"

void Circle::setR(double r){m_r = r;}
double Circle::getR(){ return m_r; }
void Circle::setCenter(double x, double y){
    m_center.setX(x);
    m_center.setY(y);
}
void Circle::setCenter(Point center){
    m_center = center;
}
Point Circle::getCenter(){
    return m_center;
}
/************************** 主函数:main.cpp*******************************/
#include <iostream>
#include "circle.h"
#include "point.h"
#include "circle.cpp"
#include "point.cpp"
using namespace std;


void isInCircle(Point p, Circle c){
    double x1 = p.getX(), y1 = p.getY();
    double x2 = c.getCenter().getX(), y2 = c.getCenter().getY();
    double dis = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);   
    if(dis == c.getR()*c.getR()){
        cout << "点在圆上" <<endl;
    }else if(dis < c.getR()*c.getR()){
        cout << "点在圆内" <<endl;
    }else{
        cout << "点在圆外" <<endl;
    }
}

int main(){
    Circle c;
    c.setCenter(10,10);
    c.setR(3);

    Point p;
    p.setX(10.5), p.setY(11);
    isInCircle(p,c);

    p.setX(20), p.setY(20);
    isInCircle(p,c);

    p.setX(10), p.setY(7);
    isInCircle(p,c);

    system("pause");
    return 0;
}

4.2 对象的初始化和清理

对象的初始化:创建对象时对其进行设置和准备的过程,类似于电子产品的出厂设置

对象的清理:在对象不再被使用时对其进行释放和销毁的过程

4.2.1 构造函数和析构函数

C++ 中 对象初始化——构造函数,对象清理——析构函数

两个函数将被编译器自动调用,完成对象初始化和清理的工作

如果自己不提供构造函数和析构函数,编译器会提供默认的空实现的构造函数和析构函数

  • 构造函数:创建对象时 为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用

    • 语法:类名(){ }
    1. 构造函数,没有返回值,也不写void
    2. 函数名称 和 类名 相同
    3. 构造函数可以有参数,因此可以发生重载
    4. 程序在创建对象时,自动调用构造函数,无需手动调用, 且只调用一次
  • 析构函数:对象销毁前,系统自动调用,执行一些清理工作

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

class Person{
public:
    Person(){ //没有返回值没有void;有参数,可以发生重载
        cout <<"Person 构造函数调用" << endl;
    }

    ~Person(){ //没有返回值没有void;没有参数,不可以发生重载
        cout <<"Person 析构函数调用" << endl;
    }
};

void func(){
    Person ccc; // p 随着函数结束而结束
}

int main(){
   func(); 
    Person p; // 对象p随着main函数的结束而结束
    system("pause");
    return 0;
}

func()函数内的对象是局部变量,在栈中,随着函数结束 变量会被销毁 弹出栈

因此在func()中,在创建对象的时候调用析构函数,对象销毁前调用析构函数

4.2.2 构造函数的分类及调用

两种分类方式:

  • 按照参数分类:有参构造 和 无参构造
  • 按照类型分类:普通构造 和 拷贝构造 // 除了拷贝构造函数以外的函数都是普通构造

三种调用方式:括号法;显示法;隐式转换法

#include <iostream>
using namespace std;

class Person{    
public:
    int age = 0;
    /************ 构造函数的分类 与 调用 
     ************ 按照参数分类:有参构造 和 无参(默认)构造
     ***********  按照类型分类:普通构造 和 拷贝构造 */
    Person(){
        cout <<"Person的构造函数调用\n";
    }
    Person(int a){
        age = a;
        cout <<"Person的有参构造函数调用\n";
    }
    Person(const Person &p){ // 克隆一份数据, cosnt表示不可以改变本体,用引用 减少开销
        // 将传入的人身上的所有属性,拷贝到当前对象上
        age = p.age;
        cout <<"Person的拷贝构造函数调用\n";
    }
    ~Person(){
        cout <<"Person的析构函数调用\n";
    }
};

void func(){
    // 1. 括号法
    printf("********************括号法********************\n");
    Person p1; // 默认构造函数的调用
    Person p2(10); // 有参构造函数的调用
    Person p3(p2); // 拷贝构造函数
    cout <<"p1的年龄"<< p1.age << endl;
    cout <<"p2的年龄"<< p2.age << endl;
    cout <<"p3的年龄"<< p3.age << endl;
    // 注意事项:调用默认构造函数的时候,不要加小括号
    // Person p4(); // 没有报错,但没有创建出来对象,编译器认为是一个函数的声明,不会认为是在创建对象
    // void func();  类似于下面的函数声明
    
    //2. 显示法
    printf("********************显示法********************\n");
    Person a1; // 默认构造函数
    Person a2 = Person(10);  // 有参构造函数
    Person a3 = Person(a2);  // 拷贝构造函数
    Person(10); // 匿名对象 特点:当前行执行结束后,系统会立即回收匿名对象
    cout <<"匿名对象在当前行结束后,先析构再执行下面的代码 \n";
    // 注意事项:不要利用拷贝构造函数,初始化匿名对象
    Person(a3); // 运行报错:a3被重定义 error: redeclaration of 'Person a3'
    // 因为编译器会认为是对象声明,即Person(a3) == Person a3; 无参构造对象a3 名称对象,重定义
    
    //3. 隐式转换法
    printf("********************隐式转换法法********************\n");
    Person b1 = 10; // 相当于写了 Person b1 = Person(10); 有参构造
    Person b2 = b1; // 拷贝构造
    // 通过使用关键字explicit显示声明构造函数,组织对象和内置类型的隐式转换

}
int main(){

    func();
    system("pause");
    return 0;
}
  • 结果:

  • 报错:利用拷贝构造函数 初始化匿名对象

通过explicit关键字显示声明构造函数,阻止对象和内置类型的隐式转换

#include <iostream>
using namespace std;
class Person{    
public:
    int age = 0;
    Person(){
        cout <<"Person的构造函数调用\n";
    }
    // 通过使用关键字explicit显示声明构造函数,阻止对象和内置类型的隐式转换
    explicit Person(int a){ 
        age = a;
        cout <<"Person的有参构造函数调用\n";
    }
    ~Person(){
        cout <<"Person的析构函数调用\n";
    }
}; 

int main(){
    Person b1 = 10; //报错
    system("pause");
    return 0;
}

报错:

4.2.3 拷贝构造函数调用时机

一共有三种情况:

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

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

// 1- 使用一个已经创建完毕的对象来初始化一个新的对象
void func1(){
    cout << "func1:" << endl;
    Person p1(10); // 有参构造函数
    Person p2(p1); // 拷贝构造函数
    cout << "p2 age:" << p2.m_age << endl;
}

// 2- 值传递的方式给函数参数传值
void work2(Person p){

}
void func2(){
    cout << "func2:" << endl;
    Person p(20); // 有参构造函数
    work2(p); // 拷贝构造函数,实参传递给形参的时候,会调用拷贝构造函数
}

// 3- 以值方式返回局部对象
Person work3(){
    Person p1; // 默认构造函数
    cout << &p1 << endl;
    return p1;
}
void func3(){
    cout << "func3:" << endl;
    Person p = work3(); // 拷贝构造函数
    cout << &p << endl;
    /* 我的编译器使用了返回值优化
       返回值优化:直接将 p1 对象放在 p 中,而不是通过调用拷贝构造函数来创建一个新的对象。
       因此,没有调用拷贝构造函数。
    */ 
}

int main(){
    func1();
    func2();
    func3();

    system("pause");
    return 0;
}
4.2.4 构造函数调用规则

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 拷贝构造函数 (创建新对象,且对根据传入对象的属性 对该对象的属性进行值拷贝)
  4. 赋值运算符函数 (不算构造函数,但是算缺省函数)(将一个对象的值 赋值给 一个已存在的对象)
class MyClass {
public:
    // 默认构造函数: 没有参数,通常用于对象的默认初始化
	MyClass::MyClass() {
    	// 构造函数的代码
	}
 
    // 拷贝构造函数: 创建一个新对象并将其初始化为已有对象的副本
    MyClass(const MyClass& other){
    	// 拷贝构造函数的代码
    }

    // 拷贝赋值运算符: 将一个对象的值赋给另一个已存在的对象
    MyClass& operator=(const MyClass& other){
        if (this != &other) {
        	// 拷贝赋值运算符的代码
   	 	}
    	return *this;
    }

    // 析构函数: 在对象被销毁时自动调用,用于执行对象的清理操作
    ~MyClass(){
        // 析构函数的代码
    }
};

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供 默认拷贝构造函数
  • 如果用户定义拷贝构造函数,C++ 不再提供其它构造函数
  1. 用户定义有参构造函数 和 无参构造函数,用户不提供拷贝构造函数,编译器会提供默认构造函数

    #include <iostream>
    using namespace std;
    
    class Person
    {
        public:
        int m_age;
        Person(){
            cout << "Person 默认构造函数" << endl;
        }
        Person(int age){
            m_age = age;
            cout << "Person 有参构造函数" << endl;
        }
        // Person(const Person &p){
        //     m_age = p.m_age;
        //     cout << "Person 拷贝构造函数" << endl;
        // }
        ~Person(){
            cout << "Person 析构函数" << endl;
        }
    };
    
    void func1(){
        Person p;
        p.m_age = 10;
        Person p1(p); // 注释掉自己写的拷贝构造函数,查看p1的年龄属性是否正确
        cout << p1.m_age << endl;
    
    }
    int main(){
        func1();
        system("pause");
        return 0;
    }
    
  1. 用户定义有参构造函数,系统不再提供 无参构造函数,但依然提供拷贝构造函数

    #include <iostream>
    using namespace std;
    
    class Person
    {
        public:
        int m_age;
        // Person(){
        //     cout << "Person 默认构造函数" << endl;
        // }
        Person(int age){
            m_age = age;
            cout << "Person 有参构造函数" << endl;
        }
        // Person(const Person &p){
        //     m_age = p.m_age;
        //     cout << "Person 拷贝构造函数" << endl;
        // }
        ~Person(){
            cout << "Person 析构函数" << endl;
        }
    };
    
    void func1(){
        Person p;
        p.m_age = 10;
        Person p1(p);
    }
    int main(){
        func1();
        system("pause");
        return 0;
    }
    
    • 用户定义有参构造函数,系统不再提供 无参构造函数

    • 但依然提供拷贝构造函数,即p2m_age属性值是10

  2. 如果用户定义拷贝构造函数,C++ 不再提供其它构造函数

    #include <iostream>
    using namespace std;
    
    class Person
    {
        public:
        int m_age;
        // Person(){
        //     cout << "Person 默认构造函数" << endl;
        // }
        // Person(int age){
        //     m_age = age;
        //     cout << "Person 有参构造函数" << endl;
        // }
        Person(const Person &p){
            m_age = p.m_age;
            cout << "Person 拷贝构造函数" << endl;
        }
        ~Person(){
            cout << "Person 析构函数" << endl;
        }
    };
    
    void func1(){
        Person p; // 报错:类 "Person" 不存在默认构造函数
        Person p1(10); // 报错:没有与参数列表匹配的构造函数,用户定义的有参构造函数
        Person p2(p1); // 系统默认的拷贝构造函数
        cout << p2.m_age << endl;
    
    }
    int main(){
        func1();
       
    
        system("pause");
        return 0;
    }
    

总结:

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

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

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

使用编译器提供的拷贝构造函数是浅拷贝:

#include <iostream>
using namespace std;

class Person
{
    public:
    int m_age;
    int* m_height;

    Person(){
        cout << "Person 默认构造函数" << endl;
    }
    Person(int age, int height){
        m_age = age;
        m_height = new int(height);// 指针接收堆区申请内存的地址
        cout << "Person 有参构造函数" << endl;
    }
    ~Person(){
        if(m_height != nullptr) {
            delete m_height;
            m_height = nullptr;
        }
        cout << "Person 析构函数" << endl;
    }
};

void func(){
    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(){
    func();
    system("pause");
    return 0;
}

报错:

原理:p2的m_agem_height 都是从p1浅拷贝而来,也就是说p1p2 中的m_height 指向的是同一块堆中的内存。因此在func()中,(栈是后进先出)p2先调用析构函数 释放m_height指向的堆空间,当p1再调用析构函数时,堆空间已经被释放,因此会报错。

在这里插入图片描述

解决:自己实现拷贝构造函数 深拷贝,解决浅拷贝带来的问题

让p2的m_height 指向另一片数据相同(都是160),但申请的堆空间不同

#include <iostream>
using namespace std;

class Person
{
    public:
    int m_age;
    int* m_height;

    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 != nullptr) {
            delete m_height;
            m_height = nullptr;
        }
        cout << "Person 析构函数" << endl;
    }
};

void func(){
    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(){
    func();
    system("pause");
    return 0;
}

总结:

构造函数中申请了堆的内存,则需要在析构函数中加入 释放堆内存的代码

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

编译器提供的默认拷贝构造函数是浅拷贝,如果不利用深拷贝在堆区创建新内存,可能会导致浅拷贝的重复释放堆区问题。

4.2.6 初始化列表

C++ 提供了初始化列表,用来初始化属性

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

#include <iostream>
using namespace std;

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

    // 初始化列表初始化属性
    Person(int a, int b, int c):m_A(a),m_B(b),m_C(c){}
    
};

void func1(){
    Person p(1,2,3);
    cout <<"m_A:\t"<< p.m_A << "\nm_B:\t"<< p.m_B <<"\nm_C:\t"<< p.m_C <<endl;
}

int main(){
    func1();
    system("pause");
    return 0;
}
4.2.7 类对象作为类成员

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

class A {};
class B{
	A a;
};

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

那么当创建B对象时,A与B的构造和析构函数的顺序是谁先谁后呢?

#include <iostream>
using namespace std;

class Phone{
    public:
    string m_pName;
    Phone(string name){
        m_pName = name;
        cout << "Phone 构造函数" <<endl;
    }
    ~Phone(){
        cout << "Phone 析构函数" <<endl;
    }
};
class Person{
    public:
    string m_name;
    Phone m_phone;
    // 自动调用Phone的构造函数 Phone m_phone = phoneName; 隐式转换法
    Person(string name, string phoneName):m_name(name),m_phone(phoneName){
        cout <<" Person 构造函数" <<endl;
    }
    ~Person(){
        cout << "Person 析构函数" <<endl;
    }  
};

// 构造:当其他类的对象作为本类的成员,先构造成员类对象,再构造自身。   先有零件,再有自己
// 析构:析构的顺序与构造相反
void func1(){
    Person p("小红","华为");
}

int main(){
    func1();
    system("pause");
    return 0;
}
4.2.8 static 静态成员

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

静态成员分为:

  • 静态成员变量

    • 所有对象都共享同一份数据,属于类 不属于对象 没有this指针

    • 在编译阶段分配内存 全局/静态数据区

    • 类内声明,类外初始化。必须有一个初始值

      #include <iostream>
      using namespace std;
      
      class Person{
          public:
          static int m_A; // 静态成员变量也是有访问权限的
          private:
          static int m_B;
      };
      
      int Person::m_A = 100; //类内声明,类外初始化
      int Person::m_B = 1000;
      
      void func1(){
          Person p1;
          cout << p1.m_A <<endl;
          Person p2;
          p2.m_A = 200;
      
          cout << p1.m_A << endl;
          cout << p2.m_A << endl;
      }
      
      void func2(){
          // 静态成员变量 不属于某个对象 而是属于类,且所有对象都可以访问
          // 因此静态成员变量的访问方式有两种
          cout << endl;
          // 1. 通过对象进行访问
          Person p;
          cout << p.m_A << endl;
          // 2. 通过类名进行访问
          cout << Person::m_A << endl;
          //cout << Person::m_B << endl; 
          // 编译器报错:成员 "Person::m_B" (已声明 所在行数:12) 不可访问
      }
      
      int main(){
          func1();
          func2();
          system("pause");
          return 0;
      }
      
  • 静态成员函数

    • 所有对象共享同一个函数
    • 静态成员函数 只能访问静态成员变量
    #include <iostream>
    using namespace std;
    
    class Person{
    public:
        static int m_A;
    
        int m_B;
    
        // 静态成员函数
        static void funcStatic(){
            m_A = 10; // 静态成语函数 访问 静态成员变量,不属于对象
            //m_B = 20; // 报错:非静态成员引用必须与特定对象相对,无法找到是哪个对象的m_B属性
            cout <<"static func"<<endl;
        }    
    private:
        static void funcStatic2(){ // 静态成员函数也具有访问权限
            cout <<"static func"<<endl;
        }
    };
    
    int Person::m_A = 0; // 静态成员变量:类内声明 类外初始化
    
    void func(){
        // 静态成员函数的调用,两种方式:用对象调用,或者用类去调用
        Person p;
        p.funcStatic();
    
        Person::funcStatic();
    
        // Person::funcStatic2(); 
        // 报错:函数 "Person::funcStatic2" (已声明 所在行数:17) 不可访问
        // 类外无法访问私有 静态成员函数
    }
    int main(){
        func();
        system("pause");
        return 0;
    }
    

静态成员变量必须类内声明,类外初始化的意义:

为了确保静态成员变量只有一个定义,并且可以在类外进行赋值。

当静态成员变量在类内声明时,它只是一个声明而不是定义。这意味着它没有分配内存空间,只有在类外进行初始化时才会分配内存。如果将静态成员变量的初始化放在类内部,每个包含该头文件的源文件都会创建一个独立的静态成员变量,这就会导致重复定义的错误。

通过将静态成员变量的初始化放在类外部,可以确保只有一个实际的定义,并且该变量可以在类外部的任何地方进行赋值。这样做的目的是为了方便对静态成员变量的使用和管理。类外初始化使得静态成员变量在类加载之前就可以被初始化,而不需要实例化类对象

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

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

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

类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中,但对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。

不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,若每个对象的内存模型中都保存一份相同的代码片段,则会浪费空间,考虑可以将这些代码片段压缩成一份。

事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存, 但是所有对象都共享同一段函数代码。如图所示,成员变量在堆区或栈区分配内存,成员函数在代码区分配内存。

  • 空对象的大小是 1字节,因为C++编译器会给每个空对象也分配一个字节的内存空间,是为了区分空对象占内存的位置,两个空对象不能占用同一个内存,每个控件对象也因该有一个独一无二的内存地址

    #include <iostream>
    using namespace std;
    
    class Person{
       
    };
    
    void func(){
        Person p;
       cout <<"sizeof p: "<< sizeof(p) << endl;
    }
    int main(){
        func();
        
        system("pause");
        return 0;
    }
    
  • 只有非静态成员变量才属于类对象的内存空间中

    • 静态变量不占用对象的内存空间
    • 普通成员变量占用对象的内存空间

    • 普通成员函数,静态成员函数,都不属于类的对象上

4.3.2 this 指针概念

已知C++中成员变量和成员函数是分开存储的,

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

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

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

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

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

this指针到底是什么?

  • this 指针实际上是成员函数的一个形参,在调用成员函数时 编译器在编译阶段将对象的地址作为实参传递给 this。 this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值
  • 成员函数最终会被编译成与对象无关的普通函数,除了 成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以 此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁

this指针的用途:

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

    this指针

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

    链式 编程思想,类似于cout<< 无线追加

    #include <iostream>
    using namespace std;
    
    class Person{
    public:
        int age;
        // 引用的方式接收 当前对象的本体
        //  值得方式接收 ,接收的是新数据的拷贝返回值,不再是本体
        Person AddPersonAge(Person p){
            age += p.age;
            return *this;
        }
    
    };
    int main(){
        Person p1, p2;
        p1.age = 10, p2.age = 10;
    
        p1.AddPersonAge(p2).AddPersonAge(p2).AddPersonAge(p2);
    
        cout << p1.age << endl;
        
        system("pause");
        return 0;
    }
    
    • 引用的方式 接收,接收的是本体

    • 值传递方式 接收,接收的不是本体

4.3.3 空指针访问成员函数

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

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

#include <iostream>
using namespace std;

class Person{
public:
    int m_age;
    void showClassName(){
        cout << "showClassName" << endl;
    }
    void showAge(){
        if(this == nullptr) return; // 确定this指针不为空
        cout <<"age: " << m_age << endl; // 会默认加上一个this->m_age
    }

};
int main(){
    Person *p = nullptr;
    p->showClassName(); // 不会报错
    p->showAge(); 
    // 报错,因为p为空指针,nullptr->m_age 无中生有
    
    system("pause");
    return 0;
}
4.3.4 const修饰成员函数

常函数:

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

常对象

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

常函数:

#include <iostream>
using namespace std;

class Person{
public:
    int m_A;
    mutable int m_B;
    
    void showPerson(){
        this->m_A = 100;

        //this = nullptr; //报错 提示:分配到“this”(记时错误)
        // Person *const this; this是指针常量 不可以修改this的朝向
    }

    // 成员函数后面加上const,修饰的是this指针,表示this指向的数据的值不可再被修改
    void showPerson1() const{
        //this->m_A = 100; // 报错 提示:表达式必须是可修改的左值
        this->m_B = 10;
        // const Person *const this;  this朝向不能改,里面的数据也不可以修改
    }
};
int main(){
    Person p;
    p.showPerson();
    p.showPerson1();
    
    system("pause");
    return 0;
}
  • 常函数的const是加在this指针上,保证在函数中,this指针的指向数据也无法被修改

  • 可以使用mutable关键字,修饰类的成员变量。当成员变量被声明为mutable时,它可以在const成员函数中被修改。

    常对象:

    #include <iostream>
    using namespace std;
    
    class Person{
    public:
        int m_A;
        mutable int m_B;
        Person(int a, int b):m_A(a),m_B(b){};
        void f1(){}
        void f2() const{};
    };
    
    int main(){
        const Person p(10,20);// 在对象前加const为常对象,不能修改该对象属性的值
        //p.m_A = 100; // 报错
        p.m_B = 100; // m_B是特殊值 被关键字mutable修饰,在常对象下也可以修改
        
        // 常对象只能调用常成员函数
        //p.f1(); // 报错,调用 普通成员函数,因为普通成员函数可能会将属性修改
        p.f2(); // 调用 常成员函数
    
        system("pause");
        return 0;
    }
    

4.4 友元

好朋友之间的友好关系用friend关键字表示

借助友元可以访问与其有好友关系的类中的所有成员,包括:public,protected, private属性。

友元的三种实现:

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

在类中使用friend关键字声明全局函数,则全局函数可以访问类的私有成员

#include <iostream>
using namespace std;

class House{
    //MyFriend 友元全局函数 可以访问House的私有成员
    friend void MyFriend(House *h); // 只要写在类里就可以,不要求写在public
public:
    string m_livingRoom; // 公有成员变量
    House(){
        m_livingRoom = "客厅";
        m_bedRoom = "卧室";
    }
private:
    string m_bedRoom; //私有成员变量

};

// 全局函数 通过在类中使用friend关键字声明该函数,MyFriend()可以访问类的私有成员变量
void MyFriend(House *h){
    cout <<"MyFriend is visiting " << h->m_livingRoom << endl;
    cout <<"MyFriend is visiting " << h->m_bedRoom << endl;
}

void func(){
    House h;
    MyFriend(&h);
}
int main(){
    func();

    system("pause");
    return 0;
}
4.4.2 类做友元

friend class 友元类的类名;

让该类作为另一个类的友元类,可以访问另一个类的私有成员

#include <iostream>
using namespace std;

class House{
    friend class MyFriend;
public:
    House();
    string m_livingRoom; 
private:
    string m_bedRoom;

};

House::House(){
    m_livingRoom = "客厅";
    m_bedRoom = "卧室";
}

class MyFriend{
public:
    MyFriend();
    void visit();
    House *h;
};
MyFriend::MyFriend(){
    h = new House();
}

void MyFriend::visit(){
    cout << "MyFriend is visiting " << h->m_livingRoom << endl;
    cout << "MyFriend is visiting " << h->m_bedRoom << endl; 
    // 通过friend声明修饰了本类,本类可以访问House类的私有成员变量

}

int main(){
   
   MyFriend f;
   f.visit();

    system("pause");
    return 0;
}

除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。

4.4.3 成员函数做友元

friend void MyFriend::visit1();

#include <iostream>
using namespace std;

class House;
class MyFriend{
public:
    MyFriend();
    void visit1();
    void visit2();
private:
    House *h;
};
MyFriend::MyFriend(){
    h = new House();
}

class House{
    //告诉编译器MyFriend类下的visit1()成员函数,作为本来的友元函数,可以访问私有成员
    friend void MyFriend::visit1(); 
public:
    House();
    string m_livingRoom; 
private:
    string m_bedRoom;

};

House::House(){
    m_livingRoom = "客厅";
    m_bedRoom = "卧室";
}

void MyFriend::visit1(){ // 让visit1函数可以访问House的私有成员
    cout << "MyFriend is visiting " << h->m_livingRoom << endl;
     cout << "MyFriend is visiting " << h->m_bedRoom << endl; 
    // 通过friend声明修饰了本类,本类可以访问House类的私有成员变量
    

}
void MyFriend::visit2(){ // 让visit2函数访问不到House中的私有成员
    cout << "MyFriend is visiting " << h->m_livingRoom << endl;
    //cout << "MyFriend is visiting " << h->m_bedRoom << endl; // 报错
}

int main(){
   
   MyFriend f;
   f.visit1();
   f.visit2();

    system("pause");
    return 0;
}

4.5 运算符重载

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

例如两个自定义类型的加减乘除

例如 C++中 使用了运算符重载的地方:

  • +号可以对不同类型(intfloat 等)的数据进行加法操作
  • << 既是位移运算符,又可以配合 cout 向控制台输出数据

operator是关键字,专门用于定义重载运算符的函数,可以将operator 运算符名称看作函数名

4.5.1 加号运算符重载 + - * /

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inBV6onD-1688787119534)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230619204207245.png)]

通过成员函数 重载+ 或者 通过全局函数 重载+

#include <iostream>
using namespace std;

class Person{
public:
    int m_A, m_B;
    // 1. 成员函数 重载 + 号
    // 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;
    // }
};

// 2. 全局函数 重载 + 号
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;
}

Person operator+(Person &p1, int num){
    Person tmp;
    tmp.m_A = p1.m_A + num;
    tmp.m_B = p1.m_B + num;
    return tmp;
}

void func(){
    Person p1, p2;
    p1.m_A = 10, p1.m_B = 20;
    p2.m_A = 20, p2.m_B = 10;

    // 成员函数本质调用:Person p3 = p1.operator+(p2);
    // 全局函数本质调用:Person p3 = operator+(p1, p2);
    Person p3 = p1 + p2; 
    cout << p3.m_A << ",  " << p3.m_B << endl; 
    //如果没有实现运算符重载,则报错:没有与这些操作数匹配的 "+" 运算符

    // 运算符重载 也可以进一步发生函数重载
    Person p4 = p1 + 10; 
    // 如果没有实现运算符重载,则报错:没有与这些操作数匹配的 "+" 运算符
    cout << p4.m_A << ",  " << p4.m_B << endl; //Person + int

}

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwlcJOeH-1688787119534)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230619205531852.png)]

注意:

对于内置的数据类型,表达式的运算符是不可以改变的

不要滥用运算符重载 (例如:operator + 中 做减法 )

4.5.2 左移运算符重载 << 输出

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

目的:直接输出对象信息 cout << p << endl;

注意事项:

  • 成员函数无法重载<<运算符,全局函数重载可以做到

  • cout 的 类型是ostream,需要用引用的方式 保证只有一个 不用副本,需要用链式思想

  • 如果是属性是私有的,则需要把全局运算符重载函数设置为友元函数

#include <iostream>
using namespace std;

class Person{
    friend ostream& operator <<(ostream &cout , Person &p);
private:
    int m_A, m_B;
public:
    Person(int a, int b):m_A(a),m_B(b){}

    // // 1. 利用成员函数 重载, 不会利用成员函数重载<<运算符,因为无法实现cout在左侧
    // void operator<< (){ // 实现 cout << p;
    // } 
};

// 2. << 只能利用 全局函数重载 左移运算符
ostream& operator <<(ostream &cout , Person &p){
    cout <<"m_A = "<< p.m_A << ", m_B = " << p.m_B;
    // 如果还想追加 <<, 则需要保持链式法则,返回一个cout类型
    return cout;
}

int main(){
    Person p1(10,20), p2(20,10);
    cout << p1 << endl;// 在左边第一个<<处报错:没有与这些操作数匹配的 "<<" 运算符
    cout << p2 << endl;
    system("pause");
    return 0;
}
4.5.3 自增 自减运算符重载 a++,++a,a–,–a

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

注意:

前置++运算符:返回引用,先增加后返回

后置++运算符(占位符):返回值,先记录当前值,增加属性值,再返回记录的值

#include <iostream>
using namespace std;

class MyInteger{
friend ostream& operator<< (ostream &cout, const MyInteger &mint);
public:
    MyInteger(int a):m_num(a){}
    
    //重载++前置运算符: 先自增,再返回表达式的值
    MyInteger& operator ++(){// 返回引用,是因为可以++(++a) 可以对同一个对象进行操作
        m_num++;      // 先把属性的值+1
        return *this; // 把自身作为一个返回
    }
    
    //重载后置++运算符: 先返回表达式(不能直接return,先记录当时结果,最后将记录结果做返回),再自增
    MyInteger operator ++(int){ // int代表占位参数,可以用于区分前置和后置递增 只认int为后置递增
        MyInteger tmp = *this; 
        m_num++;
        return tmp;// 局部对象tmp函数结束后被释放掉,所以返回值,不能返回引用(非法操作)
    }
    
    // 前置递减运算符: 先自减,再返回
    MyInteger& operator -- (){
        m_num--;
        return *this;
    }
    // 后置递减运算符 占位符:先记录当前值,自减,返回记录值
    MyInteger operator--(int){
        MyInteger tmp = *this;
        m_num--;
        return tmp;
    }

private:
    int m_num;
};

// 重载 << 运算符

ostream& operator<< (ostream &cout, const MyInteger &mint){
    cout << "mint = " << mint.m_num;
    return cout;
}

int main(){
    MyInteger mint(10);
    cout << mint << endl;   // 10
    cout << ++mint << endl; // 11
    cout << mint++ << endl; // 11
    cout << mint << endl;   // 12

    cout << --mint << endl; // 11
    cout << mint-- <<endl;  // 11
    cout << mint << endl;   // 10



    // mint++; // 报错:没有与这些操作数匹配的 "++" 运算符
    // mint--; // 报错:没有与这些操作数匹配的 "--" 运算符
    system("pause");
    return 0;
}

结果:

4.5.4 赋值运算符重载 =

等号

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

  • 默认构造函数(无参,函数体为空)

  • 默认析构函数(无参,函数体为空)

  • 默认拷贝构造函数

  • 赋值运算符 operator =,对属性进行值拷贝

    • 编译器自己给的 重载赋值运算符是浅拷贝,可能导致堆内存释放的问题

    • #include <iostream>
      using namespace std;
      
      class Person{
      public:
          int *m_Age;
      public:
          Person(int a):m_Age(new int(a)){}
          ~Person(){
              if(m_Age != nullptr){
                  delete m_Age;
                  m_Age = nullptr;
              }
          }
      };
      
      void func(){
          Person p1(10);
          Person p2(20);
          cout << *p1.m_Age << endl;
          cout << *p2.m_Age << endl;
          p2 = p1; // 赋值操作:将p1的所有属性值,赋给p2
          cout << *p1.m_Age << endl;
          cout << *p2.m_Age << endl;
      }
      int main(){
         func();
          system("pause");
          return 0;
      }
      

解决上面的问题:自己重写 赋值运算符重载函数 operator =,函数体内需利用深拷贝 实现赋值操作

#include <iostream>
using namespace std;

class Person{
public:
    int *m_Age;

    Person(int age){
        m_Age = new int(age);
    }

    Person& operator =(Person &p){ // 形式p2 = p1
        // 0. 编译器提供的是浅拷贝
        // m_Age = p.m_Age;
        
        // 1. 先处理自我赋值
        if (this == &p) {
            return *this;
        }

        // 2. 再判断是否对象 属性指向堆区,如果有则先释放干净后,然后再进行深拷贝
        if(m_Age != nullptr){
            delete m_Age;
            m_Age = nullptr;
        }

        m_Age = new int(*p.m_Age); // 深拷贝
        return *this; // 返回自身
    }
   
    ~Person(){
        if(m_Age != nullptr){
            delete m_Age;
            m_Age = nullptr;
        }
    }

};

ostream& operator<< (ostream &cout, Person &p){
    cout << "m_Age = " << *p.m_Age;
    return cout;
}



void func(){
    Person p1(10), p2(20), p3(30);
    cout << p1 << endl << p2 << endl << p3 << endl << endl;
    
    p1 = p2 = p3; // 需要把p3属性赋值给p2,再拿p2属性值赋值给p1
    cout << p1 << endl << p2 << endl << p3 << endl << endl;

    int a = 10, b = 20, c = 30;
    a = b = c; // 连等操作
    printf("a=%d, b=%d, c=%d\n",a,b,c); //a=30, b=30, c=30
}

int main(){
    func();
    system("pause");
    return 0;
}
4.5.5 关系运算符重载 !=, ==

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

#include <iostream>
using namespace std;

class Person{
public:
    string m_Name;
    int m_Age;
    Person(string name, int age):m_Name(name),m_Age(age){}
    // 重载 == 运算符
    bool operator == (const Person &p){
        if(m_Name == p.m_Name && m_Age == p.m_Age) return true;
        return false;
    }

    // 重载 != 运算符
    bool operator != (const Person &p){
        if(m_Name == p.m_Name && m_Age == p.m_Age) return false;
        return true;
    }
};
void func(){
    Person p1("Rachel",20), p2("Rachel", 20), p3("Tom", 20);

    if(p1 == p2)  cout << "p1 和 p2 相等" << endl;
    else  cout << "p1 和 p2 不相等" << endl;

    if(p2 == p3)  cout << "p2 和 p3 相等" << endl;
    else  cout << "p2 和 p3 不相等" << endl;

    
    if(p1 != p2)  cout << "p1 和 p2 不相等" << endl;
    else  cout << "p1 和 p2 相等" << endl;

    if(p2 != p3)  cout << "p2 和 p3 不相等" << endl;
    else  cout << "p2 和 p3 相等" << endl;
   

    // if(p1 == p2)  cout << "p1和p2相等" << endl; // 报错:没有与这些操作数匹配的 "==" 运算符
    
}
int main(){
    func();
    system("pause");
    return 0;
}
4.5.6 函数调用运算符重载 ()
  • 函数调用运算符() 也可以重载
  • 由于重载后的方式非常像函数的调用,因此称为仿函数(STL中有使用该概念)
  • 函数没有固定写法,非常灵活
#include <iostream>
using namespace std;

class MyPrint{
public:
   void operator() (string test){
    cout << test << endl;
   }
};

void MyPrint1(string test){
    cout << test << endl;
}
void func(){
  MyPrint myprint;
  myprint("hello world"); // 由于使用非常像一个函数,因此被称为仿函数
  MyPrint1("hello world"); // 真正的函数调用
}

/**********仿函数非常灵活,没有固定写法********************
 * 例如下面的类的()重载函数 有两个参数 且有返回值
 *   而上面的类的()重载函数中,有一个参数 且 没有返回值
 * ******************************************************/
class MyAdd{
public:
    int operator()(int num1, int num2){
        return num1+num2;
    }
};
void func1(){
    MyAdd myadd;
    int ret = myadd(100,100);
    cout << ret << endl;

    // 匿名对象 这行使用完就释放 生命周期就一行
    cout << MyAdd()(200,200) << endl;
}
int main(){
    func();
    func1();
    system("pause");
    return 0;
}

4.6 继承

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

没有继承的类,可能会导致代码重复:

举例:网页页面

#include <iostream>
using namespace std;

// 普通实现页面: Java页面
class Java{
public:
    void header(){
        cout <<"首页、登录、注册...(公共头部)" << endl;
    }
    void footer(){
        cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
    }
    void left(){
        cout << "Java、Python、C++...(公共分类列表)" <<endl;
    }
    void contect(){
        cout <<"Java 学科视频" << endl;
    }
};
// 普通实现页面: Python页面
class Python{
public:
    void header(){
        cout <<"首页、登录、注册...(公共头部)" << endl;
    }
    void footer(){
        cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
    }
    void left(){
        cout << "Java、Python、C++...(公共分类列表)" <<endl;
    }
    void contect(){
        cout <<"Python 学科视频" << endl;
    }
};
// 普通实现页面: C++页面
class CPP{
public:
    void header(){
        cout <<"首页、登录、注册...(公共头部)" << endl;
    }
    void footer(){
        cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
    }
    void left(){
        cout << "Java、Python、C++...(公共分类列表)" <<endl;
    }
    void contect(){
        cout <<"C++ 学科视频" << endl;
    }
};


void func(){
    cout<<"*************************\nJava 页面如下:\n";
   Java ja;
   ja.header(),ja.footer(),ja.left(),ja.contect();
   cout<<"*************************\nPython 页面如下:\n";
   Python p;
   p.header(),p.footer(),p.left(),p.contect();
   CPP c;
   cout<<"*************************\nCPP 页面如下:\n";
   c.header(),c.footer(),c.left(),c.contect();

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

4.6.1 继承的基本语法
class Base{};
class Java : public Base{};
class Python : public Base{};
class CPP : public Base{};
// 语法:class 子类:继承方式 父类{};

继承实现以上页面,实际开发中 减少重复代码

#include <iostream>
using namespace std;

class Base{
public:
    void header(){
        cout <<"首页、登录、注册...(公共头部)" << endl;
    }
    void footer(){
        cout <<"帮助中心、交流合作、站内地图...(公共底部)" <<endl;
    }
    void left(){
        cout << "Java、Python、C++...(公共分类列表)" <<endl;
    }
};
// 普通实现页面: Java页面
class Java : public Base{
public:
    void contect(){
        cout <<"Java 学科视频" << endl;
    }
};
// 普通实现页面: Python页面
class Python : public Base{
public:
    void contect(){
        cout <<"Python 学科视频" << endl;
    }
};
// 普通实现页面: C++页面
class CPP : public Base{
public:
    void contect(){
        cout <<"C++ 学科视频" << endl;
    }
};

void func(){
    cout<<"*************************\nJava 页面如下:\n";
   Java ja;
   ja.header(),ja.footer(),ja.left(),ja.contect();
   cout<<"*************************\nPython 页面如下:\n";
   Python p;
   p.header(),p.footer(),p.left(),p.contect();
   CPP c;
   cout<<"*************************\nCPP 页面如下:\n";
   c.header(),c.footer(),c.left(),c.contect();
}
int main(){
    func();
    system("pause");
    return 0;
}

总结:

  • 继承的好处:可以减少重复代码
  • 语法:class A: public B {};
  • A类 称为子类 或者派生类
  • B类 被称为父类 或者 基类
  • 派生类中的成员包含两大部分:一类是从基类继承过来的,一类是自己增加的成员
  • 从基类继承过来的表现其共性,而新增的成员体现了其个性
4.6.2 继承方式

继承方式:

  • 公共继承

  • 保护继承

  • 私有继承

#include <iostream>
using namespace std;

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

// public公有继承
class Son1:public Base{ 
    void fun(){
        m_A = 10; // 父类中的public权限成员 到子类中依然是public权限
        m_B = 10; // 父类中的protected权限成员 到子类中依然是protected权限 类内访问类外无法访问
        //m_C = 10; // 父类中的private权限成员 子类无法访问
    }
};
void func1(){ // 类外访问Son1的属性
    Son1 s1;
    s1.m_A = 10;
    //s1.m_B = 10; // 在Son1中,m_B是protected权限
    //s1.m_C = 10; // private
}

class Son2:protected Base{
    void fun(){
        m_A = 10; // 父类中public成员,到子类中变为protected权限
        m_B = 10; // 父类中protected成员,到子类中变为protected权限
        m_C = 10; // 子类访问不到父类的private私有成员
    }
};
void func2(){ // 类外访问Son1的属性
    Son2 s2;
    //s2.m_A = 10; // 在Son2中,m_A变为protected权限,因此类外访问不到
    //s2.m_B = 10; // 在Son2中,m_B是protected权限 类外不能访问
    //s2.m_C = 10; // 在Son2中,m_C是private权限
}

class Son3:private Base{
    void fun(){
        m_A = 10; // 父类中public成员,到子类中变为private权限
        m_B = 10; // 父类中protected成员,到子类中变为private权限
        //m_C = 10; // 子类访问不到父类的private私有成员
    }
};
void func3(){ // 类外访问Son1的属性
    Son3 s3;
    //s3.m_A = 10;  // 在Son3中,m_A变为private权限,因此类外访问不到
    //s3.m_B = 10; // 在Son3中,m_B变为private权限,因此类外访问不到
    //s3.m_C = 10; // 在Son3中,m_C是private权限,类外访问不到
}


class GrandSon3:public Son3{ // 公共继承Son3 也无法使用其属性,证明儿子也访问不到,是private
    void func(){
        //m_A = 10; // 子类访问不到父类的private私有成员
        //m_B = 10; // 子类访问不到父类的private私有成员
        //m_C = 10; // 子类访问不到父类的private私有成员
    }
};
int main(){
    func1();
    func2();
    func3();
    system("pause");
    return 0;
}
4.6.3 继承中的对象内存模型
  • 复习:没有继承时对象内存的分布情况。

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

    • 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象)
    • 成员函数与对象内存分离,存储在代码区

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

  • 父类中所有非静态成员变量都会被子类继承下去,父类中的私有成员属性 是被编译器隐藏了,因此子类访问不到,但是确实是继承了。

  • 所有成员函数仍然存储 在另外一个区域——代码区,由所有对象共享

举例1:父类中含有不同权限的属性,子类继承后 子类的内存大小是多少? 16字节 (父类所有 + 子类所有)

举例2:父类和子类中出现成员变量的遮蔽,即父类成员变量和子类成员变量同名,则子类内存大小? 不变

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 func(){
	//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
	Son s;
}

int main() {
	func();
	system("pause");
	return 0;
}
4.6.5 继承同名成员处理方式

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

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

举例:

#include <iostream>
using namespace std;

class Base {
public:
	int m_A;
    Base():m_A(100){} // 构造函数中给属性m_A赋值100
    void prints(){
        cout <<"Base prints()"<<endl;
    }
    void prints(int a){
        cout <<"Base prints(int a)"<<endl;
    }
};

class Son : public Base{
public:
	int m_A;
    Son():m_A(200){} // 构造函数中给属性m_A赋值200
    void prints(){
        cout <<"Son"<<endl;
    }
};

void func(){
    /*同名属性的处理方式:子类对象访问父类的同名成员变量,需要加上父类的作用域*/
	Son s; //子类中将会有两个m_A,一个来自父类(100),一个来自子类(200)
    cout << s.m_A << endl; // 200
    cout << s.Base::m_A << endl; // 100

    /*同名属性的处理方式:和属性一样*/
    s.prints();
    s.Base::prints();
    // s.prints(100); //出错,但删除子类同名函数后不报错,被隐藏 必须加上作用域
    s.Base::prints(100);
    /* 
    如果子类中出现和父类同名的成员函数,
    子类的同名成员函数会隐藏掉父类所有的同名成员函数(所有重载都不能直接访问)
    */
}

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHi6Wwut-1688787119535)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620111911987.png)]

总结:

  1. 子类对象可以直接访问到子类的同名成员
  2. 子类对象加上父类的作用域可以访问到父类的同名成员
  3. 当子类和父类拥有同名的成员函数,子类会隐藏父类中所有的同名成员函数,必须加上父类的作用域才可以访问到父类中的同名函数
4.6.6 继承同名静态成员处理方式

复习:静态成员变量和静态成员函数的特点

静态成员变量特点:所有对象共享同一片数据,编译期在全局/静态数据区分配内存,类内声明类外初始化

静态成员函数特点:函数中只能访问静态成员变量

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

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

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

class Base {
public:
	static int m_A;
    static void f(){
        cout << "Base f()" << endl;
    }
     static void f(int a){
        cout <<"Base f(int a)" << endl;
    }
};
int Base::m_A = 200; // 静态成员变量,必须类内声明,类外初始化

class Son : public Base{
public:
	static int m_A;
    static void f(){
        cout << "Son f()" << endl;
    }
};
int Son::m_A = 200; //Son会继承Base类的静态成员变量,因此Son作用于下有两个静态成员变量 m_A, Base::m_A

void func(){
    /*******************同名静态成员变量*******************/
    // 1.通过对象访问静态成员变量
    Son s;
    cout << s.m_A << endl;
    cout << s.Base::m_A << endl;
    // 2.通过类名访问静态成员变量
    cout << Son::m_A << endl;
    cout << Son::Base::m_A << endl;
    //:: 第一个表示以类名方式访问; 第二个表示访问父类作用域下的

    /*******************同名静态成员函数*******************/
    // 1.通过对象访问静态成员函数
    s.f();
    s.Base::f();
    // 2.通过类名访问静态成员函数
    Son::f();
    Son::Base::f();
    // Son::f(10); 
    // 报错:和非静态成员函数一样,子类会把父类所有同名函数隐藏掉 包括重载函数,如果使用需加上父类作用域
    Son::Base::f(10);
}

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

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

4.6.7 多继承语法

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

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

多继承的问题:若父类1和父类2中有同名成员出现,子类使用时不知道是哪一个,因此需加父类作用域加以区分

举例:

#include <iostream>
using namespace std;
class Base1 {
public:
	Base1():m_A(100){ }
public:
	int m_A;
};

class Base2 {
public:
	Base2():m_A(200){ }
public:
	int m_A;
};

//语法:class 子类:继承方式 父类1 ,继承方式 父类2 
class Son : public Base2, public Base1 {
public:
	Son():m_C(300),m_D(400){ }
public:
	int m_C;
	int m_D;
};

//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员

int main() {
	Son s;
    cout << "sizeof Son = " << sizeof(s) << endl; //16字节

    // cout << s.m_A << endl; // 报错,"Son::m_A" 不明确, 需要显示给出作用域
	cout << s.Base1::m_A << endl;
	cout << s.Base2::m_A << endl;

	system("pause");
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NPq7N1JU-1688787119535)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620150812353.png)]

4.6.8 菱形继承

菱形继承概念: 两个派生类B,C继承同一个基类A,又有某个类D同时继承 上面的两个派生类。这种继承被称为菱形继承,或者钻石继承

典型的案例:

  • 没有虚继承

    • #include <iostream>
      using namespace std;
      class Animal{
      public:
      	int m_Age;
      };
      class Sheep :  public Animal {}; 
      class Tuo   :  public Animal {};
      class SheepTuo : public Sheep, public Tuo {};
      void func()
      {
      	SheepTuo st;
          //st.m_Age = 18; //报错:"SheepTuo::m_Age" 不明确,出现二义性
      
          // 菱形继承,两个父类拥有相同的数据,需要加作用域 以区分
      	st.Sheep::m_Age = 18;
      	st.Tuo::m_Age = 28;
      
          // 问题:数据m_Age只要一份即可,菱形继承导致数据有两份,只要一份即可,资源浪费 
      }
      

  • m_Age 只需要一份即可,使用虚继承后:

    • #include <iostream>
      using namespace std;
      class Animal{
      public:
      	int m_Age;
      };
      
      //继承前加 virtual 关键字后,变为虚继承
      //此时公共的父类Animal称为虚基类
      class Sheep : virtual public Animal {}; 
      
      class Tuo   : virtual public Animal {};
      
      class SheepTuo : public Sheep, public Tuo {};
      
      void func(){
      	SheepTuo st;
      
          // 菱形继承,两个父类拥有相同的数据,需要加作用域 以区分
      	st.Sheep::m_Age = 18;
      	st.Tuo::m_Age = 28;
      
          // 问题:数据m_Age只要一份即可 菱形继承导致数据有两份,只要一份即可 资源浪费 -> 利用虚继承,解决问题 
      
      	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; // 28 只有一份
      	cout << "st.Tuo::m_Age = " <<  st.Tuo::m_Age << endl;    // 28
      	cout << "st.m_Age = " << st.m_Age << endl;      // 28 m_Age只有一份,不会出错
          cout << "sizeof: " << sizeof(st) << endl;
      }
      
      
      int main() {
      
      	func();
      	system("pause");
      	return 0;
      }
      

4.7 多态

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

4.7.1 多态的基本概念

多态分为两类:

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

函数在内存中是一个代码段,函数名是进入该代码段的入口。在编译期间将函数名换为代码段入口,称为函数绑定

  • 静态多态的函数早绑定 —— 编译阶段 确定函数地址
    • 绑定的是静态类型,在编译器就可以确定函数或者对象的类型
    • 静态类型:对象在声明时 采用的类型,在编译期确定
  • 动态多态的函数玩绑定 —— 运行阶段 确定函数地址
    • 绑定的是动态类型,只有在运行时才可以确定函数或者对象的类型
    • 动态类型:一个指针或者引用目前的对象类型,只有运行时才能确定。
#include <iostream>
using namespace std;

class Animal{
public:
	// Speak函数就是虚函数
	// 函数前面加上virtual关键字,变成虚函数,那么编译器在编译期 无法确定函数地址
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal{
public:
	void speak()
	{
		cout << "小猫: 喵" << endl;
	}
};

class Dog :public Animal{
public:

	void speak(){
		cout << "小狗: 汪" << endl;
	}

};

/*我们希望传入什么对象,那么就调用什么对象的函数
  如果函数地址在编译阶段就能确定,那么静态编译
  如果函数地址在运行阶段才能确定,就是动态编译 */
void DoSpeak(Animal & animal){
	animal.speak();
}

/*
多态满足条件: 
1、有继承关系
2、子类重写父类中的虚函数
多态使用: 父类指针或引用指向子类对象 
*/
int main() {
	Cat cat;
	DoSpeak(cat);
    Dog dog;
	DoSpeak(dog);

	system("pause");
	return 0;
}

总结:

  • 多态满足条件
    • 有继承关系
    • 子类重写父类中的虚函数
  • 多态使用条件:父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就 使用派生类的成员。即基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有 多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

多态底层原理:虚函数(表)指针,虚函数表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NeYb93Qt-1688787119535)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620170229403.png)]

  • Animal类 函数不加virtual关键字的 类大小:1字节,空类1字节的"区分占位符"

  • Animal类 函数加上 virtual 关键字的 类大小:8字节,是虚表指针的大小 64位8字节,32位4字节

    • Cat类不写 speak() 函数,继承Animal类的虚函数指针,指向Animal的speak()函数

      • Cat类 重写speak() 函数:子类Cat类将自己的虚函数表中的函数地址替换成自己重写的speak()函数。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那 么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。

        当通过指针调用虚函数时,现根据指针找到子类对象的vfptr,再根据vfptr找到子类的虚函数入口地址,进而获取到子类重写虚函数的函数地址

哪些函数不能是虚函数?

构造函数,静态成员函数(不属于类),内联函数(编译器替换),友元函数,普通函数

  • 构造函数:

    • 意义上 构造函数初始化派生类对象,派生类必须知道基类函数的初始化定义 才能构造
    • 实现上,构造函数初始化时 会初始化虚表指针,指向虚函数。若构造函数是虚函数,则需要通过虚表指针查找虚表 找对应虚函数地址,但此时虚表指针还没有被初始化,因此也无法实现,编译器会报错
  • 静态函数:静态成员函数 不属于对象属于类,静态成员函数没有this指针,没有意义

  • 内联函数:内联函数是编译器进行替换工作,虚函数在运行期间才确定类型,因此内联函数不能是虚函数

  • 友元函数:友元函数不属于类的成员函数,不能被继承

  • 普通函数:不同函数不属于类的成员函数,不能被继承,没有继承特性的函数没有虚函数的说法

4.7.2 多态案例一:计算器类(两种实现方式:普通类,多态实现)

案例描述:

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

多态的优点:

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

示例:

#include <iostream>
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;
		}
		//如果要提供新的运算,需要修改源码
        return m_Num1 / m_Num2;
	}
public:
	double m_Num1;
	double m_Num2;
};

void func1(){
	//普通实现测试
    cout << "测试 普通类实现的计算器:" << endl;
	Calculator c;
	c.m_Num1 = 10;
	c.m_Num2 = 10;
	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;
}



//多态实现,利用抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class BaseCalculator{
public :
	virtual double getResult(){
		return 0;
	}
	double m_Num1;
	double m_Num2;
};

//加法计算器
class AddCalculator :public BaseCalculator{
public:
	double getResult(){
		return m_Num1 + m_Num2;
	}
};

//减法计算器
class SubCalculator :public BaseCalculator{
public:
	double getResult(){
		return m_Num1 - m_Num2;
	}
};

//乘法计算器
class MulCalculator :public BaseCalculator{
public:
	double getResult(){
		return m_Num1 * m_Num2;
	}
};


void func2(){
    cout << "测试 多态实现的计算器:" << endl;
	//创建加法计算器
	BaseCalculator *cal = new AddCalculator;
	cal->m_Num1 = 10;
	cal->m_Num2 = 10;
	cout << cal->m_Num1 << " + " << cal->m_Num2 << " = " << cal->getResult() << endl;
	delete cal;  //用完销毁 释放申请的堆内存空间

	//创建减法计算器
	cal = new SubCalculator;
	cal->m_Num1 = 10;
	cal->m_Num2 = 10;
	cout << cal->m_Num1 << " - " << cal->m_Num2 << " = " << cal->getResult() << endl;
	delete cal;  

	//创建乘法计算器
	cal = new MulCalculator;
	cal->m_Num1 = 10;
	cal->m_Num2 = 10;
	cout << cal->m_Num1 << " * " << cal->m_Num2 << " = " << cal->getResult() << endl;
	delete cal;
}

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UzF14bIh-1688787119536)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620173738606.png)]

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

4.7.3 纯虚函数 和 抽象类

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

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

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

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

抽象类特点

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

示例:

#include <iostream>
using namespace std;

/*
抽象类特点:
1. 无法实例化对象
2. 子类必须重写父类纯虚函数,否则子类也是抽象类
*/
class Base{
public:
    virtual void func() = 0; //纯虚函数:虚函数virtual关键字的基础上,在函数后面 = 0
};

class Son : public Base{
public:
    void func(){
        cout <<"Son func()" <<endl;
    }
};

int main() {
     /* 尝试实例化抽象类 */
    //Base b; //报错:不允许使用抽象类类型 "Base" 的对象, 函数 "Base::func" 是纯虚拟函数
    //new Base; // 报错同上,无论是栈上实例化 还是堆区实例化,都无法实例化具有纯虚函数的抽象类

    /* 如果子类不重写纯虚函数 子类也是抽象类*/
    // Son s;// 报错:不允许使用抽象类类型 "Son" 的对象, 纯虚拟 函数 "Base::func" 没有强制替代项

    /* 如果子类重写了纯虚函数 可以实例化对象*/
    Son s; // 没问题,子类必须重写纯虚函数,否则无法实例化对象
    

    Base *b = new Son;// 使用多态调用func()函数
    b->func();
    delete b;// 销毁

	system("pause");
	return 0;
}
4.7.4 多态案例二:制作饮品

案例描述:

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

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

#include <iostream>
using namespace std;

class AbstractMakeDrink{
public:
    //四个步骤:煮水 -  冲泡 - 倒入杯中 - 加入辅料
    virtual void boilingWater() = 0;
    virtual void brewing() = 0;
    virtual void pouring() = 0;
    virtual void addingIngredients() = 0;
    void makeDrink(){
        boilingWater();
        brewing();
        pouring();
        addingIngredients();
    }
};

class MakeCoffee : public AbstractMakeDrink{
    void boilingWater(){ printf("煮水\n"); }
    void brewing(){ printf("冲咖啡\n"); }
    void pouring(){ printf("倒入咖啡杯\n"); }
    void addingIngredients(){ printf("加入牛奶\n"); }
};

class MakeTea : public AbstractMakeDrink{
    void boilingWater(){ printf("煮矿泉水\n"); }
    void brewing(){ printf("泡茶\n"); }
    void pouring(){ printf("倒入茶壶\n"); }
    void addingIngredients(){ printf("加入桂花\n"); }
};

void doWork(AbstractMakeDrink *make){
    make->makeDrink();
    delete make;
    cout << "--------------" << endl;
}

int main() {
    
    doWork(new MakeCoffee);

    doWork(new MakeTea);

	system("pause");
	return 0;
}
4.7.5 虚析构和纯虚析构

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

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

  • 案例1:不使用虚析构函数

  • 案例2:使用虚析构函数

    • #include <iostream>
      using namespace std;
      
      class Animal {
      public:
      	Animal(){
      		cout << "Animal 构造函数" << endl;
      	}
      	virtual void Speak() = 0;
      
      	// 析构函数加上virtual关键字,变成虚析构函数
      	 virtual ~Animal(){
      		cout << "Animal析构函数" << endl;
      	}
      
      };
      
      
      class Cat : public Animal {
      public:
      	Cat(string name):m_Name(new string(name)){
      		cout << "Cat构造函数" << endl;
      	}
      	virtual void Speak(){
      		cout << *m_Name <<  "小猫: 喵" << endl;
      	}
      
      	~Cat(){
      		cout << "Cat析构函数" << endl;
      		if (this->m_Name != NULL) {
      			delete m_Name;
      			m_Name = NULL;
      		}
      	}
      
      public:
      	string *m_Name;
      };
      
      void func(){
      	Animal *animal = new Cat("Tom");
      	animal->Speak();
      
      	//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
      	//怎么解决?给基类增加一个虚析构函数
      	//虚析构函数就是用来解决通过父类指针释放子类对象
      	delete animal;
      }
      
      int main() {
      	func();
      	system("pause");
      	return 0;
      }
      
    • 在这里插入图片描述

  • 案例3:使用纯虚析构函数

    • #include <iostream>
      using namespace std;
      
      class Animal {
      public:
      	Animal(){
      		cout << "Animal 构造函数" << endl;
      	}
      	virtual void Speak() = 0;
      
          virtual ~Animal() = 0;
      
      };
      
      /*
          无论是虚析构函数 还是 纯虚析构函数,目的都是为了父类指针释放子类对象
          区别:
          纯虚析构函数需要声明 也需要有实现,因为父类可能也有堆内存需要释放
          纯虚析构 该类也是抽象类,无法被实例化
         
      */
      Animal::~Animal() 
      {
      	cout << "Animal 纯虚析构函数调用!" << endl;
      }
      
      
      class Cat : public Animal {
      public:
      	Cat(string name):m_Name(new string(name)){
      		cout << "Cat构造函数" << endl;
      	}
      	virtual void Speak(){
      		cout << *m_Name <<  "小猫: 喵" << endl;
      	}
      
      	~Cat(){
      		cout << "Cat析构函数" << endl;
      		if (this->m_Name != NULL) {
      			delete m_Name;
      			m_Name = NULL;
      		}
      	}
      
      public:
      	string *m_Name;
      };
      
      void func(){
      	Animal *animal = new Cat("Tom");
      	animal->Speak();
      	delete animal;
      }
      
      int main() {
      	func();
      	system("pause");
      	return 0;
      }
      

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

​ 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

​ 3. 纯虚析构函数需要声明与实现(因为父类对象也可能有堆内存需要释放),拥有纯虚析构函数的类也属于抽象类,无法被实例化

4.7.6 多态案例二:电脑组装

案例描述:

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-26Jtfnqh-1688787119536)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620200733141.png)]

#include <iostream>
using namespace std;


class CPU{
public:
    virtual void calculate() = 0;
};
class VideoCard{
public:
    virtual void display() = 0;
};
class Memory{
public:
    virtual void storage() = 0;
};

class Computer{
public:
    Computer(CPU *c, VideoCard *v,Memory *m):m_CPU(c),m_VideoCard(v),m_Memory(m){    }
    void work(){
        cout << "组装电脑,配置如下:" <<endl;
        m_CPU->calculate();
        m_VideoCard->display();
        m_Memory->storage();
    }
    ~Computer(){
        if(m_CPU != nullptr){
            delete m_CPU;
            m_CPU = nullptr;
        }
        if(m_VideoCard != nullptr){
            delete m_VideoCard;
            m_VideoCard = nullptr;
        }
        if(m_Memory != nullptr){
            delete m_Memory;
            m_Memory = nullptr;
        }
    }
private:
    CPU *m_CPU;
    VideoCard *m_VideoCard;
    Memory *m_Memory;
};

/*********************不同品牌的CPU 显卡 内存条的实现*********************/
class IntelCPU : public CPU{
public:
    void calculate(){
        cout <<"InterCPU calculate()" <<endl;
    }
};
class IntelVideoCard : public VideoCard{
public:
    void display(){
        cout <<"InterVideoCard display()" <<endl;
    }
};
class IntelMemory : public Memory{
public:
    void storage(){
        cout <<"InterMemory storage()" <<endl;
    }
};

class LenovoCPU : public CPU{
public:
    void calculate(){
        cout <<"LenovoCPU calculate()" <<endl;
    }
};
class LenovoVideoCard : public VideoCard{
public:
    void display(){
        cout <<"LenovoVideoCard display()" <<endl;
    }
};
class LenovoMemory : public Memory{
public:
    void storage(){
        cout <<"LenovoMemory storage()" <<endl;
    }
};

void func1(){
    CPU *intelCPU = new IntelCPU();
    VideoCard *intelCard = new IntelVideoCard();
    Memory *intelMem = new IntelMemory();
    Computer *computer1 = new Computer(intelCPU,intelCard,intelMem);
    computer1->work();
    delete computer1;
    cout <<"---------------------------------"<<endl;
    Computer *computer2 = 
        new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
    computer2->work();
    delete computer2;
    
    cout <<"---------------------------------"<<endl;
    Computer *computer3 = new Computer
        (new IntelCPU, new LenovoVideoCard, new IntelMemory);
    computer2->work();
    delete computer3;
}
int main() {
	func1();
	system("pause");
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqiJOPWZ-1688787119540)(C:\Users\16210\AppData\Roaming\Typora\typora-user-images\image-20230620202504034.png)]

自己要注意的点:

哪里创建,哪里就析构 -> Computer类

不要在func1() 中逐个delete intelCPU指针,intelCard指针,intelMem指针

而是在析构函数中 判断指针是否为空,然后在析构函数中释放指针所指向的内存空间

使用析构函数 思维 还是不太熟练

5 文件操作

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

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

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

文件类型分为两种:

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

操作文件的三大类:

  1. ofstream:写操作 output CPU向内存中写
  2. ifstream: 读操作 input CPU把数据从内存读向CPU
  3. fstream : 读写操作 file

5.1文本文件

5.1.1写文件

写文件步骤如下:

  1. 包含头文件

    #include <fstream>

  2. 创建流对象

    ofstream ofs;

  3. 打开文件

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

  4. 写数据

    ofs << "写入的数据";

    类似于cout << "输出到显示屏的数据"; 只不过一个输出到显示屏,一个输出到内存文件

  5. 关闭文件

    ofs.close();

文件打开方式:

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

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

**例如:**用二进制方式写文件 ios::binary | ios:: out,类似 Linux中的文件权限 也是通过| 组合不同的权限

示例:

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

void func()
{
	ofstream ofs; // 2.创建 输出流/写流对象
	ofs.open("test.txt", ios::out); // 3.打开文件text.txt ,打开方式out 为写文件打开文件

    // 4.用左移运算符 写入内容
	ofs << "姓名:小红" << endl;
	ofs << "性别:女" << endl;
	ofs << "年龄:9" << endl;

    // 5. 关闭文件
	ofs.close();
}

int main() {

	func();
	system("pause");

	return 0;
}

总结:

  • 文件操作必须包含头文件 fstream
  • 写文件 可以利用 ofstream ,或者 fstream
  • 记得创建对应流的对象
  • 打开文件时候需要指定操作文件的路径,以及打开方式
  • 利用 << 可以向文件中写数据
  • 操作完毕,要关闭文件 对象名.close()
5.1.2读文件

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

读文件步骤如下:

  1. 包含头文件

    #include <fstream>

  2. 创建流对象

    ifstream ifs;

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

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

  4. 读数据

    四种方式读取

  5. 关闭文件

    ifs.close();

示例:

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

void func(){
	ifstream ifs;
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open()){
		cout << "文件打开失败" << endl;//路径写错或者文件不存在
		return;
	}

	// // 第一种方式 字符数组全初始化为0,while循环把数据>>右移运算符放到buf中,读不到数据退出
	// char buf[1024] = { 0 };
	// while (ifs >> buf){ 
	// 	cout << buf << endl;
	// }

	// // 第二种 ifstream类提供的方法getline():获取一行 放到buf中 参数1一个字符数组 用于存储数据;参数2 最大要提取的字符数
    // char buff[1024] = { 0 };
	// while (ifs.getline(buf,sizeof(buf))){
	// 	cout << buff << endl;
	// }

	// // 第三种 把数据放到C++的字符串中 getline()系统函数 参数1-输入流 参数2-用于存储的缓冲区
	// string buffer;
	// while (getline(ifs, buffer)){
	// 	cout << buf << endl;
	// }

    // 第四种 每次只获取一个字符,只要能c不是文件末尾 就一直读
	char c;
	while ((c = ifs.get()) != EOF){ // end of file,#define EOF (-1)
		cout << c;
	}

	ifs.close();

}

int main() {
	func();
	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> // 1.包含头文件fstream
#include <string>
using namespace std;


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

//二进制文件  写文件
void func()
{
	//1、包含头文件

	//2、创建输出流对象   也可以用构造函数的 时候打开文件 省略第三步
	ofstream ofs("person.txt", ios::out | ios::binary);
	
	//3、打开文件 
	//ofs.open("person.txt", ios::out | ios::binary);

	Person p = {"张三"  , 18};

	//4、写文件 ostream& write(const char * buffer,int len)
	ofs.write((const char *)&p, sizeof(p));

	//5、关闭文件
	ofs.close();
}


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

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

5.2.2 读文件

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

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

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

示例:

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

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

void func(){
	ifstream ifs("person.txt", ios::in | ios::binary);
	if (!ifs.is_open()){
		cout << "文件打开失败" << endl;
	}

	Person p;
    // istream& read(char *buffer,int len);
	ifs.read((char *)&p, sizeof(p));
	cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}


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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值