The obvious problems to me are:
You are not using your function's parameter.
You are using scanf
.
You are using recursion where you should not use recursion.
You are mixing several different concerns (node creation, list organization, user input, input validation).
You are not handling errors.
Using your function's parameter
Since you don't use the parameter, we can ask, "what should the parameter be?"
This is a pitfall for linked lists. If your list uses a data structure to store list metadata, then the parameter would be a pointer to that data structure.
If the list just uses a single variable to store a pointer to the first node in the list, then your parameter should be a pointer to that variable. (So you can change the actual pointer to NULL, or replace NULL with a new pointer when you create the first node.)
Thus, your parameter would be something like:
...
crearLista(nodoNum ** primer)
{
...
}
And you would call it something like:
nodoNum * La_lista = NULL;
... main(...) {
... crearLista(&La_lista) ...
}
Using scanf
.
Q: What is the difference between:
- [[something cringe-worthy and incredibly painful]]; and
- using
scanf()
?
A: If someone held a gun to your head, you would agree to #1.
The scanf
function is very, very hard to use correctly. It is even harder to use effectively. The various ways it can fail are difficult for even experienced developers to understand and avoid. Did your pattern allow for white space? Will your pattern consume trailing newlines? What is the difference between " 1\n"
and "1 \n"
and "1\n"
? The general answer to dealing with scanf
is not to deal with scanf
!
Here is a list of ways to avoid scanf
, which you might want to bookmark
Recursion
Recursion can be a useful tool, which is why it is taught in even introductory programming courses. However, in C it is a tool that you must use judiciously.
Recursion implicitly uses the call stack to avoid looping. The cost of doing this is the creation of extra stack frames, which consume memory in the stack region.[1]
This is important because the C stack is generally a limited resource. See this question & answer for more information.
What this means for you is that a linked list, which is normally an effectively-infinite data structure, will be constrained by the limits of the run-time stack. And, of course, when you reach those limits you will not encounter a recoverable error, but instead will suffer a stack overflow.
Instead of using recursion to input more nodes, use a looping control structure instead:
nodoNum ** registro; // parameter
// like this:
for (;;) {
nodoNum * nn = create_nn_from_input(stdin);
*registro = nn;
registro = &nn->nn_sig;
}
// OR like this:
while (true) {
nodoNum * nn = create_nn_from_input(stdin);
*registro = nn;
registro = &nn->nn_sig;
}
// OR like this:
nodoNum * nn = NULL;
do {
nn = create_nn_from_input(stdin);
if (not_null(nn)) {
*registro = nn;
registro = &nn->nn_sig;
}
} while (not_null(nn));
Separation of concerns
In the object-oriented world this is sometimes referred to as the Single Responsibility Principle (or "SRP").
In essence, a function should have a single concern in order to clearly convey what it is doing, to avoid errors, and to be reusable.
Your function has to deal with multiple concerns, which creates confusion and introduces chances for errors to occur.
Consider reading one single number from the user. You are using scanf
, which is not a great idea. In the event of non-numeric input, how do you recover from errors? (You don't.)
Now consider reading one node from the user. If the user enters zero (0), we will agree that is a valid sentinel value for "no node/end of input", so how would it work? What happens if the user enters a valid first value (non-zero), but then you see end-of-file for the second value?
Now consider what happens if you are unable to allocate memory for your next node. What happens?
In every case, the answer to "what happens if ... occurs" is going to be either "You have to restructure your code" or "You have to write more code".
When your different concerns are isolated in separate functions, both of those tasks are simple. When your different concerns are mixed together, they interfere with each other. It can be hard to reorganize your code when you have to think about the effects of changes on multiple different things. It can be really hard to add more code when you have to keep mixing in additional concerns.
I suggest that you identify the various things you are doing and create separate functions to perform each of them.
You may be alarmed to find that each of those functions will be small, and very simple. That is the objective!
A small, simple function will have few bugs. It will be easily understood. It can be easily re-used. These are all good things. ;-)
You may also find that "small simple functions" will tend to grow. When you realize that the concern of a function is doing exactly one simple thing, you may start thinking up "edge cases" to handle. Eventually you will have a very robust function that is not quite so small or simple, but which reliably does what it should do in even the most stressful conditions.
Handling errors
C is famous for its error handling. Not in a good way.
One of the challenges of being a C developer is knowing how to handle errors. When you start work on an existing project, there will be an existing error-handling mechanism that you will hopefully learn. When you are writing your own code de novo, you have to provide the error handling yourself.
For a simple command-line program, the easiest way to handle errors is to print some kind of message and then exit the program immediately. As your program becomes larger, you will want to allow the user to retry certain operations, or perform some kind of "graceful shutdown," or just abort whatever was in process and go back to an earlier state. These are all valid things to do, but C doesn't actually support you in any of them.
My suggestion would be to write yourself a failure function and a FAIL
macro, and call it to handle errors for you:
noreturn void fail(const char * file, signed long line, const char * mesg, ...);
#define FAIL(...) fail(__FILE__, __LINE__, __VA_ARGS__)
The idea is that you make a decision about how failure is to be handled. Maybe it's just print-a-message-and-exit. (In fact, for beginner-level code that's always the right answer.) Then you implement that behavior inside fail()
. Then you always call FAIL
which will call fail
on failure.
If you don't want to print-and-exit, there is one place to change the behavior. (Maybe you want to add logging. Maybe you want to longjmp
to main
and start over. Whatever, it's your code.)
Doing this let's you develop coding idioms that make sense for you. If you're going to call malloc
, you'll want to check for errors. So...
if (malloc(size) == NULL) FAIL("Could not malloc(%u) memory for <reason>");
In fact, you might find yourself always writing your calls to malloc that way, in which case you can just create a new function for your personal library.
Obviously the same thing is true for fopen
, strdup
, etc. If all you do with errors is call FAIL,
then write a wrapper function to simplify your code even more. If you do more than call FAIL
, then ask yourself if you need to add lines to your fail
function to simplify your code even more. ;-)
[1] To truly be called recursion, the memory used must be from the Stack region. If the memory does not come from the Stack region, what you are doing is merely sparkling iteration.