深入运算符重载(2)
目录
1、运算符重载时,什么时候以引用返回,什么时候以值返回?
以值返回:
Int operator+(const Int& it)
{
this->value += it.value;
return Int(this->value + it.value);
}
以引用返回:
Int &operator+=(const Int &it)
{
this->value += it.value;
return *this;
}
a += b;
总结:凡是运算符返回自身时候,以引用返回。但是以将亡值返回的时候,需要使用值返回。
2、构造函数的三个作用
class Int
{
private:
int value;
public:
//1 2 3
Int(int x = 0):value(x)
{
cout << "Int()" << this << endl;
}
Int(const Int &it):value(it.value)
{
cout << "Copy Int:" << this << endl;
}
Int &operator=(const Int &it)
{
if(this != &it)
{
value = it.value;
}
cout << this << " = " << &it << endl;
return *this;
}
}
int main()
{
Int a = 10; //自定义类型
//Int a(10);
int b = 100; //内置类型
a = b;
}
构造函数的三个作用:
- 构建对象
- 初始化对象
- 类型转换(仅限单参数的构造函数)
3、什么是类型转换
我们看下面这个例子
问题:如上main函数所示,能否将b赋值给a?
回答:构造函数的第三个功能,类型转化
首先我们观察,一个是int 类型,一个是自定义的Int类型。如果将b给a赋值,显然是不能通过的,但是编译器并没有报错,这是怎么一回事?
我们将b进行转化,转化为自定义类型。然后将转化后的类型给a赋值。
通过调用我们可以发现:
我们创建出一个临时对象地址为012FF690,然后将临时对象赋值给a对象。
再来看一下析构过程:
~Int()
{
cout << "Destroy Int: " << this << endl;
}
可以发现临时对象在赋值完毕后就会被销毁。
3.1 此处可以类型转换的构造函数必须是单参数的。
//构造函数
Int(int x,int y):value()
{
cout << "Int()" << this << endl;
}
int main()
{
Int a(10,20);
int b = 100;
a = b; //error:必须只含有一个参数
}
如果给参数初始值的化也可以以当作是一个参数:
Int(int x,int y = 0):value()
{
cout << "Int()" << this << endl;
}
int main()
{
Int a(10);
int b = 100;
a = b; //right :赋了默认值
}
问题:a的值是多少呢?
Int(int x,int y = 0):value(x + y)
{
cout << "Int()" << this << endl;
}
int main()
{
Int a(1,2);
int b = 100;
//a = b;
//a = b,20; //逗号运算符,按最右的为准。
//a = (Int)(b,20); //也是逗号表达式,取20,也就是(Int) 20;
//a = Int(b,20); //调用两个参数的构造函数,创建一个无名对象,将20给y,b给x,结果是120;
}
最后两者都会产生临时对象,第一种按照类型转换方式来调用构造函数,第二种产生一个无名对象,接收两个参数。
接上面的问题,那如果给两个参数,还不给默认初始化呢?
Int(int x,int y):value(x + y)
{
cout << "Int()" << this << endl;
}
int main()
{
Int a(10,20);
int b = 100;
a = (Int)(b,100); //error:强转形式创建无名对象
a = Int(b,100); //right:调动构造函数创建无名对象
}
对于类型强转类型的对象,我们只能给一个有效参数,给多了error
3.2 如果不允许隐式进行构造函数:构造函数前添加explicit关键字。
...
explicit Int(int x = 0):value(x)
{
cout << "Int()" << this << endl;
}
...
int main()
{
Int a(10);
int b = 100;
a = b; //error,不能隐式的类型转换
a = (Int) (200); //right,强制转换后可以
}
3.3 我们能否将变量给对象赋值呢?
explicit Int(int x):value(x)
{
cout << "Create Int:" << this << endl;
}
int main()
{
Int a(10);
int b = 100;
a = (Int)b;
b = a; //error
b = (int)a; //尝试类型转换:error
}
不可以这样操作,我们没有强制转换类型的缺省函数。我们需要自己写。返回类型是强转的类型(系统给出)
operator int()const
{
return value;
//返回类型是强转的类型
}
int operator int()const
{
return value;
}
//如果是这种情况则刚好:
// b = (int)a;
//如果是这种情况:b = (float)a;
//不可能再写一个float类型的重载函数
//上述代码的main函数
int main()
{
Int a(10);
int b = 100;
a = (Int)b;
b = a;
//调用强转函数
//相当于:b = a.operator int();
//相当于:b = operator int(&a);
b = (int)a;
}
总结:
- 当需要将内置类型赋值给对象的时候,我们可以通过强制转换,通过单参构造函数来构建无名对象;
- 当需要将对象赋值给内置类型的时候,可以通过运算符重载函数来实现。
3.4 mutable关键字和仿函数
有如下类:
class Add
{
mutable int value;
//即使
public:
Add(int x = 0) : value(x) {
}
int operator()(int a,int b) const
//添加const value将无法被修改,可以给value添加 mutable 关键字
{
value = a + b;
return value;
}
};
如下main函数:
int main()
{
int a = 10,int b = 20, c = 0;
Add add;
c = add(a,b); //仿函数
}
仿函数概念:仿函数即仿照的函数,表示它功能与函数类似但并不是真正的函数,仿函数又叫函数对象。在C++中它通过重载函数调用运算符即 ()运算符。
问题:赋值那句add对象是如何调用构造函数的?
add不会调用构造函数,会调用仿函数
c = add(a,b); //我们重载了括号运算符
//相当于:
c = add.operator()(a,b);
更复杂的情况:
int main()
{
int a = 10, b = 20, c = 0;
c = Add()(a,b);
}
类型名加 () 调用构造函数,产生一个将亡值对象,然后调用自己的
() 运算符重载,将a和b相加,最后赋值给c。
总结:
- 被mutable修饰的成员变量,即使在成员方法中为this指针添加const 修饰也可以被修改。
- 凡是重载了括号的运算符,我们就叫做仿函数。当碰见对象( )的时候要注意,可能是对象构造,可能是调用仿函数。
4、构造函数使用初始化列表和赋值的区别
思考下面代码的输出方式:
#include<iostream>
using namespace std;
class Int
{
private:
int value;
public:
Int(int x = 0):value(x)
{
cout << "Int()" << this << endl;
}
Int(const Int &it):value(it.value)
{
cout << "Copy Int:" << this << endl;
}
Int &operator=(const Int &it)
{
if(this != &it)
{
value = it.value;
}
cout << this << " = " << &it << endl;
return *this;
}
~Int()
{
cout << "Destory Int:" << this << endl;
}
};
class Object
{
int num;
Int val; //Int类型对象
public:
Object(int x,int y)
{
num = x;
val = y;
cout << "create Objcet:" << this << endl;
}
~Object()
{
cout << "destroy Object:"<< this << endl;
}
};
int main()
{
Object obj(1,2);
}
过程分析:
输出结果:
初始化列表和在构造函数里面赋值等同的效果(仅限内置类型)
Object(int x,int y):num(x) //等价赋值语句
{
cout << "Create Object:"<< this << endl;
//num = x; //等价于初始化列表
val = y;
}
但是自定义类型就不一样,不需要构造临时对象
Object(int x,int y):num(x),val(y)
{
cout << "Create Object:"<< this << endl;
//num = x; //等价于初始化列表
//val = y;
}
总结:对于内置类型,效率相同;对于自定义类型的初始化,尽量使用初始化列表的方式进行,节省空间。
5、类数据成员的构造顺序
按照设计顺序进行构建
#include<iostream>
using namespace std;
class Object
{
int num;
int sum; //与定义数据相关
public:
Object(int x = 0):sum(x),num(sum)
{
}
void Print()const
{
cout << num << " " << sum << endl;
}
};
C++中类数据成员的构造与初始化列表中成员的顺序无关,与类数据成员再类中定义的数据有关。
过程分析:
- 首先构造的是num,其次是sum。根据编译器会将所有的数据成员先初始化为0,再赋值。
- num先被构造,将sum的值赋给num。但是此时sum未被构造,是0或是随机数,将这个值赋给了num。
- 后来将x的值赋给sum。
6、对象生存期管理方式
class Object
{
Int *ip;
public:
Object(Int *s = NULL):ip(s) {
}
~Object()
{
if(ip != NULL)
{
delete ip;
}
ip = NULL;
}
};
int main()
{
Object obj(new Int(10));
//new 的三个作用
//1、从堆区申请空间
//2、调用构造函数,构造对象
//3、返回对象的地址
}
过程分析:
- new有三个作用,首先会从堆区开辟一块空间,接着调用构造函数来构建对象,最后会返回对象的地址。将10存放到开辟的空间中,指针s来指向这个空间,在构造函数中还会将s来赋值给ip。
- 对象结束后,如果ip为空,则删除ip。将ip赋值为空。
这样做有什么好处呢?
可以将 Int 对象的生存期进行自动管理,我们将上下两个代码进行对比。
手动管理:
int fun()
{
Int *ip = new Int(10);
...
delete ip; //如果没有这一步,会发生内存泄漏,由我们手动进行
}
int main()
{
fun();
}
自动管理:
int fun()
{
Object obj(new Int(10));
//在堆区申请的空间,如果fun函数结束,obj局部对象会被销毁,会调用它的析构函数。而它的析构函数中有delete,由系统自动进行
}
int main()
{
fun();
}
7、*运算符和->运算符重载后返回对象的区别
分析下面代码:
class Int
{
private:
int value;
public:
Int(int x = 0):value(x)
{
cout << "Int()" << this << endl;
}
Int(const Int &it):value(it.value)
{
cout << "Copy Int:" << this << endl;
}
Int &operator=(const Int &it)
{
if(this != &it)
{
value = it.value;
}
cout << this << " = " << &it << endl;
return *this;
}
~Int()
{
cout << "Destory Int:" << this << endl;
}
int &Value()
{
return value;
}
const int &Value()const
{
return value;
}
};
class Object
{
Int *ip;
public:
Object(Int *s = NULL):ip(s) {
}
~Object()
{
if(ip != NULL)
{
delete ip;
}
ip = NULL;
}
Int &operator*()
{
return *ip;
}
const Int& operator*() const
{
return *ip;
}
Int *operator->()
{
return ip;
}
const Int* operator->() const
{
return ip;
}
};
int main()
{
Object obj(new Int(10));
Int *ip = new Int(10);
(*ip).Value();
(*obj).Value();
obj->Value();
ip->Value();
}
重载*返回的是对象本身,重载指向符返回的是对象的指向。
为什么呢,我们看一下组合的概念:
组合:
- 值类型
- 指针类型
- 引用类型(最复杂)
第一种组合最常见,val 对象是整个Object 类的一部分。
第二种指针类型的组合,ip并不是Object类的一部分,是一种弱关联,指针指向的对象可以被改变。
如果使用对ip进行访问,返回对象本身。如果通过*->进行访问,得到的是堆区的地址**。
第三中组合通过引用来修饰类内对象,是强关联,修改val对象会影响到。
明白了这个我们就可以将->进行进一步的修改:
Int * opearotr->() //重载->运算符
{
return &**this;
//return ip; //返回的是对象的地址
}
const Int *opearotr->()const
{
return &**this;
//return ip;
}
问题:&**this是如何理解的呢?
this指针指向当前的对象,*this的意思就是对象本身。
再来一个就是 * * this,他会调用重载函数,返回的是所指向的对象的别名(返回 * ip)。
这时加上&,与解引用相互抵消,就是ip的地址。
那么,返回最初的代码,为什么解引用加.能和->有同样的作用呢?
class Int
{
private:
int value;
public:
Int(int x = 0):value(x)
{
cout << "Int()" << this << endl;
}
Int(const Int &it):value(it.value)
{
cout << "Copy Int:" << this << endl;
}
Int &operator=(const Int &it)
{
if(this != &it)
{
value = it.value;
}
cout << this << " = " << &it << endl;
return *this;
}
~Int()
{
cout << "Destory Int:" << this << endl;
}
int &Value()
{
return value;
}
const int &Value()const
{
return value;
}
};
class Object
{
Int *ip;
public:
Object(Int *s = NULL):ip(s) {
}
~Object()
{
if(ip != NULL)
{
delete ip;
}
ip = NULL;
}
Int &operator*()
{
return *ip;
}
const Int& operator*() const
{
return *ip;
}
Int *operator->()
{
return ip;
}
const Int* operator->() const
{
return ip;
}
};
int main()
{
Object obj(new Int(10));
Int *ip = new Int(10);
(*ip).Value();
(*obj).Value();
obj->Value();
ip->Value();
}
在创建对象之前,会为obj对象和p对象分配空间。obj对象中有着指针对象,所以占4个字节(32位);p对象就是传统的指针对象,也占4个字节。
当要构造obj对象的时候,会先将他的临时对象参数先构造出来,在堆区创建一个空间来保存无名对象,值为10。接着p对象也需要创建一个无名对象,由p直接指向它,值为1。
接着p访问的就是p所致空间的内容;而 * obj就需要调用运算符的重载函数,返回的是解引用后的ip,也就是值为10的临时对象临时对象,通过Value来取到它的值。
p->Value()没什么好说的。关键在于obj->Value(),首先会调用->重载运算符。*obj的含义是得到ip,**obj 就是 * ip,得到临时对象的内容。& *ip,& 和 * 相互抵消,就是ip。ip->Value(),与最普通的p->Value()类似。
文章评论