Không phải luôn luôn khởi tạo các biến, nên dẫn đến các lỗi quan trọng bị ẩn?


35

Nguyên tắc cốt lõi C ++ có quy tắc ES.20: Luôn khởi tạo một đối tượng .

Tránh các lỗi được sử dụng trước khi đặt và hành vi không xác định liên quan của chúng. Tránh các vấn đề với sự hiểu biết về khởi tạo phức tạp. Đơn giản hóa tái cấu trúc.

Nhưng quy tắc này không giúp tìm ra lỗi, nó chỉ che giấu chúng.
Giả sử rằng một chương trình có một đường dẫn thực thi trong đó nó sử dụng một biến chưa được khởi tạo. Đây là một lỗi. Hành vi không xác định sang một bên, điều đó cũng có nghĩa là đã xảy ra sự cố và chương trình có thể không đáp ứng yêu cầu sản phẩm của nó. Khi nó sẽ được triển khai để sản xuất, có thể mất tiền, hoặc thậm chí tệ hơn.

Làm thế nào để chúng tôi sàng lọc lỗi? Chúng tôi viết bài kiểm tra. Nhưng các bài kiểm tra không bao gồm 100% đường dẫn thực hiện và các bài kiểm tra không bao giờ bao gồm 100% các đầu vào chương trình. Hơn thế nữa, ngay cả một bài kiểm tra bao gồm một đường dẫn thực thi bị lỗi - nó vẫn có thể vượt qua. Cuối cùng, đó là hành vi không xác định, một biến chưa được khởi tạo có thể có giá trị hợp lệ.

Nhưng ngoài các thử nghiệm của chúng tôi, chúng tôi có các trình biên dịch có thể viết một cái gì đó như 0xCDCDCDCD cho các biến chưa được khởi tạo. Điều này hơi cải thiện tỷ lệ phát hiện của các xét nghiệm.
Thậm chí tốt hơn - có các công cụ như Trình khử trùng địa chỉ, sẽ bắt tất cả các lần đọc các byte bộ nhớ chưa được khởi tạo.

Và cuối cùng, có các máy phân tích tĩnh, có thể nhìn vào chương trình và cho biết rằng có một giá trị đọc trước được đặt trên đường dẫn thực thi đó.

Vì vậy, chúng tôi có nhiều công cụ mạnh mẽ, nhưng nếu chúng tôi khởi tạo biến - chất khử trùng không tìm thấy gì .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Có một quy tắc khác - nếu thực thi chương trình gặp lỗi, chương trình sẽ chết càng sớm càng tốt. Không cần phải giữ cho nó sống, chỉ cần sụp đổ, viết một vụ tai nạn, đưa nó cho các kỹ sư để điều tra.
Việc khởi tạo các biến không cần thiết sẽ làm ngược lại - chương trình đang được giữ nguyên, khi đó nó sẽ bị lỗi phân đoạn.


10
Mặc dù tôi nghĩ rằng đây là một câu hỏi hay, tôi không hiểu ví dụ của bạn. Nếu xảy ra lỗi đọc và bytes_readkhông được thay đổi (vì vậy giữ nguyên số 0), tại sao điều này được coi là lỗi? Chương trình vẫn có thể tiếp tục một cách lành mạnh miễn là nó không hoàn toàn mong đợi bytes_read!=0sau đó. Vì vậy, nó là vệ sinh tốt không phàn nàn. Mặt khác, khi bytes_readkhông được khởi tạo trước, chương trình sẽ không thể tiếp tục một cách lành mạnh, do đó, không khởi tạo bytes_readthực sự đưa ra một lỗi không có trước đó.
Doc Brown

2
@Abyx: ngay cả khi đó là bên thứ ba, nếu nó không xử lý bộ đệm bắt đầu bằng \0lỗi. Nếu nó được ghi nhận là không giải quyết điều đó, mã cuộc gọi của bạn bị lỗi. Nếu bạn sửa mã cuộc gọi của mình để kiểm tra bytes_read==0trước khi gọi sử dụng, thì bạn sẽ quay lại nơi bạn đã bắt đầu: mã của bạn bị lỗi nếu bạn không khởi tạo bytes_read, an toàn nếu bạn thực hiện. ( Thông thường các hàm được cho là điền vào các tham số ngoài của chúng ngay cả trong trường hợp có lỗi : không thực sự. Thông thường, các đầu ra thường bị bỏ lại một mình hoặc không xác định.)
Mat

1
Có một số lý do mã này bỏ qua err_ttrả lại bởi my_read()? Nếu có một lỗi ở bất cứ đâu trong ví dụ, thì đó là.
Blrfl

1
Thật dễ dàng: chỉ khởi tạo các biến nếu nó có ý nghĩa. Nếu không thì không. Tôi có thể đồng ý mặc dù việc sử dụng dữ liệu "giả" để làm điều đó là xấu, bởi vì nó che giấu các lỗi.
Pieter B

1
"Có một quy tắc khác - nếu thực thi chương trình gặp lỗi, chương trình sẽ chết càng sớm càng tốt. Không cần phải giữ nó sống, chỉ cần gặp sự cố, viết một sự cố, đưa nó cho các kỹ sư để điều tra.": Hãy thử điều đó trên một chuyến bay phần mềm điều khiển. Chúc may mắn phục hồi bãi rác từ đống đổ nát máy bay.
Giorgio

