作业:
1> 将之前定义的栈类和队列类都实现成模板类
栈:
#include <iostream>
#define MAX 40
using namespace std;
template <typename T>
class Stack{
private:
T *data;
int top;
public:
Stack();
~Stack();
Stack(const Stack &other);
Stack &operator=(const Stack &other);
//判空
bool empty();
//判满
bool full();
//获取栈的大小
int size();
//入栈
void push();
//出栈
void pop();
//获取栈顶元素
T get_top();
};
template <typename T>
Stack<T>::Stack():data(new T[MAX]),top(-1){
cout<<"无参构造"<<endl;
}
template <typename T>
Stack<T>::~Stack(){
delete []data;
cout<<"析构函数"<<endl;
}
template <typename T>
Stack<T>::Stack(const Stack &other){
top = other.top;
data = new T[MAX];
memcpy(data,other.data,sizeof(other.data[0])*(other.top+1));
cout<<"拷贝构造"<<endl;
}
template <typename T>
Stack<T>&Stack<T>::operator=(const Stack &other){
top = other.top;
memcpy(data,other.data,sizeof(other.data[0])*(other.top+1));
cout<<"拷贝赋值"<<endl;
}
//判空
template <typename T>
bool Stack<T>::empty(){
return top == -1;
}
//判满
template <typename T>
bool Stack<T>::full(){
return top == MAX-1;
}
//获取栈的大小
template <typename T>
int Stack<T>::size(){
return top+1;
}
//入栈
template <typename T>
void Stack<T>::push(){
if(full()){
cout<<"栈满!入栈失败!"<<endl;
return;
}
T value;
cout<<"请输入入栈的元素:";
cin>>value;
top++;
data[top] = value;
cout<<value<<" 入栈成功"<<endl;
}
//出栈
template <typename T>
void Stack<T>::pop(){
if(empty()){
cout<<"栈空!出栈失败!"<<endl;
return;
}
T temp = data[top--];
cout<<temp<<" 出栈成功"<<endl;
}
//获取栈顶元素
template <typename T>
T Stack<T>::get_top(){
return data[top];
}
int main()
{
Stack<int> s;
s.push();
s.push();
s.push();
cout<<"s.size = "<<s.size()<<endl;
s.pop();
cout<<"s.get_top = "<<s.get_top()<<endl;
s.pop();
s.pop();
s.pop();
return 0;
}
队列:
#include <iostream>
#define MAX 40
using namespace std;
template<typename T>
class Queue{
private:
T *data;
int front;
int back;
public:
Queue();
~Queue();
Queue(const Queue &other);
Queue &operator=(const Queue &other);
//判空
bool empty();
//判满
bool full();
//获取队列大小
int size();
//入队
void push();
//出队
void pop();
};
template <typename T>
Queue<T>::Queue():data(new T[MAX]),front(0),back(0){
cout<<"无参构造"<<endl;
}
template <typename T>
Queue<T>::~Queue(){
delete []data;
cout<<"析构函数"<<endl;
}
template <typename T>
Queue<T>::Queue(const Queue &other){
front = other.front;
back = other.back;
data = new T[MAX];
memcpy(data,other.data,sizeof(other.data[0])*((other.back+MAX-other.front)%MAX));
cout<<"拷贝构造"<<endl;
}
template <typename T>
Queue<T>&Queue<T>::operator=(const Queue &other){
front = other.front;
back = other.back;
memcpy(data,other.data,sizeof(other.data[0])*((other.back+MAX-other.front)%MAX));
cout<<"拷贝赋值"<<endl;
}
//判空
template <typename T>
bool Queue<T>::empty(){
return front == back;
}
//判满
template <typename T>
bool Queue<T>::full(){
return (back+1)%MAX == front;
}
//获取队列大小
template <typename T>
int Queue<T>::size(){
return (back+MAX-front)%MAX;
}
//入队
template <typename T>
void Queue<T>::push(){
if(full()){
cout<<"队满!入队失败!"<<endl;
return;
}
T value;
cout<<"请输入要入队的元素:";
cin>>value;
data[back] = value;
back = (back+1)%MAX;
cout<<value<<" 入队成功"<<endl;
}
//出队
template <typename T>
void Queue<T>::pop(){
if(empty()){
cout<<"队空!出队失败!"<<endl;
return;
}
T temp = data[front];
front = (front+1)%MAX;
cout<<temp<<" 出队成功"<<endl;
}
int main()
{
Queue<int> q;
q.push();
q.push();
q.push();
cout<<"q.size = "<<q.size()<<endl;
q.pop();
q.pop();
q.pop();
q.pop();
cout<<"q.size = "<<q.size()<<endl;
return 0;
}
2> 将继承和多态的课堂代码敲一遍
一、继承过程中的特殊成员函数
1.1 构造函数
1> 父子类中的构造函数不是同一个构造函数,父子类中拥有各自的构造函数
2> 需要在子类的构造函数初始化列表中,显性调用父类的有参构造完成对子类从父类中继承下来成员的初始化,否则系统会自动调用父类的无参构造来完成对其初始化,此时,如果父类中没有无参构造,则子类的构造函数会报错
3> 构造顺序:先构造父类再构造子类
1.2 析构函数
1> 父子类中会拥有各自的析构函数
2> 无需在子类的析构函数中显性调用父类的析构函数,当子类对象消亡时,系统会自动调用父类的析构函数,完成对子类从父类继承下来成员的内存回收工作,
3> 析构顺序:先析构子类再析构父类
1.3 拷贝构造
1> 父子类中拥有各自的拷贝构造函数
2> 需要在子类的拷贝构造函数初始化列表中,显性调用父类的拷贝构造函数完成对子类从父类中继承下来成员的初始化,如果没有显性调用父类的拷贝构造函数,系统会自动调用父类的无参构造来完成对其进行初始化工作,如果父类中没有无参构造,则子类的拷贝构造会报错
3> 如果父类中有指针成员指向堆区空间,则父类的拷贝构造需要使用深拷贝,如果子类有指针成员指向堆区空间,则子类需要使用深拷贝
1.4 拷贝赋值
1> 父子类中拥有给中的拷贝赋值函数
2> 需要在子类的拷贝赋值函数体内,显性调用父类的拷贝赋值函数,来完成对子类从父类中继承下来成员的赋值,如果没有显性调用父类的拷贝赋值函数,则系统啥也不干,子类从父类继承下来的成员的值保持不变。
3> 拷贝赋值也涉及深浅拷贝问题
二、多重继承
2.1 含义
所谓多重继承,就是可以由多个类共同派生出一个子类,该子类会继承所有父类的特征
2.2 继承格式
class 子类名:继承方式1 父类1,继承方式2 父类2,。。。,继承方式n 父类n
{
//子类拓展成员
}
三、虚继承
3.1 菱形继承(钻石继承)问题
A --> 公共基类
/ \
B C --> 中间子类
\ /
D --> 汇聚子类
在继承过程中,由一个公共基类,派生出多个中间子类,再由这若干个子类的一部分共同派生出一个汇聚子类,就会导致汇聚子类中保留多份由公共基类传下来的成员,使得子类内存冗余、并且访问起来也比较麻烦
3.2 虚继承
1> 为了解决以上的菱形继承问题,我们可以引入虚继承:就是在生成中间子类时,在继承方式前加关键字virtual,那么,再由这些中间子类生成汇聚子类时,汇聚子类中就只保留一份公共基类传下来的数据
2> 一般的继承,需要在子类的构造函数初始化列表中,显性调用直接父类的构造函数,完成对继承下来成员的初始化。但是,在虚继承中,由于汇聚子类中只有一份公共基类的成员,不能确定由哪个直接父类对公共基类继承下来成员进行初始化,索性不使用直接父类对其进行初始化工作,直接调用公共基类的构造函数来完成对从公共基类中继承下来成员的初始化工作,如果没有显性调用公共基类的有参构造,那么系统会自动调用公共基类的无参构造完成初始化工作,此时,如果公共基类中没有无参构造,则汇聚子类的构造函数会报错。
#include <iostream>
using namespace std;
class A
{
protected:
int value_a;
public:
A() {cout<<"A::无参构造"<<endl;}
A(int a):value_a(a) {cout<<"A::有参构造"<<endl;}
};
//虚继承格式:再生成中间子类时,继承方式前加virtual关键字
class B:virtual public A
{
protected:
int value_b;
public:
B() {cout<<"B::无参构造"<<endl;}
B(int a, int b):A(a), value_b(b){cout<<"B::有参构造"<<endl;}
};
class C:virtual public A
{
protected:
int value_c;
public:
C() {cout<<"C::无参构造"<<endl;}
C(int a, int c):A(a), value_c(c){cout<<"C::有参构造"<<endl;}
};
//由中间子类生成汇聚子类
class D:public B, public C
{
private:
int value_d;
public:
D() {}
D(int a1, int a2, int b, int c, int d):A(a2),B(a1,b), C(a2,c), value_d(d) {cout<<"D::有参构造"<<endl;}
void show()
{
cout<<"value_b = "<<value_b<<endl;
cout<<"value_c = "<<value_c<<endl;
cout<<"value_d = "<<value_d<<endl;
// cout<<"value_a = "<<B::value_a<<endl;
// cout<<"value_a = "<<C::value_a<<endl;
cout<<"value_a = "<<value_a<<endl;
}
};
四、多态
4.1 概念
所谓多态,就是一种形式的多种状态,多态是实现泛型编程的重要部分,能够实现“一条语句多用”
泛型编程:试图以不变的程序执行可变的功能
4.2 多态实现条件
1> 继承:没有继承就没有多态
2> 虚函数:实现函数重写,保证父子类中使用同一个函数
3> 父类指针或引用指向子类对象,调用子类中重写的父类的虚函数
4.3 虚函数
1> C++中可以将成员函数定义成虚函数,定义格式:在定义成员函数前加关键字virtual
2> 作用:以保证父类空间能够寻找到子类中重写的跟父类函数原型相同的函数
4.4 虚函数的底层实现
4.5 虚析构函数
1> 引入目的:正确引导delete关键字,在释放父类指针空间时,将子类空间一并释放
2> 定义格式:在析构函数前面加关键字virtual
3> 如果类中某个函数设置成虚函数,那么该类的子子孙孙类中的该函数都是虚函数
4> 在特殊的成员函数中,只有析构函数能设置成虚函数
5> 在定义类的时候,要养成将析构函数定义成虚析构函数,以便于内存的管理工作
#include <iostream>
using namespace std;
class Father
{
protected:
string name;
int age;
public:
Father() {}
Father(string n, int a):name(n), age(a) {cout<<"Father::有参构造"<<endl;}
virtual ~Father() {cout<<"Father::析构函数"<<endl;}
};
class Son:public Father
{
private:
string toy;
public:
Son(){}
Son(string n, int a, string t):Father(n,a), toy(t) {cout<<"Son::有参构造"<<endl;}
~Son(){cout<<"Son::析构函数"<<endl;}
};
int main()
{
//定义父类指针,指向堆区的子类对象
Father *p = new Son("张三", 18, "car");
//释放堆区空间
delete p;
return 0;
}
4.6 纯虚函数
1> 引入背景:对于一个类而言,某些函数没有实现的必要,或者实现这些函数没有意义。定义这些函数,纯粹为了让子类对其进行重写,以便于后期可以使用父类的指针或引用指向子类对象,去调用子类中重写的该函数,此时就可以将该函数定义成纯虚函数
2> 纯虚函数的定义格式:将虚函数的函数体去掉,用=0进行替换:virtual 函数类型 函数名(形参列表) = 0;
3> 包含纯虚函数的类称为抽象类,抽象类不能实例化对象
4> 纯虚函数必须由子类进行重写,如果子类中没有重写纯虚函数,那么在子类中该函数还依然是纯虚函数,子类也是抽象类
#include <iostream>
using namespace std;
class Shape
{
protected:
double perimeter;
double area;
public:
Shape():perimeter(0), area(0) {}
//定义两个虚函数
virtual double get_p() = 0; //该函数就是纯虚函数
virtual double get_a() = 0;
};
//定义圆形类
class Circle:public Shape
{
private:
double radius;
public:
Circle(double r = 0):radius(r){}
//重写父类中提供的虚函数
double get_a() override
{
area = 3.14*radius*radius;
return area;
}
double get_p() override
{
perimeter = 2*radius*3.14;
return perimeter;
}
};
//定义矩形类
class Rectangle:public Shape
{
private:
double width;
double height;
public:
Rectangle(double w=0, double h=0):width(w), height(h){}
//重写父类中提供的虚函数
double get_a() override
{
area = width*height;
return area;
}
double get_p() override
{
perimeter = 2*(width+height);
return perimeter;
}
};
//定义全局函数输出给定图形的周长和面积
void show(Shape &s)
{
cout<<"周长 = "<<s.get_p()<<" 面积 = "<<s.get_a()<<endl;
//cout<<"周长 = "<<s.Shape::get_p()<<" 面积 = "<<s.Shape::get_a()<<endl;
}
int main()
{
//实例化一个圆形
Circle c1(2);
show(c1);
Rectangle r1(3,4);
show(r1);
cout<<"*******************************"<<endl;
//Shape s; //包含纯虚函数的类称为抽象类,抽象类不能实例化对象
return 0;
}
五、泛型编程之模板(template)
模板是能够将数据类型进一步抽象,我们之前所接触的抽象,都是完成对数据的相关抽象
模板分为模板函数、模板类
5.1 模板函数
1> 程序员有时定义函数时,会因为函数参数类型不同,导致同一功能的函数需要定义多个,即使有函数重载,系统会自动匹配相应的函数,也会造成代码的冗余
2> C++提供函数模板机制:使用函数时,形参的类型也由实参进行传递,定义模板函数时,模板函数的参数,不仅能接收实参的值,也能接收实参的类型
3> 定义格式
template <typename 类型形参1,typename 类型形参2,。。。,typename 类型形参n>
函数返回值类型 函数名(形参列表){函数体内容}
注意
template是定义模板的关键字
<>:中是类型的参数,用于接收实参传递的类型
typename:用于定义类型形参变量的,也可以使用class
函数体中,可以使用模板中的类型作为函数返回值、函数形参、局部变量的类型
4> 调用方式
1、隐式调用:调用时跟普通函数调用一样,系统会自动根据传进来的实参类型,推导出模板的类型参数
2、显式调用:在函数名后,实参括号前,加<>,传递类型形参的值,调用原则:尖找尖、圆找圆
3、一般模板函数要求显式调用
5> 注意事项
一个模板下面只能定义一个函数,如果要定义多个模板函数,需要定义多个模板
#include <iostream>
using namespace std;
//定义一个模板函数
template<typename T>
T sum(T m, T n)
{
return m+n;
}
//定义新的模板函数,不能多个模板函数共同使用一个模板
template<typename T>
T div(T m, T n)
{
return m-n;
}
int main()
{
cout << sum<int>(3.3,5) << endl; //8 显式调用模板函数
cout << sum(3.2,5.3) << endl; //8.5
cout << sum(string("hello"),string("world")) << endl;
return 0;
}
6> 模板函数的特化
当基础模板和特化模板同时出现时,如果是隐式调用,默认调用基础模板,如果是显式调用,则会调用特化模板
5.2 模板类
1> 引入背景:程序员有时定义类时,可能由于数据类型的不同,导致同样功能的类,需要定义多个,造成代码冗余问题
2> C++引入模板类,类中的一些类型可以是由调用时给定,由实参进行传递
3> 定义格式
template <typename 类型形参1,typename 类型形参2,。。。,typename 类型形参n>
class 类名
{
//成员可以使用模板中的类型
};
4> 调用方式:只能显式调用,不能隐式调用
5> 在定义模板时,模板类外,但凡使用到类名,都需要重新再定义模板,并且使用类名时,需要给定类型
#include <iostream>
using namespace std;
template<typename T>
class Node
{
private:
T data; //数据域
Node *next; //指针域
public:
Node():next(NULL) {} //无参构造
Node(T d):data(d), next(NULL){} //有参构造
void show();
};
//在模板类外,但凡使用到模板类,都必须显性调用给定类型,需要重新定义模板
template <typename T>
void Node<T>::show()
{
cout<<"data = "<<data<<endl;
}
int main()
{
Node<int> n1(520); //模板类的使用必须显性调用
n1.show();
Node<double> n2(3.14);
n2.show();
return 0;
}