1. 引言
不同的数据在计算机内存中的存储方式不同,导致了“类型”这一抽象概念的产生。
对于一个变量,需要回答三个问题:
- 变量的起点(内存地址)
- 变量的内存长度(类型决定)
- 变量的解析方式(类型决定)
一般情况下不同类型的两个变量无法直接进行运算。这样就需要类型转换。存在两个概念:隐式类型转换和显式类型转换。
- 隐式类型转换:不通过专门的类型转换操作,而是通过其他规定或代码上下文隐式发生的类型转换
- 显式类型转换:通过专门的类型转换操作进行,具有强制性,不会受任何类型转换以外的因素影响,又被称为强制类型转换
2. 类型提升和算术类型转换
- 对于同类算术类型,如
short
与int
、float
与double
,占用内存小的类型转换为另一类型。这称为类型提升 - 整型转换为浮点型
- 仅当无符号类型占用的内存小于有符号类型时,无符号类型才提升为有符号类型,否则,有符号类型转换为无符号类型
int main() {
unsigned short a = 1;
unsigned b = 1;
cout << ( a > -1) << " " << (b > -1) << endl;
}
//将输出 1 0
3. 转换构造函数
- 构造函数只有一个形参
- 构造函数不止一个形参,但只有第一个形参无默认值
- 构造函数不止一个形参,但都有默认值
- 第一形参的类型不是类本身(此时为拷贝构造函数)或其附加类型(此时为移动构造函数)
若要避免隐式类型转换,可以利用 explicit
关键字。
4. 类型转换运算符
定义了与转换构造函数相反的操作,从类类型转换为其他类型。在 operator
之后跟着转换的目标类型。
struct A {
operator int() const {
return 0;
}
};
void test(int) {}
int main() {
test(A());
}
若要避免隐式类型转换,可以利用 explicit
关键字。
对于类型转换运算符与 explicit
有一个例外:operator bool()
在条件表达式或逻辑表达式中发生的隐式类型转换不受 explicit
的影响。
5. 继承类到基类的类型转换
继承类到基类的类型转换,称为向上类型转换。
struct A {};
struct B: A {};
int main() {
A a1 = B(); //值向上转换,切除继承类部分,实际调用拷贝复制构造函数
A *a1 = new B; //指针向上转换
A &a3 = a1; //左值引用向上转换
A &&a4 = B(); //右值引用向上转换
}
5.1 静态类型和动态类型
- 静态类型:变量声明的类型
- 动态类型:变量实际存储的数据的类型
绝大多数情况下,静态类型与动态类型都是必须一致的,否则将发生隐式类型转换或引发编译错误。当且仅当使用基类的指针或引用存储继承类对象时,变量的静态类型与动态类型将不一致。此时,虽然看上去发生了向上类型转换,实际上并未发生,称为动态绑定。
一个变量的静态类型,决定了由此变量能够访问到的成员名称。当静态类型时基类指针或引用时,即使变量存放的是继承类对象,也只能访问到基类中声明的数据成员。
即:如果发生向上类型转换的类型是类的指针或引用,将以丢失继承类部分的成员名称为代价进行向上类型转换。
但是由于虚函数的存在,访问成员名称所得到的实际成员函数将不一定与静态类型保持一致,此性质是 C++ 多态 的核心。
5.2 阻止向上类型转换
为什么继承类可以访问基类的成员?
不难发现,“继承类可以访问基类成员”这一性质并不是天经地义的,因为继承类中并没有“复制粘贴”一个基类,而只有继承类本身的部分,故原则上继承类虽然继承了基类,但其本身仍然是没有能力访问基类的成员的。
继承类对象之所以能够访问基类成员,是因为在进行这样的访问时,继承类的 this
指针通过向上类型转换操作转换成了一个基类类型的指针,然后以基类指针的身份访问到了基类的成员。
如果希望阻止这种隐式的向上类型转换呢?
让我们认真考察 public
、protected
与 private
这三个关键字。
public
:当用于访问说明符时,表示对类的一切用户可见;用于继承时,表示继承时不修改基类的一切访问说明符protected
:当用于访问说明符时,表示仅对类的继承用户可见,对类的实例用户不可见;用于继承时,表示将基类的一切public
访问说明符在继承类中修改为protected
private
:当用于访问说明符时,表示对一切类的用户均不可见;用于继承时,表示将基类的一切public
和protected
访问说明符在继承类中修改为private
如果我们从向上类型转换这一角度思考,就能得出答案:
public
:不阻止任何用户进行向上类型转换protected
:阻止类的实例用户进行向上类型转换private
:阻止一切用户进行向上类型转换
struct A {};
struct B: A {}; // 不阻止任何B类的用户向A进行类型转换
struct C: protected A {}; // 阻止C类的实例用户向A进行类型转换
struct D: private A {}; // 阻止D类的一切用户向A进行类型转换
struct E: B { void test() { static_cast<A *>(this); } }; // B类的继承类用户可以向A进行类型转换
struct F: C { void test() { static_cast<A *>(this); } }; // C类的继承类用户可以向A进行类型转换
struct E: D { void test() { static_cast<A *>(this); } }; // Error!D类的继承类用户不可以向A进行类型转换
int main()
{
static_cast<A *>(new B); // B类的实例用户可以向A进行类型转换
static_cast<A *>(new C); // Error!C类的实例用户不可以向A进行类型转换
static_cast<A *>(new D); // Error!D类的实例用户不可以向A进行类型转换
}
由此可见,public
继承将不阻止类的任何用户进行向上类型转换,而 private
继承将阻止类的一切用户进行向上类型转换,protected
继承只阻止类的实例用户进行向上类型转换,但不阻止类的继承类用户进行向上类型转换。
5.3 多重继承与向上类型转换
对于多重继承,其向上类型转换对于同一继承层的多个基类是全面进行的。
参考以下代码:
struct A { int i; };
struct B { int i; };
struct C: A, B { int i; };
struct D: A, B {};
int main()
{
C().i; // 访问C::i
D().i; // Error!存在二义性!
}
6. 其它隐式类型转换
- 0 转换为空指针
- 数组名退化为指针
- 空指针或数字 0 转为
false
,其它指针或数字转为true
- T 转换为
void
- 非
const
转换为const