1
\$\begingroup\$

I've written the following sample program as a way to help improve my understanding of passing pointers to a function, and it currently works:

#include "stdio.h"

typedef struct Car {
    char*           name;
    unsigned int    price;
} Car;

void print_cars(Car* cars[]) {
    for (int i=0; cars[i] != NULL;i++) {
        printf("\n<Car: %s, Price: $%d>", car->name, car->price);
    }
}
void depreciate(Car* cars[]) {
    for (int i=0; cars[i] != NULL; i++) {
        cars[i]->price = cars[i]->price * 0.95;
    }
}

int main(int argc, char* argv[]) {

    Car chevy = {.name = "Chevy", .price = 45000};
    Car mazda = {.name = "Mazda", .price = 30000};
    Car ferrari = {.name = "Ferrari", .price = 200000};

    Car* my_cars[] = {&chevy, &mazda, &ferrari, NULL};
    print_cars(my_cars);
    printf("\n--------- Depreciating -----------");
    depreciate(my_cars); // <-- ignoring this
    print_cars(my_cars);

    return 1;

}

Ignoring the line that modifies the value of the struct, would it be possible to do the print_cars function without using a pointer? When I tried doing it I would receive the error:

new5.c:13:27: error: invalid operands to binary expression ('Car' (aka 'struct Car') and 'void *')

Which I think may be related to using the NULL in the my_cars array to signal the end of it, but wasn't positive. Additionally, is using NULL in a non-char array a common way to show that the end has been reached? Where else could this be improved?

\$\endgroup\$
3
  • \$\begingroup\$ This appears to be a cross-posting of: stackoverflow.com/questions/65588231/… \$\endgroup\$ – Craig Estey Jan 6 at 2:53
  • \$\begingroup\$ @CraigEstey this is a follow-up. Here I'm iterating over arrays, then depreciating and doing other things. The question you link asks between the difference between print_cars and print_cars2 (print_cars2 doesn't exist in the above). \$\endgroup\$ – David542 Jan 6 at 3:51
  • \$\begingroup\$ "would it be possible to do the print_cars function without using a pointer?" --> Yes. Yet not worth using a non-pointer approach. \$\endgroup\$ – chux - Reinstate Monica Jan 6 at 22:01
4
\$\begingroup\$

While your code "works" for you, there are a lot of small mistakes in it. I'm listing them straight from top to bottom.

#include "stdio.h"

Since stdio.h is a header from the standard library, as opposed to a header you define yourself in your project, you should include it using #include <stdio.h> instead.

typedef struct Car {
    char*           name;
    unsigned int    price;
} Car;

The name of the car should be a const char * since later, you assign string literals like "Chevy" to that field, and string literals are read-only.

To prevent this possible bug in the future, let the C compiler warn you about issues like these. For GCC, pass the compile options -Wall -Wextra -Werror -O2. Since you didn't say which development environment you use, you have to find out how to add these options yourself.

void print_cars(Car* cars[]) {
    for (int i=0; cars[i] != NULL;i++) {
        printf("\n<Car: %s, Price: $%d>", car->name, car->price);
    }
}

Since print_cars does not modify the cars, its parameter should be const Car* cars[], just add the const.

There should be a single space around assignment operators. That is, write int i = 0 instead of int i=0. There should be a single space after the semicolon as well.

When a program produces output, the end-of-line marker \n should go to the end of the line, not to the beginning.

void depreciate(Car* cars[]) {
    for (int i=0; cars[i] != NULL; i++) {
        cars[i]->price = cars[i]->price * 0.95;
    }
}

Instead of writing cars[i]->price twice, you can make use of the combined assignment operator *= by writing: cars[i]->price *= 0.95.

The floating-point number 0.95 cannot be represented exactly in binary. You should experiment a little with a car price that would end up evenly. But even then you could end up with rounding errors. I wrote the following test snippet, which shows that for some prices, the actual result ends up lower than expected, because what you see as 0.95 is really 0.9499999999999999555910790149937383830547332763671875 for the computer.

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 1000; i++) {
        printf("%d: %.20f, %.20f, %d\n",
            i,
            i * 0.95,
            i * 95 / 100.0,
            i * 95 / 100);
    }
}

To keep the programming simple, you should stick to the last variant i * 95 / 100, until you get more experienced with floating point numbers.

int main(int argc, char* argv[]) {

    ...
    return 1;
}

The function main is special regarding its return value. Returning 1 means failure, returning 0 means success. Many other C functions return 0 for success (just like main) and -1 for failure. Others return non-zero for success and zero for failure. Things vary a lot, and you have to learn the meaning for each function individually. There are some patterns though, so it's not completely chaotic.

All in all, it's a well-written program. After modifying the few details I mentioned, it's perfect.

