Ngôn ngữ lập trình C++
Chương Mảng, trỏ, tham chiếu Chương giới thiệu mảng, trỏ, kiểu liệu tham chiếu minh họa cách dùng chúng để định nghĩa biến Mảng (array) gồm tập đối tượng (được gọi phần tử) tất chúng có kiểu xếp liên tiếp nhớ Nói chung có mảng có tên đại diện khơng phải phần tử Mỗi phần tử xác định số biểu thị vị trí phần tử mảng Số lượng phần tử mảng gọi kích thước mảng Kích thước mảng cố định phải xác định trước; khơng thể thay đổi suốt trình thực chương trình Mảng đại diện cho liệu hỗn hợp gồm nhiều hạng mục riêng lẻ tương tự Ví dụ: danh sách tên, bảng thành phố giới với nhiệt độ chúng, giao dịch hàng tháng tài khoản ngân hàng Con trỏ (pointer) đơn giản địa đối tượng nhớ Thơng thường, đối tượng truy xuất hai cách: trực tiếp tên đại diện gián tiếp thông qua trỏ Các biến trỏ định nghĩa trỏ tới đối tượng kiểu cụ thể cho trỏ hủy vùng nhớ mà đối tượng chiếm giữ thu hồi Các trỏ thường dùng cho việc tạo đối tượng động thời gian thực thi chương trình Khơng giống đối tượng bình thường (tồn cục cục bộ) cấp phát lưu trữ runtime stack, đối tượng động cấp phát vùng nhớ từ vùng lưu trữ khác gọi heap Các đối tượng không tuân theo luật phạm vi thông thường Phạm vi chúng điều khiển rõ ràng lập trình viên Tham chiếu (reference) cung cấp tên tượng trưng khác gọi biệt hiệu (alias) cho đối tượng Truy xuất đối tượng thông qua tham chiếu giống truy xuất thơng qua tên gốc Tham chiếu nâng cao tính hữu dụng trỏ tiện lợi việc truy xuất trực tiếp đối tượng Chúng sử dụng để hỗ trợ kiểu gọi thông qua tham chiếu tham số hàm đặc biệt đối tượng lớn truyền tới hàm Chapter 5: Mảng, trỏ, tham chiếu 59 5.1 Mảng (Array) Biến mảng định nghĩa cách đặc tả kích thước mảng kiểu phần tử Ví dụ mảng biểu diễn 10 thước đo chiều cao (mỗi phần tử số nguyên) định nghĩa sau: int heights[10]; Mỗi phần tử mảng truy xuất thông qua số mảng Phần tử mảng ln có số Vì thế, heights[0] heights[9] biểu thị tương ứng cho phần tử đầu phần tử cuối mảng heights Mỗi phần tử mảng heights xem biến số ngun Vì thế, ví dụ để đặt phần tử thứ ba tới giá trị 177 viết: heights[2] = 177; Việc cố gắng truy xuất phần tử mảng khơng tồn (ví dụ, heights[-1] heights[10]) dẫn tới lỗi thực thi nghiêm trọng (được gọi lỗi ‘vượt biên’) Việc xử lý mảng thường liên quan đến vòng lặp duyệt qua phần tử mảng phần tử Danh sách 5.1 minh họa điều việc sử dụng hàm nhận vào mảng số nguyên trả giá trị trung bình phần tử mảng Danh sách 5.1 const int size = 3; double Average (int nums[size]) { double average = 0; } for (register i = 0; i < size; ++i) average += nums[i]; return average/size; Giống biến khác, mảng có khởi tạo Các dấu ngoặc nhọn sử dụng để đặc tả danh sách giá trị khởi tạo phân cách dấu phẩy cho phần tử mảng Ví dụ, int nums[3] = {5, 10, 15}; khởi tạo ba phần tử mảng nums tương ứng tới 5, 10, 15 Khi số giá trị khởi tạo nhỏ số phần tử phần tử cịn lại khởi tạo tới 0: int nums[3] = {5, 10}; // nums[2] khởi tạo tới Chapter 5: Mảng, trỏ, tham chiếu 60 Khi khởi tạo sử dụng hoàn tất kích cỡ mảng trở thành dư thừa số phần tử ẩn khởi tạo Vì định nghĩa nums viết tương đương sau: int nums[] = {5, 10, 15}; // khơng cần khai báo tường minh // kích cỡ mảng Một tình khác mà kích cỡ bỏ qua mảng tham số hàm Ví dụ, hàm Average cải tiến cách viết lại cho kích cỡ mảng nums không cố định tới mà định tham số thêm vào Danh sách 5.2 minh họa điều Danh sách 5.2 double Average (int nums[], int size) { double average = 0; } for (register i = 0; i < size; ++i) average += nums[i]; return average/size; Một chuỗi C++ mảng ký tự Ví dụ, char str[] = "HELLO"; định nghĩa chuỗi str mảng ký tự: năm chữ ký tự null Ký tự kết thúc null chèn vào trình biên dịch Trái lại, char str[] = {'H', 'E', 'L', 'L', 'O'}; định nghĩa str mảng ký tự Kích cỡ mảng tính cách dễ dàng nhờ vào tồn tử sizeof Ví dụ, với mảng ar cho mà kiểu phần tử Type kích cỡ ar là: sizeof(ar) / sizeof(Type) 5.2 Mảng đa chiều Mảng có chiều (nghĩa là, hai, ba, cao hơn.Việc tổ chức mảng nhớ tương tự khơng có thay đổi (một chuỗi liên tiếp phần tử) cách tổ chức mà lập trình viên lĩnh hội lại khác Ví dụ muốn biểu diễn nhiệt độ trung bình theo mùa cho ba thành phố Úc (xem Bảng 5.1) Chapter 5: Mảng, trỏ, tham chiếu 61 Bảng 5.1 Nhiệt độ trung bình theo mùa Mùa xuân 26 24 28 Sydney Melbourne Brisbane Mùa hè 34 32 38 Mùa thu 22 19 25 Mùa đông 17 13 20 Điều biểu diễn mảng hai chiều mà phần tử mảng số nguyên: int seasonTemp[3][4]; Cách tổ chức mảng nhớ 12 phần tử số nguyên liên tiếp Tuy nhiên, lập trình viên tưởng tượng mảng gồm ba hàng với hàng có bốn phần tử số ngun (xem Hình 5.1) Hình 5.1 Cách tổ chức seasonTemp nhớ 26 34 22 17 24 First đầu row hàng 32 19 13 hàng hai Second row 28 38 25 20 Third row hàng ba Như trước, phần tử truy xuất thông qua số mảng Một số riêng biệt cần cho mảng Ví dụ, nhiệt độ mùa hè trung bình thành phố Sydney (hàng cột thứ hai) cho seasonTemp[0][1] Mảng khởi tạo cách sử dụng khởi tạo lồng nhau: int seasonTemp[3][4] = { {26, 34, 22, 17}, {24, 32, 19, 13}, {28, 38, 25, 20} }; Bởi điều ánh xạ tới mảng chiều gồm 12 phần tử nhớ nên tương đương với: int seasonTemp[3][4] = { 26, 34, 22, 17, 24, 32, 19, 13, 28, 38, 25, 20 }; Bộ khởi tạo lồng ưa chuộng linh hoạt dễ hiểu Ví dụ, khởi tạo phần tử hàng phần lại mặc định 0: int seasonTemp[3][4] = {{26}, {24}, {28}}; Chúng ta bỏ qua chiều dẫn xuất từ khởi tạo: int seasonTemp[][4] = { {26, 34, 22, 17}, {24, 32, 19, 13}, Chapter 5: Mảng, trỏ, tham chiếu 62 }; {28, 38, 25, 20} Xử lý mảng nhiều chiều tương tự mảng chiều phải xử lý vịng lặp lồng thay vịng lặp đơn Danh sách 5.3 minh họa điều cách trình bày hàm để tìm nhiệt độ cao mảng seasonTemp Danh sách 5.3 const int rows const int columns = 3; = 4; int seasonTemp[rows][columns] = { {26, 34, 22, 17}, {24, 32, 19, 13}, {28, 38, 25, 20} }; int HighestTemp (int temp[rows][columns]) { 10 int highest = 0; 11 12 13 14 15 16 } for (register i = 0; i < rows; ++i) for (register j = 0; j < columns; ++j) if (temp[i][j] > highest) highest = temp[i][j]; return highest; 5.3 Con trỏ Con trỏ đơn giản địa vị trí nhớ cung cấp cách gián tiếp để truy xuất liệu nhớ Biến trỏ định nghĩa để “trỏ tới” liệu thuộc kiểu liệu cụ thể Ví dụ, int char *ptr1; *ptr2; // trỏ tới int // trỏ tới char Giá trị biến trỏ địa mà trỏ tới Ví dụ, với định nghĩa có int num; viết: ptr1 = # Ký hiệu & toán tử lấy địa chỉ; nhận biến đối số trả địa nhớ biến Tác động việc gán địa Chapter 5: Mảng, trỏ, tham chiếu 63 num khởi tạo tới ptr1 Vì thế, nói ptr1 trỏ tới num Hình 5.2 minh họa sơ lược điều Hình 5.2 Một trỏ số nguyên đơn giản ptr1 num Với ptr1 trỏ tới num biểu thức *ptr1 nhận giá trị biến ptr1 trỏ tới tương đương với num Ký hiệu * toán tử lấy giá trị; nhận trỏ đối số trả nội dung vị trí mà trỏ trỏ tới Thơng thường kiểu trỏ phải khớp với kiểu liệu mà trỏ tới Tuy nhiên, trỏ kiểu void* hợp với tất kiểu Điều thật thuận tiện để định nghĩa trỏ trỏ đến liệu kiểu khác kiểu liệu gốc khơng biết Con trỏ ép (chuyển kiểu) thành kiểu khác Ví dụ, ptr2 = (char*) ptr1; chuyển trỏ ptr1 thành trỏ char trước gán tới trỏ ptr2 Khơng quan tâm đến kiểu trỏ gán tới giá trị null (gọi trỏ null) Con trỏ null sử dụng để khởi tạo cho trỏ tạo điểm kết thúc cho cấu trúc dựa trỏ (ví dụ, danh sách liên kết) 5.4 Bộ nhớ động Ngoài vùng nhớ stack chương trình (thành phần sử dụng để lưu trữ biến toàn cục khung stack cho lời gọi hàm), vùng nhớ khác gọi heap cung cấp Heap sử dụng cho việc cấp phát động khối nhớ thời gian thực thi chương trình Vì heap gọi nhớ động (dynamic memory) Vùng nhớ stack chương trình gọi nhớ tĩnh (static memory) Có hai tốn tử sử dụng cho việc cấp phát thu hồi khối nhớ heap Toán tử new nhận kiểu đối số cấp phát khối nhớ cho đối tượng kiểu Nó trả trỏ tới khối cấp phát Ví dụ, int *ptr = new int; char *str = new char[10]; cấp phát tương ứng khối cho lưu trữ số nguyên khối đủ lớn cho lưu trữ mảng 10 ký tự Chapter 5: Mảng, trỏ, tham chiếu 64 Bộ nhớ cấp phát từ heap không tuân theo luật phạm vi biến thơng thường Ví dụ, void Foo (void) { char *str = new char[10]; // } Foo trả biến cục str thu hồi khối nhớ trỏ tới str khơng Các khối nhớ cịn chúng giải phóng rõ ràng lập trình viên Tốn tử delete sử dụng để giải phóng khối nhớ cấp phát new Nó nhận trỏ đối số giải phóng khối nhớ mà trỏ tới Ví dụ: delete ptr; delete [] str; // xóa đối tượng // xóa mảng đối tượng Chú ý khối nhớ xóa mảng cặp dấu [] phải chèn vào để định công việc Sự quan trọng giải thích sau thảo luận lớp Toán tử delete nên áp dụng tới trỏ mà trỏ tới thứ đối tượng cấp phát động (ví dụ, biến stack), lỗi thực thi nghiêm trọng xảy Hồn tồn vơ hại áp dụng delete tới biến không trỏ Các đối tượng động sử dụng để tạo liệu kéo dài tới lời gọi hàm tạo chúng Danh sách 5.4 minh họa điều cách sử dụng hàm nhận tham số chuỗi trả chuỗi Danh sách 5.4 #include char* CopyOf (const char *str) { char *copy = new char[strlen(str) + 1]; } strcpy(copy, str); return copy; Chú giải Đây tập tin header chuỗi chuẩn khai báo dạng hàm cho thao tác chuỗi Hàm strlen (được khai báo thư viện string.h) đếm ký tự đối số chuỗi (nhưng khơng vượt q) ký tự null sau Bởi ký tự null khơng tính vào việc đếm nên cộng thêm tới tổng cấp phát mảng ký tự kích thước Chapter 5: Mảng, trỏ, tham chiếu 65 Hàm strcpy (được khai báo thư viện string.h) chép đối số thứ hai đến đối số thứ theo ký tự bao gồm ln ký tự null sau Vì tài nguyên nhớ có giới hạn nên nhớ động bị cạn kiệt thời gian thực thi chương trình, đặc biệt nhiều khối lớn cấp phát khơng có giải phóng Tốn tử new khơng thể cấp phát khối có kích thước u cầu trả Chính lập trình viên phải chịu trách nhiệm giải vấn đề Cơ chế điều khiển ngoại lệ C++ cung cấp cách thức thực tế giải vấn đề 5.5 Tính tốn trỏ Trong C++ thực cộng hay trừ số nguyên trỏ Điều thường xuyên sử dụng lập trình viên gọi tính tốn trỏ Tính tốn trỏ khơng giống tính tốn số ngun kết phụ thuộc vào kích thước đối tượng trỏ tới Ví dụ, kiểu int biểu diễn byte Bây có char *str = "HELLO"; int nums[] = {10, 20, 30, 40}; int *ptr = &nums[0]; // trỏ tới phần tử str++ tăng str lên char (nghĩa byte) cho trỏ tới ký tự thứ hai chuỗi "HELLO" ngược lại ptr++ tăng ptr lên int (nghĩa bytes) cho trỏ tới phần tử thứ hai nums Hình 5.3 minh họa sơ lược điều Hình 5.3 Tính tốn trỏ H E L L O \0 10 20 30 40 ptr str ptr++ str++ Vì thế, phần tử chuỗi "HELLO" tham khảo tới *str, *(str + 1), *(str + 2), vâng Tương tự, phần tử nums tham khảo tới *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3) Một hình thức khác tính toán trỏ cho phép C++ liên quan đến trừ hai trỏ kiểu Ví dụ: int *ptr1 = &nums[1]; int *ptr2 = &nums[3]; int n = ptr2 - ptr1; // n trở thành Chapter 5: Mảng, trỏ, tham chiếu 66 Tính tốn trỏ cần khéo léo xử lý phần tử mảng Danh sách 5.5 trình bày ví dụ hàm chép chuỗi tương tự hàm định nghĩa sẵn strcpy Danh sách 5.5 void CopyString (char *dest, char *src) { while (*dest++ = *src++) ; } Chú giải Điều kiện vòng lặp gán nội dung chuỗi src cho nội dung chuỗi dest sau tăng hai trỏ Điều kiện trở thành ký tự null kết thúc chuỗi src chép tới chuỗi dest Một biến mảng (như nums) địa phần tử mảng mà đại diện Vì phần tử mảng nums tham khảo tới cách sử dụng tính tốn trỏ nums, nghĩa nums[i] tương đương với *(nums + i) Khác nums ptr chỗ nums khơng thể tạo để trỏ tới thứ ptr biến tạo để trỏ tới số nguyên Danh sách 5.6 trình bày hàm HighestTemp (đã trình bày trước Danh sách 5.3) cải tiến cách sử dụng tính tốn trỏ Danh sách 5.6 int HighestTemp (const int *temp, const int rows, const int columns) { int highest = 0; } for (register i = 0; i < rows; ++i) for (register j = 0; j < columns; ++j) if (*(temp + i * columns + j) > highest) highest = *(temp + i * columns + j); return highest; Chú giải Thay truyền mảng tới hàm, truyền trỏ int hai tham số thêm vào đặc tả kích cỡ mảng Theo cách hàm khơng bị hạn chế tới kích thước mảng cụ thể Biểu thức *(temp + i * columns + j) tương đương với temp[i][j] phiên hàm trước Chapter 5: Mảng, trỏ, tham chiếu 67 Hàm HighestTemp đơn giản hóa cách xem temp mảng chiều row * column số nguyên Điều trình bày Danh sách 5.7 Danh sách 5.7 int HighestTemp (const int *temp, const int rows, const int columns) { int highest = 0; } for (register i = 0; i < rows * columns; ++i) if (*(temp + i) > highest) highest = *(temp + i); return highest; 5.6 Con trỏ hàm Chúng ta lấy địa hàm lưu vào trỏ hàm Sau trỏ sử dụng để gọi gián tiếp hàm Ví dụ, int (*Compare)(const char*, const char*); định nghĩa trỏ hàm tên Compare giữ địa hàm nhận hai trỏ ký tự đối số trả số nguyên Ví dụ hàm thư viện so sánh chuỗi strcmp thực Vì thế: Compare = &strcmp; // Compare trỏ tới hàm strcmp Toán tử & khơng cần thiết bỏ qua: Compare = strcmp; // Compare trỏ tới hàm strcmp Một lựa chọn khác trỏ định nghĩa khởi tạo lần: int (*Compare)(const char*, const char*) = strcmp; Khi địa hàm gán tới trỏ hàm hai kiểu phải khớp với Định nghĩa hợp lệ hàm strcmp có nguyên mẫu hàm khớp với hàm int strcmp(const char*, const char*); Với định nghĩa Compare hàm strcmp gọi trực tiếp gọi gián tiếp thơng qua Compare Ba lời gọi hàm sau tương đương: strcmp("Tom", "Tim"); (*Compare)("Tom", "Tim"); Compare("Tom", "Tim"); // gọi trực tiếp // gọi gián tiếp // gọi gián tiếp (ngắn gọn) Cách sử dụng chung trỏ hàm truyền đối số tới hàm khác; thơng thường hàm sau yêu cầu phiên khác hàm trước tình khác Một ví dụ dễ hiểu hàm tìm Chapter 5: Mảng, trỏ, tham chiếu 68 kiếm nhị phân thông qua mảng xếp chuỗi Hàm sử dụng hàm so sánh (như strcmp) để so sánh chuỗi tìm kiếm ngược lại chuỗi mảng Điều khơng thích hợp tất trường hợp Ví dụ, hàm strcmp phân biệt chữ hoa hay chữ thường Nếu thực tìm kiếm theo cách khơng phân biệt dạng chữ sau hàm so sánh khác cần Như trình bày Danh sách 5.8 cách hàm so sánh tham số hàm tìm kiếm, làm cho hàm tìm kiếm độc lập với hàm so sánh Danh sách 5.8 int BinSearch (char *item, char *table[], int n, int (*Compare)(const char*, const char*)) { int bot = 0; int top = n - 1; int mid, cmp; 10 11 12 13 14 15 16 17 } while (bot