数据处理

大小端转换

大端和小端是指字节序的存储方式,大端是指高位字节存储在低地址,小端是指低位字节存储在低地址。

  1. 基本定义与存储方式
    大端模式
    • 高位字节存放在低地址上,低位字节存放在高地址上。
    • 0x12345678在内存中的存储方式为:低地址--> 0x12 0x34 0x56 0x78 -->高地址
      小端模式
    • 高位字节存放在高地址上,低位字节存放在低地址上。
    • 0x12345678在内存中的存储方式为: 低地址--> 0x78 0x56 0x34 0x12 -->高地址

直观对比​(以0x1234为例):

|内存地址 |大端模式| 小端模式|
|–|----|----|----|
|0x4000 |0x12 |0x34|
|0x4001 |0x34 |0x12|

  1. 设计原因与应用场景

为何存在两种模式?

  • 硬件差异: 不同处理器架构对多字节数据的处理逻辑不同。例如,x86架构(如Intel/AMD CPU)采用小端模式,而PowerPC、MIPS等处理器多采用大端模式。
  • ​效率考量: 小端模式在强制类型转换时无需调整字节顺序(直接截取低地址数据即可),而大端模式便于快速判断符号位(符号位位于低地址)

应用场景

  1. 网络通信

    • 网络传输: TCP/IP协议规定数据传输采用大端模式(网络字节序),因此小端设备需通过htonl()等函数转换字节序传输。
    • 例如,发送0x12345678时,小端设备需先转换为大端模式0x78563412再传输,接收后需转回小端。
  2. 跨平台开发

    • 文件存储(如二进制文件)需明确字节序,否则不同平台读取时可能出错。
    • 例如,嵌入式设备与服务器通信时需统一字节序。
  3. 判断当前系统的字节序
    可通过以下方法检测当前系统是大端还是小端:

  • **联合体法:**利用联合体的内存共享特性读取多字节数据的首字节。

#include <iostream>
using namespace std;

bool isLittleEndian(){

    union { 
        uint32_t i; 
        uint8_t c[4]; 
    } test = {0x01020304};

    return test.c[0] == 0x04; // 若为小端,则首字节为0x04
}
  • **宏定义法:**利用宏定义计算多字节数据的值。
    使用__BYTE_ORDER宏(Linux/Unix)或std::endian::native(C++20)直接判断
  1. 字节序转换函数
    • 大端转小端:
      • uint16_t ntohs(uint16_t netshort);:将16位无符号整数从网络字节序转换为主机字节序。(windows下为htons)
      • uint32_t ntohl(uint32_t netlong);:将32位无符号整数从网络字节序转换为主机字节序。(windows下为htonl)
    • 小端转大端:
      • uint16_t htons(uint16_t hostshort);:将16位无符号整数从主机字节序转换到网络字节序。(windows下为ntohs)

野指针

指向无效内存地址(如未初始化、已释放或越界的内存)的指针,访问时可能导致程序崩溃或数据损坏

int* p;  
*p = 10;  // p 未初始化,指向随机地址(野指针)  

野指针的常见成因:

|场景 |描述 |示例|
|–|----|----|----|
|​未初始化指针 |指针变量未赋初值,默认指向随机内存区域| int* p ;(未初始化为 NULL 或有效地址)|
|​释放后未置空 |freedelete 后未将指针设为 NULL,仍指向已释放内存| free(p) ; 后未执行 p = NULL;|
|​越界访问 |指针操作超出变量作用域(如返回局部变量地址)| 函数返回栈内存指针导致后续访问失效|

