Tất cả về chuỗi trong c. Nhập và xuất chuỗi ký tự trong C

Không phải ngẫu nhiên mà tôi đặt chủ đề về chuỗi vào phần “Mảng”. Vì một chuỗi về cơ bản là một mảng các ký tự. Đây là một ví dụ:

char str = "Đây chỉ là một chuỗi";

Để hiểu rõ hơn, dòng tương tự có thể được viết như thế này:

char str = ("E","t","o"," ","p","r","o","s","t","o","","s", “t”, “r”, “o”, “k”, “a”);

Những thứ kia. vẫn là mảng đó, chỉ gồm các ký tự. Vì vậy, bạn có thể làm việc với nó, giống như với mảng số nguyên.

Bây giờ hãy thử làm việc với chuỗi trong c. Trong các bài học giới thiệu, chúng ta đã biết rằng các ký hiệu thuộc loại số nguyên, tức là. mỗi ký tự có giá trị số riêng. Đây là một ví dụ và giải pháp của nó:

  1. bạn cần chuyển từ đã nhập sang chữ hoa:
  2. #bao gồm
    #bao gồm

    Int chính()
    {
    char str = "sergey";

    str[i] -= 32;
    }
    for (int i=0; str[i] != "\0";i++)(
    printf("%c", str[i]);
    }
    getch();

    Trả về 0;
    }

    Để lấy mã của một số, chỉ cần sử dụng công cụ xác định %d trong hàm printf. Đúng, và một điểm quan trọng nữa: sự kết thúc của bất kỳ dòng là dấu kết thúc null, được biểu thị bằng ký tự đặc biệt - "\0".

Một cách khác để chỉ định một chuỗi là khai báo nó bằng char*. Đây là một ví dụ:

char *str = "dây";

Những thứ kia. một con trỏ tới một chuỗi được tạo và đặt ở đâu đó trong bộ nhớ.

Và đây là cách bạn có thể nhập chuỗi thông qua toán tử scanf, vốn đã quen thuộc với chúng ta:

char str; scanf("%s", str);

Có hai sự tinh tế ở đây:

  1. ở đây không cần lấy dấu địa chỉ vì tên của mảng, như chúng ta đã biết, là địa chỉ
  2. Độ dài của chuỗi đầu vào không được vượt quá 15 ký tự, vì chuỗi cuối cùng phải là dấu kết thúc null. Hơn nữa, chính trình biên dịch sẽ điền ký hiệu này sau ký hiệu được nhập cuối cùng của bạn.

Vì ngôn ngữ C là ngôn ngữ cấu trúc nên đã có sẵn các hàm dành cho làm việc với chuỗi và với các ký hiệu. Để xử lý chuỗi, bạn cần bao gồm tệp: ctype.h. Tệp này chứa các hàm để xác định định dạng chữ và ký tự. Về cơ bản, mọi thứ bạn cần biết về một ký tự đều có thể được thực hiện bằng cách sử dụng các hàm trong tệp ctype.h

Đôi khi bạn có thể cần chuyển đổi một chuỗi sang kiểu dữ liệu khác. Để chuyển đổi chuỗi sang các loại khác, có thư viện stdlib. Đây là chức năng của nó:

  1. int atoi (char *str)
  2. atol dài (char *str)
  3. atof kép (char *str)

Đôi khi các hàm này rất hữu ích, chẳng hạn như khi bạn cần trích xuất năm hoặc giá trị số từ một chuỗi. Làm việc với chuỗi trong c (si) là một chủ đề rất quan trọng, vì vậy hãy cố gắng hiểu bài học này.

Trong một chương trình, chuỗi có thể được định nghĩa như sau:

  • dưới dạng hằng chuỗi;
  • dưới dạng mảng ký tự;
  • thông qua một con trỏ tới một kiểu ký tự;
  • giống như mảng chuỗi.

Ngoài ra, phải cấp phát bộ nhớ để lưu trữ chuỗi.

Bất kỳ chuỗi ký tự nào nằm trong dấu ngoặc kép "" đều được coi là hằng chuỗi.

Để đầu ra chính xác, bất kỳ chuỗi nào cũng phải kết thúc bằng ký tự null "\0", giá trị nguyên của ký tự này là 0. Khi khai báo một hằng chuỗi, ký tự null sẽ tự động được thêm vào chuỗi đó. Do đó, một chuỗi ký tự là hằng số chuỗi sẽ được đặt trong RAM của máy tính, bao gồm cả byte 0.

Các ô RAM liên tiếp được phân bổ để lưu trữ chuỗi. Vì vậy, một chuỗi là một mảng các ký tự. 1 byte được phân bổ để lưu trữ mã của từng ký tự trong chuỗi.

Để đặt một số ký tự dịch vụ trong một hằng chuỗi, các tổ hợp ký tự được sử dụng. Vì vậy, nếu bạn cần bao gồm ký tự trích dẫn kép trong một chuỗi, thì trước ký tự đó phải có ký tự dấu gạch chéo ngược: ‘\”‘ .

Các hằng chuỗi được đặt trong bộ nhớ tĩnh. Địa chỉ bắt đầu của chuỗi ký tự trong dấu ngoặc kép được coi là địa chỉ của chuỗi. Các hằng chuỗi thường được sử dụng để cung cấp sự tương tác của người dùng trong các hàm như printf().

Khi xác định mảng ký tự bạn cần cho trình biên dịch biết kích thước bộ nhớ cần thiết.

quyến rũ;

Trình biên dịch cũng có thể xác định độc lập kích thước của mảng ký tự nếu việc khởi tạo mảng được chỉ định khi khai báo nó dưới dạng hằng chuỗi:

ký tự m2=;
char m3=( "T", "i", "x", "i", "e", ", "d", "o", "l", "i", "n", "s", ", "p ", "o", "l", "n", "y", ", "s", "v", "e", "zh", "e", "y", ", "m", "g","l","o","y","\0"};

Trong trường hợp này, tên m2 và m3 là con trỏ tới phần tử đầu tiên của mảng:

  • m2 tương đương với &m2
  • m2 tương đương với 'G'
  • m2 tương đương với 'o'
  • m3 tương đương với &m3
  • m3 tương đương với 'x'

