You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

调用std::string::begin()时代码崩溃的原因排查(命令模式编辑器)

命令模式简易编辑器插入操作崩溃问题

问题描述

我写了一段用命令模式模拟简易编辑器的C++代码,执行插入操作时程序崩溃。GDB回溯显示调用Document::insert时this指针为0x0,说明Document对象无效。

崩溃代码片段

#include <iostream>
#include <string>
#include <cstdint>
#include <algorithm>
#include <cstdint>
#include <memory>
#include <functional>
#include <deque>
#include <limits>
#include <sstream>

class Document  
{
public:
    enum class DocPos:uint8_t
    {
        DOC_END,
        DOC_BGN,
        DOC_MID,
    };

    int insert(size_t row, size_t col, const std::string str, size_t& ins_pos)
    {
        size_t pos = 0;
        ins_pos = std::string::npos;
        for(int i = 0; i<row-1; i++)
        {
            pos = content.find('\n', pos);  //for simpleness, just consider '\n' other than `\r\n` and etc.
            if(std::string::npos == pos)
            {
                return -1;
            }
        }
    
        std::cout << "LOC A " << pos << " col=" << col << std::endl;
        auto itr = content.begin()+pos;
        std::cout << "*itr " << *itr << std::endl;
        for(int j=0; j<col-1; j++)
        {
            std::cout << "*itr " << *itr << std::endl;
            if(*itr++=='\n')
            {
                return -1;
            }
        }

        ins_pos = pos+col-1;
        std::cout << ins_pos << std::endl;
        content.insert(ins_pos, str);
        return 0;
    }

    int append(const std::string& str)
    {
        content += str;
        return 0;
    }

    int erase(std::size_t length, DocPos pos = DocPos::DOC_END, std::size_t offset=0)
    {
        switch(pos)
        {
            case DocPos::DOC_END:
                if(content.size() < length)
                {
                    return -1;
                }
                content.erase(content.end()-length, content.end());
                break;
            case DocPos::DOC_BGN:
                if(content.size() < length)
                {
                    return -1;
                }
                content.erase(content.begin(), content.begin()+offset);
                break;
            case DocPos::DOC_MID:
                if(std::distance(content.begin()+offset, content.end()) < length)
                {
                    return -1;
                }

                content.erase(content.begin()+offset, content.begin()+offset+length);
                break;
            default:
                return -2;
        }

        return 0;
    }

    friend std::ostream& operator<<(std::ostream& os, const Document& doc)
    {
        os << doc.content;
        return os;
    }
private:
    std::string content;
};

class ICommand
{
public:
    virtual int execute()=0;
    virtual int unexecute()=0;
    virtual ~ICommand() = default;
};

class Append final:  public ICommand
{
public:
    Append(std::shared_ptr<Document> doc_ptr, const std::string& str):m_doc_ptr(doc_ptr), m_str(str){};
    int execute() override
    {
        m_doc_ptr->append(m_str);
        return 0;
    }
    int unexecute() override
    {
        m_doc_ptr->erase(m_str.length(), Document::DocPos::DOC_END);
        return 0;
    }
    ~Append() 
    {
        //std::cout << "append resource is released, str=" << m_str << std::endl;
    }
private:
    std::shared_ptr<Document> m_doc_ptr;
    std::string m_str;
};

class Insert final: public ICommand
{
public:
    Insert(std::shared_ptr<Document> doc_ptr, size_t row, size_t col, const std::string str):m_row(row),m_col(col),m_str(str),m_ins_pos(std::string::npos){}
    int execute() override
    {
        std::size_t pos = std::string::npos;
        return m_doc_ptr->insert(m_row, m_col, m_str, m_ins_pos);
    }

    int unexecute() override
    {
        if(std::string::npos == m_ins_pos)
        {
            return -1;
        }

        return m_doc_ptr->erase(m_str.length(), Document::DocPos::DOC_MID, m_ins_pos);
    }
private:
    std::shared_ptr<Document> m_doc_ptr;
    size_t m_row;
    size_t m_col;
    std::string m_str;
    size_t m_ins_pos;
};

class Invoker
{
public:
    Invoker() = default;

    int execute(std::shared_ptr<ICommand> cmd)
    {
        cmd->execute();
        m_history.push_back(cmd);

        m_redo_list.clear();

        return 0;
    }
    
