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

猜你喜欢

转载自www.cnblogs.com/ptfe/p/11257015.html