一、运算符重载
运算符重载:对已有的运算符重新定义,赋予其另一种功能,以适应不同的数据类型
1、加号运算符重载
作用:实现两个自定义数据类型相加的运算
方法1、成员函数重载
class Person {
public:
Person operator+(Person& p) {
Person temp;
temp.m_a = this->m_a + p.m_a;
temp.m_b = this->m_b + p.m_b;
return temp;
}
int m_a;
int m_b;
};
void test() {
Person p1;
p1.m_a = 10;
p1.m_b = 20;
Person p2;
p2.m_a = 10;
p2.m_b = 20;
Person p3 = p1 + p2; //等价于p1.operator+(p2)
cout << "p3.m_a = " << p3.m_a << endl;
cout << "p3.m_b = " << p3.m_b << endl;
}
int main() {
test();
system("pause");
return 0;
}
方法2、全局函数重载
//定义类
class Person {
public:
int m_a;
int m_b;
};
//全局函数重载
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;
}
void test() {
Person p1;
p1.m_a = 10;
p1.m_b = 20;
Person p2;
p2.m_a = 10;
p2.m_b = 20;
Person p3 = p1 + p2; //等价于p1.operator+(p2)
cout << "p3.m_a = " << p3.m_a << endl;
cout << "p3.m_b = " << p3.m_b << endl;
}
int main() {
test();
system("pause");
return 0;
}
总结:
1、成员函数重载本质调用:Person p3 = p1.operator+(p2)
2、全局函数重载本质调用:Person p3 = operator(p1,p2)
3、运算符重载,也可以发生函数重载
2、左移运算符重载
作用:可以输出自定义数据类型
所以, 只能利用全局函数重载左移运算符
//左移重载
class Person {
public:
int m_a;
int m_b;
};
//全局函数重载
ostream& operator<<(ostream& cout, Person& p) {
cout <<"p.m_a = "<<p.m_a <<"p.m_a = "<<p.m_b << endl;;
return cout;
}
//测试
void test() {
Person p1;
p1.m_a = 10;
p1.m_b = 20;
cout << p1 << endl;
}
int main() {
test();
system("pause");
return 0;
}
扩展:如果我们需要打印的成员属性是私有的呢?全局函数无法访问,怎么重载呢?
答:对全局函数使用友元
//左移重载
class Person {
public:
//声明友元
friend ostream& operator<<(ostream& cout, Person& p);
//构造函数赋初值
Person(int a,int b) {
m_a = a;
m_b = b;
}
private:
int m_a;
int m_b;
};
//全局函数重载
ostream& operator<<(ostream& cout, Person& p) {
cout <<"p.m_a = "<<p.m_a <<" p.m_a = "<<p.m_b << endl;;
return cout;
}
//测试
void test() {
Person p1(10,20);//调用有参构造函数
cout << p1 << endl;
}
int main() {
test();
system("pause");
return 0;
}
3、递增运算符重载
作用:通过重载,实现自己的数据类型递增
前置++:
//++运算符重载
class Person {
public:
//前置++
Person& operator++() {
++m_a;
return *this;
}
int m_a = 0;
};
ostream& operator<<(ostream& cout,Person p) {
cout << p.m_a << endl;
return cout;
}
//测试
void test01() {
Person p;
p.m_a = 100;
cout << ++p << endl; //前置++
}
int main() {
test01();
system("pause");
return 0;
}
后置++:
重载后置递增,需要用int代表占位参数,用于区分前置和后置递增
//++运算符重载
class Person {
public:
//后置++
Person operator++(int) {
Person temp = *this; //记录操作前的结果
m_a++;
return temp;
}
int m_a = 0;
};
ostream& operator<<(ostream& cout,Person p) {
cout << p.m_a << endl;
return cout;
}
//测试
void test02() {
Person p1;
p1.m_a = 100;
cout << "后置++:" << p1++ << endl;//后置++
cout << "后置++:" << p1 << endl;
}
int main() {
test02();
system("pause");
return 0;
}
总结:
1、前置++,返回对象自身(*this),且是用引用的方式返回
2、后置++,需要记录操作前的结果,返回记录的结果,并且是用值的方式返回(因为用于记录操作前的结果是一个临时变量,返回引用会出错)
3、后置++,在参数上需要用int 作为占位参数
4、赋值运算符重载
c++编译器至少给一个类添加4个函数
1、默认构造函数(无参,函数体为空)
2、默认析构函数(无参,函数体为空)
3、默认拷贝构造函数,对属性进行值拷贝
4、赋值运算符 operator=, 对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
示例:
class Person {
public:
Person(int age) {
m_a = new int(age);
}
~Person() {
if (m_a != NULL) {
delete m_a;
m_a = NULL;
}
}
Person& operator=(Person& p) {
//系统默认浅拷贝
//this->m_a = p.m_a;
//深拷贝
if (this->m_a = NULL) {
delete m_a;
m_a = NULL;
}
this->m_a = new int(*p.m_a);
//返回自身
return *this;
}
int* m_a = NULL;
};
//测试
void test01() {
Person p(10);
Person p2(20);
Person p3(30);
cout << "赋值前:p1:" << *p.m_a << " p2:" << *p2.m_a << " p3:" << *p3.m_a << endl;
p = p2 = p3;
cout << "赋值后:p1:" << *p.m_a << " p2:" << *p2.m_a << " p3:" << *p3.m_a << endl;
}
int main() {
test01();
system("pause");
return 0;
}
5、关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行比较
示例:
//关系运算符重载
//1、== 2、 !=
class Person {
public:
Person(string name,int age) {
m_Name = name;
m_Age = age;
}
//重载 ==
int operator==(Person& p){
if (this->m_Age == p.m_Age && this->m_Name == p.m_Name) {
return 1;
}
else
return 0;
}
//重载 !=
int operator!=(Person& p){
if (this->m_Age != p.m_Age || this->m_Name != p.m_Name) {
return 1;
}
else
return 0;
}
string m_Name;
int m_Age;
};
//测试
void test01() {
Person p("Tom",18);
Person p2("Tom", 18);
Person p3("Tom", 18);
Person p4("Jiu", 18);
if (p == p2) {
cout << "p 和 p2 相等!" << endl;
}
else
cout << "p 和 p2 不相等!" << endl;
if (p3 != p4) {
cout << "p3 和 p4 不相等!" << endl;
}
else
cout << "p3 和 p4 相等!" << endl;
}
int main() {
test01();
system("pause");
return 0;
}
6、函数调用运算符重载
函数调用运算符 () 也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活
示例:
//函数调用运算符重载
//打印类
class myPrint {
public:
void operator()(string name) {
m_Name = name;
cout << m_Name << endl;
}
string m_Name;
};
void test() {
myPrint mp;
mp("Hello World");
}
int main() {
test();
system("pause");
return 0;
}
二、继承
继承是面向对象的三大特征之一
在定义一些类的时候,如果下级别的成员除了拥有上一级的共性,还有自己的特性,这时候我们就可以选择继承,来减少重复代码
1、继承的基本语法
class 子类 :继承类型 父类
例如:
class son : public father{
//成员
};
注意事项:
1、子类又称:派生类,父类又称:基类
2、子类的成员包括从父类继承来的,以及自己增加的成员
3、继承过来的表现共性,增加的表现个性
2、继承方式
继承方式分三种:
1、公有继承(public):当一个子类公有继承父类时,父类的公有成员也是子类的公有成员,父类的保护成员也是子类的保护成员,父类的私有成员不能直接被子类访问,但是可以通过调用父类的公有和保护成员来访问。
2、保护继承(protected): 当一个子类保护继承父类时,父类的公有和保护成员将成为子类的保护成员。
3、私有继承(private):当一个子类私有继承父类时,父类的公有和保护成员将成为子类的私有成员。
3、继承中的对象模型
问题:从父类继承过来的成员,那些属于子类对象?
答:在父类中的非静态成员属性都会被子类继承,私有属性也会继承,只是无法访问。会被编译器隐藏。
扩展:利用开发人员命令提示工具查看对象模型
1、跳转盘符:E:
(存放文件的盘符)
2、跳转文件路径: cd 具体路径下
3、查看命令:cl /d1 reportSinglcleClassLayout类名 "文件名"
4、继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序谁先谁后?
答:
1、先构造父类,在构造子类
2、析构与构造相反
可以看见,子类父类的顺序,同,一个类中的类成员顺序是一致的
5、继承同名成员处理方式
问题:当子类和父类出现同名的成员,如何通过子类或父类中同名的数据?
答:访问子类同名成员,直接访问;访问父类同名成员,需要加作用域。
注意事项:
1、如果子类中出现了和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数,即,不可直接调用重载
解决方法:调用时,增加作用域即可
6、继承同名静态成员处理
静态成员和非静态成员出现同名时,处理方法一致
1、访问子类同名成员,直接访问
2、访问父类同名成员,需要加作用域
7、多继承语法
c++中允许一个类继承多个类
语法:class 子类 : 继承方式 父类1,继承方式 父类2...
多继承可能回引发父类中同名成员出现,需要加作用域区分,实际开发中,不建议使用
//多父类继承
class Base1 { //父类1
public:
Base1() {
m_b1 = 100;
}
int m_b1;
};
class Base2 { //父类2
public:
Base2() {
m_b2 = 200;
}
int m_b2;
};
class Base3 { //父类3
public:
Base3() {
m_b3 = 300;
}
int m_b3;
};
//子类多继承
class Son : public Base1,public Base2, public Base3{
public:
int m_s1 = 400;
};
void test() {
Son s;
cout << "多继承子类字节数:" << sizeof(s) << endl;
cout << "Base1 = " << s.Base1::m_b1 << endl;;
cout << "Base2 = " << s.Base2::m_b2 << endl;;
cout << "Base3 = " << s.Base3::m_b3 << endl;;
cout << "Son = " << s.m_s1 << endl;;
}
int main() {
test();
system("pause");
return 0;
}
8、菱形继承
概念:
1、两个子类继承同一个父类
2、又有某个类同时继承两个子类
3、这种继承被称为菱形继承,或砖石继承
菱形继承的问题:
1、羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
答:加以作用域区分!
2、羊驼继承自动物的数据继承了两份,导致资源浪费
答:利用虚继承(在继承之前加上关键字virtual,变为虚继承),可以解决!
//菱形继承
class BASE { //父父类
public:
int m_a;
};
//父类1
class Base1 :virtual public BASE {};
//父类2
class Base2 :virtual public BASE {};
//子类
class Son : public Base1, public Base2{};
void test() {
Son s;
s.Base1::m_a = 100;
s.Base2::m_a = 200;
cout << "父类1,base1 :" << s.Base1::m_a << endl;
cout << "父类2,base3 :" << s.Base2::m_a << endl;
cout << "子类:Son :" << s.m_a << endl;
}
int main() {
test();
system("pause");
return 0;
}
虚继承原理:子类继承父类的两个指针,指针通过偏移量,找到同一份数据。
三、多态
1、多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类:
1、静态多态: 函数重载 和 运算符重载 属于静态多态,复用函数名
2、动态多态: 子类和虚函数实现运行时多态
区别:
1、静态多态的函数地址早绑定 - 编译阶段确定函数地址
2、动态多态的函数地址晚绑定 - 运行阶段确定函数地址
示例:
//多态
//动物类
class Animal {
public:
//虚函数,用virtual关键字修饰的函数
virtual void speek() {
cout << "动物说话..." << endl;
}
};
//猫类
class Cat : public Animal {
void speek() {
cout << "猫在说话..." << endl;
}
};
//狗类
class Dog : public Animal {
void speek() {
cout << "狗在说话..." << endl;
}
};
void doSpeek(Animal& ani) { //父类和子类类型可以相互转换
ani.speek();
}
void test() {
Cat c;
Dog d;
//调用执行说话函数
doSpeek(c);
doSpeek(d);
}
int main() {
test();
system("pause");
return 0;
}
总结:
动态多态的满足条件:
1、有继承关系
2、子类重写父类的虚函数
区分重写和重载:
重写:函数返回值类型,函数名,参数列表完全相同
重载:函数的参数的数量,类型或位置不同
动态多态的使用:
父类的指针或引用指向子类对象
2、深入剖析多态原理:
Animal类内部结构:
Cat类内部结构:
1、未重写父函数
2、重写父函数后
当子类重写父类虚函数后,子类的虚函数表内部会替换成子类的虚函数地址,父类不发生改变。
3、纯虚函数和抽象类
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
但类中有了纯虚函数,这个类就被称为抽象类
抽象类特点:
1、无法实例化对象
2、子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
4、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
1、可以解决父类指针释放子类对象
2、都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
示例:
//虚析构和纯虚析构
class Base {
public:
Base() {
cout << "Base构造函数调用" << endl;
}
virtual ~Base() = 0;
virtual void func() = 0;
};
Base:: ~Base(){
cout << "Base析构函数调用" << endl;
}
class Son :public Base {
public:
Son(string name) {
m_name = new string(name);
cout << "son构造函数调用" << endl;
}
~Son() {
cout << "son析构函数调用" << endl;
if (m_name != NULL) {
delete m_name;
m_name = NULL;
}
}
void func() {
cout << *m_name << endl;
}
string* m_name;
};
void test01() {
Base* b = new Son("TOM");
b->func();
delete b;
}
int main() {
test01();
system("pause");
return 0;
}
使用虚析构或纯虚析构前:
使用虚析构或纯虚析构后:
四、文件操作
文件操作头文件:<fstream>
操作文件的三大流:
1、ofstream:写操作
2、ifstream:读操作
3、fstream:读写操作
1、文本文件
写文件
步骤:
1、包含头文件:#include<fstream>
2、创建流对象:ofstream ofs;
3、打开文件:ofs.open("文件路径",打开方式);
4、写数据:ofs<<"写入的数据"
5、关闭文件:ofs.close();
文件打开方式:
打开方式
解释
ios::in
为读文件而打开文件
ios::out
为写文件而打开文件
ios::ate
初始位置:文件尾
ios::app
追加方式写文件
ios::trunc
如果文件存在先删除,再创建
ios::binary
二进制方式
注意:文件的打开方式可以配合使用,用 | (或)操作符
例如:用二进制方式写文件
ios::binary | ios::out
示例:
#include<fstream> //1、包含头文件
void wfile() {
//2、流对象
ofstream ofs;
//3、打开文件
ofs.open("test.txt",ios::out);
//4、写文件
ofs << "大鹏一日同风起" << endl;
ofs << "扶摇直上九万里" << endl;
ofs << "有约不来过夜半" << endl;
ofs << "闲敲棋子落灯花" << endl;
//关闭文件
ofs.close();
}
int main() {
wfile();
system("pause");
return 0;
}
读文件:
步骤:
1、包含头文件:#include<fstream>
2、创建流对象:ifstream ifs;
3、打开文件并判断是否成功打开:ifs.open("文件路径",打开方式);
4、读数据:四种方式读取
5、关闭文件:ifs.close();
四种读取方式:
不推荐使用第四种,其余的可根据个人喜好使用,个人推荐第三种~
示例:
#include<fstream> //1、包含头文件
#include <string>
void rfile() {
//2、流对象
ifstream ifs;
//3、打开文件,判断是否成功
ifs.open("test.txt", ios::in);
//判断
if (! ifs.is_open()) {
cout << "文件打开失败" << endl;
return;
}
//4、写文件
////第一种
//char Buf[1024] = { 0 };
//while (ifs>>Buf) {
// cout << Buf << endl;
//}
////第二种
//char Buf[1024] = { 0 };
//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;
//}
//关闭文件
ifs.close();
}
int main() {
rfile();
system("pause");
return 0;
}
2、二进制文件
打开方式指定为:ios::binary
写文件:
二进制方式写文件主要利用流对象调用成员函数write
函数原型 :ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include<fstream> //1、包含头文件
#include <string>
//二进制文件
class Person {
public:
char Name[10]; //姓名
int age;//年龄
};
//写二进制文件
void binWfile() {
Person p = { "张三",18};
//1、定义头文件
//2、流对象
ofstream ofs("Person.txt", ios::out | ios::binary);
//3、打开文件
//ofs.open("Person.txt", ios::in | ios::binary);
//4、写文件
ofs.write((const char*) & p,sizeof(Person));
//5、关闭文件
ofs.close();
}
int main() {
binWfile();
}
读文件:
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include<fstream> //1、包含头文件
//二进制文件
class Person {
public:
char Name[10]; //姓名
int age;//年龄
};
//读二进制文件
void binRfile() {
Person p;
//1、定义头文件
//2、流对象
ifstream ifs("Person.txt", ios::in | ios::binary);
if (!ifs.is_open()) {
cout << "二进制文件打开失败" << endl;
return;
}
//3、打开文件
//ofs.open("Person.txt", ios::in | ios::binary);
//4、读文件
ifs.read((char*)&p, sizeof(Person));
//ofs.write((const char*) & name, sizeof(Person));
cout << "姓名:" << p.Name << " 年龄:" << p.age << endl;
//5、关闭文件
ifs.close();
}
int main() {
binRfile();
}