跳转至

类型

数据类型

下面是一张表, 列出了 C++ 中的基本数据类型及其在不同平台上的大小. 请注意, C++ 标准并没有规定这些类型的确切大小, 但它们通常遵循以下规则:

类型说明符 等效类型 C++ 标准 (位) LP32 ILP32 LLP64 LP64
signed char signed char 至少 8 8 8 8 8
unsigned char unsigned char 至少 8 8 8 8 8
short, short int, signed short, signed short int short int 至少 16 16 16 16 16
unsigned short, unsigned short int unsigned short int 至少 16 16 16 16 16
int, signed, signed int int 至少 16 16 32 32 32
unsigned, unsigned int unsigned int 至少 16 16 32 32 32
long, long int, signed long, signed long int long int 至少 32 32 32 32 64
unsigned long, unsigned long int unsigned long int 至少 32 32 32 32 64
long long, long long int, signed long long, signed long long int long long int (C++11) 至少 64 64 64 64 64
unsigned long long, unsigned long long int unsigned long long int (C++11) 至少 64 64 64 64 64

示例

#include <iostream>
#include <string>

int main() {
    int x = 42;
    std::cout << x << std::endl; // 42
    std::cout << sizeof(x) << std::endl; // 4
    long long y = 543254325432;
    std::cout << y << std::endl; // 543254325432
    std::cout << sizeof(y) << std::endl; // 8
    int64_t z = 889789790;
    std::cout << z << std::endl; // 889789790
    std::cout << sizeof(z) << std::endl; // 8
    bool e = true;
    std::cout << e << std::endl; // 1
    std::cout << sizeof(e) << std::endl; // 1
    float f = 3.14f;
    std::cout << f << std::endl; // 3.14
    std::cout << sizeof(f) << std::endl; // 4
    double d = 3.141592653589793f;
    std::cout << d << std::endl; // 3.14159
    std::cout << sizeof(d) << std::endl; // 8
    char c = 'A';
    std::cout << c << std::endl; // A
    std::cout << sizeof(c) << std::endl; // 1
    std::string str = "Hello, World!"; // string不是一种fundamental type
    std::cout << str << std::endl; // Hello, World!
    std::cout << sizeof(str) << std::endl; // 32
    return 0;
}

const类型

最简单的用法是将 const 关键字放在变量声明的前面, 这会使变量成为只读的.

示例

#include <iostream>

int main() {

    int x = 7;
    std::cout << x << std::endl;
    x = 3;
    std::cout << x << std::endl;

    const float PI = 3.14f;
    std::cout << PI << std::endl;
    // PI = -42; // 不能改变PI
    std::cout << PI << std::endl;

    return 0;
}

可以使用<type_traits>来判断类型是否为const

std::is_const<T>::value可以用来判断类型T是否为const. 例如:

#include <iostream>
#include <type_traits>

int main() {
    std::cout << std::boolalpha; // 后续输出的布尔值将以true/false形式显示
    std::cout << std::is_const<int>::value << std::endl; // false
    std::cout << std::is_const<const int>::value << std::endl; // true
    return 0;
}

sizeof运算符

注意, sizeof运算符返回的是那个类型的大小, 不是里面放的所有东西的大小.

#include <iostream>
#include <vector>

int main() {
    int x = 7;
    int *px = &x;
    int array[] = {1, 3, 5, 7, 9};
    int* dynamicallyAllocatedArray = new int[100];
    std::vector<int> v;
    v.push_back(1);
    v.push_back(1);
    v.push_back(1);
    v.push_back(1);
    std::cout << "x                          :" << sizeof(x) << std::endl;  // 4
    std::cout << "px                         :" << sizeof(px) << std::endl; // 8
    std::cout << "array                      :" << sizeof(array) << std::endl; // 20
    std::cout << "dynamicallyAllocatedArray  :" << sizeof(dynamicallyAllocatedArray) << std::endl; // 8, 这里是8, 而不是400
    std::cout << "v                          :" << sizeof(v) << std::endl; // 24, 这里是24, 而不是16
    return 0;
}

左值右值

这一节比较重要. 左值是一个有确定内存地址的东西. 右值是一个临时的没有内存地址的东西. 例如

  • a: 是左值
  • b: 是左值
  • a + b: 是右值, 因为没法获取到地址
  • array[10+a]: 是左值
  • 10 + a: 是右值

左值引用

  • 符号: &
  • 作用: 这是 C++ 传统的引用, 它是已存在对象的一个别名 (alias). 你通过引用修改对象, 就像直接修改原对象一样.
  • 绑定: 通常只能绑定到左值.
  • 例外: const 左值引用 (const T&) 可以绑定到右值.
  • 目的: 主要是为了避免拷贝 (尤其是在函数传参和返回值时) 以及允许函数修改其参数.

例子:

int x = 10;
int& ref_l = x;

// int& ref_bad = 10; // 错误: 不能绑定到右值 10
const int& ref_const = 10; // 正确: const左值引用可以绑定到右值

右值引用

  • 符号: &&
  • 作用: 这是 C++11 引入的新特性, 专门用于绑定到右值 (临时对象).
  • 绑定: 只能绑定到右值.
  • 目的: 主要用于实现移动语义 (Move Semantics, 所有权转移) 和完美转发 (Perfect Forwarding). 移动语义允许我们"窃取" 临时对象的资源 (比如动态分配的内存), 避免不必要的深拷贝, 从而极大地提高性能.

例子:

int&& ref_r = 10;
ref_r = 20;
std::cout << ref_r << std::endl; // 20

std::string s = "world";
// std::string&& ref_s = s; // 错误: 不能绑定到左值 s
std::string&& ref_s = std::move(s);  // 注意, 这种写法其实没有移动
std::cout << ref_s << std::endl; // "world"
std::cout << s << std::endl; // "world"
std::string s_move = std::move(s);  // 这种写法有移动
std::cout << s << std::endl; // ""

std::string s1 = "wenzexu";
std::string s2 = "a really long str";
std::string&& s3 = s1 + s2; // 注意, 这种写法其实没有移动
std::cout << s3 << std::endl; // "wenzexua really long str"
std::string s4 = std::move(s1);  // 这种写法有移动
std::cout << s1 << std::endl; // ""

生命周期延长

当你将一个右值引用绑定到一个临时对象时, 这个临时对象的生命周期会被延长到右值引用的作用域结束, const T&的右值引用也会延长临时对象的生命周期. 例如:

std::string&& ref = std::string("Hello");
const std::string& ref_const = std::string("World");
std::cout << ref << std::endl; // 输出 "Hello"
std::cout << ref_const << std::endl; // 输出 "World"

std::string s_move = std::move(s)std::string&& ref_s = std::move(s)的区别

