NJUのC++课:数据类型

C++数据类型核心要点解析

我们只提炼最重要和最难以理解的部分,作为复习之用

学过一两种计算机语言的人都知道语言的一些基本数据类型,在这里我们不会多做赘述了。

但这里会有几个“坑”,我们在这里重点讲一下!

基本数据类型的几个“坑”

整型的溢出行为

对于整形来说,分为无符号整型和有符号整型,他们两个溢出的结果并不同!

  • unsigned (无符号整型):溢出会导致Wrap Around,是确定的、可预测的
    • unsigned int y = UINT_MAX + 1; // y 的值是 0
    • unsigned int z = 0 - 1; // z 的值是 UINT_MAX
  • signed (有符号整型):溢出会导致 Undefined Behavior (UB, 未定义行为)
  • 附加内容:这些内容不是很重要
    • 如何获得不同类型的max和min值,使用numeric_limit<类型>::max/min()
    • C++23中我们使用<stdckdint.h>中的_builtin_add_overflow(x, y, &result)去进行检查(如果溢出会发出警报)

Float中的溢出行为

我们自然知道Float的存储遵循IEEE754标准,由符号、指数、尾数三部分组成。

  • 当我们的指数为全零的时候,表示非规格化数,非全零非全一表示规格化数,这里面我们不怎么需要注意
  • 重点我们去注意指数位全一的时候,这就是Float对于溢出行为的处理
  • 无穷大 (Infinity, inf):尾数全0
    • 一个比任何数字都大的特殊值。有正无穷 (inf) 和负无穷 (-inf),当你的值真的是无穷大的时候就会出现,比如说double d = 1.0/0.0
    • inf + 任何数 = inf;inf * 任何数 = inf;inf / 任何数 = inf
  • 非数字 (Not a Number, nan):尾数非全0
    • 表示一个未定义的、或不合法的、或无法表示的数值结果。
    • 比如说0.0/0.0、对负数开根、无穷大之间的不确定运算
    • 任何数和 nan 一起运算,结果永远是 nan
  • 附加内容:
    • std::fpclassify(number)可以返回一个浮点数的类型(NORMAL,SUBNORMAL,INFINITE,NAN)
    • 使用isNormal等直接判断

四种类型转换

static_cast<要转的类型>()

  • 问题1:int到short会出现高位截断的现象
  • 问题2:int到float(“四舍五入”)或者float到int会出现精度损失(小数截断)

reinterpret_cast<要转的类型>()

  • 顾名思义:把这块内存里的 0 和 1,当成另一种完全不相关的类型来解释!

  • 不安全但是有用处,这里讲两个用法

  • 探查对象的底层二进制表示:

    使用**reinterpret_cast<const unsigned char*>(&value)** 将地址强行转换为一个unsigned char 指针。

  • 序列化 (Serialization)

    把内存中的一个对象(比如一个结构体),原封不动地变成一串字节,以便写入文件或通过网络发送。

    仅对POD有效,即传统的C语言风格的结构体,不包含虚函数

    #pragma pack(1)表示Padding不留间隙,这对于不同系统的兼容很重要

any_cast<要转的类型>()

  • 这里配合any使用
    • std::any 和 C 语言里的 void* 有本质区别!void* 只存了一个地址,完全丢掉了类型信息,非常不安全。而 std::any 在存值的同时,会牢牢记住这个值原来的类型。
  • any怎么存 —— 直接赋值就可以了
  • any怎么用 —— 这里就需要使用any_cast了
    • 不过如果any_cast 的类型与 any 实际存储的类型不匹配,会抛出异常

const_cast<要转的类型>()

  • 核心用法:把这个 const 指针/引用,暂时当成一个非 const 的指针/引用来用。

  • 常见用法:

    #include <iostream>
    using namespace std;
    
    int main() {
        // 对指针使用const_cast
        const int* constPtr = new int(42);
        int* ptr = const_cast<int*>(constPtr);
        *ptr = 100;  // 合法
    
        // 对引用使用const_cast
        const int constRef = 42;
        int& ref = const_cast<int&>(constRef);
        ref = 100;  // 行为未定义
    
        cout << "指针值: " << *ptr << endl;
        cout << "引用值: " << ref << endl;
    
        return 0;
    }
    
  • 我们举个例子来说明一下这里的问题

    const int c = 128;
    int* q = const_cast<int*>(&c); // q 指向 c,但 q 本身不是 const
    *q = 111; // 企图通过 q 修改 c 的值
    
    cout << c;   // 输出 128
    cout << *q;  // 输出 111
    
    • 这里出现了类似于悖论的现象:为什么同一个内存地址,通过 c 访问是 128,通过 *q 访问却是 111?

      解释:编译器比较懒,你早就已经定下了“誓言”(常量的规定),他就直接将c改成字面量了,不知道你居然通过q改了这个地址内的值

    • 这是未定义行为 (UB),在某些情况下会出现崩溃的现象

