cpp学习笔记1

C++

学习资源来自bilibili黑马

使用编译器visual studio 2022

#include <iostream>
using namespace std;

int main() {
	cout << "Hello world !!!" << endl;
	return 0;
}

编译器使用时发现程序经过调试后运行窗口一闪而过:

​ (1)项目->属性->配置属性->连接器->系统->子系统设置为“控制台 (/SUBSYSTEM:CONSOLE)”

​ (2)使用调试->开始执行(不调试)而不是调试器

数据类型 - 字符串

//c语言中表示字符串只能使用字符串数组
char str[] = "abc";
//c语言提供的库<string.h>也只是包含了一些对字符串数组进行某些功能实现的函数

//而c++语言中既可以用c语言中的方法表示字符串
char str[] = "abc";
//又可以通过引用库<string>中的string数据类型来表示字符串
#include<string>
string str = "abc";

函数 - 函数的分文件编写及使用

步骤

  • 创建(名称).h后缀名的头文件
  • 创建(名称).cpp后缀名的源文件
  • 在头文件中引入iostream,namespace以及函数的声明(带上分号)
  • 在源文件中引入“(名称).h”以及函数的定义
  • 在需要使用的源文件中引入“(名称).h”即可使用该头文件中所声明的函数
//swap.h
#pragma once

#include<iostream>
using namespace std;

void swap(int * a, int * b);
//swap.cpp
#include"swap.h"

void swap(int * a, int * b) {

	int temp = 0;

	temp = * a;
	* a = * b;
	* b = temp;
    
}
//main.cpp
//由于swap.h头文件中已经包含标准输入输出库和命名空间,因而不需要重复引入
#include"swap.h"

int main() {

	int a = 10;
	int b = 20;

	swap(&a, &b);

	cout << a << endl << b << endl;

	return 0;

}

指针 - const修饰

  • const修饰指针,常量指针
//指针指向的值不可以改变,指针的指向可以改变
const int * p;
  • const修饰常量,指针常量
//指针指向的值可以改变,指针的指向不可以改变
int * const p;
  • const同时修饰指针和值
//指针指向的值和指向都不可以改变
const int * const p;

程序的内存模型

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

  • 全局区:存放全局变量,静态变量和部分常量

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

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

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

常量分为字符串常量和const修饰的变量

  • 字符串常量
"hello world"//这就是字符串常量
cout << &"hello world" << endl;//输出其地址
  • const修饰的变量
    • const修饰的全局变量
    • const修饰的局部变量

const修饰的全局变量(全局常量)在全局区

const修饰的局部变量(局部常量)不在全局区

程序运行前:

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

  • 代码区
    • 存放cpu执行的机器指令
    • 共享的,对于被频繁执行的程序,只需要在内存中有一份代码即可
    • 只读的,防止被程序意外修改
  • 全局区
    • 全局变量和静态变量存放在此
    • 还包含常量区,字符串常量和const修饰的全局变量
    • 该区域的数据在程序结束后由操作系统释放

程序运行后:

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

    💡不要返回局部变量的地址

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

    • 在cpp中主要利用new来在堆区开辟内存,返回该数据类型的指针
    int * p = new int(10);
    
    • delete来进行手动释放

引用

引用的基本使用

作用:给变量起别名,共用同一块空间

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

注意事项

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

引用作为函数参数

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

//引用传递
void swap(int & a, int & b) {
    //形参a,b分别为实参a,b的别名
    int temp = a;
    a = b;
    b = temp;
}
...
int a = 10;
int b = 20;
swap(a, b);//即可实现交换

引用作为函数返回值

  • 不能返回局部变量的引用
int& test() {
    int a = 10;
    return a;//错误,a创建在栈区,调用结束内存被释放
}
//尽管部分编译器依旧可以正常打印,但这是非法操作
  • 可以调用函数作为左值
int & fun() {
    static int a = 10;//静态变量在全局区
    return a;
}
...
int & a = fun();//作为右值
fun() = 20;//作为左值

引用的本质

本质:实现一个指针常量

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

常量引用

作用:常量引用主要来修饰形参,防止实参被形参修改

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

函数提高

函数默认参数

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

