Điều gì xảy ra với một biến được khai báo, chưa được khởi tạo trong C? Liệu nó có một giá trị?


138

Nếu trong CI viết:

int num;

Trước khi tôi gán bất cứ điều gì num, giá trị của numkhông xác định?


4
Ừm, đó không phải là một biến xác định , không phải là một biến được khai báo sao? (Tôi xin lỗi nếu đó là C ++ của tôi tỏa sáng ...)
sbi

6
Không. Tôi có thể khai báo một biến mà không cần xác định nó: extern int x;Tuy nhiên, việc xác định luôn hàm ý khai báo. Điều này không đúng trong C ++, với các biến thành viên lớp tĩnh, người ta có thể định nghĩa mà không cần khai báo, vì khai báo phải nằm trong định nghĩa lớp (không phải khai báo!) Và định nghĩa phải nằm ngoài định nghĩa lớp.
bdonlan

ee.hawaii.edu/~tep/EE160/Book/chap14/subection2.1.1.4.html Có vẻ như được xác định có nghĩa là bạn cũng phải khởi tạo nó.
atp

Câu trả lời:


187

Các biến tĩnh (phạm vi tệp và hàm tĩnh) được khởi tạo thành 0:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Các biến không tĩnh (biến cục bộ) là không xác định . Đọc chúng trước khi gán kết quả giá trị trong hành vi không xác định.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

Trong thực tế, ban đầu họ có xu hướng chỉ có một số giá trị vô nghĩa - một số trình biên dịch thậm chí có thể đưa vào các giá trị cố định, cụ thể để làm cho nó rõ ràng khi tìm kiếm một trình gỡ lỗi - nhưng nói đúng ra, trình biên dịch có thể tự do làm bất cứ điều gì từ sự cố đến việc triệu tập quỷ qua đường mũi của bạn .

Về lý do tại sao hành vi không xác định thay vì chỉ đơn giản là "giá trị không xác định / tùy ý", có một số kiến ​​trúc CPU có các bit cờ bổ sung trong biểu diễn của chúng cho các loại khác nhau. Một ví dụ hiện đại sẽ là Itanium, có bit "Không phải là thứ" trong các thanh ghi của nó ; Tất nhiên, các nhà soạn thảo tiêu chuẩn C đã xem xét một số kiến ​​trúc cũ hơn.

Cố gắng làm việc với một giá trị với các bit cờ được đặt này có thể dẫn đến ngoại lệ CPU trong một hoạt động thực sự không nên thất bại (ví dụ: thêm số nguyên hoặc gán cho một biến khác). Và nếu bạn đi và để lại một biến chưa được khởi tạo, trình biên dịch có thể nhặt một số rác ngẫu nhiên với các bit cờ được đặt - có nghĩa là chạm vào biến chưa được khởi tạo đó có thể gây chết người.


2
ồ không, họ không có. Họ có thể, ở chế độ gỡ lỗi, khi bạn không ở trước khách hàng, vào những tháng có chữ R, nếu bạn may mắn
Martin Beckett

8
cái gì không việc khởi tạo tĩnh được yêu cầu bởi tiêu chuẩn; xem ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan

2
ví dụ đầu tiên là tốt như tôi có thể nói. Tôi không hiểu tại sao trình biên dịch có thể bị sập trong cái thứ hai mặc dù vậy :)

6
@Stuart: có một thứ gọi là "biểu diễn bẫy", về cơ bản là một mẫu bit không biểu thị một giá trị hợp lệ và có thể gây ra các ngoại lệ phần cứng khi chạy. Loại C duy nhất có bảo đảm rằng bất kỳ mẫu bit nào cũng có giá trị hợp lệ là char; tất cả những người khác có thể có đại diện bẫy. Ngoài ra - vì dù sao việc truy cập biến chưa được khởi tạo là UB - trình biên dịch tuân thủ có thể chỉ cần thực hiện một số kiểm tra và quyết định báo hiệu sự cố.
Pavel Minaev

5
bdonia là chính xác. C luôn được chỉ định khá chính xác. Trước C89 và C99, một bài báo của dmr đã chỉ định tất cả những điều này vào đầu những năm 1970. Ngay cả trong hệ thống nhúng thô sơ, chỉ cần một bộ nhớ () để thực hiện đúng, vì vậy không có lý do gì cho một môi trường không phù hợp. Tôi đã trích dẫn các tiêu chuẩn trong câu trả lời của tôi.
DigitalRoss

