6

我最近(4 天)开始学习来自 C / Java 背景的 C++。为了学习一门新语言,我通常从重新实现不同的经典算法开始,尽可能地针对特定语言。

我来到了这段代码,它是一个 DFS - 无向图中的深度优先搜索。仍然从我读到的最好通过 C++ 中的引用传递参数。不幸的是,我不能完全掌握参考的概念。每次我需要参考时,我都会感到困惑,我会根据指针来思考。在我当前的代码中,我使用按值传递。

这是代码(可能不是应有的 Cppthonic):

#include <algorithm>
#include <iostream>
#include <fstream>
#include <string>
#include <stack>
#include <vector>

using namespace std;

template <class T>
void utilShow(T elem);

template <class T>
void utilShow(T elem){
    cout << elem << " ";
}

vector< vector<short> > getMatrixFromFile(string fName);
void showMatrix(vector< vector<short> > mat);
vector<unsigned int> DFS(vector< vector<short> > mat);

/* Reads matrix from file (fName) */
vector< vector<short> > getMatrixFromFile(string fName)
{
    unsigned int mDim;
    ifstream in(fName.c_str());
    in >> mDim;
    vector< vector<short> > mat(mDim, vector<short>(mDim));
    for(int i = 0; i < mDim; ++i) {
        for(int j = 0; j < mDim; ++j) {
            in >> mat[i][j];
        }
    }
    return mat;
}

/* Output matrix to stdout */
void showMatrix(vector< vector<short> > mat){
    vector< vector<short> >::iterator row;
    for(row = mat.begin(); row < mat.end(); ++row){
        for_each((*row).begin(), (*row).end(), utilShow<short>);
        cout << endl;
    }
}

/* DFS */
vector<unsigned int> DFS(vector< vector<short> > mat){
    // Gives the order for DFS when visiting
    stack<unsigned int> nodeStack;
    // Tracks the visited nodes
    vector<bool> visited(mat.size(), false);
    vector<unsigned int> result;
    nodeStack.push(0);
    visited[0] = true;
    while(!nodeStack.empty()) {
        unsigned int cIdx = nodeStack.top();
        nodeStack.pop();
        result.push_back(cIdx);
        for(int i = 0; i < mat.size(); ++i) {
            if(1 == mat[cIdx][i] && !visited[i]) {
                nodeStack.push(i);
                visited[i] = true;
            }
        }
    }
    return result;
}

int main()
{
    vector< vector<short> > mat;
    mat = getMatrixFromFile("Ex04.in");
    vector<unsigned int> dfsResult = DFS(mat);

    cout << "Adjancency Matrix: " << endl;
    showMatrix(mat);

    cout << endl << "DFS: " << endl;
    for_each(dfsResult.begin(), dfsResult.end(), utilShow<unsigned int>);

    return (0);
}

您能否通过引用此代码给我一些关于如何使用引用的提示?

我当前的编程风格是否与 C++ 的结构兼容?

对于 C++ 中的二维数组,vector 和 type** 是否有标准替代方案?

后期编辑:

好的,我已经分析了你的答案(谢谢大家),我已经以更 OOP 的方式重写了代码。我也明白什么是参考,并且要使用它。它有点类似于 const 指针,不同之处在于该类型的指针可以保存 NULL。

这是我最新的代码:

#include <algorithm>
#include <fstream>
#include <iostream>
#include <ostream>
#include <stack>
#include <string>
#include <vector>

using namespace std;

template <class T> void showUtil(T elem);

/**
* Wrapper around a graph
**/
template <class T>
class SGraph
{
private:
    size_t nodes;
    vector<T> pmatrix;
public:
    SGraph(): nodes(0), pmatrix(0) { }
    SGraph(size_t nodes): nodes(nodes), pmatrix(nodes * nodes) { }
    // Initialize graph from file name
    SGraph(string &file_name);
    void resize(size_t new_size);
    void print();
    void DFS(vector<size_t> &results, size_t start_node);
    // Used to retrieve indexes.
    T & operator()(size_t row, size_t col) {
        return pmatrix[row * nodes + col];
    }
};

template <class T>
SGraph<T>::SGraph(string &file_name)
{
    ifstream in(file_name.c_str());
    in >> nodes;
    pmatrix = vector<T>(nodes * nodes);
    for(int i = 0; i < nodes; ++i) {
        for(int j = 0; j < nodes; ++j) {
            in >> pmatrix[i*nodes+j];
        }
    }
}

