• C++学习笔记


    黑马程序员》学习记录

    一、C++ 基础编程

    1、初识
    • 注释:单行注释,多行注释
    • 变量
    • 常量
      • define 宏常量:#define A 10
      • const 修饰的变量:const int A = 10;
    • 关键字
    • 标识符命名规则
      • 不能是关键字
      • 只能由字母、数字、下划线组成
      • 第一个字符不能是数字
      • 标识符中字母区分大小写
    2、数据类型
    • 整型:short(2bytes),int(4bytes),long(win为4bytes,Linux为8bytes),long long(8bytes)
    • sizeof 关键字:统计数据类型所占空间的大小
    • 实型:浮点型,float(4bytes,7位有效数字),double(8bytes,15~16位有效数字)

    默认情况下,C++ 只会输出六位有效数字的小数!!!

    • 字符型:char(1bytes),存放的是 ASCII 值;char a = '1';
    • 转义字符
    • 字符串string a = "1234" (C++风格),char str[] = "1234"(C语言风格)

    C++ 风格的字符串需要引用头文件 # include

    • 布尔类型:1bytes
    • 数据输入:cin >> a;
    3、运算符
    • 加减乘除:两个整数相除得到的依然是整数
    • 取余:小数不能取余
    • 自增自减:前置自增自减效率更高
    • 赋值运算符
    • 比较运算符:cout << (a==b) << endl; 输出表达式时要带括号;
    • 逻辑运算符:C++ 中逻辑运算符返回 true/false(这跟Python中and/or/not有些区别)

    C++:3 && 2 返回 1;Python:3 and 2 返回 2!

    4、程序流程结构
    • 顺序结构
    • 选择结构
      • if 语句
      • 三目运算符:a>b?a:b; 它返回的是变量,不是变量的值,a>b?a:b=100; 是正确的;
      • switch 语句:swicth...case...break...default,尤其注意不能少了 break

    if 和 switch 的区别:

    (1)if 可以判断区间,switch 只能判断整型或字符型,不能是区间;

    (2)switch 结构清晰,执行效率比 if 高;

    Switch 不允许一个 case 使用另一个 case 后声明定义的变量,错误信息为jump bypasses variable initialization

    • 循环结构
      • while:
      • do…while:
      • for:初始化语句只执行一次,判断条件之后进入循环体,循环体执行完后再执行自加语句,再执行判断语句,…
    • 跳转语句
      • break:switch 中终止 case;循环语句中终止最近的循环;
      • continue:在循环语句中,跳过本次循环,继续执行下一次循环;
      • goto:跳转到标记的位置;
    cout << "aaa" << endl;
    goto FLAG;
    cout << "bbb" << endl;	// 被跳过
    cout << "ccc" << endl;	// 被跳过
    FLAG;
    cout << "ddd" << endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    5、数组
    • 数组的内存是连续的

    • 一维数组

      • 定义:
      // 三种定义方式
      int a[10];
      int b[10] = {0, 1, 2, 3};
      int c[] = {0, 1, 2, 3};
      
      • 1
      • 2
      • 3
      • 4
      • 数组名的作用

        • 查看数组的内存大小:sizeof(a)

        • 查看数组的地址:a 返回的就是数组的首地址,也就是 a[0] 的地址;a == &a[0]

        • 注意:数组名是常量(也就是数组的地址),不能修改;a=100 是错误用法!

      • 数组元素个数(长度):int a_length = sizeof(a) / sizeof(a[0]);;字符数组 char a[] = "123" 可以用 strlen

    • 二维数组

      • 定义:
      // 四种定义方式
      int a[2][3];
      int b[2][3] = {{0, 1, 2}, {3, 4, 5}};
      int c[2][3] = {0, 1, 2, 3, 4, 5};
      int d[][3] = {0, 1, 2, 3, 4, 5};
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 数组名的作用
        • 查看二维数组所占用的空间大小:sizeof(a)
        • 获取二维数组的首地址:a 返回的就是二维数组的首地址,也就是 a[0][0] 的地址;a == a[0], a == &a[0][0]
      • 数组的行数、列数:
      int a_row = sizeof(a) / sizeof(a[0]);
      int a_col = sizeof(a[0]) / sizeof(a[0][0]);
      
      • 1
      • 2
    6、函数
    • 参数传递类型:
      • 值传递:
      • 引用传递:传递的是对象的引用,形参和实参是同一对象
      • 地址传递:传递的是对象的地址,形参和实参指向同一对象
    • 函数的声明:main() 中用到的函数必须在它之前有声明或者定义(跟Python不一样),否则报错!可以多次声明。
    • 头文件与源文件
      • 头文件:xxx.h,引入 iostream, std
      • 源文件:xxx.cpp,引用头文件
      • 主函数:int main(),引用头文件
    7、指针
    • 指针的作用:通过指针间接访问内存(指针就是一个内存地址)
    • 指针的定义、赋值、使用
    int a = 10;
    int *p = &a;		// 定义,赋值
    *p = 20;				// 使用指针(解引用:在指针变量前面加一个 * 就是解引用)
    
    • 1
    • 2
    • 3
    • 内存大小:32 位系统占用 4 个字节;64 位系统占用 8 个字节(要获取指针的十进制数值,用 cout << (long)pt << endl;
    • 空指针:指针变量指向内存中编号为 0 的空间,int *p1 = NULL; 此时 p==0;内存编号 0~255 是系统内存,不允许访问
    • 野指针:指针变量指向非法内存空间,例如 int *p = (int *)0x1100;
    • 指针与 const
      • const 修饰指针:常量指针,指针的指向可以更改,指针指向的值不能改;
      • const 修饰常量:指针常量,指针的指向不可以改,指针指向的值可以改;
      • const 修饰指针和常量:指针的指向和指针指向的值都不可以改;
    int a = 10;
    int b = 20;
    int c = 30
    const int * p1 = &a;						// 常量指针,指向的值不能改
    int * const p2 = &b;						// 指针常量,指针的指向不能改
    const int * const p3 = &c;			// 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 指针和数组
      • 数组指针:int (*p)[5];,数组的指针(()的优先级比[]高,所以p会先跟*结合,构成一个指针)
      • 指针数组:int *p[5];,由指针组成的数组([]的优先级比*要高,所以p会先跟[]结合,构成一个数组)
    // 数组跟指针的关系
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = a;						// 指针指向数组,也就是第一个元素
    cout << *p << endl;
    p++;									// 指针指向第二个元素
    cout << *p << endl;
    
    // 数组指针,指针数组
    int (*p1)[5];					// 数组指针,数组的指针
    int *p2[5];						// 指针数组,数组中每一个元素都是指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 指针和函数:形参的地址传递!!!
      • 数组指针的形式传入函数
        • 函数如何定义形参:void func(int *arr),定义一个指针即可
        • 如何传递数组实参:func(arr),直接传入数组名(也就是数组地址)即可
        • 函数内部如何使用数组:arr[i],将指针当做数组名一样使用即可(但是无法使用sizeof(arr)获取数组内存大小)
    8、结构体
    • 概念:用户自定义的数据类型,允许用户存储不同类型的数据
    • 定义:
    struct Student
    {
        string name;
        int grade;
        double math;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 创建变量:
    // 方式一:
    Student a;
    a.name = "张三";
    a.grade = 2;
    a.math = 92.5;
    
    // 方式二:
    Student b = {"李四", 3, 85.5};
    
    // 方式三:在结构体定义的最后创建变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 结构体数组:数组中每个一元素都是结构体类型
    • 结构体指针:通过指针访问结构体对象中的成员,但是要注意访问方式不是.而是->

    .-> 的联系和区别:

    (1)联系:.-> 都可以用来访问成员

    (2)区别:. 是实体对象访问成员用的,左边必须是实体对象;-> 是指针访问成员用的,左边必须是指针;

    • 结构体与『函数–指针–const』:
      • 结构体作为参数传入函数时,使用指针,可以节省内存空间(不必复制一份数据)
      • 结构体指针加一个 const 传入函数时,可以防止在函数内部误操作,修改了结构体的值

    二、C++ 核心编程

    1、内存模型
    • 内存四区:
      • 代码区:存放函数体的二进制代码,由操作系统进行管理;共享,只读
      • 全局区:存放全局变量、静态变量、全局常量(全局区包含常量区),在程序结束后由系统释放
      • 栈区:存放局部变量、局部常量、函数参数值、函数返回值,由编译器自动分配、释放
      • 堆区:存放new、malloc申请的内存,由程序员分配和释放(delete和free)
    // new int(10) 创建整型数据,放在堆区,返回数据的指针
    // int *p 是一个整型指针,放在栈区,指向堆区变量
    int *p = new int(10);				// new 创建变量
    int *arr = new int[10];			// new 创建数组
    
    • 1
    • 2
    • 3
    • 4

    不要返回局部变量的地址!!!编译器在局部变量使用完以后,会保留一次局部变量的使用权,所以在外部第一次使用局部变量的地址时,不会出错;但是这一次机会用完以后,放在栈区的局部变量就被销毁掉了,第二次再使用就会出错!

    • 程序运行前有代码区、全局区的概念,程序运行后才会区分栈区、堆区
    • new/delete 的使用:
      • new 返回的是所创建的数据的指针,可能是整数的指针,也可能是数组的指针
      • delete 释放数组需要加一个 []
    delete p;
    delete[] arr;		// 堆区数组的释放要加一个 []
    
    • 1
    • 2
    2、引用
    • 引用的本质:给变量起别名,在 C++ 内部它的实现是一个指针常量
    int a = 10;
    int &ref_a = a;						// a 的引用
    int *const pt_a = &a;			// a 的指针常量,其实就是引用,它的值跟 &a、&ref_a 是完全一致的
    
    • 1
    • 2
    • 3
    • 引用的注意事项:(1)引用必须初始化;(2)引用初始化以后不能更改
    int a = 10;
    int &b = a;		// b 是 a 的别名
    int c = 20;	
    b = c;				// 这个并不是改变引用的对象,而是一个赋值操作,将 b 所代表内存中的数据改为 c 的值,之后 a/b/c 都等于 20
    
    • 1
    • 2
    • 3
    • 4
    • 引用做函数形参:引用传递,被调函数的形参作为局部变量在栈中开辟内存空间,在内存中存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

    地址传递:变量,独立;可变,可空;替身,无类型检查;

    引用传递:别名,依赖;不变,非空;本体,有类型检查;

    • 引用做函数返回:
      • 函数调用可以作为左值
      • 不要返回局部变量的引用!!!(这同上面说的,不要返回局部变量的地址,是一样的)

    总结:引用传递和地址传递,函数形参如何定义?函数调用如何传参?

    关键:形参定义方式跟引用、指针的定义一样,函数传参方式跟引用、地址的赋值一样!!!

    实例:

    • 引用传递:
      • 形参定义:int function(int &a)
      • 函数传参:function(aa),接收参数就类似于 int &a = aa;,这其实就是引用的定义与赋值;
    • 地址传递:
      • 形参定义:int function(int *a)
      • 函数传参:function(&aa),接收参数就类似于 int *a = &aa;,这其实就是地址的定义与赋值;
    • 常量引用:一般用在函数形参,防止误操作!它的性质是『只读,不可修改』
    • 引用的对象必须是一个合法的内存空间(栈区,堆区)!!!
    int &ref_a = 10;				// 这是错误的,不能直接引用一个数值,数值是在常量区,不在栈区,也不在堆区
    const int &ref_a = 10;	// 这是对的,它相当于创建了一个缓存变量 tmp,再引用它:int tmp=10; const int &ref_a=tmp;
    
    • 1
    • 2
    3、函数高级
    • 函数默认参数:函数声明和函数实现只能有一个有默认参数!如果两个都有默认参数,编译器会出现重定义错误
    • 函数占位参数:
      • 在调用时,占位参数也必须有对应的实参;
      • 占位参数也可以有默认值;
    int func(int a, int);				// 这里的第二个参数就是占位参数
    func(10, 20);								// 调用时必须传实参
    int func1(int a, int = 10);	// 占位参数的默认值
    
    • 1
    • 2
    • 3
    • 函数重载:函数名相同,函数参数不同(类型不同,或个数不同,或顺序不同)
      • 函数返回值不同不能作为函数重载的条件!
      • 引用作为函数重载的条件:void func(int &a)void func(const int &a) 构成函数重载
      • 函数重载碰到默认参数:void func(int a)void func(int a, int b=10) 不能构成函数重载
    4、类和对象

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

    (1)封装
    • 权限问题

      • public:类内可以访问,类外也可以访问
      • private:类内可以访问,类外不可以访问
      • protect:类内可以访问,类外不可以访问
      • protectedprivate的区别体现在继承中,子类可以访问父类的protected成员,但不可以访问private成员
    • classstruct的区别class默认权限是privatestruct默认权限是public

    • 运算符重载bool operator==(const Cube &c);

    (2)对象特性
    a. 构造函数
    • 按照参数分类:有参构造,无参构造
    • 按照类型分类:普通构造,拷贝构造
      • 默认构造函数:Person();
      • 拷贝构造函数Person(const Person &p);,它的调用时机有以下三种:
        • 使用一个已经创建好的对象初始化另一个新的对象:Person p2(p1);
        • 函数形参使用值传递
        • 函数以值的方式返回局部对象
    Person p1;							// 调用默认构造函数
    Person p2();						// 编译器会认为是一个函数的申明,而不是调用默认构造函数
    // 1. 括号法
    Person p2(10);					// 调用有参构造函数
    Person p2(p1);					// 调用拷贝构造函数
    // 2. 隐式法
    Person p3 = 10;					// 调用有参构造函数
    Person p3 = p2;					// 调用拷贝构造函数
    // 3. 显式法
    Person p3 = Person(10);	// 调用有参构造函数
    Person p3 = Person(p2);	// 调用拷贝构造函数
    // 注意事项:
    Person(10);			// 匿名对象,在此行执行完后会立即调用析构函数,销毁该内存
    Person(p3);			// 错误,编译器会将其视为 Person p3; ,而 p3 在上面定义过了,所以会报错
    								// 因此,不要用拷贝构造函数初始化一个匿名对象!!!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建一个类,编译器至少会添加四个函数:

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

    但是,如果自定义了有参构造函数,编译器则不会提供默认构造函数,但是会提供拷贝构造函数,此时Person p;报错;

    如果自定义了拷贝构造函数,编译器不再提供任何构造函数,需要自定义有参构造函数、默认构造函数;

    • 析构函数:主要的功能是,将在堆区开辟的内存空间手动释放掉!如果堆区内存不手动释放,会造成内存泄漏。
    • 浅拷贝与深拷贝:
      • 浅拷贝:一般的赋值、拷贝都是浅拷贝
      • 深拷贝:new在堆区开辟空间做的是深拷贝
      • 注意:如果类中有在堆区开辟的属性,一定要在析构函数中释放,进而要在拷贝构造函数中用new做深拷贝
    • 初始化类属性列表:只针对于类中的函数、属性而言

    当A类对象作为B类属性时,创建一个B类对象,会先调用A类构造函数,再调用B类构造函数;析构函数则相反!

    b. 静态成员
    • 静态成员变量
      • 所有对象共享同一份数据(不属于某一个对象,所以其访问方式可以是通过对象,也可以直接用类去访问)
      • 在编译阶段分配内存(在全局区)
      • 类内声明,类外初始化(为什么必须要初始化???)
      • 有访问权限的区别,private 静态成员变量在类外无法直接访问,public 可以
    class Person{
      public:
      	static int m_A;			// 类内声明;权限为 public
    }
    
    int Person::m_A = 10;		// 类外初始化
    
    int main(){
      Person p;
      cout << p.m_A << endl;				// 通过对象访问
      cout << Person::m_A << endl;	// 通过类访问
      
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 静态成员函数
      • 所有对象共享同一个函数(跟静态成员变量一样,可以通过对象访问,也可以通过类直接访问)
      • 只能访问静态成员变量
    class Person{
      public:
      	static int test_static_value;
      	static void test_static_func(){
          cout << test_static_value << endl;	// 这里不能使用 this 指针,this 指针只能用于非静态成员函数内部
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    c. this指针
    • 其本质是一个指针常量!
    • 成员变量和成员函数分开存储,只有非静态成员变量才属于对象上
      • C++ 编译器给每一个空对象分配一个字节的内存空间,有一个独一无二的内存地址;也就是说,如果对象 p 是空对象,那么 sizeof(p) 就等于 1;
      • 通过 sizeof 函数可以查看对象的大小,非空对象的大小只跟它内部的非静态成员变量大小有关;
    class Person{
      public:
      	int aa;
    }
    Person p;
    cout << sizeof(p) << endl;	// 输出为4,因为对象 p 内部只有一个整型的非静态成员变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 非静态成员函数只有一份函数实例,存在代码区,所有的对象共用这一个函数实例,那如何区分是哪个对象调用了的这个非静态成员函数呢?就是用 this 指针去区分的。

    • this 指针指向被调用非静态成员函数所属的对象,也就是说:如果有 Person p; 这样一个对象,当用 p 调用非静态成员函数 func() 时,func() 内部会有一个默认的 this 指针,它指向对象 p(不会指向其他的 Person 对象)。

    • this 指针的作用:

      • 解决非静态成员变量跟函数形参之间的名称冲突
      • 返回对象本身(使用*this,返回类型可以是值,也可以是引用,二者是有区别的)
    • 空指针调用成员函数:成员函数如果使用了成员变量,此时this指针会为空,导致调用失败;一般的解决方式是,在成员函数中加一个指针为空的判断,使得成员函数更加健壮。

    class Person{
      public:
      	int age;
      	void func(){
          if(this == NULL){						// 判断this指针是否为空
            return;										// 如果不加这个判断,后面空指针调用该成员函数时,下面的代码会崩掉
          }
          cout << this->age << endl;	// 用到了成员函数
        }
    }
    Person *p = NULL;
    p.func();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 常函数:const修饰的成员函数,其内部的成员属性不能修改,除非使用关键字mutable修饰
      • 常函数的const修饰的是this指针的指向,本来this指针就是指针常量,也就不能修改它的指向,现在再用一个const修饰它的指向,也就是说它指向的值也不能修改,即成员变量不能修改!
    • 常对象:对象声明前加const修饰,它只能调用常函数,只能修改mutable修饰的成员变量;
    class Person{
      public:
      	int m_A;						// 常函数中不可修改的成员变量
      	mutable int m_B;		// 常函数中可以修改的成员变量
      	void func() const{	// 常函数
          this->m_B = 10;		// 常函数中只能修改有mutable的成员变量
        }
    }
    const PersonX p;				// 常对象
    p.m_B = 10;							// 常对象可以修改有mutable的成员变量
    p.func();								// 常对象只能调用常函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    (3)友元

    让函数和类,能够访问另一个类的私有成员;使用 friend 关键字

    • 全局友元函数:将全局函数声明写在类里面,前面加一个friend修饰
    • 友元类:将类的声明写在另一个类里面,前面加一个friend修饰
    • 成员友元函数:将类的成员函数写在另一个类里面,前面加一个friend修饰;
    class Building
    {
        friend void building_friend(Building b); 	// 友元全局函数
        friend class GoodGay01;                   // 友元类
        friend void GoodGay02::visit(); 					// 友元成员函数
    
    private:
        string bed_room;
    
    public:
        string setting_room;
        Building();
        ~Building();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 要注意的一个点是:如果A类是B类的友元,A在定义B类对象时,B类必须在它之前有定义,或者是有声明;如果只有一个声明,那么在A类中定义B类对象时,就只能用指针或者引用,因为编译到这里时还没有发现B类的定义,不知道该类的内部成员,没有办法具体地构造一个对象。(参考
    class Building;
    class GoodGay
    {
    private:
        Building *b;		// 这个地方只能用指针或者引用,在构造函数中相应的要用new手动创建对象
    
    public:
        GoodGay();
        ~GoodGay();
    
        void visit();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    (4)运算符重载
    a. 加号运算符+
    • 可以通过成员函数重载(推荐),也可以通过全局函数重载
    Person operator+(Person p){...}								// 成员函数重载,跟类有关,本质是 p1.operator+(p2)
    Person operator+(Person p1, Person p2){...}		// 全局函数重载,跟类无关,本质是 operator+(p1, p2)
    
    • 1
    • 2
    b. 左移运算符<<
    • 最常用的用途是输出某种类型的数据,cout << ... << endl;
    • 对于自定义类,通常不会利用成员函数重载<<,因为这样没办法实现cout在左边,所以只能用全局函数来重载<<
    • 全局函数重载<<时,又不能访问类中的私有成员变量,所以要将全局函数变为友元;
    # 在.h中写全局函数的声明
    class Person{
      friend ostream &operator<< (ostream &cout, Person &p);	// 将重载函数变为类的友元,方便访问私有成员
    Private:
      int m_A;
      int m_B;
    }
    
    # 在.cpp中写全局函数的实现
    ostream &operator<< (ostream &cout, Person &p){						// 返回类型是ostream,这是输出流对象的类型
      cout << "m_A: " << p.m_A << " m_B: " << p.m_B;					// 自定义输出内容、形式,也就是重载的核心功能
      return cout;																						// 将cout返回,方便链式输出
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 注意:
      • 一:左移运算符的重载模板比较固定,函数类型是全局友元函数,函数参数是两个引用,函数返回值是引用ostream使用引用,是为了全局只使用一个cout输出流对象;自定义类对象使用引用,是为了输出对象自身(如果使用值传递,那么它会调用拷贝构造函数,复制一份新的数据)。
      • 二:重载函数的声明与定义写在.h头文件中,实现最好写在.cpp源文件中,这跟C++中头文件和源文件的使用方式有关,如果实现也写在头文件中,在两个源文件main.cpptest.cpp都包含头文件的话,编译会报错,相当于重载函数被重定义了;分开写的话则不会有这个问题。(参考1参考2
    c. 递增运算符++
    • 前置递增运算符:先自加,再返回,所以它可以返回自身;Person &operator++();
    • 后置递增运算符:先返回,再自加,它返回的是原始值,并不是自加后的自身;Perosn operator++(int);
    class Perosn{
    private:
      int m_A;
      int m_B;
    
    public:
      Perosn &operator++();			// 前置递增运算符重载,返回的是引用,形参为空
      Person operator++(int);		// 后置递增运算符重载,返回的是值,形参需要一个int占位参数(声明为后置),且必须是int
    }
    
    // 前置递增运算符重载的实现
    PersonX &PersonX::operator++()
    {
        ++(this->m_A);					// 属性递增
        ++(this->m_B);					// 属性递增
        return *this;						// 返回自身的引用
    }
    
    // 后置递增运算符重载的实现
    PersonX PersonX::operator++(int)
    {
        PersonX tmp = *this;		// 先保存原始值,后面需要返回
        (this->m_A)++;					// 属性递增
        (this->m_B)++;					// 属性递增
        return tmp;							// 返回原始值
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 注意:
      • 一:因为后置递增运算符重载函数的返回是一个临时对象tmp,它只能是值传递,不能是引用传递;
      • 二:因为后置递增运算符重载函数的返回只能是值传递,那么左移运算符在调用后置递增运算符的结果时,传入的参数类型就不能是引用,所以它只能定义为friend ostream &operator<<(ostream &cout, Person p);,不能用Perosn &p
    d. 赋值运算符=
    • 如果有成员是在堆上创建的,就必须让赋值操作符重载,因为默认的赋值操作符是浅拷贝,会发生内存泄漏;

    • 赋值操作符重载形式:Perosn &operator=(Perosn &p);

    class Person{
    private:
      int *m_A;
    public:
      Person(int a){
        m_A = new int(a);
      }
      
      Person &operator=(Perosn &p);		// 赋值运算符重载定义
    }
    
    Person &operator=(Person &p){
      if (this->m_A != NULL){
        delete this->m_A;							// 一定要注意,如果堆区内存还存在,要先释放,防止内存泄漏
      }
      
      this->m_A = new (*p.m_A);				// 将赋值操作转移到堆区
      return *this;										// 返回自身,方便进行链式赋值(a=b=c)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    e. 关系运算符
    • 基本形式:bool operator==(Person &p);
    f. 函数调用运算符()
    • 重载之后的函数称为仿函数
    // 案例一:打印类的函数调用运算符重载
    class MyPrint{
    public:
      void operator()(string s){
        cout << s << endl;
      }
    }
    MyPrint mp;
    mp("hello world!");				// 因为它的使用方式很像函数调用,但实际上它是一个运算符,所以叫做仿函数
    
    // 案例二:加法类的函数调用运算符重载
    class MyAdd{
    public:
      int operator()(int a, int b){
        return a + b;
      }
    }
    int a = b = 10;
    MyAdd md;
    int c = md(a, b);					// 两个形参,返回整型值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    (5)继承
    • 基本语法:class FootballPlayer : public PersonXclass 子类 : 继承方式 父类

    • 继承方式:

      • 公共继承:对于父类中的成员,public还是publicprotect还是protectprivate不可访问
      • 保护继承:对于父类中的成员,public变为protectprotect还是protectprivate不可访问
      • 私有继承:对于父类中的成员,public还是privateprotect变为privateprivate不可访问
      image-20221013201926772
    • 继承中的对象模型:

      • 父类中所有的非静态成员都会被子类继承下来,私有成员虽然不能访问,但是只是被编译器隐藏了,实际上是继承了的
      • 具体对象占用多大内存,可以通过sizeof()查看

      VS 中可以使用 cl 工具看对象内存模型(参考cl /d1 reportSingleClassLayout类名 文件名),Mac+VSCode 要怎么看???

    • 继承中的构造和析构顺序:先调用父类构造函数,再调用子类构造函数;析构顺序相反。

    • 继承中同名成员处理:

      • 子类对象访问同名的子类成员:直接.出来即可,son.age
      • 子类对象访问同名的父类成员:需要指定作用域才能访问,son.Base::age
      • 对于同名的成员函数,子类的同名成员函数会隐藏掉父类中所有的同名成员函数,要访问的话需要加作用域(对于不同名的父类成员函数,可以直接访问)
      • 静态成员函数也是一样的处理;
    • 多继承语法:class 子类 : 继承方式 父类1, 继承方式 父类2, ...

    • 菱形继承

      • B和C都继承自A,D则同时继承了B和C,此关系称为菱形继承
      • 问题:对于A中的成员,B和C都会继承,D则会同时继承B和C中的成员,存在两份同样的数据,重复了
      • 解决:虚继承可以解决菱形继承带来的数据重复问题(数据只有一份,通过vbtablevbptr来访问)
        • vbtable :虚基类表
        • vbptr:虚基类指针,指向vbtable
      image-20221016181457189
    // 菱形继承
    class Animal{
    public:
      int m_Age;
    }
    class Sheep : public Animal{}
    class Tuo : public Animal{}
    class SheepTuo : public Sheep, public Tuo{}
    SheepTuo st;
    st.Sheep::m_Age = 18;				// 菱形继承时需要加作用域才能访问继承成员
    st.Tuo::m_Age = 28;					// 此时 st 中有两份 m_Age,分别来自于 Sheep、Tuo
    
    // 虚继承解决菱形继承的问题
    class Animal{
    public:
      int m_Age;
    }
    class Sheep : virtual public Animal{}					// 虚继承
    class Tuo : virtual public Animal{}						// 虚继承
    class SheepTuo : public Sheep, public Tuo{}
    SheepTuo st;
    st.Sheep::m_Age = 18;				// 第一次修改 st 的 m_Age
    st.Tuo::m_Age = 28;					// 第二次修改 st 的 m_Age,修改的是同一份数据
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    (6)多态
    • 多态的类型:

      • 静态多态:主要有两种,函数重载、运算符重载
      • 动态多态:主要是一种,继承类与虚函数
      • 区别:
        • 静态多态的函数地址早绑定,也就是在编译阶段就确定了函数地址
        • 动态多态的函数地址晚绑定,也就是在运行阶段才确定函数地址
    • 动态多态:

      • 动态多态的实现
        • 条件一:子类要继承父类,必须有继承关系
        • 条件二:子类要重写父类的虚函数,所谓重写就是返回值、函数名、参数都一样
    class Animal
    {
    public:
        virtual void speak(); // 虚函数
    };
    
    class Cat : public Animal
    {
    public:
        void speak(); // 重写虚函数,不需要再加virtual
    };
    
    class Dog : public Animal
    {
    public:
        void speak(); // 重写虚函数,不需要再加virtual
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 动态多态的本质
      • 首先父类中有虚函数存在时,其对象就会有一个vfptr,指向vftable中的虚函数入口地址

        • vfptr:虚函数指针,属于对象内存模型中的一部分
        • vftable:虚函数表,里面存储的是虚函数的入口地址(父类是父类的,子类是子类的)
      • 其次,当子类继承父类时,它也会继承vfptr,但是这个vfptr指向的是子类的vftable,不再是父类的vftable,它的实现也就是子类的实现(也就是,子类重写虚函数时,它的vftable就会替换成子类虚函数实现的入口地址)

      • 从下面的对象模型可以看到:

        • 父类对象模型确实多了一个vfptr
        • 如果不重写虚函数,子类就会直接继承父类的vfptr,它指向的还是父类的vftable,函数实现也是父类的
        • 如果子类重写了虚函数,那么它的vftable就会被替换成子类自己的虚函数表,其函数实现就是子类自己的
    iShot_2022-10-18_20.31.55 iShot_2022-10-18_20.31.55 iShot_2022-10-18_20.33.07
    • 纯虚函数和抽象类:

      • 纯虚函数:虚函数声明后面加=0,不需要实现
      • 抽象类:包含纯虚函数的类,有两个特点:
        • 抽象类不可以实例化对象
        • 抽象类的子类必须要重写父类中的纯虚函数,否则子类也属于抽象类
    • 虚析构和纯虚析构:

      • 虚析构:析构函数定义为虚函数

        • 使用场景:子类中有成员创建在堆区,且父类指针指向子类对象,当手动delete父类指针时,父类指针无法调用子类析构函数,子类对象在堆区的成员就无法被释放掉,会造成内存泄露

        • 使用方式:将父类析构函数定义为虚函数

        • 注意事项:抽象类(有纯虚函数的父类)的析构函数不能是虚函数(只能是纯虚函数???)

        class Animal
        {
        public:
            Animal()
            {
                cout << "Animal构造函数" << endl;
            }
            virtual ~Animal() // 虚析构
            {
                cout << "Animal析构函数" << endl;
            }
            virtual void speak(){}; // 虚函数
        };
        
        Animal *animal = new Cat("Tom");	// 父类指针指向子类对象(在堆区)
        animal->speak();									// 调用子类重写的虚函数
        delete animal;										// 手动释放内存(不然无法调用析构函数)
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
      • 纯虚析构:析构函数定义为纯虚函数

        • 纯虚析构的作用跟虚析构是一样的,都是为了让父类指针调用子类对象的析构函数,从而释放子类对象的堆区成员
        • 纯虚析构需要写函数实现,这一点跟纯虚函数是不一样的
        • 拥有纯虚析构的类也属于抽象类,无法实例化对象
        class Animal
        {
        public:
            Animal()
            {
                cout << "Animal构造函数" << endl;
            }
            virtual ~Animal() = 0;    // 纯虚析构
            virtual void speak() = 0; // 纯虚函数
        };
        
        // 纯虚析构必须要写函数实现
        Animal::~Animal()
        {
            cout << "Animal析构函数" << endl;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
    5、文件操作
    (1)文本文件读写

    C++ 操作文件需要包含 fstream 头文件,主要有三个大类:

    • ofstream将数据写入文件操作,out输出到文件
    #include 
    ofstream ofs;
    ofs.open("文件路径", 打开方式);
    ofs << "写入内容" << endl;
    ofs.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • ifstream从文件中读取数据
    #include 
    ifstream ifs;
    ifs.open("文件路径", 打开方式);
    if(ifs.is_open()){
      // 四种读取方式
      ifs.close();
    }
    
    // 第一种读取方式
    char buf[1024] = {};	// 读取到字符数组
    while (ifs >> buf) 		// 逐行读入到buf中,读完返回false
    {
      	cout << buf << endl;
    }
    
    // 第二种读取方式
    char buf[1024] = {};
    while (ifs.getline(buf, sizeof(buf)))
    {
    		cout << buf << endl;
    }
    
    // 第三种读取方式
    string buf;								// 读取到字符串
    while (getline(ifs, buf))	// 逐行读取
    {
    		cout << buf << endl;
    }
    
    // 第四种读取方式
    char c;						   // 读取到字符中
    while ((c = ifs.get()) != EOF) // 逐个字符读取
    {
    		cout << c;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • fstream:读写文件数据

    文件打开方式:打开方式可以配合使用,使用 | 连接就可以

    打开方式解释
    ios::in以只读方式打开文件
    ios::out以写入方式打开文件
    ios::ate打开文件,且初始位置在文件尾
    ios::app以追加方式写入文件
    ios::trunc如果文件存在,则先删除再创建
    ios::binary二进制方式
    (2)二进制文件读写

    打开方式指定为ios::binary,写入方式使用成员函数ostream& write(const char *buffer, int len);

    ofstream ofs;
    ofs.open("test04.txt", ios::binary | ios::out);
    PersonX p("程", 28, 173);
    ofs.write((const char *)&p, sizeof(p));
    ofs.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    读二进制文件使用成员函数istream& read(char *buffer, int len);

  • 相关阅读:
    HCIA --- 动态路由协议之OSPF
    【opencv】传统图像识别:hog+svm实现图像识别详解
    【数据库】MySQL的事务特性与隔离级别
    MES必懂知识,市场需求下的生产管理系统
    上帝视角看Vue源码整体架构+相关源码问答
    STC单片机17——adc 8032
    广告原生化发展,助力开发者收益更上一层楼
    JavaWeb核心(2)
    响应式系统与react笔记 | 青训营笔记
    【Python】去除列表中的重复元素
  • 原文地址:https://blog.csdn.net/DaGongJiGuoMaLu09/article/details/127622565