void fun(int a, int b = 10, int c = 10){}
  • 如果某个位置参数被赋予了默认值,那么随后的参数都应该被赋予默认值
  • 在声明函数和实现函数中,只允许其一实现默认参数

函数占位参数

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

void fun(int a, int){}
...
fun(10)//报错
fun(10, 10);

函数重载

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

函数重载满足条件

  • 同一作用域下
  • 函数名相同
  • 函数参数数据类型不同或者个数不同

函数重载注意事项

  • 函数返回值的类型不同不可以作为函数重载的条件
  • 引用作为重载条件
void fun(int & a){}
void fun(const int & a){}
...
int a = 10;
fun(a);//fun(int & a)
fun(10);//fun(const int & a)
  • 在进行函数重载时,尽量避免使用默认参数

类和对象

面向对象:封装,继承,多态

封装

属性和行为作为整体

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

属性:成员属性,成员变量

行为:成员函数,成员方法

const double PI = 3.14;

class Circle {
public://访问权限
	int radius;//属性
	double getPerimeter() {//行为
		return 2 * PI * radius;
	}
    
};
...
Circle c;
c.radius = 10;
cout << c.getPerimeter() << endl;
访问权限
公共权限public成员类内可以访问,类外可以访问儿子可以访问父亲的公共内容
保护权限protected成员类内可以访问,类外不可以访问儿子可以访问父亲的保护内容
私有权限private成员类内可以访问,类外不可以访问儿子不可以访问父亲的私有内容

struct与class的区别

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

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

对象特性

构造函数和析构函数

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

构造函数:在创建对象时为对象成员属性赋值

语法:类名(){}

  • 没有返回值也不写void
  • 函数名和类名相同
  • 构造函数可以有参数,可以发生重载
  • 对象在创建时会自动调用构造函数,且只调用一次

析构函数:在销毁对象前执行一些清理工作

语法:~类名(){}

  • 没有返回值也不写void
  • 函数名和类名相同,并且在名称前加~
  • 析构函数不可以有参数,不可以发生重载
  • 对象在销毁前会自动调用析构函数,且只调用一次
class Person {
public:
	Person() {
        //构造函数
    }

	~Person() {
        //析构函数
    }
};
构造函数的分类及调用

分类

  • 按照参数分类
    • 无参构造(默认构造)
    • 有参构造
  • 按照类型分类
    • 普通构造
    • 拷贝构造
class Person {
public:
    //默认无参构造函数
    Person() {
    }
    //有参构造函数
    Person(int a) {
        age = a;
    }
    //拷贝构造函数
    Person(const Person & p) {
        age = p.age;
    }
    //析构函数
    ~Person() {
    }
private:
    int age;
};

调用

  • 括号法
Person p1;//调用默认构造调用
Person p2(10);//调用有参构造
Person p3(p2);//调用拷贝构造

💡使用默认构造函数的时候,不要加()

Person p1();编译器会认为这是一个函数的声明,不会认为在创建对象

  • 显示法
Person p1 = Person(10);//调用有参构造
Person p2 = Person(p1);//调用拷贝构造

匿名对象Person(10);

特点:当前语句结束后,系统会立即回收掉匿名对象

💡不要利用拷贝构造函数初始化匿名对象eg.Person(p1);

  • 隐式转换法
Person p1 = 10;//调用有参构造
Person p2 = p1;//调用拷贝构造

拷贝构造函数的调用时机

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
构造函数的调用规则

默认情况下,c++编译器至少给一个类添加默认构造函数,默认析构函数和默认拷贝构造函数三个函数

  • 如果定义有参构造函数,c++不会再提供默认无参构造函数,但是会提供默认拷贝构造函数
  • 如果定义拷贝构造函数,c++不会再提供其他构造函数(包括无参和有参)
深拷贝和浅拷贝

浅拷贝:简单的赋值拷贝操作,容易导致堆区内存重复释放

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

💡如果属性有在堆区开辟的,一定要自己提供拷贝构造函数

初始化列表

作用:初始化类属性,构造函数的拓展

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

