Bài giảng Ngôn ngữ lập trình C, C++ - Phạm Hồng Thái PDF
Document Details
Uploaded by MerryHeliotrope4949
Đại học Quốc gia Hà Nội
2003
Phạm Hồng Thái
Tags
Summary
Bài giảng Ngôn ngữ lập trình C/C++ do Phạm Hồng Thái biên soạn, năm 2003, tại Đại học Quốc gia Hà Nội. Tài liệu hướng dẫn các khái niệm cơ bản, cú pháp và ví dụ thực hành về ngôn ngữ lập trình C++. Bài giảng phù hợp cho sinh viên đại học bắt đầu học C/C++.
Full Transcript
**ĐẠI HỌC QUỐC GIA HÀ NỘI\ TRƯỜNG ĐẠI HỌC CÔNG NGHỆ\ Khoa Công nghệ Thông tin** PHẠM HỒNG THÁI Bài giảng NGÔN NGỮ LẬP TRÌNH C/C++ **Hà Nội - 2003** LỜI NÓI ĐẦU Ngôn ngữ lập trình (NNLT) C/C++ là một trong những ngôn ngữ lập trình hướng đối tượng mạnh và phổ biến hiện nay do tính mềm dẻo và đa...
**ĐẠI HỌC QUỐC GIA HÀ NỘI\ TRƯỜNG ĐẠI HỌC CÔNG NGHỆ\ Khoa Công nghệ Thông tin** PHẠM HỒNG THÁI Bài giảng NGÔN NGỮ LẬP TRÌNH C/C++ **Hà Nội - 2003** LỜI NÓI ĐẦU Ngôn ngữ lập trình (NNLT) C/C++ là một trong những ngôn ngữ lập trình hướng đối tượng mạnh và phổ biến hiện nay do tính mềm dẻo và đa năng của nó. Không chỉ các ứng dụng được viết trên C/C++ mà cả những chương trình hệ thống lớn đều được viết hầu hết trên C/C++. C++ là ngôn ngữ lập trình hướng đối tượng được phát triển trên nền tảng của C, không những khắc phục một số nhược điểm của ngôn ngữ C mà quan trọng hơn, C++ cung cấp cho người sử dụng (NSD) một phương tiện lập trình theo kỹ thuật mới: lập trình hướng đối tượng. Đây là kỹ thuật lập trình được sử dụng hầu hết trong các ngôn ngữ mạnh hiện nay, đặc biệt là các ngôn ngữ hoạt động trong môi truờng Windows như Microsoft Access, Visual Basic, Visual Foxpro \... Hiện nay NNLT C/C++ đã được đưa vào giảng dạy trong hầu hết các trường Đại học, Cao đẳng để thay thế một số NNLT đã cũ như FORTRAN, Pascal \... Tập bài giảng này được viết ra với mục đích đó, trang bị kiến thức và kỹ năng thực hành cho sinh viên bắt đầu học vào NNLT C/C++ tại Khoa Công nghệ, Đại học Quốc gia Hà Nội. Để phù hợp với chương trình, tập bài giảng này chỉ đề cập một phần nhỏ đến kỹ thuật lập trình hướng đối tượng trong C++, đó là các kỹ thuật đóng gói dữ liệu, phương thức và định nghĩa mới các toán tử. Tên gọi của tập bài giảng này nói lên điều đó, có nghĩa nội dung của bài giảng thực chất là NNLT C được mở rộng với một số đặc điểm mới của C++. Về kỹ thuật lập trình hướng đối tượng (trong C++) sẽ được trang bị bởi một giáo trình khác. Tuy nhiên để ngắn gọn, trong tập bài giảng này tên gọi C/C++ sẽ được chúng tôi thay bằng C++. Nội dung tập bài giảng này gồm 8 chương. Phần đầu gồm các chương từ 1 đến 6 chủ yếu trình bày về NNLT C++ trên nền tảng của kỹ thuật lập trình cấu trúc. Các chương còn lại (chương 7 và 8) sẽ trình bày các cấu trúc cơ bản trong C++ đó là kỹ thuật đóng gói (lớp và đối tượng) và định nghĩa phép toán mới cho lớp. Tuy đã có nhiều cố gắng nhưng do thời gian và trình độ người viết có hạn nên chắc chắn không tránh khỏi sai sót, vì vậy rất mong nhận được sự góp ý của bạn đọc để bài giảng ngày càng một hoàn thiện hơn. Tác giả. CHƯƠNG 1 CÁC KHÁI NIỆM CƠ BẢN CỦA C++ Các yếu tố cơ bản Môi trường làm việc của C++ Các bước để tạo và thực hiện một chương trình Vào/ra trong C++ I. []{#bookmark10.anchor}CÁC YẾU TỐ CƠ BẢN Một ngôn ngữ lập trình (NNLT) bậc cao cho phép người sử dụng (NSD) biểu hiện ý tưởng của mình để giải quyết một vấn đề, bài toán bằng cách diễn đạt gần với ngôn ngữ thông thường thay vì phải diễn đạt theo ngôn ngữ máy (dãy các kí hiệu 0,1). Hiển nhiên, các ý tưởng NSD muốn trình bày phải được viết theo một cấu trúc chặt chẽ thường được gọi là *thuật toán* hoặc *giải thuật* và theo đúng các qui tắc của ngôn ngữ gọi là *cú pháp* hoặc *văn phạm.* Trong giáo trình này chúng ta bàn đến một ngôn ngữ lập trình như vậy, đó là ngôn ngữ lập trình C++ và làm thế nào để thể hiện các ý tưởng giải quyết vấn đề bằng cách viết thành chương trình trong C++. Trước hết, trong mục này chúng ta sẽ trình bày về các qui định bắt buộc đơn giản và cơ bản nhất. Thông thường các qui định này sẽ được nhớ dần trong quá trình học ngôn ngữ, tuy nhiên để có một vài khái niệm tương đối hệ thống về NNLT C++ chúng ta trình bày sơ lược các khái niệm cơ bản đó. Người đọc đã từng làm quen với các NNLT khác có thể đọc lướt qua phần này. 1. []{#bookmark12.anchor}Bảng ký tự của C++ Hầu hết các ngôn ngữ lập trình hiện nay đều sử dụng các kí tự tiếng Anh, các kí hiệu thông dụng và các con số để thể hiện chương trình. Các kí tự của những ngôn ngữ khác không được sử dụng (ví dụ các chữ cái tiếng Việt). Dưới đây là bảng kí tự được phép dùng để tạo nên những câu lệnh của ngôn ngữ C++. - - Dấu gạch dưới: \_ - Các chữ số thập phân: 0, 1,.., 9. - Các ký hiệu toán học: +, -, \*, /, % , &, \|\|, !, \>, \ 1. []{#bookmark49.anchor}Khởi động - Thoát khỏi C++ Để kết thúc làm việc với C++ (soạn thảo, chạy chương trình \...) và quay về môi trường Windows chúng ta ấn **Alt-X**. 2. []{#bookmark53.anchor}Giao diện và cửa sổ soạn thảo a. Mô tả chung Khi gọi chạy C++ trên màn hình sẽ xuất hiện một menu xổ xuống và một cửa sổ soạn thảo. Trên menu gồm có các nhóm chức năng: **File, Edit, Search, Run, Compile, Debug, Project, Options, Window, Help**. Để kích hoạt các nhóm chức năng, có thể ấn **Alt+chữ cái** biểu thị cho menu của chức năng đó (là chữ cái có gạch dưới). Ví dụ để mở nhóm chức năng **File** ấn **Alt+F**, sau đó dịch chuyển hộp sáng đến mục cần chọn rồi ấn Enter. Để thuận tiện cho NSD, một số các chức năng hay dùng còn được gắn với một tổ hợp các phím cho phép người dùng có thể chọn nhanh chức năng này mà không cần thông qua việc mở menu như đã mô tả ở trên. Một số tổ hợp phím cụ thể đó sẽ được trình bày vào cuối phần này. Các bộ chương trình dịch hỗ trợ người lập trình một môi trường tích hợp tức ngoài chức năng soạn thảo, nó còn cung cấp nhiều chức năng, tiện ích khác giúp người lập trình vừa có thể soạn thảo văn bản chương trình vừa gọi chạy chương trình vừa gỡ lỗi \... Các chức năng liên quan đến soạn thảo phần lớn giống với các bộ soạn thảo khác (như WinWord) do vậy chúng tôi chỉ trình bày tóm tắt mà không trình bày chi tiết ở đây. b. Các chức năng soạn thảo Giống hầu hết các bộ soạn thảo văn bản, bộ soạn thảo của Turbo C hoặc Borland C cũng sử dụng các phím sau cho quá trình soạn thảo: - - trước vị trí con trỏ (xoá lùi). - - Tổ hợp phím Ctrl-A rất thuận lợi khi cần đánh dấu nhanh toàn bộ văn bản. c. Chức năng tìm kiếm và thay thế Chức năng này dùng để dịch chuyển nhanh con trỏ văn bản đến từ cần tìm. Để thực hiện tìm kiếm bấm Ctrl-QF, tìm kiếm và thay thế bấm Ctrl-QA. Vào từ hoặc nhóm từ cần tìm vào cửa sổ Find, nhóm thay thế (nếu dùng Ctrl-QA) vào cửa sổ Replace và đánh dấu vào các tuỳ chọn trong cửa sổ bên dưới sau đó ấn Enter. Các tuỳ chọn gồm: không phân biệt chữ hoa/thường, tìm từ độc lập hay đứng trong từ khác, tìm trong toàn văn bản hay chỉ trong phần được đánh dấu, chiều tìm đi đến cuối hay ngược về đầu văn bản, thay thế có hỏi lại hay không hỏi lại \... Để dịch chuyển con trỏ đến các vùng khác nhau trong một menu hay cửa sổ chứa các tuỳ chọn ta sử dụng phím Tab. d. Các chức năng liên quan đến tệp - - Soạn thảo tệp mới: Chọn menu File\\New. Hiện ra cửa sổ soạn thảo trắng và tên file tạm thời lấy là Noname.cpp. - vào tên tệp cụ thể. - e. Chức năng dịch và chạy chương trình - Ctrl-F9: Khởi động chức năng dịch và chạy toàn bộ chương trình. - F4: Chạy chương trình từ đầu đến dòng lệnh hiện tại (đang chứa con trỏ) - F7: Chạy từng lệnh một của hàm main(), kể cả các lệnh con trong hàm. - Các chức năng liên quan đến dịch chương trình có thể được chọn thông qua menu Compile (Alt-C). f. Tóm tắt một số phím nóng hay dùng - - Các phím dịch chuyển con trỏ khi soạn thảo. - - F2: ghi tệp lên đĩa. - F3: mở tệp cũ ra sửa chữa hoặc soạn thảo tệp mới. - F4: chạy chương trình đến vị trí con trỏ. - F5: Thu hẹp/mở rộng cửa sổ soạn thảo. - F6: Chuyển đổi giữa các cửa sổ soạn thảo. - F7: Chạy chương trình theo từng lệnh, kể cả các lệnh trong hàm con. - F8: Chạy chương trình theo từng lệnh trong hàm chính. - - Alt-F7: Chuyển con trỏ về nơi gây lỗi trước đó. - Alt-F8: Chuyển con trỏ đến lỗi tiếp theo. - Ctrl-F9: Chạy chương trình. - Ctrl-Insert: Lưu khối văn bản được đánh dấu vào bộ nhớ đệm. - Shift-Insert: Dán khối văn bản trong bộ nhớ đệm vào văn bản tại vị trí con trỏ. - Shift-Delete: Xoá khối văn bản được đánh dấu, lưu nó vào bộ nhớ đệm. - Ctrl-Delete: Xoá khối văn bản được đánh dấu (không lưu vào bộ nhớ đệm). - Alt-F5: Chuyển sang cửa sổ xem kết quả của chương trình vừa chạy xong. - Alt-X: thoát C++ về lại Windows. 3. []{#bookmark113.anchor}Cấu trúc một chương trình trong C++ - - - \#include \ 1. []{#bookmark124.anchor}Qui trình viết và thực hiện chương trình Trước khi viết và chạy một chương trình thông thường chúng ta cần: 1. 2. Xác định thuật toán giải. 3. 4. Dịch chương trình nguồn để tìm và sửa các lỗi gọi là lỗi cú pháp. 5. Chạy chương trình, kiểm tra kết quả in ra trên màn hình. Nếu sai, sửa lại 3. []{#bookmark134.anchor}Dịch chương trình 4. []{#bookmark138.anchor}Chạy chương trình IV. []{#bookmark142.anchor}VÀO/RA TRONG C++ 1. []{#bookmark146.anchor}Vào dữ liệu từ bàn phím hoặc: 2. []{#bookmark150.anchor}In dữ liệu ra màn hình Để in giá trị của các **biểu thức** ra màn hình ta dùng câu lệnh sau: hoặc: hoặc có thể chỉ bằng 1 lệnh: hoặc gộp tất cả thành 1 câu lệnh: \#include \ x; cin.ignore(1); cin.get(c); 4. []{#bookmark164.anchor}Vào/ra trong C a. In kết quả ra màn hình **printf(dòng định dạng, bt\_1, bt\_2, \..., bt\_n) ;** Ví dụ, giả sử x = 4, câu lệnh: printf("%d %0.2f", 3, x + 1) ; Ví dụ câu lệnh: printf("Tỉ lệ học sinh giỏi: %0.2f %%", 32.486) ; [Chú ý]: Mỗi bt\_i cần in phải có một định dạng tương ứng trong dòng định dạng. Ví dụ câu lệnh trên cũng có thể viết: - - Dấu + trước độ rộng để in giá trị số kèm theo dấu (dương hoặc âm) - *[Ví dụ 4]* : main() { } sẽ in ra: Chương trình tính tổng 2 số nguyên: i + j = 5. b. Nhập dữ liệu từ bàn phím Cuối cùng, chương trình trong ví dụ 3 được viết lại với printf() và scanf() như sau: *[Ví dụ 5]* : \#include \ // để sử dụng các hàm printf() và scanf() \#include \ // để sử dụng các hàm clrscr() và getch() void main() { } BÀI TẬP 1. Những tên gọi nào sau đây là hợp lệ: - x - 123variabe - tin\_hoc - toan tin - so-dem - RADIUS - one.0 - number\# - Radius - nam2000 2. Bạn hãy thử viết một chương trình ngắn nhất có thể được. 3. Tìm các lỗi cú pháp trong chương trình sau: \#include (iostream.h) void main(); / Giải phương trình bậc 1 { cout \ 1. []{#bookmark197.anchor}Khái niệm về kiểu dữ liệu - tên kiểu: là một từ dành riêng để chỉ định kiểu của dữ liệu. - số byte trong bộ nhớ để lưu trữ một đơn vị dữ liệu thuộc kiểu này: Thông - Miền giá trị của kiểu: Cho biết một đơn vị dữ liệu thuộc kiểu này sẽ có thể lấy giá trị trong miền nào, ví dụ nhỏ nhất và lớn nhất là bao nhiêu. Hiển nhiên các giá trị này phụ thuộc vào số byte mà hệ thống máy qui định cho từng kiểu. NSD cần nhớ đến miền giá trị này để khai báo kiểu cho các biến cần sử dụng một cách thích hợp. -------------- --------------- ---------- ----------------------------- Loại dữ liệu Tên kiểu Số ô nhớ Miền giá trị Kí tự char 1 byte \- 128.. 127 unsigned char 1 byte 0.. 255 Số nguyên int 2 byte \- 32768.. 32767 unsigned int 2 byte 0.. 65535 short 2 byte \- 32768.. 32767 long 4 byte \- 2^15^.. 2^15^ - 1 Số thực float 4 byte ± 10 ^-3^7.. ± 10 +^38^ double 8 byte ± 10 -^307^.. ± 10 ^+308^ -------------- --------------- ---------- ----------------------------- Bảng 1. Các loại kiểu đơn giản 2. []{#bookmark204.anchor}Kiểu ký tự Theo bảng trên ta thấy có 2 loại kí tự là char với miền giá trị từ -128 đến 127 và // c, d được phép gán giá trị từ -128 đến 127 // e được phép gán giá trị từ 0 đến 255 // d có giá trị ngoài miền cho phép // f có giá trị ngoài miền cho phép // in ra chữ cái \'A\' và giá trị số 65 // in ra là kí tự \'\|\' và giá trị số -77 // in ra là kí tự \'\|\' và giá trị số 179 // in ra là kí tự \'J\' và giá trị số 74 3. []{#bookmark208.anchor}Kiểu số nguyên 4. []{#bookmark212.anchor}Kiểu số thực \#include \ 1. []{#bookmark220.anchor}Hằng nguyên - kiểu short, int: 3, -7, \... - kiểu unsigned: 3, 123456, \... - kiểu long, long int: 3L, -7L, 123456L, \... (viết L vào cuối mỗi giá trị) 2. []{#bookmark227.anchor}Hằng thực a. Dạng dấu phảy tĩnh Theo cách viết thông thường. Ví dụ: 3.0, -7.0, 3.1416, \... b. Dạng dấu phảy động n = \... = 0.031416e2 = 0.31416e1 = 3.1416e0 = 31.416e-1 = 314.16e-2 = \... vì n = 0.031416 x 10^2^ = 0.31416 x 10^1^ = 3.1416 x 10^0^ = \... 3. []{#bookmark239.anchor}Hằng kí tự a. Cách viết hằng - \'\\kkk\': không quá 3 chữ số trong hệ 8. Ví dụ \'\\11\' biểu diễn kí tự có mã 9. - \'\\xkk\': không quá 2 chữ số trong hệ 16. Ví dụ \'\\x1B\' biểu diễn kí tự có mã 27. 65, 0101, 0x41 hoặc \'A\' , \'\\101\' , \'\\x41\' b. Một số hằng thông dụng +-----------------------------------+-----------------------------------+ | | biểu thị kí tự xuống dòng (cũng | | | tương đương với endl) | | | | | | kí tự tab | | | | | | kéo trang | +-----------------------------------+-----------------------------------+ | | dấu \\ | | | | | | dấu chấm hỏi ? | | | | | | dấu nháy đơn \' | +-----------------------------------+-----------------------------------+ | | dấu nháy kép \" | | | | | | kí tự có mã là kkk trong hệ 8 | | | | | | kí tự có mã là kk trong hệ 16 | +-----------------------------------+-----------------------------------+ Ví dụ: cout \ 1. []{#bookmark267.anchor}Khai báo biến a. Khai báo không khởi tạo tên\_kiểu tên\_biến\_1 ; tên\_kiểu tên\_biến\_2 ; tên\_kiểu tên\_biến\_3 ; Ví dụ: void main() { } b. Khai báo có khởi tạo **tên\_kiểu tên\_biến\_1 = gt\_1, tên\_biến\_2 = gt\_2, tên\_biến\_3 = gt\_3 ;** trong đó các giá trị gt\_1, gt\_2, gt\_3 có thể là các hằng, biến hoặc biểu thức. Ví dụ: const int n = 10 ; void main() // khai báo i và khởi tạo bằng 2, k bằng 15 // khai báo biến thực epsilon khởi tạo bằng 10^-6^ // khai báo biến kí tự c và khởi tạo bằng \'A\' // khai báo xâu kí tự d chứa dòng chữ \"Tin học\" } 2. []{#bookmark279.anchor}Phạm vi của biến Như đã biết chương trình là một tập hợp các hàm, các câu lệnh cũng như các khai báo. Phạm vi tác dụng của một biến là nơi mà biến có tác dụng, tức hàm nào, câu lệnh\ nào được phép sử dụng biến đó. Một biến xuất hiện trong chương trình có thể được sử dụng bởi hàm này nhưng không được bởi hàm khác hoặc bởi cả hai, điều này phụ thuộc chặt chẽ vào vị trí nơi biến được khai báo. Một nguyên tắc đầu tiên là biến sẽ có tác dụng kể từ vị trí nó được khai báo cho đến hết khối lệnh chứa nó. Chi tiết cụ thể hơn sẽ được trình bày trong chương 4 khi nói về hàm trong C++. 3. []{#bookmark283.anchor}Gán giá trị cho biến (phép gán) **tên\_biến = biểu thức ;** int n, i = 3; n = 10; cout \ 1. []{#bookmark295.anchor}Phép toán a. Các phép toán số học: +, -, \*, /, % - - b. Các phép toán tự tăng, giảm: i++, ++i, i\--, \--i ------------------------- ------------------------- ----------------- Phép toán Tương đương Kết quả i = ++j ; // tăng trước j = j + 1 ; i = j ; i = 16 , j = 16 i = j++ ; // tăng sau i = j ; j = j + 1 ; i = 15 , j = 16 j = ++i + 5 ; i = i + 1 ; j = i + 5 ; i = 4, j = 9 j = i++ + 5 ; j = i + 5; i = i + 1; i = 4, j = 8 ------------------------- ------------------------- ----------------- c. Các phép toán so sánh và lôgic Các phép toán so sánh Hai toán hạng của các phép toán này phải cùng kiểu. Ví dụ: Chú ý: cần phân biệt phép toán gán (=) và phép toán so sánh (==). Phép gán vừa gán giá trị cho biến vừa trả lại giá trị bất kỳ (là giá trị của toán hạng bên phải), trong khi phép so sánh luôn luôn trả lại giá trị 1 hoặc 0. Các phép toán lôgic: && (và), \|\| (hoặc ), ! (không, phủ định) Hai toán hạng của loại phép toán này phải có kiểu lôgic tức chỉ nhận một trong hai giá trị \"đúng\" (được thể hiện bởi các số nguyên khác 0) hoặc \"sai\" (thể hiện bởi 0). Khi đó giá trị trả lại của phép toán là 1 hoặc 0 và được cho trong bảng sau: --- --- -------- ------------ ----- a b a && b ^a^ II ^b^ ! a 1 1 1 1 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 1 --- --- -------- ------------ ----- Tóm lại: - Phép toán \"và\" đúng khi và chỉ khi hai toán hạng cùng đúng - Phép toán \"hoặc\" sai khi và chỉ khi hai toán hạng cùng sai - +-----------------------------------+-----------------------------------+ | | // = 0 vì có hạng thức (4\5) sai | | | | | | // = 1 vì cả hai hạng thức cùng | | | đúng | | | | | | //=0 | +-----------------------------------+-----------------------------------+ | | // = 1 vì (4+3\ 7. Nhập a, b, c. In ra màn hình dòng chữ phương trình có dạng ax^A^2 + bx + c = 0, trong đó các giá trị a, b, c chỉ in 2 số lẻ (ví dụ với a = 5.141, b = -2, c = 0.8 in ra 5.14 xA2 -2.00 x + 0.80). 8. Viết chương trình tính và in ra giá trị các biểu thức sau với 2 số lẻ: ^2+^à 2+4 9. Nhập a, b, c là các số thực. In ra giá trị của các biểu thức sau với 3 số lẻ: 10. In ra tổng, tích, hiệu và thương của 2 số được nhập vào từ bàn phím. 11. In ra trung bình cộng, trung bình nhân của 3 số được nhập vào từ bàn phím. 12. Viết chương trình nhập cạnh, bán kính và in ra diện tích, chu vi của các hình: vuông, chữ nhật, tròn. 13. Nhập a, b, c là độ dài 3 cạnh của tam giác (chú ý đảm bảo tổng 2 cạnh phải lớn\ hơn cạnh còn lại). Tính chu vi, diện tích, độ dài 3 đường cao, 3 đường trung tuyến, 3 đường phân giác, bán kính đường tròn nội tiếp, ngoại tiếp lần lượt theo các công thức sau: C = 2p = a + b + c ; S = 7 *p(p-a)(p-b)(p-c)* ; 14. Tính diện tích và thể tích của hình cầu bán kính R theo công thức: S = 4nR^2^ ; V = RS/3 15. Nhập vào 4 chữ số. In ra tổng của 4 chữ số này và chữ số hàng chục, hàng đơn vị của tổng (ví dụ 4 chữ số 3, 1, 8, 5 có tổng là 17 và chữ số hàng chục là 1 và hàng đơn vị là 7, cần in ra 17, 1, 7). 16. Nhập vào một số nguyên (có 4 chữ số). In ra tổng của 4 chữ số này và chữ số đầu, chữ số cuối (ví dụ số 3185 có tổng các chữ số là 17, đầu và cuối là 3 và 5, kết quả in ra là: 17, 3, 5). 17. Hãy nhập 2 số a và b. Viết chương trình đổi giá trị của a và b theo 2 cách: - dùng biến phụ t: t = a; a = b; b = t; - không dùng biến phụ: a = a + b; b = a - b; a = a - b; In kết quả ra màn hình để kiểm tra. 18. Viết chương trình đoán số của người chơi đang nghĩ, bằng cách yêu cầu người chơi nghĩ một số, sau đó thực hiện một loạt các tính toán trên số đã nghĩ rồi cho biết kết quả. Máy sẽ in ra số mà người chơi đã nghĩ. (ví dụ yêu cầu người chơi lấy số đã nghĩ nhân đôi, trừ 4, bình phương, chia 2 và trừ 7 rồi cho biết kết quả, máy sẽ in ra số người chơi đã nghĩ). 19. Một sinh viên gồm có các thông tin: họ tên, tuổi, điểm toán (hệ số 2), điểm tin (hệ số 1). Hãy nhập các thông tin trên cho 2 sinh viên. In ra bảng điểm gồm các chi tiết nêu trên và điểm trung bình của mỗi sinh viên. 20. Một nhân viên gồm có các thông tin: họ tên, hệ số lương, phần trăm phụ cấp (theo lưong) và phần trăm phải đóng BHXH. Hãy nhập các thông tin trên cho 2 nhân viên. In ra bảng lương gồm các chi tiết nêu trên và tổng số tiền cuối cùng mỗi nhân viên được nhận. CHƯƠNG 3 CẤU TRÚC ĐIỀU KHIỂN VÀ DỮ LIỆU KIỂU MẢNG Cấu trúc rẽ nhánh Cấu trúc lặp Mảng dữ liệu Mảng hai chiều I. []{#bookmark397.anchor}CẤU TRÚC RẼ NHÁNH Nói chung việc thực hiện chương trình là hoạt động tuần tự, tức thực hiện từng lệnh một từ câu lệnh bắt đầu của chương trình cho đến câu lệnh cuối cùng. Tuy nhiên, để việc lập trình hiệu quả hơn hầu hết các NNLT bậc cao đều có các câu lệnh rẽ nhánh và các câu lệnh lặp cho phép thực hiện các câu lệnh của chương trình không theo trình tự tuần tự như trong văn bản. Phần này chúng tôi sẽ trình bày các câu lệnh cho phép rẽ nhánh như vậy. Để thống nhất mỗi câu lệnh được trình bày về cú pháp (tức cách viết câu lệnh), cách sử dụng, đặc điểm, ví dụ minh hoạ và một vài điều cần chú ý khi sử dụng lệnh. 1. []{#bookmark401.anchor}Câu l ệnh điều kiện if a. *Ý nghĩa* Một câu lệnh if cho phép chương trình có thể thực hiện khối lệnh này hay khối lệnh khác phụ thuộc vào một điều kiện được viết trong câu lệnh là đúng hay sai. Nói cách khác câu lệnh if cho phép chương trình rẽ nhánh (chỉ thực hiện 1 trong 2 nhánh). b. *Cú pháp* - **if (điều kiện) { khối lệnh 1; } else { khối lệnh 2; }** - **if (điều kiện) { khối lệnh 1; }** Trong cú pháp trên câu lệnh if có hai dạng: có else và không có else. điều kiện là một biểu thức lôgic tức nó có giá trị đúng (khác 0) hoặc sai (bằng 0). Khi chương trình thực hiện câu lệnh if nó sẽ tính biểu thức điều kiện. Nếu điều kiện đúng chương trình sẽ tiếp tục thực hiện các lệnh trong khối lệnh 1, ngược lại nếu điều kiện sai chương trình sẽ thực hiện khối lệnh 2 (nếu có else) hoặc không làm gì (nếu không có else). c. *Đặc điểm* - - là tương đương với d. *Ví dụ minh hoạ* *[Ví dụ 1]* : Bằng phép toán gán có điều kiện có thể tìm số lớn nhất max trong 2 số a, b như sau: max = (a \ b) ? a: b ; hoặc max được tìm bởi dùng câu lệnh if: *[Ví dụ 2]* : Tính năm nhuận. Năm thứ n là nhuận nếu nó chia hết cho 4, nhưng không chia hết cho 100 hoặc chia hết 400. Chú ý: một số nguyên a là chia hết cho b nếu phần dư của phép chia bằng 0, tức a%b == 0. cout \ a. *Ý nghĩa* Câu lệnh if cho ta khả năng được lựa chọn một trong hai nhánh để thực hiện, do đó nếu sử dụng nhiều lệnh if lồng nhau sẽ cung cấp khả năng được rẽ theo nhiều nhánh. Tuy nhiên trong trường hợp như vậy chương trình sẽ rất khó đọc, do vậy C++ còn cung cấp một câu lệnh cấu trúc khác cho phép chương trình có thể chọn một trong nhiều nhánh để thực hiện, đó là câu lệnh switch. b. *Cú pháp* - biểu thức điều khiển: phải có kiểu nguyên hoặc kí tự, - các biểu\_thức\_i: được tạo từ các hằng nguyên hoặc kí tự, - các dãy lệnh có thể rỗng. Không cần bao dãy lệnh bởi cặp dấu {}, - c. *Cách thực hiện* Để thực hiện câu lệnh switch đầu tiên chương trình tính giá trị của biểu thức điều khiển (btđk), sau đó so sánh kết quả của btđk với giá trị của các biểu\_thức\_i bên dưới lần lượt từ biểu thức đầu tiên (thứ nhất) cho đến biểu thức cuối cùng (thứ n), nếu giá trị của btđk bằng giá trị của biểu thức thứ i đầu tiên nào đó thì chương trình sẽ thực hiện dãy lệnh thứ i và tiếp tục thực hiện tất cả dãy lệnh còn lại (từ dãy lệnh thứ i+1) cho đến hết (gặp dấu ngoặc đóng } của lệnh switch). Nếu quá trình so sánh không gặp biểu thức (nhánh case) nào bằng với giá trị của btđk thì chương trình thực hiện dãy lệnh trong default và tiếp tục cho đến hết (sau default có thể còn những nhánh case khác). Trường hợp câu lệnh switch không có nhánh default và btđk không khớp với bất cứ nhánh case nào thì chương trình không làm gì, coi như đã thực hiện xong lệnh switch. Nếu muốn lệnh switch chỉ thực hiện nhánh thứ i (khi btđk = biểu\_thức\_i) mà không phải thực hiện thêm các lệnh còn lại thì cuối dãy lệnh thứ i thông thường ta đặt thêm lệnh break; đây là lệnh cho phép thoát ra khỏi một lệnh cấu trúc bất kỳ. d. *Ví dụ minh hoạ* *[Ví dụ 1]* : In số ngày của một tháng bất kỳ nào đó được nhập từ bàn phím. Trong chương trình trên giả sử NSD nhập tháng là 5 thì chương trình bắt đầu thực hiện dãy lệnh sau case 5 (không có lệnh nào) sau đó tiếp tục thực hiện các lệnh còn lại, cụ thể là bắt đầu từ dãy lệnh trong case 7, đến case 12 chương trình gặp lệnh in kết quả \"tháng này có 31 ngày\", sau đó gặp lệnh break nên chương trình thoát ra khỏi câu lệnh switch (đã thực hiện xong). Việc giải thích cũng tương tự cho các trường hợp khác của tháng. Nếu NSD nhập sai tháng (ví dụ tháng nằm ngoài phạm vi 1..12), chương trình thấy th không khớp với bất kỳ nhánh case nào nên sẽ thực hiện câu lệnh trong default, in ra màn hình dòng chữ \"Bạn đã nhập sai tháng, không có tháng này\" và kết thúc lệnh. *[Ví dụ 2]* : Nhập 2 số a và b vào từ bàn phím. Nhập kí tự thể hiện một trong bốn phép toán: cộng, trừ, nhân, chia. In ra kết quả thực hiện phép toán đó trên 2 số a, b. void main() { } Trong chương trình trên ta chấp nhận các kí tự x,., \* thể hiện cho phép toán nhân và :, / thể hiện phép toán chia. 3. []{#bookmark449.anchor}Câu l ệnh nhảy goto a. *Ý nghĩa* Một dạng khác của rẽ nhánh là câu lệnh nhảy goto cho phép chương trình chuyển đến thực hiện một đoạn lệnh khác bắt đầu từ một điểm được đánh dấu bởi một nhãn trong chương trình. Nhãn là một tên gọi do NSD tự đặt theo các qui tắt đặt tên gọi. Lệnh goto thường được sử dụng để tạo vòng lặp. Tuy nhiên việc xuất hiện nhiều lệnh goto dẫn đến việc khó theo dõi trình tự thực hiện chương trình, vì vậy lệnh này thường được sử dụng rất hạn chế. b. *Cú pháp* Vị trí chương trình chuyển đến thực hiện là đoạn lệnh đứng sau nhãn và dấu hai chấm (:). c. *Ví dụ minh hoạ* *[Ví dụ 3]* : Nhân 2 số nguyên theo phương pháp Ản độ. Phương pháp Ản độ cho phép nhân 2 số nguyên bằng cách chỉ dùng các phép toán nhân đôi, chia đôi và cộng. Các phép nhân đôi và chia đôi thực chất là phép toán dịch bit về bên trái (nhân) hoặc bên phải (chia) 1 bit. Đây là các phép toán cơ sở trong bộ xử lý, do vậy dùng phương pháp này sẽ làm cho việc nhân các số nguyên được thực hiện rất nhanh. Có thể tóm tắt phương pháp như sau: Giả sử cần nhân m với n. Kiểm tra m nếu lẻ thì cộng thêm n vào kq (đầu tiên kq được khởi tạo bằng 0), sau đó lấy m chia 2 và n nhân 2. Quay lại kiểm tra m và thực hiện như trên. Quá trình dừng khi không thể chia đôi m được nữa (m = 0), khi đó kq là kết quả cần tìm (tức kq = m\*n). Để dễ hiểu phương pháp này chúng ta tiến hành tính trên ví dụ với các số m, n cụ thể. Giả sử m = 21 và n = 11. Các bước tiến hành được cho trong bảng dưới đây: ------ ------------ ------------ ------------------------------------------ Bước m (chia 2) n (nhân 2) kq (khởi tạo kq = 0) 1 21 11 m lẻ, cộng thêm 11 vào kq = 0 + 11 = 11 2 10 22 m chẵn, bỏ qua 3 5 44 m lẻ, cộng thêm 44 vào kq = 11 + 44 = 55 ------ ------------ ------------ ------------------------------------------ --- --- ----- --------------------------------------------- 4 2 88 m chẵn, bỏ qua 5 1 176 m lẻ, cộng thêm 176 vào kq = 55 + 176 = 231 6 0 m = 0, dừng cho kết quả kq = 231 --- --- ----- --------------------------------------------- II. []{#bookmark465.anchor}CẤU TRÚC LẶP Một trong những cấu trúc quan trọng của lập trình cấu trúc là các câu lệnh cho phép lặp nhiều lần một đoạn lệnh nào đó của chương trình. Chẳng hạn trong ví dụ về bài toán nhân theo phương pháp Ản độ, để lặp lại một đoạn lệnh chúng ta đã sử dụng câu lệnh goto. Tuy nhiên như đã lưu ý việc dùng nhiều câu lệnh này làm chương trình rất khó đọc. Do vậy cần có những câu lệnh khác trực quan hơn và thực hiện các phép lặp một cách trực tiếp. C++ cung cấp cho chúng ta 3 lệnh lặp như vậy. Về thực chất 3 lệnh này là tương đương (cũng như có thể dùng goto thay cho cả 3 lệnh lặp này), tuy nhiên để chương trình viết được sáng sủa, rõ ràng, C++ đã cung cấp nhiều phương án cho NSD lựa chọn câu lệnh khi viết chương trình phù hợp với tính chất lặp. Mỗi bài toán lặp có một đặc trưng riêng, ví dụ lặp cho đến khi đã đủ số lần định trước thì dừng hoặc lặp cho đến khi một điều kiện nào đó không còn thoả mãn nữa thì dừng \... việc sử dụng câu lệnh lặp phù hợp sẽ làm cho chương trình dễ đọc và dễ bảo trì hơn. Đây là ý nghĩa chung của các câu lệnh lặp, do vậy trong các trình bày về câu lệnh tiếp theo sau đây chúng ta sẽ không cần phải trình bày lại ý nghĩa của chúng. 1. []{#bookmark469.anchor}Lệnh lặp for a. Cú pháp - - Điều kiện lặp: là biểu thức lôgic (có giá trị đúng, sai). - b. Cách thực hiện Khi gặp câu lệnh for trình tự thực hiện của chương trình như sau: - Thực hiện dãy biểu thức 1 (thông thường là các lệnh khởi tạo cho một số biến), - Tóm lại, biểu thức 1 sẽ được thực hiện 1 lần duy nhất ngay từ đầu quá trình lặp sau đó thực hiện các câu lệnh lặp và dãy biểu thức 2 cho đến khi nào không còn thoả điều kiện lặp nữa thì dừng. c. Ví dụ minh hoạ *[Ví dụ 1]* : Nhân 2 số nguyên theo phương pháp Ản độ void main() { } So sánh ví dụ này với ví dụ dùng goto ta thấy chương trình được viết rất gọn. Để bạn đọc dễ hiểu câu lệnh for, một lần nữa chúng ta nhắc lại cách hoạt động của nó thông qua ví dụ này, trong đó các thành phần được viết trong cú pháp là như sau: - Dãy biểu thức 1: kq = 0, - Điều kiện lặp: m. Ở đây điều kiện là đúng nếu m \^ 0 và sai nếu m = 0. - - Cách thực hiện của chương trình như sau: - - - Quay lại thực hiện các biểu thức 2 tức chia đôi m và nhân đôi n và vòng lặp được tiếp tục lại bắt đầu bằng việc kiểm tra m \... - Đến một bước lặp nào đó m sẽ bằng 0 (vì bị chia đôi liên tiếp), điều kiện không thoả, vòng lặp dừng và cho ta kết quả là kq. *[Ví dụ 2]* : Tính tổng của dãy các số từ 1 đến 100. Chương trình dùng một biến đếm i được khởi tạo từ 1, và một biến kq để chứa tổng. Mỗi bước lặp chương trình cộng i vào kq và sau đó tăng i lên 1 đơn vị. Chương trình còn lặp khi nào i còn chưa vượt qua 100. Khi i lớn hơn 100 chương trình dừng. Sau đây là văn bản chương trình. void main() { } *[Ví dụ 3]* : In ra màn hình dãy số lẻ bé hơn một số n nào đó được nhập vào từ bàn phím. Chương trình dùng một biến đếm i được khởi tạo từ 1, mỗi bước lặp chương trình sẽ in i sau đó tăng i lên 2 đơn vị. Chương trình còn lặp khi nào i còn chưa vượt qua n. Khi i lớn hơn n chương trình dừng. Sau đây là văn bản chương trình. void main() { } d. Đặc điểm Thông qua phần giải thích cách hoạt động của câu lệnh for trong ví dụ 7 có thể thấy các thành phần của for có thể để trống, tuy nhiên các dấu chấm phẩy vẫn giữ lại để ngăn cách các thành phần với nhau. Ví dụ câu lệnh for (kq = 0 ; m ; m \\= 1, n \ a. Cú pháp b. Thực hiện Khi gặp lệnh while chương trình thực hiện như sau: đầu tiên chương trình sẽ kiểm tra điều kiện, nếu đúng thì thực hiện khối lệnh lặp, sau đó quay lại kiểm tra điều kiện và tiếp tục. Nếu điều kiện sai thì dừng vòng lặp. Tóm lại có thể mô tả một cách ngắn gọn về câu lệnh while như sau: *lặp lại các lệnh trong khi điều kiện vẫn còn đúng.* c. Đặc điểm - Khối lệnh lặp có thể không được thực hiện lần nào nếu điều kiện sai ngay từ đầu. - - d. Ví dụ minh hoạ *[Ví dụ 1]* : Nhân 2 số nguyên theo phương pháp Ản độ void main() { } Trong chương trình trên câu lệnh while (m) \... được đọc là \"trong khi m còn khác 0 thực hiện.\", ta thấy trong khối lệnh lặp có lệnh m \\= 1, lệnh này sẽ ảnh hưởng đến điều kiện (m), đến lúc nào đó m bằng 0 tức (m) là sai và chương trình sẽ dừng lặp. Câu lệnh while (m) \... cũng có thể được thay bằng while (1) \... như sau: void main() { } *[Ví dụ 2]* : Bài toán cổ: vừa gà vừa chó bó lại cho tròn đếm dủ 100 chân. Hỏi có mấy gà và mấy con chó, biết tổng số con là 36. void main() { } *[Ví dụ 3]* : Tìm ước chung lớn nhất (UCLN) của 2 số nguyên m và n. Áp dụng thuật toán Euclide bằng cách liên tiếp lấy số lớn trừ đi số nhỏ khi nào 2 số bằng nhau thì đó là UCLN. Trong chương trình ta qui ước m là số lớn và n là số nhỏ. Thêm biến phụ r để tính hiệu của 2 số. Sau đó đặt lại m hoặc n bằng r sao cho m \ n và lặp lại. Vòng lặp dừng khi m = n. void main() { } *[Ví dụ 4]* : Tìm nghiệm xấp xỉ của phương trình e^x^ - 1.5 = 0, trên đoạn \[0, 1\] với độ chính xác 10^-6^ bằng phương pháp chia đôi. Để viết chương trình này chúng ta nhắc lại phương pháp chia đôi. Cho hàm f(x) liên tục và đổi dấu trên một đoạn \[a, b\] nào đó (tức f(a), f(b) trái dấu nhau hay f(a)\*f(b)\ *\ a. Cú pháp b. Thực hiện Đầu tiên chương trình sẽ thực hiện khối lệnh lặp, tiếp theo kiểm tra điều kiện, nếu điều kiện còn đúng thì quay lại thực hiện khối lệnh và quá trình tiếp tục cho đến khi điều kiện trở thành sai thì dừng. c. Đặc điểm Các đặc điểm của câu lệnh do \... while cũng giống với câu lệnh lặp while trừ điểm khác biệt, đó là khối lệnh trong do \... while sẽ được thực hiện ít nhất một lần, trong khi trong câu lệnh while có thể không được thực hiện lần nào (vì lệnh while phải kiểm tra điều kiện trước khi thực hiện khối lệnh, do đó nếu điều kiện sai ngay từ đầu thì lệnh sẽ dừng, khối lệnh không được thực hiện lần nào. Trong khi đó lệnh do \... while sẽ thực hiện khối lệnh rồi mới kiểm tra điều kiện lặp để cho phép thực hiện tiếp hoặc dừng). d. Ví dụ minh hoạ *„.,\...\...1.1 -. n* 2 111 1 *[Ví dụ 1]* : Tính xấp xỉ số pi theo công thức Euler ---- = -2+-y+-2+*\...*+ 2 , với 6 12 22 32 *n* -^1^-\ a. Lệnh break Công dụng của lệnh dùng để thoát ra khỏi (chấm dứt) các câu lệnh cấu trúc, chương trình sẽ tiếp tục thực hiện các câu lệnh tiếp sau câu lệnh vừa thoát. Các ví dụ minh hoạ bạn đọc có thể xem lại trong các ví dụ về câu lệnh switch, for, while. b. Lệnh continue Lệnh dùng để quay lại đầu vòng lặp mà không chờ thực hiện hết các lệnh trong khối lệnh lặp. *[Ví dụ 1]* : Giả sử với mỗi i từ 1 đến 100 ta cần thực hiện một loạt các lệnh nào đó trừ những số i là số chính phương. Như vậy để tiết kiệm thời gian, vòng lặp sẽ kiểm tra nếu i là số chính phương thì sẽ quay lại ngay từ đầu để thực hiện với i tiếp theo. int i ; for (i = 1; i \ 1. []{#bookmark574.anchor}Mảng một chiều a. Ý nghĩa Khi cần lưu trữ một dãy n phần tử dữ liệu chúng ta cần khai báo n biến tương ứng với n tên gọi khác nhau. Điều này sẽ rất khó khăn cho người lập trình để có thể nhớ và quản lý hết được tất cả các biến, đặc biệt khi n lớn. Trong thực tế, hiển nhiên chúng ta gặp rất nhiều dữ liệu có liên quan đến nhau về một mặt nào đó, ví dụ chúng có cùng kiểu và cùng thể hiện một đối tượng: như các toạ độ của một vectơ, các số hạng của một ma trận, các sinh viên của một lớp hoặc các dòng kí tự của một văn bản. Lợi dụng đặc điểm này toàn bộ dữ liệu (cùng kiểu và cùng mô tả một đối tượng) có thể chỉ cần chung một tên gọi để phân biệt với các đối tượng khác, và để phân biệt các dữ liệu trong cùng đối tượng ta sử dụng cách đánh số thứ tự cho chúng, từ đó việc quản lý biến sẽ dễ dàng hơn, chương trình sẽ gọn và có tính hệ thống hơn. Giả sử ta có 2 vectơ trong không gian ba chiều, mỗi vec tơ cần 3 biến để lưu 3 toạ độ, vì vậy để lưu toạ độ của 2 vectơ chúng ta phải dùng đến 6 biến, ví dụ x1, y1, z1 cho vectơ thứ nhất và x2, y2, z2 cho vectơ thứ hai. Một kiểu dữ liệu mới được gọi là mảng một chiều cho phép ta chỉ cần khai báo 2 biến v1 và v2 để chỉ 2 vectơ, trong đó mỗi v1 hoặc v2 sẽ chứa 3 dữ liệu được đánh số thứ tự từ 0 đến 2, trong đó ta có thể ngầm định thành phần 0 biểu diễn toạ độ x, thành phần 1 biểu diễn toạ độ y và thành phần có số thứ tự 2 sẽ biểu diễn toạ độ z. Tóm lại, mảng là một dãy các thành phần có cùng kiểu được sắp kề nhau liên tục trong bộ nhớ. Tất cả các thành phần đều có cùng tên là tên của mảng. Để phân biệt các thành phần với nhau, các thành phần sẽ được đánh số thứ tự từ 0 cho đến hết mảng. Khi cần nói đến thành phần cụ thể nào của mảng ta sẽ dùng tên mảng và kèm theo số thứ tự của thành phần đó. Dưới đây là hình ảnh của một mảng gồm có 9 thành phần, các thành phần được đánh số từ 0 đến 8. 012345678 b. Khai báo - - - Một mảng dữ liệu được lưu trong bộ nhớ bởi dãy các ô liên tiếp nhau. Số - - Ví dụ: - Khai báo biến chứa 2 vectơ a, b trong không gian 3 chiều: - Khai báo 3 phân số a, b, c; trong đó a = 1/3 và b = 3/5: - Khai báo mảng L chứa được tối đa 100 số nguyên dài: - Khai báo mảng dong (dòng), mỗi dòng chứa được tối đa 80 kí tự: - Khai báo dãy Data chứa được 5 số thực độ chính xác gấp đôi: c. Cách sử dụng i. ii. Tương tự, giả sử chúng ta cần cộng 2 phân số a, b và đặt kết quả vào c. Không thể viết: mà cần phải tính từng phần tử của c: Để khắc phục nhược điểm này, trong các chương sau C++ cung cấp một kiểu dữ liệu mới gọi là lớp, và cho phép NSD có thể định nghĩa riêng phép cộng cho 2 mảng tuỳ ý, khi đó có thể viết một cách đơn giản và quen thuộc c = a + b để cộng 2 phân số. d. Ví dụ minh hoạ *[Ví dụ 1]* : Tìm tổng, tích 2 phân số. void main() { } *[Ví dụ 2]* : Nhập dãy số nguyên, tính: số số hạng dương, âm, bằng không của dãy. void main() { } *[Ví dụ 3]* : Tìm số bé nhất của một dãy số. In ra số này và vị trí của nó trong dãy. Chương trình sử dụng mảng a để lưu dãy số, n là số phần tử thực sự trong dãy, min lưu số bé nhất tìm được và k là vị trí của min trong dãy. min được khởi tạo bằng giá trị đầu tiên (a\[0\]), sau đó lần lượt so sánh với các số hạng còn lại, nếu gặp số hạng nhỏ hơn, min sẽ nhận giá trị của số hạng này. Quá trình so sánh tiếp tục cho đến hết dãy. Vì số số hạng của dãy là biết trước (n), nên số lần lặp cũng được biết trước (n-1 lần lặp), do vậy chúng ta sẽ sử dụng câu lệnh for cho ví dụ này. void main() { cout \ 12. Giá trị của x bằng bao nhiêu sau khi thực hiện cấu trúc for sau: 13. Bạn bổ sung gì vào lệnh for sau: để khi kết thúc nam có giá trị 2000. 14. Bao nhiêu kí tự 'X' được in ra màn hình khi thực hiện đoạn chương trình sau: 15. Nhập vào tuổi cha và tuổi con hiện nay sao cho tuổi cha lớn hơn 2 lần tuổi con. Tìm xem bao nhiêu năm nữa tuổi cha sẽ bằng đúng 2 lần tuổi con (ví dụ 30 và 12, sau 6 năm nữa tuổi cha là 36 gấp đôi tuổi con là 18). 16. Nhập số nguyên dương N. Tính: a. *S,* ^=^ 77 b. *S2* = 712 + 2^2^ + 3^2^ +*\...* + *N^2^* 17. Nhập số nguyên dương n. Tính: a\. *S*1 *= yỊ*3*+[ạ/]{.smallcaps}*3*+\^Ị*3 + *\...+\^ỉ* *~S~* = [1,] ^*S*\ 2^ 1 ^2+^\^" ^2+^\^1 18. Nhập số tự nhiên n. In ra màn hình biểu diễn của n ở dạng nhị phân. 19. In ra màn hình các số có 2 chữ số sao cho tích của 2 chữ số này bằng 2 lần tổng của 2 chữ số đó (ví dụ số 36 có tích 3\*6 = 18 gấp 2 lần tổng của nó là 3 + 6 = 9). 20. Số *hoàn chỉnh* là số bằng tổng mọi ước của nó (không kể chính nó). Ví dụ 6 = 1 + 2 + 3 là một số hoàn chỉnh. Hãy in ra màn hình tất cả các số hoàn chỉnh \< 1000. 21. Các số *sinh đôi* là các số nguyên tố mà khoảng cách giữa chúng là 2. Hãy in tất cả cặp số sinh đôi \< 1000. 22. Nhập dãy kí tự đến khi gặp kí tự '.' thì dừng. Thống kê số chữ cái viết hoa, viết thường, số chữ số và tổng số các kí tự khác đã nhập. Loại kí tự nào nhiều nhất ? 23. Tìm số nguyên dương n lớn nhất thoả mãn điều kiện: a\. 1+^1^+^1^+*\...* +-\^ \< 2*.*101999. b. *e^n^* -1999*log~10~ n \ 24. Cho s = 1e-6. Tính gần đúng các số sau: n\'1,1,1 ,1 6 = ỳ^+^22^+^22^+^*. 2*2 ^dừn^g ^lặ^p ^khi^ I *^s^n ^-\ s^n*-J \ a. e^x^ - 1.5 = 0, trên đoạn \[0, 1\]. b. x2^x^ - 1 = 0, trên đoạn \[0, 1\]. c. a~0~x^n^ + aix^11^\'^1^ + \... + a~n~ = 0, trên đoạn \[a, b\]. Các số thực ai, a, b được nhập từ bàn phím sao cho f(a) và f(b) trái dấu. Mảng 27. Nhập vào dãy n số thực. Tính tổng dãy, trung bình dãy, tổng các số âm, dương và tổng các số ở vị trí chẵn, vị trí lẻ trong dãy. Tìm phần tử gần số trung bình nhất của dãy. 28. Tìm và chỉ ra vị trí xuất hiện đầu tiên của phần tử x trong dãy. 29. Nhập vào dãy n số. Hãy in ra số lớn nhất, bé nhất của dãy. 30. Nhập vào dãy số. In ra dãy đã được sắp xếp tăng dần, giảm dần. 31. Cho dãy đã được sắp tăng dần. Chèn thêm vào dãy phần tử x sao cho dãy vẫn sắp xếp tăng dần. 32. Hãy nhập vào 16 số nguyên. In ra thành 4 dòng, 4 cột. 33. Nhập ma trận A và in ra ma trận đối xứng của nó. 34. Cho một ma trận nguyên kích thước m\*n. Tính: - Tổng tất cả các phần tử của ma trận. - Tổng tất cả các phần tử dương của ma trận. - Tổng tất cả các phần tử âm của ma trận. - Tổng tất cả các phần tử chẵn của ma trận. - Tổng tất cả các phần tử lẻ của ma trận. 35. Cho một ma trận thực kích thước m\*n. Tìm: - Số nhỏ nhất, lớn nhất (kèm chỉ số) của ma trận. - Số nhỏ nhất, lớn nhất (kèm chỉ số) của từng hàng của ma trận. - Số nhỏ nhất, lớn nhất (kèm chỉ số) của từng cột của ma trận. - Số nhỏ nhất, lớn nhất (kèm chỉ số) của đường chéo chính của ma trận. - Số nhỏ nhất, lớn nhất (kèm chỉ số) của đường chéo phụ của ma trận. 36. Nhập 2 ma trận vuông cấp n A và B. Tính A + B, A - B, A \* B và A^2^ - B^2^. Xâu kí tự 37. Hãy nhập một xâu kí tự. In ra màn hình đảo ngược của xâu đó. 38. Nhập xâu. Thống kê số các chữ số \'0\', số chữ số \'1\',.*..,* số chữ số \'9\' trong xâu. 39. In ra vị trí kí tự trắng đầu tiên từ bên trái (phải) một xâu kí tự. 40. Nhập xâu. In ra tất các các vị trí của chữ \'a\' trong xâu và tổng số lần xuât hiện của nó. 41. Nhập xâu. Tính số từ có trong xâu. In mỗi dòng một từ. 42. Nhập xâu họ tên, in ra họ, tên dưới dạng viết hoa. 43. Thay kí tự x trong xâu s bởi kí tự y (s, x, y được đọc vào từ bàn phím) 44. Xoá mọi kí tự x có trong xâu s (s, x được đọc vào từ bàn phím). (Gợi ý: nên xoá ngược từ cuối xâu về đầu xâu). 45. Nhập xâu. Không phân biệt viết hoa hay viết thường, hãy in ra các kí tự có mặt trong xâu và số lần xuất hiện của nó (ví dụ xâu "Trach - Van - Doanh" có chữ a xuất hiện 3 lần, c(1), d(1), h(2), n(2), o(1), r(1), t(1), -(2), space(4)). CHƯƠNG 4 HÀM VÀ CHƯƠNG TRÌNH Con trỏ và số học địa chỉ Hàm Đệ qui Tổ chức chương trình I. []{#bookmark733.anchor}CON TRỎ VÀ SỐ HỌC ĐỊA CHỈ Trước khi bàn về hàm và chương trình, trong phần này chúng ta sẽ nói về một loại biến mới gọi là con trỏ, ý nghĩa, công dụng và sử dụng nó như thế nào. Biến con trỏ là một đặc trưng mạnh của C++, nó cho phép chúng ta thâm nhập trực tiếp vào bộ nhớ để xử lý các bài toán khó bằng chỉ vài câu lệnh đơn giản của chương trình. Điều này cũng góp phần làm cho C++ trở thành ngôn ngữ gần gũi với các ngôn ngữ cấp thấp như hợp ngữ. Tuy nhiên, vì tính đơn giản, ngắn gọn nên việc sử dụng con trỏ đòi hỏi tính cẩn thận cao và giàu kinh nghiệm của người lập trình. 1. []{#bookmark737.anchor}Địa chỉ, phép toán & Mọi chương trình trước khi chạy đều phải bố trí các biến do NSD khai báo vào đâu đó trong bộ nhớ. Để tạo điều kiện truy nhập dễ dàng trở lại các biến này, bộ nhớ được đánh số, mỗi byte sẽ được ứng với một số nguyên, được gọi là địa chỉ của byte đó từ 0 đến hết bộ nhớ. Từ đó, mỗi biến (với tên biến) được gắn với một số nguyên là địa chỉ của byte đầu tiên mà biến đó được phân phối. Số lượng các byte phân phối cho biến là khác nhau (nhưng đặt liền nhau từ thấp đến cao) tuỳ thuộc kiểu dữ liệu của biến (và tuỳ thuộc vào quan niệm của từng NNLT), tuy nhiên chỉ cần biết tên biến hoặc địa chỉ của biến ta có thể đọc/viết dữ liệu vào/ra các biến đó. Từ đó ngoài việc thông qua tên biến chúng ta còn có thể thông qua địa chỉ của chúng để truy nhập vào nội dung. Tóm lại biến, ô nhớ và địa chỉ có quan hệ khăng khít với nhau. C++ cung cấp một toán tử một ngôi & để lấy địa chỉ của các biến (ngoại trừ biến mảng và xâu kí tự). Nếu x là một biến thì &x là địa chỉ của x. Từ đó câu lệnh sau cho ta biết x được bố trí ở đâu trong bộ nhớ: int x ; cout \ a. *Ý nghĩa* - - Để con trỏ p trỏ tới x ta phải gán địa chỉ của x cho p. b. *Khai báo biến con trỏ* Địa chỉ của một biến là địa chỉ byte nhớ đầu tiên của biến đó. Vì vậy để lấy được nội dung của biến, con trỏ phải biết được số byte của biến, tức kiểu của biến mà con trỏ sẽ trỏ tới. Kiểu này cũng được gọi là kiểu của con trỏ. Như vậy khai báo biến con trỏ cũng giống như khai báo một biến thường ngoại trừ cần thêm dấu \* trước tên biến (hoặc sau tên kiểu). Ví dụ: int \*p ; // khai báo biến p là biến con trỏ trỏ đến kiểu dữ liệu nguyên. float \*q, \*r ; // hai con trỏ thực q và r. c. *Sử dụng con trỏ, phép toán \** - Để con trỏ p trỏ đến biến x ta phải dùng phép gán p = địa chỉ của x. - Nếu x không phải là mảng ta viết: p = &x. - Nếu x là mảng ta viết: p = x hoặc p = &x\[0\]. - Không gán p cho một hằng địa chỉ cụ thể. Ví dụ viết p = 200 là sai. - - (\*q)++ ; cout \ a. *Con trỏ và mảng 1 chiều* Việc cho con trỏ trỏ đến mảng cũng tưong tự trỏ đến các biến khác, tức gán địa chỉ của mảng (chính là tên mảng) cho con trỏ. Chú ý rằng địa chỉ của mảng cũng là địa chỉ của thành phần thứ 0 nên a+i sẽ là địa chỉ thành phần thứ i của mảng. Tưong tự, nếu p trỏ đến mảng a thì p+i là địa chỉ thành phần thứ i của mảng a và do đó \*(p+i) = a\[i\] = \*(a+i). Chú ý khi viết \*(p+1) = \*(a+1) ta thấy vai trò của p và a trong biểu thức này là như nhau, cùng truy cập đến giá trị của phần tử a\[1\]. Tuy nhiên khi viết \*(p++) thì lại khác với \*(a++), cụ thể viết p++ là hợp lệ còn a++ là không được phép. Lý do là tuy p và a cùng thể hiện địa chỉ của mảng a nhưng p thực sự là một biến, nó có thể thay đổi được giá trị còn a là một hằng, giá trị không được phép thay đổi. Ví dụ viết x = 3 và sau đó có thể tăng x bởi x++ nhưng không thể viết x = 3++. *[Ví dụ 1]* : In toàn bộ mảng thông qua con trỏ. int a\[5\] = {1,2,3,4,5}, \*p, i; 1: p = a; for (i=1; i\ a. *Khái niệm chung* Thực chất một con trỏ cũng là một biến thông thường có tên gọi (ví dụ p, q, \...), do đó cũng giống như biến, nhiều biến cùng kiểu có thể tổ chức thành một mảng với tên gọi chung, ở đây cũng vậy nhiều con trỏ cùng kiểu cũng được tổ chức thành mảng. Như vậy mỗi phần tử của mảng con trỏ là một con trỏ trỏ đến một mảng nào đó. Nói cách khác một mảng con trỏ cho phép quản lý nhiều mảng dữ liệu cùng kiểu. Cách khai báo: Ví dụ: khai báo một mảng chứa 10 con trỏ. Mỗi con trỏ a\[i\] chứa địa chỉ của một mảng nguyên nào đó. b. *Mảng xâu kí tự* Là trường hợp riêng của mảng con trỏ nói chung, trong đó kiểu cụ thể là char. Mỗi thành phần mảng là một con trỏ trỏ đến một xâu kí tự, có nghĩa các thao tác tiến hành trên \*a\[i\] như đối với một xâu kí tự. *[Ví dụ 1]* : Nhập vào và in ra một bài thơ. } II. []{#bookmark827.anchor}HÀM Hàm là một chương trình con trong chương trình lớn. Hàm nhận (hoặc không) các đối số và trả lại (hoặc không) một giá trị cho chương trình gọi nó. Trong trường hợp không trả lại giá trị, hàm hoạt động như một thủ tục trong các NNLT khác. Một chương trình là tập các hàm, trong đó có một hàm chính với tên gọi main(), khi chạy chương trình, hàm main() sẽ được chạy đầu tiên và gọi đến hàm khác. Kết thúc hàm main() cũng là kết thúc chương trình. Hàm giúp cho việc phân đoạn chương trình thành những môđun riêng rẽ, hoạt động độc lập với ngữ nghĩa của chương trình lớn, có nghĩa một hàm có thể được sử dụng trong chương trình này mà cũng có thể được sử dụng trong chương trình khác, dễ cho việc kiểm tra và bảo trì chương trình. Hàm có một số đặc trưng: - - - Không lồng nhau. - Có 3 cách truyền giá trị: Truyền theo tham trị, tham biến và tham trỏ. 1. []{#bookmark835.anchor}Khai báo và định nghĩa hàm a. Khai báo Một hàm thường làm chức năng: tính toán trên các tham đối và cho lại giá trị kết quả, hoặc chỉ đơn thuần thực hiện một chức năng nào đó, không trả lại kết quả tính toán. Thông thường kiểu của giá trị trả lại được gọi là kiểu của hàm. Các hàm thường được khai báo ở đầu chương trình. Các hàm viết sẵn được khai báo trong các file nguyên mẫu \*.h. Do đó, để sử dụng được các hàm này, cần có chỉ thị \#include \ a. Truyền mảng 1 chiều cho hàm Thông thường chúng ta hay xây dựng các hàm làm việc trên mảng như vectơ hay ma trận các phần tử. Khi đó tham đối thực sự của hàm sẽ là các mảng dữ liệu này. Trong trường hợp này ta có 2 cách khai báo đối. Cách thứ nhất đối được khai báo bình thường như khai báo biến mảng nhưng không cần có số phần tử kèm theo, ví dụ: - int x\[\]; - float x\[\]; Cách thứ hai khai báo đối như một con trỏ kiểu phần tử mảng, ví dụ: - int \*p; - float \*p Trong lời gọi hàm tên mảng a sẽ được viết vào danh sách tham đối thực sự, vì a là địa chỉ của phần tử đầu tiên của mảng a, nên khi hàm được gọi địa chỉ này sẽ gán cho con trỏ p. Vì vậy giá trị của phần tử thứ i của a có thể được truy cập bởi x\[i\] (theo khai báo 1) hoặc \*(p+i) (theo khai báo 2) và nó cũng có thể được thay đổi thực sự (do đây cũng là cách truyền theo dẫn trỏ). Sau đây là ví dụ đơn giản, nhập và in vectơ, minh hoạ cho cả 2 kiểu khai báo đối. *[Ví dụ 8]* : Hàm nhập và in giá trị 1 vectơ void nhap(int x\[\], int n) // n: số phần tử { } void in(int \*p, int n) { } main() { } b. Truyền mảng 2 chiều cho hàm Đối với mảng 2 chiều khai báo đối cũng như lời gọi là phức tạp hơn nhiều so với mảng 1 chiều. Ta có hai cách khai báo đối như sau: - float x\[\]\[n\] ; // mảng với số phần tử không định trước, mỗi phần tử là n số float (\*x)\[n\] ; // một con trỏ, có kiểu là mảng n số (float\[n\]) Để truy nhập đến đến phần tử thứ i,j ta vẫn sử dụng cú pháp x\[i\]\[j\]. Tên của mảng a được viết bình thường trong lời gọi hàm. Nói chung theo cách khai báo này việc truy nhập là đơn giản nhưng phương pháp cũng có hạn chế đó là số cột của mảng truyền cho hàm phải cố định bằng n. - - - - trong đó n là số cột của mảng truyền cho hàm. Điều này có nghĩa để truy cập đến a\[i\]\[j\] ta có thể viết \*(p+i\*n+j), ngược lại biết chỉ số k có thể tính được dòng i, cột j của phần tử này. Ưu điểm của cách khai báo này là ta có thể truyền mảng với kích thước bất kỳ (số cột không cần định trước) cho hàm. Sau đây là các ví dụ minh hoạ cho 2 cách khai báo trên. *[Ví dụ 9]* : Tính tổng các số hạng trong ma trận float tong(float x\[\]\[10\], int m, int n) // hoặc float tong(float (\*x)\[10\], int m, int n) { // m: s ố dòng, n: số cột } main() { } *[Ví dụ 10]* : Tìm phần tử bé nhất của ma trận void minmt(float \*x, int m, int n) // m: số dòng, n: số cột { main() { } *[Ví dụ 11]* : Cộng 2 ma trận và in kết quả. void inmt(float \*x, int m, int n) { } void cong(float \*x, float \*y, int m, int n) { } main() { } Xu hướng chung là chúng ta xem mảng (1 hoặc 2 chiều) như là một dãy liên tiếp các số trong bộ nhớ, tức một ma trận là một đối con trỏ trỏ đến thành phần của mảng. Đối với mảng 2 chiều m\*n khi truyền đối địa chỉ của ma trận cần phải ép kiểu về kiểu con trỏ. Ngoài ra bước chạy k của con trỏ (từ 0 đến m\*n-1) tương ứng với các toạ độ của phần tử a\[i\]\[j\] trong mảng như sau: - k = \*(p + i\*n + j) - i = k/n - j = k%n từ đó, chúng ta có thể viết các hàm mà không cần phải băn khoăn gì về kích thước của ma trận sẽ truyền cho hàm. c. Giá trị trả lại của hàm là một mảng Không có cách nào để giá trị trả lại của một hàm là mảng. Tuy nhiên thực sự mỗi\ mảng cũng chính là một con trỏ, vì vậy việc hàm trả lại một con trỏ trỏ đến dãy dữ liệu kết quả là tương đương với việc trả lại mảng. Ngoài ra còn một cách dễ dùng hơn đối với mảng 2 chiều là mảng kết quả được trả lại vào trong tham đối của hàm (giống như nghiệm của phương trình bậc 2 được trả lại vào trong các tham đối). Ở đây chúng ta sẽ lần lượt xét 2 cách làm việc này. // giá trị trả lại là con trỏ trỏ đến dãy số nguyên // tạo mảng kết quả với 3 giá trị 1, 2, 3 // trả lại địa chỉ cho con trỏ kết quả hàm int\* tragiatri2() // giá trị trả lại là con trỏ trỏ đến dãy số nguyên { main() { } Qua ví dụ trên ta thấy hai hàm trả giá trị đều tạo bên trong nó một mảng 3 số nguyên và trả lại địa chỉ mảng này cho con trỏ kết quả hàm. Tuy nhiên, chỉ có tragiatri2() là cho lại kết quả đúng. Tại sao ? Xét mảng kq được khai báo và khởi tạo trong tragiatri1(), đây là một mảng cục bộ (được tạo bên trong hàm) như sau này chúng ta sẽ thấy, các loại biến \"tạm thời\" này (và cả các tham đối) chỉ tồn tại trong quá trình hàm hoạt động. Khi hàm kết thúc các biến này sẽ mất đi. Do vậy tuy hàm đã trả lại địa\ chỉ của kq trước khi nó kết thúc, thế nhưng sau khi hàm thực hiện xong, toàn bộ kq sẽ được xoá khỏi bộ nhớ và vì vậy con trỏ kết quả hàm đã trỏ đến vùng nhớ không còn các giá trị như kq đã có. Từ điều này việc sử dụng hàm trả lại con trỏ là phải hết sức cẩn thận. Muốn trả lại con trỏ cho hàm thì con trỏ này phải trỏ đến dãy dữ liệu nào sao cho nó không mất đi sau khi hàm kết thúc, hay nói khác hơn đó phải là những dãy dữ liệu được khởi tạo bên ngoài hàm hoặc có thể sử dụng theo phương pháp trong hàm tragiatri2(). Trong tragiatri2() một mảng kết quả 3 số cũng được tạo ra nhưng bằng cách xin cấp phát vùng nhớ. Vùng nhớ được cấp phát này sẽ vẫn còn tồn tại sau khi hàm kết thúc (nó chỉ bị xoá đi khi sử dụng toán tử delete). Do vậy hoạt động của tragiatri2() là chính xác. Tóm lại, ví dụ trên cho thấy nếu muốn trả lại giá trị con trỏ thì vùng dữ liệu mà nó trỏ đến phải được cấp phát một cách tường minh (bằng toán tử new), chứ không để chương trình tự động cấp phát và tự động thu hồi. Ví dụ sau minh hoạ hàm cộng 2 vectơ và trả lại vectơ kết quả (thực chất là con trỏ trỏ đến vùng nhớ đặt kết quả) int\* congvt(int \*x, int \*y, int n) // n số phần tử của vectơ { } main() { } [Chú ý]: a\[i\], b\[i\], c\[i\] còn được viết dưới dạng tương đương \*(a+i), \*(b+i), \*(c+i). 2. *[Ví dụ 12]* : Cộng 2 vectơ, vectơ kết quả trả lại trong tham đối của hàm. So với ví dụ trước giá trị trả lại là void (không trả lại giá trị) còn danh sách đối có thêm con trỏ z để chứa kết quả. void congvt(int \*x, int \*y, int \*z, int n) // z lưu kết quả { } main() { } *[Ví dụ 13]* : Nhân 2 ma trận kích thước m\*n và n\*p. Hai ma trận đầu vào và ma trận kết quả (kích thước m\*p) đều được khai báo dưới dạng con trỏ và là đối của hàm nhanmt(). Nhắc lại, trong lời gọi hàm địa chỉ của 3 mảng cần được ép kiểu về (int\*) để phù hợp với các con trỏ tham đối. void nhanmt(int \*x, int \*y, int \*z, int m, int n, int p) // z lưu kết quả { } main() { } d. Đối và giá trị trả lại là xâu kí tự Giống các trường hợp đã xét với mảng 1 chiều, đối của các hàm xâu kí tự có thể khai báo dưới 2 dạng: mảng kí tự hoặc con trỏ kí tự. Giá trị trả lại luôn luôn là con trỏ kí tự. Ngoài ra hàm cũng có thể trả lại giá trị vào trong các đối con trỏ trong danh sách đối. Ví dụ sau đây dùng để tách họ, tên của một xâu họ và tên. Ví dụ gồm 3 hàm. Hàm họ trả lại xâu họ (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng mảng. Hàm tên trả lại xâu tên (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng con trỏ kí tự. Thực chất đối họ và tên trong hai hàm họ, tên có thể được khai báo theo cùng cách thức, ở đây chương trình muốn minh hoạ các cách khai báo đối khác nhau (đã đề cập đến trong phần đối mảng 1 chiều). Hàm thứ ba cũng trả lại họ, tên nhưng cho vào trong danh sách tham đối, do vậy hàm không trả lại giá trị (void). Để đơn giản ta qui ước xâu { char\* kq = new char\[10\]; int i=0; while (hoten\[i\] != \'\\40\') i++; strncpy(kq, hoten, i) ; return kq; } char\* ten(char\* hoten) { // hàm trả lại họ // cấp bộ nhớ để chứa họ // i dừng tại dấu cách đầu tiên // copy i kí tự của hoten vào kq // hàm trả lại tên // cấp bộ nhớ để chứa tên // i dừng tại dấu cách cuối cùng // copy tên vào kq { int i=0; while (hoten\[i\] != \'\\40\') i++; strncpy(ho, hoten, i) ; i=strlen(hoten); // i dừng tại dấu cách đầu tiên // copy i kí tự của hoten vào ho // i dừng tại dấu cách cuối cùng // copy tên vào ten } e. Đối là hằng con trỏ Theo phần truyền đối cho hàm ta đã biết để thay đổi biến ngoài đối tương ứng phải được khai báo dưới dạng con trỏ. Tuy nhiên, trong nhiều trường hợp các biến ngoài không có nhu cầu thay đổi nhưng đối tương ứng với nó vẫn phải khai báo dưới dạng con trỏ (ví dụ đối là mảng hoặc xâu kí tự). Điều này có khả năng do nhầm lẫn, các biến ngoài này sẽ bị thay đổi ngoài ý muốn. Trong trường hợp như vậy để cẩn thận, các đối con trỏ nếu không muốn thay đổi (chỉ lấy giá trị) cần được khai báo như là một hằng con trỏ bằng cách thêm trước khai báo kiểu của chúng từ khoá const. Từ khoá này khẳng định biến tuy là con trỏ nhưng nó là một hằng không thay đổi được giá trị. Nếu trong thân hàm ta cố tình thay đổi chúng thì chương trình sẽ báo lỗi. Ví dụ đối hoten trong cả 3 hàm ở trên có thể được khai báo dạng const char\* hoten. *[Ví dụ 14]* : Đối là hằng con trỏ. In hoa một xâu kí tự void inhoa(const char\* s) { } main() { } 8. []{#bookmark959.anchor}Con trỏ hàm Một hàm (tập hợp các lệnh) cũng giống như dữ liệu: có tên gọi , có địa chỉ lưu trong bộ nhớ và có thể truy nhập đến hàm thông qua tên gọi hoặc địa chỉ của nó. Để truy nhập (gọi hàm) thông qua địa chỉ chúng ta phải khai báo một con trỏ chứa địa chỉ này và sau đó gọi hàm bằng cách gọi tên con trỏ. a. Khai báo Ta thấy cách khai báo con trỏ hàm cũng tương tự khai báo con trỏ biến (chỉ cần đặt dấu \* trước tên), ngoài ra còn phải bao \*tên hàm giữa cặp dấu ngoặc (). Ví dụ: - float (\*f)(int); // khai báo con trỏ hàm có tên là f trỏ đến hàm - void (\*f)(float, int); // con trỏ trỏ đến hàm với cặp đối (float, int). hoặc phức tạp hơn: - char\* (\*m\[10\])(int, char) // khai báo một mảng 10 con trỏ hàm trỏ đến [Chú ý]: phân biệt giữa 2 khai báo: float (\*f)(int) và float \*f(int). Cách khai báo trước là khai báo con trỏ hàm có tên là f. Cách khai báo sau có thể viết lại thành float\* f(int) là khai báo hàm f với giá trị trả lại là một con trỏ float. b. Khởi tạo Một con trỏ hàm cũng giống như các con trỏ, được phép khởi tạo trong khi khai báo hoặc gán với một địa chỉ hàm cụ thể sau khi khai báo. Cũng giống như kiểu dữ liệu mảng, tên hàm chính là một hằng địa chỉ trỏ đến bản thân nó. Do vậy cú pháp của khởi tạo cũng như phép gán là như sau: trong đó f và tên hàm được trỏ phải giống nhau về kiểu trả lại và danh sách đối. Nói cách khác với mục đích sử dụng con trỏ f trỏ đến hàm (lớp hàm) nào đó thì f phải được khai báo với kiểu trả lại và danh sách đối giống như hàm đó. Ví dụ: float luythua(float, int); // khai báo hàm luỹ thừa float (\*f)(float, int); // khai báo con trỏ f tương thích với hàm luythua f = luythua; // cho f trỏ đến hàm luỹ thừa c. Sử dụng con trỏ hàm Để sử dụng con trỏ hàm ta phải gán nó với tên hàm cụ thể và sau đó bất kỳ nơi nào được phép xuất hiện tên hàm thì ta đều có thể thay nó bằng tên con trỏ. Ví dụ như các thao tác gọi hàm, đưa hàm vào làm tham đối hình thức cho một hàm khác \... Sau đây là các ví dụ minh hoạ. *[Ví dụ 15]* : Dùng tên con trỏ để gọi hàm float bphuong(float x) // hàm trả lại x^2^ { } void main() { } *[Ví dụ 16]* : Dùng hàm làm tham đối. Tham đối của hàm ngoài các kiểu dữ liệu đã biết còn có thể là một hàm. Điều này có tác dụng rất lớn trong các bài toán tính toán trên những đối tượng là hàm toán học như tìm nghiệm, tính tích phân của hàm trên một đoạn \... Hàm đóng vai trò tham đối sẽ được khai báo dưới dạng con trỏ hàm. Ví dụ sau đây trình bày hàm tìm nghiệm xấp xỉ của một hàm liên tục và đổi dấu trên đoạn \[a, b\]. Để hàm tìm nghiệm này sử dụng được trên nhiều hàm toán học khác nhau, trong hàm sẽ chứa một biến con trỏ hàm và hai cận a, b, cụ thể bằng khai báo float timnghiem(float (\*f)(float), float a, float b). Trong lời gọi hàm f sẽ được thay thế bằng tên hàm cụ thể cần tìm nghiệm. \#define EPS 1.0e-6 float timnghiem(float (\*f)(float), float a, float b); float emu(float); float loga(float); void main() { } float timnghiem(float (\*f)(float), float a, float b) { } float emux(float x) { return (exp(x)-2); } float logx(float x) { return (log(x)-1); } d. Mảng con trỏ hàm Tương tự như biến bình thường các con trỏ hàm giống nhau có thể được gộp lại vào trong một mảng, trong khai báo ta chỉ cần thêm \[n\] vào sau tên mảng với n là số lượng tối đa các con trỏ. Ví dụ sau minh hoạ cách sử dụng này. Trong ví dụ chúng ta xây dựng 4 hàm cộng, trừ, nhân, chia 2 số thực. Các hàm này giống nhau về kiểu, số lượng đối, \... Chúng ta có thể sử dụng 4 con trỏ hàm riêng biệt để trỏ đến các hàm này hoặc cũng có thể dùng mảng 4 con trỏ để trỏ đến các hàm này. Chương trình sẽ in ra kết quả cộng, trừ, nhân, chia của 2 số nhập vào từ bàn phím. *[Ví dụ 17]* : void cong(int a, int b) { cout \ 1. []{#bookmark986.anchor}Khái niệm đệ qui Một hàm gọi đến hàm khác là bình thường, nhưng nếu hàm lại gọi đến chính nó thì ta gọi hàm là đệ qui. Khi thực hiện một hàm đệ qui, hàm sẽ phải chạy rất nhiều lần, trong mỗi lần chạy chương trình sẽ tạo nên một tập biến cục bộ mới trên ngăn xếp (các đối, các biến riêng khai báo trong hàm) độc lập với lần chạy trước đó, từ đó dễ gây tràn ngăn xếp. Vì vậy đối với những bài toán có thể giải được bằng phương pháp lặp thì không nên dùng đệ qui. Để minh hoạ ta hãy xét hàm tính n giai thừa. Để tính n! ta có thể dùng phương pháp lặp như sau: main() { } Mặt khác, n! giai thừa cũng được tính thông qua (n-1)! bởi công thức truy hồi n! = 1 nếu n = 0 n! = (n-1)!n nếu n \ 0 do đó ta có thể xây dựng hàm đệ qui tính n! như sau: double gt(int n) { } main() { } Trong hàm main() giả sử ta nhập 3 cho n, khi đó để thực hiện câu lệnh cout \ - suy biến. Như vậy trong trường hợp tính n! nếu n = 0 hàm cho ngay giá trị 1 mà không cần phải gọi lại chính nó, đây chính là trường hợp suy biến. Trường hợp n \ 0 hàm sẽ gọi lại chính nó nhưng với n giảm 1 đon vị. Việc gọi này được lặp lại cho đến khi n = 0. Một lớp rất rộng của bài toán dạng này là các bài toán có thể định nghĩa được dưới dạng đệ qui như các bài toán lặp với số bước hữu hạn biết trước, các bài toán UCLN, tháp Hà Nội, \... 3. []{#bookmark1000.anchor}Cấu trúc chung của hàm đệ qui Dạng thức chung của một chưong trình đệ qui thường như sau: 4. []{#bookmark1002.anchor}Các ví dụ *[Ví dụ 1]* : Tìm UCLN của 2 số a, b. Bài toán có thể được định nghĩa dưới dạng đệ qui như sau: - nếu a = b thì UCLN = a - nếu a \ b thì UCLN(a, b) = UCLN(a-b, b) - nếu a \< b thì UCLN(a, b) = UCLN(a, b-a) Từ đó ta có chưong trình đệ qui để tính UCLN của a và b như sau. *[Ví dụ 2]* : Tính số hạng thứ n của dãy Fibonaci là dãy f(n) được định nghĩa: - f(0) = f(1) = 1 - f(n) = f(n-1) + f(n-2) với Vn \ 2. long Fib(int n) { } *[Ví dụ 3]* : Chuyển tháp là bài toán cổ nổi tiếng, nội dung như sau: Cho một tháp n tầng, đang xếp tại vị trí 1. Yêu cầu bài toán là hãy chuyển toàn bộ tháp sang vị trí 2 (cho phép sử dụng vị trí trung gian 3) theo các điều kiện sau đây - mỗi lần chỉ được chuyển một tầng trên cùng của tháp, - tại bất kỳ thời điểm tại cả 3 vị trí các tầng tháp lớn hơn phải nằm dưới các tầng tháp nhỏ hơn. Bài toán chuyển tháp được minh hoạ bởi hình vẽ dưới đây. ![](media/image4.png) Bài toán có thể được đặt ra tổng quát hơn như sau: chuyển tháp từ vị trí di đến vị trí den, trong đó di, den là các tham số có thể lấy giá trị là 1, 2, 3 thể hiện cho 3 vị trí. Đối với 2 vị trí di và den, dễ thấy vị trí trung gian (vị trí còn lại) sẽ là vị trí 6-di-den (vì di+den+tg = 1+2+3 = 6). Từ đó để chuyển tháp từ vị trí di đến vị trí den, ta có thể xây dựng một cách chuyển đệ qui như sau: - chuyển 1 tầng từ di sang tg, - chuyển n-1 tầng còn lại từ di sang den, - chuyển trả tầng tại vị trí tg về lại vị trí den hiển nhiên nếu số tầng là 1 thì ta chỉ phải thực hiện một phép chuyển từ di sang den. Mỗi lần chuyển 1 tầng từ vị trí i đến j ta kí hiệu i \^ j. Chương trình sẽ nhập vào input là số tầng và in ra các bước chuyển theo kí hiệu trên. Từ đó ta có thể xây dựng hàm đệ qui sau đây ; void chuyen(int n, int di, int den) // n: số tầng, di, den: vị trí đi, đến { } main() { } Ví dụ nếu số tầng bằng 3 thì chương trình in ra kết quả là dãy các phép chuyển sau đây: IV. []{#bookmark1016.anchor}TỔ CHỨC CHƯƠNG TRÌNH 1. []{#bookmark1020.anchor}Các loại biến và phạm vi a. Biến cục bộ Là các biến được khai báo trong thân của hàm và chỉ có tác dụng trong hàm này, kể cả các biến khai báo trong hàm main() cũng chỉ có tác dụng riêng trong hàm main(). Từ đó, tên biến trong các hàm là được phép trùng nhau. Các biến của hàm nào sẽ chỉ\ tồn tại trong thời gian hàm đó hoạt động. Khi bắt đầu hoạt động các biến này được tự động sinh ra và đến khi hàm kết thúc các biến này sẽ mất đi. Tóm lại, một hàm được xem như một đon vị độc lập, khép kín. Tham đối của các hàm cũng được xem như biến cục bộ. *[Ví dụ 1]* : Dưới đây ta nhắc lại một chưong trình nhỏ gồm 3 hàm: luỹ thừa, xoá màn hình và main(). Mục đích để minh hoạ biến cục bộ. float luythua(float x, int n) // hàm trả giá trị x^n^ { } void xmh(int n) { main() { Qua ví dụ trên ta thấy các biến i, đối n được khai báo trong hai hàm: luythua() và xmh(). kq được khai báo trong luythua và main(), ngoài ra các biến x và n trùng với đối của hàm luythua(). Tuy nhiên, tất cả khai báo trên đều hợp lệ và đều được xem như khác nhau. Có thể giải thích như sau: \- Tất cả các biến trên đều cục bộ trong hàm nó được khai báo. - - b. Biến ngoài Là các biến được khai báo bên ngoài của tất cả các hàm. Vị trí khai báo của chúng có thể từ đầu văn bản chương trình hoặc tại một một vị trí bất kỳ nào đó giữa văn bản chương trình. Thời gian tồn tại của chúng là từ lúc chương trình bắt đầu chạy đến khi kết thúc chương trình giống như các biến trong hàm main(). Tuy nhiên về phạm vi tác dụng của chúng là bắt đầu từ điểm khai báo chúng đến hết chương trình, tức tất cả các hàm khai báo sau này đều có thể sử dụng và thay đổi giá trị của chúng. Như vậy các biến ngoài được khai báo từ đầu chương trình sẽ có tác dụng lên toàn bộ chương trình. Tất cả các hàm đều sử dụng được các biến này nếu trong hàm đó không có biến khai báo trùng tên. Một hàm nếu có biến trùng tên với biến ngoài thì biến ngoài bị che đối với hàm này. Có nghĩa nếu i được khai báo như một biến ngoài và ngoài ra trong một hàm nào đó cũng có biến i thì như vậy có 2 biến i độc lập với nhau và khi hàm truy nhập đến i thì có nghĩa là i của hàm chứ không phải i của biến ngoài. Dưới đây là ví dụ minh hoạ cho các giải thích trên. *[Ví dụ 2]* : Chúng ta xét lại các hàm luythua() và xmh(). Chú ý rằng trong cả hai hàm này đều có biến i, vì vậy chúng ta có thể khai báo i như một biến ngoài (để dùng chung cho luythua() và xmh()), ngoài ra x, n cũng có thể được khai báo như biến ngoài. Cụ thể: Trong ví dụ này ta thấy các biến x, n, i đều là các biến ngoài. Khi ta muốn sử dụng biến ngoài ví dụ i, thì biến i sẽ không được khai báo trong hàm sử dụng nó. Chẳng hạn, luythua() và xmh() đều sử dụng i cho vòng lặp for của mình và nó không được khai báo lại trong 2 hàm này. Các đối x và n trong luythua() là độc lập với biến ngoài x và n. Trong luythua() khi sử dụng đến x và n (ví dụ câu lệnh kq \*= x) thì đây là x của hàm chứ không phải biến ngoài, trong khi trong main() không có khai báo về x và n nên ví dụ câu lệnh cout \ a. Biến hằng và từ khoá const Để sử dụng hằng có thể khai báo thêm từ khoá const trước khai báo biến. Phạm vi và miền tác dụng cũng như biến, có nghĩa biến hằng cũng có thể ở dạng cục bộ hoặc toàn thể. Biến hằng luôn luôn được khởi tạo trước. Có thể khai báo từ khoá const trước các tham đối hình thức để không cho phép thay đổi giá trị của các biến ngoài (đặc biệt đối với với mảng và xâu kí tự, vì bản thân các biến này được xem như con trỏ do đó hàm có thể thay đổi được giá trị của các biến ngoài truyền cho hàm này). Ví dụ sau thể hiện hằng cũng có thể được khai báo ở các phạm vi khác nhau. const int MAX = 30; // toàn thể void vidu(const int \*p) // cục bộ { } void main() { } Trong Turbo C, BorlandC và các chương trình dịch khác có nhiều hằng số khai báo sẵn trong tệp values.h như MAXINT, M\_PI hoặc các hằng đồ hoạ trong graphics.h như WHITE, RED, \... b. Biến tĩnh và từ khoá static Được khai báo bằng từ khoá static. Là biến cục bộ nhưng vẫn giữ giá trị sau khi ra khỏi hàm. Phạm vi tác dụng như biến cục bộ, nghĩa là nó chỉ được sử dụng trong hàm khai báo nó. Tuy nhiên thời gian tác dụng được xem như biến toàn thể, tức sau khi hàm thực hiện xong biến vẫn còn tồn tại và vẫn lưu lại giá trị sau khi ra khỏi hàm. Giá trị này này được tiếp tục sử dụng khi hàm được gọi lại, tức biến static chỉ được khởi đầu một lần trong lần chạy hàm đầu tiên. Nếu không khởi tạo, C++ tự động gán giá trị 0 (ngầm định = 0). Ví dụ: c. Biến thanh ghi và từ khoá register Để tăng tốc độ tính toán C++ cho phép một số biến được đặt trực tiếp vào thanh ghi thay vì ở bộ nhớ. Khai báo bằng từ khoá **register** đứng trước khai báo biến. Tuy nhiên khai báo này chỉ có tác dụng đối với các biến có kích thước nhỏ như biến char, int. Ví dụ: register char c; register int dem; d. Biến ngoài và từ khoá extern Như đã biết một chương trình có thể được đặt trên nhiều file văn bản khác nhau. Một biến không thể được khai báo nhiều lần với cùng phạm vi hoạt động. Do vậy nếu một hàm sử dụng biến được khai báo trong file văn bản khác thì biến này phải được khai báo với từ khoá extern. Từ khoá này cho phép chương trình dịch tìm và liên kết biến này từ bên ngoài file đang chứa biến. Chúng ta hãy xét ví dụ gây lỗi sau đây và tìm phương án khắc phục chúng. - - Giả thiết khai báo lại như sau: void in(); void main() { \... } // Bỏ khai báo i trong main() int i; // Đưa khai báo i ra trước in() và sau main() void in() { \... } cách khai báo này cũng gây lỗi vì main() không nhận biết i. Cuối cùng để main() có thể nhận biết i thì i phải được khai báo dưới dạng biến extern. Thông thường trong trường hợp này cách khắc phục hay nhất là khai báo trước main() để bỏ các extern (không cần thiết). Giả thiết 2 chương trình trên nằm trong 2 tệp khác nhau. Để liên kết (link) biến i giữa 2 chương trình cần định nghĩa tổng thể i trong một và khai báo extern trong chương trình kia. Hàm in() nằm trong tệp văn bản program2.cpp, được dùng để in giá trị của biến i khai báo trong programl.cpp, tạm gọi là tệp gốc (hai tệp này khi dịch sẽ được liên kết với nhau). Từ đó trong tệp gốc, i phải được khai báo là biến ngoài, và bất kỳ hàm ở tệp khác muốn sử dụng biến i này đều phải có câu lệnh khai báo extern int i (nếu không có từ khoá extern thì biến i lại được xem là biến cục bộ, khác với biến i trong tệp gốc). Để liên kết các tệp nguồn có thể tạo một dự án (project) thông qua menu PROJECT (Alt-P). Các phím nóng cho phép mở dự án, thêm bớt tệp vào danh sách tệp của dự án \... được hướng dẫn ở dòng cuối của cửa sổ dự án. 3. []{#bookmark1056.anchor}Các chỉ thị tiền xử lý Như đã biết trước khi chạy chương trình (bắt đầu từ văn bản chương trình tức chương trình nguồn) C++ sẽ dịch chương trình ra tệp mã máy còn gọi là chương trình đích. Thao tác dịch chương trình nói chung gồm có 2 phần: xử lý sơ bộ chương trình và dịch. Phần xử lý sơ bộ được gọi là tiền xử lý, trong đó có các công việc liên quan đến các chỉ thị được đặt ở đầu tệp chương trình nguồn như \#include, \#define \... a. Chỉ thị bao hàm tệp \#include Cho phép ghép nội dung các tệp đã có khác vào chương trình trước khi dịch. Các tệp cần ghép thêm vào chương trình thường là các tệp chứa khai báo nguyên mẫu của các hằng, biến, hàm \... có sẵn trong C hoặc các hàm do lập trình viên tự viết. Có hai dạng viết chỉ thị này. Dạng khai báo 1 cho phép C++ ngầm định tìm tệp tại thư mục định sẵn (khai báo thông qua menu Options\\Directories) thường là thư mục TC\\INCLUDE và tệp là các tệp nguyên mẫu của thư viện C++. Dạng khai báo 2 cho phép tìm tệp theo đường dẫn, nếu không có đường dẫn sẽ tìm trong thư mục hiện tại. Tệp thường là các tệp (thư viện) được tạo bởi lập trình viên và được đặt trong cùng thư mục chứa chương trình. Cú pháp này cho phép lập trình viên chia một chương trình thành nhiều môđun đặt trên một số tệp khác nhau để dễ quản lý. Nó đặc biệt hữu ích khi lập trình viên muốn tạo các thư viện riêng cho mình. b. Chỉ thị macro \#define **\#define tên\_macro xaukitu** Trước khi dịch bộ tiền xử lý sẽ tìm trong chương trình và thay thế bất kỳ vị trí xuất hiện nào của tên\_macro bởi xâu kí tự. Ta thường sử dụng macro để định nghĩa các hằng hoặc thay cụm từ này bằng cụm từ khác dễ nhớ hơn, ví dụ: từ đó trong chương trình ta có thể viết những đoạn lệnh như: trước khi dịch bộ tiền xử lý sẽ chuyển đoạn chương trình trên thành theo đúng cú pháp của C++ và rồi mới tiến hành dịch. Ngoài việc chỉ thị \#define cho phép thay tên\_macro bởi một xâu kí tự bất kỳ, nó còn cũng được phép viết dưới dạng có đối. Ví dụ, để tìm số lớn nhất của 2 số, thay vì ta phải viết nhiều hàm max (mỗi hàm ứng với một kiểu số khác nhau), bây giờ ta chỉ cần thay chúng bởi một macro có đối đon giản như sau: **\#define max(A,B) ((A) \ (B) ? (A): (B))** khi đó trong chưong trình nếu có dòng x = max(a, b) thì nó sẽ được thay bởi: x = ((a) \ (b) ? (a): (b)) Chú ý: - - - - thì kết quả in ra sẽ là 6 thay vì kết quả đúng là 4. Lí do là ở chỗ chương trình dịch sẽ thay bp(++i) bởi ((++i)\*(++i)), và với i = 1 chương trình sẽ thực hiện như 2\*3 = 6. Do vậy cần cẩn thận khi sử dụng các phép toán tự tăng giảm trong các macro có đối. Nói chung, nên hạn chế việc sử dụng các macro phức tạp, vì nó có thể gây nên những hiệu ứng phụ khó kiểm soát. c. Các chỉ thị biên dịch có điều kiện \#if, \#ifdef, \#ifndef - Chỉ thị: Các chỉ thị này giống như câu lệnh if, mục đích của nó là báo cho chương trình dịch biết đoạn lệnh giữa \#if (điều kiện ) và \#endif chỉ được dịch nếu điều kiện đúng. Ví dụ: hoặc: **Chỉ thị \#ifdef và \#ifndef** Chỉ thị này báo cho chương trình dịch biết đoạn lệnh có được dịch hay không khi một tên gọi đã được định nghĩa hay chưa. \#ifdef được hiểu là nếu tên đã được định nghĩa thì dịch, còn \#iíhdef được hiểu là nếu tên chưa được định nghĩa thì dịch. Để định nghĩa một tên gọi ta dùng chỉ thị \#define tên. Chỉ thị này đặc biệt có ích khi chèn các tệp thư viện vào để sử dụng. Một tệp thư viện có thể được chèn nhiều lần trong văn bản do vậy nó có thể sẽ được dịch nhiều lần, điều này sẽ gây ra lỗi vì các biến được khai báo nhiều lần. Để tránh việc này, ta cần sử dụng chỉ thị trên như ví dụ minh hoạ sau: Giả sử ta đã viết sẵn 2 tệp thư viện là mylib.h và mathfunc.h, trong đó mylib.h chứa hàm max(a,b) tìm số lớn nhất giữa 2 số, mathfunc.h chứa hàm max(a,b,c) tìm số lớn nhất giữa 3 số thông qua sử dụng hàm max(a,b). Do vậy mathfunc.h phải có chỉ thị \#include mylib.h để sử dụng được hàm max(a,b). \- Thư viện 1. tên tệp: MYLIB.H int max(int a, int b) { } \- Thư viện 2. tên tệp: MATHFUNC.H \#include \"mylib.h\" int max(int a, int b) { } Hàm main của chúng ta nhập 3 số, in ra max của từng cặp số và max của cả 3 số. Chương trình cần phải sử dụng cả 2 thư viện. Trước khi dịch chương trình, bộ tiền xử lý sẽ chèn các thư viện vào trong tệp chính (chứa main()) trong đó mylib.h được chèn vào 2 lần (một lần của tệp chính và một lần của mathfunc.h), do vậy khi dịch chương trình, C++ sẽ báo lỗi (do hàm int max(inta, int b) được khai báo hai lần). Để khắc phục tình trạng này trong mylib.h ta thêm chỉ thị mới như sau: Như vậy khi chương trình dịch xử lý mylib.h lần đầu do \_MYLIB\_ chưa định nghĩa nên máy sẽ định nghĩa từ này, và dịch đoạn chương trình tiếp theo cho đến \#endif. Lần thứ hai khi gặp lại đoạn lệnh này do \_MYLIB\_ đã được định nghĩa nên chương trình bỏ qua đoạn lệnh này không dịch. Để cẩn thận trong cả mathfunc.h ta cũng sử dụng cú pháp này, vì có thể trong một chương trình khác mathfunc.h lại được sử dụng nhiều lần. BÀI TẬP Con trỏ 1. Hãy khai báo biến kí tự ch và con trỏ kiểu kí tự pc trỏ vào biến ch. Viết ra các cách gán giá trị 'A' cho biến ch. 2. Cho mảng nguyên cost. Viết ra các cách gán giá trị 100 cho phần tử thứ 3 của mảng. 3. Cho p, q là các con trỏ cùng trỏ đến kí tự c. Đặt \*p = \*q + 1. Có thể khẳng định: \*q = \*p - 1 ? 4. Cho p, q là các con trỏ trỏ đến biến nguyên x = 5. Đặt \*p = \*q + 1; Hỏi \*q ? 5. Cho p, q, r, s là các con trỏ trỏ đến biến nguyên x = 10. Đặt \*q = \*p + 1; \*r = \*q + 1; \*s = \*r + 1. Hỏi giá trị của biến x ? 6. Chọn câu đúng nhất trong các câu sau: A: Địa chỉ của một biến là số thứ tự của byte đầu tiên máy dành cho biến đó. B: Địa chỉ của một biến là một số nguyên. C: Số học địa chỉ là các phép toán làm việc trên các số nguyên biểu diễn địa chỉ của biến D: a và b đúng 7. Chọn câu sai trong các câu sau: A: Các con trỏ có thể phân biệt nhau bởi kiểu của biến mà nó trỏ đến. B: Hai con trỏ trỏ đến các kiểu khác nhau sẽ có kích thước khác nhau. C: Một con trỏ kiểu void có thể được gán bởi con trỏ có kiểu bất kỳ (cần ép kiểu). D: Hai con trỏ cùng trỏ đến kiểu cấu trúc có thể gán cho nhau. 8. Cho con trỏ p trỏ đến biến x kiểu float. Có thể khẳng định ? A: p là một biến và \*p cũng là một biến B: p là một biến và \*p là một giá trị hằng C: Để sử dụng được p cần phải khai báo float \*p; và gán \*p = x; D: Cũng có thể khai báo void \*p; và gán (float)p = &x; 9. Cho khai báo float x, y, z, \*px, \*py; và các lệnh px = &x; py = &y; Có thể khẳng định ? A: Nếu x = \*px thì y = \*py B: Nếu x = y + z thì \*px = y + z C: Nếu \*px = y + z thì \*px = \*py + z D: a, b, c đúng 10. Cho khai báo float x, y, z, \*px, \*py; và các lệnh px = &x; py = &y; Có thể khẳng định ? A: Nếu \*px = x thì \*py = y B: Nếu \*px = \*py - z thì \*px = y - z C: Nếu \*px = y - z thì x = y - z D: a, b, c đúng 11. Không dùng mảng, hãy nhập một dãy số nguyên và in ngược dãy ra màn hình. 12. Không dùng mảng, hãy nhập một dãy số nguyên và chỉ ra vị trí của số bé nhất, lớn nhất. 13. Không dùng mảng, hãy nhập một dãy số nguyên và in ra dãy đã được sắp xếp. 14. Không dùng mảng, hãy nhập một dãy kí tự. Thay mỗi kí tự 'a' trong dãy thành kí tự 'b' và in kết quả ra màn hình. Con trỏ và xâu kí tự 15. Giả sử p là một con trỏ kiểu kí tự trỏ đến xâu \"Tin học\". Chọn câu đúng nhất trong các câu sau: A: cout \ 1. []{#bookmark1132.anchor}Khai báo, khởi tạo Để tạo ra một kiểu cấu trúc NSD cần phải khai báo tên của kiểu (là một tên gọi do NSD tự đặt), tên cùng với các thành phần dữ liệu có trong kiểu cấu trúc này. Một kiểu cấu trúc được khai báo theo mẫu sau: - - - - - hoặc Các biến được khai báo cũng có thể đi kèm khởi tạo: Ví dụ: - hoặc: - Kiểu ngày tháng gồm 3 thành phần nguyên chứa ngày, tháng, năm. một biến holiday cũng được khai báo kèm cùng kiểu này và được khởi tạo bởi bộ số 1. 5. 2000. Các giá trị khởi tạo này lần lượt gán cho các thành phần theo đúng thứ tự trong khai báo, tức ng = 1, th = 5 và nam = 2000. - hoặc: - Khai báo cùng với cấu trúc Sinhvien có các biến x, con trỏ p và mảng K41T với 60 phần tử kiểu Sinhvien. Một biến y được khai báo thêm và kèm theo khởi tạo giá trị {\"NVA\", {1,1,1980}, 1}, tức họ tên của sinh viên y là \"NVA\", ngày sinh là 1/1/1980, giới tính nam và điểm thi để trống. Đây là kiểu khởi tạo thiếu giá trị, giống như khởi tạo mảng, các giá trị để trống phải nằm ở cuối bộ giá trị khởi tạo (tức các thành phần bỏ khởi tạo không được nằm xen kẽ giữa những thành phần được khởi tạo).Ví dụ này còn minh hoạ cho các cấu trúc lồng nhau, cụ thể trong kiểu cấu trúc Sinhvien có một thành phần cũng kiểu cấu trúc là thành phần ns. 2. []{#b