Câu trả lời:


44

Lý luận của bạn đi sai trên một số tài khoản:

  1. Lỗi phân khúc là xa nhất định để xảy ra. Sử dụng một biến chưa được khởi tạo dẫn đến hành vi không xác định . Lỗi phân đoạn là một cách mà hành vi như vậy có thể tự biểu hiện, nhưng dường như chạy bình thường cũng có khả năng như vậy.
  2. Trình biên dịch không bao giờ lấp đầy bộ nhớ chưa khởi tạo với một mẫu xác định (như 0xCD). Đây là điều mà một số trình gỡ lỗi thực hiện để hỗ trợ bạn trong việc tìm kiếm các địa điểm nơi các biến chưa được khởi tạo được sử dụng. Nếu bạn chạy một chương trình như vậy bên ngoài trình gỡ lỗi, thì biến sẽ chứa rác hoàn toàn ngẫu nhiên. Cũng có khả năng là một bộ đếm như bytes_readcó giá trị 10vì nó có giá trị 0xcdcdcdcd.
  3. Ngay cả khi bạn đang chạy trong trình gỡ lỗi đặt bộ nhớ chưa được khởi tạo thành một mẫu cố định, chúng chỉ làm như vậy khi khởi động. Điều này có nghĩa là cơ chế này chỉ hoạt động đáng tin cậy cho các biến tĩnh (và có thể được phân bổ heap). Đối với các biến tự động, được phân bổ trên ngăn xếp hoặc chỉ sống trong một thanh ghi, khả năng cao là biến được lưu trữ ở một vị trí đã được sử dụng trước đó, do đó mẫu bộ nhớ kể đã bị ghi đè.

Ý tưởng đằng sau hướng dẫn để luôn khởi tạo các biến là cho phép hai tình huống này

  1. Biến chứa một giá trị hữu ích ngay từ khi bắt đầu tồn tại. Nếu bạn kết hợp điều đó với hướng dẫn chỉ khai báo một biến khi bạn cần nó, bạn có thể tránh các lập trình viên bảo trì trong tương lai rơi vào bẫy bắt đầu sử dụng một biến giữa khai báo của nó và gán đầu tiên, trong đó biến sẽ tồn tại nhưng không được khởi tạo.

  2. Biến chứa một giá trị được xác định mà bạn có thể kiểm tra sau này, để biết liệu một hàm như my_readđã cập nhật giá trị hay chưa. Nếu không khởi tạo, bạn không thể biết liệu bytes_readthực sự có giá trị hợp lệ hay không, bởi vì bạn không thể biết nó bắt đầu với giá trị nào.


8
1) đó là tất cả về xác suất, như 1% so với 99%. 2 và 3) VC ++ cũng tạo mã khởi tạo như vậy cho các biến cục bộ. 3) biến tĩnh (toàn cầu) luôn được khởi tạo với 0.
Abyx

5
@Abyx: 1) Theo kinh nghiệm của tôi, xác suất là ~ 80% "không có sự khác biệt rõ ràng về hành vi", 10% "làm sai", 10% "segfault". Đối với (2) và (3): VC ++ chỉ thực hiện điều này trong các bản dựng gỡ lỗi. Dựa vào đó là một ý tưởng tồi tệ khủng khiếp vì nó chọn lọc phá vỡ các bản dựng phát hành và không xuất hiện trong rất nhiều thử nghiệm của bạn.
Christian Aichinger

8
Tôi nghĩ rằng "ý tưởng đằng sau hướng dẫn" là phần quan trọng nhất của câu trả lời này. Các hướng dẫn là hoàn toàn không nói với bạn để làm theo mọi khai báo biến với = 0;. Mục đích của lời khuyên là khai báo biến tại điểm mà bạn sẽ có một giá trị hữu ích cho nó và ngay lập tức gán giá trị này. Điều này được làm rõ ràng trong các quy tắc ngay sau ES21 và ES22. Cả ba nên được hiểu là làm việc cùng nhau; không phải là quy tắc cá nhân không liên quan.
GrandOpener

1
@GrandOpener Chính xác. Nếu không có giá trị có ý nghĩa để gán tại điểm mà biến được khai báo, phạm vi của biến có thể sai.
Kevin Krumwiede

5
"Trình biên dịch không bao giờ điền" không phải lúc nào cũng vậy?
CodeInChaos

25

Bạn đã viết "quy tắc này không giúp tìm lỗi, nó chỉ che giấu chúng" - tốt, mục tiêu của quy tắc không phải là giúp tìm lỗi, mà là để tránh chúng. Và khi một lỗi được tránh, không có gì ẩn.

Hãy giải quyết vấn đề theo ví dụ của bạn: giả sử my_readhàm có hợp đồng bằng văn bản để khởi tạo bytes_readtrong mọi trường hợp, nhưng trong trường hợp này không có lỗi, do đó, nó bị lỗi, ít nhất là trong trường hợp này. Ý định của bạn là sử dụng môi trường thời gian chạy để hiển thị lỗi đó bằng cách không khởi tạo bytes_readtham số trước. Miễn là bạn biết chắc chắn có một chất khử trùng địa chỉ, đó thực sự là một cách có thể để phát hiện ra một lỗi như vậy. Để sửa lỗi, người ta phải thay đổi my_readchức năng bên trong.