class Person {
public:
    //作为无参构造器
    Person() : p_a(10), p_b(20), p_c(30) {
        
    }
private:
    int p_a;
    int p_b;
    int p_c;
};
class Person {
public:
    //作为有参构造器
    Person(int a, int b, int c) : p_a(a), p_b(b), p_c(c) {
        
    }
private:
    int p_a;
    int p_b;
    int p_c;
};
类对象作为类成员
class A {};
class B {
    A a;//对象成员
};
静态成员

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

访问方式:通过对象,通过类名

都有访问权限的限制

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存(全局区)
    • 类内声明,类外初始化
class Person {
public:
    static int a;//静态成员变量
};
...
//通过对象 
Person p;
p.a = 10;
//通过类名 
int Person::a = 10;//函数外
Person::a = 10;//函数内
  • 静态成员函数
    • 所有对象共享一个函数
    • 静态成员函数只能访问静态成员变量
成员变量和成员函数分开储存

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

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

空对象占用内存空间为1

相关内存中占用字节数计算涉及内存对齐相关知识

this指针的用途

this指针指向被调用的成员函数所属的对象,只存在于非静态成员函数中

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

本质:指针常量类名 * const this;

用途

  • 用于区分形参和成员变量
  • 在类的非静态成员函数中返回对象本身return *this;
class Person {
public:
    Person() {}
    
    Person(int age) {
        this->age = age;//形参传值给成员变量
    }
    
    //&实现链式编程思想
    Person & personAddAge(Person p) {
        this->age += p.age;
        //返回对象本身
        return *this;
    }
private:
    int age;
};
...
Person p1(10);
Person p2(10);
//链式编程思想
p2.personAddAge(p1).personAddAge(p1)...;

链式编程思想的实现&

如果让personAddAge成员函数仅仅返回Person数据类型,则会发现无论多少次进行叠加p2只使得其最多只进行一次追加

这是因为当成员函数仅仅返回Person数据类型时会被进行拷贝构造,使得返回的对象是一个新的拷贝而来的对象,和原来对象没有任何联系,因此无论怎样累加只有第一次才是真正对原来对象的累加,而后面都是对其他新拷贝对象的累加

而返回Person &数据类型使得返回的拷贝对象都是对原来对象的引用,因而此后的累加都是对原对象的累加,以实现链式编程思想

空指针访问成员函数

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

如果用到则需要加以判断以保证代码的健壮性

if(this == NULL) {
    return;
}
const修饰成员函数

常函数

  • 成员函数后加const使得该函数成为常函数
void show() const {
    //函数后面的const相当于对this值的修饰
    //使得函数内this的值和指针都被const修饰
    //因而不能修改其他成员属性
}
  • 常函数内不可以修改成员属性

    Exception:当成员变量被mutable关键字修饰后,在常函数中依旧可以修改

常对象

  • 声明对象前加const使得该对象成为常对象
const Person p;
  • 常对象只能调用常函数

友元

作用:使得一个函数或者类访问另一个类中的私有成员

使用friend关键字

友元实现的三种方式

  • 全局函数作友元
class Home {
    //将全局函数的声明放入访问类中,并带上friend关键字
    friend void goodGuy(Home* home);
private:
    string m_bedroom;
};

void goodGuy(Home* home) {
    cout << home->m_bedroom << endl;
}
  • 类作友元
friend class 类名;
  • 成员函数作友元
friend 返回值类型 类名::函数名();

类外写成员函数,构造函数

方法:类内声明,类外编写

语法:类名::函数名

class Home {
//需要公共权限
public:
 //构造函数声明
 Home();
 //成员函数声明
 void goodGuy(Home* home);
};

Home::Home() {}
void Home::goodGuy(Home* home) {}

运算符重载

加号运算符重载

实现Person p3 = p1 + p2;

  • 成员函数重载加号
  • 全局函数重载加号
class Person {
    friend Person operator+(Person p1, Person p2);
public:
    Person() {}
    
    Person(int a, int b) {
        this->a = a;
        this->b = b;
    }
    //成员函数重载加号
    Person operator+(Person p) {
        Person temp;
        temp.a = this->a + p.a;
        temp.b = this->b + p.b;
        return temp;
    }
private:
    int a;
    int b;
};
//全局函数重载加号
Person operator+(Person p1, Person p2) {
    Person temp;
    temp.a = p1.a + p2.a;
    temp.b = p1.b + p2.b;
    return temp;
}
...
Person p1(10, 10);
Person p2(20, 20);
Person p3 = p1 + p2;
//成员函数Person p3 = p1.operator+(p2);
//全局函数Person p3 = operator+(p1, p2);
左移运算符重载

