Modern C: Derived data types
In Chapter 6: Derived data types of Jens Gustedt’s Modern C: A Guide to the C23 Standard, the focus shifts from basic numerical and scalar types to constructing complex data structures. C provides four main strategies for deriving data types (Arrays, Structures, Pointers, and Unions) plus a mechanism for creating type aliases (typedef).
Here is a detailed breakdown of the fundamental concepts and key takeaways from Chapter 6 across its four main sections:
1. Grouping Objects into Arrays (Section 6.1)
Arrays combine multiple subobjects of the same base type into a single encapsulating object.
- Arrays Are Not Pointers: Gustedt strongly emphasizes that arrays and pointers are distinct concepts. While array names decay to pointers in many contexts, treating them as identical creates deep confusion.
- Array Objects vs. Array Values:
- There are array objects, but there are no array values.
- Because arrays lack values, arrays cannot be assigned to (e.g.,
A = Bis illegal) and cannot be compared using==or!=. - In boolean conditional contexts, an array always evaluates to
truedue to array-to-pointer decay.
- Array Lengths (FLAs vs. VLAs):
- Fixed-Length Arrays (FLAs): Length is determined by an Integer Constant Expression (ICE) at compile time or inferred directly from an initializer.
- Variable-Length Arrays (VLAs): Length is evaluated at runtime (non-ICE). In C23, automatic VLAs are optional (
__STDC_NO_VLA__), can only use default initializers{}, and cannot be declared at file scope (globals). - Array Size Formula: For any array
A, its element count is computed as(sizeof A) / (sizeof A[0]).
- Array Parameters to Functions:
- When passed to a function, the innermost dimension of an array parameter is lost and rewritten to a pointer.
- Inside function bodies, array parameters behave as if passed by reference, and using
sizeofon an array parameter yields the size of the pointer, not the array.
- Strings:
- A string in C is a
0-terminated array ofchar. For example, the string literal"hello"has a length of 5 visible characters, but occupies 6 bytes in memory (ending with'\0'). - Passing a character array that is not null-terminated to standard string functions (like
strlenorstrcpy) causes program failure or undefined behavior.
- A string in C is a
Example
Arrays combine objects of the same type. Arrays are not pointers, and there are no array values—meaning arrays cannot be directly assigned or compared using ==.
#include <stdio.h>
#include <stddef.h>
// Function taking an array parameter:
// Note: In function headers, 'double A[len]' is rewritten by the compiler to 'double* A'.
void print_array_info(size_t len, double const A[len]) {
// INSIDE A FUNCTION: sizeof A yields the size of the POINTER, not the array!
printf("Inside function: sizeof A = %zu bytes (size of pointer!)\n", sizeof A);
for (size_t i = 0; i < len; ++i) {
printf("%g ", A[i]);
}
printf("\n");
}
void demo_arrays(void) {
// 1. Fixed-Length Array (FLA) initialized with C23 universal initializer {}
double weights = {1.5, 2.5, 3.5, 4.5};
// Array element count formula: (sizeof A) / (sizeof A)
size_t count = sizeof weights / sizeof weights;
printf("Array element count: %zu (Total bytes: %zu)\n", count, sizeof weights);
// Arrays decay to pointers when passed to functions:
print_array_info(count, weights);
// 2. Variable-Length Array (VLA): Length evaluated at runtime
size_t vla_len = 3;
double vla_arr[vla_len]; // VLA allocated on stack for dynamic size
vla_arr = 10.0;
// 3. Strings: 0-terminated character arrays
// "C23" has 3 visible characters, but occupies 4 bytes ending in '\0'
char const str[] = "C23";
printf("String '%s' has sizeof = %zu (includes null terminator '\\0')\n", str, sizeof str);
// ARRAYS ARE NOT ASSIGNABLE OR COMPARABLE:
// double copy; copy = weights; // COMPILER ERROR! Cannot assign arrays
// if (weights == copy) ... // ILL-ADVISED: Compares pointer addresses, not array elements!
}2. Pointers as Opaque Types (Section 6.2)
Before delving into address arithmetic in later levels, Gustedt introduces pointers strictly as opaque reference types.
- Pointer States: A pointer does not hold data directly; it refers or “points” to data elsewhere in memory. A pointer is always in one of three states: valid, null, or invalid.
- Null Pointers:
- Initializing or assigning a pointer with C23’s
nullptrkeyword puts it in a null state. - In boolean logical expressions, null pointers evaluate to
false.
- Initializing or assigning a pointer with C23’s
- Invalid Pointers & Mandatory Initialization:
- Uninitialized local pointers contain arbitrary, indeterminate addresses (invalid pointers). Dereferencing or evaluating an invalid pointer leads to program failure.
- Rule: Always explicitly initialize pointers (either to a valid target address or to
nullptr).
Example
Pointers are reference types that refer to objects in memory. A pointer is always in one of three states: valid, null, or invalid.
#include <stdio.h>
void demo_pointers_opaque(void) {
// 1. Null Pointer State: Explicitly initialized using C23 'nullptr'
double* ptr1 = nullptr;
// In boolean logical contexts, null pointers evaluate to 'false'
if (!ptr1) {
printf("ptr1 is nullptr (evaluates to false in boolean context)\n");
}
// 2. Valid Pointer State: Refers to an existing object's memory address
double val = 42.0;
double* ptr2 = &val; // Valid pointer referring to 'val'
if (ptr2) { // Evaluates to 'true'
printf("ptr2 is valid, points to value: %g\n", *ptr2);
}
// 3. Invalid Pointer State:
// double* uninit_ptr; // DANGER: Uninitialized local pointer contains arbitrary address.
// Evaluating or dereferencing an uninitialized pointer leads to UNDEFINED BEHAVIOR!
// RULE: Always initialize pointers upon definition (e.g., to nullptr or a valid object).
}3. Combining Objects into Structures (Section 6.3)
Structures (struct) group items that may have different base types into a single cohesive data unit accessed by named members.
- Member Access & Passing Semantics:
- Structure fields/members are accessed using the dot operator (e.g.,
today.tm_year). - Structure arguments are passed by value to functions (the entire structure object is copied).
- Structure fields/members are accessed using the dot operator (e.g.,
- Assignment vs. Comparison:
- Structures can be assigned using
=(copying all fields). - Structures cannot be compared using
==or!=.
- Structures can be assigned using
- Padding & Memory Alignment:
- Compilers place structure fields in the order declared, but may insert padding bytes between fields to align members on hardware word boundaries.
- There is no padding at the start of a structure. Reordering structure members (e.g., placing larger types first) can significantly reduce wasted padding memory.
- Bit-Fields in C23:
- Bit-fields allow specifying exact bit widths for structure members (e.g., for flags or compact data).
- Avoid bare
intfor bit-fields due to implementation-defined signedness issues. Use_BitInt(N)for numerical bit-fields of width \(N\), andboolfor single-bit flags.
Example
Structures group items of different base types into a cohesive unit. Structs can be assigned, are passed by value, and can use padding/bit-fields.
#include <stdio.h>
#include <stdbool.h>
// Struct definition with member alignment padding considerations:
// Reordering larger fields first reduces wasted padding bytes.
struct sensor_reading {
double value; // 8 bytes
unsigned long timestamp; // 8 bytes
// C23 Bit-Fields: Use _BitInt(N) for bit-width numbers and bool for flags
unsigned _BitInt(4) status_code : 4; // 4-bit status code (0 to 15)
bool is_active : 1; // 1-bit boolean flag
};
// Functions receive struct parameters BY VALUE (copies the entire struct)
void print_reading(struct sensor_reading s) {
printf("Sensor [TS: %lu]: %g (Status: %u, Active: %s)\n",
s.timestamp, s.value, (unsigned int)s.status_code, s.is_active ? "yes" : "no");
}
void demo_structures(void) {
// Designated Initializers: Explicitly initialize named fields; unlisted fields zero out
struct sensor_reading r1 = {
.value = 98.6,
.timestamp = 1600000000UL,
.status_code = 3wb, // C23 bit-precise integer literal
.is_active = true
};
print_reading(r1);
// STRUCTURE ASSIGNMENT (=): Copies all fields directly from r1 to r2
struct sensor_reading r2 = r1;
r2.value = 100.2;
printf("r1.value = %g, r2.value = %g (Structs are independent copies!)\n", r1.value, r2.value);
// NOTE: Structs CANNOT be compared directly!
// if (r1 == r2) ... // COMPILER ERROR! Struct comparison is illegal in C.
}4. Type Aliases (typedef) (Section 6.4)
The typedef keyword introduces user-defined names for existing types.
- Aliases, Not New Types: A
typedefcreates a new alias/name for an existing type; it never creates a new distinct type in C’s type system. - Forward-Declaring Structs: To avoid repeatedly typing the
structkeyword, forward-declare the structure tag name inside atypedefusing the identical identifier (e.g.,typedef struct person person;). - Reserved Naming Conventions: Identifiers ending with
_t(such assize_t,ptrdiff_t) are reserved by the standard library and POSIX. Avoid naming custom types with a_tsuffix to prevent future standard header conflicts.
Example
typedef creates a convenient alias for an existing type; it does not create a new distinct type.
#include <stdio.h>
// Forward-declaring a struct tag name with a typedef using the exact same identifier
typedef struct bird bird;
// Complete struct definition matching the forward declaration
struct bird {
char const* name;
double wingspan;
};
// Creating a type alias for a standard type
typedef double velocity_mps; // 'velocity_mps' is an alias for 'double'
void demo_typedef(void) {
// Clean usage without needing the 'struct' keyword repeatedly
bird raven = { .name = "Raven", .wingspan = 1.2 };
velocity_mps speed = 15.5; // Exactly equivalent to 'double speed = 15.5;'
printf("Bird: %s, Wingspan: %g m, Speed: %g m/s\n",
raven.name, raven.wingspan, speed);
// BEST PRACTICE: Avoid naming custom typedefs with a '_t' suffix (e.g., bird_t),
// because identifiers ending in '_t' are reserved by standard libraries and POSIX!
}