Understanding C Structures: Declaration, Initialization, Member Access, and Parameter Passing

  • Introduction to Structures
  • Declaring Structure Types
  • Structure Member Types
  • Initializing Structure Variables
  • Accessing Structure Members
  • Passing Structures to Functions

Introduction

Structures represent one of the most fundamental concepts in C programming for creating complex data types. A structure is a collection of values called members, where each member can be of a different type. Unlike arrays that contain elements of the same type, structures allow you to combine different data types into a single entity.

Consider a practical scenario: representing a person requires storing multiple attriubtes such as name, age, gender, height, and telephone number. These attributes have different data types, making structures the ideal choice for encapsulating all this information in to a single variable.

Declaring Structure Types

Before creating structure variables, you must first declare a structure type. The general syntax follows this pattern:

struct tag {
    member_list;
} variable_list;

The struct keyword is mandatory, while tag is a user-defined name for the structure type. The variable_list is optional and contains one or more variables of this structure type.

Method 1: Declaration Without Varible List

This approach only declares the structure type without creating any variables:

struct Person {
    char fullName[50];
    int birthYear;
    char occupation[30];
};

This declaration creates a structure type struct Person but does not allocate any memory. Memory allocation occurs only when you create actual variables of this type.

Method 2: Declaration With Variable List

This approach declares the type and creates variables simultaneously:

struct Employee {
    char identifier[20];
    float salary;
} staffOne, staffTwo;

Here, staffOne and staffTwo are global structure variables. While this method works, using global variables is generally discouraged due to potential side effects and reduced code modularity.

Important Notes on Scope

Structure type declarations typically appear outside the main() function, making them available throughout the entire file. You can also declare structure types within functions, but such declarations create local types visible only within that function's scope.

Key Concept: Declaring a structure type does not allocate memory in the system. Think of a structure type as a blueprint for a house—the blueprint itself occupies no physical space. Memory allocation happens only when you create actual variables (analogous to building the house).

Structure Member Types

Structure members can be of any valid C type, including scalar types, arrays, pointers, and even other structures. This flexibility enables the creation of complex nested data types.

#include <stdio.h>

struct Contact {
    char email[50];
    int zipCode;
};

struct Person {
    char name[30];
    int age;
    float height;
    char phoneNumber[15];
    struct Contact contactInfo;
} personAlpha, personBeta;

struct Contact branchOffice;

int main(void) {
    struct Person personOne;
    return 0;
}

In this example, personOne is a local variable, while personAlpha, personBeta, and branchOffice are global variables. The Person structure contains another structure (Contact) as one of its members, demonstrating nested structures.

Initializing Structure Variables

Initializing a structure variable means providing initial values at the time of creation. Initialization can be complete (all members assigned values) or partial (only some members assigned values).

Complete Initialization

#include <stdio.h>

struct Person {
    char name[30];
    int age;
    char occupation[30];
    float height;
    char phone[15];
} employeeX, employeeY;

int main(void) {
    struct Person manager = { "John Smith", 42, "Engineering Lead", 178.5, "555-1234" };
    return 0;
}

Values are assigned in the order that members appear in the structure declaration, separated by commas.

Initializing Nested Structures

When a structure contains another structure as a member, the nested structure is initialized using curly braces:

#include <stdio.h;

struct Address {
    char street[50];
    int houseNumber;
    char city[30];
};

struct EmployeeRecord {
    char name[30];
    int departmentId;
    struct Address residence;
};

int main(void) {
    struct EmployeeRecord staff = {
        "Alice Johnson",
        101,
        { "Broadway Avenue", 42, "New York" }
    };
    return 0;
}

Partial Initialization

When fewer initializers are provided than total members, uninitialized members receive default values. Numeric types get zero, while character arrays get null characters:

#include <stdio.h>

struct Address {
    char street[50];
    int houseNumber;
    char city[30];
};

struct EmployeeRecord {
    char name[30];
    int departmentId;
    struct Address residence;
};

int main(void) {
    struct EmployeeRecord staff = {
        "Alice Johnson",
        101,
        { "Main Street", 256 }
    };
    return 0;
}

After execution, the city field contains an empty string (implicitly initialized to '\0'). This behavior mirrors array initialization rules in C.

