Skip to content

结构化编程

ADT

  • 抽象数据类型: arraystructunion 等。
  • 结合性
  • 优先级
  • 类型转换: coersion(隐式转换) 和 casting(强制转换),溢出处理;
  • 运算顺序:副作用和精度

声明 cast

  • static.cast <TYPE> (DATA):是最常用的类型转换之一,在编译时刻做表示形式级别的转换,适用于大多数显式类型转换。相对于直接类型转换,这种方式可以提供更强的类型检查。一般用于基础的类型转换(如 intfloat 等。

  • const.cast <TYPE> (DATA)const_cast 是用于移除或添加 constvolatile 修饰符的唯一合法方法。它可以将 const 对象的常量性去掉,使其可以被修改,或者反之。例如:

    c++
    const int i = 42;
    int* p = const_cast<int*>(&i);  // 移除 const,允许修改 i
    *p = 24;  // 可能会产生未定义行为

    注意: 如果对原本是常量的对象进行修改,结果是未定义行为,一般的编译器会在直接引用常量时填入原来的值,而在对指针解引用时返回新值。因此需谨慎使用 const_cast

  • dynamic.cast <TYPE> (DATA):主要用于类层次结构中,尤其是在有虚函数的多态情况下。它会在运行时进行类型检查,确保转换是安全的。它可以向上转换(将派生类对象转换为基类类型)和向下转换(将基类对象转换为派生类类型)。如果转换失败,指针类型会返回 nullptr,而引用类型会抛出 std::bad_cast 异常。

    考虑下面的一个简单的基类和派生类:

    c++
    class F {
    public:
        void foo() {
            cout << "Father" << endl;
        }
    };
    
    class S1 : public F {
    public:
        int x = 1;
    
        void foo() {
            cout << "Son1" << endl;
        }
    };
    
    class S2 : public F {
    public:
        int x = 2;
    
        void foo() {
            cout << "Son2" << endl;
        }
    };

    运行一下几组代码,可以看出 dynamic_cast 具有更强的类型检查:

    c++
    F* obj = new S1;
    obj->foo();	//print 'Father'
    static_cast<S1*>(obj)->foo();	//print 'Son1'
    
    F* obj1 = new F;
    static_cast<S1*>(obj1)->foo();	//Unsafe
    cout << static_cast<S1 *>(obj1)->x << endl;	//Undefined behavior
    
    F* obj2 = new S1;
    dynamic_cast<S1*>(obj2)->foo();	//Compile Error...
    
    F* obj3 = new F;
    dynamic_cast<S1*>(obj3)->foo();	//Exception
  • reinterpret.cast <TYPE> (DATA)它保持位的二进制序列不变,只是以新的类型解释变量。这是一种非常危险的转换,它允许几乎任意类型之间的强制转换,甚至可以将指针转换为整数,或者将整数转换为指针。它不会执行任何类型检查或安全保证,适用于底层操作和低级别编程。

表达式

  • auto 关键字:可以使用 auto 关键字来避免冗余的类型定义。

  • decltype 关键字:用法是 decltype (实体或表达式),推导出一个与括号中实体相同的类型,并将该类型作用于后面的对象。例如

    c++
    int i = 33;
    decltype(i) j = i * 2;	//Type of j is int
  • 使用 range-for 使语法简洁:这是 C++ 中新增的语法糖。如果只是遍历容器中的元素,就可以使用 range-for 语法来简化,如下

    c++
    vector<int> v = {1, 2, 3, 4, 5};
    for (int i : v) {
        cout << i << endl;
    }

Union 组合体

union 是一种特殊的数据结构,它允许在相同的内存位置存储不同的数据类型。union 的大小是其成员中最大的成员的大小。其核心思想就是共享内存。如果某一组合体最近被写入为某一个成员变量的类型,那么就只能按这一类型读取,否则会出现未定义行为。例如可以定义以下数据结构 S

c++
union S
{
    std::int32_t n;     // 占用 4 字节
    std::uint16_t s[2]; // 占用 4 字节
    std::uint8_t c;     // 占用 1 字节
};                      // 整个联合体占用 4 字节

下面我们来看一个例子:

c++
#include <cstdint>
#include <iostream>
int main()
{
    S s = {0x12345678}; // 初始化首个成员,s.n 现在是活跃成员
    // 于此点,从 s.s 或 s.c 读取是未定义行为
    std::cout << std::hex << "s.n = " << s.n << '\n';
    s.s[0] = 0x0011; // s.s 现在是活跃成员
    // 在此点,从 n 或 c 读取是 UB 但大多数编译器都对其有定义
    std::cout << "s.c 现在是 " << +s.c << '\n' // 11 或 00,取决于平台
              << "s.n 现在是 " << s.n << '\n'; // 12340011 或 00115678
}

这一程序的输出是:

c++
s.n = 12345678
s.c 现在是 11
s.n 现在是 12340011

我们可以看出,利用 Union 可以实现简单的多态

结构体

struct 是一种用户自定义的数据类型,它可以包含不同类型的数据成员。和 class 一样,struct 也是类关键词。但 struct 的默认访问权限是 public,而 class 的默认访问权限是 privatestruct 可以包含成员函数,但是不能包含构造函数和析构函数。此外,struct 可以继承自其他类,也可以作为基类被继承。

结构体内存布局

在结构体对象内,其成员的地址(及位域分配单元的地址)按照成员定义的顺序递增。可以把指向结构体的指针转型为指向其首成员(或者若首成员为位域,则指向其分配单元)的指针。类似地,能转型指向结构体首成员的指针为指向整个结构体的指针。在任意两个成员间和最后的成员后可能存在无名的填充字节,但首成员前不会有。结构体的大小至少与其成员的大小之和一样大。