Trong ví dụ của bạn, *(p1 + 1) = 10;
nên là UB, bởi vì nó là một phần cuối của mảng có kích thước 1. Nhưng chúng ta đang ở trong một trường hợp rất đặc biệt ở đây, bởi vì mảng được xây dựng động trong một mảng char lớn hơn.
Tạo đối tượng động được mô tả trong 4.5 Mô hình đối tượng C ++ [intro.object] , §3 của bản nháp n4659 của tiêu chuẩn C ++:
3 Nếu một đối tượng hoàn chỉnh được tạo (8.3.4) trong bộ nhớ được liên kết với một đối tượng e khác thuộc loại “mảng của N unsigned char” hoặc thuộc loại “mảng của N std :: byte” (21.2.1), mảng đó cung cấp bộ nhớ đối với đối tượng được tạo nếu:
(3.1) - thời gian tồn tại của e đã bắt đầu và chưa kết thúc, và
(3.2) - bộ nhớ cho đối tượng mới hoàn toàn phù hợp với e, và
(3.3) - không có đối tượng mảng nào nhỏ hơn đáp ứng các những ràng buộc.
3.3 có vẻ khá rõ ràng, nhưng các ví dụ dưới đây làm cho ý định rõ ràng hơn:
struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B;
int *p = new (b->b + 4) int;
Vì vậy, trong ví dụ, buffer
mảng cung cấp bộ nhớ cho cả *p1
và *p2
.
Các đoạn văn sau chứng minh rằng đối tượng hoàn chỉnh cho cả hai *p1
và *p2
là buffer
:
4 Một đối tượng a được lồng trong một đối tượng khác b nếu:
(4.1) - a là một đối tượng chính của b, hoặc
(4.2) - b cung cấp bộ nhớ cho a, hoặc
(4.3) - tồn tại một đối tượng c trong đó a được lồng trong c , và c được lồng trong b.
5 Với mọi đối tượng x, có một đối tượng nào đó được gọi là đối tượng hoàn chỉnh của x, được xác định như sau:
(5.1) - Nếu x là đối tượng hoàn chỉnh thì đối tượng hoàn chỉnh của x là chính nó.
(5.2) - Nếu không, đối tượng hoàn chỉnh của x là đối tượng hoàn chỉnh của đối tượng (duy nhất) chứa x.
Khi điều này được thiết lập, phần liên quan khác của bản nháp n4659 cho C ++ 17 là [basic.coumpound] §3 (nhấn mạnh của tôi):
3 ... Mọi giá trị của kiểu con trỏ là một trong những giá trị sau:
(3.1) - một con trỏ tới một đối tượng hoặc hàm (con trỏ được cho là trỏ đến đối tượng hoặc hàm), hoặc
(3.2) - một con trỏ ở cuối của một đối tượng (8.7), hoặc
(3.3) - giá trị con trỏ null (7.11) cho kiểu đó, hoặc
(3.4) - giá trị con trỏ không hợp lệ.
Một giá trị của kiểu con trỏ là một con trỏ đến hoặc qua phần cuối của một đối tượng đại diện cho địa chỉ của byte đầu tiên trong bộ nhớ (4.4) bị chiếm bởi đối tượng hoặc byte đầu tiên trong bộ nhớ sau khi kết thúc bộ nhớ
mà đối tượng chiếm giữ , tương ứng. [Lưu ý: Một con trỏ qua phần cuối của một đối tượng (8.7) không được coi là trỏ đến một đối tượng không liên quanđối tượng của loại đối tượng có thể được đặt tại địa chỉ đó. Một giá trị con trỏ trở nên không hợp lệ khi bộ nhớ mà nó biểu thị đạt đến cuối thời hạn lưu trữ của nó; xem 6.7. —End note] Đối với mục đích số học con trỏ (8.7) và so sánh (8.9, 8.10), một con trỏ qua phần cuối của phần tử cuối cùng của mảng x gồm n phần tử được coi là tương đương với một con trỏ đến phần tử giả định x [ n]. Biểu diễn giá trị của các kiểu con trỏ được xác định bởi việc triển khai. Con trỏ tới các loại tương thích với bố cục phải có cùng giá trị biểu diễn và các yêu cầu liên kết (6.11) ...
Các lưu ý Một con trỏ qua cuối cùng ... không áp dụng ở đây vì các đối tượng được trỏ đến bởi p1
và p2
và không liên quan , nhưng được lồng vào đối tượng hoàn toàn giống nhau, vì vậy arithmetics con trỏ có ý nghĩa bên trong đối tượng cung cấp lưu trữ: p2 - p1
được định nghĩa và là (&buffer[sizeof(int)] - buffer]) / sizeof(int)
đó là 1.
Vì vậy, p1 + 1
là một con trỏ tới *p2
, và *(p1 + 1) = 10;
có hành vi được xác định và thiết lập giá trị của *p2
.
Tôi cũng đã đọc phụ lục C4 về khả năng tương thích giữa C ++ 14 và các tiêu chuẩn hiện tại (C ++ 17). Loại bỏ khả năng sử dụng số học con trỏ giữa các đối tượng được tạo động trong một mảng ký tự đơn lẻ sẽ là một thay đổi quan trọng mà IMHO nên được trích dẫn ở đó, vì nó là một tính năng thường được sử dụng. Vì không có gì về nó tồn tại trong các trang tương thích, tôi nghĩ rằng nó xác nhận rằng nó không phải là mục đích của tiêu chuẩn để cấm nó.
Đặc biệt, nó sẽ đánh bại cấu trúc động phổ biến của một mảng đối tượng từ một lớp không có hàm tạo mặc định:
class T {
...
public T(U initialization) {
...
}
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem);
for (i=0; i<N; i++) {
U u(...);
new(arr + i) T(u);
}
arr
sau đó có thể được sử dụng như một con trỏ đến phần tử đầu tiên của một mảng ...