实现只对类的直接打印cout << p << ...

因为左移运算符的左边是cout,因而每一次的返回值应该使用cout所在类,并且应该使用全局函数重载法

cout转定义可知,其属于ostream类中

_EXPORT_STD extern "C++" __PURE_APPDOMAIN_GLOBAL _CRTDATA2_IMPORT ostream cout;

💡又因为只允许一个cout存在,因而在数据传递时应该使用引用&

class Person {
	friend ostream& operator<<(ostream& cout, Person p);
public:
    Person() {}
    
    Person(int a, int b) {
        this->a = a;
        this->b = b;
    }
private:
    int a;
    int b;
};
//全局函数重载左移运算符进行输出
ostream& operator<<(ostream& cout, Person p) {
    cout << p.a << " " << p.b << endl;
    return cout;
}
...
Person p1(10, 10);
Person p2(20, 20);
cout << p1 << p2 << endl;

实现只对类的直接打印,如果倒着顺序输出... << p << cout;

每一次的返回值为p所在的类名,使用成员函数重载法

class Person {
public:
	Person() {}

	Person(int a, int b) {
    	this->a = a;
    	this->b = b;
	}

	Person operator<<(ostream& cout) {
		cout << this->a << " " << this->b;
		return *this;
	}

	Person operator<<(Person p) {
		cout << this->a << " " << this->b;
		return p;
	}

private:
 int a;
 int b;
};
...
Person p1(10, 10);
Person p2(20, 20);
p1 << p2 << cout;
递增运算符重载
//implement
Person p(1);
cout << p++ << endl;//对自己操作,返回自身类,使用成员函数重构
cout << ++p << endl;
...
class Person {
    friend ostream& operator<<(ostream& cout, Person p);
public:
	Person() {}

	Person(int a) {
    	this->a = a;
	}
    
    //后置++运算重载
    Person operator++(int) {
        //int作为占位参数,可以用于表示后置递增
        Person temp = *this;//记录
        this->a++;//递增
        return temp;//返回
        //这是局部对象不能返回引用
    }
    
    //前置++运算运算符重载
    Person& operator++() {
        this->a++;//递增
        return *this;//返回
    }
private:
    int a;
};

ostream& operator<<(ostream& cout, Person p) {
    cout << p.a << endl;
    return cout;
}

前置++函数中的&用于实现嵌套式的前置++运算,并且同步自身值

后置++函数中却不能使用&符号来实现嵌套,因为本身嵌套式的后置++操作本身便是违法操作(p++)++因为当p进行完后置++操作后返回的是一个临时变量,生命周期短暂,无法继续对其自身进行操作

赋值运算符重载
//implement
Person p1(1);
Person p2(2);
Person p3(3);
p3 = p2 = p1;
...
class Person {
public:
	Person(int a) {
		this->a = new int(a);
	}
    
    //由于存在堆区内存,因而需要手动释放
	~Person() {
		if (this->a != NULL) {
			delete a;
			a = NULL;
		}
	}

    //赋值运算符重载
	Person& operator=(Person& p) {
	//少一个&都会崩
		if (this->a != NULL) {
			delete a;
			a = NULL;
		}
        //深拷贝
		this->a = new int(*p.a);
		return *this;
	}

private:
	int* a;
};

为什么Person& operator=(Person& p)一定得使用两个引用?

如果不使用引用或者指针,那么当调用这个成员函数的时候,没有被引用或者指针修饰的对象需要被拷贝出一个与之对应的局部变量,如果你用的是默认的拷贝构造,那么拷贝出的局部变量对象中的a指向的空间和被拷贝对象中的a指向的空间是相同的,所以当拷贝出的局部变量对象被使用完进行析构时就会把该指针指向的空间释放,但是被拷贝对象在程序结束后也要进行析构,这就会导致该空间被二次释放,从而引发报错

class Person {
	friend void test1();
public:
	Person(int a) {
		this->a = new int(a);
	}
    
