Công cụ sửa đổi kiểu dữ liệu trong C. Các kiểu dữ liệu và khai báo của chúng

Ngoài việc chia dữ liệu thành các biến và hằng còn có sự phân loại dữ liệu theo loại. Việc mô tả các biến chủ yếu bao gồm việc khai báo kiểu của chúng. Kiểu dữ liệu đặc trưng cho phạm vi giá trị của nó và hình thức biểu diễn trong bộ nhớ máy tính. Mỗi loại được đặc trưng bởi một tập hợp các hoạt động được thực hiện trên dữ liệu. Theo truyền thống, các ngôn ngữ lập trình đa năng có các kiểu tiêu chuẩn như số nguyên, số thực, ký tự và logic 3. Chúng ta hãy lưu ý ngay rằng không có loại logic trong C. Một biểu thức (trong trường hợp cụ thể là một biến) được coi là đúng nếu nó khác 0, nếu không thì nó được coi là sai.

Sự tồn tại của hai kiểu số (số nguyên và số thực) gắn liền với hai dạng biểu diễn số có thể có trong bộ nhớ máy tính.

Dữ liệu toàn bộ loạiđược lưu trữ dưới dạng trình bày điểm cố định. Nó được đặc trưng bởi độ chính xác tuyệt đối trong việc biểu diễn các số và thực hiện các phép tính trên chúng, cũng như một phạm vi giới hạn của các giá trị số. Kiểu số nguyên được sử dụng cho dữ liệu về nguyên tắc không thể có phần phân số (số người, ô tô, v.v., số và bộ đếm).

Nhập thực tế tương ứng với dạng biểu diễn số dấu phẩy động, được đặc trưng bởi sự biểu diễn gần đúng của một số có số chữ số có nghĩa nhất định (dấu hiệu mantissa) và một phạm vi lớn về thứ tự của số, giúp có thể biểu thị cả số rất lớn và rất nhỏ ở giá trị tuyệt đối. Do sự biểu diễn gần đúng của dữ liệu kiểu thực, chúng so sánh bằng nhau là sai.

Trong các triển khai hiện đại của các ngôn ngữ lập trình phổ quát, thường có một số số nguyên và một số loại thực, mỗi loại được đặc trưng bởi kích thước bộ nhớ được phân bổ cho một giá trị và theo đó, phạm vi giá trị số và cho các loại thực - và độ chính xác của nó (số chữ số của lớp phủ).

Dữ liệu loại ký tự lấy các giá trị trên toàn bộ bộ ký tự được phép cho một máy tính nhất định. Một byte được phân bổ để lưu trữ một giá trị ký tự; các ký tự được mã hóa theo bảng mã hóa tiêu chuẩn (thường là ASCII).

Có 4 loại cơ bản trong C:

ký tự- loại ký tự;

int- cả một loại,

trôi nổi- loại thực có độ chính xác duy nhất,

gấp đôi- loại thực có độ chính xác gấp đôi.

Để xác định các loại dẫn xuất, hãy sử dụng vòng loại:ngắn(ngắn) - dùng với loại int,dài(dài) - dùng với các loại intgấp đôi;đã ký(có dấu hiệu), chưa ký(không dấu) - áp dụng cho mọi loại số nguyên. Trong trường hợp không có từ không dấu, giá trị được coi là đã ký, tức là. tức là mặc định đã được ký. Do khả năng được chấp nhận của sự kết hợp tùy ý giữa các từ hạn định và tên của các loại cơ bản, một loại có thể có nhiều tên gọi. Thông tin về các loại C tiêu chuẩn được trình bày trong Bảng 1 và 2. Các mô tả từ đồng nghĩa được liệt kê trong các ô của cột đầu tiên, cách nhau bằng dấu phẩy.

Bảng 1. Các kiểu dữ liệu số nguyên C tiêu chuẩn

Loại dữ liệu

Phạm vi giá trị

char, char đã ký

int không dấu, không dấu

int, int đã ký, int ngắn, ngắn

2147483648...2147483647

Điều thú vị là trong C, kiểu char có thể được sử dụng làm ký tự hoặc kiểu số nguyên, tùy thuộc vào ngữ cảnh.

Bảng 2. Các kiểu dữ liệu thực C chuẩn

Bình luận.Để viết chương trình cho phần đầu tiên của sách hướng dẫn, chúng ta chủ yếu cần hai loại:trôi nổiint.

Khái niệm cơ bản về ngôn ngữ

Mã chương trình và dữ liệu mà chương trình thao tác được ghi vào bộ nhớ máy tính dưới dạng một chuỗi bit. Chút là phần tử nhỏ nhất của bộ nhớ máy tính có khả năng lưu trữ 0 hoặc 1. Ở cấp độ vật lý, điều này tương ứng với điện áp, như chúng ta biết, có hoặc không có. Nhìn vào nội dung bộ nhớ của máy tính, chúng ta sẽ thấy có nội dung như sau:
...

Rất khó để hiểu được trình tự như vậy, nhưng đôi khi chúng ta cần thao tác với dữ liệu phi cấu trúc đó (thường thì nhu cầu này nảy sinh khi lập trình trình điều khiển thiết bị phần cứng). C++ cung cấp một tập hợp các thao tác để làm việc với dữ liệu bit. (Chúng ta sẽ nói về điều này trong Chương 4.)
Thông thường, một số cấu trúc được áp đặt trên một chuỗi bit, nhóm các bit thành bytetừ. Một byte chứa 8 bit và một từ chứa 4 byte hoặc 32 bit. Tuy nhiên, định nghĩa của từ này có thể khác nhau trên các hệ điều hành khác nhau. Quá trình chuyển đổi sang hệ thống 64 bit hiện đang bắt đầu và gần đây các hệ thống có từ 16 bit đã trở nên phổ biến. Mặc dù phần lớn các hệ thống có cùng kích thước byte, chúng tôi vẫn gọi những đại lượng này là dành riêng cho máy.

Bây giờ chúng ta có thể nói về một byte ở địa chỉ 1040 hoặc một từ ở địa chỉ 1024 và nói rằng một byte ở địa chỉ 1032 không bằng một byte ở địa chỉ 1040.
Tuy nhiên, chúng ta không biết bất kỳ byte nào, bất kỳ từ máy nào đại diện cho cái gì. Làm thế nào để hiểu ý nghĩa của 8 bit nhất định? Để diễn giải duy nhất ý nghĩa của byte (hoặc từ hoặc tập hợp bit khác) này, chúng ta phải biết loại dữ liệu mà byte đó biểu thị.
C++ cung cấp một tập hợp các kiểu dữ liệu có sẵn: ký tự, số nguyên, số thực - và một tập hợp các kiểu tổng hợp và mở rộng: chuỗi, mảng, số phức. Ngoài ra, đối với các hành động có dữ liệu này, còn có một tập hợp các phép toán cơ bản: so sánh, số học và các phép toán khác. Ngoài ra còn có các toán tử chuyển tiếp, các vòng lặp và các toán tử có điều kiện. Những thành phần này của ngôn ngữ C++ tạo nên tập hợp các viên gạch mà từ đó bạn có thể xây dựng một hệ thống có độ phức tạp bất kỳ. Bước đầu tiên để thành thạo C++ là nghiên cứu các thành phần cơ bản được liệt kê, đó là nội dung mà Phần II của cuốn sách này sẽ đề cập đến.
Chương 3 cung cấp cái nhìn tổng quan về các kiểu tích hợp và mở rộng cũng như các cơ chế mà bạn có thể tạo các kiểu mới. Tất nhiên, đây chủ yếu là cơ chế lớp được giới thiệu trong Phần 2.3. Chương 4 đề cập đến các biểu thức, các thao tác tích hợp và mức độ ưu tiên của chúng cũng như chuyển đổi kiểu. Chương 5 nói về hướng dẫn ngôn ngữ. Cuối cùng, Chương 6 giới thiệu Thư viện chuẩn C++ và các loại vectơ và mảng kết hợp vùng chứa.

3. Các kiểu dữ liệu C++

Chương này cung cấp một cái nhìn tổng quan được xây dựng trong, hoặc tiểu học, kiểu dữ liệu của ngôn ngữ C++. Nó bắt đầu với một định nghĩa chữ, chẳng hạn như 3,14159 hoặc pi, sau đó khái niệm này được giới thiệu Biến đổi, hoặc sự vật, phải thuộc về một trong các kiểu dữ liệu. Phần còn lại của chương được dành để mô tả chi tiết từng loại tích hợp. Ngoài ra, các kiểu dữ liệu dẫn xuất cho chuỗi và mảng do Thư viện chuẩn C++ cung cấp cũng được cung cấp. Mặc dù những loại này không phải là cơ bản nhưng chúng rất quan trọng để viết chương trình C++ thực sự và chúng tôi muốn giới thiệu với người đọc về chúng càng sớm càng tốt. Chúng ta sẽ gọi những kiểu dữ liệu này sự bành trướng các loại C++ cơ bản.

3.1. chữ

C++ có một tập hợp các kiểu dữ liệu dựng sẵn để biểu diễn số nguyên và số thực, ký tự, cũng như kiểu dữ liệu “mảng ký tự”, được sử dụng để lưu trữ các chuỗi ký tự. Kiểu char được sử dụng để lưu trữ các ký tự riêng lẻ và số nguyên nhỏ. Nó chiếm một byte máy. Các kiểu short, int và long được thiết kế để biểu diễn số nguyên. Các loại này chỉ khác nhau ở phạm vi giá trị mà các số có thể nhận và kích thước cụ thể của các loại được liệt kê phụ thuộc vào việc triển khai. Thông thường, short chiếm nửa từ máy, int lấy một từ, long chiếm một hoặc hai từ. Trên hệ thống 32 bit, int và long thường có cùng kích thước.

Các kiểu float, double và long double được dành cho các số có dấu phẩy động và khác nhau về độ chính xác biểu diễn (số chữ số có nghĩa) và phạm vi của chúng. Thông thường, float (độ chính xác đơn) lấy một từ máy, double (độ chính xác kép) lấy hai từ và double dài (độ chính xác mở rộng) lấy ba từ.
char, short, int và long cùng nhau tạo nên toàn bộ các loại, đến lượt nó, có thể là mang tính biểu tượng(đã ký) và chưa ký(chưa ký). Trong các loại có dấu, bit ngoài cùng bên trái lưu dấu (0 là cộng, 1 là trừ) và các bit còn lại chứa giá trị. Trong các loại không dấu, tất cả các bit được sử dụng cho giá trị. Loại char có dấu 8 bit có thể biểu thị các giá trị từ -128 đến 127 và loại char không dấu có thể biểu thị các giá trị từ 0 đến 255.

Khi gặp một số nhất định, ví dụ 1, trong chương trình, số này được gọi là theo nghĩa đen, hoặc hằng số theo nghĩa đen. Một hằng số vì chúng ta không thể thay đổi giá trị của nó và một hằng số vì giá trị của nó xuất hiện trong văn bản chương trình. Một chữ là một giá trị không thể định địa chỉ: mặc dù tất nhiên nó thực sự được lưu trữ trong bộ nhớ của máy nhưng không có cách nào để biết địa chỉ của nó. Mỗi nghĩa đen có một loại cụ thể. Vì vậy, 0 là kiểu int, 3.14159 là kiểu double.

Các chữ kiểu số nguyên có thể được viết dưới dạng thập phân, bát phân và thập lục phân. Đây là hình thức của số 20 được biểu thị bằng chữ số thập phân, bát phân và thập lục phân:

20 // số thập phân
024 // bát phân
0x14 // thập lục phân

Nếu một chữ bắt đầu bằng 0, nó được coi là số bát phân, nếu bằng 0x hoặc 0X thì là thập lục phân. Ký hiệu thông thường được coi là số thập phân.
Theo mặc định, tất cả các chữ số nguyên đều có kiểu signature int. Bạn có thể xác định rõ ràng toàn bộ chữ là loại dài bằng cách thêm chữ L vào cuối số (cả chữ L viết hoa và chữ l thường đều được sử dụng, nhưng để dễ đọc, bạn không nên sử dụng chữ thường: nó có thể dễ bị nhầm lẫn với

1). Chữ cái U (hoặc u) ở cuối xác định chữ cái là kiểu int không dấu và hai chữ cái - UL hoặc LU - là kiểu dài không dấu. Ví dụ:

128u 1024UL 1L 8Lu

Chữ đại diện cho số thực có thể được viết bằng dấu thập phân hoặc ký hiệu khoa học (khoa học). Theo mặc định chúng có kiểu double. Để chỉ rõ loại float, bạn cần sử dụng hậu tố F hoặc f và đối với long double - L hoặc l, nhưng chỉ khi được viết bằng dấu thập phân. Ví dụ:

3.14159F 0/1f 12.345L 0,0 3el 1,0E-3E 2. 1,0L

Các từ đúng và sai là các chữ bool.
Các hằng ký tự chữ có thể biểu diễn được viết dưới dạng ký tự trong dấu ngoặc đơn. Ví dụ:

"a" "2" "," " " (dấu cách)

Các ký tự đặc biệt (tab, xuống dòng) được viết dưới dạng chuỗi thoát. Các chuỗi như vậy sau đây được xác định (chúng bắt đầu bằng ký tự dấu gạch chéo ngược):

Dòng mới \n tab ngang \t backspace \b tab dọc \v xuống dòng \r nạp giấy \f cuộc gọi \a dấu gạch chéo ngược \\ câu hỏi \? trích dẫn đơn \" trích dẫn kép \"

Chuỗi thoát chung có dạng \ooo, trong đó ooo là một đến ba chữ số bát phân. Số này là mã ký tự. Sử dụng mã ASCII, chúng ta có thể viết các chữ sau:

\7 (đổ chuông) \14 (dòng mới) \0 (null) \062 ("2")

Một ký tự chữ có thể được bắt đầu bằng L (ví dụ: L"a"), có nghĩa là loại đặc biệt wchar_t là loại ký tự hai byte được sử dụng để lưu trữ các ký tự trong bảng chữ cái quốc gia nếu chúng không thể được biểu thị bằng loại char thông thường , chẳng hạn như chữ cái Trung Quốc hoặc tiếng Nhật.
Chuỗi ký tự là một chuỗi ký tự được đặt trong dấu ngoặc kép. Một chữ như vậy có thể kéo dài nhiều dòng; trong trường hợp này, dấu gạch chéo ngược được đặt ở cuối dòng. Các ký tự đặc biệt có thể được biểu diễn bằng chuỗi thoát riêng của chúng. Dưới đây là ví dụ về chuỗi ký tự:

"" (chuỗi trống) "a" "\nCC\toptions\tfile.\n" "một chuỗi ký tự nhiều dòng \ báo hiệu sự tiếp tục \ của nó bằng dấu gạch chéo ngược"

Trên thực tế, một chuỗi ký tự là một mảng các hằng ký tự, trong đó, theo quy ước của ngôn ngữ C và C++, phần tử cuối cùng luôn là một ký tự đặc biệt có mã 0 (\0).
Chữ "A" chỉ định một ký tự đơn A và chuỗi ký tự "A" chỉ định một mảng gồm hai phần tử: "A" và \0 (ký tự trống).
Vì có một loại wchar_t nên cũng có các chữ thuộc loại này, được biểu thị, như trong trường hợp các ký tự riêng lẻ, bằng tiền tố L:

L "một chuỗi chữ rộng"

Một chuỗi ký tự kiểu wchar_t là một mảng các ký tự cùng loại được kết thúc bằng null.
Nếu hai hoặc nhiều chuỗi ký tự (chẳng hạn như char hoặc wchar_t) xuất hiện liên tiếp trong quá trình kiểm tra chương trình, trình biên dịch sẽ nối chúng thành một chuỗi. Ví dụ: văn bản sau

"hai" "vài"

sẽ tạo ra một mảng gồm tám ký tự - twosome và một ký tự null kết thúc. Kết quả của việc nối các chuỗi thuộc các loại khác nhau là không xác định. Nếu bạn viết:

// đây không phải là ý hay đâu "hai" L"some"

Trên một máy tính, kết quả sẽ là một chuỗi có ý nghĩa nào đó, nhưng trên máy tính khác, nó có thể là một chuỗi hoàn toàn khác. Các chương trình sử dụng các tính năng triển khai của một trình biên dịch hoặc hệ điều hành cụ thể không thể mang theo được. Chúng tôi đặc biệt không khuyến khích việc sử dụng các cấu trúc như vậy.

Bài tập 3.1

Giải thích sự khác biệt trong định nghĩa của các nghĩa đen sau:

(a) "a", L"a", "a", L"a" (b) 10, 10u, 10L, 10uL, 012, 0*C (c) 3.14, 3.14f, 3.14L

Bài tập 3.2

Những lỗi nào mắc phải trong các ví dụ dưới đây?

(a) "Ai đi với F\144rgus?\014" (b) 3.14e1L (c) "hai" L"some" (d) 1024f (e) 3.14UL (f) "nhận xét nhiều dòng"

3.2. Biến

Hãy tưởng tượng rằng chúng ta đang giải bài toán nâng 2 lên lũy thừa 10. Chúng ta viết:

#bao gồm
int chính() (
// giải pháp đầu tiên
cout<< "2 raised to the power of 10: ";
cout<< 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2;
cout<< endl;
trả về 0;
}

Vấn đề đã được giải quyết, mặc dù chúng tôi phải liên tục kiểm tra xem chữ 2 có thực sự được lặp lại 10 lần hay không. Chúng tôi đã không mắc lỗi khi viết chuỗi dài gồm hai số này và chương trình đã tạo ra kết quả chính xác - 1024.
Nhưng bây giờ chúng tôi được yêu cầu tăng 2 lên lũy thừa thứ 17, rồi lên lũy thừa thứ 23. Việc sửa đổi văn bản chương trình mỗi lần là vô cùng bất tiện! Và tệ hơn nữa, rất dễ mắc lỗi khi viết thêm hai hoặc bỏ sót... Nhưng nếu bạn cần in một bảng lũy ​​thừa của hai từ 0 đến 15 thì sao? Lặp lại hai dòng có dạng tổng quát 16 lần:

cout<< "2 в степени X\t"; cout << 2 * ... * 2;

trong đó X liên tiếp tăng thêm 1 và số chữ cần thiết được thay thế cho số thập phân?

