Tôi có cần xử lý rõ ràng số âm hoặc số 0 khi tính tổng các chữ số bình phương không?


220

Gần đây tôi đã có một bài kiểm tra trong lớp. Một trong những vấn đề là như sau:

Cho một số n , viết một hàm trong C / C ++ trả về tổng các chữ số của số bình phương . (Sau đây là quan trọng). Các phạm vi của n là [- (10 ^ 7), 10 ^ 7]. Ví dụ: Nếu n = 123, hàm của bạn sẽ trả về 14 (1 ^ 2 + 2 ^ 2 + 3 ^ 2 = 14).

Đây là chức năng mà tôi đã viết:

int sum_of_digits_squared(int n) 
{
    int s = 0, c;

    while (n) {
        c = n % 10;
        s += (c * c);
        n /= 10;
    }

    return s;
}

Nhìn đúng với tôi. Vì vậy, bây giờ bài kiểm tra đã trở lại và tôi thấy rằng giáo viên đã không cho tôi tất cả các điểm vì một lý do mà tôi không hiểu. Theo ông, để chức năng của tôi được hoàn thành, tôi nên đã thêm chi tiết sau:

int sum_of_digits_squared(int n) 
 {
    int s = 0, c;

    if (n == 0) {      //
        return 0;      //
    }                  //
                       // THIS APPARENTLY SHOULD'VE 
    if (n < 0) {       // BEEN IN THE FUNCTION FOR IT
        n = n * (-1);  // TO BE CORRECT
    }                  //

    while (n) {
        c = n % 10;
        s += (c * c);
        n /= 10;
    }

    return s;
}

Đối số cho điều này là số n nằm trong phạm vi [- (10 ^ 7), 10 ^ 7], vì vậy nó có thể là số âm. Nhưng tôi không thấy phiên bản chức năng của mình thất bại ở đâu. Nếu tôi hiểu đúng, ý nghĩa của while(n)while(n != 0), không while (n > 0) , vì vậy trong phiên bản của tôi của hàm số n sẽ không thất bại để vào vòng lặp. Nó sẽ làm việc như nhau.

Sau đó, tôi đã thử cả hai phiên bản của chức năng trên máy tính của tôi ở nhà và tôi nhận được chính xác các câu trả lời giống nhau cho tất cả các ví dụ mà tôi đã thử. Vì vậy, sum_of_digits_squared(-123)bằng sum_of_digits_squared(123)(một lần nữa, bằng 14) (ngay cả khi không có chi tiết mà tôi rõ ràng nên thêm vào). Thật vậy, nếu tôi cố gắng in trên màn hình các chữ số của số (từ mức tối thiểu đến mức quan trọng nhất), trong 123trường hợp tôi nhận được 3 2 1và trong -123trường hợp tôi nhận được -3 -2 -1(điều này thực sự thú vị). Nhưng trong vấn đề này, nó không thành vấn đề vì chúng ta bình phương các chữ số.

Vậy ai sai?

EDIT : Thật tệ, tôi đã quên chỉ định và không biết nó quan trọng. Phiên bản của C được sử dụng trong lớp của chúng tôi và các bài kiểm tra phải là C99 hoặc mới hơn . Vì vậy, tôi đoán (bằng cách đọc các bình luận) rằng phiên bản của tôi sẽ có câu trả lời chính xác theo bất kỳ cách nào.


120
n = n * (-1)là một cách vô lý để viết n = -n; Chỉ có một học giả thậm chí sẽ nghĩ về nó. Hãy để một mình thêm các dấu ngoặc đơn dư thừa.
dùng207421

32
Viết một loạt các bài kiểm tra đơn vị để kiểm tra xem một triển khai đã cho có phù hợp với thông số kỹ thuật không. Nếu có một vấn đề (chức năng) với một đoạn mã, có thể viết một bài kiểm tra chứng minh kết quả không chính xác cho một đầu vào cụ thể.
Carl

94
Tôi thấy thú vị rằng "tổng các chữ số của số bình phương" có thể được hiểu theo ba (3) cách hoàn toàn khác nhau. (Nếu số là 123, các diễn giải có thể mang lại 18, 14 và 36.)
Andreas Rejbrand

23
@ilkkachu: "tổng các chữ số của số bình phương". Chà, "số bình phương" rõ ràng là 123 ^ 2 = 15129, vì vậy "tổng các chữ số của số bình phương" là "tổng của các chữ số 15129", rõ ràng là 1 + 5 + 1 + 2 + 9 = 18.
Andreas Rejbrand

