Nhầm lẫn về khởi tạo mảng trong C


102

Trong ngôn ngữ C, nếu khởi tạo một mảng như thế này:

int a[5] = {1,2};

thì tất cả các phần tử của mảng không được khởi tạo một cách rõ ràng sẽ được khởi tạo ngầm bằng các số không.

Nhưng, nếu tôi khởi tạo một mảng như thế này:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

đầu ra:

1 0 1 0 0

Tôi không hiểu, tại sao a[0]in 1thay vì 0? Đó có phải là hành vi không xác định?

Lưu ý: Câu hỏi này đã được hỏi trong một cuộc phỏng vấn.


35
Biểu thức a[2]=1đánh giá là 1.
tkausl 13/09/18

14
Một câu hỏi rất sâu sắc. Tôi tự hỏi liệu người phỏng vấn có biết câu trả lời của họ không. Tôi không. Thật vậy, bề ngoài thì giá trị của biểu thức a[2] = 1là vậy 1, nhưng tôi không chắc liệu bạn có được phép lấy kết quả của một biểu thức khởi tạo được chỉ định làm giá trị của phần tử đầu tiên hay không. Thực tế là bạn đã thêm thẻ luật sư có nghĩa là tôi nghĩ rằng chúng tôi cần một câu trả lời trích dẫn tiêu chuẩn.
Bathsheba

15
Chà nếu đó là câu hỏi yêu thích của họ, bạn có thể đã né được một viên đạn. Cá nhân tôi thích một bài tập lập trình bằng văn bản (với quyền truy cập vào trình biên dịch và trình gỡ lỗi) được thực hiện trong vài giờ hơn là những câu hỏi kiểu "át chủ bài" như ở trên. Tôi có thể phỏng đoán một câu trả lời, nhưng tôi không nghĩ rằng nó sẽ có bất kỳ cơ sở thực tế nào.
Bathsheba

1
@Bathsheba Tôi sẽ làm ngược lại, vì câu trả lời ở đây bây giờ trả lời cho cả hai câu hỏi.
Tạm biệt SE

1
@Bathsheba sẽ là tốt nhất. Tuy nhiên, tôi vẫn công nhận câu hỏi cho OP, khi anh ấy đưa ra chủ đề. Nhưng điều này không phải để tôi quyết định những gì tôi cảm thấy sẽ là "điều đúng đắn".
Tạm biệt SE

Câu trả lời:


95

TL; DR: Tôi không nghĩ rằng hành vi của int a[5]={a[2]=1};được xác định rõ ràng, ít nhất là trong C99.

Phần buồn cười là phần duy nhất có ý nghĩa với tôi là phần bạn đang hỏi: a[0]được đặt thành 1bởi vì toán tử gán trả về giá trị đã được gán. Mọi thứ khác đều không rõ ràng.

Nếu có mã int a[5] = { [2] = 1 }, mọi thứ sẽ dễ dàng: Đó là cài đặt trình khởi tạo được chỉ định a[2]cho 1và mọi thứ khác 0. Nhưng với việc { a[2] = 1 }chúng ta có một trình khởi tạo không được chỉ định có chứa một biểu thức gán, và chúng ta rơi xuống một lỗ hổng.