57

0 nếu tĩnh hoặc toàn cục, không xác định nếu lớp lưu trữ là tự động

C luôn luôn rất cụ thể về các giá trị ban đầu của các đối tượng. Nếu toàn cầu hoặc static, họ sẽ bằng không. Nếu auto, giá trị là không xác định .

Đây là trường hợp trong trình biên dịch trước C89 và được chỉ định bởi K & R và trong báo cáo C ban đầu của DMR.

Đây là trường hợp trong C89, xem phần 6.5.7 Khởi tạo .

Nếu một đối tượng có thời lượng lưu trữ tự động không được khởi tạo một cách rõ ràng, giá trị của nó là không xác định. Nếu một đối tượng có thời lượng lưu trữ tĩnh không được khởi tạo một cách rõ ràng, thì nó được khởi tạo hoàn toàn như thể mọi thành viên có loại số học được gán 0 và mọi thành viên có loại con trỏ đều được gán một hằng con trỏ null.

Đây là trường hợp trong C99, xem phần 6.7.8 Khởi tạo .

Nếu một đối tượng có thời lượng lưu trữ tự động không được khởi tạo một cách rõ ràng, giá trị của nó là không xác định. Nếu một đối tượng có thời lượng lưu trữ tĩnh không được khởi tạo một cách rõ ràng, thì:
- nếu nó có loại con trỏ, nó được khởi tạo thành một con trỏ null;
- nếu nó có loại số học, nó được khởi tạo thành (dương hoặc không dấu) bằng không;
- nếu là tổng hợp, mọi thành viên được khởi tạo (đệ quy) theo các quy tắc này;
- nếu là liên minh, thành viên có tên đầu tiên được khởi tạo (đệ quy) theo các quy tắc này.

Về ý nghĩa chính xác của việc không xác định , tôi không chắc chắn về C89, C99 nói:

3.17.2
giá trị không xác định

hoặc là giá trị không xác định hoặc đại diện bẫy

Nhưng bất kể tiêu chuẩn nói gì, trong thực tế, mỗi trang ngăn xếp thực sự bắt đầu bằng 0, nhưng khi chương trình của bạn xem bất kỳ autogiá trị lớp lưu trữ nào , nó sẽ thấy bất cứ thứ gì bị chương trình của chính bạn bỏ lại khi sử dụng lần cuối các địa chỉ ngăn xếp đó. Nếu bạn phân bổ nhiều automảng, bạn sẽ thấy chúng cuối cùng bắt đầu gọn gàng với số không.

Bạn có thể tự hỏi, tại sao nó lại theo cách này? Một câu trả lời SO khác liên quan đến câu hỏi đó, xem: https://stackoverflow.com/a/2091505/140740


3
không xác định thường (được sử dụng để?) có nghĩa là nó có thể làm bất cứ điều gì. Nó có thể bằng 0, nó có thể là giá trị trong đó, nó có thể làm hỏng chương trình, nó có thể làm cho máy tính sản xuất bánh kếp việt quất ra khỏi khe CD. bạn hoàn toàn không có gì đảm bảo Nó có thể gây ra sự hủy diệt của hành tinh. Ít nhất là theo thông số kỹ thuật ... bất cứ ai tạo ra một trình biên dịch thực sự làm bất cứ điều gì như thế đều sẽ rất khó chịu khi B-)
Brian Postow

Trong dự thảo C11 N1570, định nghĩa về indeterminate valuecó thể được tìm thấy tại 3.19.2.
dùng3528438

Có phải vì vậy mà nó luôn phụ thuộc vào trình biên dịch hoặc HĐH mà giá trị mà nó đặt cho biến tĩnh? Ví dụ: nếu ai đó viết một hệ điều hành hoặc trình biên dịch của riêng tôi và nếu họ cũng đặt giá trị ban đầu theo mặc định cho các số liệu thống kê là không xác định, điều đó có khả thi không?
Aditya Singh