Vâng, chúng tôi đã hoàn thành nhiệm vụ. Khách hàng khó có thể đi sâu vào chi tiết và hài lòng với kết quả thu được. Trong cuộc sống thực, cách tiếp cận này thường hiệu quả; hơn nữa, nó hợp lý: vấn đề được giải quyết một cách không hề dễ dàng, nhưng trong khung thời gian mong muốn. Việc tìm kiếm một lựa chọn đẹp hơn và hiệu quả hơn có thể trở thành một sự lãng phí thời gian không thực tế.

Trong trường hợp này, “phương pháp vũ phu” cho câu trả lời đúng, nhưng thật khó chịu và nhàm chán biết bao khi giải quyết vấn đề theo cách này! Chúng ta biết chính xác những bước cần thực hiện, nhưng bản thân những bước này rất đơn giản và đơn điệu.

Theo quy luật, việc sử dụng các cơ chế phức tạp hơn cho cùng một nhiệm vụ sẽ làm tăng đáng kể thời gian của giai đoạn chuẩn bị. Ngoài ra, các cơ chế càng phức tạp được sử dụng thì khả năng xảy ra lỗi càng cao. Nhưng ngay cả khi không thể tránh khỏi những sai lầm và những bước đi sai lầm, việc sử dụng “công nghệ cao” vẫn có thể mang lại lợi ích về tốc độ phát triển, chưa kể đến việc những công nghệ này mở rộng đáng kể khả năng của chúng ta. Và – thật thú vị! – bản thân quá trình ra quyết định có thể trở nên hấp dẫn.
Hãy quay lại ví dụ của chúng tôi và cố gắng “cải thiện công nghệ” việc triển khai nó. Chúng ta có thể sử dụng một đối tượng được đặt tên để lưu trữ giá trị lũy thừa mà chúng ta muốn nâng số của mình lên. Ngoài ra, thay vì lặp lại một dãy chữ, có thể sử dụng toán tử vòng lặp. Nó sẽ trông như thế này:

#bao gồm
int chính()
{
// đối tượng kiểu int
giá trị int = 2;
int pow = 10;
cout<< value << " в степени "
<< pow << ": \t";
int res = 1;
// toán tử vòng lặp:
// lặp lại phép tính res
// cho đến khi cnt lớn hơn pow
cho (int cnt=1; cnt<= pow; ++cnt)
res = res * giá trị;
cout<< res << endl;
}

value, pow, res và cnt là các biến cho phép bạn lưu trữ, sửa đổi và truy xuất các giá trị. Câu lệnh vòng lặp for lặp lại số lần thực hiện của dòng kết quả.
Không còn nghi ngờ gì nữa, chúng tôi đã tạo ra một chương trình linh hoạt hơn nhiều. Tuy nhiên, đây vẫn không phải là một chức năng. Để có được một hàm thực có thể được sử dụng trong bất kỳ chương trình nào để tính lũy thừa của một số, bạn cần chọn phần chung của phép tính và đặt các giá trị cụ thể bằng các tham số.

Int pow(int val, int exp) ( for (int res = 1; exp > 0; --exp) res = res * val; return res; )

Bây giờ sẽ không khó để có được bất kỳ lũy thừa nào của con số mong muốn. Đây là cách thực hiện nhiệm vụ cuối cùng của chúng tôi - in bảng lũy ​​thừa hai từ 0 đến 15:

#bao gồm int bên ngoài pow(int,int); int main() ( int val = 2; int exp = 15;
cout<< "Степени 2\n";
cho (int cnt=0; cnt<= exp; ++cnt)
cout<< cnt << ": "
<< pow(val, cnt) << endl;
trả về 0;
}

Tất nhiên, hàm pow() của chúng ta vẫn chưa đủ tổng quát và chưa đủ mạnh. Nó không thể hoạt động với số thực, nó tăng số lên lũy thừa âm một cách không chính xác - nó luôn trả về 1. Kết quả của việc nâng một số lớn lên lũy thừa cao hơn có thể không khớp với biến int và sau đó một số giá trị không chính xác ngẫu nhiên sẽ được trả về. Bạn có thấy việc viết các hàm được thiết kế để sử dụng rộng rãi khó khăn như thế nào không? Khó hơn nhiều so với việc thực hiện một thuật toán cụ thể nhằm giải quyết một vấn đề cụ thể.

3.2.1. Biến là gì

Biến đổi, hoặc một đối tượng– đây là vùng bộ nhớ được đặt tên mà chúng ta có quyền truy cập từ chương trình; Bạn có thể đặt các giá trị ở đó và sau đó lấy chúng. Mỗi biến C++ có một loại cụ thể, đặc trưng cho kích thước và vị trí của vị trí bộ nhớ đó, phạm vi giá trị mà nó có thể lưu trữ và tập hợp các thao tác có thể áp dụng cho biến đó. Dưới đây là một ví dụ về việc xác định năm đối tượng thuộc các loại khác nhau:

Int sinh viên_count; lương gấp đôi; bool on_loan; strins street_address; dấu phân cách char;

Một biến, giống như một chữ, có một kiểu cụ thể và lưu trữ giá trị của nó trong một vùng bộ nhớ nào đó. Khả năng định địa chỉ- đó là điều mà nghĩa đen còn thiếu. Có hai đại lượng gắn liền với một biến:

  • giá trị thực hoặc giá trị r (từ giá trị đọc - giá trị để đọc), được lưu trữ trong vùng bộ nhớ này và vốn có trong cả biến và chữ;
  • giá trị địa chỉ của vùng bộ nhớ được liên kết với biến hoặc giá trị l (từ giá trị vị trí) - nơi lưu trữ giá trị r; vốn chỉ có ở đối tượng.

Trong biểu hiện

Ch = ch - "0";

biến ch nằm ở cả bên trái và bên phải của ký hiệu gán. Bên phải là giá trị cần đọc (ch và ký tự chữ "0"): dữ liệu liên quan đến biến được đọc từ vùng bộ nhớ tương ứng. Bên trái là giá trị vị trí: kết quả của phép trừ được đặt trong vùng bộ nhớ liên kết với biến ch. Nói chung, toán hạng bên trái của thao tác gán phải là giá trị l. Chúng ta không thể viết các biểu thức sau:

// lỗi biên dịch: các giá trị bên trái không phải là giá trị l // lỗi: chữ không phải là giá trị l 0 = 1; // lỗi: biểu thức số học không phải là giá trị l lương + lương * 0,10 = new_salary;

Câu lệnh định nghĩa biến phân bổ bộ nhớ cho nó. Vì một đối tượng chỉ có một vị trí bộ nhớ được liên kết với nó nên câu lệnh như vậy chỉ có thể xuất hiện một lần trong chương trình. Nếu một biến được xác định trong một tệp nguồn phải được sử dụng trong một tệp nguồn khác thì sẽ phát sinh vấn đề. Ví dụ:

// file module0.C // định nghĩa một đối tượng fileName string fileName; // ... gán tên tệp một giá trị
// tập tin module1.C
// sử dụng đối tượng fileName
// than ôi, không biên dịch được:
// fileName không được xác định trong module1.C
ifstream input_file(fileName);

C++ yêu cầu một đối tượng phải được biết trước khi nó được truy cập lần đầu tiên. Điều này là cần thiết để đảm bảo rằng đối tượng được sử dụng đúng theo loại của nó. Trong ví dụ của chúng tôi, module1.C sẽ gây ra lỗi biên dịch vì biến fileName không được xác định trong đó. Để tránh lỗi này, chúng ta phải thông báo cho trình biên dịch về biến fileName đã được xác định. Điều này được thực hiện bằng cách sử dụng câu lệnh khai báo biến:

// file module1.C // sử dụng đối tượng fileName // fileName được khai báo, tức là chương trình nhận được
// thông tin về đối tượng này mà không có định nghĩa phụ
Tên tệp chuỗi bên ngoài; ifstream input_file(fileName)

Một khai báo biến cho trình biên dịch biết rằng một đối tượng có tên đã cho, thuộc loại đã cho, được xác định ở đâu đó trong chương trình. Bộ nhớ không được cấp phát cho một biến khi nó được khai báo. (Từ khóa extern được đề cập trong Phần 8.2.)
Một chương trình có thể chứa bao nhiêu khai báo của cùng một biến tùy thích, nhưng nó chỉ có thể được định nghĩa một lần. Thật thuận tiện khi đặt những khai báo như vậy trong các tệp tiêu đề, bao gồm chúng trong các mô-đun yêu cầu nó. Bằng cách này, chúng ta có thể lưu trữ thông tin về các đối tượng ở một nơi và dễ dàng sửa đổi nếu cần. (Chúng ta sẽ nói nhiều hơn về các tệp tiêu đề trong Phần 8.2.)

3.2.2. Tên biến

Tên biến, hoặc định danh, có thể bao gồm các chữ cái Latinh, số và ký tự gạch dưới. Chữ hoa và chữ thường trong tên là khác nhau. Ngôn ngữ C++ không giới hạn độ dài của mã định danh, nhưng việc sử dụng những tên quá dài như gosh_this_is_an_impossible_name_to_type thì bất tiện.
Một số từ là từ khóa trong C++ và không thể được sử dụng làm định danh; Bảng 3.1 cung cấp danh sách đầy đủ về chúng.

Bảng 3.1. Từ khóa C++

asm tự động bool phá vỡ trường hợp
nắm lấy ký tự lớp học hằng số const_cast
Tiếp tục mặc định xóa bỏ LÀM gấp đôi
động_cast khác liệt kê rõ ràng xuất khẩu
bên ngoài SAI trôi nổi người bạn
đi đến nếu như nội tuyến int dài
có thể thay đổi không gian tên mới nhà điều hành riêng tư
được bảo vệ công cộng đăng ký diễn giải lại_cast trở lại
ngắn đã ký kích thước của tĩnh tĩnh_cast
cấu trúc công tắc bản mẫu cái này ném
typedef ĐÚNG VẬY thử kiểu chữ tên loại
liên hiệp liên minh vô hiệu sử dụng ảo trống rỗng

