Итак, я покажу вам 3 решения: от простого для понимания кода C -Style, затем более современный код C ++ с использованием библиотеки и итераторов std::algorithm
и, наконец, объектно-ориентированное решение C ++.
Я также объясню вам, что std::getline
может быть, но следует использовать , а не для разделения строк на токены.
Я понял из твоего вопроса, что тебе было трудно это понять. И я понимаю вашу озабоченность.
Но давайте начнем с простого решения. Я показываю код и затем объясняю его вам:
#include <iostream>
#include <fstream>
#include <string>
int main() {
// Open the source text file, and check, if there was no failure
if (std::ifstream fCode{ "r:\\code.txt" }; fCode) {
size_t tripletCounter{ 0 };
// Now, read all triplets from the file in a simple for loop
for (std::string triplet{}; fCode >> triplet; ) {
// Prepare output
std::cout << "\ni:\t" << tripletCounter++ << "\tcodeColumn:\t";
// Go through the triplet, search for comma, then output the parts
for (size_t i{ 0U }, startpos{ 0U }; i <= triplet.size(); ++i) {
// So, if there is a comma or the end of the string
if ((triplet[i] == ',') || (i == (triplet.size()))) {
// Print substring
std::cout << (triplet.substr(startpos, i - startpos)) << ' ';
startpos = i + 1;
}
}
}
}
else {
std::cerr << "\n*** Error, Could not open source file\n";
}
return 0;
}
Видите ли, нам нужно всего несколько строк простого для понимания кода, который удовлетворит ваши требования и даст желаемый результат.
Некоторые, возможно, новые функции:
Оператор if с инициализатором . Это доступно начиная с C ++ 17. Вы можете (в дополнение к условию) определить переменную и инициализировать ее. Итак, в
if (std::ifstream fCode{ "r:\\code.txt" }; fCode) {
мы сначала определяем переменную с именем "fCode" типа std::ifstream
. Мы используем единый инициализатор "{}", чтобы инициализировать его именем входного файла.
Это вызовет конструктор для переменной "fCode" и откроет файл. (Это был этот конструктор). После закрывающего «}» оператора if переменная «fCode» выйдет из области видимости и будет вызван деструктор для std::ifstream
. Это автоматически закроет файл.
Этот тип оператора if был введен, чтобы помочь предотвратить решение пространства имен. Переменная должна быть видна только в области видимости, где она используется. Без этого вам бы пришлось определить std::ifstream
вне (до) if, и он был бы видим для внешнего контекста, и файл был бы закрыт в очень позднее время. Итак, пожалуйста, ознакомьтесь с этим.
Далее мы определяем "tripletCounter". Это необходимо для вывода. Другого использования нет.
Затем снова такой оператор if с initailizer. Сначала мы определяем пустой std::string
«триплет», а затем используем оператор экстрактора для чтения текста до следующего пробела. Вот как работает «экстрактор» (>>). Мы используем все выражение в качестве условия, чтобы проверить, была ли извлечена ошибка, или мы достигли конца файла (или какой-либо другой ошибки). Это работает, потому что оператор извлечения возвращает поток, в котором он работал, поэтому ссылка на «fCode». И поток имеет перезаписанный логический оператор!, Чтобы проверить состояние потока. Пожалуйста, смотрите здесь .
Вы должны всегда и при каждой проверке IO-операции проверять, сработала она или нет.
Итак, далее мы разбиваем тройку (например, «0»). , 1,0 ") в его подстроки с очень легко для l oop. Мы go просматриваем все символы в строке и проверяем, является ли текущий символ запятой или концом строки. В этом случае мы выводим символы перед разделителем.
Очень просто и легко понять. std::getline
здесь не нужен.
Итак, следующее решение, более продвинутое:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iterator>
#include <regex>
std::regex re(",");
int main() {
// Open the source text file, and check, if there was no failure
if (std::ifstream fCode{ "r:\\code.txt" }; fCode) {
size_t tripletCounter{ 0 };
// Now, read all triplets from the file into a vector
std::vector triplets(std::istream_iterator<std::string>(fCode), {});
// Next, go through all triplets
for (const std::string &triplet : triplets) {
// Prepare output
std::cout << "\ni:\t" << tripletCounter++ << "\tcodeColumn:\t";
// Split triplet into code column. All codes are in vector codeColums
std::vector codeColumns(std::sregex_token_iterator(triplet.begin(), triplet.end(), re, -1), {});
//Show codes
for (const std::string& code : codeColumns) std::cout << code << ' ';
}
}
else {
std::cerr << "\n*** Error, Could not open source file\n";
}
return 0;
}
Начало то же самое. Но тогда:
// Now, read all triplets from the file into a vector
std::vector triplets(std::istream_iterator<std::string>(fCode), {});
Мм. Что это такое. Давайте начнем с std :: istream_iterator . Если вы прочтете связанное описание, то обнаружите, что оно будет вызывать оператор экстрактора >> для указанного типа. И поскольку это итератор, он будет вызывать его снова и снова, если итератор увеличивается. Хорошо, понятно, но тогда
Мы определяем триплеты переменных как std::vector
и вызываем его конструктор с 2 аргументами. Этот конструктор является так называемым конструктором диапазона std::vector
. Пожалуйста, смотрите описание для конструктор 5 . Ага, он получает итератор begin () и итератор end (). Ага, а что это за странный {} вместо "end ()" - итератор. Это инициализатор по умолчанию (см. здесь и здесь . И если мы посмотрим на описание std::istream_iterator
, мы увидим, что по умолчанию это конечный итератор. Хорошо, понятно .
Я предполагаю, что вы знаете о диапазоне на основе, который будет следующим. Хорошо. Но теперь мы подошли к самому сложному моменту. Расщепление строки с разделителями. Люди используют std::getline
. Но почему? Почему люди делают такие странные вещи?
Что люди ожидают от функции, когда они читают
getline?
Большинство людей сказали бы, Хм, я думаю, он прочитает полную строку откуда-то. И угадайте, что это было основным намерением для этой функции. Прочитайте строку из потока и поместите ее в строку.
Как видите, здесь std::getline
имеет некоторые дополнительные функции.
И это привело к серьезному неправильному использованию этой функции для разделения std::string
s на токены .
Разделение строк на токены - очень старая задача. В самом начале C существовала функция strtok
, которая все еще существует даже в C ++. Пожалуйста, смотрите std::strtok
.
Но из-за дополнительной функциональности std::getline
он сильно использовался для токенизации строк. Если вы посмотрите на главный вопрос / ответ о том, как анализировать файл CSV (см. здесь ), то вы поймете, что я имею в виду.
Люди используют std::getline
для чтения текстовая строка, строка из исходного потока, затем вставка ее в std::istringstream
снова и использование std::getline
с разделителем снова для разбора строки в токены.
Странно.
Поскольку на протяжении многих лет у нас есть специальная специальная функция для токенизации строк, специально разработанная специально для этой цели. Это
std::sregex_token_iterator
И поскольку у нас есть такая выделенная функция, мы должны просто использовать ее.
Эта вещь является итератором. Для итерации по строке, следовательно, имя функции начинается с s. Начальная часть определяет, с каким диапазоном ввода мы будем работать (begin (), end ()), затем есть std :: regex для того, что должно совпадать / или что не должно совпадать во входной строке. Тип стратегии сопоставления указан с последним параметром.
- 0 -> дайте мне то, что я определил в регулярном выражении, и
- -1 -> дайте мне то, что НЕ соответствует на основе регулярного выражения.
Мы можем использовать этот итератор для хранения токенов в std::vector
. std::vector
имеет конструктор диапазона, который принимает 2 итератора в качестве параметра и копирует данные между первым и вторым итератором в std :: vector. Оператор
std::vector tokens(std::sregex_token_iterator(s.begin(), s.end(), re, -1), {});
определяет переменную «tokens» как std :: vector и снова использует конструктор диапазона std :: vector. Обратите внимание: я использую C ++ 17 и могу определить std::vector
без аргумента шаблона. Компилятор может вывести аргумент из заданных параметров функции. Эта функция называется CTAD («дедукция аргумента шаблона класса»). Я также использовал это для вектора выше.
Кроме того, вы можете видеть, что я не использую итератор end () явно.
Этот итератор будет построен из пустой фигурной скобки -Закрытый инициализатор по умолчанию с правильным типом, потому что он будет выведен таким же как тип первого аргумента из-за конструктора std::vector
, требующего, как уже описано.
Вы можете прочитать любое число из жетонов в строке и положить его в std::vector
Но вы можете сделать еще больше. Вы можете подтвердить свои данные. Если вы используете 0 в качестве последнего параметра, вы определяете std::regex
, который даже проверяет ваш ввод. И вы получаете только действительные токены.
В целом, использование выделенной функциональности превосходит злоупотребление std::getline
, и люди должны просто ее использовать.
Некоторые люди могут жаловаться на издержки функции Но сколько из них используют большие данные. И даже тогда подход, вероятно, будет тогда использовать string.find
и string.substring
или std::stringviews
или что-то еще.
Так что, как-то продвинутый, но вы в конечном итоге выучите это.
А теперь мы будем использовать объектно-ориентированный подход. Как вы знаете, C ++ является объектно-ориентированным языком.
Мы можем поместить данные и методы, работающие с этими данными, в класс (struct). Функциональность заключена в капсулу. Только класс должен знать, как оперировать его данными. Sw, мы определим класс «Код». Содержит std::array
, состоящий из 3 st::string
с. и связанные функции. Для массива мы сделали typedef для облегчения написания. Функции, которые нам нужны, это ввод и вывод. Итак, мы перезапишем экстрактор и оператор вставки.
В этих операторах мы используем функции, как описано выше.
И в результате всей этой работы мы получаем элегантную основную функцию где вся работа выполняется в 3 строки кода.
Пожалуйста, смотрите:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iterator>
#include <regex>
#include <array>
#include <algorithm>
using Triplet = std::array<std::string, 3>;
std::regex re(",");
struct Code {
// Our Data
Triplet triplet{};
// Overwrite extractor operator for easier input
friend std::istream& operator >> (std::istream& is, Code& c) {
// Read a triplet with commans
if (std::string s{}; is >> s) {
// Copy the single columns of the triplet in to our internal Data structure
std::copy(std::sregex_token_iterator(s.begin(), s.end(), re, -1), {}, c.triplet.begin());
}
return is;
}
// Overwrite inserter for easier output
friend std::ostream& operator << (std::ostream& os, const Code& c) {
return os << c.triplet[0] << ' ' << c.triplet[1] << ' ' << c.triplet[2];
}
};
int main() {
// Open the source text file, and check, if there was no failure
if (std::ifstream fCode{ "r:\\code.txt" }; fCode) {
// Now, read all triplets from the file, split it and put the Codes into a vector
std::vector code(std::istream_iterator<Code>(fCode), {});
// Show output
for (size_t tripletCounter{ 0U }; tripletCounter < code.size(); tripletCounter++)
std::cout << "\ni:\t" << tripletCounter << "\tcodeColumn:\t" << code[tripletCounter];
}
else {
std::cerr << "\n*** Error, Could not open source file\n";
}
return 0;
}