15
n = n * (-1)? Wut ??? Cái mà prof của bạn đang tìm kiếm là đây: `n = -n '. Ngôn ngữ C có một toán tử trừ đơn nguyên.
Kaz

Câu trả lời:


245

Tóm tắt một cuộc thảo luận đang tràn ngập trong các ý kiến:

  • Không có lý do chính đáng để kiểm tra trước cho n == 0. Bài while(n)kiểm tra sẽ xử lý trường hợp đó một cách hoàn hảo.
  • Có khả năng giáo viên của bạn vẫn quen với thời gian trước đó, khi kết quả của %các toán hạng âm được xác định khác nhau. Trên một số hệ thống cũ (bao gồm cả, đáng chú ý, đầu Unix trên một PDP-11, nơi Dennis Ritchie phát triển ban đầu C), kết quả của a % bluôn luôn nằm trong phạm vi [0 .. b-1], có nghĩa là -123% 10 là 7. Trên một hệ thống như vậy, các thử nghiệm trước cho n < 0sẽ là cần thiết.

Nhưng viên đạn thứ hai chỉ áp dụng cho thời gian trước đó. Trong các phiên bản hiện tại của cả hai tiêu chuẩn C và C ++, phép chia số nguyên được xác định để cắt về 0, do đó hóa ra nó n % 10được đảm bảo cung cấp cho bạn chữ số cuối cùng (có thể âm) nngay cả khi nâm.

Vì vậy, câu trả lời cho câu hỏi "ý nghĩa của là while(n)gì?" "Chính xác giống như while(n != 0)" và câu trả lời cho "Mã này có hoạt động đúng cho tiêu cực cũng như tích cực nkhông?" "Có, dưới bất kỳ trình biên dịch tuân thủ Tiêu chuẩn hiện đại nào." Câu trả lời cho câu hỏi "Vậy tại sao người hướng dẫn đánh dấu nó xuống?" có lẽ là họ không biết về một định nghĩa lại ngôn ngữ quan trọng đã xảy ra với C vào năm 1999 và với C ++ vào năm 2010 hoặc lâu hơn.


39
"Không có lý do chính đáng để kiểm tra trước cho n == 0" - về mặt kỹ thuật, điều đó đúng. Nhưng cho rằng chúng ta đang nói về một giáo sư trong một môi trường giảng dạy, họ có thể đánh giá cao sự rõ ràng về sự ngắn gọn hơn nhiều so với chúng ta. Thêm bài kiểm tra bổ sung n == 0ít nhất làm cho nó ngay lập tức và hoàn toàn rõ ràng cho bất kỳ độc giả nào xảy ra trong trường hợp đó. Không có nó, người đọc phải tự thỏa mãn rằng vòng lặp thực sự bị bỏ qua và giá trị mặc định được strả về là đúng.
ilkkachu

22
Ngoài ra, giáo sư có thể muốn biết rằng sinh viên nhận thức được và đã suy nghĩ về lý do và cách thức hoạt động của hàm với đầu vào bằng 0 (nghĩa là nó không trả về giá trị chính xác một cách tình cờ ). Họ có thể đã gặp những sinh viên không nhận ra điều gì sẽ xảy ra trong trường hợp đó, rằng vòng lặp có thể chạy 0 lần, v.v. Khi bạn xử lý một thiết lập giảng dạy, tốt nhất là nên rõ ràng hơn về bất kỳ giả định và trường hợp góc nào. ..
ilkkachu

36
@ilkkachu Nếu đó là trường hợp, thì giáo viên nên đưa ra một nhiệm vụ yêu cầu một bài kiểm tra như vậy để hoạt động chính xác.
klutt

38
@ilkkachu Vâng, tôi đưa ra quan điểm của bạn trong trường hợp chung, bởi vì tôi hoàn toàn đánh giá cao sự rõ ràng hơn sự ngắn gọn - và hầu như mọi lúc, không nhất thiết chỉ trong một môi trường sư phạm. Nhưng như đã nói, đôi khi sự ngắn gọn rõ ràng và nếu bạn có thể sắp xếp mã chính để xử lý cả trường hợp chung trường hợp cạnh, mà không làm lộn xộn mã (và phân tích phạm vi bảo hiểm) với các trường hợp đặc biệt cho các trường hợp cạnh , đó là một điều tuyệt đẹp! Tôi nghĩ rằng đó là một cái gì đó để được đánh giá cao ngay cả ở cấp độ mới bắt đầu.
Hội nghị thượng đỉnh Steve

49
@ilkkachu Theo lập luận đó, bạn chắc chắn cũng nên thêm các bài kiểm tra cho n = 1, v.v. Không có gì đặc biệt n=0. Giới thiệu các nhánh và biến chứng không cần thiết không làm cho mã trở nên dễ dàng hơn, nó làm cho nó khó hơn vì bây giờ bạn không chỉ phải chứng minh rằng thuật toán chung là chính xác mà bạn còn phải suy nghĩ về tất cả các trường hợp đặc biệt.
Voo

107

Mã của bạn là hoàn toàn tốt

Bạn hoàn toàn chính xác và giáo viên của bạn sai. Hoàn toàn không có lý do nào để thêm sự phức tạp đó, vì nó hoàn toàn không ảnh hưởng đến kết quả. Nó thậm chí còn giới thiệu một lỗi. (Xem bên dưới)

Đầu tiên, việc kiểm tra riêng nếu nbằng 0 rõ ràng là hoàn toàn không cần thiết và điều này rất dễ nhận ra. Thành thật mà nói, tôi thực sự đặt câu hỏi về năng lực giáo viên của bạn nếu anh ấy phản đối về điều này. Nhưng tôi đoán mọi người có thể có một bộ não xì hơi theo thời gian. Tuy nhiên, tôi nghĩ rằng while(n)nên thay đổi thành while(n != 0)vì nó thêm một chút rõ ràng mà không tốn thêm một dòng. Đó là một điều nhỏ mặc dù.

Điều thứ hai thì dễ hiểu hơn một chút, nhưng anh vẫn sai.

Đây là những gì tiêu chuẩn C11 6.5.5.p6 nói:

Nếu thương số a / b là đại diện, biểu thức (a / b) * b + a% b sẽ bằng a; mặt khác, hành vi của cả a / b và% b là không xác định.

Các chú thích nói điều này:

Điều này thường được gọi là "cắt ngắn về không".

Cắt ngắn về 0 có nghĩa là giá trị tuyệt đối a/bbằng với giá trị tuyệt đối (-a)/bcho tất cả abđiều đó có nghĩa là mã của bạn hoàn toàn ổn.

Modulo là toán dễ, nhưng có thể phản trực giác

Tuy nhiên, giáo viên của bạn có một điểm mà bạn nên cẩn thận, bởi vì thực tế là bạn bình phương kết quả thực sự rất quan trọng ở đây. Tính toán a%btheo định nghĩa trên là toán học dễ dàng, nhưng nó có thể đi ngược lại với trực giác của bạn. Đối với phép nhân và phép chia, kết quả là dương nếu các toán hạng có dấu bằng. Nhưng khi nói đến modulo, kết quả có cùng dấu với toán hạng đầu tiên . Toán hạng thứ hai hoàn toàn không ảnh hưởng đến dấu hiệu. Ví dụ, 7%3==1nhưng (-7)%(-3)==(-1).

Đây là một đoạn chứng minh điều đó:

$ cat > main.c 
#include <stdio.h>

void f(int a, int b) 
{
    printf("a: %2d b: %2d a/b: %2d a\%b: %2d (a%b)^2: %2d (a/b)*b+a%b==a: %5s\n",
           a, b ,a/b, a%b, (a%b)*(a%b), (a/b)*b+a%b == a ? "true" : "false");
}

int main(void)
{
    int a=7, b=3;
    f(a,b);
    f(-a,b);
    f(a,-b);
    f(-a,-b);
}

$ gcc main.c -Wall -Wextra -pedantic -std=c99

$ ./a.out
a:  7 b:  3 a/b:  2 a%b:  1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a: -7 b:  3 a/b: -2 a%b: -1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a:  7 b: -3 a/b: -2 a%b:  1 (a%b)^2:  1 (a/b)*b+a%b==a:  true
a: -7 b: -3 a/b:  2 a%b: -1 (a%b)^2:  1 (a/b)*b+a%b==a:  true

Vì vậy, trớ trêu thay, giáo viên của bạn đã chứng minh quan điểm của mình bằng cách sai.

Mã giáo viên của bạn bị thiếu sót

Vâng, nó thực sự là. Nếu đầu vào là INT_MINAND thì kiến ​​trúc là hai phần bù VÀ mẫu bit trong đó bit dấu là 1 và tất cả các bit giá trị là 0 KHÔNG phải là giá trị bẫy (sử dụng phần bù hai không có giá trị bẫy là rất phổ biến) thì mã của giáo viên của bạn sẽ mang lại hành vi không xác định trên dòng n = n * (-1). Mã của bạn là - nếu có một chút như vậy - tốt hơn của anh ấy. Và xem xét giới thiệu một lỗi nhỏ bằng cách làm cho mã trở nên phức tạp không cần thiết và đạt được giá trị hoàn toàn bằng không, tôi nói rằng mã của bạn RẤT NHIỀU.

Nói cách khác, trong các phần tổng hợp trong đó INT_MIN = -32768 (mặc dù hàm kết quả không thể nhận được đầu vào là <-32768 hoặc> 32767), đầu vào hợp lệ của -32768 gây ra hành vi không xác định, vì kết quả của - (- 32768i16) không thể được biểu diễn dưới dạng số nguyên 16 bit. (Trên thực tế, -32768 có thể sẽ không gây ra kết quả không chính xác, bởi vì - (- 32768i16) thường ước tính là -32768i16 và chương trình của bạn xử lý chính xác các số âm.) (SHRT_MIN có thể là -32768 hoặc -32767, tùy thuộc vào trình biên dịch.)

Nhưng giáo viên của bạn tuyên bố rõ ràng rằng ncó thể nằm trong phạm vi [-10 ^ 7; 10 ^ 7]. Số nguyên 16 bit quá nhỏ; bạn sẽ phải sử dụng [ít nhất] một số nguyên 32 bit. Việc sử dụng intdường như làm cho mã của anh ấy an toàn, ngoại trừ đó intkhông nhất thiết là số nguyên 32 bit. Nếu bạn biên dịch cho kiến ​​trúc 16 bit, cả hai đoạn mã của bạn đều bị thiếu sót. Nhưng mã của bạn vẫn tốt hơn nhiều vì kịch bản này giới thiệu lại lỗi INT_MINđã được đề cập ở trên với phiên bản của anh ta. Để tránh điều này, bạn có thể viết longthay vì int, đó là số nguyên 32 bit trên một trong hai kiến ​​trúc. A longđược đảm bảo có thể giữ bất kỳ giá trị nào trong phạm vi [-2147483647; 2147483647]. Tiêu chuẩn C11 5.2.4.2.1 LONG_MIN thường-2147483648nhưng giá trị tối đa (có, tối đa, đó là số âm) cho phép LONG_MIN2147483647.

Những thay đổi tôi sẽ làm cho mã của bạn?

Mã của bạn là tốt như vậy, vì vậy đây không phải là khiếu nại. Sẽ giống như vậy nếu tôi thực sự, thực sự cần nói bất cứ điều gì về mã của bạn, có một số điều nhỏ có thể làm cho nó rõ ràng hơn một chút.

  • Tên của các biến có thể tốt hơn một chút, nhưng nó là một hàm ngắn dễ hiểu, vì vậy nó không phải là vấn đề lớn.
  • Bạn có thể thay đổi điều kiện từ nsang n!=0. Về mặt ngữ nghĩa, nó tương đương 100%, nhưng nó làm cho nó rõ ràng hơn một chút.
  • Di chuyển khai báo c(mà tôi đã đổi tên thành digit) vào bên trong vòng lặp while vì nó chỉ được sử dụng ở đó.
  • Thay đổi loại đối số longđể đảm bảo nó có thể xử lý toàn bộ bộ đầu vào.
int sum_of_digits_squared(long n) 
{
    long sum = 0;

    while (n != 0) {
        int digit = n % 10;
        sum += (digit * digit);
        n /= 10;
    }

    return sum;
}

Trên thực tế, điều này có thể gây ra một chút sai lầm bởi vì - như đã đề cập ở trên - biến digitcó thể nhận được giá trị âm, nhưng bản thân một chữ số không bao giờ dương hoặc âm. Có một vài cách xung quanh vấn đề này, nhưng đây là sự thật sự gây nghiện, và tôi sẽ không quan tâm đến những chi tiết nhỏ như vậy. Đặc biệt là chức năng riêng biệt cho chữ số cuối cùng đang đưa nó đi quá xa. Trớ trêu thay, đây là một trong những điều mà mã giáo viên của bạn thực sự giải quyết được.

  • Thay đổi sum += (digit * digit)để sum += ((n%10)*(n%10))và bỏ qua biến digithoàn toàn.
  • Thay đổi dấu digitnếu âm. Nhưng tôi sẽ khuyên mạnh mẽ chống lại việc làm cho mã phức tạp hơn chỉ để làm cho một tên biến có ý nghĩa. Đó là một mùi mã RẤT mạnh mẽ.
  • Tạo một hàm riêng biệt trích xuất chữ số cuối cùng. int last_digit(long n) { int digit=n%10; if (digit>=0) return digit; else return -digit; }Điều này rất hữu ích nếu bạn muốn sử dụng chức năng đó ở một nơi khác.
  • Chỉ cần đặt tên cnhư bạn ban đầu làm. Tên biến đó không cung cấp bất kỳ thông tin hữu ích nào, nhưng mặt khác, nó cũng không gây hiểu lầm.

Nhưng thành thật mà nói, tại thời điểm này bạn nên chuyển sang công việc quan trọng hơn. :)


19
Nếu đầu vào là INT_MINvà kiến ​​trúc đang sử dụng hai phần bù (rất phổ biến) thì mã giáo viên của bạn sẽ mang lại hành vi không xác định. Ôi. Điều đó sẽ để lại một dấu ấn. ;-)
Andrew Henle

5
Cần phải đề cập rằng ngoài (a/b)*b + a%b ≡ amã của OP cũng dựa vào thực tế là làm /tròn về số 0, và điều đó (-c)*(-c) ≡ c*c. Có thể lập luận rằng các kiểm tra bổ sung là hợp lý mặc dù tiêu chuẩn đảm bảo tất cả điều đó, bởi vì nó đủ không rõ ràng. (Tất nhiên cũng có thể lập luận rằng không nên có một bình luận liên kết các phần tiêu chuẩn có liên quan, nhưng hướng dẫn về phong cách khác nhau.)
leftaroundabout

7
@MartinRosenau Bạn nói "có thể". Bạn có chắc chắn điều này thực sự xảy ra hoặc nó được cho phép bởi tiêu chuẩn hoặc một cái gì đó hoặc bạn chỉ đang suy đoán?
klutt

6
@MartinRosenau: Ok, nhưng sử dụng các công tắc đó sẽ khiến nó không còn C nữa. GCC / clang không có bất kỳ công tắc nào phá vỡ phân chia số nguyên trên bất kỳ ISA nào tôi biết. Mặc dù bỏ qua bit dấu có thể có thể tăng tốc bằng cách sử dụng nghịch đảo nhân thông thường cho các ước số không đổi. (Tuy nhiên, tất cả các ISA Tôi quen thuộc mà có một hướng dẫn bộ phận phần cứng thực hiện nó theo cách C99, cắt xén đối với zero, do đó, C %/các nhà khai thác có thể biên dịch để chỉ một idivtrên x86, hoặc sdivtrên ARM hoặc bất cứ điều gì. Tuy nhiên, đó là không liên quan đến nhiều mã gen nhanh hơn cho các ước số không đổi thời gian biên dịch)
Peter Cordes

5
@TonyK AFIK, đó là cách nó thường được giải quyết, nhưng theo tiêu chuẩn đó là UB.
klutt

20

Tôi không hoàn toàn thích phiên bản của bạn hoặc của giáo viên của bạn. Phiên bản của giáo viên của bạn thực hiện các bài kiểm tra bổ sung mà bạn chỉ ra một cách chính xác là không cần thiết. Toán tử mod của C không phải là một mod toán học thích hợp: một mod số âm 10 sẽ tạo ra kết quả âm (mô đun toán học thích hợp luôn không âm). Nhưng vì dù sao bạn cũng bình phương nó, không có gì khác biệt.

Nhưng điều này là không rõ ràng, vì vậy tôi sẽ thêm vào mã của bạn không phải là kiểm tra của giáo viên của bạn, mà là một nhận xét lớn giải thích lý do tại sao nó hoạt động. Ví dụ:

/ * LƯU Ý: Điều này hoạt động cho các giá trị âm, vì mô đun được bình phương * /


9
C %tốt nhất được gọi là phần còn lại , bởi vì đó là, ngay cả đối với các loại đã ký.
Peter Cordes

15
Bình phương là quan trọng, nhưng tôi nghĩ đó là phần rõ ràng. Điều cần được chỉ ra là (ví dụ) -7 % 10 sẽ thực sự-7 thay vì 3.
Jacob Raihle

5
Mô-đun toán học thích hợp, không có nghĩa gì. Thuật ngữ chính xác là Hồi Euclidean modulo (nhớ hậu tố!) Và đó thực sự là điều mà %toán tử C không có.
Jan Hudec

Tôi thích câu trả lời này vì nó giải quyết câu hỏi về nhiều cách để giải thích modulo. Không bao giờ để một điều như thế để cơ hội / giải thích. Đây không phải là mã golf.
Harper - Tái lập Monica

1
"mô đun toán học thích hợp luôn không âm" - Không thực sự. Kết quả của phép toán modulo là một lớp tương đương , nhưng thông thường coi kết quả là số không âm nhỏ nhất thuộc về lớp đó.
klutt

10

LƯU Ý: NHƯ Tôi đã viết câu trả lời này, bạn đã làm rõ rằng bạn đang sử dụng C. Phần lớn câu trả lời của tôi là về C ++. Tuy nhiên, vì tiêu đề của bạn vẫn có C ++ và câu hỏi vẫn được gắn thẻ C ++, tôi đã chọn trả lời bằng mọi cách trong trường hợp điều này vẫn hữu ích cho người khác, đặc biệt là vì hầu hết các câu trả lời tôi thấy cho đến nay hầu hết đều không thỏa đáng.

Trong C ++ hiện đại (Lưu ý: Tôi không thực sự biết C đứng ở đâu về điều này), giáo sư của bạn dường như sai về cả hai cách tính.

Đầu tiên là phần này ngay tại đây:

if (n == 0) {
        return 0;
}

Trong C ++, điều này về cơ bản giống như :

if (!n) {
        return 0;
}

Điều đó có nghĩa là thời gian của bạn tương đương với một cái gì đó như thế này:

while(n != 0) {
    // some implementation
}

Điều đó có nghĩa là vì bạn chỉ đơn thuần là thoát khỏi nếu khi nào đó sẽ không thực hiện được, nên thực sự không có lý do gì để đặt cái này nếu ở đây, vì những gì bạn đang làm sau vòng lặp và trong trường hợp tương tự. Mặc dù tôi nên nói rằng vì một số lý do, những điều này là khác nhau, bạn cần phải có điều này nếu.

Vì vậy, thực sự, điều này nếu tuyên bố không đặc biệt hữu ích trừ khi tôi nhầm.

Phần thứ hai là nơi mọi thứ có lông:

if (n < 0) {
    n = n * (-1);
}  

Trọng tâm của vấn đề là đầu ra của mô đun của đầu ra số âm.

Trong C ++ hiện đại, điều này dường như được xác định rõ :

Toán tử nhị phân / toán tử mang lại thương số và toán tử% nhị phân mang lại phần còn lại từ cách chia biểu thức thứ nhất cho biểu thức thứ hai. Nếu toán hạng thứ hai của / hoặc% bằng 0 thì hành vi không được xác định. Đối với các toán hạng tích phân, toán tử / mang lại thương số đại số với bất kỳ phần phân số nào bị loại bỏ; nếu thương số a / b có thể biểu diễn theo loại kết quả, (a / b) * b + a% b bằng a.

Và sau đó:

Nếu cả hai toán hạng là không âm, phần còn lại là không âm; nếu không, dấu hiệu của phần còn lại được xác định theo thực hiện.

Như poster của câu trả lời được trích dẫn chỉ ra một cách chính xác, phần quan trọng của phương trình này ngay tại đây:

(a / b) * b + a% b

Lấy một ví dụ về trường hợp của bạn, bạn sẽ nhận được một cái gì đó như thế này:

-13/ 10 = -1 (integer truncation)
-1 * 10 = -10
-13 - (-10) = -13 + 10 = -3 

Bắt duy nhất là dòng cuối cùng:

Nếu cả hai toán hạng là không âm, phần còn lại là không âm; nếu không, dấu hiệu của phần còn lại được xác định theo thực hiện.

Điều đó có nghĩa là trong trường hợp như thế này, chỉ có dấu hiệu được xác định theo thực thi. Đó không phải là một vấn đề trong trường hợp của bạn bởi vì dù sao bạn cũng đang bình phương giá trị này.

Điều đó nói rằng, hãy nhớ rằng điều này không nhất thiết phải áp dụng cho các phiên bản trước của C ++ hoặc C99. Nếu đó là những gì giáo sư của bạn đang sử dụng, đó có thể là lý do tại sao.


EDIT: Không, tôi sai. Đây có vẻ là trường hợp cho C99 hoặc sau này :

C99 yêu cầu khi a / b có thể biểu diễn:

(a / b) * b + a% b sẽ bằng a

một nơi khác :

Khi các số nguyên được chia và phép chia không chính xác, nếu cả hai toán hạng đều dương thì kết quả của toán tử / là số nguyên lớn nhất nhỏ hơn thương số đại số và kết quả của toán tử% là dương. Nếu một toán hạng là âm, thì kết quả của toán tử / là số nguyên lớn nhất nhỏ hơn thương số đại số hay số nguyên nhỏ nhất lớn hơn thương số đại số được xác định theo thực thi, như là dấu hiệu của kết quả của toán tử%. Nếu thương số a / b là đại diện, biểu thức (a / b) * b + a% b sẽ bằng a.

Có phải ANSI C hoặc ISO C chỉ định -5% 10 nên là gì không?

Vì vậy, vâng. Ngay cả trong C99, điều này dường như không ảnh hưởng đến bạn. Phương trình là như nhau.


1
Các phần bạn đã trích dẫn không hỗ trợ câu trả lời này. "Dấu hiệu của phần còn lại là việc thực hiện được xác định" không có nghĩa là (-1)%10có thể tạo ra -1hoặc 1; điều đó có nghĩa là nó có thể tạo ra -1hoặc 9, và trong trường hợp sau (-1)/10sẽ tạo ra -1và mã của OP sẽ không bao giờ chấm dứt.
stewbasic

Bạn có thể chỉ ra một nguồn cho điều này? Tôi có một thời gian rất khó tin (-1) / 10 là -1. Đó phải là 0. Ngoài ra, (-1)% 10 = 9 dường như vi phạm phương trình quản lý.
Chipster

1
@Chipster, bắt đầu với (a/b)*b + a%b == a, sau đó cho a=-1; b=10, cho (-1/10)*10 + (-1)%10 == -1. Bây giờ, nếu -1/10thực sự được làm tròn xuống (hướng tới -inf), thì chúng ta có (-1/10)*10 == -10, và bạn cần phải có (-1)%10 == 9phương trình đầu tiên khớp. Giống như các câu trả lời khác , đây không phải là cách nó hoạt động trong (các) tiêu chuẩn hiện tại, mà là cách nó được sử dụng để hoạt động. Nó không thực sự về dấu hiệu của thời gian còn lại như vậy, nhưng làm thế nào các vòng phân chia và những gì còn lại sau đó được để thỏa mãn phương trình.
ilkkachu

1
@Chipster Nguồn là đoạn trích bạn đã trích dẫn. Lưu ý rằng (-1)*10+9=-1, do đó, sự lựa chọn (-1)/10=-1(-1)%10=9không vi phạm phương trình quản lý. Mặt khác, sự lựa chọn (-1)%10=1không thể thỏa mãn phương trình quản trị cho dù (-1)/10được chọn như thế nào ; không có số nguyênq như vậy q*10+1=-1.
stewbasic

8

Như những người khác đã chỉ ra, cách xử lý đặc biệt cho n == 0 là vô nghĩa, vì đối với mọi lập trình viên C nghiêm túc, rõ ràng là "while (n)" thực hiện công việc.

Hành vi cho n <0 không rõ ràng, đó là lý do tại sao tôi muốn thấy 2 dòng mã đó:

if (n < 0) 
    n = -n;

hoặc ít nhất là một nhận xét:

// don't worry, works for n < 0 as well

Thành thật mà nói, lúc nào bạn bắt đầu xem xét rằng n có thể là tiêu cực? Khi viết mã hoặc khi đọc nhận xét của giáo viên?


1
Bất kể N là âm, N bình phương sẽ dương. Vì vậy, tại sao loại bỏ các dấu hiệu ở nơi đầu tiên? -3 * -3 = 9; 3 * 3 = 9. Hay toán học đã thay đổi trong 30 năm kể từ khi tôi học được điều này?
Merovex

2
@CB Công bằng mà nói, tôi thậm chí không nhận thấy rằng n có thể âm tính khi tôi đang viết bài kiểm tra, nhưng khi nó quay lại tôi chỉ có cảm giác rằng vòng lặp while sẽ không bị bỏ qua, ngay cả khi số đó là âm. Tôi đã làm một số thử nghiệm trên máy tính của tôi và nó đã xác nhận sự hoài nghi của tôi. Sau đó, tôi đã đăng câu hỏi này. Vì vậy, không, tôi đã không suy nghĩ sâu sắc trong khi viết mã.
dùng010517720

5

Điều này nhắc nhở tôi về một nhiệm vụ mà tôi đã thất bại

Đường về những năm 90. Giảng viên đã nói về các vòng lặp và, câu chuyện dài, nhiệm vụ của chúng tôi là viết một hàm sẽ trả về số chữ số cho bất kỳ số nguyên nào> 0.

Vì vậy, ví dụ, số chữ số trong 321sẽ là3 .

Mặc dù bài tập nói đơn giản là viết một hàm trả về số chữ số, nhưng kỳ vọng là chúng ta sẽ sử dụng một vòng lặp chia cho 10 cho đến khi ... bạn có được nó, như được trình bày trong bài giảng .

Nhưng việc sử dụng các vòng lặp không được tuyên bố rõ ràng nên tôi: took the log, stripped away the decimals, added 1và sau đó đã nổi giận trước cả lớp.

Vấn đề là, mục đích của bài tập là kiểm tra sự hiểu biết của chúng ta về những gì chúng ta đã học được trong các bài giảng . Từ bài giảng tôi nhận được, tôi học được rằng giáo viên tin học có chút giật mình (nhưng có lẽ là một thằng ngốc với một kế hoạch?)


Trong tình huống của bạn:

viết một hàm trong C / C ++ trả về tổng các chữ số của số bình phương

Tôi chắc chắn sẽ cung cấp hai câu trả lời:

  • câu trả lời đúng (bình phương số đầu tiên) và
  • câu trả lời không đúng với ví dụ, chỉ để giữ cho anh ta hạnh phúc ;-)

5
Và một số thứ ba bình phương tổng các chữ số?
kriss

@kriss - vâng, tôi không thông minh lắm :-(
SlowLearner

1
Tôi cũng đã có những chia sẻ của mình về những khẳng định quá mơ hồ trong thời sinh viên của mình. Một giáo viên muốn một thư viện thao túng một chút nhưng anh ta ngạc nhiên về mã của tôi và nói rằng nó không hoạt động. Tôi đã phải chỉ ra cho anh ta rằng anh ta không bao giờ xác định được endian trong bài tập của mình và anh ta chọn bit thấp hơn là bit 0 trong khi tôi đưa ra lựa chọn khác. Điều khó chịu duy nhất là anh ta lẽ ra đã có thể tìm ra sự khác biệt ở đâu mà không cần tôi nói với anh ta.
kriss

1

Nói chung trong các bài tập không phải tất cả các dấu được đưa ra chỉ vì mã hoạt động. Bạn cũng nhận được điểm để làm cho một giải pháp dễ đọc, hiệu quả và thanh lịch. Những điều này không phải luôn luôn loại trừ lẫn nhau.

Một trong những điều tôi không thể đủ là "sử dụng tên biến có ý nghĩa" .

Trong ví dụ của bạn, nó không tạo ra nhiều khác biệt, nhưng nếu bạn đang làm việc trong một dự án với hàng triệu dòng mã dễ đọc trở nên rất quan trọng.

Một điều khác tôi có xu hướng nhìn thấy với mã C là mọi người đang cố gắng để trông thông minh. Thay vì sử dụng while (n! = 0) tôi sẽ cho mọi người thấy tôi thông minh như thế nào bằng cách viết while (n) bởi vì nó có nghĩa tương tự. Vâng, nó có trong trình biên dịch mà bạn có nhưng như bạn đề xuất, phiên bản cũ hơn của giáo viên đã không triển khai nó theo cùng một cách.

Một ví dụ phổ biến là tham chiếu một chỉ mục trong một mảng trong khi tăng nó cùng một lúc; Số [i ++] = iPrime;

Bây giờ, lập trình viên tiếp theo làm việc với mã phải biết tôi được tăng trước hay sau khi gán, để ai đó có thể thể hiện.

Một megabyte không gian đĩa rẻ hơn một cuộn giấy vệ sinh, đi cho rõ ràng hơn là cố gắng tiết kiệm không gian, các lập trình viên đồng nghiệp của bạn sẽ hạnh phúc hơn.


2
Tôi đã lập trình trong C chỉ một vài lần và tôi biết rằng ++ităng trước khi đánh giá và i++tăng sau đó. while(n)cũng là một tính năng ngôn ngữ phổ biến. Dựa trên logic như thế này tôi đã thấy rất nhiều mã như thế nào if (foo == TRUE). Tôi đồng ý re: tên biến, mặc dù.
alan ocallaghan

3
Nói chung đó không phải là lời khuyên tồi, nhưng tránh các tính năng ngôn ngữ cơ bản (dù sao mọi người chắc chắn sẽ bắt gặp) vì mục đích lập trình phòng thủ là quá mức. Mã ngắn, rõ ràng thường dễ đọc hơn. Ở đây chúng ta không nói về perl crazy hay bash one-liners, chỉ là những tính năng ngôn ngữ thực sự cơ bản.
alan ocallaghan

1
Không chắc chắn, những gì downvote về câu trả lời này đã được đưa ra cho. Đối với tôi mọi thứ được nêu ở đây là đúng và quan trọng và là một lời khuyên tốt cho mọi nhà phát triển. Đặc biệt là phần lập trình ass-smart, mặc dù while(n)không phải là ví dụ tồi tệ nhất cho điều đó (tôi "thích" if(strcmp(one, two))hơn)
Kai Huppmann

1
Và bên cạnh đó, bạn thực sự không nên để bất kỳ ai không biết sự khác biệt giữa i++++isửa đổi mã C nên được sử dụng trong sản xuất.
klutt

2
@PaulMcCarthy Chúng tôi đều cho khả năng đọc mã. Nhưng chúng tôi không đồng ý về điều đó có nghĩa là gì. Ngoài ra, nó không khách quan. Những gì dễ đọc cho một người có thể khó cho người khác. Tính cách và kiến ​​thức nền tảng ảnh hưởng mạnh mẽ đến điều này. Quan điểm chính của tôi là bạn không có được khả năng đọc tối đa bằng cách tuân theo một số quy tắc.
klutt

0

Tôi sẽ không tranh luận về việc định nghĩa ban đầu hay hiện đại của '%' là tốt hơn nhưng bất cứ ai viết hai câu trả về vào một hàm ngắn như vậy đều không nên dạy lập trình C. Trả về thêm là một tuyên bố goto và chúng tôi không sử dụng goto trong C. Hơn nữa, mã không có kiểm tra 0 sẽ có kết quả tương tự, trả lại thêm làm cho khó đọc hơn.


4
"Trả lại thêm là một tuyên bố goto và chúng tôi không sử dụng goto trong C." - đó là sự kết hợp của một khái quát rất rộng và kéo dài rất xa.
SS Anne

1
"Chúng tôi" chắc chắn sử dụng goto trong C. Không có gì sai với điều đó.
klutt

1
Ngoài ra, không có gì sai với một chức năng nhưint findChar(char *str, char c) { if(!str) return -1; int i=0; while(str[i]) { if(str[i] == c) return i; i++; } return -1; }
klutt

1
@PeterKrassoi Tôi không ủng hộ mã khó đọc, nhưng tôi đã thấy hàng tấn ví dụ trong đó mã trông giống như một mớ hỗn độn chỉ để tránh một goto đơn giản hoặc một tuyên bố trả lại thêm. Đây là một số mã với việc sử dụng goto thích hợp. Tôi thách bạn xóa các câu lệnh goto đồng thời làm cho mã dễ đọc và dễ đọc hơn: pastebin.com/aNaGb66Q
klutt

1
@PeterKrassoi Vui lòng cũng hiển thị phiên bản này của bạn: pastebin.com/qBmR78KA
klutt

0

Báo cáo vấn đề là khó hiểu, nhưng ví dụ bằng số làm rõ ý nghĩa của tổng các chữ số của số bình phương . Đây là một phiên bản cải tiến:

Viết hàm trong tập con chung của C và C ++, lấy một số nguyên ntrong phạm vi [-10 7 , 10 7 ] và trả về tổng bình phương của các chữ số biểu diễn của nó trong cơ sở 10. Ví dụ: if nis 123, hàm của bạn nên trả về 14(1 2 + 2 2 + 3 2 = 14).

Các chức năng mà bạn đã viết là tốt, ngoại trừ 2 chi tiết:

  • Đối số phải có loại longđể phù hợp với tất cả các giá trị trong phạm vi được chỉ định vì loại longđược đảm bảo bởi Tiêu chuẩn C để có ít nhất 31 bit giá trị, do đó, một phạm vi đủ để biểu thị tất cả các giá trị trong [-10 7 , 10 7 ] . (Lưu ý rằng loại intnày là đủ cho loại trả về, có giá trị tối đa là 568.)
  • Hành vi của %các toán hạng phủ định là không trực quan và đặc điểm kỹ thuật của nó khác nhau giữa Tiêu chuẩn C99 và các phiên bản trước. Bạn nên ghi lại lý do tại sao cách tiếp cận của bạn hợp lệ ngay cả đối với các đầu vào tiêu cực.

Đây là một phiên bản sửa đổi:

int sum_of_digits_squared(long n) {
    int s = 0;

    while (n != 0) {
        /* Since integer division is defined to truncate toward 0 in C99 and C++98 and later,
           the remainder of this division is positive for positive `n`
           and negative for negative `n`, and its absolute value is the last digit
           of the representation of `n` in base 10.
           Squaring this value yields the expected result for both positive and negative `c`.
           dividing `n` by 10 effectively drops the last digit in both cases.
           The loop will not be entered for `n == 0`, producing the correct result `s = 0`.
         */
        int c = n % 10;
        s += c * c;
        n /= 10;
    }
    return s;
}

Câu trả lời của giáo viên có nhiều sai sót:

  • loại intcó thể có một phạm vi giá trị không đủ.
  • không cần phải đặc biệt trường hợp giá trị 0.
  • phủ định các giá trị âm là không cần thiết và có thể có hành vi không xác định cho n = INT_MIN.

Với các ràng buộc bổ sung trong báo cáo vấn đề (C99 và phạm vi giá trị cho n), chỉ có lỗ hổng đầu tiên là một vấn đề. Các mã bổ sung vẫn tạo ra các câu trả lời chính xác.

Bạn sẽ đạt điểm cao trong bài kiểm tra này, nhưng cần có lời giải thích trong bài kiểm tra viết để thể hiện sự hiểu biết của bạn về các vấn đề tiêu cực n, nếu không, giáo viên có thể cho rằng bạn không biết và chỉ gặp may mắn. Trong một bài kiểm tra miệng, bạn sẽ nhận được một câu hỏi và câu trả lời của bạn sẽ đóng đinh nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.