[TOC]

运算符重载

运算符重载的基本概念

  1. 为什么引入运算符重载?

    在数学上,两个复数可以直接进行+、-等运算。但在C++中,直接将+或-用于复数对象是不允许的。有时会希望,让对象也能通过运算符进行运算。这样代码更简洁,容易理解。

    例如:

    complex_a和complex_b是两个复数对象;

    求两个复数的和, 希望能直接写: complex_a + complex_b

  2. 运算符重载是什么?

    • 运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。

    • 运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。

    • 同一个运算符,对不同类型的操作数,所发生的行为不同。

      complex_a + complex_b =新的复数对象

      5 + 4 = 9

  3. 运算符重载的形式?

    • 本质是函数重载

    • 可以重载为普通函数,也可以重载为成员函数

    • 把含运算符的表达式转换成对运算符函数的调用。

    • 把运算符的操作数转换成运算符函数的参数。

    • 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。

      返回值类型 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. 重载成普通函数,参数个数为运算符目数;

    重载成成员函数,参数个数为运算符目数-1

  2. 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);
}

运算符重载为友元函数

  1. 为什么要将运算符重载为友元?

    有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。

    例子

    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中对“«”进行了重载。

  1. 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")

  2. 将«重载为全局函数(需要自己写的)

    假定下面程序输出为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,因此第一个参数类型必须为ostreamostream&

    由于我们需要继续输出“hello”,因此返回值必须为cout,故返回值类型为ostream&

  3. 将«重载为全局函数,且定义成相关类的友元函数(需要自己写的)

    这样可以访问指定类的私有成员

    假定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
}

自增自减运算符的重载

  1. 如何将前置/后置的++,–区分开?

    自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符,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);
        //重载为全局函数时需要的参数个数比成员函数时多一个
        
  2. 重载运算符的返回值

    • 重载的原则:对运算符的重载要尽量维持运算符原本的属性
    • 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)

注意事项

  1. C++不允许定义新的运算符 ;
  2. 重载后运算符的含义应该符合日常习惯:
    • complex_a + complex_b
    • word_a > word_b
    • date_b = date_a + n
  3. 运算符重载不改变运算符的优先级;
  4. 以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;
  5. 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为 类的成员函数。

继承

  • 继承:

    • 在定义一个新的类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条狗)

  1. 凑合的写法

    //为狗类设一个业主类的对象指针
    //为业主类设一个狗类的对象数组
    
    class CMaster;
    //CMaster必须提前声明,不能先写CMaster再写CDog类
    
    class CDog{
        CMaster* pm;
    };
    class CMaster{
        CDog dogs[10];
    };
    /*这种写法的缺陷:
    1.对象的成员变量理论上应是该对象的不可分割的组成部分,但主人对于狗并不是这种关系
    2.所有的狗对象都被放在一个数组中,对狗的操作必须通过主人来进行
    
  2. 正确的写法

    //为狗类设一个业主类对象指针
    //为业主类设一个狗类对象指针数组
    
    class CMaster;
    
    class CDog{
        CMaster* pm;
    };
    class CMaster{
        CDog* dogs[10]
    };
    

    image-20210304144737070

覆盖和保护成员

覆盖

派生类可以定义一个和基类成员同名的成员,这叫做覆盖

在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员

要在派生类中访问由基类定义的同名成员时,要使用作用域符号::

类的保护成员

  • 基类的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;

规则:

  1. 派生类的对象可以赋值给基类对象

    b=d;
    
  2. 派生类对象可以初始化基类的引用

    base & br=d;
    
  3. 派生类对象的地址可以赋值给基类指针

    base * pb=&d;
    

直接基类和间接基类:

image-20210307215222740

  • 声明派生类时,只需要列出其直接基类

  • 派生类沿着类的层次向上自动继承他的间接基类

  • 派生类的成员包括:

    • 直接基类的成员

    • 所有间接基类的所有成员

    • 自己的成员

#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"的指针
    

类模板的类型参数表中可以出现非类型参数:

![image-20210308125420974](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210308125420974.png)

标准模板库STL

概述

image-20210318145622473

image-20210318145709507

![image-20210318145751989](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210318145751989.png)

迭代器

  • 用于指向顺序容器/关联容器中的元素

  • 用法和指针类似

  • 有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(双向列表)

成员函数

![image-20210318143445110](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210318143445110.png)

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)

所有容器适配器中都有成员函数:

  1. empty():用于判断适配器是否为空
  2. size():用于返回适配器中元素个数

算法

类型参数Pred

大多重载的算法都是有两个版本的:

  1. 一个是
    • 用“==”判断元素是否相等
    • 用“<”来比较大小
  2. 另一个多出来一 个类型参数Pred,以及函数形参Pred op
    • 表达式“op(x,y)”的返回值是ture,则x等于y
    • 表达式“op(x,y)”的返回值是false,则x小于y

不变序列算法(以下图片上传失败)

  • 不会修改算法作用的容器或对象
  • 适用于顺序容器和关联容器
  • 时间复杂度:O(n)

![image-20210315213723396](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210315213723396.png)

![image-20210315213758362](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210315213758362.png)

![image-20210315213935514](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210315213935514.png)

  • find很重要

排序算法

  • 需要随机访问迭代器的支持
  • 不适用于关联容器和list
  • 时间复杂度:O(log(n))

![image-20210315215204555](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210315215204555.png)

![image-20210315215228540](/Users/abigail/OneDrive - sjtu.edu.cn/Abigail/大三下/北大c++笔记/assets/image-20210315215228540.png)

sort:快速排序

  • 模板

    template<class Ranlt>
    void sort(Ranlt first,Ranlt last);
    
    1. 按升序排列
    2. 判断x是否应该比y靠前,就看x<y是否为true
    template<class Ranlt,class Pred>
    void sort(Ranlt first,Ranlt last,Pred pr);
    
    1. 按升序排列
    2. 判断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