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\$
4
  • \$\begingroup\$ This appears to be a cross-posting of: stackoverflow.com/questions/65588231/… \$\endgroup\$ Jan 6, 2021 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, 2021 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\$ Jan 6, 2021 at 22:01
  • \$\begingroup\$ Please edit your question so that the title describes the purpose of the code, rather than its mechanism. We really need to understand the motivational context to give good reviews. Thanks! \$\endgroup\$ Sep 22, 2022 at 6:23

3 Answers 3

5
\$\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, 2021 at 3:52
  • \$\begingroup\$ Why would -O2 affect warnings? \$\endgroup\$
    – Reinderien
    Jan 6, 2021 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\$ Jan 6, 2021 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\$ Jan 6, 2021 at 15:14
  • 1
    \$\begingroup\$ It is the lack of rounding. \$\endgroup\$ Jan 8, 2021 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\$ Jan 6, 2021 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\$ Jan 7, 2021 at 19:30
  • \$\begingroup\$ Would you consider int printf(const char * restrict format, ...); a variadic function? \$\endgroup\$ Jan 7, 2021 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\$ Jan 7, 2021 at 21:17
  • 1
    \$\begingroup\$ @chux: Sentence 4 of §6.5.2.2 ¶6 (which you quoted) addresses the situation in which the declaration in the current translation unit does not have a prototype, but the definition (in another translation unit or later in the current translation unit) does have a prototype. That sentence specifies that for variadic functions, this situation always leads to undefined behavior. All of this is explained in greater detail in the link that I provided in my third comment. \$\endgroup\$ Jan 8, 2021 at 20:53
2
\$\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 and acknowledge you have read our privacy policy.

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