Để làm cho chương trình của bạn dễ hiểu hơn, chúng tôi khuyên bạn nên tuân theo các quy ước đặt tên đối tượng được chấp nhận rộng rãi:

  • tên biến thường được viết bằng chữ thường, ví dụ chỉ mục (để so sánh: Chỉ mục là tên của loại và INDEX là hằng số được xác định bằng chỉ thị tiền xử lý #define);
  • mã định danh phải có ý nghĩa nào đó, giải thích mục đích của đối tượng trong chương trình, ví dụ: ngày sinh hoặc lương;

Nếu tên như vậy bao gồm một số từ, chẳng hạn như ngày sinh, thì theo thông lệ, người ta thường phân tách các từ bằng dấu gạch dưới (ngày_sinh) hoặc viết mỗi từ tiếp theo bằng chữ in hoa (Ngày sinh). Người ta nhận thấy rằng các lập trình viên quen với Cách tiếp cận hướng đối tượng thích đánh dấu các từ bằng chữ in hoa, trong khi đó_who_have_writing_a lot_in_C sử dụng ký tự gạch dưới. Phương pháp nào trong hai phương pháp tốt hơn là vấn đề về khẩu vị.

3.2.3. Định nghĩa đối tượng

Trong trường hợp đơn giản nhất, toán tử định nghĩa đối tượng bao gồm công cụ xác định kiểutên của môn học và kết thúc bằng dấu chấm phẩy. Ví dụ:

Lương gấp đôi lương gấp đôi; int tháng; int ngày; int năm; đường dài không dấu;

Bạn có thể định nghĩa nhiều đối tượng cùng loại trong một câu lệnh. Trong trường hợp này, tên của họ được liệt kê cách nhau bằng dấu phẩy:

Lương gấp đôi, tiền công; int tháng, ngày, năm; đường dài không dấu;

Việc xác định đơn giản một biến sẽ không mang lại cho nó một giá trị ban đầu. Nếu một đối tượng được định nghĩa là toàn cục, đặc tả C++ đảm bảo rằng nó sẽ được khởi tạo về 0. Nếu biến là cục bộ hoặc được phân bổ động (sử dụng toán tử mới), giá trị ban đầu của nó không được xác định, nghĩa là nó có thể chứa một số giá trị ngẫu nhiên.
Việc sử dụng các biến như vậy là một lỗi rất phổ biến và cũng khó phát hiện. Nên chỉ định rõ ràng giá trị ban đầu của một đối tượng, ít nhất là trong trường hợp không biết liệu đối tượng có thể tự khởi tạo hay không. Cơ chế lớp giới thiệu khái niệm về hàm tạo mặc định, được sử dụng để gán các giá trị mặc định. (Chúng ta đã nói về điều này trong Phần 2.3. Chúng ta sẽ nói về các hàm tạo mặc định sau, trong Phần 3.11 và 3.15, nơi chúng ta sẽ xem xét chuỗi và các lớp phức tạp từ thư viện chuẩn.)

Int main() ( // đối tượng cục bộ chưa được khởi tạo int ival;
// đối tượng kiểu chuỗi được khởi tạo
// nhà xây dựng mặc định
dự án chuỗi;
// ...
}

Giá trị ban đầu có thể được chỉ định trực tiếp trong câu lệnh định nghĩa biến. Trong C++, hai hình thức khởi tạo biến được cho phép - rõ ràng, sử dụng toán tử gán:

Int ival = 1024; dự án chuỗi = "Fantasia 2000";

và ẩn, với giá trị ban đầu được chỉ định trong ngoặc đơn:

Int ival(1024); dự án chuỗi ("Fantasia 2000");

Cả hai tùy chọn đều tương đương nhau và đặt giá trị ban đầu cho biến số nguyên ival thành 1024 và cho chuỗi dự án thành "Fantasia 2000".
Khởi tạo rõ ràng cũng có thể được sử dụng khi xác định các biến trong danh sách:

Lương gấp đôi = 9999,99, lương = lương + 0,01; int tháng = 08; ngày = 07, năm = 1955;

Biến sẽ hiển thị (và hợp lệ trong chương trình) ngay sau khi được xác định, vì vậy chúng ta có thể khởi tạo biến lương bằng tổng của biến lương mới được xác định với một hằng số nào đó. Vì vậy, định nghĩa là:

// đúng, nhưng vô nghĩa int kỳ quái = kỳ quái;

có giá trị về mặt cú pháp, mặc dù vô nghĩa.
Các kiểu dữ liệu tích hợp có cú pháp đặc biệt để chỉ định giá trị null:

// ival nhận giá trị 0 và dval nhận 0.0 int ival = int(); đôi dval = double();

Trong định nghĩa sau:

// int() được áp dụng cho mỗi phần tử trong số 10 phần tử vector< int >ivec(10);

Mỗi phần tử trong số mười phần tử của vectơ được khởi tạo bằng int(). (Chúng ta đã nói về lớp vectơ trong Phần 2.8. Để biết thêm về điều này, hãy xem Phần 3.10 và Chương 6.)
Biến có thể được khởi tạo với một biểu thức có độ phức tạp bất kỳ, bao gồm cả các lệnh gọi hàm. Ví dụ:

#bao gồm #bao gồm
giá gấp đôi = 109,99, chiết khấu = 0,16;
sale_price(giá * chiết khấu);
chuỗi pet("nếp nhăn"); int bên ngoài get_value(); int val = get_value();
unsigned abs_val = abs(val);

abs() là hàm tiêu chuẩn trả về giá trị tuyệt đối của tham số.
get_value() là một số hàm do người dùng định nghĩa trả về một giá trị nguyên.

Bài tập 3.3

Định nghĩa biến nào sau đây có lỗi cú pháp?

(a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival(int()); (d) lương gấp đôi = lương = 9999,99; (e) cin >> int input_value;

Bài tập 3.4

Giải thích sự khác biệt giữa giá trị l và giá trị r. Cho ví dụ.

Bài tập 3.5

Tìm sự khác biệt trong cách sử dụng tên và biến sinh viên ở dòng đầu tiên và dòng thứ hai của mỗi ví dụ:

(a) tên chuỗi bên ngoài; tên chuỗi("bài tập 3.5a"); (b) vectơ bên ngoài sinh viên; vectơ sinh viên;

Bài tập 3.6

Tên đối tượng nào không hợp lệ trong C++? Thay đổi chúng sao cho đúng về mặt cú pháp:

(a) int double = 3,14159; (b) vectơ< int >_; (c) không gian tên chuỗi; (d) chuỗi bắt-22; (e) char 1_or_2 = "1"; (f) float Float = 3,14f;

Bài tập 3.7

Sự khác biệt giữa các định nghĩa biến toàn cục và cục bộ sau đây là gì?

Chuỗi Global_class; int toàn cầu_int; int chính() (
int local_int;
chuỗi local_class; // ...
}

3.3. Biển chỉ dẫn

Con trỏ và cấp phát bộ nhớ động đã được giới thiệu ngắn gọn ở Phần 2.2. Con trỏ là một đối tượng chứa địa chỉ của đối tượng khác và cho phép thao tác gián tiếp đối tượng này. Thông thường, con trỏ được sử dụng để thao tác với các đối tượng được tạo động, để xây dựng các cấu trúc dữ liệu liên quan như danh sách liên kết và cây phân cấp cũng như để truyền các đối tượng lớn—mảng và đối tượng lớp—làm tham số cho hàm.
Mỗi con trỏ được liên kết với một kiểu dữ liệu nhất định và cách biểu diễn bên trong của chúng không phụ thuộc vào kiểu bên trong: cả kích thước bộ nhớ bị chiếm bởi một đối tượng thuộc loại con trỏ và phạm vi giá trị đều giống nhau. Sự khác biệt là cách trình biên dịch xử lý đối tượng được đánh địa chỉ. Con trỏ tới các loại khác nhau có thể có cùng giá trị, nhưng vùng bộ nhớ nơi chứa các loại tương ứng có thể khác nhau:

  • một con trỏ tới int chứa giá trị địa chỉ 1000 được hướng đến vùng bộ nhớ 1000-1003 (trên hệ thống 32 bit);
  • một con trỏ double chứa giá trị địa chỉ 1000 được hướng tới vùng bộ nhớ 1000-1007 (trên hệ thống 32 bit).

Dưới đây là một số ví dụ:

Int *ip1, *ip2; tổ hợp *cp; chuỗi *chuỗi; vectơ *pvec; gấp đôi *dp;

Chỉ mục được biểu thị bằng dấu hoa thị trước tên. Khi xác định các biến bằng danh sách, dấu hoa thị phải đứng trước mỗi con trỏ (xem ở trên: ip1 và ip2). Trong ví dụ bên dưới, lp là con trỏ tới một đối tượng dài và lp2 là một đối tượng dài:

*lp dài, lp2;

Trong trường hợp sau, fp được hiểu là một đối tượng float và fp2 là một con trỏ tới nó:

Nổi fp, *fp2;

Toán tử quy định (*) có thể được phân tách bằng dấu cách khỏi tên và thậm chí liền kề trực tiếp với từ khóa loại. Do đó, các định nghĩa trên đều đúng về mặt cú pháp và hoàn toàn tương đương:

//chú ý: ps2 không phải là con trỏ tới một chuỗi! chuỗi* ps, ps2;

Có thể giả định rằng cả ps và ps2 đều là con trỏ, mặc dù con trỏ chỉ là con trỏ đầu tiên trong số chúng.
Nếu giá trị con trỏ là 0 thì nó không chứa bất kỳ địa chỉ đối tượng nào.
Đặt một biến kiểu int được chỉ định:

Int ival = 1024;

Sau đây là ví dụ về cách xác định và sử dụng con trỏ tới int pi và pi2:

//pi được khởi tạo ở địa chỉ 0 int *pi = 0;
// pi2 được khởi tạo với địa chỉ ival
int *pi2 =
// đúng: pi và pi2 chứa địa chỉ ival
pi = pi2;
// pi2 chứa địa chỉ 0
pi2 = 0;

Một con trỏ không thể được gán một giá trị không phải là địa chỉ:

// lỗi: pi không thể nhận giá trị int pi = ival

Theo cách tương tự, bạn không thể gán một giá trị cho một con trỏ thuộc một loại nhưng lại là địa chỉ của một đối tượng thuộc loại khác. Nếu các biến sau được xác định:

dval đôi; gấp đôi *ps =

thì cả hai biểu thức gán bên dưới sẽ gây ra lỗi biên dịch:

// lỗi biên dịch // gán kiểu dữ liệu không hợp lệ: int*<== double* pi = pd pi = &dval;

Không phải là biến pi không thể chứa địa chỉ của đối tượng dval - địa chỉ của các đối tượng thuộc các loại khác nhau có cùng độ dài. Các hoạt động trộn địa chỉ như vậy bị cố tình cấm vì việc giải thích các đối tượng của trình biên dịch phụ thuộc vào loại con trỏ tới chúng.
Tất nhiên, có những trường hợp chúng ta quan tâm đến giá trị của chính địa chỉ chứ không phải đối tượng mà nó trỏ tới (giả sử chúng ta muốn so sánh địa chỉ này với một số địa chỉ khác). Để giải quyết những tình huống như vậy, một con trỏ void đặc biệt đã được giới thiệu, con trỏ này có thể trỏ đến bất kỳ loại dữ liệu nào và các biểu thức sau sẽ đúng:

// đúng: void* có thể chứa // địa chỉ thuộc bất kỳ loại nào void *pv = pi; pv = pd;

Loại đối tượng được trỏ bởi void* không xác định và chúng ta không thể thao tác đối tượng này. Tất cả những gì chúng ta có thể làm với một con trỏ như vậy là gán giá trị của nó cho một con trỏ khác hoặc so sánh nó với một giá trị địa chỉ nào đó. (Chúng ta sẽ nói nhiều hơn về con trỏ void trong Phần 4.14.)
Để truy cập một đối tượng được cung cấp địa chỉ của nó, bạn cần sử dụng thao tác hội thảo hoặc đánh địa chỉ gián tiếp, được biểu thị bằng dấu hoa thị (*). Có các định nghĩa biến sau:

Int ival = 1024;, ival2 = 2048; int *pi =

// gán gián tiếp biến ival cho giá trị ival2 *pi = ival2;
// sử dụng gián tiếp biến ival làm rvalue và lvalue
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;

Khi chúng ta áp dụng toán tử địa chỉ (&) cho một đối tượng kiểu int, chúng ta nhận được kết quả kiểu int*
int *pi =
Nếu chúng ta áp dụng thao tác tương tự cho một đối tượng kiểu int* (con trỏ tới int), chúng ta sẽ nhận được một con trỏ tới một con trỏ tới int, tức là. int**. int** là địa chỉ của một đối tượng chứa địa chỉ của đối tượng kiểu int. Bằng cách hủy tham chiếu ppi, chúng ta nhận được một đối tượng kiểu int* chứa địa chỉ ival. Để có được chính đối tượng ival, thao tác dereference trên ppi phải được áp dụng hai lần.

Int **ppi = π int *pi2 = *ppi;
cout<< "Значение ival\n" << "явное значение: " << ival << "\n"
<< "косвенная адресация: " << *pi << "\n"
<< "дважды косвенная адресация: " << **ppi << "\n"

Con trỏ có thể được sử dụng trong các biểu thức số học. Hãy chú ý ví dụ sau, trong đó hai biểu thức thực hiện những việc hoàn toàn khác nhau:

Int i, j, k; int *pi = // i = i + 2
*pi = *pi + 2; // tăng địa chỉ chứa trong pi lên 2
pi = pi + 2;

Bạn có thể thêm một giá trị số nguyên vào một con trỏ hoặc bạn có thể trừ nó. Thêm 1 vào một con trỏ sẽ làm tăng giá trị mà nó chứa theo kích thước của bộ nhớ được phân bổ cho một đối tượng thuộc loại tương ứng. Nếu char là 1 byte, int là 4 byte và double là 8 thì việc thêm 2 vào các con trỏ char, int và double sẽ tăng giá trị của chúng lên lần lượt là 2, 8 và 16. Nếu các đối tượng cùng loại được đặt lần lượt trong bộ nhớ thì việc tăng con trỏ lên 1 sẽ khiến nó trỏ đến đối tượng tiếp theo. Vì vậy, số học con trỏ thường được sử dụng nhiều nhất khi xử lý mảng; trong bất kỳ trường hợp nào khác, chúng hầu như không được biện minh.
Đây là một ví dụ điển hình về việc sử dụng số học địa chỉ khi lặp qua các phần tử của một mảng bằng trình vòng lặp:

Int ia; int *iter = int *iter_end =
trong khi (iter != iter_end) (
do_something_with_value (*iter);
++it;
}

Bài tập 3.8

Định nghĩa của các biến được đưa ra:

Int ival = 1024, ival2 = 2048; int *pi1 = &ival, *pi2 = &ival2, **pi3 = 0;

Điều gì xảy ra khi bạn thực hiện các thao tác gán sau? Có sai lầm nào trong những ví dụ này không?

(a) ival = *pi3; (e) pi1 = *pi3; (b) *pi2 = *pi3; (f) ival = *pi1; (c) ival = pi2; (g) pi1 = ival; (d) pi2 = *pi1; (h)pi3 =

Bài tập 3.9

Làm việc với con trỏ là một trong những khía cạnh quan trọng nhất của C và C++, nhưng rất dễ mắc lỗi. Ví dụ, mã

Pi = pi = pi + 1024;

gần như chắc chắn sẽ khiến pi trỏ đến một vị trí bộ nhớ ngẫu nhiên. Toán tử gán này làm gì và khi nào nó không gây ra lỗi?

Bài tập 3.10

Chương trình này có lỗi liên quan đến việc sử dụng con trỏ không chính xác:

Int foobar(int *pi) ( *pi = 1024; return *pi; )
int chính() (
int *pi2 = 0;
int ival = foobar(pi2);
trả về 0;
}

Lỗi là gì? Làm thế nào tôi có thể sửa chữa nó?

Bài tập 3.11

Các lỗi từ hai bài tập trước sẽ bộc lộ và dẫn đến hậu quả nghiêm trọng do C++ thiếu kiểm tra tính chính xác của các giá trị con trỏ trong quá trình thực thi chương trình. Tại sao bạn nghĩ rằng việc kiểm tra như vậy không được thực hiện? Bạn có thể đưa ra một số hướng dẫn chung để làm việc với con trỏ an toàn hơn không?

3.4. Các loại chuỗi

C++ hỗ trợ hai loại chuỗi - một loại tích hợp được kế thừa từ C và lớp chuỗi từ thư viện chuẩn C++. Lớp chuỗi cung cấp nhiều khả năng hơn và do đó thuận tiện hơn khi sử dụng, tuy nhiên, trong thực tế thường có những tình huống cần sử dụng loại có sẵn hoặc hiểu rõ về cách thức hoạt động của nó. (Một ví dụ là phân tích các tham số dòng lệnh được truyền cho hàm main(). Chúng ta sẽ đề cập đến vấn đề này trong Chương 7.)

3.4.1. Kiểu chuỗi tích hợp

Như đã đề cập, kiểu chuỗi tích hợp có nguồn gốc từ C++, kế thừa từ C. Chuỗi ký tự được lưu trữ trong bộ nhớ dưới dạng một mảng và được truy cập bằng con trỏ char*. Thư viện chuẩn C cung cấp một tập hợp các hàm để thao tác với chuỗi. Ví dụ:

// trả về độ dài của chuỗi int strlen(const char*);
// so sánh hai chuỗi
int strcmp(const char*, const char*);
// sao chép dòng này sang dòng khác
char* strcpy(char*, const char*);

Thư viện chuẩn C là một phần của thư viện C++. Để sử dụng nó, chúng ta phải bao gồm tệp tiêu đề:

#bao gồm

Con trỏ char mà chúng ta sử dụng để truy cập một chuỗi, trỏ đến mảng ký tự tương ứng với chuỗi đó. Ngay cả khi chúng ta viết một chuỗi ký tự như

Const char *st = "Giá một chai rượu\n";

trình biên dịch đặt tất cả các ký tự của chuỗi vào một mảng và sau đó gán st địa chỉ của phần tử đầu tiên của mảng. Làm cách nào bạn có thể làm việc với một chuỗi bằng cách sử dụng một con trỏ như vậy?
Thông thường, số học địa chỉ được sử dụng để lặp lại các ký tự trong một chuỗi. Vì chuỗi luôn kết thúc bằng ký tự rỗng nên bạn có thể tăng con trỏ lên 1 cho đến khi ký tự tiếp theo trở thành ký tự rỗng. Ví dụ:

Trong khi (*st++) ( ... )

st bị hủy đăng ký và giá trị kết quả được kiểm tra xem nó có đúng không. Bất kỳ giá trị nào khác 0 đều được coi là đúng và do đó vòng lặp kết thúc khi đạt đến ký tự có mã 0. Phép toán tăng ++ thêm 1 vào con trỏ st và do đó chuyển nó sang ký tự tiếp theo.
Đây là cách triển khai hàm trả về độ dài của chuỗi. Lưu ý rằng vì một con trỏ có thể chứa giá trị null (không trỏ tới bất cứ thứ gì), nên nó cần được kiểm tra trước thao tác dereference:

Int string_length(const char *st) ( int cnt = 0; if (st) while (*st++) ++cnt; return cnt; )

Một chuỗi dựng sẵn có thể được coi là trống trong hai trường hợp: nếu con trỏ tới chuỗi có giá trị null (trong trường hợp đó chúng ta không có chuỗi nào cả) hoặc nếu nó trỏ đến một mảng bao gồm một ký tự null (nghĩa là , thành một chuỗi không chứa bất kỳ ký tự quan trọng nào).

// pc1 không xử lý bất kỳ mảng ký tự nào char *pc1 = 0; // pc2 đánh địa chỉ ký tự null const char *pc2 = "";

Đối với một lập trình viên mới làm quen, việc sử dụng các chuỗi kiểu tích hợp sẽ gặp nhiều lỗi do mức độ triển khai quá thấp và không thể thực hiện được nếu không có số học địa chỉ. Dưới đây chúng tôi sẽ chỉ ra một số lỗi phổ biến của người mới bắt đầu. Nhiệm vụ rất đơn giản: tính độ dài của chuỗi. Phiên bản đầu tiên không chính xác. Sửa chữa cô ấy.

#bao gồm const char *st = "Giá một chai rượu\n"; int chính() (
int len ​​= 0;
while (st++) ++len; cout<< len << ": " << st;
trả về 0;
}

Trong phiên bản này, con trỏ st không bị hủy đăng ký. Do đó, không phải ký tự được chỉ tới bởi st được kiểm tra bằng 0 mà là chính con trỏ. Vì con trỏ này ban đầu có giá trị khác 0 (địa chỉ của chuỗi), nên nó sẽ không bao giờ trở thành 0 và vòng lặp sẽ chạy vô tận.
Trong phiên bản thứ hai của chương trình, lỗi này đã được loại bỏ. Chương trình kết thúc thành công nhưng kết quả đầu ra không chính xác. Lần này chúng ta đang sai ở đâu?

#bao gồm const char *st = "Giá một chai rượu\n"; int chính()
{
int len ​​= 0;
while (*st++) ++len; cout<< len << ": " << st << endl;
trả về 0;
}

Lỗi là sau khi vòng lặp kết thúc, con trỏ st không xử lý ký tự gốc của ký tự gốc mà là ký tự nằm trong bộ nhớ sau ký tự null kết thúc của ký tự đó. Bất cứ thứ gì cũng có thể ở nơi này và đầu ra của chương trình sẽ là một chuỗi ký tự ngẫu nhiên.
Bạn có thể thử sửa lỗi này:

St = st – len; cout<< len << ": " << st;

Bây giờ chương trình của chúng ta tạo ra thứ gì đó có ý nghĩa nhưng không hoàn toàn. Câu trả lời trông như thế này:

18: giá chai rượu

Chúng tôi đã quên tính đến việc ký tự null ở cuối không được bao gồm trong độ dài được tính toán. st phải được bù bằng độ dài của chuỗi cộng 1. Cuối cùng, đây là toán tử đúng:

St = st – len - 1;

và đây là kết quả đúng:

18: Giá một chai rượu

Tuy nhiên, chúng tôi không thể nói rằng chương trình của chúng tôi trông tao nhã. Nhà điều hành

St = st – len - 1;

được thêm vào để sửa lỗi mắc phải ở giai đoạn đầu của quá trình thiết kế chương trình - trực tiếp tăng con trỏ st. Câu lệnh này không phù hợp với logic của chương trình và mã hiện rất khó hiểu. Những loại bản sửa lỗi này thường được gọi là bản vá—thứ được thiết kế để lấp lỗ hổng trong chương trình hiện có. Một giải pháp tốt hơn nhiều là xem xét lại logic. Một tùy chọn trong trường hợp của chúng tôi là xác định con trỏ thứ hai được khởi tạo với giá trị st:

Const char *p = st;

Bây giờ p có thể được sử dụng trong vòng lặp tính toán độ dài, giữ nguyên giá trị của st:

Trong khi (*p++)

3.4.2. Lớp chuỗi

Như chúng ta vừa thấy, việc sử dụng kiểu chuỗi tích hợp dễ xảy ra lỗi và bất tiện vì nó được triển khai ở mức độ thấp như vậy. Do đó, việc phát triển lớp hoặc các lớp của riêng bạn để thể hiện một loại chuỗi là điều khá phổ biến - hầu hết mọi công ty, bộ phận hoặc dự án cá nhân đều có cách triển khai chuỗi riêng. Tôi có thể nói gì đây, trong hai lần xuất bản trước của cuốn sách này, chúng tôi đã làm điều tương tự! Điều này đã làm phát sinh các vấn đề về khả năng tương thích và tính di động của chương trình. Việc triển khai lớp chuỗi tiêu chuẩn bằng thư viện chuẩn C++ nhằm mục đích chấm dứt việc phát minh lại xe đạp này.
Hãy thử chỉ định tập hợp các thao tác tối thiểu mà lớp chuỗi nên có:

  • khởi tạo bằng một mảng ký tự (kiểu chuỗi tích hợp) hoặc đối tượng khác thuộc kiểu chuỗi. Loại tích hợp không có khả năng thứ hai;
  • sao chép dòng này sang dòng khác. Đối với loại tích hợp sẵn, bạn phải sử dụng hàm strcpy();
  • truy cập vào các ký tự riêng lẻ của một chuỗi để đọc và viết. Trong một mảng tích hợp, việc này được thực hiện bằng cách sử dụng thao tác chỉ mục hoặc đánh địa chỉ gián tiếp;
  • so sánh hai chuỗi về sự bằng nhau. Đối với loại tích hợp sẵn, hãy sử dụng hàm strcmp();
  • nối hai chuỗi, tạo ra kết quả dưới dạng chuỗi thứ ba hoặc thay vì một trong các chuỗi ban đầu. Đối với loại có sẵn, hàm strcat() được sử dụng, nhưng để có kết quả ở một dòng mới, bạn cần sử dụng tuần tự các hàm strcpy() và strcat();
  • tính độ dài của một chuỗi. Bạn có thể tìm ra độ dài của chuỗi kiểu dựng sẵn bằng hàm strlen();
  • khả năng tìm hiểu xem một chuỗi có trống không. Đối với các chuỗi dựng sẵn, hai điều kiện phải được kiểm tra cho mục đích này: char str = 0; //... if (! str || ! *str) return;

Lớp chuỗi của Thư viện chuẩn C++ thực hiện tất cả các thao tác này (và nhiều thao tác khác, như chúng ta sẽ thấy trong Chương 6). Trong phần này chúng ta sẽ học cách sử dụng các hoạt động cơ bản của lớp này.
Để sử dụng các đối tượng của lớp chuỗi, bạn phải bao gồm tệp tiêu đề tương ứng:

#bao gồm

Đây là một ví dụ về một chuỗi ở phần trước, được biểu thị bằng một đối tượng có kiểu chuỗi và được khởi tạo thành một chuỗi ký tự:

#bao gồm string st("Giá một chai rượu\n");

Độ dài của chuỗi được hàm thành viên size() trả về (độ dài không bao gồm ký tự null kết thúc).

cout<< "Длина " << st << ": " << st.size() << " символов, включая символ новой строки\n";

Dạng thứ hai của định nghĩa chuỗi chỉ định một chuỗi trống:

Chuỗi st2; // dòng trống

Làm thế nào để chúng ta biết một chuỗi có trống không? Tất nhiên, bạn có thể so sánh độ dài của nó với 0:

Nếu (! st.size()) // đúng: trống

Tuy nhiên, cũng có một phương thức đặc biệt Empty() trả về true cho một chuỗi trống và false cho một chuỗi không trống:

Nếu (st.empty()) // đúng: trống

Dạng thứ ba của hàm tạo khởi tạo một đối tượng kiểu chuỗi với một đối tượng khác cùng kiểu:

Chuỗi st3(st);

Chuỗi st3 được khởi tạo bằng chuỗi st. Làm cách nào chúng ta có thể đảm bảo các chuỗi này khớp với nhau? Hãy sử dụng toán tử so sánh (==):

Nếu (st == st3) // quá trình khởi tạo thành công

Làm thế nào để sao chép dòng này sang dòng khác? Sử dụng toán tử gán bình thường:

St2 = st3; // sao chép st3 sang st2

Để nối các chuỗi, hãy sử dụng phép cộng (+) hoặc phép cộng với phép gán (+=). Cho hai dòng:

Chuỗi s1("xin chào, "); chuỗi s2("thế giới\n");

Chúng ta có thể nhận được chuỗi thứ ba bao gồm sự nối của hai chuỗi đầu tiên, theo cách này:

Chuỗi s3 = s1 + s2;

Nếu chúng ta muốn thêm s2 vào cuối s1, chúng ta nên viết:

S1 += s2;

Hoạt động bổ sung có thể nối các đối tượng của lớp chuỗi không chỉ với nhau mà còn với các chuỗi thuộc loại có sẵn. Bạn có thể viết lại ví dụ trên để các ký tự đặc biệt và dấu chấm câu được biểu thị bằng một kiểu có sẵn và các từ có ý nghĩa được biểu thị bằng các đối tượng của chuỗi lớp:

Const char *pc = ", "; chuỗi s1("xin chào"); chuỗi s2("thế giới");
chuỗi s3 = s1 + pc + s2 + "\n";

Các biểu thức như vậy hoạt động vì trình biên dịch biết cách tự động chuyển đổi các đối tượng thuộc loại có sẵn thành các đối tượng của lớp chuỗi. Cũng có thể chỉ cần gán một chuỗi dựng sẵn cho một đối tượng chuỗi:

Chuỗi s1; const char *pc = "một mảng ký tự"; s1 = máy tính; // Phải

Tuy nhiên, việc chuyển đổi ngược lại không hoạt động. Cố gắng thực hiện khởi tạo chuỗi kiểu tích hợp sau đây sẽ gây ra lỗi biên dịch:

Char *str = s1; // dịch lỗi

Để thực hiện việc chuyển đổi này, bạn phải gọi rõ ràng hàm thành viên c_str() có tên hơi kỳ lạ:

Char *str = s1.c_str(); // Gần đúng

Hàm c_str() trả về một con trỏ tới một mảng ký tự chứa chuỗi của đối tượng chuỗi giống như nó xuất hiện trong kiểu chuỗi dựng sẵn.
Ví dụ khởi tạo con trỏ char *str ở trên vẫn chưa hoàn toàn chính xác. c_str() trả về một con trỏ tới một mảng không đổi để ngăn nội dung của đối tượng bị sửa đổi trực tiếp bởi con trỏ này, có kiểu

ký tự hằng *

(Chúng tôi sẽ đề cập đến từ khóa const trong phần tiếp theo.) Tùy chọn khởi tạo chính xác trông như thế này:

Const char *str = s1.c_str(); // Phải

Các ký tự riêng lẻ của một đối tượng chuỗi, giống như một kiểu có sẵn, có thể được truy cập bằng thao tác chỉ mục. Ví dụ: đây là đoạn mã thay thế tất cả các dấu chấm bằng dấu gạch dưới:

Chuỗi str("fa.disney.com"); kích thước int = str.size(); vì (int ix = 0; ix< size; ++ix) if (str[ ix ] == ".")
str[ix] = "_";

Đó là tất cả những gì chúng tôi muốn nói về lớp chuỗi ngay bây giờ. Trên thực tế, lớp này có nhiều đặc tính và khả năng thú vị hơn. Giả sử ví dụ trước cũng được triển khai bằng cách gọi một hàm thay thế ():

Thay thế(str.begin(), str.end(), ".", "_");

thay thế() là một trong những thuật toán chung mà chúng tôi đã giới thiệu trong Phần 2.8 và sẽ được thảo luận chi tiết trong Chương 12. Hàm này chạy trong phạm vi từ start() đến end(), trả về con trỏ về đầu và cuối của chuỗi và thay thế các phần tử bằng tham số thứ ba của nó thành tham số thứ tư.

Bài tập 3.12

Tìm lỗi sai trong các phát biểu dưới đây:

(a) char ch = “Con đường dài và quanh co”; (b) int ival = (c) char *pc = (d) chuỗi st(&ch); (e) máy tính = 0; (i) pc = “0”;
(f) st = pc; (j) st =
(g) ch = pc; (k) ch = *pc;
(h) pc = st; (l) *pc = ival;

Bài tập 3.13

Giải thích sự khác biệt trong hành vi của các câu lệnh vòng lặp sau:

Trong khi (st++) ++cnt;
trong khi (*st++)
++cnt;

Bài tập 3.14

Hai chương trình tương đương về mặt ngữ nghĩa được đưa ra. Cái đầu tiên sử dụng kiểu chuỗi tích hợp, cái thứ hai sử dụng lớp chuỗi:

// ***** Triển khai bằng chuỗi C ***** #include #bao gồm
int chính()
{
lỗi int = 0;
const char *pc = "một chuỗi ký tự rất dài"; vì (int ix = 0; ix< 1000000; ++ix)
{
int len ​​= strlen(pc);
char *pc2 = char mới[ len + 1 ];
strcpy(pc2, pc);
nếu (strcmp(pc2, pc))
++ lỗi; xóa pc2;
}
cout<< "C-строки: "
<< errors << " ошибок.\n";
) // ***** Triển khai bằng cách sử dụng lớp chuỗi ***** #include
#bao gồm
int chính()
{
lỗi int = 0;
string str("một chuỗi ký tự rất dài"); vì (int ix = 0; ix< 1000000; ++ix)
{
int len ​​​​= str.size();
chuỗi str2 = str;
nếu (str != str2)
}
cout<< "класс string: "
<< errors << " ошибок.\n;
}

Những chương trình này làm gì?
Hóa ra lần triển khai thứ hai chạy nhanh gấp đôi so với lần triển khai đầu tiên. Bạn có mong đợi một kết quả như vậy? bạn giải thích nó như thế nào?

Bài tập 3.15

Bạn có thể cải thiện hoặc thêm bất cứ điều gì vào tập hợp các phép toán của lớp chuỗi được đưa ra trong phần trước không? Giải thích đề xuất của bạn

3.5. thông số const

Hãy lấy ví dụ mã sau:

Vì (int chỉ mục = 0; chỉ mục< 512; ++index) ... ;

Có hai vấn đề khi sử dụng số 512. Đầu tiên là sự dễ dàng nhận thức về văn bản chương trình. Tại sao giới hạn trên của biến vòng lặp phải chính xác là 512? Điều gì ẩn đằng sau giá trị này? Cô ấy có vẻ ngẫu nhiên...
Vấn đề thứ hai liên quan đến sự dễ dàng sửa đổi và bảo trì mã. Giả sử chương trình có 10.000 dòng và số 512 xuất hiện trong 4% trong số đó. Giả sử rằng trong 80% trường hợp, số 512 phải được đổi thành 1024. Bạn có thể tưởng tượng được mức độ phức tạp của công việc đó và số lượng lỗi có thể mắc phải khi sửa sai giá trị không?
Chúng tôi giải quyết cả hai vấn đề này cùng một lúc: chúng tôi cần tạo một đối tượng có giá trị 512. Đặt cho nó một cái tên có ý nghĩa, chẳng hạn như bufSize, chúng tôi làm cho chương trình dễ hiểu hơn nhiều: rõ ràng chính xác biến vòng lặp được so sánh là gì ĐẾN.

Mục lục< bufSize

Trong trường hợp này, việc thay đổi kích thước của bufSize không yêu cầu phải xem qua 400 dòng mã để sửa đổi 320 dòng trong số đó. Khả năng xảy ra lỗi sẽ giảm đi bao nhiêu khi chỉ thêm một đối tượng! Bây giờ giá trị là 512 bản địa hóa.

Int bufSize = 512; // kích thước bộ đệm đầu vào // ... for (int chỉ mục = 0; chỉ mục< bufSize; ++index)
// ...

Vẫn còn một vấn đề nhỏ: biến bufSize ở đây là giá trị l có thể vô tình bị thay đổi trong chương trình, dẫn đến lỗi khó bắt. Đây là một lỗi phổ biến: sử dụng toán tử gán (=) thay vì toán tử so sánh (==):

// thay đổi ngẫu nhiên trong giá trị bufSize if (bufSize = 1) // ...

Việc thực thi mã này sẽ khiến giá trị bufSize trở thành 1, điều này có thể dẫn đến hành vi chương trình hoàn toàn không thể đoán trước. Những lỗi loại này thường rất khó phát hiện vì đơn giản là chúng không thể nhìn thấy được.
Sử dụng công cụ xác định const sẽ giải quyết được vấn đề này. Bằng cách khai báo đối tượng là

Const int bufSize = 512; // kích thước bộ đệm đầu vào

chúng ta biến biến thành một hằng số có giá trị 512, giá trị này không thể thay đổi: những nỗ lực như vậy bị trình biên dịch ngăn chặn: việc sử dụng sai toán tử gán thay vì so sánh, như trong ví dụ trên, sẽ gây ra lỗi biên dịch.

// lỗi: cố gắng gán giá trị cho một hằng số if (bufSize = 0) ...

Vì một hằng số không thể được gán một giá trị nên nó phải được khởi tạo tại nơi nó được xác định. Việc xác định một hằng số mà không khởi tạo nó cũng gây ra lỗi biên dịch:

Const kép pi; // lỗi: hằng số chưa được khởi tạo

Const gấp đôi mức lương tối thiểu = 9,60; // Phải? lỗi?
gấp đôi *ptr =

Trình biên dịch có nên cho phép gán như vậy không? Vì minWage là một hằng số nên nó không thể được gán một giá trị. Mặt khác, không có gì ngăn cản chúng tôi viết:

*ptr += 1,40; // thay đổi đối tượng minWage!

Theo quy định, trình biên dịch không có khả năng bảo vệ khỏi việc sử dụng con trỏ và sẽ không thể báo hiệu lỗi nếu chúng được sử dụng theo cách này. Điều này đòi hỏi phải phân tích quá sâu về logic chương trình. Do đó, trình biên dịch đơn giản cấm việc gán địa chỉ không đổi cho các con trỏ thông thường.
Vậy có phải chúng ta không có khả năng sử dụng con trỏ tới hằng số? KHÔNG. Với mục đích này, có các con trỏ được khai báo bằng mã xác định const:

Const kép *cptr;

trong đó cptr là một con trỏ tới một đối tượng có kiểu const double. Điều tinh tế là bản thân con trỏ không phải là hằng số, nghĩa là chúng ta có thể thay đổi giá trị của nó. Ví dụ:

Hằng số kép *pc = 0; const gấp đôi mức lương tối thiểu = 9,60; // đúng: chúng tôi không thể thay đổi minWage bằng pc
pc = gấp đôi dval = 3,14; // đúng: chúng tôi không thể thay đổi minWage bằng pc
// mặc dù dval không phải là hằng số
pc = // đúng dval = 3.14159; //Phải
*pc = 3,14159; // lỗi

Địa chỉ của một đối tượng không đổi chỉ được gán cho một con trỏ tới một hằng số. Đồng thời, một con trỏ như vậy cũng có thể được gán địa chỉ của một biến thông thường:

máy tính =

Một con trỏ không đổi không cho phép sửa đổi đối tượng mà nó đánh địa chỉ bằng cách sử dụng địa chỉ gián tiếp. Mặc dù dval trong ví dụ trên không phải là hằng số nhưng trình biên dịch sẽ không cho phép thay đổi dval qua máy tính. (Một lần nữa, vì nó không thể xác định địa chỉ của đối tượng nào có thể chứa con trỏ bất kỳ lúc nào trong quá trình thực hiện chương trình.)
Trong các chương trình thực, con trỏ tới hằng thường được sử dụng làm tham số hình thức của hàm. Việc sử dụng chúng đảm bảo rằng đối tượng được truyền cho một hàm dưới dạng đối số thực tế sẽ không bị hàm đó sửa đổi. Ví dụ:

// Trong các chương trình thực, con trỏ tới hằng số thường // được sử dụng làm tham số hình thức của hàm int strcmp(const char *str1, const char *str2);

(Chúng ta sẽ nói nhiều hơn về con trỏ hằng trong Chương 7 khi nói về hàm.)
Ngoài ra còn có con trỏ liên tục. (Lưu ý sự khác biệt giữa con trỏ hằng và con trỏ hằng số!). Con trỏ hằng có thể định địa chỉ của một hằng hoặc một biến. Ví dụ:

Int errNumb = 0; int *const currErr =

Ở đây curErr là một con trỏ hằng tới một đối tượng không phải là hằng. Điều này có nghĩa là chúng ta không thể gán cho nó địa chỉ của một đối tượng khác, mặc dù bản thân đối tượng đó có thể được sửa đổi. Đây là cách sử dụng con trỏ curErr:

Làm việc gì đó(); if (*curErr) (
errorHandler();
*curErr = 0; // đúng: đặt lại giá trị errNumb
}

Cố gắng gán giá trị cho con trỏ hằng sẽ gây ra lỗi biên dịch:

CurErr = // lỗi

Một con trỏ hằng tới một hằng là sự kết hợp của hai trường hợp được xem xét.

Const kép pi = 3,14159; const kép *const pi_ptr = π

Cả giá trị của đối tượng được trỏ bởi pi_ptr cũng như giá trị của chính con trỏ đều không thể thay đổi trong chương trình.

Bài tập 3.16

Giải thích ý nghĩa của 5 định nghĩa sau. Có ai trong số họ sai?

(a) int tôi; (d) int *const cpi; (b) hằng int ic; (e) const int *const cpic; (c) const int *pic;

Bài tập 3.17

Định nghĩa nào sau đây là đúng? Tại sao?

(a) int i = -1; (b) const int ic = i; (c) const int *pic = (d) int *const cpi = (e) const int *const cpic =

Bài tập 3.18

Sử dụng các định nghĩa từ bài tập trước, hãy xác định các toán tử gán chính xác. Giải thích.

(a) i = ic; (d) pic = cpic; (b) pic = (i) cpic = (c) cpi = pic; (f) ic = *cpic;

3.6. Loại tham chiếu

Loại tham chiếu, đôi khi được gọi là bí danh, được sử dụng để đặt tên bổ sung cho đối tượng. Một tham chiếu cho phép bạn thao tác một đối tượng một cách gián tiếp, giống như bạn có thể làm với một con trỏ. Tuy nhiên, thao tác gián tiếp này không yêu cầu cú pháp đặc biệt cần thiết cho con trỏ. Thông thường, các tham chiếu được sử dụng làm tham số hình thức của hàm. Trong phần này, chúng ta sẽ xem xét việc sử dụng các đối tượng kiểu tham chiếu.
Loại tham chiếu được biểu thị bằng cách chỉ định toán tử địa chỉ (&) trước tên biến. Liên kết phải được khởi tạo. Ví dụ:

Int ival = 1024; // đúng: refVal - tham chiếu tới ival int &refVal = ival; // lỗi: tham chiếu phải được khởi tạo thành int

Int ival = 1024; // lỗi: refVal thuộc loại int, không phải int* int &refVal = int *pi = // đúng: ptrVal là tham chiếu đến một con trỏ int *&ptrVal2 = pi;

Khi một tham chiếu được xác định, bạn không thể thay đổi nó để hoạt động với một đối tượng khác (đó là lý do tại sao tham chiếu phải được khởi tạo tại thời điểm nó được xác định). Trong ví dụ sau, toán tử gán không thay đổi giá trị của refVal; giá trị mới được gán cho biến ival - biến mà refVal đánh địa chỉ.

Int min_val = 0; // ival nhận giá trị của min_val, // thay vì refVal thay đổi giá trị thành min_val refVal = min_val;

Giá trị tham chiếu += 2; thêm 2 vào ival, biến được tham chiếu bởi refVal. Tương tự int ii = refVal; gán ii giá trị hiện tại của ival, int *pi = khởi tạo pi với địa chỉ của ival.

// hai đối tượng kiểu int được định nghĩa int ival = 1024, ival2 = 2048; // một tham chiếu và một đối tượng được định nghĩa int &rval = ival, rval2 = ival2; // một đối tượng, một con trỏ và một tham chiếu được xác định
int inal3 = 1024, *pi = ival3, &ri = ival3; // hai liên kết được xác định int &rval3 = ival3, &rval4 = ival2;

Một tham chiếu const có thể được khởi tạo bởi một đối tượng thuộc loại khác (tất nhiên, giả sử có thể chuyển đổi loại này sang loại khác), cũng như bằng một giá trị không có địa chỉ, chẳng hạn như hằng số bằng chữ. Ví dụ:

Giá trị kép = 3,14159; // chỉ đúng với tham chiếu hằng
const int &ir = 1024;
const int &ir2 = dval;
const kép &dr = dval + 1.0;

Nếu chúng ta không chỉ định trình xác định const thì cả ba định nghĩa tham chiếu đều có thể gây ra lỗi biên dịch. Tuy nhiên, lý do tại sao trình biên dịch không vượt qua được những định nghĩa như vậy vẫn chưa rõ ràng. Hãy cố gắng tìm ra nó.
Đối với chữ, điều này ít nhiều rõ ràng: chúng ta không thể gián tiếp thay đổi giá trị của chữ bằng cách sử dụng con trỏ hoặc tham chiếu. Đối với các đối tượng thuộc loại khác, trình biên dịch chuyển đổi đối tượng gốc thành một số đối tượng phụ trợ. Ví dụ: nếu chúng ta viết:

Giá trị kép = 1024; const int &ri = dval;

sau đó trình biên dịch chuyển đổi nó thành một cái gì đó như thế này:

Int temp = dval; const int &ri = temp;

Nếu chúng ta có thể gán một giá trị mới cho tham chiếu ri, chúng ta sẽ thực sự thay đổi không phải dval mà là temp. Giá trị dval sẽ giữ nguyên, điều này hoàn toàn không được người lập trình biết đến. Do đó, trình biên dịch cấm những hành động như vậy và cách duy nhất để khởi tạo một tham chiếu với một đối tượng thuộc loại khác là khai báo nó là const.
Đây là một ví dụ khác về một liên kết khó hiểu trong lần đầu tiên. Chúng tôi muốn xác định một tham chiếu đến địa chỉ của một đối tượng không đổi, nhưng tùy chọn đầu tiên của chúng tôi gây ra lỗi biên dịch:

Const int ival = 1024; // lỗi: cần tham chiếu liên tục
int *&pi_ref =

Nỗ lực khắc phục sự cố bằng cách thêm công cụ xác định const cũng không thành công:

Const int ival = 1024; // vẫn còn lỗi const int *&pi_ref =

Lý do là gì? Nếu đọc kỹ định nghĩa, chúng ta sẽ thấy pi_ref là một tham chiếu đến một con trỏ hằng tới một đối tượng có kiểu int. Và chúng ta cần một con trỏ không phải hằng tới một đối tượng không đổi, vì vậy mục sau đây sẽ đúng:

Const int ival = 1024; // Phải
int *const &piref =

Có hai điểm khác biệt chính giữa một liên kết và một con trỏ. Đầu tiên, liên kết phải được khởi tạo tại nơi nó được xác định. Thứ hai, bất kỳ thay đổi nào đối với một liên kết đều không biến đổi liên kết mà là đối tượng mà nó đề cập đến. Hãy xem xét các ví dụ. Nếu chúng ta viết:

Int *pi = 0;

chúng ta khởi tạo con trỏ pi về 0, nghĩa là pi không trỏ tới bất kỳ đối tượng nào. Đồng thời ghi

const int &ri = 0;
có nghĩa là một cái gì đó như thế này:
int tạm thời = 0;
const int &ri = temp;

Đối với thao tác gán, trong ví dụ sau:

Int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = pi = pi2;

biến ival được trỏ tới bởi pi không thay đổi và pi nhận giá trị địa chỉ của biến ival2. Cả pi và pi2 hiện đều trỏ đến cùng một đối tượng, ival2.
Nếu chúng ta làm việc với các liên kết:

Int &ri = ival, &ri2 = ival2; ri = ri2;

// ví dụ về sử dụng liên kết // Giá trị được trả về trong tham số next_value
bool get_next_value(int &next_value); // toán tử quá tải Toán tử ma trận+(const Matrix&, const Matrix&);

Int ival; trong khi (get_next_value(ival)) ...

Int &next_value = ival;

(Việc sử dụng tham chiếu làm tham số hình thức của hàm sẽ được thảo luận chi tiết hơn trong Chương 7.)

Bài tập 3.19

Có sai sót nào trong các định nghĩa này không? Giải thích. Bạn sẽ sửa chúng như thế nào?

(a) int ival = 1,01; (b) int &rval1 = 1,01; (c) int &rval2 = ival; (d) int &rval3 = (e) int *pi = (f) int &rval4 = pi; (g) int &rval5 = pi*; (h) int &*prval1 = pi; (i) const int &ival2 = 1; (j) const int &*prval2 =

Bài tập 3.20

Có bất kỳ toán tử gán nào sau đây sai không (sử dụng các định nghĩa từ bài tập trước)?

(a) rval1 = 3,14159; (b) prval1 = prval2; (c) prval2 = rval1; (d) *prval2 = ival2;

Bài tập 3.21

Tìm lỗi trong các hướng dẫn đã cho:

(a) int ival = 0; const int *pi = 0; const int &ri = 0; (b)pi =
ri =
số pi =

3.7. Nhập bool

Một đối tượng kiểu bool có thể nhận một trong hai giá trị: true và false. Ví dụ:

// khởi tạo chuỗi string search_word = get_word(); // khởi tạo biến tìm thấy
bool tìm thấy = sai; chuỗi next_word; trong khi (cin >> next_word)
nếu (next_word == search_word)
tìm thấy = đúng;
// ... // viết tắt: if (tìm thấy == true)
Nếu được tìm thấy)
cout<< "ok, мы нашли слово\n";
cách khác<< "нет, наше слово не встретилось.\n";