std::string s_move = std::move(s); 是通过移动构造函数创建新对象并转移资源, 而 std::string&& ref_s = std::move(s); 仅创建一个绑定到原对象的右值引用, 不发生移动.

std::string s_move = std::move(s);的实现: std::move(s)是一个强制类型转换, 实际上可以写为(std::string&&)s, 它将左值转换为一个右值引用. 等式左边的代码std::string s_move会调用构造函数, 由于等式的右边是一个右值引用, 编译器会选择调用str::string的移动构造函数. 移动构造函数的核心是资源窃取. 也就是说, 右值引用在这里只是作为一种类型标记, 触发C++的重载机制, 从而让编译器选择移动构造函数, 而不是拷贝构造函数. 为什么它会选择移动构造函数呢, 因为移动构造函数的函数签名里面接受的是一个右值引用, 而复制构造函数里面接受的是一个左值引用. 移动构造函数里面做了什么事情呢? 它会将s_move的内部指针指向s的资源, 然后将s的指针置空, 这样就完成了资源转移而不是复制.

std::string&& ref_s = std::move(s);这句代码不会执行移动, 没有触发任何构造函数. 等式右边的这个std::move(s)的类型是std::string&&, 但是鉴于它是一个表达式, 所以它是右值, 或者更加具体化的说, 它是一个xvalue, 将亡值, 仍然属于右值的范畴, 所以可以赋值给等号的左边.

std::move

实际上, 我们在写C++代码的时候, 可能会包含很多的复制, 例如:

std::string s1 = "long string........";
std::string s2 = s1; // 触发复制构造函数
void func(std::string s3) { /* ... */ }
func(s1); // 也是复制

但是, 我们想要的是转交s1的所有权, 因为我们用不到s1了. 这个时候就要用到std::move了. std::move是一个函数模板, 它的作用是将一个左值转换为右值引用, 这样就可以触发移动构造函数而不是复制构造函数, 为什么呢? 因为移动构造函数的参数是右值引用, 而复制构造函数的参数是左值引用, 根据重构策略, 应该调用移动构造函数. 移动构造函数里面做了什么事情呢? 它会将s_move的内部指针指向s的资源, 然后将s的指针置空, 这样就完成了资源转移而不是复制.

std::string myString = "copy construct me";
std::string newValue;
std::cout << "myString: " << myString << std::endl; // "copy construct me"
std::cout << "newValue: " << newValue << std::endl; // ""
newValue = std::move(myString); // 等价于newValue = (std::string&&)myString;
std::cout << "myString: " << myString << std::endl; // ""
std::cout << "newValue: " << newValue << std::endl; // "copy construct me"

所以, 右值引用到底是怎么发挥它的效果的呢? 我认为如果一个函数传入的是右值引用, 这就相当于告诉我们这个参数是一个临时对象, 可以直接抢占它的资源, 或者随意玩弄它, 这也就是为什么右值引用会被用于资源/句柄的转移. 这样, 如果在代码中调用这个函数之后还有100完行代码, 这个可恶的临时对象就不会一直占着坑位, 浪费内存资源, 我们要在函数中榨干它. 推荐阅读对象里面的"移动构造函数和移动赋值操作符"一小节, 里面有一个很有趣的例子.

const的用法

  1. 创建只读的变量: const int x = 10;
  2. 创建只读的函数参数: void func(const int x);, 在拷贝构造函数中UDT(const UDT& rhs), 这个const使我们不仅能接受左值, 还可以接受右值(见上面的左值右值部分).
  3. 作为一种成员函数修饰符: void func() const;, 这意味着这个函数不会修改类的成员变量
  4. const int *var 表示指针可变但所指整数不可通过它修改; int * const var 表示指针自身不可变但所指整数可通过它修改; 而 const int * const var 则表示指针不可变且所指整数也不可通过它修改

decltype的用法

#include <iostream>
#include <string>
#include <vector>

int main() {
    int i = 10;
    decltype(i) j = 20; // j 的类型是 int

    double x = 3.14;
    decltype(x) y = 2.71; // y 的类型是 double

    std::string s = "hello";
    decltype(s) t = "world"; // t 的类型是 std::string

    decltype(i + x) k; // k 的类型是 double, 因为 i (int) + x (double) 的结果是 double

    std::cout << "j: " << j << std::endl;
    std::cout << "y: " << y << std::endl;
    std::cout << "t: " << t << std::endl;
    // std::cout << "k: " << k << std::endl; // k 未初始化, 输出其值是未定义行为

    std::vector<int> vec = {1, 2, 3};
    decltype(vec[0]) first_element_ref = vec[0]; // first_element_ref 的类型是 int& (引用)
    first_element_ref = 100; // 修改 vec[0] 的值

    std::cout << "vec[0]: " << vec[0] << std::endl; // 输出 100

    const int ci = 5;
    decltype(ci) cj = 15; // cj 的类型是 const int

    return 0;
}

在这个例子里:

  1. decltype(i) 推断出 i 的类型是 int, 所以 j 也是 int.
  2. decltype(i + x) 推断出表达式 i + x 的结果类型是 double, 所以 kdouble.
  3. decltype(vec[0]) 推断出 vec[0] (访问 std::vector 元素) 的类型是 int& (对 int 的引用).
  4. decltype(ci) 推断出 ci 的类型是 const int, 所以 cj 也是 const int.

union的用法

在C++中, union是一种特殊的数据结构, 它允许在相同的内存位置存储不同的数据类型. 但在任何时候, 只有一个成员可以包含值. 在union中, 所有成员共享同一块内存空间. 并且, 你只能同时使用union中的一个成员, 给一个成员赋值会覆盖其他成员的值. 它的大小取决于最大成员的大小.

#include <iostream>

union U {
    int i;
    short s;
    float f;

    void printi() {
        std::cout << i << std::endl;
    }
};

int main() {
    U myUnion;
    myUnion.i = 50253;
    std::cout << "Integer value: " << myUnion.i << std::endl;
    std::cout << "Short value: " << myUnion.s << std::endl;
    std::cout << "size of union: " << sizeof(myUnion) << " bytes" << std::endl;
    myUnion.printi();
    return 0;
}

输出:

Integer value: 50253
Short value: -15283
size of union: 4 bytes
50253

为什么要用union呢? 举个例子, 看SDL_Event这个union(它是用C实现的, 所以有typedef):

