目录
[(2) 左移运算符的重载](#(2) 左移运算符的重载)
1.运算符重载
(1)其实这个知识我们并不是很陌生,我们在C语言学习阶段,就知道了*这个运算符,在我们学习基本的运算的时候,这个运算符就是用来计算乘法的,但是后来我们学到了指针之后,这个运算符就用来表示对于一个指针变量的解引用,这个时候我们就已经认识到了一个运算符的重载功能;
(2)我们通过下面这个实际的场景认识一下什么是c++里面的运算符重载,我们下面的这个代码场景是要为这个超女的分数上面加上30分,这个时候如果我们直接拿这个对象和数字相加,编译器是肯定不会通过的,因为这个编译器不知道要把这个数据加到那个成员变量上面去;
(3)针对这个问题,我们有两种解决方案,第一种就是设置一个函数,实现这个加分的功能,指定这个分数加到那个成员变量上面去,我们原本初始化这个分数是90,加上30分之后这个分数就变成了120分;
#include<string>
using namespace std;
class cgril
{
friend void addscore(cgril& g, int score);
private:
int m_xw;//超女的行为
int m_score;//超女的分数
public:
string m_name;//超女的姓名
cgril()
{
m_xw = 60;
m_name = "张三";
m_score = 90;
}
void show()
{
cout << "行为:" << m_xw << "分数:" << m_score << "姓名:" << m_name << endl;
}
};
void addscore(cgril& g, int score)//利用函数,实现加分的功能
{
g.m_score = g.m_score + score;
}
int main()
{
//导演的要求就是,每轮表演之后,加上对应的得分
cgril g;
addscore(g, 30);
g.show();
return 0;
}
(4)上面的这个写法偏向于C的写法,接下来我们看一下如何使用C++写法实现这个加分的功能,就是把这个函数的名字修改为operator+即可,这个operator是关键字,+就是我们想要重载的运算符 ;
(5)上面的这个运算符重载的基本写法,实际上这个运算符的重载既可以写作成员函数,也可以写作全局函数,当我们实现一个类和一个整形数据加法的时候,如果我们写在成员函数里面,就可以只写一个参数,最后的返回值是*this,这个时候写法是这样的;
cgril& operator+(int score)//利用函数,实现加分的功能
{
m_score = m_score + score;
return *this;
}
};
int main()
{
//导演的要求就是,每轮表演之后,加上对应的得分
cgril g;
g = g + 30;
g.show();
return 0;
}
这个时候,那个对象调用这个成员函数,那个对象就是this指针,这样写在成员函数里面,我们就不需要写这个友元函数了,因为这个在类的里面,我们是可以对于这个私有的成员变量直接进行使用,这个时候g=g+30我们需要把这个返回值设置为这个类的引用类型;
(6)当这个函数写在全局里面时候,有几个操作数,我们就需要写几个操作数,而且这个操作数的数据类型和我们使用的数据类型之间应该是一一对应的,例如我们想要实现这个cgril+int,我们在使用这个加法运算符的时候,我们就必须把这个类写在左边,int类型的数据写在右边,这个顺序是不可以改变的;
(7)下面的这些运算符重载的时候必须写在这个成员函数里面去:赋值运算符= 函数调用运算符( ) 下标运算符[ ] 以及这个指针访问成员操作符-> ;
2.四种运算符重载
(1)关系运算符的重载
关系运算符就是我们常说的这个大于小于和等于这些符号的比较,我们对于两个类之间的比较,需要自己进行运算符的重载;
下面的就是重载了等于号大于号和小于号的运算符重载代码;
class student
{
string m_name;
int m_cj;//学生成绩
int m_bx;//学生的表现
int m_td;//学生的学习态度
public:
student(string name, int cj, int bx, int td)
{
m_name = name;
m_cj = cj;
m_bx = bx;
m_td = td;
}
bool operator==(const student& s)
{
if (m_cj + m_bx + m_td == s.m_cj + s.m_bx + s.m_td)
{
return true;
}
else
{
return false;
}
}
bool operator<(const student& s)
{
if (m_cj + m_bx + m_td < s.m_cj + s.m_bx + s.m_td)
{
return true;
}
else
{
return false;
}
}
bool operator>(const student& s)
{
if (m_cj + m_bx + m_td > s.m_cj + s.m_bx + s.m_td)
{
return true;
}
else
{
return false;
}
}
};
int main()
{
student s1("张三", 80, 20, 70);
student s2("李四", 70, 70, 30);
if (s1 == s2)
{
cout << "张三和李四的分数一样" << endl;
}
else
{
if (s1 < s2)
{
cout << "张三比李四的分数低" << endl;
}
else
{
cout << "张三比李四的分数高" << endl;
}
}
return 0;
}
(2) 左移运算符的重载
这个cout有15种重载,因此可以输出各种数据类型的数据,但是cout无法输出我们自己定义的类和对象,这个时候就需要我们对于其进行重载,cout这个对象的返回值实际上就是一个ostream类型的,这个如果是全局函数,就需要两个参数,一个就是cout对象,一个就是我们的自定义类型的对象,如果是成员函数的话,,就只需要一个参数,因为对象本身是隐式传递的,不能出现在形参列表里面;
using namespace std;
class student
{
friend ostream& operator<<(ostream& cout, const student& s);
int m_mark;
string m_name;
public:
student()
{
m_mark = 90;
m_name = "张三";
}
void show()
{
cout << "这个同学的姓名是" << m_name << "成绩是:" << m_mark << endl;
}
};
ostream& operator<<(ostream& cout, const student& s)
{
cout << "姓名:" << s.m_name << " 分数:" << s.m_mark << endl;
return cout;
}
int main()
{
student s1;
cout << s1 << endl;
return 0;
}
成员函数版本的运算符重载:
#include<string>
using namespace std;
class student
{
//friend ostream& operator<<(ostream& cout, const student& s);
int m_mark;
string m_name;
public:
student()
{
m_mark = 90;
m_name = "张三";
}
void show()
{
cout << "这个同学的姓名是" << m_name << "成绩是:" << m_mark << endl;
}
ostream& operator<<(ostream& cout)
{
cout << "姓名:" << this->m_name << " 分数:" << this->m_mark << endl;
return cout;
}
};
int main()
{
student s1;
s1 << cout << endl;
return 0;
}
我们可以理解为这个运算符左右两边一共有两个操作数,这个时候我们的自定义类型作为一个隐藏的参数,所以对应的主函数里面这个自定义对象和cout对象的位置需要被改变;
这个时候cout在右边,不是我们想要的结果,因此这个左移运算符的重载,只能写成这个全局函数,不能写成成员函数;
(3)下标运算符的重载
如果没有重载下标运算符,如果我们想要查看这个数组元素,我们就需要自己定义函数,使用这个小括号加上下标的方式表示,但是我们知道这个访问操作符就是中括号,因此下面我们介绍小标访问运算符的重载;
#include<iostream>
#include<string>
using namespace std;
class student
{
private:
string m_zy[3];
public:
student()
{
m_zy[0] = "父母";
m_zy[1] = "老师";
m_zy[2] = "朋友";
}
void show()
{
cout << m_zy[0] << " " << m_zy[1] << " " << m_zy[2] << " " << endl;
}
string& operator[](int ii)
{
return m_zy[ii];
}
};
int main()
{
student s1;
s1[2] = "知己";
cout << "第三亲密:" << s1[2] << endl;
s1.show();
return 0;
}
实际在开发的过程中,我们需要有两个版本的重载函数,一个版本是可以对于我们的数据进行修改的,另外的一个就是针对常变量只能调用常函数;
#include<iostream>
#include<string>
using namespace std;
class student
{
private:
string m_zy[3];
public:
student()
{
m_zy[0] = "父母";
m_zy[1] = "老师";
m_zy[2] = "朋友";
}
void show()
{
cout << m_zy[0] << " " << m_zy[1] << " " << m_zy[2] << " " << endl;
}
string& operator[](int ii)
{
return m_zy[ii];
}
const string& operator[](int ii) const
{
return m_zy[ii];
}
};
int main()
{
student s1;
s1[2] = "知己";
cout << "第三亲密:" << s1[2] << endl;
s1.show();
const student s2 = s1;
cout << "第三亲密:" << s2[2] << endl;
return 0;
}
(4)赋值运算符的重载
赋值运算符其实编译器会提供默认的,我们了解即可,这个我们在重载赋值运算符的时候,需要判断这个时不时会出现自己给自己赋值的情况,这个是合法的,我们直接返回this解引用即可;this就是我们想要赋值的对象;
编译器默认提供的赋值构造函数是浅拷贝,如果这个不存在动态开辟空间,就可以满足需求,否则的话就需要我们自己实现深拷贝;
#include<iostream>
#include<string>
using namespace std;
class student
{
public:
string m_name;
int m_score;
void show()
{
cout << "姓名:" << m_name << " 年龄:" << m_score << endl;
}
student& operator=(const student& s)
{
if (this == &s)
return *this;
else
{
m_name = s.m_name;
m_score = s.m_score;
}
return *this;
}
};
int main()
{
student s1;
s1.m_name = "张三";
s1.m_score = 100;
s1.show();
student s2;
s2 = s1;
s2.show();
return 0;
}
3.继承的方式
(1)三种不同的继承方式
上面展示的就是三种不同的继承方式,我们假设这个基类有三个成员变量,一个是共有的,一个是受保护的,一个是私有的,经过不同的继承方式之后,这个成员变量的类型会发生什么变化;
第一种就是公有继承,公有继承的时候,我们的父类里面的公有成员还是公有成员,受保护的成员还是受保护的成员,私有成员就不存在了;
第二种是受保护的继承,这个时候,原来的公有成员就变成了受保护的成员,原来的父类里面的受保护的成员还是受保护的,原来的私有成员变量这个时候还是不存在的;
第三种就是私有的继承,这个时候,原来的父类里面的共有的成员变量和保护的成员变量就是私有的了,原来的父类里面的受保护的成员变量也是不存在的;
(2)我们在这个继承的子类里面是没有办法访问这个所谓的父类里面的私有成员变量的,但是如果我们想要在这个子类里面去访问这个私有的成员变量,可以在父类里面添加共有的函数访问这个私有的成员变量,这个时候我们就可以直接调用这个函数访问父类里面的私有的成员变量;
就像下面的展示那样,我们在子类里面本来是没有办法去访问父类的私有成员变量的,但是我们可以在父类里面创建共有的成员函数,这样在子类里面,我们就可以通过调用这个函数达到间接访问了父类的私有成员变量的目的;
class comers
{
private:
int m_aa;
public:
void func()
{
cout << m_aa << endl;
}
};
class comerss :public comers
{
private:
int m_bb;
public:
void show()
{
func();
}
};
int main()
{
comerss c1;
c1.show();
return 0;
}
(3)我们可以使用using 关键字改变父类的成员变量的访问权限,例如我们可以把这个共有的改变成为受保护的,把这个受保护的改变权限为共有的,这个父类里面的私有的成员变量的权限是没有办法通过这个关键字改变的,因为这个父类里面的私有的成员变量在这个子类里面根本就不会存在,因此这个关键字只对于public权限和protected权限发挥作用;
class a
{
public:
int m_a;
protected:
int m_b;
private:
int m_c;
};
class b :public a
{
public:
using a::m_b;
protected:
using a::m_a;
};
int main()
{
b bb1;
return 0;
}
通过上面的这个具体的实例,我们就可以把这个父类里面的公有成员变量修改为受保护的,把这个受保护的成员变量修改权限为公有的,当然也可以把他们的权限设置为私有的;
4.继承的对象模型
(1)这个地方是帮助我们了解C++继承语法的底层逻辑,而不是学懂这个底层逻辑,我们只是了解,这样就可以让我们更好理解前面的一些原因;
(2)第一点就是基类在写这个构造函数的时候,会先运行这个父类的构造函数,再去运行这个基类的构造函数,这个我们可以进行尝试,当我们创建两个类,假设是A类和B类,如果A是父类,B是子类,我们把这个A类里面的构造函数设置为私有的,这个时候我们在B这个子类里面去写构造函数就会报错,原因就是我们上面提及到的我们在执行子类的构造函数的时候,会先执行父类的构造函数,这个也是为什么我们建议把这个函数写为共有的,成员变量写为私有的,这个时候我们学习了继承,就可以更好地进行理解;
(3)销毁子类对象的时候,会先执行子类的析构函数,再去执行父类的析构函数,这个和构造函数的执行顺序是恰好相反的;
(4)创建派生类对象的时候,只申请了一次这个内存,申请的内存空间的大小就是基类加上派生类的内存的和,先调用基类的构造函数,初始化基类,再调用派生类的构造函数,初始化派生类,在基类和派生类的构造函数里面,this指针是相同的地址,派生类和基类都有的成员变量的地址也是一样的;
(5)在C++里面这个成员变量的访问权限只是语法上面的限制,我们可以使用memset函数清零数据,包括这个私有的成员变量,即使这个私有的成员变量在类的外面不能被访问;我们可以使用指针突破这个访问权限的限制,所以这个访问权限只是语法上面的一个解释罢了;
5.基类的构造
(1)我们可以使用初始化列表的方式指定要使用的基类的构造函数类型,因为我们前面提及过,这个创建子类也就是派生类的对象的时候,需要先去调用这个父类的构造函数,这个时候如果父类里面有多个构造函数,我们就可以使用初始化列表的方式指定调用基类里面的哪个构造函数;
(2)我们在创建派生类的对象的时候,是使用基类初始化这个基类的成员变量,派生类初始化派生类的成员变量,你可能会问,这些基类的成员变量都被派生类给继承了,为什么不使用派生类初始化基类的成员变量,这个有两点原因;
第一就是我们的基类里面的私有的成员变量我们是无法在这个派生类里面看到的,更别说去对于这个变量进行初始化了;
第二就是这个如果我们在这个派生类里面对于这个基类的成员变量进行初始化,如果这个基类有很多个派生类,我们的这个代码就显得很冗余,而且这个派生类存在的意义就是减少这个相同的成员变量的书写带来的麻烦,这样搞得话就不符合这个逻辑了;
(3)下面的就是我们定义了一个派生类和一个基类,通过初始化列表的方式,确定这个派生类调用的是基类里面的哪个构造函数;
class a
{
public:
int m_a;
private:
int m_b;
public:
a()
:m_a(0)
, m_b(0)
{
cout << "调用了基类的构造函数" << endl;
}
a(int aa, int bb)
:m_a(aa)
, m_b(bb)
{
cout << "调用了含有两个参数的构造函数" << endl;
}
a(const a& aa)
:m_a(aa.m_a + 1)
, m_b(aa.m_b + 1)
{
cout << "调用了基类的拷贝构造函数" << endl;
}
void showa()
{
cout << "m_a数值大小:" << m_a << "m_b的数值大小" << m_b << endl;
}
};
class b :public a
{
public:
int m_c;
b()
:m_c(0)
,a()
{
cout << "调用了这个派生类的构造函数" << endl;
}
b(int a, int b, int c)
:a(a, b)
, m_c(c)
{
cout << "调用派生类的构造函数b(int a, int b, int c)" << endl;
}
b(const a& aa, int c)
:a(aa)
, m_c(c)
{
cout << "调用派生类的拷贝构造函数" << endl;
}
void showb()
{
cout << "m_c数值大小:" << m_c << endl;
}
};
int main()
{
b b1;
b1.showa();
b1.showb();
cout << endl;
b b2(1, 2, 3);
b2.showa();
b2.showb();
cout << endl;
a a1(10, 20);
b b11(a1, 30);
return 0;
}
我们分别使用初始化列表的方式指定了调用默认的构造函数,调用含有多个参数的构造函数,以及这个拷贝构造函数;子类里面的第二个初始化列表我们使用的是这个基类的含有两个参数的构造函数进行初始化的,第三个初始化列表是使用基类里面的拷贝构造函数进行初始化的;
6.名字遮蔽和类作用域
(1)下面就是定义了一个基类和一个子类,我们创建了一个子类的对象,这个基类和子类的这个成员变量的名字以及这个成员函数的名字都是一样的,这个时候我们的打印结果就是基类的成员变量和成员函数;
using namespace std;
class a
{
public:
int m_a = 20;
void show()
{
cout << "调用了基类的函数" << endl;
}
};
class b :public a
{
public:
int m_a = 30;
void show()
{
cout << "调用了派生类的函数" << endl;
}
};
int main()
{
b b1;
cout << "b1对象的m_a的值是:" << b1.m_a << endl;
b1.show();
return 0;
}
(2)当基类的函数名和这个派生类里面的函数名字相同的时候,因为这个是继承,所以派生类里面就会隐含的包括了这个基类的成员变量和成员函数,名字相同的时候,派生类的函数就会把这个基类里面函数覆盖掉;
using namespace std;
class a
{
public:
int m_a = 20;
void show()
{
cout << "调用了基类的函数" << endl;
}
void func(int a)
{
cout << "调用了这个函数" << endl;
}
};
class b :public a
{
public:
int m_a = 30;
//void func() { cout << "调用了派生类的函数" << endl; }
};
int main()
{
b b1;
cout << "b1对象的m_a的值是:" << b1.m_a << endl;
b1.show();
b1.func(1);
}
例如上面的这个程序,两个相同的函数func,如果我们把这个继承类里面的func函数放开,这个时候运行就会报错,因为这个时候父类的func函数会被覆盖掉,我们传递参数是不合语法的,但是我们把这个注释掉之后,就可以运行成功,因为这个时候,不存在函数名字相同的情况,我们就可以直接调用这个子类里面的有一个参数的func函数了,实际上这个成员函数在子类里面也是存在的,只要不重名,这个基类里面的这个函数就不会被覆盖掉;
实际上这个名字遮蔽就是一个表象,类作用域才是实质,因为实际上当存在继承关系的时候,这个基类的作用域是嵌套在这个派生类里面的,我们一般会在这个派生类里面去寻找,只有这个在派生类里面找不到的时候,才回到基类里面去寻找;
7.类继承的特殊关系
(1)如果我们使用派生类的对象赋值给基类的对象(包括私有成员),但是这个时候会舍弃掉非基类成员:
下面这个代码里面,我们对于这个b1对象进行操作,最后把他赋值给a1这个对象,这个时候我们会发现a1这个对象里面的私有成员m_b也会被更新的,也就是说即使是私有的成员变量,也是会被赋值的;
class a
{
public:
int m_a = 0;
private:
int m_b = 0;
public:
void show()
{
cout << "m_a:" << m_a << " m_b:" << m_b << endl;
}
void setb(int b)
{
m_b = b;
}
};
class b :public a
{
public:
int m_c = 0;
void show()
{
cout << "m_a:" << m_a << " m_c:" << m_c << endl;
}
};
int main()
{
a a1;
b b1;
b1.m_a = 10;
b1.setb(20);
b1.m_c = 30;
a1.show();
a1 = b1;
a1.show();
return 0;
}
(2)基类的指针可以不经过显示转换指向派生类的对象,因为基类和派生类有特殊的关系,他们的内存模型是一样的,所以使用基类的指针执行派生类,没有问题;
还是上面得继承关系,我们定义基类的指针指向派生类的对象:
int main()
{
b b1;
a* a1 = &b1;
b1.m_a = 10;
b1.setb(20);
b1.m_c = 30;
b1.show();
a1->m_a = 10;
a1->setb(20);
a1->show();
return 0;
}
8.多态和虚函数
(1)什么是多态?
多态就是我们定义了两个类之间的继承关系,定义了基类的指针pa,指向的是派生类的对象g,这个时候我们使用这个指针去调用函数,就只能调用基类的构造函数,因为这个指针就是基类的;
我们在这个基类的成员函数show前面加上virtual关键字,就可以使用这个执行派生类对象的指针去访问这个派生类的函数,即使这个类是基类的,我们也是可以访问到这个派生类的成员函数的;
因为这个是函数前面加上virtual关键字,我们把这个函数叫做虚函数,有了虚函数之后,基类的指针指向基类对象就会调用基类的成员变量和函数,指向派生类的对象之后就会调用派生类的成员变量和函数,基类的指针表现出来多种形态,我们把这个现象叫做多态;
class allcomer
{
public:
int m_bh;
void show()
{
cout << "class allcomer我的编号是:" << m_bh << endl;
}
};
class cgril :public allcomer
{
public:
int m_age;
void show()
{
cout << "class cgril :public allcomer我的年龄是" << m_age << endl;
}
};
int main()
{
cgril g;
g.m_bh = 8;
g.m_age = 18;
g.show();
allcomer* pa = &g;
pa->show();
return 0;
}