Tăng dần trong C ++ - Khi nào sử dụng x ++ hoặc ++ x?


91

Tôi hiện đang học C ++ và tôi đã học về sự gia tăng một thời gian trước. Tôi biết rằng bạn có thể sử dụng "++ x" để tăng trước và "x ++" để tăng sau.

Tuy nhiên, tôi thực sự không biết khi nào sử dụng một trong hai ... Tôi chưa bao giờ thực sự sử dụng "++ x" và mọi thứ luôn hoạt động tốt cho đến nay - vậy, khi nào tôi nên sử dụng nó?

Ví dụ: Trong vòng lặp for, khi nào sử dụng "++ x" là thích hợp hơn?

Ngoài ra, ai đó có thể giải thích chính xác cách hoạt động của các mức tăng (hoặc giảm) khác nhau không? Tôi thực sự sẽ đánh giá cao nó.

Câu trả lời:


114

Vấn đề không phải là sở thích, mà là vấn đề logic.

x++tăng giá trị của biến x sau khi xử lý câu lệnh hiện tại.

++xtăng giá trị của biến x trước khi xử lý câu lệnh hiện tại.

Vì vậy, chỉ cần quyết định logic bạn viết.

x += ++isẽ tăng i và thêm i + 1 vào x. x += i++sẽ thêm i vào x, sau đó tăng i.


27
và xin lưu ý rằng trong vòng lặp for, trên các bản gốc, hoàn toàn không có sự khác biệt. Nhiều phong cách mã hóa sẽ khuyên bạn không bao giờ sử dụng toán tử tăng dần vì nó có thể bị hiểu nhầm; tức là, x ++ hoặc ++ x chỉ nên tồn tại trên dòng riêng của nó, không bao giờ là y = x ++. Cá nhân tôi không thích điều này, nhưng nó không phổ biến
Mikeage

2
Và nếu được sử dụng trên dòng riêng của nó, mã được tạo gần như chắc chắn là giống nhau.
Nosredna

14
Điều này có vẻ giống như một bước (chủ yếu là vì nó là như vậy :)) nhưng trong C ++, x++là một rvalue với giá trị xtrước khi gia tăng, x++là một giá trị có giá trị xsau một gia số. Cả hai biểu thức đều không đảm bảo khi giá trị tăng thực tế được lưu trữ trở lại x, nó chỉ được đảm bảo rằng nó xảy ra trước điểm trình tự tiếp theo. 'sau khi xử lý câu lệnh hiện tại' không hoàn toàn chính xác vì một số biểu thức có điểm trình tự và một số câu lệnh là câu lệnh ghép.
CB Bailey

10
Thực ra, câu trả lời là sai lệch. Thời điểm mà biến x được sửa đổi có lẽ không khác nhau trong thực tế. Sự khác biệt là x ++ được định nghĩa để trả về một giá trị của giá trị trước đó của x trong khi ++ x vẫn tham chiếu đến biến x.
sellibitze

5
@BeowulfOF: Câu trả lời ngụ ý một đơn đặt hàng không tồn tại. Không có gì trong tiêu chuẩn để nói khi các bước tăng diễn ra. Trình biên dịch được quyền triển khai "x + = i ++" như: int j = i; i = i + 1; x + = j; "(tức là. 'i' tăng lên trước khi" xử lý câu lệnh hiện tại "). Đây là lý do tại sao" i = i ++ "có hành vi không xác định và lý do tại sao tôi nghĩ câu trả lời cần" điều chỉnh ". Mô tả của" x + = ++ i "đúng vì không có gợi ý nào về thứ tự:" sẽ tăng i và thêm i + 1 vào x ".
Richard Corden

53

Scott Meyers cho bạn biết thích tiền tố hơn ngoại trừ những trường hợp mà logic sẽ ra lệnh cho hậu tố đó là phù hợp.

Mục # 6 "C ++ hiệu quả hơn" - đó là đủ thẩm quyền đối với tôi.

Đối với những người không sở hữu cuốn sách, đây là những trích dẫn thích hợp. Từ trang 32:

