Storage Class in C

In this tutorial, we will discuss the storage classes in C language.

You can also read the atomic variables in Linux kernel, typedef in c, pointers in c, compilation steps, and memory layout in C.

Storage classes in C language Tutorial

Before moving ahead, let’s quickly understand the difference between the lifetime and the scope of a variable.

Scope of variable

In which function the value of the variable would be available. Basically, this is the visibility of the variable.

Life of the variable

How long the variable would exist in the program.

Scope of variables

In C language there are four types of Scope is available.

Scope Meaning
 

File scope

Starts at the beginning of the file (also called a translation unit) and ends at the end of the file. It refers only to those identifiers that are declared outside of all functions. File scope identifiers are visible throughout the entire file. Variables that have file scope are global.
 

Block scope

Begins with the opening { of a block and ends with its associated closing }. However, block scope also extends to function parameters in a function definition. That is, function parameters are included in a function’s block scope. Variables with block scope are local to their block.

Function prototype scope

Identifiers declared in a function prototype. visible within the prototype.
 

Function scope

Begins with the opening { of a function and ends with its closing }. Function scope applies only to labels. A label is used as the target of a goto statement, and that label must be within the same function as the goto.

Storage Classes in C

In the C language, the lifetime and scope of a variable are defined by its Storage Classes in C.

The following are four types of Storage Classes in C.

  • Automatic (auto)
  • Register
  • External (extern)
  • Static

Auto

Features :

Storage

Memory

Scope

Local / Block Scope

Lifetime

Exists as long as Control remains in the block

Default initial Value

Garbage

The auto storage classes in C is the default storage class for all local variables.

{
  int mount;
  auto int month;
}

The example above defines two variables within the same storage class. By default, all the local variables are auto. ‘auto‘ can only be used within functions, i.e., local variables.

Register

Features :

Storage

CPU Register

Scope

Local to the block

Life time

Local to the block in which variable is
declared

Default initial Value

Garbage

A value stored in a CPU register can always be accessed faster than the one that is stored in memory. Therefore, if a variable is used at many places in a program, it is better to declare its storage class as register.

There are no guarantees that we have declared any variable as register and it would be stored in the CPU register! Why? The reason is that CPU registers are limited, and they may be busy doing some other task. In that case, that variable works as default storage classes in C i.e. automatic storage class.

Note: Any variable stored in the CPU register or not depends on the capacity of the microprocessor. For example, if the microprocessor has a 16-bit register then it cannot hold a float value or a double value, which require 4 and 8 bytes respectively. However, if you use the register storage class for float, double variable then you won’t get any error messages because the compiler treats it as the default storage class i.e. auto storage class.

All looping programs where a variable is frequently used, declare a variable as register.

Extern

Features :

Storage

Memory

Scope

Global / File Scope

Lifetime

As long as the program’s execution
doesn’t come to an end.

Default initial Value

Zero

The extern specifier gives the declared variable external storage class. The principal use of extern is to specify that a variable is declared with external linkage elsewhere in the program. To understand why this is important, it is necessary to understand the difference between a declaration and a definition. A declaration declares the name and type of a variable or function. A definition causes storage to be allocated for the variable or the body of the function to be defined. The same variable or function may have many declarations, but there can be only one definition for that variable or function.

When an extern specifier is used with a variable declaration then no storage is allocated to that variable and it is assumed that the variable has already been defined elsewhere in the program. When we use the extern specifier the variable cannot be initialized because with the extern specifier variable is declared, not defined.

In the following sample C program, if you remove extern int x; you will get an error “Undeclared identifier ‘x’” because variable x is defined later than it has been used in printf. In this example, the extern specifier tells the compiler that variable x has already been defined and it is declared here for the compiler’s information.

#include <stdio.h>

extern int x;

int main()
{
    printf("x: %d\n", x);
}

int x=10;

Also, if you change the statement extern int x; to extern int x = 50; you will again get an error “Redefinition of ‘x’” because with extern specifier the variable cannot be initialized if it is defined elsewhere. If not then extern declaration becomes a definition.

Mostly this extern keyword will be used when we want to share the global variable in two or more .c files.

Note that extern can also be applied to a function declaration, but doing so is redundant because all function declarations are implicitly extern.

Static Variables

Features :

Storage

Memory

Scope

Block Scope

Lifetime

Value of the variable persists between different function calls

Default initial Value

Zero

Static variables affect both the lifetime and the scope.

Impact on Life Time

static variables are those variables whose lifetime remains equal to the lifetime of the program like global variables. Any local or global variable can be made static depending upon what the logic expects out of that variable. Let’s consider the following example :

#include<stdio.h>

char** func_Str();

int main(void)
{
  char **ptr = NULL;
  ptr = func_Str();
  printf("\n [%s] \n",*ptr);
  return 0;
}

