如何攻克 C++ 中复杂的类型转换?

1.引言

不同的数据在计算机内存中的存储方式不同,导致了“类型”这一抽象概念的出现。

对于一个变量而言,其必须要回答三个问题:

1. 在哪可以访问到这个变量的起点?

2. 从起点向后需要读取多少内存?

3. 应该如何解析读取到的二进制数据?

上述的三个问题中,问题 1,由内存地址回答,问题 2 和 3,均由类型回答。

由此可见,类型与内存地址共同构成了一个变量的完整组分。之所以不能对 void 取值,也是由于无法回答问题 2 和 3 导致。

进一步的,我们可以得到一条十分重要的结论:对于两个不同类型的变量,由于其对问题 2 和 3 的答案不同,故如果将这样的两个变量直接进行运算,在绝大多数情况下都将无法产生有价值的计算结果。

故在几乎所有的编程语言中都有一条重要的规定:不同类型的两个变量无法直接进行运算。

虽然不同类型的两个变量无法进行运算,但显然,我们可将其中的一个变量通过类型转换,转为与另一个变量类型一致,此时就满足“同类型变量才能进行运算”这一规定了。

同时,由于某些类型转换是“理所应当”的,而另一些不是,故由此又派生出两个概念:隐式类型转换与显式类型转换。

隐式类型转换指不通过专门的类型转换操作,而是通过其它规定或代码上下文隐式发生的类型转换,而显式类型转换则通过专门的类型转换操作进行转换,显式类型转换具有强制性,其将不受任何类型转换以外的因素影响,故显式类型转换又称为强制类型转换 。

在 C++ 中,类型转换是一个非常复杂的话题。本文将先从隐式类型转换入手,逐步讨论各类 C++ 的类型转换话题。

2.不同的数据在计算机内存中的存储方式不同,导致了“类型”这一抽象概念的出现。

对于一个变量而言,其必须要回答三个问题:

1. 在哪可以访问到这个变量的起点?

2. 从起点向后需要读取多少内存?

3. 应该如何解析读取到的二进制数据?

上述的三个问题中,问题 1,由内存地址回答,问题 2 和 3,均由类型回答。

由此可见,类型与内存地址共同构成了一个变量的完整组分。之所以不能对 void 取值,也是由于无法回答问题 2 和 3 导致。

进一步的,我们可以得到一条十分重要的结论:对于两个不同类型的变量,由于其对问题 2 和 3 的答案不同,故如果将这样的两个变量直接进行运算,在绝大多数情况下都将无法产生有价值的计算结果。

故在几乎所有的编程语言中都有一条重要的规定:不同类型的两个变量无法直接进行运算。

虽然不同类型的两个变量无法进行运算,但显然,我们可将其中的一个变量通过类型转换,转为与另一个变量类型一致,此时就满足“同类型变量才能进行运算”这一规定了。

同时,由于某些类型转换是“理所应当”的,而另一些不是,故由此又派生出两个概念:隐式类型转换与显式类型转换。

隐式类型转换指不通过专门的类型转换操作,而是通过其它规定或代码上下文隐式发生的类型转换,而显式类型转换则通过专门的类型转换操作进行转换,显式类型转换具有强制性,其将不受任何类型转换以外的因素影响,故显式类型转换又称为强制类型转换 。

在 C++ 中,类型转换是一个非常复杂的话题。本文将先从隐式类型转换入手,逐步讨论各类 C++ 的类型转换话题。

2.类型提升与算术类型转换

算术类型转换专指 C++ 提供的各种内置算术类型之间的隐式类型转换。

内置算术类型主要包括以下类型:

1. bool

2. char, signed char, unsigned char

3. short, int, long, long long, unsignedshort, unsigned int, unsigned long, unsigned long long

4. float, double, long double

5. size_t, ptrdiff_t, nullptr_t 等其它特殊类型

算术类型转换是一类不完全明确的,且与底层密切相关的隐式类型转换。

其遵循以下几条主要原则:

1. 对于同类算术类型,如short 与 int,float 与 double,占用较小内存的类型将转换成另一类型。如  short+ int将被转换为 int + int。此种类型转换称为类型提升。

2. 整形将转为浮点型。如 int+ double 将被转换为 double + double。

3. 仅当无符号类型占用的内存小于有符号类型时,无符号类型才发生类型提升从而转为有符号类型,否则,有符号类型将转为无符号类型。这是一个非常需要注意的点。

参考以下代码:

上述代码中,-1 作为 int 直接量而存在,由于变量 a 是unsigned short 类型,故其将被转为 int,值仍为 1。