Từ những ngày còn là một lập trình viên C, bạn có thể nhớ lại rằng dạng tiền tố của toán tử tăng dần đôi khi được gọi là "tăng và tìm nạp", trong khi dạng hậu tố thường được gọi là "tìm nạp và tăng dần". Hai cụm từ này rất quan trọng cần nhớ, bởi vì chúng đều đóng vai trò là thông số kỹ thuật chính thức ...

Và trên trang 34:

Nếu bạn là người lo lắng về hiệu quả, có lẽ bạn đã đổ mồ hôi khi lần đầu tiên nhìn thấy chức năng tăng hậu tố. Hàm đó phải tạo một đối tượng tạm thời cho giá trị trả về của nó và việc triển khai ở trên cũng tạo ra một đối tượng tạm thời rõ ràng phải được xây dựng và hủy. Hàm tăng tiền tố không có thời gian tạm thời như vậy ...


4
Nếu trình biên dịch không nhận ra rằng giá trị trước khi gia tăng là không cần thiết, nó có thể triển khai bước tăng hậu tố trong một số hướng dẫn - sao chép giá trị cũ, rồi sau đó tăng. Phần tăng tiền tố phải luôn luôn chỉ là một lệnh.
gnud

8
Tôi đã tình cờ kiểm tra điều này ngày hôm qua với gcc: trong vòng lặp for, trong đó giá trị bị loại bỏ sau khi thực thi i++hoặc ++i, mã được tạo giống nhau.
Giorgio

Hãy thử nó bên ngoài vòng lặp for. Hành vi trong một bài tập phải khác.
duffymo

Tôi hoàn toàn không đồng ý với Scott Meyers về điểm thứ hai của anh ấy - nó thường không liên quan vì 90% trở lên các trường hợp "x ++" hoặc "++ x" thường được tách biệt khỏi bất kỳ nhiệm vụ nào và trình tối ưu hóa đủ thông minh để nhận ra rằng không cần biến tạm thời được tạo ra trong những trường hợp như vậy. Trong trường hợp đó, hai hình thức hoàn toàn có thể thay thế cho nhau. Hàm ý của điều này là các cơ sở mã cũ có "x ++" nên được để yên - bạn có nhiều khả năng mắc các lỗi nhỏ khi thay đổi chúng thành "++ x" hơn là để cải thiện hiệu suất ở bất kỳ đâu. Có thể cho rằng tốt hơn là sử dụng "x ++" và khiến mọi người phải suy nghĩ.
omatai

2
Bạn có thể tin tưởng Scott Meyers tất cả những gì bạn muốn, nhưng nếu mã của bạn phụ thuộc vào hiệu suất đến mức có bất kỳ sự khác biệt nào về hiệu suất giữa ++xx++thực sự quan trọng, thì điều quan trọng hơn là bạn thực sự sử dụng một trình biên dịch có thể tối ưu hóa hoàn toàn và chính xác một trong hai phiên bản bất kể bối cảnh. "Vì tôi đang sử dụng chiếc búa cũ kỹ này, tôi chỉ có thể đóng đinh ở góc 43,7 độ" là một lập luận tồi cho việc xây nhà bằng cách đóng đinh vào chỉ 43,7 độ. Sử dụng một công cụ tốt hơn.
Andrew Henle

28

Từ cppreference khi tăng vòng lặp:

