C Programming: Understanding Data Types
Think of data types as the fundamental building blocks that allow our programs to understand
and manipulate information. Just like a carpenter needs to know the difference between wood,
metal, and plastic to build something, a programmer needs to know the different types of data
they can work with.
In C, data types define:
● What kind of data a variable can hold (e.g., whole numbers, decimal numbers,
characters).
● How much memory that data will occupy.
● What operations can be performed on that data.
Without understanding data types, your C programs would be like a chef trying to cook without
knowing their ingredients – a recipe for disaster!
1. What is a Data Type in C?
A data type in C specifies the type of data a variable can store. It tells the compiler how to
interpret the value stored in a memory location and what operations are permissible.
● Analogy: Imagine variables as containers. Data types are like labels on these
containers specifying what they can hold: a "milk" jug for liquids, a "flour" bag for
powders, a "sugar" box for granules. You wouldn't put flour in a milk jug, right? Similarly,
you shouldn't store a floating-point number in a variable designed for integers.
C provides several built-in data types:
● Primary/Basic Data Types: These are the fundamental types provided by the C
language.
○ int: For whole numbers.
○ char: For single characters.
○ float: For single-precision floating-point numbers (decimal numbers).
○ double: For double-precision floating-point numbers (decimal numbers with
higher precision).
● Derived Data Types: These are created by combining basic data [Link]
○ Pointers
○ Structures
○ Unions
● Enumerated Data Type: enum
● Void Data Type: void
2. Primary Data Types in Detail
Let's dive into the core data types. The exact size and range of these types can vary slightly
depending on the compiler and the system architecture (e.g., 32-bit vs. 64-bit systems), but the
general principles remain the same.
2.1. Integer Data Types (int, char, short, long)
These types are used to store whole numbers (positive, negative, or zero) without any
fractional part.
a) int
● Purpose: Stores signed integers. It's the most commonly used integer type.
● Storage: Typically occupies 4 bytes (32 bits) on most modern systems.
● Range: Varies by system, but commonly -32,767 to +32,767 for 16 bit signed
integer. 0 to 65,535. This corresponds to a 16-bit unsigned integer.
● Example:
#include <stdio.h>
int main() {
int age = 30;
int year = 2023;
int temperature = -5;
printf("Age: %d, Year: %d, Temperature: %d\n", age, year, temperature);
return 0;
}
●
● Output: Age: 30, Year: 2023, Temperature: -5
● How Negative Values are Represented:
○ C typically uses Two's Complement representation for signed integers.
○ In this method, the most significant bit (MSB) acts as the sign bit (0 for positive, 1
for negative).
○ To get the two's complement of a number:
■ Invert all the bits (0 becomes 1, 1 becomes 0) – this is One's
Complement.
■ Add 1 to the result.
○ Example (8-bit representation for simplicity): To represent -5:
■ Positive 5 in binary: 00000101
■ One's Complement: 11111010
■ Add 1: 11111011 (This is the two's complement representation of -5)
● Where Stored: int variables declared inside functions (local variables) are typically
stored on the stack. Global and static int variables are stored in the data segment
(initialized) or BSS segment (uninitialized) of memory.
bss - a section of memory in an object file or executable that stores uninitialized global
and static variables.
The text segment (also known as code segment) is where the executable code of the
program is stored. It contains the compiled machine code of the program's functions
and instructions. This segment is usually read-only and stored in the lower parts of the
memory to prevent accidental modification of the code while the program is running.
The data segment stores global and static variables that are created by the
programmer. It is present just above the code segment of the program.
Heap segment is where dynamic memory allocation usually takes place. The heap area
begins at the end of the BSS segment and grows towards the larger addresses from
there. It is managed by functions such as malloc(), realloc(), and free(). The heap
segment is shared by all shared libraries and dynamically loaded modules in a process.
For example, the variable pointed by ptr will be stored in the heap segment:
The stack is a region of memory used for local variables and function call management.
Each time a function is called, a stack frame is created to store local variables, function
parameters, and return addresses. This stack frame is stored in this segment. The stack
segment is generally located in the higher addresses of the memory and grows opposite
to heap. They adjoin each other so when stack and heap pointer meet, free memory of
the program is said to be exhausted.
b) char (stores b/w single inverted commons'')
● Purpose: Stores a single character (alphabetic, numeric, symbol, or control character).
It's also an integer type, capable of storing small integer values.
● Storage: Always occupies 1 byte (8 bits).
● Range:
○ Signed char: Typically -128 to 127.
○ Unsigned char: Typically 0 to 255.
● Example:
● C
#include <stdio.h>
int main() {
char initial = 'J';
char digit = '7'; // Stored as ASCII value
char symbol = '$';
printf("Initial: %c, Digit: %c, Symbol: %c\n", initial, digit, symbol);
return 0;
}
● Output: Initial: J, Digit: 7, Symbol: $
● How Negative Values are Represented: If char is treated as signed (which is
implementation-defined but common), negative values use two's complement, similar to
int. However, char is often used to store ASCII values, where the range 0-255 is more
relevant.
● Where Stored: Similar to int, local char variables are on the stack, and global/static
ones are in the data/BSS segments.
c) short int (or short)
● Purpose: Stores smaller signed integers than int. Useful when memory is a concern and
the full range of int is not needed.
● Storage: Typically 2 bytes (16 bits).
● Range: Typically -32,768 to 32,767.
● Example:
#include <stdio.h>
int main() {
short int small_number = 15000;
printf("Small Number: %hd\n", small_number); // %hd for short int
return 0;
}
●
● Output: Small Number: 15000
● How Negative Values are Represented: Two's Complement.
● Where Stored: Stack for local, Data/BSS for global/static.
d) long int (or long)
● Purpose: Stores larger signed integers than int.
● Storage: Typically 4 bytes (32 bits) on 32-bit systems and 8 bytes (64 bits) on 64-bit
systems.
● Range:
○ On 32-bit systems: Similar to int (-2,147,483,648 to 2,147,483,647).
○ On 64-bit systems: Up to 263−1 (a much larger range).
● Example:
#include <stdio.h>
int main() {
long int very_large_number = 1000000000L; // 'L' suffix indicates long
printf("Very Large Number: %ld\n", very_large_number); // %ld for long int
return 0;
}
●
● Output: Very Large Number: 1000000000
● How Negative Values are Represented: Two's Complement.
● Where Stored: Stack for local, Data/BSS for global/static.
e) long long int (or long long)
● Purpose: Stores extremely large signed integers. Introduced in C99.
● Storage: Typically 8 bytes (64 bits).
● Range: Typically from −2^63 to 2^63−1.
● Example:
#include <stdio.h>
int main() {
long long int huge_number = 123456789012345678LL; // 'LL' suffix
printf("Huge Number: %lld\n", huge_number); // %lld for long long int
return 0;
}
● Output: Huge Number: 123456789012345678
● How Negative Values are Represented: Two's Complement.
● Where Stored: Stack for local, Data/BSS for global/static.
f) Unsigned Integer Types (unsigned char, unsigned short, unsigned int, unsigned long, unsigned
long long)
● Purpose: These types store only non-negative integers (0 and positive numbers). By
removing the sign bit, they can represent a larger range of positive values for the same
number of bits.
● Storage: Same as their signed counterparts.
● Range: Double the positive range of their signed equivalents.
○ unsigned char: 0 to 255
○ unsigned short: 0 to 65,535
○ unsigned int: 0 to 4,294,967,295 (on 32-bit systems)
○ unsigned long: Varies (similar to unsigned int on 32-bit, larger on 64-bit)
○ unsigned long long: 0 to 18,446,744,073,709,551,615
● Example:
#include <stdio.h>
int main() {
unsigned int positive_count = 50000;
unsigned char small_positive = 200;
printf("Positive Count: %u, Small Positive: %u\n", positive_count, small_positive); // %u for unsigned int
return 0;
}
● Output: Positive Count: 50000, Small Positive: 200
● How Negative Values are Represented: Not applicable. If you try to assign a negative
value to an unsigned type, it will wrap around due to modular arithmetic. For example,
assigning -1 to an unsigned int will result in UINT_MAX (the maximum value for
unsigned int).
● Where Stored: Stack for local, Data/BSS for global/static.
2.2. Floating-Point Data Types (float, double, long double)
These types are used to store real numbers, i.e., numbers with a decimal point or fractional
part. They adhere to the IEEE 754 standard for representing floating-point numbers.
a) float
● Purpose: Stores single-precision floating-point numbers.
● Storage: Typically 4 bytes (32 bits).
● Range: 1.2E-38 to 3.4E+38
● Precision: Typically offers about 6-7 decimal digits of precision.
● Example:
#include <stdio.h>
int main() {
float price = 99.95f; // 'f' suffix is optional but good practice
float pi_approx = 3.14159f;
printf("Price: %.2f, Pi Approx: %f\n", price, pi_approx); // %.2f for 2 decimal places
return 0;
}
● Output: Price: 99.95, Pi Approx: 3.141590
● How Negative Values are Represented: IEEE 754 standard uses a sign bit, an
exponent, and a mantissa. The sign bit determines if the number is positive or negative.
● Where Stored: Stack for local, Data/BSS for global/static.
b) double
● Purpose: Stores double-precision floating-point numbers. Offers greater range and
precision than float.
● Storage: Typically 8 bytes (64 bits).
● Range: Approximately 1.7E-308 to 1.7E+308
● Precision: Typically offers about 15-17 decimal digits of precision.
● Example:
#include <stdio.h>
int main() {
double precise_pi = 3.141592653589793;
double large_distance = 1.5e10; // Scientific notation (1.5 x 10^10)
printf("Precise Pi: %lf, Large Distance: %lf\n", precise_pi, large_distance); // %lf for double
return 0;
}
● Output: Precise Pi: 3.141593, Large Distance: 15000000000.000000 (Precision might
vary in output based on default print settings)
● How Negative Values are Represented: IEEE 754 standard, similar to float but with
more bits for exponent and mantissa, leading to higher precision and range.
● Where Stored: Stack for local, Data/BSS for global/static.
c) long double
● Purpose: Stores extended-precision floating-point numbers.
● Storage: Varies by implementation, often 10, 12, or 16 bytes.
● Range & Precision: Greater than double.
● Example:
#include <stdio.h>
int main() {
long double very_precise_value = 123.4567890123456789L; // 'L' suffix
printf("Very Precise Value: %Lf\n", very_precise_value); // %Lf for long double
return 0;
}
● Output: Very Precise Value: 123.456789 (Actual output depends heavily on compiler
and platform precision)
● How Negative Values are Represented: IEEE 754 standard, extended.
● Where Stored: Stack for local, Data/BSS for global/static.
Key Differences in Floating-Point Types:
Type Storage Range Precision When to Use
(Typical) (Approximate) (Decimal
Digits)
float 4 bytes pm3.4times1038 6-7 General
floating-point
calculations
double 8 bytes pm1.7times10308 15-17 Scientific
calculations,
financial
applications
long 10-16 Greater than double >17 Highly precise
double bytes calculations,
specific needs
Important Note on Precision: Floating-point arithmetic is not always exact. Due to their binary
representation, some decimal numbers cannot be represented perfectly, leading to tiny
inaccuracies. double is generally preferred over float for most calculations unless memory is a
critical constraint.
2.3. void Data Type
● Purpose: Represents the absence of a type or value. It's not used to declare variables
but has specific uses:
○ Function Return Type: A function declared with void return type does not return
any value.
○ Function Parameters: A function declared with void as its parameter list takes
no arguments.
○ Pointers: A void * pointer is a generic pointer that can point to any data type but
cannot be dereferenced directly without casting.
● Storage: Does not occupy any memory when used as a return type or parameter type. A
void * pointer occupies the same size as a regular pointer.
● Example:
#include <stdio.h>
// Function that returns nothing (void return type)
void greet(char name[]) {
printf("Hello, %s!\n", name);
}
// Function that takes no arguments (void parameter list)
void print_message(void) {
printf("This function has no parameters.\n");
}
int main() {
greet("Alice");
print_message();
int num = 10;
void *ptr = # // Generic pointer
// printf("%d\n", *ptr); // ERROR: cannot dereference void pointer
printf("Value pointed to by void pointer: %d\n", *(int*)ptr); // Cast to int* to dereference
return 0;
}
● Output:
Hello, Alice!
This function has no parameters.
Value pointed to by void pointer: 10
●
3. Data Type Modifiers
C provides type modifiers that can be used with basic data types (primarily integers) to alter
their properties, especially their range and memory usage.
● signed: Indicates that the data type can hold both positive and negative values. This is
usually the default for integer types like int, short, long.
● unsigned: Indicates that the data type can hold only non-negative values (0 and
positive). This doubles the positive range compared to its signed counterpart.
● short: Used with int to specify a shorter integer type (usually 2 bytes), potentially saving
memory but with a reduced range.
● long: Used with int to specify a longer integer type (often 4 or 8 bytes), allowing for a
larger range. long long further extends this.
Examples of using modifiers:
● unsigned int: Stores large positive integers.
● signed char: Explicitly states a character type can be negative (though often 0-255 is
more useful).
● short int: For smaller integer values.
● long int: For larger integer values.
4. Storage of Data Types and Memory Locations
Understanding where data is stored is crucial for efficient programming. C programs typically
use the following memory segments:
1. Text Segment (Code Segment): Stores the compiled machine code of your program
(instructions). This area is usually read-only.
2. Data Segment: Stores global and static variables that are initialized.
○ Initialized Data Segment: Variables explicitly assigned a value at declaration
(e.g., int global_var = 10;).
3. BSS (Block Started by Symbol) Segment: Stores global and static variables that are
uninitialized. The operating system automatically initializes these to zero (or null for
pointers) when the program starts.
○ Example: int uninitialized_global;, static int static_var;
4. Heap: Dynamically allocated memory. Memory is allocated and deallocated at runtime
using functions like malloc(), calloc(), realloc(), and free(). This area grows upwards.
5. Stack: Used for local variables (declared inside functions), function parameters, and
return addresses. It's a LIFO (Last-In, First-Out) structure. When a function is called, a
"stack frame" is created; when it returns, the frame is destroyed. This area grows
downwards.
Summary of Storage:
● Local variables (inside functions): Primarily stored on the stack.
● Global variables: Stored in the data segment (if initialized) or BSS segment (if
uninitialized).
● Static variables (inside or outside functions): Similar to global variables, stored in
data or BSS segments. Their lifetime is the entire program execution.
● Dynamically allocated memory: Stored on the heap.
5. Differences Between Data Types
The key differences lie in:
1. Type of Data: Integers vs. floating-point numbers vs. characters.
2. Size (Memory Occupancy): char (1 byte), short (2 bytes), int (4 bytes), float (4 bytes),
double (8 bytes), long long (8 bytes), etc. This directly impacts memory usage.
3. Range of Values: How large or small a number or character the type can hold. unsigned
types offer a larger positive range. long and long long offer larger integer ranges. double
offers a larger range and precision for decimals.
4. Precision: Crucial for floating-point types (float vs. double).
5. Representation of Negative Numbers: Signed types use methods like two's
complement; unsigned types do not represent negative numbers directly.
6. When and Where to Use Data Types
Choosing the right data type is a fundamental programming skill that impacts:
● Correctness: Using an int for a temperature value with decimals will lead to incorrect
results.
● Efficiency: Using long long when a short would suffice wastes memory and can
sometimes slow down computations.
● Readability: Using descriptive variable names and appropriate types makes code easier
to understand.
General Guidelines:
● Counting, indices, IDs, quantities that are whole numbers: Use int. If you need very
large counts, consider long or long long. For counts that will never be negative (e.g.,
array indices, sizes), unsigned int or unsigned long can be appropriate.
● Single characters (letters, symbols): Use char.
● Decimal numbers, measurements, scientific calculations, financial values: Use
double for better precision. Use float only if memory is extremely constrained and
precision loss is acceptable.
● Boolean logic (true/false): In modern C (C99 onwards), you can use _Bool or include
<stdbool.h> and use bool (which is typically an int or char in practice).
● No value/type: Use void for function return types or parameters.
Example Scenario:
Imagine you're building a program to:
1. Store a person's age: int age; (Age is a whole number, positive).
2. Store a student's grade (e.g., 85.5): float grade; or double grade; (Grades can have
decimals). double is generally safer for precision.
3. Store a character input from the user: char choice;
4. Store the population of a large country: long long population; (int might overflow).
5. Store the exact value of Pi for complex calculations: long double PI =
3.14159265358979323846L;
7. Derived Data Types (Brief Overview)
While not the focus today, it's good to know these exist:
● Arrays: A collection of elements of the same data type, stored contiguously in memory.
Example: int scores[10]; (An array of 10 integers).
● Pointers: Variables that store memory addresses of other variables. Example: int *ptr;
(A pointer to an integer).
● Structures (struct): A collection of variables of different data types grouped under a
single name. Example: struct Person { char name[50]; int age; };
● Unions (union): Similar to structures, but all members share the same memory location.
Only one member can hold a value at a time.
Syntax errors: Errors that occur when you
violate the rules
of writing C/C++ syntax are known as syntax errors. This compiler error indicates something that must be fixed
before the code can be compiled. All these errors are detected by compiler and thus are known as compile-time
errors.
Most frequent syntax errors are:
● Missing Parenthesis (})
● Printing the value of variable without declaring it
● Missing semicolon like this:
#include<stdio.h> void main() {
int x = 10; int y = 15;
printf("%d", (x, y)) // semicolon missed }
while(.) -> syntactic error
Syntax errors are easy to figure out because the compiler highlights the line of code that caused the
error. Generally, we can find the error's root cause on the highlighted line or above the highlighted line.
int var = 10
return 0;
error: expected ',' or ';' before 'return'
4 | return 0;
we will go through line 4 and a few lines above it. Once we do that, we can quickly determine that we are
missing a semi-colon (;) in line 4.
Run-time Errors : Errors which occur during program execution(run-time) after successful compilation are called
run-time errors. One of the most common run-time error is division by zero also known as Division error. These
types of error are hard to find as the compiler doesn't point to the line at which the error occurs. the compiler may
generate a warning.
int n = 9, div = 0;
// wrong logic
// number is divided by 0,
// so this program abnormally terminates
div = n/0;
Sometimes, the compiler does not throw a run time error. Instead, it returns a garbage value. In situations like
these, we have to figure out why did we get an incorrect output by comparing the output with the expected output.
In other cases, the compiler does not display any error at all. The program execution just ends abruptly in cases
like these.
int arr[1];
arr[0] = 10;
int val = arr[10000];
In the above code, we are trying to access the 10000th element but the size of array is only 1 therefore there is no
space allocated to the 10000th element, this is known as segmentation fault.
while a certain program is running, if it encounters the square root of -1 in the code, the program will not be able to
generate an output because calculating the square root of -1 is not possible. Hence, the program will produce an
error.
Runtime errors can occur because of various reasons. Some of the reasons are:
1. Mistakes in the Code: Let us say during the execution of a while loop, the programmer forgets to
enter a break statement. This will lead the program to run infinite times, hence resulting in a run
time error.
2. Memory Leaks: If a programmer creates an array in the heap but forgets to delete the
array's data, the program might start leaking memory, resulting in a run time error.
3. Mathematically Incorrect Operations: Dividing a number by zero, or calculating the
square root of -1 will also result in a run time error.
4. Undefined Variables: If a programmer forgets to define a variable in the code, the
program will generate a run time error.
int var = 2147483649;
printf("%d", var);
This is an integer overflow error. The maximum value an integer can hold in C is 2147483647. Since in
the above example, we assigned 2147483649 to the variable var, the variable overflows, and we get
-2147483647 as the output (because of the circular property).
Linker Errors: These error occurs when after compilation we link the different object files with main's object using
Ctrl+F9 key(RUN). These are errors generated when the executable of the program cannot be generated. This
may be due to wrong function prototyping, incorrect header files. One of the most common linker error is writing
Main() instead of main().
void Main() // Here Main() should be main()
{
int a = 10;
printf("%d", a);
}
Logical Errors : On compilation and execution of a program, desired output is not obtained when certain input
values are given. These types of errors which provide incorrect output but appears to be error free are called
logical errors. These are one of the most common errors done by beginners of programming.
These errors solely depend on the logical thinking of the programmer and are easy to detect if we follow the line of
execution and determine why the program takes that path of execution.
for(i = 0; i < 3; i++);
—--------------
float a = 10;
float b = 5;
if (b = 0) { // we wrote = instead of ==
printf("Division by zero is not possible");
} else {
printf("The output is: %f", a/b);
}
we do not get the output we expected after the compilation and execution of a program. Even though the code
seems error free, the output generated is different from the expected one. These types of errors are called Logical
Errors. Logical errors are those errors in which we think that our code is correct, the code compiles without any
error and gives no error while it is running, but the output we get is different from the output we expected.
Semantic errors : This error occurs when the statements written in the program are not meaningful to the
compiler.
a + b = c; //semantic error
A semantic error will be generated if the code makes no sense to the compiler, even though it is syntactically
correct. It is like using the wrong word in the wrong place in the English language. For example, adding a string to
an integer will generate a semantic error.
Semantic errors are different from syntax errors, as syntax errors signify that the structure of a program is incorrect
without considering its meaning. On the other hand, semantic errors signify the incorrect implementation of a
program by considering the meaning of the program.
int a, b, c;
a * b = c;
// This will generate a semantic error