union 的“痛”

黑union,为了说明C++中多态的好

内在本质

  • PPT上举了个这样的例子说明了Union的本质:借用Union相同内存表示的方案去解决float2B的算法

    union {
        float f;
        int   i;
    } u;
    u.f = f; // 把浮点数存进去
    s += (u.i & (1 << i)); // 通过 u.i 把同一块内存解释成 int 来操作!
    
  • Share Memory:类似于停车场:大小是固定的(由最大的车决定)

    但同一时间,你只能停一辆单车进去,要么是自行车,要么是电动车。后停进去的车会把前面的“覆盖”掉。

C 风格“伪多态”的笨拙

场景:我们需要一个数组,能存储 100 个不同的图形(直线、矩形、圆形)。这些图形的数据结构完全不同。

  • Union的思路:使用union 存数据 + enum 做标签 + switch 做分发的方式进行

    typedef struct {
    	FIGURE_TYPE t; // 标签,指示 union 中存储的图形类型
    	FIGURE_DATA data; // 存储图形数据的 union
    } Figure;
    

C++的解决方案

  • 抽象基类:定义一个共同的基类 class FIGURE,把所有图形共有的属性(如 color, width)和共有的行为(如 draw())放在里面。
  • 继承:让 class Line, class Rectangle 等具体的图形类都继承自 FIGURE 类。它们自动获得了 color 和 width 属性。
  • 虚函数 (Virtual Function):将基类中的 draw() 函数声明为虚函数 (virtual void draw())。
  • 多态:现在,你可以用一个基类指针 FIGURE* 指向任何一个子类对象(Line, Rectangle 等)。当你通过这个基类指针调用 p->draw() 时,C++ 的运行时系统会自动帮你判断这个指针实际指向的是哪种图形,并调用那个图形自己的 draw() 版本

现代C++工具

这一部分接着Union,通过对C语言的批判展开

问题

  • C语言的Union的问题:
    • 类型不安全:比如说int用double读出来,就会出现乱码
    • 当前存储类型不清楚
    • 类型局限性:必须是平凡类型,不能管理析构和构造函数(省空间)
  • any和继承多态的问题
    • any:运行时检查太晚 (too late)
    • 继承多态:开销太大,对象必须在堆 (heap) 上分配,而且虚函数表也有开销

Variant

  • 定义与赋值:举例,std::variant<int, double, std::string> v;

    • 它是一个编译时就确定了类型范围的容器,只能存这三种类型之一。
    • 赋值非常简单:v = 42; v = 3.14; v = "hello";
  • 访问:有两种方案

    • 方案一:std::get<T>(v)std::get<index>(v)

      具体来说并不推荐,因为你得猜对应的值,很不好猜

      如果访问了未定义在容器中的类型,在编译时就会报错;

      如果访问了定义但是并非最后赋值的类型,运行时报错

    • 方案二:v.index()或者std::holds_alternative<T>(v)

      这会比较推荐,前者返回当前的赋值位,后者返回是否存在该类型

      避免exception

  • 应用:在这里我们讲访问者模式

    • 首先讲一下什么是传统的访问者模式,以PPT上的例子为例

      基本的思路就是双重分发:

      shape->accept(calculator): 第一次分发

      visitor->visit(this): 第二次分发

      
      #include <iostream>
      class Circle;
      class Square;
      // 访问者
      class Visitor {
      public:
          virtual void visit(Circle*) = 0;
          virtual void visit(Square*) = 0;
      };
      // 形状基类
      class Shape {
      public:
          virtual void accept(Visitor* v) = 0;
      };
      
      // 圆形
      class Circle : public Shape {
      public:
          void accept(Visitor* v) override { v->visit(this); }
      };
      
      // 正方形
      class Square : public Shape {
      public:
          void accept(Visitor* v) override { v->visit(this); }
      };
      
      // 具体访问者:计算面积
      class AreaVisitor : public Visitor {
      public:
          void visit(Circle*) override { 
              std::cout << "计算圆形面积" << std::endl; 
          }
          void visit(Square*) override { 
              std::cout << "计算正方形面积" << std::endl; 
          }
      };
      
    • 其次讲一下Variant如何简化访问者模式吧,以下是基本的代码,简化了访问者模式的处理

      std::visit(visitor, variant_object);

      std::visit 会自动检查 my_variant 当前存储的是什么类型,然后调用 visitor 中对应类型的 operator() 或实例化 Lambda。具体内容看PPT就可以了