Mặc dù bool là một trong các kiểu số nguyên nhưng nó không thể được khai báo là có dấu, không dấu, ngắn hoặc dài, vì vậy định nghĩa trên là sai:

// lỗi short bool Found = false;

Các đối tượng kiểu bool được ngầm chuyển thành kiểu int. Đúng trở thành 1 và sai trở thành 0. Ví dụ:

Bool được tìm thấy = sai; int lần xuất hiện_count = 0; trong khi (/* lầm bầm */)
{
đã tìm thấy = look_for(/* gì đó */); // giá trị tìm thấy được chuyển thành 0 hoặc 1
số lần xuất hiện += được tìm thấy; )

Theo cách tương tự, các giá trị của kiểu số nguyên và con trỏ có thể được chuyển đổi thành giá trị kiểu bool. Trong trường hợp này, 0 được hiểu là sai và mọi thứ khác đều đúng:

// trả về số lần xuất hiện extern int find(const string&); bool tìm thấy = sai; if (found = find("rosebud")) // đúng: đã tìm thấy == true // trả về một con trỏ tới phần tử
extern int* find(int value); if (found = find(1024)) // đúng: đã tìm thấy == true

3.8. chuyển nhượng

Thường thì bạn phải xác định một biến lấy các giá trị từ một tập hợp nhất định. Giả sử một tệp được mở ở bất kỳ chế độ nào trong ba chế độ: để đọc, để viết, để nối thêm.
Tất nhiên, ba hằng số có thể được định nghĩa để biểu thị các chế độ này:

Const int đầu vào = 1; const int đầu ra = 2; const int nối thêm = 3;

và sử dụng các hằng số này:

Bool open_file(chuỗi file_name, int open_mode); // ...
open_file("Phoenix_and_the_Crane", nối thêm);

Giải pháp này có thể thực hiện được nhưng không hoàn toàn chấp nhận được vì chúng tôi không thể đảm bảo rằng đối số được truyền cho hàm open_file() chỉ là 1, 2 hoặc 3.
Sử dụng loại enum sẽ giải quyết được vấn đề này. Khi chúng tôi viết:

Enum open_modes(đầu vào = 1, đầu ra, nối thêm);

chúng tôi xác định một loại open_modes mới. Các giá trị hợp lệ cho một đối tượng thuộc loại này được giới hạn ở tập hợp 1, 2 và 3, với mỗi giá trị được chỉ định có một tên dễ nhớ. Chúng ta có thể sử dụng tên của loại mới này để xác định cả đối tượng của loại đó và loại tham số hình thức của hàm:

Void open_file(string file_name, open_modes om);

đầu vào, đầu ra và nối thêm là phần tử liệt kê. Tập hợp các phần tử liệt kê chỉ định tập giá trị được phép cho một đối tượng thuộc một loại nhất định. Một biến kiểu open_modes (trong ví dụ của chúng tôi) được khởi tạo với một trong các giá trị này; nó cũng có thể được gán bất kỳ giá trị nào trong số đó. Ví dụ:

Open_file("Phượng Hoàng và Sếu", nối thêm);

Việc cố gắng gán một giá trị không phải là một trong các phần tử liệt kê cho một biến thuộc loại này (hoặc chuyển nó dưới dạng tham số cho hàm) sẽ gây ra lỗi biên dịch. Ngay cả khi chúng ta cố gắng truyền một giá trị số nguyên tương ứng với một trong các phần tử liệt kê, chúng ta vẫn sẽ nhận được lỗi:

// lỗi: 1 không phải là thành phần của bảng liệt kê open_modes open_file("Jonah", 1);

Có một cách để xác định một biến kiểu open_modes, gán cho nó giá trị của một trong các phần tử liệt kê và truyền nó dưới dạng tham số cho hàm:

Open_modes om = đầu vào; // ... om = nối thêm; open_file("TailTell", om);

Tuy nhiên, không thể có được tên của các yếu tố đó. Nếu chúng ta viết câu lệnh đầu ra:

cout<< input << " " << om << endl;

thì chúng ta vẫn nhận được:

Vấn đề này được giải quyết bằng cách xác định một mảng chuỗi trong đó phần tử có chỉ số bằng giá trị của phần tử liệt kê sẽ chứa tên của nó. Cho một mảng như vậy, chúng ta có thể viết:

cout<< open_modes_table[ input ] << " " << open_modes_table[ om ] << endl Будет выведено: input append

Ngoài ra, bạn không thể lặp qua tất cả các giá trị của một enum:

// không được hỗ trợ cho (open_modes iter = input; iter != nối thêm; ++inter) // ...

Để xác định một bảng liệt kê, hãy sử dụng từ khóa enum và tên thành phần được chỉ định trong dấu ngoặc nhọn, phân tách bằng dấu phẩy. Theo mặc định, giá trị đầu tiên là 0, giá trị tiếp theo là 1, v.v. Bạn có thể thay đổi quy tắc này bằng toán tử gán. Trong trường hợp này, mỗi phần tử tiếp theo không có giá trị được chỉ định rõ ràng sẽ nhiều hơn phần tử đứng trước nó trong danh sách 1 đơn vị. Trong ví dụ của chúng tôi, chúng tôi đã chỉ định rõ ràng giá trị 1 cho đầu vào, còn đầu ra và phần bổ sung sẽ bằng 2 và 3. Đây là một ví dụ khác:

// hình dạng == 0, hình cầu == 1, hình trụ == 2, đa giác == 3 enum Các dạng( chia sẻ, hình tròn, hình trụ, đa giác);

Các giá trị nguyên tương ứng với các phần tử khác nhau của cùng một bảng liệt kê không nhất thiết phải khác nhau. Ví dụ:

// point2d == 2, point2w == 3, point3d == 3, point3w == 4 enum Điểm ( point2d=2, point2w, point3d=3, point3w=4 );

Một đối tượng có kiểu liệt kê có thể được xác định, sử dụng trong các biểu thức và được truyền cho hàm dưới dạng đối số. Một đối tượng như vậy được khởi tạo chỉ với giá trị của một trong các phần tử liệt kê và chỉ giá trị đó được gán cho nó, rõ ràng hoặc là giá trị của một đối tượng khác cùng loại. Ngay cả các giá trị số nguyên tương ứng với các phần tử liệt kê hợp lệ cũng không thể được gán cho nó:

Void mumble() ( Điểm pt3d = point3d; // đúng: pt2d == 3 // lỗi: pt3w được khởi tạo với kiểu int Điểm pt3w = 3; // lỗi: đa giác không được bao gồm trong bảng liệt kê Điểm pt3w = đa giác; / /đúng: cả hai đối tượng thuộc loại Points pt3w = pt3d )

Tuy nhiên, trong các biểu thức số học, một phép liệt kê có thể được tự động chuyển thành kiểu int. Ví dụ:

Const int array_size = 1024; // đúng: pt2w được chuyển thành int
int chunk_size = array_size * pt2w;

3.9. Nhập "mảng"

Chúng ta đã đề cập đến mảng trong phần 2.1. Mảng là một tập hợp các phần tử cùng kiểu, được truy cập bằng chỉ mục - số thứ tự của phần tử trong mảng. Ví dụ:

Int ival;

định nghĩa ival là một biến int và lệnh

Int ia[ 10 ];

chỉ định một mảng gồm mười đối tượng kiểu int. Đối với mỗi đối tượng này, hoặc phần tử mảng, có thể được truy cập bằng thao tác chỉ mục:

Ival = ia[ 2 ];

gán cho biến ival giá trị của phần tử mảng ia với chỉ số 2. Tương tự

Ia[ 7 ] = ival;

gán cho phần tử ở chỉ số 7 giá trị ival.

Một định nghĩa mảng bao gồm một bộ xác định kiểu, tên mảng và kích thước. Kích thước chỉ định số phần tử mảng (ít nhất là 1) và được đặt trong dấu ngoặc vuông. Kích thước của mảng phải được biết ở giai đoạn biên dịch và do đó nó phải là một biểu thức không đổi, mặc dù nó không nhất thiết phải được chỉ định dưới dạng chữ. Dưới đây là ví dụ về định nghĩa mảng đúng và sai:

Extern int get_size(); // hằng số buf_size và max_files
const int buf_size = 512, max_files = 20;
int Staff_size = 27; // đúng: hằng số char input_buffer[ buf_size ]; // đúng: biểu thức hằng số: 20 - 3 char *fileTable[ max_files-3 ]; // lỗi: không phải lương gấp đôi cố định[ Staff_size ]; // lỗi: không phải là biểu thức hằng int test_scores[ get_size() ];

Các đối tượng buf_size và max_files là các hằng số, do đó các định nghĩa mảng input_buffer và fileTable là chính xác. Nhưng kích thước nhân viên là một biến (mặc dù được khởi tạo bằng hằng số 27), có nghĩa là mức lương không được chấp nhận. (Trình biên dịch không thể tìm thấy giá trị của biến Staff_size khi mảng lương được xác định.)
Biểu thức max_files-3 có thể được đánh giá tại thời điểm biên dịch, do đó định nghĩa mảng fileTable có cú pháp chính xác.
Việc đánh số phần tử bắt đầu từ 0, do đó, đối với một mảng gồm 10 phần tử, phạm vi chỉ số chính xác không phải là 1 – 10 mà là 0 – 9. Dưới đây là ví dụ về việc lặp qua tất cả các phần tử của mảng:

Int main() ( const int array_size = 10; int ia[ array_size ]; for (int ix = 0; ix< array_size; ++ ix)
ia[ix] = ix;
}

Khi xác định một mảng, bạn có thể khởi tạo nó một cách rõ ràng bằng cách liệt kê các giá trị của các phần tử của nó trong dấu ngoặc nhọn, cách nhau bằng dấu phẩy:

Const int array_size = 3; int ia[ array_size ] = ( 0, 1, 2 );

Nếu chúng ta chỉ định rõ ràng danh sách các giá trị, chúng ta không phải chỉ định kích thước của mảng: chính trình biên dịch sẽ đếm số phần tử:

// mảng có kích thước 3 int ia = ( 0, 1, 2 );

Khi cả kích thước và danh sách các giá trị đều được chỉ định rõ ràng, có thể có ba tùy chọn. Nếu kích thước và số lượng giá trị trùng nhau thì mọi thứ đều rõ ràng. Nếu danh sách các giá trị ngắn hơn kích thước đã chỉ định thì các phần tử mảng còn lại sẽ được khởi tạo về 0. Nếu có nhiều giá trị hơn trong danh sách, trình biên dịch sẽ hiển thị thông báo lỗi:

// ia ==> ( 0, 1, 2, 0, 0 ) const int array_size = 5; int ia[ array_size ] = ( 0, 1, 2 );

Một mảng ký tự có thể được khởi tạo không chỉ bằng danh sách các giá trị ký tự trong dấu ngoặc nhọn mà còn bằng một chuỗi ký tự. Tuy nhiên, có một số khác biệt giữa các phương pháp này. Hãy cùng nói nào

Const char cal = ("C", "+", "+" ); const char cal2 = "C++";

Kích thước của mảng ca1 là 3, mảng ca2 là 4 (trong chuỗi ký tự, ký tự null kết thúc được tính đến). Định nghĩa sau đây sẽ gây ra lỗi biên dịch:

// lỗi: chuỗi "Daniel" gồm 7 phần tử const char ch3[ 6 ] = "Daniel";

Một mảng không thể được gán giá trị của một mảng khác và không được phép khởi tạo một mảng bằng một mảng khác. Ngoài ra, không được phép sử dụng một loạt các tài liệu tham khảo. Dưới đây là ví dụ về cách sử dụng mảng đúng và sai:

Const int array_size = 3; int ix, jx, kx; // đúng: mảng con trỏ kiểu int* int *iar = ( &ix, &jx, &kx ); // lỗi: mảng tham chiếu không được phép int &iar = ( ix, jx, kx ); int chính()
{
int ia3( array_size ]; // đúng
// lỗi: mảng tích hợp không thể sao chép được
ia3 = ia;
trả về 0;
}

Để sao chép mảng này sang mảng khác, bạn phải thực hiện việc này cho từng phần tử riêng biệt:

Const int array_size = 7; int ia1 = ( 0, 1, 2, 3, 4, 5, 6 ); int chính() (
int ia3[kích thước mảng]; vì (int ix = 0; ix< array_size; ++ix)
ia2[ ix ] = ia1[ ix ]; trả về 0;
}

Chỉ mục mảng có thể là bất kỳ biểu thức nào tạo ra kết quả là kiểu số nguyên. Ví dụ:

Int someVal, get_index(); ia2[ get_index() ] = someVal;

Chúng tôi nhấn mạnh rằng ngôn ngữ C++ không cung cấp khả năng kiểm soát các chỉ mục mảng, ở giai đoạn biên dịch hoặc ở giai đoạn chạy. Bản thân lập trình viên phải đảm bảo chỉ số không vượt ra ngoài ranh giới của mảng. Lỗi khi làm việc với chỉ mục là khá phổ biến. Thật không may, không khó lắm để tìm thấy các ví dụ về các chương trình biên dịch và thậm chí hoạt động, nhưng vẫn chứa những lỗi nghiêm trọng sớm hay muộn dẫn đến sự cố.

Bài tập 3.22

Định nghĩa mảng nào sau đây là sai? Giải thích.

(a) int ia[ buf_size ]; (d) int ia[ 2 * 7 - 14 ] (b) int ia[ get_size() ]; (e) char st[ 11 ] = "cơ bản"; (c) int ia[ 4 * 7 - 14 ];

Bài tập 3.23

Đoạn mã sau phải khởi tạo từng phần tử của mảng bằng một giá trị chỉ mục. Tìm những lỗi mắc phải:

Int main() ( const int array_size = 10; int ia[ array_size ]; for (int ix = 1; ix<= array_size; ++ix)
ia[ ia ] = ix; // ...
}

3.9.1. Mảng đa chiều

Trong C++ có thể sử dụng mảng nhiều chiều, khi khai báo chúng phải chỉ ra ranh giới bên phải của mỗi chiều trong dấu ngoặc vuông riêng biệt. Sau đây là định nghĩa của mảng hai chiều:

Int ia[ 4 ][ 3 ];

Giá trị đầu tiên (4) chỉ định số hàng, giá trị thứ hai (3) – số cột. Đối tượng ia được định nghĩa là một mảng gồm bốn chuỗi, mỗi chuỗi có ba phần tử. Mảng đa chiều cũng có thể được khởi tạo:

Int ia[ 4 ][ 3 ] = ( ( 0, 1, 2 ), ( 3, 4, 5 ), ( 6, 7, 8 ), ( 9, 10, 11 ) );

Các dấu ngoặc nhọn bên trong, dùng để chia danh sách các giá trị thành các dòng, là tùy chọn và thường được sử dụng để làm cho mã dễ đọc hơn. Việc khởi tạo bên dưới hoàn toàn giống với ví dụ trước, mặc dù nó kém rõ ràng hơn:

Int ia = ( 0,1,2,3,4,5,6,7,8,9,10,11 );

Định nghĩa sau đây chỉ khởi tạo các phần tử đầu tiên của mỗi dòng. Các phần tử còn lại sẽ bằng 0:

Int ia[ 4 ][ 3 ] = ( (0), (3), (6), (9) );

Nếu bạn bỏ qua dấu ngoặc nhọn bên trong thì kết quả sẽ hoàn toàn khác. Tất cả ba phần tử của hàng đầu tiên và phần tử đầu tiên của hàng thứ hai sẽ nhận được giá trị được chỉ định và phần tử còn lại sẽ được khởi tạo ngầm thành 0.

Int ia[ 4 ][ 3 ] = ( 0, 3, 6, 9 );

Khi truy cập các phần tử của mảng nhiều chiều, bạn phải sử dụng chỉ mục cho từng chiều (chúng được đặt trong dấu ngoặc vuông). Đây là cách khởi tạo một mảng hai chiều bằng cách sử dụng các vòng lặp lồng nhau:

Int main() ( const int rowSize = 4; const int colSize = 3; int ia[ rowSize ][ colSize ]; for (int = 0; i< rowSize; ++i)
vì (int j = 0; j< colSize; ++j)
ia[ i ][ j ] = i + j j;
}

Thiết kế

Ia[ 1, 2 ]

là hợp lệ theo quan điểm của cú pháp C++, nhưng nó hoàn toàn không có nghĩa là điều mà một lập trình viên thiếu kinh nghiệm mong đợi. Đây không phải là khai báo mảng 1 x 2 hai chiều. Tổng hợp trong dấu ngoặc vuông là danh sách các biểu thức được phân tách bằng dấu phẩy sẽ dẫn đến giá trị cuối cùng là 2 (xem toán tử dấu phẩy trong Phần 4.2). Do đó khai báo ia tương đương với ia. Đây là một cơ hội khác để phạm sai lầm.

3.9.2. Mối quan hệ giữa mảng và con trỏ

Nếu chúng ta có một định nghĩa mảng:

Int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 );