Khi khai báo một mảng ký tự và khởi tạo nó bằng hằng chuỗi, bạn có thể chỉ định rõ ràng kích thước của mảng, nhưng kích thước mảng đã chỉ định phải lớn hơn kích thước của hằng chuỗi khởi tạo:

ký tự m2= "Những đỉnh núi ngủ trong bóng tối của màn đêm.";

Để đặt một chuỗi bạn có thể sử dụng con trỏ tới kiểu ký tự.

ký tự *m4;

Trong trường hợp này, khi khai báo một mảng, biến m4 có thể được gán địa chỉ của mảng:

m4 = m3;
*m4 tương đương với m3="T"
*(m4+1) tương đương với m3="and"

Ở đây m3 là một hằng số con trỏ. Bạn không thể thay đổi m3 , vì điều này có nghĩa là thay đổi vị trí (địa chỉ) của mảng trong bộ nhớ, không giống như m4 .

Đối với con trỏ, bạn có thể sử dụng thao tác tăng dần (di chuyển đến ký tự tiếp theo):

Mảng chuỗi ký tự

Đôi khi các chương trình cần mô tả mảng chuỗi ký tự. Trong trường hợp này, bạn có thể sử dụng chỉ mục hàng để truy cập vào nhiều hàng khác nhau.

char *nhà thơ = ( “Nhà thơ đã chết!”, “- nô lệ danh dự -”,
“Sụp đổ”, “bị tin đồn vu khống…”};

Trong trường hợp này, nhà thơ là một mảng bao gồm bốn con trỏ tới các chuỗi ký tự. Mỗi chuỗi ký tự là một mảng ký tự nên có bốn con trỏ mảng. Con trỏ nhà thơ đề cập đến dòng đầu tiên:
*nhà thơ tương đương "P",
*nhà thơ[l] tương đương "-" .

Việc khởi tạo được thực hiện theo các quy tắc được xác định cho mảng.
Văn bản được trích dẫn tương đương với việc khởi tạo từng chuỗi trong mảng. Dấu phẩy ngăn cách liền kề
trình tự.
Bạn cũng có thể đặt rõ ràng kích thước của chuỗi ký tự bằng cách sử dụng mô tả như thế này:

nhà thơ char;

Sự khác biệt là biểu mẫu này xác định một mảng "hình chữ nhật" trong đó tất cả các hàng có cùng độ dài.

Mảng miễn phí

Sự miêu tả

char *nhà thơ;


định nghĩa một mảng tự do, trong đó độ dài của mỗi dòng được xác định bởi con trỏ khởi tạo dòng này. Một mảng miễn phí không lãng phí bộ nhớ.

Hoạt động chuỗi

Hầu hết các phép toán C xử lý chuỗi đều hoạt động với con trỏ. Để đặt một chuỗi ký tự vào RAM, bạn phải:

  • phân bổ một khối RAM cho mảng;
  • khởi tạo chuỗi.

Để cấp phát bộ nhớ để lưu trữ một chuỗi, có thể sử dụng các hàm cấp phát bộ nhớ động. Trong trường hợp này, cần phải tính đến kích thước dòng yêu cầu:

tên nhân vật;
tên = (char *)malloc(10);
scanf("%9s", name);

Hàm scanf() được sử dụng để nhập một chuỗi và chuỗi đã nhập không được vượt quá 9 ký tự. Ký tự cuối cùng sẽ chứa "\0" .

Các hàm nhập chuỗi

Hàm scanf() có thể được sử dụng để nhập một chuỗi. Tuy nhiên, scanf() được thiết kế để truy xuất một từ chứ không phải một chuỗi. Nếu bạn sử dụng định dạng "%s" để nhập, dòng này sẽ được nhập trước (nhưng không bao gồm) ký tự trống tiếp theo, có thể là dấu cách, tab hoặc dòng mới.

Để nhập một chuỗi, bao gồm cả dấu cách, hãy sử dụng hàm

char * được(char *);


hoặc tương đương của nó

char * get_s(char *);

Một con trỏ tới chuỗi cần nhập sẽ được truyền dưới dạng đối số cho hàm. Hàm này yêu cầu người dùng nhập một chuỗi, chuỗi này sẽ được đặt vào một mảng cho đến khi người dùng nhấn Đi vào.

Hàm xuất chuỗi

Để xuất chuỗi, bạn có thể sử dụng hàm đã thảo luận trước đó

printf("%s" , str); // str - con trỏ tới chuỗi

hoặc ở dạng rút gọn

printf(str);

Hàm này cũng có thể được sử dụng để xuất chuỗi

int đặt (char *s);

in chuỗi s và di chuyển con trỏ sang một dòng mới (không giống như printf() ). Hàm put() cũng có thể được sử dụng để xuất ra các hằng chuỗi được đặt trong dấu ngoặc kép.

Chức năng nhập ký tự

Chức năng này có thể được sử dụng để nhập ký tự

char getchar();


trả về giá trị của ký tự được nhập từ bàn phím. Hàm này được sử dụng trong các ví dụ đã thảo luận trước đó để trì hoãn cửa sổ bảng điều khiển sau khi chương trình được thực thi cho đến khi một phím được nhấn.

Chức năng xuất ký tự

Chức năng này có thể được sử dụng để xuất ký tự

char putchar(char);


trả về giá trị của ký tự cần in và in ký tự được truyền dưới dạng đối số ra màn hình.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

#bao gồm
#bao gồm
#bao gồm
int chính() (
char s, sym;
số int, i;
hệ thống ("chcp 1251");
hệ thống("cls" );
printf( "Nhập chuỗi:");
được_s(s);
printf( "Nhập ký tự:");
sym = getchar();
đếm = 0;
vì (i = 0; s[i] != "\0" ; i++)
{
nếu (s[i] == sym)
đếm++;
}
printf("Trong dòng\n" );
đặt(các); // Xuất ra một chuỗi
printf("ký tự" );
putchar(sym); // Xuất ký hiệu
printf( "xảy ra %d lần", đếm);
getchar(); getchar();
trả về 0;
}

Kết quả thực hiện

Các chức năng chính của thư viện chuẩn string.h

Các chức năng chính của thư viện chuẩn string.h được đưa ra trong bảng.

Chức năng Sự miêu tả

char *strcat(char *s1, char *s2)

