Làm cách nào để nối chuỗi const / bằng chữ trong C?


346

Tôi đang làm việc tại C, và tôi phải kết hợp một vài điều.

Ngay bây giờ tôi có cái này:

message = strcat("TEXT ", var);

message2 = strcat(strcat("TEXT ", foo), strcat(" TEXT ", bar));

Bây giờ nếu bạn có kinh nghiệm về C Tôi chắc chắn rằng bạn nhận ra rằng điều này mang lại cho bạn một lỗi phân khúc khi bạn cố chạy nó. Vì vậy, làm thế nào để tôi làm việc xung quanh đó?


6
Tôi muốn đề nghị bạn sử dụng strlcat thay vì strcat! gratisoft.us/todd/ con / strlcpy.html
activout.se

3
Tôi muốn nhắc lại đề nghị đó. Strcat gây ra lỗ hổng để khai thác tràn bộ đệm. Ai đó có thể cung cấp dữ liệu chương trình của bạn khiến nó thực thi mã tùy ý.
Brian

Câu trả lời:


386

Trong C, "chuỗi" chỉ là charmảng đơn giản . Do đó, bạn không thể nối trực tiếp chúng với các "chuỗi" khác.

Bạn có thể sử dụng strcathàm, nối thêm chuỗi được trỏ vào srccuối chuỗi được trỏ bởi dest:

char *strcat(char *dest, const char *src);

Dưới đây là một ví dụ từ cplusplus.com :

char str[80];
strcpy(str, "these ");
strcat(str, "strings ");
strcat(str, "are ");
strcat(str, "concatenated.");

Đối với tham số đầu tiên, bạn cần cung cấp bộ đệm đích. Bộ đệm đích phải là bộ đệm mảng char. Ví dụ:char buffer[1024];

Đảm bảo rằng tham số đầu tiên có đủ dung lượng để lưu trữ những gì bạn đang cố sao chép vào đó. Nếu có sẵn cho bạn, sẽ an toàn hơn khi sử dụng các hàm như: strcpy_sstrcat_snơi bạn rõ ràng phải chỉ định kích thước của bộ đệm đích.

Lưu ý : Không thể sử dụng một chuỗi ký tự như một bộ đệm, vì nó là một hằng số. Vì vậy, bạn luôn phải phân bổ một mảng char cho bộ đệm.

Giá trị trả về của strcatđơn giản có thể bị bỏ qua, nó chỉ trả về cùng một con trỏ như được truyền vào làm đối số đầu tiên. Nó ở đó để thuận tiện và cho phép bạn xâu chuỗi các cuộc gọi thành một dòng mã:

strcat(strcat(str, foo), bar);

Vì vậy, vấn đề của bạn có thể được giải quyết như sau:

char *foo = "foo";
char *bar = "bar";
char str[80];
strcpy(str, "TEXT ");
strcat(str, foo);
strcat(str, bar);

66
Bạn có thể đặt "Hãy cẩn thận rằng ..." in đậm không? Điều này không thể được nhấn mạnh đủ. Việc sử dụng sai strcat, strcpy và sprintf là trái tim của phần mềm không ổn định / không an toàn.
plinth

12
Cảnh báo: Như đã viết, mã này sẽ để lại một lỗ hổng khổng lồ trong mã của bạn để khai thác tràn bộ đệm.
Brian

11
Không có khai thác tràn bộ đệm trong ví dụ trên. Và vâng, tôi đồng ý nói chung tôi sẽ không sử dụng ví dụ trên cho độ dài chuỗi không xác định của foo và bar.
Brian R. Bondy

13
@psihodelia: Cũng đừng quên rằng thìa tốt hơn nhiều so với dĩa! vì vậy hãy chắc chắn luôn luôn sử dụng một cái muỗng
Brian R. Bondy

20
Để thứ hai @dolmen, Joel Spolsky đã viết bài viết khá công phu về vấn đề này. Nên là một bài đọc bắt buộc. ;-)
peter.slizik

247

Tránh sử dụng strcattrong mã C. Cách sạch nhất và quan trọng nhất là cách an toàn nhất là sử dụng snprintf:

char buf[256];
snprintf(buf, sizeof buf, "%s%s%s%s", str1, str2, str3, str4);