但由于变量 b 的类型是与 int 同级的 unsigned 类型,故此时 -1 将被转为 unsigned 类型,这明显不是我们需要的结果。

由此可见,当有符号类型与无符号类型(如 size_t)发生混用时,一定要小心可能会发生的隐式类型转换。

3.转换构造函数

3.1 定义转换构造函数

C++ 中,如果一个构造函数满足以下所有条件,则其成为一个转换构造函数:

1. 至多有一个不含默认值的形参。这主要包括以下几种情况:

构造函数只有一个形参

构造函数有不止一个形参,但只有第一形参无默认值

构造函数有不止一个形参,但全部形参均有默认值

2. 第一形参的类型不为类本身或其附加类型(否则此构造函数将成为拷贝构造函数或移动构造函数)

如果一个类定义了某种转换构造函数,则被定义的类型将可以通过任何类型转换方式转为当前类类型。这常见于以下几种情况:

1. 赋值时发生的隐式类型转换

2. 实参传递时发生的隐式类型转换

3. 基于 static_cast 的显式类型转换

参考以下代码:

c++

struct A { A (int) {} };  // 转换构造函数

void test(A) {}

int main()

{

    A _ = 0;  // 赋值时发生的隐式类型转换

    test(0);  // 实参传递时发生的隐式类型转换

}

上述代码中,我们为类A 定义了从 int 到 A 的转换构造函数。则此时,我们既可以将一个 int 直接赋值给类型为 A 的变量,也可以直接将一个 int 作为实参传给类型为 A 的形参。这两种情况发生时,都隐式地通过转换构造函数构造了类 A 的一个实例。

3.2 阻止基于转换构造函数进行的隐式类型转换

由上文可知,当定义了一个转换构造函数后,就打通了某个其它类型向类类型进行转换的通道。此时,如果我们希望禁用基于转换构造函数进行的隐式类型转换,则需要在转换构造函数前追加 explicit 声明。

当一个转换构造函数被声明为 explicit 后,其具有以下性质:

1. 禁止一切场合下的基于转换构造函数的隐式类型转换

2. 不影响被转换类型到类类型的强制类型转换

3. 不影响对转换构造函数的正常调用

参考以下代码:

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 只是禁用了转换构造函数的隐式类型转换功能,其构造函数功能以及显式类型转换功能并不受影响。

4.类型转换运算符

转换构造函数定义了其它类型向类类型的转换方案,类型转换运算符则定义了与之相反的过程:其用于定义类类型向其它类型的转换方案。当类定义了某种类型的类型转换运算符后,类类型将可以向被定义类型发生类型转换。

参考以下代码:

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中也能发生隐式类型转换

}

5.继承类到基类的类型转换

5.1 静态类型与动态类型

C++ 的继承机制决定了这样的抽象模型:继承类 = 基类部分 + 继承类部分。这意味着每一个继承类都含有其所有基类(如果基类不止一个)的数据各一份。也就是说,对于一个继承类对象,对其基类部分进行操作显然是可行的,这主要包括:

1. 得到基类部分的数据

2. 将类型转换为基类类型(以丢失某些信息为代价)

也就是说,我们可以将一个继承类对象直接赋值给一个基类类型的变量,显然,这样的赋值建立在隐式类型转换之上,称为继承类到基类的类型转换,或称为向上类型转换。

根据附加类型的不同,向上类型转换分为以下几种情况:

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++ 多态的核心。虚函数相关话题与本文无关,这里不再详述。

5.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 继承只阻止类的实例用户进行向上类型转换,但不阻止类的继承类用户进行向上类型转换。

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!存在二义性!

}

对于类 C,由于其自身定义了变量 i,故访问 C 类的i变量时并未发生向上类型转换。而对于类 D,由于其自身没有定义变量 i,故访问D 类的i变量时需要在其各个基类中分别进行查找。由于编译器发现D -> A -> i 与 D -> B -> i 这两种查找路线都是可行的,故此时编译器判定此查找存在二义性。

6.其它隐式类型转换

C++ 中还定义了一些特殊的类型转换,以下列举出一些常见的情况:

1. 0 转换为空指针

int main()

{

    int *p = 0;

}

2. 数组退化为指针

int main()

{

    int a[10];

    int *p = a;

}

3. 空指针或数字 0 转为 false,其它指针或数字转为 true

int main()

{

    if(nullptr) {}

    if (2) {}

}

4. T转换为 void

int main()

{

    void *p = new int;

}

5. 非 const 转换为 const

int main()

{

    int *a;

    const int * const b = a;

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容