Tất cả các hành vi không xác định phổ biến mà một lập trình viên C ++ nên biết là gì? [đóng cửa]


201

Tất cả các hành vi không xác định phổ biến mà một lập trình viên C ++ nên biết là gì?

Nói, như:

a[i] = i++;


3
Bạn có chắc không. Điều đó có vẻ được xác định rõ.
Martin York

17
6.2.2 Thứ tự đánh giá [expr.ev Assessment] trong ngôn ngữ lập trình C ++ nói như vậy. Tôi không có bất kỳ tài liệu tham khảo nào khác
yesraaj

4
Anh ấy đúng .. chỉ cần nhìn vào 6.2.2 trong Ngôn ngữ lập trình C ++ và nó nói v [i] = i ++ không xác định
dancavallaro

4
Tôi sẽ tưởng tượng bởi vì trình biên dịch thực hiện i ++ trước hoặc sau khi tính toán vị trí bộ nhớ của v [i]. chắc chắn, tôi sẽ luôn luôn được chỉ định ở đó. nhưng nó có thể ghi vào v [i] hoặc v [i + 1] tùy theo thứ tự hoạt động ..
Evan Teran

2
Tất cả những gì Ngôn ngữ lập trình C ++ nói là "Thứ tự hoạt động của các biểu thức con trong một biểu thức là không xác định. Đặc biệt, bạn không thể cho rằng biểu thức được đánh giá từ trái sang phải."
dancavallaro

Câu trả lời:


233

Con trỏ

  • Dereferences một NULLcon trỏ
  • Hủy bỏ hội nghị một con trỏ được trả về bởi phân bổ "mới" có kích thước bằng 0
  • Sử dụng các con trỏ tới các đối tượng có thời gian kết thúc (ví dụ: ngăn xếp các đối tượng được phân bổ hoặc các đối tượng bị xóa)
  • Hủy bỏ hội nghị một con trỏ chưa được khởi tạo chắc chắn
  • Thực hiện số học con trỏ mang lại kết quả bên ngoài các ranh giới (ở trên hoặc dưới) của một mảng.
  • Hủy bỏ con trỏ tại một vị trí nằm ngoài phần cuối của một mảng.
  • Chuyển đổi con trỏ thành các đối tượng của các loại không tương thích
  • Sử dụng memcpyđể sao chép bộ đệm chồng chéo .

Bộ đệm tràn

  • Đọc hoặc ghi vào một đối tượng hoặc mảng ở phần bù âm hoặc vượt quá kích thước của đối tượng đó (tràn / chồng đống)

Số nguyên tràn

  • Đã tràn số nguyên
  • Đánh giá một biểu thức không được xác định theo toán học
  • Giá trị dịch chuyển trái theo số lượng âm (dịch chuyển phải theo số lượng âm được xác định)
  • Thay đổi giá trị theo số lượng lớn hơn hoặc bằng số bit trong số (ví dụ: int64_t i = 1; i <<= 72không xác định)

Các loại, diễn viên và Const

  • Truyền một giá trị số thành một giá trị không thể được biểu thị bằng loại mục tiêu (trực tiếp hoặc qua static_cast)
  • Sử dụng một biến tự động trước khi nó được gán chắc chắn (ví dụ int i; i++; cout << i;:)
  • Sử dụng giá trị của bất kỳ đối tượng nào khác volatilehoặc sig_atomic_tkhi nhận tín hiệu
  • Cố gắng sửa đổi một chuỗi ký tự hoặc bất kỳ đối tượng const nào khác trong suốt vòng đời của nó
  • Nối hẹp với một chuỗi ký tự rộng trong quá trình tiền xử lý

Chức năng và mẫu

  • Không trả về giá trị từ hàm trả về giá trị (trực tiếp hoặc bằng cách chuyển từ khối thử)
  • Nhiều định nghĩa khác nhau cho cùng một thực thể (lớp, mẫu, liệt kê, hàm nội tuyến, hàm thành viên tĩnh, v.v.)
  • Đệ quy vô hạn trong việc khởi tạo các mẫu
  • Gọi một hàm sử dụng các tham số hoặc liên kết khác nhau đến các tham số và liên kết mà hàm được định nghĩa là sử dụng.