Một số bình luận đưa ra một vấn đề rằng số lượng đối số có thể không khớp với chuỗi định dạng và mã sẽ vẫn biên dịch, nhưng hầu hết các trình biên dịch đã đưa ra cảnh báo nếu đây là trường hợp.


3
Người kiểm tra, anh ta đang nói về dấu ngoặc đơn xung quanh "buf" của đối số sizeof. chúng không được yêu cầu nếu đối số là một biểu thức. Nhưng tôi không hiểu tại sao bạn bị hạ thấp. Tôi nghĩ rằng câu trả lời của bạn là tốt nhất trong tất cả, mặc dù nó là c99. (có lẽ vì điều đó mà họ không đồng ý! than thở!) +1
Johannes Schaub - litb

4
sizeof () chỉ hoạt động ở đây cho char buf [...]. KHÔNG cho char * buf = malloc (...). Không có nhiều sự khác biệt giữa mảng và con trỏ, nhưng đây là một trong số chúng!
Mr.Ree

2
Ngoài ra, anh ta đang cố gắng thực hiện nối. Sử dụng kết hợp snprintf()là LỚN không không.
Leonardo Herrera

5
@MrRee: Sự khác biệt giữa con trỏ và mảng là rất lớn và đầy đủ! Đó là cách bạn sử dụng chúng không phải lúc nào cũng khác nhau. Ngoài ra, con trỏ và phân bổ động là các khái niệm thực sự trực giao.
Các cuộc đua nhẹ nhàng trong quỹ đạo

34
Một trong những thú cưng của tôi là những người như @unwind, người khăng khăng đòi phân biệt vô nghĩa giữa sizeof(x)sizeof x. Ký hiệu ngoặc đơn luôn hoạt động và ký hiệu không được mã hóa đôi khi chỉ hoạt động, vì vậy luôn luôn sử dụng ký hiệu ngoặc đơn; đó là một quy tắc đơn giản để ghi nhớ và an toàn. Điều này vướng vào một cuộc tranh luận tôn giáo - tôi đã tham gia vào các cuộc thảo luận với những người phản đối trước đây - nhưng sự đơn giản của "luôn luôn sử dụng dấu ngoặc đơn" vượt trội hơn bất kỳ giá trị nào khi không sử dụng chúng (tất nhiên là IMNSHO). Điều này được trình bày cho sự cân bằng.
Jonathan Leffler

24

Mọi người , sử dụng str n cpy (), str n cat () hoặc s n printf ().
Vượt quá không gian bộ đệm của bạn sẽ rác bất cứ thứ gì khác theo sau trong bộ nhớ!
(Và nhớ cho phép không gian cho ký tự null '\ 0'!)


3
Bạn không chỉ nên nhớ cho phép không gian cho ký tự NULL, bạn cần nhớ để thêm ký tự NULL. strncpy và strncat không làm điều đó cho bạn.
Graeme Perrow

Ừm strncpy () và strncat () chắc chắn thêm ký tự kết thúc. Trong thực tế, họ thêm quá nhiều. Ít nhất là miễn là còn chỗ trống trong bộ đệm, đó là một cái bẫy lớn với những cuộc gọi này. Không được khuyến khích.
thư giãn

3
@unwind, tôi nghĩ rằng điểm của Graeme là nếu bộ đệm quá nhỏ, strncpy hoặc strncat sẽ không thêm dấu chấm dứt '\ 0'.
quinmars

2
snprintf là tốt, strncpy / strncat là đề xuất tồi tệ nhất có thể, strlcpy / strlcat là tốt hơn nhiều.
Robert Gamble

9
Đừng sử dụng strncpy(). Đây không phải là phiên bản "an toàn" hơn strcpy(). Mảng ký tự đích có thể được đệm một cách không cần thiết với các '\0'ký tự phụ , hoặc tệ hơn, nó có thể bị bỏ lại (nghĩa là không phải là một chuỗi). (Nó được thiết kế để sử dụng với cấu trúc dữ liệu hiếm khi được sử dụng nữa, một mảng ký tự được đệm đến cuối bằng 0 hoặc nhiều '\0'ký tự.)
Keith Thompson

22

Các chuỗi cũng có thể được nối vào thời gian biên dịch.

#define SCHEMA "test"
#define TABLE  "data"

