Tài liệu ôn tập C/C++ Phạm Thanh Trà Ngày 10 tháng 7 năm 2026 1 Các thành phần của ngôn ngữ lập trình C/C++ Phần này là phần nền tảng của môn học. Với hình thức thi trắc nghiệm, nội dung thường không đi vào thuật toán dài mà tập trung vào các bẫy cú pháp, quy tắc đặt tên, kiểu dữ liệu, thứ tự ưu tiên phép toán và dự đoán kết quả in ra màn hình. Mục tiêu ôn tập: Nhận diện nhanh định danh hợp lệ, phân biệt kiểu dữ liệu và hằng số, xử lý chính xác các phép toán dễ nhầm, đọc đúng biểu thức theo thứ tự ưu tiên và tránh lỗi định dạng khi dùng printf() hoặc scanf() 1.1 Cạm bẫy về từ khóa và định danh Dạng câu hỏi phổ biến là cho một danh sách tên biến rồi yêu cầu chọn tên không hợp lệ. Cần nhớ chắc các quy tắc sau: • C/C++ phân biệt chữ hoa và chữ thường. Ví dụ, BaiTap và baitap là hai định danh hoàn toàn khác nhau. • Không được dùng từ khóa để đặt tên. Các từ dành riêng như int , char , if , while , return không được dùng làm tên biến, tên hàm hoặc tên hằng. • Định danh không được bắt đầu bằng chữ số. Ví dụ, 1A là sai, còn A1 là hợp lệ. • Định danh không được chứa ký tự đặc biệt hoặc khoảng trắng. Các tên như PI$ , Giai pt , a.b đều không hợp lệ. • Chỉ nên dùng chữ cái, chữ số và dấu gạch dưới. Ví dụ, tong_diem , x2 , _temp là các dạng thường gặp. 1 1.2 Cạm bẫy về kiểu dữ liệu và hằng số Nháy đơn và nháy kép Hằng ký tự dùng nháy đơn, còn hằng chuỗi dùng nháy kép. Vì vậy, ’A’ khác hoàn toàn "A" 1 char c = ’A’; // Hang ky tu 2 char s[] = "A"; // Chuoi gom ’A’ va ky tu ket thuc ’\0’ Kích thước kiểu dữ liệu Kiểu dữ liệu Kích thước Ghi nhớ nhanh char 1 byte Lưu một ký tự short 2 bytes Số nguyên ngắn int 2 bytes long 4 bytes Số nguyên dài float 4 bytes Số thực độ chính xác đơn double 8 bytes Số thực độ chính xác kép Với int 2 bytes, miền giá trị là − 32768 ≤ int ≤ 32767 Giá trị đúng/sai ngầm định Trong C/C++, giá trị 0 được xem là sai. Mọi giá trị khác 0 , kể cả 1 , 2 , 2.5 hoặc -5 , đều được xem là đúng. Nhận diện hệ đếm • Số bắt đầu bằng 0 là hệ bát phân. Ví dụ, 017 là số bát phân. • Số bắt đầu bằng 0x hoặc 0X là hệ thập lục phân. Ví dụ, 0x41 và 0XF là số hexa. 1.3 Các phép toán tử huyệt 2 Chia hai số nguyên Khi cả hai toán hạng đều là số nguyên, phép chia chỉ lấy phần nguyên: 1 3 / 2 // Ket qua: 1 2 5 / 2 // Ket qua: 2 3 1 / 2 // Ket qua: 0 Muốn nhận kết quả là số thực, phải có ít nhất một toán hạng là số thực hoặc ép kiểu rõ ràng: 1 ( float )3 / 2 // Ket qua: 1.5 2 3.0 / 2 // Ket qua: 1.5 Chia lấy dư Toán tử % chỉ dùng cho số nguyên. Biểu thức như 5.5 % 2 là sai cú pháp vì có toán hạng số thực. Toán tử tăng/giảm Cách viết Ý nghĩa Ví dụ ++x Tăng trước, dùng sau x=10; y=++x; cho x=11, y=11 x++ Dùng trước, tăng sau x=10; y=x++; cho y=10, x=11 --x Giảm trước, dùng sau Tương tự ++x x-- Dùng trước, giảm sau Tương tự x++ Toán tử phẩy Toán tử phẩy tính các biểu thức từ trái sang phải, nhưng giá trị cuối cùng của toàn bộ biểu thức là giá trị của biểu thức ngoài cùng bên phải. 1 x = (a++, b = b + 2); Trong ví dụ trên, a++ vẫn được thực hiện, nhưng x nhận giá trị của biểu thức b = b + 2 3 Toán tử điều kiện ba ngôi Cú pháp cần nhớ: 1 dieu_kien ? gia_tri_khi_dung : gia_tri_khi_sai Ví dụ: 1 s1 = (1 > 2) ? 2912 : 1706; Vì 1 > 2 là sai nên s1 nhận giá trị 1706 Toán tử sizeof Toán tử sizeof trả về số byte của một kiểu dữ liệu hoặc một biểu thức. Ví dụ, sizeof ( long ) trả về 4 1.4 Thứ tự ưu tiên phép toán Khi đề bài cho một biểu thức dài không có ngoặc, cần đọc theo thứ tự ưu tiên sau: Mức Nhóm toán tử Ví dụ 1 Ngoặc, truy xuất () , [] 2 Toán tử một ngôi !, ++ , -- , - , sizeof 3 Nhân, chia, chia dư * , / , % 4 Cộng, trừ + , - 5 Quan hệ > , < , >= , <= 6 So sánh bằng/khác == , != 7 Logic AND && 8 Logic OR || 9 Gán = , += , *= Ví dụ, biểu thức a > 1 && b < 2 được hiểu là (a > 1) && (b < 2) vì các toán tử quan hệ > và < có độ ưu tiên cao hơn toán tử logic && 1.5 Cạm bẫy xuất/nhập dữ liệu 4 Đặc tả định dạng trong printf và scanf Đặc tả Dùng cho Ghi nhớ %d Số nguyên int %f Số thực float khi in bằng printf %lf Số thực double khi nhập bằng scanf %c Ký tự char %s Chuỗi ký tự Mảng ký tự %x , %X Hệ thập lục phân Chữ hexa thường hoặc hoa Lỗi kinh điển khi dùng scanf Khi nhập vào một biến thông thường bằng scanf() , cần truyền địa chỉ của biến, tức là phải có dấu & trước tên biến. 1 int n; 2 scanf("%d", n); // Sai: thieu dia chi cua n 3 scanf("%d", &n); // Dung Mẹo: Hễ thấy biểu thức chia hai số nguyên như 5/2 hoặc 1/2 , hãy nhớ kết quả luôn bị chặt cụt phần thập phân. Vì vậy, hai biểu thức này lần lượt cho kết quả 2 và 0 , không phải 2.5 và 0.5 2 Cấu trúc điều khiển trong C/C++ Cấu trúc điều khiển là phần quyết định luồng chạy của chương trình. Trong bài thi trắc nghiệm, chương này thường xuất hiện dưới dạng đọc hiểu code, dự đoán kết quả in ra màn hình hoặc phát hiện lỗi cú pháp do thiếu ngoặc, thừa dấu chấm phẩy và hiểu sai thứ tự chạy của vòng lặp. Mục tiêu ôn tập: Phân biệt chính xác lệnh đơn và khối lệnh, đọc đúng if ... else , switch ... case , for , while , do ... while và nắm tác dụng của break , continue , goto trong từng ngữ cảnh. 2.1 Cạm bẫy cấu trúc rẽ nhánh if...else 5 Thiếu ngoặc nhọn Nếu sau if hoặc else không có cặp ngoặc nhọn { } , trình biên dịch chỉ xem một câu lệnh duy nhất ngay sau nó là thuộc cấu trúc rẽ nhánh. 1 if (a > 0) 2 x++; 3 y++; // Luon duoc thuc hien Đoạn code trên tương đương với: 1 if (a > 0) { 2 x++; 3 } 4 y++; Vì vậy, y++ nằm ngoài if và luôn được thực hiện, dù a > 0 đúng hay sai. Dấu chấm phẩy ngay sau điều kiện Không đặt dấu ; ngay sau điều kiện của if . Nếu viết như sau, if chỉ chứa một câu lệnh rỗng: 1 if (a != 0); 2 printf("a khac 0"); Khi đó, dòng printf("a khac 0"); luôn được thực hiện bất chấp điều kiện a != 0 đúng hay sai. Dangling else Khi có nhiều if lồng nhau, else luôn bắt cặp với if gần nó nhất mà chưa có else 1 if (a > 0) 2 if (b > 0) 3 printf("A"); 4 else 5 printf("B"); Trong đoạn trên, else thuộc về if (b > 0) , không thuộc về if (a > 0) 6 Giá trị đúng/sai trong điều kiện Trong C/C++, giá trị 0 được xem là sai. Mọi giá trị khác 0 đều được xem là đúng. Do đó, if (1) , if (-5) hoặc if (2.5) đều là điều kiện đúng. 2.2 Cạm bẫy cấu trúc switch...case Biểu thức và giá trị case Biểu thức trong switch thường phải là kiểu nguyên hoặc ký tự. Các nhãn case trong cùng một switch phải có giá trị khác nhau. 1 switch (x) { 2 case 1: 3 printf("Mot"); 4 break ; 5 case 1: // Sai: trung gia tri case 6 printf("Lap lai"); 7 break ; 8 } Thiếu break và hiện tượng rơi tự do Khi tìm thấy case khớp giá trị, chương trình bắt đầu chạy từ vị trí đó xuống dưới cho đến khi gặp break hoặc kết thúc switch . Các nhãn case phía sau không còn được kiểm tra lại như điều kiện mới. 1 int x = 2; 2 3 switch (x) { 4 case 1: 5 printf("A"); 6 case 2: 7 printf("B"); 8 case 3: 9 printf("C"); 10 break ; 11 default : 12 printf("D"); 13 } 7 Kết quả là BC , vì chương trình nhảy vào case 2 rồi chạy tiếp qua case 3 trước khi gặp break Mẹo tính nhanh: Với switch thiếu break , hãy tìm case đầu tiên khớp giá trị, sau đó đọc thẳng xuống dưới và cộng dồn mọi lệnh được thực hiện cho đến break hoặc dấu đóng ngoặc của switch 2.3 Cạm bẫy các vòng lặp Vòng lặp for Cấu trúc tổng quát: 1 for (khoi_tao; dieu_kien; buoc_nhay) { 2 // Than vong lap 3 } Thứ tự chạy là: Khởi tạo → kiểm tra điều kiện → chạy thân vòng lặp → bước nhảy → kiểm tra điều kiện tiếp theo. Nếu bỏ trống điều kiện, vòng lặp trở thành vòng lặp vô hạn trừ khi bên trong có lệnh thoát như break 1 for (i = 1; ; i++) { 2 printf("%d", i); // Vong lap vo han neu khong co break 3 } Vòng lặp while while kiểm tra điều kiện trước rồi mới chạy thân vòng lặp. Vì vậy, nếu điều kiện sai ngay từ đầu, thân vòng lặp không chạy lần nào. 1 int n = 10; 2 3 while (n < 5) { 4 printf("%d", n); // Khong duoc thuc hien lan nao 5 n++; 6 } 8 Vòng lặp do...while do ... while chạy thân vòng lặp trước rồi mới kiểm tra điều kiện. Vì vậy, thân vòng lặp luôn được thực hiện ít nhất một lần. 1 int n = 10; 2 3 do { 4 printf("%d", n); // Duoc thuc hien 1 lan 5 n++; 6 } while (n < 5); Lưu ý cú pháp: sau while (dieu_kien) của do ... while bắt buộc phải có dấu ; 2.4 Lệnh kiểm soát luồng break break dùng để thoát ngay khỏi switch hoặc vòng lặp gần nhất đang chứa nó. Nếu có nhiều vòng lặp lồng nhau, break chỉ thoát khỏi vòng lặp bên trong nhất. 1 for ( int i = 1; i <= 3; i++) { 2 for ( int j = 1; j <= 3; j++) { 3 if (j == 2) break ; 4 printf("%d%d ", i, j); 5 } 6 } Trong ví dụ này, break chỉ thoát khỏi vòng lặp theo j , vòng lặp theo i vẫn tiếp tục. continue continue bỏ qua phần còn lại của lần lặp hiện tại và chuyển sang lần lặp tiếp theo. • Với for , chương trình nhảy đến phần bước nhảy rồi kiểm tra điều kiện. • Với while và do ... while , chương trình nhảy đến phần kiểm tra điều kiện. • continue dùng cho vòng lặp, không dùng để thoát khỏi switch 9 1 for ( int i = 1; i <= 5; i++) { 2 if (i < 4) continue ; 3 printf("%d", i); 4 } Kết quả in ra là 45 , vì các lần i = 1 , i = 2 và i = 3 đều bị bỏ qua phần printf goto goto là lệnh nhảy vô điều kiện đến một nhãn. Nhãn có dấu hai chấm phía sau. 1 goto ket_thuc; 2 3 printf("Dong nay bi bo qua"); 4 5 ket_thuc: 6 printf("Xong"); Trong lập trình có cấu trúc, nên hạn chế dùng goto vì nó làm luồng chương trình khó theo dõi và dễ gây lỗi. 2.5 Dạng bài dự đoán kết quả code Khi gặp bài đọc hiểu vòng lặp, nên lập bảng giá trị biến qua từng lượt chạy thay vì tính nhẩm vội. Các dạng thường gặp gồm: • Lặp tính tổng hoặc đếm: Theo dõi từng lần biến đếm thay đổi, đặc biệt khi bên trong có if làm thay đổi biến phụ. • Dùng continue để bỏ qua lệnh in: Kiểm tra xem lần lặp nào bị continue chặn trước khi đến printf • Lặp vô hạn: Nếu thấy while (1) hoặc điều kiện như while (n < 10) nhưng trong thân vòng lặp không có lệnh thay đổi n , rất có thể chương trình bị lặp vô tận. Mẹo: Luôn phân biệt lệnh đơn và khối lệnh . Nếu không có { } , chỉ câu lệnh đầu tiên ngay sau if , else , for , while mới thuộc cấu trúc đó. Đây là bẫy thị giác xuất hiện rất thường xuyên trong đề trắc nghiệm. 10 3 Chương trình con: Hàm trong C/C++ Chương trình con giúp chia một chương trình lớn thành các phần nhỏ, dễ đọc và dễ kiểm tra hơn. Trong bài thi trắc nghiệm, phần hàm thường tập trung vào đọc hiểu sự thay đổi của biến, phân biệt truyền tham trị với truyền địa chỉ hoặc tham chiếu, nhận diện tầm vực biến và kiểm tra cú pháp của return hoặc nguyên mẫu hàm. Mục tiêu ôn tập: Nắm được cú pháp khai báo/định nghĩa hàm, phân biệt tham số hình thức với đối số, xác định đúng tầm vực biến, dự đoán được biến gốc có thay đổi sau lời gọi hàm hay không và tránh lỗi thiếu nguyên mẫu hàm. 3.1 Lý thuyết cốt lõi về hàm Hàm và hàm void Trong C/C++, chương trình con được tổ chức dưới dạng hàm. Nếu hàm có giá trị trả về, kiểu trả về đặt trước tên hàm. Nếu hàm không trả về giá trị, dùng kiểu void 1 int binh_phuong( int x) { 2 return x * x; 3 } 4 5 void in_thong_bao() { 6 printf("Xin chao"); 7 } Một hàm không có tham số có thể được viết theo dạng ten_ham() hoặc ten_ham( void ) tùy ngữ cảnh và chuẩn ngôn ngữ đang học. Tham số hình thức và đối số • Tham số hình thức: Là các biến được viết trong cặp ngoặc khi khai báo hoặc định nghĩa hàm. Tham số hình thức phải là biến, không thể là một biểu thức. • Tham số thực sự, hay đối số: Là giá trị được truyền vào khi gọi hàm. Đối số có thể là hằng, biến hoặc biểu thức. 11 1 int tong( int a, int b) { // a, b la tham so hinh thuc 2 return a + b; 3 } 4 5 int x = 3; 6 int y = tong(x, 2 + 5); // x va 2 + 5 la doi so Hai nguyên tắc viết hàm • Tính kết dính: Mỗi hàm nên làm một nhiệm vụ cụ thể, ví dụ chỉ tính toán hoặc chỉ nhập/xuất dữ liệu. • Tính độc lập: Hàm nên hoạt động như một hộp đen, hạn chế phụ thuộc vào biến bên ngoài thân hàm, đặc biệt là biến toàn cục. 3.2 Tầm vực và đặc tính của biến Đề thi thường đặt các biến trùng tên ở nhiều vị trí để kiểm tra khả năng xác định biến nào đang được sử dụng. Biến toàn cục và biến cục bộ • Biến toàn cục: Khai báo bên ngoài tất cả các hàm và có thể được dùng trong nhiều phần của chương trình. • Biến cục bộ: Khai báo bên trong một hàm hoặc một khối lệnh { } . Biến này chỉ tồn tại trong phạm vi khối lệnh đó. • Khi trùng tên: Biến cục bộ được ưu tiên hơn biến toàn cục trong phạm vi của nó. 1 int x = 100; // Bien toan cuc 2 3 void f() { 4 int x = 5; // Bien cuc bo, che bien toan cuc 5 printf("%d", x); // In ra 5 6 } 12 Biến static cục bộ Biến static khai báo bên trong hàm vẫn có phạm vi cục bộ, nhưng vùng nhớ của nó không bị hủy sau khi hàm kết thúc. Giá trị được giữ lại cho lần gọi tiếp theo. 1 void dem() { 2 static int x = 0; 3 x++; 4 printf("%d ", x); 5 } 6 7 dem(); // In 1 8 dem(); // In 2 9 dem(); // In 3 Nếu bỏ từ khóa static , biến x sẽ được tạo lại mỗi lần gọi hàm và thường cho kết quả 1 1 1 Biến register Từ khóa register gợi ý trình biên dịch lưu biến trong thanh ghi CPU để truy xuất nhanh hơn. register thường gắn với biến đếm trong vòng lặp. 1 register int i; 2 for (i = 0; i < n; i++) { 3 // Xu ly lap 4 } 3.3 Truyền tham trị, truyền địa chỉ và truyền tham chiếu Cần nhìn vào dòng khai báo hàm để biết biến gốc bên ngoài có bị thay đổi hay không. Truyền tham trị Khi truyền tham trị, hàm nhận một bản sao của biến gốc. Mọi thay đổi trên tham số trong hàm không làm thay đổi biến bên ngoài. 1 void tang( int a) { 2 a++; 3 } 13 4 int x = 5; 5 tang(x); 6 printf("%d", x); // In ra 5 Dấu hiệu nhận biết: tham số khai báo bình thường, không có * hoặc & , ví dụ void tang( int a) Truyền địa chỉ bằng con trỏ Khi truyền địa chỉ, hàm nhận địa chỉ vùng nhớ của biến gốc. Muốn thay đổi giá trị biến gốc, cần truy cập qua toán tử * 1 void tang( int *a) { 2 (*a)++; 3 } 4 5 int x = 5; 6 tang(&x); 7 printf("%d", x); // In ra 6 • Dấu hiệu ở khai báo hàm: tham số có * như int *a • Dấu hiệu khi gọi hàm: thường truyền địa chỉ bằng &x • Bẫy cú pháp: Nếu hàm cần int * nhưng truyền giá trị như tang(5) , chương trình sẽ lỗi kiểu dữ liệu. Truyền tham chiếu trong C++ Tham chiếu giúp thay đổi biến gốc giống truyền địa chỉ, nhưng cú pháp gọi hàm đơn giản hơn. 1 void tang( int &a) { 2 a++; 3 } 4 5 int x = 5; 6 tang(x); 7 printf("%d", x); // In ra 6 Dấu hiệu ở khai báo hàm là tham số có & , ví dụ int &a . Khi gọi hàm chỉ truyền tên biến bình thường, không viết &x 14 Ví dụ hoán vị dễ nhầm So sánh ba cách viết sau: 1 void swap_value( int a, int b) { 2 int t = a; 3 a = b; 4 b = t; 5 } 6 7 void swap_pointer( int *a, int *b) { 8 int t = *a; 9 *a = *b; 10 *b = t; 11 } 12 13 void swap_reference( int &a, int &b) { 14 int t = a; 15 a = b; 16 b = t; 17 } swap_value(x, y) không đổi x và y , còn swap_pointer(&x, &y) và swap_reference(x, y) đều làm đổi giá trị hai biến gốc. 3.4 Cạm bẫy câu lệnh return return kết thúc hàm và trả giá trị về nơi gọi hàm. Với hàm có kiểu trả về khác void , giá trị trả về phải phù hợp với kiểu trả về của hàm. • return có thể trả về một giá trị hoặc một biểu thức. • Một hàm có thể có nhiều câu lệnh return , thường nằm trong các nhánh if ... else • Khi gặp một lệnh return được thực hiện, hàm kết thúc ngay; các lệnh phía sau trong cùng nhánh sẽ không chạy nữa. 1 int max2( int a, int b) { 2 if (a > b) { 3 return a; 4 } 5 return b; 15 6 } 7 8 int f( int x) { 9 return x * x + 2 * x + 1; 10 } Với hàm void , có thể dùng return ; để thoát sớm khỏi hàm mà không trả về giá trị. 1 void in_so_duong( int x) { 2 if (x <= 0) { 3 return ; 4 } 5 printf("%d", x); 6 } 3.5 Nguyên mẫu hàm Nếu lời gọi hàm xuất hiện trước phần định nghĩa hàm, chương trình cần có nguyên mẫu hàm ở phía trên để trình biên dịch biết tên hàm, kiểu trả về, số lượng tham số và kiểu dữ liệu của từng tham số. 1 int tong( int , int ); // Nguyen mau ham 2 3 int main() { 4 int s = tong(2, 3); 5 printf("%d", s); 6 return 0; 7 } 8 9 int tong( int a, int b) { 10 return a + b; 11 } • Nguyên mẫu hàm phải kết thúc bằng dấu chấm phẩy ; • Trong nguyên mẫu hàm, có thể ghi hoặc bỏ tên tham số. Vì vậy, int tong( int , int ); và int tong( int a, int b); đều hợp lệ. • Nguyên mẫu hàm giúp kiểm tra lời gọi hàm có đúng số lượng và kiểu tham số hay không. 16 Mẹo: Khi gặp main gọi một hàm khác, hãy nhìn ngay vào danh sách tham số của hàm được gọi. Nếu tham số không có * hoặc & , biến bên ngoài thường giữ nguyên. Nếu tham số có * hoặc & , biến gốc có thể bị thay đổi theo thao tác bên trong hàm. 4 Mảng và con trỏ trong C/C++ Mục tiêu ôn tập: Nắm được cách mảng được lưu trong bộ nhớ, cách khai báo – khởi tạo đúng, cách truyền mảng vào hàm, các quy tắc với mảng hai chiều và những phép toán con trỏ thường xuất hiện trong câu hỏi trắc nghiệm. 4.1 Bản chất và cách tính bộ nhớ của mảng • Kích thước cố định: Kích thước của mảng phải được xác định khi khai báo và không tự thay đổi trong quá trình chạy chương trình. • Liên tục trong bộ nhớ: Các phần tử của mảng được lưu trong một khối bộ nhớ liên tiếp. Vì vậy, từ địa chỉ phần tử đầu tiên, máy tính có thể tính được địa chỉ của mọi phần tử còn lại. • Tổng kích thước bộ nhớ: bytes = n × sizeof( type ) Ví dụ, với int a[100] , nếu sizeof ( int ) = 4 thì mảng chiếm: 100 × 4 = 400 bytes • Địa chỉ phần tử thứ i : Nếu d 0 là địa chỉ của phần tử đầu tiên, địa chỉ của a[i] được tính theo công thức: d i = d 0 + i × sizeof( type ) 4.2 Cạm bẫy khai báo và khởi tạo • Kích thước mảng trong C++ chuẩn phải là hằng số tại thời điểm biên dịch. Ví dụ sau không hợp lệ trong C++ chuẩn, dù một số trình biên dịch có thể cho phép như phần mở rộng: 17 1 int n = 10; 2 int a[n]; // Sai trong C++ chuan Cách viết đúng là dùng hằng số hoặc cấp phát động: 1 const int N = 10; 2 int a[N]; • Khởi tạo thiếu phần tử: Nếu chỉ cung cấp một phần giá trị ban đầu, các phần tử còn lại được tự động gán bằng 0 1 int a[5] = {1, 2}; // a = {1, 2, 0, 0, 0} 2 int b[5] = {0}; // b = {0, 0, 0, 0, 0} • Khai báo bằng typedef : Cú pháp này dễ gây nhầm lẫn nhưng hoàn toàn hợp lệ. 1 typedef int Mang[10]; 2 Mang m1, m2; // m1 va m2 deu la mang 10 so nguyen • Mảng động: Khi kích thước chỉ biết lúc chạy chương trình, có thể cấp phát động bằng new [] và phải giải phóng bằng delete [] 1 int * a = new int [n]; 2 // Su dung mang a 3 delete [] a; Trong C++ hiện đại, nên ưu tiên std::vector< int > hoặc std::array< int , N> khi phù hợp. 4.3 Cạm bẫy truy xuất và phép gán • Không được gán trực tiếp mảng này cho mảng kia. Câu lệnh b = a; là sai nếu a và b đều là mảng tĩnh. 1 int a[3] = {1, 2, 3}; 2 int b[3]; 3 b = a; // Sai 4 for ( int i = 0; i < 3; ++i) { 5 b[i] = a[i]; // Dung 6 } • Truy xuất ngoài phạm vi: Với int a[4] , các chỉ số hợp lệ là 0 , 1 , 2 , 3 . Những biểu thức như a[-1] hoặc a[4] đều vượt phạm vi mảng. 18 • Lưu ý: Truy xuất ngoài phạm vi không nhất thiết báo lỗi ngay khi biên dịch hoặc khi chạy. Trong C/C++, đây là hành vi không xác định, nên kết quả có thể rất khó đoán. 4.4 Mảng trong hàm • Bản chất khi truyền mảng: Khi truyền mảng vào hàm, tham số nhận được thường là địa chỉ của phần tử đầu tiên. Do đó, mọi thay đổi trên mảng bên trong hàm sẽ làm thay đổi mảng gốc ở bên ngoài. 1 void tang( int a[], int n) { 2 for ( int i = 0; i < n; ++i) { 3 ++a[i]; // tang phan tu thu i len 1 4 } 5 } • Mảng một chiều trong tham số: Hai cách viết sau tương đương trong danh sách tham số hàm: 1 void nhap( int a[], int n); 2 void nhap( int * a, int n); Vì mảng không tự mang theo số lượng phần tử, biến n cần được truyền riêng. • Mảng hai chiều trong tham số: Bắt buộc phải chỉ định số lượng cột, vì máy cần biết độ dài mỗi dòng để tính địa chỉ phần tử. 1 const int C = 4; 2 3 void nhap( int a[][C], int soDong); 4 void nhap( int (*a)[C], int soDong); Cách viết int a[][] là sai vì thiếu số cột. 4.5 Mảng hai chiều và ma trận Với ma trận vuông kích thước n × n , các điều kiện sau thường được dùng trong bài tập đọc hiểu hoặc dịch code: Vùng cần xét Điều kiện Ví dụ truy xuất Đường chéo chính i = j a[i][i] Đường chéo phụ i + j = n − 1 a[i][n - 1 - i] Tam giác dưới i > j Các ô dưới đường chéo chính Tam giác trên i < j Các ô trên đường chéo chính 19 Trong đó, i là chỉ số dòng và j là chỉ số cột. Nếu đề dùng tên biến khác, chỉ cần đổi tương ứng: dòng so với cột. 4.6 Mảng và con trỏ • Tên mảng không phải là biến con trỏ thông thường. Trong nhiều biểu thức, tên mảng tự chuyển thành con trỏ trỏ tới phần tử đầu tiên. Vì vậy, a thường có cùng giá trị địa chỉ với &a[0] , nhưng &a có kiểu khác: con trỏ tới toàn bộ mảng. • Không thể tăng hoặc giảm tên mảng. Vì tên mảng không phải biến con trỏ có thể gán lại, biểu thức a++ là sai. Muốn di chuyển qua các phần tử, hãy dùng một con trỏ khác. 1 int a[3] = {10, 20, 30}; 2 int * p = a; 3 ++p; // p tro toi a[1] • Các cặp cú pháp tương đương cần nhớ: a + i ⇐⇒ & a [ i ] ∗ ( a + i ) ⇐⇒ a [ i ] Nếu đề viết *(p + n) , hãy hiểu đó chính là phần tử p[n] • Phép toán trên con trỏ: Nếu p1 và p2 cùng trỏ vào một mảng, p1 - p2 cho biết khoảng cách tính theo số phần tử. Không được thực hiện phép nhân * hoặc phép chia / giữa hai con trỏ. 4.7 Ánh xạ mảng hai chiều thành một chiều Mảng hai chiều trong C/C++ được lưu theo từng dòng liên tiếp trong bộ nhớ. Với mảng có D dòng và C cột: 1 int a[D][C]; 2 int * p = &a[0][0]; Ta có các công thức ánh xạ sau: • Từ tọa độ dòng d , cột c sang chỉ số một chiều: i = d × C + c. • Từ chỉ số một chiều i quay lại tọa độ hai chiều: d = i/C, c = i mod C. 20