vậy thì việc chỉ nêu tên anh ấy trong chương trình có nghĩa là gì?

Sử dụng mã định danh mảng trong chương trình tương đương với việc chỉ định địa chỉ của phần tử đầu tiên:

Tương tự, bạn có thể truy cập giá trị của phần tử đầu tiên của mảng theo hai cách:

// cả hai biểu thức đều trả về phần tử đầu tiên *ia; ia;

Để lấy địa chỉ của phần tử thứ hai của mảng, chúng ta phải viết:

Như chúng tôi đã đề cập trước đó, biểu thức

cũng cung cấp địa chỉ của phần tử thứ hai của mảng. Theo đó, ý nghĩa của nó được trao cho chúng ta theo hai cách sau:

*(ia+1); ia;

Lưu ý sự khác biệt trong biểu thức:

*ia+1 và *(ia+1);

Hoạt động dereference có hiệu suất cao hơn một ưu tiên hơn hoạt động bổ sung (ưu tiên của nhà điều hành được thảo luận trong Phần 4.13). Do đó, biểu thức đầu tiên trước tiên sẽ hủy tham chiếu biến ia và lấy phần tử đầu tiên của mảng, sau đó thêm 1 vào biểu thức thứ hai mang lại giá trị của phần tử thứ hai.

Bạn có thể duyệt mảng bằng cách sử dụng chỉ mục, như chúng ta đã làm ở phần trước hoặc sử dụng con trỏ. Ví dụ:

#bao gồm int main() ( int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 ); int *pbegin = ia; int *pend = ia + 9; while (pbegin != pend) ( cout<< *pbegin <<; ++pbegin; } }

Con trỏ pbegin được khởi tạo bằng địa chỉ của phần tử đầu tiên của mảng. Mỗi lần đi qua vòng lặp sẽ tăng con trỏ này lên 1, nghĩa là nó được dịch chuyển sang phần tử tiếp theo. Làm thế nào để bạn biết nơi để ở? Trong ví dụ của chúng tôi, chúng tôi đã xác định một con trỏ pend thứ hai và khởi tạo nó với địa chỉ theo sau phần tử cuối cùng của mảng ia. Ngay khi giá trị của pbegin bằng với pend, chúng ta biết rằng mảng đã kết thúc. Hãy viết lại chương trình này để phần đầu và phần cuối của mảng được truyền dưới dạng tham số cho một hàm tổng quát nhất định có thể in một mảng có kích thước bất kỳ:

#bao gồm void ia_print(int *pbegin, int *pend) (
trong khi (pbegin != chờ) (
cout<< *pbegin << " ";
++ bắt đầu;
}
) int chính()
{
int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 );
ia_print(ia, ia + 9);
}

Hàm của chúng ta đã trở nên phổ biến hơn, tuy nhiên, nó chỉ có thể hoạt động với các mảng kiểu int. Có một cách để loại bỏ hạn chế này: chuyển hàm này thành một mẫu (các mẫu đã được giới thiệu ngắn gọn ở phần 2.5):

#bao gồm bản mẫu void print(elemType *pbegin, elemType *pend) ( while (pbegin != pend) ( cout<< *pbegin << " "; ++pbegin; } }

Bây giờ chúng ta có thể gọi hàm print() để in các mảng thuộc bất kỳ loại nào:

Int main() ( int ia = ( 0, 1, 1, 2, 3, 5, 8, 13, 21 ); double da = ( 3.14, 6.28, 12.56, 25.12 ); chuỗi sa = ("heo con", " eeyore", "pooh" ); print(ia, ia+9);
print(da, da+4);
in(sa, sa+3);
}

Chúng tôi đã viết khái quát chức năng. Thư viện chuẩn cung cấp một tập hợp các thuật toán chung (chúng tôi đã đề cập đến điều này trong Phần 3.4) được triển khai theo cách tương tự. Các tham số của các hàm như vậy là các con trỏ tới đầu và cuối của mảng mà chúng thực hiện các hành động nhất định. Ví dụ: đây là cách gọi một thuật toán sắp xếp tổng quát trông như sau:

#bao gồm int main() ( int ia = ( 107, 28, 3, 47, 104, 76 ); chuỗi sa = ("piglet", "eeyore", "pooh" );sort(ia, ia+6);
sắp xếp(sa, sa+3);
};

(Chúng ta sẽ đi vào chi tiết hơn về các thuật toán tổng quát trong Chương 12; Phụ lục sẽ đưa ra ví dụ về cách sử dụng chúng.)
Thư viện chuẩn C++ chứa một tập hợp các lớp gói gọn việc sử dụng các vùng chứa và con trỏ. (Điều này đã được thảo luận trong Phần 2.8.) Trong phần tiếp theo, chúng ta sẽ xem xét vectơ loại vùng chứa tiêu chuẩn, đây là một triển khai hướng đối tượng của một mảng.

3.10. Lớp vectơ

Sử dụng lớp vectơ (xem Phần 2.8) là một cách thay thế cho việc sử dụng các mảng có sẵn. Lớp này cung cấp nhiều chức năng hơn nên việc sử dụng nó là thích hợp hơn. Tuy nhiên, có những tình huống mà bạn không thể thực hiện nếu không có mảng kiểu tích hợp sẵn. Một trong những tình huống này là việc xử lý các tham số dòng lệnh được truyền vào chương trình, điều mà chúng ta sẽ thảo luận trong phần 7.8. Lớp vectơ, giống như lớp chuỗi, là một phần của thư viện chuẩn C++.
Để sử dụng vectơ, bạn phải bao gồm tệp tiêu đề:

#bao gồm

Có hai cách tiếp cận hoàn toàn khác nhau để sử dụng vectơ, hãy gọi chúng là thành ngữ mảng và thành ngữ STL. Trong trường hợp đầu tiên, một đối tượng vectơ được sử dụng theo cách giống hệt như một mảng có sẵn. Một vectơ có kích thước nhất định được xác định:

Vectơ< int >ivec(10);

tương tự như việc xác định một mảng có kiểu tích hợp:

Int ia[ 10 ];

Để truy cập các phần tử riêng lẻ của một vectơ, thao tác chỉ mục được sử dụng:

Void simp1e_examp1e() ( const int e1em_size = 10; vector< int >ivec(e1em_size); int ia[ e1em_size ]; vì (int ix = 0; ix< e1em_size; ++ix)
ia[ ix ] = ivec[ ix ]; // ...
}

Chúng ta có thể tìm ra kích thước của vectơ bằng hàm size() và kiểm tra xem vectơ có trống hay không bằng hàm Empty(). Ví dụ:

Void print_vector(vector ivec) ( if (ivec.empty()) return; for (int ix=0; ix< ivec.size(); ++ix)
cout<< ivec[ ix ] << " ";
}

Các phần tử của vectơ được khởi tạo với giá trị mặc định. Đối với các kiểu số và con trỏ, giá trị này là 0. Nếu các phần tử là đối tượng lớp, thì bộ khởi tạo cho chúng được chỉ định bởi hàm tạo mặc định (xem phần 2.3). Tuy nhiên, trình khởi tạo cũng có thể được chỉ định rõ ràng bằng cách sử dụng biểu mẫu:

Vectơ< int >ivec(10, -1);

Tất cả mười phần tử của vectơ sẽ bằng -1.
Một mảng thuộc loại dựng sẵn có thể được khởi tạo rõ ràng bằng một danh sách:

Int ia[ 6 ] = ( -2, -1, O, 1, 2, 1024 );

Một hành động tương tự không thể thực hiện được đối với một đối tượng thuộc lớp vectơ. Tuy nhiên, một đối tượng như vậy có thể được khởi tạo bằng cách sử dụng kiểu mảng có sẵn:

// 6 phần tử ia được sao chép vào vector ivec< int >ivec(ia, ia+6);

Hàm tạo của vectơ ivec được truyền vào hai con trỏ - một con trỏ tới phần đầu của mảng ia và tới phần tử theo sau phần tử cuối cùng. Là danh sách các giá trị ban đầu, được phép chỉ định không phải toàn bộ mảng mà là một phạm vi nhất định của nó:

// 3 phần tử được sao chép: ia, ia, ia vector< int >ivec(&ia[ 2 ], &ia[ 5 ]);

Một điểm khác biệt giữa vectơ và mảng tích hợp là khả năng khởi tạo một đối tượng vectơ với một đối tượng vectơ khác và sử dụng toán tử gán để sao chép đối tượng. Ví dụ:

Vectơ< string >svec; void init_and_sign() ( // một vectơ được khởi tạo bởi một vectơ khác< string >user_names(svec); // ... // một vectơ được sao chép sang một vectơ khác
svec = tên_người_dùng;
}

Khi chúng ta nói về thành ngữ STL, chúng tôi muốn nói đến một cách tiếp cận hoàn toàn khác khi sử dụng vectơ. Thay vì chỉ định ngay kích thước mong muốn, chúng tôi xác định một vectơ trống:

Vectơ< string >chữ;

Sau đó, chúng tôi thêm các phần tử vào nó bằng nhiều chức năng khác nhau. Ví dụ: hàm push_back() chèn một phần tử vào cuối vectơ. Đây là một đoạn mã đọc một chuỗi các dòng từ đầu vào tiêu chuẩn và thêm chúng vào một vectơ:

Chuỗi từ; while (cin >> word) ( text.push_back(word); // ... )

Mặc dù chúng ta có thể sử dụng thao tác chỉ mục để lặp lại các phần tử của vectơ:

cout<< "считаны слова: \n"; for (int ix =0; ix < text.size(); ++ix) cout << text[ ix ] << " "; cout << endl;

Điển hình hơn trong thành ngữ này là sử dụng các trình vòng lặp:

cout<< "считаны слова: \n"; for (vector::iterator it = text.begin(); nó != text.end(); ++nó) cout<< *it << " "; cout << endl;

Trình vòng lặp là một lớp thư viện tiêu chuẩn về cơ bản là một con trỏ tới một phần tử mảng.
Sự biểu lộ

hủy đăng ký iterator và đưa ra chính phần tử vectơ. Hướng dẫn

Di chuyển con trỏ tới phần tử tiếp theo. Không cần thiết phải kết hợp hai cách tiếp cận này. Nếu bạn làm theo thành ngữ STL khi xác định một vectơ trống:

Vectơ ivec;

Sẽ là sai lầm nếu viết:

Chúng ta chưa có một phần tử nào của vectơ ivec; Số phần tử được xác định bằng hàm size().

Sai lầm ngược lại cũng có thể được thực hiện. Ví dụ: nếu chúng ta xác định một vectơ có kích thước nào đó:

Vectơ ia(10);

Sau đó, việc chèn các phần tử sẽ tăng kích thước của nó, thêm các phần tử mới vào các phần tử hiện có. Mặc dù điều này có vẻ hiển nhiên, nhưng một lập trình viên mới vào nghề có thể viết:

Const kích thước int = 7; int ia[kích thước] = ( 0, 1, 1, 2, 3, 5, 8 ); vectơ< int >ivec(kích thước); vì (int ix = 0; ix< size; ++ix) ivec.push_back(ia[ ix ]);

Điều này có nghĩa là khởi tạo vectơ ivec với các giá trị của các phần tử ia, dẫn đến vectơ ivec có kích thước 14.
Theo thành ngữ STL, bạn không chỉ có thể thêm mà còn có thể xóa các phần tử khỏi một vectơ. (Chúng ta sẽ xem xét tất cả những điều này một cách chi tiết và với các ví dụ ở Chương 6.)

Bài tập 3.24

Có sai sót nào trong các định nghĩa sau đây không?
int ia[ 7 ] = ( 0, 1, 1, 2, 3, 5, 8 );

(a) vectơ< vector< int >>ivec;
(b) vectơ< int >ivec = ( 0, 1, 1, 2, 3, 5, 8 );
(c)vectơ< int >ivec(ia, ia+7);
(d)vectơ< string >svec = ivec;
(e)vectơ< string >svec(10, string("null"));

Bài tập 3.25

Thực hiện chức năng sau:
bool is_equal(const int*ia, int ia_size,
vectơ hằng &ivec);
Hàm is_equal() so sánh hai phần tử vùng chứa theo từng phần tử. Trong trường hợp các thùng chứa có kích thước khác nhau, "đuôi" của thùng dài hơn không được tính đến. Rõ ràng là nếu tất cả các phần tử được so sánh đều bằng nhau thì hàm trả về true, nếu ít nhất một phần tử khác thì trả về false. Sử dụng một iterator để lặp qua các phần tử. Viết hàm main() gọi is_equal().

3.11. lớp học phức tạp

Lớp số phức phức là một lớp khác trong thư viện chuẩn. Như thường lệ, để sử dụng nó, bạn cần bao gồm tệp tiêu đề:

#bao gồm

Một số phức bao gồm hai phần - thực và ảo. Phần ảo là căn bậc hai của một số âm. Số phức thường được viết dưới dạng

trong đó 2 là phần thực và 3i là phần ảo. Dưới đây là ví dụ về định nghĩa của các đối tượng thuộc loại phức tạp:

// số thuần ảo: 0 + 7-i phức< double >nguyên chất(0, 7); // phần ảo là 0:3 + phức Oi< float >rea1_num(3); // cả phần thực và phần ảo đều bằng 0: 0 + 0-i phức< long double >số không; // khởi tạo một số phức với một số phức khác< double >purei2(purei);

Vì phức tạp, giống như vectơ, là một mẫu nên chúng ta có thể khởi tạo nó bằng các kiểu float, double và long double, như trong các ví dụ đã cho. Bạn cũng có thể định nghĩa một mảng các phần tử có kiểu phức tạp:

Tổ hợp< double >liên hợp[2] = (phức tạp< double >(2, 3), phức tạp< double >(2, -3) };

Tổ hợp< double >*ptr = phức tạp< double >&ref = *ptr;

3.12. chỉ thị typedef

Lệnh typedef cho phép bạn chỉ định từ đồng nghĩa cho kiểu dữ liệu tùy chỉnh hoặc tích hợp. Ví dụ:

Typedef lương gấp đôi; vectơ typedef vec_int; typedef vec_int test_scores; typedef bool in_attendance; typedef int *Pint;

Các tên được xác định bằng chỉ thị typedef có thể được sử dụng giống như cách xác định kiểu:

// gấp đôi mỗi giờ, hàng tuần; lương theo giờ, hàng tuần; //vectơ vecl(10);
vec_int vecl(10); //vectơ test0(c1ass_size); const int c1ass_size = 34; test_scores test0(c1ass_size); //vectơ< bool >sự tham dự; vectơ< in_attendance >tham dự(c1ass_size); // int *bảng[ 10 ]; Bàn pint [10];

Lệnh này bắt đầu bằng từ khóa typedef, theo sau là một công cụ xác định loại và kết thúc bằng một mã định danh, trở thành từ đồng nghĩa với loại đã chỉ định.
Tên được xác định bằng lệnh typedef được sử dụng để làm gì? Bằng cách sử dụng tên dễ nhớ cho các kiểu dữ liệu, bạn có thể làm cho chương trình của mình dễ hiểu hơn. Ngoài ra, người ta thường sử dụng những tên như vậy cho các kiểu hỗn hợp phức tạp khó hiểu (xem ví dụ ở Phần 3.14), để khai báo các con trỏ tới các hàm và hàm thành viên của một lớp (xem Phần 13.6).
Dưới đây là ví dụ về một câu hỏi mà hầu hết mọi người đều trả lời sai. Lỗi xảy ra do hiểu nhầm lệnh typedef là sự thay thế macro văn bản đơn giản. Định nghĩa đã cho:

Typedef char *cstring;

Kiểu của biến cstr trong khai báo sau là gì:

bên ngoài const cstring cstr;

Câu trả lời có vẻ hiển nhiên là:

Const char *ctr

Tuy nhiên, điều này là không đúng sự thật. Công cụ xác định const đề cập đến cstr, vì vậy câu trả lời đúng là một con trỏ const tới char:

Char *const cstr;

3.13. công cụ xác định dễ bay hơi

Một đối tượng được khai báo là không ổn định nếu giá trị của nó có thể thay đổi mà trình biên dịch không nhận thấy, chẳng hạn như một biến được đồng hồ hệ thống cập nhật. Trình xác định này cho trình biên dịch biết rằng nó không cần tối ưu hóa mã để hoạt động với đối tượng này.
Công cụ xác định dễ bay hơi được sử dụng tương tự như công cụ xác định const:

Dễ bay hơi int disp1ay_register; Nhiệm vụ dễ bay hơi *curr_task; int dễ bay hơi ixa[ max_size ]; Màn hình dễ bay hơi bitmap_buf;

display_register là một đối tượng dễ thay đổi kiểu int. curr_task là một con trỏ tới một đối tượng dễ bay hơi của lớp Task. ixa là một mảng số nguyên không ổn định và mỗi phần tử của mảng đó được coi là không ổn định. bitmap_buf là một đối tượng dễ bay hơi của lớp Screen, mỗi thành viên dữ liệu của nó cũng được coi là dễ bay hơi.
Mục đích duy nhất của việc sử dụng công cụ xác định dễ bay hơi là để báo cho trình biên dịch biết rằng nó không thể xác định ai có thể thay đổi giá trị của một đối tượng nhất định và bằng cách nào. Do đó, trình biên dịch không nên thực hiện tối ưu hóa mã sử dụng đối tượng này.

3.14. Cặp lớp

Lớp pair của thư viện chuẩn C++ cho phép chúng ta định nghĩa một cặp giá trị với một đối tượng nếu có bất kỳ kết nối ngữ nghĩa nào giữa chúng. Các giá trị này có thể giống nhau hoặc khác loại. Để sử dụng lớp này, bạn phải bao gồm tệp tiêu đề:

#bao gồm

Ví dụ, hướng dẫn

Đôi< string, string >tác giả("James", "Joyce");

tạo một đối tượng tác giả kiểu cặp, bao gồm hai giá trị chuỗi.
Các phần riêng lẻ của một cặp có thể thu được bằng cách sử dụng thành viên thứ nhất và thứ hai:

Chuỗi sách đầu tiên; if (Joyce.first == "James" &&
Joyce.second == "Joyce")
firstBook = "Anh hùng Stephen";

Nếu bạn cần định nghĩa một số đối tượng cùng loại của lớp này, sẽ thuận tiện hơn khi sử dụng chỉ thị typedef:

cặp Typedef< string, string >Tác giả; Tác giả proust("marcel", "proust"); Tác giả joyce("James", "Joyce"); Tác giả musil("robert", "musi1");

Đây là một ví dụ khác về việc sử dụng một cặp. Giá trị đầu tiên chứa tên của một đối tượng nào đó, giá trị thứ hai là một con trỏ tới thành phần bảng tương ứng với đối tượng này.

Khe đầu vào lớp; extern EntrySlot* 1ook_up(string); cặp typedef< string, EntrySlot* >Biểu tượngEntry; SymbolEntry current_entry("tác giả", 1ook_up("tác giả"));
// ... if (EntrySlot *it = 1ook_up("editor")) (
current_entry.first = "biên tập viên";
current_entry.second = nó;
}