规避野指针的最佳实践

  1. 初始化与置空
    • **初始化:**在声明指针时立即赋初值,避免指向随机内存。
    int* p = NULL;  // 初始化为 NULL  
    
    • **置空:**释放内存后立即将指针置为 NULL,防止重复释放或访问已释放内存。
    free(p);  
    p = NULL;    
    
  2. 作用域与生命周期管理
    • 作用域: 确保指针操作在变量有效作用域内,避免返回局部变量地址。
    • 边界检查: 访问数组元素时进行边界检查,防止越界访问。
    • 智能指针: 使用智能指针(如 std::unique_ptrstd::shared_ptr)自动管理内存,避免手动释放内存带来的问题。
  3. 多线程环境下的防御措施
    • 隔离线程数据: 通过动态分配或线程局部存储(thread_local)为每个线程提供独立数据副本,避免数据竞争。
    • 互斥锁: 在多线程访问共享数据时使用互斥锁(mutex)保护,防止并发访问导致的问题。
    • 原子操作: 使用原子操作(atomic)确保对共享数据的访问是线程安全的。

拷贝构造与运算符重载

拷贝构造函数和赋值运算符重载是C++中用于对象复制的两种机制,它们在对象创建和赋值时非常重要。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并将另一个对象的值复制到新对象中。它的定义形式如下:

ClassName(const ClassName& other);

其中,ClassName是类名,other是另一个对象,类型为ClassName的引用。

拷贝构造函数的用途:

  1. 对象初始化: 当使用一个已存在的对象来初始化另一个对象时,拷贝构造函数会被调用。
ClassName obj1;
ClassName obj2 = obj1;  // 调用拷贝构造函数
  1. 函数参数传递: 当将一个对象作为函数参数传递时,拷贝构造函数会被调用。
void func(ClassName obj) {
    // ...
}

ClassName obj1;
func(obj1);  // 调用拷贝构造函数
  1. 函数返回值: 当一个函数返回一个对象时,拷贝构造函数会被调用。
ClassName func() {
    ClassName obj;  // ...
    return obj;  // 调用拷贝构造函数
}

赋值运算符重载

赋值运算符重载是一种运算符重载,用于将一个对象的值赋给另一个对象。它的定义形式如下:

ClassName& operator=(const ClassName& other);

其中,ClassName是类名,other是另一个对象,类型为ClassName的引用。

赋值运算符重载的用途:

  1. 对象赋值: 当使用一个已存在的对象来赋值给另一个对象时,赋值运算符重载会被调用。
ClassName obj1;
ClassName obj2;
obj2 = obj1;  // 调用赋值运算符重载
  1. 函数返回值: 当一个函数返回一个对象时,赋值运算符重载会被调用。
ClassName func() {
    ClassName obj;  // ...
    return obj;  // 调用赋值运算符重载
}

拷贝构造函数与赋值运算符重载的区别

  1. 调用时机:

    • 拷贝构造函数在对象创建时调用,用于初始化新对象。 - 赋值运算符重载在对象赋值时调用,用于将一个对象的值赋给另一个对象。
    • 例如,当使用一个已存在的对象来初始化另一个对象时,会调用拷贝构造函数;当使用一个已存在的对象来赋值给另一个对象时,会调用赋值运算符重载。
  2. 参数类型:

    • 拷贝构造函数的参数类型为const ClassName&,表示引用另一个对象。 - 赋值运算符重载的参数类型为const ClassName&,表示引用另一个对象。
    • 例如,拷贝构造函数的参数类型为const ClassName&,表示引用另一个对象;赋值运算符重载的参数类型为const ClassName&,表示引用另一个对象。
  3. 返回值类型:

    • 拷贝构造函数的返回值类型为ClassName,表示返回新对象。 - 赋值运算符重载的返回值类型为ClassName&,表示返回对象的引用。

拷贝构造函数与赋值运算符重载的示例

class MyClass {
public:
    int data;

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        data = other.data;
    }

    // 赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            data = other.data;
        }
        return *this;
    }
};

