C has no concept of string objects, but it's handy to refer to certain pieces of memory as strings. These certain pieces of memory are a collection of consecutive characters ending in a NUL terminator. Because C has no concept of an object associated with those pieces of memory, the string.h header provides the common functions you might usually find associated with strings such as length, copy, concatenate, compare, and search. In addition to these functions, the header provides a type and a definition which we'll cover in this post.

size_t and NULL

The type defined in string.h is size_t, and the single macro it has is NULL... but we've seen these already in stddef.h. So, we have two options:

  • include stddef.h
  • redeclare/redefine them in string.h, or

Although both approaches are valid from a general programming perspective, The Standard says this in 7.1.3, Reserved identifiers

Each header declares or defines all identifiers listed in its associated
subclause, ...

Which means we can't include other headers to get a declaration or definition, it must be done directly in the header itself. So, even though it will duplicate code, we have to redefine the size_t type and macro for NULL.

Redeclare and redefine

I won't go over what size_t and NULL are since that was covered in the stddef.h post, so refer back to that if you need.

We'll first start off with copying some code from stddef.h into string.h:

#ifndef _STRING_H
#define _STRING_H

typedef unsigned long size_t;

#define NULL    ((void *) 0)

#endif /* _STRING_H */

Now, let's look at how this causes a slight challenge. I'll use an example piece of code here that pulls from both stddef.h and string.h to demonstrate the issues:

#include <stddef.h>
#include <string.h>

int
main(void)
{
    size_t testSize = 5;
    size_t *pSize = &testSize;

    *pSize = 7;

    return 0;
}

Compile this with the -Wpedantic flag at a minimum to produce the warning:

$ gcc -Wpedantic string_test.c -o string_test
In file included from string_test.c:2:0:
string.h:4:23: warning: redefinition of typedef ‘size_t’ [-Wpedantic]
 typedef unsigned long size_t;
                        ^
In file included from string.c:1:0:
stddef.h:4:23: note: previous declaration of ‘size_t’ was here
 typedef unsigned long size_t;
                        ^

What we didn't see an error with was the redefinition of the NULL macro, which is because we redefined it to the exact same thing. Had the text replacement been different, we would have seen a warning like so:

$ gcc -Wpedantic string_test.c -o string_test
In file included from string_test.c:3:0:
string.h:4:0: warning: "NULL" redefined [enabled by default]
 #define NULL    ((void *) 1)
 ^
In file included from string_test.c:2:0:
stddef.h:4:0: note: this is the location of the previous definition
 #define NULL    ((void *) 0)
 ^

To protect against these things, we can use something similar to header guards to indicate whether or not we have defined size_t or NULL already. These need to go in both stddef.h and string.h:

#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned long size_t;
#endif /* _SIZE_T */

#ifndef _NULL
#define _NULL
#define NULL    ((void *) 0)
#endif /* _NULL */

Since NULL is itself a macro, we could test for the definition directly rather than creating a new #define. However, this would allow a program to provide its own definition of NULL which would supersede, and possibly break, our library. By creating a new test macro inside of the reserved name space, our implementation does its best to prevent that from happening.

These checks will get rid of the warnings, but they also allow for size_t and NULL to become unsynchronized if we change their definitions at some point. Files including string.h could get a different definition than files including stddef.h, which could become difficult to debug. Although it could be error prone, the definitions for size_t and NULL will not change for a given implementation because The Standard won't change (within a given standard at least, e.g. C90 is set in stone at this point).

Conclusion

Due to The Standard, we must have the macro and type directly in the header, luckily we can copy them from stddef.h. However, we now need to introduce checks to prevent redefinitions and add those same checks to stddef.h.

#ifndef _STRING_H
#define _STRING_H

#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned long size_t;
#endif /* _SIZE_T */

#ifndef NULL
#define NULL    ((void *) 0)
#endif /* NULL */

#endif /* _STRING_H */

The initial test is simple as well, we just need to ensure that we can access the size_t type and the NULL macro:

#include <string.h>

int
stringTest(void)
{
    int ret         = 0;
    int *pTest      = NULL;
    size_t pSize    = sizeof(pTest);

    /* "Use" pSize to get rid of the warning */
    pSize = pSize;

    return ret;
}
comments powered by Disqus