(Chúng ta sẽ quay lại lớp cặp khi nói về các loại vùng chứa trong Chương 6 và về các thuật toán chung trong Chương 12.)

3.15. Các loại lớp

Cơ chế lớp cho phép bạn tạo các kiểu dữ liệu mới; với sự trợ giúp của nó, các kiểu chuỗi, vectơ, phức và cặp được thảo luận ở trên đã được giới thiệu. Trong Chương 2, chúng tôi đã giới thiệu các khái niệm và cơ chế hỗ trợ các cách tiếp cận đối tượng và hướng đối tượng, sử dụng ví dụ về cách triển khai lớp Array. Ở đây, dựa trên cách tiếp cận đối tượng, chúng ta sẽ tạo một lớp String đơn giản, việc triển khai lớp này sẽ giúp chúng ta hiểu, cụ thể là nạp chồng toán tử - chúng ta đã nói về nó trong phần 2.3. (Các lớp được trình bày chi tiết trong Chương 13, 14 và 15.) Chúng tôi đã đưa ra một mô tả ngắn gọn về lớp học để đưa ra những ví dụ thú vị hơn. Người đọc mới làm quen với C++ có thể muốn bỏ qua phần này và chờ mô tả các lớp có hệ thống hơn trong các chương sau.)
Lớp String của chúng ta phải hỗ trợ khởi tạo bởi một đối tượng của lớp String, một chuỗi ký tự và một kiểu chuỗi tích hợp cũng như thao tác gán giá trị cho các kiểu này. Chúng tôi sử dụng các hàm tạo của lớp và toán tử gán được nạp chồng cho việc này. Việc truy cập vào các ký tự Chuỗi riêng lẻ sẽ được triển khai dưới dạng thao tác lập chỉ mục quá tải. Ngoài ra, chúng ta sẽ cần: hàm size() để lấy thông tin về độ dài của chuỗi; hoạt động so sánh các đối tượng thuộc loại Chuỗi và đối tượng Chuỗi với một chuỗi thuộc loại có sẵn; cũng như các hoạt động I/O của đối tượng của chúng ta. Cuối cùng, chúng tôi triển khai khả năng truy cập vào biểu diễn bên trong của chuỗi dưới dạng loại chuỗi tích hợp.
Định nghĩa lớp bắt đầu bằng từ khóa class, theo sau là mã định danh—tên của lớp hoặc loại. Nói chung, một lớp bao gồm các phần được đặt trước bởi các từ public (mở) và riêng tư (đóng). Phần công khai thường chứa một tập hợp các thao tác được lớp hỗ trợ, được gọi là các phương thức hoặc hàm thành viên của lớp. Các hàm thành viên này xác định giao diện chung của lớp, hay nói cách khác là tập hợp các hành động có thể được thực hiện trên các đối tượng của lớp đó. Phần riêng tư thường bao gồm các thành viên dữ liệu cung cấp khả năng triển khai nội bộ. Trong trường hợp của chúng tôi, các thành viên bên trong bao gồm _string - một con trỏ tới một char, cũng như _size của kiểu int. _size sẽ lưu trữ thông tin về độ dài của chuỗi và _string sẽ là một mảng ký tự được phân bổ động. Đây là định nghĩa lớp trông như thế nào:

#bao gồm Chuỗi lớp; istream& toán tử>>(istream&, String&);
ostream& toán tử<<(ostream&, const String&); class String {
công cộng:
// tập hợp các hàm tạo
// để khởi tạo tự động
// Chuỗi strl; // Sợi dây()
// Chuỗi str2("chữ"); // Chuỗi(const char*);
// Chuỗi str3(str2); // Chuỗi(const Chuỗi&); Sợi dây();
Chuỗi(const char*);
Chuỗi(const Chuỗi&); // hàm hủy
~Chuỗi(); // toán tử gán
// strl = str2
// str3 = "một chuỗi ký tự" String& operator=(const String&);
Chuỗi& toán tử=(const char*); // toán tử kiểm tra đẳng thức
// strl == str2;
// str3 == "một chuỗi ký tự"; toán tử bool==(const String&);
toán tử bool==(const char*); // nạp chồng toán tử truy cập theo chỉ mục
// strl[ 0 ] = str2[ 0 ]; char& toán tử(int); // truy cập vào các thành viên của lớp
kích thước int() ( return _size; )
char* c_str() ( return _string; ) riêng tư:
int_size;
char *_string;
}

Lớp String có ba hàm tạo. Như đã thảo luận trong Phần 2.3, cơ chế nạp chồng cho phép bạn xác định nhiều cách triển khai hàm có cùng tên, miễn là chúng đều khác nhau về số lượng và/hoặc loại tham số của chúng. Hàm tạo đầu tiên

Nó là hàm tạo mặc định vì nó không yêu cầu giá trị ban đầu rõ ràng. Khi chúng tôi viết:

Đối với str1, hàm tạo như vậy được gọi.
Hai hàm tạo còn lại đều có một tham số. Vâng, đối với

Chuỗi str2("chuỗi ký tự");

Hàm tạo được gọi

Chuỗi(const char*);

Chuỗi str3(str2);

Người xây dựng

Chuỗi(const Chuỗi&);

Kiểu của hàm tạo được gọi được xác định bởi kiểu đối số thực tế. Hàm tạo cuối cùng, String(const String&), được gọi là hàm tạo sao chép vì nó khởi tạo một đối tượng bằng một bản sao của một đối tượng khác.
Nếu bạn viết:

Chuỗi str4(1024);

Điều này sẽ gây ra lỗi biên dịch vì không có hàm tạo nào có tham số kiểu int.
Một khai báo toán tử quá tải có định dạng sau:

Toán tử return_type op(parameter_list);

Trong đó toán tử là một từ khóa và op là một trong các toán tử được xác định trước: +, =, ==, v.v. (Xem Chương 15 để biết định nghĩa chính xác về cú pháp.) Đây là khai báo của toán tử chỉ số được nạp chồng:

Toán tử Char&(int);

Toán tử này có một tham số duy nhất kiểu int và trả về một tham chiếu đến một char. Một toán tử bị quá tải có thể bị quá tải nếu danh sách tham số của các phiên bản riêng lẻ khác nhau. Đối với lớp String, chúng ta sẽ tạo hai toán tử gán và đẳng thức khác nhau.
Để gọi hàm thành viên, hãy sử dụng toán tử truy cập thành viên dấu chấm (.) hoặc mũi tên (->). Chúng ta hãy khai báo các đối tượng kiểu String:

Đối tượng chuỗi ("Danny");
Chuỗi *ptr = Chuỗi mới("Anna");
Mảng chuỗi;
Đây là giao diện của lệnh gọi size() trên các đối tượng này:
vectơ kích cỡ(3);

// truy cập thành viên cho đối tượng (.); // các đối tượng có kích thước 5 kích cỡ[ 0 ] = object.size(); // truy cập thành viên cho con trỏ (->)
// ptr có kích thước 4
kích thước [ 1 ] = ptr->size(); // truy cập thành viên (.)
// mảng có kích thước 0
kích thước [ 2 ] = array.size();

Nó trả về 5, 4 và 0 tương ứng.
Các toán tử nạp chồng được áp dụng cho một đối tượng giống như các toán tử thông thường:

Tên chuỗi("Yadie"); Tên chuỗi2("Yodie"); // toán tử bool==(const String&)
nếu (tên == tên2)
trở lại;
khác
// Chuỗi& toán tử=(const Chuỗi&)
tên = tên2;

Khai báo hàm thành viên phải nằm trong định nghĩa lớp và định nghĩa hàm có thể nằm trong hoặc bên ngoài định nghĩa lớp. (Cả hai hàm size() và c_str() đều được định nghĩa bên trong một lớp.) Nếu một hàm được định nghĩa bên ngoài một lớp, thì chúng ta phải chỉ định, ngoài những thứ khác, nó thuộc về lớp nào. Trong trường hợp này, định nghĩa của hàm được đặt trong tệp nguồn, chẳng hạn như String.C, và định nghĩa của chính lớp đó được đặt trong tệp tiêu đề (String.h trong ví dụ của chúng tôi), tệp này phải được đưa vào nguồn :

// nội dung của file nguồn: String.C // cho phép định nghĩa lớp String
#include "String.h" // bao gồm định nghĩa của hàm strcmp()
#bao gồm
bool // kiểu trả về
String:: // lớp chứa hàm đó
operator== // tên hàm: toán tử đẳng thức
(const String &rhs) // danh sách các tham số
{
nếu (_size != rhs._size)
trả về sai;
trả về strcmp(_strinq, rhs._string) ?
sai đúng;
}

Hãy nhớ lại rằng strcmp() là một hàm thư viện chuẩn C. Nó so sánh hai chuỗi có sẵn, trả về 0 nếu các chuỗi bằng nhau và khác 0 nếu chúng không bằng nhau. Toán tử điều kiện (?:) kiểm tra giá trị trước dấu chấm hỏi. Nếu đúng, giá trị của biểu thức ở bên trái dấu hai chấm sẽ được trả về; nếu không, giá trị ở bên phải sẽ được trả về. Trong ví dụ của chúng tôi, giá trị của biểu thức là sai nếu strcmp() trả về giá trị khác 0 và đúng nếu nó trả về giá trị 0. (Toán tử điều kiện được thảo luận trong phần 4.7.)
Thao tác so sánh được sử dụng khá thường xuyên, hàm thực hiện nó hóa ra lại nhỏ nên việc khai báo hàm này tích hợp sẵn (nội tuyến) sẽ rất hữu ích. Trình biên dịch thay thế văn bản của hàm thay vì gọi nó, do đó không lãng phí thời gian cho lệnh gọi như vậy. (Các hàm dựng sẵn sẽ được thảo luận trong Phần 7.6.) Một hàm thành viên được định nghĩa trong một lớp được cài sẵn theo mặc định. Nếu nó được định nghĩa bên ngoài lớp, để khai báo nó tích hợp, bạn cần sử dụng từ khóa inline:

Bool nội tuyến String::operator==(const String &rhs) ( // điều tương tự )

Định nghĩa của hàm dựng sẵn phải có trong tệp tiêu đề chứa định nghĩa lớp. Bằng cách xác định lại toán tử == làm toán tử nội tuyến, chúng ta phải di chuyển chính văn bản hàm từ tệp String.C sang tệp String.h.
Sau đây là cách thực hiện thao tác so sánh một đối tượng String với kiểu chuỗi có sẵn:

Chuỗi bool nội tuyến::operator==(const char *s) ( return strcmp(_string, s) ? false: true; )

Tên hàm tạo giống với tên lớp. Nó được coi là không trả về một giá trị, do đó không cần chỉ định giá trị trả về trong định nghĩa hoặc trong phần nội dung của nó. Có thể có một số nhà xây dựng. Giống như bất kỳ hàm nào khác, chúng có thể được khai báo nội tuyến.

#bao gồm // hàm tạo nội tuyến mặc định String::String()
{
_kích thước = 0;
_string = 0;
) Chuỗi nội tuyến::String(const char *str) ( if (! str) ( _size = 0; _string = 0; ) else ( _size = str1en(str); strcpy(_string, str); ) // sao chép hàm tạo
Chuỗi nội tuyến::String(const String &rhs)
{
kích thước = rhs._size;
nếu (! rhs._string)
_string = 0;
khác(
_string = char mới[ _size + 1 ];
} }

Vì chúng ta cấp phát bộ nhớ động bằng toán tử new nên chúng ta cần giải phóng nó bằng cách gọi delete khi không còn cần đối tượng String nữa. Một hàm thành viên đặc biệt khác phục vụ mục đích này - hàm hủy, được gọi tự động trên một đối tượng tại thời điểm đối tượng này không còn tồn tại. (Xem Chương 7 về vòng đời của đối tượng.) Tên của hàm hủy được hình thành từ ký tự dấu ngã (~) và tên của lớp. Đây là định nghĩa của hàm hủy lớp String. Đây là nơi chúng tôi gọi thao tác xóa để giải phóng bộ nhớ được phân bổ trong hàm tạo:

Chuỗi nội tuyến: :~String() ( delete _string; )

Cả hai toán tử gán quá tải đều sử dụng từ khóa đặc biệt this.
Khi chúng tôi viết:

Chuỗi namel("orville"), name2("wilbur");
name = "Orville Wright";
đây là một con trỏ đánh địa chỉ đối tượng name1 trong phần thân hàm của thao tác gán.
this luôn trỏ đến đối tượng lớp mà qua đó hàm được gọi. Nếu như
ptr->kích thước();
obj[ 1024 ];

Khi đó bên trong size() giá trị của this sẽ là địa chỉ được lưu trong ptr. Bên trong thao tác chỉ mục, phần này chứa địa chỉ của obj. Bằng cách hủy tham chiếu this (sử dụng *this), chúng ta sẽ có được chính đối tượng đó. (Con trỏ this được mô tả chi tiết ở Phần 13.4.)

Chuỗi nội tuyến& Chuỗi::operator=(const char *s) ( if (! s) ( _size = 0; delete _string; _string = 0; ) else ( _size = str1en(s); delete _string; _string = new char[ _size + 1 ]; strcpy(_string, s); trả về *this;

Khi thực hiện thao tác gán, một lỗi thường mắc phải là họ quên kiểm tra xem đối tượng đang được sao chép có phải là đối tượng mà bản sao đang được tạo vào hay không. Chúng tôi sẽ thực hiện kiểm tra này bằng cách sử dụng cùng một con trỏ this:

Inline String& String::operator=(const String &rhs) ( // trong biểu thức // namel = *pointer_to_string // cái này đại diện cho name1, // rhs - *pointer_to_string. if (this != &rhs) (

Đây là nguyên văn đầy đủ của thao tác gán một đối tượng cùng loại cho một đối tượng String:

Chuỗi nội tuyến& Chuỗi::operator=(const String &rhs) ( if (this != &rhs) ( delete _string; _size = rhs._size; if (! rhs._string)
_string = 0;
khác(
_string = char mới[ _size + 1 ];
strcpy(_string, rhs._string);
}
}
trả lại *cái này;
}

Thao tác lấy chỉ mục gần giống với thao tác thực hiện đối với mảng Array mà chúng ta đã tạo ở phần 2.3:

#bao gồm ký tự nội tuyến &
Chuỗi::toán tử (int elem)
{
khẳng định(elem >= 0 && elem< _size);
return _string[ elem ];
}

Các toán tử đầu vào và đầu ra được triển khai dưới dạng các hàm riêng biệt chứ không phải là thành viên lớp. (Chúng ta sẽ nói về lý do của điều này trong Phần 15.2. Phần 20.4 và 20.5 nói về việc nạp chồng các toán tử đầu vào và đầu ra của thư viện iostream.) Toán tử đầu vào của chúng ta có thể đọc tối đa 4095 ký tự. setw() là một trình thao tác được xác định trước, nó đọc một số ký tự nhất định trừ 1 từ luồng đầu vào, do đó đảm bảo rằng chúng ta không làm tràn bộ đệm nội bộ inBuf. (Chương 20 thảo luận chi tiết về trình thao tác setw().) Để sử dụng các trình thao tác, bạn phải bao gồm tệp tiêu đề tương ứng:

#bao gồm inline istream& operator>>(istream &io, String &s) ( // giới hạn nhân tạo: 4096 ký tự const int 1imit_string_size = 4096; char inBuf[ limit_string_size ]; // setw() được bao gồm trong thư viện iostream // nó giới hạn kích thước của khối có thể đọc được thành 1imit_string_size -l io >> setw(1imit_string_size) >> inBuf; s = mBuf; // String::operator=(const char*);

Toán tử đầu ra cần quyền truy cập vào biểu diễn bên trong của Chuỗi. Vì toán tử<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:

Toán tử & luồng nội tuyến<<(ostream& os, const String &s) { return os << s.c_str(); }

Dưới đây là một chương trình ví dụ sử dụng lớp String. Chương trình này lấy các từ từ luồng đầu vào và đếm tổng số của chúng, cũng như số từ "the" và "it", đồng thời ghi lại các nguyên âm gặp phải.

#bao gồm #include "String.h" int main() ( int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0; / / Các từ “The” và “It”
// chúng ta sẽ kiểm tra bằng cách sử dụng operator==(const char*)
Chuỗi nhưng, the("the"), it("it"); // toán tử>>(ostream&, String&)
trong khi (cin >> buf) (
++wdCnt; // nhà điều hành<<(ostream&, const String&)
cout<< buf << " "; if (wdCnt % 12 == 0)
cout<< endl; // String::operator==(const String&) and
// String::operator==(const char*);
if (buf == the | | buf == "The")
++theCnt;
khác
if (buf == nó || buf == "Nó")
++nóCnt; // gọi String::s-ize()
vì (int ix =0; ix< buf.sizeO; ++ix)
{
// gọi String::operator(int)
switch(buf[ ix ])
{
trường hợp “a”: trường hợp “A”: ++aCnt; phá vỡ;
trường hợp “e”: trường hợp “E”: ++eCnt; phá vỡ;
trường hợp "i": trường hợp "I": ++iCnt; phá vỡ;
trường hợp "o": trường hợp "0": ++oCnt; phá vỡ;
trường hợp "u": trường hợp "U": ++uCnt; phá vỡ;
mặc định: ++notVowe1; phá vỡ;
}
}
) // nhà điều hành<<(ostream&, const String&)
cout<< "\n\n"
<< "Слов: " << wdCnt << "\n\n"
<< "the/The: " << theCnt << "\n"
<< "it/It: " << itCnt << "\n\n"
<< "согласных: " < << "a: " << aCnt << "\n"
<< "e: " << eCnt << "\n"
<< "i: " << ICnt << "\n"
<< "o: " << oCnt << "\n"
<< "u: " << uCnt << endl;
}

Hãy kiểm tra chương trình: chúng tôi sẽ cung cấp cho nó một đoạn trong câu chuyện thiếu nhi được viết bởi một trong những tác giả của cuốn sách này (chúng ta sẽ gặp câu chuyện này ở Chương 6). Đây là kết quả của chương trình:

Alice Emma có mái tóc dài màu đỏ bồng bềnh. Bố cô ấy nói khi gió thổi qua mái tóc của cô ấy, nó trông gần như sống động, giống như một con chim lửa đang bay. Anh nói với cô rằng một con chim lửa xinh đẹp, huyền diệu nhưng chưa được thuần hóa. “Bố ơi, im đi, không có chuyện đó đâu,” cô nói với ông, đồng thời muốn ông kể thêm cho cô nghe. Cô bé ngượng ngùng hỏi, "Ý con là bố có ở đó không?" Số từ: 65
cái/cái: 2
nó/Nó: 1
phụ âm: 190
một: 22
đ: 30
tôi: 24
ồ: 10
bạn: 7

Bài tập 3.26

Việc triển khai các hàm tạo và toán tử gán của chúng tôi chứa rất nhiều sự lặp lại. Hãy cân nhắc việc chuyển mã trùng lặp sang một hàm thành viên riêng biệt, như đã thực hiện trong Phần 2.3. Hãy chắc chắn rằng tùy chọn mới hoạt động.

Bài tập 3.27

Sửa đổi chương trình kiểm tra để nó cũng đếm các phụ âm b, d, f, s, t.

Bài tập 3.28

Viết hàm thành viên đếm số lần xuất hiện của một ký tự trong Chuỗi bằng cách sử dụng khai báo sau:

Chuỗi lớp ( public: // ... int count(char ch) const; // ... );

Bài tập 3.29

Triển khai toán tử nối chuỗi (+) để nó nối hai chuỗi và trả về kết quả trong một đối tượng Chuỗi mới. Đây là khai báo hàm:

Chuỗi lớp ( public: // ... String operator+(const String &rhs) const; // ... );

Trong ngôn ngữ C, có sự phân biệt giữa khái niệm “kiểu dữ liệu” và “công cụ sửa đổi kiểu”. Kiểu dữ liệu là số nguyên và bộ sửa đổi được ký hoặc không dấu. Số nguyên có dấu sẽ có cả giá trị dương và âm, trong khi số nguyên không dấu sẽ chỉ có giá trị dương. Có năm loại cơ bản trong ngôn ngữ C.

  • char – ký tự.
  • Một biến kiểu char có kích thước 1 byte, giá trị của nó là các ký tự khác nhau từ bảng mã, ví dụ: 'f', ':', 'j' (khi viết trong chương trình, chúng được đặt trong một biến duy nhất). trích dẫn).

  • int - toàn bộ.
  • Kích thước của biến kiểu int không được xác định trong tiêu chuẩn ngôn ngữ C. Trong hầu hết các hệ thống lập trình, kích thước của biến int tương ứng với kích thước của toàn bộ từ máy. Ví dụ: trong trình biên dịch dành cho bộ xử lý 16 bit, một biến kiểu int có kích thước 2 byte. Trong trường hợp này, các giá trị có dấu của biến này có thể nằm trong khoảng từ -32768 đến 32767.

  • nổi - thực.
  • Từ khóa float cho phép bạn xác định các biến có kiểu thực. Các giá trị của chúng có phần phân số cách nhau bằng dấu chấm, ví dụ: -5,6, 31,28, v.v. Số thực cũng có thể được viết dưới dạng dấu phẩy động, ví dụ: -1,09e+4. Số trước ký hiệu “e” được gọi là số mũ, và số sau “e” được gọi là số mũ. Một biến kiểu float chiếm 32 bit trong bộ nhớ. Nó có thể nhận các giá trị trong khoảng từ 3,4e-38 đến 3,4e+38.

  • double – độ chính xác gấp đôi thực tế;
  • Từ khóa double cho phép bạn xác định một biến thực có độ chính xác kép. Nó chiếm dung lượng bộ nhớ gấp đôi so với biến float. Một biến kiểu double có thể nhận các giá trị trong khoảng từ 1.7e-308 đến 1.7e+308.

  • khoảng trống - không có giá trị.
  • Từ khóa void được sử dụng để vô hiệu hóa giá trị của một đối tượng, ví dụ như để khai báo một hàm không trả về bất kỳ giá trị nào.

Các loại biến:

Các chương trình hoạt động với nhiều dữ liệu khác nhau, có thể đơn giản hoặc có cấu trúc. Dữ liệu đơn giản là số nguyên và số thực, ký hiệu và con trỏ (địa chỉ của các đối tượng trong bộ nhớ). Số nguyên không có phần phân số nhưng số thực thì có. Dữ liệu có cấu trúc là mảng và cấu trúc; chúng sẽ được thảo luận dưới đây.

Biến là một ô trong bộ nhớ máy tính có tên và lưu trữ một số giá trị. Giá trị của biến có thể thay đổi trong quá trình thực hiện chương trình. Khi một giá trị mới được ghi vào một ô, giá trị cũ sẽ bị xóa.

Đó là phong cách tốt để đặt tên biến có ý nghĩa. Tên biến có thể chứa từ một đến 32 ký tự. Được phép sử dụng chữ thường và chữ in hoa, số và dấu gạch dưới, được coi là một chữ cái trong C. Ký tự đầu tiên phải là một chữ cái. Tên biến không thể khớp với các từ dành riêng.

Nhập ký tự

char là loại tiết kiệm nhất. Kiểu char có thể được ký hoặc không dấu. Được ký hiệu là “char ký tự có dấu” (loại có dấu) và “char không dấu” (loại không dấu). Kiểu đã ký có thể lưu trữ các giá trị trong phạm vi -128 đến +127. Chưa ký - từ 0 đến 255. 1 byte bộ nhớ (8 bit) được phân bổ cho biến char.

Các từ khóa có dấu và không dấu cho biết cách diễn giải bit 0 của biến được khai báo, tức là nếu từ khóa không dấu được chỉ định, thì bit 0 được hiểu là một phần của số, nếu không thì bit 0 được hiểu là có dấu.

Kiểu int

Giá trị nguyên int có thể ngắn hoặc dài. Từ khóa ngắn được đặt sau từ khóa đã ký hoặc chưa ký. Vì vậy có các loại: int ngắn có dấu, int ngắn không dấu, int dài có dấu, int dài không dấu.

Một biến thuộc loại signature short int (số nguyên ngắn có dấu) có thể lấy các giá trị từ -32768 đến +32767, unsigned short int (số nguyên ngắn không dấu) - từ 0 đến 65535. Mỗi biến trong số chúng được phân bổ chính xác hai byte bộ nhớ (16 chút ít).

Khi khai báo một biến kiểu signature short int, từ khóa signature và short có thể bị bỏ qua, và kiểu biến đó có thể được khai báo đơn giản là int. Cũng có thể khai báo loại này bằng một từ khóa ngắn gọn.

Biến int unsigned short có thể được khai báo là unsigned int hoặc unsigned short.

Đối với mỗi giá trị int long int hoặc unsigned long int, 4 byte bộ nhớ (32 bit) được phân bổ. Giá trị của các biến thuộc loại này có thể nằm trong khoảng từ -2147483648 đến 2147483647 và từ 0 đến 4294967295 tương ứng.

Ngoài ra còn có các biến kiểu int dài dài, trong đó 8 byte bộ nhớ được phân bổ (64 bit). Chúng có thể được ký hoặc không được ký. Đối với loại đã ký, phạm vi giá trị là từ -9223372036854775808 đến 9223372036854775807, đối với loại không dấu - từ 0 đến 18446744073709551615. Loại đã ký cũng có thể được khai báo đơn giản bằng hai từ khóa dài và dài.

Kiểu Phạm vi phạm vi hex Kích cỡ
ký tự không dấu 0 … 255 0x00...0xFF 8 bit
ký tự đã ký
hoặc đơn giản
ký tự
-128 … 127 -0x80…0x7F 8 bit
int ngắn không dấu
hoặc đơn giản
int không dấu hoặc ngắn không dấu
0 … 65535 0x0000…0xFFFF 16 bit
int ngắn đã ký hoặc int đã ký
hoặc đơn giản
ngắn hoặc int
-32768 … 32767 0x8000…0x7FFF 16 bit
int dài không dấu
hoặc đơn giản
dài không dấu
0 … 4294967295 0x00000000 … 0xFFFFFFFF 32bit
ký dài
hoặc đơn giản
dài
-2147483648 … 2147483647 0x80000000 … 0x7FFFFFFF 32bit
không dấu dài dài 0 … 18446744073709551615 0x0000000000000000 … 0xFFFFFFFFFFFFFFFF 64bit
ký dài dài
hoặc đơn giản
dài dài
-9223372036854775808 … 9223372036854775807 0x8000000000000000 … 0x7FFFFFFFFFFFFFF 64bit

Khai báo biến

Các biến được khai báo trong câu lệnh khai báo. Câu lệnh khai báo bao gồm đặc tả kiểu và danh sách tên biến được phân tách bằng dấu phẩy. Phải có dấu chấm phẩy ở cuối.

[sửa đổi] mã định danh type_specifier [, mã định danh] ...

Công cụ sửa đổi – từ khóa đã ký, không dấu, ngắn, dài.
Công cụ xác định loại là từ khóa char hoặc int chỉ định loại biến được khai báo.
Mã định danh là tên của biến.

Char x; int a, b, c; không dấu dài dài y;

Khi được khai báo, một biến có thể được khởi tạo, nghĩa là được gán một giá trị ban đầu.

Int x = 100;

Khi khai báo, biến x sẽ chứa ngay số 100. Tốt nhất nên khai báo các biến khởi tạo trên các dòng riêng biệt.

Lập trình viên nói:

Xin chào! Tôi đã đọc bài viết của bạn. Tôi đã rất buồn và buồn cười cùng một lúc. Cụm từ này của bạn đặc biệt hấp dẫn: "Vì một biến kiểu char thường được sử dụng như một mảng nên số lượng giá trị có thể có sẽ được xác định." 😆 😆 😆
Tôi không cười nhạo bạn. Tạo một trang web thực sự là một kỳ công. Tôi chỉ muốn hỗ trợ bạn lời khuyên và chỉ ra một số sai sót.

1. Giá trị của biến kiểu char được gán như sau:

Đây:

Char a = *"A";

Con trỏ tới mảng bị hủy địa chỉ và kết quả là giá trị của phần tử đầu tiên của mảng được trả về, tức là. 'MỘT'

2. Việc reset xảy ra như sau:

Char a = NULL;
ký tự b = ();

// Và đây là cách xóa dòng trong phần thân chương trình

"" - ký hiệu này được gọi là dấu kết thúc số 0. Nó được đặt ở cuối dòng. Chính bạn mà không hề biết đã điền vào mảng s1 trong bài viết của mình bằng ký hiệu này. Nhưng chỉ có thể gán ký hiệu này cho phần tử 0 của mảng.

3. Hãy thoải mái sử dụng thuật ngữ.
Dấu = là phép gán.
Dấu * là thao tác khử địa chỉ.
Ý tôi là đoạn này của bài viết: “Mọi thứ hóa ra đơn giản đến vậy, trước dấu = bạn phải đặt dấu * và bạn phải khai báo số phần tử (số 0 tương ứng với số đầu tiên)”

Đừng hiểu sai ý tôi, bài viết không thể tồn tại ở dạng hiện tại. Đừng lười biếng, hãy viết lại nó.
Bạn có một trách nhiệm lớn lao! Tôi nghiêm túc đấy. Các trang trên trang web của bạn được đưa vào trang đầu tiên của kết quả Yandex. Nhiều người đã bắt đầu lặp lại sai lầm của bạn.

Chúc may mắn! Bạn có thể làm được!

:
Tôi biết điều này từ lâu rồi, chỉ là đọc đi đọc lại 200 bài liên tục để sửa cái gì đó cũng khó. Và một số kiểu thô lỗ viết theo cách mà ngay cả khi biết điều gì là tốt nhất để sửa, họ cũng không có ý định sửa nó.

Tôi sẽ vui lòng sửa các lỗi khác. sửa bất kỳ điểm không chính xác nào nếu chúng xuất hiện. Tôi đánh giá cao sự giúp đỡ của bạn. cảm ơn bạn. Tôi đã biết điều này từ lâu, thật khó để đọc lại 200 bài viết liên tục để sửa một cái gì đó. Và một số kiểu thô lỗ viết theo cách mà ngay cả khi biết điều gì là tốt nhất để sửa, họ cũng không có ý định sửa nó.
Với char b = (); Đây không phải là số 0 chút nào. Ít nhất tôi sẽ kiểm tra nó.
nếu chúng ta nói về ký tự null "" ; Tôi biết rõ khi tôi lấp đầy đường kẻ đó rằng mục tiêu là thể hiện sự làm sạch thực sự chứ không phải thứ gì đó có thể nhìn thấy bằng mắt, bởi vì đường kẻ này bao gồm rác, đôi khi gây cản trở. Bạn nên cẩn thận hơn với các thuật ngữ, "ký hiệu kết thúc null" hoặc chỉ "ký hiệu null", không phải dấu kết thúc))) Và biểu tượng kết thúc nghe có vẻ hay.

Tôi sẽ hiện đại hóa bài viết, nhưng tôi sẽ không chuyển sang phong cách của người khác. Nếu tôi nghĩ rằng người mới bắt đầu hiểu nó theo cách này dễ dàng hơn là theo cách họ muốn, thì tôi sẽ để nó như vậy. Bạn cũng đừng hiểu lầm tôi. Đối với người mới bắt đầu yếu, từ “dấu hiệu” dễ hiểu và dễ nhớ hơn nhiều so với định nghĩa và tên của từng dấu hiệu. Không có sai lầm nào cả, đó là một dấu hiệu - một dấu hiệu. Ít nhấn mạnh hơn vào cái này sẽ nhấn mạnh hơn vào cái kia.

Tôi sẽ vui lòng sửa các lỗi khác. sửa bất kỳ điểm không chính xác nào nếu chúng xuất hiện. Tôi đánh giá cao sự giúp đỡ của bạn. Cảm ơn.

Xin chào lần nữa!
Tôi muốn làm rõ. Thuật ngữ “zero-terminator” (terminator trong tiếng Anh limiter) được giáo viên của tôi ở trường đại học sử dụng. Rõ ràng đây là trường học cũ!
Đối với việc đặt lại hàng.
ký tự b = (); Đây thực sự là một sự thiết lập lại. Toàn bộ mảng chứa đầy số không. Nếu bạn không tin tôi, hãy kiểm tra nó!
Nếu chúng ta xem xét một dòng theo nghĩa tự nhiên, hàng ngày của nó, thì một dòng “trống” sẽ là một dòng không có một ký tự nào. Do đó, trong 99,9% trường hợp, việc đặt ký tự null ở đầu là đủ. Thông thường, một chuỗi được xử lý tối đa ký tự null đầu tiên và những ký tự nào theo sau chuỗi đó không còn quan trọng nữa. Tôi hiểu rằng bạn muốn đặt lại dòng. Tôi vừa quyết định đưa ra một lựa chọn cổ điển đã được thử nghiệm theo thời gian.

:
Khi “Thông thường quá trình xử lý chuỗi tiến tới ký tự rỗng đầu tiên và những ký tự nào theo sau nó không còn quan trọng nữa” - vâng, chuỗi bị rỗng
Nếu chúng ta xem xét "số 0 thực sự của tất cả các ô trong một hàng (mà tôi đã viết)" - không, không phải số 0 và ngay cả ký tự đầu tiên cũng không bằng 0. Tôi đã kiểm tra tùy chọn này. MinGW(CodeBlock) - toàn bộ mảng cung cấp ký tự "a"
Tôi không nghĩ đây là lý do để tranh luận.