\$\endgroup\$
8
  • \$\begingroup\$ thanks for your time and feedback! \$\endgroup\$ – David542 Jan 6 at 3:52
  • \$\begingroup\$ Why would -O2 affect warnings? \$\endgroup\$ – Reinderien Jan 6 at 4:21
  • 1
    \$\begingroup\$ @Reinderien Some compilers don't generate all warnings without it. (e.g): void set(int *x) { } int main(void) { int i; set(&i); return i; } With gcc -Wall compiles cleanly. If we add: -O, we get: warning: ‘i’ is used uninitialized in this function [-Wuninitialized] Note that gcc [correctly] detected that set did not actually assign a value to *x, so it was flagged. If we add *x = 3; to set, no warning occurs. With -O, gcc did cross function analysis. If we have just void set(int *x);, no warning because set is extern and gcc can't see the body of set ... \$\endgroup\$ – Craig Estey Jan 6 at 7:55
  • 1
    \$\begingroup\$ "After modifying the few details I mentioned, it's perfect." -- Nitpick: No, after that, it is not yet perfect. It still causes undefined behavior due to a wrong conversion specifier, as pointed out in my answer. :-) \$\endgroup\$ – Andreas Wenzel Jan 6 at 15:14
  • 1
    \$\begingroup\$ It is the lack of rounding. \$\endgroup\$ – chux - Reinstate Monica Jan 8 at 3:19
3
\$\begingroup\$

In the line

printf("\n<Car: %s, Price: $%d>", car->name, car->price);

the variable car->price is of type unsigned int, but you are using %d to print it, which is intended for signed int. This causes undefined behavior according to §7.21.6.1 ¶9 of the ISO C standard. However, on most platforms, this will probably just cause the number to be reinterpreted as signed.

The correct conversion specifier for unsigned int is %u.


Additionally, is using NULL in a non-char array a common way to show that the end has been reached? Where else could this be improved?

It is more common to store the length of the array in a separate variable.

In your specific use-case, it doesn't matter which way you do it. However, generally, I would recommend to store the length in a separate variable instead. For example, if you want to add random-access with bounds-checking, then it would be better to store the length in a separate variable. Otherwise, you would have to traverse the entire array for every such access.

\$\endgroup\$
9
  • \$\begingroup\$ "This causes undefined behavior" --> Not UB when the value is representable as an int and unsigned. C17dr § 6.5.2.2 6. Still, best to match type/specifier. \$\endgroup\$ – chux - Reinstate Monica Jan 6 at 21:58
  • \$\begingroup\$ @chux: The paragraph you refer to addresses cases in which functions are declared without a parameter type list, i.e. cases where the compiler must guess the parameter types. That paragraph specifies in which cases mismatches between the guessed parameter types and actual paramater types are acceptable and in which cases they lead to undefined behavior. In the case of a variadic function, a missing parameter list in the function declaration always leads to undefined behavior. I fail to see how that paragraph is relevant here, as it has nothing to do with printf conversion specifiers. \$\endgroup\$ – Andreas Wenzel Jan 7 at 19:30
  • \$\begingroup\$ Would you consider int printf(const char * restrict format, ...); a variadic function? \$\endgroup\$ – chux - Reinstate Monica Jan 7 at 20:22
  • \$\begingroup\$ @chux: Yes, that is a prototype declaration of a variadic function. The paragraph you reference only applies to translation units without a prototype declaration before the function call. \$\endgroup\$ – Andreas Wenzel Jan 7 at 21:17
  • \$\begingroup\$ Paragraph has "If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or ..." so is at odds with "only applies to translation units without a prototype declaration". I am using draft N2176. \$\endgroup\$ – chux - Reinstate Monica Jan 7 at 21:32
1
\$\begingroup\$

is using NULL in a non-char array a common way to show that the end has been reached?

NULL, the null pointer constant, is often used to indicate the end of an array of pointers as done nicely below. It is not a question if the array if "non-char" or not. It is a question if the array elements are pointers or not.

Car* my_cars[] = {&chevy, &mazda, &ferrari, NULL};

An alternative is to pass the array (or allocated memory) count. This is not the sizeof of the data, but the number of elements in the data.

Car* my_cars[] = {&chevy, &mazda, &ferrari};  // No NULL here.
const size_t my_cars_n = sizeof my_cars_n / sizeof my_cars_n[0];
print_cars(my_cars_n, my_cars);

// or

size_t my_cars_n = 3;
Car* my_cars = malloc(sizeof *my_cars * my_cars_n);
my_cars[0] = &chevy;
my_cars[1] = &mazda;
my_cars[2] = &ferrari;
print_cars(my_cars_n, my_cars);
free(my_cars);

A char array often uses a null character, to indicate the end of a string.

Do not mix NULL, the null pointer constant, with (char) '\0', a null character. The first is used with pointers, the second with strings.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.