Là thực hành trả lại một biến tham chiếu C ++ ác?


341

Đây là một chút chủ quan tôi nghĩ; Tôi không chắc liệu ý kiến ​​có nhất trí hay không (Tôi đã thấy rất nhiều đoạn mã nơi các tài liệu tham khảo được trả về).

Theo một bình luận cho câu hỏi này tôi vừa hỏi, liên quan đến việc khởi tạo các tài liệu tham khảo , việc trả lại một tài liệu tham khảo có thể là xấu bởi vì, [theo tôi hiểu] nó dễ dàng bỏ lỡ việc xóa nó, điều này có thể dẫn đến rò rỉ bộ nhớ.

Điều này làm tôi lo lắng, vì tôi đã làm theo các ví dụ (trừ khi tôi đang tưởng tượng mọi thứ) và thực hiện điều này ở một vài nơi công bằng ... Tôi có hiểu lầm không? Có ác không? Nếu vậy, chỉ ác thế nào?

Tôi cảm thấy rằng vì túi hỗn hợp các con trỏ và tài liệu tham khảo của mình, kết hợp với thực tế là tôi mới sử dụng C ++ và hoàn toàn nhầm lẫn về việc sử dụng khi nào, các ứng dụng của tôi phải bị rò rỉ bộ nhớ ...

Ngoài ra, tôi hiểu rằng sử dụng con trỏ thông minh / chia sẻ thường được chấp nhận là cách tốt nhất để tránh rò rỉ bộ nhớ.


Nó không phải là xấu nếu bạn đang viết các hàm / phương thức giống như getter.
John Z. Li

Câu trả lời:


411

Nói chung, trả về một tài liệu tham khảo là hoàn toàn bình thường và xảy ra mọi lúc.

Nếu bạn có nghĩa là:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Đó là tất cả các loại xấu xa. Ngăn xếp được phân bổ isẽ biến mất và bạn không đề cập gì. Điều này cũng là xấu xa:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Bởi vì bây giờ khách hàng cuối cùng phải làm điều kỳ lạ:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Lưu ý rằng các tham chiếu rvalue vẫn chỉ là các tham chiếu, vì vậy tất cả các ứng dụng độc ác vẫn giữ nguyên.

Nếu bạn muốn phân bổ thứ gì đó nằm ngoài phạm vi của hàm, hãy sử dụng một con trỏ thông minh (hoặc nói chung, một thùng chứa):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

Và bây giờ khách hàng lưu trữ một con trỏ thông minh:

std::unique_ptr<int> x = getInt();

Tài liệu tham khảo cũng được phép truy cập vào những thứ mà bạn biết cả đời đang được mở ở cấp độ cao hơn, ví dụ:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Ở đây chúng tôi biết sẽ ổn khi trả lại một tham chiếu đến i_vì bất cứ điều gì đang gọi chúng tôi đều quản lý thời gian tồn tại của thể hiện của lớp, vì vậy i_sẽ tồn tại ít nhất là như vậy.

Và tất nhiên, không có gì sai chỉ với:

int getInt() {
   return 0;
}

Nếu trọn đời nên để lại cho người gọi và bạn chỉ đang tính toán giá trị.

Tóm tắt: không thể trả lại tham chiếu nếu thời gian tồn tại của đối tượng sẽ không kết thúc sau cuộc gọi.


21
Đây là tất cả các ví dụ xấu. Ví dụ tốt nhất về cách sử dụng hợp lý là khi bạn trả về một tham chiếu đến một đối tượng được truyền vào. Toán tử ala <<
Arelius

171
Vì lợi ích của hậu thế, và đối với bất kỳ lập trình viên mới nào đang theo đuổi điều này, con trỏ không phải là xấu . Không phải là con trỏ đến bộ nhớ động xấu. Cả hai đều có vị trí hợp pháp của họ trong C ++. Con trỏ thông minh chắc chắn phải là hướng đi mặc định của bạn khi nói đến quản lý bộ nhớ động, nhưng con trỏ thông minh mặc định của bạn phải là unique_ptr, không phải shared_ptr.
Jamin Gray

12
Chỉnh sửa người phê duyệt: không phê duyệt các chỉnh sửa nếu bạn không thể đảm bảo tính chính xác của nó. Tôi đã quay lại chỉnh sửa không chính xác.
GManNickG

7
Vì lợi ích của hậu thế, và đối với bất kỳ lập trình viên mới nào đang theo đuổi điều này, đừng viếtreturn new int .
Các cuộc đua nhẹ nhàng trong quỹ đạo

