共计 6199 个字符,预计需要花费 16 分钟才能阅读完成。
0. 引言
不同的数据在计算机内存中的存储形式不同,导致了“类型”这一抽象概念的呈现。对于一个变量而言,其必须要答复三个问题:
- 在哪能够拜访到这个变量的终点?
- 从终点向后须要读取多少内存?
- 应该如何解析读取到的二进制数据?
上述的三个问题中,问题 1,由内存地址答复,问题 2 和 3,均由类型答复。由此可见,类型与内存地址独特形成了一个变量的残缺组分。之所以不能对 void * 取值,也是因为无法回答问题 2 和 3 导致。
进一步的,咱们能够失去一条非常重要的论断:对于两个不同类型的变量,因为其对问题 2 和 3 的答案不同,故如果将这样的两个变量间接进行运算,在绝大多数状况下都将无奈产生有价值的计算结果。故在简直所有的编程语言中都有一条重要的规定:不同类型的两个变量无奈间接进行运算。
尽管不同类型的两个变量无奈进行运算,但显然,咱们可将其中的一个变量通过类型转换,转为与另一个变量类型统一,此时就满足“同类型变量能力进行运算”这一规定了。同时,因为某些类型转换是“理所应当”的,而另一些不是,故由此又派生出两个概念:隐式类型转换与显式类型转换。隐式类型转换指不通过专门的类型转换操作,而是通过其余规定或代码上下文隐式产生的类型转换,而显式类型转换则通过专门的类型转换操作进行转换,显式类型转换具备强制性,其将不受任何类型转换以外的因素影响,故显式类型转换又称为强制类型转换。
在 C ++ 中,类型转换是一个非常复杂的话题。本文将先从隐式类型转换动手,逐渐探讨各类 C ++ 的类型转换话题。
1. 类型晋升与算术类型转换
算术类型转换专指 C ++ 提供的各种内置算术类型之间的隐式类型转换。内置算术类型次要包含以下类型:
- bool
- char, signed char, unsigned char
- short, int, long, long long, unsigned short, unsigned int, unsigned long, unsigned long long
- float, double, long double
- size_t, ptrdiff_t, nullptr_t 等其余非凡类型
算术类型转换是一类不齐全明确的,且与底层密切相关的隐式类型转换。其遵循以下几条次要准则:
- 对于同类算术类型,如 short 与 int,float 与 double,占用较小内存的类型将转换成另一类型。如 short + int 将被转换为 int + int。此种类型转换称为类型晋升。
- 整形将转为浮点型。如 int + double 将被转换为 double + double。
- 仅当无符号类型占用的内存小于有符号类型时,无符号类型将产生类型晋升从而转为有符号类型,否则,有符号类型将转为无符号类型。这是一个十分须要留神的点。
参考以下代码:
int main()
{
unsigned short a = 1;
unsigned b = 1;
cout << (a > -1) << " " << (b > -1) << endl; // 1 0!
}
上述代码中,- 1 作为 int 间接量而存在,因为变量 a 是 unsigned short 类型,故其将被转为 int,值仍为 1。但因为变量 b 的类型是与 int 同级的 unsigned 类型,故此时 - 1 将被转为 unsigned 类型,这显著不是咱们须要的后果。由此可见,当有符号类型与无符号类型(如 size_t)产生混用时,肯定要小心可能会产生的隐式类型转换。
2. 转换构造函数
2.1 定义转换构造函数
C++ 中,如果一个构造函数满足以下所有条件,则其成为一个转换构造函数:
- 至少有一个不含默认值的形参。这次要包含以下几种状况:
- 构造函数只有一个形参
- 构造函数有不止一个形参,但只有第一形参无默认值
- 构造函数有不止一个形参,但全副形参均有默认值
- 第一形参的类型不为类自身或其附加类型(否则此构造函数将成为拷贝构造函数或挪动构造函数)
如果一个类定义了某种转换构造函数,则被定义的类型将能够通过任何类型转换形式转为以后类类型。这常见于以下几种状况:
- 赋值时产生的隐式类型转换
- 实参传递时产生的隐式类型转换
- 基于 static_cast 的显式类型转换
参考以下代码:
struct A {A (int) {}}; // 转换构造函数
void test(A) {}
int main()
{
A _ = 0; // 赋值时产生的隐式类型转换
test(0); // 实参传递时产生的隐式类型转换
}
上述代码中,咱们为类 A 定义了从 int 到 A 的转换构造函数。则此时,咱们即能够将一个 int 间接赋值给类型为 A 的变量,也能够间接将一个 int 作为实参传给类型为 A 的形参。这两种状况产生时,都隐式地通过转换构造函数结构了类 A 的一个实例。
2.2 阻止基于转换构造函数进行的隐式类型转换
由上文可知,当定义了一个转换构造函数后,就买通了某个其余类型向类类型进行转换的通道。此时,如果咱们心愿禁用基于转换构造函数进行的隐式类型转换,则须要在转换构造函数前追加 explicit 申明。
当一个转换构造函数被申明为 explicit 后,其具备以下性质:
- 禁止所有场合下的基于转换构造函数的隐式类型转换
- 不影响被转换类型到类类型的强制类型转换
- 不影响对转换构造函数的失常调用
参考以下代码:
struct A {explicit A (int) {}}; // explicit 转换构造函数
void test(A) {}
int main()
{
A _ = 0; // Error!禁止赋值时产生的隐式类型转换!test(0); // Error!禁止实参传递时产生的隐式类型转换!static_cast<A>(0); // explicit 不影响强制类型转换
A(0); // explicit 不影响对转换构造函数的失常调用
}
上述代码中,咱们将类 A 的转换构造函数申明为 explicit,则此时 int 将不能通过赋值或实参传递的形式隐式的转换为 A。但显然,explicit 只是禁用了转换构造函数的隐式类型转换性能,其构造函数性能以及显式类型转换性能并不受影响。
3. 类型转换运算符
转换构造函数定义了其余类型向类类型的转换计划,类型转换运算符则定义了与之相同的过程:其用于定义类类型向其余类型的转换计划。当类定义了某种类型的类型转换运算符后,类类型将能够向被定义类型产生类型转换。
参考以下代码:
struct A {operator int() const {return 0;} }; // 定义 A -> int 进行类型转换的计划
void test(int) {}
int main()
{test(A()); // 产生了 A -> int 的隐式类型转换
}
与转换构造函数相似,如果心愿禁用隐式类型转换,则须要对类型转换运算符追加 explicit 申明。同样的,explicit 不影响强制类型转换。
参考以下代码:
struct A {explicit operator int() const {return 0;} }; // explicit 类型转换运算符
void test(int) {}
int main()
{test(A()); // Error!禁止 A -> int 的隐式类型转换
test(static_cast<int>(A())); // explicit 不影响强制类型转换
}
对于类型转换运算符与 explicit 还有一条额定规定:operator bool() 在条件表达式(这次要包含:if, while, for, ?: 的条件局部)或逻辑表达式中产生的隐式类型转换将不受 explicit 影响。
参考以下代码:
struct A {explicit operator bool() const {return true;} }; // explicit 类型转换运算符
int main()
{if (A()) {} // 即便 operator bool() 被申明为 explicit,其在 if 中也能产生隐式类型转换
}
4. 继承类到基类的类型转换
4.1 动态类型与动静类型
C++ 的继承机制决定了这样的形象模型:继承类 = 基类局部 + 继承类局部,这意味着每一个继承类都含有其所有基类(如果基类不止一个)的数据各一份。也就是说,对于一个继承类对象,对其基类局部进行操作显然是可行的,这次要包含:
- 失去基类局部的数据
- 将类型转换为基类类型(以失落某些信息为代价)
也就是说,咱们能够将一个继承类对象间接赋值给一个基类类型的变量,显然,这样的赋值建设在隐式类型转换之上,称为继承类到基类的类型转换,或称为向上类型转换。依据附加类型的不同,向上类型转换分为以下几种状况:
struct A {};
struct B: A {};
int main()
{A a1 = B(); // 值向上转换
A *a2 = new B; // 指针向上转换
A &a3 = a1; // 左值援用向上转换
A &&a4 = B(); // 右值援用向上转换}
上述代码中,变量 a1 的类型是 A,这是一个非指针或援用变量,故变量的内存大小就是 A 类对象的大小。如果对基类类型变量应用继承类对象赋值,则将强行去除继承类对象的继承类局部,而将基类局部赋值给变量。故对于 a1 而言,其失去的应该是一个 B 类对象的 A 类局部。即: 如果产生向上类型转换的类型是类自身,则将以失落继承类对象的继承类局部为代价进行向上类型转换。
事实上,此赋值操作调用了 A 类的合成拷贝赋值运算符,而非基于隐式类型转换。C++ 对于类的某些成员函数的合成操作是一个非常复杂的话题,且波及大量与本文无关的内容,故本文不再详述。
对于变量 a2-4,其类型都是 A 的指针或援用(也是指针),而非 A 的本体。因为指针自身并不与类型间接挂钩,故实践上,此类变量中真正寄存的值能够是一个非 A 类型的数据。由此,咱们引出动态类型与动静类型的概念。
C++ 中,一个变量申明的类型称为动态类型,而其理论存储的数据的类型称为动静类型。在绝大多数状况下,动态类型与动静类型都是必须统一的,如果不统一,将产生隐式类型转换或引发编译谬误。当且仅当应用基类的指针或援用存储继承类对象时,变量的动态类型与动静类型将不统一。此时,尽管看上去产生了向上类型转换,但实际上并未产生,此过程称为动静绑定。
一个变量的动态类型,决定了由此变量可能拜访到的成员名称。当动态类型是基类指针或援用时,即便变量寄存的是继承类对象,也只可能拜访到基类中申明的成员名称。即: 如果产生向上类型转换的类型是类的指针或援用,则将以失落继承类局部的成员名称为代价进行向上类型转换。 但因为虚函数的存在,拜访成员名称所失去的理论成员函数将不肯定与动态类型保持一致,此性质是 C ++ 多态的外围。虚函数相干话题与本文无关,这里不再详述。
4.2 阻止向上类型转换
让咱们从新思考这样一个问题:为什么继承类能够拜访基类的成员?不难发现,“继承类能够拜访基类成员”这一性质并不是理所当然的,因为继承类中并没有“复制粘贴”一个基类,而只有继承类自身的局部,故原则上继承类尽管继承了基类,但其自身依然是没有能力拜访基类的成员的。继承类对象之所以可能拜访基类成员,是因为在进行这样的拜访时,继承类的 this 指针通过向上类型转换操作转换成了一个基类类型的指针,而后以基类指针的身份拜访到了基类的成员。
如果心愿阻止这种隐式的向上类型转换呢?让咱们认真考查 public,protected,private 这三个关键字。
依照惯例的解读,这三个关键词用于限定类的用户的拜访权,须要留神的是:“类的用户”不仅指类实例,也指继承此类的类。 阐明如下:
- public:当用于拜访说明符时,示意对类的所有用户可见;用于继承时,示意继承时不批改基类的所有拜访说明符
- protected:当用于拜访说明符时,示意仅对类的继承用户可见,对类的实例用户不可见;用于继承时,示意将基类的所有 public 拜访说明符在继承类中批改为 protected
- private:当用于拜访说明符时,示意对所有类的用户均不可见;用于继承时,示意将基类的所有 public 和 protected 拜访说明符在继承类中批改为 private
上述形容中,“将基类的 xxx 拜访说明符在继承类中批改为 xxx”是一个很奇怪且魔幻的形容,咱们不禁要思考,为什么 C ++ 会给出这样的三种继承模式?又为什么要“随同着继承批改拜访说明符”呢?
如果咱们从向上类型转换这一角度思考,就能得出答案:
- 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 进行类型转换
}
上述代码中,类 B,C,D 别离以三种不同的拜访说明符继承自类 A,同时,咱们别离为类 B,C,D 各定义了一个继承类用户和一个实例用户。由此可见,public 继承将不阻止类的任何用户进行向上类型转换,而 private 继承将阻止类的所有用户进行向上类型转换,protected 继承只阻止类的实例用户进行向上类型转换,但不阻止类的继承类用户进行向上类型转换。
4.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!存在二义性!}
对于类 C,因为其本身定义了变量 i,故拜访 C 类的 i 变量时并未产生向上类型转换。而对于类 D,因为其本身没有定义变量 i,故拜访 D 类的 i 变量时须要在其各个基类中别离进行查找。因为编译器发现 D -> A -> i 与 D -> B -> i 这两种查找路线都是可行的,故此时编译器断定此查找存在二义性。
5. 其余隐式类型转换
C++ 中还定义了一些非凡的类型转换,以下列举出一些常见的状况:
- 0 转换为空指针:
int main()
{int *p = 0;}
- 数组进化为指针
int main()
{int a[10];
int *p = a;
}
- 空指针或数字 0 转为 false,其余指针或数字转为 true
int main()
{if(nullptr) {}
if (2) {}}
- T 转换为 void
int main()
{void *p = new int;}
显然,不存在 void * -> T * 的隐式类型转换。
- 非 const 转换为 const
int main()
{
int *a;
const int * const b = a;
}
樱雨楼
2019.8 于苏州