我们只提炼最重要和最难以理解的部分,作为复习之用
学过一两种计算机语言的人都知道语言的一些基本数据类型,在这里我们不会多做赘述了。
但这里会有几个“坑”,我们在这里重点讲一下!
基本数据类型的几个“坑”
整型的溢出行为
对于整形来说,分为无符号整型和有符号整型,他们两个溢出的结果并不同!
- 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::any std::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中应用
- 如何解包?
- std::get<index/类型>(my_tuple):通过索引或类型获取元组中的元素。
- std::tie (C++11):std::tie(i, f) = foo(); 可以将元组中的元素直接解包到已存在的变量中。std::ignore 可以用来忽略不想要的返回值。
- 结构化绑定 (Structured Binding) (C++17):auto [i, f] = foo(); 这是最简洁、最推荐的方式!可以直接声明并初始化多个变量来接收元组的返回值。
optional
- 旧时代的“三宗罪”:
- 返回魔术数字 (Magic Number):return -1; 如果 -1 本身就是一个合法的 UserID 怎么办?有歧义!
- 返回指针 (Memory Leak Risk):return new int(...) 或 return nullptr。用 new 就有忘记 delete 的风险,导致内存泄漏!
- 使用输出参数 (Additional parameter):bool find(..., int& outID)。接口笨拙,调用不便。
- C++17 的救星:std::optional<T>
- 它是一个类型安全的包装器,要么包含一个 T 类型的值,要么什么都不包含 (空状态)。
- 如何表示“没有值”? return std::nullopt; 或者 return {};
- 如何表示“有值”? 直接 return value;
- 如何安全使用?(必考!)
- 检查:if (opt.has_value()) 或者更简洁的 if (opt)。
- 取值 (不安全):opt.value()。如果 opt 为空,会抛出 std::bad_optional_access 异常。
- 取值 (安全):opt.value_or(default_value)。如果 opt 为空,返回你指定的默认值。
指针:未完待续
NJUのC++课:数据类型
C++数据类型核心要点解析
最新推荐文章于 2025-11-30 21:05:18 发布

455

被折叠的 条评论
为什么被折叠?



