在c++中读取.dat文件并创建多个数据类型



我正在使用C++在Gurobi上构建优化模型,我有一个关于如何为系数赋值的问题。目前,我在.cpp文件中作为进行了这些操作

const int A = 4;
double B[] = { 1, 2, 3 };
double C[][A] = { 
{ 5, 1, 0, 3 },
{ 7, 0, 2, 4 },
{ 4, 6, 8, 9 } 
};

这意味着B[1]=1,B[2]=2,B[3]=3,并且C[1][1]=5,C[1][2]=1,等等。但是,我希望对不同的系数集运行相同的模型,因此,如果我可以从多个.dat文件中读取,那么就更容易了,而不是更改.ccp文件中的值。我可以知道怎么做吗?

如果我将.dat文件保存为以下格式,可以吗?

[4]
[1, 2, 3]
[[5, 1, 0, 3],
[7, 0, 2, 4],
[4, 6, 8, 9]]

我不建议这样做。有些人会建议使用JSON或YAML,但如果你的系数总是那么简单,这里有一个建议:

原始文件

4
1 2 3
5 1 0 3
7 0 2 4
4 6 8 9
#include <iostream>
#include <sstream>
#include <vector>

struct Coefficients {
unsigned A;
std::vector<double> B;
std::vector< std::vector<double> > C;
};
std::vector<double> parseFloats( const std::string& s ) {
std::istringstream isf( s );
std::vector<double> res;
while ( isf.good() ) {
double value;
isf >> value;
res.push_back( value );
}
return res;
}
void readCoefficients( std::istream& fs, Coefficients& c ) {
fs >> c.A;
std::ws( fs );
std::string line;
std::getline( fs, line );
c.B = parseFloats( line );
while ( std::getline( fs, line ) ) {
c.C.push_back( parseFloats( line ) );
}
}

一个使用示例:

std::string data = R"(
4
1 2 3
5 1 0 3
7 0 2 4
4 6 8 9
)";
int main() {
Coefficients coef;
std::istringstream isf( data );
readCoefficients( isf, coef );
std::cout << "A:" << coef.A << std::endl;
std::cout << "B:" << std::endl << "  ";
for ( double val : coef.B ) {
std::cout << val << " ";
}
std::cout << std::endl;
std::cout << "C:" << std::endl;
for ( const std::vector<double>& row : coef.C  ) {
std::cout << "  ";
for ( double val : row ) {
std::cout << val << " ";
}
std::cout << std::endl;
}
}

结果:

Program stdout
A:4
B:
1 2 3 
C:
5 1 0 3 
7 0 2 4 
4 6 8 9 

代码:https://godbolt.org/z/9s3zffahj

Gurobi。非常有趣!你选择了C++来连接它。很好。

我试着用非常简单的陈述给你一个非常简单的答案。

所有的代码都具有最低的复杂性,并且只需要使用少数语句。

不需要很多类似C的语句,因为C++是一种表达能力很强的语言。你可以做一个真正可以理解的或";易于阅读的";为你的问题抽象。如果你看看main,你只看到几行代码,然后你就会明白我的意思。

另外,我会给你详细的解释一切。所以,我不只是转储代码,而是逐行解释。此外,我添加了许多评论,并使用了合理的long和"说";变量名。

这段代码将使用C++,而不是C风格,正如您经常看到的那样。

然后让我们集中讨论一下如何在C++中进行操作。

如果我们看看你的第一个定义:

const int A = 4;
double B[] = { 1, 2, 3 };
double C[][A] = { 
{ 5, 1, 0, 3 },
{ 7, 0, 2, 4 },
{ 4, 6, 8, 9 } 
};

我们可以在这里看到C样式的数组。括号内有[]的。B数组的元素数量由初始化器元素的数量定义。因此,初始化器列表中的3个元素将在数组中使用3个元素-->数组大小为3。好的,理解

C元素是一个2维矩阵。列数由"const int A=4"定义。所以,我不确定,A是一个大小,还是一个系数。但最终,这并不重要。行数由源文本文件中的行数给定。Sor,我们有一个3行4列的矩阵。

第一个重要信息:在C++中,我们不使用C样式数组[]。我们基本上有两匹通用的工作马:

  1. 如果在编译时已知数组的大小,则为std::array
  2. C++主容器std::vector。可以根据需要动态增长的阵列。这是非常强大的,并且在C++中使用了很多。它还知道它包含多少元素,所以现在需要像A=4这样的显式定义

std::vector是用于此目的的容器。请阅读此处有关std::vector的信息。因此,即使我们事先不知道行和列的数量,我们也可以使用std::vectorand it来根据需要增长。

