Project #0 - C++ Primer前置知识学习
这次的Lab0主要以熟悉C++ 11和C++ 17的新特性。这里先把相关新特性分析一下。
目录
一、Doxygen注释
二、函数签名分析
三、构造函数与delete、explicit
四、static constexpr 解释
五、各种cast转换(重点)
5.1 const_cast与reinterpret_cast
5.2 static_cast与dynamic_cast
何时使用 static_cast 和 dynamic_cast?
总结
六、几种整形类总结
一、Doxygen注释
#pragma once
- 作用:#pragma once 是一种常用的预处理指令,通常用在头文件中,表示该头文件在同一编译单元中只会被包含一次。
- 解释:它告诉编译器,如果头文件已经被包含过,则不要再重复包含。这样可以避免重复定义和提高编译效率。它的功能和 #ifndef、#define 和 #endif 组合的作用相似,但语法更加简洁,且一般认为性能更好。
@brief
- 作用:@brief 是 Doxygen(一个文档生成工具)用于文档注释的标签之一。
- 解释:@brief 标签用于简要描述函数、类、变量等的功能。Doxygen 会根据这个注释生成文档并展示函数或类的简短描述。在本代码中,@brief 被用来为类和函数提供简要的描述,帮助开发者理解代码的功能。
@todo
- 作用:@todo 也是 Doxygen 的标签之一,用于标记尚未完成或需要改进的代码部分。
- 解释:当代码中有开发者希望未来进行改进或补充的地方时,可以使用 @todo 标注。Doxygen 会在生成的文档中列出所有的 @todo 标签,提醒开发者这些部分尚未完成。
@param[in]
- @param 是 Doxygen 的注释标记,用来解释函数的参数。
- [in] 表示这个参数是 输入参数,即在函数内部不会被修改或更新。它的值是由调用者传递的,函数可以使用该值,但不负责修改它。
二、函数签名分析
using hash_t = std::size_t;
- 这个 using 语句并不是宏定义,而是 C++11 引入的 类型别名。它为 std::size_t 类型创建了一个新的别名 hash_t。这样,之后你可以在代码中使用 hash_t 来表示 std::size_t,提高代码的可读性和抽象层次。
auto ComputeBinary(const hash_t &hash) const -> std::bitset<BITSET_CAPACITY>;
· const:表示这是一个常量成员函数,不能修改类的成员变量,只能读取它们。
· const hash_t &hash:这是函数的参数,表示一个常量引用类型的 hash_t(即 std::size_t)对象。const 表示参数 hash 在函数内部不会被修改。
auto 关键字
- 在函数声明中,auto 表示编译器会根据函数体的返回值自动推导出返回类型。
- 在 C++14 及更高版本中,auto 可以用于函数的返回类型。通常这种做法可以简化代码,尤其是当返回类型比较复杂时。比如,在本例中,我们用 auto 表示返回类型,并在函数签名的末尾(-> std::bitset<BITSET_CAPACITY>)明确指定返回类型。
尾返回类型(Trailing Return Type)
- -> std::bitset<BITSET_CAPACITY> 指定了函数的返回类型。在这个例子中,返回类型是 std::bitset<BITSET_CAPACITY>,即一个包含 BITSET_CAPACITY 位的二进制位集。
- std::bitset 是一个模板类,表示固定大小的二进制位集合。BITSET_CAPACITY 是一个常量,表示位集合的大小。
三、构造函数与delete、explicit
delete 关键字用于 禁用 默认构造函数。它的作用是禁止编译器生成一个默认的构造函数。
- 默认构造函数是指没有任何参数的构造函数。例如,如果你没有显式提供任何构造函数,C++ 编译器会自动生成一个默认构造函数,它会将类的成员变量初始化为默认值(例如,对于基本数据类型会设置为零)。
- 当你使用 delete 禁用默认构造函数时,编译器将无法自动生成该构造函数,从而防止你使用默认构造函数来创建对象。
- HyperLogLog() = delete; HyperLogLog hll; // 这是无效的,因为默认构造函数被禁用了
explicit 关键字用来 防止隐式类型转换。
- 在 C++ 中,构造函数可以用于隐式类型转换。例如,如果类有一个单参数构造函数,并且你将一个合适类型的对象传递给类的构造函数,编译器可能会自动进行类型转换,调用该构造函数来创建对象。
- explicit 关键字的作用是 防止这种隐式类型转换,强制要求在创建对象时显式地传递正确的参数。
- HyperLogLog hll = 10; // 这是无效的,因为构造函数是 explicit 的,无法隐式传入一个 int 类型。
四、static constexpr 解释
static constexpr double CONSTANT = 0.79402;
在 C++ 中,static constexpr 是两个关键字组合使用的,分别有不同的含义。
1 static 关键字
- static 关键字在类内定义成员变量时的作用是 使变量成为类的静态成员。静态成员属于类本身,而不是类的任何特定实例。
- 静态成员变量在所有类的实例之间共享,而非每个实例有一个副本。
static 关键字的作用:
- 使 CONSTANT 成为类的静态成员变量,它在所有 HyperLogLog 类的对象中只有一份副本。
- 即使你没有创建任何 HyperLogLog 对象,静态成员变量也可以通过类名来访问。
2 constexpr 关键字
- constexpr 表示常量表达式,用于定义编译时常量。被 constexpr 修饰的变量,必须是一个常量,且其值必须在编译时就已确定。
constexpr 关键字的作用:
- 在编译时计算常量值,因此它具有常量的特点,确保其值不会在程序运行时改变。
- 它还允许在编译时执行计算并用于数组大小、模板参数等场景。
3 static constexpr 的组合
当你将 static 和 constexpr 结合使用时,意味着:
- CONSTANT 是一个类的静态常量成员变量。
- CONSTANT 的值是编译时常量,它的值在类的所有实例中共享。
- CONSTANT 的值在程序运行时不可改变,必须在编译时就知道其值。
五、各种cast转换(重点)
5.1 const_cast与reinterpret_cast
从一句代码入手
int32_t offset = *reinterpret_cast<int32_t *>(const_cast<char *>(data_ + col.GetOffset()));
这段代码涉及到类型转换,特别是通过 reinterpret_cast
和 const_cast
进行的转换。我们逐一分析这段代码及其背后的类型转换原理。
1、data_ + col.GetOffset()
-
data_
是一个指向char
数组的指针,长度由模板参数KeySize
决定。 -
col.GetOffset()
返回的是某个列的偏移量,它应该是一个整数(通常是size_t
或int
类型),表示该列数据相对于data_
的偏移位置。
这行代码是将 col.GetOffset()
加到 data_
上,得到一个新的指针,该指针指向 data_
数组中某个列的起始位置。也就是说,data_ + col.GetOffset()
计算的是某个特定列在 data_
数组中的位置。
2、const_cast<char \*>(...)
-
const_cast
是一种类型转换运算符,它用于在指针或引用上移除const
限定符,或者添加const
限定符。 -
在这里,
data_
是char data_[KeySize]
类型的数组,它本身是const
类型(假设data_
是const
修饰的),因此data_ + col.GetOffset()
的类型是const char *
。但在此处,需要一个char *
类型的指针来进行后续的reinterpret_cast
操作,因为reinterpret_cast
要操作的是一个普通的指针类型。
const_cast<char *>(...)
用来去掉 data_ + col.GetOffset()
结果中的 const
,将其转换为 char *
类型。这样做是为了让指针指向的内存可以进行修改或重新解释(比如 reinterpret_cast
)。但值得注意的是,const_cast
只改变指针的 const
属性,并不会改变实际内存中的数据,因此这种转换可能会导致未定义行为,除非你确信不需要保证该数据是 const
的。
3、reinterpret_cast<int32_t \*>(...)
-
reinterpret_cast
是一种强制类型转换,用于将一个指针或引用转换成其他类型的指针或引用。 -
它通常用来进行低级的、完全不安全的类型转换,允许你将一个类型的指针转换为与其完全不同类型的指针。
-
在这个例子中,
reinterpret_cast<int32_t *>(...)
将char *
类型的指针转换为int32_t *
类型的指针。意味着你可以通过该指针访问一个int32_t
类型的数据,而不考虑它实际是存储在char
数组中的某一段字节。
通过这种转换,你实际上是将内存中的字节块视为一个 int32_t
类型的值,按 int32_t
类型来读取数据。这里的 int32_t
是 4 字节,而 char
是 1 字节,所以这个转换的效果是将内存中连续的 4 字节(从 data_ + col.GetOffset()
开始)解释为一个 int32_t
类型的整数。
4. 解引用操作
reinterpret_cast<int32_t *>(...)
-
解引用操作符
*
用于访问指针所指向的值。在这里,解引用的是reinterpret_cast<int32_t *>(...)
返回的int32_t *
类型的指针。 -
也就是说,解引用后,我们将从
data_ + col.GetOffset()
开始的 4 个字节读取并解释为一个int32_t
类型的值,这个值会赋给offset
变量。
类型转换总结
这段代码使用了两种类型转换:
const_cast
:
用于去掉
const
限定符,使得data_ + col.GetOffset()
结果可以被修改或解释为其他类型。它确保data_ + col.GetOffset()
从const char *
转换为char *
。
const_cast
是最轻量级的类型转换方式,通常用于去掉或添加常量属性。
reinterpret_cast
:
- 用于将
char *
指针转换为int32_t *
指针。这种转换是低级别的,不会考虑源数据的实际布局,只是根据内存地址将其重新解释为不同类型的数据。在这里,它将从data_ + col.GetOffset()
开始的 4 字节解释为一个int32_t
类型的值。reinterpret_cast
是一种非常危险的类型转换方式,因为它可以突破类型系统的限制,将不同类型的数据按字节重解释,因此在使用时需要非常小心,确保转换后的指针能够正确地访问数据。
5.2 static_cast与dynamic_cast
何时使用
static_cast
和dynamic_cast
?
使用
static_cast
当你知道类型之间的关系并且转换是安全的。它对编译器更高效,并且适用于已知类型之间的转换。使用
dynamic_cast
当你不确定基类指针或引用的实际类型时,尤其是在涉及多态的情况下。它会确保转换的类型正确性并避免潜在的错误。
在 C++ 中,类型转换(cast
)有四种主要形式:
1、static_cast
:
-
用于类型之间的转换,通常用于类型之间有明确定义的转换关系时(如整数类型与浮点类型之间、指针类型之间等)。
-
它通常用于需要在编译时检查类型合法性的转换。
-
示例:
float f = 3.14f;
int x = static_cast<int>(f); // 将 float 转换为 int
2、dynamic_cast
:
-
主要用于类之间的指针或引用转换,尤其是多态类型(有虚函数的类)之间的转换。它在运行时执行类型检查。
-
用于安全地将基类指针或引用转换为派生类指针或引用。
-
示例:
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 运行时检查是否是 Derived 类型
3、const_cast
:
-
用于添加或去除
const
限定符,改变对象的常量性。const_cast
不能用来改变对象的类型,它只改变常量性。 -
示例:
const int x = 10;
int* y = const_cast<int*>(&x); // 去掉 const,得到一个非 const 指针
reinterpret_cast
:
-
用于执行低级的指针类型转换,可以将任何指针类型转换为另一个指针类型,甚至是完全不相关的类型。它仅仅根据内存地址进行转换,几乎不做任何类型检查,因此非常不安全。
-
示例:
int x = 42; char* p = reinterpret_cast<char*>(&x); // 将 int* 转换为 char*,按字节访问
总结
-
const_cast
用来移除const
限定符,使得可以修改指向的内容,或者用于访问原本被声明为const
的数据。 -
reinterpret_cast
用于指针类型之间的强制转换,允许将指针转换为完全不同类型的指针,按新的类型去解释数据。这种转换是非常低级且不安全的,可能会引发未定义行为,必须小心使用
六、几种整形类总结
最后,把几个整数类型都理清楚一下。这里重点调int、int8、int8_t、size_t
int | 大小:通常是 4 字节(32 位),但这取决于平台(例如,某些 64 位平台上的 int 可能为 8 字节)。 符号:有符号类型,可以表示负数、零和正数。通常是 -2,147,483,648 到 2,147,483,647(即 -2^31 到 2^31 - 1),但具体范围取决于平台和编译器。 |
int8 | 用途:int8 不是 C++ 标准库中的一种类型,它通常是一个非标准的、可能依赖于特定平台或库的 8 位有符号整数类型。在某些情况下,int8 可能作为某些编译器或库的自定义类型,用于表示 8 位的有符号整数。 |
int8_t | 大小:1 字节(8 位)。 符号:有符号类型,能表示负数、零和正数。 范围:-128 到 127(即 -2^7 到 2^7 - 1) 用途:通常用于跨平台编程,确保类型的大小严格为 8 位。它不受平台架构(如 32 位或 64 位)影响,保证类型大小一致。 |
size_t | size_t(无符号整数,用于表示大小或索引) 大小:依赖于平台,通常在 32 位系统上为 4 字节,在 64 位系统上为 8 字节。 用途:用于表示对象的大小或数组索引,通常用于数组大小、容器大小等。 范围:取决于平台。32 位平台的 size_t 范围为 0 到 4,294,967,295,64 位平台为 0 到 18,446,744,073,709,551,615。 |