nối s2 vào s1, trả về s1

char *strncat(char *s1, char *s2, int n)

nối tối đa n ký tự s2 vào s1, kết thúc chuỗi bằng "\0", trả về s1

char *strсpy(char *s1, char *s2)

sao chép chuỗi s2 sang chuỗi s1, bao gồm cả "\0", trả về s1
);
strncpy(m3, m1, 6); // không thêm "\0" vào cuối dòng
đặt( "Kết quả strncpy(m3, m1, 6)");
đặt(m3);
strcpy(m3, m1);
đặt( "Kết quả strcpy(m3, m1)");
đặt(m3);
đặt( "Kết quả của strcmp(m3, m1) là");
printf("%d" , strcmp(m3, m1));
strncat(m3, m2, 5);
đặt( "Kết quả strncat(m3, m2, 5)");
đặt(m3);
strcat(m3, m2);
đặt( "Kết quả strcat(m3, m2)");
đặt(m3);
đặt( "Số ký tự trong chuỗi m1 là strlen(m1) : ");
printf("%d\n" , strlen(m1));
_strnset(m3, "f", 7);
đặt( "Kết quả strnset(m3, "f", 7)");
đặt(m3);
_strset(m3, "k" );
đặt( "Kết quả strnset(m3, "k")");
đặt(m3);
getchar();
trả về 0;
}

Kết quả thực hiện

thẻ: dòng C. Mảng Char.

Chuỗi trong C. Giới thiệu.

Đây là bài viết giới thiệu về chuỗi C. Một mô tả chi tiết hơn và các ví dụ sẽ xuất hiện khi chúng ta học cách làm việc với bộ nhớ và con trỏ. Trong máy tính, tất cả các giá trị được lưu trữ dưới dạng số. Và cả dòng nữa, không có ký hiệu hay chữ cái nào ở đó. Một thuật ngữ là một dãy số. Mỗi số tương ứng với một ký tự cụ thể được lấy từ bảng mã hóa. Khi hiển thị trên màn hình, biểu tượng được hiển thị theo một cách nhất định.
Mảng kiểu char được sử dụng để lưu trữ chuỗi. Tôi nhắc lại một lần nữa - kiểu char là số, nó lưu trữ một byte dữ liệu. Nhưng theo bảng mã hóa, mỗi số này đều gắn với một ký tự. Và theo hướng ngược lại - mỗi ký tự được xác định bởi số sê-ri của nó trong bảng mã hóa.
Ví dụ

#bao gồm #bao gồm void main() ( char c = "A"; int i = 65; printf("hiển thị dưới dạng char %c\n", c); printf("hiển thị dưới dạng int %d\n", c); printf(" hiển thị dưới dạng char %c\n", i); printf("hiển thị dưới dạng char %d\n", i); getch(); )

Chúng tôi đã tạo hai biến, một biến thuộc loại ký tự, khác int. Chữ "A" có giá trị số là 65. Nó là một chữ cái, không phải một chuỗi và do đó được bao quanh bởi các dấu ngoặc đơn. Chúng ta có thể in nó dưới dạng một lá thư

Printf("hiển thị dưới dạng char %c\n", c);

Sau đó nó sẽ được xuất ra
MỘT
Nếu bạn xuất nó dưới dạng số, nó sẽ là
65
Bạn có thể làm tương tự với số 65, số này được lưu trong một biến như int.
Ký tự đặc biệt cũng có số riêng

#bao gồm #bao gồm void main() ( printf("%c", "\a"); printf("%d", "\a"); printf("%c", 7); getch(); )

Ở đây, tín hiệu âm thanh sẽ được “đầu ra” trước, sau đó là giá trị số của nó, sau đó lại là tín hiệu âm thanh.
Một chuỗi trong C là một mảng kiểu ký tự, phần tử cuối cùng lưu ký tự đầu cuối "\0". Giá trị số của ký tự này là 0, vì vậy chúng ta có thể nói rằng mảng kết thúc bằng 0.
Ví dụ

#bao gồm #bao gồm void main() ( char word; word = "A"; word = "B"; word = "C"; word = "\0"; //word = 0; tương đương với printf("%s", word) ; getch(); )

Phím %s được sử dụng để xuất dữ liệu. Trong trường hợp này, dòng được in tới ký tự đầu cuối đầu tiên, vì hàm printf không biết kích thước của mảng từ.
Nếu trong ví dụ này bạn không đặt

Từ = "\0";

sau đó một chuỗi ký tự có độ dài tùy ý sẽ được xuất ra cho đến khi gặp byte đầu tiên chứa đầy số 0.

#bao gồm #bao gồm void main() ( char word = "ABC"; char text = ("H", "E", "L", "L", "O"); printf("%s\n", word); printf ("%s", văn bản); getch(); )

Trong trường hợp này, mọi thứ đều đúng. Chuỗi "ABC" kết thúc bằng 0 và chúng ta khởi tạo mảng từ với chuỗi đó. Chuỗi văn bản được khởi tạo từng chữ cái, tất cả các ký tự còn lại, như sau trong chương về mảng, được điền bằng số không.

Đọc dòng

Để yêu cầu một chuỗi từ người dùng, bạn cần tạo bộ đệm. Kích thước bộ đệm phải được chọn trước để từ đã nhập vừa với nó. Khi đọc dòng, có nguy cơ người dùng sẽ nhập nhiều dữ liệu hơn mức bộ đệm cho phép. Dữ liệu này sẽ được đọc và lưu vào bộ nhớ, đồng thời sẽ ghi đè lên giá trị của người khác. Bằng cách này, bạn có thể thực hiện một cuộc tấn công bằng cách ghi lại các byte cần thiết, chẳng hạn như đi tới phần mã có chương trình độc hại hoặc ghi dữ liệu.

#bao gồm #bao gồm void main() ( char buffer; scanf("%19s", buffer); printf("%s", buffer); getch(); )

Trong trường hợp này, số lượng ký tự được nhập được giới hạn ở 19 và kích thước bộ đệm lớn hơn 1 ký tự vì cần phải lưu trữ ký tự đầu cuối. Hãy viết một chương trình đơn giản yêu cầu người dùng nhập một chuỗi và trả về độ dài của chuỗi đó.