OOP

  • Phá hủy tầng của các đối tượng với thời gian lưu trữ tĩnh
  • Kết quả của việc gán cho các đối tượng chồng chéo một phần
  • Nhập lại một cách đệ quy một hàm trong quá trình khởi tạo các đối tượng tĩnh của nó
  • Thực hiện các cuộc gọi hàm ảo đến các hàm ảo thuần túy của một đối tượng từ hàm tạo hoặc hàm hủy của nó
  • Đề cập đến các thành viên phi lý của các đối tượng chưa được xây dựng hoặc đã bị phá hủy

Tập tin nguồn và tiền xử lý

  • Tệp nguồn không trống không kết thúc bằng dòng mới hoặc kết thúc bằng dấu gạch chéo ngược (trước C ++ 11)
  • Dấu gạch chéo ngược theo sau là một ký tự không phải là một phần của mã thoát được chỉ định trong hằng số ký tự hoặc chuỗi (đây là định nghĩa triển khai được xác định trong C ++ 11).
  • Vượt quá giới hạn triển khai (số khối lồng nhau, số hàm trong chương trình, không gian ngăn xếp có sẵn ...)
  • Các giá trị số tiền xử lý không thể được biểu thị bằng long int
  • Chỉ thị tiền xử lý ở phía bên trái của định nghĩa macro giống như hàm
  • Tự động tạo mã thông báo được xác định trong #ifbiểu thức

Được phân loại

  • Gọi thoát trong khi hủy chương trình với thời lượng lưu trữ tĩnh

Hừm ... NaN (x / 0) và Vô cực (0/0) được bao phủ bởi IEE 754, nếu C ++ được thiết kế sau, tại sao nó lại ghi x / 0 là không xác định?
new123456

Re: "Dấu gạch chéo ngược theo sau là một ký tự không phải là một phần của mã thoát được chỉ định trong hằng số ký tự hoặc chuỗi." Đó là UB trong C89 (§3.1.3.4) và C ++ 03 (kết hợp C89), nhưng không có trong C99. C99 nói rằng "kết quả không phải là mã thông báo và cần chẩn đoán" (§6.4.4.4). Có lẽ C ++ 0x (kết hợp C89) sẽ giống nhau.
Adam Rosenfield

1
Tiêu chuẩn C99 có một danh sách các hành vi không xác định trong phụ lục J.2. Sẽ mất một số công việc để điều chỉnh danh sách này thành C ++. Bạn sẽ phải thay đổi các tham chiếu thành các mệnh đề C ++ chính xác thay vì các mệnh đề C99, loại bỏ bất cứ điều gì không liên quan và cũng kiểm tra xem tất cả những điều đó có thực sự không được xác định trong C ++ cũng như C. Nhưng nó cung cấp một sự khởi đầu.
Steve Jessop

1
@ new123456 - không phải tất cả các đơn vị dấu phẩy động đều tương thích với IEE754. Nếu C ++ yêu cầu tuân thủ IEE754, trình biên dịch sẽ cần kiểm tra và xử lý trường hợp RHS bằng 0 thông qua kiểm tra rõ ràng. Bằng cách làm cho hành vi không được xác định, trình biên dịch có thể tránh được chi phí đó bằng cách nói "nếu bạn sử dụng một IEU754 không phải IEE754, bạn sẽ không nhận được hành vi của IEEE754 FPU".
SecurityMatt

1
"Đánh giá một biểu thức có kết quả không nằm trong phạm vi của các loại tương ứng" .... tràn số nguyên được xác định rõ cho các loại tích phân chưa được ký, chỉ các loại không được ký.
nacitar Sevaht

31

Thứ tự mà các tham số chức năng được ước tính là hành vi không xác định . (Điều này sẽ không làm cho chương trình của bạn bị sập, phát nổ hoặc đặt mua pizza ... không giống như hành vi không xác định .)

Yêu cầu duy nhất là tất cả các tham số phải được đánh giá đầy đủ trước khi hàm được gọi.


Điều này:

// The simple obvious one.
callFunc(getA(),getB());

Có thể tương đương với điều này:

int a = getA();
int b = getB();
callFunc(a,b);

Hoặc này:

int b = getB();
int a = getA();
callFunc(a,b);

Nó có thể là một trong hai; tùy thuộc vào trình biên dịch. Kết quả có thể quan trọng, tùy thuộc vào tác dụng phụ.


23
Thứ tự là không xác định, không xác định.
Rob Kennedy

1
Tôi ghét điều này :) Tôi đã mất một ngày làm việc khi theo dõi một trong những trường hợp này ... dù sao cũng đã học được bài học của tôi và may mắn không bị ngã lần nữa
Robert Gould