Đây là những gì tôi đã tìm thấy cho đến nay:

  • a phải là một biến cục bộ.

    6.7.8 Khởi tạo

    1. Tất cả các biểu thức trong bộ khởi tạo cho một đối tượng có thời lượng lưu trữ tĩnh phải là các biểu thức hằng số hoặc các ký tự chuỗi.

    a[2] = 1không phải là một biểu thức hằng, vì vậy aphải có lưu trữ tự động.

  • a nằm trong phạm vi khởi tạo của chính nó.

    6.2.1 Phạm vi nhận dạng

    1. Các thẻ cấu trúc, liên hợp và liệt kê có phạm vi bắt đầu ngay sau sự xuất hiện của thẻ trong bộ chỉ định kiểu khai báo thẻ. Mỗi hằng số liệt kê có phạm vi bắt đầu ngay sau khi xuất hiện điều tra viên xác định của nó trong danh sách điều tra viên. Bất kỳ mã định danh nào khác có phạm vi bắt đầu ngay sau khi hoàn thành bộ khai báo của nó.

    Bộ khai báo là a[5], vì vậy các biến nằm trong phạm vi khởi tạo của chính chúng.

  • a vẫn tồn tại trong quá trình khởi tạo của chính nó.

    6.2.4 Thời lượng lưu trữ của các đối tượng

    1. Một đối tượng có nhận dạng được khai báo không có mối liên hệ và không có sự xác định lưu trữ-lớp staticthời gian lưu trữ tự động .

    2. Đối với một đối tượng như vậy không có kiểu mảng có độ dài thay đổi, thời gian tồn tại của nó kéo dài từ khi nhập vào khối mà nó được liên kết cho đến khi việc thực thi khối đó kết thúc theo bất kỳ cách nào. (Việc nhập một khối kèm theo hoặc gọi một hàm sẽ tạm dừng, nhưng không kết thúc, việc thực thi khối hiện tại.) Nếu khối được nhập đệ quy, một thể hiện mới của đối tượng sẽ được tạo mỗi lần. Giá trị ban đầu của đối tượng là không xác định. Nếu một khởi tạo được chỉ định cho đối tượng, nó được thực hiện mỗi khi đạt được khai báo trong quá trình thực thi khối; nếu không, giá trị trở nên không xác định mỗi khi đạt đến khai báo.

  • Có một điểm trình tự sau a[2]=1.

    6.8 Các câu lệnh và khối

    1. Một biểu thức đầy đủ là một biểu thức không phải là một phần của một biểu thức khác hoặc của một bộ khai báo. Mỗi điều sau đây là một biểu thức đầy đủ: một trình khởi tạo ; biểu thức trong một câu lệnh biểu thức; biểu thức điều khiển của một câu lệnh lựa chọn ( ifhoặc switch); biểu thức điều khiển của một whilehoặc docâu lệnh; mỗi biểu thức (tùy chọn) của một forcâu lệnh; biểu thức (tùy chọn) trong một returncâu lệnh. Phần cuối của một biểu thức đầy đủ là một điểm trình tự.

    Lưu ý rằng ví dụ như trong int foo[] = { 1, 2, 3 }các { 1, 2, 3 }phần là danh sách cú đúp kín của initializers, mỗi trong số đó có một điểm chuỗi sau nó.

  • Khởi tạo được thực hiện theo thứ tự danh sách trình khởi tạo.

    6.7.8 Khởi tạo

    1. Mỗi danh sách bộ khởi tạo có dấu ngoặc nhọn có một đối tượng hiện tại được liên kết . Khi không có chỉ định nào, các subobject của đối tượng hiện tại được khởi tạo theo thứ tự theo kiểu của đối tượng hiện tại: phần tử mảng theo thứ tự chỉ số con tăng dần, thành viên cấu trúc theo thứ tự khai báo và thành viên được đặt tên đầu tiên của một liên hợp. [...]

     

    1. Việc khởi tạo sẽ diễn ra theo thứ tự danh sách trình khởi tạo, mỗi trình khởi tạo được cung cấp cho một subobject cụ thể sẽ ghi đè bất kỳ trình khởi tạo nào được liệt kê trước đó cho cùng một subobject; tất cả các subobject không được khởi tạo rõ ràng sẽ được khởi tạo ngầm giống như các đối tượng có thời lượng lưu trữ tĩnh.
  • Tuy nhiên, các biểu thức khởi tạo không nhất thiết phải được đánh giá theo thứ tự.

    6.7.8 Khởi tạo

    1. Thứ tự mà bất kỳ tác dụng phụ nào xảy ra giữa các biểu thức danh sách khởi tạo là không xác định.

