C is a statically-typed language, meaning every variable must be declared with a specific data type before it can be used. Understanding variables and data types is fundamental to writing efficient and correct C programs.
Table of Contents
- What are Variables?
- Variable Declaration and Definition
- Basic Data Types
- Type Modifiers
- Storage Classes
- Constants and Literals
- Type Conversion
- Scope and Lifetime
- Best Practices
What are Variables?
A variable is a named memory location that stores a value. In C, variables have:
- Name: Identifier used to reference the variable
- Type: Determines the size and layout of memory
- Value: The data stored in the variable
- Address: Memory location where the variable is stored
int age = 25; // name: age, type: int, value: 25, address: &age
Variable Declaration and Definition
Declaration vs Definition
Declaration: Announces the existence of a variable to the compiler
extern int count; // declaration only, no memory allocated
Definition: Declares AND allocates memory for the variable
int count; // definition (tentative) int count = 10; // definition with initialization
Rules for Variable Names
- Must begin with a letter or underscore (
_) - Can contain letters, digits, and underscores
- Case-sensitive (
age≠Age≠AGE) - Cannot use C keywords (e.g.,
int,if,while) - Should be meaningful (prefer
student_ageoversa)
// Valid names int student_age; float _temperature; char firstName[20]; int counter1; // Invalid names int 2nd_attempt; // cannot start with digit float my-name; // hyphen not allowed char class; // 'class' is a keyword in C++ int if; // 'if' is a keyword
Multiple Declarations
int a, b, c; // multiple variables, same type int x = 10, y = 20, z = 30; // with initialization float price, tax = 0.05, total; // mixed initialization
Basic Data Types
C provides several fundamental data types:
1. Integer Types
| Type | Size (bytes)* | Range | Format Specifier |
|---|---|---|---|
char | 1 | -128 to 127 or 0 to 255 | %c |
signed char | 1 | -128 to 127 | %hhd |
unsigned char | 1 | 0 to 255 | %hhu |
short | 2 | -32,768 to 32,767 | %hd |
unsigned short | 2 | 0 to 65,535 | %hu |
int | 4 | -2,147,483,648 to 2,147,483,647 | %d |
unsigned int | 4 | 0 to 4,294,967,295 | %u |
long | 4 or 8 | -2,147,483,648 to 2,147,483,647 (32-bit) | %ld |
unsigned long | 4 or 8 | 0 to 4,294,967,295 (32-bit) | %lu |
long long | 8 | -9.22×10¹⁸ to 9.22×10¹⁸ | %lld |
unsigned long long | 8 | 0 to 1.84×10¹⁹ | %llu |
* Sizes are platform-dependent; shown for typical 32/64-bit systems
#include <stdio.h>
int main() {
char grade = 'A';
signed char temperature = -15;
unsigned char small_count = 200;
short year = 2023;
unsigned short population = 50000;
int count = 1000;
unsigned int positive = 4000000000U; // 'U' suffix for unsigned
long distance = 123456789L; // 'L' suffix for long
long long big_number = 123456789012345LL; // 'LL' suffix
printf("char: %c (size: %zu bytes)\n", grade, sizeof(grade));
printf("int: %d (size: %zu bytes)\n", count, sizeof(int));
printf("long long: %lld (size: %zu bytes)\n", big_number, sizeof(long long));
return 0;
}
2. Floating-Point Types
| Type | Size (bytes) | Precision | Range | Format Specifier |
|---|---|---|---|---|
float | 4 | ~6-7 decimal digits | ±1.5×10⁻⁴⁵ to ±3.4×10³⁸ | %f |
double | 8 | ~15-16 decimal digits | ±5.0×10⁻³²⁴ to ±1.7×10³⁰⁸ | %lf |
long double | 10/12/16 | ~19-34 decimal digits | platform-dependent | %Lf |
#include <stdio.h>
int main() {
float pi_float = 3.141592653589793f; // 'f' suffix for float
double pi_double = 3.141592653589793;
long double pi_long_double = 3.141592653589793L; // 'L' suffix for long double
float scientific = 1.23e-4f; // 0.000123 in scientific notation
printf("float: %.10f (size: %zu bytes)\n", pi_float, sizeof(float));
printf("double: %.15lf (size: %zu bytes)\n", pi_double, sizeof(double));
printf("long double: %.18Lf (size: %zu bytes)\n", pi_long_double, sizeof(long double));
printf("Scientific: %f\n", scientific);
return 0;
}
3. Void Type
void represents the absence of type. Used for:
- Functions that return nothing
- Functions with no parameters
- Generic pointers
void print_message(void) { // no parameters, no return value
printf("Hello, World!\n");
}
void* generic_ptr; // generic pointer (can point to any type)
4. _Bool Type (C99)
Boolean type introduced in C99:
#include <stdbool.h> // for bool, true, false
int main() {
bool is_valid = true;
bool is_finished = false;
if (is_valid) {
printf("Valid!\n");
}
return 0;
}
Type Modifiers
Type modifiers alter the behavior and range of basic types:
Size Modifiers
short- reduces sizelong- increases sizelong long- further increases size (C99)
Sign Modifiers
signed- can store positive and negative values (default for most types)unsigned- stores only non-negative values
signed int normal = -100; // can be negative unsigned int only_positive = 100; // cannot be negative short int small = 10; // same as "short" long int large = 100000; // same as "long" long long int very_large = 10000000000LL;
Combined Modifiers
unsigned short int usi = 65535; signed long int sli = -100000L; unsigned long long int ulli = 18446744073709551615ULL;
Storage Classes
Storage classes define the scope, lifetime, and visibility of variables:
| Storage Class | Keyword | Scope | Lifetime | Initial Value |
|---|---|---|---|---|
| Automatic | auto | Block | Function | Garbage |
| External | extern | Global | Program | Zero |
| Static | static | Block/File | Program | Zero |
| Register | register | Block | Function | Garbage |
| Thread Local (C11) | _Thread_local | Thread | Thread | Zero |
1. Automatic Variables (auto)
Default storage class for local variables:
void function() {
auto int x = 10; // same as "int x = 10;"
auto float y = 3.14; // rarely used explicitly
}
2. External Variables (extern)
Declare variables defined elsewhere:
// File: global.h
extern int global_counter; // declaration
// File: global.c
int global_counter = 0; // definition
// File: main.c
#include "global.h"
int main() {
global_counter++; // accessing external variable
return 0;
}
3. Static Variables (static)
Local static - retains value between function calls:
#include <stdio.h>
void counter() {
static int count = 0; // initialized only once
count++;
printf("Called %d times\n", count);
}
int main() {
counter(); // Called 1 times
counter(); // Called 2 times
counter(); // Called 3 times
return 0;
}
Global static - limits scope to the current file:
// file1.c
static int file_only = 100; // cannot be accessed from other files
void function() {
file_only++;
}
4. Register Variables (register)
Hint to compiler to store variable in CPU register:
void loop() {
register int i; // hint, may be ignored
for (i = 0; i < 1000000; i++) {
// fast loop
}
}
Note: Cannot take address of register variable (&i is invalid)
Constants and Literals
Integer Constants
42 // decimal 052 // octal (leading zero) 0x2A // hexadecimal (0x prefix) 0b101010 // binary (C23, some compilers support as extension) 42U // unsigned 42L // long 42LL // long long 42UL // unsigned long 42ULL // unsigned long long
Floating-Point Constants
3.14159 // double 3.14159F // float 3.14159L // long double 2.5e-3 // scientific notation (0.0025) .5 // 0.5
Character Constants
'A' // character constant '\n' // newline '\t' // tab '\'' // single quote '\\' // backslash '\x41' // hexadecimal ASCII (A)
String Constants
"Hello" // string literal "Hello " "World" // concatenated (preprocessor) "Line 1\nLine 2" // with escape sequences
Symbolic Constants
Using #define preprocessor:
#define PI 3.14159
#define MAX_SIZE 100
#define GREETING "Hello, World!"
int main() {
float area = PI * radius * radius;
int array[MAX_SIZE];
printf("%s\n", GREETING);
return 0;
}
Using const keyword:
const float PI = 3.14159;
const int MAX_SIZE = 100;
const char* GREETING = "Hello, World!";
int main() {
// PI = 3.14; // ERROR: cannot modify const
return 0;
}
Enumeration constants:
enum week { MON, TUE, WED, THU, FRI, SAT, SUN };
enum status { SUCCESS = 0, ERROR = -1, TIMEOUT = 5 };
int main() {
enum week today = WED;
enum status result = SUCCESS;
return 0;
}
Type Conversion
Implicit Conversion (Automatic)
Compiler automatically converts one type to another:
int i = 10; float f = i; // int to float (10.0) float a = 5.5; int b = a; // float to int (5, truncation) int x = 10; long y = x; // int to long (10L) int result = 5 / 2; // integer division: 2 (truncated) float result2 = 5 / 2; // still 2.0 (division happens first) float result3 = 5 / 2.0; // 2.5 (floating point division)
Conversion hierarchy:
char → int → long → long long → float → double → long double
Explicit Conversion (Casting)
Programmer forces type conversion:
float f = 3.14; int i = (int)f; // C-style cast: 3 int a = 5, b = 2; float result = (float)a / b; // 2.5 (float division) // Pointer casts void* ptr = &i; int* int_ptr = (int*)ptr;
Scope and Lifetime
Scope Types
Block Scope - within {}:
void function() {
int x = 10; // block scope
{
int y = 20; // nested block scope
printf("%d %d\n", x, y); // OK
}
// printf("%d\n", y); // ERROR: y out of scope
}
File Scope - outside all functions:
int global = 100; // file scope
static int file_static = 200; // file scope, limited to this file
void func1() {
global++;
}
void func2() {
global += 10;
}
Function Scope - labels only:
void function() {
goto error;
// ...
error:
printf("Error occurred\n");
}
Lifetime
- Automatic lifetime: Local variables (created when block entered, destroyed when block exits)
- Static lifetime: Global and static variables (entire program execution)
- Allocated lifetime: Dynamically allocated memory (
malloc/free)
#include <stdlib.h>
int global = 10; // static lifetime
void function() {
int auto_var = 20; // automatic lifetime
static int static_var = 30; // static lifetime
int* heap_var = malloc(sizeof(int)); // allocated lifetime
*heap_var = 40;
// ...
free(heap_var); // manually free
}
Type Qualifiers
const
Variable cannot be modified after initialization:
const int MAX = 100; // MAX = 200; // ERROR const int* ptr1 = &MAX; // pointer to const int int* const ptr2 = &MAX; // const pointer to int (dangerous if MAX is const) const int* const ptr3 = &MAX; // const pointer to const int
volatile
Tells compiler that variable may change unexpectedly:
volatile int status_register; // hardware register
void wait_for_flag() {
while (!flag) {
// compiler won't optimize this loop
}
}
restrict (C99)
Optimization hint that pointer is the only reference to data:
void copy(int* restrict dest, const int* restrict src, size_t n) {
// compiler can optimize knowing no overlap
for (size_t i = 0; i < n; i++) {
dest[i] = src[i];
}
}
_Atomic (C11)
Atomic operations for thread safety:
#include <stdatomic.h>
atomic_int counter = 0;
void increment() {
atomic_fetch_add(&counter, 1);
}
sizeof Operator
sizeof returns size in bytes of a type or variable:
#include <stdio.h>
int main() {
printf("char: %zu\n", sizeof(char));
printf("short: %zu\n", sizeof(short));
printf("int: %zu\n", sizeof(int));
printf("long: %zu\n", sizeof(long));
printf("long long: %zu\n", sizeof(long long));
printf("float: %zu\n", sizeof(float));
printf("double: %zu\n", sizeof(double));
printf("pointer: %zu\n", sizeof(void*));
int array[10];
printf("array: %zu\n", sizeof(array)); // 40 (if int is 4 bytes)
printf("element: %zu\n", sizeof(array[0])); // 4
struct Data {
int x;
char y;
};
printf("struct: %zu\n", sizeof(struct Data)); // 8 (with padding)
return 0;
}
limits.h and float.h
Standard headers provide size limits for data types:
#include <stdio.h>
#include <limits.h>
#include <float.h>
int main() {
printf("Integer ranges:\n");
printf("INT_MIN: %d\n", INT_MIN);
printf("INT_MAX: %d\n", INT_MAX);
printf("UINT_MAX: %u\n", UINT_MAX);
printf("LONG_MIN: %ld\n", LONG_MIN);
printf("LONG_MAX: %ld\n", LONG_MAX);
printf("\nFloating-point ranges:\n");
printf("FLT_MIN: %e\n", FLT_MIN);
printf("FLT_MAX: %e\n", FLT_MAX);
printf("DBL_MIN: %e\n", DBL_MIN);
printf("DBL_MAX: %e\n", DBL_MAX);
return 0;
}
Best Practices
1. Choose Appropriate Types
// Good int age; // age fits in int unsigned int count; // count is never negative float temperature; // temperature can be fractional double precise_measurement; // needs high precision // Avoid short int huge_number; // might overflow char large_array[1000000]; // char is 1 byte, but array too large for stack
2. Initialize Variables
// Good
int counter = 0;
char* name = NULL;
float total = 0.0f;
// Bad - uninitialized
int counter;
// printf("%d", counter); // undefined behavior
3. Use Meaningful Names
// Good int student_count; float average_temperature; unsigned int bytes_received; // Avoid int a; float b; unsigned int c;
4. Understand Type Limits
#include <limits.h>
int safe_add(int a, int b) {
if ((b > 0) && (a > INT_MAX - b)) {
// would overflow
return INT_MAX;
}
if ((b < 0) && (a < INT_MIN - b)) {
// would underflow
return INT_MIN;
}
return a + b;
}
5. Be Explicit About Unsigned vs Signed
// Good unsigned int positive_only = 100; signed int can_be_negative = -50; // Good for bit operations unsigned int flags = 0x0F;
6. Use size_t for Sizes and Indices
#include <stddef.h>
size_t i;
size_t length = strlen(str); // strlen returns size_t
for (i = 0; i < length; i++) {
// process
}
7. Prefer int for General Use
// Good for most cases int index; int count; int result;
8. Be Careful with Type Conversions
// Explicit cast when narrowing double pi = 3.14159; int approx_pi = (int)pi; // explicit truncation // Avoid implicit narrowing long big = 1000000L; int small = (int)big; // explicit, shows you know it might overflow
Common Pitfalls
1. Integer Overflow
#include <limits.h>
int main() {
int max = INT_MAX;
int result = max + 1; // overflow, undefined behavior
printf("%d\n", result); // unpredictable
unsigned int umax = UINT_MAX;
unsigned int uresult = umax + 1; // wraps to 0 (defined behavior)
printf("%u\n", uresult); // 0
}
2. Signed/Unsigned Mismatch
int main() {
int a = -1;
unsigned int b = 1;
if (a < b) { // WARNING: a converted to unsigned
printf("This might not print\n");
}
printf("%u\n", a); // prints large positive number
}
3. Float Precision Issues
#include <math.h>
#include <stdio.h>
int main() {
float a = 0.1f;
float b = 0.2f;
float c = 0.3f;
if (a + b == c) { // likely false due to precision
printf("Equal\n");
}
// Better
if (fabs((a + b) - c) < 1e-6) {
printf("Approximately equal\n");
}
}
4. Uninitialized Pointers
int* ptr; // uninitialized, points to random location *ptr = 10; // BUG: undefined behavior // Correct int value; int* ptr = &value; // initialize with valid address *ptr = 10;
Quick Reference Table
| Type | Format Specifier | Example |
|---|---|---|
char | %c | char c = 'A'; |
signed char | %hhd | signed char sc = -10; |
unsigned char | %hhu | unsigned char uc = 200; |
short | %hd | short s = 1000; |
unsigned short | %hu | unsigned short us = 60000; |
int | %d | int i = 100000; |
unsigned int | %u | unsigned int ui = 4000000000U; |
long | %ld | long l = 1000000L; |
unsigned long | %lu | unsigned long ul = 4000000000UL; |
long long | %lld | long long ll = 10000000000LL; |
unsigned long long | %llu | unsigned long long ull = 10000000000ULL; |
float | %f | float f = 3.14f; |
double | %lf | double d = 3.14159; |
long double | %Lf | long double ld = 3.141592653589793238L; |
size_t | %zu | size_t sz = sizeof(int); |
ptrdiff_t | %td | ptrdiff_t diff = ptr2 - ptr1; |
void* | %p | void* ptr = &i; |
Conclusion
Understanding variables and data types is essential for C programming. Key takeaways:
- C is statically typed - every variable has a fixed type
- Choose appropriate types for your data (int for counting, float/double for real numbers)
- Be aware of type sizes and limits (use
<limits.h>and<float.h>) - Understand scope and lifetime of variables
- Initialize variables before use
- Be careful with type conversions, especially signed/unsigned mixing
- Use
constfor values that shouldn't change - Consider storage classes for controlling visibility and lifetime
Proper use of variables and data types leads to efficient, portable, and bug-free C programs.