[C++] Speicherzugriffsfehler

FBIagent

Erfahrenes Mitglied
Guten Tag,

ich habe einige meiner alten Klassen herausgekramt und wollte diese nun ein wenig
verändern. Nur leider scheine ich bei der Speicherverwaltung etwas falsch zu machen.

Ich bekomme einen access violation beim cleanup, wobei der debugger mir sagt,
das alle Zeiger gültig sind.

Wäre schön wenn jemand einen kurzen blick darauf werfen kann.

SqlStatement.hpp
C++:
class SQLPP_API SqlStatement {
public:
    /*
     * Description:
     *    Created the statement
     *    No nullpointer check
     *
     * Exception on error:
     *    std::exception
     *
     * Return:
     *    no
     */
    SqlStatement( const char *Query );
    /*
     * Description:
     *    Cleans the statement
     *
     * Exception on error:
     *    no
     *
     * Return:
     *    no
     */
    virtual ~SqlStatement();

    /*
     * Description:
     *    Replaces the first occurence of "?" in m_Query with Number
     *
     * Exception on error:
     *    std::exception
     *
     * Return:
     *    no
     */
    void AddNumber( int Number );
    /*
     * Description:
     *    Replaces the first occurence of "?" in m_Query with Str
     *    No nullpointer check
     *
     * Exception on error:
     *    std::exception
     *
     * Return:
     *    no
     */
    void AddStr( const char *Str );

    /*
     * Description:
     *    Returns m_Query
     *
     * Exception on error:
     *    no
     *
     * Return:
     *    m_Query
     */
    const char *GetQuery() const;
private:
    char *m_Query;
};

SqlStatement.cpp
C++:
SqlStatement::SqlStatement( const char *Query )
: m_Query( CStr::Copy( Query ) ) {
    if ( m_Query == 0 ) {
        throw std::exception( "SQL++ Exception: Insufficient memory" );
    }
}

SqlStatement::~SqlStatement() {
    delete[] m_Query;
}

void SqlStatement::AddNumber( int Number ) {
    char *NumberStr = CStr::FromInt( Number );
    char *NewQuery = CStr::ReplaceOnce( m_Query, "?", NumberStr );

    if ( NewQuery == 0 ) {
        throw std::exception( "SQL++ Exception: Insufficient memory" );
    }

    delete[] NumberStr;
    delete[] m_Query;
    m_Query = NewQuery;
}

void SqlStatement::AddStr( const char *Str ) {
    char *NewQuery = CStr::ReplaceOnce( m_Query, "?", Str );

    if ( NewQuery == 0 ) {
        throw std::exception( "SQL++ Exception: Insufficient memory" );
    }

    delete[] m_Query;
    m_Query = NewQuery;
}

const char *SqlStatement::GetQuery() const {
    return m_Query;
}

Diese Klasse in eine DLL exportiert. Desweiteren benutzt sie:
C++:
    char *Copy( const char *Str ) {
        char *NewStr = new char[ strlen( Str ) + 1 ];

        if ( NewStr == 0 ) {
            return 0;
        }

        strcpy( NewStr, Str );
        return NewStr;
    }

    char *ReplaceOnce( char *Str, const char *SearchStr, const char *Replacement ) {
        size_t StrLen = strlen( Str );
        size_t SearchLen = strlen( SearchStr );
        size_t ReplaceLen = strlen( Replacement );

        for ( size_t i = 0;i < StrLen;++ i ) {
            if ( i + SearchLen > StrLen ) {
                return Copy( Str );
            }

            if ( StartsWith( Str + i, SearchStr ) ) {
                char *NewStr = new char[ StrLen - SearchLen + ReplaceLen ];

                if ( NewStr == 0 ) {
                    return 0;
                }

                strncpy( NewStr, Str, i );
                NewStr[ i ] = '\0';
                strcat( NewStr, Replacement );
                strcat( NewStr, Str + i + SearchLen );
                return NewStr;
            }
        }

        return Copy( Str );
    }

Nun linke ich in einem Testprojekt meine .lib und benutze die Klasse folgendermaßen:
C++:
int main( int argc, char *argv[] ) {
    try {
/*        MySqlConnection Connection( "127.0.0.1", "", "", "", 3306 );*/
        SqlStatement Statement( "SELECT * FROM accounts WHERE login='?'" );
        Statement.AddStr( "fbiagent" );
        std::cout << "Query: " << Statement.GetQuery() << std::endl << std::endl;
        /*SqlResult *Result = Connection.ExecuteQuery( &Statement );*/

/*        try {
            while ( Result->Next() ) {
                std::cout << "---- " << Result->GetValue( "login" ) << " ----" << std::endl;
                std::cout << "Password: " << Result->GetValue( "password" ) << std::endl;
                std::cout << "Last Active: " << Result->GetValue( "lastactive" ) << std::endl;
                std::cout << "Access Level: " << Result->GetValue( "accessLevel" ) << std::endl;
                std::cout << "Last IP: " << Result->GetValue( "lastIP" ) << std::endl;
                std::cout << "Last Server: " << Result->GetValue( "lastServer" ) << std::endl << std::endl;
            }
        } catch( std::exception &e ) { // no such column
            std::cout << e.what() << std::endl;
        }*/

        /*delete Result;*/
    } catch ( std::exception &e ) { // connect failed, insufficient mem, query error
        std::cout << e.what() << std::endl;
    }
}

Wenn das Ende des Gültigkeitsbereiches von Statement erreicht ist am Ende des try
Blocks wird mir angezeigt, dass eine HEAP CORRUPTION vorliegt und ich versuche
Speicher freizugeben der mir nicht gehört. Der Debugger zeigt nun aber an, dass
alle Zeiger gültig sind. Nun verstehe ich nicht was ich falsch gemacht habe.

Best wishes
FBIagent
 
Zuletzt bearbeitet von einem Moderator:
Das unten aufgeführte trifft nur zu, wenn du tatsächlich eine DLL erstellst und die .lib nur auf die DLL verweist:

Bei DLL-übergreifender Speicherbenutzung (das ist auch die einzige Entschuldigung, warum nicht std::string verwendet wird) musst du aufpassen.
Wenn du nicht auf DLL-Runtime umgestellt hast (für die DLL und das Hauptprojekt), bekommst du Probleme, wenn du new in der einen Umgebung und das delete in der anderen machst.

Dann hast du zwei Heaps, die sich gegenseitig nicht kennen. Wenn du dann in einem der beiden das new ausführst, und im anderen das delete, dann beschwert sich der, weil er die Speicheralloziierung in seiner Liste nicht findet.

Da gibt es zwei Möglichkeiten:

1) Auf DLL-Runtime umstellen. Dann benutzen beide den gleichen Heap und das Problem erledigt sich. Hat den Nachteil, dass du jetzt eine zusätzliche DLL mit ausliefern/installieren musst. Und sowas ist grundsätzlich sch***e.

2) Über ein Interface arbeiten, das die SQLStatement-Klasse wegkapselt. Mit anderen Worten, dein Hauptprogramm hat keinen Code von der SQLStatement-Klasse in der Hand (was ja das eigentliche Problem ist). Hat den Nachteil, dass du da ein Interface dazwischenklemmen musst. Und eine Create/Destroy-Funktion, die dir eine SQLStatement-Klasse in der DLL erzeugt und das Interface zurückgibt.
 
Wie du schon richtig erahnt hast, sind die runtime libraries statisch gelinkt.
Also in MSVC++ in den Projekt optionen unter C++->Code Generation
von Multithreaded DLL auf Multithreaded und von Multithreaded Debug DLL auf
Multithreaded Debug gestellt. Bei der DLL und dem Testprojekt.

Das mit den zwei Heaps wusste ich bis jetzt noch nicht.

Ich hatte auch nocht etwas vergessen, undzwar gibt es den Zugriffsfehler beim freigeben
von m_Query im D'tor, nicht beim freigeben von SqlStatement selbst.

Der C'tor von SqlStatement allokiert speicher für m_Query auf dem DLL-Heap und
der D'tor versucht diesen auf dem Anwendungs-Heap zu zerstören?

In wie fern Kapseln? Eine klasse von wegen SqlStatementWrapper die einen Member
hat vom typ SqlStatement als Zeiger. Im C'tor beispielsweise eine exportierte Funktion
aufrufen beispielsweise GetNewSqlStatement() die einen Zeiger auf ein neues
SqlStatement Objekt zurückgibt und im D'tor eine exportierte Funktion beispielsweise
DeleteSqlStatement() die als parameter einen pointer zu einem SqlStatement nimmt
die der C'tor mit seinem SqlStatement Zeiger aufruft?

Ein kleines Beispiel wäre nicht schlecht da ich so noch nicht gearbeitet habe.

Best wishes
FBIagent
 
Das ist nicht weltbewegend schön, aber so würde es gehen:

Du machst dir zu deiner SqlStatement-Klasse ein abstraktes Interface:

C++:
class SQLPP_API ISqlStatement {
public:
    ISqlStatement( const char *Query ) = 0;
    virtual ~SqlStatement() = 0;

    virtual void AddNumber( int Number ) = 0;
    virtual void AddStr( const char *Str ) = 0;

    virtual const char *GetQuery() const = 0;
};

Davon leitest du dann deine SqlStatement-Klasse ab.

In der DLL erstellst du zwei neue Funktionen, die dir ein SqlStatement erzeugen/löschen. Dein Hauptprogramm kennt nur das Interface, nicht die davon abgeleitete SqlStatement-Klasse selbst!

ISqlStatement* CreateStatement( const char* Query );
ReleaseStatement( ISqlStatement* pStatement );

Die beiden Funktionen kannst du exportieren und dir damit im Hauptprogramm einen ISqlStatement-Pointer erzeugen lassen. Damit hast du nur Zugriff auf die Interface-Funktionen, das aber hinterrücks ein volles SqlStatement ist. Dadurch liegt der Code von SqlStatement komplett in der DLL und da hast alles eindeutig auf einem Heap.
 
Zuletzt bearbeitet von einem Moderator:
Das heißt also um etwas objektorientierter zu bleiben noch einmal eine Klasse die
dann einen Member vom Typ ISqlStatement* hat, nochmal die gleichen Methoden
Deklariert und Definiert und im C'tor/D'tor die beiden Funktionen aufrufen.

Hm... da muss ich mir überlegen ob das mitliefern der MSVC++ runtime libraries nicht
doch die Lösung mit weniger Overhead ist.

Best wishes
FBIagent
 
Zurück