Skip to content

Chapter 15 Miscellaneous Points

1 Named Casts

为什么不用 C 风格强转?

C 风格 cast 含义过于宽泛,容易做出危险转换,也不方便搜索和维护。

1. static_cast:适合语义上合理、编译器可检查的转换

double d = 7.1;
int a;

a = d;                    // implicit
a = (int)d;               // C-style
a = static_cast<int>(d);  // 更明确

2. reinterpret_cast:最危险的一类转换之一

int a = 7;
double* p;

p = (double*)&a;                    // C-style
// p = static_cast<double*>(&a);    // error
p = reinterpret_cast<double*>(&a);  // 强行按位解释

3. const_cast:用于增加/去除顶层 const(修改不一定合法)

const int c = 7;
int* q;

// q = &c;                  // Error:&c 是 const int*,不能直接赋值给普通 int*
q = (int*)&c;               // 可行,但后续 *q = 2 会修改 const 常量,属于未定义行为
q = static_cast<int*>(&c);  // Error:static_cast 不允许去掉 const,它只做安全的类型转换
q = const_cast<int*>(&c);   // I really mean it

4. dynamic_cast:基类必须是多态类型,通常意味着至少有一个虚函数

struct A { virtual void f() {} };
struct B : public A {};
struct C : public A {};

A* pa = new B;
C* pc1 = static_cast<C*>(pa);   // OK: but *pa is B!
C* pc2 = dynamic_cast<C*>(pa);  // 运行时失败,得到 nullptr

2 Multiple Inheritance

示例

class Employee {
protected:
    String name;
    EmpID id;
};

class MTS : public Employee {
protected:
    Degrees degree_info;
};

class Temporary {
protected:
    Company employer;
};

class Consultant : public MTS, public Temporary {
    // ...
};

Consultant 会同时拿到 nameiddegree_infoemployer

普通多继承(Vanilla MI)数据布局复杂,可能复制出多份基类子对象,名称与转换可能变得二义。

Consultant 同时含有 MTSTemporary 两个基类子对象,如果这些基类链上再共享同一个祖先类,就可能出现重复祖先子对象。

示例

重复基类
struct B1 { int m_i; };
struct D1 : public B1 {};
struct D2 : public B1 {};
struct M : public D1, public D2 {};

M m;
// B1* p = &m; // ERROR: 哪一个 B1?
B1* p1 = static_cast<D1*>(&m);
B1* p2 = static_cast<D2*>(&m);
数据成员歧义
M m;
// m.m_i++; // ERROR: D1::B1::m_i 还是 D2::B1::m_i ?

若基类只是纯接口类、不含数据状态,复制通常问题不大,因此较安全的用途是组合多个协议/接口类(protocol / interface classes)

抽象基类需满足以下条件:

  • 除析构函数外,所有非静态成员函数均为纯虚函数
  • 析构函数为虚函数,且函数体为空
  • 不包含任何非静态成员变量,可包含静态成员

示例:接口类

class CDevice {
public:
    virtual ~CDevice() {}
    virtual int read(...) = 0;
    virtual int write(...) = 0;
    virtual int open(...) = 0;
    virtual int close(...) = 0;
    virtual int ioctl(...) = 0;
};

为避免重复基类,可把基类声明为 virtual,在 C++ 语境里,virtual 往往意味着间接访问。

虚基类可以让最底层派生类只保留一份虚基类子对象,代价是实现会通过间接指针访问虚基类部分,因此空间与运行期复杂度都会上升。

iostream 中共享基类的图示

示例:Virtual bases

struct B1 { int m_i; };
struct D1 : virtual public B1 {};
struct D2 : virtual public B1 {};
struct M : public D1, public D2 {};

int main() {
    M m;
    m.m_i++;       // OK,只剩一份 B1
    B1* p = new M; // OK
}

多重继承应谨慎使用,尤其避免菱形继承,若只是组合多个接口,优先考虑接口类 + 受控多继承的写法。

如果基类子对象的重复拷贝(复制)不会造成问题,那么就无需将基类声明为虚基类。

抽象基类(除虚表指针 vptr 外不包含任何数据成员)即使被多次复制也不会产生问题,因此这类场景下可以不必使用虚基类。

3 Namespaces

命名空间用于避免全局名字冲突,表达逻辑分组,它本身就是一种作用域。

示例
旧头文件冲突
// old1.h
void f();
void g();

// old2.h
void f();
void g();
用 namespace 包装
namespace old1 {
    void f();
    void g();
}

namespace old2 {
    void f();
    void g();
}

Defining and Implementing Namespaces

Mylib.h
namespace MyLib {
    void foo();
    class Cat {
    public:
        void Meow();
    };
}
MyLib.cpp
#include "MyLib.h"

void MyLib::foo() {
    cout << "foo\n";
}

void MyLib::Cat::Meow() {
    cout << "meow\n";
}
  • 显式限定:使用命名空间中的名称,繁琐且容易分散注意力。

示例

#include "MyLib.h"

int main() {
    MyLib::foo(); 
    MyLib::Cat c; 
    c.Meow();
}
  • using 声明:为名称引入一个局部别名,消除重复的作用域限定符。

示例

int main() {
    using MyLib::foo;
    using MyLib::Cat;
    foo();
    Cat c;
    c.Meow();
}
  • using namespace 指令:把整个命名空间中的名字都引入可见范围。

示例

int main() {
    using namespace std;
    using namespace MyLib;
    foo();
    Cat c;
    c.Meow();
    cout << "hello" << endl;
}
using namespace 方便,但可能引入二义性
namespace XLib {
    void x();
    void y();
}

namespace YLib {
    void y();
    void z();
}

using namespace XLib;
using namespace YLib;

x();       // OK
// y();    // Error: ambiguous
XLib::y(); // OK
z();       // OK
  • 命名空间别名(Namespace aliases):可以通过别名来创建便于使用的名称,也可用于为不同版本的库设置标识。过短可能会发生命名冲突,过长使用不便。

示例

namespace supercalifragilistic {
    void f();
}

namespace short_ns = supercalifragilistic;
short_ns::f();
  • 命名空间组合(Namespace composition):可以把其他命名空间中的名字重新组织到新命名空间中,using 声明可以解决潜在的命名冲突,显式定义的函数优先级更高。

示例

namespace first {
    void x();
    void y();
}

namespace second {
    void y();
    void z();
}

namespace mine {
    using namespace first;
    using namespace second;
    using first::y; // 解决冲突
    void mystuff();
}

int main() {
    mine::x();
    mine::y(); // call first::y()
    mine::mystuff();
}
  • 命名空间的选择(Namespace selection):只选择你需要的名称,而非引入全部内容。

示例

namespace mine {
    using orig::Cat;
    void x();
    void y();
}

命名空间是开放的:多次声明同一个命名空间会向其中追加内容,命名空间可以分散定义在多个文件中。

示例

// header1.h
namespace X {
    void f();
}

// header2.h
namespace X {
    void g();  // X how has f() and g();
}