St.Antario
St.Antario

Reputation: 27375

Understanding linkage of identifiers

I'm reading the Standard: N1570 and came across some misunderstanding. I wrote the following simple example:

test.h:

#ifndef TEST_H
#define TEST_H

extern int second;

#endif //TEST_H

test.c:

#include "test.h"

enum test_enum{ 
    first,
    second
};

But it fails to compile with the error:

error: ‘second’ redeclared as different kind of symbol
     second
     ^~~~~~

This is strange, because Section 6.4.4.3#2 specifies:

2 An identifier declared as an enumeration constant has type int.

I our case the enumeration constant has file scope so I expected it to compile fine.

I rewrote the example above as follows: main.c:

extern int second;
int main(int argc, char const *argv[])
{
    printf("Second: %d\n", second);
}

And now linker complains:

undefined reference to `second'

Why? It should find the definition in the test.c because as the Section 6.2.2#5 specifies:

If the declaration of an identifier for an object has file scope and no storage-class specifier, its linkage is external.

Upvotes: 0

Views: 85

Answers (2)

Eric Postpischil
Eric Postpischil

Reputation: 222486

Objects and Constants Are Different Things

Your citation of 6.4.4.3 2, that an enumeration constant has type int, suggests you think that because extern int second and enum { second } declare second to be an int, that these two declarations of second may refer to the same thing. That is not correct.

extern int second declares second to be the name of an object (a region of memory) that will hold an int. enum { second } declares second to be an enumeration constant which will have a particular value. The enumeration constant has no object (no memory) associated with it. An int object and an int constant are different things, and you may not use the same identifier for them in the same scope.

Not All Declarations Are Definitions

Regarding your question about the link error, “undefined reference to ‘second’”, although test.c may contain external int second (since it is provided by the included test.h, this is not a definition of second. It is only a declaration, which tells the compiler that the name refers to an object. It does not define the object. Alternatively, if test.c contains enum { second }, this only declares second to be a constant. It does not define an object.

The rules for what is a definition are a bit complicated due to the history of programming language development. For identifiers of objects declared at file scope, there are essentially four cases:

  • A declaration with extern is only a declaration, not a definition. Example: extern int second;.
  • A declaration with an initializer is a definition. Example: int second = 2;.
  • A declaration without extern and without an initializer is a tentative definition. If no definition appears in the translation unit (source file being compiled, with all included files), the tentative definition becomes a definition. Example: int second;.

The linkage is not helpful here. The extern int second in test.c and the extern int second in main.c may refer to the same object due to linkage, but no object for them to refer to was defined. Or, in the alternative, if test.c contains enum { second }, then it does not define an object named second, so there is no object that the extern int second in main.c can refer to.

Upvotes: 2

David C. Rankin
David C. Rankin

Reputation: 84551

To do what you are attempting, you simply need to refactor you code a bit so that test.[ch] does not see a redeclaration of second. The problem is the symbol second is defined once as extern int second; and then again as the symbol within the enum. You can't have both visible within the same file.

To do that you can write test1.h using a second preprocessor conditional similar to:

#ifndef TEST_H
#define TEST_H

#ifdef USE_ENUM
    enum test_enum{
        first,
        second
    };
#else
    extern int second;
#endif

#endif

Where depending on whether USE_ENUM is defined, the code will use either the symbol provided by the enum, where if not, then you need to define second in test1.c

#include "test1.h"

#ifdef USE_ENUM
    char stub (void)    /* stub to prevent empty compilation unit */
    { return 0; }
#else
    int second = 2;
#endif

(note the use of a stub function to prevent an empty compilation unit if USE_ENUM is defined -- as there would be no code in test1.c otherwise)

Now all that is required is to include test1.h in your file containing main() and passing the compiler define -DUSE_ENUM as a compiler option depending on which code you want to use, e.g.

#include <stdio.h>
#include "test1.h"

int main (void) {

    printf ("second: %d\n", second);

}

Compile Using int second defined in test.c

Example:

$ gcc -Wall -Wextra -pedantic -std=c11 -o bin/main1 main1.c test1.c

Example Use/Output

When USE_ENUM is not defined, then the definition of second defined in test1.c and accessed via extern will result in second having the value 2, e.g.

$ ./bin/main1
second: 2

Compile Using enum defined in test.h

Example:

$ gcc -Wall -Wextra -pedantic -std=c11 -o bin/main1 main1.c test1.c -DUSE_ENUM

Example Use/Output

When USE_ENUM is defined, then the value for the symbol second is provided by the enum in test1.h, e.g.

$ ./bin/main1
second: 1

While this is a slight refactoring of what you were attempting, I don't see another way of doing both without using the preprocessor conditional.

Upvotes: 2

Related Questions