多态
- 通用概念:同一论域中一个元素可有多种解释。
- 提高面向对象设计的语言灵活性
- 程序设计语言:OO程序设计
- 多态形式
- 函数重载:(静态多态),和虚函数的动态多态不同(一名多用):函数重载包含操作符重载
- 类属多态:模板:template
1. 操作符重载
- 函数重载
- 名同、参数不同,返回值不同没有用的:参数顺序、参数类型匹配(找到最佳匹配)
- 静态绑定
- 歧义控制:
- 顺序:
- 最佳匹配:
- 原则一:这个匹配每一个参数不必其他的匹配更差
- 原则二:这个匹配有一个参数更精确匹配
- 整形提升:更好的,标准转换(标准转换都是一视同仁的)
- 窄转换?允许的,大->小
- 操作符重载(变为一种函数)
- 动机:操作符语义
- built_in 类型
- 自定义数据类型
- 作用:
- 提高可读性
- 提供可扩充性
- 动机:操作符语义
- 重点记忆返回的变量
1.1. 操作符 + 的重载
- 重载第一步
class Complex {
double real, imag;
public:
Complex() {real = 0; imag = 0;}
Complex(double r, double i) { real = r; imag = i; }
Complex add(Complex& x);
};
Complex a(1,2),b(3,4),c;
c=a.add(b);//想要写成 a + b
//使用操作符重载
class Complex {
double real, imag;
public:
Complex() { real = 0; imag = 0; }
Complex(double r, double i) { real = r; imag = i; }
Complex operator + (Complex& x) {
Complex temp;
temp.real = real + x.real;
temp.imag = imag + x.imag;
return temp;
}
};
Complex a(1,2),b(3,4),c;
c = a.operator + (b);
//进一步完成操作符重载
class Complex {
double real, imag ;
public :
Complex() { real = 0 ; imag = 0 ; }
Complex(double r, double i) {
real = r;
imag = i;
}
friend Complex operator+(Complex& c1 , Complex& c2);//这个是已经预定义好的,我们这样子写就是重载
};
//全局函数
Complex operator+ (Complex& c1 , Complex& c2 ) {//全局函数重载至少包含一个用户自定义类型
Complex temp;
temp.real = c1.real + c2.real;
temp.imag = c1.imag + c2.imag;
return temp;
}//一般返回临时变量
Complex a(1,2),b(3,4),c;
c = a + b;//自动进行翻译
- 自增和自减的问题
class Counter {
int value;
public:
Counter() { value = 0; }
Counter& operator ++()//++a 左值
{
value ++;
return *this;
}
Counter operator ++(int)//a++ 右值
{
Counter temp = *this;
value++;
return temp;
}
}
- 重载++函数:封装SAT的问题
- 返回值引用或者是值是有区别的
enum Day { SUN, MON, TUE, WED, THU, FRI, SAT};
Day& operator++(Day& d)
{ return d= (d==SAT)? SUN: Day(d+1); }
//重载重定向符号,用的很多,不能进成员函数重载
ostream& operator << (ostream& o, Day& d)
{ switch (d)
{ case SUN: o << "SUN" << endl;break;//直接使用ostream中的<<
case MON: o << "MON" << endl;break;
case TUE: o << "TUE" << endl;break;
case WED: o << "WED" << endl;break;
case THU: o << "THU" << endl;break;
case FRI: o << "FRI" << endl;break;
case SAT: o << "SAT" << endl;break;
}
return o;//为什么要return ostream类型的变量:需要连续的使用可以链式调用,Cout << 1 << 2;
}
void main(){
Day d = SAT;
++d;
cout << d;
}
1.2. 可以重载的操作符
- 不可以重载的操作符:
.
(成员访问操作符)、.*
(成员指针访问运算符,如下)、::
(域操作符)、?:
(条件操作符)、sizeof
:也不重载- 原因:前两个为了防止类访问出现混乱
- ::后面是名称不是变量
- ?:条件运算符涉及到跳转,如果重载就影响了理解
class A
{ int x;
public:
A(int i):x(i){}
void f() {}
void g() {}
};
void (A::*p_f)();//A类成员的函数指针
p_f= &A::f;
(a.*p_f)();
int a = 0;b = 0;
b?(a = 1):(b = 1);//a == b == 1
operator ?: (p,a = 1,b = 1)//均执行了
- 重载基本原则:
- 方法:(大多数都支持,但是有的不支持)
- 类成员函数
- 带有类参数的全局函数
- 遵循原有语法
- 单目/双目:一一对应
- 优先级
- 结合性
- 方法:(大多数都支持,但是有的不支持)
- 永远不要重载&&和||:会造成极大的问题
1.3. 双目操作符的重载
1.3.1. 类成员函数(双目操作符)
- 类成员函数:
- 格式:
<ret type>operator #(<arg>)
- this: 隐含,必然是第一个参数
- 格式:
- 使用:
<class name> a,b;
a # b;//a -> this
a.operator#(b)
1.3.2. 全局函数
- 友元:
friend <ret type> operator #(<arg1>,<arg2>)
- 格式:
<ret type> operator #(<arg1>,<arg2>)
- 注意:
=
、()
、[]
、->
不可以作为全局函数重载- 大体上来讲,C++ 一个类本身对这几个运算符就已经有了相应的解释了。
- 如果将这四种符号进行友元全局重载,则会出现一些冲突
- 下标和箭头运算符为什么?有保留调用顺序,我们希望能保留原来的顺序,而全局不能要求,而成员函数的this就可以解决这个问题
- 参考
- 全局函数作为补充:
- 单目运算符最好重载为类的成员函数
- 双目运算符最好重载为类的友元函数
class CL {
int count;
CL(int i){...}//10可以直接隐式类型转换
public:
friend CL operator +(int i, CL& a);
friend CL operator +(CL& a, int i);
};//支持隐式类型转换就行
//如果最左边不是类对象,则必须作为友元函数
- 永远不要重载 && 和 ||:逻辑与和逻辑或
- 原因:短路,类似?:
- 虽然绝大多数都没有问题,但是如果有逻辑短路容易出现问题
char *p;
if ((p != 0) && (strlen(p) >10)) //利用了短路,一旦计算就没有短路行为了
if (expressin1 && expression2)
if (expression1.operator && (expression2))
if (operator && (expression1, expression2))
- 返回类型的问题:如果没有&的时候,第一个return出现了对象拷贝,避免:临时变量不能返回拷贝
class Rational {
public:
Rational(int,int);
const Rational& operator *(const Rational& r) const;//const写不写都行,写了更好
private:
int n, d;
};
// operator * 的函数体
return Rational(n * r.n, d * r.d);
Rational *result = new Rational(n*r.n, d*r.d);
return *result;//返回引用的问题?
// w = x * y * z出现问题:出现内存泄露的问题
static Rational result;//声明为static
result.n = n * r.n;
result.d = d * r.d;
return result;//static是全局的,可以吗?不可以,同时出现两个的结果会出现问题
//if((a * b) == (c * d)) ->永真式
- 操作符重载的哲理:尽量让事情有效率,但不是过度有效率(返回引用)
- 结论:每次就是返回一个拷贝,而不是引用
1.4. 返回值总结
- 加减乘除:就是拷贝,不是引用,效率不太高?为了解决这个问题:可以返回值优化,第一个return没有拷贝,直接返回的是一个对象(无拷贝),先计算,最后生成一个对象返回。
1.5. 单目操作符的重载
- 类成员函数:
- this:隐含
- 格式:
<ret type> operator#()
:this的隐含
- 全局函数:
<ret type> operator#(<arg>)
- 参数必须为自定义类型
- 单目操作符在绝大多数情况下重载为类的成员函数
- 15min没了
- a++ 和 ++ a
class Counter{
int value;
public:
Counter() { value = 0; }
Counter& operator ++() // ++a
{ value++;
return *this;
}
Counter operator ++(int) //a++
{ Counter temp=*this;//这里的int值是什么意义?区分两个函数,dummy argument,哑元变量
value++;
return temp;
}
}
1.6. 操作符 = 的重载
- 默认赋值操作符重载函数
- 逐个成员赋值
- 对含有对象成员的类,该定义是递归的
- 赋值操作符的重载不可以被继承:因为拷贝构造,派生出来的类有一些新的部分
- 返回引用类型:返回*this的引用,支持链式赋值
- this引用应该是非常量引用,返回出来的是作为右值进行计算
- a = b = c:不要求非常量引用
- (a = b).f():要求非常量引用
- 例一:
class A;
A a = b;//需要调用拷贝构造函数(更重要的是构造,在构造对象时候调用)
A a(b);
A a,b;
a = b;//需要调用
class A {
int x,y ;
char *p ;
public :
A(int i,int j,char *s):x(i),y(j){
p = new char[strlen(s)+ 1 ];
strcpy(p,s);//进行拷贝,最后留一个\0
}
virtual ~A(){
delete[] p;
}
A& operator = (A& a) {
//赋值
x = a.x;
y = a.y;
delete []p;
p = new char[strlen(a.p)+1];
strcpy(p,a.p);
return *this;//也会出现悬垂
}//还有问题,就是赋值自身会出现问题
};
A a, b;
a = b;//调用自己的复制
//idle pointer,B被析构的时候会将p释放掉,导致p指向已经被释放掉的指针
//Memory leak,A申请的区域可能没有办法被释放
//更安全的拷贝,先new再delete
char *pOrig = p;
p = new char ...
strcpy();
delete pOrig;
return *this;
//自我赋值可以吗?可以,换了一块内存空间,没有内存泄露
- 注意:避免自我赋值(因为是相同的内存地址)
- Sample: class string
- s = s
class {... A void f(A& a);...}
void f (A& a1, A& a2)
int f2(Derived &rd,Base& rb);
- Object identity
- Content
- Same memory location
- Object identifier
if(this == &a)
return *this;
delete p;//48min - 50min
class A{
public:
ObjectID identity() const;
};
A *p1,*p2;
p1-> identity() == p2-> identity()
1.7. 操作符 [] 的重载
class string {
char *p;
public :
string(char *p1){
p = new char [strlen(p1)+ 1];
strcpy(p,p1);//#pragma warning(disable:4996)来屏蔽问题
}
char& operator[](int i){
return p[i];
}
const char operator[](int i) const{
return p[i];
}
//可以用两个重载函数吗?是可以的
virtual ~string() { delete[] p ; }
};
string s("aacd");
s[2] = 'b' ;
//第一个重载加上const可以使得const或者非const对象都可以调用
const string cs('const');
cout << cs[0];
const cs[0] = 'D';//const 版本不想被赋值(返回const的),非const版本想要被赋值,之后再进行重载的时候就需要同时重载两个
1.8. 多维数组 class Array2D
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7Lad9lp-1645750964352)(https://spricoder.oss-cn-shanghai.aliyuncs.com/2020-C-plus-plus-advanced-programming/C+±OOP/img/18.png)]
class Array2D{
int n1, n2;
int *p;
public:
Array2D(int l, int c):n1(l),n2(c){
p = new int[n1*n2];
}
virtual ~Array2D() { delete[] p; }
};
int & Array2D::getElem(int i, int j) { ... }
//上面是实现高维数组
Array2D data(2,3);
data.getElem(1,2) = 0;
//target -> data[1][2]
//想法:化解为两次调用
data.operator[](1)[2];//int *operator[](int i) 一次偏移一行,转化成Array1D
data.operator[](1).operator[](2)
//问题:三维数组重载问题:重载一次降维一次,3D->2D等等,多个依次进行重载,重载之后返回对象
//代理对象:Array1D
class Array1D{
int *q;//一维降低到int *就行
Array1D(int *p){ q = p; }
int& operator[](j){
return q[j];
}
}
1.8.1. 多维数组的最终版本
class Array2D{
private:
int *p;
int num1, num2;
public:
class Array1D{//Surrogate 多维,proxy class
public:
Array1D(int *p) { this->p = p; }
int& operator[ ] (int index) { return p[index]; }
const int operator[ ] (int index) const { return p[index]; }
private:
int *p;
};
Array2D(int n1, int n2) {
p = new int[n1 * n2];
num1 = n1;
num2 = n2;
}
virtual ~Array2D() {
delete [] p;
}
Array1D operator[](int index) {
return p + index * num2;//return的值和int*相同,构造函数不能声明成显式构造函数。
}
//这里为什么是array1D?通过构造函数进行类型转换
const Array1D operator[] (int index) const {
return p+index*num2;
}
};
1.9. 操作符 () 的重载
- ()的意义
- 函数调用
- 类型转换操作符
1.9.1. 函数调用
class Func {
double para;
int lowerBound , upperBound ;
public:
double operator()(double,int,int);
};
Func f;
f(2.4, 0, 8);
class Array2D{
int n1, n2;
int *p;
public:
Array2D(int l, int c):n1(l),n2(c){
p = new int[n1*n2];
}
virtual ~Array2D() {
delete[] p;
}
int& operator()(int i, int j){
return (p+i*n2)[j];//优化getElement
}
};
1.9.2. 操作符类型转换的重载
- 基本数据类型
- 自定义类
class Rational {
public: Rational(int n1, int n2) {
n = n1;
d = n2;
}
operator double() {//类型转换操作符,语法特殊
return (double)n/d;
}
private:
int n, d;
};
//减少混合计算中需要定义的操作符重载函数的数量
Rational r(1,2);
double x = r;
x = x + r;//避免的double 和 rational 的全局函数重载,会自动全部转换为double
//48min
ostream f("abc.txt");
if (f)
//重载 数值型:如 int
- 问题:为什么禁止在类外禁止重载赋值操作符?
- 如果没有类内提供一个赋值操作符,则编译器会默认提供一个类内的复制操作符
- 查找操作符优先查找类内,之后查找全局,所以全局重载赋值操作符不可能被用到
1.10. 操作符 -> 的重载
- ->为二元运算符,重载的时候按照一元操作符重载描述。
A a;
a->f();
a.operator->(f)
a.operator->()->f() //重载时按一元操作符重载描述,这时,a.operator->()返回一个指针(或者是已经重定义过->的对象)
- 例子:画图板程序
class CPen {
int m_color;
int m_width;
public:
void setColor(int c){ m_color = c;}
int getWidth(){ return m_width; }
};
class CPanel {
CPen m_pen;
int m_bkColor;
public:
CPen* getPen(){return &m_pen;}
void setBkColor(int c){ m_bkColor =c;}
};
CPanel c;
c.getPen()->setColor(16);
//简单修改,可以返回一个对象内部对象的指针
class CPen {
int m_color;
int m_width;
public:
void setColor(int c){ m_color = c;}
int getWidth(){return m_width; }
};
class CPanel {
CPen m_pen;
int m_bkColor;
public:
CPen* getPen(){return &m_pen;}
void setBkColor(int c) { m_bkColor =c;}
};
CPanel c;
c->setColor(16);
//等价于
//c.operator->()->setColor(16);
//c.m_pen.setColor(16)
c->getWidth();
//等价于
//c.operator->()->getWidth();
//c.m_pen.getWidth()
CPanel *p=&c;
p->setBkColor(10);
- Prevent memory Leak:需要符合compiler控制的生命周期
class A{
public:
void f();
int g(double);
void h(char);
};
void test(){
A *p = new A;
p->f();//如果出错可能会导致问题
p->g(1.1);//返回值
p->h('A');
delete p;
}
//更好的管理A对象,不用在任何退出的地方写delete p
void test()
{
AWrapper p(new A);
p->f();//如果出错可能会导致问题
p->g(1.1);//返回值
p->h('A');
delete p;
}
//须符合compiler控制的生命周期
class AWrapper{//不包含逻辑
A* p;// ? T p; 支持多个类型
public:
AWrapper(A *p) { this->p = p;}
~AWrapper() { delete p;}
A*operator->() { return p;}
};//RAII 资源获取及初始化
//函数返回,销毁局部指针的时候会直接删除
1.11. 操作符 new 和 delete 的重载
- 频繁调用系统的存储管理,影响效率。
- 程序自身管理内存,提高效率
- 方法:
- 调用系统存储分配,申请一块较大的内存
- 针对该内存,自己管理存储分配、去配
- 通过重载new与delete来实现
- 重载的new与delete是静态成员(隐式的,不需要额外声明,不允许操作任何类的数据成员)
- 重载的new与delete遵循类的访问控制,可继承(注意派生类和继承类的大小问题,开始5min左右)
- 我们想要对某些程序进行自己的资源管理的话,可以重载这两个操作符。
- 有些我们重复新建销毁的,比如Restful的可以单独管理
- new构造和返回指针
- delete析构和释放内存
- 可以重载成全局函数,也可以重载成类成员函数
1.11.1. 重载 new
void *operator new (size_t size, s...)
- 名:operator new
- 返回类型:void *
- 第一个参数:size_t(unsigned int)
- 系统自动计算对象的大小,并传值给size
- 其他参数:可有可无
A *p = new (...) A
,表示传给new的
- new的重载可以有多种
- 如果重载一个new,那么通过new动态创建该类的对象时将不再调用内置的(预定义的)new
- 允许进行全局重载,但是不推荐使用全局重载
if(size != sizeof(base))
return ::operator new (size);//调用全局标准库的new进行size的分配,标准库的new永远是可以使用的
operator new;
new A[10];
operator new [];
void * operator new (size_t size, void*)//是不可以被重载的,标准库版本
void * operator new (size_t size, ostream & log);//可以同时写入到日志
void * operator new (size_t size, void * pointer);//定位new,placement new,被调用的时候,在指针给定的地方的进行new(可能预先已经分配好的),分配比较快,长时间运行不被打断(不会导致内存不足)
- new也可以new在栈上
class A{};
char buf[sizeof(A)];
A* a = new(buf) A;//定位new,不用分配内存,直接使用buf指向的区域
1.11.2. 重载 delete
void operator delete(void *,size_t size)
- 名:operator delete
- 返回类型:void
- 第一个参数:void *(必须):被撤销对象的地址
- 第二个参数:可有可无;如果有,则必须为size_t类型:被撤销对象的大小
- delete 的重载只能有一个
- 如果重载了delete,那么通过 delete 撤消对象时将不再调用内置的(预定义的)delete
- 动态删除其父类的所有的。
- 如果子类中有一个虚继承函数,则size_t大小会根据继承情况进行确定大小
1.11.3. new和delete考试
- 主要考核集中在这些上面
2. 模板 template
2.1. 模板
- 模板是一种源代码复用机制
- 参数化模块:
- 对程序模块(如:类、函数)加上类型参数
- 对不同类型的数据实施相同的操作
- 实例化:生成具体的函数/类
- 模板定义了若干个类,需要显式实例化
- 编译系统自动实例化函数模板:是否实例化模板的某个实例由使用点来决定;如果未使用到一个模板的某个实例,则编译系统不会生成相应实例是的代码。
2.2. 类属函数 templat function
- 同一函数对不同类型的数据完成相同的操作
- 宏实现:
#define max(a,b) ((a)>(b)?(a):(b))
- 缺陷:
- 只能实现简单的功能
- 没有类型检查
2.3. 函数重载
int max(int,int);
double max(double,double);
A max(A,A) ;
- 缺陷:
- 需要定义的重载函数太多
- 定义不全
- 不可以只有返回值不同
2.4. 函数指针
void sort(void * , unsigned int, unsigned int, int (* cmp) (void *, void *) )
- 缺陷:
- 需要定义额外参数
- 大量指针运算
- 实现起来复杂
- 可读性差
- template更加结构清晰,实现简单
2.5. 函数模板
//int和double都可以使用,编译器编译的并不是之下的代码,而是T转化成具体代码,然后分别编译
template <typename T>
void sort(int A[], unsigned int num) {
for(int i=1; i<num; i++)
for (int j=0; j< num - i; j++) {
if (A[j] > A[j+1]) {
T t = A[j];
A[j] = A[j+1];
A[j+1] = t;
}
}
}
class C {...}
C a[300];
sort(a, 300);//没有重载>
- 必须重载操作符 >
- 函数模板定义了一类重载的函数
- 函数模板的实例化:
- 隐式实现
- 根据具体模板函数调用
- 函数模板的参数
- 可有多个类型参数,用逗号分隔
- 可带普通参数
- 必须列在类型参数之后
- 调用时需显式实例化,使用默认参数值可以不显式实例化
template <class T1, class T2>
void f(T1 a, T2 b) {}
template <class T, int size>
void f(T a) {T temp[size];}
f<int,10>(1);
- 函数模板与函数重载配合使用(编译器优先使用没有使用模板的函数)
template <class T> T max(T a, T b) {
return a>b?a:b;
}
int x, y, z;
double l, m, n;
z = max(x,y);
l = max(m,n);
//为了解决max(x,m)我们使用函数重载更新
double max(int a, double b) {
return a>b? a : b;
}
2.6. 类模板
- 类定义带有类型参数,类属类需要显式实例化
- 类模板中的静态成员属于实例化后的类
- 类模板的实例化:创建对象时显式指定
class Stack {
int buffer[100];
public:
void push(int x);
int pop();
};
void Stack::push(int x) {...}
int Stack::pop(){...}
Stack st1;
template <class T>
class Stack {
T buffer[100];
public:
void push( T x);
T pop();
};
template <class T>
void Stack <T> ::push(T x) {...}
template <class T>
T Stack <T> ::pop() {...}
//如下是显式实例化
Stack <int> st1;
Stack <double> st2;
2.7. 模板例子
template <class T, int size>
class Stack {
T buffer[size];
public:
void push(T x);
T pop();
};
template <class T, int size>
void Stack <T, size>::push(T x) {...}
template <class T, int size>
T Stack <T, size>::pop() {...}
Stack <int, 100 > st1 ;//上面改为template<class T = int,int size = 100>,这里可以改成stack<> st1用来显示实例化
Stack <double, 200 > st2 ;
- 类型参数也可以给出初始值,模板类如果不按照从右往左指定默认值参数,会导致编译错误
- 函数模板的默认值不一定是从右向左的,C++11之后函数模板才接受默认值参数。
- 总而言之从右向左给出默认值总是没有问题的。
2.8. C++中模板的完整定义通常出现在头文件中
- 如果在模块A中要使用模块B中定义的某模板的实例,而在B中未使用这个实例,则模板无法使用这个实例
- 为什么C++中模板的完整定义常常出现在头文件中?
//file1.h
template <class T> class S {
T a;
public:
void f();
};
//file1.cpp
#include "file1.h"
template <class T>
void S<T>::f(){...}
template <class T>
T max(T x, T y){return x>y?x:y;}
void main() {
int a,b;
max(a,b);//实例化函数模板
S<int> x;
x.f();
}
//file2.cpp
#include "file1.h"
extern double max(double,double);
void sub(){
max(1.1,2.2);//error
S<float> x;
x.f();//error
}
//不能通过编译,为什么?file2.cpp找不到max定义,也找不到完整的S代码
- 解决方案:将file1.cpp中的代码放置到头文件中
- 连接器可以去掉多重定义
2.9. Template MetaProgramming 元编程
- 元程序就是编写一些直接可以生成或者操作其他程序的程序,要在更高层次上。
- 编写元程序就是元编程(两级编程),在编译的时候就已经完成编程
template<int N>
class Fib
{
public:
enum { value = Fib<N - 1>::value + Fib<N - 2>::value };
};
//模板显式实例化
template<>
class Fib<0>{
public:
enum { value = 1 };
};
template<>
class Fib<1>
{
public:
enum { value = 1 };
};
void main() {
cout << Fib<8>::value << endl;// calculated at compile time
}
- 元编程的特点
- 输入就是模板中的参数(int N)
- 返回值往往是enum、static、final等等
- 往往是只支持整数,但是浮点数也是可以的
- 选择和循环语句如何操作?
- 选择可以通过特殊实例化实现:
class isTen<N==10>
:模板的特例化 - 递归的调用模板就提供了循环的能力
- 选择可以通过特殊实例化实现:
- 模板元编程是图灵完备的
- 不作为考核内容