Baeldung Pro – Linux – NPI EA (cat = Baeldung on Linux)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (tag=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

When we write programs in the C language, especially on Linux, we often split our code into multiple source files. This splitting helps us manage and organize our program files. But what happens when we want to share a struct we defined in one source file with another? Unsurprisingly, this is a common scenario, and if we don’t handle it correctly, we may run into compiler errors or messy code duplication.

In this tutorial, we’ll look at how we can use a struct defined in one C source file in another. We’ll focus on the solutions, how they’re implemented, and how they compare to each other.

2. Understanding the Problem

Suppose we have a simple struct definition in one C source file called main.c:

$ cat main.c
#include <stdio.h>

// Local struct definition
struct test_st {
    int state;
    int status;
};

// create an instance of the struct structure
struct test_st instance = {.state = 5, .status = 10};

//function to print a struct attribute
void print_main() {
    printf("Main: state=%d\n", instance.state);
}

int main() {
    print_main();
    external_function();  // Defined in othersrc.c
    return 0;
}

This is a simple source file that defines our struct, creates an instance of it, and calls a function to print the struct. Notably, we also want to execute an external function in another C file.

Now, let’s say we want to use this test_st struct in another C file, such as othersrc.c:

$ cat othersrc.c
#include <stdio.h>

// Attempt to use struct without definition
void external_function() {
    struct test_st copy;  // ERROR: incomplete type
    copy.state = 20;      // Won't compile
    printf("Other: %d\n", copy.state);
}

Let’s try to compile these two files:

$ gcc main.c othersrc.c -o broken

This code uses gcc to create an executable named broken. When we compile this, an error is displayed in the output:

othersrc.c: In function 'external_function':
othersrc.c:5:18: error: storage size of 'copy' isn't known
    5 |     struct test_st copy;
      |                  ^~~~

The output shows that our compiler doesn’t recognize the struct, as it doesn’t know where to find the definition. A straightforward solution is to copy the struct definition into the other C source file. Although this idea might seem quick, it’s inefficient.

Let’s imagine we had hundreds of files that use the test_st struct. If we change its attributes later, we’d have to go into these different files to change its copy-pasted definition, which is a recipe for bugs.

How then do we share our struct definition for clean and efficient code without copying it into the new source file?

3. The Classic Header File Method

One of the most common and reliable ways to do this is to use a header file. Generally, this method is recommended as best practice since defining the struct in a header file enables us to use it across multiple files:

$ cat main.h
#ifndef MAIN_H
#define MAIN_H

struct test_st {
    int state;
    int status;
};
extern struct test_st instance; // Declaration

#endif

Then, we can access this definition in other files using the include statement. Let’s modify the othersrc.c file:

$ cat othersrc.c
#include <stdio.h>
#include "main.h"

void external_function() {
    printf("Other: status=%d\n", instance.status);  // Full access
    instance.state = 15;  // Modifies shared instance
}

Here, we modified the code to include the header file called main.h. This inclusion allows the compiler to link the struct definition and our functions during the compilation process.

The main.c file also changes:

$ cat main.c
#include <stdio.h>
#include "main.h"

struct test_st instance = {.state = 5, .status = 10};

void print_main() {
    printf("Main: state=%d\n", instance.state);
}

int main() {
    print_main();
    external_function();
    return 0;
}

Let’s compile again by running the same command, this time creating a new executable:

$ gcc main.c othersrc.c -o working
$ ./working

After compiling our program and checking out the output, we see that the compiler now recognizes the struct:

Main: state=5
Other: status=10

Additionally, we were able to do this without duplicating the struct definition in both files. That’s simple and efficient! Here’s why this method works so well:

  • The compiler sees the same struct layout everywhere
  • We only need to update the main.h file when we want to modify the struct
  • It works seamlessly with Linux build tools like gcc, Make, and CMake

4. Using Pointers Without Full struct Definitions

Another solution is to use pointers. Sometimes, we want to avoid exposing the full struct definition in other files. An example is when we want to hide implementation details or reduce dependencies.

In such cases, we can use a forward declaration and pointers. Let’s see what this looks like in code.

In the othersrc.c file, we declare the struct without defining it:

$ cat othersrc.c
#include <stdio.h>

// Forward declaration
struct test_st;

// External function declarations
extern void get_struct_ptr(struct test_st **);
extern int get_state(struct test_st *ptr);
extern void set_state(struct test_st *ptr, int new_state);

void external_function() {
    struct test_st *ptr;
    get_struct_ptr(&ptr);

    int current_state = get_state(ptr);
    printf("Other: current state = %d\n", current_state);

    set_state(ptr, 25);
    printf("Other: state updated\n");
}

In this code, we have a forward declaration of the test_st struct. This forward declaration tells the compiler that a struct named test_st exists, but its attributes are unknown at this point. To access and work with the struct‘s data, we need functions that operate on pointers declared in our main.c file:

$ cat main.c
#include <stdio.h>

struct test_st {
    int state;
    int status;
};

struct test_st instance = { .state = 5, .status = 10 };

void get_struct_ptr(struct test_st **pptr) {
    *pptr = &instance;
}

int get_state(struct test_st *ptr) {
    return ptr->state;
}

void set_state(struct test_st *ptr, int new_state) {
    ptr->state = new_state;
}

int main() {
    printf("Main: initial state=%d\n", instance.state);
    external_function();
    printf("Main: modified state=%d\n", instance.state);
    return 0;
}

Here, we’ve defined the struct and the functions that work based on the pointers. Let’s compile the program and see the result:

$ gcc main.c othersrc.c -o opaque
$ ./opaque
Main: initial state=5
Other: current state = 5
Other: state updated
Main: modified state=25

The results show that the othersrc.c file can now access the defined struct in main.c and modify its attributes using pointers. This approach is suitable when working with large projects to reduce recompilation time. The downside is that it’s more complex to maintain.

5. Comparing Both Approaches

Although both of these methods can help us achieve our goal, we have cases where they’re used ideally:

Aspect Header File Method Pointer Method
Access to struct Members Direct access to struct members (e.g., struct_name.struct_member) Indirect access via functions
Ease of Use Simple and straightforward Requires extra accessor functions
Code Maintenance Centralized struct definition More modular, encapsulates data
Compilation Requires recompilation if the header changes Reduces recompilation in some cases
Use Case More general-purpose projects Ideal for building libraries that need encapsulation

At a glance, the header file method provides more organization and is ideal for simple, straightforward projects. Conversely, the pointer method is ideal for projects that require some level of encapsulation and privacy.

6. Conclusion

In this article, we discussed two ways to share struct information between source files in C. The header file approach provides a clear, concise solution, whereas the pointer-based method is suited for instances where we don’t want to expose implementation details. We also considered the trade-offs in areas of compilation, code maintenance, and ease of use.