typedef union SDL_Event
{
    Uint32 type;                            /**< Event type, shared with all events */
    SDL_CommonEvent common;                 /**< Common event data */
    SDL_DisplayEvent display;               /**< Display event data */
    SDL_WindowEvent window;                 /**< Window event data */
    SDL_KeyboardEvent key;                  /**< Keyboard event data */
    SDL_TextEditingEvent edit;              /**< Text editing event data */
    SDL_TextEditingExtEvent editExt;        /**< Extended text editing event data */
    SDL_TextInputEvent text;                /**< Text input event data */
    SDL_MouseMotionEvent motion;            /**< Mouse motion event data */
    SDL_MouseButtonEvent button;            /**< Mouse button event data */
    SDL_MouseWheelEvent wheel;              /**< Mouse wheel event data */
    SDL_JoyAxisEvent jaxis;                 /**< Joystick axis event data */
    SDL_JoyBallEvent jball;                 /**< Joystick ball event data */
    SDL_JoyHatEvent jhat;                   /**< Joystick hat event data */
    SDL_JoyButtonEvent jbutton;             /**< Joystick button event data */
    SDL_JoyDeviceEvent jdevice;             /**< Joystick device change event data */
    SDL_JoyBatteryEvent jbattery;           /**< Joystick battery event data */
    SDL_ControllerAxisEvent caxis;          /**< Game Controller axis event data */
    SDL_ControllerButtonEvent cbutton;      /**< Game Controller button event data */
    SDL_ControllerDeviceEvent cdevice;      /**< Game Controller device event data */
    SDL_ControllerTouchpadEvent ctouchpad;  /**< Game Controller touchpad event data */
    SDL_ControllerSensorEvent csensor;      /**< Game Controller sensor event data */
    SDL_AudioDeviceEvent adevice;           /**< Audio device event data */
    SDL_SensorEvent sensor;                 /**< Sensor event data */
    SDL_QuitEvent quit;                     /**< Quit request event data */
    SDL_UserEvent user;                     /**< Custom event data */
    SDL_SysWMEvent syswm;                   /**< System dependent window event data */
    SDL_TouchFingerEvent tfinger;           /**< Touch finger event data */
    SDL_MultiGestureEvent mgesture;         /**< Gesture event data */
    SDL_DollarGestureEvent dgesture;        /**< Gesture event data */
    SDL_DropEvent drop;                     /**< Drag and drop event data */

    /* This is necessary for ABI compatibility between Visual C++ and GCC.
       Visual C++ will respect the push pack pragma and use 52 bytes (size of
       SDL_TextEditingEvent, the largest structure for 32-bit and 64-bit
       architectures) for this union, and GCC will use the alignment of the
       largest datatype within the union, which is 8 bytes on 64-bit
       architectures.

       So... we'll add padding to force the size to be 56 bytes for both.

       On architectures where pointers are 16 bytes, this needs rounding up to
       the next multiple of 16, 64, and on architectures where pointers are
       even larger the size of SDL_UserEvent will dominate as being 3 pointers.
    */
    Uint8 padding[sizeof(void *) <= 8 ? 56 : sizeof(void *) == 16 ? 64 : 3 * sizeof(void *)];
} SDL_Event;

可以看到, 如果我们创建一个非unionSDL_Event, 那么我们就需要给这么多这么多的变量分配内存, 而实际情况是, 由于实际上我们每一时刻仅仅会使用其中的一个变量, 所以需要使用union来节省空间.

std::variant

std::variant是C++17引入的一个非常有用的特性, 它是一个类型安全的union. 它可以在任何时候持有其预定义类型列表中的一个值. 在声明std::variant的时候, 你必须指定它可以持有的所有可能类型, 例如std::variant<int, double, std::string>, 这个v可以持有int, double, 或者std::string. 访问值的方法一般由两种:

  1. std::get<T>v: 如果v当前持有类型T的值, 则返回该值的引用, 如果v不持有类型T的值, 则会抛出std::bad_variant_access异常
  2. std::get<I>v: 如果v当前持有第I个备选类型(索引从0开始)的值, 则返回该值的引用, 否则抛出std::bad_variant_access异常

那么, 它的类型安全是啥意思呢?

union MyUnion {
    int i;
    double d;
};
MyUnion u;
u.i = 10;
// double value = u.d; // 编译通过, 但这是未定义行为, value可能是垃圾值

但是对于std::variant:

std::variant<int, double> v;
v = 10; // v 现在持有 int
// double d = std::get<double>(v); // 会抛出 std::bad_variant_access 异常
// int i = std::get<int>(v);     // 正确, i 的值为 10

另外, 你会发现相同情况下, std::variant的内存占用比union要大:

#include <iostream>
#include <variant>

union U {
    int i;
    float s;
};

int main() {
    std::variant<int, float> data;
    std::cout << "U: " << sizeof(U) << std::endl;
    std::cout << "data: " << sizeof(data) << std::endl;
    return 0;
}

使用C++17编译之后, 发现:

U: 4
data: 8

为啥呢? 这是因为std::variant会和最大的类型对齐(float, 4个字节), 然后用一些额外的空间存储当前类型的tag, 由于要和最大的类型对齐, 所以用于存储tag的空间是4个字节, 所以加起来总共8个字节. 这就是为啥std::variant又被称为tagged union.

还有另一个点是std:get_if的使用. 它的主要作用是, 在不抛出异常的情况下(上面的std::bad_variant_access), 尝试获取std::variant中特定类型的值的指针. std::get_if<T>(&v)会检查std::variant对象v是否持有类型为T的值. 如果v确实持有类型为T的值, std::get_if会返回一个指向该值的指针. 如果v不持有类型为T的值, std::get_if会返回nullptr. 它和std::get的主要区别是, std::get<T>(v)在类型不匹配的时候会抛出std::bad_variant_access异常, 而std::get_if则通过返回nullptr来表示类型不匹配或者值不存在, 使得你可以使用更加平和的方式(如if语句检查指针)来处理这种情况.

constexpr的用法

constexpr是C++11引入的关键字. 它用于声明可以在编译时求值的常量表达式.

具体来说, constexpr可以用于以下几个方面:

  1. constexpr变量: 声明的变量必须在编译时初始化, 且其值在整个程序运行期间保持不变. 初始化的表达式只能包含字面值, constexpr变量和constexpr函数. 例如:

    constexpr int max_size = 100 + 5;
    constexpr double pi = 3.14159 + 0.001;
    
  2. constexpr函数: 声明的函数如果其参数也是常量表达式, 则可以在编译时被求值. constexpr函数必须满足一些限制, 例如函数体只能包含return语句, 空语句和constexpr声明等. 例如:

    constexpr int square(int n) {
        return n * n;
    }
    
    int main() {
        constexpr int result = square(5); // result在编译时被计算为25
        int x = 2;
        // int runtime_result = square(x); // 可以在运行时计算
    }
    
  3. constexpr构造函数: 声明的构造函数可以用于创建constexpr对象. constexpr类的所有成员都必须是字面值类型, 并且构造函数的函数体必须为空. 例如:

    struct Point {
        constexpr Point(double x_val, double y_val) : x(x_val), y(y_val) {}
        double x;
        double y;
    };
    
    constexpr Point origin(0.0, 0.0);
    

