I am trying to get a better understanding of how one would structure an API in C.
- I create a struct
Person
- I have a
init
function that sets data on that struct - I have multiple “helper” functions that work on that struct
I’m wondering if the following code in C can be considered idiomatic from the point of view of more seasoned C developers.
#include <stdio.h>
#include <stdlib.h>
typedef struct Person Person;
struct Person
{
unsigned int age;
unsigned int weight;
Person *next_person;
};
void person_init(Person *p, unsigned int age, unsigned int weight, Person *next_person)
{
p->age = age;
p->weight = weight;
p->next_person = next_person;
}
void person_print(Person *p)
{
printf("person is %dy oldn", p->age);
printf("person weight is %dkgn", p->weight);
}
int main(void)
{
Person p1, p2, p3, p4;
Person *p_cur;
person_init(&p1, 28, 80, &p2);
person_print(&p1);
person_init(&p2, 58, 93, &p3);
person_print(&p2);
person_init(&p3, 60, 60, &p4);
person_print(&p3);
person_init(&p4, 78, 50, NULL);
person_print(&p4);
printf("==================n");
p_cur = &p1;
while (p_cur != NULL) {
person_print(p_cur);
p_cur = p_cur->next_person;
}
return 0;
}
In particular I’m unsure about the signature of the functions in general and about the use of a Person *
pointer.
When is it OK to not-pass a pointer but the struct/char array directly?
In most APIs I have seen so far, a pointer to the struct/char array is passed, like snprintf(char * s, ... )
. But sometimes, like in getline(char **lineptr, ...)
even a char **
is passed. Why? When make the distinction?
4
Your question is a matter of opinion, however, it looks like Person
is generally supposed to be allocated in the heap (read about C dynamic memory allocation).
Then you should define and document how Person
-s are allocated and who is in charge of free
-ing them and when.
I would suggest to have a function to create Person
-s like
Person* make_person(unsigned p_age, p_weight) {
Person* p = malloc(sizeof(Person));
if (!p) { perror("malloc"); exit(EXIT_FAILURE); };
p->age = p_age;
p->weight = p_weight;
p->next_person = NULL;
}
We don’t know what is next_person
for. We might guess you want to have a global linked list of Person
-s. Then you want some global pointer to the head of that list, the make_person
should modify that global pointer, etc.
I would recommend against having Person
automatic variables on the call stack (in particular, because that would limit the number of Person
-s your program can handle, often to some staticly known small number; you may want to be able to handle millions of Person
-s) Intuitively the call stack should not grow a lot (on current desktops with ordinary OSes like Linux or Windows, a stack limit of a few megabytes is typical) But we don’t know what are you really coding.
Be afraid of undefined behavior, memory leaks. Read something about garbage collection. In a few months, you might be interested by Boehm’s conservative garbage collector. If your system has it, consider using valgrind -for debugging purposes.
You might want a void destroy_person(Person*p)
function which would either recursively (or iteratively) call destroy_person
on the p->next_person
or remove it from your global list, then call free(p)
…
3
Different linked libraries may use different heaps. This makes dynamic allocation and memory management tricky.
The most common way is to make Person an opaque struct (declared but not defined in the public header) and the programmer using it must use the functions in the library to get access to the attributes.
This means that allocating and freeing will be done inside the library using functions like Person* create_person(...)
and void destroy_person(Person*)
.
4