Codectionary / Developer documentation / C++

C++ Project Structure

Learn how .cpp files, headers, declarations, object files, libraries, executables and build directories fit together in a small C++ project.

Why projects grow beyond one file

A first program fits in main.cpp. As an application grows, keeping every function in one file makes it harder to find, review and reuse code. A small project can place the entry point in one .cpp file and related functions in another, with a header that tells other source files what functions are available. This is organisation, not a requirement that every function gets its own file.

The example below has a greeting function and an entry point. Both .cpp files are compiled, and their object files are linked into one executable. This extends the single-file walkthrough; read that first if #include or main is unfamiliar.

A small, readable folder layout

Keep editable source under src/, shared headers under include/ and generated build output under build/. CMakeLists.txt is optional if you compile manually, but it is included here to show where a build description usually lives. Folder names are conventions, not C++ syntax. Do not hand-edit object files or executables in build/.

main.cpp starts the program, greeting.cpp defines the greeting function, and greeting.hpp declares that function. The .hpp extension commonly marks a C++ header; .h is also common and is not reserved for C. The project must tell its compiler where include/ is and which .cpp files to compile.

Project tree

hello-project/
  CMakeLists.txt
  include/
    greeting.hpp
  src/
    main.cpp
    greeting.cpp
  build/                 generated files after building

Define the function and call it

Save the second block as src/greeting.cpp. It includes the declaration it implements, then defines the function body. A definition supplies the work the function performs; in this case it returns a string literal. Save the third block as src/main.cpp. It includes <iostream> for std::cout and greeting.hpp for the function declaration, then calls greeting() and prints the returned message.

Both source files are translation units after preprocessing. Including greeting.hpp in main.cpp does not paste the body of greeting.cpp into it. If you build only main.cpp, compilation may succeed but linking fails because the definition is missing. In this simple project there should be one definition of greeting and one main function in the final executable.

src/greeting.cpp

#include "greeting.hpp"

const char* greeting() {
  return "Hello, Codectionary!";
}

src/main.cpp

#include <iostream>
#include "greeting.hpp"

int main() {
  std::cout << greeting() << '\n';
  return 0;
}

Where libraries and build directories fit

A library collects reusable compiled code or interfaces for another target to use; it can be your own or supplied by a toolchain or dependency. This tiny project builds one executable directly rather than a separate library. If greeting.cpp later belongs to a reusable library, the executable must link that library as well as see its public declarations. A header alone cannot supply an ordinary out-of-line definition to the linker.

An include directory tells the compiler where to search for headers such as greeting.hpp. A build directory holds generated files such as object files, build-system metadata and executables. Keeping it separate from source makes it easier to rebuild and keeps generated artifacts out of ordinary source edits. The exact output layout depends on the build system and generator; do not assume every platform produces a build/hello file.

Common multi-file mistakes

If the compiler says greeting.hpp cannot be found, check the filename, the include/ directory and the -Iinclude option or build-system include setting. If the linker cannot find greeting, check that greeting.cpp is part of the build and that its signature matches the declaration. If the linker reports more than one definition, check whether a function body was placed in a header and copied into several translation units without an appropriate inline arrangement.

Start with this two-source-file example and make one change: return a different greeting, rebuild and verify the output changes. Then read the CMake guide to see how a build description records the same source list and include path without retyping the compiler command every time.

Syntax

#include <iostream>
#include "greeting.hpp"

int main() {
  std::cout << greeting() << '\n';
  return 0;
}

Examples

Call a function defined in another source file

Use this as src/main.cpp with the greeting.hpp and greeting.cpp files shown above; compile both source files.

#include <iostream>
#include "greeting.hpp"

int main() {
  std::cout << greeting() << '\n';
  return 0;
}
  1. The goal is to print a greeting returned by a function stored in another translation unit.
  2. The header gives main.cpp the declaration needed to type-check the greeting() call. greeting.cpp supplies its actual definition.
  3. The build must compile and link both .cpp files. Once linked, main sends the returned text and newline to std::cout.
  4. If you omit greeting.cpp, adding more includes does not repair the missing linker definition; add the source file to the build.

Best practices

  • Keep declarations in guarded headers and ordinary function definitions in appropriate .cpp files.
  • Compile and link every source file needed by the executable.
  • Separate editable source from generated build output.

Runtime & versions

Compiled native applications. Feature availability depends on the language and runtime version used by your project; consult the official reference below.

ISO C++ resources

Specifications & further reading