Nhưng có một quan điểm khác, ít nhất là có giá trị như nhau: hành vi bị lỗi chỉ xuất hiện từ sự kết hợp không khởi tạo bytes_readtrước gọi my_readsau đó (với kỳ vọng bytes_readđược khởi tạo sau đó). Đây là một tình huống sẽ xảy ra thường xuyên trong các thành phần trong thế giới thực khi thông số kỹ thuật bằng văn bản cho một chức năng như my_readkhông rõ ràng 100% hoặc thậm chí sai về hành vi trong trường hợp có lỗi. Tuy nhiên, miễn bytes_readlà được khởi tạo về 0 trước cuộc gọi, chương trình hoạt động giống như khi khởi tạo được thực hiện bên trong my_read, do đó, nó hoạt động chính xác, trong kết hợp này không có lỗi trong chương trình.

Vì vậy, khuyến nghị của tôi theo sau đó là: chỉ sử dụng phương pháp không khởi tạo nếu

  • bạn muốn kiểm tra nếu một hàm hoặc khối mã khởi tạo một tham số cụ thể
  • bạn chắc chắn 100% rằng hàm có cổ phần có hợp đồng trong đó chắc chắn sai khi không gán giá trị cho tham số đó
  • bạn chắc chắn 100% môi trường có thể bắt được điều này

Đây là những điều kiện bạn thường có thể sắp xếp trong mã kiểm tra , cho một môi trường công cụ cụ thể.

Tuy nhiên, trong mã sản xuất, tốt hơn là luôn luôn khởi tạo một biến như vậy trước đó, đó là cách tiếp cận phòng thủ hơn, ngăn ngừa lỗi trong trường hợp hợp đồng không đầy đủ hoặc sai, hoặc trong trường hợp vệ sinh địa chỉ hoặc các biện pháp an toàn tương tự không được kích hoạt. Và quy tắc "sụp đổ sớm" được áp dụng, như bạn đã viết chính xác, nếu thực thi chương trình gặp lỗi. Nhưng khi khởi tạo một biến trước có nghĩa là không có gì sai, thì không cần phải dừng thực thi thêm.


4
Đây chính xác là những gì tôi đã nghĩ khi tôi đọc nó. Nó không quét những thứ dưới tấm thảm, nó quét chúng vào thùng rác!
corsiKa

22

Luôn khởi tạo các biến của bạn

Sự khác biệt giữa các tình huống bạn đang xem xét là trường hợp không khởi tạo dẫn đến hành vi không xác định , trong khi trường hợp bạn dành thời gian để khởi tạo sẽ tạo ra một định nghĩa rõ ràng và xác định lỗi . Tôi không thể nhấn mạnh hai trường hợp này cực kỳ khác nhau như thế nào là đủ.

Hãy xem xét một ví dụ giả thuyết có thể xảy ra với một nhân viên giả định trong chương trình mô phỏng giả thuyết. Nhóm giả thuyết này đã cố gắng đưa ra một mô phỏng xác định để chứng minh rằng sản phẩm mà họ đang bán theo giả thuyết đáp ứng nhu cầu.

Được rồi, tôi sẽ dừng lại với việc tiêm từ. Tôi nghĩ rằng bạn sẽ có được điểm ;-)

Trong mô phỏng này, có hàng trăm biến số chưa được khởi tạo. Một nhà phát triển đã chạy valgrind trên mô phỏng và nhận thấy có một số lỗi "nhánh trên giá trị chưa được khởi tạo". "Hmm, có vẻ như điều đó có thể gây ra sự không xác định, khiến cho việc lặp lại thử nghiệm khó khăn khi chúng ta cần nó nhất." Nhà phát triển đã đi đến quản lý, nhưng quản lý đã có một lịch trình rất chặt chẽ và không thể có nguồn lực dự phòng để theo dõi vấn đề này. "Chúng tôi cuối cùng đã khởi tạo tất cả các biến của chúng tôi trước khi chúng tôi sử dụng chúng. Chúng tôi có các thực hành mã hóa tốt."

Một vài tháng trước khi giao hàng cuối cùng, khi mô phỏng ở chế độ hoàn chỉnh, và toàn bộ đội đang chạy nước rút để hoàn thành tất cả những gì quản lý đã hứa với ngân sách, giống như mọi dự án từng được tài trợ, quá nhỏ. Ai đó nhận thấy rằng họ không thể kiểm tra một tính năng thiết yếu bởi vì, vì một số lý do, sim xác định không hoạt động xác định để gỡ lỗi.

Toàn bộ nhóm có thể đã bị tạm dừng và dành phần tốt hơn trong 2 tháng để xử lý toàn bộ cơ sở mã mô phỏng sửa các lỗi giá trị chưa được khởi tạo thay vì thực hiện và kiểm tra các tính năng. Không cần phải nói, nhân viên đã bỏ qua "Tôi đã nói với bạn như vậy" và đi thẳng vào việc giúp các nhà phát triển khác hiểu giá trị chưa được khởi tạo là gì. Thật kỳ lạ, các tiêu chuẩn mã hóa đã được thay đổi ngay sau sự cố này, khuyến khích các nhà phát triển luôn khởi tạo các biến của họ.

