数据处理
大小端转换
大端和小端是指字节序的存储方式,大端是指高位字节存储在低地址,小端是指低位字节存储在低地址。
- 基本定义与存储方式
大端模式- 高位字节存放在低地址上,低位字节存放在高地址上。
0x12345678
在内存中的存储方式为:低地址--> 0x12 0x34 0x56 0x78 -->高地址
小端模式- 高位字节存放在高地址上,低位字节存放在低地址上。
0x12345678
在内存中的存储方式为:低地址--> 0x78 0x56 0x34 0x12 -->高地址
直观对比(以0x1234
为例):
|内存地址 |大端模式| 小端模式|
|–|----|----|----|
|0x4000 |0x12 |0x34|
|0x4001 |0x34 |0x12|
- 设计原因与应用场景
为何存在两种模式?
- 硬件差异: 不同处理器架构对多字节数据的处理逻辑不同。例如,x86架构(如Intel/AMD CPU)采用小端模式,而PowerPC、MIPS等处理器多采用大端模式。
- 效率考量: 小端模式在强制类型转换时无需调整字节顺序(直接截取低地址数据即可),而大端模式便于快速判断符号位(符号位位于低地址)
应用场景
-
网络通信
- 网络传输: TCP/IP协议规定数据传输采用大端模式(网络字节序),因此小端设备需通过
htonl()
等函数转换字节序传输。 - 例如,发送
0x12345678
时,小端设备需先转换为大端模式0x78563412
再传输,接收后需转回小端。
- 网络传输: TCP/IP协议规定数据传输采用大端模式(网络字节序),因此小端设备需通过
-
跨平台开发
- 文件存储(如二进制文件)需明确字节序,否则不同平台读取时可能出错。
- 例如,嵌入式设备与服务器通信时需统一字节序。
-
判断当前系统的字节序
可通过以下方法检测当前系统是大端还是小端:
- **联合体法:**利用联合体的内存共享特性读取多字节数据的首字节。
#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)直接判断
- 字节序转换函数
- 大端转小端:
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 或有效地址)|
|释放后未置空 |free
或 delete
后未将指针设为 NULL
,仍指向已释放内存| free(p)
; 后未执行 p = NULL
;|
|越界访问 |指针操作超出变量作用域(如返回局部变量地址)| 函数返回栈内存指针导致后续访问失效|
规避野指针的最佳实践
- 初始化与置空
- **初始化:**在声明指针时立即赋初值,避免指向随机内存。
int* p = NULL; // 初始化为 NULL
- **置空:**释放内存后立即将指针置为
NULL
,防止重复释放或访问已释放内存。
free(p); p = NULL;
- 作用域与生命周期管理
- 作用域: 确保指针操作在变量有效作用域内,避免返回局部变量地址。
- 边界检查: 访问数组元素时进行边界检查,防止越界访问。
- 智能指针: 使用智能指针(如
std::unique_ptr
、std::shared_ptr
)自动管理内存,避免手动释放内存带来的问题。
- 多线程环境下的防御措施
- 隔离线程数据: 通过动态分配或线程局部存储(thread_local)为每个线程提供独立数据副本,避免数据竞争。
- 互斥锁: 在多线程访问共享数据时使用互斥锁(mutex)保护,防止并发访问导致的问题。
- 原子操作: 使用原子操作(atomic)确保对共享数据的访问是线程安全的。
拷贝构造与运算符重载
拷贝构造函数和赋值运算符重载是C++中用于对象复制的两种机制,它们在对象创建和赋值时非常重要。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并将另一个对象的值复制到新对象中。它的定义形式如下:
ClassName(const ClassName& other);
其中,ClassName
是类名,other
是另一个对象,类型为ClassName
的引用。
拷贝构造函数的用途:
- 对象初始化: 当使用一个已存在的对象来初始化另一个对象时,拷贝构造函数会被调用。
ClassName obj1;
ClassName obj2 = obj1; // 调用拷贝构造函数
- 函数参数传递: 当将一个对象作为函数参数传递时,拷贝构造函数会被调用。
void func(ClassName obj) {
// ...
}
ClassName obj1;
func(obj1); // 调用拷贝构造函数
- 函数返回值: 当一个函数返回一个对象时,拷贝构造函数会被调用。
ClassName func() {
ClassName obj; // ...
return obj; // 调用拷贝构造函数
}
赋值运算符重载
赋值运算符重载是一种运算符重载,用于将一个对象的值赋给另一个对象。它的定义形式如下:
ClassName& operator=(const ClassName& other);
其中,ClassName
是类名,other
是另一个对象,类型为ClassName
的引用。
赋值运算符重载的用途:
- 对象赋值: 当使用一个已存在的对象来赋值给另一个对象时,赋值运算符重载会被调用。
ClassName obj1;
ClassName obj2;
obj2 = obj1; // 调用赋值运算符重载
- 函数返回值: 当一个函数返回一个对象时,赋值运算符重载会被调用。
ClassName func() {
ClassName obj; // ...
return obj; // 调用赋值运算符重载
}
拷贝构造函数与赋值运算符重载的区别
-
调用时机:
- 拷贝构造函数在对象创建时调用,用于初始化新对象。 - 赋值运算符重载在对象赋值时调用,用于将一个对象的值赋给另一个对象。
- 例如,当使用一个已存在的对象来初始化另一个对象时,会调用拷贝构造函数;当使用一个已存在的对象来赋值给另一个对象时,会调用赋值运算符重载。
-
参数类型:
- 拷贝构造函数的参数类型为
const ClassName&
,表示引用另一个对象。 - 赋值运算符重载的参数类型为const ClassName&
,表示引用另一个对象。 - 例如,拷贝构造函数的参数类型为
const ClassName&
,表示引用另一个对象;赋值运算符重载的参数类型为const ClassName&
,表示引用另一个对象。
- 拷贝构造函数的参数类型为
-
返回值类型:
- 拷贝构造函数的返回值类型为
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;
}
};
拷贝构造函数与赋值运算符重载的最佳实践
- 避免浅拷贝: 在拷贝构造函数和赋值运算符重载中,应避免浅拷贝,即直接复制指针而不是复制指针指向的内容。如果需要复制指针指向的内容,可以使用深拷贝。
- 避免循环引用: 在拷贝构造函数和赋值运算符重载中,应避免循环引用,即两个对象相互引用对方。如果需要相互引用,可以使用智能指针(如
std::shared_ptr
)来管理对象的生命周期。 - 避免内存泄漏: 在拷贝构造函数和赋值运算符重载中,应避免内存泄漏,即释放内存后未将指针置为
NULL
。如果需要释放内存,应将指针置为NULL
。 - 避免异常安全: 在拷贝构造函数和赋值运算符重载中,应确保异常安全,即当异常发生时,对象的状态应保持不变。可以使用RAII(Resource Acquisition Is Initialization)模式来确保资源正确释放。
- 避免未定义行为: 在拷贝构造函数和赋值运算符重载中,应避免未定义行为,即访问未初始化的指针或释放已释放的内存。可以使用断言(assert)来检测指针是否为
NULL
,或使用智能指针来管理内存。 - 避免性能问题: 在拷贝构造函数和赋值运算符重载中,应避免性能问题,即复制大量数据或进行复杂的操作。可以使用移动语义(move semantics)来优化性能。
- 避免代码重复: 在拷贝构造函数和赋值运算符重载中,应避免代码重复,即重复编写相同的代码。可以使用模板函数或模板类来减少代码重复。
浅拷贝与深拷贝
浅拷贝
仅复制对象的顶层结构(如指针或引用),底层数据仍共享同一内存地址。修改拷贝后的对象会影响原对象
场景
- 默认拷贝构造函数: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;
}
};
浅拷贝与深拷贝的区别
| 特性 | 浅拷贝 | 深拷贝 |
| :---- | :----- | :------ |
| 复制方式 | 复制顶层结构,共享底层数据 | 复制顶层结构及底层数据,独立内存 |
| 修改影响 | 修改拷贝后的对象会影响原对象 | 修改拷贝后的对象不影响原对象 |
| 内存管理 | 共享内存,可能导致数据竞争或内存泄漏 | 独立内存,内存安全 |
| 性能开销 | 较低,直接复制指针 | 较高,需要复制大量数据 |