应该如何在循环中创建稍后需要使用的地图?

How should maps be created in a loop that need to be used later?

假设我有一个任意的 classes 层次结构。 Books class 包含由 Chapter class 的 object 组成的地图。每个 Chapter 都有自己的 object 的 Section class.

地图

class 键是每个对应 object 的标题,例如如果我们有一本标题为 Moby Dick 的书,该书的 chapters 地图将包含 LoomingsThe Spouter-Inn 等章节。

这里可能有一组简单的 classes 来描述这种关系:

class Section {
  public: 
    string title;
    Section (string t) { title = t; }; 
};

class Chapter {
  public: 
    map <string, Section *> sections;
    string title;
    Chapter (map <string, Section *> s, string t) { sections = s; title = t; };
};

class Book {
  public: 
    map <string, Chapter *> chapters;
    string title;
    Book (map <string, Chapter *> c, string t) { chapters = c; title = t; };
};

如果我有 space-separated 本书、章节和小节标题的文本文件(我们就称之为 books.txt),例如

Moby_Dick Loomings Section_I
Moby_Dick Loomings Section_II
Moby_Dick The_Spouter-Inn Section_I
Moby_Dick The_Spouter-Inn Section_II
Moby_Dick Loomings Section_I
Moby_Dick The_Carpet-Bag Section_I
Moby_Dick The_Spouter-Inn Section_I
Moby_Dick The_Counterpane Section_I
Moby_Dick Breakfast Section_I
Moby_Dick The_Street Section_I
Frankenstein Chapter_1 Section_I
Frankenstein Chapter_1 Section_II
Frankenstein Chapter_2 Section_I
Frankenstein Chapter_2 Section_II
Frankenstein Chapter_2 Section_III
Frankenstein Chapter_3 Section_I
Frankenstein Chapter_4 Section_I
Frankenstein Chapter_5 Section_I
Frankenstein Chapter_6 Section_I
Pride_and_Prejudice Chapter_1 Section_I
Pride_and_Prejudice Chapter_2 Section_I
Pride_and_Prejudice Chapter_3 Section_I
Pride_and_Prejudice Chapter_4 Section_I
Pride_and_Prejudice Chapter_5 Section_I
Pride_and_Prejudice Chapter_6 Section_I

等等,我可能会用 ifstream 打开文件并处理每一行,将一本新书 object 添加到图书地图,然后添加到那本书 object的章节映射每个章节,每个章节的部分映射每个部分,等等。

假设最终目标是将描述这些书籍、章节和部分的文件行解析为地图,然后能够按字母顺序列出它们。

所以我从收集书籍开始:

#include <iostream> // cout, cerr, etc.
#include <cstdlib> // exit, EXIT_FAILURE
#include <string>
#include <fstream> // ifstream
#include <sstream> // istringstream
#include <fstream> // ifstream
#include <map> 
using namespace std;

class Section {
  public: 
    string title;
    Section (string t) { title = t; }; 
};

class Chapter {
  public: 
    map <string, Section *> sections;
    string title;
    Chapter (map <string, Section *> s, string t) { sections = s; title = t; };
};

class Book {
  public: 
    map <string, Chapter *> chapters;
    string title;
    Book (map <string, Chapter *> c, string t) { chapters = c; title = t; };
};



int main() {

  map <string, Book *> books;

  string filename = "books.txt";

  ifstream fin(filename.c_str()); // open file 
  string line;

  if (fin.is_open()) {

    while (getline(fin, line)) {
      istringstream sin(line);
      string bookTitle, chapterTitle, sectionTitle;
      sin >> bookTitle >> chapterTitle >> sectionTitle;


      if (books.count(bookTitle)) {
        // the book is already in our map
        // ... now what?
       
      } else {
        // the book doesn't exist yet, so go ahead and create fresh book, chapter, and section objects (and their maps) 
        Section section (sectionTitle); // create new section object
        map <string, Section *> sections; 
        sections[sectionTitle] = & section;

        Chapter chapter (sections, chapterTitle); // create new chapter object
        map <string, Chapter *> chapters; // create new chapters map 
        chapters[chapterTitle] = & chapter; // add chapter to chapters map

        Book book (chapters, bookTitle); // create new book object
        books.insert(make_pair(bookTitle, & book)); // add book to books map
      }


    }
  }

  fin.close();

  map <string, Book *>::iterator book_iter;

  // print our resulting book map
  for (book_iter = books.begin(); book_iter != books.end(); book_iter++) {
    cout << "book: " << book_iter->first << endl;
  }

}

很简单,效果很好:

$ g++ -Wall -Wextra -std=c++98 -o books books.cpp && ./books 
book: Frankenstein
book: Moby_Dick
book: Pride_and_Prejudice

问题是当我需要向图书地图中的 现有 本书添加章节时。

所以首先我展开这个部分:

      if (books.count(bookTitle)) {
        // the book is already in our map
        // ... now what?
       
      } else {

变成:

      if (books.count(bookTitle)) {
        // the book is already in our map
        Book book = (*books.at(bookTitle));

        if (book.chapters.count(chapterTitle)) {
          // the chapter is already in our map
          // ... now what?

        } else {
          // the chapter doesn't exist yet, so go ahead and create a fresh chapter and section
          Section section (sectionTitle); // create new section object
          map <string, Section *> sections; 
          sections[sectionTitle] = & section;

          Chapter chapter (sections, chapterTitle); // create new chapter object
          book.chapters.insert(make_pair(chapterTitle, & chapter));
        }   
            
      } else {

但是现在当我 运行 代码时,我得到:

Segmentation fault: 11

导致段错误的具体原因是 book.chapters.count(chapterTitle) - 我假设是因为 book.chapters 不再可访问。

随着循环的每次迭代发生,在该迭代范围内创建的变量被清除,然后我尝试返回并再次引用该映射,它就消失了。

那我该怎么办?我不确定处理此问题的正确方法是什么。

您的代码存在一个主要问题。

如果您存储指向某个对象的指针,您自己有责任确保该对象在您计划使用该指针期间保持活动状态。 C++ 没有垃圾回收。具有自动存储持续时间的变量一旦超出范围就会消失。

} else {
    // the book doesn't exist yet, so go ahead and create fresh book, chapter, and section objects (and their maps) 
    Section section (sectionTitle); // create new section object
    map <string, Section *> sections; 
    sections[sectionTitle] = & section;

    Chapter chapter (sections, chapterTitle); // create new chapter object
    map <string, Chapter *> chapters; // create new chapters map 
    chapters[chapterTitle] = & chapter; // add chapter to chapters map

    Book book (chapters, bookTitle); // create new book object
    books.insert(make_pair(bookTitle, & book)); // add book to books map
}

一旦我们到达该代码块的末尾,您在堆栈上创建的书籍、章节和部分将被销毁。但是您已经在容器中插入了指向它们的指针。现在,一旦您尝试使用这些指针,它们就会悬空。它们将指向对象曾经存在于堆栈中的位置,但它们已经被销毁了。

如果您真的想在地图中存储指针,我建议您使用智能指针。他们很聪明,因为他们会为你管理对象的生命周期。

但在你的情况下,我看不出有任何理由不直接在地图中存储对象。

完成那部分内容后,我们可以继续讨论逻辑了。

Map::insert 的描述如下:

Inserts element(s) into the container, if the container doesn't already contain an element with an equivalent key.

Return value 1-3) Returns a pair consisting of an iterator to the inserted element (or to the element that prevented the insertion) and a bool denoting whether the insertion took place.

有了这些知识,我们可以将您的代码逻辑稍微简化为:

auto [book, book_inserted] = books.insert(make_pair(bookTitle, Book({}, bookTitle)));

现在 book 是指向以下之一的迭代器:

  • 我们刚刚创建的新空书
  • 已经在地图中bookTitle
  • 下的书

book_inserted 告诉我们是否真的插入了空书,或者是否已经有一本之前存储在该书名下的书。我们并不关心这两种方式,我们只需要迭代器。

接下来我们可以使用该迭代器插入章节

auto [chapter, chapter_inserted] = book->second.chapters.insert(make_pair(chapterTitle, Chapter({}, chapterTitle)));

最后,我们使用 chapter 迭代器插入该部分。

chapter->second.sections.insert(make_pair(sectionTitle, Section(sectionTitle)));

这是一个完整的例子

#include <iostream> // cout, cerr, etc.
#include <cstdlib> // exit, EXIT_FAILURE
#include <string>
#include <fstream> // ifstream
#include <sstream> // istringstream
#include <fstream> // ifstream
#include <map> 
using namespace std;

class Section {
  public: 
    string title;
    Section (string t) { title = t; }; 
};

class Chapter {
  public: 
    map <string, Section> sections;
    string title;
    Chapter (map <string, Section> s, string t) { sections = s; title = t; };
};

class Book {
  public: 
    map <string, Chapter> chapters;
    string title;
    Book (map <string, Chapter> c, string t) { chapters = c; title = t; };
};



