Introduction to C Language | Lecture 7: Complete Guide to Functions and Program Structure
π‘ Foreword: If variables are the building blocks of a program, then functions are the rooms of the building. This article will take you from zero to fully understand functions and program structure in C language. Whether you are a beginner or someone looking to solidify your foundation, this article will enlighten you!
1. Why Do We Need Functions? π€
Imagine if you had to write printf every time you wanted to print “Hello World”, or if you had to write a bunch of code every time you wanted to calculate an average. Wouldn’t that be cumbersome?
The Three Major Benefits of Functions:
(1) Code Reusability – Write once, use countless times
// Without functions (bad exampleβ)
printf("Welcome to the game world!\n");
printf("Loading...\n");
// In the game...
printf("Welcome to the game world!\n"); // Written again
printf("Loading...\n"); // Written again
// Using functions (recommendedβ
)
void showWelcome() {
printf("Welcome to the game world!\n");
printf("Loading...\n");
}
// Call with just one line
showWelcome(); // First time
showWelcome(); // Second time
(2) Modularity – Makes code as clear as building blocks
// Main program becomes very concise
int main() {
initializeGame();
displayMenu();
startGame();
saveRecord();
exitProgram();
return 0;
}
(3) Ease of Maintenance – Modify one place, affect everywhere
If you need to change the welcome message, just modify the function internally, and all calling places will update automatically!
2. Basic Knowledge of Functions π
(1) Basic Structure of a Function
ReturnType functionName(ParameterList)
{
// Function body (the code to execute)
return returnValue; // If there is a return value
}
Breakdown Explanation:
- β’ Return Type: What type of data does the function return after execution? (int, float, void, etc.)
- β’ Function Name: Give the function a meaningful name that indicates its purpose
- β’ Parameter List: What “raw materials” does the function need to work
- β’ Function Body: The specific code that does the work
- β’ return: Hand over the result
(2) The Simplest Function: No Parameters, No Return Value
#include <stdio.h>
// Define the simplest function
// void means "none", indicating no return value
void sayHello() {
printf("Hello, Micu!\n");
printf("Nice to meet you!\n");
}
int main() {
// Call the function: functionName + parentheses
sayHello(); // Output: Hello, Micu! Nice to meet you!
sayHello(); // Can be called multiple times
return 0;
}
π‘ Tip:
- β’
<span>void</span>before the function indicates “no return value” - β’
<span>void</span>in the parameter position indicates “no parameters” (can be omitted)
(3) Functions with Parameters and Return Values
#include <stdio.h>
// Calculate the average of two integers
// Parameters: int a, int b (needs two integers)
// Return value: float (returns a decimal)
float calculateAverage(int a, int b) {
// Why use 2.0f instead of 2?
// Because integer division will lose the decimal part!
// 3 / 2 = 1 (integer division)
// 3 / 2.0f = 1.5 (floating-point division)
float result = (a + b) / 2.0f;
return result; // Return the result to the caller
}
int main() {
int num1 = 10;
int num2 = 15;
// Call the function and receive the return value in a variable
float avg = calculateAverage(num1, num2);
printf("Average is: %.2f\n", avg); // Output: Average is: 12.50
// Function calls can also be used directly in expressions
printf("Average of another set of numbers: %.2f\n", calculateAverage(20, 30));
return 0;
}
π₯ Important Reminder:
// β Incorrect writing
int average = (a + b) / 2; // Result is an integer, will lose decimal
// β
Correct writing
float average = (a + b) / 2.0f; // Result is a decimal
(4) Multiple Return Methods of Functions
#include <stdio.h>
// Example 1: Return calculation result
int add(int a, int b) {
return a + b; // Directly return the result of the expression
}
// Example 2: Return comparison result (check if passed)
int isPassed(int score) {
if (score >= 60) {
return 1; // 1 means true (passed)
} else {
return 0; // 0 means false (not passed)
}
}
// Example 3: Early return (exit immediately upon error)
float safeDivide(int a, int b) {
if (b == 0) {
printf("Error: Divisor cannot be 0!\n");
return 0.0f; // Return early upon error
}
return (float)a / b; // Return under normal circumstances
}
int main() {
printf("5 + 3 = %d\n", add(5, 3));
printf("Did 85 pass? %d\n", isPassed(85)); // Output 1 (true)
printf("Did 45 pass? %d\n", isPassed(45)); // Output 0 (false)
printf("10 / 2 = %.2f\n", safeDivide(10, 2));
printf("10 / 0 = %.2f\n", safeDivide(10, 0)); // Will prompt error
return 0;
}
3. Value Passing vs Pointer Passing (Key Difficulty) β οΈ
This is the part that beginners find most confusing! Letβs clarify it in the simplest way.
(1) Why Can’t Value Passing Swap Variables?
Problem Scenario: Want to write a function to swap the values of two variables
#include <stdio.h>
// β Incorrect swap function (value passing)
void wrongSwap(int x, int y) {
printf("Before swap in function: x=%d, y=%d\n", x, y);
int temp = x;
x = y;
y = temp;
printf("After swap in function: x=%d, y=%d\n", x, y);
// It seems the swap was successful, but this is just an "illusion"!
}
int main() {
int a = 10;
int b = 20;
printf("Before call: a=%d, b=%d\n", a, b);
wrongSwap(a, b);
printf("After call: a=%d, b=%d\n", a, b);
// π± Surprised! a and b did not swap!
return 0;
}
/* Output:
Before call: a=10, b=20
Before swap in function: x=10, y=20
After swap in function: x=20, y=10
After call: a=10, b=20 <-- Still the original values!
*/
π€ Why is this the case?
Let me explain with a real-life example:
Imagine you have two notes:
Note A says "10", Note B says "20"
Value passing is like:
1. You copy the contents of Note A and B onto two new notes (copies)
2. Give the copies to the function
3. The function modifies the copies
4. But the originals (a and b) in your hand remain unchanged!
This is "value passing": passing a copy of the value, not the variable itself
(2) The Correct Method: Pointer Passing
#include <stdio.h>
// β
Correct swap function (pointer passing)
void correctSwap(int *x, int *y) {
// *x means "the variable pointed to by x"
// *y means "the variable pointed to by y"
printf("Before swap in function: *x=%d, *y=%d\n", *x, *y);
int temp = *x; // temp = value pointed to by x
*x = *y; // Assign value pointed to by y to the variable pointed to by x
*y = temp; // Assign the value of temp to the variable pointed to by y
printf("After swap in function: *x=%d, *y=%d\n", *x, *y);
}
int main() {
int a = 10;
int b = 20;
printf("Before call: a=%d, b=%d\n", a, b);
// &a means "the address of a", &b means "the address of b"
// We are not passing values, but "addresses" (telling the function where the variables are)
correctSwap(&a, &b);
printf("After call: a=%d, b=%d\n", a, b);
// π Success! a and b really swapped!
return 0;
}
/* Output:
Before call: a=10, b=20
Before swap in function: *x=10, *y=20
After swap in function: *x=20, *y=10
After call: a=20, b=10 <-- Swap successful!
*/
π Key to Pointer Passing:
Continuing with the note example:
Pointer passing is like:
1. You donβt give the function the content of the notes
2. Instead, you tell the function "Note A is on the left side of the table, Note B is on the right side" (the address)
3. The function directly goes to your table and modifies the originals
4. This way, it really changes the original notes!
Remember:
- &a β Get address (tell the function where a is)
- *x β Get value (access the variable pointed to by x)
(3) Memory Tricks for Pointers
int a = 10; // Define a normal variable
int *p; // Define a pointer variable (a variable that can store addresses)
p = &a; // p stores the address of a (p points to a)
// When reading:
printf("%d", a); // Directly read the value of a: 10
printf("%d", *p); // Read the value pointed to by p: also 10
// When modifying:
a = 20; // Directly modify a
*p = 20; // Modify a through the pointer (same effect)
// Memory mnemonic:
// & β "Get address" β "Where"
// * β "Get value" β "What is it"
4. The Mystery of Recursive Functions π
Recursion is “calling itself”, which sounds mysterious, but is actually easy to understand!
(1) What is Recursion?
Real-life example of recursion:
What do you see when you hold up a mirror in front of another mirror?
β Mirrors in mirrors, mirrors in mirrors in mirrors...
β This is recursion! Repeating until a certain condition terminates
(2) Essential Elements of Recursive Functions
int recursiveFunction(int n) {
// 1. Termination condition (very important! Without it, it will loop infinitely)
if (meets a certain condition) {
return some value; // Stop recursion
}
// 2. Recursive call (approaching the termination condition)
return some expression + recursiveFunction(smaller n);
}
(3) Practical Example: Calculating Factorial
#include <stdio.h>
// Calculate n factorial: n! = n Γ (n-1) Γ (n-2) Γ ... Γ 2 Γ 1
// For example: 5! = 5 Γ 4 Γ 3 Γ 2 Γ 1 = 120
int factorial(int n) {
// Termination condition: 1 factorial is 1, 0 factorial is also 1
if (n <= 1) {
return 1;
}
// Recursive call: n! = n Γ (n-1)!
// Break the problem into smaller subproblems
return n * factorial(n - 1);
}
int main() {
int num = 5;
printf("%d! = %d\n", num, factorial(num));
return 0;
}
π Visualization of Recursive Execution Process:
Calculating factorial(5):
factorial(5)
= 5 Γ factorial(4)
= 5 Γ (4 Γ factorial(3))
= 5 Γ (4 Γ (3 Γ factorial(2)))
= 5 Γ (4 Γ (3 Γ (2 Γ factorial(1))))
= 5 Γ (4 Γ (3 Γ 2)) β Reached termination condition
= 5 Γ (4 Γ 6)
= 5 Γ 24
= 120
Just like Russian nesting dolls:
Open β Open β Open β To the smallest β Assemble β Assemble β Assemble
(4) Recursion vs Looping
The same functionality can be achieved using loops:
// Method 1: Implementing with recursion
int factorial_recursive(int n) {
if (n <= 1) return 1;
return n * factorial_recursive(n - 1);
}
// Method 2: Implementing with loops
int factorial_loop(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
Comparison:
| Feature | Recursion | Looping |
| Code Conciseness | β More concise and elegant | β Relatively cumbersome |
| Understanding Difficulty | β Requires some mental training | β More intuitive |
| Performance | β Slower (function call overhead) | β Faster |
| Applicable Scenarios | Complex structures like trees, graphs | Simple repetitive operations |
5. Complete Structure of a C Program ποΈ
A standard C program should look like this:
// ========== Part 1: Preprocessing Directives ==========
// Tell the compiler what "toolkits" are needed
#include <stdio.h> // Standard input/output (printf, scanf, etc.)
#include <stdlib.h> // Standard library functions (malloc, exit, etc.)
#include <string.h> // String processing functions
// ========== Part 2: Symbol Constant Definitions ==========
#define PI 3.14159 // Define constant
#define MAX_SIZE 100
// ========== Part 3: Global Variable Declarations ==========
// Global variables: accessible throughout the program
int globalCount = 0; // Global variables are automatically initialized to 0
// ========== Part 4: Function Declarations ==========
// Declare first, tell the compiler "this function exists"
void printMenu(); // Display menu
int getUserChoice(); // Get user choice
float calculateSum(int a, int b); // Calculate sum
// ========== Part 5: main function (program entry) ==========
int main() {
// Local variables: only valid within the main function
int choice = 0; // β οΈ Must initialize! Otherwise, it is a random value
float result = 0.0f;
// The program starts executing from here
printf("Program started!\n");
printMenu(); // Call function
choice = getUserChoice(); // Call and receive return value
result = calculateSum(10, 20); // Call and calculate
printf("Result: %.2f\n", result);
return 0; // Return 0 indicates the program ended normally
}
// ========== Part 6: Function Definitions (specific implementations) ==========
// Write the specific code for the functions just declared
void printMenu() {
printf("===================\n");
printf("1. Start Game\n");
printf("2. View Settings\n");
printf("3. Exit\n");
printf("===================\n");
}
int getUserChoice() {
int choice;
printf("Please enter your choice:");
scanf("%d", &choice);
return choice;
}
float calculateSum(int a, int b) {
return (float)(a + b);
}
(1) Key Rule Analysis
β Rule 1: All code must be inside functions
// β Incorrect: Cannot write execution statements outside functions
printf("Hello"); // Compilation error!
// β
Correct: Must be inside a function
int main() {
printf("Hello"); // OK
return 0;
}
β Rule 2: Local variables must be initialized
void test() {
int a; // β οΈ Dangerous! a is a random value (garbage value)
printf("%d", a); // May output any number: 3847582, -23, 0...
int b = 0; // β
Safe! b is initialized to 0
printf("%d", b); // Output: 0
}
β Rule 3: Function calls must be declared before
int main() {
test(); // β Error! The compiler does not know what test is
return 0;
}
void test() {
printf("Test\n");
}
// Solution 1: Write function definition before main
// Solution 2: Declare before main
void test(); // Function declaration
6. Practical Multi-file Programming ποΈ
When programs grow larger, writing all code in one file becomes messy. Multi-file programming makes the code more organized!
(1) Why Use Multiple Files?
Imagine you are writing a book:
- One file = One chapter
- Header file (.h) = Directory (tells others what this chapter contains)
- Source file (.c) = Specific content
Benefits:
β
Clear code classification
β
Convenient for collaboration
β
Modifying one file does not affect others
β
Code can be reused
(2) Practical Case: Math Utility Library
Step 1: Create Header File <span>math_utils.h</span>
// math_utils.h - Directory of math utilities
// Purpose: Tell others "What functions are available here"
#ifndef MATH_UTILS_H // Prevent duplicate inclusion (very important!)
#define MATH_UTILS_H // Define it if not defined before
// ========== Function Declarations (Interface) ==========
// Only tell "What functionalities are available", not "How to implement"
/**
* Calculate the sum of two integers
* @param a First integer
* @param b Second integer
* @return Sum of the two integers
*/
int add(int a, int b);
/**
* Calculate the maximum of two integers
* @param a First integer
* @param b Second integer
* @return The larger number
*/
int max(int a, int b);
/**
* Calculate the average of an array
* @param arr Integer array
* @param size Length of the array
* @return Average of the array elements
*/
float average(int arr[], int size);
#endif // End of conditional compilation
// These three lines ensure the header file is included only once, preventing redefinition errors
Step 2: Create Source File <span>math_utils.c</span>
// math_utils.c - Specific content of math utilities
// Purpose: Implement all functions declared in the header file
#include "math_utils.h" // Include own header file (using double quotes)
// ========== Function Definitions (Implementation) ==========
// Implement addition functionality
int add(int a, int b) {
return a + b; // Simple and direct
}
// Implement maximum functionality
int max(int a, int b) {
// Ternary operator: condition ? true value : false value
return (a > b) ? a : b;
// Equivalent to:
// if (a > b) {
// return a;
// } else {
// return b;
// }
}
// Implement average functionality
float average(int arr[], int size) {
// 1. Check if input is valid
if (size <= 0) {
return 0.0f; // Return 0 for empty array
}
// 2. Calculate the sum of all elements
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i]; // Accumulate each element
}
// 3. Calculate average (note type conversion)
return (float)sum / size; // Force conversion to float
}
Step 3: Create Main Program <span>main.c</span>
// main.c - Main program
// Purpose: Use the tools we created
#include <stdio.h>
#include "math_utils.h" // Include our own header file
int main() {
printf("========== Math Utility Test ==========%s\n\n", "");
// Test 1: Addition
int a = 15, b = 27;
printf("Testing addition: %d + %d = %d\n", a, b, add(a, b));
// Test 2: Find maximum
printf("Testing maximum: max(%d, %d) = %d\n", a, b, max(a, b));
// Test 3: Calculate average
int scores[] = {85, 92, 78, 95, 88}; // Scores array
int count = 5;
float avg = average(scores, count);
printf("Testing average: [85, 92, 78, 95, 88] average = %.2f\n", avg);
// Test 4: Nested calls (function calls within functions)
int x = 10, y = 20, z = 30;
int maxOfTwo = max(x, y); // First find max of x and y
int maxOfThree = max(maxOfTwo, z); // Then compare with z
printf("Testing nesting: max(%d, %d, %d) = %d\n", x, y, z, maxOfThree);
return 0;
}
/* Output:
========== Math Utility Test ==========
Testing addition: 15 + 27 = 42
Testing maximum: max(15, 27) = 27
Testing average: [85, 92, 78, 95, 88] average = 87.60
Testing nesting: max(10, 20, 30) = 30
*/
(3) Compiling Multi-file Programs
# Method 1: Compile all files at once
gcc main.c math_utils.c -o program
# Method 2: Step-by-step compilation (recommended for large projects)
gcc -c main.c -o main.o # Compile main.c to generate object file
gcc -c math_utils.c -o math_utils.o # Compile math_utils.c
gcc main.o math_utils.o -o program # Link all object files
# Run the program
./program
(4) Importance of Header File Protection
// Why do we need #ifndef #define #endif?
// Suppose there is no protection:
// file1.c
#include "math_utils.h" // Include once
#include "math_utils.h" // Include again
// Result: All functions declared twice, compilation error!
// With protection:
// First include: #ifndef is true, define MATH_UTILS_H, include content
// Second include: #ifndef is false (already defined), skip content
// Perfect! Avoids redefinition
7. External Variables and Global Variables π
(1) Global Variables vs Local Variables
#include <stdio.h>
// Global variable: defined outside all functions
int globalVar = 100; // Accessible by all functions
void func1() {
printf("func1 accesses global variable: %d\n", globalVar);
globalVar = 200; // Modify global variable
}
void func2() {
printf("func2 sees modified value: %d\n", globalVar);
}
int main() {
// Local variable: only valid within main function
int localVar = 10; // Other functions cannot see this variable
printf("Initial global variable: %d\n", globalVar);
func1(); // Modify global variable
func2(); // See modified value
return 0;
}
/* Output:
Initial global variable: 100
func1 accesses global variable: 100
func2 sees modified value: 200
*/
(2) Sharing Variables Across Files (extern)
File 1: config.c
// config.c - Define configuration variables
int maxUsers = 100; // Maximum number of users
float version = 1.5f; // Software version
char appName[] = "MyApp"; // Application name
File 2: utils.c
// utils.c - Use configuration variables
#include <stdio.h>
// Declare external variables (tell the compiler: this variable is defined in another file)
extern int maxUsers;
extern float version;
extern char appName[];
void showConfig() {
printf("Application Name: %s\n", appName);
printf("Version: %.1f\n", version);
printf("Max Users: %d\n", maxUsers);
}
File 3: main.c
// main.c - Main program
#include <stdio.h>
extern int maxUsers; // Declare external variable
void showConfig(); // Declare function
int main() {
showConfig(); // Show configuration
// Modify external variable
maxUsers = 200;
printf("After modification:\n");
showConfig();
return 0;
}
β οΈ Notes:
// Definition (can only be done once): allocate memory
int globalVar = 10;
// Declaration (can be done multiple times): tell the compiler the variable exists
extern int globalVar;
// Common error:
extern int errorVar = 10; // β extern cannot be initialized
8. Advanced Applications of Functions π
(1) Functions as Parameters
#include <stdio.h>
// Define a simple math operation function type
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// Higher-order function: receives a function as a parameter
// operation is a function pointer, pointing to a function that takes two ints and returns an int
int calculate(int x, int y, int (*operation)(int, int)) {
return operation(x, y); // Call the passed function
}
int main() {
int a = 10, b = 5;
// Pass different functions to achieve different calculations
printf("%d + %d = %d\n", a, b, calculate(a, b, add));
printf("%d Γ %d = %d\n", a, b, calculate(a, b, multiply));
return 0;
}
(2) Classic Application of Recursion: Fibonacci Sequence
#include <stdio.h>
// Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13, 21...
// Rule: Current number = sum of the previous two numbers
int fibonacci(int n) {
// Termination condition
if (n == 1 || n == 2) {
return 1;
}
// Recursion: F(n) = F(n-1) + F(n-2)
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
printf("First 10 Fibonacci numbers:\n");
for (int i = 1; i <= 10; i++) {
printf("F(%d) = %d\n", i, fibonacci(i));
}
return 0;
}
/* Output:
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3
F(5) = 5
F(6) = 8
F(7) = 13
F(8) = 21
F(9) = 34
F(10) = 55
*/
9. Common Errors and Debugging Techniques π§
(1) Common Errors
// Error 1: Forgetting to return
int getNumber() {
int num = 10;
// β Forgot to write return num;
} // Compilation warning: function should return a value
// Error 2: Type mismatch
float divide(int a, int b) {
return a / b; // β Integer division, result loses decimal
}
// Correct: return (float)a / b;
// Error 3: Array out of bounds
int arr[5] = {1, 2, 3, 4, 5};
arr[10] = 100; // β Array only has 5 elements, accessing the 11th will crash
// Error 4: Local variable uninitialized
void test() {
int x; // β Not initialized
x = x + 1; // Result is unpredictable!
}
9.2 Debugging Techniques
#include <stdio.h>
// Technique 1: Print intermediate results
int factorial(int n) {
printf("Calculating %d! \n", n); // Debug output
if (n <= 1) {
printf("Returning 1\n");
return 1;
}
int result = n * factorial(n - 1);
printf("%d! = %d\n", n, result); // View each step's result
return result;
}
// Technique 2: Assertion checks
#include <assert.h>
float divide(float a, float b) {
assert(b != 0); // If b is 0, the program will stop and report an error
return a / b;
}
// Technique 3: Using debugging macros
#define DEBUG 1 // Set to 1 during development, 0 for release
#if DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg) // Do nothing
#endif
int main() {
LOG("Program started");
// ... your code ...
LOG("Program ended");
return 0;
}
10. Summary and Suggestions π
(1) Core Points Review
β Three Elements of Functions: Return type, function name, parameter listβ Value Passing vs Pointer Passing: Value passing cannot change the original variable, pointer passing canβ Recursion must have a termination condition: Otherwise, it will cause stack overflow (program crash)β Function Declaration and Definition: Declare first (inform existence), then define (specific implementation)β Multi-file Programming: .h files declare, .c files implement, use #ifndef to prevent duplicate inclusionβ Variable Scope: Local variables are only valid within functions, global variables should be used cautiously
(2) Learning Suggestions
- 1. Start Simple: First write functions with no parameters and no return values, then gradually add parameters and return values
- 2. Practice Actively: Understanding β Writing, you must code yourself
- 3. Draw to Understand Pointers: Use arrows to illustrate the relationship between variables and addresses
- 4. Debugging is a Good Habit: Use printf to check intermediate results
- 5. Read Excellent Code: There are many C language projects on GitHub to learn from
(3) π¬ Conclusion
Congratulations on finishing this long article! Functions are the soul of C language; mastering functions allows you to write clear and maintainable programs.
Remember:Programming is a craft; only through practice can you become skilled. Donβt panic when encountering bugs; learn to debug and research, every programmer grows from countless bugs!
If this article helped you, donβt forget to like π, bookmark β, and follow me for more C language tutorials in the future!
If you have any questions, feel free to leave a comment, and I will reply as soon as possibleο½