template <class T>
void SGraph<T>::resize(size_t new_size)
{
    this->pmatrix.resize(new_size * new_size);
}

template <class T>
void SGraph<T>::print()
{
    for(int i = 0; i < nodes; ++i){
        cout << pmatrix[i];
        if(i % nodes == 0){
            cout << endl;
        }
    }
}

template <class T>
void SGraph<T>::DFS(vector<size_t> &results, size_t start_node)
{
    stack<size_t> nodeStack;
    vector<bool> visited(nodes * nodes, 0);
    nodeStack.push(start_node);
    visited[start_node] = true;
    while(!nodeStack.empty()){
        size_t cIdx = nodeStack.top();
        nodeStack.pop();
        results.push_back(cIdx);
        for(int i = 0; i < nodes; ++i){
            if(pmatrix[nodes*cIdx + i] && !visited[i]){
                nodeStack.push(i);
                visited[i] = 1;
            }
        }
    }
}

template <class T>
void showUtil(T elem){
    cout << elem << " ";
}

int main(int argc, char *argv[])
{
    string file_name = "Ex04.in";
    vector<size_t> dfs_results;

    SGraph<short> g(file_name);
    g.DFS(dfs_results, 0);

    for_each(dfs_results.begin(), dfs_results.end(), showUtil<size_t>);

    return (0);
}
4

4 回答 4

8

使用 C++ 的 4 天,你做得很好。您已经在使用标准容器、算法并编写自己的函数模板。我看到的最缺乏的东西正是关于您的问题:需要通过引用/常量引用传递。

每当您按值传递/返回 C++ 对象时,您都在调用其内容的深层副本。这一点都不便宜,尤其是对于像矩阵类这样的东西。

首先让我们看一下showMatrix。此函数的目的是输出矩阵的内容。需要复印件吗?不。是否需要更改矩阵中的任何内容?不,它的目的只是为了展示它。因此,我们希望通过 const 引用传递 Matrix。

typedef vector<short> Row;
typedef vector<Row> SquareMatrix;
void showMatrix(const SquareMatrix& mat);

[注意:我使用了一些 typedef 来使其更易于阅读和编写。当您有很多模板参数化时,我推荐它]。

现在让我们看一下 getMatrixFromFile:

SquareMatrix getMatrixFromFile(string fName);

在这里按值返回 SquareMatrix 可能会很昂贵(取决于您的编译器是否对这种情况应用了返回值优化),按值传递字符串也是如此。使用 C++0x,我们有 rvalue 引用来实现它,因此我们不必返回一个副本(我还修改了要通过 const 引用传递的字符串,原因与 showMatrix 相同,我们不需要文件名):

SquareMatrix&& getMatrixFromFile(const string& fName);

但是,如果您没有具有这些功能的编译器,那么一个常见的折衷方案是通过引用传入一个矩阵并让函数填充它:

void getMatrixFromFile(const string& fName, SquareMatrix& out_matrix);

这并没有为客户端提供方便的语法(现在他们必须编写两行代码而不是一行),但它始终避免了深度复制开销。也有MOJO来解决这个问题,但随着 C++0x 的到来,这将变得过时。

一个简单的经验法则:如果您有任何用户定义的类型(不是普通的旧数据类型)并且想要将其传递给函数:

  1. 如果函数只需要从中读取,则通过 const 引用传递。
  2. 如果函数需要修改原始的,则通过引用传递。
  3. 仅当函数需要修改副本时才按值传递。

在某些例外情况下,您可能拥有一个便宜的 UDT(用户定义类型),它的复制成本比通过 const 引用传递的成本低,例如,但现在坚持这条规则,您将踏上安全编写的道路, 高效的 C++ 代码,不会在不必要的副本上浪费宝贵的时钟周期(C++ 程序编写不佳的常见祸害)。

于 2010-06-28T16:14:40.340 回答
2

要通过引用传递,您通常会更改此:

vector<unsigned int> DFS(vector< vector<short> > mat){

至:

vector<unsigned int> DFS(vector<vector<short>> const &mat) { 

从技术上讲,这是传递一个const引用,但是当/如果您不打算修改原始对象时,这通常是您想要使用的。

另一方面,我可能会改变这个:

for_each((*row).begin(), (*row).end(), utilShow<short>);

类似于:

std::copy(row->begin(), row->end(), std::ostream_iterator<short>(std::cout, " "));

同样地:

for_each(dfsResult.begin(), dfsResult.end(), utilShow<unsigned int>);

会成为:

std::copy(dfsResult.begin(), dfsResult.end(),
          std::ostream_iterator<unsigned int>(std::cout, " "));

(......看起来它会utilShow完全避免)。

就二维矩阵而言,除非您需要一个参差不齐的矩阵(其中不同的行可以是不同的长度),否则您通常使用简单的前端来处理单个向量中的索引:

template <class T>
class matrix { 
    std::vector<T> data_;
    size_t columns_;
public:
    matrix(size_t rows, size_t columns) : columns_(columns), data_(rows * columns)  {}

    T &operator()(size_t row, size_t column) { return data[row * columns_ + column]; }
};

请注意,这用于索引,因此您将使用,operator()代替, 就像在 BASIC 或 Fortran 中一样。如果您愿意,您可以通过一种允许您使用该符号的方式重载,但这是相当多的额外工作,而(IMO)几乎没有真正的好处。 m[x][y]m(x,y)operator[]

于 2010-06-28T16:07:21.337 回答
1
void utilShow(T& elem);
vector< vector<short> > getMatrixFromFile(const string& fName);
void showMatrix(vector< vector<short> >& mat);
vector<unsigned int> DFS(vector< vector<short> >& mat);

一些我可以弄清楚的。如果可能的话,如果您不更改或打算更改方法主体内对象的状态,请将变量作为 const 传递。

我不会要求您在第一次尝试中包含所有 C++ 结构,但要逐步进行,以免让自己陷入抑郁。Vector 是最常用的 STL 容器。容器的使用取决于您的需求,而不是觉得一个接一个地使用。

容器的简要说明。 http://msdn.microsoft.com/en-us/library/1fe2x6kt%28VS.80%29.aspx

@Jerry 感谢您的编辑。Vector 并没有被过度使用,而是因为它对简单对象的简单性而不是大型的整体类对象而被更多地使用。它类似于 C 风格的数组,但不是,有很多额外的算法。另外两个经常使用的是地图和列表。可能是因为我工作的地方比其他地方更需要使用这些容器。

于 2010-06-28T16:03:26.323 回答
1

引用和指针密切相关。两者都是传递参数而不将参数值复制到子例程的堆栈帧的方法。

它们之间的主要区别:

  • 一个指针p指向一个对象o
  • 引用i 一个对象o。换句话说,在别名中。

为了让事情变得更加混乱,据我所知,两者之间的编译器实现几乎相同。

想象一下函数Ptr(const T* t)Ref(const T& t)

int main() { int a; 指针(&a); 参考(一);}

Ptr,t将指向 的位置a。您可以取消引用它并获取a. 如果这样做&t(获取 的地址t),您将获得参数的地址。

Reft a。您可以使用a的值a。你可以得到awith的地址&a。这是 c++ 给你的一点语法糖。

两者都提供了一种无需复制即可传递参数的机制。在您的函数中(顺便说一句,您不需要声明):

template <class T> void utilShow(T elem) { ... }

每次它被调用时,T都会被复制。如果 T 是一个大向量,则它正在复制向量中的所有数据。这是相当低效的。你不想将整个向量传递给新的堆栈帧,你想说“嘿 - 新的堆栈帧,使用这个数据”。所以你可以通过引用传递。那看起来像什么?

template <class T> void utilShow(const T &elem) { ... }

elemconst,因为它没有被函数改变。它还将使用elem存储在调用者中的内存,而不是将其复制到堆栈中。

同样,出于同样的原因(为了避免复制参数),请使用:

vector< vector<short> > getMatrixFromFile(const string &fName) { ... }
void showMatrix(const vector< vector<short> > &mat) { ... }

一个棘手的部分是你可能会想:“嘿,引用意味着没有副本!我要一直使用它!我要从函数返回引用!” 这就是你的程序崩溃的地方。

想象一下:

// Don't do this!
Foo& BrokenReturnRef() {
  Foo f;
  return f;
}

int main() {
  Foo &f = BrokenReturnRef();
  cout << f.bar();
}

不幸的是,这被打破了!BrokenReturnRef运行时,f在范围内,一切都很酷。然后您返回main并继续引用f. 创建的堆栈帧f已经消失,并且该位置不再有效,并且您正在引用垃圾内存。在这种情况下,您必须值返回(或在堆上分配一个新指针)。

“不返回引用”规则的一个例外是当您知道内存将超过堆栈时。这就是 STLoperator[]为其容器实现的方式。

希望有帮助!:)

于 2010-06-28T16:17:58.613 回答