    //自带的拷贝构造函数
    Person(const Person& p) {
        this->a = p.a;
    }
    
    //重写拷贝构造函数
	Person(const Person& p) {
		this->a = new int(*p.a);
	}
    
	~Person() {
		if (this->a != NULL) {
			delete a;
			a = NULL;
		}
	}

	Person operator=(Person p) {
		if (this->a != NULL) {
			delete a;
			a = NULL;
		}
		this->a = new int(*p.a);
		return *this;
	}

private:
	int* a;
};

解决方案:对默认拷贝构造函数进行重写,当拷贝时分配一块新的内存来解决”拷贝出的局部变量对象中的a指向的空间和被拷贝对象中的a指向的空间是相同的“这一问题,这样引用或者指针与否就没有必要了

关系运算符重载
//实现,易知返回值为bool类型
p1 == p2;
p1 != p2;
...
class Person {
public:
    //成员函数关系运算符重载
    bool operator==(Person& p) {
        return this->a == p.a && this->b == p.b;
    }
    
    bool operator!=(Person& p) {
        return !(this->a == p.a && this->b == p.b);
    }
    
private:
    int a;
    string b;
};
函数调用运算符重载
class Calculator {
public:
    int operator()(int a, int b)
        return a + b;
};
...
//可以实现
Calculate cal;
int a = cal(10, 10);

cal(10, 10)重载的()运算符类似于函数的调用,因而这样的函数被称为仿函数

结合之前了解过的匿名函数还可以直接这样调用

int a = Calculator()(10, 10);

继承

1 基本语法

语法:class A : 继承方式 B;

A类称为子类或者派生类

B类称为父类或者基类

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

  • 一类是从基类中继承过来的
  • 一类是自己增加的成员
2 继承方式
  • 公共继承
  • 保护继承
  • 私有继承

在这里插入图片描述

3 继承中的对象模型

在继承的过程中,父亲对象中的私有成员也被继承给了儿子,只是编译器将其隐藏

如何查询某个类的成员?

class Base {
public:
	int A;
protected:
	int B;
private:
	int C;
};

class Base : public Son {
public:
	int D;
};
  • 开始界面找到vs自带的开发人员命令提示工具
    在这里插入图片描述

  • 找到程序所在项目的所属文件夹路径
    在这里插入图片描述
    找到在资源管理器中打开文件夹
    复制该文件夹路径
    在开发人员命令提示工具窗口中输入cd 拷贝的路径
    继续输入dir可以查询该文件夹中所包含的文件信息

  • 查询某个类中的成员
    继续输入cl /d1 reportSingleClassLayout类名 "类所在的.cpp文件名"
    在这里插入图片描述
    在这里插入图片描述
    最后可以得到
    在这里插入图片描述

4 继承中构造和析构的顺序
  • 先构造父类再构造儿子
  • 先析构儿子在析构父亲
5 继承中同名成员的处理方式

当子类与父类出现同名的成员,通过子类对象,如何分别访问其中的对象呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域父类名::
class Base {
public:
    void fun() {}
    
    void fun(int a) {}
    
    int a;
};

class Son : public Base {
public:
    void fun() {}
    
    int a;
};
,,,
Son s;
//调用子类成员
s.fun();
s.a;

//调用父类成员
s.Base::fun();
s.Base::a;
s.Base::fun(10);//重载也需要作用域
6 继承中同名静态成员处理方式

静态成员的处理方式和非静态的相同

但是有两种访问方式

  • 通过对象访问
  • 通过类名访问
class Base {
public:
    static void fun() {}
    
    static int a;//类内声明类外初始化
};

int Base::a = 10;

class Son : public Base {
public:
    static void fun() {}
    
    static int a;//类内声明类外初始化
};

int Son::a = 20;
,,,
//由于是静态成员变量所以还可以使用类名访问
//调用子类成员
Son::a;
Son::fun();
//调用父类成员
Son::Base::a;
Son::Base::fun();
7 多继承语法

语法:class 子类名 : 访问权限 父类名1, 访问权限 父类名2 ... {}

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

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

8 菱形继承
继承
继承
继承
继承
Base
Son1
Son2
grandSon
class Base {
public:
    int a;
};