1
@AdityaSingh, HĐH có thể giúp trình biên dịch dễ dàng hơn nhưng cuối cùng, trách nhiệm chính của nhà biên dịch là chạy danh mục mã C hiện có trên thế giới và chịu trách nhiệm phụ để đáp ứng các tiêu chuẩn. Nó chắc chắn sẽ có thể làm điều đó khác đi, nhưng, tại sao? Ngoài ra, thật khó để làm cho dữ liệu tĩnh không xác định được, bởi vì HĐH sẽ thực sự muốn không có các trang trước vì lý do bảo mật. (Biến tự động chỉ không thể đoán trước được vì chương trình của bạn thường sử dụng các địa chỉ ngăn xếp đó vào thời điểm sớm hơn.)
DigitalRoss

@BrianPostow Không, điều đó không chính xác. Xem stackoverflow.com/a/40674888/584518 . Sử dụng một giá trị không xác định gây ra hành vi không xác định , không phải là hành vi không xác định, lưu cho trường hợp biểu diễn bẫy.
Lundin

11

Nó phụ thuộc vào thời gian lưu trữ của biến. Một biến có thời lượng lưu trữ tĩnh luôn được khởi tạo ngầm bằng 0.

Đối với các biến tự động (cục bộ), một biến chưa được khởi tạo có giá trị không xác định . Giá trị không xác định, trong số những thứ khác, có nghĩa là bất kỳ "giá trị" nào bạn có thể "nhìn thấy" trong biến đó không chỉ là không thể đoán trước, nó thậm chí còn không được đảm bảo ổn định . Ví dụ, trong thực tế (tức là bỏ qua UB trong một giây) mã này

int num;
int a = num;
int b = num;

không đảm bảo rằng các biến absẽ nhận được các giá trị giống hệt nhau. Thật thú vị, đây không phải là một số khái niệm lý thuyết mang tính mô phạm, điều này dễ dàng xảy ra trong thực tế do hậu quả của việc tối ưu hóa.

Vì vậy, nói chung, câu trả lời phổ biến rằng "nó được khởi tạo với bất kỳ rác nào trong bộ nhớ" thậm chí không chính xác từ xa. Uninitialized hành vi biến là khác nhau từ đó của một biến được khởi tạo với rác.


Tôi không thể hiểu (tôi cũng rất có thể ) tại sao điều này có ít sự ủng hộ hơn so với cái từ DigitalRoss chỉ một phút sau: D
Antti Haapala

7

Ví dụ Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Đủ tiêu chuẩn, chúng ta hãy nhìn vào một triển khai :-)

Biến cục bộ

Tiêu chuẩn: hành vi không xác định.

Triển khai: chương trình phân bổ không gian ngăn xếp và không bao giờ di chuyển bất cứ thứ gì đến địa chỉ đó, vì vậy bất cứ thứ gì trước đó đều được sử dụng.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

biên dịch với:

gcc -O0 -std=c99 a.c

đầu ra:

0

và dịch ngược với:

objdump -dr a.out

đến:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

Từ kiến ​​thức của chúng tôi về các quy ước gọi x86-64:

  • %rdilà đối số printf đầu tiên, do đó, chuỗi "%d\n"tại địa chỉ0x4005e4

  • %rsilà đối số printf thứ hai, do đó i.

    Nó đến từ -0x4(%rbp), là biến cục bộ 4 byte đầu tiên.

    Tại thời điểm này, rbptrong trang đầu tiên của ngăn xếp đã được cấp phát bởi kernel, vì vậy để hiểu giá trị đó, chúng ta sẽ xem xét mã hạt nhân và tìm hiểu xem nó đặt cái gì.

    TODO kernel có đặt bộ nhớ đó thành một cái gì đó trước khi sử dụng lại nó cho các tiến trình khác khi một tiến trình chết không? Nếu không, quá trình mới sẽ có thể đọc bộ nhớ của các chương trình đã hoàn thành khác, rò rỉ dữ liệu. Xem: Các giá trị chưa được khởi tạo có bao giờ là rủi ro bảo mật không?

Sau đó chúng ta cũng có thể chơi với các sửa đổi ngăn xếp của riêng mình và viết những điều thú vị như:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Biến cục bộ trong -O3

Phân tích triển khai tại: <value được tối ưu hóa> có nghĩa là gì trong gdb?

Biến toàn cầu

Tiêu chuẩn: 0

