[TOC]
运算符重载
运算符重载的基本概念
-
为什么引入运算符重载?
在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的。有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。
例如:
complex_a和complex_b是两个复数对象;
求两个复数的和, 希望能直接写: complex_a + complex_b
-
运算符重载是什么?
-
运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。
-
运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。
-
同一个运算符,对不同类型的操作数,所发生的行为不同。
complex_a + complex_b =新的复数对象
5 + 4 = 9
-
-
运算符重载的形式?
-
本质是函数重载
-
可以重载为普通函数,也可以重载为成员函数
-
把含运算符的表达式转换成对运算符函数的调用。
-
把运算符的操作数转换成运算符函数的参数。
-
运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。
返回值类型 operator 运算符 (形参表) { ... }
-
例子:
class Complex
{
public:
double real,imag;
Complex(double r=0.0,double r=0.0):real(r),imag(i){}
//上方的构造函数添加了初始化列表,将real初始化为r,将imag初始化为i,这种风格比在函数体内直接用r,i赋值的风格更好
};
//重载成普通函数,参数个数为运算符目数
Complex operator+(const Complex& a,const Complex& b)
{return Complex(a.real+b.real,a.imag+b.imag);}
//返回一个Complex类的临时对象
//重载成成员函数,参数个数为运算符目数-1,因为此时一个参数已经确定了,就是这个重载归属的那个类定义的对象
Complex Complex::operator-(const Complex& c)
{return Complex(real-c.real,imag+c.imag);}
//返回一个Complex类的临时对象
int main()
{
Complex a(4,4),b(1,1),c;
c=a+b;
//等价于c=operator+(a,b);
cout<<c.real<<","<<c.imag<<endl;
cout<<(a-b).real<<","<<(a-b).imag<<endl;
//a-b等价于a.operator-(b),operator-被固定的那个变量就是对象a
return 0;
}
Output:
5,5
3,3
小结:
-
重载成普通函数,参数个数为运算符目数;
重载成成员函数,参数个数为运算符目数-1
-
c=a+b
等价于c=operator+(a,b)
;a-b
等价于a.operator-(b)
赋值运算符的重载
为什么引入赋值运算符重载?
有时候希望赋值运算符两边的类型可以不匹配
比如,把一个int类型变量赋值给一个Complex对象, 或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。
赋值运算符”=“只能重载为成员函数
例子
class String
{
private:char* str;
public:String():str(new char[1]){str[0]=0;}
//上面的构造函数添加了初始化列表,which new了一个只有一个元素的字符串数组,然后用这个数组的地址初始化str。在构造函数中往str写入一个0。最终str指向一个空字符串
const char* c_str(){return str;};
//上面的成员函数没有参数,返回一个指向常量字符串的指针,也就是str
String& operator=(const char* s);
//将“=”重载为读取一个指向char型数据的指针,返回一个String类临时对象的引用
String::~String(){delete[]str;}
//由于str指向的字符串数组是被new出来的,所以删除时必须使用delete[]
};
//下面的重载是为了使得obj="hello"能够成立
String& String::operator=(const char* s)
{
delete[]str;
//先删除对象String中变量str原本指向的字符串数组
str=new char[strlen(s)+1];
//初始化str,令str指向一个new出来的字符串数组,该数组大小为“=”参数数组长度+1
strcpy(str,s);
//上上句新建好str后,这句把s的内容拷贝到了str里面
return *this;
//返回这个成员函数作用的对象String的引用
}
int main()
{
String s;
s="Good Luck,";//等价于s.operator=("Good Luck,");
cout<<s.c_str()<<endl;
//String s2="hello!";
//这句话不注释掉就会出错,因为这句话不是赋值语句,而是初始化语句,会调用构造函数,但我们之前的构造函数不接受参数
s="Shenzhou 8!";
cout<<s.c_str()<<endl;
return 0;
}
Output:
Good Luck,
Shenzhou 8!
浅拷贝和深拷贝
class String
{
private:char* str;
public:String():str(new char[1]){str[0]=0;}
const char* c_str(){return str;};
String& operator=(const char* s);
String::~String(){delete[]str;}
};
String& String::operator=(const char* s)
{
delete[]str;
str=new char[strlen(s)+1];
strcpy(str,s);
return *this;
}
还是这个例子,但此时我们想要实现:
String S1,S2;
S1=“this”;
S2=“that”;
S1=S2;
如果不改变上面的代码(也就是浅拷贝),实际执行情况是下面这样的:
这导致以下几种问题:
- 如不定义自己的赋值运算符,那么S1=S2实际上导致 S1.str和 S2.str 指向同一地方。原先S1指向的地方无法删除被浪费掉。
- 如果S1对象消亡,析构函数将释放 S1.str指向的空间,则S2消亡时还 要释放一次,但一片被new出来的空间只能被delete一次。
- 另外,如果执行 S1 = “other”;会导致S2.str指向的地方被delete
因此要在 class String里添加成员函数:
String& operator=(const String& s)
{
delete[]str;
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
但是这样还不够,考虑下面的语句
String s;
s = “Hello”;
s = s;
如果等号两边的对象一样,=应该什么都不做。所以重载“=”应为:
String& operator=(const String& s)
{
if(this==&s)
return *this;
delete[]str;
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
return *this;
}
上面的重载即实现了深拷贝。
扩展:https://www.zhihu.com/question/36370072/answer/67181275
那么如果原来的物体销毁了,但是现在拷贝的物体还在,那么这时候你拷贝后的物体的成员指针就是一个悬挂指针,指向了不再存在的物体,那么你访问的话,那就不知道会发生什么了。
而对于深拷贝,这一个勤奋的人,他不会只做表面,他会把每一个细节都照顾好。于是,当他遇到指针的时候,他会知道new出来一块新的内存,然后把原来指针指向的值拿过来,这样才是真正的完成了克隆体和原来的物体的完美分离,如果物体比作人的话,那么原来的人的每一根毛细血管都被完美的拷贝了过来,而绝非只是表面。所以,这样的代价会比浅拷贝耗费的精力更大,付出的努力更多,但是是值得的。当原来的物体销毁后,克隆体也可以活的很好。
对operator=返回值的讨论
对运算符进行重载的时候,好的风格应该尽量保留运算符原本的特性
-
返回值为什么不能是void?
a=b=c
等价于a.operator=(b.operator=(c))
若
b.operator=(c)
返回值为void,则a=void,不可 -
返回值为什么不能是String?
(a=b)=c
等价于(a.operator=(b)).operator=(c)
若
a.operator=(b)
返回值是a,下一步就会让a=c的值。也就是这句话先让a=b的值,再让a=c的值,最终b并没有等于a和c,不可
复制构造函数的相同困境
为 String类编写复制构造函数的时候,会面临和 = 同样的问 题,用同样的方法处理。
String( String & s) {
str = new char[strlen(s.str)+1]; strcpy(str,s.str);
}
运算符重载为友元函数
-
为什么要将运算符重载为友元?
有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
例子:
class Complex { double real,imag; public: Complex( double r, double i):real(r),imag(i){ }; Complex operator+( double r ); }; Complex Complex::operator+( double r ) { //能解释 c+5 return Complex(real + r,imag); }
经过上述重载后:
Complex c ;
c = c + 5; //有定义,相当于 c = c.operator +(5);
c = 5 + c; //编译出错
所以,为了使得上述的表达式能成立,需要将 + 重载为普通函数。
Complex operator+ (double r,const Complex & c) { //能解释 5+c return Complex( c.real + r, c.imag); }
但是普通函数又不能访问私有成员,所以,需要将运算符 + 重载为友元。
class Complex { double real,imag; public: Complex( double r, double i):real(r),imag(i){ }; Complex operator+( double r ); friend Complex operator + (double r,const Complex & c); };
实例:可变长数组的实现
要编写可变长整型数组类,使之能如下使用
int main()
{
CArray a;//开始时数组是空的
for(int i=0;i<5;++i)
a.push_back(i);
//❗要用动态分配的内存来存放数组元素,需要一个指针成员变量
CArray a2,a3;
a2=a;
//❗要重载“=”
for(int i=0;i<a.length();++i)
cout<<a2[i]<<" ";
//❗要重载中括号[]
a2=a3;//a2变成空的
for(int i=0;i<a2.length();++i)//此时a2.length返回0
cout<<a2[i]<<" ";
cout<<endl;
a[3]=100;//将数组a的第三个数改为100
CArray a4(a);
//❗要自己写一个复制构造函数,不能用缺省的
for(int i=0;i<a4.length();++i)
cout<<a4[i]<<" ";
return 0;
}
Output:
0 1 2 3 4
0 1 2 100 4
该怎么写这个数组类??
class CArray
{
int size;//数组元素的个数
int* ptr;//指向动态分配的数组
public
CArray(int s=0);//构造函数,s代表数组元素的个数
CArray(CArray& a);//复制构造函数
~CArray();//析构函数
void push_back(int v);//用于在数组尾部添加一个元素v
CArray& operator=(const CArray& a);//用于数组对象间的赋值
int length(){return size;}//返回数组元素个数
int& CArray::operator[](int i){return ptr[i];}
//用于支持根据下标访问数组元素,如n=a[i]和a[i]=4这样的语句
//❗❗❗对于返回值的解释,看下面解释
}
对于int& CArray::operator[](int i){return ptr[i];}
要注意:
返回值类型必须是int&
,不能是int
!!!这是因为如果一个函数的返回值不是引用,不能将它写在等号左边,所以a[i]=4
这句话将编译出错
/**********************构造函数**********************************/
CArray::CArray(int s):size(s)
//这个初始化列表用s初始化size,s的缺省值是0(即不给参数时使用的s值)
{
if(s==0)
ptr=NULL;
else
ptr=new int[s];
}
/**********************复制构造函数*******************************/
CArray::CArray(CArray& a)
{
//如果a.ptr指向空数组,就令ptr指向空数组
if(!a.ptr){
ptr=NULL;
size=0;
return;
}
//如果a.ptr指向非空数组,就创建一个同样大小的空间复制上a.ptr的内容并将地址赋给ptr
ptr=new int[a.size];
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size=a.size;
}//上面这个复制构造函数完成了深拷贝的工作
/**********************析构函数**********************************/
CArray::~CArray()
{
if(ptr)
delete[]ptr;
}
/**********************“=”的重载函数*****************************/
//赋值号的作用是使“=”左边对象里存放的数组,大小和内容都和右边的对象一样
CArray& CArray::operator=(const CArray& a)
{
if(ptr==a.ptr)//防止a=a这样的赋值出错
return *this;
if(a.ptr==NULL){//如果a里面的数组是空的
if(ptr)//如果ptr指向的数组不是空的
delete[]ptr;
ptr=NULL;
size=0;
return *this;
}
if(size<a.size){//如果原有空间不够用,就需要分配新的空间
if(ptr)
delete[]ptr;
ptr=new int[a.size];
}
memcpy(ptr,a.ptr,sizeof(int)*a.size);
//如果原有空间够大,就不分配新的空间
size=a.size;
return *this;
}
/**********************push_back函数*****************************/
void CArrary::push_back(int v)
{
/*下面做分配空间的工作*/
if(ptr){//原数组不空
int* tmpPtr=new int[size+1];//创造一个比原数组多一个空间的数组
memcpy(tmpPtr,ptr,sizeof(int)*size);//拷贝原数组到tmpPtr里
delete[]ptr;//原数组被复制好后就可以删除释放空间
ptr=tmpPtr;//现在的ptr指向的空间比原来的大1
}
else//原数组是空的
ptr=new int[1];
/*下面做加入新的数组元素的工作*/
ptr[size++]=v;
}
流插入运算符和流提取运算符的重载
流插入运算符(左移运算符):«
cout是在iostream中定义的,ostream类的对象。“«”能用在cout上是因为再iostream中对“«”进行了重载。
-
ostream类中对«的重载(头文件中别人已经写好的代码)
考虑怎么重载才能使得下列语句成立:
cout«5;
cout«“this”;
cout«5«“this”;
按照以下方式重载成ostream类的成员函数,返回值是ostream类的引用
ostream& ostream::operator<<(int n) { ...//输出n的代码 return *this; } ostream& ostream::operator<<(const char* s) { ...//输出s的代码 return *this; }
cout<<5<<"this"
等价于cout.operator<<(5).operator<<("this")
-
将«重载为全局函数(需要自己写的)
假定下面程序输出为5hello,我们该如何补写?
class CStudent{ public:int nAge; }; int main() { CStudent s ; s.nAge = 5; cout << s <<"hello"; return 0; }
ostream& ostream<<(ostream& o,const CStudent& s) { o<<s.nAge; return o; }
正如:
重载成普通函数,参数个数为运算符目数;
重载成成员函数,参数个数为运算符目数-1
«被重载成全局函数,第一个参数就是
cout
,因此第一个参数类型必须为ostream
或ostream&
由于我们需要继续输出“hello”,因此返回值必须为
cout
,故返回值类型为ostream&
-
将«重载为全局函数,且定义成相关类的友元函数(需要自己写的)
这样可以访问指定类的私有成员
假定c是Complex复数类的对象,现在希望 写
cout << c;
,就能以a+bi
的形 式输出c的值,写cin>>c;
,就能从键 盘接受a+bi
形式的输入,并且使得c.real = a,c.imag = b
int main() { Complex c; int n; cin>>c>>n; cout<<c<<","<<n; return 0; }
示例输入/输出
input:13.2+133i 87 output:13.2+133i,87
我们编写Complex类如下:
#include<iostream> #include<string> #include<cstdlib> using namespace std; class Complex{ double real,imag; public: Complex(double r=0,double i=0):real(r),imag(i){}; friend ostream& operator<<(ostream& os,const Complex& c); friend istream& operator>>(istream& is,const Complex& c); //上面语句将<<,>>重载为Complex类的友元,可以访问Complex类的私有成员real,imag }; /****************对<<的重载***********************************/ ostream& operator<<(ostream& os,Complex& c) { os<<c.real<<"+"<<c.imag<<"i";//以“a+bi”的形式输出 return os; } /****************对>>的重载***********************************/ istream& operator>>(istream& is,Complex& c) { //将“a+bi”作为字符串读入,“a+bi”中间不能有空格 string s; is>>s; //确定实部和虚部的分界点 int pos=s.find("+",0); //分离出代表实部的字符串 string sTmp=s.substr(0,pos); c.real=atof(sTmp.c_str()); //atof库函数能将const char*指针指向的内容转换成float //分离出代表虚部的字符串 sTmp=s.substr(pos+1,s.length()-pos-2); c.imag=atof(sTmp.c_str()); return is; }
❗❗❗
c_str()
:该函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。 这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串
类型转换运算符的重载
#include<iostream>
using namespace std;
class Complex
{
double real,imag;
public:
Complex(double r=0,double i=0):real(r),imag(i){};
operator double(){return real;}
//重载了 强制类型转换运算符 double
};
int main()
{
Complex c(1.2,3.4);
/*显式转换*/
cout<<(double)c<<endl;//输出1.2
/*隐式转换*/
double n=2+c;//等价于double n=2+c.operator double()
cout<<n;//输出3.2
}
自增自减运算符的重载
-
如何将前置/后置的++,–区分开?
自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,C++规定:
-
前置运算符作为一元运算符重载
-
重载为成员函数
T& operator++() T& operator--()
-
重载为全局函数
T1& operator++(T2); T1& operator--(T2); //重载为全局函数时需要的参数个数比成员函数时多一个
-
-
后置运算符作为二元运算符重载,要多写一个没用的参数
-
重载为成员函数
T operator++(int); T operator--(int);
-
重载为全局函数
T1 operator++(int,T2); T1 operator--(int,T2); //重载为全局函数时需要的参数个数比成员函数时多一个
-
-
-
重载运算符的返回值
- 重载的原则:对运算符的重载要尽量维持运算符原本的属性
- c++中内置的++a返回值是a的引用, a++返回值是临时变量a
这也是为什么可以有
(++a)=1
,但不能有(a++)=1
,(函数的返回值如果不是引用,不能放在等好的左边) - 为了维持上面那种性质,前置运算符的返回值是对象,后置运算符的返回值是临时变量
例子:
int main()
{
CDemo d(5);
cout<<(d++)<<",";
cout<<d<<",";
cout<<(++d)<<",";
cout<<d<<"endl";
cout<<(d--)<<",";
cout<<d<<",";
cout<<(++d)<<",";
cout<<d<<"endl";
return 0;
}
要求输出结果为
5,6,7,7 7,6,5,5
该如何编写CDemo?
class Demo
{
private:
int n;
public:
CDemo(int i=0):n(i){}//初始化列表用i初始化n
operator int(){return n;}s.int()
//强制类型转换运算符的重载,使得(int)s等价于s.int()
//类型强制转换运算符重载时不能写返回值类型,实际上其返回值类型就是该运算符代表的类型
CDemo& operator++();//前置成员
CDemo operator++(int)//后置成员
friend CDemo& operator--(CDemo&);//前置全局
friend CDemo operator--(CDemo&,int);//后置全局
};
/*************************++a重载为成员函数*********************************/
CDemo& CDemo::operator++()
{
n++;//这个n是operator++()作用的那个对象的私有变量n
return *this;//返回修改后的对象的引用
}
//++s等价于s.operator++()
/*************************a++重载为成员函数*********************************/
CDemo CDemo::operator++(int k)//k是一个没用的参数
{
CDemo tmp(*this);//用复制构造函数构造一个临时对象,将修改前的对象的n值赋给他
n++;
return tmp;//返回修改前的对象
}//s++等价于s.operator++(0)
/*************************--a重载为全局函数*********************************/
CDemo& operator--(CDemo& d)//对一个全局函数,传进来的参数必须是引用才能修改他的值
{
d.n++;
return d;
}//--s等价于operator--(s)
/*************************a--重载为全局函数*********************************/
CDemo operator--(CDemo& d)
{
CDemo tmp(d);
n++;
return tmp;
}//s--等价于operator--(s,0)
注意事项
- C++不允许定义新的运算符 ;
- 重载后运算符的含义应该符合日常习惯:
- complex_a + complex_b
- word_a > word_b
- date_b = date_a + n
- 运算符重载不改变运算符的优先级;
- 以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;
- 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为 类的成员函数。
继承
-
继承:
- 在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么可以把A作为一个基类而把B作为基类的一个派生类
这是为了避免重复定义相似的类的麻烦
-
派生类的性质:
-
派生类中可以添加新的成员变量和成员函数
-
派生类一经定义可以独立使用
-
派生类拥有基类的全部成员**(但是依旧不能访问private)**
-
-
例子-学生管理系统
class CStudent{ private: string sName; int nAge; public: bool IsThreeGood(){}; void SetName(const string &name)//&表示引用 {sName=name;} }; //派生类的写法:类名:public基类名 class CundergraduateStudent:public CStudent{ private: int nDepartement; public: bool IsThreeGood(){...};//这个新的成员函数将基类的覆盖了 bool CanBaoYan(){...}; }; class CGraduatedStudent:public CStudent{ private: int nDepartement; char szMentorName[20]; public: int CountSalary(){...}; };
-
派生类对象的内存空间:
派生类对象体积=基类对象体积+派生类对象自己的成员变量体积
基类对象的存储位置位于派生类对象新增成员变量之前
class CBase{ int v1,v2; }; class CDerived:public CBase{ int v3; } //CDerived体积为12个字节
-
继承示例程序:学籍管理
#include<iostream> #include<string> using namespace std; class CStudent{ private: string name; string id; char gender; int age; public: void PrintInfo(); void Setnfo(const string & name_,const string & id_,int age_.char gender_); //&参数是引用 string GetName(){return name;} }; class CUndergraduateStudent:public CStudent{ private: string department; public: void QualifiedForBaoyan(){ cout<<"qualified for baoyan"<<endl; } //PrintInfo对于基类的同名函数是覆盖的关系 void PrintInfo(){ CStudent::PrintInfo();//调用基类的 cout<<"Department:"<<departement<<endl; void SetInfo(const string& name_,const string& id_,int age_,char gender_,const string& department_){ CStudent::SetInfo(name_,id_,age_,gender_);//调用基类的 department=department_; } } };
继承关系和复合关系
-
继承:"是“关系
A是基类,B是A的派生类
逻辑上要求:一个B对象也是一个A对象
-
复合:"有“关系
逻辑上要求:A对象是B对象的成员变量
例子:几何形体程序中,需要写"点"类,也需要写"圆"类,两者的关系就是复合关系,每一个圆对象内都包含有一个点对象,这个点对象就是圆心
class CPoint{
double x,y;
friend class CCircle;
//便于CCircle类操作其圆心
};
class CCircle{
double r;
CPoint center;
};
-
复合关系的使用
如果要写一个小区养狗管理程序, 需要写一个“业主”类,还需要写一个“狗”类。 而狗是有 “主人” 的,主人当然是业主(假定狗只有 一个主人,但一个业主可以有最多10条狗)
-
凑合的写法
//为狗类设一个业主类的对象指针 //为业主类设一个狗类的对象数组 class CMaster; //CMaster必须提前声明,不能先写CMaster再写CDog类 class CDog{ CMaster* pm; }; class CMaster{ CDog dogs[10]; }; /*这种写法的缺陷: 1.对象的成员变量理论上应是该对象的不可分割的组成部分,但主人对于狗并不是这种关系 2.所有的狗对象都被放在一个数组中,对狗的操作必须通过主人来进行
-
正确的写法
//为狗类设一个业主类对象指针 //为业主类设一个狗类对象指针数组 class CMaster; class CDog{ CMaster* pm; }; class CMaster{ CDog* dogs[10] };
覆盖和保护成员
覆盖
派生类可以定义一个和基类成员同名的成员,这叫做覆盖
在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员
要在派生类中访问由基类定义的同名成员时,要使用作用域符号::
类的保护成员
-
基类的private成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
-
基类的public成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数
– 派生类的友元函数
– 其他的函数
-
基类的protected成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数可以访问当前对象的基类的保护成员
派生类的构造函数
class Bug{
private:
int nlegs;
int ncolor;
public:
int ntype;
Bug(int legs,int color);
void PrintBug(){};
};
class FlyBug:public Bug{
private:
int nwings;
public:
FlyBugs(int legs,int color,int wings);
}
Bug::Bug(int legs,int color)
{
nlegs=legs;
ncolor=color;
}
错误的FlyBug构造函数写法:
FlyBug::FlyBug(int legs,int color,int wings)
{
nlegs=legs;//不能访问
ncolor=color;//不能访问
//上面的操作是错误的!!!!nlegs,ncolor是基类的私有成员,不能被派生类的成员函数访问!
ntypes=1;//okk
nwings=wings;
}
正确的FlyBug构造函数写法:
FlyBug::FlyBug(int legs,int color,int wings):Bug(legs,color)
//初始化列表
{
nlegs=legs;
ncolor=color;
//上面的操作是错误的!!!!nlegs,ncolor是基类的私有成员,不能被派生类的成员函数访问!
nwings=wings;
};
-
在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数 之前,总是先执行基类的构造函数。
-
调用基类构造函数的两种方式
-
显式方式:在派生类的构造函数中,为基类的构造函数提供参数.
derived::derived(arg_derived-list):base(arg_base-list)
-
隐式方式:在派生类的构造函数中,省略基类构造函数时, 派生类的构造函数则自动调用基类的默认构造函数.
-
-
派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
公有继承的赋值兼容规则
公有继承:class derived: public base{ };
class base{};
class derived:public base{};
base b;
derived d;
规则:
-
派生类的对象可以赋值给基类对象
b=d;
-
派生类对象可以初始化基类的引用
base & br=d;
-
派生类对象的地址可以赋值给基类指针
base * pb=&d;
直接基类和间接基类:
-
声明派生类时,只需要列出其直接基类
-
派生类沿着类的层次向上自动继承他的间接基类
-
派生类的成员包括:
-
直接基类的成员
-
所有间接基类的所有成员
-
自己的成员
-
#include<iostream>
using namespace std;
class base{
public:
int n;
base(int i):n(i){
//构造函数有参数,也有初始化列表
//n是成员变量,i是参数表
cout<<"base"<<n<<"constructed"<<endl;
}
~base(){
cout<<"base"<<n<<"destructed"<<endl;
}
};
class derived:public base{
public:
derived(int i):base(i){
//构造函数有参数,也有初始化列表
cout<<"derived constructed"<<endl;
}
~derived(){
cout<<"derived destructed"<<endl;
}
};
class morederived:public derived{
public:
morederived():derived(4){
//构造函数有参数,也有初始化列表
//只需要直接基类的初始化列表
cout<<"morederived constructed"<<endl;
}
~morederived(){
cout<<"morederived destructed"<<endl;
}
};
int main()
{
morederived obj;
return 0;
}
output:
base4constructed
derived constructed
morederived constructed
morederived destructed
derived destructed
base4destructed
多态
虚函数和多态
-
虚函数
在类的定义中,前面有virtual关键字的成员函数
class base{ virtual int get(); };
❗❗virtual关键字只用在类定义里的函数声明中,写函数体时候不用
int base::get()//不需要在get前面加virtual!!!
❗❗构造函数和静态成员函数不能是虚函数
❗❗虚函数和普通函数的本质差别:虚函数可以参与多态,静态函数不能
-
多态的表现形式一:指针
-
派生类的对象可以赋给基类指针
-
通过基类指针调用基类和派生类中的同名虚函数时候:
-
若指针指向一个基类的对象,则被调用的是基类的虚函数
-
若指针指向一个派生类的对象,则被调用的是派生类的虚函数
-
class Cbase(){ public: virtual void SomeVirtualFunction(){} }; class Cderived:public Cbase(){ public: virtual void SomeVirtualFunction(){} }; int main(){ Cderived Oderived; Cbase *p= &Oderived; p->SomeVirtualFunction(); //p指向派生类的对象,调用的是派生类的虚函数 return 0; }
-
-
多态的表现形式二:对象
- 派生类的对象可以赋给基类引用
- 通过基类引用调用基类和派生类中的同名虚函数时:
- 若该引用引得是一个基类的对象,那么被调用的是基类的虚函数
- 若该引用引得是一个派生类的对象,那么被调用的是派生类的虚函数
class Cbase(){
public:
virtual void SomeVirtualFunction(){}
};
class Cderived(){
public:
virtual void SomeVirtualFunction(){}
};
int main(){
Cderived Oderived;
Cbase &r=Oderived;
r.SomeVirtualFunction();//!!!引用调用函数时用"."!!!
//r引用的是派生类的对象,调用派生类的虚函数
}
-
多态的作用
增强程序的可扩充性**(程序需要修改或增加功能的时候,需要改动和增加的代码较少)**
使用多态的游戏程序示例
几何形体处理程序
#include<iostream>
#include<stdlib.h>
#include<math.h>
using namespace std;
//定义基类
class CShape{
public:
virtual double Area()=0;
//后面写了一个"=0",说明这是纯虚函数,连函数体都没有
virtual void PrintInfo()=0;
//因为我们要处理的图形只有圆形三角矩形几种,不存在CShape这样一种抽象的形状,所以不需要为CShape编写这两程序,这两程序在派生类中会分别定义
};
//定义派生类
class CRectangle:public CShape{
public:
int w,h;
virtual double Area();
virtual void PrintInfo();
};
class CCircle:public CShape{
public:
int r;
virtual double Area();
virtual void PrintInfo();
};
class CTriangle:public CShape{
public:
int a,b,c;
virtual double Area();
virtual void PrintInfo();
};
//实现派生类的成员函数
double CRectangle::Area(){
return w*h;
}
void CRectangle::PrintInfo(){
cout<<"Rectangle:"<<Area()<<endl;//输出的末尾要加endl!!
}
double CCircle::Area(){
return 3.14*r*r;
}
void CCircle::PrintInfo(){
cout<<"Circle:"<<Area()<<endl;
}
double CTriangle::Area(){
double p=(a+b+c)/2.0;
return sqrt(p*(p-a)*(p-b)*(p-c));
}
void CCircle::PrintInfo(){
cout<<"Triangle:"<<Area()<<endl;
}
//存放不同类型的几何形体
CShape *pShapes[100];
int MyCompare(const void *s1,const void *s2);
//用三个数组来存浪费空间,难以增改,难以实现排序等进阶功能
//所以我们使用多态来存储
//pShapes数组中的元素都是基类指针,由于基类指针能够指向派生类对象,我们可以在后面把指针指向new出来的派生类对象
//主函数
int main(){
int i;int n;
CRectangle *pr;CCircle *pc;CTriangle *pt;
cin>>n;
for(i=0;i<n;i++){
char c;
cin>>c;
switch(c){
case'R':
pr=new CRectangle();
cin>>pr->w>>pr->h;
pShapes[i]=pr;
break;
case 'C':
pc=new CCircle(); cin>>pc->r;
pShapes[i]=pc;
break;
case'T':
pt=new CTriangle();
cin>>pt->a>>pt->b>>pt->c;
pShapes[i]=pt;
break;
}
}
qsort(pShapes,n,sizeof(CShape*),MyCompare);
for(i=0;i<n;i++)
pShapes[i]->PrintInfo();
return 0;
}
//为什么变量前面要加关键字void*?然后可以将指向任何类型数据的指针赋给这个void*类型指针
int MyCompare(const void *s1,const void *s2){
double a1,a2;
CShape**p1;
CShape**p2;
//一定要写两个*,不能用"*s1"来取得s1指向的内容
p1=(CShape**)s1;
p2=(CShape**)s2;
//s1,s2指向数组中的元素,数组中元素的类型是CShape*
//因此p1,p2是指向指针的指针,类型为CShape**
a1=(*p1)->Area();//*p1的类型是CShape*是基类指针
a2=(*p2)->Area();
if(a1<a2)
return -1;
else if(a2<a1)
return 1;
else
return 0;
}
*下面是 qsort()
函数的声明。
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
- base – 指向要排序的数组的第一个元素的指针。
- nitems – 由 base 指向的数组中元素的个数。
- size – 数组中每个元素的大小,以字节为单位。
- compar – 用来比较两个元素的函数。
虚析构函数,纯虚函数,抽象类
虚析构函数:
class son{
public:
~son(){cout<<"bye from son"<<endl;}
};
class grandson:public son{
public:
~grandson(){cout<<"bye from grandson"<<endl;}
};
int main()
{
son *pson;
pson=new grandson();
//new一个对象的时候,如果对象的构造函数不需要参数就要写一对括号,跟new一个数据不一样
delete pson;
return 0;
}
output:
bye from son
new出来的grandson没有被删除!!!!!,反而执行了son的析构函数,然而我们并没有定义son类的对象
解决办法:
把基类son的析构函数变成虚函数
纯虚函数和抽象类:
-
包含纯虚函数的类就是抽象类
-
抽象类不能创建自己的对象
-
抽象类的指针/引用可以指向其派生类的对象
-
在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数。
-
如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。
输入输出和模板
函数模板
- 例子-求数组最大元素的函数模板
template<class T>
T MaxElement(T a[],int size){
T tmpMax=a[0];
for(int i=1;i<size;i++)
if(tmpMax<a[i])
tmpMax=a[i]
return tmpMax;
}
- 例子-不通过参数实例化函数模板
#include<iostream>
using namespace std;
template<class T>
T Inc(T n){
return 1+T;
//由于Inc的返回值是T类型的,这里要对+进行重载
}
int main(){
cout<<Inc<double>(4)/2;
//用<double>直接规定T是double类型
}
-
函数模板是可以重载的,只要他们的形参表和类型参数表不一样即可
-
函数模板和函数的次序:
参数匹配+普通函数—–>参数匹配模板函数—–>参数经自动类型转换后能匹配的普通函数
类模板
编写模板是为了实现泛型程序设计,即写出一个类/函数后,可以作用与多种数据类型
类模板的定义
template<typename 类型参数,typename 类型参数,...>
class 模板名字
{
成员变量;
成员函数;
}
类模板内成员函数定义
template<typename 类型参数,typename 类型参数,...>
(返回值)类型参数 模板名字 <类型参数名字,类型参数名字,...>::成员函数名字(参数表)
{
...
}
类模板定义对象的写法
模板名字<实际类型参数表> 对象名(构造函数实参表);
示例:Pair类模板
#include<iostream>
#include<string.h>
using namespace std;
//类型参数就是这个类需要的参数的类型
//其个数就该类需要的参数个数
template<typename T1,typename T2>
class Pair
{
public:
T1 key;//关键字
T2 value;//值
Pair(T1 k,T2 v):key(k),value(v){};
//构造函数及其初始化列表
bool operator < (const Pair<T1,T2>&p)const;
//对"<"进行重载,其形参是pair模板定义的类的常引用
//我们希望这个"<"是不变的,最后加个const
};
template<typename T1,typename T2>
bool Pair<T1,T2>::operator < (const Pair<T1,T2>&p)const
{
return key<p.key;
}
int main()
{
Pair<string,int> student("Tom",19);
//对象的名字是:student
//注意:由类模板"pair"生成类"pair<string,int>",再由这个类生成对象"student"
cout<<student.key<<" "<<student.value;
return 0;
}
output:
Tom 19
-
类模板的实例化:编译器由类模板生成类的过程
-
同一个模板实例化出的类是不兼容的,他们根本就是两个类
Pair<string,int> *p; Pair<string,double> a; p=a;//报错
函数模板作为类模板成员
#include<iostream>
using namespace std;
template<typename T>
class A
{
public:
//注意这里不能写T,因为这里成员函数的类型参数相对类的类型参数来说是形参
template<typename T2>
void Func(T2 t){cout<<t;}
};
int main()
{
A<int> a;
//类模板名: A
//类名: A<int>
//对象名: a,其需要的参数类型为int(虽然下面的定义里我没有用这个参数)
a.Func('K');//成员函数模板Func被实例化,其参数类型为char
a.Func("hello");//成员函数模板Func再次被实例化,其参数类型为const char*
return 0;
}
output:
Khello
-
A<int>
这个类通过上面对函数模板Func
的两次实例化有了两个名为Func
的成员函数:Func(char c);//赋给该函数的值是:K Func(const char* string);//赋给该函数的值是:指向字符串"hello"的指针
类模板的类型参数表中可以出现非类型参数:

标准模板库STL
概述

迭代器
-
用于指向顺序容器/关联容器中的元素
-
用法和指针类似
-
有const和非const两种
-
通过迭代器可以读取其指向的元素
-
通过非const迭代器可以修改其指向的元素
代码:
定义一个容器类的迭代器的方法:
容器类名::iterator 变量名;
容器类名::const_iterator 变量名;
访问一个迭代器指向的元素:
*迭代器变量名
迭代器示例:
#include<vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> v;
//v是一个存放int类型元素的动态数组,一开始里面没有元素
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//正向迭代器
vector<int>::const_iterator i;
//常量迭代器,只能读取元素,不能修改元素
for(i=v.begin();i!=v.end();i++)
//begin()获取容器第一个元素的位置
//end()获取容器最后一个元素后面的位置
cout<<*i<<",";
cout<<endl;
//反向迭代器
vector<int>::reverse_iterator r;
for(r=v.rbegin();r!=v.rend;r++)
//rbegin()获取容器最后一个元素的位置
//rend()获取容器第一个元素前面的位置
cout<<*r<<",";
cout<<endl;
//非常量迭代器
vector<int>::iterator j;
for(j=v.begin();j!=v.end();j++)
*j=100;//用非常量迭代器修改指向的元素
for(i=v.begin();i!=v.end();i++)
cout<<*i<<",";//用常量迭代器读取指向的元素
cout<<endl;
}
两种不同的的迭代器:
-
双向迭代器p&p1
-
++p,p++
-
–p,p–
-
p*(实际上返回值是p指向的对象的引用)**
-
p=p1
-
p==p1;p!=p1
-
-
随即访问迭代器p&p1
- 双向迭代器的所有操作
- p+=i(将p向后移动i个元素)
- p-=i
- p+i (p+i的值=指向p后面第i个元素的迭代器)
- p-i
- p[i](p[i]的值=p后面第i个元素的引用)
- p<p1;p<=p1;p>p1;p>=p1
容器
顺序容器
vector(动态数组)
vector上的操作
#include<iostream>
#include<vector>
using namespace std;
template<class T>
void PrintVector(T s,T e)
{
for(;s!=e;++s)
cout<<*s<<" ";
cout<<endl;
}
int main()
{
int a[5]={1,2,3,4,5};
vector<int> v(a,a+5);
//将数组a中下标从0到4的元素都拷贝到v里面
cout<<v.end()-v.begin();
//vector的迭代器是随即迭代器可以相减
//上面输出的v中元素的个数:5
v.insert(v.begin()+2,13);
PrintVector(v.begin(),v.end());
//在v下标为2的位置插入一个元素13,后面的元素全部后推
v.erase(v.begin()+2);
PrintVector(v.begin(),v.end());
//删除位于v中下标为2的元素
vector<int> v2(4,100);
//v2有4个元素,都是100
v2.insert(v2.begin(),v.begin()+1,v.begin()+3);
//将v的一段插入v2开头
//v下标为1的元素到v下标为3的元素(不包括v下标为3的元素!!!)
PrintVector(v2.begin(),v2.end());
v.erase(v.begin()+1,v.begin()+3);
PrintVector(v.begin(),v.end());
//删除v上的一个区间,即2,3
return 0;
}
用vector实现二维数组
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<vector<int> >v(3);
//v中有3个元素,每个元素都是vector<int>空容器
//注意vector<int>后面一定要加一个空格,不然编译器会把两个尖括号当成右移运算符
//下面往v的元素里添加元素
for(int i=0;i<v.size();++i)
for(int j=0;j<4;++j)
v[i].push_back(j);
//下面显示v的元素里的元素
for(int i=0;i<v.size();++i){
for(int j=0;j<v[i].size();++j)
cout<<v[i][j]<<" ";
cout<<endl;
}
return 0;
}
deque(双向队列)
-
所有vector的操作都适用于deque
-
deque多了:
push_front
:将元素插入到前面pop_front
:删除最前面的元素
list(双向列表)
成员函数

list上的操作
#include<list>
#include<iostream>
#include<algorithm>
using namepsace std;
class A
{
private:
int n;
public:
A(int n_){n=n_;}
friend bool operator<(const A & a1,const A & a2);
friend bool operator==(const A & a1,const A & a2);
friend ostream & operator << (ostream & o,const A & a);
}
bool operator<(const A & a1,const A & a2)
{return a1.n<a2.n}
bool operator==(const A & a1,const A & a2)
{return a1.n==a2.n}
ostream & operator << (ostream & o,const A & a)
{o<<a.n;return o;}
//下面定义一个函数模板,其参数是"类型可变列表的引用"
template <class T>
void PrintList(const list<T> & lst)//参数是"类型可变列表的引用"
{
typename list<T>::const_iterator i;
//typename用来说明"list<T>::const_iterator"描述的是个类型
i=lst.begin();
for(i=lst.begin();i!=lst.end();i++)
cout<<*i<<",";
}
int main()
{
list<A> lst1,lst2;
//定义了两个元素为A类对象的列表
...
PrintLsit(lst1);
}
容器适配器
容器适配器上没有迭代器
stack
-
是后进先出的数据结构,
-
可以
push
插入pop
删除top
返回栈顶元素的引用
-
可用vector,list,deque来实现,缺省情况下,用deque实现。(vector、deque实现的性能比list好)
template<class T,class Container=deque<T>> //第一个类型参数是栈里的元素类型 //第二个类型参数是栈用什么容器来实现,并表示缺省情况下是deque class stack{ ... };
queue
-
是先进先出的数据结构,
-
可以
push
插入(发生在队尾)pop
删除front
返回栈顶元素的引用back
返回队尾元素的引用
-
可用list,deque来实现,缺省情况下,用deque实现。(vector、deque实现的性能比list好)
template<class T,class Container=deque<T>> //第一个类型参数是容器适配器里的元素类型 //第二个类型参数是容器适配器用什么容器来实现,并表示缺省情况下是deque class stack{ ... };
priority_queue
-
可用vector和deque实现,缺省情况下用vector实现
template<class T,class Container=vector<T>,class Compare=less<T>> //第一个类型参数是容器适配器里的元素类型 //第二个类型参数是容器适配器用什么容器来实现,并表示缺省情况下是vector //第三个类型参数是优先队列的元素比较器用什么来实现,并表示缺省情况下是小于号 class priority_queue;
-
priority_queue通常用堆排序技术实现,保证最大的元素总是在最前面,即执行pop操作时,删除的是最大的元素;执行top操作时,返回的是最大元素的引用,默认的元素比较器是less
-
push
,pop
时间复杂度:O(logn) -
top
时间复杂度:O(1)
所有容器适配器中都有成员函数:
- empty():用于判断适配器是否为空
- size():用于返回适配器中元素个数
算法
类型参数Pred
大多重载的算法都是有两个版本的:
- 一个是
- 用“==”判断元素是否相等
- 用“<”来比较大小
- 另一个多出来一 个类型参数
Pred
,以及函数形参Pred op
- 表达式“op(x,y)”的返回值是ture,则x等于y
- 表达式“op(x,y)”的返回值是false,则x小于y
不变序列算法(以下图片上传失败)
- 不会修改算法作用的容器或对象
- 适用于顺序容器和关联容器
- 时间复杂度:O(n)



find
很重要
排序算法
- 需要随机访问迭代器的支持
- 不适用于关联容器和list
- 时间复杂度:O(log(n))


sort:快速排序
-
模板
template<class Ranlt> void sort(Ranlt first,Ranlt last);
- 按升序排列
- 判断x是否应该比y靠前,就看x<y是否为true
template<class Ranlt,class Pred> void sort(Ranlt first,Ranlt last,Pred pr);
- 按升序排列
- 判断x是否应该比y靠前,就看pr(x<y)是否为true
应用:
#include<iostream>
#include<algorithm>
using namespace std;
//定义Pred pr
class MyLess
{
public:
bool operator()(int n1,int n2)
{
return(n1%10)<(n2%10);
}
};
int main()
{
int a[]={14,2,9,111,78};
int i;
sort(a,a+5,MyLess());//按个位数大小排序
for(i=0;i<5;i++)
cout<<a[i]<<" ";
cout<<endl;
sort(a,a+5,greater<int>());//按降序排序
for(i=0;i<5;i++)
cout<<a[i]<<" ";
}
注意:
-
sort实际上是快速排序,时间复杂度为:O(n*log(n))
- 平均性能最优
- 最坏的情况性能非常差
-
stable_sort实际上是归并排序
- 能保证相等元素之间的先后次序
- 存储空间足够时,时间复杂度:n*log(n)
- 存储空间不够时,时间复杂度:n*log(n)*log(n)
-
list只能使用内置的排序算法:
list::sort