Nihal’s Learnings
I will share my learnings here.
This is to document my progress and to share my learnings with others who may find them helpful.
Table of Contents
C
Learning Rust
Data Structures and Algorithms
Introduction to C Programming
Overview
C is a general-purpose programming language known for its efficiency, flexibility, and close interaction with computer hardware. Originally designed by Dennis Ritchie in 1972 at Bell Labs, USA, C has become a fundamental component of modern computing, powering everything from embedded systems and operating systems to high-performance applications.
Advantages and Disadvantages
Advantages of C Language | Disadvantages of C Language |
---|---|
- Exceptional performance and efficiency | - Lack of object-oriented features |
- Widespread compatibility and portability | - Limited support for avoiding namespace collisions |
- Low-level access and close proximity to hardware | - No garbage collector |
- Ideal for developing operating systems | - Limited support for polymorphism |
- Efficient memory management and speed | - Limited support for exceptions |
Program Structure
A program is a set of logically grouped instructions that are executed sequentially.
Compilation Process
High-Level Code -> Compiler -> Assembly -> Assembler -> Machine Code (input/output)
Software
Software is a collection of programs categorized into:
- Application Software: Programs designed according to specific needs/utilities (e.g., Games, Antivirus, MS Office).
- System Software: Essential for proper computer functioning (e.g., Operating Systems, Linkers, Loaders).
Software Portability
C is hardware portable but not software portable.
Compilation Steps
- Source Code (.c): Written in C programming language.
- Object Code: Generated by the compiler.
- Linking: Object code linked with libraries by the linker (system software).
- Executable File: Ready for execution.
- Loading: Executable file loaded from hard disk to RAM by the loader.
History of Programming Languages
Programming languages have undergone a remarkable evolution since the inception of computing. From the rudimentary binary code to the sophisticated high-level languages of today, each stage in this journey has contributed to making software development more accessible and efficient. Let’s delve into the history and development of programming languages.
Low Level languages
Machine Language or Binary Language
- Consists of 0s and 1s.
- Computers can only understand binary.
- Unambiguous and simple.
- Difficult for humans to read and understand.
- Not suitable for representing complex data structures.
- Machine-dependent due to differences in code architecture.
Assembly Language
- Uses mnemonics like
ADD
,SUB
,MUL
. - Operands are represented in binaries.
- Language translators (Assemblers) were built to make computers understand mnemonics.
- Assembly language is translated into machine language by an assembler.
High Level languages
- Resemble English.
- Examples: BASIC, COBOL, FORTRAN, etc.
- Can’t be understood by computers directly, hence language translators are needed.
- Compiler
- Interpreter
High-Level Languages ➡️ Character User Interface
Fourth Generation languages
- Aimed to have minimum input and maximum output.
- Examples: Visual Basic (VB), SQL, etc.
- Time is saved because of shorter input, but it takes more memory, making it less efficient than High-Level Languages.
Fifth Generation Languages
- Designed for AI.
- Examples: LISP, PROLOG.
Variables in C Programming
A variable in C is a type of container that can hold a particular type of value, and its type is defined by its data type.
For example: int x;
Here, x is a variable of integer type, so it can hold only one integer value at a time.
Memory is allocated for a variable when it is defined, and the size of the allocation depends on the compiler. Generally, 2 bytes are allocated for an integer variable. The value that was present in those 2 bytes before assigning a value to the variable is known as a garbage value.
In C, when you assign a value to a variable, you are actually copying the value to the memory location that the variable represents.
When you assign a value to a variable, you are essentially giving that value a name. So, when you write a = b;, you are saying “give the value that b refers to the name a”.
int a = 1;
int b = 2;
a = b; // This doesn't mean b is assigned to a.
Variables in C are memory locations that hold values. When you assign a value to a variable in C, you are actually copying the value to the memory location that the variable represents.
Basic terms related to variables
-
Definition
- Allocates memory for a variable
- For example, in
int x;
2 bytes are allocated for x.
-
Declaration
- Means providing information to the compiler about the data type of the variable.
- For example, in
int x;
, the information is given to the compiler that the data type of x is int.
-
Initialization
- Assigns an initial value to a variable at the time of declaration.
data_type variable_name = value;
- For example,
int x = 1;
- Note:
- Static initialization happens at compile time.
- Dynamic initialization happens at runtime.
Define and Declare
Define | Declare | Possible |
---|---|---|
- [x] | - [x] | Obviously |
- [ ] | - [x] | Possible |
- [x] | - [ ] | Not possible |
- [ ] | - [ ] | Very nice |
Sign bit and range
- The first bit is known as the sign bit. It’s the most significant bit.
- 0 represents a positive value, and 1 represents a negative value.
- Range is determined by
C is an expanding language where byte allocation depends on the compiler. While bytes are cheap for us, C prioritizes accuracy and range at the cost of efficiency and memory.
Datatypes
Type of data which user wants to store in memory
Primitive / Predefined / Builtin
int
- negative numbers are stored in 2’s compliment notation.
- largest +ve number 32767 ()
- for more we have to use modifiers
- unsigned (without a sign bit)
- to i.e to
- long
- 4 bytes are allocated. Range to
- short
- smallest -ve number -32768 ()
- Range to
- unsigned (without a sign bit)
- Generalize nbits to ⭐ bound checking is not present in C
float
- used to define variables which can store real numbers
-
float x;
4 bytes of memory allocated for x.- Range to
- no modifiers for float
-
double
- used to define variables which can store real numbers and its range is more than float
double x;
8 bytes of memory allocated for x.- Range to
- Only one modifier: Long.
- 10 bytes of memory allocated
- to
char
- used to define variables which can store character
char x;
only 1 byte of memory is allocated for x.- to => -128 to 127
- Only one modifier Unsigned. One byte memory
- to => 0 to 255
- Character constant can initialize variable having char data
char x='a';
herea
is a character constantchar x=a;
herea
is a variable
- Character constant can be single or double ch long. It can’t be of triple characters.
void
- non returnable functions & pointers
User defined
- structure
- union
- enum
C uses extended ASCII code of 8bits.
because of escape sequences:
char x = 97 // x = 9
char x = 'ab' // `x = 'a`
Operators
Operators are symbols used to manipulate data.
In the expression z = x + y
, z
, x
, and y
are operands, while =
and +
are operators.
Operators Based on the Number of Operands
-
Unary
- 1 operand
- Examples:
x++
,++x
,+x
,-x
-
Binary
- 2 operands
- Examples:
x + y
,x - y
,x! = y
,x==y
-
Ternary
- 3 operands
- Only one operator:
?:
Operators on basis of functions
- Arithmetic
- Assignment
- Relational
- Logical
- Increment
- Decrement
- Shorthand
- Ternary
- Size of operator
- Miscellaneous
General order of Precedence from high to low:
- Postfix operators (all have the same precedence, so sequences of operators will be evaluated left-to-right)
- array subscript operator
[]
- function call operator
()
- component selection operators
.
and->
- postfix
++
and--
- array subscript operator
- Unary operators (all have the same precedence, so sequences of operators will be evaluated left-to-right)
- prefix
++
and--
sizeof
- bitwise negation operator
~
- logical negation operator
!
- unary sign operators
-
and+
- address-of operator
&
- dereference operator
*
- prefix
- Cast expressions
(
type name)
- Multiplicative operators
*
,/
,%
- Additive operators
+
and-
- Shift operators
<<
and>>
- Relational operators
<
,>
,<=
,>=
- Equality operators
==
and!=
- Bitwise AND
&
- Bitwise XOR
^
- Bitwise OR
|
- Logical AND
&&
- Logical OR
||
- Conditional operator
?:
- Assignment operators
=
,+=
.-=
,*=
,/=
,%=
,<<=
,>>=
,&=
,^=
,|=
- Sequential (comma) operator
,
So, expressions like*x++
are parsed as*(x++)
, since the postfix++
has higher precedence than the unary*
.
Similarly,sizeof x + 1
is parsed as(sizeof x) + 1
, sincesizeof
has higher precedence than addition.
An expression likep++->x
is parsed as(p++)->x
; both postfix++
and->
operators have the same precedence, so they’re
parsed from left to right.
When in doubt, use parentheses.
Associativity
Associativity refers to the order in which operators of the same precedence are evaluated in an expression.
There are two types of associativity: left associativity and right associativity.
-
Left Associativity:
- For operators with left associativity, evaluation proceeds from left to right. This means that if multiple operators of the same precedence appear consecutively in an expression, they are evaluated from left to right.
int result = 10 - 5 + 2;
In this expression, both the subtraction (
-
) and addition (+
) operators have the same precedence. Since they are left-associative, the subtraction operation10 - 5
is evaluated first, followed by the addition operation5 + 2
. -
Right Associativity:
- Conversely, for operators with right associativity, evaluation occurs from right to left. This means that if multiple operators of the same precedence appear consecutively, they are evaluated from right to left.
`int result = 2 ^ 3 ^ 2;`
In this expression, the bitwise XOR operator (
^
) has right associativity. Therefore, the evaluation starts from the rightmost operator3 ^ 2
, and then the result is XORed with2
.
Understanding associativity is crucial for correctly interpreting expressions and determining the order of operations. It helps in writing code that produces the expected results and avoids ambiguity in complex expressions.
Arithmetic
+
, -
, *
, /
(quotient operator), %
(remainder operator or modulus)
Divison and Modulus operator
in if then output is
in if then output is
Rules for arithmetic operators:-
-
Integer Divison
- int int is always int
-
Modulus with Integers Only
- Modulus operator cannot be used with floating point numbers as by default every float is treated as double. C is an expanding language.
// Example of incorrect usage: float result = 5.2 % 2; // Error
-
Operator Precedence
- If more than 1 operator is present in an expression then precedence rule will be applied.
4 Associativity
- If multiple operations with the same precedence appear, the associativity rule determines the order of evaluation.
- Arithmetic Operators are left associative.
Assignment
Assignment operators are used to assign values to variables. They not only perform the assignment but also allow for combining arithmetic or bitwise operations in a single step.
=
Assign+=
Add and Assign-=
Subtract and Assign*=
Multiply and Assign/=
Divide and Assign%=
Modulus and Assign<<=
Left Shift and Assign>>=
Right Shift and Assign&=
Bitwise AND and Assign^=
Bitwise XOR and Assign|=
Bitwise OR and Assign
These operators allow for combining arithmetic or bitwise operations with assignment in a single step.
Types of Assignment Operators
-
Simple Assignment:
- In simple assignment, a single variable is assigned a value using the = operator.
-
Compound Assignment:
- Compound assignment operators combine arithmetic or bitwise operations with assignment. They operate on the variable itself, modifying its value in place.
Rules for assignment operators
- Single variable is allowed in LHS of assignment operator
- Cascading of assignment operator
int x=y=z=2;
- Associativity of assignment operator is from Right to Left
Q. Swap values without introducing a third variable. x = 5
and y = 2
Sol. There are many ways
// Method 1: Using Arithmetic Operations
x = x + y; // x = 5 + 2 => 7
y = x - y; // y = 7 - 2 => 5
x = x - y; // x = 7 - 5 => 2
// Method 2: Using Multiplication and Division
x = x * y; // x = 10
y = x / y; // y = 10/2 => 5
x = x / y; // x = 10/5 => 2
Relational
Relational operators are fundamental for comparing two values. These operators evaluate the relationship between operands and return a Boolean value (1 for true or 0 for false).
>
Greater Than<
Less Than>=
Greater Than or Equal To<=
Less Than or Equal To==
Equal To!=
Not Equal To
Key Points
-
Associativity of relational operators is Left to Right
-
Precedence of Arithmetic Operators is more than of Relational Operators
int a = 5, b = 2, c = 1, d;
d = c + a > b;
// (c + a) > b
// (1 + 5) > b
// 6 > 2
// Since the comparison is true, the output will be 1.
Additional Point
- Character Comparison:
- Relational operators can also be used to compare characters in C. Characters are compared based on their ASCII values. For example,
'a' < 'b'
would evaluate to true because the ASCII value of'a'
(97) is less than that of'b'
(98).
- Relational operators can also be used to compare characters in C. Characters are compared based on their ASCII values. For example,
Logical
Logical operators are used to combine two or more conditions to determine the logic between values
-
NOT Operator (
!
)- The Not Operator is represented by an exclamation mark (!). It reverses the logical state of its operand. It converts true to false and false to true.
- In this example, the condition x > 5 evaluates to false, but the ! operator reverses it, so the message will be printed.
int x = 5; if (!(x > 5)) { printf("x is not greater than 5\n"); }
-
AND Operator (
&&
)- The AND Operator is represented by double ampersands (&&). It returns true only if both operands are true. For instance:
- In this case, both x > 5 and y < 10 are true, so the message will be printed.
int x = 6, y = 8; if (x > 5 && y < 10) { printf("Both conditions are true\n"); }
-
OR Operator (
||
)- The OR Operator is represented by double pipes (||). It returns true if at least one of the operands is true.
- In this example, x > 5 is false, but y < 10 is true, so the message will still be printed.
int x = 3, y = 12; if (x > 5 || y < 10) { printf("At least one condition is true\n"); }
Short-circuiting refers to a feature of logical operators in programming languages that prevents unnecessary computation when the result of an expression becomes determinable based on earlier parts of the expression.
Specifically, for the OR
operator (||
), short-circuiting means that once a true value is found among the operands, the entire expression returns true without evaluating subsequent operands.
Conversely, for the AND
operator (&&
), short-circuiting means that once a false value is found among the operands, the entire expression returns false without evaluating subsequent operands
Any non-zero value in case of C is considered as True
Order of precedence:
NOT > AND > OR
Increment
The increment operator ++
increases the value of the variable by 1.
The increment operator can be used in two ways:
- Pre-increment
++x
: first increment then assign. - Post-increment
x++
: first assign then increment.
Both Pre- and post-work the same if used as a single statement.
However, when used as part of a larger expression, they exhibit different behaviors.
Example 1:
int a = 5, b;
b = a++ + a++;
printf("%d %d", a, b);
// 7 10
Example 2:
int a = 5, b;
b = ++a + ++a;
printf("%d %d", a, b);
// 7 13
Explanation of Example 1:
-
We declare two integer variables a and b. Initially, a is assigned the value of 5, while we haven’t yet set any initial value for b.
-
The line b = a++ + a++; performs several operations at once. Firstly, it adds the current values of a (which is still 5) with itself (again, currently 5). This results in y = 10.
-
Secondly, after adding these values together, it increments a twice – first from its original value of 5 to 6, then again to 7.
Note that ++ before a variable means incrementing it by one immediately after the operation. So when used within an expression like here, it will be applied only once, not twice as you might expect based on the order of appearance.
Use pen and paper. make a table of values of a and b at each step.
a | b |
---|---|
10 | |
7 |
Example 3:
x = 1;
y = x++ * ++x * x++ * ++x * x++ * x++;
x |
---|
3 |
y = (6 times we are mulitplying 3 with itself)
x = 7 (x = 1 and there are 6 elemts [ 6 increments]
int x ;
x = 5++
//error: lvalue required as increment operand
//Build failed
// To correct the code we can either use a variable instead of a constant for incrementing
// or assign the result of the increment operation to another variable.
int x; declares an integer variable named x, and x = 5++; attempts to increment the value of 5, which is not allowed in C.
The ++ operator is a unary operator that increments the value of a variable by 1, but it cannot be applied to a constant like 5.
An expression that can appear on the left side of an assignment operator (=).
It represents an object that occupies some identifiable location in memory. An L-value error occurs when you try to assign a value to something that is not a valid storage location, such as a constant or an expression that does not represent a memory location.
In the given code, 5 is a constant and cannot be assigned a new value, leading to an L-value error.
Associativity of increment operator
int x = 5;
int y = x++ + ++x;
printf("%d %d", x, y);
// 7 12
The increment operator has right-to-left associativity, which means that if multiple increment operators are used in the same expression, the rightmost one will be evaluated first.
In the given code, x++
is evaluated first, incrementing the value of x from 5 to 6. Then, ++x
is evaluated, incrementing
the value of x from 6 to 7. Finally, the sum of the incremented values of x (7 and 7) is assigned to y, resulting in y
being assigned the value 14. The final value of x is 7.
Decrement
The decrement operator --
decreases the value of the variable by 1.
The decrement operator can be used in two ways:
- Pre
--x
first decrement then assign. - Post
x--
first assign then decrement.
Both Pre and post decrement work the same if you are using it as a single statement. But if you are using it as a part of a bigger expression, then it will make a difference.
Example 1:
int a = 5, b;
b = a-- + a--;
printf("%d %d", a, b);
// 3 10
Example 2:
int a = 5, b;
b = --a + --a;
printf("%d %d", a, b);
// 3 8
Explanation of Example 1:
- We declare two integer variables a and b. Initially,
a
is assigned the value of 5, while we haven’t yet set any initial value forb
. - The line
b = a-- + a--;
performs several operations at once. Firstly, it adds the current values of a (which is still 5) with itself (again, currently 5). This results iny
= 10.
Note that --
before a variable means decrementing it by one immediately after the operation. So when used within an
expression like here, it will be applied only once, not twice as you might expect based on the order of appearance.
Use pen and paper. make a table of values of a and b at each step.
a | b |
---|---|
10 | |
3 |
Example 3:
x = 6;
y = x-- * --x * x-- * --x * x-- * x--;
x |
---|
4 |
y = (6 times we are multiplying 6 with itself)
x = 3 (x = 6 and there are 6 elements [ 6 decrements] )
Associativity of Decrement Operator
int a = 5, b;
b = a-- + a--;
printf("%d %d", a, b);
// 3 10
The decrement operator also has right-to-left associativity, which means that the rightmost operator will be evaluated first.
Initially, the value of a
is 5. The rightmost a--
operator is evaluated first, decrementing the value of a
from 5 to 4.
Then, the leftmost a--
operator is evaluated, decrementing the value of a
from 4 to 3. Finally, the sum of the
decremented values of a
(3 and 4) is assigned to b, resulting in b being assigned the value 7.
The final value of a
is 3, as it has been decremented twice. Therefore, the output of the code will be 3 10
.
Shorthand
Arithmetic Shorthand Operators
The arithmetic shorthand operators allow us to combine assignment with an operation, resulting in more compact code.
These include +=
, -=
, *=
, /=
, %=
, and **=
.
For example, instead of using separate statements like x = x + y;
, we can use x += y
. This not only saves space but
also improves code flow by reducing redundancy.
Bitwise Shorthand Operators
Bitwise shorthand operators enable efficient manipulation of binary data at the bit level. They consist
of &=
, |=
, ^=
, <<=
, and >>=
. The first three operate on bits as logical AND
, OR
, and XOR
respectively,
while the latter two handle left shift and right shift operations. Here’s an illustrative example:
int x = 5; // 101 in binary
x &= 3; // 101 & 011 = 001
int result = value & mask; // Perform bitwise AND
result |= new_value; // Set specific bits to 'new_value'
These operators help maintain code brevity when dealing with complex bit patterns.
Conditional Compilation Shorthands
Conditional compilation shorthands simplify conditional logic by allowing inline expansion of preprocessor directives.
Two such operators are ?:
conditional operator and #if ... #endif
.
The former performs a ternary expression, where the condition determines whether one expression or another should be used.
On the other hand, the latter allows for conditional inclusion or exclusion of blocks of code based on defined macros.
For instance, consider the following snippet:
#define DEBUG
#if defined(DEBUG)
printf("Debugging is enabled\n");
#endif
Here, the #if
directive checks if the DEBUG
macro is defined. If so, the printf
statement is included in the code.
These shorthands are particularly useful for managing code complexity and ensuring that only relevant code is included in the final build.
Ternary
Conditional operator that takes three operands. It is also known as the conditional operator because it evaluates a condition and returns one of two values based on the result of the evaluation. The ternary operator is represented by the question mark (?) and the colon (:).
condition ? expression1 : expression2;
The condition is evaluated first. If the condition is true, then expression1 is executed, and its value is returned. If the condition is false, then expression2 is executed, and its value is returned.
int num = 5;
num % 2 == 0 ? printf("Even") : printf("Odd");
Advantageous over the if-else statement. It is concise, easy to read, and saves time and effort.
Useful to assign a value to a variable based on a condition.
Associativity is from Right to Left.
a = 10?0?2:3:1
First step go from right to left and draw paratheses around the ?
and :
a = 10 ? (0 ? 2 : 3) : 1;
Now, evaluate the condition from left to right.
a = 10 ? (0 ? 2 : 3) : 1;
// any non zero value is true. we have 10 so, we will go to the first expression.
// Now we evaluate the nested ternary operator (0 ? 2 : 3).
// Since 0 is considered false, we move to the second expression which is 3.
// Therefore, a is assigned the value 3.
a = 3
Size of operator
It returns the size of its operand, which can be a data type, a variable, or an expression. The result of the sizeof operator is of type size_t, which is an unsigned integer type defined in the <stddef.h> header file.
essential for writing portable and efficient code in C programming. It helps you avoid hardcoding sizes and ensures that your code adapts to different environments and architectures. Additionally, it is useful for dynamically allocating memory based on the size of data types.
#include <stdio.h>
int main() {
printf("Size of int: %zu bytes\n", sizeof(int)); // 4 bytes
printf("Size of char: %zu bytes\n", sizeof(char)); // 1 byte
printf("Size of float: %zu bytes\n", sizeof(float)); // 4 bytes
printf("Size of double: %zu bytes\n", sizeof(double)); // 8 bytes
return 0;
}
Miscellaneous Operators
There are several additional shorthand operators available in C, each serving unique purposes. Some examples include:
Comma operator:
It used as an operator[for expressions] and as a separator[for declarations]. It evaluates both operands and returns the value of the second operand.
Evaluates both operands sequentially and returns the last operand’s value. It is commonly used in loops to update multiple variables simultaneously
// Update x and y together
for (int x = 0, y = 10; x < 10; ++x, --y) {
// Use updated x and y
}
note: The comma operator has the lowest precedence of all C operators.
Pointer dereferencing (*):
Allows indirect access to memory locations stored in pointers.
char str[] = "Hello World";
printf("%c", *(str + 5)); // Output: 'o'
Type Casting
It refers to explicitly converting values from one data type to another during runtime.
This conversion can be done using the type cast operator ((type))
int x = 3;
float y = (float)x; // Converting int to float
double z = (double)y; // Converting float to double
printf("%f\n%lf", y, z);
Input and Output
Standard Input and Output in C is done using the printf
and scanf
functions.
These functions are defined in the stdio.h
header file.
Format specifiers
Some format specifiers are as follows:
Format Specifier | Description |
---|---|
%d | Integer |
%f | Float |
%c | Character |
%s | String |
%lf | Double |
%x | Hexadecimal |
%o | Octal |
%u | Unsigned Integer |
%e | Scientific Notation |
%p | Pointer |
%n | Number of characters written so far |
%m | Error message corresponding to the last error |
%[] | Scanset |
%* | Suppresses assignment |
%l | Long |
%ll | Long Long |
%h | Short |
%hh | Char |
#include <stdio.h>
standard input output header file
#include
is a preprocessor directive which tells the compiler to include the contents of the file specified in the
program.
Input
To obtain input from users within C programs, there are several built-in functions available. Four common methods
include scanf
, getchar
, getch
, and getche
. Each has its own characteristics and usage scenarios.
scanf
: This function allows you to read formatted data from the standard input.
int num;
scanf("%d", &num);
Standard functions for input
getc()
can read from any source (keyboard, files, etc)
getchar()
can read only from keyboard
Non standard functions for input
getch()
: r reads a single character from the standard input stream (usually the keyboard) without echoing it to the screen. It doesn’t let the input go into buffer.
getche()
: is similar to getch(), but it does echo the character to the screen.
Standard functions for output
printf()
: is most liberal in terms of reading strings.
putc()
: writes a single character to the standard output stream. Can redirect output to any source (keyboard, files, etc) also, it accepts two arguments, the character to be written and the file pointer where the character is to be written.
putchar()
: is a macro, not a function. Simplified version of putc(). reads until it encounters a newline character or EOF(negative integer).
Non standard functions for output
gets()
: reads a line of text from the standard input stream, but it is no longer recommended due to potential security vulnerabilities.
fgets()
: is a safer alternative to gets(), which reads a line of text from a given input stream and stores it in a buffer.
putch()
: reads until it encounters a newline character or EOF.
Control Statements
Control statements are used to control the flow of execution of a program. They are used to alter the flow of the program based on a certain condition.
Control Structures are of 3 types:
- Sequential
- The statements are executed in the order they appear.
- Conditional
- The statements are executed based on a condition.
- If-else
- Switch
- Break and Continue
- Goto
- Iterative
if statements
The if
statement is used to execute a block of code if the condition is true. If the condition is false, another block of code can be executed using the else
statement. The else
statement is optional.
Every control structure has bydefault scope of one statement [semi colon]. If you want to execute more than one statement, you need to use a block statement
{}
.
Examples and Explanations:
Example 1: Scope
int x = 5;
if (x == 4);
printf"x is equal to 5");
// output will be "x is equal to 5"
// because the if statement has a semicolon at the end and scope ended there.
Example 2: Scope of if Statement
int x = 9;
if (x == 5)
printf("x is equal to 5");
printf("x is equal to 9");
// output will be "x is equal to 9"
// because scope of if statement is only one statement.
if-else statements
The if-else statement allows execution of different code blocks based on whether a condition is true or false.
int x = 5;
if (x == 4) {
printf("x is equal to 4");
} else {
printf("x is not equal to 4");
}
// Output: "x is not equal to 4"
int x = 3;
if (x > 3) {
printf("abc");
}
printf("def"); // Incorrect placement
else {
printf("ghi");
}
misplaced else
The term ‘misplaced else’ refers to placing an else statement outside of its intended block (the one following the last if or elif) when writing nested conditionals. This leads to unexpected behavior because the else clause will be associated with the nearest preceding if or elif, which may not have been intended.
float x = 0.7;
if (x == 0.7)
printf("abc");
else
printf("def");
Here, the issue lies in the comparison if (x == 0.7). The problem arises due to the way floating-point numbers are stored and represented in computer memory.
When the value 0.7 is assigned to the variable x, it is stored as a binary floating-point number with a limited precision. This binary representation may not be an exact match for the decimal value 0.7 due to the inherent limitations of representing real numbers in a finite amount of memory.
As a result, when comparing x with the literal 0.7, there may be a slight difference between the two values due to rounding errors or precision issues in floating-point arithmetic. This can lead to the comparison (x == 0.7) evaluating to false even though logically we might expect it to be true.
if both had the exact binary then there would be no issue. like in float x = 0.5 if (x == 0.5) printf("abc"); else printf("def");
the output will be “abc”
if-else-if
if-else-if statement allows you to test multiple conditions sequentially.
int num = 10;
if (num > 0) {
printf("Number is positive\n");
} else if (num < 0) {
printf("Number is negative\n");
} else {
printf("Number is zero\n");
}
Switch
The switch
statement is used to execute a block of code based on the value of a variable or an expression. It provides an alternative to using multiple if-else
statements when there are multiple possible execution paths based on a single variable.
Syntax:
The switch statement evaluates the expression and compares its value with the values specified in the case labels.
The default case is optional and is executed when none of the case labels match the value of the expression. default case can be written anywhere in the switch statement.
#include <stdio.h>
int main() {
int choice;
printf("Enter a number between 1 and 3: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("You entered one\n");
break;
case 2:
printf("You entered two\n");
break;
case 3:
printf("You entered three\n");
break;
default:
printf("Invalid choice\n");
}
return 0;
}
Unlike if-else statements, switch statements can only be used with integral types (char, int, enum). No variables. Each case label must be a constant expression (integer constants, character constants, or enumerated constants).
Logic building theoretical knowledge
Example 1:
#include <stdio.h>
char x = 'a'
switch (x) {
// 97
case 'a':
// 97 OR
case 'e':
case 'i':
case 'o':
case 'u':
printf("vowel");
break;
default:
printf("not vowel");
}
// the expression became 97 OR which will result shortcuiting in TRUE -> 1 -> case 1 -> 97 compared to 1 -> Not Vowel
Example 2: Using switch to print grade of a student based on marks:
61-70 -> D
71-80 -> C
81-90 -> B
91-100 -> A
if(x%10 == 0)
x --;
switch(marks/10)
{
case 6:
printf("D");
break;
case 7:
printf("C");
break;
case 8:
printf("B");
break;
case 9:
printf("A");
break;
default:
printf("invalid");
}
Example 3:
You can not put case 'a'
and case '97'
together. It will result in a duplicate case error.
Else if ladder | Switch |
---|---|
no break | break required |
any condition | only equality condition |
any data type | only character constant |
Loops
While loop
Do-While Loops
It is similar to the while
loop but with one key difference: the do-while
loop always executes its body at least once, even if the condition is initially false.
Understanding how while
and do-while
loops can be simulated in place of each other by rearranging the code structure. :
Example:
int i = 0;
while (i < 10) {
// Code to be executed
i++;
}
This can be done using a Do-While loop as:
int i = 0;
do {
// Code to be executed
i++;
} while (i < 10);
The While
loop checks the condition before executing the code, whereas, the Do-While
loop executes the code first and then checks the condition. The examples above demonstrate how to rearrange the code to achieve the same functionality using either loop construct.
For loop
The for loop is suitable for situations where the number of iterations is known beforehand and depends on a counter variable.
Any or all of the initialization, condition, and update statements can be omitted, but the semicolons must still be present.
for (initialization; condition; update) {
// code to be executed
}
#include <stdio.h>
int main() {
int n, i;
for (int n = 1; n < 10; n += 3)
{
for(int i = 1; i < 11; i++)
{
printf("%d*%d=%d\t%d*%d=%d\t%d*%d=%d\n",
n,i,n*i,
(n+1),i,(n+1)*i,
(n+2),i,(n+2)*i);
}
printf("\n");
}
return 0;
}
Break
Break
can come in switch or a body of loop. Break
is used to exit the loop or switch statement.
for (n=1;, n<=500; n+1)
{
for (i = 2; i <= n/2; i++)
{
if (n%i == 0)
break;
}
if (i == n/2 + 1)
printf("%d\n", n);
}
Continue
Continue
can come only in body of a loop. Continue
is used to skip the current iteration and continue with the next iteration of the loop by bringing the control at the begining of body of loop.
for (i = 1; i <= 10; i++)
{
if (i == 5)
continue;
if (i == 6)
break;
printf("%d\n", i);
}
jumping to a specific location in the program.
int main() {
int x = 5;
if (x < 0) {
goto error;
}
// Code to be executed when x is positive
return 0;
error:
printf("Error: x is negative\n");
return -1;
}
int complex_function(void) {
if (initialize_1() != SUCCESS) { goto out1; }
if (initialize_2() != SUCCESS) { goto out2; }
if (initialize_3() != SUCCESS) { goto out3; }
/* other statements */
return SUCCESS;
out3:
deinitialize_3();
out2:
deinitialize_2();
out1:
deinitialize_1();
return ERROR;
}
Patterns
Key points
- Count the number of lines and loops for the same
- Find relation between line number, number of characters and spaces in that line. Apply loop for the same.
1
12
123
1234
12345
Code
for (l = 1, l <= 5, l++){
for (ch = 1, ch <= l, ch++){
printf("%d", ch);
}
printf("\n");
}
1
23
456
78910
Code
int x = 1;
for (l = 1; l <= 4; l++){
for (ch = 1, ch <= l, ch++){
printf("%d", ch);
x++;
}
printf("\n");
}
1
11
111
1111
111
11
1
Code
for (l = 1; l <= 7; l++)
{
if (l <= 4){
for (ch = 1; ch <= l; ch++)
printf("1");
}else{
for (ch = 7; ch >= l; ch--)
printf("1");
}
printf("\n");
}
1111
111
11
1
11
111
1111
Code
for (l = 1; l <=7; l++)
{
if (l<=4){
for (s = 2; s <= l; s++){
printf(" ");
}
for (ch = 4; ch >= l; ch--){
printf("1");
}
} else {
for (s = 6; s >= l; s--){
printf(" ");
}
for (ch = 4; ch <= l; ch++){
printf("1");
}
}
}
1 1
11 11
111 111
1111111
Code
int ch, l, s, s1 = 1, l1= 1;
for (l = 1; l<= 4; l++)
{
for (ch = 1; ch <= l; ch++){
printf("1");
}
for (s = 5; s >= s1; s--){
printf(" ");
}
s1 += 2;
for (ch = 1; ch <= l1; ch++){
printf("1");
}
if (l != 3){
l1++;
}
printf("\n");
}
or
#include <stdio.h>
int main() {
int rows = 4;
int spaces = rows * 2 - 2;
for (int i = 1; i <= rows; i++) {
for (int j = 1; j <= i; j++) {
printf("1");
}
for (int k = 1; k <= spaces; k++) {
printf(" ");
}
spaces -= 2;
for (int l = 1; l <= i; l++) {
printf("1");
}
printf("\n");
}
return 0;
}
abcdefgfedcba
abcdef fedcba
abcde edcba
abcd dcba
abc cba
ab ba
a a
Code
#include <stdio.h>
int main(){
int l, s, s1 = -2 ;
char ch, ch1, end = 'g';
for (l = 1; l <= end; ch++){
for(ch = 'a'; l <= end; ch++;){
printf("%c", ch);
}
for(s = 0; s <= s1; s++){
printf(" ");
s1 += 2;
}
if(l == 1){
end--;
}
for(ch1 = ch-1; ch1 >= 'a'; ch1--;){
printf("%c", ch1);
}
printf("\n");
}
}
Arrays
An array in C is a collection of elements of the same data type stored in contiguous memory locations. Each element in an array is accessed by its index.
Arrays in C are static, meaning their size is fixed upon declaration and cannot be changed during runtime.
Arrays in C do not support heterogeneous data storage. As such, we cannot store different values of different datatypes in an array.
Elements can be inputted in both row major and column major but stored only in row major fashion.
Importantce of Arrays
- Easier storage, access and data management
- Useful to perform matrix operations
- useful to implement other data structures
Declaration
datatype array_name[array_size];
For example:
int x[100];
In this declaration, an array of 100 integers named x is created. The index x[0] refers to the first element, and x[99] refers to the last element.
The variable’s value is called subscript.
All the variables are independent of each other. Changing one variable does not affect the other.
x[100] = {0, 1}
is an array of 100 elements with the first 2 elements initialized to 0 and 1. The rest of the elements are initialized to 0.
Initialization
Arrays in C can be initialized during declaration or afterward using the following syntax:
int x[3];
x[0] = 5;
x[1] = 3;
x[2] = 1;
or
int x[3] = {5, 3, 1};
Dynamic initialization of arrays is also possible using loops. For example, to initialize all elements of an array scores to 0:
int scores[100];
for (int i = 0; i < 100; i++) {
scores[i] = 0;
}
Alternatively, the memset
function from the <string.h>
library can be used to set all elements of an array to a specific value, such as 0:
#include <string.h>
int scores[100];
memset(scores, 0, 100*sizeof(int));
Accessing Elements
Elements in an array are accessed using square brackets []
notation with the index of the element. Arrays in C are zero-indexed, meaning the index of the first element is 0, and the index of the last element is array_size - 1
.
For example, to access the first and last elements of an array x
:
int first_element = x[0];
int last_element = x[99];
Arguements to Functions
- Ordinary variables are passed by value
- Values of the arguments passed are copied into the parameters of the function.
- Any change made to function parameters is not reflected in the original arguments
- Arrays are passed by reference
- Changes made to the array passed in the called function are retained in the calling function
Array Bounds Checking in C
In C, array bounds checking is performed only while writing to arrays, not while reading from them. This design choice prioritizes speed but requires programmers to be cautious about inadvertently accessing invalid memory locations.
A program which inputs positive integers and converts them into binary using arrays
int n, i, j;
int x[16];
printf("Enter a positive integer: ");
scanf("%d", &n);
while(n!=0){
x[i] = n%2;
n = n/2;
i++;
}
printf("Binary: ");
for(j=i-1; j>=0; j--){
printf("%d", binary[j]);
}
doing the same without using arrays
// using pow function
#include <math.h>
int n, i=0, x, s=0
printf("Enter a positive integer: ");
scanf("%d", &n);
while(n!=0){
x = n%2;
n = n/2;
s = s + pow(10, i)*x;
i++;
}
printf("Binary: %d", s);
Wrong Syntax
float benchmarks[];
benchmarks = {2.35, 42.30, 60.03, 400.5, 0.001};
The compiler has no clue how much space to reserve for our “benchmarks” array based on our first statement, and so even though we are providing all of that info in the very next line, compiler will demand that we provide the array size in the declaration line itself. Furthermore, arrays cannot be assigned values by listing them out using curly braces in any statement other than the declaration itself.
Multidimensional Arrays
Multidimensional arrays are arrays of arrays. They are useful to implement other data structures, such as matrices.
Declaration
datatype array_name[number_of_rows][number_of_columns];
For example:
int x[2][3];
In this declaration, a 2D array of 2 rows and 3 columns named x is created. The index x[0][0] refers to the first element, and x[1][2] refers to the last element.
2D Arrays
absctraction of 1D array. every element is an 1D array itself
int x[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
it can also be said that we have defined 3 one dimensional arrays.
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|----0th element--------|------1st element------|----2nd element-----|
Valid Unvalid Declarations
-
int x[2][3] = {{1,2,3},{4,5,6}};
- Valid
-
int x[][3] = {{1,2,3},{4,5,6}};
- Valid.
- This syntax is often used when the compiler can infer the first dimension from the number of initializers provided. In this case, since two inner arrays are provided, the compiler infers that the first dimension should be 2. Therefore, it’s equivalent to the first declaration.
-
int x[2][] = {{1,2,3},{4,5,6}};
- Not Valid.
- In C, when you omit the size of one dimension, you must specify it for the outermost dimension. compiler needs to know the size of each dimension in order to allocate memory properly. To understand why this is an issue, consider the initializer list {{1,2,3},{4,5,6}}. The compiler can infer that the first dimension of x should be 2 because there are two sets of curly braces. However, it cannot determine the size of the second dimension from the initializer list alone. In C/C++, arrays are stored contiguously in memory, so the compiler needs to know the size of each dimension to calculate the memory offsets correctly. When you omit the size of the second dimension, the compiler doesn’t have enough information to calculate these offsets, resulting in a compilation error.
-
int x[][] = {{1,2,3},{4,5,6}};
- Not Valid.
Program to Find the Transpose of a Matrix
#include <stdio.h>
int main() {
int a[10][10], transpose[10][10], r, c;
printf("Enter rows and columns: ");
scanf("%d %d", &r, &c);
// asssigning elements to the matrix
printf("\nEnter matrix elements:\n");
for (int i = 0; i < r; ++i)
for (int j = 0; j < c; ++j) {
printf("Enter element a%d%d: ", i + 1, j + 1);
scanf("%d", &a[i][j]);
}
// printing the matrix a[][]
printf("\nEntered matrix: \n");
for (int i = 0; i < r; ++i)
for (int j = 0; j < c; ++j) {
printf("%d ", a[i][j]);
if (j == c - 1)
printf("\n");
}
// computing the transpose
for (int i = 0; i < r; ++i)
for (int j = 0; j < c; ++j) {
transpose[j][i] = a[i][j];
}
// printing the transpose
printf("\nTranspose of the matrix:\n");
for (int i = 0; i < c; ++i)
for (int j = 0; j < r; ++j) {
printf("%d ", transpose[i][j]);
if (j == r - 1)
printf("\n");
}
return 0;
}
Program to add two matrices
#include <stdio.h>
int main() {
int r, c, a[100][100], b[100][100], sum[100][100], i, j;
printf("Enter the number of rows (between 1 and 100): ");
scanf("%d", &r);
printf("Enter the number of columns (between 1 and 100): ");
scanf("%d", &c);
printf("\nEnter elements of 1st matrix:\n");
for (i = 0; i < r; ++i)
for (j = 0; j < c; ++j) {
printf("Enter element a%d%d: ", i + 1, j + 1);
scanf("%d", &a[i][j]);
}
printf("Enter elements of 2nd matrix:\n");
for (i = 0; i < r; ++i)
for (j = 0; j < c; ++j) {
printf("Enter element b%d%d: ", i + 1, j + 1);
scanf("%d", &b[i][j]);
}
// adding two matrices
for (i = 0; i < r; ++i)
for (j = 0; j < c; ++j) {
sum[i][j] = a[i][j] + b[i][j];
}
// printing the result
printf("\nSum of two matrices: \n");
for (i = 0; i < r; ++i)
for (j = 0; j < c; ++j) {
printf("%d ", sum[i][j]);
if (j == c - 1) {
printf("\n\n");
}
}
return 0;
}
Multiple Matrices
#include <stdio.h>
// function to get matrix elements entered by the user
void getMatrixElements(int matrix[][10], int row, int column) {
printf("\nEnter elements: \n");
for (int i = 0; i < row; ++i) {
for (int j = 0; j < column; ++j) {
printf("Enter a%d%d: ", i + 1, j + 1);
scanf("%d", &matrix[i][j]);
}
}
}
// function to multiply two matrices
void multiplyMatrices(int first[][10],
int second[][10],
int result[][10],
int r1, int c1, int r2, int c2) {
// Initializing elements of matrix mult to 0.
for (int i = 0; i < r1; ++i) {
for (int j = 0; j < c2; ++j) {
result[i][j] = 0;
}
}
// Multiplying first and second matrices and storing it in result
for (int i = 0; i < r1; ++i) {
for (int j = 0; j < c2; ++j) {
for (int k = 0; k < c1; ++k) {
result[i][j] += first[i][k] * second[k][j];
}
}
}
}
// function to display the matrix
void display(int result[][10], int row, int column) {
printf("\nOutput Matrix:\n");
for (int i = 0; i < row; ++i) {
for (int j = 0; j < column; ++j) {
printf("%d ", result[i][j]);
if (j == column - 1)
printf("\n");
}
}
}
int main() {
int first[10][10], second[10][10], result[10][10], r1, c1, r2, c2;
printf("Enter rows and column for the first matrix: ");
scanf("%d %d", &r1, &c1);
printf("Enter rows and column for the second matrix: ");
scanf("%d %d", &r2, &c2);
// Taking input until
// 1st matrix columns is not equal to 2nd matrix row
while (c1 != r2) {
printf("Error! Enter rows and columns again.\n");
printf("Enter rows and columns for the first matrix: ");
scanf("%d%d", &r1, &c1);
printf("Enter rows and columns for the second matrix: ");
scanf("%d%d", &r2, &c2);
}
// get elements of the first matrix
getMatrixElements(first, r1, c1);
// get elements of the second matrix
getMatrixElements(second, r2, c2);
// multiply two matrices.
multiplyMatrices(first, second, result, r1, c1, r2, c2);
// display the result
display(result, r1, c2);
return 0;
}
Strings
Strings, represent arrays of characters and are fundamental for handling text data.
Initialization
Strings in C can be initialized in a couple of ways:
char x[3] = {'a', 'b', '\0'};
Here, ‘\0’ denotes the null character, which terminates the string. It’s essential for string manipulation and is represented by ASCII 0.
Alternatively:
char x[3] = "ab";
This form automatically appends the null character at the end of the string.
It’s crucial to ensure that the size of the array is one more than the number of characters in the string to accommodate the null character properly. Otherwise, it can result in compilation errors such as ‘Too many initializers.’
char x[10];
x = "abc";
printf("%s", x[6]);
// This will result in an L-value error
Attempting to assign a string directly to an array (x) will result in an L-value error. Instead, strings can only be assigned to pointers in C.
gets() | scanf() |
---|---|
Accepts only input strings | Accepts input for various data types |
Recognizes space | doesn’t recognize space |
No need for the ‘&’ symbol | Requires the ‘&’ symbol for input variables |
Allows input of only one string at a time gets(x/y) //not possible | Can accept multiple strings at once scanf("%s%s",x,y) |
<string.h> | <stdio.h> |
Similar distinctions apply to output functions like puts()
and printf()
.
puts()
automatically moves the cursor to the next line after printing the string, whereas printf()
does not. These functions are essential for outputting strings efficiently in C programs.
Functions
Part of a program designed to perform a specific task. It is a self-contained block of code that encapsulates a specific task or related group of tasks.
Fucntions can be Predeined or Userdefined.
Reusability, modularity, and abstraction are the main advantages of using functions.
Disadvantages include memory overhead and the potential for slower execution.
Function can never be defined inside definitionof another function
Function can return onlya single value at a time
#include <stdio.h>
// Function declaration
void calculate(int x, int *y, int *z);
int main() {
int a = 10;
int b, c;
// Function call
calculate(a, &b, &c); // here &b and &c are actual arguments. Also known as call by reference.
// Output
printf("a: %d square of a: %d cube of a: %d", a, b, c);
return 0;
}
// Function definition
void calculate(int x, int *y, int *z) {
// here int *y and int *z are pointers or addresses of b and c. This is also known as call as formal arguments.
*y = pow(x, 2);
*z = pow(x, 3);
}
Function execution can be understoodjk using the example of reading a book with a bookmark. When a person reads a book and uses a bookmark to attend to another matter, they can easily return to the exact point where they left off in the book. This process mirrors how functions in programming work, allowing for the temporary suspension of one task to perform another and then seamlessly returning to the initial task.
Prototyping
It is done to give the compiler an idea about the function name, return type and parameters.
IF functions are defined before calling then no need to prototype.
Function Calling
It is also called as function invocation. Whenever a function is called, the control of the program is transferred to the function definition. The function definition is executed and the control is transferred back to the calling function.
Before transferring the control to the function definition, the actual arguments are passed to the formal arguments. The formal arguments are the parameters of the function definition. The actual arguments are the parameters of the function call.
Next line address of main() is pushed onto the stack and all the variables used in main() are now dead. ie. they cant be accessed in calculate() and all the variables of calculate() are now alive.
Afgter executing the calculate() the control is transferred back to main() and the next line address of main() is popped from the stack and all the variables of calculate() are now dead and all the variables of main() are now alive.
Return Type
It is the type of value that the function returns. If the function does not return any value then the return type is void.
Recursion
Self calling functions. i.e inside the definition of a function, the function is called.
Repiteadly doing the same thing again and again is recursion.
Solving bigger problem using smaller problems is recursion.
graph TD A[Start] --> B(Recursive Function) B --> C{Base Case?} C -- Yes --> D[Return Base Case Value] C -- No --> E[Recursive Call] E --> B D --> F[End]
Recursive programs have a termination condition and a base case. If there is no base case, the program will run indefinitely and cause stack to overflow.
function call means push in stack and function return means pop from stack.
As soon as a fucntion is called it’s activiation record is pushed onto the stack.
Number of stack units required for a recursive program is directly proportional to the depth of the recursion tree.
In comparision a non-recursive program requires less or equal stack space.
Variables in recursion:
- Arguements
- Return Value
- Body of the function
To understand and approach a recursive program:
- Understand the problem statement and break it into smaller problems.
- Figure out recurrence relation
- Make stack or recursion tree to understand the flow of the program
- Use a debugger
- See how and what type of values are returned at each step. See where the fucntion call comes out with each value.
Problems
To calculate using recursion. n and m are positive integers:
pow(2,0) = 1
por(2,1) = 2
pow(2,2) = 2 * 2 = 4
pow(2,3) = 2 * 2 * 2 = 8
pow(2,3) = 2 * pow(2,2)
pow(2,2) = 2 * pow(2,1)
pow(2,1) = 2 * pow(2,0)
pow(2,0) = 1
int pow(int x, int y) {
if (y == 0) {
return 1;
} else {
return x * pow(x, y-1);
}
}
To find nth number of fibonacci series using recursion:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib(0) = 0
recurrence relation is fib(n) = fib(n-1) + fib(n-2)
int fib(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fib(n-1) + fib(n-2);
}
}
graph TD A("(fib(n))") B("(fib(n-1))") C("(fib(n-2))") A --> B A --> C B --> D("(fib(n-2))") B --> E("(fib(n-3))") C --> F("(fib(n-3))") C --> G("(fib(n-4))") D --> H("(fib(n-3))") D --> I("(fib(n-4))") E --> J("(fib(n-4))") E --> K("(fib(n-5))") F --> L("(fib(n-4))") F --> M("(fib(n-5))") G --> N("(fib(n-5))") G --> O("(fib(n-6))")
Here, Each node represents a call to the fib function with the corresponding parameter. The function recursively calls itself with n-1 and n-2 until reaching the base cases of 0 and 1.
Printing Numbers from n to 1 and Back
void fn(int n)
{
if(n == 0)
return;
fun(n/2);
printf("%d", n%2);
}
int fn(int x, int y)
{
if(x == 0)
return y;
else
return f(x-1, x+y);
}
void fn(int n)
{
int i = 0;
if(n > 1)
fn(n-1)
for(i=0; i<n; i++)
printf("*");
printf("\n");
}
void main(){
fn(4)
}
int fn(int *a, int n)
{
if (n <= 0) return 0;
else if (*a % 2 == 0) return *a + f(a+1, n-1);
else return f(a+1, n-1);
}
int main()
{
int a[] = {12, 7, 13, 4, 11, 6};
printf("%d", f(a, 6));
return 0;
}
void abc(char*s)
{
if(s[0]=='\0') return;
abc(s+1);
abc(s+1);
printf("%c", s[0]);
}
main()
{
abc("123");
}
void foo(int n, int sum)
{
int k = 0, j = 0;
if(n == 0) return;
k = n % 10l
j = n/10;
sum = sum + k;
foo(j, sum);
printf("%d", k);
}
int main()
{
int a = 2048, sum = 0;
foo(a, sum);
printf("%d", sum);
}
Storage Classes
Storage classes define 4 properties of a variable in C:
- Storage class: The storage class of a variable determines where the variable is stored in memory. stored in RAM or CPU registers. There are 4 storage classes in C:
- auto: Variables with the
auto
storage class are stored on the stack. - register: Variables with the
register
storage class are stored in the CPU registers. Registers are faster and limited in number, so the compiler may ignore theregister
keyword. - static: Variables with the
static
storage class are stored in the data segment. Defined only once and retains its value between function calls; memory is not deallocated when the function exits. Default value is 0. static variables can be accessed only in the declared file. - extern: Variables with the
extern
storage class are used to declare variables that are defined in another part of the program. The extern keyword does not allocate memory for the variable but rather informs the compiler about the existence of a variable defined elsewhere. When a local variable shares the same name as a global variable, the local variable takes precedence within its scope. To access the global variable in such a scenario, you can use the scope resolution operator :: If global variable is initialized then memory will be allocated for it.
- auto: Variables with the
Storage | Default Value | Scope | Lifetime | |
---|---|---|---|---|
Auto | RAM | garbage | D.B Defining Block scope | D.B |
Register | Register | garbage | D.B | D.B |
Static | RAM | 0 | D.B | W.P Whole Program |
Extern | RAM | 0 | W.P | W.P |
-
Default initial value: The default value of a variable is the value it has before it is initialized. The default value of a variable depends on its storage class and scope. For example, the default value of an
auto
variable is garbage, while the default value of astatic
variable is 0. -
Scope: The scope of a variable is the part of the program where the variable can be accessed. There are 4 types of scope in C:
- Block scope: Variables declared inside a block (within curly braces) have block scope.
- Function scope: Variables declared inside a function have function scope.
- File scope: Variables declared outside of all functions have file scope.
- Function prototype scope: Variables declared in a function prototype have function prototype scope.
-
Lifetime: The lifetime of a variable is the period of time during which the variable exists in memory. There are 3 types of lifetime in C:
- Automatic storage duration: Variables with automatic storage duration are created when the block in which they are declared is entered, and destroyed when the block is exited.
- Static storage duration: Variables with static storage duration are created when the program starts and destroyed when the program ends.
- Dynamic storage duration: Variables with dynamic storage duration are created and destroyed by the programmer using functions like
malloc()
andfree()
.
3 Areas in memory management:
- Statick Area: Global variables, static variables, and constant variables are stored in the static area.
- Stack Area: Local variables are stored in the stack area.
- Heap Area: Dynamically allocated memory is stored in the heap area.
During compile time, memory will be allocated for global and static variables in static area.
During run time, memory will be allocated for local variables in runtime stack area.
Preprocessor Directives
Manipulating source code before it undergoes compilation. These directives serve to modify or augment the source code in various ways, facilitating efficient development and customization of programs.
.c
-> preprocessor
-> pure high level c
-> compiler
-> assembly machine code
File Inclusion
One of the fundamental tasks of the preprocessor is to include header files into the source code using the #include directive. This directive allows for modularization and reuse of code by incorporating external files containing function prototypes, declarations, and definitions.`
#include <stdio.h>
#include "myheader.h"
Macro
Macros provide a powerful mechanism for defining reusable code snippets, constants, or inline functions using the #define
directive. These macros undergo substitution by their respective definitions before the actual compilation process begins.
Before compilation macro template will be replaced with the macro expansion in the entire program.
macro template is also known as symbolic constant i.e it can’t be changed during program execution.
#define SQR(x) x*x
void main(){
int y = 5, z;
z = y / SQR(y);
// z = y / y * y;
// z = 5 / 5 * 5;
// z = 1 * 5;
printf("%d", z);
}
#define MAX(a, b) ((a) > (b) ? (a) : (b))
void main() {
int x = 10, y = 20;
int max_value = MAX(x, y);
printf("Maximum value is %d", max_value);
}
const int x = 10; | #define N 10 |
---|---|
memory is needed | no memory is needed |
Scope can be local or global depending on where it is defined | Scope is always global |
User Defined Datatypes
They provide a way to encapsulate different types of data under a single name, enhancing code readability, maintainability, and organization.
In C, user-defined datatypes primarily include structures, typedef, unions, and enumerations.
Structures
A structure is a composite data type that allows you to group variables of different types under a single name. It enables you to create a new datatype to suit your specific needs.
Syntax:
struct structure_name {
data_type member1;
data_type member2;
// Additional members...
} structure_variable1, structure_variable2, ...;
Example:
struct student {
int roll_no;
char name[50];
float marks;
} s1, s2;
Accessing Structure Members:
s1.roll_no = 1;
strcpy(s1.name, "John");
s1.marks = 85.5;
Arrays and pointers can also be members of a structure, enabling you to store collections or references within a structure.
Typedef
The typedef keyword allows you to create aliases for existing data types, making your code more readable and portable.
typedef existing_data_type new_data_type;
Example:
typedef struct student {
int roll_no;
char name[50];
float marks;
} Student;
Student s1, s2;
Unions
Similar to a structure, but it allows storing different data types in the same memory location. The memory allocated is equal to the size of the largest member.
Syntax:
union union_name {
data_type member1;
data_type member2;
// Additional members...
} union_variable;
Unions are particularly useful in scenarios where you need to conserve memory, such as in embedded systems programming.
Enumerations
Enumerations provide a way to define sets of named integer constants. It makes the code more readable and maintainable by assigning meaningful names to numeric values.
Syntax:
enum enum_name {
value1,
value2,
// Additional values...
} enumeration_variable;
Example:
enum Days {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
} day;
day = MONDAY;
Enumerations are commonly used to represent a set of related named constants, such as days of the week or error codes.
Bit-Fields
Bit-fields allow you to specify the size of individual members within a structure in terms of the number of bits they occupy, rather than full bytes. This feature is particularly useful when working with memory-constrained systems or when dealing with hardware-level programming where specific bits in a register need to be accessed or manipulated.
Syntax:
struct {
type [member_name] : width;
};
Example:
struct {
unsigned int flag1 : 1;
unsigned int flag2 : 1;
unsigned int bits : 4;
} status;
Accessing Bit-Fields:
status.flag1 = 1; // Setting flag1
status.bits = 0xA; // Setting bits to 1010
Bit-fields allow for more efficient usage of memory by packing multiple variables into a single byte or word. However, they come with some limitations, such as platform-dependency and potential compiler optimizations.
Pointers in C
pointers are variables that store memory addresses of another variable. They allow direct manipulation of memory, which is a powerful feature but also requires careful handling to avoid errors.
Basic Pointer Syntax
-
Declaring a pointer:
int *ptr;
declares a pointer namedptr
that can point to an integer value. -
Dereferencing a pointer:
*ptr
refers to the value stored at the memory address pointed to byptr
. -
Datatype of pointers is always an unsigned integer.
Understanding declarations
int n;
to get an int just use n
int n[3]
to get an int just use n[i]
int foo(int n, float n1);
to get an int just call foo with related inputs.
int *n;
to get an int just dereference n
* | & |
---|---|
value at | address of |
*p; value at address p | &n; address of x |
Void Pointers
- A pointer without an associated data type.
- Can point to any data type: int, float, char, long, structures, and more.
- Due to its generic nature, it cannot be dereferenced directly.
int main(){
generic *ptr;
}
Generic Data Handling
Type casting to VOID* is a way to pass any type of data to a function. aka generic. It’s a way to pass data without knowing the type of data. “we’ll let you know bro”.
we can do void *
arithmetic with a GNU extention.
GNU Extention refers to additional language features or behaviour provided by GCC that go beyond the standard C language.
GNU C provided an extention called Pointer Arithmetic on void *
that allows performing arithmetic operations on void *
pointers by treating them as byte pointers. This extention is by default enabled in GCC.
Pointer Arithmetic
The pointer arithmetic is done is done in terms of the size of char rather than the size of the pointer type. the size of the data type the pointer points to is considered.
Addition
-
Integer addition to a Pointer is valid. It increments the size of the data type it points to.
ptr + n * sizeof(data_type)
#include <stdio.h> int main(){ long array[3] = {1, 2, 3}; long *p = array; printf("%ld\n", *(p + 1)); // prints 2, you can use p[..] or *(p + ..) }
-
Pointer addition to Pointer is not valid in C as the sum of two memory addresses doesn’t logically translate to any meaningful value in most contexts.
- multiplication and division between pointers also doesn’t make sense in C and will cause a compiler error.
Substraction
- Subtracting two pointers gives you their difference in memory addresses. The result is then divided by the size of the pointer’s data type.
P - P / sizeof(data_type)
#include <stdio.h> int main(){ long array[5] = {10, 20, 30, 40, 50}; // Two pointers pointing to different positions in the array long *ptr_a = &array[1]; // Pointing to the value 20 long *ptr_b = &array[3]; // Pointing to the value 40 printf("Value at ptr_a: %ld\n", *ptr_a); // Outputs: 20 printf("Value at ptr_b: %ld\n", *ptr_b); // Outputs: 40 // Subtracting the pointers long difference = ptr_b - ptr_a; printf("Difference in positions: %ld\n", difference); // Outputs: 2 return 0; }
- Integer substraction to Pointer
P - (int * sizeof(data_type))
- You can only subtract pointers of the same type
-
Arrays decay into pointers, except when used with the
&
operator. Decaying allows for more efficient memory usage. -
Whenever you work with an array identifier, most likely you are working with an pointer to element 0.
-
arr[n]
==*(arr + n)
-
By decaying an array into a pointer, it allows for more efficient memory usage. When passing an array to a function, instead of creating a copy of the entire array, only the memory address of the first element is passed. This reduces the overhead of memory allocation and copying when dealing with large arrays.
Function Pointers
Function pointers allow calling functions indirectly or taking their addresses.
The only 2 things we can do with a function is to either call it or take its address.
So, if the name isn’t followed by a (
to signify a function call, then the name evaluates to the address of the function with no ambiguity.
#include <stdio.h>
// Function prototypes
long long sum(int a, int b);
long long product(int a, int b);
// Define the function sum
long long sum(int a, int b){
return a + b;
}
// Define the function product
long long product(int a, int b){
return a * b;
}
int main(){
// Declare a function pointer called operation
long long (*operation)(int, int);
// Assign the address of the sum function to operation pointer
operation = ∑
// Use the function pointer to invoke the sum function
printf("Using function pointer: %lld\n", operation(5, 7)); // Outputs: 12
// Just for demo, let's switch the function pointer to point to the product function
operation = &product;
printf("Using function pointer to multiply: %lld\n", operation(5, 7)); // Outputs: 35
return 0;
}
Practical use case:
-
Event Handlers: In UNIX-like operating systems, signal handlers often utilize function pointers.
-
Callbacks: After a task completes, you might want to execute a specific function. Callbacks, often implemented via function pointers, allow this.
Double Pointers
-
Double pointers are just pointers pointing to other pointers.
-
They’re useful when you need to modify pointer values within functions.
-
Like pointers, they’re a tool: powerful when used correctly, but they can be confusing. Practice helps solidify understanding.
Quirks
char* str[20];
Is str a pointer to an array of 20 characters, or is it an array of 20 pointers to characters?
This declaration creates an array of 20 pointers to characters, not the other way around. Here’s why:
The array notation ([]) applies more closely to the variable than the pointer notation (*). In essence, we’ve defined 20 of whatever type precedes the array brackets. To confirm this, you can print the size of the array.
Null Pointers
Whenever we declare a pointer, we initialize it with a valid address. If you don’t initialize it explicitly, it is initialized to a random value like any other variable. However, dereferencing it now will lead to undefined behavior. It might also lead to a segmentation fault if the user tries to access an inaccessible memory location.
To prevent this undefined behavior, we always try to initialize our variables. For pointers, we have a reserved address 0 that is used to indicate that the pointer is not pointing to anything. This address is called the null pointer. It is defined in many header files, including stdio.h and stdlib.h.
-
Null pointers are used to initialize a pointer when that pointers isn’t assigned any valid memory address yet.
-
Useful for handling errors when using malloc function
-
It is a good practice to perform NULL check before deferencing any pointer to avoid segmentation fault.
if (ptr == NULL) {...}
argv[] or **argv
argv[] or **argv represent pointers to pointers in C.
int main(){
int *ptr;
int *ptr1;
int ***ptr2;
int ****ptr3;
int n = 42;
ptr = &n;
ptr1 = &ptr;
ptr2 = &ptr2;
ptr3 = &ptr3;
return 0;
}
Pointers
Misc
Distinguishing Between Void Pointers and Char Pointers
Void Pointers | Character Pointers |
---|---|
Truly generic. Can point to any data type | Points to characters or bytes (given a byte is just an unsigned char) |
Cannot be dereferenced directly since the data type it points to is unknown | Can be dereferenced directly |
Requires type casting before dereferencing | Used for byte-level operations. |
Pass by Value and Pass by Reference
Pass by value
Advantages | Disadvantages |
---|---|
Easy to understand - you can just pass copies, nothing to deference. | Performance overhead - A big object like a struct to be copied will be time and memory intensive. |
Safe - Original data won’t be modified by the called function | Short reach(Lack of direct access) - Called functions can only modify local copies. This can be a good thing too. |
Pass by reference
Advantages | Disadvantages |
---|---|
Efficiency - You just pass an address, it can point to a gigantic structure, not a problem. | Can be complex to understand - You have to dereference the pointer to get the value. |
Direct access - can modify data outside the called function. This trick allows us to return multiple values from a function. |
Associativity
pointers have unary operators therfore associativity is right to left
int *p;
int x = 5.2;
p = &x;
printf("%d", *(int*)p);
here if we had
printf("%f", (int*)*p);
It would result in a compilation error because of associativity rule.
Another case of associativity:
printf("%u", (int*)p++);
This will result in an error because the compiler will try to add 1 to the pointer first and then typecast it to an integer pointer. To avoid this, we can use parentheses to enforce the correct order of operations:
printf("%u", ((int*)p)++);
This will add 1 to the pointer p first and then typecast the result to an integer pointer.
*(arr + 1) = arr[1]
int *a[5] mean “a” is an array of 5 integer pointers.
#include <stdio.h>
int main() {
int *ptr[3], i , iA[]={5,10,15}, iB[]={1,2,3}, iC[]={2,4,6};
ptr[0]=iA; ptr[1]=iB; ptr[2]=iC;
for(i=0; i<3 ; i++){
printf("iA[%d]: address=%p data=%d", i, ptr[0]+i, *(ptr[0]+i));
printf("iB[%d]: address=%p data=%d", i, ptr[0]+i, *(ptr[0]+i));
printf("iC[%d]: address=%p data=%d", i, ptr[0]+i, *(ptr[0]+i));
}
return 0;
}
Resources to refer to:
- Syntax Explained
- Syntax Visualized
- Binary representations of floating point formats
- oceanO’s Article on pointers
Practice
Some practice questions to solidify concepts
Q. If float needs 4 bytes. float **p
p++ will move the pointer by how many bytes?
Answer: 8 bytes
the pointer moves forward by the size of the data type it points to (4 bytes for a float), but since the pointer is incremented after the value is accessed, the pointer moves two memory locations forward (4 bytes for the float value and 4 bytes for the increment).
- In this case, p is a pointer to a pointer to a float.
- Each float takes up 4 bytes of memory.
- When you do p++, you are incrementing the pointer p to point to the next location in memory where a float pointer is stored.
- Since each float pointer takes up 4 bytes, incrementing p by 1 (using p++) will move the pointer to the next float pointer, which is 4 bytes away. This is why p+2 will point to the memory location that is 8 bytes (2 * 4 bytes) away from the original p.
Q. char **p;
p++ will move the pointer by how many bytes?
Answer: 1 byte
- In this case, p is a pointer to a pointer to a char.
- Each char takes up 1 byte of memory.
- When you do p++, you are incrementing the pointer p to point to the next location in memory where a char pointer is stored.
- Since each char pointer takes up 1 byte, incrementing p by 1 (using p++) will move the pointer to the next char pointer, which is 1 byte away. This is why p+1 will point to the memory location that is 1 byte away from the original p.
Q. What will be the output of the following code snippet?
int *p;
float x = 5.2;
p = &x;
printf("%d", *p);
Answer: Undefined behavior, garbage
- *p will attempt to interpret the bytes of the float x as an integer value.
- However, the size of a float (4 bytes) is different from the size of an int (also 4 bytes on most systems), and the bit patterns will be interpreted differently.
- This results in a completely different value being printed, which is usually some random garbage value that doesn’t correspond to the original float value.
- For example, if x is 5.2, the bit pattern in memory might be something like 0x40a00000. When we try to print this as an int using %d, we’ll get a completely different value, such as 1072693248, which is just garbage.
Q. If P is a generic pointer then what does P+1 means?
void *p;
int x;
p = &x;
printf("%u", p+1);
Answer: cannot increment value of void pointer unless, it is typecasted to a specific type.
For e.g. printf("%u", (int*)p+1);
Q. What will be the output of the following code snippet?
int* p;
int x = 5;
p = &x;
printf("p: %p\n", p);
printf("p + 1: %p\n", p + 1);
printf("*p: %d\n", *p);
printf("*p + 1: %d\n", *p + 1);
printf("*(p+1): %d\n", *(p+1));
Answer: The output will have
memory address of x for p
memory address of x + 4 bytes for p + 1
value of x for *p which will be 5
value of x + 1 for *p + 1 which will be 6
garbage value for *(p+1) because it is not pointing to any
valid memory location.
Q. How would you print the content of a memory location 2000?
Assume int takes 2 bytes and char takes 1 byte.
a)
int *p = 2000;
printf("%d", *p);
b)
char *p1 = 2000;
printf("%c", *p1);
c) both
d) none
Answer: b)
Option a is incorrect because it will result in contents of memory location 2000 and 2001 being printed as an integer.
Q. Imagine p is pointing to an integer. Which of the following expressions would print the contents of the location that p is pointing to and make p point to the next location in memory without modifying any data?
a) printf("%d", *p++);
b) printf("%d", (*p)++);
Answer. (*p)++
increments the value that the pointer points to, while *p++
increments the pointer itself to point to the next element.
a. printf("%d", (*p)++);
- This first dereferences the pointer p to get the value it points to.
- It then increments that value by 1 (post-increment).
- Finally, it prints the original value (before the increment) using the %d format specifier.
- So, this expression:
- Accesses the value pointed to by p
- Increments that value by 1
- Prints the original, pre-incremented value
b. printf("%d", *p++);
- This first prints the value pointed to by p using the %d format specifier.
- It then increments the pointer p itself by the size of the type it points to (e.g. if p is a pointer to int, it would increment by 4 bytes).
- This first prints the value pointed to by p using the %d format specifier.
- It then increments the pointer p itself by the size of the type it points to (e.g. if p is a pointer to int, it would increment by 4 bytes).
- So, this expression:
- Prints the value pointed to by p
- Increments the pointer p to point to the next element in memory
Complex Pointers
Rules in order of precedence
()
and[]
- Evaluated left to right
- Parentheses that are grouping together multiple parts of a declaration have the highest precedence. Next are the postfix operators () and []
*
andid
Name of Pointer or identifier- Evaluated right to left
- Data Type
Code Snippets with numbers indicating precedence
int *p;
3 21
// declare p as pointer to int
int **p;
4 321
// declare p as pointer to pointer to int
int *p[5];
4 32 1
// declare p as array of 5 pointers to int
int (*p)[5];
.2 .1 // paranthesis # 1.1 and 1.2
3 2 1
// declare p as pointer to array of 5 ints
int *p();
4 32 1
// declare p as function returning pointer to int
int (*p)();
.2 .1
3 1 2
// declare p as pointer to function returning int
int (*p)(int *);
.2 .1
3 1 2
// declare p as pointer to function that takes an int pointer returning int
int **p();
5 432 1
// declare p as function returning pointer to pointer to int
int *(*p)();
.2 .1
4 3 1 2
// declare p as pointer to function returning pointer to int
int (**p)();
.3.2.1
4 3 1 2
// declare p as pointer to pointer to function returning int
int (*p)(int, char);
1.2 1.1
2.1 2.2
3 1 2
// declare p as pointer to function
// taking int and char as arguments and returning int
int **p[10];
5 432 1
// declare p as array of 10 pointers to pointer to int
int *(*p)[10];
.2 .1
4 3 1 2
// declare p as pointer to array of 10 pointers to int
int (**p)[10];
.3.2.1
3 1 2
// declare p as pointer to pointer to array of 10 ints
void *(**p[5])(int, char);
.3.2.1
4 3 1 2
// declare p as array of 5 pointers to pointer to function
// taking int and char as arguments and returning void pointer
int (*(*p)[5])();
// declare p as pointer to array of 5 pointers
// to function returning int
int *(*(*p)(int))[10];
// p is pointer to function having int argumenet
// and returning pointer to array of 10 pointers to int
int *(*(*p[5])())();
// declare p as array of 5 pointers to function returning pointer
// to function returning pointer to int
float(*(*p())[])();
// declare p as function returning pointer
// to array of pointers to function returning float
int *((*p)[5])();
// invalid declaration
Extreame case:
step by step explanation
Pointers and Functions
#include <stdio.h>
// Formal arguments: int *a, int *b
void swap (int *a, int *b){
int t;
t = *a;
*a = *b;
*b = t;
}
int main() {
int x = 5, y = 2;
// Actual arguments: &x, &y
swap(&x, &y);
printf("%d %d", x, y);
return 0;
}
In main()
, the values of x
and y
are 5 and 2, respectively.
When swap(&x
, &y
) is called, the addresses of x
and y
(let’s say they are 0x1234
and 0x5678
respectively) are passed as the actual arguments.
Inside the swap function, the formal parameters a
and b
are assigned the copies of the addresses 0x1234
and 0x5678
.
Within the swap function, the values pointed to by a
and b
(which are the values of x
and y
) are swapped, so now *a
is 2 and *b
is 5.
After the swap function returns, the values of x
and y
in main()
have been swapped, so x
is now 2 and y
is now 5.
The key point here is that the swap function receives copies of the addresses, not the actual variables x
and y
. This is an example of call by value, where the function operates on the copies of the arguments, not the original variables.
Value of actual arguement is copied into formal arguements and any change done to the formal arguements will not reflect in the actual arguements.
If we had used call by reference, the swap function would have had direct access to the original variables x
and y
, and the swap would have been performed on the original values, not copies.
#include <stdio.h>
void mystery(int *ptra, int *ptrb) {
int *temp;
temp = ptrb;
ptrb = ptra;
ptra = temp;
}
int main() {
int a = 2016, b = 0, c = 4, d = 42;
mystery(&a, &b);
if (a < c)
mystery(&c, &a);
mystery(&a, &d);
printf("%d", a);
return 0;
}
#include <stdio.h>
int f(int x, int *py, int **ppz) {
int y, z;
**ppz += 1;
z = **ppz;
*py += 2;
y = *py;
x += 3;
return x + y + z;
}
int main() {
int c, *b, **a;
c = 4;
b = &c;
a = &b;
printf("%d", f(c, b, a));
return 0;
}
#include <stdio.h>
void f(int *p, int *q)
{
p = q;
*p = 2;
}
int i = 0, j = 1;
int main()
{
f(&i, &j);
printf("%d %d \n", i, j);
getchar();
return 0;
}
Pointers and 1D Arrays
int a;
int x[5];
a = 5;
x = 5; // Error
// Name of array is a constant pointer to 1st element of array.
printf("%u",x);
printf("%d",*x);
printf("%d",*x+1);
printf("%d",*(x+1));
2000000
2000004
5
6
0
-
printf("%u\n", x);
- This statement would print the memory address of the array x. The %u format specifier is used to print an unsigned integer, which is the format of memory addresses. -
printf("%d\n", x+1);
- This statement would print the memory address of the second element of the array x. The x array is implicitly converted to a pointer to its first element, and adding 1 to that pointer advances it to the next element. -
printf("%d\n", *x);
- This statement would print the value of the first element of the array x. The * operator is used to dereference the pointer to the first element, which retrieves the value stored at that memory location. -
printf("%d\n", *x+1);
- This statement would print the value of the first element of the array x plus 1. The *x expression retrieves the value stored at the memory location pointed to by x, and the +1 expression increments that value by 1. -
printf("%d\n", *(x+1));
- This statement would print the value of the second element of the array x. The x array is implicitly converted to a pointer to its first element, and adding 1 to that pointer advances it to the next element. The * operator is then used to dereference the pointer to the second element, which retrieves the value stored at that memory location.
Passing 1D Array to Function
#include <stdio.h>
void array(int *p){
*p = 10;
*(p+1) = 20;
p[2] = 3;
3[p] = 5;
}
int main() {
int x[4];
array(x);
return 0;
}
As in above example, Generally 1D array is passed as call by reference.
If we pass by value then we would not be able to change the value of array elements.
Practice Questions
#include <stdio.h>
int main() {
int x[5] = {1, 3, 6, 9, 4};
printf("%d\n", *x);
printf("%d\n", *(x + 1));
printf("%d\n", *x + 1);
printf("%d\n", *(x + 1)+2;
printf("%p\n", x);
printf("%p\n", &x);
printf("%p\n", x + 1);
printf("%p\n", &x + 1);
printf("%d\n", *x++);
printf("%d\n", (*x)++);
printf("%d\n", ++*x);
printf("%d\n", *++x);
return 0;
}
1
3
2
5
0x7ffee1234560
0x7ffee1234560
0x7ffee1234564
0x7ffee1234580
Error
1
3
Error
-
printf("%d\n", *x);
- prints the value stored at the address pointed to by
x
. Sincex
is an array,*x
refers to the first element of the array, which is 1.
- prints the value stored at the address pointed to by
-
printf("%d\n", *(x + 1));
- Prints the value stored at the address
x + 1
, which is the second element of the array, which is 3. The expressionx + 1
is a pointer arithmetic operation that moves the pointer to the next element of the array.
- Prints the value stored at the address
-
printf("%d\n", *x + 1);
- prints the sum of the first element of the array (1) and 1, resulting in 2.
-
printf("%d\n", *(x + 1)+2);
- Prints the value of the second element of the array x (which is 3) plus 2, resulting in 5.
-
printf("%p\n", x);
- Prints the address of the first element of the array, which is
0x7ffee1234560
(the actual address may vary).
- Prints the address of the first element of the array, which is
-
printf("%p\n", &x);
- Prints the address of the entire array, which is also
0x7ffee1234560
.
- Prints the address of the entire array, which is also
-
printf("%p\n", x + 1);
- Prints the address of the second element of the array, which is
0x7ffee1234564
.
- Prints the address of the second element of the array, which is
-
printf("%p\n", &x + 1);
- Prints the address of the memory location immediately after the array, which is
0x7ffee1234580
.
- Prints the address of the memory location immediately after the array, which is
-
printf("%d\n", *x++);
-
Error. First it will try to solve
x++
. Name of array is a constant pointer to the first element. Increment not possible.
-
-
printf("%d\n", (*x)++);
- First dereferences the pointer x to access the second element of the array, and then increments the value of that element. Prints 1 and increments the value as 2.
-
printf("%d\n", ++*x);
- First increments the value of the element (which is now 2), and then prints the new value 3.
-
printf("%d\n", *++x);
- Not possible. Constant pointer.
#include <stdio.h>
void array(int *p){
// 6. p pointer created at address 4000 having value 2000
printf("%d", *p++); // 7. print 5 and increment p to 2002
print("%d", (*p)++); // 8. print 10 and increment value at 2002 to 11
printf("%d", *++p); // 9. increment p to 2004 and print value at 2004 -> 15
printf("%d", ++*p); // 10. increment value at 2004 to 16 and print 16
printf("%d", p[-1]) // 11. from 2004 points to 2002 and prints value at 2002 -> 11
}
int main() {
int x[3] = {5, 10, 15};
// 1. 3 elements in array let address be 2000, 2002, 2004
// of value 5, 10, 15 respectively.
printf("%d", *x); // 2. value at 2000 -> 5
printf("%d", *(x+1)); //3. value at 2002 -> 10
printf("%d", *x++); // 4. Error.
array(x); // 5. control transfer
return 0;
}
Pointer to an array and An Array of pointers
#include <stdio.h>
int main() {
int (*p)[5]; // Pointer to an array of 5 integers
int a[5];
int *p1[5]; // Array of 5 pointers to integers
int x = 5, y = 10;
p1[1] = &x;
p1[2] = &y;
*(p1 + 3) = &x;
p = &a;
sizeof(p);
sizeof(p1);
p + 1; // p + 10 => 10
p1 + 1; // p1 + 2 => 6002
p1 // 6000
*p1 // 9000
**p1 // 10
*(*p1+1) // p1 is 6000. value at 6000 is 9000. 9000 + 1 is 9002.
// value at 9002 is junk
return 0;
#include <stdio.h>
int main() {
int a[0] = {0, 1, 2, 3, 4}
int *p[] = {a, a+1, a+2, a+3, a+4};
int **ptr = p;
a, *a // 2000 0
p, *p, **p // 4000 2000 0
ptr, *ptr, **ptr // 4000 2000 0
// ptr will not print 6000 as ptr is not an array with base address. ptr is a pointer
ptr++; // 4000 and ptr is now pointing to 4002
ptr-p, *ptr-a, **ptr // 1 1 1
*ptr++; // 2002 and will increment 4002 to 4004
ptr-p, *ptr-a, **ptr // 2 2 2
*++ptr; // will increment 4004 to 4006 and print 4006
ptr-p, *ptr-a, **ptr //3 3 3
}
Pointers and 2D Arrays
int x[3][2] = {{5,9}, {3,10}, {6,8}};
Here, we have declared a 2D array of integers with 3 rows and 2 columns.
or
3 1D arrays having 2 elements.
name of array x is a constant pointer to x[0] 1d array of 2 elements.
x = 2000
*x = x[0] = 2000
&x = 2000
x + 1 = 2004
*x + 1 = 2002
&x + 1 = 2010
int x[3][4] = {{5, 9, 3, 10}, {6, 8, 2, 4}, {1, 7, 15, 25}};
x = 2000
x + 1 = 2008
x + 2 = 2016
*x = 2000
*x + 1 = 2002
*(x + 1) = 2008
((x + 1) + 2) = 2 = x[1][2]
**x = 5
int x[] = int *a
Pointers and Structures
struct stud{
int roll;
char dept_code[25];
float cgpa;
} class, *ptr;
struct book{
char Name[20];
float price;
char ISB[30];
}; struct book b, *br;
Once ptr
points to a structure variable, the members can be accessed through a dot operator or an arrow operator.
(*ptr).roll
(*ptr).dept_code
(*ptr).cgpa
ptr->roll
ptr->dept_code
ptr->cgpa
Order of precendence
()
[]
->
.
-
ptr->roll
is equivalent to (*ptr).roll
-
*ptr.roll
is invalid -
*p->x
is same as *(p->x)
-
*p[n]
is same as*(p+[n])
-
*p->x++
is same as*((p->x)++)
-
++*p[n]
is same as++(*(p[n]))
-
++*p->y
is same as++(*(p->y))
and++(*((*p).y))
structure variables are passed by value just like primitive datatypes(intger, char, float, etc.) The only difference is that the structure name is used instead of the data type name. arrays are generally passed by reference.
Misc
Index
Type Conversion
Type conversion is the process of converting one data type to another. In C, there are two types of type conversion:
- Implicit Type Conversion
- Explicit Type Conversion(a.k.a Type Casting)
and there are two rules of type conversion:
- Widening Conversion: When a data type of lower rank is converted to a data type of higher rank. Applicable in expressions.
- Narrowing Conversion: When a data type of higher rank is converted to a data type of lower rank. Can be applicable in both expressions and assignments. Generally applicable in assignments.
Implicit Type Conversion
Implicit type conversion is done automatically by the compiler. It is also known as coercion. In this type of conversion, the compiler automatically converts one data type to another without any user intervention. This is done when the data type of the expression is automatically converted to a “higher” data type.
signed will be converted into unsigned
if one operand has an unsigned type whose conversion rank is at least as high as that of the other operand’s type, then the other operand is converted to the type of the operand with the higher rank.
Explicit Type Conversion
int a;
a = 2 + (int) 3.5;
// 2 + 3;
// 5;
int a;
a = 2 + (int) 3.5;
// 2.0 + 3;
// now compiler will do implicit coversion
// 2.0 + 3.0;
// 5.0;
Memory Allocations
Memory allocation is a process by which computer programs and services are assigned with physical or virtual memory space. The memory allocation is done either before or at the time of program execution.
A memory is divided into 4 different portions:
- Stack segment
- Heap segment
- Data segment
- Text segment
Stack
- One frame is allocated to each function call
- Auto (or local) variables defined the frame allocated for a function
- Storage allocated for each frame is reclaimed when the function call is complete
Heap
- Dynamically and explicitly allocated memory
- It is a large pool of memory in which the program can request memory dynamically at runtime.. Allocated storage is not reclaimed automatically on function return. In heap, the memory is allocated or deallocated without any order.
- Programmer is responsible for deallocation
- The memory allocated in a head is accessible globally to all functions.
There are two types of memory allocations:
- Compile-time or Static Memory Allocation
- Run-time or Dynamic Memory Allocation
Compile-time or Static Memory Allocation
int main(){
int variable = 10;
}
When allocating memory at compile-time, the size at the time of declaration is fixed and cannot be changed during the execution of the program. The memory is allocated on the stack.
Run-time or Dynamic Memory Allocation
int main(){
int *ptr = (int*)malloc(sizeof(int));
*ptr = 10;
free(ptr);
}
When allocating memory at run-time, the size of memory can be changed during the execution of the program. The memory is allocated on the heap.
Dynamic Memory Allocation using malloc() a.k.a Memory Allocation
malloc()
is a bulit in function declared in stdlib.h
header file. It is used to dynamically allocate a block of memory on the heap. The function returns a pointer of type void
which can be cast into a pointer of any form.
#include <stdio.h>
(void* ) malloc (size_t size);
The malloc()
function takes the number of bytes to be allocated as an argument and returns a pointer to the first byte of the allocated memory. If the memory allocation is successful, the function returns a pointer to the first byte of the allocated memory. If the memory allocation fails, the function returns NULL
.
Malloc allocates memory without knowing the type of data to be stored in the memory. It is the programmer’s responsibility to typecast the pointer to the appropriate type.
#include <stdio.h>
#include <stdlib.h>
int main() {
int i, n;
printf("Enter the number of integers: ");
scanf("%d", &n);
int *ptr = (int *)malloc(n * sizeof(int));
if (ptr == NULL) {
printf("Memory not available.");
exit(1);
}
for (i = 0; i < n; i++) {
printf("Enter an integer: ");
scanf("%d", ptr + i);
}
for (i = 0; i < n; i++)
printf("%d ", *(ptr + i));
return 0;
}
Dynamic Arrays
Static Arrays | Dynamic Arrays | |
---|---|---|
Syntax | int arr[10]; | int *arr = (int*) malloc(sizeof(int)*10); |
Storage | Stack frame of the function defining it. | Heap Area |
Use of pointers | Implicit use of pointers | Explicit use of pointers |
Re-sizing | Arrays can’t be resized once defined | Arrays can be resized |
Size of the array | Small sized arrays only | Large arrays can also be initialized |
Returning Arrays from Functions Solutions:
- Declare array in the calling function and pass it to the called function.
#include <stdio.h>
#include <stdlib.h>
void copy(int c[], int n, int d[]){
for (int i = 0; i < n; i++){
d[i] = c[i]
// or *(b+1) = *(a+i);
}
}
int main() {
int a[5] = {1, 2, 3,4, 5};
int b[5];
copy(a, 5, b);
for(int i = 0; i < 5; i++){
printf("%d\t", b[i]);
}
return 0;
}
- Declare a pointer inside the called function, allocate memory and return the pointer to calling function.
- Since memory is allocated in the heap, it will also be accessible in the calling function.
#include <stdio.h>
#include <stdlib.h>
int *copy(int a[], int n){
int *b = (int *) malloc(sizeof(int)*n);
for (int i = 0; i < n; i++){
b[i] = a[i];
// or *(b+1)=*(a+i);
}
return b;
}
int main(){
int a[5] = {1, 2, 3, 4, 5};
int *b;
b = copy(a, 5);
for (int i = 0; i < 5; i++){
printf("%d\t", b[i]);
}
}
Dynamic 2D Arrays
int **a;
a = (int **) malloc(3 * sizeof(int *));
The above code block allocates a 2-D array a of 3 rows. Each row is a pointer to an integer. We can allocate memory for each row using the following syntax:
for (int i = 0; i < 3; i++)
a[i] = (int *) malloc(2 * sizeof(int));
The above code block allocates memory for each row of the 2-D array. The first row is pointed to by a[0], the second row is pointed to by a[1], and so on. The above code allocates memory for 2 integers in each row.
a[0] = (int *) malloc(1 * sizeof(int));
a[1] = (int *) malloc(3 * sizeof(int));
a[2] = (int *) malloc(5 * sizeof(int));
Dynamic Structures
struct student
{
char roll;
int dept_code[25];
float cgpa;
};
struct student *s;
s = (struct student *) malloc(sizeof(struct student));
The above code allocates a single structure variable dynamically. The malloc() function returns a void pointer, which we cast to a pointer to a structure of type struct student.
Similarly, we can allocate an array of structures dynamically using the following syntax:
struct student *s_arr;
s_arr = (struct student *) malloc(10 * sizeof(struct student));
#include <stdio.h>
#include <stdlib.h>
struct stud
{
int roll;
char dept_code[25];
float cgpa;
};
int main()
{
struct stud *studArray =
(struct stud *)malloc(sizeof(struct stud *) * 10);
int i = 0;
while (i < 10)
{
printf("Enter roll no of student %d:", i);
scanf("%d", &(studArray[i].roll));
printf("Enter dept_code of student %d:", i);
scanf("%s", &(studArray[i].dept_code));
printf("Enter cgpa of student %d:", i);
scanf("%f", &(studArray[i].cgpa));
i++;
}
struct stud temp = studArray[0];
i = 1;
while (i < 10)
{
if (temp.cgpa < studArray[i].cgpa)
temp = studArray[i];
i++;
}
printf("The roll number of Student with max CGPA is: %d",
temp.roll);
}
Dynamic Memory Allocation using calloc() a.k.a Contiguous Allocation
It is different from malloc()
in two ways:
-
malloc()
does not initialize the memory allocated, whilecalloc()
initializes the allocated memory to zero.- Both malloc and calloc return
NULL
when sufficient memroy is not available in the heap.
- Both malloc and calloc return
-
malloc()
takes a single argument, the number of bytes to be allocated, whilecalloc()
takes two arguments, the number of elements to be allocated and the size of each element.-
malloc() way
int *ptr = (int *)malloc(n * sizeof(int));
-
calloc() way
int *ptr = (int *)calloc(n, sizeof(int));
-
Dynamic Memory Allocation using realloc() a.k.a Reallocation
realloc()
is used to change the size of the memory block that was previously allocated using malloc()
or calloc()
. The function takes two arguments, a pointer to the previously allocated memory block and the new size of the memory block.
void *realloc(void *ptr, size_t size);
The function moves the contents of the old block to a new block and returns a pointer to the newly allocated memory block. If the memory allocation is successful, the function returns a pointer to the first byte of the newly allocated memory block. If the memory allocation fails, the function returns NULL
. The data of the old block is not lost. We may lose the data when the new size is smaller than the old size. Newly allocated memory is not initialized.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(10 * sizeof(int));
int i;
if (ptr == NULL) {
printf("Memory not available.");
exit(1);
}
for (i = 0; i < 10; i++)
*(ptr + i) = i;
ptr = (int *)realloc(ptr, 20 * sizeof(int));
if (ptr == NULL) {
printf("Memory not available.");
exit(1);
}
for (i = 10; i < 20; i++)
*(ptr + i) = i;
for (i = 0; i < 20; i++)
printf("%d ", *(ptr + i));
return 0;
}
Releasing the Dynamic Memory Allocation using free()
The free()
function is used to deallocate the memory that was previously allocated using malloc()
, calloc()
, or realloc()
. The function takes a pointer to the memory block that needs to be deallocated as an argument. The function does not return any value.
void free(ptr)
The memory allocated in the heap is not automatically deallocated when the program terminates. It is the programmer’s responsibility to deallocate the memory using the free()
function. If the memory is not deallocated, it leads to memory leaks.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(10 * sizeof(int));
int i;
if (ptr == NULL) {
printf("Memory not available.");
exit(1);
}
for (i = 0; i < 10; i++)
*(ptr + i) = i;
for (i = 0; i < 10; i++)
printf("%d ", *(ptr + i));
free(ptr);
return 0;
}
Memory Leak
Memory leak is a situation where the programmer forgets to deallocate the memory that was previously allocated using malloc()
, calloc()
, or realloc()
. Memory leaks can lead to the exhaustion of memory resources and can cause the program to crash. It is the programmer’s responsibility to deallocate the memory using the free()
function.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
// Allocate memory for an integer
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed.");
return 1;
}
// Assign a value to the allocated memory
*ptr = 42;
// Use the allocated memory
printf("Value stored at allocated memory: %d\n", *ptr);
// Free the allocated memory
free(ptr);
// Try to access the memory after freeing (This is undefined behavior)
// Here, we are just printing the value at the memory location to demonstrate that it's not guaranteed to be zeroed out or invalidated after freeing.
printf("Value after freeing memory: %d\n", *ptr);
return 0;
}
Files
Applications require information to be read from or written to a memory device
-
All files are administered by the Operating System(OS) which allocates and deallocates disk blocks.
-
The OS also controls the transfer of data between programs and the files they access
The most convenient way to handle such large scales of data is to read/write data from/to files.
Handling Files in C programs
A file has to be opened before data can be read from or written to it.
When the file is opened, the OS associates a stream with the file
A common buffer and a file postion indication are maintained in the memory for a function to know how much of the file has already been read.
The stream is disconnected when the files is closed.
Opening a File
FILE *file_pointer_name;
file_pointer_name = fopen("filename", "mode");
mode can be one of the following:
- r: Open a file for reading. The file must exist.
- w: Create an empty file for writing. If a file with the same name already exists, its content is erased and the file is considered as a new empty file.
- a: Append to a file. Writing operations append data at the end of the file. The file is created if it does not exist.
- r+ or w+: Open a file for update both reading and writing. The file must exist.
- a+: Supports both reading and writing but but writing is only supported if it is appended at the end of the file.
- rb: Open a file for reading in binary mode.
- wb: Create an empty file for writing in binary mode.
- ab: Append to a file in binary mode.
- rb+ or wb+: Open a file for update both reading and writing in binary mode.
Closing a File
Although it is not required to close a file. However, it is a good practice to do so for various reasons:
Operating systems have a limit on the number of files that a program can open.
Multiple programs might be trying to access the same file, and they cannot do so until the current program closes the file stream.
fclose(file_pointer_name);
Reading and Writing to a File
The standard library offers a number of functions for performing read and write on the files
- character oriented functions (
fgetc
andfputc
) - line oriented functions (
fgets and
fputs`) - formatted functions (
fscanf
andfprintf
)
fgetc
:- It fetches the next character that has not yet been read from the file.
fgetc(FILE *stream);
- It could be used as follows:
int c = fgetc(fp);
- If there are no more characters to be read from the file, fgetc returns EOF.
EOF is an integer defined in stdio.h, having a value of -1. So, if we wanted to read all characters from a file, it could be done inside a while loop, as follows:
while((c = fgetc(fp)) != EOF) {
// Process the current character in c here.
}
This loop will automatically terminate on EOF.
-
fputc
:- It outputs a character to the current file.
fputc(int c, FILE *stream);
- Note that although the syntax specifies int, the output is a character, so we need to enter the ASCII code. For example,
fputc(70, fp);
outputs ‘F’, because the ASCII code of ‘F’ is 70.
-
fgets
:fgets(char *s, int size, FILE *stream);
- This is a line-oriented function. fgets reads at most size - 1 characters from the file stream and saves the characters in s. After that,
\0
, the string-terminating character, is automatically appended to s. - Note that if a newline is encountered before size - 1 characters, then fgets automatically stops reading. So, fgets will read size - 1 characters or one entire line, whichever is smaller. Example usage:
char buf[10];
fgets(buf, 10, fp);
-
fputs
:fputs(const char* s, FILE *stream);
- The fputs function writes the string of characters in
s
to the file stream.
-
fscanf
andfprintf
:-
These function exactly like scanf and printf, except we need to specify the file stream as the first argument.
-
fscanf(FILE *stream, ….)
(the rest is the same as normal scanf/printf).fscanf(fp1, “%s%d%d”, name, &marks1, &marks1); fprintf(fp2, “%s %d %d %d”, name, marks1, marks2, marks1 + marks2);
-
Example
#include <stdio.h>
int main() {
FILE* fp = open("file.txt", "w+");
char buf[] = "A1234 abcd";
fputs(buf, fp); // Writes "A1234 abcd" to fp.
rewind(fp); // Go back to position 0 of fp.
int c = fgetc(fp); // c = 'A';
fputc(c, stdout); // Prints A to standard output (explained later).
int x;
fscanf(fp, "%d", &x); // x = 1234
fprintf(stdout, " %d", x); // Prints 1234
fclose(fp);
}
Special Files
stdin
: Standard input file. It is a file that is automatically opened when a program starts. It is used to read input from the keyboard.stdout
: Standard output file. It is a file that is automatically opened when a program starts. It is used to write output to the screen. It is associated with the terminal.stderr
: Standard error file. It is mainly used for debugging purposes. The default output of stderr is also in the terminal, the same as stdout.
fscanf()
and fprintf()
can achieve all of the functionality of scanf()
and printf()
, simply by using the special files, stdin and stdout.
fscanf()
can read from file streams, but scanf()
cannot.
Error Handling
The functions that open files return NULL if they fail to open the file.
FILE *fp = fopen("file.txt", "r");
if (fp == NULL) {
printf("Error opening file\n");
return 1;
}
Flowcharts and Diagrams
Flowcharts are a graphical representation of an algorithm. They are used to represent the flow of control in a program.
The algorithms are:
- Unambiguous/well-defined: Each step performs a very clear and unambiguous task.
- Finite: The total number of steps in the algorithm is finite. So, the algorithm will terminate after executing these steps and provide a definite output.
Any structured programming language supports three types of statements (constructs), and these are:
- Sequencing: Executing a set of statements in a sequential order
- Decision-making: Choosing from many alternative paths
- Repetition/Iteration: Executing one or a group of statements in a loop iteratively
Flowcharts are made up of different symbols that represent different elements of the program. The symbols are connected by arrows to show the flow of control.
Symbol | Purpose | Description |
---|---|---|
Arrow symbol | Flow Line | Indicates the flow of logic by connecting symbols |
Oval | Terminal(Stop/ Start) | Represents the start and end of a flowchart |
Parallelogram | I/O | Used for input and output operations. |
Rectangle | Processing | Used for arithmetic operations and data manipulations. |
Diamond | Decision | Used for decision making between two or more alternatives |
Circle | On-page connected | Used to join different flowline |
Circle Downward Pointed | Off page connector | Used to connect the flowchart on a different page |
Margined Rectangle | redefined Process/ Function | Represents a group of statements performing one processing task. |
Projects
Grocery List Management System
#include <stdio.h>
struct item
{
int ID;
char name[50];
float price;
int quantity;
};
struct item* readGroceryList(int count)
{
struct item* gItems = (struct item*)malloc(sizeof(struct item) * count);
for(int i = 0; i < count; i++)
{
gItems[i].ID = i + 1;
printf("Enter the details for item %d: \n", i + 1);
printf("Name:");
scanf("%s", gItems[i].name);
printf("Price:");
scanf("%f", &gItems[i].price);
printf("Quantity:");
scanf("%d", &gItems[i].quantity);
}
return gItems;
}
void printGroceryList(struct item* gItems, int count)
{
printf("\n");
for(int i = 0; i < count; i++)
{
printf("Item ID: %d, ", gItems[i].ID);
printf("Name: %s, ", gItems[i].name);
printf("Price: %f, ", gItems[i].price);
printf("Quantity: %d\n", gItems[i].quantity);
}
}
struct item findItem(int qVal, struct item* gItems, int count)
{
int index = -1;
for(int i = 0; i < count; i++)
{
if(gItems[i].quantity == qVal)
{
index = i;
return gItems[index];
}
}
struct item emptyItem;
emptyItem.ID = -1;
return emptyItem;
}
struct item findMaxPriceItem(struct item* gItems, int count)
{
int maxIndex = -1;
int maxPrice = -1;
for(int i = 0; i < count; i++)
{
if(gItems[i].price > maxPrice)
{
maxPrice = gItems[i].price;
maxIndex = i;
}
}
return gItems[maxIndex];
}
int main()
{
int num_q;
printf("Enter the number of unique grocery items in the store - ");
scanf("%d", &num_q);
struct item* gItems_main = readGroceryList(num_q);
printGroceryList(gItems_main, num_q);
int qVal;
printf("\n Enter the quantity of the item you wish to find - ");
scanf("%d", &qVal);
struct item fItem = findItem(qVal, gItems_main, num_q);
if(fItem.ID == -1) {
printf("Item not found!\n");
} else {
printf("The item found with quantity %d is:\n", qVal);
printf("ID: %d, Name = %s, Price = %f\n", fItem.ID, fItem.name, fItem.price);
}
struct item maxItem = findMaxPriceItem(gItems_main, num_q);
printf("\nThe item with maximum price is: \n");
printf("Item ID: %d, ", maxItem.ID);
printf("Name: %s, ", maxItem.name);
printf("Price: %f, ", maxItem.price);
printf("Quantity: %d\n", maxItem.quantity);
}
Acknowledgements
Professors, BITS Pilani
Endianness blog by Kealan Parr
Comples Declarations by BrianBarto
Rust 🦀
“The Rust programming language helps you write faster, more reliable software. High-level ergonomics and low-level control are often at odds in programming language design; Rust challenges that conflict. Through balancing powerful technical capacity and a great developer experience, Rust gives you the option to control low-level details (such as memory usage) without all the hassle traditionally associated with such control.” - Rust Foundation
Specialities
- High-level language features without performance penalties
- Program behaviors can be enforced at compile time
- Enhanced program reliability
- Built—in dependency management, similar to npm
- Quickly growing ecosystem of libraries
- First-class multithreading
- Compiler error to improperly access shared data
- Type system:
- Can uncover bugs at compile time
- Makes refactoring simple
- Reduces the number of tests needed
- Module system makes code separation simple
- Adding a dependency is 1 line in a config file
- Tooling:
- Generate docs, lint code, auto format
Match
Match expressions are similar to if else if
It matches only on same type and accounts every possibility. Making code more robust
It works on expressions instead of statements. Therefore, at the end of statement instead of ;
we will use ,
.
fn main() { let some_int = 3; match some_int { 1 => println!("its 1"), 2 => println!("its 2"), 3 => println!("its 3"), _ => println!("its something else") } }
Match will be checked by the compiler, so if a new possibility is added, you will be notified when this occurs
In contrast, else..if is not checked by the compiler. If a new possibility is added, your code may contain a bug
Prefer using match over else..if when working with a single variable
Traits
Traits define similar functionality for different types.
It can be used to standardize functionality across multiple different types.
Standardization permits functions to operate on multiple different types. [ Code deduplication ]
trait Move {
fn move_to(&self, x: i32, y: i32);
}
struct Snake;
impl Move for Snake {
fn move_to(&self, x: i32, y: i32) {
println!("slither to ({}, {})", x, y);
}
}
struct Horse;
impl Move for Horse {
fn move_to(&self, x: i32, y: i32) {
println!("gallop to ({}, {})", x, y);
}
}
Generics
Generic Functions
Generic functions are functions that can operate on multiple different types.
A function that can have a single parameter to operate on multiple different types.
Trait is used as function parameter instead of data type. Function depends on existence of functions declared by trait.
Efficient code. Automatically deduces the type of the parameter when new data type is used.
3 types of Generic Syntaxes
-
First Way
-
Any type that implements a trait.
-
Go with this when you have small number of traits and small number of parameters.
fn function(param1: impl Trait1, param2: impl Trait2) { // code }
fn make_move(thing: impl Move, x: i32, y: i32) { thing.move_to(x, y); }
-
-
Second Way
-
A generic type
T
is constrained to implement a specific TraitTrait1
andU
is contrained to implementTrait2
. The function parameter must be of TypeT
andU
and the function will only work if the parameters implement the traits. -
Used only with small number of traits and parameters.
fn function<T: Trait1, U: Trait2>(param1: T, param2: U){ // code }
- Generic type
T
(used to define the type of the parameter) then the trait that the type must implement. In the function parameters we setthing
to be of typeT
and then we call themove_to
method onthing
.
fn make_move<T: Move>(thing: T, x: i32, y: i32) { thing.move_to(x, y); }
fn make_move<T: Move>(thing: T, x: i32, y: i32) { thing.move_to(x, y); }
-
-
Third Way
- A generic type
T
andU
are used as parameters and then we use thewhere
keyword to specify the constraints.
fn function<T, U>(param1: T, param2: U) where T: Trait1 + Trait2, U: Trait3 + Trait4 // type 1 and type 2 must implement Trait1 and Trait2 // type 3 and type 4 must implement Trait3 and Trait4 { // code }
fn make_move<T>(thing: T, x: i32, y: i32) where T: Move { thing.move_to(x, y); }
- A generic type
Generic Structures
Store data of any type within the structure
- may be any type or constrained by traits
Useful when making own data collections
struct Name<T: Trait1 + Trait2, U: Trait3 + Trait4> {
field1: T,
field2: U
}
struct Name<T, U>
where
T: Trait1 + Trait2,
U: Trait3
{
field1: T,
field2: U
}
Example usage with single type
trait Seat {
fn show(&self);
}
struct Ticket<T: Seat> {
location: T,
}
fn tickect_info(ticket: Ticket<AirlineSeat>) {
ticket.location.show();
// regular non generic fn that accepts a ticket structure as a function parameter.
// we always need to specify the type of the ticket. Here, we are using AirlineSeat.
// This AirlineSeat is a type that implements the Seat trait.
}
let airline = Ticket {location: AirlineSeat :: FirstClass};
tickect_info(airline);
In the above example, Ticket
struct is generic over any Seat type and the fn ticket_info
only accepts a Ticket
struct with a AirlineSeat
type.
To maximize our usage of this function we can use generics.
trait Seat {
fn show(&self);
}
struct Ticket<T: Seat> {
location: T,
}
fn tickect_info<T: Seat>(ticket: Ticket<T>) {
ticket.location.show();
}
let airline = Ticket {location: AirlineSeat :: FirstClass};
let concert = Ticket {location: ConcertSeat :: FrontRow};
tickect_info(airline);
tickect_info(concert);
- cannot mix generic structures in a single collection
- Generic Structures expand to structures of a specific type
Implementing Traits for Generic Structures
-
Generic Implementation
- Implements functionality for any type that can be used with the structure
- applies to all types that alo implement in the indicated trait.
- Implements functionality for any type that can be used with the structure
-
Concretee Implementation
- Implements functionality for a specific type
- only apply to the type indicated in the angle braces
- Implements functionality for a specific type
struct Name<T: Trait1 + Trait2, U: Trait3 + Trait4> {
field1: T,
field2: U
}
impl<T: Trait1 + Trait2, U: Trait3 + Trait4> Name <T, U> {
fn function(&self, arg1: T, arg2: U) {}
}
or
struct Name<T, U>
where
T: Trait1 + Trait2,
U: Trait3
{
field1: T,
field2: U
}
impl <T, U> Name <T, U>
where
T: Trait1 + Trait2,
U: Trait3
{
fn function(&self, arg1: T, arg2: U) {}
}
Memory
Stack
- Data placed sequentially
- LIFO (Last In First Out)
- All variables are stored in the stack
- this does not mean all the data is on the stack
- Very fast to work with the stack
Heap
- Data placed algorithmically
- slower than stack
- Unlimited space
- Uses pointers
usize
data type
- All dynamincally sized collections use Heap
- Eg. Vextors and HashMaps
Placing data on the heap
struct Entry { id: i32, } fn main() { let entry = Box::new(Entry { id: 1 }); println!("{}", entry.id); let data_stack = *data_ptr // to put it on the stack. dereferencing }
Trait Objects
Trait Objects offer a way to dynamically change program behaviour at runtime.
- Dynamically Allocated Objects
- Can be understood as Runtime Generics.
- Dynamic Dispatch in contrast to Static Dispatch of Generics.
- Trait Objects are also more flexible than Generics.
- Can be understood as Runtime Generics.
- Allows mixed types in a collection.
- Can be used to implement Polymorphism.
trait Clicky{
fn click(&self);
}
struct Keyboard;
impl Clicky for Keyboard{
fn click(&self){
println!("Keyboard Clicked");
}
}
let kb = Keyboard;
let kb: &dyn Clicky = &Keyboard; // Reference(&) to Struct implementing Trait object (dyn Clicky) pointing to an instance of a struct (Keyboard) that implements the Clicky trait.
// or
let kb_obj: &dyn Clicky = &kb; // Reference to Trait Object
// or
let kb: Box<dyn Clicky> = Box::new(Keyboard); // Boxed Trait Object
// `Box::new` function is used to allocate memory on the heap for the Keyboard struct and to create the trait object.
Using trait objects with functions
Borrow
fn borrow_clicky(obj : &dyn Clicky){
obj.click();
}
let kb = Keyboard;
borrow_clicky(&kb);
To move trait objects we use Box
fn move_clicky(obj : Box<dyn Clicky>){
obj.click();
}
let kb = Box::new(Keyboard);
move_clicky(kb);
Heterogenous Vector
struct Mouse;
impl Clicky for Mouse {
fn click(&self) {
println!("Mouse Clicked");
}
}
fn make_clicks(clickeys: Vec<Box<dyn Clicky>>){
for clicker in clickeys{
clicker.click();
}
}
// one way to create a vector of trait objects
// let kb = Box<dyn Clicky> = Box::new(Keyboard);
// let mouse = Box<dyn Clicky> = Box::new(Mouse);
// let clickers = vec![kb, mouse];
let kb = Box::new(Keyboard);
let mouse = Box::new(Mouse);
let clickers: Vec<Box<dyn Clicky>> = vec![kb, mouse];
make_clicks(clickers);
Lifetimes
- All data in Rust is owned by an owner.
- The owner is responsible for cleaning up data.
- Functions, closures, structs, enums, and scopes
- Ownership can be transferred and borrowed
- Function calls, variable reassignment and closures
Lifetimes are a way to inform the compiler that the borrowed data will be valid at a specific point in time. This is important because the compiler needs to know when the borrowed data will be valid and when it will be invalid.
It is needed for storing references in structs, enums & function signatures and returning borrowed data from functions.
All data has a lifetime, most of the time it is implicit and inferred by the compiler.
struct Person<'a> {
name: &'a str,
}
In the above example, the lifetime of the reference is 'a
. The lifetime of the reference is the same as the lifetime of the struct.
'a
, 'b
, 'c
are lifetime parameters. They are used to specify the lifetime of the reference.
'static
is a resever keyword. static lifetimes lasts for the entire duration of the program. It is used for string literals.
Using in function
fn name<'a>(name: &'a Datatype) -> &'a Datatype {}
Lifetime annotations indicate that there exists some owned data that:
- “Lives at least as long” as the borrowed data
- “Outlives or outlasts” the scope of a borrow
- “Exists longer than” the scope of a borrow
Structures utilizing borrowed data must:
- Always be created after the owner was created
- Always be destroyed before the owner is destroyed
Custom Errors
Functions may fail in more than one way
Error Enumerations
- allows errors to be easily defined.
- can match on different error types.
- prefer using match wherever possible with errors.
Error Requirements
- must implement the Debug trait
- Displays error info in debug context.
- Display traits.
- Displays error info in user context.
- must implement the Error trait.
- Interoperability with code using dynamic errors.
Manual Error Creation
#[derive(Debug)]
enum LockError {
MechanicalError(i32),
NetworkError,
NotAuthorized,
}
use std::error::Error;
impl Error for LockError {} // will work with default implementation
use std::fmt;
impl fmt::Display for LockError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LockError::MechanicalError(ref err) => write!(f, "Mechanical error: {}", err),
LockError::NetworkError => write!(f, "Network error"),
LockError::NotAuthorized => write!(f, "Not authorized"),
}
}
}
Using thiserror
crate
thiserror
crate provides a procedural macro to derive the Error trait.
#[derive(Debug, Error)]
enum LockError {
#[error("Mechanical error: {0}")]
MechanicalError(i32),
#[error("Network error")]
NetworkError,
#[error("Not authorized")]
NotAuthorized,
}
fn lock_door() -> Result<(), LockError> {
// code
Err(LockError::NotAuthorized)
}
Error Conversion
use thiserror::Error;
#[derive(Debug, Error)]
enum NetworkError {
#[error("Connection timed out")]
Timeout,
#[error("Connection reset by peer")]
Reset,
#[error("Unreachable")]
Unreachable
}
enum LockError {
#[error("Mechanical error: {0}")]
MechanicalError(i32),
#[error("Network error")]
// type conversion
NetworkError(#[from] NetworkError),
#[error("Not authorized")]
NotAuthorized,
}
Never put unrelated error into a single enumeration
changes to the enumeration will cascade across the codebase
Acknowledgements
to be updated
Selection Sort
simple sorting algorithm that works by repeatedly selecting the minimum element from the unsorted portion of the array and swapping it with the first element of the unsorted portion.
#include <stdio.h>
void selectionSort(int arr[], int n) {
int i, j, min;
for (i = 0; i < n-1; i++) {
min = i; // minimum element in unsorted array
for (j = i+1; j < n; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
// swap the minimum element with the first element of the unsorted array
if (min != i) {
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
History of Programming Languages
Programming languages have undergone a remarkable evolution since the inception of computing. From the rudimentary binary code to the sophisticated high-level languages of today, each stage in this journey has contributed to making software development more accessible and efficient. Let’s delve into the history and development of programming languages.
Low Level languages
Machine Language or Binary Language
- Consists of 0s and 1s.
- Computers can only understand binary.
- Unambiguous and simple.
- Difficult for humans to read and understand.
- Not suitable for representing complex data structures.
- Machine-dependent due to differences in code architecture.
Assembly Language
- Uses mnemonics like
ADD
,SUB
,MUL
. - Operands are represented in binaries.
- Language translators (Assemblers) were built to make computers understand mnemonics.
- Assembly language is translated into machine language by an assembler.
High Level languages
- Resemble English.
- Examples: BASIC, COBOL, FORTRAN, etc.
- Can’t be understood by computers directly, hence language translators are needed.
- Compiler
- Interpreter
High-Level Languages ➡️ Character User Interface
Fourth Generation languages
- Aimed to have minimum input and maximum output.
- Examples: Visual Basic (VB), SQL, etc.
- Time is saved because of shorter input, but it takes more memory, making it less efficient than High-Level Languages.
Fifth Generation Languages
- Designed for AI.
- Examples: LISP, PROLOG.
Compiler
Interpreter
Feature | Compiler | Interpreter |
---|---|---|
Processing | Converts the entire source code into object code before execution | Translates and executes source code line by line |
Execution Speed | Compiled code runs faster | Interpreted code runs slower |
Error Handling | Displays all errors after compilation | Displays errors of each line one by one |
Memory Requirement | Requires more memory | Requires less memory |
Portability | Cannot be easily ported as it is bound to a specific target machine | Works well in web environments and can be easily ported |
Debugging | Difficult to debug as the program cannot be changed without getting back to the source code | Easier to debug as programs written in an interpreted language are easier to debug |
Endianness
Endianness is the fundamental part of how computers read and understand bytes. It is the order of bytes in a multi-byte data type.
There are two types of endianness:
- Big-endian: The most significant byte is stored first and at the lowest memory address.
- Little-endian: The least significant byte is stored at the lowest memory address.
When discussing endianness, we often refer to the byte holding the smallest position as the Least Significant Byte (LSbyte) and the bit holding the smallest position as the Least Significant Bit (LSbit). Conversely, the byte occupying the most significant position is known as the Most Significant Byte (MSbyte), and the bit holding the most significant bit position is termed the Most Significant Bit (MSbit).
Big endian stores data MSbyte first
Little endian stores data MSbyte last
Big-endian encoding is notably prevalent in network protocols, often termed as the network order. Conversely, the little-endian format is commonly found in personal computer architectures.