3
Vì lợi ích của hậu thế, và đối với bất kỳ lập trình viên mới nào đang theo đuổi điều này, chỉ cần trả về T từ hàm. RVO sẽ lo tất cả mọi thứ.
Giày

64

Không, không, một ngàn lần không.

Điều gì là xấu xa là làm cho một tham chiếu đến một đối tượng được phân bổ động và mất con trỏ ban đầu. Khi bạn newlà một đối tượng, bạn có nghĩa vụ phải có một bảo đảm delete.

Nhưng hãy xem, ví dụ operator<<: phải trả về một tham chiếu, hoặc

cout << "foo" << "bar" << "bletch" << endl ;

sẽ không làm việc


23
Tôi đã từ chối vì điều này không trả lời được câu hỏi (trong đó OP đã nói rõ rằng anh ta cần phải xóa) cũng như không giải quyết nỗi sợ hãi chính đáng rằng việc trả lại một tham chiếu đến một đối tượng tự do có thể dẫn đến nhầm lẫn. Thở dài.

4
Việc thực hành trả lại một đối tượng tham chiếu không phải là xấu xa. Không có. Nỗi sợ hãi mà anh ấy thể hiện là một nỗi sợ chính xác, như tôi đã chỉ ra trong graf thứ hai.
Charlie Martin

Bạn thực sự đã không. Nhưng điều này không đáng với thời gian của tôi.

2
Iraimbilanja @ Về "Không" - tôi không quan tâm. nhưng bài đăng này đã chỉ ra một thông tin quan trọng còn thiếu từ GMan.
Kobor42

48

Bạn nên trả lại một tham chiếu đến một đối tượng hiện tại sẽ không biến mất ngay lập tức và nơi bạn không có ý định chuyển quyền sở hữu.

Không bao giờ trả lại một tham chiếu đến một biến cục bộ hoặc một số như vậy, bởi vì nó sẽ không ở đó để được tham chiếu.

Bạn có thể trả về một tham chiếu đến một cái gì đó độc lập với chức năng mà bạn không mong đợi chức năng gọi sẽ chịu trách nhiệm xóa. Đây là trường hợp cho các operator[]chức năng điển hình .

Nếu bạn đang tạo một cái gì đó, bạn nên trả về một giá trị hoặc một con trỏ (thông thường hoặc thông minh). Bạn có thể trả về một giá trị một cách tự do, vì nó sẽ đi vào một biến hoặc biểu thức trong hàm gọi. Không bao giờ trả lại một con trỏ đến một biến cục bộ, vì nó sẽ biến mất.


1
Câu trả lời tuyệt vời nhưng cho "Bạn có thể trả lại tạm thời dưới dạng tham chiếu const." Đoạn mã sau sẽ biên dịch nhưng có thể bị sập vì tạm thời bị hủy ở cuối câu lệnh return: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ có một số quy tắc lạ đối với các tham chiếu đến tạm thời, trong đó thời gian tồn tại tạm thời có thể được kéo dài. Tôi xin lỗi tôi không hiểu nó đủ tốt để biết nếu nó bao gồm trường hợp của bạn.
Đánh dấu tiền chuộc

3
@Mark: Vâng, nó có một số quy tắc kỳ lạ. Thời gian tồn tại tạm thời chỉ có thể được kéo dài bằng cách khởi tạo một tham chiếu const (không phải là thành viên lớp) với nó; sau đó nó sống cho đến khi ref đi ra khỏi phạm vi. Đáng buồn thay, trả lại một const ref không được bảo hiểm. Trả lại một temp theo giá trị là an toàn tuy nhiên.
j_random_hacker

Xem Tiêu chuẩn C ++, 12.2, đoạn 5. Cũng xem Giáo sư đi lạc trong tuần của Herb Sutter tại Herbutter.wordpress.com/2008/01/01/ .
David Thornley