使用 constexpr 的原因有很多, 主要包括以下几点:

  1. 性能优化: constexpr 允许在编译时计算表达式的值. 这意味着在程序运行时, 这些值已经是预先计算好的, 避免了运行时的计算开销, 从而提高了程序的性能.
  2. 编译时检查: constexpr 函数和变量的值在编译时确定, 编译器可以对它们进行更严格的类型检查和错误诊断. 这有助于在程序运行之前发现潜在的错误.
  3. 定义常量: constexpr 可以用来定义真正的常量, 这些常量可以用于模板参数, 数组大小, 枚举值等需要在编译时确定的地方. 这增强了代码的灵活性和可读性.
  4. 更好的代码可读性和可维护性: 通过使用 constexpr, 可以将一些计算逻辑放在编译时进行, 使得代码更加清晰, 易于理解和维护.
  5. 在模板编程中的应用: constexpr 函数可以作为模板参数的非类型参数, 从而实现更强大的模板元编程.

简单来说, constexpr 的核心优势在于将计算从运行时提前到编译时, 从而提升性能, 增强类型安全, 并使代码更具表达力. 其实template也是在编译的时候起作用的.

auto的用法

在C++中, auto关键字主要用于类型推导. 它允许你在声明变量时不必显式指定其类型, 而是让编译器根据初始化表达式自动推断出变量的类型. 自C++11标准引入以来,auto的主要作用体现在以下几个方面:

  1. 简化代码, 提高可读性: 当变量的类型很长或很明显时, 使用auto可以减少代码的冗余, 使代码更简洁易懂. 例如:

    std::vector<std::pair<std::string, int>> my_vector;
    // 不使用 auto
    std::vector<std::pair<std::string, int>>::iterator it = my_vector.begin();
    // 使用 auto
    auto it = my_vector.begin();
    for (it; my_vector.end(); it++) {
        ...
    }
    
    在上面的例子中, 使用auto可以避免写出冗长的迭代器类型.

  2. 处理复杂类型: 对于一些难以书写或名称复杂的类型 (例如 lambda 表达式的类型),auto非常有用. 你不需要知道或显式写出 lambda 表达式的具体类型.

    // lambda 表达式
    auto my_lambda = [](int x) { return x * 2; };
    

  3. 泛型编程: 在模板编程中,auto可以方便地处理依赖于模板参数的类型.

    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u) {
        return t + u;
    }
    
    虽然上面的例子使用了尾置返回类型(使用decltype自动推导), 但在 C++14 中, 函数的返回类型也可以直接使用auto让编译器推导.

  4. 避免类型不匹配: 有时, 表达式的类型可能很复杂或容易出错, 使用auto可以确保变量的类型与初始化表达式的类型完全一致, 从而避免潜在的类型不匹配问题.

需要注意的是:

  • 使用auto声明的变量必须进行初始化, 因为编译器需要根据初始化表达式来推导类型.
  • auto不是一个占位符, 它会根据初始化表达式推导出一个确切的类型.
  • auto不能用于函数参数的类型 (C++14 中 lambda 表达式的参数可以使用 auto).
  • auto可以和引用(&)或指针(*)结合使用. 例如: auto& ref = variable;auto* ptr = &variable;.
  • auto会忽略初始化表达式的顶层 constvolatile限定符, 但如果需要保留这些限定符, 可以显式地添加, 例如const autovolatile auto. 对于引用类型, const会被保留.

类型转换

在C++中, "casting"(类型转换) 是指将一个数据类型的值转换为另一个数据类型的过程. 这在需要不同类型的数据进行操作或交互时非常有用. 类型转换主要分为隐式转换和显示转换.

  1. 隐式转换

    记起来类那一节讲到的explicit关键字吗? 它的作用就是防止隐式地类型转换, 例如MyString s1 = 10; 如果构造函数前面有explicit, 那么会报错, 因为将10隐式转换为了MyString对象. 必须显式地写成例如MyString s1{10};才行. 隐式转换是由编译器自动完成的, 通常发生在安全且无信息丢失风险的情况下, 例如将较小的整数类型转换为较大的整数类型, 或将派生对象转换为其基类指针或者引用.

  2. 显式转换

    需要程序员明确指定要进行的转换, 用于可能存在信息丢失或者类型不兼容风的情况, C++提供了4种命名的强制类型转换操作符, static_cast, dynamic_cast, reinterpret_cast, const_cast. 下面将会一一展开.

C风格转换

看这个例子:

#include <iostream>

int main() {
    std::cout << 7/5 << std::endl;
    return 0;
}

输出:

1

这是因为7long, 7long, 5int, 所以7/5的结果是long/long, 结果是1. 如果我们想要得到1.4, 那么就需要将至少其中一个数转换为浮点数, 例如:

#include <iostream>

int main() {
    std::cout << float(7)/5 << std::endl; // C风格的类型转换
    return 0;
}

输出:

1.4

再来看下面的这个例子:

#include <iostream>

int main() {
    int result = 50000;
    short c = result;
    std::cout << c << std::endl;
    std::cout << sizeof(result) << std::endl>>
    std::cout << sizeof(c) << std:;endl;
    return 0;
}

输出:

-15536
4
2

你会发现, 咦, 为啥不是5000, 这是因为int的大小是4字节, 但是short的大小是2字节, 我们进行了隐式类型转换, 中间损失了两个字节.

C风格的转换会尝试显式转换, 直到找到一个可以成功还行的转换. 它的行为可以被理解为尝试以下C++转换, 大致按照顺序进行: 1.const_cast, 2.static_cast, 3.reinterpret_cast. 因此, C风格类型的转换功能非常强大, 但也因此不安全, 它会尝试"最不坏"的转换, 但可能不是程序员真正想要的, 强烈建议在C++代码中优先使用C++命名的转换, 因为它们更加明确, 更加安全.

比较

在不同的两个类型进行比较前, 编译器会自动对它们进行隐式类型转换, 已使它们具有相同的类型, 然后再进行比较. 所以在这个过程中, 可能会产生一些问题, 例如:

#include <iostream>

int main() {
    int i = -2;
    unsigned int u = 1;
    if (i > u) {
        std::cout << "huh?" << std::endl;
    }
    return 0;
}

输出:

huh?

可以看到, 这里即便i是-2, 但是结果是iu大. 对于这种情况, 我们大概有两种方法:

  1. 使用-Wall选项: 在编译的时候, 我们可以加上-Wall选项, 例如g++ -Wall my_program.cpp, 因为当你使用这个命令编译代码的时候, 编译器会像一个严格的代码审核员, 对你的代码进行更加深入的静态分析, 并报告它发现出的各种潜在问题.
  2. 使用std::cmp_greater进行比较.

static_cast

static_cast是 C++ 提供的四种主要转换运算符之一, 用于在编译时进行已知安全的类型转换. 它会在编译阶段根据类型信息执行相应转换, 不会进行运行时检查.