char** func_Str()
{
  char *p = "Linux";
  return &p;
}

In the code above, the function ‘func_str()’ returns the address of the pointer ‘p’ to the calling function which uses it further to print the string ‘Linux’ to the user through ‘printf()’. Let’s look at the output :

$ ./static
[Linux]
$

The output above is as expected. So, is everything fine here? Well, there is a hidden problem in the code. More specifically, its the return value of the local character pointer (char *p) in the function ‘func_Str()’. The value being returned is the address of the local pointer variable ‘p’. Since ‘p’ is local to the function, so as soon as the function returns, the lifetime of this variable is over and hence its memory location becomes free for other operations.

Let’s prove this observation. Look at the code below :

#include<stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
  char **ptr1 = NULL;
  char **ptr2 = NULL;
  
  ptr1 = func1_Str();
  printf("\n [%s] \n",*ptr1);
  
 ptr2 = func2_Str();
 printf("\n [%s] \n",*ptr2);
 
 printf("\n [%s] \n",*ptr1);

return 0;
}

char** func1_Str()
{
  char *p = "Linux";
  return &p;
}

char** func2_Str()
{
  char *p = "Windows";
  return &p;
}

In the code above, now there are two functions ‘func1_Str()’ and ‘func2_Str()’. The logical problem remains the same here too. Each of these functions returns the address of its local variable. In the main() function, the address returned by the func1_Str() is used to print the string ‘Linux’ (as pointed by its local pointer variable) and the address returned by the function func2_Str() is used to print the string ‘Windows’ (as pointed by its local pointer variable). An extra step towards the end of the main() function is done by again using the address returned by func1_Str() to print the string ‘Linux’.

Now, let’s see the output :

$ ./static
[Linux]
[Windows]
[Windows]

The output above is not as per expectations. The third print should have been ‘Linux’ instead of ‘Windows’. Well, I’d rather say that the above output was expected. It’s just the correct scenario that exposed the loophole in the code.

Let’s go a bit deeper to see what happened after the address of the local variable was returned. See the code below :

#include<stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
  char **ptr1 = NULL;
  char **ptr2 = NULL;
  
  ptr1 = func1_Str();
  printf("\n [%s] :: func1_Str() address = [%p], its returned address is [%p]\n",*ptr1,(void*)func1_Str,(void*)ptr1);

  ptr2 = func2_Str();
  printf("\n [%s] :: func2_Str()address = [%p], its returned address is [%p]\n",*ptr2,(void*)func2_Str,(void*)ptr2);
  printf("\n [%s] [%p]\n",*ptr1,(void*)ptr1);

  return 0;
}

char** func1_Str()
{
  char *p = "Linux";
  return &p;
}

char** func2_Str()
{
  char *p = "Windows";
  return &p;
}

The code is above is modified to print the address of the functions and the address of their respective local pointer variables. Here is the output :

$ ./static
[Linux] :: func1_Str() address = [0x4005d5], its returned address is [0x7fff705e9378]
[Windows] :: func2_Str()address = [0x4005e7], its returned address is [0x7fff705e9378]
[Windows] [0x7fff705e9378]

The above output makes it clear that once the lifetime of the local variable of the function ‘func1_Str()’ gets over then same memory address is being used for the local pointer variable of the function ‘func2_Str()’ and hence the third print is ‘Windows’ and not ‘Linux’.

So, now we see the root of the problem is the lifetime of the pointer variables. This is where the ‘static’ storage class comes to rescue. As already discussed the static Storage classes make the lifetime of a variable equal to that of the program. So, let’s make the local pointer variables as static and then see the output :

#include<stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
  char **ptr1 = NULL;
  char **ptr2 = NULL;

  ptr1 = func1_Str();
  printf("\n [%s] :: func1_Str() address = [%p], its returned address is [%p]\n",*ptr1,(void*)func1_Str,(void*)ptr1);

  ptr2 = func2_Str();
  printf("\n [%s] :: func2_Str()address = [%p], its returned address is [%p]\n",*ptr2,(void*)func2_Str,(void*)ptr2);
  printf("\n [%s] [%p]\n",*ptr1,(void*)ptr1);

  return 0;
}

char** func1_Str()
{
  static char *p = "Linux";
  return &p;
}

char** func2_Str()
{
  static char *p = "Windows";
  return &p;
}

Note that in the code above, the pointers were made static. Here is the output :

$ ./static
[Linux] :: func1_Str() address = [0x4005d5], its returned address is [0x601028]
[Windows] :: func2_Str()address = [0x4005e0], its returned address is [0x601020]
[Linux] [0x601028]

So, we see that after making the variables as static, the lifetime of the variables becomes equal to that of the program.

Impact on Scope