4
@David: Khi kiểu trả về của hàm là "T const &", điều thực sự xảy ra là câu lệnh return hoàn toàn chuyển đổi temp, thuộc loại T, để gõ "T const &" theo 6.6.3.2 (một chuyển đổi hợp pháp nhưng là một không kéo dài tuổi thọ), và sau đó mã gọi khởi tạo tham chiếu loại "T const &" với kết quả của hàm, cũng thuộc loại "T const &" - ​​một lần nữa, một quy trình hợp pháp nhưng không kéo dài suốt đời. Kết quả cuối cùng: không kéo dài suốt đời, và nhiều nhầm lẫn. :(
j_random_hacker

26

Tôi thấy câu trả lời không thỏa đáng nên tôi sẽ thêm hai xu của mình.

Hãy phân tích các trường hợp sau:

Sử dụng sai

int& getInt()
{
    int x = 4;
    return x;
}

Đây rõ ràng là lỗi

int& x = getInt(); // will refer to garbage

Sử dụng với các biến tĩnh

int& getInt()
{
   static int x = 4;
   return x;
}

Điều này đúng, bởi vì các biến tĩnh tồn tại trong suốt vòng đời của một chương trình.

int& x = getInt(); // valid reference, x = 4

Điều này cũng khá phổ biến khi triển khai mẫu Singleton

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Sử dụng:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Người vận hành

Các thùng chứa thư viện tiêu chuẩn phụ thuộc rất nhiều vào việc sử dụng các toán tử trả về tham chiếu, ví dụ

T & operator*();

có thể được sử dụng sau đây

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Truy cập nhanh vào dữ liệu nội bộ

Đôi khi & có thể được sử dụng để truy cập nhanh vào dữ liệu nội bộ

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

với cách sử dụng:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

TUY NHIÊN, điều này có thể dẫn đến cạm bẫy như thế này:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!

Việc trả lại tham chiếu cho một biến tĩnh có thể dẫn đến hành vi không mong muốn, ví dụ như xem xét một toán tử nhân trả về một tham chiếu cho một thành viên tĩnh, sau đó sẽ luôn dẫn đến true:If((a*b) == (c*d))
SebNag

Container::data()Việc triển khai nên đọcreturn m_data;
Xeaz

Điều này rất hữu ích, cảm ơn! @Xeaz sẽ không gây ra vấn đề với cuộc gọi chắp thêm chứ?
Andrew

@SebTu Tại sao bạn muốn làm điều đó?
thorhunter

@Andrew Không, đó là một cú pháp shenanigan. Nếu bạn, ví dụ, trả về một loại con trỏ, thì bạn sẽ sử dụng địa chỉ tham chiếu để tạo và trả về một con trỏ.
thorhunter

14

Nó không xấu xa. Giống như nhiều thứ trong C ++, thật tốt nếu được sử dụng đúng cách, nhưng có nhiều cạm bẫy mà bạn nên biết khi sử dụng nó (như trả lại một tham chiếu cho một biến cục bộ).

Có những điều tốt có thể đạt được với nó (như map [name] = "hello world")


1
Tôi chỉ tò mò, những gì là tốt về map[name] = "hello world"?
tên người dùng sai

5
@wrongusername Cú pháp trực quan. Bạn đã bao giờ thử tăng số lượng giá trị được lưu trữ HashMap<String,Integer>trong Java chưa? : P
Mehrdad Afshari

Haha, chưa, nhưng nhìn vào các ví dụ HashMap, nó có vẻ khá
sởn gai ốc

Vấn đề tôi gặp phải với điều này: Hàm trả về tham chiếu đến một đối tượng trong một thùng chứa, nhưng mã hàm gọi đã gán nó cho một biến cục bộ. Sau đó sửa đổi một số thuộc tính của đối tượng. Vấn đề: Đối tượng ban đầu trong container không bị ảnh hưởng. Lập trình viên dễ dàng bỏ qua giá trị & trong giá trị trả về, và sau đó bạn nhận được các hành vi thực sự bất ngờ ...
flohack

10

"trả lại một tài liệu tham khảo là xấu bởi vì, chỉ đơn giản là [theo tôi hiểu] nó làm cho việc bỏ qua nó dễ dàng hơn"

Không đúng. Trả lại một tài liệu tham khảo không ngụ ý ngữ nghĩa sở hữu. Đó là, chỉ vì bạn làm điều này:

Value& v = thing->getTheValue();

... không có nghĩa là bây giờ bạn sở hữu bộ nhớ được gọi bởi v;

Tuy nhiên, đây là mã khủng khiếp:

int& getTheValue()
{
   return *new int;
}

Nếu bạn đang làm một cái gì đó như thế này bởi vì "bạn không yêu cầu một con trỏ trong trường hợp đó " thì: 1) chỉ cần hủy bỏ con trỏ nếu bạn cần một tham chiếu và 2) cuối cùng bạn sẽ cần con trỏ, bởi vì bạn phải khớp với mới với một xóa, và bạn cần một con trỏ để gọi xóa.


7

Có hai trường hợp:

  • const tham chiếu - ý tưởng tốt, đôi khi, đặc biệt đối với các đối tượng nặng hoặc các lớp proxy, tối ưu hóa trình biên dịch

  • không tham chiếu - ý tưởng, đôi khi, phá vỡ đóng gói

