一、继承过程中的特殊成员函数
1.1 构造函数
啥是构造函数?为啥要构造函数?所谓构造函数,其实就为后面实例化对象准备的。比如,我们定义了人“类”。那么人“类”是怎样的吗?我们就要“构造函数”来描述,否则很有可能是把猩猩看成人。
1> 父子类中的构造函数不是同一个构造函数,父子类中拥有各自的构造函数。比如”人类“与“男人类”是父子类,他们都有不同的构造函数。否则,没法区分“人类”中某个具体对象(个体)属不属于“男人类”的成员。
2> 需要在子类的构造函数初始化列表中,显性调用父类的有参构造完成对子类从父类中继承下来成员的初始化,否则系统会自动调用父类的无参构造来完成对其初始化,此时,如果父类中没有无参构造,则子类的构造函数会报错
3> 构造顺序:先构造父类再构造子类。
1.2 析构函数
为了实例化对象,我们构造函数;为了释放对象,我们必须析构函数。
1> 父子类中会拥有各自的析构函数
2> 无需在子类的析构函数中显性调用父类的析构函数,当子类对象消亡时,系统会自动调用父类的析构函数,完成对子类从父类继承下来成员的内存回收工作,
3> 析构顺序:先析构子类再析构父类
1.3 拷贝构造
所谓拷贝构造函数,字面意思就是拷贝(重载)已经构造好的函数。其形参必须是引用。
1> 父子类中拥有各自的拷贝构造函数
2>需要在子类的拷贝构造函数初始化列表中,显性调用父类的拷贝构造函数完成对子类从父类中继承下来成员的初始化,如果没有显性调用父类的拷贝构造函数,系统会自动调用父类的无参构造来完成对其进行初始化工作,如果父类中没有无参构造,则子类的拷贝构造会报错
3> 如果父类中有指针成员指向堆区空间,则父类的拷贝构造需要使用深拷贝,如果子类有指针成员指向堆区空间,则子类需要使用深拷贝
1.4 拷贝赋值
其实就是拷贝(重载)赋值函数。&operator=
1> 父子类中拥有给中的拷贝赋值函数
2>需要在子类的拷贝赋值函数体内,显性调用父类的拷贝赋值函数,来完成对子类从父类中继承下来成员的赋值,如果没有显性调用父类的拷贝赋值函数,则系统啥也不干,子类从父类继承下来的成员的值保持不变。
3> 拷贝赋值也涉及深浅拷贝问题
#include <iostream>
using namespace std;
class Father
{
protected:
string name;
int age;
public:
Father() {cout<<“Father::无参构造”<<endl;}
Father(string n, int a):name(n), age(a) {cout<<“Father::有参构造”<<endl;}
Father(const Father &other):name(other.name), age(other.age)
{cout<<“Father::拷贝构造”<<endl;}
Father &operator=(const Father &other)
{
if(this != &other)
{
this->name = other.name;
this->age = other.age;
}
cout<<“Fathsr::拷贝赋值”<<endl;
return *this;
}
~Father() {cout<<“Father::析构函数”<<endl;}
};
//定义子类继承自父类
class Son : public Father
{
private:
string toy;
public:
Son() {cout<<“Son::无参构造”<<endl;}
Son(string n, int a, string t):Father(n, a), toy(t) {cout<<“Son::有参构造”<<endl;}
~Son() {cout<<“Son::析构函数”<<endl;}
Son(const Son &other):Father(other), toy(other.toy) {cout<<“Son::拷贝构造”<<endl;}
Son &operator=(const Son &other)
{
if(this != &other)
{
//this->name = other.name;
Father::operator=(other); //需要在子类的拷贝赋值函数中显性调用父类的拷贝赋值函数
this->toy = other.toy;
}
cout<<“Son::拷贝赋值”<<endl;
return *this;
}
void show()
{
cout<<“name = “<<name<<” age = “<<age<<” toy = “<<toy<<endl;
}
};
int main()
{
Son s1; //无参构造
cout<<“******************************”<<endl;
Son s2(“张三”, 18, “car”); //有参
cout<<“******************************”<<endl;
Son s3(s2); //拷贝构造
s3.show();
cout<<“******************************”<<endl;
s1 = s2;
s1.show();
cout<<“******************************”<<endl;
return 0;
}
二、多重继承
2.1 含义
所谓多重继承,就是可以由多个类共同派生出一个子类,该子类会继承所有父类的特征
2.2 继承格式
class 子类名:继承方式1 父类1,继承方式2 父类2,。。。,继承方式n 父类n
{
//子类拓展成员
}
#include <iostream>
using namespace std;
class Father_1
{
protected:
string name;
int age;
double value;
public:
Father_1() {}
Father_1(string n, int a, double v):name(n), age(a), value(v) {cout<<“Father_1::有参构造”<<endl;}
};
class Father_2
{
protected:
double score;
double value;
public:
Father_2() {}
Father_2(double s):score(s) {cout<<“Father_2::有参构造”<<endl;}
};
//定义子类继承自多个父类
class Son:public Father_1, public Father_2
{
public:
Son() {}
//子类中调用直接父类的构造函数顺序是由继承顺序决定,而不是由调用构造函数的顺序决定
Son(string n, int a, double v, double s):Father_1(n,a,v), Father_2(s) {cout<<“Son::有参构造”<<endl;}
void show()
{
cout<<“name = “<<name<<endl;
cout<<“age = “<<age<<endl;
cout<<“score = “<<score<<endl;
cout<<“value = “<<this->Father_1::value<<endl; //使用的是第一个父类提供的value
cout<<“value = “<<this->Father_2::value<<endl; //使用的是第二个父类提供的value
}
};
int main()
{
Son s(“zhangs”, 18, 520, 90);
s.show();
return 0;
}
三、虚继承
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> 作用:以保证父类空间能够寻找到子类中重写的跟父类函数原型相同的函数
#include <iostream>
using namespace std;
class Hero
{
protected:
string name;
int Hp; //基础血量
public:
Hero() {}
Hero(string n, int h):name(n), Hp(h) {}
//在父类中提供虚函数
virtual void attack_jungle()
{
cout<<name<<“攻击了野怪:AAAAAAAAAAAAA”<<endl;
}
};
//定义法师类,继承自英雄类
class Magic:public Hero
{
private:
int ap; //基础法强
public:
Magic() {}
Magic(string n, int h, int a):Hero(n,h), ap(a) {}
//子类重写父类的虚函数
void attack_jungle()
{
cout<<name<<“攻击了野怪:BBBBBBBBBBBBB”<<endl;
}
};
//定义刺客类,继承自英雄类
class Tank:public Hero
{
private:
int dikang; //基础抗性
public:
Tank() {}
Tank(string n, int h, int d):Hero(n,h),dikang(d) {}
void attack_jungle()
{
cout<<name<<“攻击了野怪:CCCCCCCCCCCCCC”<<endl;
}
//子类拓展的其他函数
void show()
{}
};
//定义全局函数,用于打野
//void fun(Magic & m)
//{
// m.attack_jungle();
//}
//void fun(Tank & m)
//{
// m.attack_jungle();
//}
void fun(Hero *h)
{
h->attack_jungle();
}
int main()
{
Magic h1(“妲己”, 300, 50);
Tank h2(“亚瑟”, 500, 200);
fun(&h1);
fun(&h2);
// h1.attack_jungle(); //调用自己定义的
// h1.Hero::attack_jungle(); //调用父类继承的
// //定义父类指针指向子类对象
// Hero *p1 = &h1;
// p1->attack_jungle(); //调用父类继承的
//p1->show();
//定义父类的引用目标为子类对象
Hero &ref = h1;
ref.attack_jungle();
return 0;
}
练习:在昨天作业的基础上,将图形类中的获取周长和获取面积函数设置成虚函数,每个子类重写该虚函数,定义一个全局函数,输出给定图形的周长和面积
#include <iostream>
using namespace std;
class Shape
{
protected:
double perimeter;
double area;
public:
Shape():perimeter(0), area(0) {}
//定义两个虚函数
virtual double get_p()
{
cout<<“AAAAAAAAAAAAAAAAAAAAAAAAAAAA”<<endl;
return perimeter;
}
virtual double get_a()
{
return area;
}
};
//定义圆形类
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
{
cout<<“BBBBBBBBBBBBBBBBBBBBB”<<endl;
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
{
cout<<“CCCCCCCCCCCCCCCCCCCCC”<<endl;
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);
return 0;
}
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> 模板函数的特化
当基础模板和特化模板同时出现时,如果是隐式调用,默认调用基础模板,如果是显式调用,则会调用特化模板
#include <iostream>
using namespace std;
//定义一个模板函数,基础模板
template<typename T>
T sum(T m, T n)
{
cout<<“AAAAAAAAAAAAAAAAAAAAAA”<<endl;
return m+n;
}
//特化模板
template<typename T>
T sum(int m, int n)
{
cout<<“BBBBBBBBBBBBBBBBBBBBBBBB”<<endl;
return m+n;
}
int main()
{
sum(3,5); //如果基础模板和特化模板同时出现时,隐式调用该函数时,会调用基础模板
sum<int>(3,5); //显式调用会调用特化模板
return 0;
}
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;
}