Và đây là phát súng cảnh báo. Đây là viên đạn sượt qua mũi của bạn. Vấn đề thực tế là rất xa, xa hơn nhiều so với bạn tưởng tượng.

Sử dụng một giá trị chưa được khởi tạo là "hành vi không xác định" (ngoại trừ một vài trường hợp góc như char). Hành vi không xác định (hay viết tắt là UB) rất điên rồ và hoàn toàn xấu đối với bạn, đến mức bạn không bao giờ nên tin rằng nó tốt hơn phương án thay thế. Đôi khi bạn có thể xác định rằng trình biên dịch cụ thể của bạn xác định UB, và sau đó nó an toàn để sử dụng, nhưng nếu không, hành vi không xác định là "bất kỳ hành vi nào trình biên dịch cảm thấy." Nó có thể làm một cái gì đó mà bạn gọi là "lành mạnh" như có một giá trị không xác định. Nó có thể phát ra các opcode không hợp lệ, có khả năng khiến chương trình của bạn bị hỏng. Nó có thể kích hoạt cảnh báo tại thời điểm biên dịch hoặc trình biên dịch thậm chí có thể coi đó là lỗi hoàn toàn.

Hoặc nó có thể không làm gì cả

Chim hoàng yến của tôi trong mỏ than cho UB là một trường hợp từ một công cụ SQL mà tôi đọc. Xin lỗi vì tôi không liên kết nó, tôi đã thất bại trong việc tìm lại bài báo. Có một vấn đề tràn bộ đệm trong công cụ SQL khi bạn chuyển kích thước bộ đệm lớn hơn cho một hàm, nhưng chỉ trên một phiên bản Debian cụ thể. Các lỗi đã đăng nhập nghiêm túc, và khám phá. Phần buồn cười là: lỗi tràn bộ đệm đã được kiểm tra . Có mã để xử lý lỗi tràn bộ đệm tại chỗ. Nó trông giống như thế này:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

Tôi đã thêm nhiều bình luận trong phần trình bày của mình, nhưng ý tưởng là như vậy. Nếu put + dataLengthkết thúc tốt đẹp, nó sẽ nhỏ hơn putcon trỏ (họ đã kiểm tra thời gian biên dịch để đảm bảo int không dấu là kích thước của một con trỏ, đối với người tò mò). Nếu điều này xảy ra, chúng ta biết các thuật toán bộ đệm vòng tiêu chuẩn có thể bị nhầm lẫn bởi sự tràn này, vì vậy chúng ta trả về 0. Hay chúng ta?

Khi nó bật ra, tràn trên con trỏ không được xác định trong C ++. Bởi vì hầu hết các trình biên dịch đang coi con trỏ là số nguyên, chúng tôi kết thúc với các hành vi tràn số nguyên điển hình, đó là hành vi chúng tôi muốn. Tuy nhiên, đây hành vi không xác định, có nghĩa là trình biên dịch được phép làm bất cứ điều gì nó muốn.

Trong trường hợp xảy ra lỗi này, Debian đã chọn sử dụng một phiên bản gcc mới mà không có bất kỳ hương vị Linux chính nào khác được cập nhật trong các bản phát hành sản xuất của họ. Phiên bản mới này của gcc có trình tối ưu hóa mã chết tích cực hơn. Trình biên dịch đã thấy hành vi không xác định và quyết định kết quả của ifcâu lệnh sẽ là "bất cứ điều gì làm cho tối ưu hóa mã tốt nhất", đó là một bản dịch hoàn toàn hợp pháp của UB. Theo đó, nó đưa ra giả định rằng vì ptr+dataLengthkhông bao giờ có thể ở dưới ptrnếu không có tràn con trỏ UB, ifcâu lệnh sẽ không bao giờ kích hoạt và tối ưu hóa kiểm tra tràn bộ đệm.

Việc sử dụng UB "lành mạnh" thực sự đã khiến một sản phẩm SQL chính bị khai thác bộ đệm tràn ngập mà nó đã viết mã để tránh!

Không bao giờ dựa vào hành vi không xác định. Không bao giờ.


Đối với một bài đọc rất thú vị về hành vi Không xác định, phần mềm.intel.com / en-us / bloss / 2013/01/06 / Đây là một bài viết tuyệt vời bằng văn bản về mức độ xấu của nó có thể đi. Tuy nhiên, bài đăng cụ thể đó là về các hoạt động nguyên tử, rất khó hiểu đối với hầu hết, vì vậy tôi tránh đề xuất nó như là một mồi cho UB và làm thế nào nó có thể đi sai.
Cort Ammon

1
Tôi ước C có bản chất để đặt một giá trị hoặc một mảng trong số chúng thành các giá trị không xác định, không bẫy, hoặc các giá trị không xác định, hoặc biến các giá trị khó chịu thành các giá trị ít khó chịu hơn (không xác định được bẫy hoặc không xác định). Trình biên dịch có thể sử dụng các chỉ thị như vậy để hỗ trợ tối ưu hóa hữu ích và các lập trình viên có thể sử dụng chúng để tránh phải viết mã vô dụng trong khi chặn phá vỡ "tối ưu hóa" khi sử dụng những thứ như kỹ thuật ma trận thưa thớt.
supercat

