于直接给对象赋值:
之前学过,如何给对象在初始化时进行赋值。
对于C++11来说,初始化方式有三种:
① man c = man{ "cc",1 };
② man d = { "dd",1 };
③ man f{ "ff",1 };
假如有一类M,他有两个私有成员a和b(int类型)。
于是新建一对象M q; 对象q使用默认构造函数(假如都赋值为0,这个不重要);
现在,我们想给对象q的第一个私有成员赋值,该怎么办?
这章刚学过运算符重载,难道要写个运算符重载么?隐式调用类对象,参数为赋值符号=后面的值?(我们知道,赋值符号是可以被成员函数重载的)
这显然太麻烦。
如果可以这样q=3,于是q的第一个成员被赋值3就十分方便了。
这是可行的,不是通过运算符重载,而是通过构造函数,构造函数设置一个参数时,使用q=3,则默认3为第一个参数,假设构造函数这么写。
M(int c = 1) { a = c; b = 1.1; };
那么使用q=3时,a将等于3,b将等于1.1(此刻相当于同时给2个私有成员赋值)。
如果我们只想给a赋值,不给b赋值,那么删掉b=1.1也是可以的(但这是默认构造函数,如果删除的话,创建新对象时,调用这个默认构造函数时则无法给b进行初始化)。
那么如果想利用赋值符号,同时给a和b赋值呢,且b使用我们给他的值,而不是上面的默认值1.1。办法是,继续使用构造函数,且这个构造函数有两个参数,赋值时使用列表化进行赋值(大括号)。
如:
M(int c, double d) { a = c;b = d; }
注意:这里不能给默认参数,否则在使用q=3这样时,会有二义性(即能匹配第一个默认构造函数,也能匹配这个构造函数)。
然后使用:q={5,4.4}; 这样的形式,可以同时给a和b赋值。
此时调用了有两个参数的构造函数。另外,需要注意的是,不能省略掉大括号,否则,会导致调用了默认构造函数。
如代码:
#include<iostream> class M { int a; double b; public: M(int c = 1) { a = c; b = 1.1; } //带一个默认参数,默认构造函数 M(int c, double d) { a = c;b = d; } //两个参数,无默认参数,以避免二义性,构造函数 void show() { std::cout << a << ", " << b << std::endl; }; //显示a和b的值 }; int main() { using namespace std; M q; //创建对象q(调用默认构造函数) q.show(); //输出 q = 5; //使用赋值符号,(此刻调用默认构造函数,但使用给的值作为参数) q.show(); q = { 5,4.4 }; //调用有两个参数的构造函数 q.show(); q = 9,3.3; //虽然给了两个参数,但是只会调用默认构造函数 q.show(); system("Pause"); return 0; }
显示:
1, 1.1 5, 1.1 5, 4.4 9, 1.1 请按任意键继续. . .
以上,将某个值直接赋给类对象,其原理是:使用类的构造函数,创造一个临时的对象,然后,将值赋给临时对象,再采用逐成员赋值的方式,将该临时对象的内容复制到目标对象之中。
这一过程,被称为 隐式转换 ,因为他是自动进行的,而不需要显式强制类型转换。
这种转换,称为是 类型转换。
只有接受一个参数的构造函数,才能作为转换函数。有两个参数的构造函数,是不能用来转换类型的(就像上面说的,需要用大括号把两个参数都括起来)。但如果给第二个参数提供默认值,便又可以用于转换类型了(但前提是不会因此导致 二义性)。
将构造函数用于自动类型转换,在某些时候比较方便,但这种特性并非总是合乎需要的(比如说我们某个时候不需要他这么做,暂时举不出例子),因为这可能导致意外的类型转换(比如说只想给一个值赋值,但因为构造函数,给另外一个值赋了默认值)。
因此,C++新增了关键字 explicit,用于关闭这种隐式转换(但仍允许显式转换),即 显式强制类型转换:
例如,double a=int (4.1); 便是一种 显式强制类型转换。double类型的4.1被强制转换为int类型(此时值为4),又被赋值给double类型,于是a=4。
构造函数改为:explicit M(int c = 1) { a = c; b = 1.1; }
此时,M q是可行的,
q=5是不可行的,
但是q = M(5);又是可行的。
另外,需要提一下的是,之前q = 9,3.3;只能导致9被赋值(调用了默认构造函数),但是修改后为 q = M(9,3.3); 则调用的是另一个构造函数了。
假如不使用explicit给构造函数(一个参数的,例如M(int c = 1)),那么,编译器将在什么启动隐式转换(使用构造函数,然后进行赋值)呢?
①声明一个类对象,并进行初始化。例如:M q(1);
②将值赋给对象时,例如:q=5;
③将值传递给接受类对象参数的函数时。例如:void abc(M t = 3); 这样。3相当于是函数的默认参数。
④返回值是类对象时,函数试图返回值时。就比如:
M abc()
{
return 3;
}
这个时候,相当于返回一个临时类对象,然后这个对象被初始化赋值为3,就像M q(3); 然后return q一样。只不过这里不是返回q,是返回一个临时的类对象
⑤在上述任意一种情况下,使用可转换为 可直接赋值给类对象的值的类型的 内置类型。
假如构造函数M(int c = 1) ,这个时候,需要用int类型的值给其赋值,例如q=3; 而我们知道,double类型可以转换为int类型,因此,我们也可以输入q=3.3; 这时,3.3被转换为int类型的3,然后int类型的3被隐式转换,赋值给类对象。这种情况也是允许的。
另外,以上隐式转换发生的前提是,函数不存在二义性,否则会提示错误。
转换函数:
隐式转换和显示转换,可以把一个比如说int类型的值,赋给类对象。
那么类对象是否能赋值给一个int类型的变量呢,也是可以的。
要进行这种转换,必须使用特殊的C++运算符函数—— 转换函数。
转换函数是 用户定义(不定义是不能用的)的强制类型转换,可以像使用强制类型那样使用他们。
转换函数的创建方法:
格式:operator 类型名();
关键点:
①转换函数必须是类方法(成员函数);
②转换函数不能指定返回类型(靠类型名);
③转换函数不能有参数(靠私有成员的值)。
例如,转换为int类型,其函数原型应该这么写:operator int();
函数定义如下写:
operator int()
{
return a;
}
这样,将返回私有成员a的值。
注意,假如有operator double(); 那么返回int值时调用第一个,返回double值时调用第二个。返回其他类型,会提示冲突。
如代码:
#include<iostream> class M { int a; double b; public: M(int c = 1) { a = c; b = 4.1; } //带一个默认参数,默认构造函数 void show() { std::cout << a << ", " << b << std::endl; }; //显示a和b的值 operator int() { return a; } //转换函数返回int值时调用这个 operator double() { return b; } //转换函数返回double值时调用这个 }; int main() { using namespace std; M q; //创建对象q(调用默认构造函数) q.show(); //输出 int a = q; //返回int值 double b = q; //返回double值 double c = int(q); //强制类型转换为int,因此返回int值 cout << "a=" << a << ",b=" << b << ",c=" << c << endl; system("Pause"); return 0; }
显示:
1, 4.1 a=1,b=4.1,c=1 请按任意键继续. . .
自动应用类型转换:
假如,一个类只有一个转换函数,例如强制转换为int。
那么调用这个对象时,显示的便是其转换函数返回的值。
例如假如上面那个类方法,只有operator int()这个转换函数。那么当我们使用cout<<q<<endl; 时,
这个时候,程序便自动应用了类型转换,显示的是“1”。
但假如同时存在两个转换函数,再这样做的话,会提示二义性,因为程序并不知道你想要显示的是int类型的值还是double类型的值,除非你使用了强制类型转换。
注意:应谨慎的使用隐式转换函数。通常,最好选择仅在被显式的调用时才会被执行的函数。
例如:用一个返回值是你想要转换函数返回的值的函数,例如,使用operator()返回私有成员a,那么,用int M_to_int(){ return a;} 这样的函数,来返回私有成员a,调用时,则使用q.M_to_int();
总之:C++为类提供了下面的类型转换:
①利用构造函数,把基本类型转换为类 类型。
②利用转换函数,把类 类型转换为基本类型。
友元函数(运算符重载)和转换函数:
有类M,有类对象成员a,有int变量b。
有赋值,M c=a+b;
(1)假如有友元函数(或者运算符重载)的情况下:a+b将调用运算符重载函数,将函数返回值赋给类对象c。
(2)假如有转换函数,且类M的构造函数接受一个int参数。那么a将被转化为int值(根据转换函数),a+b的和(int值)作为参数,赋给类对象c。
假如有转换函数,但类M的构造函数不接受一个int参数(例如有两个参数且无默认参数),那么则提示出错,无法编译。
(3)假如既有运算符重载(“+”运算符),又有转换函数。
那么,根据实际测试来看,优先调用运算符重载函数或友元函数(假如他能完全匹配的话);
如果不能完全匹配,那么就可能导致二义性错误。
如代码:
#include<iostream> class M { int a; double b; public: M(int c=3) { a = c; b = 4.4; } //带一个默认参数,默认构造函数 void show() { std::cout << a << ", " << b << std::endl; }; //显示a和b的值 operator int() { return a; } //转换函数返回int值时调用这个 friend int operator+(int c, M & q); int operator+(int c) { return b + c; } }; int main() { using namespace std; M q; //创建对象q(调用默认构造函数) M c = 3 + q; M d = q + 3; c.show(); d.show(); system("Pause"); return 0; } int operator+(int c, M & q) { return c + q.b; }
显示:
7, 4.4 7, 4.4 请按任意键继续. . .
之所以结果为7,是因为优先使用了运算符重载函数(友元函数和成员函数),其用成员函数b和参数c相加,结果为7(也就是第一个数字)。
而若使用转换函数的话,其结果应为6,因为转换函数的返回值是3,3+3,其第一个数字应为6。——但具体为什么,推测是函数列表匹配度问题
①假如把3+q或者q+3,改为3.3+q和q+3.3,那么函数则会提示错误。原因在于,友元函数和成员函数不能完全匹配了,在匹配列表里优先级降低,成为了标准匹配(第三优先级)。而转换函数的返回值也是int类型,需要进行转换,因此也成为了标准转换(优先级也是第三级),因为匹配优先级相同,因此提示错误。
②又假如把q+3改为int(q)+3,那么首先对对象q执行强制类型转换,其为int值3(因为转换函数返回值为3),因此3+3=6,启用构造函数,于是第一个值便变成了6.
由于匹配优先级问题,如果想让q+3结果变为6,那么则在运算符重载函数修改为:int operator+(M&c) { return b + c.b; } 这时,转换函数的优先级则更高。
假如有:
#include<iostream> class M { int a; double b; public: M(int c = 2) { a = c; b = 4.4; } //带一个默认参数,默认构造函数 void show() { std::cout << a << ", " << b << std::endl; }; //显示a和b的值 operator int() { return a; } //转换函数返回int friend M operator+(M&a,M&c) { return a.b + c.b; } }; int main() { using namespace std; M q; //创建对象q(调用默认构造函数) M d = q + 3; d.show(); system("Pause"); return 0; }
则d的输出结果为5,而不是8。使用的是转换函数,而不是运算符重载函数。原因我推测为:q+3,通过调用转换函数,被转换为2+3,然后将5赋值给了对象d。
而若使用运算符重载函数,则可能成为了用户定义的类型转换了。因此优先级不如转换函数。即使把q+3改为q+M(3)这种,将3强制转换为类M的临时对象,其优先级也没有转换函数高。
除非改为:M q;M w=3; M d=q+w; 这种形式,才会使用运算符重载函数。
总之,这种容易引起意料之外结果的,我觉得还是转换函数和运算符重载函数,二者取其一吧。或者放弃转换函数,而是改使用显式的转换函数(加关键字explicit)。