数值类型转型

常见用法有: 数值类型之间转换, 如 double 转 int:

int x = static_cast<int>(3.14);

实体类型转型

实体类型转换是指将一个类的实体对象按照继承链转为另一个类的实体对象. 分为两种, 一种是上转型(up casting), 一种是下转型(down casting). 上转型是指将派生对象转换为基类对象, 下转型是指将基类对象转换为派生类对象.

  1. 下转型: 编译直接报错, 不允许
  2. 上转型: 会发生对象切片, 只保留基类对象的部分, 派生类的部分会被丢弃.

指针类型转型

指针类型转换是指将一个类的指针按照继承链转为另一个类的指针. 分为两种, 一种是上转型(up casting), 一种是下转型(down casting). 上转型是指将派生对象指针转换为基类对象指针, 下转型是指将基类对象指针转换为派生类对象指针.

  1. 下转型: 编译的时候允许, 但是运行的时候不安全. 可能导致未定义行为, 见dynamic_cast部分
  2. 上转型: 编译和运行都安全, 不会发生切片

dynamic_cast

注意

dynamic_cast仅仅可以用于:

  • 至少有一个虚函数的类
  • 指针类型转换, 实体类型转型不适用, 数值类型转型不适用

指针类型上转型

指针类型上转型通常采用static_cast进行, 因为dynamic_cast在运行的时候会做额外的类型检查, 性能上更加耗费, 而上转型在运行时是安全的, 所以没必要用dynanmic_cast.

指针类型下转型

