一、继承
继承是面向对象三大特性之一
有些类除了有自己的特性,还与其他类存在共同的属性,比如学生和老师,他们都是属于同一个学校、同一个班等等。
这个时候我们可以考虑利用继承,减少重复代码。例如在很多网站中,它们都有共同的头部、底部等内容,它们都是由继承技术实现的。
1.继承的基本语法
基本语法:
class 父类名(基类) {
父类(基类)的成员变量和成员函数
};
class 子类(派生类)名 : 继承方式 父类名 {
子类(派生类)的成员变量和成员函数
};
例:
class Student
{
public:
string schoolname;
void print()
{
cout << "学校名字 :" << schoolname << endl;
}
private:
int student_id;
};
class Teacher : public Student
{
private:
int teacher_id;
};
int main()
{
Teacher t;
t.schoolname = "清华大学";//子类对象可以访问父类对象的属性
t.print();
return 0;
}
C++中允许一个类继承多个类,多继承语法:
class 子类: 继承方式 父类1 , 继承方式 父类2 ······
C++实际开发中不建议使用多继承,可能会引发有同名成员出现,需要加作用域区分,后面我们会学习到。
2.继承方式
继承方式也有三种:公共继承(public),保护继承(protected),私有继承(private)
public公共继承:可以继承父类的public和protected成员,且在子类中父类的public成员权限仍为public,父类的protected成员权限仍为protected;不可以访问父类的private成员,但仍会继承
private私有继承:可以继承父类的public和protected成员,且在子类中父类的public和protected成员的权限都变为private;不可以访问父类的private成员,但仍会继承
protected保护继承:可以继承父类的public和protected成员,且在子类中父类的public和protected成员的权限都变为protected;不可以访问父类的private成员,但仍会继承
例:
class ABC; //声明一个ABC类,作为父类
class a; class b; class c; //声明三个类a , b , c作为子类
class ABC
{
public:
int PublicMembers;
protected:
int ProtectedMembers;
private:
int PrivateMember;
};
class a:public ABC //公有继承
{
public:
int PublicMembers; //父类的公有成员在a中仍为公有成员
protect:
int ProtectedMembers; //父类的保护成员在a中仍为保护成员
};
class b:private ABC //私有继承
{
private:
int PublicMembers;
int ProtectedMembers; //父类的公有成员和保护成员在b中都为私有成员
};
class c:protected ABC //保护继承
{
protected:
int PublicMembers;
int ProtectedMembers; //父类的公有成员和保护成员在b中都为保护成员
};
由上可知,三种继承方式子类都是无法访问父类的private成员,但是子类仍会继承,只是被隐藏了。
例:
class A
{
public:
int PublicMembers;
protected:
int ProtectedMembers;
private:
int PrivateMember;
};
class a : public A //公有继承
{
public:
int a;
};
int main()
{
cout<<"size of class_a "<<sizeof(a)<<endl; //输出16
return 0;
}
输出结果为16,可知子类a继承了父类A的三类成员加上它自身的成员一共有4个整型数据成员,所以sizeof(a)的结果得到16
3.继承中的构造和析构顺序
例:
class Father
{
public:
Father()
{
cout << "调用父类构造函数" << endl;
}
~Father()
{
cout << "调用父类析构函数" << endl;
}
};
class Son : public Father
{
public:
Son()
{
cout << "调用子类构造函数" << endl;
}
~Son()
{
cout << "调用子类析构函数" << endl;
}
};
void test_01() //测试函数,这样我们就可以在里边看到析构函数的结果
{
Son s;
}
int main()
{
test_01();
return 0;
}
输出结果:
由结果我们可以知道,构造函数的调用顺序是先父后子,析构函数的调用顺序是先子后父。
4.继承的同名成员处理
面对父类和子类的同名成员,为了在调用时区分它们,我们在调用父类同名成员时可以使用父类名+指定作用域符号(::)来调用。
例:
class Father
{
public:
int a;
Father()
{
a = 10;
}
};
class Son:public Father
{
public:
int a = 20;
};
int main()
{
Son s;
cout<<" 调用Son 下的 a成员变量:"<<s.a<<endl;
cout<<" 调用Father 下的 a成员变量:"<<s.Father::a<<endl;
return 0;
}
对于同名的静态成员,我们也是以同样的方式处理。
5.菱形继承(钻石继承)和 虚继承
菱形继承概念:
两个子类继承同一个父类,又有某个类同时继承这两个子类,此时它们之间的关系图像一个菱形,所以称为菱形继承或钻石继承。
菱形继承的问题:
子类Aa和Ab会继承父类A的成员数据,而子子类Aaa同时继承了子类Aa和Ab,此时就会产生二义性,即子子类Aaa就会继承了两份一样的父类A的成员数据,造成资源浪费,其实我们只需要一份就足够了。此时我们可以用到虚继承的方式来解决。
虚继承语法:在继承方式前加上关键字virtual
class 子类: virtual 继承方式 父类 {};
二、多态
多态是C++面向对象的三大特性之一
多态又分为两类:
①静态多态:函数重载 和 运算符重载 属于静态多态 ,复用函数名
②动态多态:子类和虚函数实现运行时多态
两者的区别:
···静态多态的函数地址在编译阶段已经确定和绑定(早)
···动态多态的函数地址在运行阶段才确定和绑定(晚)
1.静态多态
静态多态的运算符重载在【Day4】中我们已经学习过了,今天我们来了解函数重载。
函数重载就是让同名但是参数不同的函数能发挥不同的功能,它有三个条件:
···同名函数必须在一个作用域下
···函数名称相同
···函数参数必须不同,可以是参数类型不同、个数不同、参数传入顺序不同等
例:
//在全局作用域下
void func()
{
cout<<" func的调用 "<<endl;
}
void func(int a)
{
cout<<" !func(int a)的调用!"<<endl;
}
int func(int a)
{
cout<<" !func(int a)的调用! "<<endl; //错误!这个函数与上面的func(int a)产生冲突
}
需要注意的是,函数的数据类型不同不能作为函数重载的条件。
2.动态多态
由于静态多态的函数地址在编译阶段已经确定和绑定,我们在通过父类地址调用子类的函数时就会失败,只能调用父类的函数。
动态多态的条件:
···有继承关系
···子类要重写父类的虚函数
动态多态的使用:
···使用父类的指针或者引用,执行子类对象
例:猫类是动物类的一个子类,我们想让猫和动物的对象能正确执行说话speak函数
class Animal
{
public:
void speak()
{
cout<<" 动物在说话 "<<endl;
}
};
class Cat:public Animal
{
public:
void speak()
{
cout<<" 猫在说话 "<<endl;
}
};
void doSpeak(Animal &animal) //使用父类的指针或者引用,指向子类对象
{
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat); //结果为 动物在说话
return 0;
}
得到的结果是因为函数的地址在编译阶段早就绑定了,调用了父类speak函数造成的,解决这一问题,我们可以采用动态多态,即采用虚函数,使函数的地址晚绑定。
例:
class Animal
{
public:
virtual void speak() //设置为虚函数
{
cout<<" 动物在说话 "<<endl;
}
};
class Cat:public Animal
{
public:
(virtual) void speak() //子类重写父类的虚函数 子类中virtual可写可不写
{
cout<<" 猫在说话 "<<endl;
}
};
void doSpeak(Animal &animal) //使用父类的指针或者引用,指向子类对象
{
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat); //结果为 猫在说话
return 0;
}
此时,我们就能输出我们想要的结果了。
3.多态的原理剖析
如上述例子,当Animal类里有一个虚函数,那么它的内存空间就会记录一个虚函数指针(vfptr),这个指针指向的是虚函数表(Vtable),表中记录的是Animal类里的虚函数地址。
而对于子类Cat,它会继承父类Animal的内容,但因为重写了父类虚函数,子类Cat虚函数表(Vtable)里记录的会是Cat类里的虚函数地址。
多态技术的优点:
···代码组织结构清晰
···可读性强
···利于前期和后期的扩展和维护
4.纯虚函数和抽象类
在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:
virtual 返回值类型 函数名(参数) = 0;
当类中有了纯虚函数,这个类也称为抽象类,抽象类有两个特点:
···无法实例化对象
···子类必须重写抽象类中的纯虚函数,否则也属于抽象类
例:
class Animal
{
public:
virtual void speak()=0;
};
class Cat:public Animal
{
public:
void speak() //重写父类中的纯虚函数
{
cout<<" 猫在说话 "<<endl;
}
};
void doSpeak(Animal &animal) //仍可以用父类的指针调用子类对象
{
animal.speak();
}
int main()
{
Animal animal;//错误,无法实例化抽象类的对象
new Animal; //错误,无法实例化抽象类的对象
return 0;
}
5.虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区(【Day3】中提到过内存分区模型),那么父类指针在释放时无法调用到子类的析构代码。为了解决这一问题,我们可以将父类中的析构函数改为虚析构或纯虚析构。
虚析构和纯虚析构共性:
···可以解决父类指针释放子类对象
···都需要有具体的函数实现
虚析构和纯虚析构区别:
···纯虚析构所在的类为抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名()= 0;
例:
class Animal
{
public:
Animal() //构造函数
{
cout<<"调用Animal构造函数"<<endl;
}
virtual void speak()=0; //纯虚函数
virtual ~Animal() //虚析构函数
{
cout<<"调用Animal析构函数"<<endl;
}
};
class Cat:public Animal
{
public:
Cat(string name) //构造函数
{
cat_name = new string(name); //动态分配内存到堆区
}
void speak()
{
cout<<*cat_name<<" 猫在说话 "<<endl;
}
~Cat() //析构函数
{
if(cat_name!=NULL)
{
cout<<"调用Cat析构函数"<<endl;
delete cat_name; //释放内存
cat_name = NULL; //释放指针
}
}
string *cat_name;
};
void doSpeak(Animal &animal)
{
animal.speak();
}
int main()
{
Animal * animal = new Cat("Tom"); //创建Tom猫对象
animal->speak();
delete animal;
return 0;
}
输出结果:
如此,我们就能正常执行子类析构函数。
若我们没有设置父类析构函数为虚时,就调用不了子类析构函数,
错误结果则会输出:
三、文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件我们可以将数据持久化
C++对文件进行操作需要包含头文件<fstream>
文件类型分为两种:
①文本文件:文件以文本的ASCⅡ码形式存储在计算机中
②二进制文件:文件以文本的二进制形式存储在计算机中,用户很难读懂
操作文件的三大类:
①ofstream:写操作 ②ifstream:读操作 ③fstream:读写操作
1. 写文件
写文件的步骤如下:
1.包含头文件: #include<fstream>
2.创建流对象:ofstream 文件名;
3.打开文件:文件名.open("文件路径" , 打开方式);
4.写数据:文件名<<"写入的数据" ;
5.关闭数据:文件名.close() ;
文件打开方式:(初学者只需掌握in、out、binary就行了)
打开方式 | 含义 |
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,在创建 |
ios::binary | 二进制方式 |
文件打开方式可以利用 | 或操作符来配合使用,例如:用二进制的方式写文件:
ios::bianry | ios::out
例:
#include "fstream" //头文件
int main()
{
ofstream file01; //创建流对象
file01.open("test.txt",ios::out); //未指定路径的话会默认将文件存在项目文件夹下
file01<<" Hello World "<<endl;
file01<<" My name is qhl "<<endl; //写文件
file01.close(); //关闭文件
return 0;
}
2.读文件
读文件的步骤如下:
1.包含头文件: #include<fstream>
2.创建流对象:ifstream 文件名;
3.打开文件并判断是否成功:文件名.open("文件路径" , 打开方式);
4.读数据:有四种方式进行读取
5.关闭数据:文件名.close() ;
例:
#include "fstream" //头文件
int main()
{
ifstream file01; //创建流对象
file01.open("test.txt",ios::in | ios::out);
if( !file01.is_open() ) //判断文件是否打开
{
cout<<"文件打开失败"<<endl;
}
//第一种读取方法
char ch[1024] = { 0 };
while ( file01>>ch ) //读取文件存入数组
{
cout<<ch<<endl;
}
第二种读取方法
char ch[1024] = {0} ;
while (file01.getline(ch,sizeof(ch)));
{
cout<<ch<<endl;
}
//第三种读取方法
string ch;
while( getline(file01,ch) )
{
cout<<ch<<endl;
}
//第四种读取方法
char ch;
while ( ( ch = file01.get( ) ) != EOF ) //EOF代表文件尾部位置
{
cout<<ch;
}
file01.close();
return 0;
}