When reading pointer types (excluding the variable name), split it into tokens at each asterisk (*), and read the tokens from rightmost to leftmost, replacing each asterisk with "[is] a pointer to".
Thus, const char *volatile p; reads as "p is volatile, a pointer to const char", meaning that the value of p, the pointer, is volatile (the compiler may not assume its value does not change unexpectedly); and it points to a string or char array that we promise not to try and change the contents of.
Because parameters are passed by value, a function that takes say char *s as a parameter, can modify both the pointer s and the value it points to, (*s), but only the latter change is visible to the caller.
In function calls, the name of an array "decays" to a pointer to the first element of an array. (This viewpoint is useful in that it also explains why sizeof(array)/sizeof(*array) returns the number of elements in array array, but passing the array to a function loses that size information, and instead yields (size of a pointer in bytes/size of the target type in bytes).)
These are not the rules C standard defines, but give you the correct intuition that you can then refine if need be.
If your function wants to modify a pointer, but doesn't return anything, return the modified pointer. For example:
const char *skip_whitespace(const char *src)
{
if (src) {
while (isspace((unsigned char)(*src)))
src++;
}
return src;
}
or say
const char *trim(char *src, size_t *len)
{
if (!src)
return ""; /* NULL turns into an empty string! */
/* Skip leading whitespace */
while (isspace((unsigned char)(*src)))
src++;
/* Trim out trailing whitespace */
char *end = src + strlen(src);
while (end > src && isspace((unsigned char)(end[-1])))
end--;
*end = '\0';
/* Save length, if requested. */
if (len)
*len = (size_t)(end - src);
return src;
}
In some cases you know the pointer won't be modified. Then, it is a good idea to consider what useful does the function calculate that a caller might be interested in. For example, you might need a function that trims a string converting all linear whitespace to a single space, returning the final length:
size_t trim_to_spaces(char *src)
{
/* NULL and empty strings have length zero */
if (!src || !*src)
return 0;
/* We keep src unmodified, but: */
char *s = src; /* Next source character */
char *d = src; /* Next destination character */
/* Skip any leading whitespace. */
while (isspace((unsigned char)(*s)))
s++;
/* Copy loop. */
while (*s) {
if (isspace((unsigned char)(*s))) {
/* Skip all consecutive/linear whitespace, */
do {
s++;
} while (isspace((unsigned char)(*s)));
/* and replace it with a single space. */
*(d++) = ' ';
} else {
*(d++) = *(s++);
}
}
/* Remove the possible final space from output. */
if (d > src && d[-1] == ' ')
d--;
/* String ends at d. */
*d = '\0';
/* Return the length of the result. */
return (size_t)(d - src);
}
The idea is that the caller can do either trim_to_spaces(stringvar); or len = trim_to_spaces(stringvar); depending on whether the length of the trimmed and space-compacted string is useful or not.
Before I decide on the function prototype/interface, I like to write a small test case for the key points in the algorithm I want to implement, to see exactly what would be useful there. Unless I'm implementing something I'm already familiar with, I often discover a completely different way of implementing the algorithm than what I originally envisioned, by just changing the helper functions suitably.
A common way to describe variable-length byte data like strings while modifying them, is to use a structure similar to
typedef struct {
char *data;
size_t size; /* Allocated size, i.e. data[0..size-1] are valid accesses */
size_t used; /* Number of bytes used data, i.e. data[0..used-1] */
} area;
#define AREA_INIT { NULL, 0, 0 }
I do believe structures like the above are what Kjelt referred to, above.
Instead of passing three pointers (one to the data pointer, one to the allocated size, and one to the current length of the contents in the buffer), you just pass a pointer to the structure. Any changes the function does to the pointer are not visible to the caller, but any changes it makes to the structure contents are: perfect.
The AREA_INIT macro is useful in that if variables of type area are initially set to AREA_INIT, we don't need a separate "init" function. That is, you declare e.g. area result = AREA_INIT;.
For example, to append data to an area would then be
int area_append(area *dst, const void *src, const size_t len)
{
if (!dst) {
errno = EINVAL;
return -1;
}
if (dst->used + len >= dst->size) {
const size_t new_size = dst->used + len + 1; /* TODO: Growth policy? */
void *new_data = realloc(dst->data, new_size);
if (!new_data) {
/* Old area is intact, but we cannot get more room. So this fails, but is not fatal. */
errno = ENOMEM;
return -1;
}
dst->size = new_size;
dst->data = new_data;
}
if (len > 0) {
memcpy(dst->data + dst->used, src, len);
dst->used += len;
}
/* In case it is string data, we append a nul byte, just to be nice. */
dst->data[dst->used] = '\0';
return 0;
}
and to append a C string,
int area_append_string(area *dst, const char *src)
{
const size_t len = (src) ? strlen(src) : 0;
return area_append(dst, src, len);
}
When destroying/freeing an area, we return it into the initial state, so it can be reused:
void area_free(area *dst)
{
if (area) {
free(area->data); /* Note: free(NULL) is safe, and does nothing. */
area->data = NULL;
area->size = 0;
area->used = 0;
}
}