Bài giảng điện tử môn học Ngôn ngữ lập trình C

pdf 106 trang vanle 3210
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng điện tử môn học Ngôn ngữ lập trình C", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfbai_giang_dien_tu_mon_hoc_ngon_ngu_lap_trinh_c.pdf

Nội dung text: Bài giảng điện tử môn học Ngôn ngữ lập trình C

  1. TRƯỜNG ĐẠI HỌC BÁCH KHOA HÀ NỘI KHOA CÔNG NGHỆ THÔNG TIN o0o Tạ Tuấn Anh Bài giảng điện tử môn học NGÔN NGỮ LẬP TRÌNH C
  2. Tóm tắt nội dung môn học C là một ngôn ngữ lập trình cấu trúc bậc cao được các nhà lập trình chuyên nghiệp sử dụng phổ biến để phát triển các phần mềm hệ thống (hệ điều hành, chương trình dịch, cơ sở dữ liệu, ). Lý do ngôn ngữ C đươc ưu chuộng chính là tính mềm dẻo và ngắn gọn của nọ. Một chương trình được viết ở ngôn ngữ C có tính khả chuyển cao. Nó có thể được dịch và chạy trong nhiều loại máy tính (PC, Sun, Mainframe, ) cũng như trên nhiều nền hệ điều hành (DOS, UNIX, ). Ngoài ra C cho phép viết chương trình bám sát cách tổ chức bộ nhớ chương trình khi chạy. Do vậy một chương trình được dịch từ C luôn có kích thước nhỏ gọn hơn một chương trình cùng loại được dịch từ các ngôn ngữ bậc cao khác như PASCAL. Nhưng cũng chính vì lí do này mà việc nắm bắt và thành thạo ngôn ngữ C sẽ khó khăn hơn nhiều so với ngôn ngữ khác. Môn học này giới thiệu cho các học viên các kiến thức căn bản cũng như nâng cao về ngôn ngữ lập trình C. Bên cạnh các kiến thức về cú pháp cũng như kĩ năng viết chương trình C, học viên còn nắm bắt được các vấn đề liên quan đến tổ chức bộ nhớ của một chương trình. Kiến thức yêu cầu Để tiếp thu tốt kiến thức môn học này, yêu cầu học viên trước khi học đã tìm hiểu các khái niệm cơ bản trong Tin học, có kĩ năng căn bản viết một chương trình có cấu trúc bằng một ngôn ngữ bậc cao như PASCAL. Ngoài ra một số kiến thức về cấu trúc dữ liệu và giải thuật (danh sách móc nối, cây tìm kiếm, ) có thể giúp học viên sử dụng C để viết các chương trình ứng dụng. Tổng thời lượng: 45 tiết MỤC LỤC TÀI LIỆU THAM KHẢO Nhập môn Lập trình Ngôn ngữ C Trần Việt Linh, Lê Đăng Hưng, Lê Đức Trung, Nguyễn Thanh Thuỷ Nhà Xuất bản Khoa học Kỹ thuật, 2000 Ngôn ngữ Lập trình C Quách Tuấn Ngọc Nhà Xuất bản Giáo dục, 1998 Language C Brian Kernighan, Denis Ritchie Prentice Hall, 1988 Programmer en langage C Claude Delannoy Eyrolles, 1998
  3. CHƯƠNG 1 - NHẬP MÔN LẬP TRÌNH C Mục đích của chương này là giới thiệu tổng quan về ngôn ngữ C bao gồm các kiến thức về lịch sử, đặc điểm và vai trò của nó. Học viên được làm quen với các chương trình viết bằng C cũng như cách dịch chúng để chạy. Yêu cầu: Có một phiên bản cài đặt của trình biên dịch Turbo C hay một trình biên dịch khác để chạy thử chương trình. Thời lượng: 5 tiết Mục 1.1 - Tổng quan về ngôn ngữ C Mục này cho phép học viên làm quen với một chương trình viết dưới ngôn ngữ C và tìm hiểu lịch sử của nó. Các thành phần cơ bản của một chương trình C được giới thiệu để học viên có một cái nhìn tổng quan về ngôn ngữ lập trình này. Yêu cầu: Đã có khái niệm về lập trình và ngôn ngữ lập trình. Thời lượng: 3 tiết Bài 1 - Lịch sử hình thành và phát triển Tóm tắt nội dung: Ngôn ngữ lập trình C ra đời vào đầu thập kỉ 70 với mục đích dùng để viết hệ điều hành UNIX. C được phát triển rất mạnh sau đó và được chuẩn hoá với tên gọi ANSI C. Ngôn ngữ này được các nhà lập trình chuyên nghiệp rất ưa chuộng để phát triển các phần mềm hệ thống. Một mở rộng của C là C++ ra đời vào đầu thập kỉ 80. Nó là một ngôn ngữ lập trình hướng đối tượng được phát triển trên nền của C. Thời lượng: 1 tiết Ngôn ngữ C do Brian W.Kernighan và Denis M. Ritchie phát triển vào đầu những năm 70 tại phòng thí nghiệm BELL (Hoa Kỳ) với mục đích ban đầu để phát triển hệ điều hành UNIX. Bối cảnh ra đời xuất phát từ nhu cầu cần phải có một ngôn ngữ lập trình hệ thống thay thế cho hợp ngữ (ASSEMBLY) rất nặng nề trong lập trình. Hơn nữa một chương trình viết bằng hợp ngữ không có tính khả chuyển vì chúng gắn chặt với bộ lệnh của vi xử lí. Tiền thân của C phải kể đến các ngôn ngữ BCPL do Martin Richard nghiên cứu. Tiếp đến là ngôn ngữ B do Ken Thompson xây dựng năm 1970 dùng để viết hệ điều hành UNIX cho dòng máy tính PDP-7. C là ngôn ngữ được kế thừa từ B và hoàn thiện để có được các tính năng mạnh của một ngôn ngữ lập trình hệ thống có khả năng ứng dụng rộng rãi như ngày nay. Đó là các tính năng:
  4. - Lập trình bậc cao: Giống như PASCAL, chương trình C sử dụng tập các câu lệnh điều khiển như rẽ nhánh, lặp ở mức độ trừu tượng của lưu đồ giải thuật. Điều này cho phép viết các giải thuật bằng ngôn ngữ C khá dễ dàng. - Lập trình cấu trúc: Một chương trình C có thể được phân chia, cấu trúc thành các modul nhỏ. Điều này giúp phát triển chương trình một cách hệ thống hơn và dễ bảo trì. - Lập trình hệ thống: Không giống như PASCAL, ngôn ngữ C không dùng nhiều kiểu dữ liệu trừu tượng. C cho phép các thao tác với bộ nhớ chương trình rất uyển chuyển. Một người lập trình trên ngôn ngữ C có thể tự do tổ chức và lưu trữ dữ liệu trên bộ nhớ theo ý mình. Tính năng này là vô cùng quan trọng khi cần phát triển các chương trình hệ thống liên quan nhiều đến bộ nhớ máy tính. Ngoài ra ngôn ngữ C hỗ trợ phần lớn các phép xử lí mà một hợp ngữ có thể làm. - Tính khả chuyển: Một chương trình viết trên ngôn ngữ C có thể được dịch ra mã chương trình trên nhiều dòng máy và hệ điều hành khác nhau bởi khả năng tương thích của các câu lệnh trong chương trình. Ngoài ra C còn có bộ tiền xử lí tạo ra khả năng biên dịch theo điều kiện để việc dịch chương trình thích ứng cho từng hệ thống khác nhau. - Tính nhỏ gọn: Một chương trình viết trên C sau khi dịch có độ tối ưu về mã lệnh hơn bất cứ một ngôn ngữ bậc cao nào khác. Chính vì vậy các chương trình được dịch từ ngôn ngữ C thường có kích thước nhỏ gọn. Với tất cả các tính năng trên, ngôn ngữ C là một ngôn ngữ cực kì hiệu quả và có sức diễn cảm trong lập trình. Nó đã trở thành ngôn ngữ lập trình mà các nhà lập trình chuyên nghiệp ưa chuộng trong nhiều lĩnh vực. Trong lĩnh vực lập trình hệ thống, có tới 90% chương trình được viết bằng ngôn ngữ C. Ngoài ra nó còn được dùng để viết chương trình trong các lĩnh vực hiện đại khác của Tin học về xử lí tín hiệu, số liệu, văn bản, Ngôn ngữ C đã được viện tiêu chuẩn quốc gia Mỹ (ANSI) chuẩn hoá và công bố vào năm 1988 với tên gọi là ANSI C. Bên cạnh ngôn ngữ C người lập trình còn biết đến một ngôn ngữ lập trình có tên tương tự là C++. Đây là một ngôn ngữ lập trình được mở rộng từ C để thêm khả năng lập trình hướng đối tượng. Vì C++ là ngôn ngữ bao trùm lên C nên để học tốt C++ yêu cầu người lập trình trước hêt phải nắm vững C. Bài 2 - Bắt đầu lập trình C Tóm tắt nội dung: Học viên sẽ được làm quen với hai chương trình đơn giản. Chúng minh họa các thành phần cơ bản có trong một chương trình C. Thời lượng: 1 tiết 1. Chương trình Hello! Cũng như với nhiều ngôn ngữ lập trình khác, chúng ta bắt đầu tìm hiểu ngôn ngữ qua một chương trình đơn giản chỉ in ra một thông báo "Hello!" cho người sử dụng. Chương trình này được viết trong ngôn ngữ C như dưới đây. #include void main()
  5. { printf("Hello!"); } Dòng đầu tiên của chương trình được gọi là một khai báo sử dụng tệp tiêu đề. Trong bất kì một chương trình nào ở ngôn ngữ C đều cần có những khai báo tệp tiêu đề bởi vì chúng cho phép gọi các hàm có sẵn trong thư viện hoặc được viết bởi một người lập trình khác. Trong chương trình này chúng ta đã khai báo sử dụng tệp tiêu đề để có thể sử dụng các hàm vào ra dữ liệu chuẩn của hệ thống (printf(), scanf(), ). Phần tiếp theo của chương trình là một hàm chính chứa các câu lệnh được thực hiện khi chương trình chạy. Một chương trình C thì luôn có một hàm chính có tên là main. Dòng thông báo "Hello!" được in ra màn hình được thực hiện nhờ câu lệnh gọi hàm hàm thư viện printf() trong chương trình chính. 2. Chương trình thứ hai Để có thể thấy rõ hơn cấu trúc của một chương trình trong C, chúng ta tiếp tuc tìm hiểu ngôn ngữ thông qua một ví dụ thứ hai. Ở ví dụ này chúng ta sẽ tạo một chương trình cho phép in diện tích của một hình tròn tương ứng với một bán kính được người sử dụng nhập vào. #include /* khai báo một hằng số PI */ #define PI 3.14 /* khai báo một hàm tạo công thức tính diện tích */ float dientich(float r) { return PI*r*r; } /* hàm chính của chương trình */ void main() { float r; printf("Nhập bán kính r ="); scanf("%f", &r); printf("Diện tích hình tròn là %f", dientich(r)); } Chương trình này đã thể hiện một phần cấu trúc của một chương trình C sẽ được mô tả chi tiết trong bài 3. Dưới đây là thuyết minh cho chương trình: - Chú thích chương trình: Các dòng văn bản được đặt nằm trong bộ dấu /* */ đều là chú thích cho một chương trình C. - Khai báo hằng số: Dòng thứ hai trong chương trình có tác dụng khai báo một hằng số PI có giá trị là 3.14. - Hàm: Một chương trình C có thể được cấu trúc bằng nhiều hàm. Trong chương trình trên, chúng ta đã khai báo một hàm để tính diện tích của một hình tròn với bán kính truyền vào như là một tham số thực (float). Kết quả của rõ ràng phải là một số thực. Hàm này được gọi trong hàm chính của chương trình để in giá trị diện tích hình tròn ra ngoài màn hình. - Hàm chính: Một chương trình hoạt động thế nào được thể hiện bởi các câu lệnh trong hàm chính. Trong hàm chính của chương trình trên chúng ta đã khai báo một biến thực để chứa giá trị của bán kính hình tròn do người sử dụng nhập vào từ bàn phím. Lệnh gọi hàm scanf() cho phép chương trình lấy dữ liệu bán kính do người sử dụng nhập vào. Dòng lệnh
  6. cuối cùng của hàm chính dùng để in giá trị diện tích của hình tròn có bán kính tương ứng ra màn hình. Giá trị diện tích được tính thông qua hàm dientich() đã được khai báo. Bài 3 - Các thành phần của chương trình C Tóm tắt nội dung: Cũng như các ngôn ngữ lập trình khác, một chương trình C được viết từ các từ khoá, biến, câu lệnh, hàm, Trong bài này chúng ta xem chi tiết các thành phần có trong một chương trình C để biết được ý nghĩa và vai trò của chúng. Thời lượng: 1 tiết 1. Từ vựng Một chương trình C được xây dựng trên một bộ từ vựng bao gồm tập kí tự có phân biệt chữ hoa và thường. Chính vì vậy khi viết chương trình C chúng ta không được phép sử dụng tuỳ tiện chữ hoa hay chữ thường. Ví dụ hàm chính của chương trình bắt buộc phải viết là main mà không được viết là MAIN hay Main. Trong một chương trình C chúng ta gặp hai loại bộ từ vựng quan trọng là từ khoá và tên của người sử dụng. Từ khoá: Từ khoá là một bộ từ vựng được định nghĩa từ trước đối với một ngôn ngữ và chỉ để sử dụng vào các mục đích đã được xác định trong chương trình. Một số từ khoá hay dùng là các từ khoá kiểu dữ liệu (char, int, long, float, double, ), các lệnh điều khiển (if, switch, for, do, while, ), v.v. Bảng các từ khoá trong ngôn ngữ C asm break case cdecl char const continue default do double else enum extern far float for goto huge if int interrupt long near pascal register return short signed sizeof static struct switch typedef union unsigned void volatile while Tên: Trong chương trình người sử dụng cần tên để đặt cho biến, hàm, kiểu dữ liệu mới. Tên của người sử dụng đòi hỏi phải là một chuỗi kí tự là các chữ cái, chữ số và gạch nối ‘_’. Tên phải bắt đầu bằng một chữ cái hoặc gạch nối. 2. Cấu trúc cơ bản của chương trình Trong bài 2 chúng ta đã thấy được cấu trúc cơ bản của chương trình C. Phần này sẽ trình bày khái quát về các thành phần trong cấu trúc của chương trình.
  7. a. Khai báo tiêu đề Một tệp tiêu đề trong chương trình C chứa mô tả các hàm có thể được gọi trong chương trình (xem mục 6.2 - Tệp tiêu đề). Khi cần gọi một hàm nào trong chương trình ta phải khai báo tệp tiêu đề có mô tả tương ứng cho hàm. Chính vì vậy các hàm thư viện luôn được chỉ ra cùng với các tệp tiêu đề tương ứng khi được trình bày. b. Khai báo hằng, cấu trúc dữ liệu Các hằng số trong chương trình C thường được định nghĩa bằng chỉ thị #define (xem bài 35 - Một số chỉ thị tiền xử lí). Ngoài các kiểu dữ liệu cơ bản, một chương trình C có thể có các cấu trúc dữ liệu phức do người sử dụng định nghĩa. Các cấu trúc này thường được khai báo ở phần đầu chương trình (xem mục 3.4 - Cấu trúc). c. Khai báo hàm và/hoặc nguyên mẫu hàm Chương trình C được cấu trúc bởi các hàm (xem mục 2.3 - Hàm). Một hàm thể hiện một modul con trong một chương trình. Nó có thể nhận tham số thực hiện và trả về kết quả cho nơi gọi hàm. Thân của một hàm được chia làm hai phần: khai báo biến (xem mục 2.1 - Biến, hằng số và biểu thức) và lệnh thực hiện (xem mục 2.2 - Các cấu trúc lệnh điều khiển). Các biến luôn được khai báo ở phần đầu thân hàm sau đó mới đến các câu lệnh. Mỗi câu lệnh trong chương trình C phải được kết thúc bằng một dấu chấm phẩy ‘;’. Chúng ta có thể tạo một khối lệnh bằng cách đặt chúng trong cặp dấu {}. Thân hàm cũng phải được đặt trong cặp dấu này. d. Khai báo biến Trong một chương trình C có thể khai báo biến dạng tổng thể hay cục bộ (biến của một hàm). Một biến tổng thể được khai báo bên ngoài hàm và nó có thể sử dụng trong tất cả các hàm của chương trình. Ngược lại biến cục bộ chỉ có thể sử dụng trong hàm nơi nó được khai báo. Mục 1.2 - Biên dich chương trình Mục này đưa ra cho người sử dụng một kiến thức chuyên sâu về vấn đề biên dịch một chương trình. Các trình IDE (Integrated Development Environment) được biết đến như là một môi trường tích hợp trợ giúp việc phát triển chương trình bao gồm các công việc soạn thảo, biên dịch và gỡ rối. Học viên sẽ được tìm hiểu một trình IDE được dùng phổ biến trong môi trường DOS là Turbo C. Yêu cầu: Có trình cài đặt Turbo C hoặc Borland C++. Thời lượng: 2 tiết
  8. Bài 4 - Quá trình biên dịch Tóm tắt nội dung: Biên dịch một chương trình là quá trình dịch một chương trình viết dưới dạng văn bản như C sang chương trình mã máy như tệp .exe trong DOS. Quá trình này có thể phải qua nhiều pha khác nhau và có thể xảy ra lỗi tại mỗi pha. Thời lượng: 1 tiết Ngôn ngữ lập trình C là một ngôn ngữ dạng biên dịch. Chương trình được viết bằng C muốn chạy được trên máy tính phải trải qua một quá trình biên dịch để chuyển đổi từ dạng mã nguồn sang chương trình dạng mã thực hiện. Toàn bộ quá trình biên dịch ở trong ngôn ngữ được minh hoạ trong hình 1. Quá trình này được chia làm hai giai đoạn chính: giai đoạn dịch và giai đoạn liên kết. Mã ngu?n Ti?n x? lí D?ch C Mã assembly D?ch Assembly Thu vi?n Mã máy Liên k?t Chuong trình Hình 1: Quá trình biên dich một chương trình C 1. Giai đoạn dịch (compiling) Mục đích của giai đoạn này là chuyển đổi tất cả mã chương trình được viết dưới ngôn ngữ C sang dạng mã máy. Ba công đoạn được thực hiện lần lượt là: tiền xử lí mã nguồn, dịch C sang Assembly và dịch Assembly sang mã máy. - Công đoạn tiền xử lí sẽ nhận mã nguồn của chương trình và thực hiện xoá bỏ tất cả chú thích của chương trình. Ngoài ra tất cả các chỉ thị tiền xử lí (bắt đầu bằng #) cũng được xử lí ngay trong công đoạn này. Ví dụ chỉ thị #include cho phép ghép thêm mã chương trình của một tệp tiêu đề vào mã nguồn cần dịch. Các hằng số được định nghĩa bằng #define sẽ được thay thế bằng giá trị cụ thể tại mỗi nơi sử dụng trong chương trình.
  9. - Công đoạn dịch C cho phép phân tích cú pháp của mã nguồn C và sau đó chuyển chúng sang dạng mã Assemly là môt ngôn ngữ bậc thấp gần với tập lệnh của bộ vi xử lí. - Công đoạn dịch Assembly cho phép dịch chương trình sang mã máy. Sau công đoạn này một tệp mã máy (.o) thường được sinh ra trong hệ thống. Như vậy sau giai đoạn dịch các lỗi chương trình có thể được phát hiện và thông báo cho người sử là các lỗi về cú pháp chương trình. Nó cũng có thể phát hiện lỗi không tồn tại (hoặc có mà đặt đường dẫn tìm kiếm sai để không tìm thấy) tệp tiêu đề trong công đoạn tiền xử lí. 2. Giai đoạn liên kết (linking) Trong giai đoạn này mã máy của một chương trình dịch từ nhiều nguồn khác nhau được liên kết lại với nhau để tạo thành chương trình đích duy nhất. Mã máy của các hàm thư viện gọi trong chương trình cũng được đưa vào chương trình cuối trong giai đoạn này. Chính vì vậy mà các lỗi liên quan đến việc gọi hàm hay sử dụng biến tổng thể mà không tồn tại sẽ bị phát hiện. Kể cả lỗi viết chương trình chính không có hàm main() cũng được phát hiện trong khi liên kết. Để liên kết với các hàm thư viện cần phải đặt đúng đường dẫn tìm kiếm thư viện đến các tệp chứa mã máy thư viện (.obj, .lib, ) Bài 5 - Turbo C Tóm tắt nội dung: Turbo C là một trình IDE cho ngôn ngữ C chạy trên nền DOS. Một số phiên bản hay dùng bao gồm Turbo C 1.0, Turbo C++ 3.0. Ngoài ra ta có thể dùng các phiên bản chạy trên nên hệ điều hành Windows có tên là Borland C++. Thời lượng: 1 tiết 1. Môi trường trợ giúp phát triển (IDE) Để trợ giúp cho việc phát triển ứng dụng trên một ngôn ngữ một cách thuận tiện, các nhà sản xuất cung cấp các tiện ích cho phép người sử dụng thao tác biên soạn chương trình, dịch hay gỡ rối bằng các thao tác đơn giản qua phím nóng. Như vậy nhờ có IDE (Intergrated Development Environment) mà lập trình viên không phải thực hiện các pha biên dịch bằng các câu lệnh phức tạp mà chỉ cần đơn giản là nhấn một phím nóng. Quá trình sửa lỗi cũng rất thuận tiện do IDE định vị giúp người lập trình vị trí lỗi của chương trình ngay trên màn hình soạn thảo. Một IDE thường có ba thành phần cơ bản là: - Công cụ soạn thảo giúp lập trình viên soạn thảo chương trình trên ngôn ngữ được hỗ trợ. Trình soạn thảo này có thể thực hiện đổi màu các từ khoá trong chương trình để giúp người lập trình quan sát chương trình một cách trực quan hơn. - Công cụ biên dịch và chạy chương trình. Bằng một thao tác đơn giản lập trình viên có thể yêu cầu biên dịch toàn bộ chương trình xong rồi chạy. Nếu chương trình có lỗi thì công cụ này sẽ thông báo loại lỗi cùng với vị trí của lỗi trong chương trình đến người sử dụng. Chính vì vậy việc tiến hành sửa lỗi chương trình trong IDE là rất đơn giản.
  10. - Công cụ gỡ rối giúp lập trình viên có thể chạy lần bước một chương trình theo từng dòng lệnh. Trong quá trình lần bước lập trình viên có thể quan sát sự thay đổi giá trị các biến trong chương trình qua đó mà phát hiện ra lỗi. 2. Turbo C Có rất nhiều trình IDE khác nhau cho ngôn ngữ C nhưng được phổ dụng nhất trong môi trường DOS hoặc Windows là các phiên bản của Turbo C. Chúng ta có thể kể ra một số phiên bản hay sử dụng là Turbo C chỉ hỗ trợ ngôn ngữ C và chạy trên nền DOS, Turbo C++ 3.0 hỗ trợ C và C++ chạy trên DOS, các phiên bản từ 3.1 trở lên được đổi tên là Borland C++ và chạy trên nền Windows. Khi dịch một chương trình trên Turbo C cần chú ý thiết lập đúng đường dẫn tìm kiếm tệp tiêu đề và thư viện. Ví dụ với phiên bản Turbo C++ để xác lập các đường dẫn thư mục này ta cần dùng menu và vào lựa chọn Options|Directories. Khi đó một hộp hội thoại sẽ xuất hiện để cho chúng ta đặt đường dẫn thư mục tìm kiếm tệp tiêu đề (Include) và thư mục tìm kiếm thư viện (Library). Thường thì các tệp tiêu đề được lưu trong thư mục con INCLUDE của thư mục cài đặt Turbo. Còn các tệp thư viện được lưu trong thư mục con LIB. Hình 2. Màn hình IDE của Turbo C++ 3.0 Chúng ta thường dùng phím nóng của IDE để thực hiện nhanh một số thao tác. Sau đây là một số phím nóng thông dụng: Phím nóng Ý nghĩa F3 Mở tệp nguồn F2 Ghi tệp F9 Biên dịch chương trình và tạo tập tin EXE Ctrl+F9 Biên dịch và chạy chương trình F4, F7, F8 Chạy lần vết, gỡ rối
  11. Alt+F3 Đóng một cửa sổ F5, F6 Chuyển cửa sổ F1 Xem trợ giúp Alt+X Thoát khỏi IDE
  12. CHƯƠNG 2 - LẬP TRÌNH C CƠ BẢN Mục đích của chương này là giới thiệu tới học viên các kiến thức căn bản để viết một chương trình C. Đó là các kiểu dữ liệu, biểu thức, hàm xuất nhập dữ liệu, các cấu trúc lệnh điều khiển trong chương trình. Một chương trình C được cấu trúc với các hàm. Học viên sẽ được tìm hiểu cách khai báo và sử dụng hàm cũng như cách truyền tham số cho nó. Yêu cầu: Đã có kiến thức tổng quan về ngôn ngữ C được giới thiệu trong chương 1. Thời lượng: 12 tiết Mục 2 .1 - Biến, hằng số và biểu thức Mục này giới thiệu cách khai báo biến trong chương trình với các kiểu dữ liệu. Người sử dụng có thể tạo một biểu thức tính toán trên cơ sở các biến và hằng số của kiểu dữ liệu cơ bản. Để xuất và nhập dữ liệu ta có thể sử dụng các hàm thư viện printf() và scanf() tương ứng. Yêu cầu: Có khái niệm về biểu diễn dữ liệu trong máy tính Thời lượng: 6 tiết Bài 6 - Các kiểu dữ liệu cơ bản Tóm tắt nội dung: Các kiểu dữ liệu cơ bản trong C được chia làm loại. Loại số nguyên bao gồm các kiểu char, int và long. Loại số thực có hai kiểu float và double. Tất cả các loại dữ liệu khác đều được biểu diễn và sử dụng trên hai loại dữ liệu cơ bản này. Thời lượng: 1 tiết 1. Biến Biến là một đơn vị bộ nhớ chứa dữ liệu của chương trình. Dữ liệu của biến có thể thay đổi trong quá trình chương trình chạy. Mỗi biến chỉ có thể chứa một kiểu loại dữ liệu nhất định. Cách khai báo biến trong chương trình C như sau: ; Ví dụ: float f; int a, b, c; Các tên biến trong danh sách được phân cách bởi dấu phẩy ‘,’. Trong ví dụ trên f là một biến thực còn a, b, c là các biến nguyên.
  13. 2. Các kiểu dữ liệu cơ bản Bảng các kiểu dữ liệu cơ bản trong C Kiểu dữ liệu Số byte Phạm vi có dấu Phạm vi không dấu char 1 -128 - 127 0 - 255 int 2 -32.768 - 32.767 0 - 65.535 long 4 -2.147.483.648 - 2.147.483.647 0 - 4.294.967.295 float 4 3,4 E -/+38 không có double 8 1,7 E -/+308 không có Để khai báo một kiểu dữ liệu không dấu ta thêm từ khoá unsigned vào phía trước kiểu dữ liệu, ngược lại nếu là có dấu thì ta thêm từ khoá signed. Nếu không có từ khoá định dấu nào được thêm trước kiểu thì trình biên dịch mặc định là số có dấu đối với int hoặc long, còn không dấu đối với char. Ví dụ: unsigned int x; /* biến nguyên không dấu */ signed int y; /* biến nguyên có dấu */ int z; /* biến nguyên có dấu */ char c; /* biến nguyên 1 byte không dấu */ Chú ý trong ngôn ngữ C không tồn tại kiểu dữ liệu cơ bản cho logic. Logic đúng sai ở trong chương trình C được biểu diễn thông qua một số nguyên với giá trị 0 thể hiện sai (FALSE) và khác 0 (hay dùng giá trị 1) thể hiện đúng (TRUE). Do vậy khi muốn tạo biến logic ta thường dùng một biến nguyên int để thay thế. Ví dụ: int flag; /* tạo biến logic bằng int */ flag = TRUE; /* flag = 1 */ Cũng như dữ liệu kiểu logic, ngôn ngữ C cũng không tạo một kiểu dữ liệu đặc thù cho các kí tự. Thực tế một kí tự được biểu diễn trong bộ nhớ máy tính là một số nguyên 1 byte. Việc ánh xạ kí tự nào tương ứng với con số nào được qui ước trong bảng mã ASCII. Chính vì chỉ có một byte bộ nhớ cho biểu diễn kí tự nên bảng mã ASCII chỉ có 256 kí tự mà thôi. Do vậy để có một biến kí tự trong ngôn ngữ C ta dùng kiểu dữ liệu char. Ví dụ: char c; c = ‘A’; /* thực chất c = 65 do ‘A’ có mã ASCII là 65 */ Như vậy chúng ta có thể thấy kiểu dữ liệu cơ bản của C chỉ bao gồm hai loại số nguyên và số thực. Tất các các loại dữ liệu khác đều được quy về cách biểu diễn số. BÀI TẬP Câu 1: Viết khai báo biến cho các loại dữ liệu sau:
  14. a) Hệ số a, b, c của một phương trình bậc 2 bất kì b) Điểm kiểm tra của một môn học c) Số dân của một đất nước d) Số nhà của một phố e) Chữ cái đầu tiên của một tên người f) Tổng số vụ tai nạn giao thông tăng/giảm so với năm trước g) Giới tính của một người Câu 2: Chỉ ra các khai báo biến sai trong những khai báo sau đây: a) int a, A; b) int a, a; c) int aa; d) int a a; e) int a, b; f) int a, int b; g) int a; int b; Bài 7 - Hằng số Tóm tắt nội dung: Bài này giới thiệu các cách khai báo hằng số trong C bao gồm các loại: hằng số nguyên, hằng số thực, hằng kí tự và hằng xâu. Thời lượng: 1 tiết 1. Biểu diễn hằng số Trong một chương trình để gán giá trị cho một biến ta phải dùng các hằng số. Với mỗi loại dữ liệu ta có cách viết hằng số khác nhau. Sau đây là cách biểu diễn hằng số trong ngôn ngữ C. a. Hằng nguyên Dạng thập phân: 12345 Dạng hexa: 0xFFFF (bắt đầu bằng 0x) Dạng bát phân: 0128 (bắt đầu bằng 0) b. Hằng thực Dạng dấu phẩy tĩnh: 1.234 Dạng dấu phẩy động: 1234E-3 (=1234x10-3) c. Hằng kí tự Dạng kí tự: 'A' Dạng mã ASCII: '\65'
  15. Ngoài ra một số kí tự đặc biệt khác được viết theo quy ước: Hằng Giá trị ‘\\’ kí tự \ ‘\'’ kí tự ' ‘\"’ kí tự " ‘\0’ kí tự NULL (mã 0) ‘\n’ kí tự xuống dòng (mã 13) ‘\t’ kí tự tab (mã 9) ‘\a’ kí tự rung chuông (mã 7) d. Hằng xâu Một xâu trong ngôn C là một mảng các kí tự có kí tự NULL đánh dấu kết thúc xâu. Hằng xâu phải được biểu diễn trong cặp dấu nháy kép (ví dụ, "DHBKHN"). 2. Khai báo biến có khởi tạo giá trị = ; Ví dụ: int a = 1234, b = 0xFFFF; char ch = ‘A’; float f = 1234E-3; 3. Biến hằng số Một biến hằng số chỉ nhận một giá trị ban đầu và không bị thay đổi trong suốt quá trình chương trình chạy. const = ; Ví dụ: const float PI = 3.14; Ngoài ra chúng ta cũng có thể định nghĩa hằng số bằng #define trong một chương trình C (xem bài 35 - Một số chỉ thị tiền xử lí). Một số hằng đã được định nghĩa bằng #define là: Hằng Giá trị TRUE 1 FALSE 0 NULL 0 BÀI TẬP Câu 1: Chỉ ra các khai báo biến hằng số sai cho những câu sau đây:
  16. a) const int a = 1000000; b) const long a = 1000000; c) const int a = 123.5; d) const float a = 123.5; e) const float a = 123; f) const int a = 'A'; g) const char a = 300; Câu 2: Chỉ ra những khai báo biến không hợp lệ a) int a, b = 100; b) const int a, b = 100; c) int a = b = 100; d) const int a = b = 100; e) const int a = 100, b = 100; Bài 8 - Nhập xuất dữ liệu Tóm tắt nội dung: Để in dữ liệu chúng ta dùng hàm printf(), còn để nhập dữ liệu thì dùng hàm scanf(). Hai hàm này sử dụng các định dạng để chỉ ra loại dữ liệu dùng trong việc xuất và nhập. Thời lượng: 1 tiết Các hàm vào ra chuẩn của C được khai báo nguyên mẫu trong tệp tiêu đề có hai hàm cơ bản là printf() và scanf(). 1. Xuất dữ liệu bằng printf() Dạng thức chung: printf ( [, ]); Ví dụ 1: in xâu printf("Hello"); /* in xâu không có xuống dòng */ printf("Hello\n"); /* in xâu có xuống dòng */ Ví dụ 2: in giá trị biến int n = 5; printf ("n = %d", n); Chuỗi định dạng là chuỗi cần được in ra màn hình trong đó có thêm các định dạng dữ liệu tại mỗi chỗ cần in giá trị của một biến hay một biểu thức. Việc in ra màn hình thế nào hoàn toàn quyết định bởi xâu định dạng này. Trong ví dụ 2 giá trị của biến nguyên n muốn được in sau chuỗi "n =" nên tại vị trí sau chuỗi này trong xâu định dạng có thêm %d để chỉ ra sẽ có một số được in ra duới dạng thập phân chính tại vị trí đó. Dữ liệu được in là các tham số truyền vào sau xâu định dạng. Sau đây là bảng mã định dạng cho dữ liệu cần in ra.
  17. Mã định dạng Dạng in %d Số nguyên thập phân %f Số thực %c Kí tự %ld Số nguyên long %x Số nguyên hexa %s Chuỗi kí tự %% Kí tự % Cách in này của C cho phép ta có thể in một dữ liệu nhưng ra màn hình dưới nhiều dạng thức khác nhau. Ví dụ: char ch = ‘A’ ; printf("In dạng thập phân ch = %d\n", ch); printf("In dạng kí tự ch = %c\n", ch); printf("In dạng hexa ch = %x\n", ch); /* Kết quả sẽ là: In dạng thập phân ch = 65 In dạng kí tự ch = A In dạng hexa ch = 41 */ Chúng ta có thể sử dụng các kí tự điều khiển để điều khiển việc in. Ví dụ dùng kí tự ‘\n’ để điều khiển in xuống dòng. Hãy xem thêm các kí tự điều khiển trong bảng mã ASCII và cách biểu diễn hằng kí tự trong C. Trong định dạng in dữ liệu chúng ta có thể thêm vào các thuộc tính chỉ ra số ô chữ dùng để in dữ liệu. Khi đó dữ liệu in ra sẽ được căn sang trái số ô mà nó dành ra để in dữ liệu. Ví dụ %5d là in số thập phân tại 5 ô trắng, hay %5.2f là in số thực với 5 ô trong đó 2 ô cho phần thực. Ví dụ: int i = 6; float f = 3.237; printf("123456789\n"); printf("i=%3d", i); printf("f=%5.2f", f); /* Kết quả in: 123456789 i= 6 f= 3.24 */ 2. Nhập dữ liệu bằng scanf() Dạng thức chung: scanf( , ); Ví dụ:
  18. float f; int a, b, c; printf ("f = "); scanf ("%f", &f); /* nhập số thực cho f */ printf ("a, b, c: "); scanf ("%d%d%d", &a, &b, &c); /* nhập số nguyên cho 3 biến a, b, c */ Tham số truyền vào cho hàm nhập dữ liệu có một chuỗi định dạng bao gồm các mã định dạng dữ liệu để mô tả loại dữ liệu được nhập. Chúng ta sử dụng các kí tự định dạng dữ liệu (‘%d’, ‘%f’, ‘%c’, ‘%s’) giống như sử dụng đối với hàm printf() trong chuỗi định dạng này. Các tham số cần thiết khác cho một hàm scanf() là địa chỉ vùng nhớ nơi lưu dữ liệu được nhập vào. Do vậy để nhập giá trị cho biến nào thì ta phải lấy địa chỉ bộ nhớ của biến đó khi truyền vào cho hàm scanf(). Để lấy địa chỉ của biến ta dùng toán tử & trước tên biến. Chú ý một lỗi bộ nhớ chương trình như ví dụ sau: int a; scanf("%d", a); /* nhập giá trị cho biến a nhưng không lấy địa chỉ */ Đây không phải là một lỗi cú pháp nên chương trình vẫn có thể dịch và chạy được. Nhưng khi chạy giá trị số nguyên được người sử dụng nhập vào sẽ không được lưu cho biến a vì địa chỉ lưu giá trị là một địa chỉ không xác định có giá trị bằng số a hiện tại. Để đảm bảo quá trình nhập một kí tự hay một xâu không nhận phải dữ liệu rác trong chương trình ta thường sử dụng câu lệnh fflush(stdin) trước mỗi lần nhập. Lí do cần phải có câu lệnh này sẽ được giải thích trong bài nói về vấn đề vào ra với kênh xuất nhập (xem bài 31 - Kênh xuất nhập). 3. Thao tác trực tiếp với màn hình và bàn phím Một chương trình C viết cho hệ điều hành DOS có thể sử dụng các hàm thao tác trực tiếp với màn hình và bàn phím được khai báo trong tệp tiêu đề . Hai hàm hay dùng trong thư viện này là: clrscr() xoá toàn bộ kí tự hiển thị trên màn hình getch() chờ nhận một phím được bấm trên bàn phím, kết quả trả về mã phím được bấm Chương trình mẫu (hexa.c): Nhập một số nguyên dạng thập phân và in ra màn hình số hexa tương ứng của nó. #include #include void main() { int x; clrscr(); printf("Mot so thap phan: "); scanf("%d", &x); /* nhập số thập phân vào biến x */ printf("So hexa cua no la %x", x); /* in số hexa của nó */ getch(); /* dừng màn hình xem kết quả */ }
  19. BÀI TẬP Câu 1: Tìm các câu lệnh xuất dữ liệu sai cho các biến sau: int a, b; float x, y; a) scanf("%d%d", &a); b) scanf("%d%d", &a, &b); c) scanf("%d%f", &a, &b); d) scanf("%f", &x); e) scanf("%f%f", &x, y); f) printf("x=", x); g) printf("x=%f", x); h) printf("a=%d, b=%d", &a, &b); i) printf("a=%d, b=%d", a); Câu 2: Viết chương trình cho phép người sử dụng nhập vào một kí tự bất kì và thực hiện in ra mã ASCII tương ứng của nó. Câu 3: Viết chương trình in ra màn hình một trang giới thiệu về bản thân bạn như tên, năm sinh, địa chỉ, Bài 9 - Biểu thức và toán tử Tóm tắt nội dung: Biểu thức được xây dựng trên cơ sở kết hợp các biến và hằng số với các toán tử đê tính toán giá trị mới. Trong C có rất nhiều toán tử khác nhau và được phân làm một số nhóm cơ bản như: nhóm gán, nhóm toán học, nhóm so sánh, nhóm quan hệ, Bởi kiểu dữ liệu của C chỉ là số nên kết quả của một biểu thức cũng luôn là một số. Thời lượng: 2 tiết Một biểu thức được xây dựng trên cơ sở kết hợp các biến và hằng số cùng với các toán tử để tạo thành các phép tính. Bởi vì trong ngôn ngữ C chỉ có các kiểu dữ liệu số (nguyên hoặc thực) nên một biểu thức luôn trả về kết quả là một số. Sau đây chúng ta sẽ xem xét các toán tử có thể được sử dụng trong biểu thức theo nhóm. 1. Nhóm toán tử số học Toán tử Ý nghĩa Ví dụ + Cộng (2 ngôi) a+12 - Trừ (2 ngôi) a-12 * Nhân (2 ngôi) a*2 / Chia (2 ngôi) a/2 % Lấy phần dư phép chia a%2 - Đảo dấu (1 ngôi) -a
  20. Các toán tử toán học có thể áp dụng cho các toán hạng là biến, hằng số hoặc là một biểu thức con. Riêng toán tử % chỉ áp dụng cho hai số kiểu nguyên. Chú ý kết quả của biểu thức là số nguyên hay số thực phụ thuộc vào kiểu dữ liệu của hai toán hạng. Nếu hai toán hạng đều là số nguyên thì kết quả của biểu thức sẽ là một số nguyên. Trường hợp có một toán hạng là số thực thì kết quả biểu thức là số thực. Ví dụ: int i = 5; float f; /* chia cho một số nguyên */ f = i/2; /* f = 2 vì kết quả của biểu thức chỉ là một số nguyên */ /* chia cho một số thực */ f = i/2.0; /* f = 2.5 vì kết quả của biểu thức là một số thực */ Thứ tự ưu tiên của các toán tử số học trong một biểu thức theo trật tự truyền thống. Ví dụ 3+5*2 sẽ cho ta kết quả là 13. 2. Nhóm toán tử so sánh Toán tử Ý nghĩa Ví dụ == So sánh bằng a==5 != So sánh khác a !=5 So sánh lớn hơn a>5 >= So sánh lớn hơn hoặc bằng a>=5 Các toán tử so sánh cũng có thể áp dụng cho các toán hạng là biến, hằng số hoặc là một biểu thức con. Kết quả của biểu thức so sánh luôn trả về kết quả là 1 thể hiện phép so sánh đúng hoặc 0 thể hiện phép so sánh sai. Ví dụ 3 5 cho kết quả là 0. 3. Nhóm toán tử quan hệ logic Toán tử Ý nghĩa Ví dụ && Quan hệ và (and) (a 6 cho kết quả là 1, !3 cho giá trị 0. 4. Nhóm toán tử trên bit Toán tử Ý nghĩa Ví dụ & Và bit (2 ngôi) a&5
  21. | Hoặc bit (2 ngôi) a|5 ^ Xor bit (2 ngôi) a^5 ~ Đảo bit (1 ngôi) ~a > Dịch bit sang phải a>>2 = Ý nghĩa của biểu thức này là gán giá trị của biểu thức vế phải cho biến chỉ định ở vế trái. Khi một biểu thức gán được sử dụng như một câu lệnh thì được gọi là lệnh gán. Biểu thức gán cũng trả về giá trị của biến sau khi gán. Chính vì lí do này mà ta có thể tạo nhiều phép gán liên tiếp trong một biểu thức. Sau đây là một vị dụ minh hoạ. int a, b, c; a = 5; /* biểu thức gán được sử dụng làm lệnh gán */ b = c = 2; /* tương đương b=(c=2); */ Cần chú ý sự nhầm lẫn trong việc sử dụng toán tử gán (=) và toán tử so sánh (==). Xét ví dụ dưới đây: int a = 5; int b = a == 5; /* tương đương b=(a==5) và khi đó b có giá trị 1 */ int c = a = 5; /* tương đương c=(a=5) và khi đó c có giá trị 5 */ Ngoài ra chúng ta còn có một số phép gán đi kèm một phép tính số học. Bảng dưới đây trình bày một số toán tử gán loại này. Toán tử Ý nghĩa Ví dụ Biểu thức tương đương += tự cộng a+=5 a=a+5 -= tự trừ a-=5 a=a-5 *= tự nhân a*=5 a=a*5 /= tự chia a/=5 a=a/5 %= tự chia dư a%=5 a=a%5 &= tự và bit a&=5 a=a&5 |= tự hoặc bit a|=5 a=a|5 ^= tự xor bit a^=5 a=a^5 >>= tự dịch bit phải a>>=5 a=a>>5
  22. ? : Nếu có giá trị đúng (khác 0) thì biểu thức lấy giá trị của còn không lấy giá trị của . Ví dụ: int a, b, max; /* tính max của a, b bằng biểu thức điều kiện */ max = a>b ? a: b; 8. Ép kiểu Khi thực hiện gán giá trị của một biểu thức vào biến thì yêu cầu kiểu dữ liệu của giá trị biểu thức phải cùng kiểu với biến. Tuy nhiên C cũng cho phép tự chuyển kiểu của giá trị biểu thức sang phù hợp với kiểu của biến trong trường hợp phép chuyển này là không mất thông tin (ví dụ từ int sang float). Trong trường hợp phép chuyển kiểu mà bị mất thông tin thì nó không thể thực hiện được tự động. Khi đó ta cần phải có một phép ép kiểu bắt buộc theo định dạng dưới đây. ( )
  23. Ví dụ: int a, b; float f; f =5/2; /* đúng vì kết quả phép chia là số nguyên 2 tự chuyển thành 2.0 */ a=5/2.0; /* sai vì kết quả là số thực 2.5 không thể tự chuyển về int */ b=(int)(5/2.0); /* đúng vì số thực 2.5 bị ép kiểu về số nguyên 2 */ Như vậy phép chuyển kiểu tự động chỉ thực hiện được khi nó không bị mất mát thông tin. Sơ đồ sau đây là thể hiện việc chuyển kiểu tự động có thể thực hiện trên các kiểu dữ liệu cơ bản. char >int >long >float >double 9. Thứ tư ưu tiên của các toán tử Một biểu thức có thể là sự kết hợp của nhiều toán tử trong các nhóm với nhau. Khi đó cần có một trật tự xác định được thứ tự thực hiện của các toán tử trong biểu thức. Sau đây là quy tắc thứ tự ưu tiên của các toán tử được sắp xếp phân theo nhóm. Trong trường hợp có nhiều toán tử trong nhóm thì trật tự kết hợp được quy ước từ phải sang trái hay ngược lại là phụ thuộc vào mỗi nhóm. Mức ưu tiên Nhóm toán tử Trật tự 1 Nhóm 1 ngôi ( !, ~, ++, , ) >> 3 Nhóm dịch bit ( >) >>> 4 Nhóm so sánh ( , >=, ==, !=) >>> 5 Nhóm toán tử trên bit (&, |, ^) >>> 6 Nhóm quan hệ logic (&&, ||) >>> 7 Toán tử điều kiện (?:) = p f) n*q g) q+3*(n>p)
  24. h) q&&n i) (q-2)&&(n-10) j) x*(q==2) k) x*(q=5) Câu 2: Hãy cho biết giá trị của z sau mỗi phép tính int x=2; float y=9.2; float z; a) z=7/x; b) z=5.0/x; c) z= x + (int)y; d) z= x>=y||y>x e) z=y/x+3; Câu 3: Viết chương trình cho phép nhập vào hai cạnh của một hình chữ nhật rồi in ra chu vi và diện tích của nó. Câu 4: Viết chương trình cho phép nhập vào hai số bất kì rồi in ra màn hình tổng, tích và bình phương của nó. Bài 10 - Các hàm toán học Tóm tắt nội dung: Để tạo các phép tính phức tạp trong chương trình chúng ta có thể sử các hàm toán học trong thư viện. Các hàm này được khai báo trong tệp tiêu đề . Thời lượng: 1 tiết Để thực hiện tính toán các công thức toán học phức tạp chúng ta cần phải sử dụng các hàm toán học của thư viện. Muốn sử dụng chúng cần phải khai báo tệp tiêu đề trong chương trình. Một số hàm toán học trong Hàm Ý nghĩa double ceil(double x) số nguyên nhỏ nhất >= x double cos(double x) cosine của x (radians) double exp(double x) ex double fabs (double x ) |x| double floor(double x) số nguyên lớn nhất <= x double log(double x) ln x. double log10(double x ) log10x double pow(double x, double y) xy double sin(double x) sine của x double sqrt(double x) căn bậc hai của x. double tan(double x) tangent của x
  25. Các hằng số toán học Hằng số Ý nghĩa M_E số tự nhiên e M_PI số pi MAXFLOAT số thực thực float lớn nhất Ngoài ra chúng ta còn có một số hàm toán học với số nguyên trong Hàm Ý nghĩa int abs(int n) |n| long labs(long n) |n| int rand() lấy một số int ngẫu nhiên BÀI TẬP Câu 1: Viết chương trình quay sổ xố cho giải đặc biệt là một số ngẫu nhiên có 9 chữ số. Câu 2: Viết chương trình cho đọc một góc theo độ rồi in ra màn hình sin, cos và tan của góc đó. Mục 2.2 - Các cấu trúc lệnh điều khiển Cũng như các ngôn ngữ lập trình bậc cao khác, C có các cấu trúc lệnh điều khiển để tạo ra các rẽ nhánh giải thuật và các vòng lặp. Trong mục này học viên sẽ được tìm hiểu các câu lệnh if, switch, for, while và do while. Yêu cầu: Có kiến thức về lưu đồ giải thuật Thời lượng: 3 tiết Bài 11 - Cấu trúc lệnh rẽ nhánh Tóm tắt nội dung: Để tạo ra hai nhánh đúng/sai trong lưu đồ giải thuật ta dùng câu lệnh điều kiện if trong C. Trong trường hợp cần có một cấu trúc đa nhánh ta dùng lệnh switch. Thời lượng: 1 tiết 1. Lệnh if Dạng thức chung:
  26. if ( ) [else ] Câu lệnh if dùng để thực hiện một câu lệnh đơn hay một khối lệnh tuỳ thuộc vào một biểu thức kiểm tra. Nếu cho kết quả là đúng thì nhánh chính của câu lệnh if được thực hiện, trường hợp sai thì nhánh else (nếu có) sẽ được thực hiện. Chú ý rằng một lệnh đơn luôn kết thúc bằng dấu chấm phẩy ‘;’ còn các lệnh của một khối nằm trong cặp dấu {}. Ví dụ: int a, b, max; /* sử dụng câu lệnh if tính max của a, b */ if (a>b) max = a; else max = b; Biểu thức kiểm tra có thể là một biểu thức bất kì của C (xem bài 9 - Biểu thức và toán tử). Chỉ cần nó trả về giá trị 0 thì được coi là sai, khác 0 coi là đúng. Vì thế một phép kiểm tra if (a !=0) có thể thay bằng if (a). Cần chú ý sự nhầm lẫn giữa toán tử gán (=) với toán tử so sánh bằng (==). Ví dụ: int a=0, b=1, c=1; /* biểu thức so sánh a bằng 0 đúng nên b = 0 */ if (a==0) b=a; /* biểu thức gán a=0 trả về giá trị sai nên c giữ nguyên giá trị */ if (a=0) c=a; Chương trình mẫu (ptb1.c): Giải và biện luận phương trình ax+b=0 #include void main() { float a, b; printf("Nhap he so cua phuong trinh bac nhat\n"); printf("He so a: "); scanf("%f", &a); printf("He so b: "); scanf("%f", &b); if (a==0) /* nhánh a==0 */ if (b==0) printf("Phuong trinh co vo so nghiem"); else printf("Phuong trinh vo nghiem"); else /* nhánh a!=0 */ printf("Phuong trinh co nghiem bang %f", -b/a); } 2. Lệnh switch Dạng thức chung: switch ( ) { case :
  27. break; case : break; . . . [default: ] } Khi gặp lệnh switch chương trình kiểm tra giá trị của với các hằng sau từ khoá case. Nếu giá trị biểu thức bằng một hằng nào thì các lệnh của nhánh tương ứng được thực hiện. Lệnh break dùng để kết thúc một nhánh thực hiện. Nhánh default chỉ được thực hiện khi tất cả các nhánh hằng số đều không thoả mãn. Một nhánh có thể dùng cho nhiều hằng số khác nhau. Khi đó mỗi hằng số được viết sau một từ khoá case. Sau đây là một ví dụ sử dụng lệnh switch để in lịch làm việc trong tuần. int ngay; printf("Nhap gia tri ngay trong tuan (1/CN, 2-7):"); scanf("%d", &ngay); switch (ngay) { case 2: case 3: case 4: case 5: case 6: printf("Lam viec ca ngay\n"); break; case 7: printf("Lam viec buoi sang\n"); break; case 1: printf("Ngay nghi\n"); break; default: printf("Nhap sai gia tri\"); } Chú ý khi lệnh break bị thiếu cho việc kết thúc nhánh thì các câu lệnh của nhánh dưới vẫn được coi là các câu lệnh của nhánh hiện tại. Việc thực hiện các câu lệnh của một nhánh chỉ kết thúc khi gặp break hoặc khi hết lệnh switch. Ví dụ: int a=1; switch(a) { case 1: printf("a=1\n"); case 2: printf("a=2\n"); break; case 3: printf("a=3\n");
  28. } /* kết quả in: a=1 a=2 */ BÀI TẬP Câu 1: Viết chương trình giải và biện luận phương trình bậc 2 (ax2+bx+c=0). Câu 2: Viết chương trình đọc vào một số của tháng trong năm rồi in ra màn hình tên của tháng đó trong tiếng Anh. Câu 3: Cho bốn số a, b, c, d đọc vào từ bàn phím. Hãy tìm giá trị cực đại và gán cho biến có tên là Max và in ra kết quả của nó. Câu 4: Viết chương trình nhập vào phần thực và phần ảo của một số phức xong rồi in ra màn hình dạng hoàn chỉnh của số phức đó. Câu 5: Viết chương trình đọc vào một số thể hiện tổng số giây rồi in ra số giờ, phút, giây tương đương. Ví dụ 7322 giây là tương đương 2 giờ, 2 phút, 2 giây. Bài 12 - Cấu trúc lệnh lặp Tóm tắt nội dung: Vòng lặp được chia làm hai loại là vòng lặp có điều kiện lặp kiểm tra trước (while) và vòng lặp kiểm tra sau (do while). Trong C vòng lặp for là vòng lặp cùng loại vòng lặp while nhưng có thêm các lệnh khởi tạo khi bắt đầu vòng lặp và lệnh thực hiện sau mỗi lần lặp. Câu lệnh break để kết thúc một vòng lặp, còn continue để quay trở lại một lần lặp mới. Thời lượng: 2 tiết 1. Lệnh for Dạng thức chung: for ([ ]; [ ]; [ ]) Vòng lặp for cho phép thực hiện một lệnh hoặc một khối lệnh lặp nhiều lần khi mà kiểm tra còn đúng. Phần của vòng for cho phép thực hiện một số lệnh khi bắt đầu thực hiện vòng lặp. Phần là những câu lệnh được thực hiện sau mỗi lần lặp. Trong mỗi phần khởi tạo và sau lặp nếu có nhiều câu lệnh thì chúng được phân cách nhau bởi dấu phẩy ‘,’. Ví dụ: int i, j;
  29. /* in 5 dòng Hello bằng cách lặp với biến i mang 5 giá trị (1-5) */ for (i=1; i , , của một vòng for có thể trắng. Nếu điều kiện lặp không có thì nó lấy mặc định là luôn đúng. Do vậy vòng for sau đây là một vòng lặp vô hạn. for ( ; ; ) { } Chương trình mẫu (bieuthuc.c): Tính giá trị biểu thức 1 + 1/2 + 1/4 + + 1/2n với n được nhập vào. #include void main() { int n, i; float sum, sh; /* nhập n */ printf("n= "); scanf("%d", &n); /* khởi tạo sum và sh bằng số hạng đầu tiên */ sum = 0; sh = 1; /* dùng vòng lặp tính tổng các số hạng */ for (i=0; i ) Các câu lệnh trong vòng lặp while được thực hiện lặp chừng nào biểu thức còn mang giá trị đúng. Các câu lệnh trong vòng lặp chỉ được thực hiện sau khi đã kiểm tra điều kiện lặp đúng. Vì vậy while được gọi là vòng lặp có số lần lặp không xác định và kiểm tra điều kiện lặp trước.
  30. Ví dụ: int i; /* in 5 dòng Hello bằng vòng lặp while */ i = 1; while (i while ( ) { } 3. Lệnh do while Dạng thức chung: do { } while ( ); Vòng lặp do while chỉ khác với vòng lặp while ở chỗ được kiểm tra sau mỗi lần lặp. Do vậy ta có thể gọi do while là vòng lặp có số lần lặp không xác định và kiểm tra điều kiện lặp sau. Chú ý: Ngược lại với vòng lặp repeat until của PASCAL, điều kiện kiểm tra của do while là điều kiện lặp lại chứ không phải điều kiện ra. Ví dụ: int i; /* in 5 dòng Hello bằng vòng lặp do while */ i = 1; do { printf("Hello\n") ; i++; }while (i<=5); Khi cần những vòng lặp kiểm tra điều kiện lặp sau thì bắt buộc phải sử dụng do while. Ví dụ cần nhập một số nguyên n trong khoảng [1, 10] bằng một vòng lặp để khi người sử dụng nhập sai giá trị thì nhập lại. Khi đó ta phải dùng do while vì rõ ràng phải nhập dữ liệu rồi ta mới kiểm tra xem có cần phải nhập lại không.
  31. int n; do { printf("Nhap n trong khoang (1 10): "); scanf("%d", &n); }while (n 10); 4. continue và break Trong một vòng lặp có thể dùng các lệnh điều khiển như continue và break với ý nghĩa của chúng như sau: - continue: bỏ qua tất cả các lệnh còn lại trong vòng lặp và quay trở lại từ đầu vòng lặp. - break: ra khỏi vòng lặp tức thì mà không phụ thuộc vào điều kiện lặp. Người ta thường dùng break để thoát ra khỏi các vòng lặp vô hạn. Ví dụ: int value; /* Nhập giá trị nguyên dương cho value, để kết thúc nhập 0 */ while (scanf("%d'', &value ) == 1 && value != 0) { if (value 100) { printf("Bỏ qua không xử lí số lớn hơn 100\n''); continue; /* Quay lại để nhập giá trị mới */ } /* Xử lí với giá trị nhập đã được đảm bảo nằm trong khoảng (1 100) */ } Chương trình mẫu (sopi.c): Tính số Pi theo công thức Pi/4 = 1-1/3+1/5-1/7+ với độ chính xác 0.001. #include void main() { /* Pi_4 là tổng của biểu thức Pi/4 */ float Pi_4, epsilon; int mauso, dau; /* khởi tạo các giá trị cho phép tính tổng */ Pi_4 = 0; mauso = 1; /* mauso của số hạng đầu tiên */ dau = 1; /* dấu của số hạng đầu tiên */ /* vòng lặp tính tổng */ do { Pi_4 += dau*(1.0/mauso); /* thêm số hạng mới */ /* độ chính xác tính toán là số hạng cuối x 4 */ epsilon = 4.0/mauso; dau = -dau; /* đảo dấu cho số hạng sau */ mauso += 2; /* tăng mẫu số cho số hạng sau */ }while(epsilon>0.001); /* lặp khi độ chính xác > 0.001 */ printf("Pi co do chinh xac 0.001 = %f", Pi_4*4);
  32. } BÀI TẬP Câu 1: Viết chương trình đọc vào 10 số nguyên rồi in ra tổng, giá trị lớn nhất và bé nhất của chúng. Câu 2: Viết chương trình tìm tất cả các số có ba chữ số abc sao cho tổng các lập phương của các chữ số bằng chính số đó. Câu 3: In ra các cách để có 20000 đồng với 3 loại giấy bạc: 500 đ, 200 đ và 100 đ. Câu 4: Viết chương trình dùng vòng lặp để hiện các hình sau trên màn hình a) $$$$$$ $$$$$ $$$$ $$$ $$ $ b) * Câu 5: Viết chương trình tính hàm COS(X) theo chuỗi Taylor sau: cos x = 1 – x2 + x4 – x6 + 2! 4! 6! Mục 2.3 - Hàm Mục này giới thiệu hàm như là cách phân chia chương trình con trong ngôn ngữ C. Một hàm có thể trả về một giá trị hoặc không. Trong các hàm của chương trình luôn tồn tại một hàm được gọi là hàm chính của chương trình (hàm main()). Hàm này được thực hiện ngay khi chương trình bắt đầu chạy. C là ngôn ngữ cho phép gọi đệ quy hàm Yêu cầu: Hiểu khái niệm phân chia modul chương trình và giải thuật đệ quy. Thời lượng: 2 tiết
  33. Bài 13 - Cấu trúc chương trình với hàm Tóm tắt nội dung: Các hàm trong chương trình C là một modul chương trình đẳng cấp. Chúng có thể gọi lẫn nhau trong chương trình. Một hàm có thể trả về kết quả nhưng cũng có thể không (hàm void). Khai báo nguyên mẫu của hàm là một khai báo hàm chỉ có tiêu đề mà không có phần thi hành. Thời lượng: 1 tiết 1. Hàm trong ngôn ngữ C Một chương trình C được cấu trúc thông qua các hàm. Mỗi hàm là một modul nhỏ trong chương trình. Nó có thể được gọi nhiều lần tại các vị trí khác nhau trong chương trình. Trong C không phân biệt thủ tục và hàm như PASCAL mà nó coi thủ tục như là một hàm mà không trả về dữ liệu. Một đặc điểm khác biệt nữa đó là không có khái niệm thủ tục con, tất cả các hàm kể cả hàm chính (main) đều có cùng một cấp duy nhất (cấu trúc hàm đẳng cấp). Một hàm có thể gọi một hàm khác bất kì của chương trình. Dạng thức khai báo một hàm: [ ] ([ ]) { } Trong đó: - là một tên do người sử dụng đặt thoả mãn yêu cầu về đặt tên trong C. Nếu hàm có tên là main thì đó là hàm chính của chương trình. - là kiểu của dữ liệu trả về cho hàm. Nó có thể là một kiểu dữ liệu cơ bản (char, int, ) hay kiểu phức hợp (xem chương 3 - Các cấu trúc dữ liệu phức). Khi muốn viết một thủ tục (hàm không có kết quả) thì kiểu trả về ta để là void. Kiểu trả về có thể không được khai báo, khi đó trình biên dịch lấy mặc định là kiểu int. - được khai báo giống như khai báo biến. Các tham số trong danh sách được phân cách bởi dấu phẩy ‘,’. Nếu dùng từ khoá void cho danh sách tham số của một hàm thì hàm đó không có bất kì một tham số nào. - bao gồm phần khai báo biến ở đầu (bắt buộc) và các câu lệnh sau khai báo biến. Để thoát ra khỏi hàm và trả về một giá trị ta dùng câu lệnh return . Nếu là hàm void thì câu lệnh return không có giá trị trả về kèm theo. Ví dụ: /* hàm tính max hai số nguyên */ int max (int a, int b) { return (a > b) ? a: b; }
  34. /* hàm in bình phương của số 1 đến 10, hàm không có tham số */ void squares() /* hoặc void squares(void) */ { int i; for (i=1;i /* hàm tính Pi trả về kết quả là một số thực tham số truyền vào là độ chính xác của số Pi cần tính độ chính xác của phép tính Pi là số hạng cuối cùng x 4 */ float Pi(float epsilon) { /* Pi_4 là tổng của biểu thức Pi/4 */ float Pi_4, sh; int mauso, dau; /* khởi tạo các giá trị cho phép tính tổng */ Pi_4 = 0; mauso = 1; /* mauso của số hạng đầu tiên */ dau = 1; /* dấu của số hạng đầu tiên */ /* vòng lặp tính tổng */ do { sh = 1.0/mauso; /* sh mang giá trị số hạng mới */ Pi_4 += dau*sh; /* thêm số hạng mới */ dau = -dau; /* đảo dấu cho số hạng sau */ mauso += 2; /* tăng mẫu số cho số hạng sau */ }while(sh*4>epsilon); /* lặp khi độ chính xác > epsilon */ return 4*Pi_4; } void main() { float epsilon; printf("Do chinh xac cua so Pi can tinh: "); scanf("%f", &epsilon); /* in kết quả của hàm tính Pi với độ chính xác epsilon */ printf("Pi co do chinh xac %f = %f", epsilon, Pi(epsilon));
  35. } 2. Khai báo nguyên mẫu (prototype) cho hàm Khi một hàm gọi đến một hàm khác trong chương trình thì hàm được gọi đã phải được biết đến từ trước trong chương trình. Một hàm được coi là đã "biết đến" khi nó đã khai báo đầy đủ hoặc có khai báo nguyên mẫu (prototype) của nó. Một khai báo nguyên mẫu cho hàm thì chỉ có phần tiêu đề hàm mà không có thân hàm. Trong nguyên mẫu của hàm cần chỉ ra được tên hàm, kiểu dữ liệu của các tham số truyền vào và kết quả trả về. Dưới đây là khai báo nguyên mẫu của các hàm ở ví dụ trên. int max(int, int); /* một khai báo nguyên mẫu hàm phải có ; kết thúc */ void squares(); Trong một chương trình để giúp người đọc nhanh chóng nắm bắt cấu trúc của nó thì phần đầu của chương trình thường có khai báo nguyên mẫu của các hàm trong chương trình. Các khai báo hàm đầy đủ có thân hàm thường được viết phần sau của chương trình vì người đọc ít khi quan tâm đến nội dung chi tiết của từng hàm. Chương trình mẫu (nguyenmau.c): Một chương trình tính max hai số có sử dụng khai báo nguyên mẫu hàm. #include /* khai báo prototype cho các hàm của chương trình */ int max(int, int); float fmax(float, float); void main() { int a, b; float fa, fb; printf("Nhap hai so nguyen: "); scanf("%d%d", &a, &b); printf("Gia tri lon nhat la %d\n", max(a, b)); printf("Nhap hai so thuc: "); scanf("%f%f", &fa, &fb); printf("Gia tri lon nhat la %f\n", fmax(fa, fb)); } /* khai báo hàm đầy đủ */ int max(int a, int b) { return a > b ? a: b; } float fmax(float fa, float fb) { return fa > fb ? fa: fb; }
  36. BÀI TẬP Câu 1: Viết hàm cho phép tính diện tích của các hình vuông, hình tròn, hình thang trong một chương trình. Tạo một menu chương trình để người sử dụng lựa chọn một phương án tính diện tích theo cách thức sau. 1. Tính diện tích hình vuông 2. Tính diện tích hình tròn 3. Tính diện tích hình thang Hãy ấn một số để chọn: Câu 2: Tạo các hàm cho giải và biện luận phương trình bậc nhất và bậc hai. Hàm giải phương trình bậc hai sẽ gọi hàm phương trình bậc nhất khi hệ số a của phương trình bằng 0. Viết chương trình cho phép người sử dụng lựa chọn các công việc như sau. 1. Giải và biện luận phương trình bậc nhất 2. Giải và biện luận phương trình bậc hai Hãy ấn một số để chọn: Câu 3: Viết chương trình theo hàm để tìm ước số chung lớn nhất và bội số chung nhỏ nhất của hai số nguyên. Bài 14 - Đệ quy Tóm tắt nội dung: C là ngôn ngữ cho phép gọi đệ quy hàm. Khi viết hàm đệ quy cần chú ý đến điểm dừng để tránh đệ quy vô tận. Thời lượng: 1 tiết C là ngôn ngữ cho phép viết các hàm đệ qui. Một hàm đệ qui là hàm mà trong thân hàm lại gọi đến chính nó. Ví dụ để tính n! ta sử dụng công thức đệ qui: | 1 khi n=0 n! = | | n(n-1)! khi n>0 Hàm đệ qui tương ứng với công thức là: int giaithua(int n) { return n==0 ? 1: n*giaithua(n-1); } Khi viết hàm đệ qui cần phải chú ý đến điểm dừng của đệ qui. Nếu một đệ qui mà không có điểm dừng thì nó sẽ bị đệ qui vô hạn và khi đó sẽ gây ra lỗi tràn bộ nhớ stack của chương trình (xem bài 27 - Bộ nhớ stack).
  37. Chương trình mẫu (daoso.c): Sử dụng hàm đệ qui để in số đảo của một số nguyên dương. Ví dụ 12345 có số đảo là 54321. #include void insodao(int n) { int donvi, conlai donvi = n%10; /* tính phần đơn vị của số n */ conlai = n/10; /* giá trị từ phần chục của n */ /* để in số đảo thì in từ phần đơn vị */ printf("%d", donvi); /* nếu còn phần chục in đảo của nó */ if (conlai>0) insodao(conlai); } void main() { int n; printf("Nhap so nguyen duong: "); scanf("%d", &n); printf("So dao: "); insodao(n); } BÀI TẬP Câu 1: Viết hàm tính an với a là số thực, n nguyên dương, theo hai cách: đệ qui và không đệ qui. Câu 2: Viết hàm đếm số chữ số của một số nguyên n bất kì dùng phương pháp đệ qui. Câu 3: Hãy dùng hàm tính n! để viết chương trình hoàn chỉnh tính Ckn = n! / (k! * (n-k)!) Câu 4: Dãy số Fibonacci là dãy số F1, F2, F3, ,Fn được tạo ra với công thức Fn=Fn-1+Fn-2 với F1 = 1, F2 = 1. Thí dụ: 1, 1, 2, 3, 5, 8, 13, 21, Hãy lập chương trình dùng hàm đệ qui để in ra dãy số đó.
  38. CHƯƠNG 3 – CÁC CẤU TRÚC DỮ LIỆU PHỨC Chỉ có các dữ liệu kiểu cơ bản là không thể đủ để viết một chương trình với những loại dữ liệu phức tạp. Trong chương này học viên sẽ nắm bắt được cách thức tạo các cấu trúc dữ liệu phức như mảng, xâu, cấu trúc bản ghi ở trong ngôn ngữ C. Con trỏ cũng được coi là một loại dữ liệu phức trong C. Loại dữ liệu con trỏ cho phép người sử dụng quản lí bộ nhớ rất hiệu quả trong chương trình C. Yêu cầu: Có kiến thức về lập trình C cơ bản trong chương 2 Thời lượng: 10 tiết Mục 3.1 - Mảng dữ liệu Mảng dữ liệu là một tập các phần tử có cùng kiểu dữ liệu được lưu trữ liên tiếp nhau trong bộ nhớ. Số phần tử của mảng luôn được xác định ban đầu là một hằng số. Chúng ta có thể truy cập vào các phần tử của mảng thông qua chỉ số. Một mảng có thể là một chiều hay nhiều chiều. Yêu cầu: Đã tìm hiểu các kiểu dữ liệu cơ bản Thời lượng: 2 tiết Bài 15 - Mảng một chiều Tóm tắt nội dung: Mảng ở trong C được khai báo với số phần tử của mảng là một hằng số. Các phần tử trong mảng luôn luôn được đánh chỉ số tăng dần từ 0 cho đến hết. Thời lượng: 1 tiết Mảng là một cấu trúc dữ liệu trong bộ nhớ có khả năng lưu trữ một tập các phần tử có cùng kiểu dữ liệu. Các phần tử này được bố trí nằm liên tiếp với nhau và được đánh chỉ số. Để truy cập một phần tử ta truy cập theo chỉ số của nó. Mảng có một hạn chế là số phần tử trong mảng luôn phải được xác định từ trước như là môt hằng số. Vì vậy khi khai báo mảng ta luôn khai báo với số phẩn tử lớn nhất cần có để dùng trong thực tế. Một mảng trong ngôn ngữ C được khai báo với số phần tử của nó. Các phần tử được đánh chỉ số từ 0 và tăng dần lên. Chỉ có một cách đánh chỉ số này ở trong C. Điều đó làm nên sự khác biệt cho mảng của C với mảng của những ngôn ngữ khác như PASCAL. Khai báo mảng: [ ];
  39. Truy cập một phần tử theo chỉ số (bắt đầu từ 0): [ ] a[n] a[0] a[1] a[n-1] Hình 3: Bộ nhớ của mảng một chiều n phần tử Ví dụ: int a[5]; /* mảng 5 số int */ int i; /* gán các phần tử của mảng về 0 */ for (i=0; i =n>=20) sau đó tìm tổng của dãy số. In ra màn hình dãy số sau khi đã sắp xếp. #include void main() { int a[20], n, i, j, sum, tg; /* nhập số phần tử có kiểm tra để nhập lại */ do { printf("Nhap n (1-20) = "); scanf("%d", &n); }while(n 20); /* nhập dãy số, chỉ số chạy từ 0 */ for(i=0; i<n; i++) { printf("a[%d]=", i); scanf("%d", &a[i]); }
  40. /* tính sum */ sum = 0; for(i=1; i a[j]) { /* đổi chỗ 2 phần tử */ tg = a[i]; a[i] = a[j]; a[j] = tg; } /* in dãy số sau khi sắp xếp */ for(i=0; i 5). b. Xếp các số có giá trị tuyệt đối lớn hơn 10 lên đầu dãy. c. Trong dãy kết quả nhận được từ câu trên hãy xóa bớt số thứ 3 của dãy và số thứ N của dãy. Đưa các dãy kết quả ra màn hình. Câu 3: Viết chương trình đê nhập một mảng số nguyên rồi thực hiện sắp xếp chúng theo thứ tự tăng dần. Đọc một số nguyên mới vào từ bàn phím để chèn nó vào dãy số đã sắp xếp mà vẫn đảm bảo thứ tự. Câu 4: Nhập dữ liệu cho 2 mảng số A, B rồi sắp xếp theo thứ tự tăng dần. Hãy trộn hai mảng đó lại để có mảng thứ 3 là mảng C với điều kiện mảng C cũng được sắp xếp theo thứ tự tăng dần ngay sau khi trộn. Bài 16 - Mảng nhiều chiều Tóm tắt nội dung: Có thể tạo ra mảng nhiều chiều trong đó mỗi phần tử trong mảng được đánh chỉ số theo nhiều chiều khác nhau. Về mặt tổ chức bộ nhớ thì mảng nhiều chiều được coi là mảng của các phần tử là mảng. Thời lượng: 1 tiết
  41. Trong C có thể khai báo một mảng nhiều chiều bằng cách tiếp tục thêm cặp ngoặc vuông [] cùng số phần tử v ào khai báo mảng. Để truy cập vào một phần tử trong mảng nhiều chiều cần phải chỉ ra chỉ số ở các chiều tương ứng. Ví dụ: /* khai báo một ma trận 3x3 bằng mảng 2 chiều */ int matrix[3][3]; int i, j; /* khởi tạo các phần tử của ma trận bằng 0 */ for (i=0; i void main() { int cuuchuong[10][10]; /* bảng cửu chương cho cả số 0 */ int i, j; /* tạo giá trị cho bảng cửu chương */ for (i=0; i<=9; i++) for (j=0; j<=9; j++) cuuchuong[i][j] = i*j; /* giá trị của i x j trong bảng */ printf("Nhap hai so cua bang cuu chuong\n"); printf("So 1: "); scanf("%d", &i);
  42. printf("So 2: "); scanf("%d", &j); printf("Giá trị trong bang cuu chuong la %d", cuuchuong[i][j]); } BÀI TẬP Câu 1: Một ma trận số nguyên X có kích thước 10x10. a. Hãy lập chương trình tạo ra 100 giá trị ngẫu nhiên cho ma trận trong khoảng [1,10]. b. Tính tổng của tất cả các phần tử nằm trên đường chéo chính. c. Tính tổng bình phương các số trên hàng chẵn. d. Tìm số phần tử của ma trận có giá trị nằm trong dải [4, 6]. Câu 2: Viết chương trình tạo ma trận chuyển vị của ma trận A hai chiều. B là ma trận chuyển vị của ma trận A nếu các phần tử của chúng có quan hệ Bi,j = Ai,j. Tìm thuật toán để chuyển vị ma trận A song kết quả để ở chính ma trận A. Câu 3: Lập chương trình con nhân hai ma trận chữ nhật C=AxB. Ma trận A có kích thước là NxL, ma trận B có kích thước là LxM và ma trận kết quả C có kích thước là NxM. Mục 3.2 - Con trỏ Con trỏ là một loại biến có khả năng lưu địa chỉ của một đối tượng khác trong bộ nhớ. Thông qua con trỏ chúng ta có thể truy xuất đến địa chỉ bộ nhớ của đối tượng để sửa đổi hay lấy thông tin về nó. Con trỏ trong C là phưong tiện căn bản để giúp quản lí bộ nhớ chương trình một cách mềm dẻo. Yêu cầu: Có khái niệm về biến bộ nhớ, mảng và hàm Thời lượng: 3 tiết Bài 17 Con trỏ và địa chỉ bộ nhớ Tóm tắt nội dung: Một con trỏ là một địa chỉ bộ nhớ. Nó có thể định kiểu để trỏ tới một biến dữ liệu hay chỉ đơn thuần là một địa chỉ tổng quát (con trỏ void). Con trỏ có thể nhận giá trị rỗng (NULL) khi không trỏ đến đâu cả. Chúng ta có thể sửa đổi hay lấy thông tin của đối tượng được trỏ bởi con trỏ. Thời lượng: 1 tiết 1. Con trỏ là gì? Một con trỏ là một biến có thể chứa địa chỉ của biến khác. Con trỏ được gọi là có định kiểu nếu nó chỉ chứa địa chỉ (hay là trỏ đến) những biến có cùng một kiểu dữ liệu. Sau đây là cách khai báo một con trỏ có định kiểu trong ngôn ngữ C.
  43. * ; Một biến chỉ là con trỏ khi trong khai báo biến đó có dấu ‘*’ đứng trước. Ví dụ: int * a; /* a là con trỏ cho biến kiểu int */ int * b, i; /* i không phải là con trỏ mà là một biến int */ int *p, *q; /* cả p và q đều là con trỏ */ Con trỏ cũng được coi là một loại dữ liệu nên ta có thể tạo một mảng các phần tử là con trỏ theo dạng thức. * [ ]; Ví dụ: int * a[20]; /* a là mảng 20 con trỏ số nguyên */ char * s[80]; /* s là mảng 80 con trỏ char* */ 2. Toán tử & và * Một con trỏ cần được gán địa chỉ một biến khi muốn trỏ đến nó. Để lấy địa chỉ biến ta dùng toán tử & theo kí pháp & . Sau khi con trỏ đã trỏ đến một biến, ta có thể truy cập vào biến được trỏ bởi con trỏ bằng cách sử dụng toán tử * phía trước biến trỏ (* ). Chú ý rằng chỉ dùng toán tử * với những con trỏ đã được xác lập để trỏ tới một biến nào đó trong bộ nhớ. Nếu không việc truy cập nội dung sẽ gây lỗi bộ nhớ chương trình khi chạy. Ví dụ: int x, *p, *q; p = &x; /* xác lập p trỏ tới x */ *p = 5; /* gán nội dung cho biến trỏ bởi p, x=5 */ *q = 6; /* gây lỗi bộ nhớ vì q chưa trỏ tới đâu */ Một con trỏ có thể được xác lập giá trị NULL (=0). Khi đó nó được gọi là con trỏ rỗng (tức là không trỏ tới đâu). Trong chương trình ta chỉ dùng toán tử lấy nội dung (*) đối với những con trỏ không rỗng. Ví dụ: int *p; p = NULL; *p = 5; /* gây lỗi chương trình khi chạy vì p đang rỗng */ 3. Con trỏ void* Ngoài các con trỏ có định kiểu, trong C còn có con trỏ không định kiểu (void *). Một con trỏ void có thể trỏ tới bất kì một loại biến nào trong bộ nhớ. Thực chất một con trỏ void chỉ chứa một địa chỉ bộ nhớ mà không biết rằng tại địa chỉ đó có đối tượng kiểu dữ liệu gì. Chính vì vậy ta không thể truy cập nội dung của một đối tượng thông qua con trỏ void. Để truy cập được đối tượng thì trước hết phải ép kiểu con trỏ void về con trỏ có định kiểu của kiểu đối tượng.
  44. Ví dụ: float x; int y; void *p; /* khai báo con trỏ void */ p = &x; /* p chứa địa chỉ số thực x */ *p = 2.5; /* báo lỗi vì p là con trỏ void */ /* cần phải ép kiểu con trỏ void trước khi truy cập đối tượng qua con trỏ */ *((float*)p) = 2.5; /* x = 2 */ p = &y; /* p chứa địa chỉ số nguyên y */ *((int*)p) = 2; /* y = 2 */ 4. Con trỏ đa cấp Một con trỏ có thể dùng để trỏ tới một biến mà biến đó lại là một con trỏ. Con trỏ như vậy là con trỏ đa cấp. Số cấp của con trỏ bằng số dấu ‘*’ cho khai báo biến của con trỏ. Ví dụ: int a, *p, pp; /* pp là con trỏ 2 cấp */ p = &a; pp = &p; /* pp trỏ tới p, p trỏ tới a */ pp = 5; /* a=5 */ int pp int *p int a 5 Hình 5: Con trỏ đa cấp trong bộ nhớ BÀI TẬP Câu 1: Chỉ ra kiểu loại dữ liệu cho mỗi khai báo biến sau a) char s[80], *p, *a[80]; b) int * a[100], x; c) void * p, *a[20]; d) char s, ch; e) char *p, a[80]; Câu 2: Tìm các câu lệnh gán hợp lệ cho khai báo biến int *a[100], x, *p, q; a) a[0] = q; b) a[0] = &x; c) q = &x; d) q = &p; e) *q = a[0]; f) *a[0] = x; g) *(a[0]) = x; h) *x = 5;
  45. Câu 3: Tìm các câu lệnh gán hợp lệ cho khai báo biến int *a[100], x; void *p; a) a[0] = p; b) p = &x; c) *p = x; d) *(a[0]) = x; e) *(int*)p = x; f) x = a[0]; g) p = a[0]; Bài 18 - Con trỏ và mảng Tóm tắt nội dung: Con trỏ có thể trỏ tới một phần tử của một mảng dữ liệu. Khi đó chúng ta có thể truy cập các phần tử của mảng thông qua chỉ số lân cận. Con trỏ cũng có thể được dịch chuyển để trỏ sang các phần tử khác của mảng. Thời lượng: 1 tiết 1. Liên hệ giữa mảng và con trỏ Trong C giữa con trỏ và mảng có mối quan hệ rất mật thiết với nhau. Giả sử có một mảng các phần tử cùng kiểu dữ liệu và một con trỏ có định kiểu cùng kiểu của mảng. Khi con trỏ được xác lập trỏ tới một phần tử nào đó của mảng thì ta không chỉ có thể truy cập vào phần tử được trỏ bởi con trỏ mà cả những phần tử lân cận nó trong mảng. Để truy cập một phần tử lân cận ta dùng dạng thức: [ ] Ví dụ: int a[5], *p; p = a; /* p chứa địa chỉ mảng a, hay địa chỉ phần tử đầu tiên (&a[0]) */ p[0] = 5; /* truy cập phần tử được trỏ bởi p (a[0] = 5) */ p[1] = 6; /* truy cập phần tử lệch 1 đơn vị so với phần tử gốc (a[1]=6) */ p = &a[3]; /* p trỏ tới đến phần tử gốc a[3] */ p[-1] = 3; /* phần tử lệch -1 đơn vị so với phần tử gốc (a[2]=3) */ Chú ý rằng mảng trong C không phải là một biến. Khi dùng tên mảng trong chương trình tức là dùng địa chỉ của mảng đó. Chính điều này đã tạo ra mối quan hệ mật thiết giữa mảng và con trỏ khi con trỏ mang địa chỉ giống địa chỉ mảng. Vì mảng không phải là biến nên không thể gán hai mảng trong chương trình C. Ví dụ: int a[5], b[5]; a = b; /* lỗi vì a, b là 2 địa chỉ bộ nhớ */
  46. 2. Dịch chuyển con trỏ Một con trỏ đang trỏ tới một phần tử trong một mảng có thể được dịch chuyển để trỏ sang các phần tử lân cận khác bằng các phép toán cộng và trừ. Một con trỏ sẽ trỏ lệch đi i phần tử khi nó được cộng với số nguyên i. a[n] p p[0] a[0] p[1] a[1] p+i p[i] a[i] a[n-1] Hình 6: Liên hệ giữa con trỏ và mảng Ví dụ: int a[5], *p; p = a ; /* p trỏ tới a[0] */ p++; /* p trỏ tới a[1] */ p[1] = 5; /* a[2] = 5 */ p += 2; /* p trỏ tới a[3] */ *p = 3; /* a[3] = 3 */ p ; /* p trỏ tới a[2] */ Chương trình mẫu (sapxep.c): Sắp xếp một dãy số nguyên được nhập vào từ bàn phím. Giải thuật sắp xếp được viết thành một hàm. #include /* Hàm sắp xếp một dãy số có tham số truyền vào p: là con trỏ đến phần tử đầu tiên của mảng số cần sắp xếp n: là số phần tử của dãy số */ void sapxep(int* p, int n ) { int i, j, tg; for (i=0; i p[j]) { /* đổi chỗ 2 phần tử */ tg = p[i]; p[i] = p[j]; p[j] = tg; } } void main() { int n, i, dayso[20]; do
  47. { printf("So phan tu cua day so (1-20): "); scanf("%d", &n); }while(n 20); for (i=0; i<n; i++) { printf("So thu %d: ", i+1); scanf("%d", &dayso[i]); } /* gọi hàm sắp xếp con trỏ đến phần tử đầu tiên chính là địa chỉ mảng dayso */ sapxep(dayso, n); printf("Day so da duoc sap xep:\n"); for (i=0; i<n; i++) printf("%5d ", dayso[i]); } BÀI TẬP Câu 1: Tìm những lệnh hợp lệ cho khai báo biến dưới đây int a[10], *p, x; a) p = a[0]; b) p = a; c) p[1] = x; d) *(p+2) = x; e) *p+2 = x; f) x = *p++; g) x = (*p)++; Câu 2: Cho một khai báo biến và câu lệnh như sau int a[] = {2, 4, 6, 8}, *p; p = a; Tìm giá trị cho mỗi biểu thức a) *p+2 b) *(p+2) c) 1+p[2] d) (1+p)[2] Câu 3: Tạo các hàm tính tổng, giá trị lớn nhất, giá trị bé nhất và in dãy số tương tự như hàm sắp xếp dãy số trong chương trình mẫu. Sau đó viết chương trình chính sử dụng các hàm đã tạo ra. Bài 19 - Con trỏ hàm Tóm tắt nội dung: Một con trỏ hàm có khả năng chứa địa chỉ mã chương trình chạy của hàm trong bộ nhớ. Thông qua con trỏ hàm chúng ta có thể kích hoạt một hàm chạy mà không phải gọi tên hàm. Người ta sử dụng con trỏ hàm để tạo ra các giải thuật tổng quát. Thời lượng: 1 tiết
  48. Con trỏ ngoài khả năng chứa địa chỉ của một biến còn có thể chứa địa chỉ của một hàm. Khi đó người ta gọi nó là con trỏ hàm. Một con trỏ hàm dùng để trỏ tới một tập các hàm có cùng nguyên mẫu về tham số và kiểu dữ liệu trả về. Sau khi một con trỏ hàm đã được xác lập trỏ đến một hàm cụ thể nào đó thì ta có thể gọi hàm đó thông qua con trỏ hàm. Dạng thức khai báo con trỏ hàm: (* )( ); Ví dụ: /* nguyên mẫu hàm so sánh hai số nguyên */ int sosanh(int, int); /* nguyên mẫu hàm so sánh giá trị tuyệt đối hai số nguyên */ int sosanhtd(int, int); /* con trỏ hàm cho nguyên mẫu giống như hai hàm so sánh */ int (*tro_sosanh) (int, int); /* con trỏ hàm xác lập trỏ đến hàm so sánh thường */ tro_sosanh = sosanh; tro_sosanh(-5, 4); /* gọi hàm qua con trỏ hàm, cho kết quả = -1 */ /* con trỏ hàm xác lập trỏ đến hàm so sánh tuyệt đối */ tro_sosanh = sosanhtd; tro_sosanh(-5, 4); /* gọi hàm qua con trỏ hàm, cho kết quả = 1 */ Cần phân biệt giữa khai báo con trỏ hàm và khai báo nguyên mẫu hàm. Ví dụ: int (*func_ptr)(int, int); /* khai báo một con trỏ hàm */ int * func(int, int); /* khai báo nguyên mẫu cho hàm func */ Chương trình mẫu (troham.c): Sắp xếp một dãy số nguyên nhập vào theo giá trị tăng dần và theo tuyệt đối tăng dần. #include #include /* hàm so sánh 2 số theo giá trị */ int sosanh(int, int); /* hàm so sánh 2 số theo tuyệt đối */ int sosanhtd(int, int); /* sắp xếp dãy số với p: là con trỏ đến phần tử đầu tiên n: là số phần tử cmp: là con trỏ đến hàm so sánh hai số nếu cmp trỏ tới hàm sosanh thì sắp xếp tăng dần theo giá trị nếu cmp trỏ tới hàm sosanhtd thì sắp xếp tăng dần theo tuyệt đối */ void sapxep(int * p, int n, int (*cmp)(int, int) ); void main()
  49. { int n, i, dayso[100]; printf("So phan tu cua day so: "); scanf("%d", &n); for (i=0; i b */ if (a==b) return 0; return a > b ? 1: -1; } int sosanhtd(int a, int b) { /* trả về 0 nếu |a|=|b|, -1 nếu |a| |b| */ if (abs(a)==abs(b)) return 0; return abs(a) > abs(b) ? 1: -1; } void sapxep(int * p, int n, int (*cmp)(int, int) ) { int i, j, tg; for (i=0; i 0) { /* đổi chỗ 2 phần tử */ tg = p[i]; p[i] = p[j]; p[j] = tg; } } Một trong những ứng dụng mạnh của con trỏ hàm là để tạo ra các giải thuật tổng quát trên mọi loại dữ liệu. Trong thư viện có hai hàm qsort() và bsearch() dùng để sắp xếp và tìm kiếm trên một mảng dữ liệu bất kì. void qsort(void *base, size_t num, size_t size, int (*compare)(void const *, void const *)); void *bsearch(const void *key, const void *base, size_t num, size_t size, int (*compare)(const void *, const void *)); Trong đó: - base là địa chỉ của mảng dữ liệu cần sắp xếp hoặc để tìm kiếm - num là số phần tử của mảng
  50. - size là kích thước bộ nhớ của một phần tử - compare là con trỏ đến một hàm so sánh hai phần tử, hàm này nhận tham số vào là địa chỉ của 2 phần tử cần so sánh - key là địa chỉ của một phần tử mang giá trị cần tìm kiếm, kết quả trả về của hàm bsearch() là con trỏ đến phần tử tìm thấy trong mảng Xem thêm bài 28 (Truyền tham biến cho hàm) để biết ý nghĩa sử dụng của từ khoá const đối với các tham số của hàm. Ví dụ: /* hàm so sánh 2 số nguyên theo nguyên mẫu con trỏ hàm của qsort() */ int sosanhso(void const* x, void const*y) { int m, n; m = *(int*)x; /* lấy giá trị của số thứ 1 */ n = *(int*)y; /* lấy giá trị của số thứ 2 */ if (m==n) return 0; return m > n ? m: n; } void main() { int a[20], n; /* nhập dãy số vào mảng */ /* sắp xếp mảng bằng qsort */ qsort(a, n, sizeof(int), sosanhso); } BÀI TẬP Câu 1: Dùng hàm qsort() để viết chương trình sắp xếp một mảng số thực nhập vào theo hai cách: giá trị tăng dần và giá trị tuyệt đối tăng dần. Câu 2: Bằng cách tương tự như hàm sắp xếp tổng quát một dãy số nguyên trong chương trình mẫu, hãy viết một hàm tổng quát để chèn một số nguyên vào một dãy số đã sắp xếp theo một trật tự nào đó. Mục 3.3 - Xâu kí tự Một xâu trong ngôn ngữ C là một mảng các kí tự của xâu trong đó có kí tự đánh dấu kết thúc xâu (kí tự NULL). Người ta truy cập xâu thông qua một con trỏ có địa chỉ là kí tự đầu tiên của mảng kí tự xâu. Các thao tác trên xâu luôn được thực hiện với các hàm xử lí chuyên dụng. Yêu cầu: Đã tìm hiểu khái niệm con trỏ và mảng. Thời lượng: 3 tiết
  51. Bài 20 - Biểu diễn xâu Tóm tắt nội dung: Một xâu được biểu diễn trong mảng các kí tự. Các kí tự trong mảng là các kí tự của xâu, trong đó kí tự cuối cùng luôn là kí tự NULL đánh dấu kết thúc xâu. Người ta truy cập xâu thông qua một con trỏ có địa chỉ là kí tự đầu tiên của mảng kí tự xâu. Một xâu có thể được nhập bằng một trong hai hàm scanf() và gets(). Thời lượng: 1 tiết 1. Biểu diễn xâu trong bộ nhớ Ngôn ngữ C không hỗ trợ kiểu dữ liệu xâu chuẩn. Một xâu trong bộ nhớ của chương trình C được biểu diễn bằng một mảng kí tự. Ngoài các kí tự có trong xâu thì mảng kí tự này còn có thêm một kí tự NULL (mã số 0) ở cuối mảng dùng để dánh dấu điểm kết thúc của xâu. Xâu thường được truy cập thông qua một con trỏ chứa địa chỉ của kí tự đầu tiên của xâu. Các kí tự còn lại trong xâu đều có thể truy cập được qua con trỏ này. Ví dụ: char * s = "DHBK"; /* khai báo xâu với con trỏ */ s ‘D’ s[0] ‘H’ s[1] ‘B’ s[2] ‘K’ s[3] ‘\0’ s[4] Hình 7: Bộ nhớ của khai báo trong ví dụ Chú ý phân biệt giữa một khai báo xâu với con trỏ và khai báo mảng kí tự được khởi tạo giá trị là một xâu kí tự như sau: /* s là một mảng được khởi tạo với 5 kí tự đầu = kí tự trong xâu */ char s[80]="DHBK"; /* dấu nháy đơn không thể hiện xâu mà là mảng kí tự bình thường */ char s[80]='DHBK'; 2. Nhập dữ liệu xâu Để nhập được một xâu vào cho chương trình, ta phải có một mảng kí tự là nơi lưu trữ xâu sẽ nhập. Có hai hàm cho phép nhập xâu là scanf() và gets(). Sự khác nhau của chúng là scanf() không cho nhập dấu cách trong xâu còn gets() thì cho. Chú ý cần phải truyền vào cho các hàm nhập xâu một tham số là địa chỉ bộ nhớ chứa dữ liệu xâu sau khi nhập. Ví dụ: char s[80], len; printf("Nhap xau: "); gets(s); /* xâu được nhập và lưu vào mảng s, tối đa 79 kí tự thật */
  52. /* đếm số kí tự trong xâu */ len = 0; while (s[len]!='\0') len++; printf("Do dai xau nhap: %d", len); Một trong những lỗi thường gặp khi nhập xâu là truyền vào cho hàm nhập xâu một địa chỉ mà chưa cấp phát bộ nhớ. Ví dụ: char *s; scanf("%s", s); /* lỗi chạy vì s chưa trỏ đến một vùng nhớ xác định */ Chương trình mẫu (demtu.c): Đếm số từ của một xâu kí tự nhập vào. #include #include void main() { char s[80]; int i, dem; printf("Nhap xau: "); /* dùng gets() để nhập một xâu có dấu cách */ gets(s); i = 0; dem = 0; /* quét các kí tự cho đến khi kết thúc */ while (s[i]!='\0') /* nếu là kí tự kết thúc từ sau nó là một kí tự cách hoặc kí tự kết thúc xâu */ if (s[i]!=' '&&(s[i+1]==' '||s[i+1]=='\0')) dem++; else i++; printf("So tu cua xau: %d", dem); } BÀI TẬP Câu 1: Hãy viết chương trình đếm số kí tự là chữ số ([0 9]) trong một xâu kí tự được đọc từ bàn phím. Câu 2: Hãy viết chương trình đếm số kí tự là chữ cái ([A Z], [a z]) trong một xâu kí tự được đọc từ bàn phím. Câu 3: Viết chương trình nhập một danh sách tên rồi in cả danh sách ra màn hình. Để có thể lưu được một danh sách các chuỗi kí tự trong bộ nhớ ta sử dụng một mảng của các mảng kí tự (ví dụ, char a[10][25] là mảng 10 phần tử với mỗi phần tử là mảng 25 kí tự).
  53. Bài 21 - Các hàm xử lí xâu Tóm tắt nội dung: Để thực hiện so sánh hay gán xâu trong C, chúng ta luôn phải sử dụng các hàm tương ứng trong thư viện các hàm xử lí xâu ( ). Trong thư viện này còn có nhiều hàm xử lí xâu khác nhau. Thời lượng: 2 tiết Do xâu không phải là một kiểu dữ liệu chuẩn như PASCAL nên mọi thao tác về xâu phải thông qua hàm xử lí chứ không phải thông qua toán tử. Ta lấy ví dụ có hai khai báo xâu như sau: char * s1 = "DHBK"; char * s2 = "DHBK"; Để so sánh nội dung hai xâu ta không thể so sánh theo cách s1==s2. Khi so sánh như vậy thực chất là so sánh hai con trỏ s1 và s2 (tức là so sánh về địa chỉ). Muốn so sánh được nội dung hai xâu ta cần phải so sánh lần lượt từng kí tự trong hai xâu. Để làm được điều này ta có thể sử dụng hàm strcmp() có khai báo nguyên mẫu trong . int strcmp(const char* s1, const char*s2); So sánh nội dung hai xâu s1 và s2 với kết quả trả về là -1 nếu s1 s2. Ví dụ: char * s1 = "DHBK"; char * s2 = "DHBK"; if (strcmp(s1, s2)==0) printf("Hai xau co noi dung giong nhau"); Một số hàm xử lí xâu khác có trong size_t strlen(const char* s); /* trả về chiều dài xâu s */ char* strcpy(char* s1, const char*s2); /* copy nội dung xâu s2 vào s1 */ char* strncpy(char* s1, const char*s2, size_t n); /* chỉ copy n kí tự của s2 vào s1 */ char* strcat(char* s1, const char*s2); /* nối xâu s2 vào cuối s1 */ char* strupr(char*s); /* chuyển xâu s thành chữ hoa */ char* strlwr(char*s); /* chuyển xâu s thành chữ thường */ char* strchr(const char *s, int c); /* tìm kí tự c trong s, trả về con trỏ tìm thấy */ char* strrchr(const char *s, int c); /* tìm kí tự c trong s từ cuối xâu */ char* strstr(const char *s1, const char *s2); /* tìm xâu s2 trong s1, trả về con trỏ tìm thấy */ Ví dụ 1: char s[80]; /* copy nội dung xâu cho một mảng kí tự */ strcpy(s, "DHBK"); /* s = "DHBK" là sai */ /* in thông tin về xâu vừa được copy */ printf("Xau %s co do dai %d", s, strlen(s)); /* màn hình in: Xau DHBK co do dai 4 */
  54. Ví dụ 2: /* tìm số lần xuất hiện của một xâu con trong một xâu lớn */ char *name = "toto titi tata toto toto titi" char* toto = "toto"; /* tìm số lần xuất hiện của toto trong name */ char *s; /* s là con trỏ đến vị trị bắt đầu tìm trong xâu */ int dem; s = name; /* bắt đầu tìm từ đầu xâu */ dem = 0; /* lặp cho đến khi nào còn tìm thấy xâu con toto */ while ( (s=strstr(s, "toto")) !=NULL ) { dem++; /* vị trí bắt đầu tìm mới là sau vị trí vừa tìm thấy */ s += 4; /* chuyển ra sau 4 kí tự của "toto" */ } Chương trình mẫu (xauchuan.c): Nhập một xâu và cắt các dấu cách thừa của xâu đó. #include #include void main() { char s[80]; int i; printf("Nhap xau: "); /* dùng gets() để nhập một xâu có dấu cách */ gets(s); i = 0; /* quét các kí tự cho đến khi kết thúc */ while (s[i]!='\0') /* nếu là dấu cách thì cắt nó khi là kí tự đầu, sau một dấu cách hay là kí tự cuối cùng */ if (s[i]==' '&&(i==0||s[i+1]==' '||s[i+1]=='\0')) strcpy(&s[i], &s[i+1]); else i++; printf("Xau da chuan hoa: %s", s); } Chương trình mẫu (tachten.c): Tách họ và tên của một xâu họ và tên cá nhân nhập vào. #include #include void main() { char hovaten[25], ho[15], ten[10]; char * ptim; int dodai; /* nhập họ và tên mà không có dấu cách thừa */ printf("Ho va ten: "); gets(hovaten); /* tìm kí tự cách cuối xâu */ ptim = strrchr(hovaten, ' ');
  55. if (ptim==NULL) { /* nếu không có dấu cách nào tức là xâu chỉ chứa tên */ strcpy(ten, hovaten); /* họ là rỗng */ strcpy(ho, ""); }else { /* cắt họ và tên, tên là xâu từ sau dấu cách tìm thấy */ strcpy(ten, ptim+1); /* họ là phần đầu xâu mà có số kí tự = ptim-hovaten */ dodai = ptim-hovaten; /* độ dài của phần họ */ strncpy(ho, hovaten, ptim-hovaten); ho[dodai] = '\0'; /* tạo kí tự kết thúc cho họ */ } printf("Ho la: %s\n", ho); printf("Ten la: %s\n", ten); } Các hàm chuyển đổi xâu kí tự thành số trong double atof(const char*s); /* chuyển xâu số thực thành số double */ int atoi(const char*s); /* chuyển xâu số nguyên thành số int */ long atol(const char*s); /* chuyển xâu số nguyên thành số long */ Các hàm chuyển số trả về giá trị 0 trong trường hợp việc chuyển đổi gặp lỗi. Ví dụ: char* s="12345"; int i = atoi(s); if(i==12345) printf("Chuyển xâu thành số đã thành công"); BÀI TẬP Câu 1: Viết chương trình xoá đi tất cả các kí tự là chữ số trong một xâu kí tự được đọc từ bàn phím. Xâu kí tự bị thay đổi xong vẫn nằm ở vị trí bộ nhớ cũ. Câu 2: Hãy viết chương trình đọc một xâu kí tự từ bàn phím và hiển thị trên màn hình theo chiều ngược lại. Câu 3: Viết chương trình in từ đầu tiên của một xâu kí tự bất kì nhập vào. Câu 4: Đọc một xâu bất kì vào từ bàn phím rồi đếm số lần xuất hiện của các từ có 1, 2, 3, kí tự. Ví dụ "a ab b abc bc b" thì số lần xuất hiện của từ 1 kí tự là 3, 2 kí tự là 2, 1 kí tự là 3. Câu 5: Viết một hàm cho phép xoá khỏi xâu những kí tự là một kí tự được truyền vào dưới tham số của hàm. Ví dụ với xâu "abcabdde" khi được yêu cầu xoá kí tự ‘e’ sẽ trở thành "bcbdde". Một cách tương tự tạo một hàm để xoá một xâu con khỏi một xâu kí tự lớn. Câu 6: Hãy viết lại các hàm xử lý xâu kí tự của C: strcpy(), strcat(), strcmp(),
  56. Mục 3.4 - Cấu trúc Trong thực tế một đối tượng dữ liệu có thể được biểu diễn với nhiều trường thông tin khác nhau. Một kiểu dữ liệu phức được xây dựng trên cở sở kết hợp một số dữ liệu thành một nhóm được gọi là kiểu cấu trúc trong C. Một kiểu hợp cũng bao gồm nhiều trường thông tin giống một cấu trúc nhưng dữ liệu các trường này luôn được lưu trữ xếp chồng trên một vùng nhớ. Yêu cầu: Có kiến thức về dữ liệu cơ bản, mảng, con trỏ và xâu. Thời lượng: 2 tiết Bài 22 - Kiểu cấu trúc Tóm tắt nội dung: Cấu trúc là cú pháp nền tảng để xây dựng các cấu trúc dữ liệu phức. Một biến cấu trúc không chỉ chứa một giá trị đơn như kiểu dữ liệu cơ bản mà có thể chứa nhiều giá trị cho nhiều trường dữ liệu khác nhau. Trong bài này học viên sẽ tìm hiểu các cách khai báo và sử dụng cấu trúc trong chương trình C. Thời lượng: 1 tiết 1. Khai báo cấu trúc Một cấu trúc (bản ghi) là dữ liệu tổ hợp từ nhiều trường dữ liệu. Khi định nghĩa một cấu trúc có nghĩa là chúng ta tạo một kiểu dữ liệu mới cho chương trình, ngoài các kiểu dữ liệu chuẩn (char, int, ). Dạng thức khai báo cấu trúc: struct { }; Các trường trong khai báo cấu trúc được khai báo giống như các biến trong chương trình. Ví dụ: struct hocsinh { char ten[25]; /* tên dài nhất là 24 kí tự (trừ đi số 0 cuối) */ int diem; }; Một cấu trúc có thể được dùng để khai báo biến, mảng hay con trỏ như một cấu trúc theo cú pháp sau:
  57. struct ; Ví dụ: struct hocsinh hs1, hs2; struct hocsinh *p; struct hocsinh ds[100]; Chúng ta có thể kết hợp việc khai báo cấu trúc và biến trong một lệnh bằng cách đặt tên biến trước chấm phẩy ‘ ;’. Ví dụ: struct hocsinh { char ten[25]; int diem; } hs; Một biến cấu trúc cũng có thể được khai báo có khởi tạo giá trị bằng cách đặt giá trị các trường trong cặp dấu {}. Ví dụ: struct hocsinh hs = {"Nguyen Van A", 9}; 2. Định nghĩa kiểu dữ liệu bằng typedef Trong C cho phép định nghĩa một tên kiểu dữ liệu mới cho một cấu trúc dữ liệu của người sử dụng bằng từ khoá typedef theo cú pháp dưới đây: typedef ; Ví dụ: /* định nghĩa kiểu byte nhớ */ typedef unsigned char byte; /* định nghĩa kiểu dữ liệu con trỏ */ typedef int* intptr; /* định nghĩa kiểu dữ liệu cấu trúc */ typedef struct { char ten[25]; int diem; } hocsinh_t; /* khai báo các biến với kiểu dữ liệu mới */ byte i, j; /* i, j là số nguyên 1 byte */ intptr p, q; /* p, q là con trỏ int* */ hocsinh_t hs; /* hs là biến kiểu cấu trúc */ 3. Truy cập biến cấu trúc Truy cập trường của một biến cấu trúc bằng cú pháp: .
  58. -> Ví dụ: struct hocsinh { char ten[25]; int diem; }; struct hocsinh hs, *p; p = &hs; strcpy(hs.ten, "Nguyen Van A"); hs.diem = 9; printf("%s co diem %d", p->ten, p->diem); /* ket qua in la: Nguyen Van A co diem 9 */ Chương trình mẫu (hocsinh.c): Nhập một danh sách học sinh với hai trường thông tin tên và điểm kiểm tra sau đó đưa ra tên những học sinh phải kiểm tra lại. #include #include typedef struct { char ten[25]; int diem; } hocsinh_t; void main() { hocsinh_t ds[100]; int n, i; printf("So hoc sinh: "); scanf("%d", &n); for (i=0; i<n; i++) { printf("Hoc sinh thu %d\n", i+1); /* trước khi nhập xâu cần làm sạch đầu vào dữ liệu */ fflush(stdin); printf("Ten: "); gets(ds[i].ten); printf("Diem: "); scanf("%d", &ds[i].diem); } printf("Danh sach nhung hoc sinh phai kiem tra lai:\n"); for (i=0; i<n; i++) if (ds[i].diem < 5) printf("%s\n", ds[i].ten); } 4. Cấu trúc và hàm Một hàm trong ngôn ngữ C có thể nhận cấu trúc làm tham số truyền vào và trả về kết quả là một cấu trúc. Ví dụ chúng ta có thể tạo một hàm tính tổng hai số phức với mỗi số phức được truyền vào dưới dạng cấu trúc. Ví dụ: /* khai báo cấu trúc số phức */ struct complex { float r, i;
  59. }; /* hàm tính tổng hai số phức */ struct complex sum(struct complex a, struct complex b) { struct complex c; c.r = a.r + b.r; c.i = a.i + b.i; return c; } BÀI TẬP Câu 1: Viết một chương trình quản lí hồ sơ cá nhân bao gồm các thông tin như tên, năm sinh, giới tính, địa chỉ, Câu 2: Một phân số được biểu diễn bằng một cấu trúc gồm hai trường tử số và mẫu số. Xây dựng các hàm để tính một phân số rút gọn, tổng, hiệu, tích, thương của hai phân số. Viết một chương trình để thử nghiệm các hàm đã xây dựng. Câu 3: Một điểm là một cấu trúc thể hiện toạ độ của nó. Một tam giác là một cấu trúc biểu diễn 3 điểm của nó. Hãy viết chương trình để tính diện tích một tam giác bất kì được nhập vào. Bài 23 - Kiểu hợp và liệt kê Tóm tắt nội dung: Biến kiểu hợp cũng bao gồm nhiều trường thông tin như một cấu trúc. Nhưng các trường trong biến kiểu hợp luôn lưu trên một vùng nhớ chung, trong khi của cấu trúc thì trên các vùng nhớ khác nhau. Kiểu liệt kê cho phép tạo một biến lấy giá trị trong một tập hằng số cho trước. Thời lượng: 1 tiết 1. Kiểu hợp Trong một cấu trúc các trường là độc lập với nhau trong bộ nhớ. Với mỗi trường dữ liệu sẽ có một vùng nhớ tương ứng với nó khi khai báo một biến cấu trúc. Tuy nhiên trong một số trường hợp trường dữ liệu là sự lựa chọn một trong những trường đó trong quá trình sử dụng. Ví dụ với một vùng nhớ 2 byte ta có thể sử dụng nó như một số nguyên int hoặc hai char liên tiếp. Khi đó ta có thể định nghĩa một kiểu hợp giống như một cấu trúc nhưng với từ khoá union như ví dụ sau. Ví dụ: /* khai báo union với trường là 1 int hoặc 2 char*/ union ic { int i; char c[2]; };
  60. /* khai báo một biến union */ union ic u; /* sử dụng union, xác lập giá trị qua trường int */ u.i = 0x1122; /* khi đó trường c cũng đã được xác lập giá trị vì nó cùng một vùng nhớ của trường i */ printf("%x %x", u.c[0], u.c[1]); /* 22 11 */ Việc sử dụng union rất phổ dụng khi ta định nghĩa một cấu trúc nhớ mà có thể diễn giải theo nhiều cách khác nhau. Ví dụ như trên với 2 byte nhớ ta có thể diễn giải nó là một int hoặc hai char. 2. Kiểu liệt kê Kiểu liệt kê chứa một danh sách các hằng số nguyên mà một biến kiểu này có thể nhận giá trị. Các hằng số này được đánh giá trị mặc định bắt đầu từ 0. Dạng thức khai báo cấu trúc liệt kê: enum { }; Ví dụ: enum days {mon, tue, wed, thu, fri, sat, sun}; /* mon=0, tue=1, wed=2, */ enum days today; /* khai báo biến với cấu trúc liệt kê */ today = mon; /* today mang giá trị 0 */ Chúng ta cũng có thể chỉ rõ giá trị tường minh cho các hằng số trong danh sách liệt kê. Ví dụ: /* mỗi hằng số có một giá trị tường minh */ enum escapes { bell = '\a', backspace = '\b', tab = '\t', newline = `\n', vtab = '\v', return = '\r'}; /* các hằng số được đánh từ 1 thay vì 0 */ enum months {jan = 1, feb, mar, , dec}; Cũng giống như cấu trúc chúng ta có thể sử dụng typedef để định nghĩa một kiểu mới cho cấu trúc liệt kê hay hợp. Ví dụ: /* định nghĩa kiểu liệt kê */ typedef enum {mon, tue, wed, thu, fri, sat, sun} weekday_t; weekday_t today = mon;
  61. CHƯƠNG 4 - CÁC VẤN ĐỀ VỀ BỘ NHỚ CHƯƠNG TRÌNH Các chương trình dù được viết dưới ngôn ngữ bậc cao nào cũng có một nguyên lí chung về bộ nhớ. Chương này giới thiệu tới học viên sự khác nhau của các loại bộ nhớ tĩnh, động và stack. Nắm bắt được các kiến thức về bộ nhớ này giúp người lập trình tránh được các lỗi liên quan đến bộ nhớ chương trình. Yêu cầu: Đã tìm hiểu kiến thức về con trỏ và hàm trong C. Thời lượng: 8 tiết Mục 4.1 - Bộ nhớ động Bộ nhớ động là vùng nhớ được cấp phát với kích thước theo nhu cầu của người sử dụng và giải phóng khi không còn được dùng nữa. Ưu điểm của bộ nhớ động là có thể tạo ra đủ bộ nhớ cần thiết và không lãng phí cho nhu cầu của từng công việc. Bộ nhớ động thường được ứng dụng để xây dựng các cấu trúc dữ liệu động như danh sách móc nối, cây tìm kiếm, Yêu cầu: Có kiến thức về con trỏ trong C và cấu trúc dữ liệu danh sách móc nối. Thời lượng: 3 tiết Bài 24 - Cấp phát bộ nhớ động Tóm tắt nội dung: Bộ nhớ được cấp phát động trong C bằng các hàm malloc() và calloc(). Chúng trả về kết quả là địa chỉ đến vùng nhớ đã được cấp phát. Để giải phóng nó ta dùng hàm free(). Thời lượng: 1 tiết Khai báo một mảng tĩnh có một nhược điểm là số phần tử của mảng là cố định và phải là hằng số. Điều này thường dẫn đến việc thừa hoặc thiếu bộ nhớ cho việc nhập dữ liệu khi chạy chương trình. Thực tế chúng ta cần một mảng dữ liệu mà số phần tử chỉ được xác định được khi chạy chương trình. Người ta gọi mảng như vậy là mảng động (hay còn gọi dữ liệu động). Mảng động phải được cấp phát trên vùng nhớ động (heap) bằng hàm malloc() có khai báo nguyên mẫu trong tệp tiêu đề . void *malloc(size_t n); Kết quả trả về của hàm malloc() là địa chỉ của vùng nhớ có kích thước n bytes được cấp phát theo yêu cầu. Kết quả có thể trả về là NULL khi không còn đủ vùng nhớ trống trên heap để cấp phát. Để sử dụng vùng nhớ đã được cấp phát vào những mục đích nhất định thì ta có thể ép
  62. kiểu con trỏ void* này thành một con trỏ có định kiểu. Ví dụ nếu vùng nhớ cấp phát dùng làm mảng số int thì ta ép kiểu con trỏ bộ nhớ động về int*. Ví dụ: int *p, n, i; printf("n = "); scanf("%d", &n); /* cấp phát mảng động n số int bằng malloc */ p = (int*) malloc(n*sizeof(int)); /* p là con trỏ đến phần tử đầu mảng */ /* khởi tạo giá trị các phần tử = 0 */ for(i=0; i ) có khai báo nguyên mẫu trong cho phép lấy kích thước của một kiểu dữ liệu. Một bộ nhớ động sau khi sử dụng xong cần phải được giải phóng bằng hàm free( ). Ngoài hàm cấp phát nhớ động malloc() còn có hai hàm khác cũng có thể được dùng làm nhiệm vụ này: void *calloc(size_t num, size_t size); void *realloc(void *ptr, size_t n); Hàm calloc() cho phép cấp phát một vùng nhớ động cho num phần tử, mỗi phần tử có kích thước là size. Còn hàm realloc() dùng để cấp phát lại một vùng nhớ tại địa chỉ ptr với kích thước mới n bytes. Ví dụ: int *p, n; /* cấp phát mảng động n số int bằng calloc */ p = (int*) calloc(n, sizeof(int)); Chương trình mẫu (mangdong.c): Dùng bộ nhớ động để nhập n số nguyên và tính giá trị lớn nhất của chúng. #include #include #include void main() { int n, i, max, *a; printf("n = "); scanf("%d", &n); /* cấp phát bộ nhớ động cho mảng kích thước cấp phát là n*sizeof(int) */ a = (int*)malloc(n*sizeof(int)); for (i=0; i<n; i++) { printf("So thu %d: ", i+1); scanf("%d", &a[i]); } max = a[0]; for (i=1; i<n; i++) if(max<a[i]) max = a[i]; printf("Max = %d", max); /* giải phóng bộ nhớ động sau khi sử dụng xong */
  63. free(a); } BÀI TẬP Câu 1: Một điểm được biểu diễn bằng một cấu trúc thể hiện toạ độ của nó trên màn hình. Viết chương trình cho phép đọc số điểm rồi tạo một mảng có kích thước đủ để lưu toạ độ cho số điểm đó. Tìm và in ra toạ độ hai điểm gần nhau nhất. Câu 2: Đọc vào hai dãy số nguyên và tạo ra một mảng có kích thước đủ để trộn hai dãy số thành một dãy số có sắp xếp. Bài 25 - Danh sách móc nối Tóm tắt nội dung: Sử dụng bộ nhớ động ta có thể xây dựng các cấu trúc danh sách móc nối. Bày này trình bày cách thức và một số ví dụ minh hoạ cho việc tạo dựng danh móc nối trong C. Thời lượng: 2 tiết Một trong những ứng dụng phổ biến của bộ nhớ động là để tạo danh sách móc nối. Một danh sách móc nối được tạo ra bởi các nút kiểu cấu trúc với hai phần: nội dung thông tin của các nút trong danh sách và con trỏ liên kết tới nút lân cận trong danh sách. Ví dụ sau đây là một mẫu cấu trúc cho một danh sách móc nối đơn. struct { struct * tiep; }; hoặc typedef struct { * tiep; } ; Ví dụ: struct nut_hs { char ten[25]; int diem; struct nut_hs* tiep; }; hoặc typedef struct {
  64. char ten[25]; int diem; nuths_t* tiep; }nuths_t; Cấu trúc danh sách được gọi là động bởi số lượng phần tử của danh sách là không cố định. Khi có thêm một phần tử mới ta chỉ việc cấp phát thêm một nút (dùng malloc) và nối nó vào một vị trí trong danh sách. Một nút khi bị xoá khỏi danh sách thì nó sẽ được giải phóng khỏi bộ nhớ (dùng free). Việc truy cập danh sách thường được thực hiện thông qua một con trỏ tới nút đầu tiên trong danh sách. Xem thêm chương trình mẫu minh hoạ dưới đây. Chương trình mẫu (mocnoi.c): Quản lí danh sách học sinh lớp học bằng danh sách móc nối. #include #include #include struct nut_hs { char ten[25]; int diem; struct nut_hs* tiep; }; /* khai báo biến tổng thể là con trỏ đến nút đầu danh sách */ struct nut_hs *nutdauds = NULL; /* ban đầu danh sách rỗng */ /* thêm một nút học sinh mới có tên (ten) và điểm (diem) vào danh sách */ void them_nuths(char* ten, int diem); /* xoá một nút học sinh có tên là ten */ void xoa_nuths(char *ten); /* in danh sách học sinh */ void in_ds(); void main() { char ten[25]; int diem; int chon; /* tạo menu lựa chọn công việc */ do { printf("1. Nhap hoc sinh\n"); printf("2. Xoa hoc sinh\n"); printf("3. In danh sach hoc sinh\n"); printf("0. Thoat\n"); printf("Lua chon: "); scanf("%d", &chon); switch(chon) { case 1: fflush(stdin); /* xoá vùng đệm stdin để nhập xâu tên */ printf("Ten: "); gets(ten); if (strcmp(ten, "")!=0) { printf("Diem: "); scanf("%d", &diem); them_nuths(ten, diem);
  65. } break; case 2: fflush(stdin); /* xoá vùng đệm stdin để nhập xâu */ printf("Ten hoc sinh can xoa: "); gets(ten); xoa_nuths(ten); break; case 3: in_ds(); } }while(chon !=0); } void them_nuths(char* ten, int diem) { struct nut_hs * hs; /* tạo nút mới và copy dữ liệu */ hs = (struct nut_hs*)malloc(sizeof(struct nut_hs)); strcpy(hs->ten, ten); hs->diem = diem; /* nối nút vào danh sách, đưa vào đầu danh sách */ hs->tiep = nutdauds; nutdauds = hs; } void xoa_nuths(char *ten) { struct nut_hs * hs, *p; /* tìm nút cần xoá */ hs = nutdauds; p = NULL; /* p trỏ tới nút trước nút cần xoá */ while(hs !=NULL&&strcmp(hs->ten, ten) !=0) { p = hs; /* xác lập lại cho p trỏ tới trước hs */ hs = hs->tiep; } if (hs !=NULL) /* nếu tìm thấy nút cần xoá */ { /* kiểm tra có nút đứng trước nút cần xoá không */ if (p !=NULL) p->tiep = hs->tiep; else /* nút đầu danh sách sẽ là nút sau nút cần xoá */ nutdauds = hs->tiep; free(hs); } } void in_ds() { struct nut_hs * hs; printf("Danh sach ten va diem hoc sinh:\n"); /* duyệt danh sách bằng vòng for */ for(hs=nutdauds; hs!=NULL; hs=hs->tiep) printf("%s: %d\n", hs->ten, hs->diem); } Một danh sách móc nối có thể dùng làm ngăn xếp hay hàng đợi khi chúng ta thêm hoặc xoá phần tử trong danh sách theo nguyên tắc của ngăn xếp hay hàng đợi. Ví dụ nếu ta luôn thêm một
  66. phần tử vào đầu danh sách và khi xoá cũng chỉ xoá phần tử ở đầu danh sách thì ta sẽ được một ngăn xếp (vào sau ra trước). Sau đây là một chương trình mẫu minh hoạ sử dụng ngăn xếp với danh sách móc nối. Chương trình mẫu (nhiphan.c): In số nhị phân cho một số nguyên bất kì. #include #include #include struct nut { char bit; /* trường thông tin biểu diễn bit nhị phân */ struct nut* tiep; }; /* con trỏ tới nút đầu tiên của danh sách làm stack */ struct nut* stack = NULL; /* thêm một bit vào ngăn xếp */ void push(char bit) { struct nut * p = (struct nut *)malloc(sizeof(struct nut)); p->bit = bit; /* thêm vào đầu ngăn xếp */ p->tiep = stack; stack = p; } /* lấy một phần tử khỏi ngãn xếp chỉ lấy khi còn phần tử trong ngăn xếp (stack != NULL) */ char pop() { struct nut* p = stack; char bit = stack->bit; /* bit trả ra là ở đầu ngăn xếp */ stack = stack->tiep; /* xác lập lại nút đầu danh sách */ free(p); /* huỷ bỏ vùng nhớ không dùng nữa */ return bit; } void main() { int n; printf("Nhap so nguyen duong: "); scanf("%d", &n); /* phân tích số nhị phân */ do { push((char)(n%2)); n /= 2; }while (n!=0); /* in ra số nhị phân */ printf("So nhi phan la: "); /* lấy trong ngăn xếp ra để in khi ngăn xếp chưa rỗng */ while (stack!=NULL) printf("%d", pop()); }