Bài giảng Lập trình hướng đối tượng - Võ Đức Lân

pdf 149 trang huongle 80
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Lập trình hướng đối tượng - Võ Đức Lân", để 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_lap_trinh_huong_doi_tuong_vo_duc_lan.pdf

Nội dung text: Bài giảng Lập trình hướng đối tượng - Võ Đức Lân

  1. TRƯỜNG ĐẠI HỌC PHẠM VĂN ĐỒNG KHOA CÔNG NGHỆ THÔNG TIN VÕ ĐỨC LÂN BÀI GIẢNG LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG (Dùng cho sinh viên các lớp đại học Công nghệ thông tin, Sư phạm tin) Quảng Ngãi, 06 - 2017
  2. TRƯỜNG ĐẠI HỌC PHẠM VĂN ĐỒNG KHOA CÔNG NGHỆ THÔNG TIN VÕ ĐỨC LÂN BÀI GIẢNG LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG (Dùng cho sinh viên các lớp đại học Công nghệ thông tin, Sư phạm tin) Lư hành nội bộ
  3. LỜI NÓI ĐẦU Lập trình cấu trúc là phương pháp tổ chức, phân chia chương trình thành các hàm, thủ tục, chúng được dùng để xử lý dữ liệu nhưng lại tách rời các cấu trúc dữ liệu. Thông qua các ngôn ngữ Foxpro, Pascal, C đa số những người làm Tin học đã khá quen biết với phương pháp lập trình này. Lập trình hướng đối tượng dựa trên việc tổ chức chương trình thành các lớp. Khác với hàm và thủ tục, lớp là một đơn vị bao gồm cả dữ liệu và các phương thức xử lý. Vì vậy lớp có thể mô tả các thực thể một cách chân thực, đầy đủ cả phần dữ liệu và yêu cầu quản lý. Tư tưởng lập trình hướng đối tượng được áp dụng cho hầu hết các ngôn ngữ mới chạy trên môi trường Windows như Microsoft Access, Visual Basic, Visual C. Vì vậy việc nghiên cứu phương pháp lập trình mới này là rất cần thiết đối với tất cả những người quan tâm, yêu thích Tin học. Bài giảng này sẽ trình bày một cách hệ thống các khái niệm của lập trình hướng đối tượng được cài đặt trong C++ như lớp, đối tượng, sự thừa kế, tính tương ứng bội và các khả năng mới trong xây dựng, sử dụng hàm như: đối tham chiếu, đối mặc định, hàm trùng tên, hàm toán tử, hàm bạn. Các ví dụ và bài tập thực hành được viết code trên môi trường Dev-C++. Bài giảng được thiết kế dành cho sinh viên đại học các ngành Công nghệ thông tin và Sư phạm tin học. Nội dung được xây dựng theo đúng chương trình chi tiết của học phần môn Lập trình hướng đối tượng đã được ban hành. Hy vọng bài giảng này sẽ là tài liệu bổ ích dành cho sinh viên ngành Công nghệ thông tin và Sư phạm tin học của trường Đại học Phạm Văn Đồng. Tuy nhiên do hạn chế về thời gian nên bài giảng chắc chắn còn nhiều thiếu sót. Mong nhận được nhiều ý kiến đóng góp từ các bạn đọc, đồng nghiệp và sinh viên. Tác giả Võ Đức Lân
  4. CHƯƠNG 1: CÁC KHÁI NIỆM CƠ SỞ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG Chương 1 trình bày những vấn đề sau:  Thảo luận về cách tiếp cận hướng đối tượng.  Các khái niệm cơ sở của phương pháp hướng đối tượng.  Các bước cần thiết để thiết kế chương trình theo hướng đối tượng.  Các ưu điểm của lập trình hướng đối tượng.  Các ngôn ngữ hướng đối tượng.  Một số ứng dụng của lập trình hướng đối tượng. 1.1. Giới thiệu Lập trình hướng đối tượng (Object-Oriented Programming, viết tắt là OOP) là một phương pháp mới trên bước đường tiến hóa của việc lập trình máy tính, nhằm làm cho chương trình trở nên linh hoạt, tin cậy và dễ phát triển. Tuy nhiên để hiểu được OOP là gì, chúng ta hãy bắt đầu từ lịch sử của quá trình lập trình – xem xét OOP đã tiến hóa như thế nào. 1.1.1. Lập trình tuyến tính Máy tính đầu tiên được lập trình bằng mã nhị phân, sử dụng các công tắt cơ khí để nạp chương trình. Cùng với sự xuất hiện của các thiết bị lưu trữ lớn và bộ nhớ máy tính có dung lượng lớn nên các ngôn ngữ lập trình cấp cao đầu tiên được đưa vào sử dụng . Thay vì phải suy nghĩ trên một dãy các bit và byte, lập trình viên có thể viết một loạt lệnh gần với tiếng Anh và sau đó chương trình dịch thành ngôn ngữ máy. Các ngôn ngữ lập trình cấp cao đầu tiên được thiết kế để lập các chương trình làm các công việc tương đối đơn giản như tính toán. Các chương trình ban đầu chủ yếu liên quan đến tính toán và không đòi hỏi gì nhiều ở ngôn ngữ lập trình. Hơn nữa phần lớn các chương trình này tương đối ngắn, thường ít hơn 100 dòng. Khi khả năng của máy tính tăng lên thì khả năng để triển khai các chương trình phức tạp hơn cũng tăng lên. Các ngôn ngữ lập trình ngày trước không còn thích hợp đối với việc lập trình đòi hỏi cao hơn. Các phương tiện cần thiết để sử dụng lại các phần mã chương trình đã viết hầu như không Trang 1
  5. có trong ngôn ngữ lập trình tuyến tính. Thật ra, một đoạn lệnh thường phải được chép lặp lại mỗi khi chúng ta dùng trong nhiều chương trình do đó chương trình dài dòng, logic của chương trình khó hiểu. Chương trình được điều khiển để nhảy đến nhiều chỗ mà thường không có sự giải thích rõ ràng, làm thế nào để chương trình đến chỗ cần thiết hoặc tại sao như vậy. Ngôn ngữ lập trình tuyến tính không có khả năng kiểm soát phạm vi nhìn thấy của các dữ liệu. Mọi dữ liệu trong chương trình đều là dữ liệu toàn cục nghĩa là chúng có thể bị sửa đổi ở bất kỳ phần nào của chương trình. Việc dò tìm các thay đổi không mong muốn đó của các phần tử dữ liệu trong một dãy mã lệnh dài và vòng vèo đã từng làm cho các lập trình viên rất mất thời gian. 1.1.2. Lập trình cấu trúc Rõ ràng là các ngôn ngữ mới với các tính năng mới cần phải được phát triển để có thể tạo ra các ứng dụng tinh vi hơn. Vào cuối các năm trong 1960 và 1970, ngôn ngữ lập trình có cấu trúc ra đời. Các chương trình có cấu trúc được tổ chức theo các công việc mà chúng thực hiện. Về bản chất, chương trình chia nhỏ thành các chương trình con riêng rẽ (còn gọi là hàm hay thủ tục) thực hiện các công việc rời rạc trong quá trình lớn hơn, phức tạp hơn. Các hàm này được giữ càng độc lập với nhau càng nhiều càng tốt, mỗi hàm có dữ liệu và logic riêng.Thông tin được chuyển giao giữa các hàm thông qua các tham số, các hàm có thể có các biến cục bộ mà không một ai nằm bên ngoài phạm vi của hàm lại có thể truy xuất được chúng. Như vậy, các hàm có thể được xem là các chương trình con được đặt chung với nhau để xây dựng nên một ứng dụng. Mục tiêu là làm sao cho việc triển khai các phần mềm dễ dàng hơn đối với các lập trình viên mà vẫn cải thiện được tính tin cậy và dễ bảo quản chương trình. Một chương trình có cấu trúc được hình thành bằng cách bẻ gãy các chức năng cơ bản của chương trình thành các mảnh nhỏ mà sau đó trở thành các hàm. Bằng cách cô lập các công việc vào trong các hàm, chương trình có cấu trúc có thể làm giảm khả năng của một hàm này ảnh hưởng đến một hàm khác. Việc này cũng làm cho việc tách các vấn đề trở nên dễ dàng hơn. Sự gói gọn này cho phép chúng ta có thể viết các chương trình sáng sủa hơn và giữ được điều khiển trên từng hàm. Các biến toàn cục không còn nữa và được thay Trang 2
  6. thế bằng các tham số và biến cục bộ có phạm vi nhỏ hơn và dễ kiểm soát hơn. Cách tổ chức tốt hơn này nói lên rằng chúng ta có khả năng quản lý logic của cấu trúc chương trình, làm cho việc triển khai và bảo dưỡng chương trình nhanh hơn và hữu hiện hơn và hiệu quả hơn. Một khái niệm lớn đã được đưa ra trong lập trình có cấu trúc là sự trừu tượng hóa (Abstraction). Sự trừu tượng hóa có thể xem như khả năng quan sát một sự việc mà không cần xem xét đến các chi tiết bên trong của nó. Trong một chương trình có cấu trúc, chúng ta chỉ cần biết một hàm đã cho có thể làm được một công việc cụ thể gì là đủ. Còn làm thế nào mà công việc đó lại thực hiện được là không quan trọng, chừng nào hàm còn tin cậy được thì còn có thể dùng nó mà không cần phải biết nó thực hiện đúng đắn chức năng của mình như thế nào. Điều này gọi là sự trừu tượng hóa theo chức năng (Functional abstraction) và là nền tảng của lập trình có cấu trúc. Ngày nay, các kỹ thuật thiết kế và lập trình có cấu trúc được sử rộng rãi. Gần như mọi ngôn ngữ lập trình đều có các phương tiện cần thiết để cho phép lập trình có cấu trúc. Chương trình có cấu trúc dễ viết, dễ bảo dưỡng hơn các chương trình không cấu trúc. Sự nâng cấp như vậy cho các kiểu dữ liệu trong các ứng dụng mà các lập trình viên đang viết cũng đang tiếp tục diễn ra. Khi độ phức tạp của một chương trình tăng lên, sự phụ thuộc của nó vào các kiểu dữ liệu cơ bản mà nó xử lý cũng tăng theo. Vấn đề trở rõ ràng là cấu trúc dữ liệu trong chương trình quan trọng chẳng kém gì các phép toán thực hiện trên chúng. Điều này càng trở rõ ràng hơn khi kích thước của chương trình càng tăng. Các kiểu dữ liệu được xử lý trong nhiều hàm khác nhau bên trong một chương trình có cấu trúc. Khi có sự thay đổi trong các dữ liệu này thì cũng cần phải thực hiện cả các thay đổi ở mọi nơi có các thao tác tác động trên chúng. Đây có thể là một công việc tốn thời gian và kém hiệu quả đối với các chương trình có hàng ngàn dòng lệnh và hàng trăm hàm trở lên. Một yếu điểm nữa của việc lập trình có cấu trúc là khi có nhiều lập trình viên làm việc theo nhóm cùng một ứng dụng nào đó. Trong một chương trình có cấu trúc, các lập trình viên được phân công viết một tập hợp các hàm và các kiểu dữ liệu. Vì có nhiều lập trình viên khác nhau quản lý các hàm riêng, có liên quan đến các Trang 3
  7. kiểu dữ liệu dùng chung nên các thay đổi mà lập trình viên tạo ra trên một phần tử dữ liệu sẽ làm ảnh hưởng đến công việc của tất cả các người còn lại trong nhóm. Mặc dù trong bối cảnh làm việc theo nhóm, việc viết các chương trình có cấu trúc thì dễ dàng hơn nhưng sai sót trong việc trao đổi thông tin giữa các thành viên trong nhóm có thể dẫn tới hậu quả là mất rất nhiều thời gian để sửa chữa chương trình. 1.1.3. Trừu tượng hóa dữ liệu Sự trừu tượng hóa dữ liệu (Data abstraction) tác động trên các dữ liệu cũng tương tự như sự trừu tượng hóa theo chức năng. Khi có trừu tượng hóa dữ liệu, các cấu trúc dữ liệu và các phần tử có thể được sử dụng mà không cần bận tâm đến các chi tiết cụ thể. Chẳng hạn như các số dấu chấm động đã được trừu tượng hóa trong tất cả các ngôn ngữ lập trình, Chúng ta không cần quan tâm cách biểu diễn nhị phân chính xác nào cho số dấu chấm động khi gán một giá trị, cũng không cần biết tính bất thường của phép nhân nhị phân khi nhân các giá trị dấu chấm động. Điều quan trọng là các số dấu chấm động hoạt động đúng đắn và hiểu được. Sự trừu tượng hóa dữ liệu giúp chúng ta không phải bận tâm về các chi tiết không cần thiết. Nếu lập trình viên phải hiểu biết về tất cả các khía cạnh của vấn đề, ở mọi lúc và về tất cả các hàm của chương trình thì chỉ ít hàm mới được viết ra, may mắn thay trừu tượng hóa theo dữ liệu đã tồn tại sẵn trong mọi ngôn ngữ lập trình đối với các dữ liệu phức tạp như số dấu chấm động. Tuy nhiên chỉ mới gần đây, người ta mới phát triển các ngôn ngữ cho phép chúng ta định nghĩa các kiểu dữ liệu trừu tượng riêng. 1.1.4. Lập trình hướng đối tượng Khái niệm lập trình hướng đối tượng (LTHĐT) được xây dựng trên nền tảng của khái niệm lập trình có cấu trúc và sự trừu tượng hóa dữ liệu. Sự thay đổi căn bản ở chỗ, một chương trình hướng đối tượng được thiết kế xoay quanh dữ liệu mà chúng ta có thể làm việc trên đó, hơn là theo bản thân chức năng của chương trình. Điều này hoàn toàn tự nhiên một khi chúng ta hiểu rằng mục tiêu của chương trình là xử lý dữ liệu. Suy cho cùng, công việc mà máy tính thực hiện vẫn thường được gọi là xử lý dữ Trang 4
  8. liệu. Dữ liệu và thao tác liên kết với nhau ở một mức cơ bản (còn có thể gọi là mức thấp), mỗi thứ đều đòi hỏi ở thứ kia có mục tiêu cụ thể, các chương trình hướng đối tượng làm tường minh mối quan hệ này. Lập trình hướng đối tượng (Object Oriented Programming - gọi tắt là OOP) hay chi tiết hơn là Lập trình định hướng đối tượng, chính là phương pháp lập trình lấy đối tượng làm nền tảng để xây dựng thuật giải, xây dựng chương trình. Thực chất đây không phải là một phương pháp mới mà là một cách nhìn mới trong việc lập trình. Để phân biệt, với phương pháp lập trình theo kiểu cấu trúc mà chúng ta quen thuộc trước đây, hay còn gọi là phương pháp lập trình hướng thủ tục (Procedure-Oriented Programming), người lập trình phân tích một nhiệm vụ lớn thành nhiều công việc nhỏ hơn, sau đó dần dần chi tiết, cụ thể hoá để được các vấn đề đơn giản, để tìm ra cách giải quyết vấn đề dưới dạng những thuật giải cụ thể rõ ràng qua đó dễ dàng minh hoạ bằng ngôn ngữ giải thuật (hay còn gọi các thuật giải này là các chương trình con). Cách thức phân tích và thiết kế như vậy chúng ta gọi là nguyên lý lập trình từ trên xuống (top-down), để thể hiện quá trình suy diễn từ cái chung cho đến cái cụ thể. Các chương trình con là những chức năng độc lập, sự ghép nối chúng lại với nhau cho chúng ta một hệ thống chương trình để giải quyết vấn đề đặt ra. Chính vì vậy, cách thức phân tích một hệ thống lấy chương trình con làm nền tảng, chương trình con đóng vai trò trung tâm của việc lập trình, được hiểu như phương pháp lập trình hướg về thủ tục. Tuy nhiên, khi phân tích để thiết kế một hệ thống không nhất thiết phải luôn luôn suy nghĩ theo hướng “làm thế nào để giải quyết công việc”, chúng ta có thể định hướng tư duy theo phong cách “với một số đối tượng đã có, phải làm gì để giải quyết được công việc đặt ra” hoặc phong phú hơn, “làm cái gì với một số đối tượng đã có đó”, từ đó cũng có thể giải quyết được những công việc cụ thể. Với phương pháp phân tích trong đó đối tượng đóng vai trò trùng tâm của việc lập trình như vậy, người ta gọi là nguyên lý lập trình từ dưới lên (Bottom-up). Lập trình hướng đối tượng liên kết cấu trúc dữ liệu với các thao tác, theo cách mà tất cả thường nghĩ về thế giới quanh mình. Chúng ta thường gắn một số các hoạt động cụ thể với một loại hoạt động nào đó và đặt các giả thiết của mình trên các quan hệ đó. Trang 5
  9. Ví dụ: Chúng ta biết rằng một chiếc xe có các bánh xe, di chuyển được và có thể đổi hướng của nó bằng cách quẹo tay lái. Tương tự như thế, một cái cây là một loại thực vật có thân gỗ và lá. Một chiếc xe không phải là một cái cây, mà cái cây không phải là một chiếc xe, chúng ta có thể giả thiết rằng cái mà chúng ta có thể làm được với một chiếc xe thì không thể làm được với một cái cây. Chẳng hạn, thật là vô nghĩa khi muốn lái một cái cây, còn chiếc xe thì lại chẳng lớn thêm được khi chúng ta tưới nước cho nó. Lập trình hướng đối tượng cho phép chúng ta sử dụng các quá trình suy nghĩ như vậy với các khái niệm trừu tượng được sử dụng trong các chương trình máy tính. Một mẫu tin (record) nhân sự có thể được đọc ra, thay đổi và lưu trữ lại; còn số phức thì có thể được dùng trong các tính toán. Tuy vậy không thể nào lại viết một số phức vào tập tin làm mẫu tin nhân sự và ngược lại hai mẫu tin nhân sự lại không thể cộng với nhau được. Một chương trình hướng đối tượng sẽ xác định đặc điểm và hành vi cụ thể của các kiểu dữ liệu, điều đó cho phép chúng ta biết một cách chính xác rằng chúng ta có thể có được những gì ở các kiểu dữ liệu khác nhau. Điểm căn bản của phương pháp LTHĐT là thiết kế chương trình xoay quanh dữ liệu của hệ thống. Nghĩa là các thao tác xử lý của hệ thống được gắn liền với dữ liệu và như vậy khi có sự thay đổi của cấu trúc dữ liệu thì chỉ ảnh hưởng đến một số ít các phương thức xử lý liên quan. LTHĐT không cho phép dữ liệu chuyển động tự do trong hệ thống. Dữ liệu được gắn chặt với từng phương thức thành các vùng riêng mà các phương thức đó tác động lên và nó được bảo vệ để cấm việc truy nhập tùy tiện từ bên ngoài. LTHĐT cho phép phân tích bài toán thành tập các thực thể được gọi là các đối tượng và sau đó xây dựng các dữ liệu cùng với các phương thức xung quanh các đối tượng đó. Tóm lại LTHĐT có những đặc tính chủ yếu như sau: 1. Tập trung vào dữ liệu thay cho các phương thức. 2. Chương trình được chia thành các lớp đối tượng. 3. Các cấu trúc dữ liệu được thiết kế sao cho đặc tả được các đối tượng. Trang 6
  10. 4. Các phương thức xác định trên các vùng dữ liệu của đối tượng được gắn với nhau trên cấu trúc dữ liệu đó. 5. Dữ liệu được bao bọc, che dấu và không cho phép các thành phần bên ngoài truy nhập tự do. 6. Các đối tượng trao đổi với nhau thông qua các phương thức. 7. Dữ liệu và các phương thức mới có thể dễ dàng bổ sung vào đối tượng nào đó khi cần thiết. 8. Chương trình được thiết kế theo cách tiếp cận bottom-up (dưới -lên). 1.2. Các khái niệm cơ bản của lập trình hướng đối tượng Những khái niệm cơ bản trong LTHĐT bao gồm: Đối tượng; Lớp; Sự đóng gói; Tính kế thừa; Tương ứng bội; Liên kết động; Truyền thông báo. 1.2.1. Đối tượng Trong thế giới thực, khái niệm đối tượng được hiểu như là một thực thể, nó có thể là người, vật hoặc một bảng dữ liệu cần xử lý trong chương trình, Trong LTHĐT thì đối tượng là biến thể hiện của lớp. 1.2.2. Lớp Lớp là một khái niệm mới trong LTHĐT so với kỹ thuật LTHTT. Nó là một bản mẫu mô tả các thông tin cấu trúc dữ liệu và các thao tác hợp lệ của các phần tử dữ liệu. Khi một phần tử dữ liệu được khai báo là phần tử của một lớp thì nó được gọi là đối tượng. Các hàm được định nghĩa hợp lệ trong một lớp được gọi là các phương thức (method) và chúng là các hàm duy nhất có thể xử lý dữ liệu của các đối tượng của lớp đó. Mỗi đối tượng có riêng cho mình một bản sao các phần tử dữ liệu của lớp. Mỗi lớp bao gồm: danh sách các thuộc tính (attribute) và danh sách các phương thức để xử lý các thuộc tính đó. Công thức phản ánh bản chất của kỹ thuật LTHĐT là: Đối tượng = Dữ liệu + Phương thức Chẳng hạn, chúng ta xét lớp HINH_CN bao gồm các thuộc tính: (x1,y1) toạ độ Trang 7
  11. góc trên bên trái, d,r là chiều dài và chiều rộng của HCN. Các phương thức nhập số liệu cho HCN, hàm tính diện tích, chu vi và hàm hiển thị. Lớp HINH_CN có thể được mô tả như sau: HINH_CN Thuộc tính : x1,y1 d,r Phương thức : Nhập_sl Diện tích Chu vi Hiển thị Hình 1.1 Mô tả lớp HINH_CN Chú ý: Trong LTHĐT thì lớp là khái niệm tĩnh, có thể nhận biết ngay từ văn bản chương trình, ngược lại đối tượng là khái niệm động, nó được xác định trong bộ nhớ của máy tính, nơi đối tượng chiếm một vùng bộ nhớ lúc thực hiện chương trình. Đối tượng được tạo ra để xử lý thông tin, thực hiện nhiệm vụ được thiết kế, sau đó bị hủy bỏ khi đối tượng đó hết vai trò 1.2.3. Sự đóng gói (Encapsulation) Sự đóng gói là cơ chế ràng buộc dữ liệu và thao tác trên dữ liệu đó thành một thể thống nhất, tránh được các tác động bất ngờ từ bên ngoài. Thể thống nhất này gọi là đối tượng. Trong Objetc Oriented Software Engineering của Ivar Jacibson, tất cả các thông tin của một hệ thống định hướng đối tượng được lưu trữ bên trong đối tượng của nó và chỉ có thể hành động khi các đối tượng đó được ra lệnh thực hiện các thao tác. Trang 8
  12. Như vật, sự đóng gói không chỉ đơn thuần là sự gom chung dữ liệu và chương trình vào trong một khối, chúng còn được hiểu theo nghĩa là sự đồng nhất giữa dữ liệu và các thao tác tác động lên dữ liệu đó. Trong một đối tượng, dữ liệu hay thao tác hay cả hai có thể là riêng (private) hoặc chung (public) của đối tượng đó. Thao tác hay dữ liệu riêng là thuộc về đối tượng đó chỉ được truy cập bởi các thành phần của đối tượng, điều này nghĩa là thao tác hay dữ liệu riêng không thể truy cập bởi các phần khác của chương trình tồn tại ngoài đối tượng. Khi thao tác hay dữ liệu là chung, các phần khác của chương trình có thể truy cập nó mặc dù nó được định nghĩa trong một đối tượng. Các thành phần chung của một đối tượng dùng để cung cấp một giao diện có điều khiển cho các thành thành riêng của đối tượng. Cơ chế đóng gói là phương thức tốt để thực hiện cơ chế che dấu thông tin so với các ngôn ngữ lập trình cấu trúc. 1.2.4. Tính kế thừa (Inheritance) Chúng ta có thể xây dựng các lớp mới từ các lớp cũ thông qua sự kế thừa. Một lớp mới còn gọi là lớp dẫn xuất (derived class), có thể thừa hưởng dữ liệu và các phương thức của lớp cơ sở (base class) ban đầu. Trong lớp này, có thể bổ sung các thành phần dữ liệu và các phương thức mới vào những thành phần dữ liệu và các phương thức mà nó thừa hưởng từ lớp cơ sở. Mỗi lớp (kể cả lớp dẫn xuất) có thể có một số lượng bất kỳ các lớp dẫn xuất. Qua cơ cấu kế thừa này, dạng hình cây của các lớp được hình thành. Dạng cây của các lớp trông giống như các cây gia phả vì thế các lớp cơ sở còn được gọi là lớp cha (parent class) và các lớp dẫn xuất được gọi là lớp con (child class). 1.2.5. Tương ứng bội Tương ứng bội là khả năng của một khái niệm (chẳng hạn các phép toán) có thể sử dụng với nhiều chức năng khác nhau. Ví dụ, phép + có thể biểu diễn cho phép “cộng” các số nguyên (int), số thực (float), số phức (complex) hoặc xâu ký tự (string) Trang 9
  13. v.v Hành vi của phép toán tương ứng bội phụ thuộc vào kiểu dữ liệu mà nó sử dụng để xử lý. Tương ứng bội đóng vai quan trọng trong việc tạo ra các đối tượng có cấu trúc bên trong khác nhau nhưng cùng dùng chung một giao diện bên ngoài (như tên gọi). 1.2.6. Liên kết động Liên kết động là dạng liên kết các thủ tục và hàm khi chương trình thực hiện lời gọi tới các hàm, thủ tục đó. Như vậy trong liên kết động, nội dung của đoạn chương trình ứng với thủ tục, hàm sẽ không được biết cho đến khi thực hiện lời gọi tới thủ tục, hàm đó. 1.2.7. Truyền thông báo Các đối tượng gửi và nhận thông tin với nhau giống như con người trao đổi với nhau. Chính nguyên lý trao đổi thông tin bằng cách truyền thông báo cho phép ta dễ dàng xây dựng được hệ thống mô phỏng gần hơn những hệ thống trong thế giới thực. Truyền thông báo cho một đối tượng là yêu cầu đối tượng thực hiện một việc gì đó. Cách ứng xử của đối tượng được mô tả bên trong lớp thông qua các phương thức. Trong chương trình, thông báo gửi đến cho một đối tượng chính là yêu cầu thực hiện một công việc cụ thể, nghĩa là sử dụng những hàm tương ứng để xử lý dữ liệu đã được khai báo trong đối tượng đó. Vì vậy, trong thông báo phải chỉ ra được hàm cần thực hiện trong đối tượng nhận thông báo. Thông báo truyền đi cũng phải xác định tên đối tượng và thông tin truyền đi. Ví dụ, lớp CONGNHAN có thể hiện là đối tượng cụ thể được đại diện bởi Hoten nhận được thông báo cần tính lương thông qua hàm TINHLUONG đã được xác định trong lớp CONGNHAN. Thông báo đó sẽ được xử lý như sau: CONGNHAN.TINHLUONG (Hoten) Đối tượng Thông báo Thông tin Trong chương trình hướng đối tượng, mỗi đối tượng chỉ tồn tại trong thời gian nhất định. Đối tượng được tạo ra khi nó được khai báo và sẽ bị hủy bỏ khi chương trình Trang 10
  14. ra khỏi miền xác định của đối tượng đó. Sự trao đổi thông tin chỉ có thể thực hiện trong thời gian đối tượng tồn tại. 1.3. Các bước cần thiết để thiết kế chương trình theo hướng đối tượng Chương trình theo hướng đối tượng bao gồm một tập các đối tượng và mối quan hệ giữa các đối tượng với nhau. Vì vậy, lập trình trong ngôn ngữ hướng đối tượng bao gồm các bước sau: 1. Xác định các dạng đối tượng (lớp) của bài tóan. 2. Tìm kiếm các đặc tính chung (dữ liệu chung) trong các dạng đối tượng này, những gì chúng cùng nhau chia xẻ. 3. Xác định lớp cơ sở dựa trên cơ sở các đặc tính chung của các dạng đối tượng. 4. Từ lớp cơ sở, xây dựng các lớp dẫn xuất chứa các thành phần, những đặc tính không chung còn lại của các dạng đối tượng. Ngoài ra, ta còn đưa ra các lớp có quan hệ với các lớp cơ sở và lớp dẫn xuất. 1.4. Các ưu điểm của lập trình hướng đối tượng Cách tiếp cận hướng đối tượng giải quyết được nhiều vấn đề tồn tại trong quá trình phát triển phần mềm và tạo ra được những sản phẩm phần mềm có chất lượng cao. Những ưu điểm chính của LTHĐT là: 1. Thông qua nguyên lý kế thừa, có thể loại bỏ được những đoạn chương trình lặp lại trong quá trình mô tả các lớp và mở rộng khả năng sử dụng các lớp đã được xây dựng. 2. Chương trình được xây dựng từ những đơn thể (đối tượng) trao đổi với nhau nên việc thiết kế và lập trình sẽ được thực hiện theo quy trình nhất định chứ không phải dựa vào kinh nghiệm và kỹ thuật như trước. Điều này đảm bảo rút ngắn được thời gian xây dựng hệ thống và tăng năng suất lao động. 3. Nguyên lý che giấu thông tin giúp người lập trình tạo ra được những chương trình an toàn không bị thay bởi những đoạn chương trình khác. Trang 11
  15. 4. Có thể xây dựng được ánh xạ các đối tượng của bài toán vào đối tượng của chương trình. 5. Cách tiếp cận thiết kế đặt trọng tâm vào đối tượng, giúp chúng ta xây dựng được mô hình chi tiết và gần với dạng cài đặt hơn. 6. Những hệ thống hướng đối tượng dễ mở rộng, nâng cấp thành những hệ lớn hơn. 7. Kỹ thuật truyền thông báo trong việc trao đổi thông tin giữa các đối tượng giúp cho việc mô tả giao diện với các hệ thống bên ngoài trở nên đơn giản hơn. 8. Có thể quản lý được độ phức tạp của những sản phẩm phần mềm. Không phải trong hệ thống hướng đối tượng nào cũng có tất cả các tính chất nêu trên. Khả năng có các tính chất đó còn phụ thuộc vào lĩnh vực ứng dụng của dự án tin học và vào phương pháp thực hiện của người phát triển phần mềm. 1.5. Các ngôn ngữ hướng đối tượng Xuất phát từ tư tưởng của ngôn ngữ SIMULA67, trung tâm nghiên cứu Palo Alto (PARC) của hãng XEROR đã tập trung 10 năm nghiên cứu để hoàn thiện ngôn ngữ OOP đầu tiên với tên gọi là Smalltalk. Sau đó các ngôn ngữ OOP lần lượt ra đời như Eiffel, Clos, Loops, Flavors, Object Pascal, Object C, C++, Delphi, Java 1.6. Một số ứng dụng của LTHĐT LTHĐT đang được ứng dụng để phát triển phần mềm trong nhiều lĩnh vực khác nhau. Trong số đó, có ứng dụng quan trọng và nổi tiếng nhất hiện nay là hệ điều hành Windows của hãng Microsoft đã được phát triển dựa trên kỹ thuật LTHĐT. Một số những lĩnh vực ứng dụng chính của kỹ thuật LTHĐT bao gồm: + Những hệ thống làm việc theo thời gian thực. + Trong lĩnh vực mô hình hóa hoặc mô phỏng các quá trình + Các cơ sở dữ liệu hướng đối tượng. + Những hệ siêu văn bản, multimedia Trang 12
  16. + Lĩnh vực trí tuệ nhân tạo và các hệ chuyên gia. + Lập trình song song và mạng nơ-ron. + Những hệ tự động hóa văn phòng và trợ giúp quyết định. Trang 13
  17. CHƯƠNG 2: CÁC MỞ RỘNG CỦA NGÔN NGỮ LẬP TRÌNH C++ Chương 2 trình bày những vấn đề sau đây:  Giới thiệu chung về ngôn ngữ C++  Một số mở rộng của ngôn ngữ C++ so với ngôn ngữ C  Vào ra trong C++, cấp phát và giải phóng bộ nhớ  Các hàm, biến tham chiếu, hằng tham chiếu.  Truyền tham số cho hàm theo tham chiếu 2.1. Giới thiệu chung về C++ C++ là ngôn ngữ lập trình hướng đối tượng và là sự mở rộng của ngôn ngữ C. Vì vậy mọi khái niệm trong C đều dùng được trong C++. Phần lớn các chương trình C đều có thể chạy được trong C++. Trong chương này chỉ tập trung giới thiệu những khái niệm, đặc tính mới của C++ hỗ trợ cho lập trình hướng đối tượng. Một số kiến thức có trong C++ nhưng đã có trong ngôn ngữ C sẽ không được trình bày lại ở đây. 2.2. Một số mở rộng của C++ so với C 2.2.1. Các từ khóa mới của C++ Để bổ sung các tính năng mới vào C, một số từ khóa (keyword) mới đã được đưa vào C++ ngoài các từ khóa có trong C. Các chương trình bằng C nào sử dụng các tên trùng với các từ khóa cần phải thay đổi trước khi chương trình được dịch lại bằng C++. Các từ khóa mới này là : Bảng 2.1: Bảng các từ khóa trong C++ asm catch class delete friend inline new operator private protected public template this throw try virtual 2.2.2. Cách ghi chú thích C++ chấp nhận hai kiểu chú thích. Các lập trình viên bằng C đã quen với cách Trang 14
  18. chú thích bằng /* */. Trình biên dịch sẽ bỏ qua mọi thứ nằm giữa /* */. C++ đưa thêm một kiểu chú thích thứ hai, đó là chú thích bắt đầu bằng // Ví dụ 2.1: Trong chương trình sau : #include using namespace std; int main() { int I; for(I = 0; I using namespace std; int main() { int X = 200; long Y = (long) X; //Chuyen doi kieu trong C long Z = long(X); //Chuyen doi kieu trong C++ cout<< "X = "<<X<<endl; cout<< "Y = "<<Y<<endl; cout<< "Z = "<<Z<<endl; Trang 15
  19. return 0; } 2.2.4. Khai báo biến Trong C tất cả các câu lệnh khai báo biến, mảng cục bộ phải đặt tại đầu khối. Vì vậy vị trí khai báo và vị trí sử dụng của biến có thể ở cách khá xa nhau, điều này gây khó khăn trong việc kiểm soát chương trình. C++ đã khắc phục nhược điểm này bằng cách cho phép các lệnh khai báo biến có thể đặt bất kỳ chỗ nào trong chương trình trước khi các biến được sử dụng. Phạm vi hoạt động của các biến kiểu này là khối trong đó biến được khai báo. Ví dụ 2.3: Chương trình mô phỏng một máy tính đơn giản #include using namespace std; int main() { int X; cout >X; int Y; cout >Y; char Op; cout >Op; switch(Op) { case ‘+’: cout<<"Ket qua:"<<X+Y<<"\n"; break; case ‘-’: cout<<"Ket qua:"<<X-Y<<"\n"; break; case ‘*’: cout<<"Ket qua:"<<long(X)*Y<<"\n"; Trang 16
  20. break; case ‘/’: if (Y) cout >biến 1>>. . . >>biến N; Toán tử cin được định nghĩa trước như một đối tượng biểu diễn cho thiết bị vào chuẩn của C++ là bàn phím, cin được sử dụng kết hợp với toán tử trích >> để nhập dữ liệu từ bàn phím cho các biến 1, 2, , N. Trang 17
  21. Chú ý: Để nhập một chuỗi không quá n ký tự và lưu vào mảng một chiều a (kiểu char) có thể dùng hàm cin.get như sau: cin.get(a,n); Để nhập một chuỗi str kể cả khoảng trắng và lưu vào kiểu string (include ) ta dung hàm getline(cin,str,delimiter) Câu lệnh trên sẽ thực hiện đọc toàn bộ xâu nhập từ bàn phím vào biến str, cho tới khi bắt gặp ký tự kết thúc (delimiter) hoặc EOF (end-of-file). Nếu không viết delimiter thì mặc định là ký tự xuống dòng ‘\n’. Toán tử nhập cin>> sẽ để lại ký tự chuyển dòng ’\n’ trong bộ đệm. Ký tự này có thể làm trôi phương thức: + cin.get. Để khắc phục tình trạng trên cần dùng phương thức cin.ignore(1). + getline, để khắc phục tình trạng trôi lệnh này thì trước mỗi lệnh getline ta đặt câu lệnh fflush(stdin). Để sử dụng các loại toán tử và phương thức nói trên cần khai báo tập tin dẫn hướng iostream.h 2.3.3. Định dạng khi in ra màn hình Để quy định số thực được hiển thị ra màn hình với p chữ số ta sử dụng đồng thời các hàm sau: setiosflags(ios::showpoint); // Bật cờ hiệu showpoint(p) setprecision(p); Các hàm này cần đặt trong toán tử xuất như sau: cout Trang 18
  22. #include using namespace std; main() { float a =10; float b=3; float c= a/b; cout #include #include #include #include #include using namespace std; int main() { struct Trang 19
  23. { string ht; float d1,d2,d3,td; }ts[100],tg; int n,i,j; cout > n; for (i=0;i >ts[i].d1>>ts[i].d2>>ts[i].d3; ts[i].td=ts[i].d1+ts[i].d2+ts[i].d3; } for (i=0;i<n-1;++i) for(j=i+1;j<n;++j) if(ts[i].td<ts[j].td) { tg=ts[i]; ts[i]=ts[j]; ts[j]=tg; } cout<< "\ Danh sach thi sinh sau khi sap xep :"; for (i=0;i<n;++i) { cout<<"\n" << setw(25)<< ts[i].ht<< setw(5)<< ts[i].td; } return 0; } 2.4. Cấp phát và giải phóng bộ nhớ Trong C có thể sử dụng các hàm cấp phát bộ nhớ như malloc(), calloc() và hàm free() để giải phóng bộ nhớ được cấp phát. C++ đưa thêm một cách thức mới để thực Trang 20
  24. hiện việc cấp phát và giải phóng bộ nhớ bằng cách dùng hai toán tử new và delete. Đoạn chương trình sau dùng để Trong C++, chúng ta có thể viết cấp phát vùng nhớ động theo lối cổ điển lại đoạn chương trình trên như sau: của C. int *P; P = new int; int *P; if (P==NULL) P = malloc(sizeof(int)); cout #include Trang 21
  25. #include #include #include using namespace std; struct TS { string ht; long sobd; float td; }; int main() { TS *ts; int n,i; cout >n; ts = new TS[n+1]; if (ts == NULL) { cout << "\n Loi cap phat vung nho"; exit(0); } for (i=0;i<n;++i) { cout << "\n Thi sinh thu "<<i+1; cout<< "\n Ho ten"; Trang 22
  26. fflush(stdin); getline(cin,ts[i].ht); cout > ts[i].sobd; cout >ts[i].td; } for (i=0;i<n-1;++i) for (int j=i+1;j<n;++j) if (ts[i].td<ts[j].td) { TS tg=ts[i]; ts[i]=ts[j]; ts[j]=tg; } cout<< "\ Danh sach thi sinh sau khi sap xep :"; cout << setiosflags(ios::showpoint)<< setprecision(2); for (i=0;i<n;++i) cout << "\n" << setw(20)<< ts[i].ht<< setw(6)<< ts[i].td; delete ts; } 2.5. Biến tham chiếu Trong C có 2 loại biến là: Biến giá trị dùng để chứa dữ liệu (nguyên, thực, ký tự, ) và biến con trỏ dùng để chứa địa chỉ. Các biến này đều được cung cấp bộ nhớ và Trang 23
  27. có địa chỉ. C++ cho phép sử dụng loại biến thứ ba là biến tham chiếu. Biến tham chiếu là một tên khác (bí danh) cho biến đã định nghĩa trước đó. Cú pháp khai báo biến tham chiếu như sau: Kiểu &Biến tham chiếu = Biến; Biến tham chiếu có đặc điểm là nó được dùng làm bí danh cho một biến (kiểu giá trị) nào đó và sử dụng vùng nhớ của biến này. Ví dụ: Với câu lệnh: int a, &tong=a; thì tong là bí danh của biến a và biến tong dùng chung vùng nhớ của biến a. Lúc này, trong mọi câu lệnh, viết a hay viết tong đều có ý nghĩa như nhau, vì đều truy nhập đến cùng một vùng nhớ. Mọi sự thay đổi đối với biến tong đều ảnh hưởng đối với biến a và ngược lại. Ví dụ: int a, &tong = a; tong =1; //a=1 cout<< tong; //in ra số 1 tong++; //a=2 ++a; //a=3 cout<<tong; //in ra số 3 Chú ý: Trong khai báo biến tham chiếu phải chỉ rõ tham chiếu đến biến nào. Biến tham chiếu có thể tham chiếu đến một phần tử mảng, nhưng không cho phép khai báo mảng tham chiếu. Biến tham chiếu có thể tham chiếu đến một hằng. Khi đó nó sử dụng vùng nhớ của hằng và có thể làm thay đổi giá trị chứa trong vùng nhớ này. Biến tham chiếu thường được sử dụng làm đối của hàm để cho phép hàm truy nhập đến các tham biến trong lời gọi hàm Trang 24
  28. 2.6. Hằng tham chiếu Cú pháp khai báo hằng tham chiếu như sau: const Kiểu dữ liệu &Biến = Biến/Hằng; Ví dụ: int n = 10; const int &m = n; const int &p = 123; Hằng tham chiếu có thể tham chiếu đến một biến hoặc một hằng. Chú ý:  Biến tham chiếu và hằng tham chiếu khác nhau ở chỗ: không cho phép dùng hằng tham chiếu để làm thay đổi giá trị của vùng nhớ mà nó tham chiếu. Ví dụ: int y=12, z; const int &p = y //Hằng tham chiếu p tham chiếu đến biến y p = p + 1; //Sai, trình biên dịch sẽ thông báo lỗi  Hằng tham chiếu cho phép sử dụng giá trị chứa trong một vùng nhớ, nhưng không cho phép thay đổi giá trị này.  Hằng tham chiếu thường được sử dụng làm tham số của hàm để cho phép sử dụng giá trị của các tham số trong lời gọi hàm, nhưng tránh làm thay đổi giá trị tham số. 2.7. Truyền tham số cho hàm theo tham chiếu Trong C chỉ có một cách truyền dữ liệu cho hàm là truyền theo theo giá trị. Chương trình sẽ tạo ra các bản sao của các tham số thực sự trong lời gọi hàm và sẽ thao tác trên các bản sao này chứ không xử lý trực tiếp với các tham số thực sự. Cơ chế này rất tốt nếu khi thực hiện hàm trong chương trình không cần làm thay đổi giá trị của biến gốc. Tuy nhiên, nhiều khi ta lại muốn những tham số đó thay đổi khi thực hiện hàm trong chương trình. C++ cung cấp thêm cách truyền dữ liệu cho hàm theo tham chiếu bằng cách dùng đối là tham chiếu. Cách làm này có ưu diểm là không cần tạo ra Trang 25
  29. các bản sao của các tham số, do dó tiết kiệm bộ nhớ và thời gian chạy máy. Mặt khác, hàm này sẽ thao tác trực tiếp trên vùng nhớ của các tham số, do đó dễ dàng thay đổi giá trị các tham số khi cần Ví dụ 2.7: Chương trình hoán đổi giá trị của hai biến. #include using namespace std; //prototype void Swap(int &X,int &Y); int main() { int X = 10, Y = 5; cout #include #include #include using namespace std; void nhapmt(float a[20][20],int m,int n); void inmt(float a[20][20],int m,int n); void maxminds(float *x,int n,int &vtmax,int &vtmin); int main() { float a[20][20]; int m,n; cout<<"\n Nhap so hang va so cot : "; Trang 26
  30. cin>>m>>n; nhapmt(a,m,n); inmt(a,m,n); float *p=(float*) a; int vtmax,vtmin; for(int i=0;i > a[i][j]; } } void inmt(float a[20][20],int m,int n) { cout x[vtmax]) vtmax=i; if(x[i]<x[vtmin]) vtmin=i; } Trang 27
  31. } 2.8. Hàm trả về giá trị tham chiếu C++ cho phép hàm trả về giá trị là một tham chiếu, lúc này định nghĩa của hàm có dạng như sau : Kiểu &Tên hàm( ) { //thân hàm return ; } Trong trường hợp này biểu thức được trả lại trong câu lệnh return phải là tên của một biến xác định từ bên ngoài hàm, bởi vì khi đó mới có thể sử dụng được giá trị của hàm. Khi ta trả về một tham chiếu đến một biến cục bộ khai báo bên trong hàm, biến cục bộ này sẽ bị mất đi khi kết thúc thực hiện hàm. Do vậy tham chiếu của hàm sẽ không còn ý nghĩa nữa. Khi giá trị trả về của hàm là tham chiếu, ta có thể gặp các câu lệnh gán hơi khác thường, trong đó vế trái là một lời gọi hàm chứ không phải là tên của một biến. Điều này hoàn toàn hợp lý, bởi vì bản thân hàm đó có giá trị trả về là một tham chiếu. Nói cách khác, vế trái của lệnh gán có thể là lời gọi đến một hàm có giá trị trả về là một tham chiếu.Xem các ví dụ sau: Ví dụ 2.8 #include #include using namespace std; int z; int &f()// ham tra ve mot bi danh cua bien toan bo z { return z; } main() { f()=50;//z=50 cout<<"\nz="<<z; Trang 28
  32. } Ví dụ 2.10 #include #include #include #include using namespace std; int & max(int& a, int& b); int main() { int b =10, a= 7, c= 20; cout b ? a:b; } Kết quả trên màn hình sẽ là : Max a,b : 10 Gia tri cua b va a : 11 7 Gia tri cua b va a va c : 11 7 5 2.9. Hàm với tham số có giá trị mặc định Một trong các đặc tính nổi bật nhất của C++ là khả năng định nghĩa các giá trị tham số mặc định cho các hàm. Bình thường khi gọi một hàm, chúng ta cần gởi một giá trị cho mỗi tham số đã được định nghĩa trong hàm đó, chẳng hạn chúng ta có đoạn chương trình sau: void MyDelay(long Loops); //prototype Trang 29
  33. void MyDelay(long Loops) { for(int I = 0; I using namespace std; int BoxVolume(int Length = 1, int Width = 1, int Trang 30
  34. Height = 1); int main() { cout << "The tich hinh hop mac dinh: " << BoxVolume() << endl << endl; cout<< "The tich hinh hop voi chieu dai=10,do rong=1,chieu cao=1:" << BoxVolume(10) << endl << endl; cout<< "The tich hinh hop voi chieu dai=10,do rong=5,chieu cao=1:" << BoxVolume(10, 5) << endl << endl; cout<< "The tich hinh hop voi chieu dai=10,do rong=5,chieu cao=2:" << BoxVolume(10, 5, 2)<< endl; return 0; } //Tinh the tich hinh hop int BoxVolume(int Length, int Width, int Height) { return Length * Width * Height; } Chú ý: Đối với các hàm có tham số mặc định thì lời gọi hàm cần viết theo quy định: Các tham số vắng mặt trong lời gọi hàm tương ứng với các tham số mặc định cuối cùng (tính từ trái sang phải) ví dụ với hàm: int BoxVolume(int Length, int Width=1, int Height=1) Các lời gọi hàm đúng: BoxVolume(1,2,3); // hàm đầy đủ tham số BoxVolume(1,2); // hàm thiếu tham số cuối BoxVolume(2); // Hàm thiếu 2 tham số cuối Các lời gọi hàm sai: BoxVolume(); BoxVolume(1, ,2); Trang 31
  35. 2.10. Các hàm nội tuyến (inline) Một chương trình có cấu trúc tốt sử dụng các hàm để chia chương trình thành các đơn vị độc lập có logic riêng. Tuy nhiên, các hàm thường phải chứa một loạt các xử lý điểm vào (entry point): tham số phải được đẩy vào stack, một lệnh gọi phải được thực hiện và sau đó việc quay trở về cũng phải được thực hiện bằng cách giải phóng các tham số ra khỏi stack. Khi các xử lý điểm vào chậm chạp thường các lập trình viên C phải sử dụng cách chép lập lại các đoạn chương trình nếu muốn tăng hiệu quả. Để tránh khỏi phải xử lý điểm vào, C++ trang bị thêm từ khóa inline để loại việc gọi hàm. Khi đó trình biên dịch sẽ không biên dịch hàm này như một đoạn chương trình riêng biệt mà nó sẽ được chèn thẳng vào các chỗ mà hàm này được gọi. Điều này làm giảm việc xử lý điểm vào mà vẫn cho phép một chương trình được tổ chức dưới dạng có cấu trúc. Cú pháp của hàm inline như sau : inline data_type function_name ( parameters ) { } Trong đó: data_type: Kiểu trả về của hàm. Function_name:Tên của hàm. Parameters: Các tham số của hàm. Ví dụ 2.12: Tính thể tích của hình lập phương #include using namespace std; inline float Cube(float S) { return S*S*S; } int main() { Trang 32
  36. cout >Side; cout #include #include using namespace std; void nhapds(int *x,int n); void nhapds(double *x,int n); int max(int x,int y); double max(double x,double y); void nhapds(int *x,int n) { for(int i=0;i >x[i]; } } void nhapds(double *x,int n) { Trang 33
  37. for (int i=0;i >x[i]; } } int max(int x,int y) { return x>y?x:y; } double max(double x,double y) { return x>y?x:y; } int max(int *x,int n) { int s=x[0]; for(int i=1;i >ni; cout >nd; cout<<"\n Nhap day so thuc: "; nhapds(x,nd); maxi=max(a,ni); maxd=max(x,nd); cout<<"\n Max day so nguyen ="<<maxi; Trang 34
  38. cout #include Using namespace std; int f(int a); void f(int a); int f(int a) { return a*a; } void f(int a) { cout<<"\n"<<a; } int main() { int b = f(5); f(b); return 0; } Trang 35
  39. Bài tập 1. Viết chương trình thực hiện các yêu cầu sau đây: - Nhập dữ liệu cho các sinh viên (dùng cấu trúc danh sách liên kết đơn), các thông tin của sinh viên bao gồm: mã sinh viên, họ tên, lớp, điểm trung bình. - Chương trình có sử dụng toán tử new và delete. - In ra danh sách sinh viên có sắp xếp vị thứ theo điểm trung bình. 2. Viết chương trình để sắp xếp một mảng thực hai chiều theo thứ tự tăng dần, trong chương trình có có sử dụng toán tử new và delete. 3. Viết các hàm tải bội để tính diện tích tam giác, diện tích hình chữ nhật, diện tích hình tròn. 4. Viết chương trình nhân hai ma trận Amxn và Bnxp , mỗi ma trân được cấp phát động và các giá trị của chúng phát sinh ngẫu nhiên. Trang 36
  40. CHƯƠNG 3: LỚP Chương này trình bày những vấn đề sau đây:  Định nghĩa lớp, tạo lập đối tượng.  Con trỏ đối tượng, con trỏ this.  Hàm bạn.  Dữ liệu thành phần tĩnh, hàm thành phần tĩnh  Hàm tạo, hàm tạo sao chép, hàm hủy 3.1. Định nghĩa lớp Lớp là khái niệm trung tâm của lập trình hướng đối tượng, nó là sự mở rộng của các khái niệm cấu trúc (struct) của C. Ngoài các thành phần dữ liệu, lớp còn chứa các thành phần hàm, còn gọi là phương thức (method) hoặc hàm thành viên (member function). Lớp có thể xem như một kiểu dữ liệu các biến, mảng đối tượng. Từ một lớp đã định nghĩa, có thể tạo ra nhiều đối tượng khác nhau, mỗi đối tượng có vùng nhớ riêng. Cú pháp: Lớp được định nghĩa theo mẫu : class tên_lớp { // Khai báo các thành phần dữ liệu (thuộc tính) // Khai báo các phương thức } ; // Định nghĩa (xây dựng) các phương thức Chú ý: Thuộc tính của lớp có thể là các biến, mảng, con trỏ có kiểu chuẩn (int, float, char, char*, long, ) hoặc kiểu ngoài chuẩn đã định nghĩa trước (cấu trúc, hợp, lớp, ) . Thuộc tính của lớp không thể có kiểu của chính lớp đó, nhưng có thể là kiểu con trỏ lớp này, ví dụ: class A Trang 37
  41. { A x ; // Không cho phép, vì x có kiểu lớp A A *p ; // Cho phép , vì p là con trỏ kiểu lớp A } ; Khi báo các thành phần của lớp (thuộc tính và phương thức) có thể dùng các từ khoá private và public để quy định phạm vi sử dụng của các thành phần. Nếu không quy định cụ thể (không dùng các từ khoá private và public) thì C++ hiểu đó là private. Các thành phần private (riêng) chỉ được sử dụng bên trong lớp (trong thân của các phương thức của lớp). Các hàm không phải là phương thức của lớp không được phép sử dụng các thành phần này. Các thành phần public (công cộng) được phép sử dụng ở cả bên trong và bên ngoài lớp. Các thành phần dữ liệu thường (nhưng không bắt buộc) khai báo là private để bảo đảm tính giấu kín, bảo vệ an toàn dữ liệu của lớp, không cho phép các hàm bên ngoài xâm nhập vào dữ liệu của lớp. Các phương thức thường khai báo là public để chúng có thể được gọi tới (sử dụng) từ các hàm khác trong chương trình. Các phương thức có thể được xây dựng bên ngoài hoặc bên trong định nghĩa lớp. Thông thường, các phương thức ngắn được viết bên trong định nghĩa lớp, còn các phương thức dài thì viết bên ngoài định nghĩa lớp. Các phương thức viết bên trong định nghĩa lớp được viết như hàm thông thường. Khi định nghĩa các phương thức ở bên ngoài lớp, ta dùng cú pháp sau đây: Kiểu_trả_về_của_hàm Tên_lớp::Tên_hàm(khai báo các tham số) { [nội dung hàm] Trang 38
  42. } Toán tử :: được gọi là toán tử phân giải miền xác định, được dùng để chỉ ra lớp mà hàm đó thuộc vào. Trong thân hàm thành phần, có thể sử dụng các thuộc tính của lớp, các hàm thành phần khác và các hàm tự do trong chương trình. 3.2. Tạo lập đối tượng Sau khi định nghĩa lớp, ta có thể khai báo các biến thuộc kiểu lớp. Các biến này được gọi là các đối tượng. Cú pháp khai báo biến đối tượng như sau: Tên_lớp Danh_sách_biến ; Đối tượng cũng có thể khai báo khi định nghĩa lớp theo cú pháp sau: class tên_lớp { } ; Mỗi đối tượng sau khi khai báo sẽ được cấp phát một vùng nhớ riêng để chứa các thuộc tính của chúng. Không có vùng nhớ riêng để chứa các hàm thành phần cho mỗi đối tượng. Các hàm thành phần sẽ được sử dụng chung cho tất cả các đối tượng cùng lớp. 3.3. Truy nhập tới các thành phần của lớp Các dữ liệu thành phần (các biến khai báo trong định nghĩa lớp) và các hàm thành phần (các hàm khai báo trong định nghĩa lớp) thuộc vào phạm vi của lớp Trong một phạm vi lớp, các thành phần của lớp được truy cập ngay lập tức bởi tất cả các hàm thành phần của lớp đó và có thể được tham chiếu một cách dễ dàng. Bên ngoài một phạm vi lớp, các thành phần của lớp được tham chiếu thông qua một tên đối tượng, một tham chiếu đến một đối tượng Trang 39
  43. Để truy nhập đến dữ liệu thành phần của lớp, ta dùng cú pháp: Tên_đối_tượng. Tên_thuộc_tính Cần chú ý rằng dữ liệu thành phần riêng chỉ có thể được truy nhập bởi những hàm thành phần của cùng một lớp, đối tượng của lớp cũng không thể truy nhập. Để sử dụng các hàm thành phần của lớp, ta dùng cú pháp: Tên_đối_tượng. Tên_hàm (Các_khai_báo_tham_số_thực_sự) Ví dụ 3.1 #include using namespace std; class Count { public: int X; void Print() { cout #include using namespace std; class DIEM { private : Trang 40
  44. int x,y ; public : void nhapsl( ) { cout >x>>y ; } void hienthi( ) { cout #include using namespace std; class Box { private: int dai; int rong; int cao; public: int get_thetich(int lth,int wdth = 2,int ht=3); }; int Box::get_thetich(int l, int w, int h) { dai = l; rong = w; cao = h; cout<< dai<<'\t'<< rong<<'\t'<<cao<<'\t'; Trang 41
  45. return dai * rong * cao; } main() { Box ob; int x = 10, y = 12, z = 15; cout #include #include using namespace std; class phrase { private: char dongtu[10]; char danhtu[10]; char cumtu[25]; public: phrase(); inline void set_danhtu(char* in_danhtu); inline void set_dongtu(char* in_dongtu); Trang 42
  46. inline char* get_phrase(void); }; void phrase::phrase() { strcpy(danhtu,""); strcpy(dongtu,""); strcpy(cumtu,""); } inline void phrase::set_danhtu(char* in_danhtu) { strcpy(danhtu, in_danhtu); } inline void phrase::set_dongtu(char* in_dongtu) { strcpy(dongtu, in_dongtu); } inline char* phrase::get_phrase(void) { strcpy(cumtu,dongtu); strcat(cumtu," the "); strcat(cumtu,danhtu); return cumtu; } main() { phrase text; cout " " " " the Cum tu la : -> the file Trang 43
  47. Cum tu la : -> Save the file Cum tu la : -> Save the program 3.4. Con trỏ đối tượng Con trỏ đối tượng dùng để chứa địa chỉ của biến, mảng đối tượng. Nó được khai báo như sau: Tên_lớp *con trỏ ; Ví dụ dùng lớp DIEM có thể khai báo: DIEM *p1 , *p2, *p3 ; // khai báo 3 con trỏ p1, p2, p3 DIEM d1, d2 ; // Khai báo 2 đối tượng d1, d2 DIEM d[20] ; // Khai báo mảng đối tượng Ta có thể thực hiện các câu lệnh: p1 = &d2 ; // p1 chứa địa chỉ của d2 , hay p1 trỏ tới d2 p2 = d ; // p2 trỏ tới đầu mảng d p3 = new DIEM // Tạo một đối tượng và chứa địa chỉ của nó vào p3 Để sử dụng thuộc tính của đối tượng thông qua con trỏ, ta viết như sau: Tên_con_trỏ->Tên_thuộc_tính Chú ý: Nếu con trỏ chứa địa chỉ đầu của mảng, có thể dùng con trỏ như tên mảng. Như vậy sau khi thực hiện các câu lệnh trên thì: p1->x và d2.x là như nhau p2[i].y và d[i].y là như nhau Tóm lại ta có quy tắc sau Quy tắc sử dụng thuộc tính: Để sử dụng một thuộc tính của đối tượng ta phải dùng phép . hoặc phép -> . Trong chương trình, không cho phép viết tên thuộc tính một cách đơn độc mà phải đi kèm tên đối tượng hoặc tên con trỏ theo các mẫu sau: Trang 44
  48. Tên_đối_tượng.Tên_thuộc_tính Tên_con_trỏ->Tên_thuộc_tính Tên_mảng_đối_tượng[chỉ_số].Tên_thuộc_tính Tên_con_trỏ[chỉ_số].Tên_thuộc_tính Ví dụ 3.5 #include #include using namespace std; class DIEM { private : int x,y ; public : void nhapsl( ) { cout >x>>y ; } void hienthi( ) { cout nhapsl(); cout hienthi(); } Trang 45
  49. 3.5. Con trỏ this 3.5.1. Con trỏ this là đối thứ nhất của phương thức C++ sử dụng con trỏ đặc biệt this trong các phương thức. Các thuộc tính viết trong phương thức được hiểu là thuộc một đối tượng do con trỏ this trỏ tới. Như vậy phương thức nhapsl() có thể viết một cách tường minh như sau: void DIEM::nhapsl() { cout >this->x>>this->y ; } Từ góc độ hàm số có thể kết luận rằng: Phương thức bao giờ cũng có ít nhất một đối là con trỏ this và nó luôn luôn là đối đầu tiên của phương thức. 3.5.2. Tham số ứng với đối con trỏ this DIEM d1; d1.nhapsl() ; Trong trường hợp này tham số truyền cho con trỏ this chính là địa chỉ của d1: this = &d1 Do đó: this->x chính là d1.x this->y chính là d1.y Như vậy câu lệnh d1.nhapsl() ; sẽ nhập dữ liệu cho các thuộc tính của đối tượng d1. Từ đó có thể rút ra kết luận sau: Tham số truyền cho đối con trỏ this chính là địa chỉ của đối tượng đi kèm với phương thức trong lời gọi phương thức. Ví dụ 3.6: #include #include Trang 46
  50. #include using namespace std; class DIEM { private : int x,y ; public : void nhapsl( ) { cout >x>>y ; } void hienthi( ) { cout x - d2.x,2) +pow(this->y - d2.y,2) ) ; } } ; main() { DIEM d1,d2; double kc; d1.nhapsl(); d2.nhapsl(); kc =d1. do_dai(d2); cout<<"Khoang cach 2 diem vua nhap la: "<<kc; } 3.6. Hàm bạn Trong thực tế thường xãy ra trường hợp có một số lớp cần sử dụng chung một hàm. C++ giải quyết vấn đề này bằng cách dùng hàm bạn. Để một hàm trở thành bạn của một lớp, có 2 cách viết: Cách 1: Dùng từ khoá friend để khai báo hàm trong lớp và xây dựng hàm bên ngoài như các hàm thông thường (không dùng từ khoá friend). Mẫu viết như sau: Trang 47
  51. class A { private: // Khai báo các thuộc tính public: // Khai báo các hàm bạn của lớp A friend void f1( ); friend double f2( ); friend A f3( ) ; } ; // Xây dựng các hàm f1, f2, f3 void f1( ) { } double f2( ) { } A f3( ) { Trang 48
  52. } Cách 2: Dùng từ khoá friend để xây dựng hàm trong định nghĩa lớp. Mẫu viết như sau: class A { private: // Khai báo các thuộc tính public: // Xây dựng các hàm bạn của lớp A void f1( ) { } double f2( ) { } A f3( ) { } } ; Trang 49
  53. Hàm bạn có những tính chất sau: + Trong thân hàm bạn của một lớp có thể truy nhập tới các thuộc tính của các đối tượng thuộc lớp này. Đây là sự khác nhau duy nhất giữa hàm bạn và hàm thông thường. + Hàm bạn không phải là phương thức của lớp + Việc truy nhập tới hàm bạn được thực hiện như hàm thông thường. Ví dụ sau sẽ so sánh phương thức, hàm bạn và hàm tự do (hàm thông thường). Xét lớp SP (số phức). Hãy so sánh 3 phương án để thực hiện việc cộng 2 số phức: Phương án 1: Dùng phương thức class SP { private: double a; // Phần thực double b; // Phần ảo public: SP cong(SP u2) { SP u: u.a = this->a + u2.a ; u.b = this->b + u2.b ; return u; } } ; Cách dùng Trang 50
  54. SP u, u1, u2; u = u1.cong(u2); Phương án 2: Dùng hàm bạn class SP { private: double a; // Phần thực double b; // Phần ảo public: friend SP cong(SP u1, SP u2) { SP u: u.a = u1.a + u2.a ; u.b = u1.b + u2.b ; return u; } }; Cách dùng SP u, u1, u2; u = cong(u1, u2); Phương án 3: Dùng hàm tự do class SP { private: Trang 51
  55. double a; // Phần thực double b; // Phần ảo public: } ; SP cong(SP u1, SP u2) { SP u: u.a = u1.a + u2.a ; u.b = u1.b + u2.b ; return u; } Phương án này không được chấp nhận, Trình biên dịch sẽ báo lỗi vì trong thân hàm không được quyền truy xuất đến các thuộc tính riêng (private) a, b của các đối tượng u, u1 và u2 thuộc lớp SP. Một hàm có thể là bạn của nhiều lớp. Lúc đó nó có quyền truy nhập tới tất cả các thuộc tính của các đối tượng trong các lớp này. Để làm cho hàm f trở thành bạn của các lớp A, B và C ta sử dụng mẩu viết sau : class B ; //Khai báo trước lớp A class B ; // Khai báo trước lớp B class C ; // Khai báo trước lớp C // Định nghĩa lớp A Chương trình sau đây minh hoạ cách dùng hàm bạn (bạn của một lớp và bạn của nhiều lớp). Chương trình đưa vào 2 lớp VT (véc tơ), MT (ma trận) và 3 hàm bạn để thực hiện các thao tác trên 2 lớp này: Trang 52
  56. // Hàm bạn với lớp VT dùng để in một véc tơ friend void in(const VT &x); // Hàm bạn với lớp MT dùng để in một ma trận friend void in(const MT &a); // Hàm bạn với cả 2 lớp MT và VT dùng để nhân ma trận với véc tơ friend VT tich(const MT &a,const VT &x); Nội dung chương trình là nhập một ma trận vuông cấp n và một véc tơ cấp n, sau đó thực hiện phép nhân ma trận với véc tơ vừa nhập. Ví dụ 3.7: #include #include #include using namespace std; class VT; class MT ; class VT { private: int n; double x[20]; // Toa do cua diem public: void nhapsl(); friend void in(const VT &x); friend VT tich(const MT &a,const VT &x) ; } ; class MT { private: int n; double a[20][20]; public: friend VT tich(const MT &a,const VT &x); friend void in(const MT &a); Trang 53
  57. void nhapsl(); } ; void VT::nhapsl() { cout > n ; for (int i=1; i > x[i]; } } void MT::nhapsl() { cout > n ; for (int i=1; i > a[i][j]; } } VT tich(const MT &a,const VT &x) { VT y; int n=a.n; if (n!=x.n) return x; y.n = n; for (int i=1; i<=n; ++i) { y.x[i]=0; for (int j=1; j<=n; ++j) y.x[i] += a.a[i][j]*x.x[j]; } return y; Trang 54
  58. } void in(const VT &x) { cout << "\n"; for (int i=1; i<=x.n; ++i) cout << x.x[i] << " "; } void in(const MT &a) { for (int i=1; i<=a.n; ++i) { cout << "\n" ; for (int j=1; j<=a.n; ++j) cout << a.a[i][j] << " "; } } main() { MT a; VT x,y; a.nhapsl(); x.nhapsl(); y=tich(a,x); cout << "\nMa tran A:"; in(a); cout << "\n\nVec to x: " ; in(x); cout << "\n\nVec y = Ax: " ; in(y); } 3.7. Dữ liệu thành phần tĩnh và hàm thành phần tĩnh 3.7.1. Dữ liệu thành phần tĩnh + Thành phần dữ liệu được khai báo bằng từ khoá static gọi là tĩnh, ví dụ: class A { private: Trang 55
  59. static int ts ; // Thành phần tĩnh int x; } ; + Thành phần tĩnh được cấp phát một vùng nhớ cố định. Nó tồn tại ngay cả khi lớp chưa có một đối tượng nào cả. + Thành phần tĩnh là chung cho cả lớp, nó không phải là riêng của mỗi đối tượng. Ví dụ xét 2 đối tượng: A u,v ; // Khai báo 2 đối tượng thì giữa các thành phần x và ts có sự khác nhau như sau: u.x và v.x có 2 vùng nhớ khác nhau u.ts và v.ts chỉ là một, chúng cùng biểu thị một vùng nhớ thành phần ts tồn tại ngay khi u và v chưa khai báo + Để biểu thị thành phần tĩnh, ta có thể dùng tên lớp, ví du: Đối với ts thì 3 cách viết sau là tương đương: A::ts u.ts v.ts + Khai báo và khởi gán giá trị cho thành phần tĩnh Thành phần tĩnh sẽ được cấp phát bộ nhớ và khởi gán giá trị ban đầu bằng một câu lệnh khai báo đặt sau định nghĩa lớp (bên ngoài các hàm, kể cả hàm main), theo các mẫu: int A::ts ; // Khởi gán cho ts giá trị 0 int A::ts = 1234; // Khởi gán cho ts giá trị 1234 Chú ý: Khi chưa khai báo thì thành phần tĩnh chưa tồn tại. Ví dụ xét chương trình sau: Ví dụ 3.8 #include Trang 56
  60. #include using namespace std; class HDBH { private: char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() { cout #include using namespace std; class HDBH { private: int shd; char *tenhang; double tienban; static int tshd; static double tstienban; public: Trang 57
  61. static void in() { cout <<"\n" << tshd; cout <<"\n" << tstienban; } }; int HDBH::tshd=5; double HDBH::tstienban=20000.0; main () { HDBH::in(); } 3.7.2. Hàm thành phần tĩnh Hàm thành phần tĩnh được viết theo một trong hai cách: - Dùng từ khoá static đặt trước định nghĩa hàm thành phần viết bên trong định nghĩa lớp. - Nếu hàm thành phần xây dựng bên ngoài định nghĩa lớp, thì dùng từ khoá static đặt trước khai báo hàm thành phần bên trong định nghĩa lớp. Không cho phép dùng từ khoá static đặt trước định nghĩa hàm thành phần viết bên ngoài định nghĩa lớp. Các đặc tính của hàm thành phần tĩnh: - Hàm thành phần tĩnh là chung cho toàn bộ lớp và không lệ thuộc vào một đối tượng cụ thể, nó tồn tại ngay khi lớp chưa có đối tượng nào. - Lời gọi hàm thành phần tĩnh như sau: Tên lớp :: Tên hàm thành phần tĩnh(các tham số thực sự) - Vì hàm thành phần tĩnh là độc lập với các đối tượng, nên không thể dùng hàm thành phần tĩnh để xử lý dữ liệu của các đối tượng trong lời gọi phương thức tĩnh. Nói cách khác không cho phép truy nhập các thuộc tính (trừ thuộc tính tĩnh) trong thân hàm thành phần tĩnh. Đoạn chương trình sau minh họa điều này: class HDBH { private: int shd; Trang 58
  62. char *tenhang; double tienban; static int tshd; static double tstienban; public: static void in() { cout #include using namespace std; class A { int m; static int n; //n la bien tinh public: void set_m(void) { m= ++n;} void show_m(void) { cout << "\n Doi tuong thu:" << m << endl; } static void show_n(void) { cout << " m = " << n << endl; } }; int A::n=1; //khoi gan gia tri ban dau bien tinh n main() { A t1, t2; t1.set_m(); t2.set_m(); A::show_n(); A t3; t3.set_m(); A::show_n(); t1.show_m(); t2.show_m(); t3.show_m(); Trang 59
  63. } Kết quả chương trình trên là: m = 3 m = 4 Doi tuong thu : 2 Doi tuong thu : 3 Doi tuong thu : 4 3.8. Hàm tạo (constructor) Hàm tạo là một hàm thành phần đặc biệt của lớp làm nhiệm vụ tạo lập một đối tượng mới. Chương trình dịch sẽ cấp phát bộ nhớ cho đối tượng, sau đó sẽ gọi đến hàm tạo. Hàm tạo sẽ khởi gán giá trị cho các thuộc tính của đối tượng và có thể thực hiện một số công việc khác nhằm chuẩn bị cho đối tượng mới. Khi xây dựng hàm tạo cần lưu ý những đặc tính sau của hàm tạo: - Tên hàm tạo trùng với tên của lớp. - Hàm tạo không có kiểu trả về. - Hàm tạo phải được khai báo trong vùng public. - Hàm tạo có thể được xây dựng bên trong hoặc bên ngoài định nghĩa lớp. - Hàm tạo có thể có tham số hoặc không có tham số. - Trong một lớp có thể có nhiều hàm tạo (cùng tên nhưng khác các tham số). Ví dụ 3.11 class DIEM { private: int x,y; public: DIEM() //Ham tao khong tham so { x = y = 0; Trang 60
  64. } DIEM(int x1, int y1) //Ham tao co tham so { x = x1;y=y1; } //Cac thanh phan khac }; Chú ý 1: Nếu lớp không có hàm tạo, chương trình dịch sẽ cung cấp một hàm tạo mặc định không đối, hàm này thực chất không làm gì cả. Như vậy một đối tượng tạo ra chỉ được cấp phát bộ nhớ, còn các thuộc tính của nó chưa được xác định. Ví dụ 3.12 #include #include using namespace std; class DIEM { private: int x,y; public: void in() { cout in(); } Chú ý 2: Khi một đối tượng được khai báo thì hàm tạo của lớp tương ứng sẽ tự động thực Trang 61
  65. hiện và khởi gán giá trị cho các thuộc tính của đối tượng. Dựa vào các tham số trong khai báo mà chương trình dịch sẽ biết cần gọi đến hàm tạo nào. Khi khai báo mảng đối tượng không cho phép dùng các tham số để khởi gán cho các thuộc tính của các đối tượng mảng. Câu lệnh khai báo một biến đối tượng sẽ gọi tới hàm tạo một lần. Câu lệnh khai báo một mảng n đối tượng sẽ gọi tới hàm tạo mặc định n lần. Với các hàm có các tham số kiểu lớp, thì chúng chỉ xem là các tham số hình thức, vì vậy khai báo tham số trong dòng đầu của hàm sẽ không tạo ra đối tượng mới và do đó không gọi tới các hàm tạo. Ví dụ 3.13: #include #include #include using namespace std; class DIEM { private: int x,y; public: DIEM() { x = y = 0; } DIEM(int x1, int y1) { x = x1; y = y1; } friend void in(DIEM d) { cout <<"\n" << d.x <<" " << d.y; } void in() { cout <<"\n" << x <<" " << y ; } }; Trang 62
  66. main() { DIEM d1; DIEM d2(2,3); DIEM *d; int i; d = new DIEM (5,6); in(d1); // Goi ham ban in() d2.in(); // Goi ham thanh phan in() in(*d); // Goi ham ban in() DIEM(2,2).in();// Goi ham thanh phan in() DIEM t[3]; // 3 lan goi ham tao khong doi DIEM *q; // Goi ham tao khong doi int n; cout > n; q = new DIEM [n+1]; //n+1 lan goi ham tao khong doi for ( i=0;i #include using namespace std; class DIEM { private: int x,y; public: Trang 63
  67. DIEM(int x1, int y1) { x=x1; y=y1; } void in() { cout #include using namespace std; class DIEM { private: int x,y; public: DIEM(int x1=0, int y1=0) { x = x1; y = y1; } void in() Trang 64
  68. { cout using namespace std; class Time { public: Time(int = 0, int = 0, int = 0); //Ham tao mac dinh void SetTime(int, int, int); void PrintMilitary(); void PrintStandard(); private: int Hour; int Minute; int Second; }; //Ham tao de khoi dong du lieu private //Cac gia tri mac dinh la 0 Time::Time(int Hr, int Min, int Sec) { SetTime(Hr, Min, Sec); } //Thiet lap cac gia tri cua Hour, Minute va Second //Gia tri khong hop le duoc thiet lap la 0 void Time::SetTime(int H, int M, int S) { Hour = (H >= 0 && H = 0 && M = 0 && S < 60) ? S : 0; } Trang 65
  69. //Hien thi thoi gian theo dang gio quan doi: HH:MM:SS void Time::PrintMilitary() { cout << (Hour < 10 ? "0" : "") << Hour << ":" << (Minute < 10 ? "0" : "") << Minute << ":" << (Second < 10 ? "0" : "") << Second; } //Hien thi thoi gian theo dang chuan: HH:MM:SS AM (hoac PM) void Time::PrintStandard() { cout << ((Hour == 0 || Hour == 12) ? 12 : Hour % 12) << ":" << (Minute < 10 ? "0" : "") << Minute << ":" << (Second < 10 ? "0" : "") << Second << (Hour < 12 ? " AM" : " PM"); } int main() { Time T1,T2(2),T3(21,34),T4(12,25,42); cout << "Ham tao voi:" << endl << "Tat ca tham so mac dinh:" << endl << " "; T1.PrintMilitary(); cout << endl << " "; T1.PrintStandard(); cout << endl << " Tham so cho gio. Phut va giay mac dinh:" << endl << " "; T2.PrintMilitary(); cout << endl << " "; T2.PrintStandard(); cout << endl << "Tham so cho gio, phut. Giay mac dinh:" << endl << " "; T3.PrintMilitary(); cout << endl << " "; T3.PrintStandard(); cout << endl << "Tham so cho gio , phut va giay:"<<endl<<" "; T4.PrintMilitary(); cout << endl << " "; T4.PrintStandard(); return 0; } Kết quả khi chạy chương trình Ham tao voi: Tat cat ham so mac dinh Trang 66
  70. 00:00:00 12:00:00 AM Tham so cho gio. Phut va giay mac dinh 02:00:00 2:00:00 AM Tham so cho gio, phut. Giay mac dinh 21:34:00 9:34:00 PM Tham so cho gio, phut va giay 12:25:42 12:25:42 PM 3.9. Hàm tạo sao chép 3.9.1. Hàm tạo sao chép mặc định Giả sử đã định nghĩa một lớp nào đó, ví dụ lớp PS (phân số). Khi đó: + Ta có thể dùng câu lệnh khai báo hoặc cấp phát bộ nhớ để tạo các đối tượng mới ví dụ: PS p1, p2 ; PS *p = new PS ; + Ta cũng có thể dùng lệnh khai báo để tạo một đối tượng mới từ một đối tượng đã tồn tại, ví dụ: PS u; PS v(u) ; // Tạo v theo u ý nghĩa của câu lệnh này như sau: Trang 67
  71. - Nếu trong lớp PS chưa xây dựng hàm tạo sao chép, thì câu lệnh này sẽ gọi tới một hàm tạo sao chép mặc định (của C++). Hàm này sẽ sao chép nội dung từng bit của u vào các bit tương ứng của v. Như vậy các vùng nhớ của u và v sẽ có nội dung như nhau. Rõ ràng trong đa số các trường hợp, nếu lớp không có các thuộc tính kiểu con trỏ hay tham chiếu, thì việc dùng các hàm tạo sao chép mặc định (để tạo ra một đối tượng mới có nội dung như một đối tượng cho trước) là đủ và không cần xây dựng một hàm tạo sao chép mới. - Nếu trong lớp PS đã có hàm tạo sao chép (cách viết sẽ nói sau) thì câu lệnh: PS v(u) sẽ tạo ra đối tượng mới v, sau đó gọi tới hàm tạo sao chép để khởi gán v theo u. Ví dụ sau minh hoạ cách dùng hàm tạo sao chép mặc định: Trong chương trình đưa vào lớp PS (phân số): + Các thuộc tính gồm: t (tử số) và m (mẫu). + Trong lớp không có phương thức nào cả mà chỉ có 2 hàm bạn là các hàm toán tử nhập (>>) và xuất ( #include using namespace std; class PS { private: int t,m ; public: friend ostream& operator<< (ostream& os,const PS &p) { os << " = " << p.t << "/" << p.m; return os; Trang 68
  72. } friend istream& operator>> (istream& is, PS &p) { cout > p.t >> p.m ; return is; } }; main() { PS d; cout > d; cout << "\n PS d " << d; PS u(d); cout << "\n PS u " << u; } 3.9.2. Hàm tạo sao chép Hàm tạo sao chép sử dụng một đối kiểu tham chiếu đối tượng để khởi gán cho đối tượng mới. Hàm tạo sao chép được viết theo mẫu: Tên_lớp (const Tên_lớp & dt) { // Các câu lệnh dùng các thuộc tính của đối tượng dt // để khởi gán cho các thuộc tính của đối tượng mới } Ví dụ có thể xây dựng hàm tạo sao chép cho lớp PS như sau: class PS { private: int t,m ; public: { Trang 69
  73. this->t = p.t ; this->m = p.m ; } } ; Khi nào cần xây dựng hàm tạo sao chép + Nhận xét: Hàm tạo sao chép trong ví dụ trên không khác gì hàm tạo sao chép mặc định. + Khi lớp không có các thuộc tính kiểu con trỏ hoặc tham chiếu, thì dùng hàm tạo sao chép mặc định là đủ + Khi lớp có các thuộc tính con trỏ hoặc tham chiếu, thì hàm tạo sao chép mặc định chưa đáp ứng được yêu cầu. Ví dụ lớp DT (đa thức) class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac he so da thuc // a0, a1, public: DT() { this->n=0; this->a=NULL; } DT(int n1) Trang 70
  74. { this->n=n1 ; this->a = new double[n1+1]; } friend ostream& operator > (istream& is,DT &d); } ; Bây giờ chúng ta hãy theo rõi xem việc dùng hàm tạo mặc định trong đoạn chương trình sau sẽ dẫn đến sai lầm như thế nào: DT d ; // Tạo đối tượng d kiểu DT cin >> d ; /* Nhập đối tượng d , gồm: nhập một số nguyên dương và gán cho d.n, cấp phát vùng nhớ cho d.a, nhập các hệ số của đa thức và chứa vào vùng nhớ được cấp phát */ DT u(d) ; /* Dùng hàm tạo mặc định để xây dựng đối tượng u theo d Kết quả: u.n = d.n và u.a = d.a. Như vậy 2 con trỏ u.a và d.a cùng trỏ đến một vùng nhớ. */ Nhận xét: Mục đích của ta là tạo ra một đối tượng u giống như d, nhưng độc lập với d. Nghĩa là khi d thay đổi thì u không bị ảnh hưởng gì. Thế nhưng mục tiêu này không đạt được, vì u và d có chung một vùng nhớ chứa hệ số của đa thức, nên khi sửa đổi các hệ số của đa thức trong d thì các hệ số của đa thức trong u cũng thay đổi theo. Còn một trường hợp nữa cũng dẫn đến lỗi là khi một trong hai đối tượng u và d bị giải phóng (thu hồi vùng nhớ chứa đa thức) thì đối tượng còn lại cũng sẽ không còn vùng nhớ nữa. Ví dụ sau sẽ minh họa nhận xét trên: Khi d thay đổi thì u cũng thay đổi và Trang 71
  75. ngược lại khi u thay đổi thì d cũng thay đổi theo. Ví dụ 3.18: #include #include #include using namespace std; class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac he //so da thuc a0,a1, public: DT() { this->n=0; this->a=NULL; } DT(int n1) { this->n=n1; this->a= new double[n1+1]; } friend ostream& operator > (istream& is,DT &d); }; ostream& operator > (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout >d.n; d.a = new double[d.n+1]; cout<<"Nhap cac he so da thuc:\n"; for (int i=0 ; i<=d.n ; ++i) Trang 72
  76. { cout > d.a[i]; } return is; } main() { DT d; cout > d; DT u(d); cout > d; cout > u; cout n = d.n; this->a = new double[d.n+1]; for (int i=0;i a[i] = d.a[i]; Trang 73
  77. } Chương trình sau sẽ minh hoạ điều này: Sự thay đổi của d không làm ảnh hưởng đến u và ngược lại sự thay đổi của u không làm ảnh hưởng đến d Ví dụ 3.19 : Viết hàm tạo sao chép cho lớp DT #include #include #include using namespace std; class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac da thuc a0, a1, public: DT() { this->n=0; this->a=NULL; } DT(int n1) { this->n=n1; this->a= new double[n1+1]; } DT(const DT &d); friend ostream& operator > (istream& is,DT &d); }; DT::DT(const DT &d) { this->n = d.n; this->a = new double[d.n+1]; for (int i=0;i a[i] = d.a[i]; } ostream& operator<< (ostream& os,const DT &d) { os<<"-Cac he so (tu ao): "; for (int i=0 ; i<=d.n ; ++i) Trang 74
  78. os > (istream& is,DT &d) { if (d.a != NULL) delete d.a; cout > d.n; d.a = new double[d.n+1]; cout > d.a[i]; } return is; } main() { DT d; cout > d; DT u(d); cout > d; cout > u; cout << "\nDa thuc d " << d; cout << "\nDa thuc u " << u; } 3.10. Hàm hủy (destructor) 3.10.1. Công dụng của hàm huỷ Hàm huỷ là một hàm thành viên của lớp (phương thức) có chức năng ngược với hàm tạo. Hàm huỷ được gọi trước khi giải phóng (xoá bỏ) một đối tượng để thực hiện một số công việc có tính “dọn dẹp” trước khi đối tượng được huỷ bỏ, ví dụ như giải phóng một vùng nhớ mà đối tượng đang quản lý, xoá đối tượng khỏi màn hình nếu như nó đang hiển thị, Trang 75
  79. Việc huỷ bỏ một đối tượng thường xẩy ra trong 2 trường hợp sau: + Trong các toán tử và các hàm giải phóng bộ nhớ, như delete, free, + Giải phóng các biến, mảng cục bộ khi thoát khỏi hàm, phương thức. 3.10.2. Hàm huỷ mặc định Nếu trong lớp không định nghĩa hàm huỷ, thì một hàm huỷ mặc định không làm gì cả được phát sinh. Đối với nhiều lớp thì hàm huỷ mặc định là đủ, và không cần đưa vào một hàm huỷ mới. 3.10.3. Quy tắc viết hàm huỷ Mỗi lớp chỉ có một hàm huỷ viết theo các quy tắc sau: + Kiểu của hàm: Hàm huỷ cũng giống như hàm tạo là hàm không có kiểu, không có giá trị trả về. + Tên hàm: Tên của hàm huỷ gồm một dẫu ngã (đứng trước) và tên lớp: ~Tên_lớp + Đối: Hàm huỷ không có đối Ví dụ 3.20: Có thể xây dựng hàm huỷ cho lớp DT (đa thứcnhư sau: class DT { private: int n; // Bac da thuc double *a; // Tro toi vung nho chua cac he so da thuc a0, a1, public: ~DT() { this->n=0; delete this->a; } } ; Trang 76
  80. Bài tập 1. Xây dựng lớp thời gian Time. Dữ liệu thành phần bao gồm giờ, phút giây. Các hàm thành phần bao gồm: hàm tạo, hàm truy cập dữ liệu, hàm normalize() để chuẩn hóa dữ liệu nằm trong khoảng quy định của giờ (0 giờ < 24) , phút (0 phút <60), giây (0 giây <60), hàm advance(int h, int m, int s) để tăng thời gian hiện hành của đối tượng đang tồn tại, hàm reset(int h, int m, int s) để chỉnh lại thời gian hiện hành của một đối tượng đang tồn tại và một hàm print() để hiển thị dữ liệu. 2. Xây dựng lớp Date. Dữ liệu thành phần bao gồm ngày, tháng, năm. Các hàm thành phần bao gồm: hàm tạo, hàm truy cập dữ liệu, hàm normalize() để chuẩn hóa dữ liệu nằm trong khoảng quy định của ngày (1 ngày <daysIn(tháng)), tháng (1 tháng < 12), năm (năm 1), hàm daysIn(int) trả về số ngày trong tháng, hàm advance(int y, int m, int d) để tăng ngày hiện lên các năm y, tháng m, ngày d của đối tượng đang tồn tại, hàm reset(int y, int m, int d) để đặt lại ngày cho một đối tượng đang tồn tại và một hàm print() để hiển thị dữ liệu. 3. Thực hiện một lớp String. Mỗi đối tượng của lớp sẽ đại diện một chuỗi ký tự. Những thành phần dữ liệu là chiều dài chuỗi, và chuỗi ký tự. Các hàm thành phần bao gồm: hàm tạo, hàm truy cập, hàm hiển thị, hàm character(int i)trả về một ký tự trong chuỗi được chỉ định bằng tham số i. 4. Xây dựng lớp ma trận có tên là Matrix cho các ma trận, các hàm thành phần bao gồm: hàm tạo mặc định, hàm nhập xuất ma trận, cộng, trừ, nhân hai ma trận. 5. Xây dựng lớp ma trận có tên là Matrix cho các ma trận vuông, các hàm thành phần bao gồm: hàm tạo mặc định, hàm nhập xuất ma trận, tính định thức và tính ma trận nghịch đảo. 6. Xây dựng lớp Stack cho ngăn xếp kiểu int. Các hàm thành phần bao gồm: Hàm tạo mặc định, hàm hủy, hàm isEmpty() kiểm tra stack có rỗng không, hàm isFull() kiểm tra stack có đầy không, hàm push() , pop(), hàm in nội dung ngăn xếp. Sử dụng một mảng để thực hiện. Trang 77
  81. 7. Xây dựng lớp hàng đợi Queue chứa phần tử kiểu int. Các hàm thành phần bao gồm: hàm tạo, hàm hủy và những toán tử hàng đợi thông thường: hàm insert() để thêm phần tử vào hàng đợi, hàm remove() để loại bỏ phần tử, hàm isEmpty() kiểm tra hàng đợi có rỗng không, hàm isFull() kiểm tra hàng đợi có đầy không. Sử dụng một mảng để thực hiện. 8. Xây dựng lớp Sinhvien để quản lý hộ tên sinh viên, năm sinh, điểm thi 9 môn học của các sinh viên. Cho biết sinh viên nào được làm khóa luận tốt nghiệp, bao nhiêu sinh viên thi tốt nghiệp, bao nhiêu sinh viên thi lại, tên môn thi lại> Tiêu chuẩn để xét như sau: - Sinh viên làm khóa luận phải có điểm trung bình từ 7 trở lên, trong đó không có môn nào dưới 5. - Sinh viên thi tốt nghiệp khi điểm trung bình nhỏ hơn 7 và điểm các môn không dưới 5. - Sinh viên thi lại môn dưới 5. 9. Xây dựng lớp vector để lưu trữ các vectơ gồm các số thực. Các thành phần dữ liệu bao gồm: - Kích thước vectơ. - Một mảng động chứa các thành phần của vectơ. Các hàm thành phần bao gồm hàm tạo, hàm hủy, hàm tính tích vô hướng hai vectơ, tính chuẩn của vectơ (theo chuẩn bất kỳ nào đó). 10. Xây dựng lớp Phanso với dữ liệu thành phần là tử và mẫu số. Các hàm thành phần bao gồm: - Cộng hai phân số, kết quả phải dược tối giản - Trừ hai phân số, kết quả phải dược tối giản - Nhân hai phân số, kết quả phải dược tối giản - Chia hai phân số, kết quả phải dược tối giản Trang 78
  82. CHƯƠNG 4 : TOÁN TỬ TẢI BỘI Chương 4 trình bày các vấn đề sau:  Định nghĩa toán tử tải bội  Một số lưu ý khi xây dựng toán tử tải bội 4.1. Định nghĩa toán tử tải bội Các toán tử cùng tên thực hiện nhiều chức năng khác nhau được gọi là toán tử tải bội. Dạng định nghĩa tổng quát của toán tử tải bội như sau: Kiểu_trả_về operator op(danh sách tham số) {//thân toán tử} Trong đó: Kiểu_trả_về là kiểu kết quả thực hiện của toán tử. op là tên toán tử tải bội operator op(danh sách tham số) gọi là hàm toán tử tải bội, nó có thể là hàm thành phần hoặc là hàm bạn, nhưng không thể là hàm tĩnh. Danh sách tham số được khai báo tương tự khai báo biến nhưng phải tuân theo những quy định sau: - Nếu toán tử tải bội là hàm thành phần thì: không có tham số cho toán tử một ngôi và một tham số cho toán tử hai ngôi. Cũng giống như hàm thành phần thông thường, hàm thành phần toán tử có đối đầu tiên (không tường minh) là con trỏ this . - Nếu toán tử tải bội là hàm bạn thì: có một tham số cho toán tử một ngôi và hai tham số cho toán tử hai ngôi. - Các phương thức toán tử một toán hạng: Dùng ngay con trỏ this để biểu thị toán hạng duy nhất này, nên trong phương thức sẽ không có đối tường minh. - Các phương thức toán tử hai toán hạng: Con trỏ this ứng với toán hạng thứ nhất, nên trong phương thức chỉ cần dùng một đối tường minh để biểu thị toán hạng thứ hai. Quá trình xây dựng toán tử tải bội được thực hiện như sau: - Định nghĩa lớp để xác định kiểu dữ liệu sẽ được sử dụng trong các toán tử tải Trang 79
  83. bội - Khai báo hàm toán tử tải bội trong vùng public của lớp - Định nghĩa nội dung cần thực hiện 4.2. Một số lưu ý khi xây dựng toán tử tải bội Trong C++ ta có thể đưa ra nhiều định nghĩa mới cho hầu hết các toán tử trong C++, ngoại trừ những toán tử sau đây: - Toán tử xác định thành phần của lớp (‘.’) - Toán tử phân giải miền xác định (‘::’) - Toán tử xác định kích thước (‘sizeof’) - Toán tử điều kiện 3 ngôi (‘?:’) Mặc dầu ngữ nghĩa của toán tử được mở rộng nhưng cú pháp, các quy tắc văn phạm như số toán hạng, quyền ưu tiên và thứ tự kết hợp thực hiện của các toán tử vẫn không có gì thay đổi. Không thể thay đổi ý nghĩa cơ bản của các toán tử đã định nghĩa trước, ví dụ không thể định nghĩa lại các phép toán +, - đối với các số kiểu int, float. Các toán tử = , ( ) , [ ] , -> yêu cầu hàm toán tử phải là hàm thành phần của lớp, không thể dùng hàm bạn để định nghĩa toán tử tải bội. 4.3. Một số ví dụ Ví dụ 4.1 Toán tử tải bội một ngôi, dùng hàm bạn #include #include using namespace std; class Diem { private: float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) Trang 80
  84. { x = x1; y = y1; z=z1;} friend Diem operator -(Diem d) { Diem d1; d1.x = -d.x; d1.y = -d.y;d1.z=-d.z; return d1; } void hienthi() { cout #include using namespace std; class Diem { private: float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1;z = z1;} Diem operator -() { x = -x; y = -y; z = -z; return (*this); } void hienthi() { cout<<"Toa do: x="<<x<<" y= "<<y <<" z = "<<z <<" \n";} }; main() { Diem p(2,3,-4),q; p.hienthi(); q = -p; q.hienthi(); Trang 81
  85. } Ví dụ 4.3 Toán tử tải bội hai ngôi, dùng hàm bạn #include #include using namespace std; class Diem { private: float x,y,z; public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1; z=z1;} friend Diem operator +(Diem d1, Diem d2) { Diem tam; tam.x = d1.x + d2.x; tam.y = d1.y + d2.y; tam.z = d1.z + d2.z; return tam; } void hienthi() { cout #include using namespace std; class Diem { private: float x,y,z; Trang 82
  86. public: Diem() {} Diem(float x1,float y1,float z1) { x = x1; y = y1; z=z1;} Diem operator +(Diem d2) { x = x + d2.x; y = y + d2.y; z = z + d2.z; return (*this); } void hienthi() { cout #include using namespace std; class Diem { private: int x,y; public: Diem() {} Diem(int x1,int y1) { x = x1; y = y1;} void operator -() { x = -x; y = -y; } void hienthi() { cout<<"Toa do: "<<x<<" "<<y <<" \n";} Trang 83
  87. }; main() { Diem p(2,3); p.hienthi(); -p; p.hienthi(); } Ví dụ 4.6 Toán tử tải bội hai ngôi #include #include using namespace std; class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} sophuc operator +(sophuc c2) { sophuc c3; c3.a= a + c2.a ; c3.b= b + c2.b ; return (c3); } void hienthi(sophuc c) { cout<<c.a<<" + "<<c.b<<"i"<<endl; } }; main() { sophuc d1 (2.1,3.4); sophuc d2 (1.2,2.3) ; sophuc d3 ; d3 = d1+d2; //d3=d1.operator +(d2); cout<<"d1= ";d1.hienthi(d1); cout<<"d2= ";d2.hienthi(d2); cout<<"d3= ";d3.hienthi(d3); } Chú ý: Trong các hàm toán tử thành phần hai ngôi (có hai toán hạng) thì con trỏ this ứng với toán hạng thứ nhất, vì vậy trong tham số của toán tử chỉ cần dùng một tham số tường minh để biểu thị toán hạng thứ hai . Trang 84
  88. Ví dụ 4.7 Phiên bản 2 của ví dụ 4.6 #include #include using namespace std; class sophuc { float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} sophuc operator +(sophuc c2) { a= a + c2.a ; b= b + c2.b ; return (*this); } void hienthi(sophuc c) { cout #include using namespace std; class sophuc {float a,b; public : sophuc() {} sophuc(float x, float y) {a=x; b=y;} friend sophuc operator +(sophuc c1,sophuc c2) { sophuc c; c.a= c1.a + c2.a ; Trang 85
  89. c.b= c1.b + c2.b ; return (c); } void hienthi(sophuc c) { cout #include #include using namespace std; class String { char s[80]; public: String() { *s='\0'; } String(char *p) { strcpy(s,p); } char *get() { return s;} String operator + (String s2); String operator = (String s2); int operator (String s2); int operator == (String s2); }; String String::operator +(String s2) { strcat(s,s2.s); return *this ; } String String::operator =(String s2) { strcpy(s,s2.s) ; Trang 86
  90. return *this; } int String::operator (String s2) { return strcmp(s,s2.s)>0 ; } int String::operator ==(String s2) { return strcmp(s,s2.s)==0 ; } main() { String s1 ("Trung Tam "), s2 (" Tin hoc"), s3; cout s2) cout s2 \n"; if (s1 < s2) cout << "s1 < s2 \n"; if (s1 == s2) cout << "s1 bang s3 \n"; s3=s1+s2; cout<<"s3 ="<<s3.get()<<'\n'; //Trung tam tin hoc s3=s2; cout<<"s2 = "<<s2.get()<<'\n'; //Tin hoc cout<<"s3 = "<<s3.get()<<'\n'; //Tin hoc if (s2 == s3) cout << "s2 bang s3 \n"; } 4.4. Định nghĩa chồng các toán tử ++ , Ta có thể định nghĩa chồng cho các toán tử ++/ theo quy định sau: - Toán tử ++/ dạng tiền tố trả về một tham chiếu đến đối tượng thuộc lớp. - Toán tử ++/ dạng hậu tố trả về một đối tượng thuộc lớp Ví dụ 4.10 Trang 87
  91. #include #include using namespace std; class Diem { private: int x,y; public: Diem() {x = y = 0;} Diem(int x1, int y1) {x = x1; y = y1;} Diem & operator ++(); //toan tu ++ tien to Diem operator ++(int); //toan tu ++ hau to Diem & operator (); //toan tu tien to Diem operator (int); //toan tu hau to void hienthi() { cout<<" x = "<<x<<" y = "<<y; } }; Diem & Diem::operator ++() { x++; y++; return (*this); } Diem Diem::operator ++(int) { Diem temp = *this; ++*this; return temp; } Diem & Diem::operator () { x ; y ; return (*this); } Diem Diem::operator (int) { Diem temp = *this; *this; Trang 88
  92. return temp; } main() { Diem d1(5,10),d2(20,25),d3(30,40),d4(50,60); cout<<"\nd1 : ";d1.hienthi(); ++d1; cout<<"\n Sau khi tac dong cac toan tu tang truoc :"; cout<<"\nd1 : ";d1.hienthi(); cout<<"\nd2 : ";d2.hienthi(); d2++; cout<<" \n cac toan tu tang sau"; cout<<"\nd2 : ";d2.hienthi(); cout<<"\nd3 : ";d3.hienthi(); d3; cout<<"\n cac toan tu giam truoc :"; cout<<"\nd3 : ";d3.hienthi(); cout<<"\nd4 : ";d4.hienthi(); d4 ; cout<<"\n cac toan tu giam sau : "; cout<<"\nd4 : ";d4.hienthi(); } Chương trình cho kết quả như sau: d1 : x = 5 y = 10 Cac toan tu tang truoc : d1 : x = 6 y = 11 d2 : x = 20 y = 25 Cac toan tu tang sau d2 : x = 21 y = 26 d3 : x = 30 y = 40 Cac toan tu giam truoc : d3 : x = 29 y = 39 d4 : x = 50 y = 60 Trang 89
  93. Cac toan tu giam sau : d4 : x = 49 y = 59 Chú ý: Đối số int trong dạng hậu tố là bắt buộc, dùng để phân biệt với dạng tiền tố, thường nó mang trị mặc định là 0. 4.5. Định nghĩa chồng toán tử > Ta có thể định nghĩa chồng cho hai toán tử vào/ra > kết hợp với cout và cin (cout >), cho phép các đối tượng đứng bên phải chúng. Lúc đó ta có thể thực hiện các thao tác vào ra như nhập dữ liệu từ bàn phím cho các đối tượng, hiển thị giá trị thành phần dữ liệu của các đối tượng ra màn hình. Hai hàm toán tử > phải là hàm tự do và khai báo là hàm bạn của lớp. Ví dụ 4.11 #include #include using namespace std; class SO { private: int giatri; public: SO(int x=0) { giatri = x; } SO (SO &tso) { giatri = tso.giatri; } friend istream& operator>>(istream&,SO&); friend ostream& operator >so1; cin>>so2; Trang 90
  94. cout >(istream& nhap,SO& so) { cout > so.giatri; return nhap; } ostream& operator<<(ostream& xuat,SO& so) { xuat<< so.giatri; return xuat; } Trang 91
  95. Bài tập 1. Định nghĩa các phép toán tải bội =, ==, ++, , +=, > trên lớp Time (bài tập 1 chương 3). 2. Định nghĩa các phép toán tải bội =, ==, ++, , +=, > trên lớp Date (bài tập 2 chương 3). 1. Định nghĩa các phép toán tải bội +, -, *, =, ==, != trên lớp các ma trận vuông. 2. Định nghĩa các phép toán tải bội +, -, * trên lớp đa thức. 3. Định nghĩa các phép toán tải bội +, -, *, /, =, ==, +=, -=, *=, /= , , =, != , ++, trên lớp Phanso (bài tập 10 chương 3). 4. Ma trận được xem là một vectơ mà mỗi thành phần của nó là một vectơ. Theo nghĩa đó, hãy định nghĩa lớp Matran dựa trên vectơ. Tìm cách để chương trình dịch hiểu được phép truy nhập m[i][j], trong đó m là một đối tượng thuộc lớp Matran Trang 92
  96. CHƯƠNG 5: KẾ THỪA Chương 5 trình bày các vấn đề sau:  Đơn kế thừa, đa kế thừa  Hàm tạo và hàm hủy đối với sự kế thừa  Hàm ảo, lớp cơ sở ảo 5.1. Giới thiệu Kế thừa là một trong các khái niệm cơ sở của phương pháp lập trình hướng đối tượng. Tính kế thừa cho phép định nghĩa các lớp mới từ các lớp đã có. Một lớp có thể là lớp cơ sở cho nhiều lớp dẫn xuất khác nhau. Lớp dẫn xuất sẽ kế thừa một số thành phần (dữ liệu và hàm) của lớp cơ sở, đồng thời có thêm những thành phần mới. Một lớp được xây dựng thừa kế một lớp khác gọi là lớp dẫn xuất. Lớp dùng để xây dựng lớp dẫn xuất gọi là lớp cơ sở. Lớp nào cũng có thể là một lớp cơ sở. Hơn thế nữa, một lớp có thể là cơ sở cho nhiều lớp dẫn xuất khác nhau. Đến lượt mình, lớp dẫn xuất lại có thể dùng làm cơ sở để xây dựng các lớp dân xuất khác. Ngoài ra một lớp có thể dẫn xuất từ nhiều lớp cơ sở. Có hai loại kế thừa là: đơn kế thừa và đa kế thừa, có thể minh họa qua các hình vẽ sau đây: A B Hình 5.1. Đơn kế thừa, lớp A là lớp cơ sở của lớp B Trang 93
  97. A A A B C A A B C B B C D D C B C D D (a) (b) (c) Hình 5.2. Đa kế thừa Hình (a): Lớp A là lớp cơ sở của lớp B, lớp B là lớp cơ sở của lớp C Hình (b): Lớp A là lớp cơ sở của các lớp B, C, D Hình (c): Lớp A, B, C là lớp cơ sở của lớp D Tính thừa kế: Một lớp dẫn xuất ngoài các thành phần của riêng nó, nó còn được thừa kế tất cả các thành phần của các lớp cơ sở có liên quan. Ví dụ trong hình 5.2 (a) thì lớp C được thừa kế các thành phần của các lớp B và A. Trong hình 5.2 (c) thì lớp D được thừa kế các thành phần của các lớp A, B và C. 5.2. Đơn kế thừa 5.2.1. Định nghĩa lớp dẫn xuất từ một lớp cơ sở Giả sử đã định nghĩa lớp A. Cú pháp để xây dựng lớp B dẫn xuất từ lớp A như sau: class B: mode A { private: // Khai báo các thuộc tính của lớp B public: Trang 94
  98. // Định nghĩa các hàm thành phần của lớp B }; Trong đó mode có thể là public, protected hoặc private. Mặc định là private - Kế thừa public, các thành phần public và protected của lớp cơ sở được kế thừa như là các thành phần public và protected của lớp dẫn xuất tương ứng. - Kế thừa theo kiểu protected thì tất cả các thành phần public và protected của lớp cơ sở sẽ trở thành các thành phần protected của lớp dẫn xuất. - Kế thừa theo kiểu private thì tất cả các thành phần public và protected của lớp cơ sở sẽ trở thành các thành phần private của lớp dẫn xuất Chú ý: Trong cả các trường hợp ở trên thì thành phần private của lớp cơ sở là không được kế thừa. Như vậy trong lớp dẫn xuất không cho phép truy nhập đến các thành phần private của lớp cơ sở. Bảng 5.1 Bảng tầm vực kế thừa Tầm vực Kế thừa public Kế thừa protected Kế thừa private public public protected private protected protected protected private Không thể truy private Không thể truy xuất Không thể truy xuất xuất 5.2.2. Truy nhập các thành phần trong lớp dẫn xuất Thành phần của lớp dẫn xuất bao gồm: các thành phần khai báo trong lớp dẫn xuất và các thành phần mà lớp dẫn xuất thừa kế từ các lớp cơ sở. Quy tắc sử dụng các thành phần trong lớp dẫn xuất được thực hiện theo theo mẫu như sau: Trang 95