Tuy nhiên, điều đó vẫn để lại một số câu hỏi chưa được giải đáp:

  • Các điểm trình tự có liên quan không? Quy tắc cơ bản là:

    6.5 Biểu thức

    1. Giữa điểm trình tự trước và điểm tiếp theo, một đối tượng sẽ có giá trị được lưu trữ của nó được sửa đổi nhiều nhất một lần bằng cách đánh giá một biểu thức . Hơn nữa, giá trị trước sẽ chỉ được đọc để xác định giá trị được lưu trữ.

    a[2] = 1 là một biểu thức, nhưng khởi tạo thì không.

    Điều này hơi mâu thuẫn với Phụ lục J:

    J.2 Hành vi không xác định

    • Giữa hai điểm trình tự, một đối tượng được sửa đổi nhiều lần, hoặc được sửa đổi và giá trị trước đó được đọc ngoài để xác định giá trị được lưu trữ (6.5).

    Phụ lục J cho biết bất kỳ sửa đổi nào cũng được tính, không chỉ sửa đổi theo biểu thức. Nhưng cho rằng các phụ lục không mang tính quy chuẩn, chúng ta có thể bỏ qua điều đó.

  • Các quá trình khởi tạo subobject được sắp xếp như thế nào đối với các biểu thức khởi tạo? Có phải tất cả các trình khởi tạo đều được đánh giá trước (theo một số thứ tự), sau đó các subobject được khởi tạo với kết quả (theo thứ tự danh sách trình khởi tạo)? Hoặc chúng có thể được xen kẽ?


Tôi nghĩ int a[5] = { a[2] = 1 }được thực hiện như sau:

  1. Bộ nhớ cho ađược cấp phát khi khối chứa của nó được nhập. Nội dung là không xác định tại thời điểm này.
  2. Bộ khởi tạo (duy nhất) được thực thi ( a[2] = 1), theo sau là một điểm trình tự. Này lưu trữ 1trong a[2]và lợi nhuận 1.
  3. Điều đó 1được sử dụng để khởi tạo a[0](trình khởi tạo đầu tiên khởi tạo subobject đầu tiên).

Nhưng ở đây mọi thứ trở nên mờ vì các yếu tố còn lại ( a[1], a[2], a[3], a[4]) có nghĩa vụ phải được khởi tạo 0, nhưng nó không rõ ràng khi: Liệu nó xảy ra trước khi a[2] = 1được đánh giá? Nếu vậy, a[2] = 1sẽ "thắng" và ghi đè a[2], nhưng liệu phép gán đó có hành vi không xác định vì không có điểm trình tự nào giữa khởi tạo 0 và biểu thức gán không? Các điểm trình tự thậm chí có liên quan không (xem ở trên)? Hoặc không khởi tạo xảy ra sau khi tất cả các trình khởi tạo được đánh giá? Nếu vậy, a[2]cuối cùng sẽ là 0.

Bởi vì tiêu chuẩn C không xác định rõ ràng những gì xảy ra ở đây, tôi tin rằng hành vi là không xác định (do bỏ sót).


1
Thay vì không xác định, tôi sẽ tranh luận rằng nó không xác định , điều này để ngỏ mọi thứ để các triển khai giải thích.
Một số lập trình viên dude

1
"chúng ta rơi vào một cái lỗ thỏ" LOL! Chưa bao giờ nghe thấy điều đó đối với UB hoặc những thứ không xác định.
BЈовић

2
@Someprogrammerdude Tôi không nghĩ rằng nó có thể không xác định được (" hành vi trong đó tiêu chuẩn này cung cấp hai hoặc nhiều khả năng và không áp đặt thêm yêu cầu nào được chọn trong bất kỳ trường hợp nào ") vì tiêu chuẩn không thực sự cung cấp bất kỳ khả năng nào trong số đó chọn. Nó chỉ đơn giản không nói gì xảy ra, mà tôi tin rằng thuộc " hành vi Không xác định là [...] đã nêu trong tiêu chuẩn quốc tế này [...] do thiếu sót của bất kỳ định nghĩa rõ ràng về hành vi. "
Melpomene

2
@ BЈовић Đây cũng là một mô tả rất hay không chỉ cho hành vi không xác định, mà còn cho hành vi đã xác định cần một chuỗi như thế này để giải thích.
gnasher729,