int main() {

  map <string, Book> books;

  string filename = "books.txt";

  ifstream fin(filename.c_str()); // open file 
  string line;

  if (fin.is_open()) {

    while (getline(fin, line)) {
      istringstream sin(line);
      string bookTitle, chapterTitle, sectionTitle;
      sin >> bookTitle >> chapterTitle >> sectionTitle;

      auto [book, book_inserted] = books.insert(make_pair(bookTitle, Book({}, bookTitle)));

      auto [chapter, chapter_inserted] = book->second.chapters.insert(make_pair(chapterTitle, Chapter({}, chapterTitle)));

      chapter->second.sections.insert(make_pair(sectionTitle, Section(sectionTitle)));
    }
  }

  fin.close();

  // print our resulting book map
  for (auto book_iter = books.begin(); book_iter != books.end(); book_iter++) {
    cout << "book: " << book_iter->first << endl;
  }

}

问题的核心是

        Section section (sectionTitle); // create new section object
        map <string, Section *> sections; 
        sections[sectionTitle] = & section;

        Chapter chapter (sections, chapterTitle); // create new chapter object
        map <string, Chapter *> chapters; // create new chapters map 
        chapters[chapterTitle] = & chapter; // add chapter to chapters map

        Book book (chapters, bookTitle); // create new book object

全部通过当前代码块声明自动变量scoped并存储指向前面对象的指针。这一切都很好,因为它们都会超出范围并在相同范围内以安全顺序被销毁。没有容器比容器长寿。

但是...

    books.insert(make_pair(bookTitle, & book));

存储指向 soon-to-expire 变量的指针 book 并且 books 将比 book 更有效。这是致命的。

简单的解决方案是没有指针。每个容器直接包含它们包含的对象而不是对它们的引用。现在包含的不能在容器之前超出范围。容器将免费管理所包含的销毁,减轻您肩上的内存管理负担。

class Section {
  public: 
    string title;
    Section (string t): title(t) { }; // using member initializer list rather than 
                                      // assignment in the body of the constructor 
                                      // allows the compiler to more easily apply 
                                      // many useful optimizations 
};

class Chapter {
  public: 
    map <string, Section> sections;
    string title;
    Chapter (map <string, Section> s, string t): sections(s), title(t)  { };

class Book {
  public: 
    map <string, Chapter> chapters;
    string title;
    Book (map <string, Chapter> c, string t): chapters(c), title(t) { };
};

现在

        Section section (sectionTitle); // create new section object
        map <string, Section *> sections; 
        sections[sectionTitle] = & section;

        Chapter chapter (sections, chapterTitle); // create new chapter object
        map <string, Chapter *> chapters; // create new chapters map 
        chapters[chapterTitle] = & chapter; // add chapter to chapters map

        Book book (chapters, bookTitle); // create new book object
        books.insert(make_pair(bookTitle, & book)); // add book to books map

变成

        Section section (sectionTitle); // create new section object
        map <string, Section> sections; 
        sections[sectionTitle] = section;

        Chapter chapter (sections, chapterTitle); // create new chapter object
        map <string, Chapter> chapters; // create new chapters map 
        chapters[chapterTitle] = chapter; // add chapter to chapters map

