Skip to main content
Code Review

Return to Answer

replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/
Source Link

In addition to ChrisWue's comments ChrisWue's comments, I'd add:

In addition to ChrisWue's comments, I'd add:

In addition to ChrisWue's comments, I'd add:

added 2609 characters in body
Source Link
Gareth Rees
  • 50.1k
  • 3
  • 130
  • 210
  1. The comment for Rational_Construct says that it returns:

     A Rational object X such that X.Top == Numerator and X.Bottom = Denominator.
    

but in fact this is not true, since the denominator may be negated. Instead it should say something like:

 A Rational object R such that R.Top/R.Bottom == Numerator/Denominator.
  1. Your operator implementations don't call Rational_Construct: they just build a new Rational structure however they please. This means that the consistency checks in Rational_Construct (such as making sure that the denominator is positive) are skipped.

It would result in shorter and more reliable code if you always called Rational_Construct. For example:

 Rational Rational_Negate(const Rational R) {
 return Rational_Construct(-R.Top, R.Bottom);
 }
 Rational Rational_Add(const Rational Left, const Rational Right) {
 return Rational_Construct(Left.Top * Right.Bottom + Right.Top * Left.Bottom,
 Left.Bottom * Right.Bottom);
 }
  1. Even small computations with your rationals result in integer overflow. For example, consider the following program:

     int main(int argc, char **argv) {
     Rational sixteenth = Rational_Construct(1, 16);
     Rational sum = Rational_Construct(0, 1);
     int i;
     for (i = 0; i < 10; ++i) {
     sum = Rational_Add(sum, sixteenth);
     printf("%d/%d\n", sum.Top, sum.Bottom);
     }
     return 0;
     }
    

Now, because you're using C's fixed-size integers, there are bound to be computations whose results are too large to be represented. But it would be better to raise an error when the result is too large, rather than causing undefined behaviour. And you should at the very least make some effort to ensure that integer overflow doesn't happen in small examples like this: when we add 1/16 ten times we ought to be able to get 10/16.

An easy way to avoid integer overflow in small examples is to always reduce rationals to their lowest terms (thus turning 2/4 as 1/2). For example, you could do this:

 #include <assert.h> /* for the assert() prototype */
 #include <limits.h> /* for INT_MIN */
 #include <stdlib.h> /* for the abs() prototype */
 Rational Rational_Construct(int Numerator, int Denominator) {
 /* Ensure that Denominator is positive */
 assert(Denominator != 0);
 if (Denominator < 0) {
 assert(Numerator != INT_MIN);
 assert(Denominator != INT_MIN);
 Numerator = -Numerator;
 Denominator = -Denominator;
 }
 /* Find the greatest common divisor of Numerator and Denominator. */
 int a = abs(Numerator), b = Denominator;
 while (b) {
 int c = a % b;
 a = b;
 b = c;
 }
 /* Reduce the fraction to lowest terms. */
 Rational newRational;
 newRational.Top = Numerator / a;
 newRational.Bottom = Denominator / a;
 return newRational;
 }

Now the program I gave above prints:

 1/16
 1/8
 3/16
 1/4
 5/16
 3/8
 7/16
 1/2
 9/16
 5/8

