Please Note: This post is focusing on pre-C99. The reason being is that it is aimed at the embedded C programmer who tends to be working with pre-C99 based cross-compilers. Also I have split it into two as it became my larger, due to feedback, than first anticipated.
On the surface declarations and definitions in C are pretty straight-forward; but once we start introducing the concepts of scope, storage-duration, linkage and namespace life is not so simple.
Contents
Program Objects (Variables)
- if the statement has an “=” it’s a definition?
- otherwise, if it has “extern” and no “=” it’s a declaration?
- otherwise it’s a tentative-definition that may become a declaration or a actual-definition
Object Definitions
int ev = 20; /* definition – reserves enough memory to hold an int */
Object Declaration
Usage
When compiling a source file, a variable must be declared before it is used or it will result in a compiler error.
int main(void) { ev = 10; /* fails to compile as ev has not been declared */ return 0; }
int ev = 20; /* definition – allocates 32-bits */
extern int ev; /* declaration – no memory reserved but defines sizeof(ev) */
int main(void) { ev = 10; /* okay to use ev as declared, knows to read (say) 32-bits; k = 20 */ return 0; }
int ev = 20; /* definition – memory reserved here and initialised */
int ev = 20; /* definition and implicit-declaration: reserves memory */
int main(void) { ev = 10; /* okay to use ev as declared (implicitly) */ return 0; }
extern int ev; /* 1st declaration */
extern int ev; /* 2nd declaration */
int main(void) { ev = 10; /* okay to use ev as declared */ return 0; } int ev = 20; /* definition */
int ev = 20; /* actual definition */
int td; /* tentative definition */
int main(void) { ... return 0; }
If an actual definition is found later in the source file, then the tentative definition just acts as a declaration. If the end of the source file is reached and no actual definition is found, then the tentative definition acts as an actual definition (and implicit declaration) with an initialisation of 0 (zero).
int ev; /* tentative definition becomes declaration */ int td; /* tentative definition become actual definition initialised to 0 */
int main(void) { ... return 0; } int ev = 20; /* actual definition */
extern int ev = 20; /* actual-definition */
I’m sure someone can (and will) tell me why this is useful, but in my 25 years of doing C I’ve never had need to use it. I my view anyone found doing this should be made to sit in the corner wearing a hat with a big ‘D’ on it!
extern int(ev);
int(ev);
int(ev) = 20;
Functions
Function declarations and definitions are in many ways simpler than variables. A function definition includes the function’s body. e.g.
void f(int p) /* definition and implicit-declaration */ { ... }
int main(void) { f(10); /* okay to call f as declared */ return 0; }
A function’s declaration (typically called its prototype) makes the compiler aware there is a valid function with this identifier. e.g.
void f(int p); /* declaration */
int main(void) { f(10); /* okay to call f as declared */ return 0; } void f(int p) /* definition */ { // ... }
- the validity of the identifier
- the storage required to pass any parameters (by stack or register)
- the storage required for any return information
void f(int); /* declaration */
Before we move on, there are two problem areas we need to cover. First, let’s look at the following snippet:
int main() { f(20); /* call f with no declaration */ return 0; }
void f(int i) /* definition and implicit-declaration */ { // ... }
Here we are trying to call a function that hasn’t been declared. As probably expected, this code fails to compile, but not for the reason you probably assume. Earlier I stated that an identifier must be declared before being used otherwise you get a compiler error. Unfortunately this only applies to variables and not functions!
f(20);
int f();
error: ‘f’ : redefinition; different basic types
If we change f’s return type to int, then this code will compile quite happily.
int main(void) { f(20); /* call f implicit-designator of int f() */ return 0; }
int f(int i) /* definition’s designator matches implicit-designator */ { // ... }
If the return type is omitted, int is assumed.
This baggage is still evident today, as the following code should compile successfully:
int main() { f(20); /* call f implicit-designator of int f() */ return 0; }
f(int i) /* definition’s designator has implicit return type of int */ { // ... }
warning: 'f' undefined; assuming extern returning int
Never ignore this warning. Some compilers (such as IAR) allow a non-standard extension requiring function prototypes. Note that C++ also requires prototypes, thus closing this loophole.
Can it get worse? Oh yes, much worse.
With a function definition, then empty parameter list is the same as void.
void f() /* definition and implicit-decln of void f(void) */ { // ... }
int main() { f(20); /* error as call doesn’t match decln */ return 0; }
void f(); /* declaration */
void f(void); /* prototype-declaration – not the same as above */
void f(); /* declaration */
int main(void)
{
f(20); /* okay to call f as declared */
return 0;
}
void f(int i) /* definition */
{
// ...
}
void f(); /* declaration */
int main(void)
{
f(20); /* okay to call f as declared!!! */
return 0;
}
void f(void) /* definition */
{
// ...
}
Guideline: For all function always supply a function-prototype.
So hopefully that lays the groundwork of declarations and definitions we can now start addressing the concepts of scope, storage-duration, linkage and namespace.
Afternote:
void f() /* definition and implicit-decln of void f(void) */ { // ... }
int main(void) { f(20); /* error a call doesn’t match decln */ return 0; }
- Navigating Memory in C++: A Guide to Using std::uintptr_t for Address Handling - February 22, 2024
- Embedded Expertise: Beyond Fixed-Size Integers; Exploring Fast and Least Types - January 15, 2024
- Disassembling a Cortex-M raw binary file with Ghidra - December 20, 2022
Co-Founder and Director of Feabhas since 1995.
Niall has been designing and programming embedded systems for over 30 years. He has worked in different sectors, including aerospace, telecomms, government and banking.
His current interest lie in IoT Security and Agile for Embedded Systems.
Thanks. This is very helpful.
I'm forwarding your posts to my colleagues who teach courses in embedded systems. Please keep up the good work!
[quote]
Microsoft compiler bug – this code should fail to compile. Microsoft compiles, whereas both IAR and Keil fail.
[/quote/
gcc compiles it as well on Linux.
To make it fail, you should have
[code]
void f(void arg)
{
//...
}
[/code]
Pingback: Sticky Bits » Blog Archive » Scope and Lifetime of Variables in C