#bao gồm #bao gồm void main() ( char buffer; unsigned len = 0; scanf("%127s", buffer); while (buffer != "\0") ( len++; ) printf("length(%s) == %d" , đệm, len); getch(); )

Vì giá trị số của ký tự "\0" bằng 0 nên chúng ta có thể viết

Trong khi (buffer != 0) ( len++; )

Hoặc thậm chí ngắn hơn

Trong khi (bộ đệm) ( len++; )

Bây giờ hãy viết chương trình hỏi người dùng hai từ và so sánh chúng

#bao gồm #bao gồm /* Kết quả so sánh sẽ là số 0 nếu các từ bằng 1 nếu từ đầu tiên lớn hơn từ thứ hai theo thứ tự từ điển -1 nếu từ thứ hai lớn hơn */ void main() ( char firstWord; / /Từ đầu tiên char thứ haiWord; //Từ thứ hai không dấu i; / /Counter int cmpResult = 0; //Kết quả so sánh scanf("%127s", firstWord); scanf("%127s", twoWord); for (i = 0 ; Tôi< 128; i++) { if (firstWord[i] >SecondWord[i]) ( //Thậm chí hơn nữa nếu từ thứ hai đã kết thúc, bởi vì // khi đó nó kết thúc bằng 0 cmpResult = 1; break; ) else if (firstWord[i]< secondWord[i]) { cmpResult = -1; break; } } printf("%d", cmpResult); getch(); }

Vì mỗi chữ cái đều có một giá trị số nên chúng có thể được so sánh với nhau dưới dạng số. Ngoài ra, thông thường (nhưng không phải luôn luôn!) các chữ cái trong bảng mã hóa được sắp xếp theo thứ tự bảng chữ cái. Vì vậy, sắp xếp theo giá trị số cũng sẽ được sắp xếp theo thứ tự bảng chữ cái.

Habra, xin chào!

Cách đây không lâu, một sự việc khá thú vị đã xảy ra với tôi, trong đó có liên quan đến một giáo viên của một trường đại học khoa học máy tính.

Cuộc trò chuyện về lập trình Linux dần dần tiến triển đến chỗ người này cho rằng sự phức tạp của việc lập trình hệ thống thực sự đã bị phóng đại quá mức. Rằng ngôn ngữ C đơn giản như một trận đấu, trên thực tế, giống như nhân Linux (theo cách nói của ông).

Tôi mang theo một chiếc máy tính xách tay chạy Linux, trong đó có một bộ tiện ích dành cho quý ông để phát triển bằng ngôn ngữ C (gcc, vim, make, valgrind, gdb). Tôi không nhớ lúc đó chúng tôi đã đặt ra mục tiêu gì cho mình, nhưng sau vài phút, đối thủ của tôi đã thấy mình ở chiếc máy tính xách tay này, hoàn toàn sẵn sàng giải quyết vấn đề.

Và theo đúng nghĩa đen, ngay từ những dòng đầu tiên, anh ấy đã mắc một sai lầm nghiêm trọng khi cấp phát bộ nhớ cho… một dòng.

Char *str = (char *)malloc(sizeof(char) * strlen(buffer));
bộ đệm - một biến ngăn xếp để ghi dữ liệu từ bàn phím vào.

Tôi nghĩ chắc chắn sẽ có người hỏi: “Sao chuyện này có thể có vấn đề gì cơ chứ?”
Hãy tin tôi, nó có thể.

Và chính xác là gì - đọc về con mèo.

Một lý thuyết nhỏ - một loại LikBez.

Nếu bạn biết, hãy cuộn đến tiêu đề tiếp theo.

Chuỗi trong C là một mảng các ký tự, luôn kết thúc bằng "\0" - ký tự cuối dòng. Các chuỗi trên ngăn xếp (tĩnh) được khai báo như sau:

Char str[n] = ( 0 );
n là kích thước của mảng ký tự, bằng độ dài của chuỗi.

Bài tập ( 0 ) - "zeroing" chuỗi (tùy chọn, bạn có thể khai báo nó mà không cần nó). Kết quả giống như khi chạy các hàm memset(str, 0, sizeof(str)) và bzero(str, sizeof(str)). Nó được sử dụng để ngăn rác bị bỏ lại trong các biến chưa được khởi tạo.

Bạn cũng có thể khởi tạo ngay một chuỗi trên ngăn xếp:

Char buf = "văn bản đệm mặc định\n";
Ngoài ra, một chuỗi có thể được khai báo như một con trỏ và bộ nhớ có thể được cấp phát cho chuỗi đó trên heap:

Char *str = malloc(size);
kích thước - số byte mà chúng tôi phân bổ cho chuỗi. Các chuỗi như vậy được gọi là động (do kích thước yêu cầu được tính toán linh hoạt + kích thước bộ nhớ được phân bổ có thể tăng lên bất kỳ lúc nào bằng cách sử dụng hàm realloc()).

Trong trường hợp biến ngăn xếp, tôi sử dụng ký hiệu n để xác định kích thước của mảng; trong trường hợp biến heap, tôi sử dụng ký hiệu kích thước. Và điều này phản ánh hoàn hảo bản chất thực sự của sự khác biệt giữa khai báo trên ngăn xếp và khai báo cấp phát bộ nhớ trên heap, bởi vì n thường được sử dụng khi nói về số lượng phần tử. Và kích thước lại là một câu chuyện hoàn toàn khác...

Valgrind sẽ giúp chúng tôi

Trong bài viết trước tôi cũng đã đề cập đến nó. Valgrind ( , two - một cách thực hiện nhỏ) là một chương trình rất hữu ích giúp lập trình viên theo dõi rò rỉ bộ nhớ và lỗi ngữ cảnh - chính xác là những thứ xuất hiện thường xuyên nhất khi làm việc với chuỗi.

Hãy xem một danh sách ngắn triển khai một cái gì đó tương tự như chương trình tôi đã đề cập và chạy nó thông qua valgrind:

#bao gồm #bao gồm #bao gồm #define HELLO_STRING "Xin chào, Habr!\n" void main() ( char *str = malloc(sizeof(char) * strlen(HELLO_STRING)); strcpy(str, HELLO_STRING); printf("->\t%s" , str); tự do(str); )
Và trên thực tế, kết quả của chương trình:

$ gcc main.c $ ./a.out -> Xin chào, Habr!
Chưa có gì bất thường cả. Bây giờ hãy chạy chương trình này với valgrind!

$ valgrind --tool=memcheck ./a.out ==3892== Memcheck, trình phát hiện lỗi bộ nhớ ==3892== Bản quyền (C) 2002-2015 và GNU GPL"d, bởi Julian Seward và cộng sự == 3892== Sử dụng Valgrind-3.12.0 và LibVEX; chạy lại với -h để biết thông tin bản quyền ==3892== Lệnh: ./a.out ==3892== ==3892== Ghi kích thước 2 không hợp lệ ==3892= = tại 0x4005B4: main (in /home/indever/prg/C/public/a.out) ==3892== Địa chỉ 0x520004c là 12 byte bên trong một khối có kích thước 13 alloc"d ==3892== tại 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== ==3892== Kích thước đọc 1 không hợp lệ == 3892== tại 0x4C30BC4: strlen (vg_replace_strmem.c:454) ==3892== bởi 0x4E89AD0: vfprintf (trong /usr/lib64/libc-2.24.so) ==3892== bởi 0x4E90718: printf (trong /usr/ lib64/libc-2.24.so) ==3892== by 0x4005CF: main (in /home/indever/prg/C/public/a.out) ==3892== Địa chỉ 0x520004d là 0 byte sau một khối có kích thước 13 alloc"d ==3892== tại 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== -> Xin chào, Habr! ==3892== ==3892== TÓM TẮT HEAP: ==3892== đang sử dụng khi thoát: 0 byte trong 0 khối ==3892== tổng mức sử dụng heap: 2 cấp phát, 2 giải phóng, 1.037 byte được phân bổ ==3892= = ==3892== Tất cả các khối heap đã được giải phóng -- không có rò rỉ nào ==3892== ==3892== Để biết số lượng lỗi được phát hiện và ngăn chặn, hãy chạy lại với: -v ==3892== TÓM TẮT LỖI: 3 lỗi từ 2 bối cảnh (bị chặn: 0 từ 0)
==3892== Tất cả các khối heap đã được giải phóng - không thể xảy ra rò rỉ- không có rò rỉ nào cả, và đó là tin tốt. Nhưng đáng để bạn hạ tầm mắt xuống một chút (mặc dù tôi muốn lưu ý rằng đây chỉ là phần tóm tắt, thông tin chính ở chỗ khác một chút):

==3892== TÓM TẮT LỖI: 3 lỗi từ 2 ngữ cảnh (bị chặn: 0 từ 0)
3 sai lầm. Trong 2 bối cảnh. Trong một chương trình đơn giản như vậy. Làm sao!?