Thực hiện: .bssphần.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

biên dịch thành:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>nói rằng đó ilà tại địa chỉ 0x601044và:

readelf -SW a.out

chứa đựng:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

trong đó nói 0x601044là ở giữa .bssphần, bắt đầu từ 0x601040và dài 8 byte.

Các tiêu chuẩn ELF sau đó đảm bảo rằng phần tên .bsslà hoàn toàn đầy các zeros:

.bssPhần này chứa dữ liệu chưa được khởi tạo góp phần vào hình ảnh bộ nhớ của chương trình. Theo định nghĩa, hệ thống khởi tạo dữ liệu bằng số không khi chương trình bắt đầu chạy. Phần này không có không gian tệp, như được chỉ định bởi loại phần , SHT_NOBITS.

Hơn nữa, loại SHT_NOBITSnày là hiệu quả và không chiếm dung lượng trên tệp thực thi:

sh_sizeThành viên này cung cấp kích thước của phần tính bằng byte. Trừ khi loại thứ hai là SHT_NOBITS, phần chiếm sh_size byte trong tệp. Một phần của loại SHT_NOBITScó thể có kích thước khác không, nhưng nó không chiếm không gian trong tệp.

Sau đó, hạt nhân Linux sẽ loại bỏ vùng nhớ đó khi tải chương trình vào bộ nhớ khi khởi động.


4

Mà phụ thuộc. Nếu định nghĩa đó là toàn cục (bên ngoài bất kỳ chức năng nào) thì numsẽ được khởi tạo thành không. Nếu nó là cục bộ (bên trong một hàm) thì giá trị của nó là không xác định. Về lý thuyết, ngay cả khi cố gắng đọc giá trị có hành vi không xác định - C cho phép khả năng các bit không đóng góp vào giá trị, nhưng phải được đặt theo những cách cụ thể để bạn thậm chí có được kết quả xác định từ việc đọc biến.


1

Bởi vì máy tính có dung lượng lưu trữ hữu hạn, các biến tự động thường sẽ được giữ trong các phần tử lưu trữ (cho dù là thanh ghi hoặc RAM) trước đây được sử dụng cho một số mục đích tùy ý khác. Nếu một biến như vậy được sử dụng trước khi giá trị được gán cho nó, thì bộ lưu trữ đó có thể giữ bất cứ thứ gì nó giữ trước đó và do đó, nội dung của biến sẽ không thể đoán trước được.

Là một nếp nhăn bổ sung, nhiều trình biên dịch có thể giữ các biến trong các thanh ghi lớn hơn các loại liên quan. Mặc dù trình biên dịch sẽ được yêu cầu để đảm bảo rằng bất kỳ giá trị nào được ghi vào biến và đọc lại sẽ bị cắt bớt và / hoặc mở rộng ký hiệu đến kích thước phù hợp của nó, nhiều trình biên dịch sẽ thực hiện cắt ngắn như vậy khi các biến được viết và hy vọng rằng nó sẽ có được thực hiện trước khi biến được đọc. Trên các trình biên dịch như vậy, một cái gì đó như:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

rất có thể dẫn đến wow()việc lưu trữ các giá trị 1234567 vào các thanh ghi 0 và 1 tương ứng và gọi foo(). Vì xkhông cần thiết trong "foo" và vì các hàm được cho là đưa giá trị trả về của chúng vào thanh ghi 0, trình biên dịch có thể phân bổ thanh ghi 0 cho q. Nếu modelà 1 hoặc 3, thanh ghi 0 sẽ được tải tương ứng với 2 hoặc 4, nhưng nếu đó là một giá trị khác, hàm có thể trả về bất cứ thứ gì có trong thanh ghi 0 (tức là giá trị 1234567) mặc dù giá trị đó không nằm trong phạm vi của uint16_t.

Để tránh yêu cầu trình biên dịch thực hiện thêm công việc để đảm bảo rằng các biến chưa được khởi tạo dường như không bao giờ giữ các giá trị bên ngoài miền của chúng và tránh cần chỉ định các hành vi không xác định chi tiết quá mức, Tiêu chuẩn nói rằng việc sử dụng các biến tự động chưa được xác định là Hành vi không xác định. Trong một số trường hợp, hậu quả của việc này có thể còn đáng ngạc nhiên hơn một giá trị nằm ngoài phạm vi của loại đó. Ví dụ: đã cho:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