class Son1 : public Base {};
class Son2 : public Base {};

class grandSon : public Son1, public Son2 {};

概念:

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

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

这种继承被称为菱形继承,又称为钻石继承

带来的问题

子类继承两份同样的数据,产生二义性,导致资源浪费以及毫无意义

//二义性
grandSon g;
g.Son1::a = 1;//必须加上作用域才能访问
cout << g.Son1::Base::a << endl;//输出1
cout << g.Son2::Base::a << endl;//无法访问
//可知尽管Son1和Son2继承同一个Base,但是他们并不是共享同一个Base
//而是各自都创建了一个属于自己的Base基类,导致资源浪费

因而需要让Son1和Son2指向同一块Base基类即可解决问题

利用虚继承可以解决菱形继承问题

虚继承class 子类名 : virtual 访问权限 父类名

组顶层的类Base被称为虚基类

虚继承
虚继承
继承
继承
Base
Son1
Son2
grandSon

只需要让Son1和Son2指向同一块Base基类即可解决问题

不需要继续为grandSon继续使用虚继承,因为两个父类指向的空间是相同的

class Base {
public:
    int a;
};

class Son1 : virtual public Base {};
class Son2 : virtual public Base {};

class grandSon : public Son1, public Son2 {};

,,,
grandSon g;
g.a = 1;//不需要作用域也可以访问
cout << g.Son1::a << endl;//输出1
cout << g.Son2::Base::a << endl;//输出1
//解决问题

通过开发人员命令提示工具可以清晰的看到

经过virtual关键词修饰后,Son1和Son2从Base中继承到的其实是一个vbptr指针

通过计算vbtable中的指针偏移量来指向一个共同Base中的a

虚继承前
在这里插入图片描述

虚继承后
在这里插入图片描述

1111

多态

1 多态的基本概念

多态分为两类

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

静态多态和动态多态的区别

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

动态多态的满足条件

  • 有继承关系
  • 子类重写父类的虚函数

动态多态的使用

  • 父亲的指针或者引用执行的子类对象
class Animal {
public:
    virtual void speak() {}//虚函数
};

class Cat : public Animal {//继承
public:
    void speak() {}//重写父类虚函数
};

class Dog : public Animal {//继承
public:
    void speak() {}//重写父类虚函数
};

void animalSpeak(Animal& animal) {
    //Animal& Animal = 子类对象;引用使用多态
    animal.speak();
}

void test() {
    Cat cat;
    Dog dog;
    animalSpeak(cat);//输出cat.speak();
    animalSpeak(dog);//输出dog.speak();
}
2 多态的原理剖析

当子类重写父类的虚函数后,改变了子类中的vfptr虚函数表指针的指向

使其从指向父类虚函数,到指向自己所在类中重写的对应的父类虚函数

class Animal {
public:
    virtual void speak() {}
};

class Cat : public Animal {
public:
    //void speak() {}
};

查询开发人员命令提示工具

由图可知,未发生重写时Cat类中继承的vfptr指针指向的vftable中的内容是父类Animal中的speak()函数

在这里插入图片描述

改动代码,让Cat子类实现对父类函数的重写

class Animal {
public:
    virtual void speak() {}
};

class Cat : public Animal {
public:
    void speak() {}
};

由图可知,发生重写后Cat类中继承的vfptr指针指向的vftable中的内容发生了改变

内容是Cat中重写的speak()函数
在这里插入图片描述

3 多态的优点
  • 代码组织结构清晰,可读性强
  • 利于前期和后期的扩展以及维护(开放闭合原则:对拓展开放,对修改关闭)
//实现一个计算器
class abstractCalculator {
public:
    abstractCalculator() {
        a = 0;
        b = 0;
    }

    virtual int compute() {
        return 0;
    }

    int a;
    int b;
};

class addCalculator : public abstractCalculator {
public:
    addCalculator(int m, int n) {
        a = m;
        b = n;
    }

    int compute() {
        return a + b;
    }
};

class subCalculator : public abstractCalculator {
public:
    subCalculator(int m, int n) {
        a = m;
        b = n;
    }

    int compute() {
        return a - b;
    }
};

