W4. Statements, Expressions, Structures, and Unions in C
1. Summary
1.1 Statements in C
1.1.1 What is a Statement?
In C, a statement is a complete instruction that tells the computer to perform an action. Every statement in C ends with a semicolon (;). The general rule is that statements do not produce values; their purpose is to control the flow of execution or to cause a side effect, such as modifying a variable’s value.
1.1.2 Categories of Statements
C statements can be grouped into several categories:
- Selection Statements: These statements choose which code path to execute based on a condition.
if-else: Executes a block of code if a condition is true, and an optionalelseblock if it is false.switch: Evaluates an integer expression and jumps to thecasethat matches the value. Abreakstatement is typically used to exit theswitch; otherwise, execution “falls through” to the next case.
- Iteration Statements (Loops): These statements execute a block of code repeatedly as long as a condition is met.
while: Evaluates the condition before each execution of the loop body.do-while: Executes the loop body at least once and then evaluates the condition after each execution.for: A compact loop structure that combines initialization, condition checking, and a post-iteration action in one line.
- Jump Statements: These statements unconditionally transfer control to another part of the program.
break: Exits the innermost loop orswitchstatement.continue: Skips the remainder of the current loop iteration and proceeds to the next one.return: Exits the current function and optionally returns a value to the caller.goto: Transfers control to a labeled statement within the same function. Its use is generally discouraged as it can make code difficult to read and debug.
- Compound Statements (Blocks): A group of zero or more statements enclosed in curly braces (
{}). A block can be used anywhere a single statement is expected, allowing you to group multiple instructions. A declaration within a block is considered a statement. - Special Statements:
- Expression Statement: An expression followed by a semicolon. The expression is evaluated, and its result is discarded. Its main purpose is for the side effect it produces (e.g., assignment
a = b + c;or a function callprintf("hello");). - Declaration Statement: A statement that introduces a new variable (e.g.,
int x = 10;). In modern C, declarations can appear anywhere a statement is allowed, not just at the beginning of a block.
- Expression Statement: An expression followed by a semicolon. The expression is evaluated, and its result is discarded. Its main purpose is for the side effect it produces (e.g., assignment
1.2 Expressions in C
1.2.1 What is an Expression?
An expression is a formula—a combination of variables, constants, operators, and function calls—that is evaluated to produce a single value. Almost every expression in C produces a value.
1.2.2 Building Blocks of Expressions
Expressions are constructed from various elements, ordered by complexity:
- Primary Expressions: The most basic elements.
- Identifier: The name of a variable or function.
- Literal: A constant value (e.g.,
123,0.01E-2,"string"). - Parenthesized expression: An expression enclosed in parentheses, like
(a + b).
- Postfix Expressions: Built on primary expressions.
- Array subscripting:
arr[i+j]. - Function call:
func(*p, 777). - Member access:
s.m(forstruct/union) orptr->m(for pointer tostruct/union). - Postfix increment/decrement:
x++,x--.
- Array subscripting:
- Unary Expressions: Operators that act on a single operand.
- Prefix increment/decrement:
++x,--x. - Address-of (
&) and indirection (*):&x,*p. - Unary plus/minus:
+x,-x. - Logical NOT (
!) and bitwise NOT (~). sizeof: An operator that returns the size, in bytes, of a type or variable.
- Prefix increment/decrement:
- Binary Expressions: Operators that act on two operands (e.g.,
a + b,c * d). - Ternary Expression: The conditional operator (
? :), which takes three operands (condition ? value_if_true : value_if_false).
1.2.3 Operator Precedence and Associativity
- Precedence: Determines the order in which operators are evaluated in a complex expression. For example,
*and/have higher precedence than+and-, soa + b * cis evaluated asa + (b * c). - Associativity: Determines the order for operators of the same precedence. Most binary operators are left-to-right associative (e.g.,
x - y + zis(x - y) + z). Unary operators and the assignment operator are right-to-left associative (e.g.,x = y = 5isx = (y = 5)).
1.2.4 Side Effects
A side effect is any change in the state of the program, such as modifying a variable or performing I/O. Expressions like x++ are valued for their side effect. The expression x++ evaluates to the original value of x, and as a side effect, x is incremented.
1.3 Recursive Functions
A recursive function is a function that calls itself to solve a problem. This is effective for problems that can be divided into smaller, self-similar subproblems.
1.3.1 Core Components of Recursion
To prevent infinite execution, a recursive function must have two parts:
- Base Case: A simple condition where the function does not call itself and returns a direct answer. This is the stopping point.
- Recursive Step: The part of the function that calls itself, but with a modified argument that brings it closer to the base case.
1.3.2 The Call Stack in Recursion
When a function is called, a frame containing its local variables is pushed onto the call stack. In recursion, a new frame is added for each self-call. The stack grows until a base case is reached. Then, as each function returns its result to its caller, its frame is popped off the stack, and the stack unwinds.
1.4 Structures (struct)
A structure is a user-defined data type that groups related variables of different types into a single logical unit.
1.4.1 Declaration and Memory
Each member of a struct is stored in its own unique memory location. The total size is the sum of the sizes of its members plus any memory padding.
- Memory Padding: Compilers insert unused bytes between members to align them on memory addresses that are optimal for the hardware (e.g., a 4-byte
inton an address divisible by 4). This speeds up access but increases the structure’s size. - Packed Structures: A non-standard feature (
__attribute__((packed))) that removes padding. This saves memory but can cause slower performance or crashes on some architectures.
1.4.2 Accessing Members
- Dot Operator (
.): Used to access members of a structure variable directly (e.g.,student.id). - Arrow Operator (
->): Used to access members via a pointer to a structure (e.g.,studentPtr->id). This is equivalent to(*studentPtr).id.
1.5 Unions (union)
A union is a special data type where all members share the same memory location.
2. Definitions
- Statement: A complete instruction in a C program, ending with a semicolon.
- Expression: A combination of values, variables, operators, and functions that evaluates to a single value.
- Expression Statement: An expression followed by a semicolon, executed for its side effects.
- Declaration Statement: A statement that declares and optionally initializes a new variable.
- Operator Precedence: The rules that define the order in which different operators are evaluated in an expression.
- Side Effect: An action by a function or expression that modifies state, such as changing a variable’s value or performing I/O.
- Recursion: A technique where a function calls itself to solve a problem.
- Base Case: The condition in a recursive function that terminates the recursion.
- Structure (
struct): A data type that groups variables of different types, with each member stored in a separate memory location. - Union (
union): A data type where all members share the same memory location. - Memory Padding: Empty bytes inserted by a compiler into a structure to align its members for more efficient memory access.
- Typedef: A keyword used to create an alias for a data type, often used to simplify struct and union declarations.
- Call Stack: A data structure that stores information about the active subroutines of a computer program.
3. Examples
3.1. Analyze Program Output with Static Variables and Recursion (Lab 4, Task 1)
What is the expected output of this program?
#include <stdio.h>
void func() {
static int x = 5;
int y = 5;
while (y < 10 && x < 10) {
printf("x = %d, y = %d\n", x, y);
x++;
y++;
func();
}
}
int main() {
func();
}Click to see the solution
Let’s trace the execution step-by-step.
maincallsfunc().func(Call 1):static int x = 5;This line only runs once for the entire program’s lifetime.xis created and set to 5.int y = 5;yis a local variable, created and set to 5 for this call.whileloop starts. Condition(y < 10 && x < 10)is(5 < 10 && 5 < 10), which is true.printfprints:x = 5, y = 5xbecomes 6. (The staticxis now 6).ybecomes 6. (The localyis now 6).func()is called recursively.
func(Call 2):- The
static int x = 5;line is skipped.xretains its value of 6. int y = 5;A new localyis created and set to 5.whileloop starts. Condition(y < 10 && x < 10)is(5 < 10 && 6 < 10), which is true.printfprints:x = 6, y = 5xbecomes 7.ybecomes 6.func()is called recursively.
- The
func(Call 3):xis 7. New localyis 5.whileis(5 < 10 && 7 < 10), which is true.printfprints:x = 7, y = 5xbecomes 8.ybecomes 6.func()is called recursively.
func(Call 4):xis 8. New localyis 5.whileis(5 < 10 && 8 < 10), which is true.printfprints:x = 8, y = 5xbecomes 9.ybecomes 6.func()is called recursively.
func(Call 5):xis 9. New localyis 5.whileis(5 < 10 && 9 < 10), which is true.printfprints:x = 9, y = 5xbecomes 10.ybecomes 6.func()is called recursively.
func(Call 6):xis 10. New localyis 5.whilecondition(y < 10 && x < 10)is(5 < 10 && 10 < 10), which is false.- The loop is skipped. The function
func(Call 6) returns.
- Execution returns to
func(Call 5).- The
whileloop in Call 5 continues. Its localywas 6. The staticxis now 10. whilecondition(y < 10 && x < 10)is(6 < 10 && 10 < 10), which is false.- Loop terminates.
func(Call 5) returns.
- The
- This pattern continues. The program unwinds from the recursion, but the
whileloop conditionx < 10is now always false for every pending call. Allfunccalls return without printing anything further.
Final Output:
x = 5, y = 5
x = 6, y = 5
x = 7, y = 5
x = 8, y = 5
x = 9, y = 53.2. Student and Exam Day Structures (Lab 4, Task 2)
Write a program that will contain two structures: student and exam_day. The first structure should contain information about the student’s name, surname, group number, and a variable for the second structure. The second structure should contain the day, year, and month of the exam. The month has to be in letter representation (For example, May), not numbers. The program should require the user to enter all the fields and then print them.
Click to see the solution
#include <stdio.h>
#include <string.h>
// Define the structure for the exam date.
struct exam_day {
int day;
char month[20]; // Character array to store the month's name
int year;
};
// Define the structure for the student.
// This structure contains another structure as one of its members.
struct student {
char name[50];
char surname[50];
int group_number;
struct exam_day exam_date; // Nested structure
};
int main() {
// Declare a variable of type 'student'.
struct student s1;
// --- Get User Input ---
printf("Enter student's first name: ");
scanf("%s", s1.name);
printf("Enter student's surname: ");
scanf("%s", s1.surname);
printf("Enter student's group number: ");
scanf("%d", &s1.group_number);
printf("Enter exam day (e.g., 22): ");
scanf("%d", &s1.exam_date.day);
printf("Enter exam month (e.g., October): ");
scanf("%s", s1.exam_date.month);
printf("Enter exam year (e.g., 2025): ");
scanf("%d", &s1.exam_date.year);
// --- Print the Stored Information ---
printf("\n--- Student Information ---\n");
printf("Name: %s\n", s1.name);
printf("Surname: %s\n", s1.surname);
printf("Group: %d\n", s1.group_number);
printf("Exam Date: %d %s %d\n", s1.exam_date.day, s1.exam_date.month, s1.exam_date.year);
return 0;
}3.3. Encrypt an Integer using a Union (Lab 4, Task 3)
Using a union, write a program that will read an unsigned long long integer via the console and then encrypt it by swapping the values of each odd byte and its neighbor even byte, beginning with the most significant byte. The program must contain an encryption(...) function and print the original, encrypted, and decrypted messages.
Click to see the solution
#include <stdio.h>
// A union allows storing different data types in the same memory location.
// Here, we can access the same 8 bytes of memory as either a single
// unsigned long long or as an array of 8 individual bytes (unsigned chars).
typedef union {
unsigned long long ull_value;
unsigned char bytes[8];
} ull_converter;
// Function to perform the byte-swapping encryption/decryption.
// The same logic works for both encryption and decryption.
void encryption(ull_converter *data) {
// An unsigned long long is 8 bytes. We swap pairs of bytes:
// bytes[0] with bytes[1]
// bytes[2] with bytes[3]
// bytes[4] with bytes[5]
// bytes[6] with bytes[7]
// The loop iterates 4 times for the 4 pairs.
for (int i = 0; i < 8; i += 2) {
// Use a temporary variable to swap the byte pair.
unsigned char temp = data->bytes[i];
data->bytes[i] = data->bytes[i+1];
data->bytes[i+1] = temp;
}
}
int main() {
// Create a union variable.
ull_converter data;
// Prompt user for input.
printf("Enter an unsigned long long integer: ");
// Read the value from the user. %llu is the format specifier for unsigned long long.
scanf("%llu", &data.ull_value);
// Print the original value.
printf("Original message: %llu\n", data.ull_value);
// Call the encryption function. We pass the address of the union.
encryption(&data);
// The bytes inside the union have been swapped. Reading the ull_value now gives the encrypted number.
printf("Encrypted message: %llu\n", data.ull_value);
// Call the function again. Swapping the swapped bytes returns them to their original positions.
encryption(&data);
// Print the decrypted message, which should match the original.
printf("Decrypted message: %llu\n", data.ull_value);
return 0;
}