một trình biên dịch có thể suy ra rằng vì việc gọi moo()với chế độ lớn hơn 3 chắc chắn sẽ dẫn đến chương trình gọi Hành vi không xác định, trình biên dịch có thể bỏ qua bất kỳ mã nào chỉ có liên quan nếu modelà 4 hoặc lớn hơn, chẳng hạn như mã thường ngăn chặn sự ra mắt của vũ khí hạt nhân trong những trường hợp như vậy. Lưu ý rằng cả triết lý Trình biên dịch tiêu chuẩn cũng như hiện đại đều không quan tâm đến thực tế là giá trị trả về từ "hey" bị bỏ qua - hành động cố gắng trả lại nó cung cấp cho trình biên dịch giấy phép không giới hạn để tạo mã tùy ý.


0

Câu trả lời cơ bản là, vâng, nó không được xác định.

Nếu bạn đang thấy hành vi kỳ quặc vì điều này, nó có thể phụ thuộc vào nơi nó được khai báo. Nếu trong một hàm trên ngăn xếp thì nội dung nhiều khả năng sẽ khác nhau mỗi khi hàm được gọi. Nếu nó là một phạm vi tĩnh hoặc mô-đun, nó không được xác định nhưng sẽ không thay đổi.


0

Nếu lớp lưu trữ là tĩnh hoặc toàn cục thì trong khi tải, BSS sẽ khởi tạo biến hoặc vị trí bộ nhớ (ML) thành 0 trừ khi biến ban đầu được gán một số giá trị. Trong trường hợp các biến chưa được khởi tạo cục bộ, biểu diễn bẫy được gán cho vị trí bộ nhớ. Vì vậy, nếu bất kỳ thanh ghi nào của bạn chứa thông tin quan trọng bị ghi đè bởi trình biên dịch, chương trình có thể bị sập.

nhưng một số trình biên dịch có thể có cơ chế để tránh vấn đề như vậy.

Tôi đã làm việc với loạt nec v850 khi tôi nhận ra Có biểu diễn bẫy có các mẫu bit đại diện cho các giá trị không xác định cho các loại dữ liệu ngoại trừ char. Khi tôi lấy một char chưa được khởi tạo, tôi nhận được giá trị mặc định bằng 0 do biểu diễn bẫy. Điều này có thể hữu ích cho any1 bằng cách sử dụng necv850es


Hệ thống của bạn không tuân thủ nếu bạn nhận được các biểu diễn bẫy khi sử dụng char không dấu. Chúng rõ ràng không được phép chứa các biểu diễn bẫy, C17 6.2.6.1/5.
Lundin

-2

Giá trị của num sẽ là một số giá trị rác từ bộ nhớ chính (RAM). Sẽ tốt hơn nếu bạn khởi tạo biến ngay sau khi tạo.


-4

Theo như tôi đã đi, nó chủ yếu phụ thuộc vào trình biên dịch, nhưng nói chung hầu hết các trường hợp, giá trị được giả định là 0 bởi các trình biên dịch.
Tôi nhận được giá trị rác trong trường hợp VC ++ trong khi TC cho giá trị là 0. Tôi in nó như dưới đây

int i;
printf('%d',i);

Nếu bạn nhận được một giá trị xác định, ví dụ như 0trình biên dịch của bạn rất có thể sẽ thực hiện các bước bổ sung để đảm bảo rằng nó nhận được giá trị đó (bằng cách thêm mã để khởi tạo các biến bằng mọi cách). Một số trình biên dịch thực hiện điều này khi thực hiện biên dịch "gỡ lỗi", nhưng chọn giá trị 0cho chúng là một ý tưởng tồi vì nó sẽ che giấu các lỗi trong mã của bạn (điều đúng đắn hơn sẽ đảm bảo một số thực sự không giống như 0xBAADF00Dhoặc một cái gì đó tương tự). Tôi nghĩ rằng hầu hết các trình biên dịch sẽ chỉ để lại bất kỳ rác nào xảy ra để chiếm bộ nhớ làm giá trị của biến (nghĩa là nó nói chung không được lắp ráp như 0).
bầu trời
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.