    int undo()
    {
        if(m_history.empty())
        {
            return -1;
        }

        auto& undo = m_history.back();
        undo ->unexecute();      
        m_redo_list.push_back(std::move(undo));
        m_history.pop_back();

        return 0;
    }
    int redo()
    {
        if(m_redo_list.empty())
        {
            return -1;
        }

        auto& redo = m_redo_list.back();
        redo ->execute();      
        m_history.push_back(std::move(redo));
        m_redo_list.pop_back();

        return 0;
    }
private:
    std::deque<std::shared_ptr<ICommand>> m_history;
    std::deque<std::shared_ptr<ICommand>> m_redo_list;
};

int main()
{
    auto diary_ptr = std::make_shared<Document>();
    auto report_ptr = std::make_shared<Document>();

    auto diary_editor = std::unique_ptr<Invoker>(new Invoker);
    auto report_editor = std::unique_ptr<Invoker>(new Invoker);

    report_editor->execute(std::make_shared<Append>(report_ptr, "Work Report\n"));

    diary_editor->execute(std::make_shared<Append>(diary_ptr, "5/2/2024 "));
    diary_editor->execute(std::make_shared<Append>(diary_ptr, "thursday"));
    diary_editor->execute(std::make_shared<Append>(diary_ptr, "\n"));
    diary_editor->execute(std::make_shared<Append>(diary_ptr, "A white doggy lies in the sunshine."));
    diary_editor->execute(std::make_shared<Append>(diary_ptr, "An old man worrys about the coming strom repoted by the weather report."));
    std::cout << *diary_ptr << std::endl;

    std::cout << "===undo" << std::endl;
    diary_editor->undo();
    std::cout << *diary_ptr << std::endl;

    std::cout << "===append" << std::endl;
    diary_editor->execute(std::make_shared<Append>(diary_ptr, "Some children run after a scaried cat."));
    std::cout << *diary_ptr << std::endl;

    std::cout << "===insert" << std::endl;
    diary_editor->execute(std::make_shared<Insert>(diary_ptr, 1, 10, " rainy"));
    std::cout << *diary_ptr << std::endl;

    report_editor->execute(std::make_shared<Append>(report_ptr, "1. implenments done\n"));
    report_editor->execute(std::make_shared<Append>(report_ptr, "2. unit tests done\n"));

    auto test_undo_redo = [](std::unique_ptr<Invoker>& editor, std::shared_ptr<Document>& doc_ptr, int times){
        std::stringstream org_ss;
        org_ss << *doc_ptr;


        for(int i=0; i<times; i++)
        {
            editor->undo();
        }

        for(int i=0; i<times; i++)
        {
            editor->redo();
        }

        std::stringstream cur_ss;
        cur_ss << *doc_ptr;

        return cur_ss.str() == org_ss.str();
    };

    if(!test_undo_redo(diary_editor, diary_ptr, 3))
    {
        std::cout << "not euqal" << std::endl;
    }

    if(!test_undo_redo(report_editor, report_ptr, 2))
    {
        std::cout << "not euqal" << std::endl;
    }

    std::cout << *report_ptr << std::endl;

    return 0;
}

GDB崩溃回溯日志

(gdb) bt
#0  0x00007ffff7ed4e74 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) ()
   from /lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x0000555555557497 in Document::insert (this=0x0, row=1, col=10, 
    str=" rainy", ins_pos=@0x55555557bfe8: 18446744073709551615) at main.cpp:36
#2  0x0000555555557d26 in Insert::execute (this=0x55555557bfa0) at main.cpp:140
#3  0x0000555555557e0f in Invoker::execute (this=0x55555557af30, 
    cmd=std::shared_ptr<ICommand> (use count 1, weak count 0) = {...})
    at main.cpp:167
#4  0x0000555555556ad1 in main () at main.cpp:223
(gdb) 

问题原因与修复

核心问题

从GDB日志看到Document::insert的this指针是0x0,说明调用该方法时Document对象是空的。追踪代码发现**Insert类的构造函数没有初始化成员变量m_doc_ptr**,导致执行execute方法时,通过空的m_doc_ptr调用insert,触发空指针解引用崩溃。

修复代码

修改Insert类的构造函数,在初始化列表中添加m_doc_ptr(doc_ptr):

Insert(std::shared_ptr<Document> doc_ptr, size_t row, size_t col, const std::string str)
    : m_doc_ptr(doc_ptr), m_row(row), m_col(col), m_str(str), m_ins_pos(std::string::npos) {}

额外优化建议

  • 开启编译器警告(比如-Wall -Wextra),这类未初始化成员变量的问题会被编译器直接提示,避免运行时崩溃。
  • 检查insert方法中的边界处理:比如当col超过当前行长度时,itr会越界访问content,需要添加长度检查,确保pos + col -1不超过content.size()。

内容的提问来源于stack exchange,提问作者John

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 17:15:52