Cả hai đều có chung một vấn đề - có khả năng có thể trỏ đến đối tượng bị phá hủy ...

Tôi sẽ khuyên bạn nên sử dụng con trỏ thông minh cho nhiều tình huống mà bạn yêu cầu trả về một tham chiếu / con trỏ.

Ngoài ra, lưu ý những điều sau:

Có một quy tắc chính thức - Tiêu chuẩn C ++ (phần 13.3.3.1.4 nếu bạn quan tâm) nói rằng tạm thời chỉ có thể được ràng buộc với một tham chiếu const - nếu bạn cố gắng sử dụng một tham chiếu không const, trình biên dịch phải gắn cờ này là một lỗi.


1
ref không phải const không nhất thiết phải phá vỡ đóng gói. xem xét véc tơ :: toán tử []

đó là một trường hợp rất đặc biệt ... đó là lý do tại sao đôi khi tôi đã nói, mặc dù tôi thực sự nên yêu cầu NHIỀU THỜI GIAN :)

Vì vậy, bạn đang nói rằng toán tử đăng ký bình thường thực hiện một điều ác cần thiết? Tôi không đồng ý hay đồng ý với điều này; vì tôi không phải là người khôn ngoan hơn
Nick Bolton

Tôi không nói vậy, nhưng nó có thể là xấu xa nếu bị lạm dụng :))) vector :: nên được sử dụng bất cứ khi nào có thể ....

Hở? vector :: cũng trả về một tham chiếu nonconst.

4

Không chỉ là không xấu xa, đôi khi nó rất cần thiết. Ví dụ, không thể thực hiện toán tử [] của std :: vector mà không sử dụng giá trị trả về tham chiếu.


À, tất nhiên rồi; Tôi nghĩ đây là lý do tại sao tôi bắt đầu sử dụng nó; như lần đầu tiên tôi triển khai toán tử đăng ký [] tôi nhận ra việc sử dụng các tham chiếu. Tôi tin rằng đây là sự thật.
Nick Bolton

Thật kỳ lạ, bạn có thể triển khai operator[]cho một container mà không cần sử dụng tài liệu tham khảo ... và std::vector<bool>không. (Và tạo ra một mớ hỗn độn thực sự trong quá trình)
Ben Voigt

@BenVoigt mmm, tại sao một mớ hỗn độn? Trả lại proxy cũng là một kịch bản hợp lệ cho các container có bộ lưu trữ phức tạp không ánh xạ trực tiếp đến các loại bên ngoài (như ::std::vector<bool>bạn đã đề cập).
Sergey.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov: Rắc rối là việc sử dụng std::vector<T>mã mẫu bị hỏng, nếu Tcó thể bool, bởi vì std::vector<bool>có hành vi rất khác với các cảnh báo khác. Nó hữu ích, nhưng đáng lẽ nó phải được đặt tên riêng và không phải là chuyên môn hóa std::vector.
Ben Voigt

@BenVoight Tôi đồng ý ở điểm về quyết định kỳ lạ về việc thực hiện một chuyên ngành "thực sự đặc biệt" nhưng tôi cảm thấy rằng nhận xét ban đầu của bạn ngụ ý rằng việc trả lại một proxy là kỳ quặc nói chung.
Sergey.quixoticaxis.Ivanov

2

Ngoài ra câu trả lời được chấp nhận:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Tôi cho rằng ví dụ này không ổn và nên tránh nếu có thể. Tại sao? Nó rất dễ dàng để kết thúc với một tài liệu tham khảo lơ lửng .

Để minh họa điểm bằng một ví dụ:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

đi vào vùng nguy hiểm:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!

1

tham chiếu trả về thường được sử dụng trong nạp chồng toán tử trong C ++ cho Đối tượng lớn, vì trả về giá trị cần thao tác sao chép. (trong quá tải perator, chúng ta thường không sử dụng con trỏ làm giá trị trả về)

Nhưng tham chiếu trở lại có thể gây ra vấn đề phân bổ bộ nhớ. Bởi vì tham chiếu đến kết quả sẽ được chuyển ra khỏi hàm dưới dạng tham chiếu đến giá trị trả về, giá trị trả về không thể là biến tự động.

nếu bạn muốn sử dụng trả về giới thiệu, bạn có thể sử dụng bộ đệm của đối tượng tĩnh. ví dụ

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

bằng cách này, bạn có thể sử dụng trả về tài liệu tham khảo một cách an toàn.