const char *table = SCHEMA "." TABLE ; // note no + or . or anything
const char *qry =               // include comments in a string
    " SELECT * "                // get all fields
    " FROM " SCHEMA "." TABLE   /* the table */
    " WHERE x = 1 "             /* the filter */ 
                ;

15

Ngoài ra malloc và realloc rất hữu ích nếu bạn không biết trước có bao nhiêu chuỗi đang được nối.

#include <stdio.h>
#include <string.h>

void example(const char *header, const char **words, size_t num_words)
{
    size_t message_len = strlen(header) + 1; /* + 1 for terminating NULL */
    char *message = (char*) malloc(message_len);
    strncat(message, header, message_len);

    for(int i = 0; i < num_words; ++i)
    {
       message_len += 1 + strlen(words[i]); /* 1 + for separator ';' */
       message = (char*) realloc(message, message_len);
       strncat(strncat(message, ";", message_len), words[i], message_len);
    }

    puts(message);

    free(message);
}

Điều này sẽ kết thúc trong một vòng lặp vô tận khi num_words>INT_MAX, có lẽ bạn nên sử dụng size_tchoi
12431234123412341234123

5

Đừng quên khởi tạo bộ đệm đầu ra. Đối số đầu tiên cho strcat phải là một chuỗi kết thúc null có đủ không gian bổ sung được phân bổ cho chuỗi kết quả:

char out[1024] = ""; // must be initialized
strcat( out, null_terminated_string ); 
// null_terminated_string has less than 1023 chars

4

Như mọi người chỉ ra xử lý chuỗi cải thiện nhiều. Vì vậy, bạn có thể muốn tìm hiểu cách sử dụng thư viện chuỗi C ++ thay vì chuỗi kiểu C. Tuy nhiên đây là một giải pháp trong C thuần túy

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void appendToHello(const char *s) {
    const char *const hello = "hello ";

    const size_t sLength     = strlen(s);
    const size_t helloLength = strlen(hello);
    const size_t totalLength = sLength + helloLength;

    char *const strBuf = malloc(totalLength + 1);
    if (strBuf == NULL) {
        fprintf(stderr, "malloc failed\n");
        exit(EXIT_FAILURE);
    }

    strcpy(strBuf, hello);
    strcpy(strBuf + helloLength, s);

    puts(strBuf);

    free(strBuf);

}

int main (void) {
    appendToHello("blah blah");
    return 0;
}

Tôi không chắc liệu nó có đúng / an toàn hay không nhưng hiện tại tôi không thể tìm ra cách nào tốt hơn để làm điều này trong ANSI C.


<string.h>là kiểu C ++. Bạn muốn "string.h". Bạn cũng tính toán strlen(s1)hai lần, điều này không cần thiết. s3nên totalLenght+1dài
Vịt Mooing

4
@MooingDuck: "string.h"là vô nghĩa.
sbi

Tôi đã không sử dụng chuỗi kiểu C trong một thời gian. Hãy đăng một phiên bản cố định.
Nils

4
@MooingDuck: Điều đó không chính xác. #include <string.h>là chính xác C. Sử dụng dấu ngoặc góc cho các tiêu đề hệ thống và tiêu chuẩn (bao gồm <string.h>), dấu ngoặc kép cho các tiêu đề là một phần của chương trình của bạn. ( #include "string.h"sẽ xảy ra nếu bạn không có tệp tiêu đề của riêng mình bằng tên đó, nhưng vẫn sử dụng <string.h>.)
Keith Thompson

Lưu ý rằng điều này phụ thuộc vào các tính năng dành riêng cho C99: trộn các khai báo và câu lệnh và mảng có độ dài thay đổi (VLAs). Cũng lưu ý rằng VLAs không cung cấp cơ chế để phát hiện hoặc xử lý các lỗi phân bổ; nếu không đủ chỗ để phân bổ VLA, hành vi của chương trình của bạn không được xác định.
Keith Thompson

4

Đó là hành vi không xác định để cố gắng sửa đổi chuỗi ký tự, đó là một cái gì đó như:

strcat ("Hello, ", name);

sẽ cố gắng làm. Nó sẽ cố gắng giải quyết namechuỗi đến cuối chuỗi bằng chữ "Hello, ", không được xác định rõ.