1
@JohnBollinger Sự khác biệt là bạn không thể thực sự khởi tạo a[0]subobject trước khi đánh giá trình khởi tạo của nó và đánh giá bất kỳ trình khởi tạo nào bao gồm một điểm trình tự (vì đó là "biểu thức đầy đủ"). Do đó, tôi tin rằng việc sửa đổi subobject mà chúng tôi đang khởi tạo là một trò chơi công bằng.
melpomene

22

Tôi không hiểu, tại sao a[0]in 1thay vì 0?

Có lẽ a[2]=1khởi tạo a[2]trước và kết quả của biểu thức được sử dụng để khởi tạo a[0].

Từ N2176 (bản nháp C17):

6.7.9 Khởi tạo

  1. Các đánh giá của các biểu thức danh sách khởi tạo được sắp xếp theo trình tự không xác định đối với nhau và do đó thứ tự xảy ra bất kỳ tác dụng phụ nào là không xác định. 154)

Vì vậy, có vẻ như đầu ra 1 0 0 0 0cũng sẽ có thể.

Kết luận: Đừng viết các trình khởi tạo sửa đổi biến được khởi tạo một cách nhanh chóng.


1
Phần đó không áp dụng: Chỉ có một biểu thức khởi tạo ở đây, vì vậy nó không cần phải được trình tự với bất kỳ thứ gì.
melpomene

@melpomene Có {...}biểu thức khởi tạo a[2]thành 0a[2]=1biểu thức con khởi tạo a[2]thành 1.
user694733 13/09/18

1
{...}là một danh sách khởi tạo có giằng. Nó không phải là một biểu thức.
melpomene

@melpomene Ok, bạn có thể ở ngay đó. Nhưng tôi vẫn tranh luận rằng vẫn còn 2 tác dụng phụ cạnh tranh để đoạn văn đó đứng.
user694733 13/09/18

@melpomene có hai thứ cần được sắp xếp theo trình tự: trình khởi tạo đầu tiên và cài đặt các phần tử khác thành 0
MM

6

Tôi nghĩ rằng tiêu chuẩn C11 bao hàm hành vi này và nói rằng kết quả là không xác định và tôi không nghĩ C18 đã thực hiện bất kỳ thay đổi liên quan nào trong lĩnh vực này.

Ngôn ngữ chuẩn không dễ phân tích cú pháp. Phần liên quan của tiêu chuẩn là Khởi tạo §6.7.9 . Cú pháp được ghi lại là:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Lưu ý rằng một trong các thuật ngữ là biểu thức gán và vì chắc chắn a[2] = 1là một biểu thức gán, nó được phép bên trong trình khởi tạo cho các mảng có thời lượng không tĩnh:

§4 Tất cả các biểu thức trong bộ khởi tạo cho một đối tượng có thời lượng lưu trữ tĩnh hoặc chuỗi phải là biểu thức hằng số hoặc chuỗi ký tự.

Một trong những đoạn chính là:

§19 Việc khởi tạo sẽ diễn ra theo thứ tự danh sách bộ khởi tạo, mỗi bộ khởi tạo được cung cấp cho một subobject cụ thể sẽ ghi đè bất kỳ bộ khởi tạo nào được liệt kê trước đó cho cùng một subobject; 151) tất cả các subobject không được khởi tạo rõ ràng sẽ được khởi tạo ngầm giống như các đối tượng có thời lượng lưu trữ tĩnh.

151) Bất kỳ trình khởi tạo nào cho subobject bị ghi đè và do đó không được sử dụng để khởi tạo subobject đó có thể hoàn toàn không được đánh giá.

Và một đoạn quan trọng khác là:

§23 Các đánh giá của các biểu thức danh sách khởi tạo được sắp xếp theo trình tự không xác định đối với nhau và do đó thứ tự xảy ra bất kỳ tác dụng phụ nào là không xác định. 152)

152) Đặc biệt, thứ tự đánh giá không cần giống như thứ tự khởi tạo subobject.

Tôi khá chắc rằng đoạn §23 chỉ ra rằng ký hiệu trong câu hỏi:

int a[5] = { a[2] = 1 };