Vâng, rất đơn giản. Toàn bộ “điều buồn cười” là hàm strlen không tính đến ký tự cuối dòng - “\0”. Ngay cả khi bạn chỉ định rõ ràng nó trong dòng đến (#define HELLO_STRING “Xin chào, Habr!\n\0”), nó sẽ bị bỏ qua.

Ngay phía trên kết quả thực hiện chương trình, dòng -> Xin chào, Habr! có một báo cáo chi tiết về những gì và ở đâu mà valgrind quý giá của chúng tôi không thích. Tôi khuyên bạn nên tự mình nhìn vào những dòng này và rút ra kết luận của riêng mình.

Trên thực tế, phiên bản chính xác của chương trình sẽ trông như thế này:

#bao gồm #bao gồm #bao gồm #define HELLO_STRING "Xin chào, Habr!\n" void main() ( char *str = malloc(sizeof(char) * (strlen(HELLO_STRING) + 1)); strcpy(str, HELLO_STRING); printf("->\ t%s", str); miễn phí(str); )
Hãy chạy nó qua valgrind:

$ valgrind --tool=memcheck ./a.out -> Xin chào, Habr! ==3435== ==3435== TÓM TẮT HEAP: ==3435== đang sử dụng khi thoát: 0 byte trong 0 khối ==3435== tổng mức sử dụng heap: 2 cấp phát, 2 giải phóng, 1.038 byte được phân bổ ==3435= = ==3435== Tất cả các khối heap đã được giải phóng -- không có rò rỉ nào ==3435== ==3435== Để biết số lượng lỗi được phát hiện và ngăn chặn, hãy chạy lại với: -v ==3435== TÓM TẮT LỖI: 0 lỗi từ 0 bối cảnh (bị chặn: 0 từ 0)
Tuyệt vời. Không có lỗi, +1 byte bộ nhớ được phân bổ đã giúp giải quyết vấn đề.

Điều thú vị là trong hầu hết các trường hợp, cả chương trình thứ nhất và chương trình thứ hai sẽ hoạt động giống nhau, nhưng nếu bộ nhớ được phân bổ cho dòng mà ký tự kết thúc không vừa sẽ không bằng 0, thì hàm printf() khi xuất ra dòng đó , cũng sẽ xuất ra tất cả rác sau dòng này - mọi thứ sẽ được in cho đến khi ký tự kết thúc dòng cản trở printf().

Tuy nhiên, bạn biết đấy, (strlen(str) + 1) là một giải pháp như vậy. Chúng tôi gặp phải 2 vấn đề:

  1. Điều gì sẽ xảy ra nếu chúng ta cần phân bổ bộ nhớ cho một chuỗi được tạo bằng cách sử dụng s(n)printf(..) chẳng hạn? Chúng tôi không ủng hộ các lập luận.
  2. Vẻ bề ngoài. Dòng khai báo biến trông thật khủng khiếp. Một số người cũng có thể đính kèm (char *) vào malloc, như thể họ viết dưới dấu cộng. Trong một chương trình mà bạn thường xuyên cần xử lý chuỗi, việc tìm một giải pháp tinh tế hơn là điều hợp lý.
Hãy đưa ra một giải pháp có thể làm hài lòng cả chúng ta và valgrind.

snprintf()

int snprintf(char *str, size_t size, const char *format, ...);- một hàm - một phần mở rộng của sprintf, định dạng một chuỗi và ghi nó vào con trỏ được truyền làm đối số đầu tiên. Nó khác với sprintf() ở chỗ str sẽ không ghi một byte lớn hơn kích thước được chỉ định.

Hàm này có một tính năng thú vị - trong mọi trường hợp, nó trả về kích thước của chuỗi được tạo ra (không tính đến ký tự cuối dòng). Nếu chuỗi trống thì trả về 0.

Một trong những vấn đề tôi đã mô tả khi sử dụng strlen có liên quan đến các hàm sprintf() và snprintf(). Giả sử chúng ta cần viết gì đó vào chuỗi str. Dòng cuối cùng chứa giá trị của các biến khác. Mục nhập của chúng tôi sẽ giống như thế này:

Char * str = /* cấp phát bộ nhớ ở đây */; sprintf(str, "Xin chào, %s\n", "Habr!");
Câu hỏi đặt ra: làm thế nào để xác định lượng bộ nhớ cần phân bổ cho chuỗi str?

Char * str = malloc(sizeof(char) * (strlen(str, "Xin chào, %s\n", "Habr!") + 1)); - nó sẽ không làm việc. Nguyên mẫu hàm strlen() trông như thế này:

#bao gồm size_t strlen(const char *s);
const char *s không ngụ ý rằng chuỗi được truyền cho s có thể là chuỗi có định dạng biến đổi.

Thuộc tính hữu ích của hàm snprintf() mà tôi đã đề cập ở trên sẽ giúp chúng ta ở đây. Chúng ta hãy xem mã cho chương trình sau:

#bao gồm #bao gồm #bao gồm void main() ( /* Vì snprintf() không tính đến ký tự cuối dòng, nên chúng tôi thêm kích thước của nó vào kết quả */ size_t Need_mem = snprintf(NULL, 0, "Xin chào, %s!\n", "Habr") + sizeof("\0"); char *str = malloc( Need_mem); snprintf(str, Need_mem, "Xin chào, %s!\n", "Habr"); printf("->\t %s", str); tự do(str); )
Chạy chương trình trong valgrind:

$ valgrind --tool=memcheck ./a.out -> Xin chào, Habr! ==4132== ==4132== TÓM TẮT HEAP: ==4132== đang sử dụng khi thoát: 0 byte trong 0 khối ==4132== tổng mức sử dụng heap: 2 cấp phát, 2 giải phóng, 1.041 byte được phân bổ ==4132= = ==4132== Tất cả các khối heap đã được giải phóng -- không có rò rỉ nào ==4132== ==4132== Để biết số lượng lỗi được phát hiện và ngăn chặn, hãy chạy lại với: -v ==4132== TÓM TẮT LỖI: 0 lỗi từ 0 bối cảnh (bị chặn: 0 từ 0) $
Tuyệt vời. Chúng tôi có hỗ trợ lập luận. Do thực tế là chúng ta chuyển null làm đối số thứ hai cho hàm snprintf() nên việc ghi vào con trỏ null sẽ không bao giờ gây ra Seagfault. Tuy nhiên, bất chấp điều này, hàm vẫn sẽ trả về kích thước cần thiết cho chuỗi.

Nhưng mặt khác, chúng tôi phải đưa vào một biến số bổ sung, và thiết kế

Size_t Need_mem = snprintf(NULL, 0, "Xin chào, %s!\n", "Habr") + sizeof("\0");
trông thậm chí còn tệ hơn trong trường hợp strlen().

Nói chung, + sizeof("\0") có thể bị xóa nếu bạn chỉ định rõ ràng "\0" ở cuối dòng định dạng (size_t Need_mem = snprintf(NULL, 0, "Xin chào, %s!\n \0 ", "Habr");), nhưng điều này không phải lúc nào cũng có thể thực hiện được (tùy thuộc vào cơ chế xử lý chuỗi, chúng ta có thể phân bổ thêm một byte).

Chúng ta cần phải làm một vài thứ. Tôi suy nghĩ một chút và quyết định rằng bây giờ là lúc phải viện đến trí tuệ của người xưa. Hãy mô tả một hàm macro sẽ gọi snprintf() với con trỏ null làm đối số đầu tiên và null làm đối số thứ hai. Và đừng quên phần cuối của dòng!

#define strsize(args...) snprintf(NULL, 0, args) + sizeof("\0")
Đúng, điều này có thể là mới đối với một số người, nhưng macro C hỗ trợ số lượng đối số thay đổi và dấu ba chấm cho bộ tiền xử lý biết rằng đối số của hàm macro đã chỉ định (trong trường hợp của chúng tôi là đối số) tương ứng với một số đối số thực.

Hãy kiểm tra giải pháp của chúng tôi trong thực tế:

#bao gồm #bao gồm #bao gồm #define strsize(args...) snprintf(NULL, 0, args) + sizeof("\0") void main() ( char *str = malloc(strsize("Xin chào, %s\n", "Habr! ")); sprintf(str, "Xin chào, %s\n", "Habr!"); printf("->\t%s", str); free(str); )
Hãy bắt đầu với valgrund:

$ valgrind --tool=memcheck ./a.out -> Xin chào, Habr! ==6432== ==6432== TÓM TẮT HEAP: ==6432== đang sử dụng khi thoát: 0 byte trong 0 khối ==6432== tổng mức sử dụng heap: 2 cấp phát, 2 giải phóng, 1.041 byte được phân bổ ==6432= = ==6432== Tất cả các khối heap đã được giải phóng -- không có rò rỉ nào ==6432== ==6432== Để biết số lượng lỗi được phát hiện và ngăn chặn, hãy chạy lại với: -v ==6432== TÓM TẮT LỖI: 0 lỗi từ 0 bối cảnh (bị chặn: 0 từ 0)
Vâng, không có lỗi. Mọi thứ đều chính xác. Và valgrind rất vui, và lập trình viên cuối cùng cũng có thể đi ngủ.

Nhưng cuối cùng, tôi sẽ nói thêm một điều nữa. Trong trường hợp chúng ta cần cấp phát bộ nhớ cho bất kỳ chuỗi nào (ngay cả khi có đối số) thì đã có sẵn giải pháp hoàn toàn sẵn sàng làm việc.

Chúng ta đang nói về hàm asprintf:

#define _GNU_SOURCE /* Xem feature_test_macros(7) */ #include int asprintf(char **strp, const char *fmt, ...);
Nó lấy một con trỏ tới một chuỗi (**strp) làm đối số đầu tiên của nó và phân bổ bộ nhớ cho con trỏ không được tham chiếu.

Chương trình của chúng tôi được viết bằng asprintf() sẽ trông như thế này:

#bao gồm #bao gồm #bao gồm void main() ( char *str; asprintf(&str, "Xin chào, %s!\n", "Habr"); printf("->\t%s", str); free(str); )
Và trên thực tế, trong valgrind:

$ valgrind --tool=memcheck ./a.out -> Xin chào, Habr! ==6674== ==6674== TÓM TẮT HEAP: ==6674== đang sử dụng khi thoát: 0 byte trong 0 khối ==6674== tổng mức sử dụng heap: 3 cấp phát, 3 giải phóng, 1.138 byte được phân bổ ==6674= = ==6674== Tất cả các khối heap đã được giải phóng -- không có rò rỉ nào ==6674== ==6674== Để biết số lượng lỗi được phát hiện và ngăn chặn, hãy chạy lại với: -v ==6674== TÓM TẮT LỖI: 0 lỗi từ 0 bối cảnh (bị chặn: 0 từ 0)
Mọi thứ đều ổn, nhưng như bạn có thể thấy, nhiều bộ nhớ hơn đã được phân bổ và hiện có 3 cấp phát chứ không phải 2. Trên các hệ thống nhúng yếu, việc sử dụng chức năng này là không mong muốn.
Ngoài ra, nếu viết man asprintf trong console, chúng ta sẽ thấy:

TUÂN THỦ Các chức năng này là phần mở rộng GNU, không phải trong C hoặc POSIX. Chúng cũng có sẵn dưới dạng *BSD. Việc triển khai FreeBSD đặt strp thành NULL do lỗi.

Điều này cho thấy rõ rằng chức năng này chỉ có sẵn trong các nguồn GNU.

Phần kết luận

Tóm lại, tôi muốn nói rằng làm việc với chuỗi trong C là một chủ đề rất phức tạp và có nhiều sắc thái. Ví dụ: để viết mã “an toàn” khi cấp phát bộ nhớ động, bạn nên sử dụng hàm calloc() thay vì malloc() - calloc lấp đầy bộ nhớ được cấp phát bằng các số 0. Hoặc sau khi cấp phát bộ nhớ, hãy sử dụng hàm memset(). Nếu không, rác ban đầu nằm trong vùng bộ nhớ được cấp phát có thể gây ra sự cố trong quá trình gỡ lỗi và đôi khi khi làm việc với chuỗi.

Hơn một nửa số lập trình viên C mà tôi biết (hầu hết là người mới bắt đầu) đã giải quyết vấn đề cấp phát bộ nhớ cho chuỗi theo yêu cầu của tôi, đã làm theo cách mà cuối cùng dẫn đến lỗi ngữ cảnh. Trong một trường hợp - thậm chí là rò rỉ bộ nhớ (à, một người đã quên làm free(str), điều đó không bao giờ xảy ra với bất kỳ ai). Trên thực tế, điều này đã thôi thúc tôi tạo ra tác phẩm mà bạn vừa đọc.

Tôi hy vọng bài viết này sẽ hữu ích cho ai đó. Tại sao tôi lại làm ầm ĩ lên thế này - không có ngôn ngữ nào là đơn giản cả. Mọi nơi đều có sự tinh tế riêng. Và bạn càng biết nhiều sự tinh tế trong ngôn ngữ thì mã của bạn càng tốt.

Tôi tin rằng sau khi đọc bài viết này, mã của bạn sẽ tốt hơn một chút :)
Chúc may mắn, Habr!

