views:

275

answers:

9

This is literally the first thing I've ever written in C, so please feel free to point out all it's flaws. :) My issue, however is this: if I write the program the way I feel is cleanest, I get a broken program:

#include <sys/queue.h> 

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Removed prototypes and non related code for brevity */

int
main()
{
    char    *cmd = NULL; 
    unsigned int acct = 0; 
    int amount = 0; 
    int done = 0; 

    while (done==0) {
        scanf ("%s %u %i", cmd, &acct, &amount);

        if (strcmp (cmd, "exit") == 0)
            done = 1;
        else if ((strcmp (cmd, "dep") == 0) || (strcmp (cmd, "deb") == 0))
            debit (acct, amount);
        else if ((strcmp (cmd, "wd") == 0) || (strcmp (cmd, "cred") == 0))
            credit (acct, amount);
        else if (strcmp (cmd, "fee") == 0)
            service_fee(acct, amount);
        else
            printf("Invalid input!\n");
    }
    return(0);
}

void
credit(unsigned int acct, int amount)
{
}

void
debit(unsigned int acct, int amount)
{
}

void
service_fee(unsigned int acct, int amount)
{
}

As it stands, the above generates no errors at compile, but gives me a segfault when ran. I can fix this by changing the program to pass cmd by reference when calling scanf and strcmp. The segfault goes away and is replaced by warnings for each use of strcmp at compile time. Despite the warnings, the affected code works.

warning: passing arg 1 of 'strcmp' from incompatible pointer type

As an added bonus, modifying the scanf and strcmp calls allows the program to progress far enough to execute return(0), at which point the thing crashes with an Abort trap. If I swap out return(0) for exit(0) then everything works as expected.

This leaves me with two questions: why was the original program wrong? How can I fix it better than I have?

The bit about needing to use exit instead of return has me especially baffled.

+6  A: 

You aren't allocating memory for cmd, so it's NULL.

Try declaring it with some space:

char cmd[1000];
dcp
+10  A: 

This is happening because of the scanf statement.

Look how cmd is pointing to NULL. When scanf is run, it writes to the address of cmd, which is NULL, thus generating a segfault.

The solution is to create a buffer for cmd, such as:

char cmd[20];

Now, your buffer can hold 20 characters. However, you now need to worry about buffer overflows if a user enters more than 20 characters.

Welcome to C.

EDIT: Also, note that your credit, debit, and service fee functions won't work as expected as you have them written. This is because the parameters are passed by value, not by reference. This means that after the method returns, any changes will be discarded. If you want them to modify the arguments you give, try changing the methods to:

void credit(unsigned int *acct, int *amount)

And then call them like:

credit(&acct, &amt);

Doing that will pass the parameters by reference, meaning that any changes you make inside the credit function will affect the parameters, even after the function returns.

samoz
Love the last sentence in this answer ;)
rubenvb
It is worth noting that the next questions *"So how do I manage this without an arbitrary limit on the length of the input?"* and that this question has been answered several times on SO. The short answer is use `fgets` or `getline`, and some note on who these things work can be found http://stackoverflow.com/questions/2532425/c-read-line-from-file-without-knowing-the-line-length/2532450#2532450 amongst many.
dmckee
Ah, that makes sense. And dmckee preempted my next question. :)For what it is worth, the credit, debit, and service fee functions are actually used to manipulate an account struct (part of the snipped code) using the values passed in. The values passed in are not modified. I suppose it would be a good idea to declare the parameters const?
Keifer
In that case, then yes. Const probably would be a good safety net to use.
samoz
+2  A: 

This :

char    *cmd = NULL; 


Should be:

char cmd[100]; 



Please note: You should ensure that the string the user will input in cmd has length less than 100 or n

Betamoo
the second one is c++in that case, he should just use std::string
Aif
Yes, you are right... I will correct it..
Betamoo
For down-voters, please state a reason...
Betamoo
I think the C++ comments were a little confusing but +1 since you corrected them. :)
BobbyShaftoe
+1  A: 

In your example, scanf() is beind passed a null pointer.

char    *cmd = NULL; 

scanf() won't allocate space for the string - you'll need to allocate somewhere for the string to go.

char   cmd[80];
...
scanf ("%s",cmd);

Your getting a segmentation fault because scanf() is attempting to write its output to unallocated space.

David Lively
A: 

Others have pointed out the error in your program, but for a better understanding of pointers, since you are just starting to learn C, look at this question at SO.

omermuhammed
+4  A: 

As others have pointed out, you haven't allocated anything for scanf to read into. But you should also test the return value of scanf:

if ( scanf ("%s %u %i", cmd, &acct, &amount) != 3 ) {
   // do some error handling
}

The scanf function returns the number of succesful conversions, so if someone types in XXXX when you expect an integer you want to be able to detect and deal with it. But frankly, user interface code that uses scanf() is never really going to be proof against this sort of thing. scanf() was actually intended for reading formatted files, not random input from humans.

anon
That's what return values are for!
BobbyShaftoe
+1  A: 

cmd is initialised to a null pointer which never points at any memory. scanf doesn't check that cmd is valid before trying to write to what cmd points to.

A preliminary solution instead creates some space for cmd to point to:

char cmd[30]; /* DANGEROUS! */

but this is a very dangerous move because you may still get segfaults if the input is longer than expected and scanf tries to write to cmd[30] and beyond.

For this reason scanf is considered unsafe and should not be used in production code. Safer alternatives include using fgets to read a line of input and sscanf to process it.

Sadly, C I/O is very difficult to get right without introducing the possibility of buffer overflows into your program. You always need to be thinking about how much memory you have available and whether it will be enough to store the longest possible input you could receive. You also need to check the return values of most I/O functions for errors.

Philip Potter
A: 

