To build a native Mac executable from several C++ files, pass every required implementation file to clang++, or compile them into object files and link those together. This walkthrough builds a small Terminal program and explains how to diagnose missing definitions.

Prepare the Mac compiler tools

Use this workflow for a small C++ program you want to run locally in Terminal. Apple provides the compiler and macOS SDK through either Xcode or the standalone Command Line Tools package. If you already have Xcode installed, you do not need the separate package. Otherwise, run the following command and complete the installation dialog. See Apple’s Installing the command-line tools.

xcode-select --install

After installation, check that the compiler is available:

clang++ --version

Use clang++ for the final link, including when your inputs are already compiled object files. Clang’s documentation explicitly recommends this driver for C++ programs because the language mode determines which runtime libraries it links automatically. See Assembling a Complete Toolchain.

Build a complete three-file C++ program

The synthetic example counts items on three desktop trays and formats the result. It has one entry point and two supporting implementation files. Create a new, empty working directory, then save these four files directly inside it:

desk-demo/
  desk.hpp
  main.cpp
  label.cpp
  totals.cpp

Save the shared declarations as desk.hpp:

#ifndef DESK_DEMO_DESK_HPP
#define DESK_DEMO_DESK_HPP

#include <string>

std::string make_label(int count);
int total_items(int trays, int per_tray);

#endif

Save the entry point as main.cpp:

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

int main() {
    const int count = total_items(3, 4);
    std::cout << make_label(count) << '\n';
    return 0;
}

Save the formatting implementation as label.cpp:

#include "desk.hpp"

std::string make_label(int count) {
    return "Desk items: " + std::to_string(count);
}

Save the counting implementation as totals.cpp:

#include "desk.hpp"

int total_items(int trays, int per_tray) {
    return trays * per_tray;
}

Each implementation includes the same header, allowing the compiler to check its function definition against the shared declaration. The header guard prevents repeated inclusion within one translation unit. Keep these ordinary, non-inline function bodies in their implementation files; C++ imposes definition requirements across the program through its one-definition rule.

Compile and link in one command

Open Terminal in the directory containing the four files and run:

clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo

The expected output is:

Desk items: 12

The command names all three implementation files and sets the output filename with -o. Because it has no stage-stopping option, Clang compiles the inputs and invokes the linker. The explicit -std=c++17 selects the language version instead of relying on a compiler default. These options are documented in the Clang compiler command guide.

The shell runs the executable only if the build succeeds because the commands are joined with &&. This matters when rebuilding: a failed build should not be followed by accidentally running an older executable and mistaking its output for the new result.

When is a wildcard appropriate?

For this exact directory, the following is an alternative to the explicit source list:

clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo

The shell expands ./*.cpp to matching files in the current directory. It does not search nested folders or include files ending in .cc or .cxx. Quote individual paths containing spaces when you name them explicitly.

Use the wildcard only while every matching file belongs to this executable. If you later add a separate demonstration program with its own main, or a platform-specific alternative implementation, keep an explicit source list. Here, “all implementation files” means all files needed by one target, with each required definition supplied appropriately.

Compile object files first, then link them

Separating the stages makes it easier to see whether a failure happens while compiling one source file or while combining the program. Using the same four files and working directory, run:

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -c main.cpp -o build/main.o
    clang++ -std=c++17 -c label.cpp -o build/label.o
    clang++ -std=c++17 -c totals.cpp -o build/totals.o
    clang++ build/main.o build/label.o build/totals.o -o build/desk-demo
    ./build/desk-demo
)

The -c option produces an object file without linking. The final compiler invocation receives the three objects and links the executable. This follows Clang’s documented compilation stages. The subshell’s set -e stops this straight-line sequence if a command fails.

Use explicit object names here as well. Linking every object in a reused build directory can accidentally include an obsolete file from an earlier version of the program. When two source files in different folders share a basename, give their objects distinct paths rather than writing both to the same output filename.

If you change only label.cpp, you can recompile that object and repeat the link command. If you change desk.hpp, rebuild every source that includes it. This manual workflow requires you to track those relationships; once the project grows, a build system is more practical.

Diagnose the failure before changing the build command

The following sequence applies the compilation stages and C++ definition rules to this example. Exact diagnostic wording varies with the installed toolchain.

Symptom What to check first Appropriate next step
A header is reported missing The include spelling and header location Restore the shown layout or configure the correct header search directory.
make_label or total_items is undefined during linking Whether its implementation is an input Add the missing .cpp file or its compiled object to the link.
The function is still undefined despite including its file Declaration and definition signatures Compare namespaces, parameter types, and qualifiers; include the shared header in the implementation.
Duplicate symbols appear Repeated implementation inputs Remove a repeated object, an included .cpp, or an unintended alternative definition.
main is undefined or duplicated Entry-point files Include the intended main.cpp exactly once and exclude other programs’ entry points.
C++ library symbols are undefined The final link driver Use clang++ so the C++ runtimes are supplied.

For example, compiling only main.cpp can accept the calls because the header declares both functions. The final link still needs their definitions. Adding another header inclusion does not supply the missing implementation object. The underlying requirements are explained by the C++ one-definition rule.

Avoid fixing missing symbols by including label.cpp inside main.cpp while also compiling label.cpp independently. That supplies the same ordinary function definition through two translation units. The shared header and separate implementation inputs already provide the intended structure.

To inspect the tool commands without executing the build, use Clang’s documented -### option. Quote it so the shell passes it intact:

clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo

This exposes the compiler and linker invocations selected by the driver, which helps distinguish the command you intended from the command the toolchain would run. See Assembling a Complete Toolchain.

Keep architecture, platform, and ABI compatible

Adding every source file cannot repair incompatible binary inputs. For a local build, start with one installed Apple toolchain and rebuild the program’s sources together. When adding prebuilt libraries, check their supported platform and architectures before changing source lists.

For an optional universal build, Apple documents compiling and linking separately for each architecture, then merging the finished executables with lipo. With a toolchain and SDK supporting both targets, the same sample can be built as follows. The macOS 11 deployment targets below are illustrative choices:

(
    set -e
    mkdir -p build
    clang++ -std=c++17 -target arm64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-arm64
    clang++ -std=c++17 -target x86_64-apple-macos11 \
        main.cpp label.cpp totals.cpp -o build/desk-x86_64
    lipo -create build/desk-arm64 build/desk-x86_64 \
        -output build/desk-universal
    lipo -archs build/desk-universal
)

Every linked dependency must provide a compatible input for the corresponding architecture build. An iOS binary does not become a macOS binary merely because both use arm64. Apple explains architecture slices, platform distinctions, and executable inspection in Building a universal macOS binary.

The C++ language version is also distinct from binary compatibility, or ABI. Matching -std=c++17 alone does not establish that separately built libraries have compatible layouts and configurations. LLVM documents ABI-affecting library configuration flags and states that users should not set those vendor flags themselves. For this task, retaining the toolchain’s normal library configuration is the practical choice. See libc++’s ABI Guarantees.

Apply the same source list in Xcode or a mixed Swift project

In an existing Xcode target, select the target and open Build Phases. Check Compile Sources for the implementation files needed by that executable. Add custom library dependencies through Link Binary with Libraries. Apple documents these separate responsibilities in Customizing the build phases of a target.

For the sample, the source list should account for main.cpp, label.cpp, and totals.cpp. Putting the header in the project does not replace compiling the implementations.

If the surrounding application uses Swift, the pure C++ commands above cover only the C++ program. Swift’s documented path requires enabling C++ interoperability to expose APIs across the languages; only supported language features can cross that boundary. See Mixing Swift and C++.

Mixed Swift and C++ code must use the same C++ standard library; on Apple platforms, Swift uses libc++. Swift also documents compiler and language-version constraints, including C++14 or newer for interoperability. Check those requirements before treating an existing library as a compatible dependency. See Supported Features and Constraints of C++ Interoperability.

For the local Terminal task, begin with the explicit three-source build. If it fails, identify the failing stage, check the missing or repeated definition, and verify compatible binary inputs. Once that command produces the expected result, it provides a concrete source list to carry into a larger build.

Sources