@supercat Đây sẽ là một tính năng hay, giả sử bạn đang nhắm mục tiêu các nền tảng trong đó đó là một giải pháp hợp lệ. Một trong những ví dụ về các vấn đề đã biết là khả năng tạo các mẫu bộ nhớ không chỉ không hợp lệ cho loại bộ nhớ mà còn không thể đạt được thông qua các phương tiện thông thường. boollà một ví dụ tuyệt vời khi có vấn đề rõ ràng, nhưng chúng xuất hiện ở nơi khác trừ khi bạn cho rằng bạn đang làm việc trên một nền tảng rất hữu ích như x86 hoặc ARM hoặc MIPS khi tất cả các vấn đề này xảy ra được giải quyết tại thời điểm opcode.
Cort Ammon

Hãy xem xét trường hợp trình tối ưu hóa có thể chứng minh rằng giá trị được sử dụng cho switchnhỏ hơn 8, do kích thước của số học số nguyên, vì vậy họ có thể sử dụng các hướng dẫn nhanh được cho là không có rủi ro về giá trị "lớn" xuất hiện. giá trị không xác định (không bao giờ có thể được xây dựng bằng quy tắc của trình biên dịch) xuất hiện, làm điều gì đó bất ngờ và đột nhiên bạn có một bước nhảy lớn từ cuối bảng nhảy. Cho phép kết quả không xác định ở đây có nghĩa là mọi câu lệnh chuyển đổi trong chương trình phải có thêm bẫy để hỗ trợ những trường hợp này "không bao giờ có thể xảy ra".
Cort Ammon

Nếu nội tại được tiêu chuẩn hóa, trình biên dịch có thể được yêu cầu làm bất cứ điều gì cần thiết để tôn vinh ngữ nghĩa; nếu ví dụ: một số đường dẫn mã đặt một biến và một số thì không, và một nội tại sau đó nói "chuyển đổi thành Giá trị không xác định nếu không được xác định hoặc không xác định; nếu không thì", một trình biên dịch cho các nền tảng có các thanh ghi "không phải là giá trị" sẽ phải chèn mã để khởi tạo biến trước bất kỳ đường dẫn mã nào hoặc trên bất kỳ đường dẫn mã nào được khởi tạo sẽ bị bỏ qua, nhưng phân tích ngữ nghĩa cần thiết để làm điều đó khá đơn giản.
supercat

5

Tôi chủ yếu làm việc trong một ngôn ngữ lập trình chức năng nơi bạn không được phép gán lại các biến. Không bao giờ. Điều đó loại bỏ hoàn toàn lớp lỗi này. Điều này ban đầu có vẻ như là một hạn chế rất lớn, nhưng nó buộc bạn phải cấu trúc mã theo cách phù hợp với thứ tự bạn học dữ liệu mới, có xu hướng đơn giản hóa mã của bạn và giúp duy trì dễ dàng hơn.

Những thói quen đó có thể được chuyển sang ngôn ngữ mệnh lệnh là tốt. Gần như luôn luôn có thể cấu trúc lại mã của bạn để tránh khởi tạo một biến có giá trị giả. Đó là những gì những hướng dẫn đang bảo bạn làm. Họ muốn bạn đặt một cái gì đó có ý nghĩa vào đó, không phải thứ gì đó sẽ làm cho các công cụ tự động hài lòng.

Ví dụ của bạn với API kiểu C khó hơn một chút. Trong những trường hợp đó, khi tôi sử dụng hàm, tôi sẽ khởi tạo thành 0 để giữ cho trình biên dịch không phàn nàn, nhưng một lần trong các my_readbài kiểm tra đơn vị, tôi sẽ khởi tạo một thứ khác để đảm bảo điều kiện lỗi hoạt động đúng. Bạn không cần phải kiểm tra mọi tình trạng lỗi có thể xảy ra sau mỗi lần sử dụng.


5

Không, nó không che giấu lỗi. Thay vào đó, nó làm cho hành vi trở nên xác định theo cách mà nếu người dùng gặp lỗi, nhà phát triển có thể tái tạo nó.


1
Và khởi tạo với -1 có thể thực sự có ý nghĩa. Trong đó "int byte_read = 0" là xấu, bởi vì bạn thực sự có thể đọc 0 byte, việc khởi tạo nó bằng -1 cho thấy rất rõ ràng không có nỗ lực nào để đọc byte đã thành công và bạn có thể kiểm tra điều đó.
Pieter B

4

TL; DR: Có hai cách để làm cho chương trình này chính xác, khởi tạo các biến của bạn và cầu nguyện. Chỉ có một kết quả nhất quán.


Trước khi tôi có thể trả lời câu hỏi của bạn, trước tiên tôi sẽ cần giải thích ý nghĩa của Hành vi không xác định . Trên thực tế, tôi sẽ để một tác giả biên dịch thực hiện phần lớn công việc:

Nếu bạn không muốn đọc những bài viết đó, TL; DR là:

Hành vi không xác định là một hợp đồng xã hội giữa nhà phát triển và trình biên dịch; trình biên dịch giả định với niềm tin mù quáng rằng người dùng của nó sẽ không bao giờ, dựa vào Hành vi không xác định.

