Under the hood: The Importance of sizeof in C and C++
Tags: c, cpp, programming
Published: 2023-07-15 14:13:39

In typed programming languages, data types play a very important role because they allow programmers to write efficient and reliable code without worrying about the underlying details of how that data type is mapped into a memory. The compiler will allocate the proper amount of memory for that variable. 

Different data types can take up varying amounts of memory. Take an integer as an example, this data type usually takes up 4 bytes of memory (DWORD in x86 architecture), while a floating-point data type can take up 8 bytes (or more, depending on what compiler you are using, on that offers a lot of flexibility is [GMP](https://gmplib.org/))

Having said that, it makes much sense to use the operator (or function, depending on how you like to view it) `sizeof`. Imagine you want to write a code that allocates an array of doubles using `malloc`. The code would look something like this:

```c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv) {
    int n = 5;
    double *arr = (double*) malloc(n * sizeof(double));
    
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // initialize and print array
    for (int i = 0; i < n; i++) {
        arr[i] = i * 1.0f;
        printf("%lf ", arr[i]);
    }
    printf("\n");
  
    // free memory to avoid memory leaks
    free(arr);
    return 0;
}
```



If we replace

```c
    double *arr = (double*) malloc(n * sizeof(double));
```

with 

```c
    # DEFINE DBL_SIZE 8
	// previous code
    double *arr = (double*) malloc(n * DBL_SIZE);
```

This could cause an issue if this code is compiled by a different compiler or a different architecture. While this may not be apparent in this simple example, the next example(s) will help demonstrate the benefits of using `sizeof`



## Memory Management

One of the primary uses of `sizeof` is in memory management. Whenever we declare a variable in our program, the compiler allocates a certain amount of memory for it based on its data type. The `sizeof` operator allows us to determine the size of this memory allocation.

For example, let's say we declare an integer variable called `number` in our program. We can use the `sizeof` operator to determine the size of this variable in bytes:

```c
int number;
printf("Size of number: %d bytes\n", sizeof(number));
```

This will print "Size of number: 4 bytes" on most modern systems, as an integer is typically 4 bytes in size. We can use this information to ensure that we allocate the appropriate amount of memory for our variables and to optimize our program's memory usage.

## Data Manipulation

One other important usage of `sizeof` is in data manipulation. Using `sizeof` operator can help us determine the size of arrays, structures, and other complex data types. This information can be used to manipulate these data types in various ways.

Let's break down each type with an example

### Arrays

For example, let's say we have an array of integers called `numbers`. We can use the `sizeof` operator to determine the number of elements in this array:

```c
int numbers[10];
int num_elements = sizeof(numbers) / sizeof(numbers[0]);
printf("Number of elements in array: %d\n", num_elements);
```

This will produce the following output "Number of elements in array: 10", as there are 10 elements in the array. This information can be very useful for any operations that we want to do on any of the array elements (traversal, sorting, etc).

Thing might appear trivial in case of arrays, nevertheless, let's see the following example as it shows an interesting behavior

```c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 5;
    double *arr = (double*) malloc(n * sizeof(double));
    
    if (arr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }
    
    // initialize array and print original array
    printf("Original array: ");
    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * 1.0f;
        printf("%lf ", arr[i]);
    }
    printf("\n");
    
    // reallocate array using realloc()
    size_t new_n = 2 * (sizeof(arr) / sizeof(arr[0]));
    arr = (double*) realloc(arr, new_n * sizeof(double));
    
    if (arr == NULL) {
        printf("Memory reallocation failed!\n");
        return 1;
    }
    
    // print new array
    printf("New array: ");
    for (int i = 0; i < new_n; i++) {
        printf("%lf ", arr[i]);
    }
    printf("\n");
    
    // free memory
    free(arr);
    return 0;
}
```



The output of this code in the current form will be 

```c
Original array: 1.000000 2.000000 3.000000 4.000000 5.000000 
New array: 1.000000 2.000000 
```

This may seem strange initially but it's actually very straight forward. `sizeof` works if the size is known at compile time and to create dynamic arrays, you usually use a pointer (which will have a different size depending on the architecture of the CPU used), the size of a pointer is fixed, regardless of the data type. So when we create call `sizeof` on `arr`, it returns 8 (the size of the pointer) and the size of `arr[0]` is 8 (double), this yields (2 * (8/8)) which gives 2, that's why the new array only has two elements.

The main point here is that for dynamic arrays, **you should store the size of the array somewhere** as using `sizeof` will yield incorrect results

### Structures

Similarly, we can use the `sizeof` operator to determine the size of a structure in bytes:

```c
struct person {
  char name[50];
  int age;
  float height;
};
struct person john;
printf("Size of person struct: %d bytes\n", sizeof(john));
```

This will output "Size of person struct: 60 bytes". This looks odd, doesn't it? We know that the structure contains a char array of 50 bytes, an integer of 4 bytes, and a float of 4 bytes, this should add up to 58 (50 + 4 + 4), yet it shows 60 bytes so why is that? Let's see if unions exhibit a similar behavior

### Unions

Unions are another complex data type that can benefit from the `sizeof` operator. Unions allow us to store different data types in the same memory location. We can use the `sizeof` operator to determine the size of a union and its members.

For example, let's say we have a union called `my_union` that can hold either an integer or a float. We can use the `sizeof` operator to determine the size of the union and its members:

```c
union person {
    char name[50];
    int age;
    float height;
};
union person p;
printf("Size of person: %d bytes\n", sizeof(p));
printf("Size of name: %d bytes\n", sizeof(p.name));
printf("Size of age: %d bytes\n", sizeof(p.age));
printf("Size of height: %d bytes\n", sizeof(p.height));	
```

This will output 

```c
Size of person: 52 bytes
Size of name: 50 bytes
Size of age: 4 bytes
Size of height: 4 bytes
```

52? That looks stranger than the case with the struct.

## What is going on?

In C/C++, structs and unions are used to group related data together which is why how the data is aligned in memory can have a significant impact on program performance. 

Memory alignment refers to the process of aligning data in memory so that it can be accessed more efficiently by the CPU. This is achieved by padding the memory with extra bytes so that each data element is aligned to a memory address that is a multiple of its size. 

The padding ensures that the data elements are accessed efficiently by the CPU, which can result in faster program execution. The downside of padding is the wasted memory space, which some call memory overhead. 

To reduce memory overhead, the concept of packing is used, which involves reducing the amount of padding by rearranging the order of data elements. 

When padding data elements, the C standard also specifies alignment requirements for the beginning address of a struct or union. The standard mandates that the starting address of a struct or union must be aligned to the boundary of the largest member in the struct or union. This ensures that the entire struct or union is properly aligned in memory.

In the case of the person union, the largest data type has a size of 4 bytes. If we divide 50 by 4, it'll give us 12, which is 48 bytes. We have two bytes remaining in case we use the `name` member of the union, and since the largest data type is 4, then 48 + 4 = 52

The case of the struct is very similar to case of the union and since the struct encompasses all elements, the result will be (48 + 2 + 2 (alignment) + 4 + 4) which is equal to 60.

Consider a simpler struct that contains a double (which typically requires 8 bytes of memory) and an int (which typically requires 4 bytes of memory), then the struct itself must be aligned on an 8-byte boundary. This means that the starting address of the struct must be a multiple of 8. If the starting address of the struct is not aligned to 8 bytes, then the CPU may need to perform additional operations to extract the data, which can lead to slower program execution.

In addition to the C standard, some processors may have their own specific alignment requirements for optimal performance. It is important for C programmers to be aware of any processor-specific alignment requirements and to take them into consideration when designing and implementing their programs.

## Conclusion

In conclusion, the `sizeof` operator is an essential tool for C and C++ programming. It allows us to determine the size of variables and data types, manipulate data, and optimize our programs. By using the `sizeof` operator effectively, we can ensure that our programs are using memory efficiently and performing at their best.

Some tips to use `sizeof` effectively in your code:

- Always use `sizeof` when allocating memory for a data type.
- Use `sizeof` to determine the size of a structure or union.
- Avoid hard-coding values in your code by using `sizeof` instead.

By following these tips, you can write more efficient and maintainable code in C and C++.



## References

https://stackoverflow.com/questions/2117486/c-pointer-arithmetic
https://stackoverflow.com/questions/14171117/implementation-of-sizeof-operator
https://www.c-faq.com/struct/align.html
https://en.wikipedia.org/wiki/Sizeof
https://cplusplus.com/forum/beginner/279863/
https://stackoverflow.com/questions/14004704/find-malloc-array-length-in-c
https://stackoverflow.com/questions/891471/union-element-alignment
https://stackoverflow.com/questions/16703211/why-128bit-variables-should-be-aligned-to-16byte-boundary