        Book book (chapters, bookTitle); // create new book object
        books.insert(make_pair(bookTitle, book)); // add book to books map

section 被复制到 sections 中,sections 被复制到 chapter 中,依此类推...一直向下复制。原件可能会超出范围并过期,而副本会一直存在,直到被移除或容器被销毁。

复制听起来很丑陋,但现代 C++ 编译器很聪明,提供了一些更快的替代方法,例如使用移动语义来减少不必要的复制,现代编译器将在可能的情况下应用 copy elision,并可能进行移动或复制不必要。移动语义和复制省略可能会变得复杂并且会使这个答案变得混乱,因此如果有必要,最好在以后的问题中讨论它们。

问题底部的修改有一个类似的问题,有一个小皱纹:

Book book = (*books.at(bookTitle));

books 中复制了 bookBook book 不是指针或对 book 的引用,*books.at(bookTitle) 中的 * 表示“获取映射到 bookTitle 中的指针处的对象books。代码块的其余部分对副本进行操作,副本超出范围,什么也没做。books 中的原始代码没有更改。

Book & book = (*books.at(bookTitle));

会对 books 中的 Book 做一个 reference 并且你可以使用引用来修改引用的 Book.

最后我需要使用 new 关键字来分配内存,这样当程序离开变量初始化的范围时它就不会被释放。正如 user4581301 指出的那样,我的代码的问题是我在没有 new 的情况下初始化变量,所以一旦离开范围,它们就会被销毁。

这是我的代码的更新版本,它按预期工作,使用 newdelete 关键字正确分配和释放内存:

#include <iostream>
#include <cstdlib>
#include <string>
#include <fstream>
#include <sstream>
#include <fstream>
#include <map> 
using namespace std;

class Section {
  public: 
    string title;
    Section (string t) { title = t; }; 
};

class Chapter {
  public: 
    map <string, Section *> * sections;
    string title;
    Chapter (map <string, Section *> * s, string t) { sections = s; title = t; };
};

class Book {
  public: 
    map <string, Chapter *> * chapters;
    string title;
    Book (map <string, Chapter *> * c, string t) { chapters = c; title = t; };
};



int main() {

  map <string, Book *> * books = new map <string, Book *>();

  string filename = "books.txt";

  ifstream fin(filename.c_str()); // open file 
  string line;

  if (fin.is_open()) {

    while (getline(fin, line)) {
      istringstream sin(line);
      string bookTitle, chapterTitle, sectionTitle;
      sin >> bookTitle >> chapterTitle >> sectionTitle;


      if ((*books).count(bookTitle)) {
        // the book is already in our map
        Book * book = (*books).at(bookTitle);


        if ((*book).chapters->count(chapterTitle)) {
          // the chapter is already in our map
          Chapter * chapter = (*book).chapters->at(chapterTitle); 
          
          if ((*chapter).sections->count(sectionTitle)) {
            // the section is already in our map
            // do nothing - it's a dupe!
          } else {
            // the section doesn't exist yet
            Section * section = new Section (sectionTitle); // create new section object
            (*chapter).sections->insert(make_pair(sectionTitle, section));
          }

        } else {
          // the chapter doesn't exist yet, so go ahead and create a fresh chapter and section
          Section * section = new Section (sectionTitle); // create new section object
          map <string, Section *> * sections = new map <string, Section *>(); 
          (*sections)[sectionTitle] = section;

          Chapter * chapter = new Chapter(sections, chapterTitle); // create new chapter object
          (*book).chapters->insert(make_pair(chapterTitle, chapter));
        }
       
      } else {
        // the book doesn't exist yet, so go ahead and create fresh book, chapter, and section objects (and their maps) 
        Section * section = new Section(sectionTitle); // create new section object
        map <string, Section *> * sections = new map <string, Section *>(); 
        (*sections)[sectionTitle] = section;

        Chapter * chapter = new Chapter(sections, chapterTitle); // create new chapter object
        map <string, Chapter *> * chapters = new map <string, Chapter *>(); // create new chapters map 
        (*chapters)[chapterTitle] = chapter; // add chapter to chapters map

        Book * book = new Book(chapters, bookTitle); // create new book object
        (*books).insert(make_pair(bookTitle, book)); // add book to books map
      }


    }
  }

  fin.close();

  map <string, Book *>::iterator book_iter;

  // delete all existing objects we created with new
  
  for (book_iter = (*books).begin(); book_iter != (*books).end(); book_iter++) {
    Book * book = book_iter->second;

    map <string, Chapter *> * chapters = (*book).chapters;

    map <string, Chapter *>::iterator chapter_iter;

    for (chapter_iter = (*chapters).begin(); chapter_iter != (*chapters).end(); chapter_iter++) {

      Chapter * chapter = chapter_iter->second;

      map <string, Section *> * sections = (*chapter).sections;

      map <string, Section *>::iterator section_iter;

      for (section_iter = (*sections).begin(); section_iter != (*sections).end(); section_iter++) {
        Section * section = section_iter->second;

        cout << "deleting section: " << (*section).title << endl;
        delete section;
      }
  
      cout << "deleting chapter: " << (*chapter).title << endl;
      delete sections;
      delete chapter;
    }

    cout << "deleting book: " << (*book).title << endl;
    delete chapters;
    delete book;
  }

  delete books;
}

请注意,解除分配的内存非常重要,以免造成内存泄漏。使用 valgrind 之类的工具进行测试帮助,例如在这种情况下 运行 我的 books 二进制我得到:

==1742004== HEAP SUMMARY:
==1742004==     in use at exit: 0 bytes in 0 blocks
==1742004==   total heap usage: 207 allocs, 207 frees, 92,144 bytes allocated
==1742004== 
==1742004== All heap blocks were freed -- no leaks are possible
==1742004== 
==1742004== For lists of detected and suppressed errors, rerun with: -s
==1742004== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

感谢 super 和 user4581301 的精彩解答,感谢 Topological Sort 的宝贵意见!