Các nguyên mẫu của "Quỷ bay từ mũi của bạn" đã hoàn toàn thất bại trong việc truyền đạt ý nghĩa của sự thật này, thật không may. Mặc dù có nghĩa là để chứng minh rằng bất cứ điều gì có thể xảy ra, nó hoàn toàn không thể tin được rằng nó chủ yếu bị nhún vai.

Tuy nhiên, sự thật là Hành vi không xác định ảnh hưởng đến chính quá trình biên dịch , rất lâu trước khi bạn thậm chí cố gắng sử dụng chương trình (có công cụ hay không, trong trình gỡ lỗi hay không) và hoàn toàn có thể thay đổi hành vi của nó.

Tôi tìm thấy ví dụ trong phần 2 ở trên:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

được chuyển thành:

void contains_null_check(int *P) {
  *P = 4;
}

bởi vì rõ ràng là Pkhông thể 0vì nó đã bị hủy đăng ký trước khi được kiểm tra.


Làm thế nào điều này áp dụng cho ví dụ của bạn?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

Chà, bạn đã phạm phải một lỗi phổ biến khi cho rằng Hành vi không xác định sẽ gây ra lỗi thời gian chạy. Nó có thể không.

Chúng ta hãy tưởng tượng rằng định nghĩa của my_readlà:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

và tiến hành như mong đợi của một trình biên dịch tốt với nội tuyến:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Sau đó, như mong đợi về một trình biên dịch tốt, chúng tôi tối ưu hóa các nhánh vô dụng:

  1. Không nên sử dụng biến không được khởi tạo
  2. bytes_readsẽ được sử dụng chưa được khởi tạo nếu resultkhông0
  3. Nhà phát triển hứa hẹn resultsẽ không bao giờ 0!

Vì vậy, resultkhông bao giờ 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ồ, resultkhông bao giờ được sử dụng:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ồ, chúng ta có thể hoãn tuyên bố bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Và ở đây chúng tôi, một sự chuyển đổi xác nhận nghiêm ngặt của bản gốc và không có trình gỡ lỗi nào sẽ bẫy một biến chưa được khởi tạo bởi vì không có biến nào.

Tôi đã đi xuống con đường đó, hiểu vấn đề khi hành vi dự kiến ​​và lắp ráp không khớp thực sự không có gì thú vị.


Đôi khi tôi nghĩ rằng các trình biên dịch sẽ nhận được chương trình xóa các tệp nguồn khi chúng thực thi một đường dẫn UB. Sau đó, các lập trình viên sẽ tìm hiểu ý nghĩa của UB đối với người dùng cuối của họ ....
mattnz

1

Hãy xem xét kỹ hơn về mã ví dụ của bạn:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Đây là một ví dụ tốt. Nếu chúng tôi dự đoán một lỗi như vậy, chúng tôi có thể chèn dòng assert(bytes_read > 0);và bắt lỗi này trong thời gian chạy, điều này là không thể với một biến chưa được khởi tạo.

Nhưng giả sử chúng ta không, và chúng ta tìm thấy một lỗi bên trong hàm use(buffer). Chúng tôi tải chương trình lên trong trình gỡ lỗi, kiểm tra backtrace và tìm ra rằng nó được gọi từ mã này. Vì vậy, chúng tôi đặt một điểm dừng ở đầu đoạn trích này, chạy lại và tái tạo lỗi. Chúng tôi đơn bước qua cố gắng để bắt nó.

Nếu chúng ta chưa khởi tạo bytes_read, nó chứa rác. Nó không nhất thiết phải chứa cùng một loại rác mỗi lần. Chúng tôi bước qua dòng my_read(buffer, &bytes_read);. Bây giờ, nếu đó là một giá trị khác so với trước đây, chúng tôi có thể không thể tái tạo lỗi của chúng tôi! Nó có thể hoạt động vào lần tiếp theo, trên cùng một đầu vào, do tai nạn hoàn toàn. Nếu nó luôn bằng không, chúng ta sẽ có hành vi nhất quán.

Chúng tôi kiểm tra giá trị, thậm chí có thể trên một backtrace trong cùng một lần chạy. Nếu nó bằng không, chúng ta có thể thấy có gì đó không ổn; bytes_readkhông nên bằng không về thành công. (Hoặc nếu có thể, chúng tôi có thể muốn khởi tạo nó thành -1.) Chúng tôi có thể bắt lỗi ở đây. Tuy nhiên, nếu bytes_readmột giá trị hợp lý, điều đó xảy ra là sai, chúng ta sẽ nhận ra nó trong nháy mắt?

Điều này đặc biệt đúng với các con trỏ: một con trỏ NULL sẽ luôn rõ ràng trong trình gỡ lỗi, có thể được kiểm tra rất dễ dàng và nên phân tách trên phần cứng hiện đại nếu chúng ta cố gắng xóa bỏ nó. Một con trỏ rác có thể gây ra lỗi hỏng bộ nhớ không thể sửa chữa sau này và chúng gần như không thể gỡ lỗi.


1

