Search code examples
c++pointersinitializationforward-declaration

In C++, how do I fix a pointer class's variable becoming a nullptr when I call it?


I want to use a class: class2, within a class: class1. From what I read, to prevent a circular dependency, one must forward declare class2 in class1.h and have it be a pointer. After calling a function from class2 in my class1.cpp file. I'm unable to call the variables within class2 without getting "Unable to read memory" or a nullptr.

Here's my code, thank you for the help:

//main.cpp

#include "Login.h"

#include <iostream>

using namespace std;

int main() {
    Login login;

    login.StartMenu();
    cout << "ENDING" << endl;

    system("pause");
    return 0;
}

//Login.h (Class1)

#pragma once

#include <iostream>

using namespace std;

class GameManager;

class Login {
public:
    void StartMenu();
    

private:
    GameManager* manager;

};

//Login.cpp

#include "Login.h"
#include "GameManager.h"

void Login::StartMenu() {
    manager->GameStart();

}

//GameManager.h (Class2)

#pragma once

class GameManager {
public:
    void GameStart();
    
private:
    int level = 1;

};

//GameManager.cpp

#include "Login.h"
#include "GameManager.h"

void GameManager::GameStart() {
    cout << level;

}

Solution

  • Generally, it is a good idea to keep dependencies between headers to a minimum, and using pointers for classes that are only forward-declared is an established way to do that. This is good practice even if there are no circular dependencies because it can greatly reduce recompilation times in large projects.

    Regarding your specific question: Essentially, the Login class, and especially the Login::StartMenu function, needs to know which GameManager instance to use. A pointer to that instance will be stored in manager. Ideally you can tell that at construction time of a Login instance via a GameManager * constructor argument:

    #ifndef LOGIN_H
    #define LOGIN_H
    
    class GameManager;
    
    /// This class handles the login procedure for a specific
    /// game manager which must be provided to the constructor.
    /// It cannot be copied (so it cannot be 
    /// in arrays) or default-constructed. 
    class Login {
    public:
        /// The constructor does nothing except initializing manager.
        /// @param gmPtr is a pointer to the game manager
        /// this instance is using.
        void Login(GameManager *gmPtr)
                   : manager(gmPtr) { /* empty */ }
        void StartMenu();
    private:
        GameManager* manager;
    };
    #endif // LOGIN_H
    

    For completeness, here is how you would use it:

    #include "Login.h"
    #include "GameManager.h"
    
    #include <iostream>
    using namespace std;
    
    int main() {
        GameManager gm;
        Login login(&gm); // <-- provide game manager to login
    
        login.StartMenu();
        cout << "ENDING" << endl;
    
        system("pause");
        return 0;
    }
    

    If that is not possible because the GameManager instance does not exist yet or is otherwise unknown during construction of a Login instance (for example, if you have an array of Login instances, whose elements must be default-constructed) you can provide the argument to the Login::StartMenu method. But the constructor argument is much preferred because you can then be sure that the class is functional in the rest of the code — this kind of "invariants" are the main reason why constructors exist.

    It is certainly possible that you don't need to hold a pointer at all, if all functions get that pointer argument. Whether the Login class has a one-to-one relationship with a GameManager (in which case it simply holds a pointer to it) or not (in which case every function is told each time) is a design decision.