北大郭炜《程序设计与算法(三)》Mooc笔记:运算符重载和继承

运算符重载

运算符重载的基本概念

  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;

如果不改变上面的代码(也就是浅拷贝),实际执行情况是下面这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7eshIldp-1636871571879)(assets/2020-10-09-12-21-54.png)]

这导致以下几种问题:

  • 如不定义自己的赋值运算符,那么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 *thisdelete[]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]
    };
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrOfSAFr-1636871571881)(assets/image-20210304144737070.png)]

覆盖和保护成员

覆盖

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

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

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

类的保护成员

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

直接基类和间接基类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dcrtg4Og-1636871571881)(assets/image-20210307215222740.png)]

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

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

  • 派生类的成员包括:

    • 直接基类的成员

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

    • 自己的成员

#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
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值