Bạn nên thích toán tử tăng trước (++ iter) hơn toán tử tăng sau (iter ++) nếu bạn không sử dụng giá trị cũ. Tăng sau thường được thực hiện như sau:

   Iter operator++(int)   {
     Iter tmp(*this); // store the old value in a temporary object
     ++*this;         // call pre-increment
     return tmp;      // return the old value   }

Rõ ràng, nó kém hiệu quả hơn so với tăng trước.

Tăng trước không tạo ra đối tượng tạm thời. Điều này có thể tạo ra sự khác biệt đáng kể nếu đối tượng của bạn đắt tiền để tạo ra.


8

Tôi chỉ muốn lưu ý rằng mã được tạo không giống nhau nếu bạn sử dụng phần tăng trước / sau trong đó ngữ nghĩa (của trước / sau) không quan trọng.

thí dụ:

pre.cpp:

#include <iostream>

int main()
{
  int i = 13;
  i++;
  for (; i < 42; i++)
    {
      std::cout << i << std::endl;
    }
}

post.cpp:

#include <iostream>

int main()
{

  int i = 13;
  ++i;
  for (; i < 42; ++i)
    {
      std::cout << i << std::endl;
    }
}

_

$> g++ -S pre.cpp
$> g++ -S post.cpp
$> diff pre.s post.s   
1c1
<   .file   "pre.cpp"
---
>   .file   "post.cpp"

5
Đối với kiểu nguyên thủy như số nguyên, có. Bạn đã kiểm tra xem sự khác biệt hóa ra là gì đối với một cái gì đó như a std::map::iteratorchưa? Tất nhiên, hai toán tử là khác nhau, nhưng tôi tò mò không biết liệu trình biên dịch có tối ưu hóa postfix thành tiền tố hay không nếu kết quả không được sử dụng. Tôi không nghĩ rằng nó được phép - cho rằng phiên bản postfix có thể chứa các tác dụng phụ.
seh

Ngoài ra, ' trình biên dịch có thể sẽ nhận ra rằng bạn không cần tác dụng phụ và tối ưu hóa nó đi ' không nên là cái cớ để viết mã cẩu thả sử dụng các toán tử postfix phức tạp hơn mà không có bất kỳ lý do gì, ngoài thực tế là có rất nhiều các tài liệu giảng dạy được cho là sử dụng postfix mà không có lý do rõ ràng và được sao chép khắp nơi bán buôn.
underscore_d

6

Điều quan trọng nhất cần ghi nhớ, imo, là x ++ cần trả về giá trị trước khi quá trình tăng thực sự diễn ra - do đó, nó phải tạo một bản sao tạm thời của đối tượng (bước tăng trước). Điều này kém hiệu quả hơn ++ x, được tăng tại chỗ và trả về.

Tuy nhiên, một điều đáng nói khác là hầu hết các trình biên dịch sẽ có thể tối ưu hóa những thứ không cần thiết như vậy khi có thể, ví dụ: cả hai tùy chọn sẽ dẫn đến cùng một mã ở đây:

for (int i(0);i<10;++i)
for (int i(0);i<10;i++)

5

Tôi đồng ý với @BeowulfOF, mặc dù để rõ ràng, tôi sẽ luôn ủng hộ việc chia nhỏ các câu lệnh để logic hoàn toàn rõ ràng, tức là:

i++;
x += i;

hoặc là

x += i;
i++;

Vì vậy, câu trả lời của tôi là nếu bạn viết mã rõ ràng thì điều này hiếm khi quan trọng (và nếu nó quan trọng thì mã của bạn có thể không đủ rõ ràng).


2

Chỉ muốn nhấn mạnh lại rằng ++ x được mong đợi sẽ nhanh hơn x ++, (đặc biệt nếu x là một đối tượng của một số kiểu tùy ý), vì vậy trừ khi được yêu cầu vì lý do logic, ++ x nên được sử dụng.


2
Tôi chỉ muốn nhấn mạnh rằng sự nhấn mạnh này có nhiều khả năng gây hiểu lầm. Nếu bạn đang nhìn vào một số vòng lặp kết thúc bằng "x ++" riêng biệt và nghĩ "Aha! - đó là lý do khiến nó chạy quá chậm!" và bạn thay đổi nó thành "++ x", sau đó mong đợi chính xác không có sự khác biệt. Những người lạc quan đủ thông minh để nhận ra rằng không cần tạo các biến tạm thời khi không ai sử dụng kết quả của họ. Hàm ý là các cơ sở mã cũ có "x ++" nên được để yên - bạn có nhiều khả năng mắc lỗi khi thay đổi chúng hơn là cải thiện hiệu suất ở bất kỳ đâu.
omatai

1

Bạn đã giải thích sự khác biệt một cách chính xác. Nó chỉ phụ thuộc vào việc bạn muốn x tăng lên trước mỗi lần chạy qua một vòng lặp hay sau đó. Nó phụ thuộc vào logic chương trình của bạn, những gì là phù hợp.

Một sự khác biệt quan trọng khi xử lý STL-Iterator (cũng triển khai các toán tử này) là nó ++ tạo ra một bản sao của đối tượng mà trình vòng lặp trỏ tới, sau đó tăng dần và sau đó trả về bản sao. ++ ngược lại, nó thực hiện việc tăng trước và sau đó trả về một tham chiếu đến đối tượng mà trình lặp hiện trỏ đến. Điều này chủ yếu chỉ phù hợp khi mỗi bit của hiệu suất được tính hoặc khi bạn triển khai STL-iterator của riêng mình.

Chỉnh sửa: sửa lỗi kết hợp ký hiệu tiền tố và hậu tố


Việc nói về "trước / sau" sự lặp lại của một vòng lặp chỉ có ý nghĩa nếu sự tăng / giảm trước / sau xảy ra trong điều kiện. Thông thường, nó sẽ nằm trong mệnh đề tiếp tục, nơi nó không thể thay đổi bất kỳ logic nào, mặc dù nó có thể chậm hơn đối với các loại lớp sử dụng postfix và mọi người không nên sử dụng mà không có lý do.
underscore_d

1

Dạng hậu tố của ++, - toán tử tuân theo quy tắc use-then-change ,

Tiền tố dạng (++ x, - x) tuân theo quy tắc thay đổi-rồi-sử dụng .

Ví dụ 1:

Khi nhiều giá trị được xếp tầng với << sử dụng cout thì các phép tính (nếu có) diễn ra từ phải sang trái nhưng việc in diễn ra từ trái sang phải, ví dụ: (nếu val nếu ban đầu là 10)

 cout<< ++val<<" "<< val++<<" "<< val;

sẽ dẫn đến

12    10    10 

Ví dụ 2:

Trong Turbo C ++, nếu nhiều lần xuất hiện của ++ hoặc (ở bất kỳ dạng nào) được tìm thấy trong một biểu thức, thì trước hết tất cả các dạng tiền tố được tính toán sau đó biểu thức được đánh giá và cuối cùng các dạng hậu tố được tính toán, ví dụ:

int a=10,b;
b=a++ + ++a + ++a + a;
cout<<b<<a<<endl;

Đầu ra của nó trong Turbo C ++ sẽ là

48 13

Trong khi đầu ra của nó trong trình biên dịch hiện đại sẽ là (vì chúng tuân theo các quy tắc nghiêm ngặt)

45 13
  • Lưu ý: Không nên sử dụng nhiều toán tử tăng / giảm trên cùng một biến trong một biểu thức. Việc xử lý / kết quả của các
    biểu thức như vậy khác nhau giữa các trình biên dịch.

Không phải là các biểu thức có chứa nhiều phép toán giảm / thay đổi "thay đổi từ trình biên dịch này sang trình biên dịch khác", mà tệ hơn: nhiều sửa đổi như vậy giữa các điểm trình tự có hành vi không xác định và đầu độc chương trình.
underscore_d

0

Hiểu cú pháp ngôn ngữ là quan trọng khi xem xét tính rõ ràng của mã. Hãy xem xét việc sao chép một chuỗi ký tự, chẳng hạn với dấu tăng sau:

char a[256] = "Hello world!";
char b[256];
int i = 0;
do {
  b[i] = a[i];
} while (a[i++]);

Chúng tôi muốn vòng lặp thực thi thông qua việc gặp phải ký tự 0 (kiểm tra sai) ở cuối chuỗi. Điều đó yêu cầu kiểm tra giá trị tăng trước và cũng tăng chỉ số. Nhưng không nhất thiết phải theo thứ tự đó - một cách để viết mã này với số tăng trước sẽ là:

int i = -1;
do {
  ++i;
  b[i] = a[i];
} while (a[i]);

Vấn đề là sở thích rõ ràng hơn và nếu máy có vô số thanh ghi thì cả hai phải có thời gian thực thi giống nhau, ngay cả khi [i] là một hàm đắt tiền hoặc có tác dụng phụ. Một sự khác biệt đáng kể có thể là giá trị thoát của chỉ mục.

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.