Cập nhật lần cuối: 31/10/2015

Nối

Việc nối hoặc kết hợp chuỗi có thể được thực hiện bằng cách sử dụng toán tử + hoặc phương thức Concat:

Chuỗi s1 = "xin chào"; chuỗi s2 = "thế giới"; chuỗi s3 = s1 + " " + s2; // kết quả: string "hello world" string s4 = String.Concat(s3, "!!!"); // kết quả: chuỗi "hello world!!!" Console.WriteLine(s4);

Phương thức Concat là một phương thức tĩnh của lớp String lấy hai chuỗi làm tham số. Ngoài ra còn có các phiên bản khác của phương pháp này có số lượng tham số khác nhau.

Phương thức Join cũng có thể được sử dụng để nối các chuỗi:

Chuỗi s5 = "quả táo"; chuỗi s6 = "một ngày"; chuỗi s7 = "giữ"; chuỗi s8 = “bác sĩ”; chuỗi s9 = "đi"; giá trị chuỗi = chuỗi mới ( s5, s6, s7, s8, s9 ); Chuỗi s10 = String.Join(" ", giá trị); // kết quả: chuỗi "táo mỗi ngày giúp tránh xa bác sĩ"

Phương thức Tham gia cũng tĩnh. Phiên bản của phương thức được sử dụng ở trên có hai tham số: một chuỗi phân cách (trong trường hợp này là khoảng trắng) và một mảng các chuỗi sẽ được nối và phân tách bằng dấu phân cách.

So sánh chuỗi

Để so sánh các chuỗi, hãy sử dụng phương thức So sánh tĩnh:

Chuỗi s1 = "xin chào"; chuỗi s2 = "thế giới"; int result = String.Compare(s1, s2); nếu (kết quả<0) { Console.WriteLine("Строка s1 перед строкой s2"); } else if (result >0) ( Console.WriteLine("Dòng s1 đứng sau dòng s2"); ) else ( Console.WriteLine("Dòng s1 và s2 giống hệt nhau"); ) // kết quả sẽ là "Dòng s1 trước dòng s2"

Phiên bản này của phương thức So sánh có hai chuỗi và trả về một số. Nếu chuỗi đầu tiên cao hơn chuỗi thứ hai theo thứ tự bảng chữ cái thì sẽ trả về một số nhỏ hơn 0. Ngược lại, một số lớn hơn 0 sẽ được trả về. Và trường hợp thứ ba - nếu các chuỗi bằng nhau thì số 0 được trả về.

Trong trường hợp này, vì ký tự h cao hơn ký tự w theo bảng chữ cái nên dòng đầu tiên sẽ cao hơn.

Tìm kiếm trong một chuỗi

