In the previous installment, I rambled on about the difference of “char *line;” and “char line[];”. The first is a “pointer to char(s)” and the second is “an array of char(s)”. But, when you pass them into a function, they both are treated as a pointer to those chars.
One “benefit” of using “line[]” was that you could use sizeof(line) on it and get the byte count of the array. This is faster than using strlen().
But if you pass it into a function, all you have is a pointer so strlen() is what you have to use.
While you can’t pass an “array of char” into a function as an array of char, you can pass a structure that contains an “array of char” and sizeof() will work on that:
#include <stdio.h>
#include <stdlib.h> // for EXIT_SUCCESS
#include <string.h>
typedef struct
{
char buffer[80];
} MyStruct;
void function (MyStruct test)
{
printf ("sizeof(test.buffer) = %zu\n", sizeof(test.buffer));
}
int main(void)
{
MyStruct test;
strncpy (test.buffer, "This is a test", sizeof(test.buffer));
function (test);
return EXIT_SUCCESS;
}
You may notice that was passing a copy of the structure in, but stay with me for a moment.
If you have a function that is supposed to copy data into a buffer:
#define VERSION_STRING "1.0.42b-alpha"
void GetVersion (char *buffer)
{
if (NULL != buffer)
{
strcpy (buffer, VERSION_STRING);
}
}
…you can easily have a buffer overrun problem if the function writes more data than is available in the caller’s buffer. Because of this potential problem, I add a size parameter to such functions:
void GetVersion (char *buffer, size_t bufferSize)
{
if (NULL != buffer)
{
// Copy up to bufferSize bytes.
strncpy (buffer, VERSION_STRING, bufferSize);
}
}
As long as the caller passes the correct parameters, this is safe:
char buffer[20];
GetVersion (buffer, 20);
But the caller could still screw up:
char buffer[20];
GetVersion (buffer, 200); // oops, one too many zeros
But if you use a structure, it is impossible for the caller to mess it up (though, of course, they could mess up the structure on their side before calling your function). The compiler type checking will flag if the wrong data type is passed in. The “buffer” will always be the “buffer.” No chance of a “bad pointer” or “buffer overrun” crashing the program.
To allow the buffer inside the structure to be modified, pass it in by reference:
#include <stdio.h>
#include <stdlib.h> // for EXIT_SUCCESS
#include <string.h>
#define VERSION_STRING "1.0.42b-alpha"
typedef struct
{
char buffer[80];
} MyStruct;
void GetVersion (MyStruct *test)
{
strncpy (test->buffer, VERSION_STRING, sizeof(test->buffer));
}
int main(void)
{
MyStruct test;
GetVersion (&test);
printf ("Version: %s\n", test.buffer);
return EXIT_SUCCESS;
}
Using this approach, you can safely pass a “buffer” into functions and they can get the sizeof() the buffer to ensure they do not overwrite anything.
But wait, there’s more…
It is pretty easy for a function to get out of control if you are trying to get back more than one thing. If you just want an “int”, that’s easy…
int GetCounter ()
{
static int s_count = 0;
return s_count++;
}
But if you wanted to get the major, minor, patch and build version, you end up passing in ints by reference to get something like this:
void GetVersion (int *major, int *minor, int *patch, int *build)
{
if (NULL != major)
{
*major = MAJOR_VERSION;
}
if (NULL != minor)
{
*major = MINOR_VERSION;
}
if (NULL != patch)
{
*major = PATCH_VERSION;
}
if (NULL != build)
{
*major = BUILD_VERSION;
}
}
Of course, anytime pointers are involved, the caller could pass in the wrong pointer and things could get screwed up. Plus, look at all those NULL checks to make sure the pointer isn’t 0. (This does not help if the pointer is pointing to some random location in memory.)
#include <stdio.h>
#include <stdlib.h> // for EXIT_SUCCESS
#define MAJOR_VERSION 1
#define MINOR_VERSION 0
#define PATCH_VERSION 0
#define BUILD_VERSION 42
typedef struct
{
int major;
int minor;
int patch;
int build;
} VersionStruct;
VersionStruct GetVersion ()
{
VersionStruct ver;
ver.major = MAJOR_VERSION;
ver.minor = MINOR_VERSION;
ver.patch = PATCH_VERSION;
ver.build = BUILD_VERSION;
return ver;
}
int main(void)
{
VersionStruct ver;
ver = GetVersion ();
printf ("Version: %u.%u.%u.%u\n",
ver.major, ver.minor, ver.patch, ver.build);
return EXIT_SUCCESS;
}
If you are concerned about overhead of passing structures, you can pass them by reference (pointer) and the compiler should still catch if a wrong pointer type is passed in:
#include <stdio.h>
#include <stdlib.h> // for EXIT_SUCCESS
#define MAJOR_VERSION 1
#define MINOR_VERSION 0
#define PATCH_VERSION 0
#define BUILD_VERSION 42
typedef struct
{
int major;
int minor;
int patch;
int build;
} VersionStruct;
void GetVersion (VersionStruct *ver)
{
if (NULL != ver)
{
ver->major = MAJOR_VERSION;
ver->minor = MINOR_VERSION;
ver->patch = PATCH_VERSION;
ver->build = BUILD_VERSION;
}
}
int main(void)
{
VersionStruct ver;
GetVersion (&ver);
printf ("Version: %u.%u.%u.%u\n",
ver.major, ver.minor, ver.patch, ver.build);
return EXIT_SUCCESS;
}
However, when dealing with pointers, there is always some risk. While the compiler will catch passing in the wrong structure pointer, there are still ways the caller can screw it up. For instance, void pointers:
int main(void)
{
void *nothing = (void*)0x1234;
GetVersion (nothing);
return EXIT_SUCCESS;
}
Yep. Crash.
...Program finished with exit code 139
Press ENTER to exit console.
Give someone access to a function in your DLL and they might find a way to crash the program as simply as using a void pointer.
It is a bit trickier when you pass the full structure:
typedef struct
{
int x;
} BogusStruct;
int main(void)
{
BogusStruct ver;
ver = GetVersion ();
return EXIT_SUCCESS;
}
Compiler don’t like:
main.c: In function ‘main’:
main.c:38:11: error: incompatible types when assigning to type ‘BogusStruct’ from type ‘VersionStruct’
38 | ver = GetVersion ();
| ^~~~~~~~~~
main.c:36:17: warning: variable ‘ver’ set but not used [-Wunused-but-set-variable]
36 | BogusStruct ver;
| ^~~
And you can’t really cast a return value like this:
int main(void)
{
BogusStruct ver;
(VersionStruct)ver = GetVersion ();
return EXIT_SUCCESS;
}
Compiler don’t like:
main.c: In function ‘main’:
main.c:38:5: error: conversion to non-scalar type requested
38 | (VersionStruct)ver = GetVersion ();
| ^
Though maybe you could cast it if it was passed in as a parameter:
void ShowVersion (VersionStruct ver)
{
printf ("Version: %u.%u.%u.%u\n",
ver.major, ver.minor, ver.patch, ver.build);
}
int main(void)
{
BogusStruct ver;
ShowVersion ((VersionStruct)ver);
return EXIT_SUCCESS;
}
Compiler still don’t like:
main.c: In function ‘main’:
main.c:44:5: error: conversion to non-scalar type requested
44 | ShowVersion ((VersionStruct)ver);
| ^~~~~~~~~~~
Hmm. Is there a way to screw this up? Let me know in the comments.
Until then…