OP không dựa vào hành vi không xác định, hoặc ít nhất là không chính xác. Thật vậy, dựa vào hành vi không xác định là xấu. Đồng thời, hành vi của một chương trình trong trường hợp bất ngờ cũng không được xác định, nhưng một loại khác không xác định. Nếu bạn đặt một biến số không, nhưng bạn không có ý định để có một con đường thực hiện mà sử dụng mà không ban đầu, sẽ cư xử chương trình của bạn sanely khi bạn có một lỗi và làm có một con đường như vậy? Bây giờ bạn đang ở trong cỏ dại; bạn không có kế hoạch sử dụng giá trị đó, nhưng dù sao bạn cũng đang sử dụng nó. Có thể nó sẽ vô hại, hoặc có thể nó sẽ khiến chương trình bị sập, hoặc có thể nó sẽ khiến chương trình âm thầm làm hỏng dữ liệu. Bạn không biết.

Điều OP đang nói là có những công cụ sẽ giúp bạn tìm ra lỗi này, nếu bạn cho phép chúng. Nếu bạn không khởi tạo giá trị, nhưng sau đó bạn vẫn sử dụng nó, có những máy phân tích tĩnh và động sẽ cho bạn biết rằng bạn có lỗi. Một bộ phân tích tĩnh sẽ cho bạn biết trước khi bạn bắt đầu kiểm tra chương trình. Mặt khác, nếu bạn khởi tạo một cách mù quáng giá trị, các máy phân tích không thể nói rằng bạn không có kế hoạch sử dụng giá trị ban đầu đó và do đó lỗi của bạn không bị phát hiện. Nếu bạn may mắn, nó vô hại hoặc chỉ làm hỏng chương trình; nếu bạn không may mắn, nó sẽ âm thầm làm hỏng dữ liệu.

Nơi duy nhất tôi không đồng ý với OP là ở cuối cùng, nơi anh ấy nói "khi nào nó sẽ có một lỗi phân khúc khác." Thật vậy, một biến chưa được khởi tạo sẽ không tạo ra lỗi phân đoạn một cách đáng tin cậy. Thay vào đó, tôi sẽ nói rằng bạn nên sử dụng các công cụ phân tích tĩnh sẽ không cho phép bạn đến điểm thậm chí cố gắng thực hiện chương trình.


0

Một câu trả lời cho câu hỏi của bạn cần được chia thành các loại biến khác nhau xuất hiện trong một chương trình:


Biến cục bộ

Thông thường khai báo phải ở đúng vị trí mà biến đầu tiên nhận được giá trị của nó. Không dự đoán trước các biến như trong kiểu cũ C:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Điều này loại bỏ 99% nhu cầu khởi tạo, các biến có giá trị cuối cùng ngay từ khi tắt. Một vài ngoại lệ là nơi khởi tạo phụ thuộc vào một số điều kiện:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Tôi tin rằng nên viết những trường hợp như thế này:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I E. khẳng định rõ ràng rằng một số khởi tạo hợp lý của biến của bạn được thực hiện.


Biến thành viên

Ở đây tôi đồng ý với những gì người trả lời khác nói: Những điều này phải luôn được khởi tạo bởi các danh sách hàm tạo / trình khởi tạo. Nếu không, bạn khó có thể đảm bảo sự thống nhất giữa các thành viên của bạn. Và nếu bạn có một tập hợp các thành viên dường như không cần khởi tạo trong mọi trường hợp, hãy cấu trúc lại lớp của bạn, thêm các thành viên đó vào một lớp dẫn xuất, nơi chúng luôn luôn cần thiết.


Bộ đệm

Đây là nơi tôi không đồng ý với các câu trả lời khác. Khi mọi người tôn giáo về việc khởi tạo các biến, họ thường kết thúc việc khởi tạo bộ đệm như thế này:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Tôi tin rằng điều này hầu như luôn luôn có hại: Hiệu ứng duy nhất của những khởi tạo này là chúng khiến các công cụ như valgrindbất lực. Bất kỳ mã nào đọc nhiều hơn từ bộ đệm khởi tạo hơn rất có thể là một lỗi. Nhưng với việc khởi tạo, lỗi đó không thể bị lộ valgrind. Vì vậy, đừng sử dụng chúng trừ khi bạn thực sự dựa vào bộ nhớ chứa đầy số không (và trong trường hợp đó, hãy bỏ một bình luận cho biết bạn cần số 0 để làm gì).

Tôi cũng rất khuyến nghị nên thêm một mục tiêu vào hệ thống xây dựng của bạn để chạy toàn bộ testsuite bên dưới valgrindhoặc một công cụ tương tự để phơi bày các lỗi sử dụng trước khi khởi tạo và rò rỉ bộ nhớ. Điều này có giá trị hơn tất cả các tiền khởi đầu của các biến. valgrindMục tiêu đó nên được thực hiện một cách thường xuyên, quan trọng nhất là trước khi bất kỳ mã nào được công khai.


Biến toàn cầu

Bạn không thể có các biến toàn cục không được khởi tạo (ít nhất là trong C / C ++, v.v.), vì vậy hãy đảm bảo rằng khởi tạo này là những gì bạn muốn.


Nhận thấy rằng bạn có thể viết khởi tạo có điều kiện với các nhà điều hành ternary, ví dụ Base& b = foo() ? new Derived1 : new Derived2;
Davislor

@Lorehead Điều đó có thể làm việc cho các trường hợp đơn giản, nhưng nó sẽ không hoạt động cho những trường hợp phức tạp hơn: Bạn không muốn làm điều này nếu bạn có ba trường hợp trở lên và các nhà xây dựng của bạn có ba hoặc nhiều đối số, đơn giản là để dễ đọc lý do. Và điều đó thậm chí không xem xét bất kỳ tính toán nào có thể cần phải được thực hiện, như tìm kiếm một đối số cho một nhánh khởi tạo trong một vòng lặp.
cmaster