Sử dụng phương thức IndexOf, chúng ta có thể xác định chỉ mục xuất hiện đầu tiên của một ký tự hoặc chuỗi con trong chuỗi:

Chuỗi s1 = "xin chào thế giới"; char ch = "o"; int indexOfChar = s1.IndexOf(ch); // bằng 4 Console.WriteLine(indexOfChar); chuỗi subString = "làm việc"; int indexOfSubstring = s1.IndexOf(subString); // bằng 6 Console.WriteLine(indexOfSubstring);

Phương thức LastIndexOf hoạt động theo cách tương tự, ngoại trừ việc nó tìm chỉ mục xuất hiện cuối cùng của một ký tự hoặc chuỗi con trong chuỗi.

Một nhóm phương pháp khác cho phép bạn tìm hiểu xem một chuỗi bắt đầu hay kết thúc bằng một chuỗi con cụ thể. Các phương thức StartsWith và EndsWith được thiết kế cho việc này. Ví dụ: chúng tôi có nhiệm vụ xóa tất cả các tệp có phần mở rộng .exe khỏi một thư mục:

Đường dẫn chuỗi = @"C:\SomeDir"; file chuỗi = Directory.GetFiles(path); vì (int i = 0; tôi< files.Length; i++) { if(files[i].EndsWith(".exe")) File.Delete(files[i]); }

Tách chuỗi

Sử dụng hàm Split chúng ta có thể chia một chuỗi thành một mảng các chuỗi con. Hàm Split lấy một mảng ký tự hoặc chuỗi làm tham số, sẽ đóng vai trò là dấu phân cách. Ví dụ: hãy đếm số từ trong một thuật ngữ bằng cách chia nó cho các ký tự khoảng trắng:

String text = "Và đó là lý do tại sao mọi chuyện lại xảy ra"; chuỗi từ = text.Split(char mới ( " " )); foreach (chuỗi s trong từ) ( Console.WriteLine(s); )

Đây không phải là cách tốt nhất để phân chia theo khoảng trắng, vì chúng ta có thể có nhiều khoảng trắng liên tiếp trong chuỗi đầu vào và mảng kết quả cũng sẽ chứa khoảng trắng, vì vậy tốt hơn nên sử dụng phiên bản khác của phương thức:

Các từ trong chuỗi = text.Split(char mới ( " " ), StringSplitOptions.RemoveEmptyEntries);

Tham số thứ hai StringSplitOptions.RemoveEmptyEntries nói rằng tất cả các chuỗi con trống sẽ bị xóa.

Cắt một dòng

Để cắt bớt ký tự đầu hoặc cuối, hãy sử dụng hàm Trim:

Chuỗi văn bản = "xin chào thế giới"; văn bản = văn bản.Trim(); // kết quả "hello world" text = text.Trim(char mới ("d", "h" )); // kết quả "ello world"

Hàm Trim, không có tham số, sẽ cắt các khoảng trắng ở đầu và cuối rồi trả về chuỗi đã được cắt bớt. Để chỉ định rõ ràng những ký tự đầu và cuối nào cần được cắt bớt, chúng ta có thể truyền một mảng các ký tự đó vào hàm.

Hàm này có một phần tương tự: hàm TrimStart cắt bớt các ký tự bắt đầu và hàm TrimEnd cắt bớt các ký tự kết thúc.

Hàm Substring cho phép bạn cắt bớt một phần cụ thể của chuỗi:

String text = "Chúc một ngày tốt lành"; // cắt bỏ bắt đầu từ ký tự thứ ba text = text.Substring(2); // kết quả "chúc một ngày tốt lành" Console.WriteLine(text); // cắt đầu tiên đến hai ký tự cuối cùng text = text.Substring(0, text.Length - 2); // kết quả "roshy de" Console.WriteLine(text);

Hàm Substring cũng trả về chuỗi bị cắt bớt. Phiên bản đầu tiên được sử dụng sử dụng chỉ mục làm tham số, bắt đầu từ đó chuỗi sẽ được cắt bớt. Phiên bản thứ hai sử dụng hai tham số - chỉ số bắt đầu cắt và độ dài của phần cắt của chuỗi.

Chèn

Để chèn một hàng vào một hàng khác, hãy sử dụng hàm Insert:

String text = "Chúc một ngày tốt lành"; chuỗi subString = "tuyệt vời"; văn bản = text.Insert(8, subString); Console.WriteLine(văn bản);

Tham số đầu tiên trong hàm Insert là chỉ mục mà chuỗi con sẽ được chèn vào và tham số thứ hai là chính chuỗi con đó.

Xóa hàng

Phương thức Remove giúp loại bỏ một phần của chuỗi:

String text = "Chúc một ngày tốt lành"; // chỉ mục của ký tự cuối cùng int ind = text.Length - 1; // cắt bỏ ký tự cuối cùng text = text.Remove(ind); Console.WriteLine(văn bản); // cắt bỏ 2 ký tự đầu tiên text = text.Remove(0, 2);

Phiên bản đầu tiên của phương thức Remove lấy một chỉ mục trong chuỗi, bắt đầu từ đó tất cả các ký tự sẽ bị xóa. Phiên bản thứ hai có thêm một tham số - cần xóa bao nhiêu ký tự.

thay thế

Để thay thế một ký tự hoặc chuỗi con bằng một ký tự hoặc chuỗi con khác, hãy sử dụng phương thức Thay thế:

Chuỗi văn bản = "chúc một ngày tốt lành"; text = text.Replace("tốt", "xấu"); Console.WriteLine(văn bản); text = text.Replace("o", ""); Console.WriteLine(văn bản);

Trong trường hợp thứ hai khi sử dụng hàm Thay thế, một chuỗi gồm một ký tự "o" được thay thế bằng một chuỗi trống, nghĩa là nó thực sự bị xóa khỏi văn bản. Phương pháp này giúp dễ dàng xóa văn bản cụ thể trong dòng.

Trường hợp thay đổi

Để chuyển đổi một chuỗi thành chữ hoa và chữ thường, hãy sử dụng hàm ToUpper() và ToLower() tương ứng:

Chuỗi xin chào = "Xin chào thế giới!"; Console.WriteLine(hello.ToLower()); // Chào thế giới! Console.WriteLine(hello.ToUpper()); // CHÀO THẾ GIỚI!