Data Types in C: Primitive, Derived & User-Defined
Data Types in C: A Comprehensive Tutorial for Programmers
Data types form the foundation of data representation in C. Every variable must have a data type that defines what kind of value it can store, how much memory it occupies, and what operations are valid on it. Understanding data types is essential for writing correct, efficient, and portable C programs.
This tutorial provides an in-depth exploration of C data types: primitive types, derived types, user-defined types, type modifiers, and practical considerations for memory and portability.
Conceptual Introduction & Fundamentals
What are Data Types?
A data type is a classification that specifies:
- Memory size: How many bytes are allocated for a variable of that type.
- Value range: The minimum and maximum values it can hold.
- Operations: What operations are valid (e.g., arithmetic on integers, indexing on arrays).
- Representation: How the bits are interpreted (e.g., two’s complement for signed integers, IEEE 754 for floats).
Why Data Types Matter
- Correctness: Using the wrong type (e.g.,
intfor a very large number) causes overflow. - Efficiency: Choosing the right type (e.g.,
charvsintfor a small counter) saves memory and improves performance. - Portability: Sizes vary across platforms; understanding this helps write portable code.
- Clarity: Types document intent (e.g.,
unsigned intfor a count that cannot be negative).
Classification of Data Types
C data types are broadly classified into:
- Primitive (Basic) Types: Built-in types for simple values.
int(integer)char(character)float(single-precision floating-point)double(double-precision floating-point)void(no value)
- Derived Types: Created from primitive types.
- Arrays
- Pointers
- Functions
- User-Defined Types: Defined by the programmer.
- Structures (
struct) - Unions (
union) - Enumerations (
enum)
- Structures (
- Type Modifiers: Modify the size and signedness of primitive types.
short,long,long longsigned,unsigned
Syntax & Core Mechanics
Primitive Data Types
Integer (int)
Stores whole numbers. Size is typically 4 bytes (32 bits), but depends on the platform.
c
#include <stdio.h> int main(void) { int count = 42; int negative = -100; int zero = 0; printf("count = %d\n", count); printf("negative = %d\n", negative); printf("zero = %d\n", zero); return 0; } Expected output:
text
count = 42 negative = -100 zero = 0
Format specifier: %d or %i for signed decimal.
Character (char)
Stores a single character. Size is always 1 byte. Can be signed or unsigned depending on the platform.
c
#include <stdio.h> int main(void) { char letter = 'A'; char digit = '5'; char newline = '\n'; printf("letter = %c\n", letter); printf("digit = %c\n", digit); printf("newline = %c", newline); /* prints a newline */ return 0; } Expected output:
text
letter = A digit = 5 newline =
Format specifier: %c for character; %hhd for signed char as integer.
Float (float)
Stores single-precision floating-point numbers. Typically 4 bytes.
c
#include <stdio.h> int main(void) { float price = 12.45f; /* 'f' suffix for float literal */ float pi = 3.14159f; printf("price = %f\n", price); printf("pi = %.2f\n", pi); return 0; } Expected output:
text
price = 12.450000 pi = 3.14
Format specifier: %f for decimal notation; %e for scientific; %g for auto.
Double (double)
Stores double-precision floating-point numbers. Typically 8 bytes, with ~15-17 significant decimal digits.
c
#include <stdio.h> int main(void) { double preciseValue = 1.4521; double large = 1.7e308; printf("preciseValue = %lf\n", preciseValue); printf("large = %e\n", large); return 0; } Expected output:
text
preciseValue = 1.452100 large = 1.700000e+308
Format specifier: %lf for printf (actually %f works for double in printf due to default promotion); %lf is required for scanf.
Void (void)
Represents no value. Used for:
- Functions with no return value.
- Generic pointers (
void *).
c
#include <stdio.h> /* Function returning void */ void greet(void) { printf("Hello, welcome!\n"); } int main(void) { greet(); return 0; } Expected output:
text
Hello, welcome!
Type Modifiers
Modifiers change the size and/or signedness of integer and floating-point types.
| Modifier | Effect |
|---|---|
short | Reduces size of int (at least 16 bits) |
long | Increases size of int or double (at least 32 bits for int, 64 for long int on some platforms) |
long long | At least 64 bits (C99) |
signed | Explicitly signed (can store negative and positive) |
unsigned | Unsigned (only non-negative, doubles the positive range) |
Examples:
c
#include <stdio.h> #include <limits.h> /* for INT_MAX, etc. */ int main(void) { short int s = 32767; /* at least 16 bits */ long int l = 2147483647L; /* at least 32 bits */ long long int ll = 9223372036854775807LL; /* at least 64 bits */ unsigned int u = 4294967295U; /* 0 to 4,294,967,295 (32-bit) */ printf("short: %hd\n", s); printf("long: %ld\n", l); printf("long long: %lld\n", ll); printf("unsigned: %u\n", u); return 0; } Expected output (typical 64-bit system):
text
short: 32767 long: 2147483647 long long: 9223372036854775807 unsigned: 4294967295
Format specifiers for modified types:
%hdforshort int%ldforlong int%lldforlong long int%uforunsigned int%zuforsize_t(size ofsizeof)
Derived Data Types
Arrays
Arrays store multiple values of the same type in contiguous memory. Access uses zero-based indexing.
c
#include <stdio.h> int main(void) { int arr[5] = {10, 20, 30, 40, 50}; printf("First element: %d\n", arr[0]); printf("Third element: %d\n", arr[2]); printf("Array size: %zu bytes\n", sizeof(arr)); /* Loop through array */ for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; } Expected output:
text
First element: 10 Third element: 30 Array size: 20 bytes 10 20 30 40 50
Pointers
Pointers store memory addresses. Use & to get an address, * to declare and dereference.
c
#include <stdio.h> int main(void) { int x = 10; int *ptr = &x; /* ptr holds address of x */ printf("Value of x: %d\n", x); printf("Address of x: %p\n", (void*)&x); printf("Value using pointer: %d\n", *ptr); *ptr = 20; /* modify x through pointer */ printf("Modified x: %d\n", x); return 0; } Expected output (address will vary):
text
Value of x: 10 Address of x: 0x7fffffffe9c4 Value using pointer: 10 Modified x: 20
Functions
Functions are derived types with a return type and parameter list. They encapsulate reusable code.
c
#include <stdio.h> /* Function definition */ int add(int a, int b) { return a + b; } int main(void) { int result = add(10, 20); printf("Result: %d\n", result); return 0; } Expected output:
text
Result: 30
User-Defined Data Types
Structures (struct)
Structures group variables of different types under one name.
c
#include <stdio.h> #include <string.h> struct Student { char name[50]; int age; float gpa; }; int main(void) { struct Student s1; strcpy(s1.name, "Riya"); /* safe: s1.name has 50 bytes */ s1.age = 20; s1.gpa = 3.8f; printf("Name: %s\n", s1.name); printf("Age: %d\n", s1.age); printf("GPA: %.1f\n", s1.gpa); return 0; } Expected output:
text
Name: Riya Age: 20 GPA: 3.8
Unions (union)
Unions share the same memory for all members. Only one member is valid at a time.
c
#include <stdio.h> union Data { int i; float f; char c; }; int main(void) { union Data data; data.i = 10; printf("Integer: %d\n", data.i); data.f = 5.5f; /* overwrites data.i */ printf("Float: %f\n", data.f); printf("Size of union: %zu bytes\n", sizeof(data)); return 0; } Expected output:
text
Integer: 10 Float: 5.500000 Size of union: 4 bytes
Enumerations (enum)
Enums define a set of named integer constants.
c
#include <stdio.h> enum Day { MON, TUE, WED, THU, FRI, SAT, SUN /* values 0 to 6 */ }; int main(void) { enum Day today = WED; printf("Day value: %d\n", today); enum Day weekend = SAT; printf("Weekend value: %d\n", weekend); return 0; } Expected output:
text
Day value: 2 Weekend value: 5
sizeof Operator
sizeof returns the size in bytes of a type or expression. It is a compile-time operator (except for variable-length arrays in C99).
c
#include <stdio.h> int main(void) { printf("Size of int: %zu bytes\n", sizeof(int)); printf("Size of char: %zu byte\n", sizeof(char)); printf("Size of float: %zu bytes\n", sizeof(float)); printf("Size of double: %zu bytes\n", sizeof(double)); printf("Size of short: %zu bytes\n", sizeof(short)); printf("Size of long: %zu bytes\n", sizeof(long)); printf("Size of long long: %zu bytes\n", sizeof(long long)); return 0; } Expected output (typical 64-bit Linux):
text
Size of int: 4 bytes Size of char: 1 byte Size of float: 4 bytes Size of double: 8 bytes Size of short: 2 bytes Size of long: 8 bytes Size of long long: 8 bytes
Note: On 32-bit Windows, long is 4 bytes. Always use sizeof rather than hardcoding sizes.
Full Practical Code Example with Explanation
This program demonstrates various data types in a realistic scenario: managing a small student database with structures, arrays, pointers, and proper memory handling.
Program: Student Record Manager
c
/** * student_manager.c * Demonstrates data types: struct, arrays, pointers, int, float, char. * Manages a list of student records with dynamic memory allocation. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_NAME_LEN 50 #define MAX_STUDENTS 100 /* Structure to represent a student */ typedef struct { char name[MAX_NAME_LEN]; int age; float gpa; } Student; /* Function prototypes */ void printStudent(const Student *s); int readInteger(const char *prompt, int *outValue); int readFloat(const char *prompt, float *outValue); int main(void) { /* Array of students (static allocation) */ Student students[MAX_STUDENTS]; int studentCount = 0; printf("Student Record Manager\n"); printf("Enter student details (type 'quit' to stop).\n\n"); while (studentCount < MAX_STUDENTS) { char nameBuffer[MAX_NAME_LEN]; printf("Student %d name: ", studentCount + 1); fflush(stdout); if (fgets(nameBuffer, sizeof(nameBuffer), stdin) == NULL) { fprintf(stderr, "Error reading name.\n"); break; } /* Remove trailing newline */ nameBuffer[strcspn(nameBuffer, "\n")] = '\0'; /* Check for quit command */ if (strcmp(nameBuffer, "quit") == 0) { break; } /* Copy name to student record */ strcpy(students[studentCount].name, nameBuffer); /* Read age */ if (readInteger("Age: ", &students[studentCount].age) != 0) { fprintf(stderr, "Invalid age.\n"); continue; } /* Read GPA */ if (readFloat("GPA: ", &students[studentCount].gpa) != 0) { fprintf(stderr, "Invalid GPA.\n"); continue; } studentCount++; printf("\n"); } /* Print all students using pointer traversal */ printf("\n--- Student List ---\n"); for (int i = 0; i < studentCount; i++) { Student *ptr = &students[i]; /* pointer to current student */ printf("Student %d:\n", i + 1); printStudent(ptr); printf("\n"); } /* Calculate average GPA using float */ if (studentCount > 0) { float sum = 0.0f; for (int i = 0; i < studentCount; i++) { sum += students[i].gpa; } float average = sum / studentCount; printf("Average GPA: %.2f\n", average); } return EXIT_SUCCESS; } /* Prints a student's details using a const pointer */ void printStudent(const Student *s) { printf("Name: %s\n", s->name); printf("Age: %d\n", s->age); printf("GPA: %.2f\n", s->gpa); } /* Reads an integer safely */ int readInteger(const char *prompt, int *outValue) { char buffer[32]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { return -1; } val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) { return -1; } *outValue = (int)val; return 0; } /* Reads a float safely */ int readFloat(const char *prompt, float *outValue) { char buffer[64]; char *endPtr; double val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { return -1; } val = strtod(buffer, &endPtr); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) { return -1; } *outValue = (float)val; return 0; } Explanation of Data Type Usage
Studentstructure: Combineschararray,int, andfloatinto a single user-defined type.Student students[MAX_STUDENTS]: Array of structures (derived type + user-defined type).Student *ptr: Pointer to a structure (derived type).int studentCount: Primitive integer for counting.float sum,average: Primitive floating-point for calculations.char nameBuffer[]: Character array for input buffer.const Student *sinprintStudent: Constant pointer parameter to prevent modification.
Expected Output
Sample interaction:
text
Student Record Manager Enter student details (type 'quit' to stop). Student 1 name: Alice Johnson Age: 20 GPA: 3.8 Student 2 name: Bob Smith Age: 22 GPA: 3.5 Student 3 name: quit --- Student List --- Student 1: Name: Alice Johnson Age: 20 GPA: 3.80 Student 2: Name: Bob Smith Age: 22 GPA: 3.50 Average GPA: 3.65
Common Pitfalls, Edge Cases & Memory Considerations
1. Integer Overflow
Signed integer overflow is undefined behavior. Unsigned integer overflow wraps around.
c
int max = 2147483647; max++; /* Undefined behavior */ unsigned int u = 4294967295U; u++; /* Wraps to 0 (well-defined) */
Solution: Check ranges before operations, use larger types, or use compiler sanitizers.
2. Floating-Point Precision
Floating-point numbers are approximations. Comparing for equality can fail.
c
float a = 0.1f; float b = 0.2f; if (a + b == 0.3f) { /* May be false due to rounding */ printf("Equal\n"); } Solution: Use an epsilon comparison: fabs(a + b - 0.3f) < 0.0001f.
3. Character Type Signedness
char can be signed or unsigned depending on the platform. Using it for arithmetic can cause surprises.
c
char c = 200; /* May overflow if char is signed (range -128 to 127) */ printf("%d\n", c); /* May print -56 on some platforms */ Solution: Use unsigned char for byte values 0-255, or signed char explicitly.
4. Type Conversion and Promotion
Implicit conversions can lose data. Integer promotion in expressions can cause unexpected results.
c
char a = 100; char b = 100; char c = a + b; /* a and b promoted to int, result 200, then truncated to char */ printf("%d\n", c); /* May print -56 if char is signed */ Solution: Use explicit casts and be aware of promotion rules.
5. Using Wrong Format Specifiers
Mismatched format specifiers lead to undefined behavior.
c
int x = 100; printf("%f\n", x); /* BUG: expects double, gets int */ float f = 3.14f; printf("%d\n", f); /* BUG: expects int, gets double (after promotion) */ Solution: Always match the format specifier to the actual argument type.
6. sizeof Pitfalls
sizeofon an array parameter inside a function returns the pointer size, not the array size.
c
void func(int arr[]) { printf("%zu\n", sizeof(arr)); /* Prints pointer size (8 on 64-bit), not array size */ } sizeofon a variable-length array (VLA) is evaluated at runtime, not compile time.
7. Memory Layout of Structures
Structures may have padding between members for alignment. sizeof(struct) may be larger than the sum of member sizes.
c
struct Example { char c; /* 1 byte */ int i; /* 4 bytes */ }; /* sizeof may be 8, not 5, due to padding */ Solution: Don’t assume structure size; use sizeof. Use #pragma pack or __attribute__((packed)) only when necessary.
8. Union Misuse
Reading from a different union member than the last written is undefined behavior (except for type punning with unsigned char arrays).
c
union Data d; d.i = 10; printf("%f\n", d.f); /* Undefined behavior */ Solution: Track which member is active, or use separate variables.
9. Large Arrays on Stack
Large automatic arrays can cause stack overflow.
c
void func(void) { int bigArray[1000000]; /* 4 MB on stack, may overflow */ } Solution: Use static or dynamic allocation with malloc.
10. Portability of Sizes
Data type sizes vary across platforms. Do not hardcode sizes; use sizeof and standard integer types from <stdint.h> (e.g., int32_t, uint64_t).
c
#include <stdint.h> int32_t preciseInt = 100; /* exactly 32 bits */ uint64_t bigUnsigned = 12345678901234567890ULL;
Summary & Best Practices
Data types in C define how data is stored, the range of values, and valid operations. Choosing the right type is crucial for correctness, efficiency, and portability.
Key Takeaways
- Primitive types:
int,char,float,double,void. - Modifiers adjust size and signedness:
short,long,long long,signed,unsigned. - Derived types include arrays, pointers, and functions.
- User-defined types:
struct,union,enum. sizeofgives the size in bytes; sizes are platform-dependent.- Use
<stdint.h>for fixed-width integer types when portability is critical.
Best Practices
- Choose the appropriate type for the data you need to store (e.g.,
unsignedfor counts that cannot be negative). - Use
sizeofinstead of hardcoding sizes. - Use fixed-width types (
int32_t, etc.) for cross-platform compatibility. - Match format specifiers to data types in
printfandscanf. - Use
floatfor memory-constrained environments,doublefor general computation. - Avoid implicit conversions that may lose precision; cast explicitly.
- Be aware of structure padding; use
sizeoffor structure size. - Use
constfor variables that should not change. - For large arrays, use dynamic allocation instead of stack arrays.
- Compile with warnings (
-Wall -Wextra) to catch type mismatches and conversion issues.
By mastering data types, you’ll write C code that is efficient, safe, and portable across platforms.



