我会尽力帮助并解释所有步骤。我将首先展示一些理论,然后展示一些简单的解决方案、一些替代解决方案和 C++(面向对象)方法。
因此,我们将从超级简单的 C++ 解决方案转向更现代的 C++ 解决方案。
让我们开始吧。假设您有一个具有某些属性的玩家。属性可以是例如:ID 名称年龄分数。如果将此数据存储在文件中,它可能如下所示:
1 Peter 23 0.98
2 Carl 24 0.75
3 Bert 26 0.88
4 Mike 24 0.95
但在某个时间点,我们注意到这种漂亮而简单的格式将不再适用。原因是带有提取运算符>> 的格式化输入函数将在空白处停止转换。这不适用于以下示例:
1 Peter Paul 23 0.98
2 Carl Maria 24 0.75
3 Bert Junior 26 0.88
4 Mike Senior 24 0.95
那么fileStream >> id >> name >> age >> score; 语句将不再起作用,一切都会失败。因此,人们广泛选择以 CSV(逗号分隔值)格式存储数据。
该文件将如下所示:
1, Peter Paul, 23, 0.98
2, Carl Maria, 24, 0.75
3, Bert Junior, 26, 0.88
4, Mike Senior, 24, 0.95
这样,我们可以清楚地看到,什么值属于哪个属性。但不幸的是,这会使阅读变得更加困难。因为您现在确实需要执行 3 个步骤:
- 将完整的行读取为
std::string
- 使用逗号作为分隔符将该字符串拆分为子字符串
- 将子字符串转换为所需的格式,例如从字符串转换为数字年龄
那么,让我们一步步解决这个问题。
读一整行很容易。为此,我们有函数std::getline。它将从流(从任何 istream,如 std::cin、std::ifstream 或 std::istringstream)中读取一行(在文本中直到行字符 '\n')并将其存储在std::string 变量。请阅读 CPP 参考 here 中的函数说明。
现在,将 CSV 字符串拆分为各个部分。可用的方法太多了,很难说什么是好的方法。稍后我还将展示几种方法,但最常见的方法是使用std::getline。 (我个人最喜欢的是std::sregex_token_iterator,因为它非常适合 C++ 算法世界。但在这里,它太复杂了。
好的,std::getline。正如您在 CPP 参考中所读到的,std::getline 会读取字符,直到找到分隔符。如果您没有指定分隔符,那么它将一直读取到行尾\n。但您也可以指定不同的分隔符。我们将在我们的案例中这样做。我们将选择分隔符‘,’。
但是,另外一个问题是,在步骤 1 中阅读了完整的一行之后,我们在 std::string 中找到了这一行。而且,std::getline 想要从流中读取。因此,以逗号作为分隔符的std::getline 不能与std::string 作为源一起使用。幸运的是,这里还有一种可用的标准方法。我们将使用std::istringstream 将std::string 转换为流。您可以简单地定义这种类型的变量并将刚刚读取的字符串作为参数传递给它的构造函数。例如:
std::istringstream iss(line);
现在我们可以通过这个“iss”来使用所有的 iostream 函数。凉爽的。我们将使用std::getline 和',' 分隔符并接收一个子字符串。
不幸的是,第三个也是最后一个也是必要的。现在我们有一堆子字符串。但是我们也有 3 个数字作为属性。 “ID”是unsigned long,“Age”是int,“Score”是double,所以我们需要使用字符串转换函数将子字符串转换为数字:std::stoul, std::stoi 和 std::stod。如果输入数据总是OK,那么这样就OK了,但是如果我们需要验证输入,那就更复杂了。让我们假设我们有一个好的输入。
那么,许多可能的例子之一:
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
struct Player {
unsigned long ID{};
std::string name{};
int age{};
double score{};
};
// !!! Demo. All without error checking !!!
int main() {
// Open the source CSV file
std::ifstream in("players.txt");
// Here we will store all players that we read
std::vector<Player> players{};
// We will read a complete line and store it here
std::string line{};
// Read all lines of the source CSV file
while (std::getline(in, line)) {
// Now we read a complete line into our std::string line
// Put it into a std::istringstream to be able to extract it with iostream functions
std::istringstream iss(line);
// We will use a vector to store the substrings
std::string substring{};
std::vector<std::string> substrings{};
// Now, in a loop, get the substrings from the std::istringstream
while (std::getline(iss, substring, ',')) {
// Add the substring to the std::vector
substrings.push_back(substring);
}
// Now store the data for one player in a Player struct
Player player{};
player.ID = std::stoul(substrings[0]);
player.name = substrings[1];
player.age = std::stoi(substrings[2]);
player.score = std::stod(substrings[3]);
// Add this new player to our player list
players.push_back(player);
}
// Debug output
for (const Player& p : players) {
std::cout << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score << '\n';
}
}
你看,它变得越来越复杂了。
如果您更有经验,那么您也可以使用其他机制。但是,您需要了解格式化未格式化输入之间的区别,并且需要更多练习。这很复杂。 (所以,不要在一开始就使用它):
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
struct Player {
unsigned long ID{};
std::string name{};
int age{};
double score{};
};
// !!! Demo. All without error checking !!!
int main() {
// Open the source CSV file
std::ifstream in("r:\\players.txt");
// Here we will store all players that we read
Player player{};
std::vector<Player> players{};
char comma{}; // Some dummy for reading a comma
// Read all lines of the source CSV file
while (std::getline(in >> player.ID >> comma >> std::ws, player.name, ',') >> comma >> player.age >> comma >> player.score) {
// Add this new player to our player list
players.push_back(player);
}
// Debug output
for (const Player& p : players) {
std::cout << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score << '\n';
}
}
如前所述,不要在一开始就使用。
但是,您应该尝试学习和理解的是:C++ 是一种面向对象的语言。这意味着我们不仅将数据放入 Player 结构体中,还将对这些数据进行操作的方法放入其中。
而这些目前只是输入和输出。正如您已经知道的那样,输入和输出是使用 iostream 功能和提取器运算符 >> 和插入器运算符 << 完成的。但是,如何做到这一点?我们的 Player 结构是一个自定义类型。它没有内置 >> 和 << 运算符。
幸运的是,C++ 是一门强大的语言,可以让我们轻松添加此类功能。
结构的签名将如下所示:
struct Player {
// The data part
unsigned long ID{};
std::string name{};
int age{};
double score{};
// The methods part
friend std::istream& operator >> (std::istream& is, Player& p);
friend std::ostream& operator << (std::ostream& os, const Player& p);
};
并且,使用上述方法为这些运算符编写代码后,我们将得到:
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
struct Player {
// The data part
unsigned long ID{};
std::string name{};
int age{};
double score{};
// The methods part
friend std::istream& operator >> (std::istream& is, Player& p) {
std::string line{}, substring{}; std::vector<std::string> substrings{};
std::getline(is, line);
std::istringstream iss(line);
// Read all substrings
while (std::getline(iss, substring, ','))
substrings.push_back(substring);
// Now store the data for one player in the given Player struct
Player player{};
p.ID = std::stoul(substrings[0]);
p.name = substrings[1];
p.age = std::stoi(substrings[2]);
p.score = std::stod(substrings[3]);
return is;
}
friend std::ostream& operator << (std::ostream& os, const Player& p) {
return os << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score;
}
};
// !!! Demo. All without error checking !!!
int main() {
// Open the source CSV file
std::ifstream in("r:\\players.txt");
// Here we will store all players that we read
Player player{};
std::vector<Player> players{};
// Read all lines of the source CSV file into players
while (in >> player) {
// Add this new player to our player list
players.push_back(player);
}
// Debug output
for (const Player& p : players) {
std::cout << p << '\n';
}
}
它只是重用我们上面学到的一切。只要把它放在正确的地方。
我们甚至可以领先一步。还有播放器列表,ste::vector<Player> 可以封装在一个类中,并使用 iostream-functionality 进行修改。
通过了解以上所有内容,现在这将非常简单。见:
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
struct Player {
// The data part
unsigned long ID{};
std::string name{};
int age{};
double score{};
// The methods part
friend std::istream& operator >> (std::istream& is, Player& p) {
char comma{}; // Some dummy for reading a comma
return std::getline(is >> p.ID >> comma >> std::ws, p.name, ',') >> comma >> p.age >> comma >> p.score;
}
friend std::ostream& operator << (std::ostream& os, const Player& p) {
return os << p.ID << "\t" << p.name << '\t' << p.age << '\t' << p.score;
}
};
struct Players {
// The data part
std::vector<Player> players{};
// The methods part
friend std::istream& operator >> (std::istream& is, Players& ps) {
Player player{};
while (is >> player) ps.players.push_back(player);
return is;
}
friend std::ostream& operator << (std::ostream& os, const Players& ps) {
for (const Player& p : ps.players) os << p << '\n';
return os;
}
};
// !!! Demo. All without error checking !!!
int main() {
// Open the source CSV file
std::ifstream in("players.txt");
// Here we will store all players that we read
Players players{};
// Read the complete CSV file and store everything in the players list at the correct place
in >> players;
// Debug output of complete players data. Ultra short.
std::cout << players;
}
如果您能看到简单而强大的解决方案,我会很高兴。
最后,正如承诺的那样。将字符串拆分为子字符串的一些进一步方法:
将字符串拆分为标记是一项非常古老的任务。有许多可用的解决方案。都有不同的属性。有些难以理解,有些难以开发,有些更复杂、更慢或更快或更灵活。
替代品
- 手工制作,多种变体,使用指针或迭代器,可能难以开发且容易出错。
- 使用旧式
std::strtok 函数。也许不安全。也许不应该再使用了
-
std::getline。最常用的实现。但实际上是一种“误用”,并不那么灵活
- 使用专门为此目的开发的专用现代功能,最灵活且最适合 STL 环境和算法环境。但速度较慢。
请在一段代码中查看 4 个示例。
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <regex>
#include <algorithm>
#include <iterator>
#include <cstring>
#include <forward_list>
#include <deque>
using Container = std::vector<std::string>;
std::regex delimiter{ "," };
int main() {
// Some function to print the contents of an STL container
auto print = [](const auto& container) -> void { std::copy(container.begin(), container.end(),
std::ostream_iterator<std::decay<decltype(*container.begin())>::type>(std::cout, " ")); std::cout << '\n'; };
// Example 1: Handcrafted -------------------------------------------------------------------------
{
// Our string that we want to split
std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
Container c{};
// Search for comma, then take the part and add to the result
for (size_t i{ 0U }, startpos{ 0U }; i <= stringToSplit.size(); ++i) {
// So, if there is a comma or the end of the string
if ((stringToSplit[i] == ',') || (i == (stringToSplit.size()))) {
// Copy substring
c.push_back(stringToSplit.substr(startpos, i - startpos));
startpos = i + 1;
}
}
print(c);
}
// Example 2: Using very old strtok function ----------------------------------------------------------
{
// Our string that we want to split
std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
Container c{};
// Split string into parts in a simple for loop
#pragma warning(suppress : 4996)
for (char* token = std::strtok(const_cast<char*>(stringToSplit.data()), ","); token != nullptr; token = std::strtok(nullptr, ",")) {
c.push_back(token);
}
print(c);
}
// Example 3: Very often used std::getline with additional istringstream ------------------------------------------------
{
// Our string that we want to split
std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
Container c{};
// Put string in an std::istringstream
std::istringstream iss{ stringToSplit };
// Extract string parts in simple for loop
for (std::string part{}; std::getline(iss, part, ','); c.push_back(part))
;
print(c);
}
// Example 4: Most flexible iterator solution ------------------------------------------------
{
// Our string that we want to split
std::string stringToSplit{ "aaa,bbb,ccc,ddd" };
Container c(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});
//
// Everything done already with range constructor. No additional code needed.
//
print(c);
// Works also with other containers in the same way
std::forward_list<std::string> c2(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {});
print(c2);
// And works with algorithms
std::deque<std::string> c3{};
std::copy(std::sregex_token_iterator(stringToSplit.begin(), stringToSplit.end(), delimiter, -1), {}, std::back_inserter(c3));
print(c3);
}
return 0;
}
编码愉快!