Nhưng bạn luôn có thể sử dụng con trỏ thay vì tham chiếu để trả về giá trị trong functiong.


0

tôi nghĩ rằng việc sử dụng tham chiếu làm giá trị trả về của hàm là thẳng hơn nhiều so với sử dụng con trỏ làm giá trị trả về của hàm. Thứ hai, sẽ luôn an toàn khi sử dụng biến tĩnh mà giá trị trả về đề cập đến.


0

Điều tốt nhất là tạo đối tượng và truyền nó dưới dạng tham số / tham chiếu con trỏ đến một hàm phân bổ biến này.

Phân bổ đối tượng trong hàm và trả nó làm tham chiếu hoặc con trỏ (tuy nhiên con trỏ an toàn hơn) là ý tưởng tồi vì giải phóng bộ nhớ ở cuối khối chức năng.


-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

Hàm getPtr có thể truy cập bộ nhớ động sau khi xóa hoặc thậm chí là một đối tượng null. Điều này có thể gây ra ngoại lệ truy cập xấu. Thay vào đó getter và setter nên được thực hiện và xác minh kích thước trước khi quay trở lại.


-2

Chức năng như lvalue (hay còn gọi là trả về các tham chiếu không phải const) nên bị xóa khỏi C ++. Nó cực kỳ không trực quan. Scott Meyers muốn một phút () với hành vi này.

min(a,b) = 0;  // What???

mà không thực sự là một cải tiến trên

setmin (a, b, 0);

Cái sau thậm chí còn có ý nghĩa hơn.

Tôi nhận ra rằng chức năng như lvalue rất quan trọng đối với các luồng kiểu C ++, nhưng đáng để chỉ ra rằng các luồng kiểu C ++ rất tệ. Tôi không phải là người duy nhất nghĩ điều này ... vì tôi nhớ Alexandrescu đã có một bài viết lớn về cách làm tốt hơn và tôi tin rằng boost cũng đã cố gắng tạo ra một phương pháp I / O an toàn tốt hơn.


2
Chắc chắn nó nguy hiểm và cần kiểm tra lỗi trình biên dịch tốt hơn, nhưng không có nó, một số cấu trúc hữu ích không thể được thực hiện, ví dụ toán tử [] () trong std :: map.
j_random_hacker

2
Trả về các tài liệu tham khảo không phải là const thực sự hữu ích. vector::operator[]ví dụ. Bạn có muốn viết v.setAt(i, x)hay v[i] = xkhông? Thứ hai là FAR vượt trội.
Tuyến Miles

1
@MilesRout Tôi sẽ đi v.setAt(i, x)bất cứ lúc nào. Đó là FAR vượt trội.
scravy

-2

Tôi gặp phải một vấn đề thực sự nơi nó thực sự xấu xa. Về cơ bản, một nhà phát triển đã trả về một tham chiếu đến một đối tượng trong một vectơ. Điều đó thật tệ!!!

Các chi tiết đầy đủ tôi đã viết trong Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Nếu bạn cần sửa đổi giá trị ban đầu trong mã gọi, thì bạn cần trả về một ref. Và trên thực tế, điều đó không hơn và không kém phần nguy hiểm so với việc trả lại một trình vòng lặp cho một vectơ - cả hai đều bị vô hiệu nếu các phần tử được thêm vào hoặc xóa khỏi vectơ.
j_random_hacker

Vấn đề cụ thể đó là do giữ một tham chiếu đến một phần tử vectơ, và sau đó sửa đổi vectơ đó theo cách làm mất hiệu lực tham chiếu: Trang 153, mục 6.2 của "Thư viện chuẩn C ++: Hướng dẫn và tham khảo" - Josuttis, đọc: "Chèn hoặc loại bỏ các phần tử làm mất hiệu lực các tham chiếu, các con trỏ và các trình lặp tham chiếu đến các phần tử sau. Nếu một phần chèn gây ra sự phân bổ lại, nó làm mất hiệu lực tất cả các tham chiếu, các trình lặp và các con trỏ "
Trent

-15

Về mã khủng khiếp:

int& getTheValue()
{
   return *new int;
}

Vì vậy, thực sự, con trỏ bộ nhớ bị mất sau khi trở về. Nhưng nếu bạn sử dụng shared_ptr như thế:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

Bộ nhớ không bị mất sau khi trở về và sẽ được giải phóng sau khi gán.


12
Nó bị mất vì con trỏ chia sẻ đi ra khỏi phạm vi và giải phóng số nguyên.

con trỏ không bị mất, địa chỉ của tham chiếu là con trỏ.
dssomerton
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.