一、拷贝赋值
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