Hãy thử một cái gì đó này. Nó đạt được những gì bạn dường như đang cố gắng làm:

char message[1000];
strcpy (message, "TEXT ");
strcat (message, var);

Điều này tạo ra một khu vực đệm đó phép sửa đổi và sau đó sao chép cả chuỗi ký tự và văn bản khác vào nó. Chỉ cần cẩn thận với tràn bộ đệm. Nếu bạn kiểm soát dữ liệu đầu vào (hoặc kiểm tra trước khi sử dụng), bạn có thể sử dụng bộ đệm có độ dài cố định như tôi có.

Mặt khác, bạn nên sử dụng các chiến lược giảm thiểu như phân bổ đủ bộ nhớ từ heap để đảm bảo bạn có thể xử lý nó. Nói cách khác, một cái gì đó như:

const static char TEXT[] = "TEXT ";

// Make *sure* you have enough space.

char *message = malloc (sizeof(TEXT) + strlen(var) + 1);
if (message == NULL)
     handleOutOfMemoryIntelligently();
strcpy (message, TEXT);
strcat (message, var);

// Need to free message at some point after you're done with it.

4
Điều gì xảy ra nếu var / foo / bar có hơn 1000 ký tự? > :)
Geo

1
Sau đó, bạn sẽ nhận được một lỗi tràn bộ đệm, mà bạn có thể thêm mã để kiểm tra trước (giả sử với strlen). Nhưng mục đích của một đoạn mã là để cho thấy một cái gì đó hoạt động như thế nào mà không làm ô nhiễm nó với quá nhiều mã bổ sung. Mặt khác, tôi sẽ kiểm tra độ dài, cho dù var / foo / bar là null, v.v.
paxdiablo

7
@paxdiablo: Nhưng bạn thậm chí không đề cập đến nó, trong câu trả lời cho câu hỏi nó sẽ xuất hiện ở đâu để đề cập đến. Điều đó làm cho câu trả lời của bạn trở nên nguy hiểm . Bạn cũng không giải thích được tại sao mã này tốt hơn mã gốc của OP, ngoại trừ huyền thoại rằng nó "đạt được kết quả giống như mã gốc của bạn" (vậy thì vấn đề là gì? Bản gốc đã bị hỏng !), Vì vậy câu trả lời cũng không đầy đủ .
Các cuộc đua nhẹ nhàng trong quỹ đạo

Hy vọng đã giải quyết mối quan tâm của bạn, @PreferenceBean, mặc dù theo cách không kịp thời hơn lý tưởng :-) Hãy cho tôi biết nếu bạn vẫn gặp vấn đề với câu trả lời, và tôi sẽ cải thiện nó hơn nữa.
paxdiablo

3

Đối số đầu tiên của strcat () cần có khả năng giữ đủ không gian cho chuỗi được nối. Vì vậy, phân bổ một bộ đệm với đủ không gian để nhận kết quả.

char bigEnough[64] = "";

strcat(bigEnough, "TEXT");
strcat(bigEnough, foo);

/* and so on */

strcat () sẽ nối đối số thứ hai với đối số thứ nhất và lưu trữ kết quả trong đối số thứ nhất, char * được trả về chỉ đơn giản là đối số đầu tiên này và chỉ để thuận tiện cho bạn.

Bạn không nhận được một chuỗi mới được phân bổ với đối số thứ nhất và thứ hai được nối, mà tôi đoán bạn mong đợi dựa trên mã của bạn.


3

Cách tốt nhất để làm điều đó mà không có kích thước bộ đệm giới hạn là sử dụng asprintf ()

char* concat(const char* str1, const char* str2)
{
    char* result;
    asprintf(&result, "%s%s", str1, str2);
    return result;
}

2
Bạn nên trở về char *, không const char *. Giá trị trả lại sẽ cần phải được chuyển đến free.
Per Johansson

Thật không may, asprintfchỉ là một phần mở rộng GNU.
Calmarius

3

Nếu bạn có kinh nghiệm về C, bạn sẽ nhận thấy các chuỗi chỉ là mảng char trong đó ký tự cuối cùng là ký tự null.

Bây giờ điều đó khá bất tiện khi bạn phải tìm nhân vật cuối cùng để nối thêm một cái gì đó. strcatsẽ làm điều đó cho bạn.