static_cast仅仅在编译期间进行类转换, 不会在运行的时候检查实际对象类型. dynamic_cast会通过RTTI(Run Time Type Information)检查对象的类型, 类似于Java的反射. 通常用于将基类的指针转为派生类的指针(down casting). 如果对象不是derived会返回nullptr, 如果对象是`derived会返回指向派生类的指针. 那么, 为什么不直接使用static_cast进行down casting呢? 来看下面的这个例子:

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() {}
};

class Derived: public Base {
public:
    void doDerived() {
        cout<<"Called Derived method"<<endl;
    }
};

class AnotherDerived: public Base {
public:
    void doAnother() {
        cout<<"Called AnotherDerived method"<<endl;
    }
};

void process(Base* ptr) {
    // 尝试使用static_cast将Base*转换为Derived*
    Derived* d1 = static_cast<Derived*>(ptr);
    // 如果ptr实际指向AnotherDerived, 以下调用会导致未定义行为
    d1->doDerived(); // 可能崩溃

    // 使用dynamic_cast更安全
    Derived* d2 = dynamic_cast<Derived*>(ptr);
    if(d2) {
        d2->doDerived();
    } else {
        cout<<"ptr不指向Derived, 转换失败"<<endl;
    }
}

int main() {
    Base* obj1 = new Derived();
    Base* obj2 = new AnotherDerived();

    process(obj1); // obj1实际指向Derived, 都能正常调用
    process(obj2); // obj2实际指向AnotherDerived, static_cast编译通过但在运行时调用doDerived未定义, dynamic_cast返回nullptr
    delete obj1;
    delete obj2;
    return 0;
}

你会发现, static_cast无论ptr是否指向Derived实例, 都会执行转换, 但是如果ptr指向其他类型, 然后你调用了其他类型的成员函数, 如d1->doDerived(), 这会导致未定义行为. dynamic_cast则会在运行时检查类型, 如果转换失败, 返回nullptr, 否则返回有效指针, 这样的转换更加安全. 这就是为啥static_cast通常用于up casting(将派生类指针转换为基类指针), 而dynamic_cast通常用于down casting(将基类指针转换为派生类指针).

总结来说, 如果想要dynamic_cast返回一个有效的指针, 需要<>里面的指针类型和ptr new的那个类型一致, 也就是说, 转换之后要能够调用new的那个类型的所有成员函数, 否则返回的是nullptr.

reinterpret_cast

reinterpret_cast是C++中一种非常底层的类型转换运算符. 它允许将任何指针类型转换为任何其他指针类型.

#include <iostream>

int main() {
    float pi = 3.14f;
    std::cout << &(pi) << std::endl;
    std::cout << reinterpret_cast<int*>(&pi) << std::endl;
    std::cout << reinterpret_cast<float*>(&pi) << std::endl;
    std::cout << *reinterpret_cast<int*>(&pi) << std::endl;
    std::cout << *reinterpret_cast<float*>(&pi) << std::endl;
    return 0;
}

输出:

0x7ffecd8dc924
0x7ffecd8dc924
0x7ffecd8dc924
1078523331
3.14

你会发现, 尽管他们的地址都是相同的, 但是存储的值却截然不同. 当通过*reinterpret_cast<int*>(&pi)访问时, 存储浮点数3.14f的内存位模式被当作整数解析, 得到了1078523331. 而通过*reinterpret_cast<float*>(&pi)访问时, 同样的位模式被正确解析为浮点数3.14.

这清晰地展示了reinterpret_cast的核心作用: 它不改变实际存储的二进制数据, 仅改变编译器如何解释这些数据对应的类型. 浮点数 (例如遵循IEEE 754标准) 与整数在内存中的二进制表示方式有本质区别. 因此, 同一段二进制码, 在不同的类型视角下会呈现出不同的数值.

那么, 什么时候用interpret_cast呢?

#include <iostream>
#include <cstring>

struct GameState {
    int level;
    int health;
    int points;
    bool GameComplete;
    bool BossDefeated;
};

int main() {
    GameState gs = {66, 100, 999, false, false};
    std::cout << sizeof(GameState) << std::endl << std::endl;
    char bagOfbytes[sizeof(GameState)];  // 假设这是一个文件, 我们要往里面写入GameState(即序列化)
    std::memcpy(bagOfbytes, &gs, sizeof(GameState));  // 将GameState的内容复制到bagOfbytes中
    // 现在, 我们要从文件中读取内容
    std::cout << *reinterpret_cast<int*>(bagOfbytes) << std::endl;
    std::cout << *reinterpret_cast<int*>(bagOfbytes + 4) << std::endl;
    std::cout << *reinterpret_cast<int*>(bagOfbytes + 8) << std::endl;
    std::cout << *reinterpret_cast<bool*>(bagOfbytes + 12) << std::endl;
    std::cout << *reinterpret_cast<bool*>(bagOfbytes + 13) << std::endl;
    return 0;
}

上面的GameState成员变量在内存中的分布为: level(4字节), health(4字节), points(4字节), GameComplete(1字节), BossDefeated(1字节), 由于最大的成员变量占用4个字节, 所以另外还有2个字节的padding, 所以整个GameState结构体占用16个字节.

输出:

16

66
100
999
0
0

#define的用法

文本替换

#define是C++预处理器指令, 用于创建宏. 宏主要有两种形式: 类对象宏 (object-like macros) 和类函数宏 (function-like macros).

  1. 类对象宏 (定义常量或符号):

    这种宏用于将一个标识符定义为一个特定的文本片段. 预处理器会在编译前将代码中所有出现的该标识符替换为指定的文本片段.

    语法:

    #define 标识符 替换文本
    

    示例:

    #define PI 3.14159
    #define GREETING "Hello, World!"
    #define MAX_USERS 100
    
    在预处理阶段:

    • double circumference = 2 * PI * radius; 会变成 double circumference = 2 * 3.14159 * radius;
    • std::cout << GREETING << std::endl; 会变成 std::cout << "Hello, World!" << std::endl;

    优点:

    • 可以提高代码的可读性, 用有意义的名称代替魔法数字或字符串.
    • 方便修改, 只需修改#define语句即可更新所有引用的地方.

    注意:

    • 替换文本可以是任何内容, 包括表达式, 但通常用于定义常量.
    • 宏定义不进行类型检查.
  2. 类函数宏 (定义带参数的宏):

    这种宏看起来像函数调用, 但实际上也是文本替换. 它可以接受参数.

    语法:

    #define 标识符(参数列表) 替换文本
    
    重要提示: 标识符和左括号(之间不能有空格.

    示例:

    #define SQUARE(x) ((x) * (x))
    #define MAX(a, b) (((a) > (b)) ? (a) : (b))
    #define PRINT_VAR(var) std::cout << #var << " = " << var << std::endl;
    
    在预处理阶段:

    • int area = SQUARE(5); 会变成 int area = ((5) * (5));
    • int larger = MAX(x + 1, y); 会变成 int larger = (((x + 1) > (y)) ? (x + 1) : (y));
    • PRINT_VAR(myVariable); 会变成 std::cout << "myVariable" << " = " << myVariable << std::endl; (这里的#var是字符串化操作符, 会将参数名转换为字符串)

    优点:

    • 可以减少函数调用的开销 (对于非常简单的操作).
    • 可以进行一些文本操作, 如字符串化 (#) 和标记连接 (##).

    重要注意事项 (陷阱):

    • 括号: 在宏定义中, 参数和整个表达式通常需要用括号括起来, 以避免操作符优先级问题. 例如, 如果SQUARE(x)定义为x*x, 那么SQUARE(2+3)会变成2+3*2+3, 结果是2+6+3=11, 而不是预期的5*5=25. 使用((x)*(x))可以避免这个问题.
    • 副作用: 如果参数带有副作用 (例如i++), 它们可能会被多次求值. 例如, int i = 3; int j = SQUARE(i++); 可能会变成 int j = ((i++) * (i++));, i的值会增加两次, 结果可能不是预期的.
    • 类型安全: 宏不进行类型检查, 这可能导致难以发现的错误.
    • 调试: 宏会在预处理阶段被替换掉, 调试时可能看到的是替换后的代码, 这会增加调试难度.
    • 作用域: 宏定义从其定义点开始到文件末尾有效, 或者直到遇到#undef指令.
    #define MY_VALUE 10
    // ... MY_VALUE is 10
    #undef MY_VALUE
    // ... MY_VALUE is no longer defined
    

尽管宏在某些情况下很有用 (例如条件编译中的标志, 非常简单的文本替换), 但在现代C++中, 对于类对象宏, 通常推荐使用constconstexpr变量, 因为它们具有类型安全且遵循作用域规则. 对于类函数宏, 通常推荐使用内联函数 (inline functions) 或模板 (templates), 它们也提供类型安全且行为更可预测.

条件编译

#define还可以用于条件编译, 通过与#ifdef, #ifndef, #if, #else, #elif, #endif等指令结合使用, 可以根据宏是否被定义来控制代码的编译. 例如:

以下是解释和示例:

当你使用#define定义一个宏时, 该宏就被认为是"已定义"状态.

#define MY_FEATURE_ENABLED
// 或者定义一个带值的宏 (尽管在条件编译中, 通常只检查是否定义)
// #define VERSION 2

然后, 其他预处理指令可以检查这个宏的状态:

  1. #ifdef 标识符: "if defined" (如果标识符已定义) 如果标识符通过#define定义了, 那么#ifdef和对应的#endif (或#else/#elif) 之间的代码块将被编译.

    #define DEBUG_MODE
    
    #ifdef DEBUG_MODE
      // 这部分代码只有在 DEBUG_MODE 被定义时才会被编译
      std::cout << "Debugging information..." << std::endl;
    #endif
    
  2. #ifndef 标识符: "if not defined" (如果标识符未定义) 如果标识符没有通过#define定义, 那么#ifndef和对应的#endif (或#else/#elif) 之间的代码块将被编译. 这常用于防止头文件被多次包含 (头文件保护符).

    #ifndef MY_HEADER_H
    #define MY_HEADER_H
      // 头文件内容
    #endif // MY_HEADER_H
    
  3. #if 表达式: "if expression is true" #if指令会计算后面的常量表达式. 如果表达式的结果为非零 (真), 则其后的代码块被编译. 你可以在表达式中使用通过#define定义的宏 (通常是代表数值的宏).

    #define VERSION_NUMBER 3
    
    #if VERSION_NUMBER > 2
      // 这部分代码只有在 VERSION_NUMBER 大于 2 时才会被编译
      std::cout << "Using features from version 3 or later." << std::endl;
    #elif VERSION_NUMBER == 2
      std::cout << "Using features from version 2." << std::endl;
    #else
      std::cout << "Using legacy features." << std::endl;
    #endif
    
    也可以使用defined(标识符)操作符在#if中检查宏是否被定义:
    #define FEATURE_A
    // #define FEATURE_B // FEATURE_B 未定义
    
    #if defined(FEATURE_A) && !defined(FEATURE_B)
      // 只有当 FEATURE_A 定义了且 FEATURE_B 未定义时, 这部分代码才会被编译
      std::cout << "Feature A is enabled, Feature B is not." << std::endl;
    #endif
    

还可以通过命令行参数定义宏

通过编译器的命令行参数来定义宏, 进而影响#if, #ifdef等条件编译指令的行为, 是一种非常常见且强大的实践. 编译器通常提供一个选项 (例如GCC/Clang中的-D, MSVC中的/D) 来在命令行中定义宏, 就像在代码中使用了#define一样.

工作原理:

  1. 在代码中: 你像之前那样使用条件编译指令.

    // main.cpp
    #include <iostream>
    
    #ifndef BUILD_MESSAGE
    #define BUILD_MESSAGE "Default build message."
    #endif
    
    #ifdef SPECIAL_FEATURE
    #define FEATURE_LEVEL 2
    #else
    #define FEATURE_LEVEL 1
    #endif
    
    int main() {
        std::cout << BUILD_MESSAGE << std::endl;
    
        #if FEATURE_LEVEL > 1
            std::cout << "Special feature is enabled at level " << FEATURE_LEVEL << std::endl;
        #else
            std::cout << "Basic feature set at level " << FEATURE_LEVEL << std::endl;
        #endif
    
        #ifdef DEBUG_BUILD
            std::cout << "This is a debug build." << std::endl;
        #endif
    
        return 0;
    }
    

  2. 在编译时: 你可以通过命令行参数传入宏定义.

    • 定义一个宏 (不带值, 主要用于#ifdefdefined()):

      g++ -DSPECIAL_FEATURE main.cpp -o main_special
      g++ -DDEBUG_BUILD main.cpp -o main_debug
      
      在第一个命令中, SPECIAL_FEATURE被定义了, 所以FEATURE_LEVEL会被设为2. 在第二个命令中, DEBUG_BUILD被定义了.

    • 定义一个带值的宏:

      g++ -DBUILD_MESSAGE="\"Custom message from command line\"" main.cpp -o main_custom_msg
      g++ -DSPECIAL_FEATURE -DFEATURE_LEVEL=3 main.cpp -o main_feature_level_3
      
      注意: 当宏的值是字符串时, 通常需要在命令行中额外使用引号来确保字符串被正确传递. 在第一个命令中, BUILD_MESSAGE会被设为"Custom message from command line". 在第二个命令中, SPECIAL_FEATURE被定义, 并且FEATURE_LEVEL被命令行直接定义为3, 这会覆盖代码中 #else 分支的 #define FEATURE_LEVEL 1. (如果SPECIAL_FEATURE未在命令行定义, 则代码中的逻辑会生效).

输出示例:

  • 执行 ./main_special:

    Default build message.
    Special feature is enabled at level 2
    

  • 执行 ./main_debug:

    Default build message.
    Basic feature set at level 1
    This is a debug build.
    

  • 执行 ./main_custom_msg:

    Custom message from command line
    Basic feature set at level 1
    

  • 执行 ./main_feature_level_3:

    Default build message.
    Special feature is enabled at level 3
    

总结:

#define用于创建这些条件编译指令赖以判断的"开关". 通过定义或取消定义不同的宏, 你可以有效地告诉预处理器哪些代码段应该包含在最终的编译单元中, 哪些应该被忽略.

常见用途:

  • 平台特定代码:
    #ifdef _WIN32
      // Windows特定代码
    #elif __linux__
      // Linux特定代码
    #endif
    
  • 调试代码的启用/禁用: 如第一个#ifdef DEBUG_MODE示例.
  • 功能切换: 根据需要启用或禁用实验性功能或可选模块.
  • 头文件保护: 防止头文件被重复包含, 如#ifndef MY_HEADER_H示例.

通过这种方式, #define与条件编译指令结合, 为C++代码的灵活性和可移植性提供了强大的机制.

__file____line__用法

__FILE____LINE__是C++中预定义的宏, 主要用于调试和日志记录.

  • __FILE__: 一个字符串字面量, 代表当前源文件的文件名.
  • __LINE__: 一个整型常量, 代表当前代码在源文件中的行号.
#include <iostream>

void log_message(const char* message, const char* file, int line) {
    std::cerr << "Message: " << message << " at " << file << ":" << line << std::endl;
}

#define LOG(msg) log_message(msg, __FILE__, __LINE__)

int main() {
    std::cout << "This is file: " << __FILE__ << " at line: " << __LINE__ << std::endl;
    int x = 10;
    if (x > 5) {
        LOG("x is greater than 5");
    }
    return 0;
}

当编译并运行上述代码时:

  • 第一条std::cout会打印出包含该语句的文件名和行号.
  • LOG("x is greater than 5");会展开为log_message("x is greater than 5", "your_file_name.cpp", 13);, 从而在错误输出流中打印包含文件名和行号的日志信息.

输出:

This is file: main.cpp at line: 10
Message: x is greater than 5 at main.cpp:13

它们帮助定位代码中产生信息或错误的确切位置.

std::source_location的用法

在C++20中, std::source_location提供了一种更强大和灵活的方式来获取源代码位置的信息. 它可以替代__FILE____LINE__, 提供更多的上下文信息, 如函数名、类名等.

使用示例:

#include <iostream>
#include <source_location>

void log_message(const char* message, const std::source_location& location = std::source_location::current()) {
    std::cerr << "Message: " << message 
              << " at " << location.file_name() 
              << ":" << location.line() 
              << " in function " << location.function_name() 
              << std::endl;
}

#define LOG(msg) log_message(msg)

int main() {
    LOG("This is a log message");
    return 0;
}

输出:

Message: This is a log message at main.cpp:15 in function int main()

内联变量

C++17引入了内联变量 (inline variables), 允许在头文件中定义变量 (包括全局变量和类的静态成员变量), 而不会违反"单一定义规则" (One Definition Rule, ODR).

核心要点:

  1. 目的: 解决在头文件中定义全局或静态变量时可能导致的多重定义链接错误.
  2. 关键字: 使用inline关键字.
  3. 行为: 即使头文件被多个.cpp文件包含, inline变量也确保在整个程序中只有一个实例. 链接器会合并所有定义为一个.
  4. 定义位置: 通常在头文件中定义.
  5. 适用场景:
    • 在头文件中定义全局常量或变量.
    • 在类定义内部初始化静态成员变量 (尤其是在C++17之前这比较麻烦, 通常需要在类外定义).

示例:

在头文件 (config.h) 中:

#ifndef CONFIG_H
#define CONFIG_H

#include <string>

// 内联全局变量
inline int globalMaxUsers = 100;
inline const std::string globalAppName = "MyApplication";

struct Settings {
    // 内联静态成员变量 (C++17起可以直接在类内初始化)
    inline static double version = 1.2;
    inline static const int defaultTimeout = 5000; // const static 成员C++17前也可类内初始化
};

#endif

在多个.cpp文件中使用:

// main.cpp
#include <iostream>
#include "config.h"

int main() {
    std::cout << "App Name: " << globalAppName << std::endl;
    std::cout << "Max Users: " << globalMaxUsers << std::endl;
    globalMaxUsers = 150; // 修改的是同一个变量实例
    std::cout << "Settings Version: " << Settings::version << std::endl;
    return 0;
}

// utils.cpp
#include <iostream>
#include "config.h"

void printConfig() {
    std::cout << "From utils - App Name: " << globalAppName << std::endl;
    std::cout << "From utils - Max Users: " << globalMaxUsers << std::endl; // 会看到main.cpp中修改后的值
    std::cout << "From utils - Settings Version: " << Settings::version << std::endl;
}

如果config.hmain.cpputils.cpp都包含, globalMaxUsers, globalAppName, Settings::version, 和 Settings::defaultTimeout 都将只有一个定义和实例在整个程序中, 避免了链接错误.

对于类的static const整型或枚举成员, C++17之前就可以在类内初始化. inline变量将此能力扩展到其他类型的静态成员变量和全局变量, 使得在头文件中管理它们更为便捷.

如果在头文件中没有使用inline会怎样

如果你在头文件中定义一个普通的全局变量 (非const且没有inline关键字), 并且这个头文件被多个.cpp文件 (编译单元) 包含, 那么在链接阶段通常会导致多重定义 (multiple definition) 错误.

原因:

C++遵循单一定义规则 (One Definition Rule, ODR). 对于具有外部链接的实体 (如全局变量或非内联函数), 在整个程序中只能有一个定义.

当头文件被多个.cpp文件#include时:

  1. 预处理器会将头文件的内容复制到每个包含它的.cpp文件中.
  2. 如果头文件中有一个变量定义, 例如 int myGlobalVar = 100;, 那么每个.cpp文件在被编译成目标文件 (.o.obj) 时, 都会包含myGlobalVar的一个定义.
  3. 当链接器试图将这些目标文件链接成一个可执行程序时, 它会发现多个名为myGlobalVar的全局变量的定义, 这就违反了ODR.

示例:

myheader.h:

#ifndef MYHEADER_H
#define MYHEADER_H

// 没有inline关键字的全局变量定义
int sharedCounter = 0;
// 注意: 如果是 const int sharedCounter = 0; 行为会不同 (默认为内部链接)

#endif

file1.cpp:

#include "myheader.h"
#include <iostream>

void incrementCounter() {
    sharedCounter++;
    std::cout << "File1: " << sharedCounter << std::endl;
}

file2.cpp:

#include "myheader.h"
#include <iostream>

void printCounter() {
    std::cout << "File2: " << sharedCounter << std::endl;
}

// 假设main函数也在这里或者另一个包含myheader.h的文件中
// int main() {
//     incrementCounter();
//     printCounter();
//     return 0;
// }

编译和链接 (例如使用g++):

g++ -c file1.cpp -o file1.o
g++ -c file2.cpp -o file2.o
g++ file1.o file2.o -o program

在链接步骤 (g++ file1.o file2.o -o program), 你很可能会遇到类似以下的链接器错误:

/usr/bin/ld: file2.o:(.data+0x0): multiple definition of `sharedCounter'; file1.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
这个错误明确指出sharedCounter被多次定义了.

