The content is inspired from the amazing book: C Programming - A Modern Approach by K.N. King. I have tried to add as many code examples as possible to illustrate the concepts. I have also tried giving reference tables. I provide concise bullet points to convey the ideas.
I have also tried to cover some niche syntax that is not covered in most tutorials. My belief is that after reading this summarized introduction to C, you will have a good understanding of the language and might know a thing or two that many beginners don't know.
- Section 1: Basic Program Structure, Directives, Linking, Compiling
- Section 2: Printing Strings and Format Specifiers
- Section 3: Variables
- Section 4: Operators
- Section 5: Conditional Statements
- Section 6: Loops
- Section 7: One-Dimensional Arrays
- Section 8: Two-Dimensional Arrays
- Section 9: Functions
- Section 10: Pointers
- Section 11: Pointers with Arrays
- Example Program: Interactive Tic Tac Toe
Some of this maybe tricky to understand first. This section is meant to give a first expsoure to the concepts so atleast you are aware then you will master them as you practice more.
Every C program follows a standard structure. Here's a simple example:
#include <stdio.h> // Preprocessor Directive
int main() { // "main" Function (must be named main)
printf("Hello, World!\n"); // Statement
return 0; // Return Statement
}Components:
- Preprocessor Directives: Lines starting with
#, e.g.,#include <stdio.h>- here
#inludeis a preprocessor directive stdio.his a header file. It's enclosed in <> because it's a global header file. If you want to include a header file from your project, you will enclose it in "".
- here
- Main Function: Entry point of the program,
int main(). - Statements: Instructions executed by the program, e.g.,
printf - Return Statement: Ends the
mainfunction, e.g.,return 0;
Note: printf is imported from the standard input/output library stdio.h. It's used to print text to the console.
Directives instruct the compiler to perform specific actions before actual compilation.
-
#include- Purpose: Includes header files.
- Example:
#include <stdio.h>
-
#define- Purpose: Defines macros or constants.
- Example:
#define PI 3.14
Note: Macros are fragments of code defined with
#definethat are replaced by their values or expressions before compilation. We will cover these later. -
#ifdef,#ifndef,#endif-
Purpose: Conditional compilation, commonly used for header guards.
-
Example:
#ifndef MYHEADER_H #define MYHEADER_H // code #endif
Purpose:
- Prevents multiple inclusion of the same header file
- Without guards, if file A.h includes B.h, and C.h also includes B.h, then B.h would be included twice
- Multiple inclusions could cause compilation errors (duplicate definitions)
How it works:
-
The
#ifndef MYHEADER_Hline:- Checks if
MYHEADER_His NOT defined
- Checks if
-
The
#define MYHEADER_Hline:- Defines the macro
MYHEADER_H - Only executes if the check above passed
- Defines the macro
-
The content between guards
-
The
#endifline ends the guard
-
Header files contain declarations of functions and macros to be shared between several source files.
-
Standard Headers: Provided by the standard library.
- Examples:
stdio.h,stdlib.h,math.h
- Examples:
-
Custom Headers: Created by the programmer.
- Example:
myheader.h
- Example:
Including Headers:
- Angle Brackets (
< >): For standard headers.#include <stdio.h>
- Quotes (
" "): For custom headers.#include "myheader.h"
Compilation Steps:
- Preprocessing: Handles directives like
#includeand#define. - Compilation: Converts preprocessed code to assembly.
- Assembly: Translates assembly code to machine code, generating an object file (
.oor.obj). - Linking: Combines object files and libraries to create an executable.
Commands:
-
Compile to Object File:
gcc -c program.c -o program.o
-
Link Object Files to Executable:
gcc program.o -o program.exe
-
TODO: Add later
| Aspect | Object File (.o / .obj) |
Executable File (.exe / no extension) |
|---|---|---|
| Contains | Machine code, but not fully linked | Fully linked machine code ready to run |
| Usage | Intermediate step in compilation | Final output that can be executed |
The main function is the entry point of every C program.
Structure:
int main() {
// Code
return 0;
}Return Value:
int: Indicates the program's return value (exit status).0: Success- Non-zero: Error codes
Variants:
- With Parameters:
int main(int argc, char *argv[]) { // argc: Argument count // argv: Argument vector (array of strings) return 0; }
- Using
void: (Less common)Note: Usingvoid main() { // Code }
int main()is standard and recommended.
printfFunction:- Purpose: Outputs formatted text to the console.
Note:
printfstands for "print formatted". You must include the<stdio.h>header file to use it since it comes from the standard input/output library.- Basic Syntax:
printf("format string", arguments);
- Basic Example:
#include <stdio.h> int main() { string name = "Alice"; printf("Hello, %s!\n", name); return 0; }
- Output:
Hello, Alice!
Note: The
printffunction can take multiple arguments and format specifiers covered below.
- String Literals:
- Enclosed in double quotes (
" "). - Include escape sequences like
\nfor newline.
- Enclosed in double quotes (
- Purpose: Define what kind of data is being printed and how it should be formatted.
- List of some Specifiers:
| Specifier | Description | Example Usage | Output |
|---|---|---|---|
%s |
String | printf("%s", "John"); |
"John" |
%d |
Signed decimal integer | printf("%d %d", 42, -42); |
"42 -42" |
%u |
Unsigned decimal integer | printf("%u %u", 42, -42); |
"42 4294967254" |
%f |
Floating-point number | printf("%.2f", 3.14159); |
"3.14" |
%c |
Single character | printf("%c", 'A'); |
"A" |
%p |
Pointer address | printf("%p", ptr); |
"0x7fff5fbff7e8" |
%x |
Hexadecimal (lowercase) | printf("%x", 255); |
"ff" |
%X |
Hexadecimal (uppercase) | printf("%X", 255); |
"FF" |
%% |
Literal % character | printf("Score: 100%%"); |
"Score: 100%" |
%b |
Binary (C23, lowercase) | printf("%b", 26); |
"11010" |
%e |
Scientific notation (lowercase) | printf("%e", 123.456); |
"1.234560e+02" |
%g |
Shorter of %e or %f | printf("%g", 123.456); |
"123.456" |
%i |
Signed decimal integer (auto-detects base) | printf("%i %i %i", 100, 0x64, 0144); |
"100 100 100" |
%o |
Octal | printf("%o", 64); |
"100" |
%lf |
Double | printf("%lf", 3.14159); |
"3.141590" |
%Lf |
Long double | printf("%Lf", 3.14159L); |
"3.141590" |
Some notable features about these specifiers:
- When given -42,
%uinterprets it as an unsigned value (4294967254 on a 32-bit system), while %d shows it as -42. %iautomatically detects the base of the number (decimal, hexadecimal, or octal).- Format specifiers can include width and precision modifiers (e.g.,
%.2ffor 2 decimal places) - Length modifiers like
l(long),ll(long long),h(short) can be added to many specifiers - The
%gspecifier automatically chooses between%eand%fbased on the value's magnitude - For floating-point values, uppercase specifiers (
%E,%G) will output uppercaseEin scientific notation
- Purpose: Modify the output format of
printfspecifiers. - Flags and Their Effects:
| Flag | Description | Example Usage | Output |
|---|---|---|---|
+ |
Force sign for numbers | printf("%+d", 42); |
"+42" |
0 |
Pad with leading zeros | printf("%05d", 123); |
"00123" |
# |
Alternate form | printf("%#.3f", 23.2); |
"23.200" |
. |
Precision control | printf("%.2f", 3.14159); |
"3.14" |
- |
Left-justify within field width | printf("%-10s World!", "Hello"); |
"Hello World!" |
Note that:
The # flag adds prefixes to make number format explicit:
- For hex: adds "0x" prefix
- For octal: adds "0" prefix
- For floating point: forces decimal point
// Without # flag
printf("%x", 255); // Output: ff
printf("%o", 64); // Output: 100
// With # flag
printf("%#x", 255); // Output: 0xff
printf("%#o", 64); // Output: 0100In C programming, variables are used to store data that your program can manipulate. Proper declaration and initialization of variables are crucial to ensure your program behaves as expected.
-
Declaration:
- Syntax:
<type> <variable_name>; - Purpose: Tells the compiler to allocate memory for the variable. for example, an int variable takes 4 bytes of memory, char takes 1 and so on.
- Example:
int age; // multiple declarations int a, b, c; float price, discount, total; bool isValid, isDone;
- Syntax:
-
Initialization:
- Syntax:
<type> <variable_name> = <value>; - Purpose: Assigns an initial value to the variable at the time of declaration.
- Example:
int age = 25; float price = 19.99f; // add an f to indicate float for the compiler char grade = 'A'; // multiple initializations int a = 10, b = 20, c = 30;
- Syntax:
Why Initialization Matters:
-
Undefined Values:
- Declaring a variable without initializing it leaves the variable with an undefined value.
- Accessing or using an uninitialized variable can lead to unpredictable behavior or bugs.
-
Memory Perspective:
- When a variable is declared, the compiler reserves a specific amount of memory based on its type.
- Without initialization, the memory location contains garbage data (undefined values from memory).
Warning
⚠️ : C will still execute your code even if you don't initialize variables, the result of that will be unpredictable and can lead to bugs that are hard to find. See the example below -
Best Practices:
- Always initialize variables with a value before use.
- Declare variables at the beginning of a block to enhance code readability and maintainability.
Example showcasing Bad Initialization Practice:
Imagine you forget to initialize a value to age in the following example, then the if condition will use whatever garbage/unexpected value is stored in the memory location reserved for age and will result in unpredictable behavior (sometimes the if condition will be true, sometimes false).
#include <stdio.h>
int main() {
int age; // Uninitialized variable ⚠️bad practice⚠️
int bonus = 1000; // Initialized variable
// Let's pretend we forgot to assign a value to age
if (age > 0) { // Using uninitialized variable!
bonus = bonus * 2; //double the bonus
}
printf("Employee bonus: $%d\n", bonus); // Output: Employee bonus: $1000 or $2000 or error
}A data type specifies the type of data that a variable can hold.
In C there are 2 kinds of numeric types:
signedandunsigned. Signed integers can represent both positive and negative numbers, using one bit to indicate the sign (positive or negative). Unsigned integers represent only non-negative numbers, allowing a larger range of positive values since all bits are used for the magnitude.
| Data Type | Description | Example Declaration | Memory Size (Typical) | Value Range |
|---|---|---|---|---|
int |
integers (whole numbers) | int count = 10, temp = -2; |
4 bytes | -2,147,483,648 to 2,147,483,647 |
float |
floating-point numbers (approx. 6-7 decimal digits of precision) | float temperature = 23.5f; |
4 bytes | ±1.2E-38 to ±3.4E+38 |
double |
floating-point numbers (approx. 15 decimal digits of precision) | double price = 19.99; |
8 bytes | ±2.3E-308 to ±1.7E+308 |
char |
single characters | char grade = 'A'; |
1 byte | 0 to 255 (or -128 to 127, signed) |
unsigned int |
non-negative integers | unsigned int score = 100; |
4 bytes | 0 to 4,294,967,295 |
long |
larger integers | long distance = 123456L; |
8 bytes | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
short |
smaller integers | short age = 25; |
2 bytes | -32,768 to 32,767 |
bool |
boolean values (true or false) |
bool isValid = true; |
1 byte | true or false |
Note: To use the
booltype, include the header<stdbool.h>
Tip: You can use
sizeof(<variable_name or constant name>)to get the size of a variable in bytes. For example,printf("%u", sizeof(23431241)"will return 4 because an integer takes 4 bytes of memory.
Let's explore how different variable types can be declared, initialized, and used in simple arithmetic operations and printed using printf.
#include <stdio.h>
#include <stdbool.h> // Required for bool type
int main() {
int apples = 10;
float pricePerApple = 0.50f; // add an f to indicate float for the compiler
double totalCost = -1;
char grade = 'A';
unsigned int stock = 500; // we may use unsigned int because stock can't be negative, this is just a design choice however
long population = 7800000000L;
short temperature_celsius = 25; // be clear about the unit of your data when possible
bool isOpen = true;
// Simple Arithmetic
totalCost = apples * pricePerApple;
// Printing Variables
printf("Apples: %d\n", apples); // use the %d format specifier for int
printf("Price per Apple: $%.2f\n", pricePerApple); // price should be displayed with 2 decimal places so use %.2f
printf("Total Cost: $%.2f\n", totalCost); // total cost can be a large number so use the 'l' flag
printf("Grade: %c\n", grade);
printf("Stock Available: %u units\n", stock); // use format specifier %u for unsigned int
printf("World Population: %ld\n", population); // population is a large number so use 'l' flag with %d
printf("Temperature: %+hd°C\n", temperature_celsius); // use %hd for short int and + flag to show positive temperature as it matters
printf("Store Open: %s\n", isOpen ? "Yes" : "No"); // ternary operator ==> if isOpen is true print 'Yes' else print 'No'
return 0;
}Output:
Apples: 10
Price per Apple: $0.50
Total Cost: $5.00
Grade: A
Stock Available: 500 units
World Population: 7800000000
Temperature: +25°C
Store Open: Yes
Note: for
totalCostwe intialized a sentinal value of -1 to indicate that the value is not yet calculated. If execution reaches the print statement without updating the -1 value, it will be clear that the value is not calculated. We won't see some random garbage value and be confused. You must decide what a sentinal value should be based on your program requirements.
Type Conversion is the process of converting a value from one data type to another. This can happen implicitly (automatically by the compiler) or explicitly (manually by the programmer).
Implicit type conversion, also known as type coercion, occurs automatically when the compiler converts a narrower data type to a broader data type to allow operations between different types. This is done to prevent data loss and ensure compatibility. For example:
-
int to float: When an integer (narrows) is used in an expression with a float (broader), the integer is automatically converted to a float.
int a = 5; float b = 2.5; float result = a + b; // 'a' is implicitly converted to float so result is now float not int
-
short to int: When a
short(narrows) is used in an expression with anint(narrows), theshortis promoted to anint.short x = 10; int y = 20; int result = x + y; // 'x' is implicitly converted to int
Explicit type conversion, also known as type casting, is when the programmer manually converts a value from one type to another. This is done using the cast operator (type). For example:
-
float to int: Converting a float to an integer truncates the decimal part.
float a = 3.14; int b = (int)a; // Explicitly convert 'a' to int
-
int to char: Converting an integer to a char truncates the value to fit within the range of a char.
int a = 65; char b = (char)a; // Explicitly convert 'a' to char
-
unsigned to signed: Converting an unsigned integer to a signed integer can lead to unexpected results if the value is too large.
unsigned int x = 4294967295; // Maximum value for unsigned int
int y = (int)x; // y will be -1 due to overflow
printf("%d", y); // Output: -1Explicit type conversion gives the programmer control over how data is interpreted, but it should be used carefully to avoid data loss or unexpected behavior.
The typedef keyword in C is used to create aliases for existing data types. This can make code more readable and easier to maintain, especially when dealing with complex data types or when the underlying type might change in the future.
Consider the following example where we define a new type Dollars as an alias for float:
typedef float Dollars;
Dollars buy_cost, sell_cost;In this example, Dollars is treated as a synonym for float by the compiler. This means that buy_cost and sell_cost are essentially float variables, but using the Dollars type makes the code more intuitive and self-documenting.
- Improved Readability: By using meaningful type names like
Dollars, the code becomes easier to understand. - Ease of Maintenance: If you later decide that all dollar-related variables should be of type
doubleinstead offloat, you only need to change thetypedefdefinition:This change will automatically update all variables of typetypedef double Dollars;
Dollarsthroughout your code, reducing the risk of errors and saving time.
✅ We will study
structandenumsoon, typedef is often used with them in practice
Operators allow you to perform operations on variables and values. Understanding some non-obvious behaviors and best practices can help you write more bug free code.
- Arithmetic Operators: Perform mathematical calculations.
- Relational Operators: Compare two values.
- Logical Operators: Combine multiple conditions.
- Assignment Operators: Assign values to variables.
- Increment and Decrement Operators: Increase or decrease a variable's value.
Arithmetic operators are used to perform mathematical operations such as addition, subtraction, multiplication, division, and modulus.
-
Common Arithmetic Operators:
Operator Description Example +Addition a + b-Subtraction a - b*Multiplication a * b/Division a / b%Modulus (remainder) a % b -
Important Notes:
-
Mixing
intandfloat:- When performing operations between
intandfloat, theintis implicitly converted tofloat, and the result is afloat. - Example:
int i = 70.99f; // i will store 70 (fractional part truncated) float j = 132; // j will store 132.0
- When performing operations between
-
Division Operator
/:- Integer Division: If both operands are integers, the division truncates the fractional part.
int result = 1 / 2; // result is 0 not 0.5
- Floating-Point Division: If at least one operand is a
float, the division retains the fractional part.float result = 1 / 2.0f; // result is 0.5
- Integer Division: If both operands are integers, the division truncates the fractional part.
-
Modulus Operator
%:- Usage: Only applicable to integer operands.
- Compilation Error: Using
%with non-integer types results in a compilation error.int remainder = 5 % 2; // valid, remainder is 1 float invalid = 5.0f % 2; // error, since % is not defined for float
-
Division or Modulus by Zero:
- Undefined Behavior: Dividing or taking modulus by zero leads to undefined behavior.
- Modern Compilers: Often throw a floating-point exception.
int division = 10 / 0; // Undefined, runtime error
-
Operator Precedence:
- Determines the order in which operations are performed.
- Example: Multiplication and division have higher precedence than addition and subtraction.
int result = 3 + 4 * 2; // result is 11, not 14 because it's equivalent to 3 + (4 * 2)
- Best Practice: Use parentheses to explicitly define the desired order.
int result = (3 + 4) * 2; // result is 14
-
The assignment operator (=) assigns the value on its right to the variable on its left. However, assignments can have side effects, especially when used within expressions.
-
Example of Side Effects:
int j; int i = 1; int k = 1 + (j = i); printf("%d %d %d\n", i, j, k); // Outputs: 1 1 2
Explanation:
- The expression
(j = i)assigns the value ofitojand returns the assigned value (1). - Therefore,
kbecomes1 + 1, which is2.
- The expression
-
Why It's Bad Practice:
- Readability: Embedded assignments can make the code harder to read and understand.
-
Lvalue:
- Assignment Operator in C requires an Lvalue on the left. Lvalue refers to an object that occupies identifiable memory locations so they can not be a constant or expression.
- Example:
int a = 5; // 'a' is an lvalue 5 = a; // Error: 5 is not an lvalue
-
Compound Assignment Operators:
- Combine an arithmetic operation with assignment.
- Common Operators:
+=,-=,*=,/=,%=
- Example:
int count = 10; count += 5; // Equates to count = count + 5; // count is now 15
-
Increment (
++) and Decrement (--) Operators:-
Purpose: Increase or decrease a variable's value by one.
-
Types:
- Prefix:
++variableor--variable- Increments/decrements the value before it's used in the expression.
- Example:
int a = 5; int b = ++a; // a becomes 6, then b is assigned 6
- Postfix:
variable++orvariable--- Increments/decrements the value after it's used in the expression.
- Example:
int a = 5; int b = a++; // b is assigned 5, then a becomes 6
- Prefix:
-
Side Effect:
- Prefix: Useful when the updated value is needed immediately.
- Postfix: Useful when the original value is needed before updating.
-
Example Showcasing Difference:
#include <stdio.h> int main() { int x = 5; printf("Prefix: %d\n", ++x); // Outputs: 6 because x is incremented BEFORE use x = 5; printf("Postfix: %d\n", x++); // Outputs: 5 because x is incremented AFTER use printf("After Postfix: %d\n", x); // Outputs: 6 return 0; }
-
-
Relational Operators: Compare two values and determine the relationship between them.
Operator Description Example >Greater than a > b<Less than a < b>=Greater than or equal to a >= b<=Less than or equal to a <= b -
Equality Operators: Check if values are equal or not.
Operator Description Example ==Equal to a == b!=Not equal to a != b -
Logical Operators: Combine multiple conditions.
Operator Description Example &&Logical AND (both conditions must be true) a > 0 && b > 0||Logical OR (at least one condition true) a > 0 || b > 0!Logical NOT (inverts the condition) !a -
Short-Circuit Evaluation:
- Logical operators evaluate expressions from left to right and stop as soon as the result is determined.
- Example:
int a = 0; if (a != 0 && (10 / a) > 1) { // This block will not execute, and (10 / a) is not evaluated, // preventing a division by zero error. }
Conditional statements control the flow of a program based on certain conditions. In C, the primary conditional statements are if, else, the ternary operator, and switch.
-
ifStatement- Purpose: Execute code block if a condition is true.
- Syntax:
if (condition) { // code to execute if condition is true }
- Example: Check Voting Eligibility
int age = 20; if (age >= 18) { printf("Eligible to vote.\n"); }
-
if-elseStatement- Purpose: Execute one block if condition is true, otherwise execute the "else" block.
- Syntax:
if (condition) { // code if true } else { // code if false }
- Example: Voting Eligibility with Else
int age = 16; if (age >= 18) { printf("Eligible to vote.\n"); } else { printf("Not eligible to vote.\n"); }
-
Issue: In nested
ifstatements without braces,elsebinds to the nearestif, which might not be the intended one. -
C Rule:
elsealways associates with the closest precedingifnot already associated. -
Example of Dangling Else:
int x = 2; if (x > 0) if (x > 5) printf("x is greater than 5\n"); else printf("x is non-positive\n"); // Else binds to inner if
Output: x is non-positive- Misinterpretation: Indentation suggests
elsebelongs to the outerif, but it binds to the innerif. So even tho x = 2 which is greater than 0 we still end up printing x is non-positive because the inner if condition returned false and the associated else block was executed.
- Misinterpretation: Indentation suggests
-
Solution: Use braces to clearly define intended blocks.
int x = 2; if (x > 0) { if (x > 5) { printf("x is greater than 5\n"); } } else { printf("x is non-positive\n"); // Else now binds to outer if }
Output:In this case nothing will be outputted beacuse the else block is associated with outer if which executed successfully. Meanwhile, the inner if condition returned false and had no else block associated with it so nothing was printed.
As you can see above, nested if statements with lots of braces can be hard to read and follow. The switch statement is a better alternative in such cases.
- Purpose: Choose between multiple possible execution paths based on a single variable's value.
- Syntax:
switch (expression) { case value1: // code block break; case value2: // code block break; // ... default: // default code block }
- Advantages: Preferred over cascaded
if-elsefor multiple discrete cases. break: Exits theswitchstatement. (see more below)
Warning
⚠️ Missingbreakleads to execution of subsequent cases unintentionally.
-
Example: Simple Calculator Operation
char operator = '+'; int num1 = 8, num2 = 5, result; switch (operator) { case '+': result = num1 + num2; break; case '-': result = num1 - num2; break; case '*': result = num1 * num2; break; case '/': result = num1 / num2; break; default: printf("Invalid operator.\n"); } printf("Result: %d\n", result);
Output: Result: 13 -
switchStatement Components
| Component | Description |
|---|---|
switch(expression) |
Evaluates the expression once and compares it with each case. |
case value: |
Defines a block of code to execute if expression matches value. |
break; |
Exits the switch after executing a case. Prevents fall-through. |
default: |
Optional. Executes if no case matches the expression. |
-
Short-hand for simple
if-elsestatements. -
Syntax:
condition ? expression_if_true : expression_if_false;
-
Use Case: Assign value based on a condition.
-
Example: Determine Max of Two Numbers
int a = 5, b = 10; printf("Maximum is %d\n", (a > b) ? a : b);
Output: Maximum is 10 -
Usecase Example 2: Assign discount based on customer type.
char customerType = 'R'; // 'R' for regular, 'P' for premium float discount = (customerType == 'R') ? 0.05 : 0.10; // give 5% discount for regular customers and 10% for premium printf("Discount: %.2f%%\n", discount * 100);
Output: Discount: 5.00%
Loops allow you to execute a block of code multiple times based on a condition. In C, the primary loop constructs are while, for, and loop control statements like break and continue.
Note: We omit the
do-whileloop since it's less common andgotodue to the criticisms and potential misuse.
-
Execution Order:
- Condition Check: Evaluates the loop condition before each iteration. (Note: If the condition is false before the loop executes, the loop body will not execute even once).
- Code Execution: Executes the loop body if the condition is true.
- Condition Check: Check condition again after each iteration.
-
Note:
do-whileloops exist but are rarely used so we will focus onwhileandforloops.
Syntax
while (condition) {
// code to execute
}Common Pitfalls
-
Infinite Loops:
- Occur when the loop condition never becomes false.
- Example:
while (1) { printf("This loop runs forever.\n"); }
- Expected Output:
This loop runs forever. This loop runs forever. This loop runs forever. ...
-
Intentional Infinite Loops:
-
Used in scenarios like event-driven programs or servers that run continuously until manually stopped.
-
Exiting the Loop: Utilize
breakstatements within conditional blocks to exit. -
Example:
#include <stdio.h> int main() { int number; while (1) { printf("Enter a positive number (negative to exit): "); scanf("%d", &number); if (number < 0) { printf("Exiting loop.\n"); break; } printf("You entered: %d\n", number); } return 0; }
-
Expected Output:
Enter a positive number (negative to exit): 10 You entered: 10 Enter a positive number (negative to exit): 5 You entered: 5 Enter a positive number (negative to exit): -1 Exiting loop.
-
Example: Simple Calculator
Description: Continuously takes user input for two numbers and an operator, performs the calculation, and displays the result. The loop continues until the user decides to exit.
#include <stdio.h>
#include <stdbool.h>
int main() {
char operator = '\0';
double num1 = 0.0, num2 = 0.0;
bool continueCalc = true;
while (continueCalc) {
printf("Enter operator (+, -, *, /) or 'q' to quit: ");
scanf(" %c", &operator); // pass a space before %c to consume the newline character \n from the previous input
if (operator == 'q') {
break; // exit the loop if 'q' is entered
}
printf("Enter two operands: ");
scanf("%lf %lf", &num1, &num2);
switch (operator) {
case '+':
printf("Result: %.2lf + %.2lf = %.2lf\n", num1, num2, num1 + num2);
break;
case '-':
printf("Result: %.2lf - %.2lf = %.2lf\n", num1, num2, num1 - num2);
break;
case '*':
printf("Result: %.2lf * %.2lf = %.2lf\n", num1, num2, num1 * num2);
break;
case '/':
if (num2 != 0) // if-else inside the switch can be used to prevent division by zero
printf("Result: %.2lf / %.2lf = %.2lf\n", num1, num2, num1 / num2);
else
printf("Error: Division by zero.\n");
break;
default:
printf("'%c' is an Invalid operator\n", operator);
}
}
printf("Calculator exited.\n");
return 0;
}Note: We needed to add a space before %c in scanf(" %c", &operator); because the newline character \n is inserted automatically when the user presses Enter after entering the operands. Without the space, scanf will read the newline character as the operator in the next iteration and we will hit the default case of the switch statement.
Also Note that we didn't need a space in scanf("%lf %lf", &num1, &num2); because the %f specifier automatically skips whitespace characters until it finds its target data type.
Expected Output:
Enter operator (+, -, *, /) or 'q' to quit: +
Enter two operands: 5 3
Result: 5.00 + 3.00 = 8.00
Enter operator (+, -, *, /) or 'q' to quit: /
Enter two operands: 10 2
Result: 10.00 / 2.00 = 5.00
Enter operator (+, -, *, /) or 'q' to quit: q
Calculator exited.
- Ideal for scenarios involving a known number of iterations, such as iterating through arrays or counting occurrences.
Syntax
for (initialization; condition; increment) {
// code to execute
}C-specific Considerations
- Before C99: Loop counter must be declared before the loop.
int i; for (i = 0; i < 10; i++) { // code }
- C99 and Later: Allows declaring the loop counter within the loop.
for (int i = 0; i < 10; i++) { // code }
- Scope: Variables declared within the
forloop are not accessible outside the loop. So if you need to use the loop counter outside the loop, declare it before the loop like the old way.
Workflow
- Initialization: Sets the starting point (e.g.,
int i = 0). - Condition Check: Evaluates the loop condition before each iteration (e.g.,
i < 10). - Loop Body Execution: Executes the code block if the condition is true.
- Increment/Decrement: Updates the loop counter (e.g.,
i++).
Example
Print numbers from 1 to 5.
#include <stdio.h>
int main() {
for (int i = 1; i <= 5; i++) {
printf("Number: %d\n", i);
}
return 0;
}Expected Output:
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5
Break Statement
- Immediately exits the innermost enclosing loop (
while,for) orswitchstatement. - Only exits the nearest loop, not all enclosing loops.
Example: Exit Loop When Number is Found
#include <stdio.h>
int main() {
int numbers[] = {3, 7, 2, 9, 5};
int target = 9;
int found = 0;
for (int i = 0; i < 5; i++) {
if (numbers[i] == target) {
printf("Number %d found at index %d.\n", target, i);
found = 1;
break;
}
}
if (!found) {
printf("Number %d not found.\n", target);
}
return 0;
}Expected Output:
Number 9 found at index 3.
Note that if break is used in a nested loop, only the inner loop is exited. The outer loop will continue executing.
- Skips the remaining code in the current loop iteration and proceeds to the next iteration.
- Effect in Nested Loops: Only affects the current loop level.
Example: Skip Even Numbers
#include <stdio.h>
int main() {
printf("Odd numbers between 1 and 5:\n");
for (int i = 1; i <= 5; i++) {
if (i % 2 == 0) {
continue; // Skip printing even numbers
}
printf("%d\n", i);
}
return 0;
}Expected Output:
Odd numbers between 1 and 5:
1
3
5
Contrast with Break Statement:
#include <stdio.h>
int main() {
printf("Numbers until 3:\n");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
break; // Exit loop when i is 3
}
printf("%d\n", i);
}
return 0;
}Expected Output:
Numbers until 3:
1
2
As you can see, the loop stops when i is 3 and does not print 4 and 5.
Arrays allow you to store multiple values of the same type in a single, contiguous block of memory.
-
Syntax:
type arrayName[arraySize];
-
Components:
type: Data type of the array elements (e.g.,int,char,float).arrayName: Identifier for the array.arraySize: Fixed integer constant representing the number of elements.
-
Best Practice: Define a macro for array size to enhance readability
#define ARRAY_SIZE 10 int numbers[ARRAY_SIZE];
-
Syntax:
type arrayName[arraySize] = {value1, value2, ..., valueN};
-
Rules:
- If fewer initializers are provided than the array size, the remaining elements are automatically initialized to zero.
- Example: Initializing all elements to zero or using boolean values.
-
Examples:
int arr[5] = {1, 2, 3}; // arr = {1, 2, 3, 0, 0} int zeros[10] = {0}; // All elements set to 0 int bools[10] = {false}; // All elements set to 0 (false) char letters[4] = {'a', 'b', 'c'}; // letters = {'a', 'b', 'c', '\0'}
Description: Track the grades of 5 students. Initialize the array with some grades and set the rest to zero.
#include <stdio.h>
#define NUM_STUDENTS 5
int main() {
int grades[NUM_STUDENTS] = {85, 90}; // Initialize first two grades
printf("Student Grades:\n");
for (int i = 0; i < NUM_STUDENTS; i++) {
printf("Student %d: %d\n", i + 1, grades[i]);
}
return 0;
}Expected Output:
Student Grades:
Student 1: 85
Student 2: 90
Student 3: 0
Student 4: 0
Student 5: 0
- Syntax:
arrayName[index]
- Index Range:
0toarraySize - 1 - Example:
int numbers[5] = {10, 20, 30, 40, 50}; printf("First number: %d\n", numbers[0]); // Outputs 10 printf("Last number: %d\n", numbers[4]); // Outputs 50
- Understand that the bracket notation for accessing an array element is syntactic sugar. Accessing
array[i]is equivalent to dereferencing at index 0 + desired index.*(array + i)==> in this example,arrayis the memory address of the first element.
Below is an example to illustrate 3 different ways to access the same element in an array.
- Dereferencing Example:
int arr[3] = {5, 10, 15}; //syntax: using name which by default points to first element printf("%d \n", *(arr + 1)); //syntax: using explicit pointer int *ptr = &arr[0]; //explicitely define pointer to first element printf("%d\n", *(ptr + 1)); // add pointer + 1 and dereference //syntax: syntactic sugar bracket notation provided by C printf("%d\n", arr[1]);
Record temperature for 7 days and then find the average temperature.
#include <stdio.h>
#define NUM_DAYS 7
int main() {
float temps[NUM_DAYS] = {0.0} // Initialize all elements to 0
//take input for each day
for (int i = 0; i < NUM_DAYS; i++) {
printf("Enter temperature for day %d: ", i + 1); // remember i starts from 0 but day starts from 1 thus i + 1
scanf("%f", &temps[i]); // to store the input into the array, we must pass the address of the element using &temps[i]
}
//average calculation
float sum = 0.0;
for (int i = 0; i < NUM_DAYS; i++) {
sum += temps[i];
}
float average = sum / NUM_DAYS;
printf("Average Temperature: %.2f°C\n", average); // remember to use %.2f to display 2 decimal places
}Expected Output:
Enter temperature for day 1: 25
Enter temperature for day 2: 30
Enter temperature for day 3: 45
Enter temperature for day 4: 22
Enter temperature for day 5: 28
Enter temperature for day 6: 33
Enter temperature for day 7: 10
Average Temperature: 27.57°C
Can be used to find the length of an array at compile time. Remember sizeof() returns the size in bytes so to get the number of elements in an array, divide the total size of the array by the size of a single element.
- Formula:
int length = sizeof(array) / sizeof(array[0]);
#include <stdio.h>
int main() {
int arr[10] = {0};
int length = sizeof(arr) / sizeof(arr[0]);
printf("Array length: %d\n", length);
return 0;
}Expected Output:
Array length: 10
| Data Type | Example Array | sizeof(array) |
sizeof(array[0]) |
Calculated Length |
|---|---|---|---|---|
int |
int nums[5]; |
20 bytes | 4 bytes | 5 |
char |
char letters[10]; |
10 bytes | 1 byte | 10 |
float |
float temps[3]; |
12 bytes | 4 bytes | 3 |
- Buffer Overflows: Writing beyond the allocated memory can corrupt data, cause crashes, or create security vulnerabilities.
- Memory Exploits: Attackers can exploit out-of-bounds access to execute arbitrary code.
- Always validate indices to ensure they are within the valid range (
0toarraySize - 1). - Example: Preventing invalid user input from accessing array elements.
#include <stdio.h>
#define SIZE 5
int main() {
int numbers[SIZE] = {10, 20, 30, 40, 50};
int index;
printf("Enter an index (0-4): ");
scanf("%d", &index);
if (index >= 0 && index < SIZE) { // Check if index is within bounds
printf("Value at index %d: %d\n", index, numbers[index]);
} else {
printf("Error: Index out of bounds.\n");
}
return 0;
}Expected Output:
Enter an index (0-4): 2
Value at index 2: 30
Or, if out of bounds:
Enter an index (0-4): 5
Error: Index out of bounds.
Note: Remember if you access out of bounds memory, C will not stop you and output whatever's is at that memory location.
There are many usecases of 2D arrays. We will briefly introduce the structure of 2D arrays and how to access elements.
A 2D array is declared with the syntax:
data_type array_name[rows][columns];Initialization can be done in several ways:
// Fully specified initialization
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
// Partial initialization (remaining elements set to 0)
int grid2[][3] = {{7}, {8, 9}}; // Row count inferred as 2Note: The column size must be specified, while the row size can be inferred during initialization.
Elements are accessed using row and column indices:
int value = grid[1][2]; // Accesses 2nd row, 3rd column (value = 6)Use nested loops to traverse all elements:
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", grid[i][j]);
}
printf("\n");
}#include <stdio.h>
#define ROWS 3
#define COLS 3
int main() {
// Initialize a 3x3 matrix representing a Tic-Tac-Toe board
char board[ROWS][COLS] = {
{'X', 'O', 'X'},
{'O', 'X', 'O'},
{' ', ' ', 'X'}
};
printf("Tic-Tac-Toe Board:\n");
// Loop through each row
for (int i = 0; i < ROWS; i++) {
// Loop through each column in the current row
for (int j = 0; j < COLS; j++) {
printf(" %c ", board[i][j]); // Print the cell value
// Separator between columns except for the last column
if (j < COLS - 1) {
printf("|");
}
}
printf("\n"); // Newline after each row
// Print a horizontal line after each row except the last
if (i < ROWS - 1) {
printf("---|---|---\n");
}
}
// Example of accessing and modifying an element
// Suppose you detected that Player marks 'O' at position (row = 2, column = 0)
board[2][0] = 'O';
printf("\nAfter Player 'O' makes a move at (3,1):\n");
// Display the updated board (in section 9 we will learn functions to avoid repetition)
printf("Tic-Tac-Toe Board:\n");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf(" %c ", board[i][j]);
if (j < COLS - 1) {
printf("|");
}
}
printf("\n");
if (i < ROWS - 1) {
printf("---|---|---\n");
}
}
return 0;
}Expected Output:
Tic-Tac-Toe Board:
X | O | X
---|---|---
O | X | O
---|---|---
| | X
After Player 'O' makes a move at (3,1):
Tic-Tac-Toe Board:
X | O | X
---|---|---
O | X | O
---|---|---
O | | X
Functions are the building blocks of C programs. They allow you to break your code into reusable, modular pieces.
We have been using this function since the beginning of this tutorial.
main()is a special function in C. It is the entry point of every C program.- When you execute a C program, the operating system calls the
main()function to start the program. - It must have the exact name
mainand a specific return type (int).
How does main() work?
main()resides in the program's stack memory when executed.- It can take two optional parameters:
argc(argument count) andargv(argument vector), which are used for command-line arguments. Though often not written explicitly. - It returns an integer value to the operating system, indicating the program's status.
0indicates successful execution, while a non-zero value indicates an error.
-
Syntax of a Function:
return_type function_name(parameter1_type parameter1, parameter2_type parameter2, ...) { // declarations // statements return value; // mandatory for non-void functions }
-
Key Points:
- Return Type: Specifies the type of value the function returns. Use
voidif the function doesn’t return anything. - Function Name: Must be unique and follow C’s naming rules.
- Parameters: Optional. Specify the data type and name of each parameter.
- Function Body: Contains the code to be executed.
- Return Type: Specifies the type of value the function returns. Use
Note: Functions cannot return arrays directly but can return pointers to arrays declared outside their scope.
-
How to Call a Function?
- Use the function name followed by parentheses
()and pass arguments if required. - Example:
int sum = add(10, 20);
- Use the function name followed by parentheses
-
Ignoring Return Values:
- You can call a function and ignore its return value.
- Example:
add(5, 10);(The result is not stored or used.)
-
What are Function Prototypes?
- A function prototype is a declaration of a function that specifies its name, return type, and parameters.
- It allows you to declare the function (without defining it) before the function is called. You can then define the function after it is called.
-
Why Use Prototypes?
- They are essential in header. files which we will cover later.
- They help the compiler understand the function’s signature before it is used. Otherwise, the compiler does an implicit declaration however if the function later has an issue, the compiler will throw an error which can be hard to debug due to the flow of the program.
-
Example:
#include <stdio.h>
// Function declarations (function prototypes)
float calculateArea(float radius);
float calculatePerimeter(float radius);
int main() {
float radius = 5.0;
float area = calculateArea(radius); // function call
float perimeter = calculatePerimeter(radius);
printf("Circle with radius %.2f:\n", radius);
printf("Area: %.2f\n", area);
printf("Perimeter: %.2f\n", perimeter);
return 0;
}
// Function definitions
float calculateArea(float radius) {
return 3.14159 * radius * radius;
}
float calculatePerimeter(float radius) {
return 2 * 3.14159 * radius;
}Output:
Circle with radius 5.00:
Area: 78.54
Perimeter: 31.42
Pointers differentiate C from other high-level languages. They provide direct access to memory addresses, trusting the programmer to manage and optimize the memory usage. Java, Python, JavaScript, and other languages abstract memory management, making them easier to use but less efficient.
- Memory Structure:
- Byte: The smallest addressable unit in memory, consisting of 8 bits.
- 32-bit: Addresses range from
0to4,294,967,295(2³² - 1). - 64-bit: Addresses range from
0to18,446,744,073,709,551,615(2⁶⁴ - 1)
- 32-bit: Addresses range from
- Byte: The smallest addressable unit in memory, consisting of 8 bits.
Fun Tidbit: No supercomputer has yet required more than 64-bit addressable memory. Even Frontier, the world's largest supercomputer, has only 9200 TB RAM, which falls well within 64-bit addressing capabilities. A 64-bit architecture can theoretically address up to 16 million TB of memory.
-
Memory Addressing:
- Each byte in memory has a unique address.
- Addresses are typically represented in hexadecimal format.
-
Pointers in C:
- Usage: Allow direct access to memory addresses and manipulation of data within those individual memory locations.
- Advantage: Offers fine-grained control for performance optimizations, unlike languages like Java or Python where compilers handle pointer arithmetic.
What Are Pointers?
- Variables that store the memory address of other variables, structs (to be covered later), arrays, etc.
- Syntax
int *ptr;
ptris a pointer to an integer.
-
Declaring Pointers
type *pointerName;
type: Data type the pointer can point to.*: Indicates that the variable is a pointer.
-
Examples:
int *ptrInt; // Pointer to an integer char *ptrChar; // Pointer to a character float *ptrFloat; // Pointer to a float
-
Initializing Pointers: Use the address-of operator
&to assign the memory address of a variable to a pointer.int i = 10; int *p = &i; // Pointer p stores the memory address of i
Visual Representation of Memory
| Variable | Value | Memory Address |
|---|---|---|
int i |
10 |
0x7ffee3b2a9a0 |
int *p |
0x7ffee3b2a9a0 |
0x7ffee3b2a9a8 |
In this table, the pointer p stores the memory address of the integer variable i.
Note: The actual value stored in
int *pis the memory address ofi. The value10is stored ini. We can access this value stored iniusing the pointerpas we will cover in dereferencing.
#include <stdio.h>
int main() {
int i = 42; // Integer variable
int *p = &i; // Pointer variable storing address of i
return 0;
}- Accessing or modifying the value stored at the memory address a pointer points to.
- Syntax:
*pointerName;
#include <stdio.h>
int main() {
int num = 25;
int *p = # // Pointer p points to num
printf("Value of num: %d\n", num);
printf("p stores the address: %p\n", (void*)p); // use %p to print memory address, use void* to cast the pointer to a void pointer
printf("p points to the value: %d\n", *p); // Dereferencing the pointer will return a int value thus use %d
// Modifying the value using dereferencing
*p = 30; //notice the *p is used to modify the value of num, only when initializing the pointer do we use int *p = &num
printf("New value of num after modification: %d\n", num);
return 0;
}Expected Output:
Value of num: 25
p stores the address: 0x7ffee3b2a9a0
p points to the value: 25
New value of num after modification: 30
- Initialize Pointers: Always initialize pointers before use to avoid undefined behavior.
int *ptr; // Uninitialized pointer are dangerous int *ptr = NULL; // Initialized pointer (Safe)
- Consequences of Uninitialized Pointers:
- May point to random memory locations.
- Can lead to program crashes or security vulnerabilities.
int *ptr; // Uninitialized pointer *ptr = 50; // You are storing 50 at a random memory location which is undefined behavior and may cause a crash
-
Dual Use of
*:- Declaration: Indicates that a variable is a pointer.
int *ptr; // ptr is a pointer to an integer
- Dereferencing: Accesses the value at the memory address the pointer points to.
int value = *ptr; // Retrieves the value pointed to by ptr
- Declaration: Indicates that a variable is a pointer.
-
Clarifying the Context:
- Declaration vs. Dereferencing:
- Left Side of
=: Declaring a pointer. - Right Side of
=: Dereferencing to get the value.
- Left Side of
- Declaration vs. Dereferencing:
-
Always Initialize Pointers:
int *ptr = NULL; // Safe initialization
-
Avoid Dereferencing NULL Pointers:
if (ptr != NULL) { // Safe to dereference }
-
Use Meaningful Pointer Names: Enhance code readability.
int *agePtr; // Pointer to age variable char *namePtr; // Pointer to name string
Using pointers with arrays is an essential concept in C programming. Since arrays and pointers are closely related, understanding their interaction helps in efficient memory management and code optimization.
In C, the name of an array acts as a pointer to its first element. This means that if we have an array:
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers; // Equivalent to traditional &numbers[0]
printf("First element: %d\n", *ptr); //dereference the value at this location
printf("First element: %d\n", numbers[0]);
return 0;
}Output:
First element: 10
First element: 10
Note: Here, ptr is assigned numbers, which is the address of the first element in the array. Accessing *ptr gives 10, the first element.
- Thus realize that
numbersis equivalent to&numbers[0]as it gives address to the first element in array. *numbersis equivalent tonumbers[0]as it gives the value at the first element in the array.
C supports three fundamental pointer arithmetic operations:
1. Adding an Integer to a Pointer
When you add an integer n to a pointer, it moves forward by n * sizeof(type). where type is the data type of the pointer.
#include <stdio.h>
int main() {
int arr[] = {100, 200, 300, 400, 500};
int *ptr = arr; //pointer to first element of arr (value at index 0 is 100 in arr)
printf("Pointer address: %p, Value: %d\n", ptr, *ptr); //address = ptr, value at that address = *ptr
ptr++; // Moves to the next element
printf("Pointer address: %p, Value: %d\n", ptr, *ptr);
return 0;
}Output:
Pointer address: 0x12345678, Value: 100
Pointer address: 0x1234567C, Value: 200
The address increases by sizeof(int), typically 4 bytes. This means you don't have to worry about adding the correct bytes manually.
2. Subtracting an Integer from a Pointer
Subtracting an integer n moves the pointer backward by n * sizeof(type).
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *ptr = &arr[3]; //adress to 40 (element at index 3)
printf("Pointer address: %p, Value: %d\n", ptr, *ptr);
ptr--; // Moves to the previous element
printf("Pointer address: %p, Value: %d\n", ptr, *ptr);
return 0;
}Output:
Pointer address: 0x12345684, Value: 40
Pointer address: 0x12345680, Value: 30
Again, it moves back by sizeof(int) bytes.
3. Subtracting One Pointer from Another
You can subtract two pointers to determine the number of elements between them.
#include <stdio.h>
int main() {
int arr[] = {5, 10, 15, 20, 25, 30};
int *ptr1 = &arr[1]; // Points to 10
int *ptr2 = &arr[5]; // Points to 30
printf("Difference: %d\n", ptr2 - ptr1);
return 0;
}Output:
Difference: 4
The output shows that ptr2 is 4 elements ahead of ptr1.
Pointer Comparisons
Pointers can be compared using comparison operators:
#include <stdio.h>
int main() {
int arr[] = {1, 2, 3, 4, 5};
int *p1 = &arr[0];
int *p2 = &arr[4];
if (p1 < p2) {
printf("p1 points to an earlier element than p2.\n");
}
return 0;
}Output:
p1 points to an earlier element than p2.
Pointers are compared based on their memory addresses.
We can traverse an array using explicit pointer notation:
#include <stdio.h>
int main() {
int arr[] = {3, 6, 9, 12, 15};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(ptr + i)); //add i = 1,2,3... to the pointer to access the next element and then dereference it
}
return 0;
}Output:
3 6 9 12 15
Here, *(ptr + i) accesses the ith element.
Let’s implement a program that finds the maximum number in an array using pointer arithmetic.
#include <stdio.h>
int findMax(int arr[], int size) { // int arr[] = int *arr = pointer to first element of array
int max = arr[0]; // setting max to first element by default
for (int i = 1; i < size; i++) {
if (arr[i] > max) { // arr[i] is equivalent to *(arr + i)
max = arr[i];
}
}
return max;
}
int main() {
int numbers[] = {23, 45, 67, 12, 89, 55};
int size = sizeof(numbers) / sizeof(numbers[0]);
int max = findMax(numbers, size);
printf("The maximum value in the array is: %d\n", max);
return 0;
}Expected Output:
The maximum value in the array is: 89
This is a complete self-contained program, try copying pasting this into an online free C compiler like Programiz to see it in action.
This can be improved in many ways, try improving it!
// Online C compiler to run C program online
#include <stdio.h>
#include <stdbool.h>
#define ROWS 3
#define COLS 3
// Function to print the Tic-Tac-Toe grid
void gridPrinter(char board[ROWS][COLS]) {
printf("Tic-Tac-Toe Board:\n");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf(" %c ", board[i][j]); // Print the cell value
if (j < COLS - 1) {
printf("|"); // Separator between columns except for the last column
}
}
printf("\n");
// Print a horizontal line after each row except the last
if (i < ROWS - 1) {
printf("---|---|---\n");
}
}
}
// Function to check if there is a winner
char checkWinner(char board[ROWS][COLS]) {
// Check rows and columns
for (int i = 0; i < ROWS; i++) {
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ')
return board[i][0]; // Row win
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ')
return board[0][i]; // Column win
}
// Check diagonals
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ')
return board[0][0]; // Main diagonal
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != ' ')
return board[0][2]; // Secondary diagonal
return ' '; // No winner yet
}
// Function to check if the board is full (draw)
bool isBoardFull(char board[ROWS][COLS]) {
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
if (board[i][j] == ' ')
return false; // Found an empty space, not full
}
}
return true;
}
int main() {
// Initialize an empty 3x3 Tic-Tac-Toe board
char board[ROWS][COLS] = {
{' ', ' ', ' '},
{' ', ' ', ' '},
{' ', ' ', ' '}
};
bool gameState = true;
char currentPlayer = 'X'; // X starts first
int xCoord, yCoord;
while (gameState) {
// Print the current board
gridPrinter(board);
// Get user input
printf("Player %c, enter your move (row and column: 0 0 to 2 2): ", currentPlayer);
scanf("%d %d", &xCoord, &yCoord);
// Validate move
if (xCoord < 0 || xCoord >= ROWS || yCoord < 0 || yCoord >= COLS || board[xCoord][yCoord] != ' ') {
printf("Invalid move out of range. Try again.\n");
continue; // Ask for input again
}
// Place the move
board[xCoord][yCoord] = currentPlayer;
// Check for winner
char winner = checkWinner(board);
if (winner != ' ') {
gridPrinter(board);
printf("Player %c wins!\n", winner);
gameState = false;
break;
}
// Check for draw
if (isBoardFull(board)) {
gridPrinter(board);
printf("It's a draw!\n");
gameState = false;
break;
}
// Switch player
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
}
return 0;
}- Notice that
void gridPrinter(char board[ROWS][COLS])we needed to define it like this because we are passing a 2D array and it needs column size to be defined, you can also define it asvoid gridPrinter(char board[][COLS])and it will work the same way since row can be left empty but column size is required.