Vì vậy, strcat tìm kiếm thông qua đối số đầu tiên cho một ký tự null. Sau đó, nó sẽ thay thế nội dung này bằng nội dung của đối số thứ hai (cho đến khi kết thúc bằng null).

Bây giờ hãy xem mã của bạn:

message = strcat("TEXT " + var);

Ở đây bạn đang thêm một cái gì đó vào con trỏ vào văn bản "TEXT" (loại "văn bản" là const char *. Một con trỏ.).

Điều đó thường sẽ không hoạt động. Ngoài ra, sửa đổi mảng "văn bản" sẽ không hoạt động vì nó thường được đặt trong một phân đoạn không đổi.

message2 = strcat(strcat("TEXT ", foo), strcat(" TEXT ", bar));

Điều đó có thể hoạt động tốt hơn, ngoại trừ việc bạn đang cố gắng sửa đổi các văn bản tĩnh. strcat không phân bổ bộ nhớ mới cho kết quả.

Tôi sẽ đề nghị làm một cái gì đó như thế này thay thế:

sprintf(message2, "TEXT %s TEXT %s", foo, bar);

Đọc tài liệu của sprintf để kiểm tra các tùy chọn của nó.

Và bây giờ là một điểm quan trọng:

Đảm bảo rằng bộ đệm có đủ không gian để giữ văn bản VÀ ký tự null. Có một số chức năng có thể giúp bạn, ví dụ: strncat và các phiên bản đặc biệt của printf phân bổ bộ đệm cho bạn. Không đảm bảo kích thước bộ đệm sẽ dẫn đến hỏng bộ nhớ và các lỗi có thể khai thác từ xa.


Các loại "TEXT"char[5], không const char* . Nó phân rã char*trong hầu hết các bối cảnh. Vì lý do tương thích ngược, chuỗi ký tự không phải là const, nhưng cố gắng sửa đổi chúng dẫn đến hành vi không xác định. (Trong C ++, chuỗi ký tự là const.)
Keith Thompson

2

Bạn có thể viết chức năng của riêng bạn làm điều tương tự strcat()nhưng điều đó không thay đổi bất cứ điều gì:

#define MAX_STRING_LENGTH 1000
char *strcat_const(const char *str1,const char *str2){
    static char buffer[MAX_STRING_LENGTH];
    strncpy(buffer,str1,MAX_STRING_LENGTH);
    if(strlen(str1) < MAX_STRING_LENGTH){
        strncat(buffer,str2,MAX_STRING_LENGTH - strlen(buffer));
    }
    buffer[MAX_STRING_LENGTH - 1] = '\0';
    return buffer;
}

int main(int argc,char *argv[]){
    printf("%s",strcat_const("Hello ","world"));    //Prints "Hello world"
    return 0;
}

Nếu cả hai chuỗi cùng dài hơn 1000 ký tự, nó sẽ cắt chuỗi ở 1000 ký tự. Bạn có thể thay đổi giá trị MAX_STRING_LENGTHcho phù hợp với nhu cầu của bạn.


Tôi thấy trước tràn bộ đệm, tôi thấy bạn được phân bổ strlen(str1) + strlen(str2), nhưng bạn viết các strlen(str1) + strlen(str2) + 1ký tự. Vì vậy, bạn có thể thực sự viết chức năng của riêng bạn?
Liviu

Ồ Bạn không bao giờ giải phóng bộ nhớ, khó chịu, khó chịu! return buffer; free(buffer);
Liviu

BTW, sizeof(char) == 1(Bên cạnh đó, có những lỗi tinh vi khác ...) Bây giờ bạn có thể thấy tại sao bạn không phải viết chức năng của riêng mình không?
Liviu

@Liviu Tôi giải phóng bộ nhớ ở dòng free(buffer);.
Vịt Donald

1
free(buffer);Sau khi return buffer;không bao giờ được thực thi, hãy xem nó trong trình gỡ lỗi;) Tôi thấy ngay bây giờ: có, bạn phải giải phóng bộ nhớ trong mainchức năng
Liviu

1