2
@Rob: Tôi sẽ tranh luận với bạn về sự thay đổi về ý nghĩa ở đây, nhưng tôi biết ủy ban tiêu chuẩn rất kén chọn định nghĩa chính xác của hai từ này. Vì vậy, tôi sẽ thay đổi nó :-)
Martin York

2
Tôi đã may mắn về điều này. Tôi đã bị nó cắn khi tôi học đại học và có một giáo sư đã nhìn vào nó và nói với tôi vấn đề của tôi trong khoảng 5 giây. Không cần nói bao nhiêu thời gian tôi sẽ lãng phí gỡ lỗi nếu không.
Bill Lizard

27

Trình biên dịch có thể tự do sắp xếp lại các phần đánh giá của một biểu thức (giả sử ý nghĩa là không thay đổi).

Từ câu hỏi ban đầu:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Kiểm tra khóa kép. Và một sai lầm dễ mắc phải.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

ý nghĩa của điểm thứ tự là gì?
yesraaj


1
Ôi ... thật là khó chịu, đặc biệt là vì tôi đã thấy cấu trúc chính xác được đề xuất trong Java
Tom

Lưu ý rằng một số trình biên dịch xác định hành vi trong tình huống này. Ví dụ, trong VC ++ 2005+, nếu a không ổn định, các bộ nhớ cần thiết được thiết lập để ngăn chặn lệnh sắp xếp lại để khóa được kiểm tra hai lần hoạt động.
Nhật thực

Martin York: <i> // (c) được đảm bảo xảy ra sau (a) và (b) </ i> Có phải vậy không? Phải thừa nhận trong ví dụ cụ thể đó là kịch bản duy nhất có thể xảy ra nếu 'i' là biến dễ bay hơi được ánh xạ tới thanh ghi phần cứng và [i] (giá trị cũ của 'i') được đặt bí danh cho nó, nhưng có bất kỳ đảm bảo rằng sự gia tăng sẽ xảy ra trước một điểm thứ tự?
supercat

5

Sở thích của tôi là "đệ quy vô hạn trong việc khởi tạo các mẫu" bởi vì tôi tin rằng đó là lần duy nhất có hành vi không xác định xảy ra tại thời điểm biên dịch.


Thực hiện điều này trước đây, nhưng tôi không thấy làm thế nào nó không xác định. Nó khá rõ ràng của bạn làm một đệ quy vô hạn trong suy nghĩ lại.
Robert Gould

Vấn đề là trình biên dịch không thể kiểm tra mã của bạn và quyết định chính xác liệu nó có bị đệ quy vô hạn hay không. Đó là một ví dụ của vấn đề tạm dừng. Xem: stackoverflow.com/questions/235984/ từ
Daniel Earwicker

Vâng, đây chắc chắn là một vấn đề tạm dừng
Robert Gould

nó làm cho hệ thống của tôi bị sập vì hoán đổi do bộ nhớ quá ít.
Julian Schaub - litb

2
Các hằng số tiền xử lý không phù hợp với int cũng là thời gian biên dịch.
Joshua

5

Chỉ định một hằng số sau khi tước constness bằng cách sử dụngconst_cast<> :

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

Bên cạnh hành vi không xác định , còn có hành vi được xác định thực hiện khó chịu không kém .

Hành vi không xác định xảy ra khi một chương trình làm một cái gì đó mà kết quả không được chỉ định bởi tiêu chuẩn.

Hành vi xác định thực hiện là một hành động của một chương trình mà kết quả không được xác định bởi tiêu chuẩn, nhưng việc thực hiện được yêu cầu phải ghi lại. Một ví dụ là "Multibyte ký tự chữ", từ câu hỏi Stack Overflow Có trình biên dịch C không biên dịch được điều này không?.

Hành vi được xác định theo triển khai chỉ cắn bạn khi bạn bắt đầu chuyển (nhưng nâng cấp lên phiên bản mới của trình biên dịch cũng đang chuyển!)


4

Các biến chỉ có thể được cập nhật một lần trong một biểu thức (về mặt kỹ thuật một lần giữa các điểm chuỗi).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

Nguyên vẹn ít nhất một lần giữa hai điểm chuỗi.
Prasoon Saurav

2
@Prasoon: Tôi nghĩ bạn có nghĩa là: nhiều nhất một lần giữa hai điểm chuỗi. :-)
Nawaz

3

