C++字符串分割
在C++中,我们可以使用多种方法来分割字符串。以下是一些常见的方法:
方法一:使用 std::istringstream 和 std::getline
这种方法不依赖于任何第三方库,只需要标准库即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>
#include <sstream>
#include <string>
std::vector<std::string> split(const std::string &s, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
方法二:使用 std::strtok
尽管 std::strtok 是一个C库函数,但是它也可以在C++中使用。这个函数会修改原始字符串,所以如果你不希望原始字符串被修改,你需要复制一份。
注意:std::strtok 不是线程安全的。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>
#include <cstring>
#include <string>
std::vector<std::string> split(char *str, const char *delimiters) {
std::vector<std::string> tokens;
char *token = std::strtok(str, delimiters);
while (token != nullptr) {
tokens.push_back(std::string(token));
token = std::strtok(nullptr, delimiters);
}
return tokens;
}
方法三:使用 Boost 库的 boost::split
如果你的项目已经使用了 Boost 库,你可以使用 boost::split 函数来分割字符串。
1
2
3
4
5
6
7
8
#include <vector>
#include <boost/algorithm/string.hpp>
std::vector<std::string> split(const std::string &s, const std::string &delimiters) {
std::vector<std::string> tokens;
boost::split(tokens, s, boost::is_any_of(delimiters));
return tokens;
}
以上三种方法都可以用来分割字符串,你可以根据你的项目需求和你的编程环境来选择最适合的一种。
C++计算结构体成员偏移量
构建一个宏函数,提供结构体类型和成员变量名称,计算该变量在结构体中的偏移量。
在C++中,你可以使用offsetof宏来获取结构体中成员的偏移量。这是一个预处理器宏,它接受两个参数:一个类型和该类型的成员名称,然后返回该成员在该类型对象中的字节偏移量。
下面是如何使用offsetof宏的一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <cstddef>
struct MyStruct {
int a;
double b;
char c;
};
int main() {
std::cout << "Offset of 'a' : " << offsetof(MyStruct, a) << "\\n";
std::cout << "Offset of 'b' : " << offsetof(MyStruct, b) << "\\n";
std::cout << "Offset of 'c' : " << offsetof(MyStruct, c) << "\\n";
return 0;
}
在这个例子中,offsetof(MyStruct, a)返回的是a成员在MyStruct中的偏移量,这通常是0,因为a是结构体的第一个成员。offsetof(MyStruct, b)和offsetof(MyStruct, c)分别返回b和c成员的偏移量。
注意,offsetof宏只能用于POD(Plain Old Data)类型。如果你尝试使用它来获取非POD类型的成员偏移量,那么结果是未定义的。在C++中,POD类型是简单的、可以通过复制内存来复制其值的类型,例如内置类型、数组和结构。
offsetof宏的具体实现可能因编译器的不同而有所不同,但通常,它基于一个事实,那就是在C++中,一个结构体或类的实例的地址就是它的首个数据成员的地址。offsetof宏通常定义如下:
1
#define offsetof(type, member) ((size_t) &((type*)0)->member)
这个宏首先将0强制转换为type指针,这样得到的就是一个指向地址0的type指针,然后它通过指针访问成员member的地址。由于type的实例在地址0,所以访问的地址就是member的偏移量。
注意,虽然这个宏在许多情况下可以正常工作,但是它实际上是未定义的行为,因为它试图访问空指针的成员。然而,由于offsetof宏只是计算地址而不是真正访问成员,所以在实际中,这通常不会造成问题。
至于POD(Plain Old Data)类型,它包括了C++中一些简单的类型,这些类型可以通过复制内存来复制其值,例如:
- 所有基本数据类型,例如
int、char、double等。 - 指针类型。
- POD类型的数组。
- 不包含构造函数、析构函数、或虚函数的结构体或类,其所有非静态成员都是POD类型,且没有任何基类或私有或保护成员。
简单来说,POD类型是那些可以用简单的内存复制来完全复制其值的类型,这使得它们可以兼容C语言的数据类型和结构。
编译
编译表示
编译器在将源代码转换为目标代码的过程中,常常会生成一种或多种中间表示(Intermediate Representation, IR)。这些中间表示有多种形式,包括但不限于以下几种:
- 抽象语法树(Abstract Syntax Tree, AST):这是源代码的高级表示,它基本上保留了源代码的结构信息。这种形式的中间表示在语法分析阶段生成。
- 三地址代码(Three-Address Code, TAC):这是一种低级表示,每条指令最多包含三个操作数。逻辑上,每个操作数可以是一个变量、一个常量或者一个间接引用。三地址代码是一种更接近于目标代码的中间表示,通常在优化阶段使用。
- 四元式(Quadruples):四元式是一种特殊的三地址代码,它明确地将每个操作符和它的操作数分开。因此,四元式的每条指令都包含四个部分:操作符、两个源操作数和一个目标操作数。
- 逆波兰表示(Reverse Polish Notation, RPN):逆波兰表示是一种没有括号的算术表示方法,它将操作符放在操作数的后面。这种表示方法的优点是它消除了运算优先级和括号的需要。
- N元表示(N-Tuples):N元表示是一种更一般的方式,它可以表示包含任意数量操作数的操作。这种表示方法通常用于更复杂的编程语言和机器语言。
这些中间表示从不同的角度抽象了源代码的信息,可以帮助编译器进行诸如优化这样的操作。它们的选择和设计取决于编译器的目标和源语言的特性。
一些特殊概念
RAII
RAII是Resource Acquisition Is Initialization的缩写,意为“资源获取即初始化”,是一种C++编程技巧,用于管理资源的生命周期。在RAII中,资源的获取和释放是与对象的生命周期绑定在一起的,即资源的获取在对象构造时进行,资源的释放在对象析构时进行,从而确保资源的正确获取和释放,避免资源泄漏和使用错误。
RAII的核心思想是将资源的管理和对象的生命周期绑定在一起,利用对象的构造和析构函数管理资源,从而保证资源的正确获取和释放。在C++中,可以使用智能指针、容器、锁等RAII封装类来管理资源。
例如,我们可以使用std::unique_ptr来管理动态分配的内存,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
int main() {
// 使用unique_ptr管理动态分配的内存
std::unique_ptr<int> ptr(new int(10));
// 使用ptr指向的内存
int value = *ptr;
// 当ptr离开作用域时,会自动释放它所管理的内存
return 0;
}
在上述示例中,我们使用std::unique_ptr来管理动态分配的内存。当ptr离开作用域时,会自动调用析构函数,释放它所管理的内存,从而避免了内存泄漏的问题。
另外,RAII还可以用于管理其他资源,如文件句柄、网络连接、锁等,例如使用std::lock_guard来管理std::mutex的锁,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <mutex>
#include <thread>
std::mutex mtx;
void foo() {
// 创建一个lock_guard对象,自动获取mtx的锁
std::lock_guard<std::mutex> lock(mtx);
// 在临界区域执行操作
// ...
}
int main() {
std::thread t1(foo);
std::thread t2(foo);
t1.join();
t2.join();
return 0;
}
在上述示例中,我们使用std::lock_guard来管理std::mutex的锁,它会在构造函数中获取锁,在析构函数中释放锁,从而避免了忘记释放锁的问题。
总之,RAII是一种重要的C++编程技巧,可以有效地管理资源的生命周期,避免资源泄漏和使用错误,提高代码的可靠性和安全性。
内存布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct MyStruct
{
int a;
double b;
char c;
void test() { std::cout << "test" << std::endl; }
};
int main()
{
std::cout << "Offset of 'a' : " << offsetof(MyStruct, a) << "\n";
std::cout << "Offset of 'b' : " << offsetof(MyStruct, b) << "\n";
std::cout << "Offset of 'c' : " << offsetof(MyStruct, c) << "\n";
std::cout << "size " << sizeof(MyStruct) << std::endl;
return 0;
}
输出:
1
2
3
4
Offset of 'a' : 0
Offset of 'b' : 8
Offset of 'c' : 16
size 24
成员函数:
成员函数(或者称为方法)不像数据成员那样存储在对象的内存布局中。这是因为无论你创建多少个对象,每个对象都会有自己的数据成员的副本,但它们共享同一个成员函数的代码。
成员函数的代码通常存储在程序的文本段(也称为代码段)。这是程序内存布局的一部分,主要用于存储程序的机器代码。每个成员函数只有一份代码,由所有对象共享。这意味着,无论你创建多少个对象,成员函数的代码只存储一次。
当你调用一个对象的成员函数时,这个函数知道它正在操作哪个对象,是通过在调用时隐式地传递一个指向对象的this指针实现的。这就是为什么你可以在成员函数中访问调用它的对象的数据成员,即使这个函数的代码是由所有对象共享的。
因此,你在代码中看到的sizeof(MyStruct)只计算了数据成员的大小,而没有计算成员函数的大小,因为成员函数并不是对象的一部分。同时,offsetof也只能用于数据成员,不能用于成员函数,因为成员函数并不占据对象的存储空间。
内存对齐
内存对齐是一种优化内存访问的技术。一些特定的硬件平台只能在特定地址边界上访问特定类型的数据。例如,一个常见的情况是,一个int可能需要在4字节的边界上进行访问,而一个double可能需要在8字节的边界上进行访问。这种限制是由硬件的内存访问机制决定的。
对于你的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MyStruct1
{
int a; // 4 bytes
double b; // 8 bytes
char c; // 1 byte
};
struct MyStruct2
{
int a; // 4 bytes
char c; // 1 byte
double b; // 8 bytes
};
在许多平台上,MyStruct1和MyStruct2的内存布局将是不同的,因为编译器会插入填充字节以满足对齐要求。
对于MyStruct1,int a后面可能会插入4个填充字节,以确保double b在8字节边界上。然后,char c后面可能会插入7个填充字节,以确保整个结构体的大小是最大对齐要求的倍数(这里是8)。所以,sizeof(MyStruct1)可能是24。
对于MyStruct2,int a后面不需要插入填充字节,因为char c可以放在紧随int a后面的字节中。但是,char c后面可能会插入7个填充字节,以确保double b在8字节边界上。所以,sizeof(MyStruct2)可能是16。
这只是一个可能的结果,实际的结果取决于具体的编译器和硬件平台。你可以使用sizeof()函数和offsetof()宏来查看实际的结果。
请注意,一般来说,我们不应该依赖特定的内存布局,除非有特殊的需要,例如与硬件或网络协议进行交互。对于这种情况,可以使用字符数组或std::byte数组,并手动进行序列化和反序列化。