dẫn đến hành vi không xác định. Việc gán cho a[2]là một tác dụng phụ và thứ tự đánh giá của các biểu thức được sắp xếp theo trình tự không xác định đối với nhau. Do đó, tôi không nghĩ có cách nào để kháng nghị tiêu chuẩn và tuyên bố rằng một trình biên dịch cụ thể đang xử lý điều này một cách chính xác hay không chính xác.


Chỉ có một biểu thức danh sách khởi tạo, vì vậy §23 không liên quan.
melpomene

2

Sự hiểu biết của tôi là a[2]=1trả về giá trị 1 để mã trở thành

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}gán giá trị cho [0] = 1

Do đó, nó in 1 cho [0]

Ví dụ

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;

2
Đây là một câu hỏi [ngôn ngữ-luật sư], nhưng đây không phải là câu trả lời phù hợp với tiêu chuẩn, do đó khiến nó không liên quan. Thêm vào đó, cũng có 2 câu trả lời chuyên sâu hơn có sẵn và câu trả lời của bạn dường như không thêm bất cứ điều gì.
Tạm biệt SE

Tôi có một chút nghi ngờ. Liệu khái niệm tôi đã đăng có sai không? Bạn có thể làm rõ cho tôi điều này được không?
Karthika

1
Bạn chỉ suy đoán lý do, trong khi có một câu trả lời rất tốt đã được đưa ra với các phần liên quan của tiêu chuẩn. Chỉ nói làm thế nào nó có thể xảy ra không phải là câu hỏi. Đó là về những gì tiêu chuẩn nói sẽ xảy ra.
Tạm biệt SE

Nhưng người đăng bài trên đã hỏi nguyên nhân do đâu và sự việc xảy ra như thế nào? Vì vậy, chỉ có tôi đã bỏ câu trả lời này, nhưng khái niệm là đúng.
Karthika

OP hỏi " Đó có phải là hành vi không xác định không? ". Câu trả lời của bạn không nói.
melpomene

1

Tôi cố gắng đưa ra một câu trả lời ngắn gọn và đơn giản cho câu đố: int a[5] = { a[2] = 1 };

  1. Đầu tiên a[2] = 1được thiết lập. Điều đó có nghĩa là mảng cho biết:0 0 1 0 0
  2. Nhưng hãy lưu ý, vì bạn đã làm điều đó trong { }dấu ngoặc, được sử dụng để khởi tạo mảng theo thứ tự, nó nhận giá trị đầu tiên (là 1) và đặt giá trị đó thành a[0]. Nó như thể int a[5] = { a[2] };sẽ vẫn còn, nơi chúng ta đã có a[2] = 1. Mảng kết quả bây giờ là:1 0 1 0 0

Một ví dụ khác: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Mặc dù thứ tự hơi tùy ý, giả sử nó đi từ trái sang phải, nó sẽ đi theo 6 bước sau:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3

1
A = B = C = 5không phải là một khai báo (hoặc khởi tạo). Đó là một biểu thức bình thường phân tích cú pháp A = (B = (C = 5))=toán tử là liên kết đúng. Điều đó không thực sự giúp giải thích cách khởi tạo hoạt động. Mảng thực sự bắt đầu tồn tại khi khối mà nó được định nghĩa được nhập vào, có thể mất nhiều thời gian trước khi định nghĩa thực được thực thi.
melpomene

1
" Nó đi từ trái sang phải, mỗi bắt đầu bằng khai báo nội bộ " là không chính xác. Các tiêu chuẩn C dứt khoát nói " Thứ tự mà bất kỳ tác dụng phụ xảy ra một trong những biểu hiện danh sách khởi tạo là không xác định. "
Melpomene

1
" Bạn kiểm tra mã từ ví dụ của tôi đủ lần và xem liệu kết quả có nhất quán hay không. " Đó không phải là cách nó hoạt động. Bạn dường như không hiểu hành vi không xác định là gì. Mọi thứ trong C đều có hành vi không xác định theo mặc định; nó chỉ là một số bộ phận có hành vi được xác định bởi tiêu chuẩn. Để chứng minh rằng điều gì đó có hành vi xác định, bạn phải trích dẫn tiêu chuẩn và chỉ ra nơi nó xác định điều gì sẽ xảy ra. Trong trường hợp không có định nghĩa như vậy, hành vi là không xác định.
melpomene