Một sự hiểu biết cơ bản về các giới hạn môi trường khác nhau. Danh sách đầy đủ nằm trong mục 5.2.4.1 của thông số kỹ thuật C. Ở đây có một ít;

  • 127 tham số trong một chức năng
  • 127 đối số trong một lệnh gọi hàm
  • 127 tham số trong một định hướng vĩ mô
  • 127 đối số trong một lệnh gọi macro
  • 4095 ký tự trong một dòng nguồn logic
  • 4095 ký tự trong một chuỗi ký tự theo nghĩa đen hoặc chuỗi ký tự rộng (sau khi ghép)
  • 65535 byte trong một đối tượng (chỉ trong môi trường được lưu trữ)
  • 15 cấp độ cho #inculled fi les
  • 1023 nhãn trường hợp cho câu lệnh chuyển đổi (không bao gồm nhãn cho câu lệnh chuyển đổi bất kỳ)

Tôi thực sự hơi ngạc nhiên khi giới hạn 1023 trường hợp nhãn cho câu lệnh chuyển đổi, tôi có thể thấy rằng việc vượt quá đối với mã / lex / trình phân tích cú pháp được tạo ra khá dễ dàng.

Nếu các giới hạn này bị vượt quá, bạn có hành vi không xác định (sự cố, lỗi bảo mật, v.v ...).

Phải, tôi biết đây là từ đặc tả C, nhưng C ++ chia sẻ những hỗ trợ cơ bản này.


9
Nếu bạn đạt các giới hạn này, bạn sẽ gặp nhiều vấn đề hơn hành vi không xác định.
new123456

Bạn có thể DỄ DÀNG vượt quá 65535 byte trong một đối tượng, chẳng hạn như STD :: vector
Demi

2

Sử dụng memcpyđể sao chép giữa các vùng bộ nhớ chồng chéo. Ví dụ:

char a[256] = {};
memcpy(a, a, sizeof(a));

Hành vi không được xác định theo Tiêu chuẩn C, được áp dụng theo Tiêu chuẩn C ++ 03.

7.21.2.1 Hàm memcpy

Tóm tắc

1 / #include void * memcpy (void * hạn chế s1, const void * hạn chế s2, size_t n);

Sự miêu tả

2 / Hàm memcpy sao chép n ký tự từ đối tượng được trỏ bởi s2 vào đối tượng được trỏ bởi s1. Nếu sao chép diễn ra giữa các đối tượng chồng lấp, hành vi không được xác định. Trả về 3 Hàm memcpy trả về giá trị của s1.

7.21.2.2 Hàm memmove

Tóm tắc

1 #incolee void * memmove (void * s1, const void * s2, size_t n);

Sự miêu tả

2 Hàm memmove sao chép n ký tự từ đối tượng được trỏ bởi s2 vào đối tượng được trỏ bởi s1. Việc sao chép diễn ra như thể các ký tự n từ đối tượng được trỏ bởi s2 trước tiên được sao chép vào một mảng tạm thời của n ký tự không trùng với các đối tượng được trỏ bởi s1 và s2, sau đó các ký tự n từ mảng tạm thời được sao chép vào đối tượng được chỉ bởi s1. Trả về

3 Hàm memmove trả về giá trị của s1.


2

Loại duy nhất mà C ++ đảm bảo kích thước là char. Và kích thước là 1. Kích thước của tất cả các loại khác phụ thuộc vào nền tảng.


Không phải đó là những gì <cstdint> dành cho? Nó định nghĩa các loại như uint16_6 et cetera.
Jasper Bekkers

Có, nhưng kích thước của hầu hết các loại, nói dài, không được xác định rõ.
JaredPar

Ngoài ra cstdint chưa phải là một phần của tiêu chuẩn c ++ hiện tại. xem boost / stdint.hpp để biết giải pháp di động hiện tại.
Evan Teran

Đó không phải là hành vi không xác định. Tiêu chuẩn nói rằng nền tảng tuân thủ xác định kích thước, thay vì tiêu chuẩn xác định chúng.
Daniel Earwicker

1
@JaredPar: Đây là một bài viết phức tạp với rất nhiều chủ đề của cuộc trò chuyện, vì vậy tôi đã tóm tắt tất cả ở đây . Điểm mấu chốt là đây: "5. Để thể hiện -2147483647 và +2147483647 ở dạng nhị phân, bạn cần 32 bit."
John Dibling

2

Các đối tượng mức không gian tên trong một đơn vị biên dịch khác nhau không bao giờ nên phụ thuộc vào nhau để khởi tạo, vì thứ tự khởi tạo của chúng không được xác định.

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.