string.h
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;
}