Xem mẫu

  1. 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 
  2. 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 
  3. 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 •
  4. 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 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. 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 using namespace std; int main(){ cout
  6. thị #include . 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ử 
  7. 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 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
  8. 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
  9. 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 #include using namespace std; // class definition class Rectangle{ private: int width; // chiều rộng int height; // chiều cao public: // set width & height
  10. 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
  11. 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(){ width=0; height=0; } /* các hàm khá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 code int a=10; int b=100; int c=1000; C++ hoàn toàn cho phép chúng ta làm điều này. Có một số cách để thiết lập những giá trị khác nhau cho các thành  phần dữ liệu trong khi khai báo. Cách thứ nhất: viết thêm một hàm tạo nữa có tham số. C++ hoàn toàn không giới hạn số lượng constructor. Chúng ta thích viết bao nhiêu constructor cũng ok. Đây chính là  khả năng cho phép quá tải hàm của C++ (function overloading), trong trường hợp của ta là quá tải hàm tạo. Tức là  cùng một tên hàm nhưng có thể định nghĩa theo nhiều cách khác nhau để dùng cho những mục đích khác nhau. Để  quá tải một hàm (bất kỳ) ta chỉ cần cho các hàm khác nhau về số lượng tham số , kiểu tham số còn giữ nguyên   tên hàm. Tạm thời cứ thế đã, tớ sẽ đề cập rõ hơn trong một bài riêng cho functions. Bây giờ ta sẽ bổ sung thêm  một constructor nữa vào định nghĩa lớp Rectangle: C++ Code: Lựa chọn code | Ẩn/Hiện code
  12. class Rectangle{ private: int width; int height; public: // constructor Rectangle(); // hàm tạo không có tham số Rectangle(int, int); // hàm tạo với hai tham số /* các hàm khác khai báo ở chỗ này */ }; // member function definitions // constructor with no parameters Rectangle::Rectangle(){ width=0; height=0; } // constructor with two parameters Rectangle::Rectangle(int a, int b){ width=a; height=b; } /* các hàm khác định nghĩa ở đây */ Bây giờ ta sẽ test bằng chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle rectA; // gọi hàm tạo không tham số Rectangle rectB(3,4); // gọi hàm tạo có tham số cout
  13. // constructor with default arguments Rectangle::Rectangle(int a, int b){ width=a; height=b; } /* các hàm khác định nghĩa ở đây */ Chúng ta chú ý đến khai báo của hàm tạo: C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle(int =0, int =0); Khai báo này cho biết, khi khai báo đối tượng, nếu đối số nào bị khuyết (tức không được truyền vào) thì sẽ được mặc  định là 0. Và để đảm bảo không xảy ra sự nhập nhằng, C++ yêu cầu tất cả những đối số mặc định đều phải tống  sang bên phải nhất (rightmost), tức ngoài cùng bên phải. Vì vậy: C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle rectA; // sẽ gán width=0, height=0 Rectangle rectB(4); // sẽ gán width=4, height=0 Rectangle rectC(2,6); // sẽ gán width=2, height=6 Chú ý: giá trị mặc định (ví dụ int =0) chỉ được viết lúc khai báo hàm, chứ không phải lúc định nghĩa hàm. Nếu ta  viết lại những giá trị mặc định này trong danh sách tham số lúc định nghĩa hàm sẽ gây lỗi biên dịch. C++ Code: Lựa chọn code | Ẩn/Hiện code // lỗi đặt đối số mặc định khi định nghĩa hàm Rectangle::Rectangle(int a=0, int b=0){ // error width=a; height=b; } 4. Hàm tạo mặc định Như đã nói ở trên, nếu ta không cung cấp hàm tạo cho lớp thì compiler sẽ làm điều đó thay chúng ta. Nó sẽ cung cấp  một hàm tạo không tham số và không làm gì cả ngoài việc lấp chỗ trống. Đôi khi hàm tạo không có tham số do người  dùng định nghĩa cũng được gọi là hàm tạo mặc định (hay ngầm định). Chúng ta xem xét chuyện gì sẽ xảy ra nếu như  không có hàm tạo ngầm định khi khai báo một mảng các đối tượng. Ví dụ vẫn là lớp Rectangle với hàm tạo hai tham  số: C++ Code: Lựa chọn code | Ẩn/Hiện code class Rectangle{ private: int width; int height; public: // constructor Rectangle(int, int); // hàm tạo với hai tham số /* các hàm khác khai báo ở chỗ này */ }; // member function definitions // constructor with 2 parameters Rectangle::Rectangle(int a, int b){ width=a; height=b; }
  14. /* các hàm khác định nghĩa ở đây */ Nếu như ta khai báo một mảng tầm chục thằng Rectangle thì chuyện gì sẽ xảy ra? C++ Code: Lựa chọn code | Ẩn/Hiện code Rectangle my_rectangle(1,2); // 1 thằng thì ok Rectangle rect_array[10]; // chục thằng thì có vấn đề - error Điều này là do ta cần khai báo 10 thằng Rectangle nhưng lại không cung cấp đủ tham số cho chúng, vì hàm tạo yêu  cầu hai tham số cần phải được truyền vào. Giải quyết chuyện này bằng cách bổ sung thêm một hàm tạo không có  tham số hoặc chỉnh lại tất cả các tham số của hàm tạo hai tham số bên trên thành dạng đối số mặc định là ok Hết bài 6 Hàm trong C++ BÀI 7a. FUNCTIONS (PART 1) ­ from alpha to omega ­ Bài này mình sẽ nói về một số vấn đề nâng cao về hàm trong C++. Vì vậy các bạn cần phải có một số kiến thức nhất  định về hàm. Nói là nâng cao cho nó oách chứ thực ra nếu học C++ thì trước sau gì cũng phải biết đến mấy thứ này.  Mình sẽ cố gắng trình bày thật đầy đủ dễ hiểu. Dưới đây là liệt kê những phần sẽ được đề cập trong bài: Tại sao phải dùng hàm? • Khai báo và định nghĩa hàm (function declarations & function definitions) • Truyền đối số cho hàm (passing arguments to functions) • Trả về giá trị của hàm (returning value from functions) • Đối số mặc định (default argument) • Quá tải hàm (function overloading) • Hàm nội tuyến (inline function) • Phạm vi và lớp lưu trữ (scope and storage classes) • Vai trò của biến toàn cục (role of global variable) • Đối hằng và hàm hằng (const arguments & const functions) • 1. Tại sao phải dùng hàm – why, why, why? Hàm là một tập các câu lệnh được nhóm lại dưới một cái tên, gọi là tên hàm, dùng để thực hiện một công việc xác định  nào đó. Những vấn đề thực tế thường rất lớn và phức tạp. Không thể giải quyết kiểu “một phát xong ngay”. Kinh nghiệm  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 phụ trợ mạnh mẽ hơn để tổ chức chương trình. Như mình đã  nói trong bài 1, khi giải quyết một “công việc lớn” ta phải chia nhỏ công việc đó ra, mỗi phần sẽ quẳng cho một hàm  đảm nhiệm. Nếu từng phần công việc vẫn còn lớn thì lại chia nhỏ tiếp cho tới khi đủ đơn giản, và tương tự cũng có các  hàm tương ứng với những phần này. Đó là nguyên nhân thứ nhất dẫn đến việc sử dụng hàm. Một nguyên nhân nữa  thúc đẩy việc sử dụng hàm là khả năng tận dụng lại mã nguồn. Một hàm khi đã được viết ra có thể được sử dụng lại  nhiều lần. Ví dụ: hàm strlen trong thư viện  của C được viết để tính chiều dài của một xâu bất kỳ, vì 
  15. vậy khi muốn tính độ dài của một xâu nào đó ta chỉ việc gọi hàm này là ok, thay vì lại phải viết một đoạn chương trình  loằng ngoằng để đếm từng ký tự trong xâu. Nói túm lại, nếu bạn không muốn viết chương trình theo kiểu “trâu bò” và  “cục súc” thì bạn phải dùng hàm  2. Khai báo và định nghĩa một hàm (function declarations & function definitions) Một nguyên tắc muôn thủa của C và C++ là mọi thứ cần phải được khai báo trước lần sử dụng đầu tiên. Bạn  không thể sử dụng một biến hay hàm nếu như không nói trước cho trình biên dịch biết điều đó (chắc compiler cho rằng  hành động dùng mà không xin phép của bạn là một sự "xúc phạm" với nó nên nó bực, nó không dịch cho   ). Vì vậy  trước khi sử dụng hàm ta phải khai báo. Nếu ta chỉ khai báo tên hàm còn viết định nghĩa thân hàm ở chỗ khác thì đó là  sự khai báo bình thường (declaration) hay khai báo nguyên mẫu hàm (prototype). Còn nếu ta viết luôn cả thân hàm  thì đó là một sự định nghĩa hàm (definition).  Khai báo nguyên mẫu hàm (function prototype declaration) C++ Code: Lựa chọn code | Ẩn/Hiện code (danh_sách_tham_số); Ví dụ: int square(int); // tính bình phương của một số nguyên Khai báo này giống như việc bạn nói với trình biên dịch: “này chú compiler, sẽ có một hàm kiểu như thế xuất hiện trong  chương trình, vì vậy nếu chú nhìn thấy chỗ nào gọi cái hàm này thì đừng có xoắn, anh sẽ viết định nghĩa nó ở một xó  nào đấy trong chương trình. Yên tâm đi, anh không lừa chú đâu”    Định nghĩa hàm (function definition) Bây giờ giả sử thằng compiler nó tạm thời “tin” theo lời chúng ta, rằng sẽ có định nghĩa đầy đủ cho cái nguyên mẫu  được khai báo trên kia, và nó bắt đầu dịch tiếp. Giả sử nó gặp một câu lệnh như sau: C++ Code: Lựa chọn code | Ẩn/Hiện code x=square(y); // giả thiết x, y đã được khai báo trước Vì đã được thông báo từ trước nên nó sẽ “không xoắn”, mà bắt đầu tìm định nghĩa cho hàm này, vì nó vẫn tin vào “lời  hứa” của chúng ta. Nếu nó tìm mà không thấy, nghĩa là chúng ta đã “lừa” nó, nó sẽ báo lỗi. Vì vậy ta phải cung cấp  định nghĩa cho hàm như đã cam kết. Dưới đây là định nghĩa cho hàm square: C++ Code: Lựa chọn code | Ẩn/Hiện code int square(int n){ return n*n; } Định nghĩa này bao gồm phần header (hay còn gọi là declarator) và theo sau nó là phần thân hàm (body). Phần  header phải tương thích với nguyên mẫu hàm, nghĩa là phải có cùng kiểu trả về, cùng tên, cùng số lượng tham số   và cùng kiểu tham số ở những vị trí tương ứng. Một số chú ý nhỏ Tham số (parameters) khác với đối số. Tham số (hay còn gọi là tham số hình thức) là những biến tượng trưng ở  • trong danh sách tham số, xuất hiện lúc khai báo nguyên mẫu hoặc định nghĩa hàm, còn đối số là dữ liệu  truyền vào cho hàm khi hàm được gọi. Ví dụ: C++ Code: Lựa chọn code | Ẩn/Hiện code int min(int a, int b); // a và b là tham số
  16. minimum=min(x,y); // x, y đối số được truyền vào cho hàm Trong danh sách tham số ở khai báo nguyên mẫu có thể chỉ cần nêu kiểu dữ liệu của của tham số mà không  • cần nêu tham số, lúc định nghĩa mới cần. Ví dụ C++ Code: Lựa chọn code | Ẩn/Hiện code int min(int, int); // khai báo nguyên mẫu không có tham số hình thức mà chỉ có kiểu … int min(int a, int b){ // bây giờ mới cần tham số hình thức // thân hàm ở đây } 3. Truyền đối số cho hàm (passing arguments to functions) Đối số (argument) là một mẩu dữ liệu nào đó như một giá trị nguyên, một ký tự thậm chí là cả một cấu trúc dữ liệu hết  sức rối rắm như một mảng các đối tượng chẳng hạn, được truyền vào cho hàm. Có nhiều cách truyền đối số cho hàm,  ta sẽ xem xét các cách này và phân tích ưu nhược điểm của chúng. Let’s go! Truyền hằng (passing constants) Xét hàm square ở trên, câu lệnh: C++ Code: Lựa chọn code | Ẩn/Hiện code x=square(10); sẽ thực hiện tính bình phương của 10, rồi gán kết quả thu được cho biến x. Sau câu lệnh này x có giá trị là 100. Ta thấy  đối truyền vào cho hàm square ở đây là một hằng số kiểu int. điều này hoàn toàn hợp lệ miễn là hằng truyền vào có  kiểu tương thích với kiểu của tham số hình thức. Ta cũng có thể truyền cho hàm một hằng ký tự, hoặc hằng xâu ký tự.  Ví dụ cho việc này là hàm printf của C. Truyền biến (passing variables) Đây là cách truyền đối số phổ biến nhất cho hàm. xét đoạn chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code n=10; x=square(n); Kết quả thu được sau khi kết thúc đoạn chương trình trên vẫn là x=100. Tuy nhiên truyền biến cho hàm có một số điều  “thú vị”. Ta có thể truyền biến cho hàm dưới hai hình thức là truyền bằng tham trị (pass­by­value) và truyền bằng   tham chiếu (pass­by­reference). Mỗi cách có một ưu, nhược điểm riêng và ta sẽ phân tích chúng để đưa ra cách tối  ưu nhất. a. Truyền bằng tham trị (pass­by­value) Xét đoạn chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include using namespace std; int min(int a, int b){ return (a
  17. int y=10; int z=min(x,y); // z là giá trị nhỏ nhất trong hai giá trị x, y cout
  18. 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 #include using namespace std; void swap(int* a, int* b){ // hoán đổi nội dung hai biến cho nhau int temp; temp=*a; *a=*b; *b=temp; } int main(){ int x=5; int y=7; // trước khi gọi swap cout
  19. Truyền tham chiếu thông qua tham chiếu Tham chiếu (reference) là một khái niệm mới của C++ so với C. Nói nôm na nó là một biệt danh hay nickname của  một biến. Chương trình sau minh họa đơn giản cách sử dụng tham chiếu trong C++ C++ Code: Lựa chọn code | Ẩn/Hiện code #include using namespace std; int main(){ int x; // khai báo biến nguyên x int &ref=x; // tham chiếu ref là nickname của x ref=10; // gán ref=10, nghĩa là x cũng bằng 10 cout
  20. Rõ ràng thằng dưới nhìn “thân thiện” hơn thằng trên (tự dưng để cái dấu & ở trước trông nó chướng mắt   ). Hơn nữa  tham chiếu đã gắn với biến nào rồi thì cố định luôn, không thay đổi được, còn con trỏ không thích trỏ biến này nữa thì  có thể trỏ sang biến khác, nên nếu lỡ tay mà ta cho nó “trỏ lung tung” thì không biết đằng nào mà lần. Lợi ích của việc truyền tham chiếu hằng (const references) Bây giờ ta lại đặt ra vấn đề: liệu có cách nào tận dụng được tính an toàn bảo mật của truyền theo tham trị nhưng   lại tận dụng được lợi thế về chi phí bộ nhớ và thời gian như truyền theo tham chiếu không? Câu trả lời đói là  dùng tham chiếu hằng. Chúng ta sẽ xem chương trình sau: C++ Code: Lựa chọn code | Ẩn/Hiện code #include using namespace std; int min(const int& a, const int& b){ return (a
nguon tai.lieu . vn