C++学习之类的自动转换和强制类型转换

类型转换:将一个标准类型变量的值赋给另一种标准类型变量时,如果两种类型兼容,则C++会自动将这个值转换为接收变量的类型。例如:

1
2
3
long count = 8;
double time = 11;
int side = 3.33;

上述语句都是可行的,C++包含进行转换的内置规则。这些转换将降低精度。例如3.33赋给int型变量将保留3,丢失0.33。

不兼容的类型如下:

1
int *p = 10;

虽然计算机内部可能有某个整数代表某个地址,但从概念上来说地址和整数完全不同。所以,这是无法自动转换的。当无法自动转换时,可以使用强制类型转换:
1
int *p = (int *)10;

将指针设置成地址10,可以做是可以这么做,当然有没有意义是另一回事。

对于类来说,可以将类定义成与基本类型、或者另一个类相关,使得从基本类型或者别的类转换成另一种类型是有意义的。即在某些情况下,程序员可以指示C++如何进行自动转换,或通过强制类型转换来完成。

通过转换函数来实现自动类型转换,显式、隐式转换,强制转换。

下面看一个类,是英式重量单位:磅与英石的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//stonewt.h -- (英式)重量的单位:磅与英石

class Stonewt
{
private:
enum { Lbs_per_stn = 14 }; //每英石是14磅
int stone; //重量,单位英石,整数
double pds_left; //剩余的重量,单位磅
double pounds; //总重,单位磅
public:
Stonewt(double lbs);
Stonewt(int stn, double lbs);
Stonewt();
~Stonewt();
void show_lbs()const;
void show_stn()const;
};

#include<iostream>
using std::cout;

Stonewt::Stonewt(double lbs)//构造函数,参数为磅
{
stone = int(lbs) / Lbs_per_stn;
pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs);
pounds = lbs;
}

Stonewt::Stonewt(int stn, double lbs)//构造函数,参数为英石和磅
{
stone = stn;
pds_left = lbs;
pounds = stn*Lbs_per_stn + lbs;
}

Stonewt::Stonewt()//默认构造函数
{
stone = pounds = pds_left = 0;
}

Stonewt::~Stonewt()//析构函数
{

}

void Stonewt::show_lbs()const
{
cout << stone << "stone, " << pds_left << " pound(s)\n";
}

void Stonewt::show_stn()const
{
cout << pounds << " pound(s)\n";
}

如何实现类的自动类型转换呢?提供一个将整型数、浮点数转换为Stonewt对象的方法就可以了。下面的构造函数就把一个double型的值转换成为了Stonewt类型。

1
Stonewt::Stonewt(double lbs)//构造函数,参数为磅

也就是说可以编写如下代码:

1
2
Stonewt mycat;
mycat = 19.6;

这一过程叫隐式转换,因为它自动创建一个临时对象,把19.6作为初始化值,然后逐个把临时对象的内容复制到mycat里。

只有接受一个参数的构造函数才能作为转换函数。下面的构造函数有两个参数,因此不能作为类型转换函数:

1
Stonewt::Stonewt(int stn, double lbs)//构造函数,参数为英石和磅

但是如果在函数声明中给第二个参数提供默认值,它就可以用于转换int。
1
Stonewt(int stn, double lbs = 0);

显式转换和隐式转换的差别

将构造函数用作自动转换类型函数似乎是一项不错的特性,然而,当程序员拥有更丰富的C++经验时,将发现这种自发转换类型并非总是需要的,有可能会导致意外的类型转换。因此C++提供了关键字explicit,用于关闭这种自动特性。也就是说,如果这样声明函数:

1
explicit Stonewt(double lbs);    //不允许隐式转换(自动转换类型)

就不会允许隐式转换了,但任然允许显式转换,显式转换即强制类型转换:
1
2
3
4
Stonewt mycat;
mycat = 19.6; //不允许,必须显式转换
mycat = Stonewt(19.6); //允许
mycat = (Stonewt)19.6; //允许

到底哪些算隐式转换?

• 将Stonewt对象初始化为double值。 例子:Stonewt mycat=19.6;
• 将double值赋给Stonewt对象。例子:mycat=19.6;
• 将double值传递给接受Stonewt对象作为参数的函数。
• 返回值被声明为Stonewt对象的函数试图返回double值时。

注意防止二义性

