This actually is something which requires both careful thought and a systematic approach. It can be frustrating in the beginning but with enough persistence, a mass of code will start making sense slowly but surely. The process is not always linear and is often sped up by prior experience and intuition. IMO, this is a most important skill to cultivate for a "professional" programmer since most of the time one spends far more time reading other peoples code, understanding it, fixing bugs and adding features. Rarely does one get an opportunity to build everything from ground up.

The following is an approach i try to follow for C/C++ code;

0) DO NOT try to understand the details of how exactly something works in the beginning. Work top-down, iterating and gradually getting into the details as needed. The key is to get a firm idea of the system as a whole before diving into the nitty-gritties.

1) We need to focus on three main aspects;
- Physical Structure: How is the code distributed across files and directories?
- Static Structure: What are the top-level logical Subsystems and Modules in the codebase? What are the dependencies amongst them? What are the major data structures and static call-trees?
- Dynamic Structure: What are the major use-case scenarios? For a given use-case scenario, what is the actual call flow at run-time? How does a relevant data structure change?

2) First, try and find some oldtimer in the group/company/wherever who has worked with the code for a while. Setup a few whiteboard sessions and pick his/her brain to get a good overview of the system. Also go through any and available documentation. Take notes as needed.

3) Sit with QA/Testing and try out the system as a "end-user". This will identify the major features/use-case scenarios.

4) The above would have given us a good overview and now we can drill down into the codebase. You can use your favourite IDE/tools (Visual Studio, Eclipse etc.) but you still need to keep the above-mentioned three aspects of the system in mind. I tend to use the following tools;
a) Doxygen, CScope, CFlow, etags, GLOBAL, grep to cross-reference data structures, symbols and follow static call flows.
b) Call graph using gprof for Dynamic call flows (ignore performance data initially).
c) Most large codebases have some sort of Trace/Debug scaffolding which you can turn on in the build system. This often provides us with a lot of insight into the runtime behaviour of the system since programmers output/verify/check important state data using these statements.

5) For each major use-case/feature scenario peruse the static call graph generated using the above tools. Note carefully the major data structures whose state is changed. Pay particular attention to "asserts" in the code since they verify pre/post-conditions and invariants thus clarifying "what" the code is supposed to do. The "how" is the code itself.

6) Now we execute the use-case with specific inputs and take a look at the Dynamic call graph generated using gprof or Trace/Debug statements and match it to the static call graph. Depending upon the system, it is often easier to generate the Dynamic call graph and then lookup its corresponding Static call graph.

Finally, we should now have the following;
- A list of all major subsystems and modules and where physically they are located in the source tree.
- A list of the major data structures in each module. In case of common/shared data structures what are its dependent modules.
- Static call graphs for major use-case scenarios. You can annotate this with the main data structures that a function touches.
- Dynamic call graphs. Note the input used. You can merge this with the above static call graph.

With the above in hand, we should be well on our way towards "grokking" the system.