出于这个原因,我不确定;值为4"的const;,在您的系数信息中需要。无论如何,我会添加它。


接下来,C++是一种面向对象的语言。在该语言的最初,它甚至被称为"带对象的C"。C++中的对象是用类或结构来建模的。

面向对象方法的一个主要思想是,将数据和对这些数据进行操作的方法放在一个类(或结构)中。

因此,我们可以定义一个Coefficient类,并在这里存储所有系数数据,如a、B、C。然后,最重要的是,我们将向该类添加函数,这些函数将对这些数据进行操作。在我们的特殊情况下,我们将添加读取写入功能。

正如您所知,C++使用了一个非常通用的IO流库。并具有所谓的"提取"运算符>>和"插入器"运算符<<。"流"被实现为分层类。这意味着,在哪个流(例如std::cout、文件流或字符串流)上使用<<>>运算符并不重要,它基本上在任何地方都以相同的方式工作。

对于许多现有的和内置的数据类型,提取器>>和插入器<<运算符已经过载。因此,您可以输出许多不同的数据类型,例如std::cout

但是,这当然不适用于自定义类型,比如我们的类"系数"。但在这里,我们可以通过定义适当的插入器和提取器运算符来简单地添加功能。之后,我们可以以与其他内置数据类型相同的方式使用我们的新类型。

现在让我们来看第一个代码示例:

struct Coefficient {
// The data
int A{};
std::vector<double> B{};
std::vector<std::vector<double>> C{};
friend std::istream& operator >>(std::istream& is, Coefficient& coefficient);
friend std::ostream& operator << (std::ostream& os, const Coefficient& coefficient);
};

仅此而已。很简单,不是吗?现在该类具有所需的功能。

我们稍后将向运营商展示实施情况。

注意,这种机制也被称为(去)序列化,因为cour类的数据将以串行和人类可读的方式写入/读取。我们需要注意数据的输出和输入结构是相同的,这样我们就可以始终使用我们的2个运算符。

您现在应该已经理解了,我们稍后可以在main或其他函数中对IO操作进行极其简单和低复杂度的处理。现在让我们看看主要内容:

// Some example data in a stream
std::istringstream exampleFile{ R"(4
1 2 3  
5 1 0 3  
7 0 2 4  
4 6 8 9  )" };
// Test/Driver code
int main() {
// Here we have our coefficients
Coefficient coefficient{};
// Simply extract all data from the file and store it in our coefficients variable
// This is just a one-liner, intuitive and simple to understand.
exampleFile >> coefficient;
// One-liner debug output. Even mor simple
std::cout << coefficient;
}

这看起来非常直观,与其他内置数据类型的输入和输出类似。

现在让我们来谈谈实际的输入和输出函数。而且,由于我们希望保持简单,我们将以易于阅读的方式在".dat"文件中构建您的数据。

也就是说:用空格分隔的数据。那么:81 999 42等等。为什么这么简单?因为在C++中,格式化的输入函数(具有提取器>>的那些)将容易地读取这样的数据。示例:

int x,y,z;
std::cin >> x >> y >> z

如果您如上所示给出一个以空格分隔的输入,它将读取字符,将其转换为数字,并将其存储在变量中。

C++中有一个问题。也就是说,在大多数情况下,行尾字符"\n"也将被视为空白。因此,在循环中读取值不会在一行的末尾停止。

这个问题的标准解决方案是使用像std::getline这样的非格式化输入函数,并首先将一整行读取到std::string变量中。然后,我们将把这个字符串放入std::istringstream中,它也是一个流,并从中提取值。

在".dat"文件中,有许多行包含数据。因此,我们需要反复进行上述操作。对于需要重复执行的事情,我们使用C++中的函数。我们需要一个函数,它接收流(任何流)读取值,将它们存储在std::vector中并返回向量。

在向您展示这个函数之前,我将节省一些打字工作,并使用using语句缩写矢量和二维矢量。

请参阅:

// Some abbreviations for easier typing and reading
using DVec = std::vector<double>;
using DDVec = std::vector<DVec>;
// ---------------------------------------------------------------------------
// A function to retrieve a number of double values from a stream for one line
DVec getDVec(std::istream& is) {
// Read one complete line
std::string line{}; std::getline(is, line);
// Put it in an istringstream for better extracting
std::istringstream iss(line);
// And use the istream_iterator to iterate over all doubles and put the data in the resulting vector
return { std::istream_iterator<double>(iss), {} };
}

你看,一个简单的3行函数。最后一行对于初学者来说可能很难理解。我稍后会解释。因此,我们的函数期望引用一个流作为输入参数,然后返回一个包含一行中所有双精度的std::vector<double>

