Hướng dẫn lập trình hướng đối tượng với C++ doc

47 327 0
Hướng dẫn lập trình hướng đối tượng với C++ doc

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

Hướng dẫn lập trình hướng đối tượng với C++ Chào mọi người trong congdongcviet. Mình là mem mới, mới vào diễn đàn không lâu. Thực ra hồi trước lúc đang học C cũng có vào diễn đàn rồi nhưng chủ yếu mục đích là vào “chôm” tài liệu và có thắc mắc gì thì nhờ các cao thủ trợ giúp chứ cũng chả pốt piếc gì hết Mình thấy mọi người hướng dẫn rất nhiệt tình, thậm chí ngay cả bác Ác-min lúc nào cũng thấy online trợ giúp mọi người. Nghĩ lại thấy mình cũng “tư lợi” quá, chỉ nghĩ đến bản thân. Haizzz … bây giờ thấy lương tâm nó cắn rứt quá, hix hix . Dạo này mới tập tẹ học lập trình hướng đối tượng (bằng C++), thấy cũng hay hay, hiểu hiểu nên muốn viết mấy bài chia sẻ những gì mình học được về OOP cũng như về C++, gọi là đóng góp chút gì đó cho lương tâm nó đỡ cắn rứt. Hy vọng giúp ích cho một số bạn. Mình nói trước là mình cũng mới học thôi nên biết gì viết nấy, nếu có gì sai sót mong mọi người tham gia góp ý. Đây là bài đầu tiên BÀI 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ORIENTED PROGRAMMING Tất cả các ngôn ngữ lập trình đều sinh ra để hỗ trợ một hoặc một số phong cách lập trình hay một mô hình lập trình nào đó (programming paradigm). Vì vậy trước khi bắt tay vào học ngôn ngữ ta nên tìm hiểu sơ lược về mô hình lập trình được ngôn ngữ hỗ trợ mà ta dự định viết chương trình theo mô hình đó. Cụ thể, nói “lập trình hướng đối tượng với C++” thì ta phải biết sơ sơ về hướng đối tượng trước khi “ngâm cứu” C+ +. Vì vậy bài đầu tiên này mình muốn dành để nói về lập trình hướng đối tượng là gì, và quan điểm của giới lập trình về nó như thế nào, tại sao nó lại là một mô hình tiên tiến và bạn sẽ không phải hối hận khi bỏ thời gian và công sức ra để học nó. Ngày xửa ngày xưa, khoảng ba chục năm về trước, quy mô các của các dự án phần mềm còn nhỏ, các lập trình viên gần như có thể viết ngay được chương trình mà không cần suy nghĩ nhiều (giả sử rằng không có lập trình viên nào bị thiểu năng về trí tuệ ). Thời đó lập trình cấu trúc (structured programming) hay còn gọi lập trình thủ tục (procedural programing) là kỹ thuật lập trình chủ yếu. Tớ sẽ nói sơ qua một chút về kỹ thuật này (trong phạm vi hiểu biết). Theo quan điểm của lập trình cấu trúc, người ta xem chương trình là một “công việc lớn” cần phải xử lý. Để giải quyết “công việc lớn” này, người ta tìm cách chia thành các phần công việc nhỏ hơn và mỗi phần này sẽ được quẳng cho một hàm đảm nhiệm. Chương trình chính sẽ gọi đến mỗi hàm vào những thời điểm cần thiết. Trong mỗi hàm, nếu như phần công việc vẫn còn lớn, thì ta lại chia nhỏ tiếp cho tới khi vấn đề trở nên đủ đơn giản. Và dĩ nhiên để giải quyết những phần con đó ta cũng phải quẳng chúng cho các hàm tương ứng. Quá trình này được gọi là “làm mịn” hay “tinh chế từng bước” (stepwise refinement). Việc trao đổi dữ liệu giữa các hàm được thực hiện thông qua việc truyền đối số hoặc các biến, mảng toàn cục. Như vậy có thể coi chương trình là một tập hợp các hàm được thiết kế để xử lý các phần công việc được giao. Các ngôn ngữ lập trình hướng thủ tục thường gặp là C, Pascal, FORTRAN … và cả C++. Tuy nhiên C++ còn được thiết kế để hỗ trợ cả lập trình hướng đối tượng nữa. Một chương trình viết theo hướng cấu trúc sẽ tập trung vào quá trình xử lý. Nghĩa là mỗi câu lệnh chỉ dẫn cho máy tính làm một việc gì đó, kiểu như: nhận 2 số nguyên từ bàn phím, cộng chúng lại với nhau, rồi đem chia đôi, hiển thị kết quả lên màn hình. Một chương trình là một tập các chỉ dẫn. Lập trình cấu trúc tỏ ra khá hiệu quả khi quy mô chương trình còn nhỏ, nhưng khi quy mô chương trình lớn dần lên và phức tạp hơn thì nó bộc lộ nhiều khiếm khuyết. Có thể nêu ra một số vấn đề sau: 1. Trọng tâm vào “hành động” hơn là “dữ liệu”: thực tế dữ liệu là cái tối thượng mà chúng ta quan tâm. Mọi chương trình đều nhằm mục đích nhét dữ liệu vào input rồi chờ đợi kết quả ở output. Rõ ràng mục đích của ta là dữ liệu đầu ra, mặc kệ chương trình nó muốn xử lý cái gì thì xử lý, ta chỉ quan tâm đến kết quả đầu ra có đạt yêu cầu hay không. Tuy nhiên lập trình cấu trúc quá chú trọng đến việc thiết kế các hàm (hành động) mà xem nhẹ dữ liệu, đây là hạn chế thứ nhất. 2. Tính bảo mật của dữ liệu không cao: (nếu như không muốn nói là không có). Dữ liệu trong chương trình gần như là của chung, và có thể dễ dàng truy cập hay sửa đổi một cách vô tội vạ. Những hàm không phận sự cũng có thể tọc mạch vào vùng dữ liệu mà nó “chằng liên quan” và sửa đổi nó . Điều này làm chương trình rất dễ phát sinh lỗi đặc biệt là những “lỗi tinh vi” hoặc “lỗi logic”. Và khi có lỗi thì rất khó debug vì phạm vi khoanh vùng là rất rộng (vì ai cũng có thể tọc mạch vào dữ liệu nên không biết nghi cho thằng nào). Đây là hạn chế thứ hai. 3. Tách rời dữ liệu với “hành động” liên quan: không phải tất cả các hàm được viết ra để dùng cho tất cả dữ liệu, và ngược lại. Mỗi nhóm dữ liệu chỉ sử dụng một nhóm các hàm “dành riêng cho chúng”. Trong lập trình, việc “đóng gói” dữ liệu và hàm liên quan được gọi là “mô-đun hóa” (modularization). Điều này có hai cái lợi. Thứ nhất, các hàm và dữ liệu được nhóm lại với nhau nên “gọn gàng” hơn và dễ kiểm soát hơn. Thứ hai, thông thường chỉ những hàm trong khối mới có thể truy nhập vào dữ liệu của khối. Do đó hạn chế sự tọc mạch từ bên ngoài, tính bảo mật dữ liệu cao hơn, hạn chế lỗi và phạm vi khoanh vùng lỗi sẽ được thu hẹp. Tuy nhiên, lập trình cấu trúc không làm được điều này. Đây là hạn chế thứ ba. 4. Phụ thuộc nặng nề vào cấu trúc dữ liệu và thuật toán: minh chứng cho điều này là câu nói nổi tiếng của bác Niklaus Wirth (creator of Pascal): Algorithms + Data Structures = Programs. Cũng xin nói thêm mô hình lập trình hướng cấu trúc được dựa trên mô hình toán học của Bohm và Guiseppe (nói thật là mình không biết hai bác này ), theo đó, một chương trình máy tính đều có thể viết dựa trên ba cấu trúc là: tuần tự (sequence), lựa chọn hay rẽ nhánh (selection) và lặp (repetition). Vì vậy một chương trình được xem là một chuỗi các hành động liên tiếp để đi đến kết quả cuối cùng. Và việc thiết kế chương trình phụ thuộc nặng nề vào việc dùng giải thuật gì và tổ chức dữ liệu như thế nào. Điều này làm cho việc thiết kế là rất “không tự nhiên” vì nó làm cho quá trình thiết kế phụ thuộc vào cài đặt và khi quy mô chương trình lớn dần lên sẽ rất khó triển khai. Đồng thời khi có thay đổi về cấu trúc dữ liệu hoặc nâng cấp chương trình gần như ta phải viết lại hầu hết các hàm liên quan và sửa đổi lại thuật toán vì mỗi cấu trúc dữ liệu chỉ phù hợp với một số thuật toán nhất định. Đây là hạn chế thứ tư. 5. Không tận dụng được mã nguồn: mặc dù hàm là một phát minh quan trọng để tăng cường khả năng sử dụng lại mã nguồn, tuy nhiên trong lập trình cấu trúc điều này không triệt để. Ta vẫn phải viết lại những đoạn code hao hao giống nhau để thực hiện những công việc tương tự nhau. Ví dụ: trong C, hàm hàm int min(int x, int y) có nhiệm vụ tính toán và trả về min trong hai số nguyên được truyền vào, còn hàm float min(float x, float y) cũng làm nhiệm vụ tương tự nhưng là với số thực. Rõ ràng nội dung hai hàm này là giống nhau đến 99%, có khác thì chỉ khác mỗi kiểu int và float, thế nhưng trong C ta vẫn phải viết hai hàm khác nhau. Trong C++, với định hướng đối tượng ta có thể viết một hàm dùng để dùng cho mọi kiểu int, float, double. Ngoài ra còn nhiều điểm mạnh khác mà OOP mang lại để tận dụng tối đa khả năng sử dụng lại mã nguồn như tính kế thừa (inheritance), đa hình (polymorphism). Đây là hạn chế thứ năm của lập trình cấu trúc. Nói chung mình chỉ mới bới ra được có thế thôi, ai biết thêm cái nào thì bổ sung nhé. Rõ ràng với nhiều hạn chế như vậy thì lập trình cấu trúc không phải là giải phải pháp tốt. Và những nỗ lực để vá những lỗ hổng này dẫn đến sự ra đời của một kỹ thuật lập trình mới lập trình hướng đối tượng (object oriented programming – OOP). Mình cũng nói sơ qua một chút về OOP. Khác với lập trình cấu trúc, OOP coi chương trình là tập hợp của các đối tượng có quan hệ nào đó với nhau. Mỗi đối tượng có dữ liệu và phương thức của riêng mình. Ví dụ một đối tượng Human sẽ có các dữ liệu như: tên, ngày sinh, tuổi, số chứng minh nhân dân, nghề nghiệp, … blah blah … và được đóng gói cùng các phương thức đi kèm ví dụ phương thức set_name() sẽ cho phép nhập tên , get_name() sẽ cho phép lấy tên của đối tượng, tương tự ta cũng cho các phương thức như set_ID(), get_ID() cho chứng minh nhân dân … Các đối tượng sử dụng những phương thức này để giao tiếp với bên ngoài. Việc này trước giúp dữ liệu được quan tâm đúng mức, và an toàn hơn. Mọi truy cập đến dữ liệu đều được kiểm soát thông qua các phương thức được cung cấp sẵn nên hạn chế được những truy cập bất hợp pháp. Tức là đã giải quyết được ba hạn chế đầu tiên của lập trình cấu trúc. Thứ hai, những thay đổi nào đó về dữ liệu chỉ ảnh hưởng đến một số lượng hàm nhất định và thay vì phải viết lại hầu hết các hàm thì ta chỉ phải viết lại một số hàm có liên quan trực tiếp đến sự thay đổi đó. Ví dụ thành phần dữ liệu name biểu thị tên của một đối tượng Human vì một lý do nào đó được đổi thành full_name thì những hàm liên quan trực tiếp đến name như set_name() hay get_name() mới phải viết lại, còn những hàm như set_ID(), get_ID() hay thậm chí những hàm gọi hàm set_name() và get_name() thì chẳng việc gì cả. Điều này thuận lợi cho việc nâng cấp và bảo trì. Tức là hạn chế thứ tư đã được giải quyết. OOP cũng cung cấp những khái niệm về kế thừa và đa hình giúp tận dụng tối đa khả năng sử dụng lại mã nguồn để giảm bớt vất vả cho lập trình viên cũng như tăng chất lượng phần mềm. Ví dụ chúng ta có thể tạo ra một lớp (class) mới là Girl, kế thừa từ lớp Human. Khi đó, một đối tượng thuộc lớp Girl sẽ có đầy đủ các thuộc tính và phương thức của Human, và ta chỉ cần bổ sung thêm những phần khác như số đo ba vòng: round_1, round_2, round_3 … Vì thể không phải viết lại toàn bộ code cho lớp Girl. Cụ thể như thế nào thì mình sẽ đề cập trong những bài post sau. Đây chỉ là bài mở đầu để giúp mọi người so sánh giữa kỹ thuật OOP với kỹ thuật lập trình cấu trúc truyền thống và có những hình dung cơ bản về OOP, những ưu điểm mà nó mang lại, và vì sao nó lại là một kỹ thuật được ưa chuộng nhất hiện nay. Trong những năm gần đây, lập trình đã dịch chuyển từ hướng cấu trúc sang hướng đối tượng vì những ưu điểm và khả năng mạnh mẽ của nó. Thực tế hiện nay OOP được sử dụng rộng rãi trong các dự án phần mềm, còn lập trình cấu trúc chỉ chiếm một phần rất nhỏ thường là giải quyết những vấn đề có quy mô nhỏ hoặc dùng trong giảng dạy để giúp người học bước đầu làm quen với lập trình. Đấy là mình cũng chỉ nghe thiên hạ nói thế thôi chứ cũng mới học OOP nên cũng không biết là thực tế doanh nghiệp bây giờ nó viết phần mềm bằng ngôn ngữ gì cả. Nhưng có điều mình cảm nhận được đúng là OOP lập trình sướng hơn hơn lập trình cấu trúc nhiều, ít ra là cái khoản thiết kế nó trực quan hơn, rõ ràng hơn, thật hơn. Còn nếu để ý kỹ thì những cài đặt chi tiết trong hướng đối tượng suy cho cùng vẫn là lập trình cấu trúc, có điều chúng được tổ chức tốt hơn và được phủ lên một giao diện mang tính hướng đối tượng mà thôi. Hết bài 1 p/s: mệt quá, phải nghỉ phát đã, bao giờ có sức thì viết tiếp __________________ Vấn đề không phải là bước nhanh, mà là luôn luôn bước Đã được chỉnh sửa lần cuối bởi first_pace : 28-02-2011 lúc 07:35 PM. Những đặc trưng cơ bản của lập trình hướng đối tượng BÀI 2. NHỮNG ĐẶC TRƯNG CƠ BẢN CỦA OOP Chúng ta sẽ xem xét sơ qua một số khái niệm và thành phần chính của OOP nói chung và của C++ nói riêng 1. Đối tượng (Objects) Khi thiết kế một chương trình theo tư duy hướng đối tượng người ta sẽ không hỏi “vấn đề này sẽ được chia thành những hàm nào” mà là “vấn đề này có thể giải quyết bằng cách chia thành những đối tượng nào”. Tư duy theo hướng đối tượng làm cho việc thiết kế được “tự nhiên” hơn và trực quan hơn. Điều này xuất phát từ việc các lập trình viên cố gắng tạo ra một phong cách lập trình càng giống đời thực càng tốt. Nếu ngoài đời có cái công nông thì khi thiết kế ta cũng bê nguyên cả cái công nông vào trong chương trình, và như vậy chương trình là tập hợp tất cả các đối tượng có liên quan với nhau. Tất cả mọi thứ đều có thể trở thành đối tượng trong OOP, nếu có giới hạn thì đó chính là trí tưởng của bạn. Đối tượng là một thực thể tồn tại trong khi chương trình chạy. Nó có các thuộc tính (attributes) và phương trức (methods) của riêng mình. 2. Lớp (Classes) Trong khi đối tượng là một thực thể xác định thì lớp lại là một khái nhiệm trừu tượng. Có thể so sánh lớp như “kiểu dữ liệu còn” đối tượng là “biến” có kiểu của lớp. Ví dụ: lớp Công_nông có thể được mô tả như sau: Lớp Công_nông Thuộc tính: • Nhãn hiệu (ví dụ Lamborghini) • Màu xe • Giá xe • Vận tốc tối đa (ví dụ 300 km/h) Phương thức: • Khởi động • Chạy thẳng • Rẽ trái / phải • Dừng • Tắt máy Một khai báo: C++ Code: Lựa chọn code | Ẩn/Hiện code Công_nông công_nông_của_tôi; Hoàn toàn tương tự như khai báo: C++ Code: Lựa chọn code | Ẩn/Hiện code int my_integer; Tạo một lớp mới tương tự như tạo ra một kiểu dữ liệu mới – kiểu người dùng tự định nghĩa (user-defined type) Lớp là “khuôn” để đúc ra các đối tượng.Một đối tượng thuộc lớp Công_nông sẽ có đầy đủ những thuộc tính và phương thức như được mô tả ở trên, trong trường hợp nàycông_nông_của_tôi được đúc ra từ “khuôn” Công_nông. Có một sự tương ứng giữa lớp và đối tượng nhưng bản chất thì lại khác nhau. Lớp là sự trừu tượng hóa của đối tượng, còn đối tượng là một sự thể hiện (instance) của lớp. Đối tượng là một thực thể có thực, tồn tại trong hệ thống, còn lớp là khái niệm trừu tượng chỉ tồn tại ở dạng khái niệm để mô tả đặc tính chung cho đối tượng. Tất cả những đối tượng của một lớp sẽ có thuộc tính và phương thức giống nhau. 3. Sự đóng gói và trừu tượng hóa dữ liệu (Encapsulation & Data Abstraction) Nhìn lại thí dụ trên thì mỗi đối tượng thuộc lớp Công_nông sẽ có cả các thuộc tính và phương thức được “đóng gói” chung lại. Muốn truy cập vào các thành phần dữ liệu bắt buộc phải thông qua phương thức, và các phương thức này tạo ra một giao diện để đối tượng giao tiếp với bên ngoài. Giao diện này giúp cho dữ liệu được bảo vệ và ngăn chặn những truy cập bất hợp pháp, đồng thời tạo ra sự thân thiện cho người dùng. Ví dụ: nếu như trong C, một xâu được lưu trữ trong một mảng str nào đó, muốn biết độ dài của xâu ta phải gọi hàm strlen() trong thư viện<string.h> thì trong C++, nếu str là một đối tượng thuộc lớp string thì tự nó “biết” kích thước của mình, và chỉ cần gọi str.size() hoặc str.length() là nó sẽ trả về độ dài của xâu str. Người dùng hoàn toàn không cần biết cài đặt chi tiết bên trong lớp string như thế nào mà chỉ cần biết “giao diện” để có thể giao tiếp với một đối tượng thuộc lớp string là ok. Điều này dẫn đến sự trừu tượng hóa dữ liệu. Nghĩa là bỏ qua mọi cài đặt chi tiết và chỉ quan tâm vào đặc tả dữ liệu và các phương thức thao tác trên dữ liệu. Đặc tả về lớp Công_nông ở trên cũng là một sự trừu tượng hóa dữ liệu. 4. Sự kế thừa (Inheritance) Những ý tưởng về lớp dẫn đến những ý tưởng về kế thừa. Trong cuộc sống hàng ngày chúng ta thấy rất nhiều ví dụ về sự kế thừa (tất nhiên là không phải thừa kế vê tài sản ). Ví dụ:lớp động vật có thể phân chia thành nhiều lớp nhỏ hơn như lớp côn trùng, lớp chim, lớp động vật có vú, không có vú … blah blah … hay lớp phương tiện có thể chia thành các lớp nhỏ hơn như xe đạp, xe thồ, xe tăng, xích lô, … Các lớp nhỏ hơn được gọi là lớp con (subclass) hay lớp dẫn xuất (derived class) còn các lớp phía trên gọi là lớp cha (super class) hay lớp cơ sở (base class). Một nguyên tắc chung là các lớp con sẽ có các đặc điểm chung được thừa hưởng từ các lớp cha mà nó kế thừa. Ví dụ lớp côn trùng và động vật có vú đều sẽ có những đặc điểm chung của lớp động vật. Và do đó ta chỉ cần bổ sung những đặc điểu cần thiết thay vì viết lại tòan bộ code. Điều này giảm gánh nặng cho các lập trình viên và do đó góp phần giảm chi phí sản xuất cũng như bảo trì, nâng cấp phần mềm. 5. Tính đa hình và sự quá tải (Polymorphism & Overloading) Giả sử ta xây dựng một lớp String để “đúc” ra các đối tượng lưu trữ xâu ký tự, ví dụ ta có 3 đối tượng s1, s2, s3 thuộc lớp String. Ta muốn thiết kế lớp String sao cho câu lệnh C++ Code: Lựa chọn code | Ẩn/Hiện code s3 = s1 + s2 ; sẽ thực hiện việc nối xâu s2 vào đuôi xâu s1 rồi gán kết quả cho xâu s3. Nếu như vậy công việc lập trình trông sẽ “tự nhiên” hơn. Nhưng thật không may ngôn ngữ lập trình không cung cấp sẵn điều này. Sử dụng các toán tử (operators) + và = như trên sẽ gây lỗi. Tuy nhiên C++ cung cấp một cơ chế cho phép lập trình viên “định nghĩa lại” các toán tử này để dùng trong các mục đích khác nhau. Việc định nghĩa lại cách sử dụng toán tử được gọi là “quá tải toán tử” (operator overloading). Một số người gọi nó là “nạp chồng toán tử” nhưng mình thích dùng từ quá tải hơn vì nghe nó có vẻ “cơ khí” . C++ cho phép quá tải hầu hết các toán tử thông dụng như +, -, *, /, [], <<, >>, … Ngoài việc cho phép quá tải toán tử, C++ còn cho phép “quá tải hàm” (function overloading), cái này mình sẽ nói kỹ hơn ở bài khác. Nói chung overloading là một cách cho phép ta sử dụng một toán tử hoặc hàm bằng những cách khác nhau tùy theo ngữ cảnh, và đó một trường hợp của “tính đa hình” (polymorphism), một tính năng rất quan trọng của OOP. Hết bài 2 BÀI 3. MỘT CHƯƠNG TRÌNH C++ ĐƠN GIẢN Bây giờ chúng ta sẽ xem xét một chương trình C++ đơn giản sau C++ Code: Lựa chọn code | Ẩn/Hiện code // my first program in C++ #include <iostream> using namespace std; int main(){ cout << “Hello, Girl” << endl; return 0; } Dòng đầu tiên là một chú thích (comment). Tất cả những gì từ sau ký hiệu // đến hết dòng được hiểu là chú thích và bị trình biên dịch bỏ qua, hoàn toàn không gây ảnh hưởng gì đến hoạt động của chương trình. Mục đích duy nhất của chú thích là làm tăng tính sáng sủa của chương trình, ta dùng chú thích để giải thích ngắn gọn mục đích của đoạn code hay của chương trình là gì. Trong ví dụ này, chú thích cho biết đây là chương trình đầu tiên bằng C++ của tôi. Ta có thể chú thích trên nhiều dòng bằng cặp ký hiệu /* Here are your comments */. Tuy nhiên tớ nghĩ dùng những chú thích ngắn gọn trên một dòng sẽ tốt hơn. Thêm nữa, chúng ta nên hạn chế sử dụng chú thích bừa bãi. Chỉ dùng khi thực sự cần thiết, và nên ngắn gon súc tích. Như vậy giúp ta trọng tâm hơn vào những phần chính và giúp chương trình không bị rối. Hãy để các đoạn code tự nói lên ý nghĩa của chúng. Dòng thứ hai là một chỉ thị tiền xử lý (preprocessor directive). Tất cả những gì bắt đầu bằng # đều là chỉ thị tiền xử lý và được xử lý bởi bộ tiền xử lý trước khi chương trình được dịch . Nó không phải là một câu lệnh (lưu ý mọi câu lệnh đều phải kết thúc bởi dấu chấm phẩy – semicolon )mà là một chỉ thị hướng dẫn preprocessor nạp nội dung của tệp <iostream> vào. Việc này giống như ta copy toàn bộ nội dung của tệp <iostream> rồi paste vào đúng vị trí của chỉ thị #include <iostream>. <iostream> là một header file liên quan đến những thao tác nhập/ xuất cơ bản. Nó chứa những khai báo (declarations) cần thiết cho nhập/ xuất, ví dụ trong trường hợp này sẽ được dùng bởi cout và toán tử <<. Thiếu những khai báo này trình biên dịch sẽ không nhận ra cout và sẽ báo lỗi. Vì vậy cần thiết phải include <iostream>. Chú ý: đôi khi ta thấy một số chương trình viết C++ Code: Lựa chọn code | Ẩn/Hiện code #include <iostream> Trong khi một số thì lại viết C++ Code: Lựa chọn code | Ẩn/Hiện code #include <iostream.h> Hai cách viết này là khác nhau. Những file có phần mở rộng .h là những file “cũ” có từ thời kỳ sơ khai của C++ và phần lớn trong số đó kế thừa và phát triển dựa trên các file của ngôn ngữ C. Khi ANSI và ISO công bố chuẩn cho C++ thì các standard header file mới đều không có phần mở rộng. Nói chung thì New Standard Header File so với Classic Standard Header File không khác nhau nhiều lắm, cái sau cải tiến và hoàn thiện một số khiếm khuyết của cái trước. Tất nhiên là những cái gì theo chuẩn mới thì thông thường sẽ tốt hơn. Tớ sẽ nói rõ hơn về phần này trong phần I/O stream. Dòng thứ ba đề cập đến một khái niệm đó là “namespace” (đôi khi còn được gọi là name scope). Thông thường một chương trình có chứa nhiều định danh (identifiers) thuộc nhiều phạm vi (scope) khác nhau. Đôi khi một đối tượng trong phạm vi này bị trùng tên với một đối tượng khác trong một phạm vi khác. Điều này dẫn đến xung đột và gây lỗi biên dịch. Sự chồng chéo tên (identifier overlapping ) có thể xảy ra ở nhiều cấp độ khác nhau, đặc biệt là trong các thư viện cung cấp bởi bên thứ ba. C++ standard nỗ lực giải quyết vấn đề này bằng cách sử dụng namespace. Mỗi namespace xác định một phạm vi mà trong đó các định danh được nhận biết, ngoài phạm vi này chúng sẽ không được nhận biết. Để sử dụng một thành phần trong namespace ta có thể dùng câu lệnh như sau C++ Code: Lựa chọn code | Ẩn/Hiện code my_namespace::member; Câu lệnh trên sử dụng một identifier có tên là member trong namespace có tên là my_namespace. Rõ ràng khi một namespace khác (ví dụ: your_namespace) cũng có một thành phần tên là member thì việc dùng hai tên này không sợ bị chồng chéo lên nhau. Toán tử :: là toán tử “phân giải phạm vi” (binary scope resolution operator). Trong câu lệnh trên toán tử :: cho biết rằng định danh member được sử dụng nằm trong phạm vi của namespace tên là my_namespace chứ không phải your_namespace. Quay trở lại chương trình của ta, nhận thấy trong hàm main, dòng thứ 5 có sử dụng cout và endl. Đây là hai định danh được khai báo trong namespace std. Để chương trình “nhận biết” được cout và endl thì ta có thể dùng cú pháp như vừa nói ở trên tức dòng lệnh thứ 5 được viết lại là: C++ Code: Lựa chọn code | Ẩn/Hiện code std::cout << “Hello, Girl” << std::endl; Tuy nhiên, rõ ràng cách viết trên là dài dòng. Nếu ta sử dụng nhiều hơn các đinh danh trong namespace std thì mỗi lần dùng ta lại phải viết thêm std::, vì vậy để có thể sử dụng được toàn bộ các định danh trong namespace std ta dùng câu lệnh như dòng thứ 3: C++ Code: Lựa chọn code | Ẩn/Hiện code using namespace std; Những dòng còn lại là định nghĩa hàm main(). Đây là hàm quan trọng nhất trong chương trình và có nhiệm vụ điều phối và kiểm soát toàn bộ chương trình, nó gọi những hàm khác khi cần thiết. Tuy nhiên mình muốn nói một điều hơi bất cập một tý. Khi mình đọc các tài liệu về C++ thì tất cả đều nói hàm main được gọi và xử lý trước mọi hàm khác trong chương trình. Điều này có luôn luôn đúng? Phần này mình nói hơi ngoài lề một tý, nó liên quan đến constructor của class nên nếu bạn nào chưa học đến phần này thì có thể bỏ qua. Xét một chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include <iostream> using namespace std; // định nghĩa lớp My_class class My_class{ private: int number; public: My_class(){ number = 0; } // constructor } My_class global_var; // khai báo một biến toàn cục // hàm main int main(){ cout << “Is main always called first ?” << endl; return 0; } Biến global_var được khai báo toàn cục bên ngoài tất cả mọi hàm. Khi khai báo biến global_var thì theo nguyên tắc phải gọi đến constructor của lớp My_class để khởi tạo number = 0. Vì vậy thực tế trong chương trình trên constructor My_class() được gọi trước main. Bây giờ trở lại vấn đề chính, ta sẽ vẫn tiếp tục phân tích nốt mấy câu lệnh còn lại. Chúng ta để ý đến dòng thứ 5. C++ Code: Lựa chọn code | Ẩn/Hiện code cout << “Hello, Girl” << endl; Dòng này có tác dụng in dòng text nằm giữa hai dấy nháy kép, cụ thể là “Hello, Girl” lên màn hình. Chúng ta sẽ phân tích kỹ hơn một chút về nguyên tắc hoạt động của nó, tuy nhiên chỉ là một sự mô tả rất thô sơ. Để hiểu biết kỹ hơn chúng ta cần biết những kiến thức về đối tượng, quá tải toán tử, và nhiều vấn đề khác nữa. Trong C, để in một đoạn văn bản lên màn hình ta có thể dùng hàm printf(). Điều này dễ làm cho ta lầm tưởng cout cũng là một hàm, nhưng không phải thế. C là ngôn ngữ hướng thủ tục, còn C++ là ngôn ngữ hướng đối tượng. Và cout là một đối tượng (object). Nó được định nghĩa sẵn trong C++ tương ứng với dòng xuất chuẩn (standard output stream). Stream là một khái niệm trừu tượng được hiểu như luồng dữ liệu (data flow). standard output stream thông thường được “kết nối” (connected to) hay “chảy” (flows to) tới màn hình. Toán tử << được gọi là toán tử chèn dòng xuất (insertion output stream operator). Nó ra lệnh chuyển những nội dung của đối tượng bên tay phải sang đối tượng bên tay trái (giống như chiều mũi tên của toán tử << luôn). Ở đây endl (đối tượng này được khai báo trong namespace std như đã nói ở trên và tác dụng của nó là kết thúc một dòng, chuyển sang dòng mới) được chuyển sang bên trái cho xâu ký tự nằm trong dấu nháy kép. Sau đó toàn bộ dữ liệu này được chuyển sang cho cout, mà cout lại kết nối tới màn hình nên kết quả là trên màn hình in ra dòng text: Hello, Girl và con trỏ chuyển xuống dòng mới. Có thể mô tả bởi hình vẽ sau: Câu lệnh cuối cùng là: C++ Code: Lựa chọn code | Ẩn/Hiện code return 0; Câu lệnh này là một cách thông thường để kết thúc hàm main. Nó báo cho trình biên dịch biết là chương trình kết thúc thành công, không có lỗi. Chương trình trên mặc dù rất đơn giản nhưng nó trình bày được cấu trúc chung của của một chương trình C++. Những bài sau mình sẽ giới thiệu những tiện ích thông dụng của C++ và cách sử dụng chúng. Hết bài 3 Lớp và đối tượng BÀI 5b. CLASSES & OBJECTS (PART 2) 3. Truy cập đến những thành phần của lớp Để truy cập đến các thành phần của lớp ta dùng toán tử chấm (selection dot operator) thông qua tên của đối tượng. Ví dụ đoạn chương trình sau gọi hàm set_name để nhập tên cho đối tượng studentA và gọi hàm get_name để lấy tên của đối tượng : C++ Code: Lựa chọn code | Ẩn/Hiện code Student studentA; // khai báo đối tượng studentA thuộc lớp Student studentA.set_name(“Bill Gates”); // gán tên cho studentA là “Bill Gates” cout << studentA.get_name(); // in ra tên đối tượng studentA Kết quả thu được là màn hình hiển thị dòng văn bản “Bill Gates”. Để ý lại định nghĩa của hàm set_name và get_name: C++ Code: Lựa chọn code | Ẩn/Hiện code // set name void Student::set_name(string str){ name=str; } // get name string Student::get_name(){ return name; } Ta nhận thấy name là thành phần dữ liệu được khai báo private. Điều đó nghĩa là chỉ có những hàm thành viên mới có quyền truy nhập đến nó (sau này ta sẽ biết thêm một trường hợp nữa, đó là hàm bạn – friend, cũng có khả năng truy nhập đến các thành phần private). Hàm set_name và get_name là hai hàm thành viên của lớp Student nên nó có thể truy nhập và thao tác được trên dữ liệu name. Nhưng nỗ lực truy nhập trực tiếp và các thành phần private mà không thông qua hàm thành viên như ví dụ sau sẽ gây lỗi biên dịch (compilation error): C++ Code: Lựa chọn code | Ẩn/Hiện code Student studentA; // khai báo đối tượng studentA thuộc lớp Student studentA.name=”Bill Gate”; // error 4. Ưu điểm của việc đóng gói dữ liệu và phương thức trong một đơn vị thống nhất – lớp Việc đóng gói dữ liệu kết hợp với quy định phạm vi truy nhập cho các thành phần của lớp có nhiều ưu điểm. Thứ nhất: tạo ra sự gọn gàng dễ kiểm soát. Việc đóng gói dữ liệu và các phương thức liên quan giúp chương trình gọn gàng hơn, lập trình viên dễ kiểm soát hơn vì tất cả đều được gói gọn trong phạm vi của lớp. Thứ hai: trừu tượng hóa dữ liệu, thông qua “giao diện”, tạo thuận lợi cho người dùng Việc cung cấp các hàm thành viên để thao tác trên các dữ liệu của đối tượng tạo sự “thân thiện” cho người dùng. Trong ví dụ lớp Student ở trên, để nhập tên cho một đối tượng ta chỉ cần gọi hàm set_name thông qua tên đối tượng mà không cần quan tâm đến cài đặt chi tiết như thế nào. Thứ ba: tính bảo mật của dữ liêu được nâng cao Để truy cập đến các dữ liệu private của một đối tượng bắt buộc phải thông qua hàm thành viên. Tức mọi “giao tiếp” với đối tượng đều phải thông qua “giao diện” mà ta đã quy định trước. Ví dụ: nhập tên cho studentA thì bắt buộc phải dùng hàm set_name, lấy tên thì dùng get_name. Do đó sẽ tránh được những truy cập và sửa đổi bất hợp pháp, đồng thời nếu phát sinh lỗi thì sẽ dễ khoanh vùng hơn. Ví dụ khi yêu cầu trả về mã số sinh viên của studentA thì phát hiện một số lỗi nào đó. Rõ ràng những lỗi đó chỉ có thể do các hàm có liên quan trực tiếp đếnstudent_code như set_student_code hoặc get_student_code chứ không thể là set_name hay get_name được. Thứ tư: tăng cường tính độc lập và ổn định hơn cho các thành phần sử dụng lớp trong chương trình Giả sử vì một lý do nào đó mà thành phần name buộc phải đổi lại thành full_name thì chương trình sẽ phải chỉnh sửa lại một chút. Tuy nhiên chỉ những hàm thành viên nào liên quan trực tiếp đến name mới phải sửa đổi, tức là các hàm set_name và get_name sẽ phải sửa lại name thành full_name. Tuy nhiên, các hàm gọi đến hàm set_name và get_name thì không hề phải sửa lại, bởi vì nó không biết cài đặt chi tiết bên trong set_name và get_name như thế nào mà chỉ biết “giao diện” của set_name và get_name vẫn thế, do đó chương trình không phải chỉnh sửa nhiều. Hết bài 5b Hàm tạo (constructor) BÀI 6. HÀM TẠO (CONSTRUCTOR) Bài này mình sẽ dành để viết về constructor trong C++. Tại sao phải dùng constructor, dùng nó như thế nào, và những vấn đề cần lưu ý khi sử dụng constructor sẽ là những nội dung chính được đưa ra. 1. Vấn đề đặt ra Giả sử ta tạo ra một lớp Rectangle (hình chữ nhật) như sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include <iostream> #include <string> using namespace std; // class definition class Rectangle{ private: int width; // chiều rộng int height; // chiều cao public: // set width & height void set_width(int); // nhập chiều rộng void set_height(int); // nhập chiều cao // get width & height int get_width(); // lấy chiều rộng int get_height(); // lấy chiều cao // calculate area int area(); // tính diện tích }; // member function definitions // set width void Rectangle::set_width(int a){ width=a; } // set height void Rectangle::set_height(int b){ height=b; } // get width int Rectangle::get_width(){ return width; } // get height int Rectangle::get_height(){ return height; } // calculate area int Rectangle::area(){ return height*width; } Điều gì sẽ xảy ra khi ta gọi hàm tính diện tích area trước khi thiết lập chiều rộng và chiều cao cho hình chữ nhật như trong đoạn chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle my_rectangle; // khai báo đối tượng my_rectangle thuộc lớp Rectangle cout << my_rectangle.area() << endl; // in ra màn hình diện tích của my_rectangle Giá trị thu được trên màn hình có thể là một số âm ! Câu lệnh thứ nhất khai báo đối tượng my_rectangle, chương trình sẽ cấp phát bộ nhớ cho các thành phần dữ liệu width và height, giả sử width rơi vào ô nhớ mà trước đó có lưu trữ giá trị 20, còn height rơi vào ô nhớ trước đó có lưu trữ giá trị -3. Ngay sau đó, câu lệnh thứ hai yêu cầu tính diện tích của my_rectangle rồi hiển thị ra màn hình, và kết quả ta thu được là diện tích my_rectangle bằng -60 ! Để đảm bảo mọi đối tượng đều được khởi tạo hợp lệ trước khi nó được sử dụng trong chương trình, C++ cung cấp một giải pháp đó là hàm tạo (constructor). 2. Hàm tạo (constructor) Constructor là một hàm thành viên đặc biệt có nhiệm vụ thiết lập những giá trị khởi đầu cho các thành phần dữ liệu khi đối tượng được khởi tạo. Nó có tên giống hệt tên lớp để compiler có thể nhận biết được nó là constructor chứ không phải là một hàm thành viên giống như các hàm thành viên khác. Trong constructor ta có thể gọi đến các hàm thành viên khác. Một điều đặc biệt nữa là constructor không có giá trị trả về, vì vậy không được định kiểu trả về nó, thậm chí là void. Constructor phải được khai báo public. Constructor được gọi duy nhất một lần khi đối tượng được khởi tạo. Những lớp không khai báo tường minh constructor trong định nghĩa lớp, như lớp Rectangle ở trên của chúng ta, trình biên dịch sẽ tự động cung cấp một “constructor mặc định" (default constructor). Construtor mặc định này không có tham số, và cũng không làm gì cả. Nhiệm vụ của nó chỉ là để lấp chỗ trống. Nếu lớp đã khai báo constructor tường minh rồi thì default constructor sẽ không được gọi. Bây giờ ta sẽ trang bị constructor cho lớp Rectangle: C++ Code: Lựa chọn code | Ẩn/Hiện code class Rectangle{ private: int width; int height; public: // constructor Rectangle(); /* các hàm khác khai báo ở chỗ này */ }; // member function definitions // constructor Rectangle::Rectangle(){ [...]... CONTROL FLOWS - Cấu trúc rẽ nhánh Như mình đã nói trong bài 1, lập trình cấu trúc (structured programming) được xây dựng dựa theo mô hình toán học của Bohm và Guiseppe Theo đó một chương trình máy tính có thể được viết dựa trên ba cấu trúc là: tuần tự, rẽ nhánh và lặp C++ được thiết kế không chỉ hỗ trợ lập trình hướng đối tượng mà còn cả cho lập trình cấu trúc vì vậy nó cung cấp những cấu trúc điều khiển... của các bậc tiền bối trong lập trình cho thấy rằng, cách tốt nhất để phát triển cũng như bảo trì một phần mềm là phân chia và tổ chức nó thành những khối nhỏ hơn, đơn giản hơn Kỹ thuật này được biết với tên gọi quen thuộc là “chia-để-trị” (devide-and-conquer) Tư tưởng chia-để-trị là một trong những nguyên lý quan trọng của lập trình cấu trúc, tuy nhiên lập trình hướng đối tượng cung cấp những cách thức... định nghĩa ở đây */ Khi đó câu lệnh C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle my_rectangle; sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0 3 Thiết lập giá trị bất kỳ cho các thành phần dữ liệu khi khởi tạo đối tượng Một vấn đề được đặt ra là có thể khởi tạo những giá trị nhau khác cho các đối tượng ngay lúc khai báo không? Giống như với kiểu int: C++ Code: Lựa chọn code | Ẩn/Hiện... inline nếu nhận thấy kích thước hàm quá lớn, có chứa các cấu trúc lặp hoặc đệ quy 7 Phạm vi (scope) Phạm vi của một đối tượng quyết định hai điểm: thứ nhất, nó quy định những đoạn code nào có thể tác động được lên đối tượng, thứ hai, nó quy định thời gian tồn tại của đối tượng (lifetime) Trong C++, nếu phân loại theo phạm vi thì sẽ có 3 loại biến: cục bộ, tham số hình thức, và biến toàn cục a Biến cục bộ... việc phải xem xét thêm một thư viện khổng lồ này làm chậm lại đáng kể quy trình chuẩn hóa Nghĩa là từ lúc C++ ra đời đến phiên bản chuẩn hóa đầu tiên của nó mất tới 30 năm! Trong 30 năm đó, người ta đã dùng C++ như một ngôn ngữ chuyên nghiệp để lập trình và có tới hàng tá phiên bản cài đặt khác nhau của C++ được tung ra Rõ ràng với một lượng code lớn như vậy hiện hữu trong các hệ thống máy tính đang... x, bây giờ là 10 return 0; } Chương trình trên nhắc lại những kiến thức hết sức cơ bản về con trỏ Bây giờ ta sẽ xem xét cách truyền đối số cho hàm thông qua con trỏ như thế nào Ví dụ chương trình sau thực hiện việc hoán đổi nội dung hai biến cho nhau, một chương trình hết sức cổ điển gần như lúc nào cũng được lôi ra làm ví dụ khi nói về truyền đối số bằng con trỏ: C++ Code: Lựa chọn code | Ẩn/Hiện code... dùng được tham chiếu thì nên dùng Nhưng lưu ý rằng nếu không cẩn thận sẽ rất dễ mắc lỗi, đó là hiện tượng “tham chiếu treo” (dangling reference), nghĩa là tham chiếu tới một đối tượng "không tồn tại", và gây là một lỗi logic Chúng ta không thể dự đoán được hành vi của chương trình Xét chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include using namespace std; int& dangling_square(int... arguments) Xét chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include using namespace std; int default_arg(int , int =1, int =2); // nguyên mẫu hàm với hai đối mặc định int default_arg(int a, int b, int c){ // định nghĩa hàm return a*b*c; }; int main(){ // truyền đủ đối, các giá trị mặc dịnh không được dùng cout . lược về mô hình lập trình được ngôn ngữ hỗ trợ mà ta dự định viết chương trình theo mô hình đó. Cụ thể, nói lập trình hướng đối tượng với C++ thì ta phải biết sơ sơ về hướng đối tượng trước khi. 1. SƠ LƯỢC VỀ LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG – OBJECT ORIENTED PROGRAMMING Tất cả các ngôn ngữ lập trình đều sinh ra để hỗ trợ một hoặc một số phong cách lập trình hay một mô hình lập trình nào đó. lỗ hổng này dẫn đến sự ra đời của một kỹ thuật lập trình mới lập trình hướng đối tượng (object oriented programming – OOP). Mình cũng nói sơ qua một chút về OOP. Khác với lập trình cấu trúc,

Ngày đăng: 13/08/2014, 01:22

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan