What is a good approach to learning the Bitcoin codebase? I've read Mastering Bitcoin and other high level resources and I believe I have a decent understanding of Bitcoin. I'd now like to read and understand the source code. However, I'm very new to cpp and don't know a good way to approach this task. Is there a systematic approach I can use to effectively learn the codebase?
Some good places to start are the places that the user or the network input something into Bitcoin Core. The obvious two places for these are the RPC functions and the network message processing. The RPC code is in src/rpc, src/wallet/rpcwallet.cpp and src/rpcdump.cpp. The network message processing is in src/net_processing.cpp. By tracing through the function calls and also looking up where the global variables are being added to and set, you can get into most parts of the codebase.

For example, suppose you looked at sendtoaddress. You would start at the RPC function in src/wallet/rpcwallet.cpp and trace the execution path. You would see that it eventually goes to src/wallet/wallet.cpp and how it uses the CreateTransaction function. From here, you would learn how coin selection works.

Now you would see how the wallet chooses coins for transaction spending. But how does it find out about those in the first place? Well, as you are tracing sendtoaddress, you will come across the AvailableCoins which iterates through some variable named mapWallet and pulls from stuff in mapWallet to build a list of selectable coins. Thus mapWallet must be where transactions are stored, so you can search for the variable mapWallet in the codebase. This will tell you where that variable is being used and you can trace through the uses of mapWallet to learn how it is populated and used.

Doing so will lead you to the functions that populate mapWallet. So now you want to trace backwards: what are calling those functions, how did we get to that function. For example, for populating mapWallet, you will run into AddToWallet. What calls AddToWallet? The function AddToWalletIfInvolvingMe. And you can keep going backwards to get an idea of how it all works.

The same methodology can be applied to the network message processing. For example, suppose you wanted to learn about block processing. The place to start is when a block is received, so in src/net_processing.cpp, look for NetMsgType::BLOCK within the ProcessMessage function. From here you can trace the function calls and global variables like we did with the RPCs. It will take you to the Proof of Work validation and how the target is calculated for a block. It will take you to transaction validation. You will end up looking at the script interpreter and how it evaluates scripts in a transaction. And so on.

You can repeat this for every RPC and every network message type to find out where and how things are processed. This will get you through the majority of the codebase. Network message processing will get you through all of consensus and the bulk of the codebase. Tracing RPCs will get you through basically everything else because almost everything a user can do can be done through the RPCs. These two general entry points are great places to go through the codebase and learn about what Bitcoin  Core is doing internally.