Arrays
We slightly modify the example code written to numerically evaluate an integral with the trapezoid rule:
#include <stdio.h>
#include <math.h>
// Define the function to integrate: f(x) = x^3
double f(double x) {
return pow(x, 3); // pow(a,b) computes a^b
}
// Trapezoidal rule for numerical integration
double trapezoidal_rule(double (*func)(double), double a, double b, int n, double x_values[], double f_values[]) {
double p = (b - a) / n; // Width of each trapezoid
double sum = 0.5 * (func(a) + func(b)); // End points contribution
// Store the x values and function evaluations in arrays
for (int i = 1; i < n; i++) {
x_values[i] = a + i * p;
f_values[i] = func(x_values[i]); // Store the function evaluation
sum += f_values[i];
}
return sum * p;
}
int main() {
double a = 0.0; // Lower limit of integration
double b = 1.0; // Upper limit of integration
int n = 1000; // Number of trapezoids (higher n for better accuracy)
printf("This program performs numerical integration of f(x) = x^3 from a = %.2f to b = %.2f using %d trapezoids.\n", a, b, n);
// Check if n is a valid number of trapezoids
if (n > 0) {
printf("The number of trapezoids is positive.\n");
} else if (n < 0) {
printf("Error: The number of trapezoids is negative.\n");
return 1;
} else {
printf("Error: The number of trapezoids is zero.\n");
return 1;
}
// Arrays to store x values and function evaluations at those points
double x_values[n]; // Array to store x points
double f_values[n]; // Array to store function evaluations f(x)
// Perform numerical integration
double result = trapezoidal_rule(f, a, b, n, x_values, f_values);
// Print the result of the integration
printf("The integral of f(x) = x^3 from %.2f to %.2f is approximately: %.5f\n", a, b, result);
// Optionally, print out the x values and their corresponding f(x) values
printf("x values and f(x) evaluations:\n");
for (int i = 1; i < n; i++) {
printf("x[%d] = %.5f, f(x[%d]) = %.5f\n", i, x_values[i], i, f_values[i]);
}
return 0;
}
Two arrays are now introduced in the main() function:
x_values[n]: This array stores the values where the function is evaluated during the integration.f_values[n]: This array stores the computed values of the function at those values.
In C, an array is a collection of elements of the same type stored in contiguous memory locations. Arrays are useful when you need to store multiple values of the same type and access them using an index.
type array_name[size];
where type is the data type of the elements (e.g., int, double, char, etc.), array_name is the name of the array and size is the number of elements in the array.
[!WARNING]
- Indexing starts at 0: the first element is accessed using arr[0], not arr[1].
- Accessing an index outside the array size (e.g., arr[5] when the array has only 5 elements) results in undefined behavior.
The function trapezoidal_rule() is modified to take two additional array parameters (x_values[] and f_values[]).
double trapezoidal_rule(double (*func)(double), double a, double b, int n, double x_values[], double f_values[])
Inside the for loop, each value is stored in x_values[], and the corresponding function evaluation is stored in f_values[].
for (int i = 1; i < n; i++) {
x_values[i] = a + i * p;
f_values[i] = func(x_values[i]); // Store the function evaluation
sum += f_values[i];
}
After the integration, the code optionally prints the stored values and their corresponding function evaluations .
printf("x values and f(x) evaluations:\n");
for (int i = 1; i < n; i++) {
printf("x[%d] = %.5f, f(x[%d]) = %.5f\n", i, x_values[i], i, f_values[i]);
}
Array initialisation
Initializing an array means assigning initial values to its elements at the time of declaration. Proper initialization is crucial to avoid unpredictable behavior caused by garbage values.
When you declare an array, you can initialize it in several ways:
- Full Initialization: you provide explicit values for all elements.
int arr[3] = {1, 2, 3}; // Array of 3 integers, initialized to {1, 2, 3} - Partial Initialization: you provide values for some elements. The remaining elements are automatically initialized to 0.
int arr[5] = {1, 2}; // Array of 5 integers, initialized to {1, 2, 0, 0, 0} - Zero Initialization: if you initialize the first element to 0, all other elements are also set to 0.
int arr[4] = {0}; // Array of 4 integers, initialized to {0, 0, 0, 0}
[!WARNING] In C, arrays are not automatically initialized when declared. If you declare an array without explicitly initializing it, the array elements will contain garbage values (random values left in memory). This can lead to unpredictable behavior in your program. Remember to always initialise them!
Here an example of arrays declaration and initialisation:
#include <stdio.h>
int main() {
float a[3]; // Uninitialized float array of size 3
int b[5], c[5]; // Uninitialized int arrays of size 5
float d[3] = {1.2, 2.0, 3.7}; // Initialized float array of size 3
int e[7] = {1}; // Initialized int array of size 7 (first element is 1, rest are 0)
return 0;
}
Multidimensional arrays
Multidimensional arrays in C are arrays of arrays. They allow you to store data in a table-like structure, such as matrices or grids.
type array_name[size1][size2][...];
An example of a 2D array:
int matrix[3][4]; // Array of 3 rows and 4 columns
It can be initialised either with a full initialisation:
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
or with a partial initialisation (remaining elements are set to 0):
int matrix[2][3] = {
{1, 2}
};
Example:
#include <stdio.h>
int main() {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// Print the 2D array
printf("Matrix:\n");
for (int i = 0; i < 2; i++) { // Loop through rows
for (int j = 0; j < 3; j++) { // Loop through columns
printf("%d ", matrix[i][j]);
}
printf("\n");
}
return 0;
}
Multidimensional arrays as input parameters for functions
Let's look a the following code:
#include <stdio.h>
#define ROWS 3
#define COLS 4
// Function that takes a 2D array as a parameter
void print_array(int arr[ROWS][COLS]) {
printf("Array elements:\n");
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
// Function to increment each element by 1
void increment_array(int arr[][COLS], int rows_local) {
for (int i = 0; i < rows_local; i++) {
for (int j = 0; j < COLS; j++) {
arr[i][j] += 1;
}
}
}
int main() {
// Initialize a 2D array
int arr[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Call the function to print the array
print_array(arr);
// Increment the elements
increment_array(arr, ROWS);
// Call the function again to print the modified array
printf("\nAfter incrementing each element by 1:\n");
print_array(arr);
return 0;
}
define
In C programming, #define is a preprocessor directive used to create symbolic constants or macros. It allows you to define names or constants that will be replaced by their corresponding values or expressions during the preprocessing stage, before the actual compilation of the code. This can help improve code readability, maintainability, and flexibility.
In our example:
#define ROWS 3
#define COLS 4
we are defining the number of rows and columns for the 2D array. Whenever ROWS (or COLS) appears in the code, it gets replaced with the value 3 (or 4).
function definition
When defining a function like:
void increment_array(int arr[][COLS], int rows_local)
you need to include one set of square brackets [] for each dimension of the array. For example, a 3D array would be passed as:
void function(int arr[][M][L], int N)
The size of the first dimension can be left empty, but the sizes of all subsequent dimensions (from the second one onward) must be explicitly declared. Since the size of the first dimension is not automatically known inside the function, you must pass it as an additional argument.
In our example, the variable rows_local was used for instructional purposes. However, a better approach would be to follow the method used in the print_array function, where macros are utilized for clarity:
void print_array(int arr[ROWS][COLS])
This way, ROWS and COLS are predefined constants (by using #define), which makes the code more readable and maintainable.
Common Pitfalls When Using Arrays
C arrays are powerful but come with caveats. Here are some common mistakes:
1. Out-of-Bounds Access
Accessing elements beyond the array limits leads to undefined behavior. In some cases, it may appear to work, but it may also crash the program or corrupt data.
int arr[5] = {1, 2, 3, 4, 5};
printf("Valid: %d\n", arr[4]); // Defined behavior
printf("Invalid: %d\n", arr[5]); // Undefined behavior: could crash or print garbage
Possible output:
Valid: 5
Invalid: -183942
Or:
Segmentation fault (core dumped)
2. Uninitialized Arrays
Arrays declared without explicit initialization contain garbage values.
int arr[3];
for (int i = 0; i < 3; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
Possible output:
arr[0] = 32767
arr[1] = 0
arr[2] = -1938123
Values depend on leftover memory content.
3. Misusing sizeof Inside Functions
The sizeof operator returns the size of a pointer when used on array parameters.
void print_size(int arr[]) {
printf("Size: %lu\n", sizeof(arr)); // Typically 8 bytes on 64-bit systems
}
int main() {
int data[10];
print_size(data);
return 0;
}
Output:
Size: 8
To get the actual size, pass it as a separate argument:
void print_size_fixed(int arr[], int size) {
printf("Size: %d\n", size);
}
Array Size Management with #define vs const int
As we have already done, you can define array sizes with preprocessor macros. Another option is tu use constants:
#define SIZE 100
int arr1[SIZE];
const int SIZE_CONST = 100;
int arr2[SIZE_CONST]; // Works in C99 and later
[!WARNING] TODO add discussion on C standards somewhere in the beginning/end
#defineis resolved at preprocessing time.const intoffers type checking and scope control.
[!NOTE]
const intmay not work in all compilers for array sizing unless C99 or later is enabled.
Memory Layout and Access Patterns
C uses row-major order for multi-dimensional arrays. This affects how efficiently memory is accessed:
int matrix[100][100];
for (int i = 0; i < 100; i++) // Fast: row-wise access
for (int j = 0; j < 100; j++)
matrix[i][j]++;
for (int j = 0; j < 100; j++) // Slower: column-wise access
for (int i = 0; i < 100; i++)
matrix[i][j]++;
[!WARNING] TODO add a link to a short snippet to show how to get the timings in C. START ORGANIZING A SMALL LIBRARY OF SNIPPETS
const Arrays and Parameters
Use const to prevent functions from modifying arrays:
void print_readonly(const double arr[], int size) {
for (int i = 0; i < size; i++)
printf("%.2f\n", arr[i]);
}
This allows the compiler to optimize and avoids accidental changes.
[!WARNING]
constapplies only at the level declared. Useconst double *const arrfor a truly immutable pointer.
Loop Idioms
Standard for loop:
for (int i = 0; i < size; i++) {
process(arr[i]);
}
This can be done using a while:
int i = 0;
while (i < size) {
process(arr[i]);
i++;
}
or using a macro:
#define LOOP(i, n) for (int i = 0; i < (n); i++)
LOOP(i, size) {
process(arr[i]);
}