(Of course this doesn't solve the integer overflow problem in the general case, but at least it allows small programs like this to complete successfully.)

  1. Even small computations with your rationals result in integer overflow. For example, consider the following program:

     int main(int argc, char **argv) {
     Rational sixteenth = Rational_Construct(1, 16);
     Rational sum = Rational_Construct(0, 1);
     int i;
     for (i = 0; i < 10; ++i) {
     sum = Rational_Add(sum, sixteenth);
     printf("%d/%d\n", sum.Top, sum.Bottom);
     }
     return 0;
     }
    

Now, because you're using C's fixed-size integers, there are bound to be computations whose results are too large to be represented. But it would be better to raise an error when the result is too large, rather than causing undefined behaviour. And you should at the very least make some effort to ensure that integer overflow doesn't happen in small examples like this: when we add 1/16 ten times we ought to be able to get 10/16.

  1. The comment for Rational_Construct says that it returns:

     A Rational object X such that X.Top == Numerator and X.Bottom = Denominator.
    

but in fact this is not true, since the denominator may be negated. Instead it should say something like:

 A Rational object R such that R.Top/R.Bottom == Numerator/Denominator.
  1. Your operator implementations don't call Rational_Construct: they just build a new Rational structure however they please. This means that the consistency checks in Rational_Construct (such as making sure that the denominator is positive) are skipped.

It would result in shorter and more reliable code if you always called Rational_Construct. For example:

 Rational Rational_Negate(const Rational R) {
 return Rational_Construct(-R.Top, R.Bottom);
 }
 Rational Rational_Add(const Rational Left, const Rational Right) {
 return Rational_Construct(Left.Top * Right.Bottom + Right.Top * Left.Bottom,
 Left.Bottom * Right.Bottom);
 }
  1. Even small computations with your rationals result in integer overflow. For example, consider the following program:

     int main(int argc, char **argv) {
     Rational sixteenth = Rational_Construct(1, 16);
     Rational sum = Rational_Construct(0, 1);
     int i;
     for (i = 0; i < 10; ++i) {
     sum = Rational_Add(sum, sixteenth);
     printf("%d/%d\n", sum.Top, sum.Bottom);
     }
     return 0;
     }
    

Now, because you're using C's fixed-size integers, there are bound to be computations whose results are too large to be represented. But it would be better to raise an error when the result is too large, rather than causing undefined behaviour. And you should at the very least make some effort to ensure that integer overflow doesn't happen in small examples like this: when we add 1/16 ten times we ought to be able to get 10/16.

An easy way to avoid integer overflow in small examples is to always reduce rationals to their lowest terms (thus turning 2/4 as 1/2). For example, you could do this:

 #include <assert.h> /* for the assert() prototype */
 #include <limits.h> /* for INT_MIN */
 #include <stdlib.h> /* for the abs() prototype */
 Rational Rational_Construct(int Numerator, int Denominator) {
 /* Ensure that Denominator is positive */
 assert(Denominator != 0);
 if (Denominator < 0) {
 assert(Numerator != INT_MIN);
 assert(Denominator != INT_MIN);
 Numerator = -Numerator;
 Denominator = -Denominator;
 }
 /* Find the greatest common divisor of Numerator and Denominator. */
 int a = abs(Numerator), b = Denominator;
 while (b) {
 int c = a % b;
 a = b;
 b = c;
 }
 /* Reduce the fraction to lowest terms. */
 Rational newRational;
 newRational.Top = Numerator / a;
 newRational.Bottom = Denominator / a;
 return newRational;
 }

Now the program I gave above prints:

 1/16
 1/8
 3/16
 1/4
 5/16
 3/8
 7/16
 1/2
 9/16
 5/8

(Of course this doesn't solve the integer overflow problem in the general case, but at least it allows small programs like this to complete successfully.)

Source Link
Gareth Rees
  • 50.1k
  • 3
  • 130
  • 210

In addition to ChrisWue's comments, I'd add:

  1. In this block of code:

     if (Numerator < 0 && Denominator < 0) {
     Numerator = -Numerator;
     Denominator = -Denominator;
     } else if (Numerator >= 0 && Denominator < 0) {
     Numerator = -Numerator;
     Denominator = -Denominator;
     } 
    

both branches do the same thing, so you could combine them into one condition:

 if (Numerator < 0 && Denominator < 0 || Numerator >= 0 && Denominator < 0) {
 Numerator = -Numerator;
 Denominator = -Denominator;
 }

Now both parts of the condition have Denominator < 0, so you could write:

 if ((Numerator < 0 || Numerator >= 0) && Denominator < 0) {
 Numerator = -Numerator;
 Denominator = -Denominator;
 }

Now it's clear that the condition Numerator < 0 || Numerator >= 0 is always true, so it can be dropped, leaving:

 if (Denominator < 0) {
 Numerator = -Numerator;
 Denominator = -Denominator;
 }
  1. In the comment for Rational_Construct you specify a precondition Denominator != 0. But if the caller passes in zero, then you just print a message to standard output. It would be better to write:

     assert(Denominator != 0)
    

so that the program aborts. Having rationals with denominator 0 leads to many sorts of trouble: for example the rational 0/0 compares equal to every other rational.

  1. Even small computations with your rationals result in integer overflow. For example, consider the following program:

     int main(int argc, char **argv) {
     Rational sixteenth = Rational_Construct(1, 16);
     Rational sum = Rational_Construct(0, 1);
     int i;
     for (i = 0; i < 10; ++i) {
     sum = Rational_Add(sum, sixteenth);
     printf("%d/%d\n", sum.Top, sum.Bottom);
     }
     return 0;
     }
    

The program looks as if it is supposed to add 1/16 ten times, getting the result 10/16. But it actually has undefined behaviour due to signed integer overflow. On my computer it prints:

 1/16
 32/256
 768/4096
 16384/65536
 327680/1048576
 6291456/16777216
 117440512/268435456
 -2147483648/0
 0/0
 0/0

Now, because you're using C's fixed-size integers, there are bound to be computations whose results are too large to be represented. But it would be better to raise an error when the result is too large, rather than causing undefined behaviour. And you should at the very least make some effort to ensure that integer overflow doesn't happen in small examples like this: when we add 1/16 ten times we ought to be able to get 10/16.

lang-c

AltStyle によって変換されたページ (->オリジナル) /