Giả sử bạn có char [fixed_size] thay vì char *, bạn có thể sử dụng một macro, một macro sáng tạo để thực hiện tất cả cùng một lúc với một <<cout<<likeđơn đặt hàng ("thay vì% s là% s \ n", "than", "printf định dạng kiểu "). Nếu bạn đang làm việc với các hệ thống nhúng, phương pháp này cũng sẽ cho phép bạn loại bỏ malloc và nhóm *printfchức năng lớn như snprintf()(Điều này khiến cho dietlibc không phàn nàn về * printf nữa)

#include <unistd.h> //for the write example
//note: you should check if offset==sizeof(buf) after use
#define strcpyALL(buf, offset, ...) do{ \
    char *bp=(char*)(buf+offset); /*so we can add to the end of a string*/ \
    const char *s, \
    *a[] = { __VA_ARGS__,NULL}, \
    **ss=a; \
    while((s=*ss++)) \
         while((*s)&&(++offset<(int)sizeof(buf))) \
            *bp++=*s++; \
    if (offset!=sizeof(buf))*bp=0; \
}while(0)

char buf[256];
int len=0;

strcpyALL(buf,len,
    "The config file is in:\n\t",getenv("HOME"),"/.config/",argv[0],"/config.rc\n"
);
if (len<sizeof(buf))
    write(1,buf,len); //outputs our message to stdout
else
    write(2,"error\n",6);

//but we can keep adding on because we kept track of the length
//this allows printf-like buffering to minimize number of syscalls to write
//set len back to 0 if you don't want this behavior
strcpyALL(buf,len,"Thanks for using ",argv[0],"!\n");
if (len<sizeof(buf))
    write(1,buf,len); //outputs both messages
else
    write(2,"error\n",6);
  • Lưu ý 1, bạn thường không sử dụng argv [0] như thế này - chỉ là một ví dụ
  • Lưu ý 2, bạn có thể sử dụng bất kỳ hàm nào tạo ra char *, bao gồm các hàm không đạt tiêu chuẩn như itoa () để chuyển đổi số nguyên thành loại chuỗi.
  • Lưu ý 3, nếu bạn đã sử dụng printf ở bất cứ đâu trong chương trình của mình, không có lý do gì để không sử dụng snprintf (), vì mã được biên dịch sẽ lớn hơn (nhưng được nội tuyến và nhanh hơn đáng kể)

1
int main()
{
    char input[100];
    gets(input);

    char str[101];
    strcpy(str, " ");
    strcat(str, input);

    char *p = str;

    while(*p) {
       if(*p == ' ' && isalpha(*(p+1)) != 0)
           printf("%c",*(p+1));
       p++;
    }

    return 0;
}

1

Bạn đang cố gắng sao chép một chuỗi vào một địa chỉ được phân bổ tĩnh. Bạn cần phải mèo vào một bộ đệm.

Đặc biệt:

... bắn tỉa ...

Nơi Đến

Pointer to the destination array, which should contain a C string, and be large enough to contain the concatenated resulting string.

... bắn tỉa ...

http://www.cplusplus.com/reference/cl Library / copes / strcat.html

Có một ví dụ ở đây là tốt.


0

Đây là giải pháp của tôi

#include <stdlib.h>
#include <stdarg.h>

char *strconcat(int num_args, ...) {
    int strsize = 0;
    va_list ap;
    va_start(ap, num_args);
    for (int i = 0; i < num_args; i++) 
        strsize += strlen(va_arg(ap, char*));

    char *res = malloc(strsize+1);
    strsize = 0;
    va_start(ap, num_args);
    for (int i = 0; i < num_args; i++) {
        char *s = va_arg(ap, char*);
        strcpy(res+strsize, s);
        strsize += strlen(s);
    }
    va_end(ap);
    res[strsize] = '\0';

    return res;
}

nhưng bạn cần xác định có bao nhiêu chuỗi bạn sẽ nối

char *str = strconcat(3, "testing ", "this ", "thing");

0

Hãy thử một cái gì đó tương tự như thế này:

#include <stdio.h>
#include <string.h>

int main(int argc, const char * argv[])
{
  // Insert code here...
  char firstname[100], secondname[100];
  printf("Enter First Name: ");
  fgets(firstname, 100, stdin);
  printf("Enter Second Name: ");
  fgets(secondname,100,stdin);
  firstname[strlen(firstname)-1]= '\0';
  printf("fullname is %s %s", firstname, secondname);

  return 0;
}
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.