第三部分 C++核心编程二
本阶段主要针对C++面向对象编写,以及文件操作
目录
1,面向对象
C++面向对象的三大特性为:封装,继承,多态
关于对象的说法:万事万物皆为对象,对象有其属性和行为
eg:
人可以作为对象,属性有:姓名,身高,体重,年龄
车也可以作为对象,属性有:轮胎,方向盘,车灯
具有相同性质的对象,可以抽象称为类。
1.1封装
封装意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class 类名{ 访问权限: 属性/行为 };
如下:创建一个圆类
#include<iostream>
#include<string>
using namespace std;
#define PI 3.14
class Circle
{
public:
int R; //属性
double Rlong() //行为
{
return 2 * PI*R;
}
};
int main()
{
Circle c1; //实例化,创建一个类
c1.R = 2;
cout << "圆的周长为" << c1.Rlong() << endl;
system("pause");
return 0;
}
类在设计时,可以把属性和行为放在不同的权限下,加以控制
三种访问权限:
- public 共有权限 类内可以访问,类外可以访问
- protected 保护权限 类内可以访问,类外不可以访问(继承时,子类可以访问)
- private 私有权限 类内可以访问,类外不可以访问(继承时,子类仍不可访问)
1.1.1struct和class区别
两者的区别在于默认的访问权限不同
区别:
- struct的默认权限为公共
- class的默认权限为私有
class Test1
{
int a; //默认为私有,不可访问
};struct Test2
{
int b; //默认为公有,可以访问
};
1.1.2成员属性设置为私有
优点:
- 将所有成员属性设置为私有,可以自己控制读写权限
- 对于写权限,我们可以检测数据的有效性
#include<iostream>
#include<string>
using namespace std;
class Test1
{
public:
void set(int n) //通过函数给私有变量赋值
{
a = n;
}
int get() //通过函数获得私有变量的值
{
return a;
}
private:
int a;
};
int main()
{
Test1 s1;
s1.set(10);
cout << s1.get() << endl;
system("pause");
return 0;
}
1.2对象的初始化和清理
1.2.1构造函数和析构函数
对象的初始化和清理十分重要
- 一个对象或者变量没有初始状态,对其使用后果是未知的
- 同样的使用完一个对象或变量,没有及时的清理,也会造成一定的安全问题
如果我们不提供构造和析构函数,则编译器会提供编译的构造函数和析构函数说空实现
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以由参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
Test1()
{
cout << "Test1的构造函数调用" << endl;
}
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前面加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
~Test1()
{
cout << "Test1的析构函数调用" << endl;
}
1.2.2构造函数的分类及调用
两种分类方式
1,按参数分类:
- 有参构造
- 无参构造
2,按类型分类
- 普通构造(包括有参构造和无参构造)
- 拷贝构造
Test1() //无参构造
{
cout << "Test1的构造函数调用" << endl;
}
Test1(int a) //有参构造
{
cout << "Test1的有参构造函数" << endl;
}
Test1(const Test1 &p) //拷贝构造函数
{
cout << "Test1的拷贝构造函数" << endl;
}
三种调用方法
Test1 p; //默认构造函数调用
- 括号法
Test1 p_1(10); //有参构造函数
Test1 p_2(p); //拷贝构造函数
- 显示法
Test1 pp_1 = Test1(10);//有参构造
Test1 pp_2 = Test1(pp_1);//拷贝构造
- 隐式转换法
Test1 ppp_1 = 10; //相当于 Test1 p_4=Test1(10)
Test1 ppp_2 = ppp_1;
匿名对象:显示法中的 Test1(10)
这一行执行完,匿名对象就会立即会收掉匿名对象,但是仍然调用了构造函数和析构函数
注意事项:
- 调用默认构造函数时,不要加();如Test1(),编译器会认为是一个函数的声明
- 不要利用拷贝构造函数,初始化匿名对象
Test1(p); //编译器会认为是Test1(p)=Test1 p;所以此时对象p就被创建出来了
1.2.3拷贝构造函数调用时机
三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
void test_1()
{
Test1 p(10);
Test1 p1(p);
}
- 值传递的方式给函数参数传值
void do1(Test1 p)
{}
void test_2()
{
Test1 p;
}这个地方可能由于编译器优化,结果会不一样,
推荐一篇我搜了几个小时找到的贺老师的文章:C++返回值为对象时复制构造函数不执行怎么破_迂者-贺利坚的博客-CSDN博客
- 以值的方式返回局部对象
Test1 do2()
{
Test1 p(10);
return p;
}
void test_3()
{
Test1 p = do2();
}
1.2.4构造函数调用规则
默认情况下,C++编译器至少给一个编译器添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性的值进行拷贝
调用规则:
- 如果用户自定义有参构造函数,C++不会提供默认无参构造函数,但是会提供默认拷贝构造函数(需要自定义无参构造)
- 如果用户自定义拷贝构造函数,C++不会再提供其它构造函数(即需要自定义有参或无参构造函数)
1.2.5深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,尽心拷贝操作
主要是当类的属性有指针时,需要用到深拷贝
(仅仅写了一个拷贝构造函数,用于解释深拷贝,其它函数没写)
class Test1{
public:
Test1(const Test1 &p)
{
a = p.a;
//b = p.b; 编译器浅拷贝的做法,次数运用到指针是错误的
b = new int(*p.b);
}
private:
int a;
int *b;
};
1.2.6初始化列表
作用:C++提供了初始化列表语法,用于初始化列表
语法:构造函数():属性1(值1),属性2(值2).....{}
class Test1{
public:
Test1(int aa, int bb) :a(aa), b(&bb)
{}
private:
int a;
int *b;
};
1.2.7类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
class A{};
class B
{
A a;
};
当创建B对象时,先构造类对象,再构造本身;析构的时候,先析构自己,再析构类对象
1.2.8静态成员
静态成员就是在成员变量和成员函数之前加上ststic关键字,称为静态成员
静态成员分为:
1,静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
static void show()
{
a = 20;
cout << "访问静态成员函数" << endl;
}
2,静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量,因为非静态成员变量不能区分出是哪一个对象的成员变量
static int a;
访问方式:
- 通过对象访问
- 通过类名访问
- 类外访问不到静态私有函数
A b;
b.show(); //通过对象A::show(); //通过类名
#include<iostream>
using namespace std;
class A
{
public:
static void show()
{
a = 20;
cout << "访问静态成员函数" << endl;
}
private:
static void showP()
{
a = 20;
cout << "私有静态成员函数" << endl;
}
int b;
static int a;
};
int A::a = 10;
int main()
{
A b;
b.show(); //通过对象
A::show(); //通过类名
//A::showP(); //错误,因为不能访问私有静态成员函数
system("pause");
return 0;
}
1.3C++对象模型和this指针
1.3.1成员变量和成员函数分开存储
在C++中,类的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
- 空对象占用的内存空间为:0;为了区分空对象所占位置,每个空对象都有一个独一无二地址
- 当类中有非静态成员变量时,大小即为非静态成员变量的大小;向类中添加静态成员变量或函数,大小都不变
1.3.2this指针概念
this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
- 当形参和成员函数同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可以用this
this指针的本质是指针常量,指针的指向是不可以修改的
A *const this
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
{
this->a = a;
}
A& add(A &p) //如果此处不是用引用返回,则每次返回都会创建一个新的对象
{
this->a += p.a;
return *this;
}
int a;
};
int main()
{
A m(10);
A n(10);
n.add(m).add(m).add(m);
cout << n.a << endl;
system("pause");
return 0;
}
1.3.3空指针访问成员函数
C++中空指针也是可以使用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断代码的健壮性
即:调用的成员函数不能包含属性等东西,因为this为空,无法显示
#include<iostream>
using namespace std;
class A
{
public:
void show()
{
cout << "打印***" << endl;
}
void put()
{
//cout << a << endl; //错误;此处a相当于this->a,this为空,所以无法输出;
}
private:
int a;
};
void test()
{
A *a = NULL;
a->show();
a->put();
}
int main()
{
test();
system("pause");
return 0;
}
1.3.4const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数 (void change_2() const)
- 常函数内不可修改成员属性
- 成员函数声明时加关键字mutable后,在常函数中依然可以修改(mutable int b;)
常对象:
- 声明对象前加const称该对象为常对象(const A q;)
- 常对象只能调用常函数
#include<iostream>
using namespace std;
class A
{
public:
void change_1()
{
a = 100;
b = 100;
}
//在成员函数后面加const,修饰的是this指针,让指针指向的值也不可以修改
void change_2() const //相当于 const A* const this
{
//a = 100; //不可以修改
b = 100; //特殊变量,可以修改
}
int a;
mutable int b; //特殊变量,即使在常函数中也可以修改
};
void test()
{
A p;
p.a = 100;
p.b = 100;
p.change_1();
p.change_2();
}
void test2()
{
const A q;
//q.a = 100; //常对象不能修改普通值
q.b = 100;
//q.change_1(); //错误,常对象只能调用常函数,因为普通成员函数可以修改属性
q.change_2();
}
int main()
{
test();
system("pause");
return 0;
}
1.4友元函数
友元的关键字:friend
三种实现形式:
- 全局函数做友元
即:当全局函数中的对象想要访问类中的私有属性时,需要将该全区函数在类中加以声明为friend类型
class Student
{
//此时全局函数show是类Student的好朋友,可以访问类的私有成员
friend void show(Student &a); //此时全局函数中的对象都可以访问类中的私有属性
........
#include<iostream>
using namespace std;
#include<string>
class Student
{
friend void show(Student &a);
public:
Student()
{
age = 18;
name = "小明";
}
string name;
private:
int age;
};
void show(Student &a)
{
cout << a.name << endl;
cout << a.age << endl;
}
int main()
{
Student m;
show(m);
system("pause");
return 0;
}
- 类做友元
class Class
{
..........
};class Student
{
friend class Class;
.........
#include<iostream>
using namespace std;
#include<string>
class Student;
class Class
{
public:
Class();
void show();
private:
Student *s;
};
class Student
{
friend class Class;
public:
Student()
{
age = 18;
name = "小明";
}
string name;
private:
int age;
};
Class::Class()
{
s = new Student;
}
void Class::show()
{
cout << s->age << endl;
cout << s->name << endl;
}
int main()
{
Class a;
a.show();
system("pause");
return 0;
- 成员函数做友元
class Class
{
..........
};class Student
{
friend void Class::show(); //和类做友元类似,此处把类变成了函数
.........
#include<iostream>
using namespace std;
#include<string>
class Student;
class Class
{
public:
Class();
void show();
void show1();
private:
Student *s;
};
class Student
{
friend void Class::show();
public:
Student()
{
age = 18;
name = "小明";
}
string name;
private:
int age;
};
Class::Class()
{
s = new Student;
}
void Class::show()
{
cout << s->age << endl; //类的友元函数,可以访问私有属性
cout << s->name << endl;
}
void Class::show1()
{
// cout << s->age << endl; //不能访问私有属性
cout << s->name << endl;
}
int main()
{
Class a;
a.show();
a.show1();
system("pause");
return 0;
}
1.5运算符重载
概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
1.5.1加号运算符重载
实现两个自定义数据类型相加的运算
和正常的相加思路类似,只不过这里需要引用其他的对象作为参数;
//成员函数实现“+”运算符重载 p1=p2+p3;类似于:p1=p2.operator+(p3);
Class operator+(const Class &p){
Class q(0, 0);
q.a = this->a + p.a;
q.b = this->b + p.b;
return q;
}
//全局函数实现“+”运算符重载 p1=p2+p3;类似于:p1=operator+(p2,p3);
Class operator+(const Class &p1, const Class &p2){
Class q(0,0);
q.a = p1.a + p2.a;
q.b = p1.b + p2.b;
return q;
}
#include<iostream>
using namespace std;
#include<string>
class Class
{
public:
Class(int a, int b) :a(a), b(b){}
void show()
{
cout<<"a="<< a << " b=" << b << endl;
}
//成员函数实现“+”运算符重载
Class operator+(const Class &p){
Class q(0, 0);
q.a = this->a + p.a;
q.b = this->b + p.b;
return q;
}
//private:
int a;
int b;
};
全局函数实现“+”运算符重载
//Class operator+(const Class &p1, const Class &p2){
// Class q(0,0);
// q.a = p1.a + p2.a;
// q.b = p1.b + p2.b;
// return q;
//}
int main()
{
Class a(10,10);
Class b(20, 30);
Class c(0, 0);
c= a + b;
c.show();
system("pause");
return 0;
}
1.5.2运算符重载
作用:可以输出自定义数据类型
//只能用全局函数实现“<<”运算符重载
ostream & operator<<(ostream &cout, const Class &p) //本质operator<<(cout,p),简化cout<<p
{
cout << p.a << " " << p.b << endl;
return cout; //此处返回cout的为了能够实现链式编程,可以多次返回输出
}
#include<iostream>
using namespace std;
#include<string>
class Class
{
public:
Class(int a, int b) :a(a), b(b){}
int a;
int b;
};
//如果利用成员函数重载 左移运算符 p.operator<<(cout) 简化版本 p<<cout;p在左侧错误
//只能用全局函数实现“<<”运算符重载
ostream & operator<<(ostream &cout, const Class &p) //本质operator<<(cout,p),简化cout<<p
{
cout << p.a << " " << p.b << endl;
return cout;
}
int main()
{
Class a(10,10);
cout << a<<endl;
system("pause");
return 0;
}
1.5.3递增运算符重载
作用:通过重载递增运算符,实现自己的整型数据
//重载前置++运算符
Class &operator++() //返回引用是为了一直对一个数据进行增值操作
{
a++;
return *this;
}//重载后置++运算符 //此时不能再使用链式操作了
Class operator++(int) //此处int作为占位符,用来区分前置还是后置
{
Class p = *this; //记录当前本身的值,然后让本身的值加1,但是返回的值还是以前的值
a++;
return p;
}前置返回的是一个引用,而后置返回的是结果是一个临时对象。因为后置的时候原来的对象已经被++改变了,所以需要一个新对象来保存改变之前的值。而前置用引用则是为了不产生临时变量,减少内存的消耗而已。
所以前置引用可以使用链式的方法,多次前置++,而后置不可以多次++;
#include<iostream>
using namespace std;
#include<string>
class Class
{
friend ostream & operator<<(ostream &cout, const Class &p);
public:
Class(int a) :a(a){}
//重载前置++运算符
Class &operator++() //返回引用是为了一直对一个数据进行增值操作
{
a++;
return *this;
}
//重载后置++运算符
Class operator++(int) //此处int作为占位符,用来区分前置还是后置
{
Class p = *this; //记录当前本身的值,然后让本身的值加1,但是返回的值还是以前的值
a++;
return p;
}
private:
int a;
};
ostream & operator<<(ostream &cout, const Class &p)
{
cout << p.a << endl;
return cout;
}
int main()
{
Class a(10);
cout << ++(++a)<<endl;
cout << a << endl;
cout << a++ << endl;
cout << a << endl;
system("pause");
return 0;
}
1.5.4赋值运算符重载
赋值运算符需要注意的地方就是当数据在堆区存储时,注意开辟新的空间以及及时释放空间
void operator = (Class &p)
{
if (a != NULL) //如果已经有值,需要先释放,再进行赋值
{
delete a;
a = NULL;
}
//深拷贝
a = new int(*p.a);
}
#include<iostream>
using namespace std;
#include<string>
class Class
{
friend ostream & operator<<(ostream &cout, const Class &p);
public:
Class(int a){
this->a = new int(a);
}
void operator = (Class &p)
{
if (a != NULL)
{
delete a;
a = NULL;
}
//深拷贝
a = new int(*p.a);
}
private:
int *a;
};
ostream & operator<<(ostream &cout, const Class &p)
{
cout << *p.a << endl;
return cout;
}
int main()
{
Class a(10);
Class b(20);
a = b;
cout << a << endl;
system("pause");
return 0;
}
1.5.5关系运算符重载
作用:可以让两个自定义类型对象进行对比操作
bool operator == (Class &p) //相对比较简单,就简单传值,然后对比一下
{
if (this->a == a) {
return true;
}
else {
return false;
} }
#include<iostream>
using namespace std;
class Class
{
public:
Class(int a){
this->a = a;
}
bool operator == (Class &p)
{
if (this->a == a)
{
return true;
}
else
{
return false;
}
}
private:
int a;
};
int main()
{
Class a(10);
Class b(20);
if (a == b)
{
cout << "两个数相等" << endl;
}
else
{
cout << "两个数不相等" << endl;
}
system("pause");
return 0;
}
1.5.6函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方法非常像函数的调用,因此称为仿函数
- 仿函数没有固定的写法,非常灵活
void operator()(string world)
{
cout << world << endl;
}
#include<iostream>
using namespace std;
#include<string>
class Class
{
public:
void operator()(string world)
{
cout << world << endl;
}
};
int main()
{
Class a;
a("hells world"); //由于使用起来类似于函数调用,因此称为仿函数;非常灵活,没有固定写法
//此处为匿名函数对象
Class()("hello world"); //Class()代替了类似于a
system("pause");
return 0;
}
1.6继承
继承的好处:减少重复代码
继承语法:class A(子类) : 继承方式 B(父类)
子类也称为派生类;父类也称为基类
派生类中的成员,包含两大部分:
- 从基类继承过来的,表现其共性
- 自己增加的函数,体现其个性
1.6.1 继承方式
一共有三种:
- 公有继承 (三种权限不变)
- 保护继承 (公+保->保,私不变)
- 私有继承 (公+保->私,私不变)
- 继承时,私有成员子类均不可访问,保护成员子类可以访问
下面这段代码,详细注释了各种情况下的访问和继承
#include<iostream>
using namespace std;
#include<string>
class Class
{
public:
int a;
protected:
int b;
private:
int c;
};
class sun1:public Class
{
void fun()
{
a = 10; //公有 不变
b = 10; //保护 不变
//c = 10; 错误,子类不可访问父类的私有成员
}
};
class sun2 :protected Class
{
void fun()
{
a = 10; //保护 变为
b = 10; //保护 不变
//c = 10; 错误,子类不可访问父类的私有成员
}
};
class sun3 :private Class
{
void fun()
{
a = 10; //私有 变为
b = 10; //私有 变为
//c = 10; 错误,子类不可访问父类的私有成员
}
};
void test()
{
sun1 p1;
p1.a = 10;
//p1.b = 20; //保护b不可访问
//p1.c = 30; //私有c不可访问
sun2 p2;
//p2.a = 10; //保护a不可访问
//p2.b = 20; //保护b不可访问
//p2.c = 30; //私有c不可访问
sun3 p3;
//p3.a = 10; //私有a不可访问
//p3.b = 20; //私有b不可访问
//p3.c = 30; //私有c不可访问
}
int main()
{
test();
system("pause");
return 0;
}
1.6.2继承中的对象模型
继承规则:
- 父类中所有非静态成员属性都会被子类继承下去
- 父类中私有成员属性,是被编译器给隐藏了,因此是访问不到的,但是确实是继承下去了
class Class{
public: int a;
protected: int b;
private: int c;
};
class sun1 :public Class{
int a;
}; //此时sun1定义出的对象大小就为16,继承的三个加上新增加的一个
1.6.3继承中构造和析构顺序
先构造父类,再构造子类,析构的顺序与构造的顺序相反(可以自己写代码实验)
1.6.4继承同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
sun p;
cout << p.a << endl;
cout << p.Base::a << endl;
- 当子类与父类拥有同名成员函数,子类会隐藏父类中所有版本的同名成员函数
- 如果想访问父类中隐藏的同名成员函数,需要加父类的作用域
p.change(); //子类中的成员函数
//p.change(a); //此处虽然是重载函数,但是只要子类父类发生重名,子类就会隐藏父类所有同名函数
p.Base::change(9); //父类中的带参成员函数
p.Base::change(); //父类中的不带参成员函数
#include<iostream>
using namespace std;
#include<string>
class Base{
public:
Base()
{
a = 10;
}
void change()
{
a = 100;
}
void change(int x)
{
a = x;
}
int a;
};
class sun:public Base{
public:
sun()
{
a = 20;
}
void change()
{
a = 200;
}
int a;
};
void test()
{
sun p;
//成员
cout << p.a << endl;
cout << p.Base::a << endl;
//成员函数
p.change(); //子类中的成员函数
//p.change(a); //此处虽然是重载函数,但是只要子类父类发生重名,子类就会隐藏父类所有同名函数
p.Base::change(9); //父类中的带参成员函数
p.Base::change(); //父类中的不带参成员函数
}
int main()
{
test();
system("pause");
return 0;
}
1.6.5继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式一致,只不过有两种访问方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
cout << p.a << endl;
cout << p.Base::a << endl;cout << sun::a << endl;
cout << sun::Base::a << endl;
1.6.6多继承语法
C++中允许一个类继承多个类
语法: class 子类 :继承方式 父类1,继承方式 父类2........
(实际开发中不建议)
class Base :public Base1, public Base2{}
当父类中出现同名成员,需要加作用域进行区分
cout << p.a << endl;
cout << p.Base1::a << endl;
cout << p.Base2::a << endl;
#include<iostream>
using namespace std;
#include<string>
class Base1{
public:
Base1()
{
a = 10;
}
int a;
};
class Base2{
public:
Base2()
{
a = 20;
}
int a;
};
class Base :public Base1, public Base2
{
public:
Base()
{
a = 30;
}
int a;
};
void test()
{
Base p;
cout << p.a << endl;
cout << p.Base1::a << endl;
cout << p.Base2::a << endl;
}
int main()
{
test();
system("pause");
return 0;
}
1.6.7菱形继承
概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承两个派生类
- 这种继承称为菱形继承
如图所示:B1,B2继承A,C又继承B1,B2。
当出现这种零星继承时,如果两个父类拥有相同数据,需要加以作用域区分
这份数据我们知道,只需要一份就足够了,而菱形继承导致数据有两份,资源浪费。
解决方法:
- 利用虚继承 解决菱形继承的问题
- 继承之前加上关键字virtual变为虚继承
- A变为虚基类
class B1 :virtual public A{};
class B2 :virtual public A{};
1.7多态
1.7.1多态的基本概念
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
地址早绑定的话,在编译阶段就确定了函数地址,因此函数就不会更改了,一直显示某一个
若要按要求执行,使得函数不提前绑定,就需要用到动态多态,即加上关键字"virtual",变成虚函数
动态多态满足的条件:
- 有继承关系
- 子类重写父类的虚函数 (重写:函数返回值类型,函数名,参数列表完全一致称为重写)
动态多态的使用:
- 父类指针或者引用,执行子类对象
#include<iostream>
using namespace std;
#include<string>
class Base1{
public:
//虚函数
virtual void show()
{
cout << "Base1" << endl;
}
};
class Base2:public Base1
{
public:
void show()
{
cout << "Base2" << endl;
}
};
class Base3 :public Base1
{
public:
void show()
{
cout << "Base3" << endl;
}
};
void test(Base1 &p)
{
p.show();
}
int main()
{
Base1 p1;
Base2 p2;
Base3 p3;
test(p1);
test(p2);
test(p3);
system("pause");
return 0;
}
1.7.2 纯虚函数和抽象类
因为在多态中,通常父类的虚函数实现毫无意义,通常都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
语法:virtual 返回值类型 函数名(参数列表)=0
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base1{
public:
//纯虚函数,所以此类也被称为抽象类
virtual void show() = 0;
};则不能Base1 p; 因为Base1是抽象类,抽象类不能实例化对象。
#include<iostream>
using namespace std;
#include<string>
class Base1{
public:
//纯虚函数,所以此类也被称为抽象类
virtual void show() = 0;
};
class Base2:public Base1
{
public:
//重写父类Base1中的show函数
void show(){ cout << "Base2"; }
};
class Base3 :public Base1
{
};
void test()
{
//Base1 p; //抽象类不能实例化对象
//Base3 p; //如果不重写抽象类中的纯虚函数,则子类也变成抽象类
Base2 p;
p.show();
}
int main()
{
test();
system("pause");
return 0;
}
1.7.3虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析类或者纯虚析构
- 如果子类中没有堆区数据,可以不写虚析构或者纯虚函数
虚析类和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析类和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名()=0
//利用虚析构可以解决父类释放子类对象时堆区数据泄露的问题
virtual ~Base1()
{
cout << "Base1的析构函数" << endl;
}//纯虚析构 需要声明也需要实现
//有了纯虚析构,这个类也属于抽象类,无法实例化对象
virtual ~Base1() = 0;
#include<iostream>
using namespace std;
#include<string>
class Base1{
public:
Base1()
{
cout << "Base1的构造函数" << endl;
}
利用虚析构可以解决父类释放子类对象时堆区数据泄露的问题
//virtual ~Base1()
//{
// cout << "Base1的析构函数" << endl;
//}
//纯虚析构 需要声明也需要实现
//有了纯虚析构,这个类也属于抽象类,无法实例化对象
virtual ~Base1() = 0;
};
Base1::~Base1()
{
cout << "Base1纯虚析构函数" << endl;
}
class Base2:public Base1
{
public:
Base2()
{
cout << "Base2的构造函数" << endl;
}
~Base2()
{
cout << "Base2的析构函数" << endl;
}
};
void test()
{
Base1 *p = new Base2;
//父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区数据,会出现内存泄露
delete p;
}
int main()
{
test();
system("pause");
return 0;
}
2,文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化,需要包含的头文件<fstream>
文件类型分为两种:
- 文本文件:文件以文本的ASCLL码形式存储在计算机中
- 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
2.1文本文件
2.1.1写文件
步骤:
- 包含头文件:#include<fstream>
- 创建流对象:ofstream test;
- 打开文件:test.open("文件路径",打开方式);
- 写数据:test<<"写入的数据";
- 关闭文件:test.close();
//头文件
ofstream test;
test.open("test.txt", ios::out);
test << "姓名:小明" << endl;
test.close();
文件打开方式
打开方式 | 解释 |
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
文件打开方式可以配合使用,利用” | ”操作符。
2.1.2读文件
读文件与写文件步骤相似,但是读取方式相对较多
读取文件步骤:
- 包含头文件:#include<fstream>
- 创建流对象:ifstream test;
- 打开文件:test.open("文件路径",打开方式);
- 读数据:四种方式读取
- 关闭文件:test.close();
#include<iostream>
using namespace std;
#include<fstream>
#include<string>
int main()
{
ifstream test;
test.open("test.txt", ios::in);
第一种
//char buf_1[1024] = { 0 };
//while (test >> buf_1)
//{
// cout << buf_1 << endl;
//}
第二种
//char buf_2[1024] = { 0 };
//while (test.getline(buf_2,sizeof(buf_2)))
//{
// cout << buf_2 << endl;
//}
第三种
//string buf_3;
//while (getline(test,buf_3))
//{
// cout << buf_3 << endl;
//}
第四种
//char buf_4;
//while ((buf_4 = test.get()) != EOF)
//{
// cout << buf_4;
//}
test.close();
system("pause");
return 0;
}
2.2二进制文件
以二进制的方式对文件进行读写操作
打开方式指定为 ios::binary
2.2.1写文件
二进制方式写文件主要是利用流对象调用成员函数write
函数原型:ostream& write(const char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
2.2.2读文件
二进制方式写文件主要是利用流对象调用成员函数read
函数原型:istream& read(const char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数。
关于文件操作链接:如何将结构体数组数据存到文件并读取:三种方法(C语言两种,C++一种)_双鱼座boyy的博客-CSDN博客_如何将结构体的数据保存到文件