因此,首先,我们将一整行读取到类型为std::string的变量中。超简单,具有现有的std::getline功能。

然后,我们将字符串放入一个std::istringstream变量中。这将基本上将字符串转换为流,并允许我们在其中使用所有流函数。记住,我们为什么这么做:因为我们想读一整行,然后从中提取数据。现在是最后一行:

return { std::istream_iterator<double>(iss), {} };

那是什么?我们期望返回一个std::vector<double>。编译器知道我们想要返回这样一个类型。因此,他将为我们创建一个该类型的临时变量,并使用其范围构造函数no5()(参见此处)来初始化我们的向量。用什么?

您可以在CPP引用中看到,它需要2个迭代器。一个开始迭代器和一个结束迭代器。迭代器之间的所有内容都将被包容性地复制到向量中。

std::istream_iterator(请阅读此处)将简单地重复调用提取器运算符>>,然后读取所有的double,直到读取所有值。

酷!

接下来,我们可以在类的提取器运算符>>中使用此功能。然后会变成这样;

// Simple extraction operator
friend std::istream& operator >>(std::istream& is, Coefficient& coefficient) {
// Get A and all the B coefficients
coefficient.B = std::move(getDVec(is >> coefficient.A >> std::ws));
// And in a simple for loop, readall C-coeeficients
for (DVec dVec{ getDVec(is) }; is and not dVec.empty(); dVec = getDVec(is))
coefficient.C.push_back(std::move(dVec));
return is;
}

它将首先读取

  • A的值(>> coefficient.A)
  • 则流中可能存在的所有空白,然后(>> std::ws)
  • 具有B系数的行(getDVec(is)

LAst,但同样重要的是,我们使用s imple for loop,读取C系数的所有行,并将它们添加到2d输出向量中。我们将跳过空行。

std::move将避免复制大数据,并为我们提供更好的效率。

输出甚至更简单。使用";循环";以显示数据。这里没有太多要解释的。

现在,我们拥有所有功能。我们把大问题分解成小问题,使我们的生活变得更简单。

最后的完整代码如下所示:

#include <iostream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <iterator>
// Some abbreviations for easier typing and reading
using DVec = std::vector<double>;
using DDVec = std::vector<DVec>;
// ---------------------------------------------------------------------------
// A function to retrieve a number of double values from a stream for one line
DVec getDVec(std::istream& is) {
// Read one complete line
std::string line{}; std::getline(is, line);
// Put it in an istringstream for better extracting
std::istringstream iss(line);
// And use the sitream_iterator to iterate over all doubles and put the data in the resulting vector
return { std::istream_iterator<double>(iss), {} };
}
// -------------------------------------------------------------
// Cooeficient class. Holds data and methods to operate on this data
struct Coefficient {
// The data
int A{};
DVec B{};
DDVec C{};
// Simple extraction operator
friend std::istream& operator >>(std::istream& is, Coefficient& coefficient) {
// Get A and all the B coefficients
coefficient.B = std::move(getDVec(is >> coefficient.A >> std::ws));
// And in a simple for loop, readall C-coeeficients
for (DVec dVec{ getDVec(is) }; is and not dVec.empty(); dVec = getDVec(is))
coefficient.C.push_back(std::move(dVec));
return is;
}
// Even more simple inserter operator. Output values in loops
friend std::ostream& operator << (std::ostream& os, const Coefficient& coefficient) {
os << coefficient.A << 'n';
for (const double d : coefficient.B) os << d << ' '; os << 'n';
for (const DVec& dv : coefficient.C) {
for (const double d : dv) os << d << ' '; os << 'n'; }
return os;
}
};
// Some example data in a stream
std::istringstream exampleFile{ R"(4
1 2 3  
5 1 0 3  
7 0 2 4  
4 6 8 9  )" };
// Test/Driver code
int main() {
// Here we have our coefficients
Coefficient coefficient{};
// Simply extract all data from the file and store it in our coefficients variable
exampleFile >> coefficient;
// One-liner debug output
std::cout << coefficient;
}

请再次查看主要的简单语句。

我希望我能帮你一点忙。


一些附加说明。

  • 在专业软件开发中,没有注释的代码被认为具有0质量
  • 此外,SO指南建议,不仅要转储代码,还要给出全面的解释
  • 请不要使用:while ( isf.good() ) {这被认为是非常糟糕的做法,而且容易出错。请阅读
  • 如果你决定使用C++,你应该尝试摆脱典型的串行C编程,使用更面向对象的方法

如果您还有其他问题,我很乐意回答。谢谢你的提问。

最新更新