拷贝构造函数与赋值运算符重载的最佳实践

  1. 避免浅拷贝: 在拷贝构造函数和赋值运算符重载中,应避免浅拷贝,即直接复制指针而不是复制指针指向的内容。如果需要复制指针指向的内容,可以使用深拷贝。
  2. 避免循环引用: 在拷贝构造函数和赋值运算符重载中,应避免循环引用,即两个对象相互引用对方。如果需要相互引用,可以使用智能指针(如std::shared_ptr)来管理对象的生命周期。
  3. 避免内存泄漏: 在拷贝构造函数和赋值运算符重载中,应避免内存泄漏,即释放内存后未将指针置为NULL。如果需要释放内存,应将指针置为NULL
  4. 避免异常安全: 在拷贝构造函数和赋值运算符重载中,应确保异常安全,即当异常发生时,对象的状态应保持不变。可以使用RAII(Resource Acquisition Is Initialization)模式来确保资源正确释放。
  5. 避免未定义行为: 在拷贝构造函数和赋值运算符重载中,应避免未定义行为,即访问未初始化的指针或释放已释放的内存。可以使用断言(assert)来检测指针是否为NULL,或使用智能指针来管理内存。
  6. 避免性能问题: 在拷贝构造函数和赋值运算符重载中,应避免性能问题,即复制大量数据或进行复杂的操作。可以使用移动语义(move semantics)来优化性能。
  7. 避免代码重复: 在拷贝构造函数和赋值运算符重载中,应避免代码重复,即重复编写相同的代码。可以使用模板函数或模板类来减少代码重复。

浅拷贝与深拷贝

浅拷贝

仅复制对象的顶层结构(如指针或引用),底层数据仍共享同一内存地址。修改拷贝后的对象会影响原对象

场景

  • 默认拷贝构造函数:C++ 中类未重写拷贝构造函数时,默认进行浅拷贝。
  • ​数组/结构体赋值:直接复制数组或结构体时,引用类型成员共享地址

风险

  • **​数据竞争:**多线程环境下共享数据可能引发不一致问题。
  • **​双重释放:**若共享内存被多次释放,可能导致内存泄漏或崩溃

浅拷贝的示例代码如下:

class MyClass {
public:
    int data;
    int* ptr;

    MyClass(int value) : data(value), ptr(new int(value)) {}

    // 浅拷贝构造函数
    MyClass(const MyClass& other) : data(other.data), ptr(other.ptr) {}

    // 浅拷贝赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            data = other.data;
            ptr = other.ptr;
        }
        return *this;
    }
};

深拷贝

复制对象的顶层结构及底层数据,确保拷贝后的对象与原对象完全独立。修改拷贝后的对象不会影响原对象

实现方式

  • 手动递归复制: 对于复杂对象,手动递归复制每个成员变量。
  • 拷贝构造函数: 重载拷贝构造函数,实现深拷贝。
  • 赋值运算符重载: 重载赋值运算符重载,实现深拷贝。

优势

  • 数据独立: 拷贝后的对象与原对象完全独立,修改一个对象不影响另一个对象。
  • 内存安全: 避免数据竞争和双重释放问题,确保内存安全。

劣势

  • 性能开销: 深拷贝需要复制大量数据,可能导致性能开销。

深拷贝的示例代码如下:

class MyClass {
public:
    int data;
    int* ptr;

    MyClass(int value) : data(value), ptr(new int(value)) {}

    // 深拷贝构造函数
    MyClass(const MyClass& other) : data(other.data), ptr(new int(*other.ptr)) {}

    // 深拷贝赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            data = other.data;
            delete ptr;  // 释放原指针指向的内存
            ptr = new int(*other.ptr);  // 分配新内存并复制数据
        }
        return *this;
    }
};

浅拷贝与深拷贝的区别

| 特性 | 浅拷贝 | 深拷贝 |

| :---- | :----- | :------ |

| 复制方式 | 复制顶层结构,共享底层数据 | 复制顶层结构及底层数据,独立内存 |

| 修改影响 | 修改拷贝后的对象会影响原对象 | 修改拷贝后的对象不影响原对象 |

| 内存管理 | 共享内存,可能导致数据竞争或内存泄漏 | 独立内存,内存安全 |

| 性能开销 | 较低,直接复制指针 | 较高,需要复制大量数据 |