Any

和上面的Variant的visit方案类似,我们也可以使用any去实现这一点

  • 类Python的变量处理,可是我们需要注意的是,这里面仍是具有严格的类型安全系统

    你必须先用 a.type() == typeid(T) 来检查 any 内部到底是什么类型,然后再用 std::any_cast<T>(a) 去转换

  • 值拷贝和指针

    如果你使用值去匹配的话std::any_cast<T>(any_obj),如果不匹配就会抛出异常

    可是如果使用去指针的方式去匹配的话std::any_cast<T>(&any_obj),不匹配返回nullptr

  • **reset & has_value:**reset清空内容,has_value()返回是否有值,其中的v表示void类型

  • 内存布局:

    • 32bytes:一个字长为管理函数指针(any Handler),剩下3个字长为SOO
    • 何为SOO:小对象直接存储,大东西存指针
    • any Handler:switch-case,根据操作调用

Variant vs Any

  • 接着上面说的any的内存观察,我们再来观察一下variant的内存:

    • 一个 union:这个 union 的大小由 variant 模板参数中最大的那个类型决定。所有可能的数据都存在这个 union 里。
    • 一个索引 (index):一个整数,用来记录当前 union 中激活的是哪个成员
    • 和any不同,他没有函数索引,因为数据的所有类型信息在编译时已经确定了,所以只需要一个简单的索引即可
  • 简单总结

    特性std::anystd::variant
    类型范围运行时确定,可以是任何类型。编译时确定,只能是模板参数列表中的几种。
    类型检查运行时检查。通过 typeid 进行比较,有开销。编译时检查。编译器就知道所有可能的类型。
    访问方式any_cast + typeid。有函数调用和类型比较的运行时开销std::get 或 std::visit。内部直接通过索引访问,几乎零开销
    内存可能有堆分配(大对象)。通常没有额外堆分配
    错误处理运行时抛异常或返回 nullptr。编译时错误 / 运行时抛异常 (std::get) / 编译时分发 (std::visit)。

Struct

  • 这一部分只需要知道**Padding(对齐)**即可,简化的规则为:

    每个成员都会被放置在它自身大小的整数倍的地址上。结构体最终的总大小,会是其最大成员大小的整数倍

Tupple

C++对于函数返回多个值的解决方案

  • 以往的解决方案:C中传入指针,通过对指针的修改在外部“返回”值,或者返回一个结构体
  • C++使用了tuple:那么std::tuple 是什么? 一个匿名的、临时的结构体模板。你可以把它看作一个“万能打包工具”。
  • 如何打包?
    • make_tuple(参数……)
    • tuple<类型……>
    • {参数} C++17中应用
  • 如何解包?
    1. std::get<index/类型>(my_tuple):通过索引或类型获取元组中的元素。
    2. std::tie (C++11):std::tie(i, f) = foo(); 可以将元组中的元素直接解包到已存在的变量中。std::ignore 可以用来忽略不想要的返回值。
    3. 结构化绑定 (Structured Binding) (C++17):auto [i, f] = foo(); 这是最简洁、最推荐的方式!可以直接声明并初始化多个变量来接收元组的返回值。

optional

  • 旧时代的“三宗罪”
    1. 返回魔术数字 (Magic Number):return -1; 如果 -1 本身就是一个合法的 UserID 怎么办?有歧义!
    2. 返回指针 (Memory Leak Risk):return new int(...) 或 return nullptr。用 new 就有忘记 delete 的风险,导致内存泄漏!
    3. 使用输出参数 (Additional parameter):bool find(..., int& outID)。接口笨拙,调用不便。
  • C++17 的救星:std::optional<T>
    • 它是一个类型安全的包装器,要么包含一个 T 类型的值,要么什么都不包含 (空状态)
    • 如何表示“没有值”? return std::nullopt; 或者 return {};
    • 如何表示“有值”? 直接 return value;
    • 如何安全使用?(必考!)
      1. 检查:if (opt.has_value()) 或者更简洁的 if (opt)。
      2. 取值 (不安全):opt.value()。如果 opt 为空,会抛出 std::bad_optional_access 异常。
      3. 取值 (安全):opt.value_or(default_value)。如果 opt 为空,返回你指定的默认值。

指针:未完待续

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值