zoukankan      html  css  js  c++  java
  • C++-拷贝赋值,静态成员,单例模式,成员指针,操作符标记,操作符函数,操作符重载(day6)

    一、拷贝赋值

    1、浅拷贝赋值

    class String{
    
    public:
    
      ...
    
      String& operator=(const String& that){
    
        m_str=that.m_str;
        return *this;
      }
    
      ...
    
    private:
    
      char* m_str;
    
    };
    
    //主函数
    
    String s1("hello world!");
    
    String s2;
    
    s2=s1;//此时发生的是浅拷贝<==>s2.operator=(s1);//operator=相当于一个函数名

    注意:

      赋值拷贝也是浅拷贝,所以,当成员变量存在指针,引用可能存在内存共享的问题时,根据实际情况来决定是否进行深拷贝。同时,上例中s2在进行构造时,已经动态分配了内存,在进行拷贝赋值时,修改了成员变量m_str的指向,造成的内存泄漏。综上称为浅拷贝赋值。

      此处,如果直接将s1的字符串数据赋值开s2指针所指向的内存的话,可能发生越界的可能。所以最好是释放原来的内存,重新分配一块与s1指针所指的内存大小一致的内存。

      在上面利用到了野指针。

    2、拷贝赋值

      类的缺省的拷贝赋值和拷贝构造函数一样,是浅拷贝。为了得到深拷贝的效果,必须自己定义拷贝赋值的运算符函数。

    类名& operator=(const 类名& 形参名){
      
      if(this!=&that){
    //防止自赋值

        //释放旧内存

        //分配新内存//考虑到new可能失败的场景,可以将这一步与上一步交换
        //拷贝数据到新内存
        
      }
      return *this   }

     也可以拷贝赋值中,创建临时变量来交换,临时变量由“}”调用,不需主动调用

    二、静态成员

      c语言中,普通的静态变量,静态函数,声明周期为整个进程。但是它的作用域被限定在单个文件中,使用Extern关键字修饰也无法在其他文件中使用。

    1、静态成员变量

    语法:

    class 类名{

      static 数据类型 变量名;//声明

    };

    数据类型 类名 ::变量名=初值;//定义和初始化

    注意:

      (1)静态成员变量属于类,不属于某个单独的对象

      (2)静态成员变量仍然存储在数据段,但是只是对类可见,相当于类给它了一个使用范围限定。

      (3)普通成员变量的定义随着对象的定义在栈区被定义,但是静态的成员变量在数据段单独被定义。不属于对象。

      (4)访问静态成员变量需要使用   类名:: 静态成员变量名来进行访问,这里类名就相当于一个命名空间。当然也可以像普通变量一样使用对象来进行访问。作用域被限定在类中。

      (5)不能在构造函数中进行初始化,必须在外部单独定义进行初始化

      (6)静态成员变量也要收到访问控制属性的影响

    例:

    class A{
    
    public:
    
      int m_data;
    
      static int s_data;//声明静态成员变量
    
    };
    
    int A::s_data = 100//也可以不初始化,那么它将被初始化为0
    
    
    
    //主函数中
    
    A a;
    
    cout<<a.s_data<<endl;
    
    cout<<A::s_data<<endl;

    2、静态成员函数

    class 类名{

      static 返回类型 函数名(形参表){...}

    };

    注意:

      (1)和静态成员变量一样,可以通过类名去访问,也可以通过对象去访问。

      (2)静态成员函数没有this指针,因为它不属于某个对象。因为常函数const修饰this,所以也没有静态常函数的说法。

      (3)由于静态成员函数没有this指针,所以不能狗访问普通的成员变量,他只能访问静态的成员变量。普通成员变量都是通过this指针访问的。同理静态成员函数也只能访问静态成员函数。

      (4)普通成员函数没有(3)中的限定

      

    class A{
    
    public:
      
      static void func(void){
        cout<<s_data<<endl;//ok
        cout<<m_data<<endl;//error
      }
    
    
      int m_data;
    
      static int s_data;//声明静态成员变量
    
    };
    
    int A::s_data = 100//也可以不初始化,那么它将被初始化为0
    
    
    
    //主函数中
    
    A a;
    
    cout<<a.s_data<<endl;
    
    cout<<A::s_data<<endl;

    三、单例模式

    1、如果一个类,只允许存在一个唯一的对象,如:任务管理器的图形界面就是一个单例模式

    单例模式的几个条件

      (1)禁止在外部创建对象:私有化构造函数

      (2)类的内部来维护这个唯一的对象:静态成员变量

    class A{
    
      static A a;
    
    };

      (3)提供访问单例对象的方法:静态成员函数

    2、创建方法

    (1)饿汉式:无论用不用,程序启动即创建

    class A{
    
    private:
      
      A(int data=0):m_data(data){}//定义私有的构造函数
    
      A(const A& that);//只声明不定义,编译器也会定义默认的拷贝构造
    
      int m_data;
    
      static A s_instance;//声明静态单例
    
    public:
      static A& getInstance(void){//定义单例的接口
        return s_instance;
      }
    };
    A A::s_instance(1234);//在外部初始化单例
    
    int main(void){
      A a;//error
      A* p =new A;//error
      A& a1= A::getInstance();
    }

    (2)懒汉式:用的时候创建,不用即销毁,需要存到堆区

    class A{
    
    private:
      
      A(int data=0):m_data(data){}//定义私有的构造函数
    
      A(const A& that);//只声明不定义,编译器也会定义默认的拷贝构造
      
      ~A(void){
        s_instance=NULL;
      }
    
      int m_data;
    
      static A* s_instance;//声明静态单例指针
    
    public:
      static A& getInstance(void){//定义单例的接口
        if(s_instance==NULL){//防止多次创建,多线程时要加这个单例保护机制,互斥量、条件变量、信号量等
          s_instance=new A(1234);
        }
        return *s_instance;
      }
      void release(void){
        delete this;
      }
    };
    A* A::s_instance=NULL;//在外部初始化单例
    
    int main(void){
      A a;//error
      A* p =new A;//error
      A& a1= A::getInstance();
      
      A& a1= A::getInstance();

      a1.release();
      a2.release();
    }

    注意:

      (1)上一段代码,单例对象拥有两个别名,实际情况下a1和a2处于不同的线程,那么在a1不用之后,a2就无法使用单例对象。所以应该在最后一个对象使用完成之后再进行释放。最简单的方式就是在接口函数中进行计数,在release进行减计数。

      (2)在release时,判断条件应为s_counter &&--s_counter ==0,防止误调用release导致s_counter为负数。

    四、成员指针

    1、成员变量指针

    (1)语法

    类型* 类名::*成员指针名=&类名::成员变量;//注意和普通指针的区别

    如:

    class Student {
    
    ...
    
    string name;
    
    }
    
    Student s;
    
    string* p=&s.name;//普通指针
    
    //定义成员变量指针
    
    string Student::*pname=&Student::name

    (2)成员变量指针使用

      1)对象.*成员指针名

    s.*pname;//.*叫做直接成员指针解引用运算符

      2)对象指针->*成员指针名

    Student* ps=&s;

    ps->*pname;//->*间接成员指针解引用运算符

    注意:

      (1)使用时需要通过对象调用

      (2).*和->*不能分开

      (3)pname的地址是什么呢?pname定义的是m_name相对于对象起始地址的相对地址,这里pname的地址是NULL;但是如果在m_name前面定义一个int类型的成员变量,那么他就是0x4。在定义对象后使用panme时,pname才指向真正的地址。

    Student s1("张三");//构造函数参数为字符串
    
    Student s2(“李四”);
    
    cout<<s1.*pname<<endl;
    
    cout<<s2->*pname<<endl;

    2、成员函数指针

    (1)语法

      普通函数指针语法 :   

    返回类型 (*函数指针)(形参表) =&函数名;//取地址可加可不加

      成员函数指针语法:

    返回类型 (类名::*函数指针)(形参表)&类名::成员函数名;//&必须加

    (2)使用方法

    (对象.*成员函数指针)(实参表);
    
    (对象->*成员函数指针)(实参表);

    注意:

      成员变量指针未实例化前保存的是相对地址,成员函数指针为实例化前保存的是虚拟的地址,是代码段绝对的地址。

    class Student {
    
    ...
    void func(void){}
    
    string name;
    
    }
    
    
    //定义成员变量指针
    void (Student::*pfunc)(void)=&Student::func;

    Student s1("张三");//构造函数参数为字符串
    
    Student s2(“李四”);
    
    (s1.*pfunc)();
    
    (s2->*pfunc)();

    五、操作符重载

    1、关于左值和右值

      左值可以被取地址,可以放置运算符左侧,可以被修改,右值不可以。

      返回基本类型的函数的返回值是右值,返回基本类型的引用是左值(函数内部返回的变量最好是全局或此处可见的,如静态变量,成员变量,全局变量等一系列的引用)。

      返回的类  类型的函数被复制会调用起拷贝赋值。

      一般的操作符返回的都是左值,但是前++,前--,+=,-=等操作符返回的是右值。后++,后--返回值是右值,如a--=30,先将a返回到临时变量,a自身再--,临时变量不能作为左值。++++a;可以,但是a----不行,因为第一个--返回了一个右值,右值不能再被第二次--。

      char ch=0;

      int& r=ch;//error,因为ch变量转化的值是临时变量,是能定义引用

      const int& r=ch;//ok,常引用称为万能引用,即可引用左值也可以引用右值

    2、操作符重载

    class Complex{
    
    public:
    
      Complex(int r,int i):m_r(r),m_i(i){}
    
     
    private:
    
      int m_r;
    
      int m_i;
    
    };
    
     
    
    //主函数中
    
    Complex c1(1,2);//如果加上const修饰,那么操作符重载函数将不能得到调用,因为成员函数有this指针,this又没有被const修饰,即传到this的实参是常对象。this定义时要求不是常对象,扩大了操作范围
    
    //所以最好在操作符重载函数中加上常函数修饰限定

    Complex c2(
    3,4); Complex c3=c1+c2;//通过操作重载可以实现自定义类型运算

    1 、双目操作符重载L#R

      L#R表达式会被编译器处理成:L.operator#(R)的成员函数形式,该函数的返回值就是表达式的值,遵循左调右参原则。

    (1)运算类双目操作符重载:+、-、*、/...

      运算类双目操作符,左右操作数可以是左值也可以是右值

      表达式的值是一个右值

    class Complex{
    
    public:
    
      Complex(int r,int i):m_r(r),m_i(i){}
      
      const Complex operator+(const Comple& c)const{//将参数改为传引用,那么c2不能为const对象,因为一般引用不能引用常对象,解决方法是将参数改为万能的常引用
        return Complex(m_r+c.m_r,m_i+c.m_i);
      }
       //三个const的作用
      //修饰返回值,使返回值为右值
      //修饰右操作数,使右操作数可以是左值也可以是右值
      //修饰左操作数,可以是左值也可以是右值
    private:   int m_r;   int m_i; }; //主函数中 Complex c1(1,2);//如果加上const修饰,那么操作符重载函数将不能得到调用,因为成员函数有this指针,this又没有被const修饰,即传到this的实参是常对象。this定义时要求不是常对象,扩大了操作范围 //所以最好在操作符重载函数中加上常函数修饰限定 Complex c2(3,4);//如果将参数改为传引用,那么c2不能为const对象,因为一般引用不能引用常对象,解决方法是将参数改为万能的常引用

    Complex c3
    =c1+c2;//通过操作重载可以实现自定义类型运算

    (c1+c2)=c3;//前面说自定义类型的这种写法会调用拷贝赋值,因为operator=可以由常对象来调用,这不符合我们基本类型的运算逻辑,解决方法是在返回前面加const
  • 相关阅读:
    浅拷贝和深拷贝问题
    指针遍历数组时用法
    一维数组和指针
    leetcode
    tmux
    git
    einsum详解
    spark快速大数据分析 读书笔记
    maven配置
    bash 学习笔记
  • 原文地址:https://www.cnblogs.com/ptfe/p/11257015.html
Copyright © 2011-2022 走看看