例如有这样的语句:Stonewt mydog(50); 因为50是int值,所以会被自动转换为double值再使用Stonewt(double lbs)构造函数。当且仅当不存在二义性错误时才会进行这种二步转换。也就是说,如果这个类还定义了Stonewt(long lbs);这种函数,则编译器会报错。(int可能被转换成double或long都可以)

谨慎使用隐式转换函数

例如有如下写错的代码:

1
2
3
4
5
6
7
8
int array[20];
...
...
Stonewt temp(14,4);
...
int Temp = 0;
...
cout<<array[temp];/*t写错成小写,本应用Temp作为数组下标*/

如果使用的是允许隐式转换的转换函数,这里坏处就体现了:在debug时编译器并不能捕捉使用了对象作为数组下标,因为定义了operator int(),所以对象可以变成int值并用做数组下标。

所以说,原则上,最好使用显式转换,避免隐式转换。

  • 方法1:在原型前使用explicit限定符。
  • 方法2:用一个功能相同的非转换函数替代转换函数,因为不是转换函数,所以必须显式调用。
1
int Stonewt::Stone_to_int(){ return int(pounds+0.5); }

总之:应谨慎地使用隐式转换函数。通常,最好选择仅在被显式调用才会执行的函数。

通过转换函数,将对象转换成数字

上述的工作是将各种类型的数字转换成Stonewt对象,那么能做相反的操作嘛?例如可否将一个Stonewt对象转换成double值?

答案是:可以这样做,但不是使用构造函数。构造函数能适用于从某种类型到类类型的转换。要进行相反的操作,需要用到C++中特殊的运算符函数——转换函数

转换函数是用户定义的强制类型转换,定义了转换函数之后,可以像使用强制类型转换那样去转换类型。编译器在发现由类转到基本类型的语句后,会寻找是否有定义与此匹配的转换函数,否则会报错。

定义转换函数的方法:

1
operator typeName();

有以下几个注意点
• 转换函数必须是类方法。
• 不能指定返回类型。
• 不能有参数。

所以,某类类型转换为double型的函数原型如下:

1
operator double();

在Stonewt类中,这样编写转换函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//转换函数
operator int()const;
operator double()const;

//方法定义
Stonewt::operator int()const
{
return int(pounds + 0.5);//四舍五入
}

Stonewt::operator double()const
{
return pounds;
}

还是要注意防范二义性错误。下面举一个例子。

在C++中,int值和double值都可以被赋给long变量,如果一个类有转换成int类和double类的函数,但出现了如下语句:(b是一个类对象)

1
long a = b;

因为编译器使用任何一个转换函数都是合法的,编译器不想承担选择转换函数的责任,所以会报错。(这就是二义性错误)但是删掉其中一条之后编译可以通过。如果删掉了double定义,则编译器使用int定义,先把b对象转换成int值,再把int值赋给long变量。

当定义了很多的转换函数时,仍可以使用显式强制类型转换来指明到底使用哪个转换函数,下列的语句就是可行的:

1
2
long a = (double)b;
long a = int (b);

总结

  1. 友元函数避开了通过使用类方法才能访问类成员的限制。
  2. 运算符重载可以是类成员函数,也可以是友元函数。
  3. 之所以要使用友元函数来重载运算符是因为这个运算符有两个操作数,而且为了使第一个操作数不是类对象。
  4. 注意不要出现二义性错误。例如重载运算符加号使两个同类对象相加,却同时使用了非友元和友元的方法(例如对于Time类的两个对象a和b来说,无论a+b或者b+a都是一样的,定义一个即可)。
  5. 重载<<运算符特别需要知道声明返回类型为std::ostream &,以及一条return os;
  6. C++允许指定类和基本类型之间进行互相转换的方式。首先,任何接受唯一参数的构造函数都可以作为转换函数,将参数类型转换成类对象。例如假设有一个String类,它包含一个将char *值作为其唯一参数的构造函数,且a是一个String对象,则可以使用像a = "abc";这样的语句了。如果在函数声明前加上explicit 将限制隐式转换,则只能这样显式地转换:a = String("abc");
  7. 将类转换成别的类型的函数原型是:operator typeName();经验表明,最好不要依赖隐式转换函数。多多注意防止出现二义性的转换函数。注意过多转换函数很容易导致在隐式转换时出现二义性。
  8. 与简单的C风格编程结构相比,使用类时必须更加小心谨慎,但作为补偿,它能为我们做的工作也更多。