class mulCalculator : public abstractCalculator {
public:
    mulCalculator(int m, int n) {
        a = m;
        b = n;
    }

    int compute() {
        return a * b;
    }
};

class divCalculator : public abstractCalculator {
public:
    divCalculator(int m, int n) {
        a = m;
        b = n;
    }

    int compute() {
        return a / b;
    }
};

void test() {
    abstractCalculator* cal = new addCalculator(10, 10);
    cout << cal->compute() << endl;
    delete cal;
    cal = NULL;

    cal = new subCalculator(10, 10);
    cout << cal->compute() << endl;
    delete cal;
    cal = NULL;

    cal = new mulCalculator(10, 10);
    cout << cal->compute() << endl;
    delete cal;
    cal = NULL;

    cal = new divCalculator(10, 10);
    cout << cal->compute() << endl;
    delete cal;
    cal = NULL;
}
4 纯虚函数和抽象类

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

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

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

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

抽象类特点

  • 无法实例化对象,但可以实例化指针
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
5 虚析构和纯虚析构

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

可以发现当执行完delete a;后其直接跳过了子类的析构函数,直接调用父类的析构并且不再执行子类的析构

class Animal {
public:
    virtual void speak() = 0;
    
    ~Animal() {}
};

class Cat : public Animal {
public:
    Cat(string name) {
        m_name = new string(name);//堆中开辟空间需要释放
    }
    
    void speak() {}
    
    //释放m_name
    ~Cat() {
        if(m_name != NULL) {
            delete m_name;
            m_name = NULL;
        }
    }
    
    string* m_name;
};
,,,
Animal* a = new Cat("Tom");//堆中开辟空间需要释放
a->speak();
delete a;//释放a

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

💡若是纯虚析构则需要类外实现

//将上述代码中Animal类进行修改
class Animal {
public:
    virtual void speak() = 0;
   
    //虚析构
    virtual ~Animal() {}
    
    //纯虚析构
    virtual ~Animal() = 0;
};

//纯虚析构类外实现
Animal :: ~Animal() {}

文件操作

文件可以将数据持久化

c++对文件操作需要包含头文件

include<fstream>

文件类型分为

  • 文本文件文件以文本的ASCII码形式储存在计算机中
  • 二进制文件文件以文本的二进制形式储存在计算机中

操作文件的三大类

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

文件打开方式

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

可以通过|符号同时调用多个

1 文本文件

写文件

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

void test() {
    //2、创建流对象
    ofstream ofs;
    //3、打开文件并确定方式
    ofs.open("test.txt", ios::out);
    //在test.txt中写,默认在本.cpp文件所属文件夹内创造
    //4、写内容
    ofs << "Hello File!" << endl;
    //5、关闭文件
    ofs.close();
}

读文件

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

void test() {
    //2、创建流对象
    ifstream ifs;
    //3、打开文件并确定是否打开成功
    ifs.open("test.txt", ios::in);
    if(!ifs.is_open()) {//is_open:判断是否打开成功
        cout << "failed to open" << endl;
        return;
    }
    //4、读数据
    //第一种:直接读入
    char buf[1024] = {0};
    while(ifs >> buf) {
        cout << buf << endl;
    }
    //第二种:getline方法读入
    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) {//EOF:end of file
        cout << c;
    }
    //5、关闭文件
    ifs.close();
}
2 二进制文件

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

写文件

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

函数原型:ofstream& write(const char*,streamsize);

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

void test() {
    //ofstream ofs;
    
    //ofs.open("test.txt", ios::out | ios::binary);
    
    //也可以直接将创建和打开合并
    ofstream ofs("test.txt", ios::out | ios::binary);
    
    Person p = {"LiHua", 18};
    ofs.write((const char*)&p, sizeof(Person));
    
    ofs.close();
    //二进制写文件,出现乱码是正常情况
}

读文件

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

函数原型:ifstream& write(char*,streamsize);

void test() {

	ifstream ifs("test.txt", ios::in | ios::binary);

	if(!ifs.is_open()) return;

	Person p;
	ifs.read((char*)&p, sizeof(p));
    
    ifs.close();
}

C++图形库

EasyX 文档 - 基本说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我还蒙在鼓里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值