1
Khẳng định ở điểm (1) là một bước nhảy vọt lớn so với câu hỏi quan trọng ở đây: liệu khởi tạo ngầm định của phần tử a [2] thành 0 có xảy ra trước khi áp dụng hiệu ứng phụ của a[2] = 1biểu thức khởi tạo không? Kết quả quan sát được là như thể nó đã xảy ra, nhưng tiêu chuẩn dường như không chỉ rõ rằng phải như vậy. Đó là trung tâm của cuộc tranh cãi, và câu trả lời này hoàn toàn bỏ qua nó.
John Bollinger

1
"Hành vi không xác định" là một thuật ngữ chuyên môn với nghĩa hẹp. Nó không có nghĩa là "hành vi mà chúng tôi không thực sự chắc chắn về". Điều quan trọng ở đây là không có thử nghiệm nào, không có trình biên dịch, có thể cho thấy một chương trình cụ thể đang hoạt động tốt hoặc không hoạt động tốt theo tiêu chuẩn , bởi vì nếu một chương trình có hành vi không xác định, trình biên dịch được phép làm bất cứ điều gì - bao gồm cả hoạt động một cách hoàn toàn có thể dự đoán được và hợp lý. Nó không chỉ đơn giản là vấn đề chất lượng triển khai nơi người viết trình biên dịch ghi lại mọi thứ - đó là hành vi không xác định hoặc do triển khai xác định.
Jeroen Mostert

0

Phép gán a[2]= 1là một biểu thức có giá trị 1và về cơ bản bạn đã viết int a[5]= { 1 };(với cả hiệu ứng phụ a[2]được gán 1).


Nhưng không rõ khi nào tác dụng phụ được đánh giá và hành vi có thể thay đổi tùy thuộc vào trình biên dịch. Ngoài ra, tiêu chuẩn dường như tuyên bố rằng đây là hành vi không xác định, việc giải thích cho các nhận thức cụ thể của trình biên dịch là không hữu ích.
Tạm biệt SE

@KamiKaze: chắc chắn, giá trị 1 đến đó một cách tình cờ.
Yves Daoust

0

Tôi tin rằng, đó int a[5]={ a[2]=1 };là một ví dụ điển hình cho việc một lập trình viên tự bắn vào chân mình.

Tôi có thể bị cám dỗ khi nghĩ rằng ý của bạn là int a[5]={ [2]=1 };phần tử cài đặt bộ khởi tạo được chỉ định C99 từ 2 đến 1 và phần còn lại là 0.

Trong trường hợp hiếm hoi mà bạn thực sự muốn nói int a[5]={ 1 }; a[2]=1;, thì đó sẽ là một cách viết hài hước. Nhưng dù sao, đây là những gì mã của bạn tóm tắt, mặc dù một số ở đây đã chỉ ra rằng nó không được xác định rõ khi nào việc ghi vào a[2]thực sự được thực thi. Cạm bẫy ở đây a[2]=1không phải là một bộ khởi tạo được chỉ định mà là một phép gán đơn giản mà bản thân nó có giá trị 1.


có vẻ như chủ đề ngôn ngữ-luật sư này đang yêu cầu tài liệu tham khảo từ các bản nháp tiêu chuẩn. Đó là lý do tại sao bạn bị đánh giá thấp (Tôi đã không làm điều đó như bạn thấy, tôi bị bỏ phiếu vì lý do tương tự). Tôi nghĩ những gì bạn viết là hoàn toàn ổn nhưng có vẻ như tất cả những luật sư ngôn ngữ này ở đây đều là người thông thạo hoặc đại loại như vậy. Vì vậy, họ không yêu cầu giúp đỡ gì cả mà họ đang cố gắng kiểm tra xem bản nháp có phù hợp với trường hợp này hay không và hầu hết những người ở đây được kích hoạt nếu bạn đặt câu trả lời giống như bạn đang giúp họ. Tôi đoán bệnh xóa câu trả lời của tôi :) Nếu đây quy tắc đề đặt rõ ràng mà có thể đã được hữu ích
Abdurrahim
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.