C++笔记(三)类和对象2
C++运算符重载
- 重新给那些已经有定义的运算符,设置新的定义
- 而原来运算符的定义依然不会修改,因此,就是重载
加号运算符重载
- 实现两个自定义数据类型相加的运算
- 运算符重载可以发生函数重载,就是函数名称相同,参数不同
- 分别有两种方式:成员函数重载和全局函数重载
//假设我们要将两个Person类的数据相加
class Person{
public:
int m_a;
int m_b;
//成员函数的运算符重载
Person operator+(Person &p){ //运算符+重载
Person temp; //这个函数的名字就是operator+
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
};
//全局函数的运算符重载
Person operator+(Person &p1, Person &p2){
Person temp;
temp.m_a = p1.m_a + p2.m_a;
temp.m_b = p1.m_b + p2.m_b;
return temp;
}
int main(){
Person p1;
p1.m_a = 20;
p1.m_b = 20;
Person p2;
p2.m_a = 10;
p2.m_b = 10;
Person p3 = p1 + p2; //因为重载了+运算符,所以这里可以运行
//其实,这里的计算时编译器其实改写了如下方式:
//对于成员函数的运算符重载:
//Person p3 = p1.operator+(p2);简化为p3=p1+p2
//对于全局函数的运算符重载:
//Person p3 = operator+(p1,p2);简化为p3=p1+p2
cout << p3.m_a << p3.m_b <, endl; //输出都是30
}
左移运算符重载
- 左移符号是<<,就是cout << 中的运算符
- 只能使用全局函数做重载,不能使用成员函数重载,因为会反了:平常都是cout << p;如果使用成员函数重载,那么就变成p<<cout;反了
- 左移运算符重载,可以输出自定义数据类型
//假设我们要cout << p就能够将类中成员打印出来
class Person{
public:
Person(int a, int b){
this->m_a = a;
this->m_b = b;
}
int m_a;
int m_b;
//如果这么写,那么调用的时候,就是p.operator<<(cout)
//那么形成的就是p<<cout,就反了,因此只能使用全局函数的重载
//void operator<<(Person &p){};
};
//如果这么写,不能做成链式编程
//void operator<<(ostream &out, Person &p){
//返回cout本身,就能形成链式编程
ostream & operator<<(ostream &out, Person &p){
//如果类中的成员是私有的,那么我们可以将此函数设定为友元函数
out << p.m_a << p.m_b;
return out;
}
void test(){
Person p(10,10);
//如果运算符重载函数不返回值,那么只能这么写,不能连续的写
cout << p;
//如果运算符重载函数返回了cout本身,那么就可以链式的编程
cout << p << endl;
}
递增运算符重载
- 通过重载递增运算符,实现自己的整型数据++
//假设打印类中的整型数据递增
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() {
m_Num = 0;
}
MyInteger& operator++() { //前置++
m_Num++;
return *this;
}
MyInteger operator++(int) { //后置++,添加一个占位参数
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
private:
int m_Num;
};
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num; //重载<<运算符
return out;
}
void test01() { //前置++ 先++ 再返回
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
void test02() { //后置++ 先返回 再++
MyInteger myInt;
cout << myInt++ << endl;
cout << myInt << endl;
}
赋值运算符重载
- C++编译器至少会给一个类添加下面4个函数:
-
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对成员变量进行值拷贝
- 赋值运算符operator=,对成员变量进行值拷贝(因此,也会出现浅拷贝的问题)
//假设我们要实现一个对类的赋值操作
class Person{
public:
Person(int age){
m_Age = new int(age);
}
Person& operator=(Person &p){ //重载赋值运算符
if (m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
//编译器提供的代码是浅拷贝
//m_Age = p.m_Age;
//提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);
return *this; //返回自身,实现链式编程
}
~Person(){
if (m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
}
int *m_Age;
};
void test01(){
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; //赋值操作
cout << "p1的年龄为:" << *p1.m_Age << endl; //10
cout << "p2的年龄为:" << *p2.m_Age << endl; //10
cout << "p3的年龄为:" << *p3.m_Age << endl; //10
}
关系运算符重载
- 就是重载==,!=这种运算符
//假设我们对比两个类中成员是否相等
class Person{
public:
Person(string name, int age){
this->m_Name = name;
this->m_Age = age;
};
bool operator==(Person & p){ //重载==运算符
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age){
return true;
}
else{
return false;
}
}
string m_Name;
int m_Age;
};
void test01(){
Person a("孙悟空", 18);
Person b("孙悟空", 18);
if (a == b){ //重载!=运算符也是相同的写法
cout << "a和b相等" << endl;
}
}
函数调用运算符重载
- 函数调用运算符(),也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
class MyPrint{
public:
void operator()(string text){ //重载()运算符
cout << text << endl;
}
};
void test01(){
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world"); //会直接打印hello world
}
//重载()运算符非常灵活,可以如下这么写:
class MyAdd{
public:
int operator()(int v1, int v2){ //可以设置自己想要的功能
return v1 + v2;
}
};
void test02(){
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
//匿名对象调用 其中下面的MyAdd()就是匿名对象,没有名字的对象
//执行完这行代码,匿名对象就释放了
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
继承
基本语法
- 语法:
class 子类 : 继承方式 父类
-
- 子类也叫派生类,父类也叫基类
- 继承的好处:减少重复代码
class BasePage{
public:
void header(){
cout << "首页、公开课、登录、注册...(公共头部)" << endl;
}
void footer(){
cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
}
void left(){
cout << "Java,Python,C++...(公共分类列表)" << endl;
}
};
class Java : public BasePage{ //继承BasePage类
public:
void content(){
cout << "JAVA学科视频" << endl;
}
//就相当于把基类的成员复制粘贴到这里了,所以使用的时候,也是跟正常调用没有区别
};
继承方式
- 继承方式:
-
- 公有继承:
-
- 父类的公共成员->子类的公共成员
- 父类的保护成员->子类的保护成员
- 父类的私有成员x>子类,不可访问
- 保护继承:
-
- 父类的公共成员->子类的保护成员
- 父类的保护成员->子类的保护成员
- 父类的私有成员x>子类,不可访问
- 私有继承:
-
- 父类的公共成员->子类的私有成员
- 父类的保护成员->子类的私有成员
- 父类的私有成员x>子类,不可访问
继承中的对象模型
- 从父类继承的成员,都有哪些属于子类中?
- 答:都在子类中,而不可访问的部分成员,其实是编译器给隐藏了
class Base{
public:
int m_A;
protected:
int m_B;
private:
int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};
class Son :public Base{ //公共继承
public:
int m_D;
};
void test01(){
cout << "sizeof Son = " << sizeof(Son) << endl;
//得到16,就是所有成员都被继承了,包括私有成员
}
构造和析构
- 子类继承父类后,那么构造函数和析构函数的顺序是什么?
- 答:构造函数:先执行父类的,再执行子类的;析构函数:先执行子类的,再执行父类的
class Base{
public:
Base(){
cout << "Base构造函数!" << endl;
}
~Base(){
cout << "Base析构函数!" << endl;
}
};
class Son : public Base{
public:
Son(){
cout << "Son构造函数!" << endl;
}
~Son(){
cout << "Son析构函数!" << endl;
}
};
void test01(){
//继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
Son s;
//尽管只创建了子类的对象,但是子类是继承于父类的,
//所以父类也一定会创建新的对象
}
继承同名成员的处理
- 就是当子类和父类出现了同名的成员时,需要怎么访问子类或者父类的同名数据
-
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域::
class Base{
public:
Base(){
m_A = 100;
}
void func(){ //建立同名的成员函数
cout << "Base - func()调用" << endl;
}
void func(int a){ //建立同名的成员重载函数
cout << "Base - func(int a)调用" << endl;
}
public:
int m_A; //建立同名的成员变量
};
class Son : public Base {
public:
Son(){
m_A = 200; //与父类同名
}
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数,因此这时候父类中有同名的重载函数,也是不生效的
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func(){ //与父类同名
cout << "Son - func()调用" << endl;
}
public:
int m_A;
};
void test01(){
Son s;
//直接访问,就是访问的子类成员
cout << "Son下的m_A = " << s.m_A << endl;
//加作用域::,就是访问对应作用域的成员
cout << "Base下的m_A = " << s.Base::m_A << endl;
s.func();
s.Base::func();
s.Base::func(10);
}
继承同名静态成员的处理
- 大体上,跟非静态成员的处理相同
class Base {
public:
static void func(){
cout << "Base - static void func()" << endl;
}
static void func(int a){
cout << "Base - static void func(int a)" << endl;
}
static int m_A;
};
int Base::m_A = 100;
class Son : public Base {
public:
static void func(){
cout << "Son - static void func()" << endl;
}
static int m_A;
};
int Son::m_A = 200;
void test01(){
//通过对象访问,跟非静态成员一样
cout << "通过对象访问: " << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;
//通过类名访问,因为静态成员是共享的,可以直接用类访问
cout << "通过类名访问: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
//这个特殊,需要注意
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
void test02(){
//通过对象访问,跟非静态成员一样
cout << "通过对象访问: " << endl;
Son s;
s.func();
s.Base::func();
cout << "通过类名访问: " << endl;
//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
//就是说,必须要使用Base的作用域,否则,子类的func会隐藏父类的所有func函数(包括重载)
//第一个作用域::符号指的是通过类名访问,第二个::符号指的是找父类中的func
Son::func();
Son::Base::func();
//想找到父类中的func重载函数,需要要使用Base指定
Son::Base::func(100);
}
多继承
- 就是一个类,继承了多个父类
- 多继承可能会引发父类中同名成员的出现,需要加作用域区分
- C++实际开发中不建议使用多继承
//假设写好了两个父类
class Son : public Base2, public Base1 {
public:
int m_C;
int m_D;
};
//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01(){
Son s;
//这个sizeof就是计算了所有父类和子类中的非静态 成员 变量的占用
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}
菱形继承(面试)
- 两个子类继承同一个夫类,又有某个类同时继承两个子类,这种继承被称为菱形继承,或者钻石继承
- 菱形继承的问题:
-
- 数据的二义性,中间层的两个类,都继承了同一个类,就会都有同一个成员,在最底层的子类继承这两个中间层的类时,出现数据的二义性,就是分不清是哪个类的成员,可以使用作用域::区分
- 这份同名的数据,最底层的子类继承了两个同名的成员,但是实际上我们只需要一份,造成了资源浪费,可以使用虚继承
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 test01(){
//这是第一个问题,使用作用域就可以访问
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
//如果使用了虚继承,那么此时底下输出的都是200
//因为使用虚继承后,实际上你访问的不管那个作用域,都是只有一份数据
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
多态
基本概念
-
多态分为两类
-
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
-
静态多态和动态多态区别:
-
-
静态多态的函数地址早绑定 - 编译阶段确定函数地址
-
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
-
-
多态满足条件:
-
- 有继承关系
- 子类重写父类中的虚函数
-
多态的使用:父类指针或者引用指向子类对象
class Animal{
public:
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak(){
cout << "动物在说话" << endl;
}
};
class Cat :public Animal{
public:
void speak(){ //子类重写虚函数,加不加virtual都行
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal{
public:
void speak(){ //子类重写虚函数,加不加virtual都行
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal){ //这个函数的形参是父类对象
animal.speak(); //但是实际上传入的实参是子类对象
} //满足多态的要求
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01(){
Cat cat;
DoSpeak(cat); //想要调用cat对象的函数,就传入cat
Dog dog;
DoSpeak(dog); //想要调用dog对象的函数,就传入dog
}
- 多态的底层理解(以上面的代码为例)
- 对于animal父类,如果没有加virtual,也就是说,没有虚函数只有普通成员函数,那么sizeof后就占用1字节,而使用了虚函数之后,就占用了4字节,因为虚函数的存在,使得父类产生了一个指针,指向了&animal::speak(就是animal作用域下的speak函数的地址)
- 对于cat子类,如果没有重写父类中的虚函数(就是speak函数),那么就是一个普通的继承,会把指向了&animal::speak(就是animal作用域下的speak函数的地址)继承过来。而重写了父类中的虚函数(就是speak函数),那么继承过来的指针,就会指向&cat::speak(就是cat作用域下的speak函数的地址)
- 因此,形成了父类的指针或者引用指向了哪个子类的对象,那么程序就会运行哪个子类的对象,形成了多态。
多态的优点
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
//多态实现计算器
class AbstractCalculator{ //抽象计算器类
public :
virtual int getResult(){
//可以不写里面的东西,毕竟主要运行的都是后面要开发的部分
return 0;
}
int m_Num1;
int m_Num2;
};
//加法计算器
class AddCalculator :public AbstractCalculator{
public:
int getResult(){
return m_Num1 + m_Num2;
}
};
//减法计算器
class SubCalculator :public AbstractCalculator{
public:
int getResult(){
return m_Num1 - m_Num2;
}
};
void test(){
//创建加法计算器
//多态的条件:需要有父类的指针或者引用指向子类对象
AbstractCalculator *abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //用完了记得销毁
//创建减法计算器
abc = new SubCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
纯虚函数和抽象类
- 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
- 纯虚函数语法:
virtual 返回值类型 函数名 (参数列表)= 0 ;
- 当类中只要有了纯虚函数,这个类就称为抽象类(可以有很多纯虚函数)
- 抽象类的特点:
-
- 抽象类无法实例化对象,就是说不能创建对象
- 子类必须重写抽象类中的纯虚函数,否则该子类也属于抽象类
class Base{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
//其实这里就是接口了,就是我准备要写这个功能了,先占个坑
};
class Son :public Base{
public:
virtual void func() { //子类的virtual写不写都行
cout << "func调用" << endl;
};
};
void test01(){
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son; //父类的指针必须指向子类的对象
base->func(); //形成多态
delete base;//记得销毁
}
虚析构和纯虚析构
-
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,可以将父类的析构函数改为虚析构或者纯虚析构
-
虚析构的语法:
virtual ~类名(){}
-
纯虚析构的语法:声明:
virtual ~类名() = 0;
;实现:类名::~类名(){}
-
虚析构和纯虚析构的特点:
-
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
class Animal {
public:
Animal(){
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
//virtual ~Animal()
//{
// cout << "Animal虚析构函数调用!" << endl;
//}
//虚析构和纯虚析构只能二选一
//下面是纯虚析构,这只是声明,还需要有实现函数
virtual ~Animal() = 0;
};
//纯虚析构的实现函数
Animal::~Animal(){
cout << "Animal 纯虚析构函数调用!" << endl;
}
//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal {
public:
Cat(string name){
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name); //把数据建立在堆区
}
virtual 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 test01(){
Animal *animal = new Cat("Tom");
animal->Speak();
//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
//怎么解决?给基类增加一个虚析构函数
//虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}
文件操作
- 对文件操作需要包含的头文件====
- 文件类型:
-
- 文本文件:以文本的ASCII码形式储存在计算机中
- 二进制文件:以文本的二进制形式储存在计算机中,一般用户看不懂
- 操作文件三大类:
-
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
文本文件
- 写文件步骤:
-
- 包含头文件:
#include<fstream>
- 创建流对象:
ofstream ofs;
- 打开文件:
ofs.open("文件路径",打开方式);
- 写数据:
ofs << "写入的数据";
- 关闭文件:
ofs.close();
- 包含头文件:
- 文件打开方式:
-
- 可以多个方式一起使用:如
ios::binary | ios::out
- 可以多个方式一起使用:如
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 打开文件后,直接指向文件尾 |
ios::app | 在文件最后追加的方式写文件 |
ios::trunc | 如果文件存在则先删除,再创建 |
ios::binary | 二进制方式 |
#include <fstream> //添加头文件
void test01(){
ofstream ofs; //创建流对象
ofs.open("test.txt", ios::out); //打开文件
ofs << "姓名:张三" << endl; //写数据
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;
ofs.close(); //关闭文件
}
- 读文件步骤:
-
- 包含头文件:
#include<fstream>
- 创建流对象:
ifstream ifs;
- 打开文件:
ifs.open("文件路径",打开方式);
- 读数据:四种方式
- 关闭文件:
ifs.close();
- 包含头文件:
#include <fstream> //包含头文件
#include <string>
void test01(){
ifstream ifs; //创建流对象
ifs.open("test.txt", ios::in); //打开文件
//判断是否打开文件,is_open()判断文件是否打开,返回bool类型
if (!ifs.is_open()){
cout << "文件打开失败" << endl;
return;
}
//第一种方式
//char buf[1024] = { 0 };
//while (ifs >> buf){
// cout << buf << endl;
//}
//第二种
//char buf[1024] = { 0 }; 这是char类型
//getline()获取一行数据
//ifs.getline(读取数据存放处,多大)
//while (ifs.getline(buf,sizeof(buf))){
// cout << buf << endl;
//}
//第三种
//string buf; 这是string类型
//while (getline(ifs, buf)){
// cout << buf << endl;
//}
char c;
//ifs.get()一个字符一个字符获取文件数据
//EOF指的是文件末尾,end of file
while ((c = ifs.get()) != EOF){
cout << c;
}
ifs.close(); //关闭文件
}
二进制文件
- 写文件主要使用流对象成员函数write
-
- 函数原型 :
ostream& write(const char * buffer,int len);
- 参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
- 函数原型 :
//假设我们将一个类数据放入二进制文件
#include <fstream>
#include <string>
class Person{
public:
char m_Name[64];
int m_Age;
};
//二进制文件 写文件
void test01(){
//创建输出流对象
//可以像如下这种,创建流对象时直接打开文件
//也可以一步一步来,像文本文件那样
ofstream ofs("person.txt", ios::out | ios::binary);
//打开文件
//ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//写文件
ofs.write((const char *)&p, sizeof(p));
//关闭文件
ofs.close();
}
- 读文件主要使用流对象成员函数read
-
- 函数原型:
istream& read(char *buffer,int len);
- 参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
- 函数原型:
//假设我们按照类的结构读取二进制文件
#include <fstream>
#include <string>
class Person{
public:
char m_Name[64];
int m_Age;
};
void test01(){
//创建流对象并打开
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open()){
cout << "文件打开失败" << endl;
}
Person p;
//按照类的结构读取数据
ifs.read((char *)&p, sizeof(p));
cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}