Đối với các trường hợp phức tạp hơn, bạn có thể gói mã khởi tạo trong hàm xuất xưởng : Base &b = base_factory(which);. Điều này hữu ích nhất nếu bạn cần gọi mã nhiều lần hoặc nếu nó cho phép bạn tạo kết quả không đổi.
Davislor

@Lorehead Điều đó đúng, và chắc chắn là con đường để đi nếu logic yêu cầu không đơn giản. Tuy nhiên, tôi tin rằng có một vùng màu xám nhỏ ở giữa nơi khởi tạo thông qua ?:là PITA và chức năng của nhà máy vẫn còn quá mức cần thiết. Những trường hợp này là rất ít và xa, nhưng chúng tồn tại.
cmaster

-2

Một trình biên dịch C, C ++ hoặc Objective-C phong nha với bộ tùy chọn trình biên dịch phù hợp sẽ cho bạn biết tại thời điểm biên dịch nếu một biến được sử dụng trước khi giá trị của nó được đặt. Vì trong các ngôn ngữ này sử dụng giá trị của biến chưa được xác định là hành vi không xác định, "đặt giá trị trước khi bạn sử dụng" không phải là một gợi ý, hoặc hướng dẫn hoặc thực hành tốt, đó là yêu cầu 100%; nếu không thì chương trình của bạn bị hỏng hoàn toàn. Trong các ngôn ngữ khác, như Java và Swift, trình biên dịch sẽ không bao giờ cho phép bạn sử dụng một biến trước khi nó được khởi tạo.

Có sự khác biệt logic giữa "khởi tạo" và "đặt giá trị". Nếu tôi muốn tìm tỷ lệ chuyển đổi giữa đô la và euro và viết "tỷ lệ gấp đôi = 0,0;" sau đó biến có một giá trị được đặt, nhưng nó không được khởi tạo. 0,0 được lưu trữ ở đây không có gì để làm với kết quả chính xác. Trong tình huống này, nếu do lỗi mà bạn không bao giờ lưu trữ tỷ lệ chuyển đổi chính xác, trình biên dịch sẽ không có cơ hội cho bạn biết. Nếu bạn chỉ viết "tỷ lệ gấp đôi;" và không bao giờ lưu trữ một tỷ lệ chuyển đổi có ý nghĩa, trình biên dịch sẽ cho bạn biết.

Vì vậy: Đừng khởi tạo một biến chỉ vì trình biên dịch cho bạn biết nó được sử dụng mà không được khởi tạo. Đó là che giấu một lỗi. Vấn đề thực sự là bạn đang sử dụng một biến mà bạn không nên sử dụng hoặc trên một đường dẫn mã mà bạn không đặt giá trị. Khắc phục sự cố, đừng che giấu nó.

Đừng khởi tạo một biến chỉ vì trình biên dịch có thể cho bạn biết nó được sử dụng mà không được khởi tạo. Một lần nữa, bạn đang che giấu vấn đề.

Khai báo các biến gần để sử dụng. Điều này cải thiện cơ hội mà bạn có thể khởi tạo nó với một giá trị có ý nghĩa tại điểm khai báo.

Tránh sử dụng lại các biến. Khi bạn sử dụng lại một biến, rất có thể nó được khởi tạo thành một giá trị vô dụng khi bạn sử dụng nó cho mục đích thứ hai.

Nó đã được nhận xét rằng một số trình biên dịch có âm tính giả, và việc kiểm tra khởi tạo là tương đương với vấn đề tạm dừng. Cả hai trong thực tế không liên quan. Nếu một trình biên dịch, như được trích dẫn, không thể tìm thấy việc sử dụng biến chưa được khởi tạo mười năm sau khi lỗi được báo cáo, thì đã đến lúc tìm kiếm một trình biên dịch thay thế. Java thực hiện điều này hai lần; một lần trong trình biên dịch, một lần trong trình xác minh, mà không có bất kỳ vấn đề nào. Cách dễ dàng để khắc phục vấn đề tạm dừng không phải là yêu cầu một biến được khởi tạo trước khi sử dụng, mà nó được khởi tạo trước khi sử dụng theo cách có thể được kiểm tra bằng thuật toán đơn giản và nhanh chóng.


Điều này nghe có vẻ bề ngoài tốt, nhưng phụ thuộc quá nhiều vào tính chính xác của các cảnh báo giá trị chưa được khởi tạo. Làm cho những điều này hoàn toàn chính xác tương đương với Vấn đề dừng, và các trình biên dịch sản xuất có thể và chịu các phủ định sai (nghĩa là chúng không chẩn đoán một biến chưa được khởi tạo khi chúng cần phải có); xem ví dụ lỗi GCC 18501 , đã không được trộn trong hơn mười năm nay.
zwol

Những gì bạn nói về gcc chỉ là nói. Phần còn lại là không liên quan.
gnasher729

Thật đáng buồn về gcc, nhưng nếu bạn không hiểu tại sao phần còn lại có liên quan thì bạn cần phải tự học.
zwol
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.