如何避免 (没有inline关键字的传统方法):

  1. 声明在头文件: 在头文件中使用extern关键字声明变量, 表示这个变量在其他地方定义. myheader.h:
    #ifndef MYHEADER_H
    #define MYHEADER_H
    extern int sharedCounter; // 声明
    #endif
    
  2. 定义在一个源文件: 在一个且仅一个.cpp文件中提供该变量的实际定义. file1.cpp (或其他某个.cpp文件):
    #include "myheader.h"
    int sharedCounter = 0; // 定义
    // ... rest of file1.cpp
    

C++17的inline变量简化了这个过程, 允许你直接在头文件中定义变量, 而编译器/链接器会确保只有一个实例存在, 避免了上述的多重定义问题.

特例: const全局变量 如果全局变量在头文件中被声明为const (例如 const int MAX_VALUE = 10;), 它默认具有内部链接 (internal linkage). 这意味着每个包含该头文件的编译单元都会有它自己的独立副本, 并且不会导致链接错误. 但它们不是同一个变量实例. 如果你需要一个所有编译单元共享的const变量实例, 你会使用extern const声明并结合一个.cpp文件中的定义, 或者自C++17起使用inline const.

size_t的用法

size_t是一个无符号类型, 它的具体大小(位数)是和平台相关的, 在32位系统上, 通常是32位无符号整数而在64位系统上, 通常是64位无符号整数. 这种设计确保了size_t足够大, 能够存储任何理论上可能存在的对象的大小. 所以, 为什么要使用size_t呢?

  1. 避免循环中的负数和溢出

    #include <iostream>
    #include <vector>
    #include <limits> // 用于检查max()
    
    int main() {
        std::vector<int> myVector;
        // 假设myVector被填充了大量元素 甚至超过20亿个
    
        // 错误示例: 使用int作为循环变量
        // 如果myVector.size()超过INT_MAX(大约20亿) 这个循环会产生错误行为或溢出
        // for (int i = 0; i < myVector.size(); ++i) {
        //     // ...
        // }
    
        // 正确示例: 使用size_t作为循环变量
        // size_t可以处理非常大的尺寸 即使myVector有数十亿个元素也能正常工作
        for (size_t i = 0; i < myVector.size(); ++i) {
            // 访问元素 myVector[i]
        }
    
        std::cout << "Maximum value of int: " << std::numeric_limits<int>::max() << std::endl;
        std::cout << "Maximum value of size_t: " << std::numeric_limits<size_t>::max() << std::endl;
        // 你会发现size_t的最大值远大于int的最大值
        // 在64位系统上 size_t的最大值大约是1.8e19
        // 在32位系统上 size_t的最大值大约是4.2e9 (也大于int的2.1e9)
    
        return 0;
    }
    
  2. 处理sizeof运算符的返回值

    #include <iostream>
    
    int main() {
        int arr[] = {1, 2, 3, 4, 5};
        size_t arrayBytes = sizeof(arr); // arr占用的总字节数
        size_t elementBytes = sizeof(arr[0]); // 单个元素占用的字节数
    
        size_t numberOfElements = arrayBytes / elementBytes;
    
        std::cout << "Array total bytes: " << arrayBytes << std::endl;
        std::cout << "Element bytes: " << elementBytes << std::endl;
        std::cout << "Number of elements: " << numberOfElements << std::endl; // 输出 5
    
        return 0;
    }
    
  3. 在字符串和向量操作中使用

    #include <iostream>
    #include <string>
    #include <vector>
    
    int main() {
        std::string text = "Hello world";
        // length()和size()都返回size_t
        size_t textLength = text.length();
        std::cout << "Text length: " << textLength << std::endl; // 输出 11
    
        // 查找字符时 find() 返回 size_t (位置)
        size_t pos = text.find('o');
        if (pos != std::string::npos) { // std::string::npos 也是 size_t 类型
            std::cout << "First 'o' found at position: " << pos << std::endl; // 输出 4
        }
    
        std::vector<double> scores = {90.5, 88.0, 95.5, 76.0};
        size_t numScores = scores.size();
        std::cout << "Number of scores: " << numScores << std::endl; // 输出 4
    
        // 遍历向量
        for (size_t i = 0; i < numScores; ++i) {
            std::cout << "Score " << i << ": " << scores[i] << std::endl;
        }
    
        return 0;
    }
    

评论