Behavior of Uninitialized Members

Understanding default values is crucial:

  • Uninitialized numeric types: Contain indeterminate values (garbage values)
  • Uninitialized character arrays: Filled with '\0' characters (null terminators)
  • Nested structures: Each member is initialized according to its type's rules

Accessing Structure Members

C provides two operators for accessing structure members: the dot operator (.) and the arrow operator (->).

Dot Operator (.)

Used when working directly with a structure variable:

structureVariable.memberName

Arrow Operator (->)

Used when working with a pointer to a structure:

structurePointer->memberName

These operators are equivalent:

(*structurePointer).memberName
structurePointer->memberName

Complete Example

#include <stdio.h>

struct ContactDetails {
    char email[50];
    int extension;
};

struct TeamMember {
    char name[30];
    int yearsExperience;
    float performanceScore;
    struct ContactDetails contact;
};

void displayInfo(struct TeamMember* memberData) {
    printf("Name: %s\n", (*memberData).name);
    printf("Experience: %d years\n", memberData->yearsExperience);
    printf("Score: %.2f\n", memberData->performanceScore);
    printf("Email: %s\n", memberData->contact.email);
}

int main(void) {
    struct TeamMember leader = {
        "Robert Chen",
        15,
        94.7,
        { "r.chen@company.com", 4521 }
    };
    
    printf("Primary Member:\n");
    printf("Name: %s\n", leader.name);
    printf("Experience: %d years\n", leader.yearsExperience);
    printf("Score: %.2f\n", leader.performanceScore);
    printf("Email: %s\n", leader.contact.email);
    
    printf("\nUsing Pointer Access:\n");
    displayInfo(&leader);
    
    return 0;
}

This example demonstrates all three access methods: direct member access via the dot operator, pointer dereferencing combined with the dot operator, and the arrow operator for pointer-based access.

Passing Structures to Functions

When passing structures to functions, you can choose between pass-by-value and pass-by-address approaches. Each method has distinct implications for performance and behavior.

Pass-by-Value

The function receives a copy of the structure. Modifications inside the function do not affect the original:

#include <stdio.h>

struct Point {
    int horizontal;
    int vertical;
};

void modifyByValue(struct Point coordinates) {
    coordinates.horizontal = 100;
    coordinates.vertical = 200;
    printf("Inside pass-by-value function: (%d, %d)\n", 
           coordinates.horizontal, coordinates.vertical);
}

int main(void) {
    struct Point position = { 10, 20 };
    modifyByValue(position);
    printf("After function call: (%d, %d)\n", 
           position.horizontal, position.vertical);
    return 0;
}

Pass-by-Address

The function receives the address of the structure, enabling direct modification of the original data:

#include <stdio.h>

struct Point {
    int horizontal;
    int vertical;
};

void modifyByAddress(struct Point* coordinates) {
    coordinates->horizontal = 100;
    coordinates->vertical = 200;
    printf("Inside pass-by-address function: (%d, %d)\n", 
           coordinates->horizontal, coordinates->vertical);
}

int main(void) {
    struct Point position = { 10, 20 };
    modifyByAddress(&position);
    printf("After function call: (%d, %d)\n", 
           position.horizontal, position.vertical);
    return 0;
}

Performance Considerations

Pass-by-address is generally preferred for the following reasons:

  • Memory efficiency: Only the address (typically 4-8 bytes) is copied, not the entire structure
  • Time efficiency: Avoids the overhead of copying large data structures
  • Stack usage: Reduces stack frame size, preventing potential stack overflow with large structures

When a structure contains many members or large arrays, the stack space required for copying can become significant. Consider a structure with hundreds of members or containing large arrays—the memory allocation and copying operations introduce measurable overhead.

Performance Analysis

Function calls involve stack operations regardless of parameter type. However, the cost scales differently:

  • Pass-by-value: Stack usage proportional to structure size
  • Pass-by-address: Fixed stack usage (pointer size), regardless of structure size

For optimal performance, especially with large structures, always pass structures by address (pointer). This practice minimizes both temporal and spatial complexity associated with function calls.

Posted on Tue, 23 Jun 2026 16:19:27 +0000 by calexa