Your basic problem is that you haven't allocated memory for your string. In C, you are responsible for all memory management. If you declare variables on the stack, this is easy. With pointers, it's a little more difficult. Since you have the line char* str = NULL, when you attempt to scanf into it, you write bytes to NULL, which is illegal. What the %s specifier does is write into what str points to; it can't change str, as parameters are passed by value. This is why you have to pass &acct instead of just acct.

So how do you fix it? You need to provide memory where the read-in string can live. Something like char str[5] = "". This makes str a five-element character array, big enough to hold "exit" and its terminating zero byte. (Arrays decay into pointers at the slightest provocation, so we're fine on that front.) However, this is dangerous. If the user enters the string malicious, you're going to write "malic" into str and the bytes for "icious\0" into whatever comes after that at memory. This is a buffer overflow, and is a classic bug. The simplest way to fix it here is to require the user to enter a command of at most N letters, where N is the longest command you have; in this case, N = 4. Then you can tell scanf to read in at most four characters: scanf("%4s %u %i", cmd, &acct, &amt). The %4s says "read in at most four characters", so you can't screw up other memory. However, note that if the user enters malformed 3 4, you won't be able to find the 3 and the 4, since you'll be looking at ormed.

The reason you could do scanf("%s %u %i", &cmd, &acct, &amount) is that C is not type-safe. When you gave it &cmd, you gave it a char**; however, it was happy to treat it as a char*. Thus, it wrote bytes over cmd, so if you passed in the string exit, cmd might (if it were four bytes wide and had the appropriate endianness) be equal to 0x65786974 (0x65 = e, 0x78 = x, 0x69 = i, 0x74 = t). And then the zero byte, or any other bytes you passed in, you would start to write over random memory. If you change it at strcmp too, however, it will also treat the value of str as a string, and everything will be consistent. As for why return 0; fails but exit(0) works, I'm not sure, but I have a guess: you may have been writing over the return address of main. That's stored on the stack too, and if it happens to come after cmd in the stack layout, then you might be zeroing it or scribbling on it. Now, exit must do its cleanup manually, jumping to the right locations, etc. However, if (as I think is the case, although I'm not sure) main behaves like any other function, its return jumps to the space on the stack stored as the return address (which is probably a cleanup routine of some sort). However, since you've scribbled over that, you get an abort.

Now, there are a couple of other small improvements you could make. First, since you're treating done as a boolean, you ought to loop while (!done) { ... }. Second, the current setup requires you to write exit 1 1 to exit the program, even though the 1 1 bit shouldn't be necessary. Third, you should check to see if you have successfully read all three arguments, so you don't get errors/inconsistencies; for instance, if you don't fix this, then the input

deb 1 2
deb 3 a

Calls debit(1,2) and debit(3,2), while still leaving the a in the input to trip you up. Finally, you should exit cleanly on EOF, rather than looping forever doing the last thing you did. If we put this together, we get the following code:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void credit(unsigned int acct, int amount);
void debit(unsigned int acct, int amount);
void service_fee(unsigned int acct, int amount);

int main() {
  char         cmd[5] = ""; 
  unsigned int acct   = 0; 
  int          amount = 0; 
  int          done   = 0; 

  while (!done) {
    if (feof(stdin)) {
      done = 1;
    } else {
      if (scanf("%4s", cmd, &acct) != 1) {
        fprintf(stderr, "Could not read the command!\n");
        scanf(" %*s "); /* Get rid of the rest of the line */
        continue;
      }

      if (strcmp(cmd, "exit") == 0) {
        done = 1;
      } else {
        if (scanf(" %u %i", &acct, &amount) != 2) {
          fprintf(stderr, "Could not read the arguments!\n");
          scanf(" %*s "); /* Get rid of the rest of the line */
          continue;
        }

        if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
          debit(acct, amount);
        else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
          credit(acct, amount);
        else if (strcmp(cmd, "fee") == 0)
          service_fee(acct, amount);
        else
          fprintf(stderr, "Invalid input!\n");
      }
    }
    /* Cleanup code ... */
  }

  return 0;
}

/* Dummy function bodies */

void credit(unsigned int acct, int amount) {
  printf("credit(%u, %d)\n", acct, amount);
}

void debit(unsigned int acct, int amount) {
  printf("debit(%u, %d)\n", acct, amount);
}

void service_fee(unsigned int acct, int amount) {
  printf("service_fee(%u, %d)\n", acct, amount);
}

Note that if there is no "cleanup code", you can replace all your uses of done with break and remove the declaration of done, giving the nicer loop

while (1) {
  if (feof(stdin)) break;

  if (scanf("%4s", cmd, &acct) != 1) {
    fprintf(stderr, "Could not read the command!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if (strcmp(cmd, "exit") == 0) break;

  if (scanf(" %u %i", &acct, &amount) != 2) {
    fprintf(stderr, "Could not read the arguments!\n");
    scanf(" %*s "); /* Get rid of the rest of the line */
    continue;
  }

  if ((strcmp(cmd, "dep") == 0) || (strcmp(cmd, "deb") == 0))
    debit(acct, amount);
  else if ((strcmp(cmd, "wd") == 0) || (strcmp(cmd, "cred") == 0))
    credit(acct, amount);
  else if (strcmp(cmd, "fee") == 0)
    service_fee(acct, amount);
  else
    fprintf(stderr, "Invalid input!\n");
}
Antal S-Z
Out of curiosity, why the downvote? Is there something wrong with the answer, or something I ought to improve?
Antal S-Z
A: 

In order to fully understand what is going on here you need to understand some basics about C pointers. I suggest you take a look here if you are really that new to C:

http://www.cprogramming.com/tutorial.html#ctutorial

The most common cause of segfaults are detailed here:

http://www.cprogramming.com/debugging/segfaults.html

logancautrell