In the case where code is spread over multiple files, the static storage type can be used to limit the scope of a variable to a particular file. For example, if we have a variable ‘count’ in one file and we want to have another variable with the same name in some other file, then, in that case, one of the variables has to be made as static. The following example illustrates it :

Here we use two files (static.c and static_1.c)

static.c

#include<stdio.h>

int count = 1;

int main(void)
{
  printf("\n count = [%d]\n",count);
  return 0;
}

static_1.c

#include<stdio.h>

int count = 4;

int func(void)
{
  printf("\n count = [%d]\n",count);
  return 0;
}

Now, when both the files are compiled and linked together to form a single executable, here is the error that is thrown by GCC :

$ gcc -Wall static.c static_1.c -o static
/tmp/ccwO66em.o:(.data+0x0): multiple definition of `count'
/tmp/ccGwx5t4.o:(.data+0x0): first defined here
collect2: ld returned 1 exit status
$

So, we see that GCC complains of multiple declarations of the variable ‘count’.

As a corrective measure, this time one of the ‘count’ variable is made static :

static.c

#include<stdio.h>

static int count = 1;

int main(void)
{
  printf("\n count = [%d]\n",count);
  return 0;
}

static_1.c

#include<stdio.h>

int count = 4;

int func(void)
{
  printf("\n count = [%d]\n",count);
  return 0;
}

Now, if both the files are compiled and linked together :

$ gcc -Wall static.c static_1.c -o static
$

So, we see that no error is thrown this time because static has limited the scope of the variable ‘count’ in file static.c to the file itself.

Static Functions

By default any function that is defined in a C file is extern. This means that the function can be used in any other source file of the same code/project (which gets compiled as a separate translational unit). Now, if there is a situation where the access to a function is to be limited to the file in which it is defined or if a function with the same name is desired in some other file of the same code/project then the functions in C can be made static.

Extending the same example that was used in the previous section, suppose we have two files :

static.c

#include<stdio.h>

void func();

int main(void)
{
  func();
  return 0;
}

void funcNew()
{
  printf("\n Hi, I am a normal function\n");
}

static_1.c

#include<stdio.h>

void funcNew();

int func(void)
{
  funcNew();
  return 0;
}

If we compile and run the code above :

$ gcc -Wall static.c static_1.c -o static
$ ./static
Hi, I am a normal function
$

So, we see that the function funcNew() was defined in one file and successfully got called from the other. Now, if the file static_1.c wants to have its own funcNew(), ie :

static_1.c

#include<stdio.h>

void funcNew();

int func(void)
{
  funcNew();
  return 0;
}

void funcNew()
{
  printf("\n Hi, I am a normal function\n");
}

Now, if both the files are compiled and linked together :

$gcc -Wall static.c static_1.c -o static
/tmp/ccqI0jsP.o: In function `funcNew':
static_1.c:(.text+0x15): multiple definition of `funcNew'
/tmp/ccUO2XFS.o:static.c:(.text+0x15): first defined here
collect2: ld returned 1 exit status
$

So, we see that the compiler complains of multiple definitions of the function funcNew(). So, we made the funcNew() in static_1.c as static :

static_1.c

#include<stdio.h>

static void funcNew();

int func(void)
{
  funcNew();
  return 0;
}

static void funcNew()
{
  printf("\n Hi, I am also a normal function\n");
}

Now, if we compile, then we see that the compiler never complains :

$ gcc -Wall static.c static_1.c -o static
$ ./static
Hi, I am also a normal function
$

Similarly, if static.c wants that its funcNew() should be accessible from within static.c only then in that case funcNew() in static.c can be made static.

Final Comparison

Features Automatic Storage Class Register Storage Class Static Storage Class External Storage Class
Keyword auto register static extern
Initial value Garbage Garbage Zero Zero
Storage Memory CPU register Memory Memory
Scope scope limited, local to block scope limited, local to block scope limited, local to block Global
Life limited to the life of block, where it is defined limited to the life of block, where it is defined Value of variable persist between different function calls Global, till the program execution
Memory Location Stack Register memory Segment Segment

If you have any doubts just comment below. 

You can also read the below tutorials.

Linux Device Driver TutorialsC Programming Tutorials
FreeRTOS TutorialsNuttX RTOS Tutorials
RTX RTOS TutorialsInterrupts Basics
I2C Protocol – Part 1 (Basics)I2C Protocol – Part 2 (Advanced Topics)
STM32 TutorialsLPC2148 (ARM7) Tutorials
PIC16F877A Tutorials8051 Tutorials
Unit Testing in C TutorialsESP32-IDF Tutorials
Raspberry Pi TutorialsEmbedded Interview Topics
Reset Sequence in ARM Cortex-M4BLE Basics
VIC and NVIC in ARMSPI – Serial Peripheral Interface Protocol
Bootloader Tutorials
5 1 vote
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
4
0
Would love your thoughts, please comment.x