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.
#2
27-02-2011, 12:43 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
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
__________________
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:39 PM.
#3
27-02-2011, 03:50 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
Chương trình C++ đơn giản
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
__________________
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:41 PM.
#4
27-02-2011, 11:14 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
Nhập xuất cơ bản trong lập trình C++
BÀI 4. NHẬP / XUẤT CƠ BẢN VỚI C++
Trong bài này mình sẽ trình bày những thao tác nhập/ xuất cơ bản (basic input/output) để
các bạn có thể viết ngay được chương trình với C++. Học C++ cũng giống như học một
ngoại ngữ như tiếng Ý hay Italia . Để học được và học tốt ngôn ngữ việc trước tiên không
phải là lao ngay vào ngâm cứu ngữ pháp hay văn phạm hoặc các luật lệ của ngôn ngữ, mà
là phải biết một số từ mới, một số câu xã giao đơn giản kiểu như : Hey, Girl ! You’re so
beautiful … Dần dần khi vốn từ vựng đã đủ dùng và ta ta đã quen dần với ngôn ngữ thì
lúc đó mới ngâm cứu những vấn đề sâu hơn, mới tìm hiểu được bản chất của vấn đề. Vì vậy
bài này giúp các bạn mới học C++ làm quen với những thao nhập xuất tác cơ bản trong C+
+ như học các từ vựng đầu tiên của ngôn ngữ, còn muốn hiểu rõ bản chất và nguyên tắc
hoạt động của chúng ra sao thì cần phải cần có những kiến thức sâu hơn một chút, mình sẽ
trình bày trong bài nói về input/ output stream và file.
1. Xuất dữ liệu ra màn hình
Để in dữ liệu của một biểu thức nào đó ra màn hình (standard output device) ta dùng câu
lệnh sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << exp ; // output content of expression
Hoặc cho nhiều biểu thức:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << exp_1 << exp_2 << … << exp_n ; // output content of expression 1,
2, … , n
Ta cũng có thể viết câu lệnh trên trên nhiều dòng:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << exp_1
<< exp_2
<< exp_3
…
<< exp_n;
Kết quả thu được là hoàn toàn tương tự. cout cùng với toán tử << có thể xuất được nhiều
kiểu dữ liệu khác nhau mà không cần sự can thiệp của lập trình viên. C++ tự động nhận
biết và xuất ra dưới định dạng phù hợp. Xét đoạn chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
int main(){
int n=3;
float x=12.08;
string str="Be aware ! I have a gun";
cout << n << endl;
cout << x << endl;
cout << str << endl;
return 0;
}
Kết quả thu được trên màn hình sẽ như sau
Trích dẫn:
3
12.08
Be aware ! I have a gun
2. Nhập dữ liệu vào từ bàn phím
Bàn phím là thiết bị nhập chuẩn (standard input device). Để vào dữ liệu từ bàn phím cho
các biến ta có thể dùng cin vùng toán tử >>. Cú pháp sẽ như sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cin >> var; // read data into variable
Hoặc cho nhiều biến:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cin >> var_1 >> var_2 >> … >> var_n;
Khi gặp những câu lệnh như thế này chương trình sẽ “pause” lại để chờ chúng ta nhập dữ
liệu vào từ bàn phím. Câu lệnh cin >> var_1 >> var_2 >> … >> var_n; coi các ký tự
trắng là ký tự phân cách các lần nhập dữ liệu. Các ký tự trắng (white space characters) bao
gồm: dấu cách, dấu tab, và ký tự xuống dòng (new line). Ví dụ a, b là hai biến kiểu int, thì
câu lệnh:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cin >> a >> b;
sẽ đợi người dùng nhập dữ liệu hai lần, cách nhau bởi ít nhất một ký tự trắng. Ví dụ ta nhập
vào bàn phím như sau : 1989 2011 ↵ thì biến a sẽ nhận giá trị 1989, còn biến bsẽ nhận giá
trị 2011. Chúng ta cũng không cần quan tâm đến kiểu của các biến, C++ cũng tự động nhận
biết điều này. Xem xét đoạn chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
int num;
cin >> num;
nếu ta nhập vào giá trị 12.08 thì biến num chỉ nhận được giá trị 12. Còn phần thập phân sẽ
không được ghi nhận, do num là một biến kiểu int nên C++ tự động nhận biết và “chặt cụt”
đi phần này.
3. Những lưu ý trong nhập/ xuất xâu ký tự.
Trong tất cả các kiểu dữ liệu, theo mình thì nhập xuất với xâu (string) là thể loại củ chuối
và rắc rối nhất. Trước hết mình nói qua một chút về thư viện chuẩn <string> của C++. Nếu
bạn nào đã học qua ngôn ngữ C rồi thì đều biết xâu ký tự là một mảng chứa các ký tự
(kiểu char) và phần tử cuối cùng của mảng là NULL (hay \0) để đánh dấu sự kết thúc xâu.
Tuy nhiên, sử dụng mảng để lưu trữ xâu có phần phức tạp vì mảng là một cấu trúc
tĩnh phải biết rõ kích thước ngay khi khai báo. Điều này làm cho chương trình “cứng nhắc”
và không “kinh tế”. Ví dụ nếu khai báo ít quá thì khi muốn chứa thêm nhiều ký tự hơn sẽ
không được, mà nếu khai báo nhiều quá, không dùng hết sẽ lãng phí bộ nhớ. Để khắc phục
người ta dùng biện pháp quản lý mảng bằng con trỏ và cấp bộ nhớ phát động cho mảng
(dynamic memory allocation). Tuy nhiên nếu việc này diễn ra thường xuyên thì chương trình
sẽ rất rối rắm, gây mệt mỏi cho lập trình viên và dễ dẫn đến lỗi. C++ giải quyết tốt vấn đề
này bằng cách xây dựng lớp string. Một đối tượng của lớp string có thể lưu trữ các ký tự
“tốt hơn” mảng trong C rất nhiều. Muốn sử dụng nó ta phải include header file <string>.
Trong các bài viết, nếu không có lý do gì đặc biệt, thì mình sẽ dùng string để lưu trữ xâu.
Bây giờ trở lại vấn đề của chúng ta, đó là nhập/ xuất xâu ký tự. Xuất thì ok, không có vấn
đề gì phải bàn luận nhiều, nhưng nhập thì lại có nhiều điều để nói:
Thứ nhất, cin >> không cho phép nhập khoảng trắng. Xét đoạn chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
int main(){
string str;
cin>> str;
cout << str << endl;
return 0;
}
Nếu ta nhập vào đoạn văn bản sau: “Osama Binladen” thì xâu str chỉ ghi nhận đoạn
đầu “Osama” vì dấu cách là một khoảng trắng mà cin coi các dấu trắng là ký tự báo hiệu kết
thúc việc nhập dữ liệu. Để khắc phục điều này C++ cung cấp một số cách thức để nhập
toàn bộ xâu ký tự. Nhưng trước hết ta phải xem xét một vấn đề về bộ đệm.
Bộ đệm và các vấn đề liên quan đến nhập dữ liệu từ bàn phím
Khi ta nhập dữ liệu vào bàn phím thì dữ liệu không được đọc ngay vào biến mà được đẩy lên
trên bộ đệm (buffer). Dữ liệu sẽ tồn tại trong bộ đệm cho tới khi một lệnh nào đó gửi yêu
cầu đến bộ đệm “xin phép” được load dữ liệu về. Ví dụ khi chương trình gặp câu lệnh:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cin >> x; // với x là một biến kiểu int
thì nó sẽ mò lên buffer để load dữ liệu về, nếu như trên bộ đệm có số 100 thì nó sẽ đọc 100
vào biến x mà không đợi ta nhập gì cả, còn nếu bộ đệm trống hoặc có những dữ liệu không
phải số nguyên (ví dụ mã phím Enter của lần nhập trước) thì nó mới dừng lại đợi ta nhập dữ
liệu vào. Như vậy ta hoàn toàn không cần để ý nhiều việc sử dụngcin >> để nhập các dữ
liệu số (nguyên hoặc dấu chấm động – floating point), nhưng để nhập dữ liệu ký tự thì lại
hoàn phải hết sức chú ý. Trong đoạn chương trình test nhập xâu ký tự bên trên ta nhận thấy
biến str chỉ lưu trữ phần “Osama”, phần còn lại thì vẫn nằm trên buffer để cho lần nhập sau.
C++ cung cấp getline cho phép nhập toàn bộ xâu ký tự kể cả khoảng trắng. Cú pháp sử
dụng hàm getline như sau.
C++ Code:
Lựa chọn code | Ẩn/Hiện code
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’. Xét chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
int main(){
string str;
getline(cin,str);
cout << str << endl;
return 0;
}
Nếu ta nhập vào bàn phím xâu “Osama Binladen” rồi nhấn Enter thì kết quả thu được trên
màn hình sẽ là trọn vẹn xâu “Osama Binladen”. Điều gì xảy ra với ký tự Enter, nó có nằm
lại trên bộ đệm không? Câu trả lời là getline đã đọc mã của ký tự Enter nhưng không gắn nó
vào trong xâu str và cũng không để lại nó trên bộ đệm mà hủy nó đi.
Bây giờ ta xét đến hiện tượng trôi lệnh getline. Xét chương trình sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
int main(){
int num;
string str;
cout << "Input an integer a= ";
cin >> num;
cout << num << endl;
cout << "Input a string str= ";
getline(cin,str);
cout << str << endl;
cout << "End program" << endl;
return 0;
}
Bạn chạy thử chương trình trên sẽ thấy ngay. Sau khi nhập dữ liệu cho biến num, chương
trình không dừng lại cho ta nhập dữ liệu cho str. Mà in ngay ra thông báo “End program”.
Nguyên nhân là do sau khi nhập dữ liệu cho biến num ta gõ phím Enter. Mã của Enter được
lưu trong bộ đệm (chính là ký tự xuống dòng ‘\n’) và do đó khi chương trình gặp câu lệnh
C++ Code:
Lựa chọn code | Ẩn/Hiện code
getline(cin,str);
nó sẽ đọc ngay ký tự này và nhận thấy đây là ký tự kết thúc (delimiter) nên nó sẽ loại bỏ ký
tự này ra khỏi bộ đệm mà không đọc vào xâu str. Sau đó nó sẽ chạy thẳng đến câu lệnh tiếp
theo là:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
cout << "End program" << endl;
Người ta thường gọi đó là hiện tượng “trôi lệnh”. Để khắc phục hiện tượng trôi lệnh này thì
trước mỗi lệnh getline ta nên đặt câu lệnh:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
fflush(stdin);
Câu lệnh này có tác dụng xóa bộ đệm và do đó ta có thể yên tâm là sẽ không bị trôi lệnh
nữa
Hết bài 4
__________________
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:44 PM.
#5
28-02-2011, 12:22 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
Lớp và đối tượng
BÀI 5a. CLASSES & OBJECTS (PART 1)
Trong C++, class là nền tảng cho lập trình hướng đối tượng. Nó là sự mở rộng của khái
niệm data structure: thay vì chỉ lưu trữ dữ liệu thì nó lưu trữ cả dữ liệu và hàm. Tuy
nhiên nó chỉ là một khái niệm trừu tượng, nó không phải là một thực thể có thực khi chương
trình đang chạy, mà chỉ là khuôn mẫu để “đúc” ra những object. Mộtobject là một thể hiện
(instance) của class. Có thể coi class giống như kiểu dữ liệu còn object như các biến.
1. Định nghĩa một lớp
Các class được tạo ra bằng cách sử dụng từ khóa class. Chúng ta sẽ xem xét một ví dụ về
định nghĩa lớp. Giả sử chúng ta lập một lớp Student trong đó lưu trữ các thông tin về sinh
viên cũng như chứa các hàm để thao tác trên các dữ liệu này.
C++ Code:
Lựa chọn code | Ẩn/Hiện code
#include <iostream>
#include <string>
using namespace std;
// class definition
class Student{
private:
string name; // tên sinh viên
int age; // tuổi
string student_code; // mã số sinh viên
public:
// set information
void set_name(string); // nhập tên
void set_age(int); // nhập tuổi
void set_student_code(string); // nhập mã sinh viên
// get information
string get_name(); // lấy tên
int get_age(); // lấy tuổi
string get_student_code(); // lấy mã sinh viên
};
Ta sẽ phân tích định nghĩa trên của lớp Student. Đầu tiên là từ khóa class, sau đó là tên lớp
mà người dùng muốn tạo (ở đây là Student). Phần còn lại nằm trong cặp ngoặc
móc {} chứa những thành của lớp. Những dữ liệu như: name, age, student_code được gọi
là các thành phần dữ liệu (data members), còn các hàm như:set_name(), get_name(),
… được gọi là các hàm thành viên (member functions) hay phương thức (methods).
Thông thường các data members được để ở chế độprivate, còn các member functions thì ở
chế độ public.
Từ khóa private và public là hai access-specifier quy định quyền truy nhập đối với các
thành phần trong lớp, nó sẽ có hiệu lực cho các thành phần của lớp đứng sau nó cho đến khi
gặp một access-specifier khác. Tất cả các thành phần được khai báo là public sẽ có thể
được truy cập “thoải mái” bất cứ chỗ nào lớp có hiệu lực. Ví dụ nó có thể được truy cập bởi
hàm thành viên của của một lớp khác, hoặc các hàm tự do trong chương trình. Các thành
phần private thì được bảo mật cao hơn. Chỉ những thành phần của lớp mới có thể truy
nhập đến chúng. Mọi cố gắng truy nhập bất hợp pháp từ bên ngoài đều sẽ gây lỗi. Do đó ta
có thể mô tả cú pháp chung để định nghĩa một lớp như sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
class Class_name{
private:
// các thành phần private
public:
// các thành phần public
};
Chú ý sau mỗi định nghĩa lớp phải có dấu chấm phẩy (semicolon) vì định nghĩa lớp là tương
đương với định nghĩa một kiểu dữ liệu mới. Cú khai báo một đối tượng của
lớpStudent giống y như cú pháp khai báo một biến bình thường:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
int num1, num2; // khai báo hai biến nguyên num1, num2
Student studentA, studentB; // khai báo hai đối tượng thuộc lớp Student là
studentA và studentB
2. Định nghĩa các hàm thành viên cho lớp
Trong định nghĩa trên của lớp mới chỉ khai báo các nguyên mẫu hàm (function prototypes)
chứ hoàn toàn chưa có thân hàm. Ta sẽ phải định nghĩa các hàm này. Có hai cách định
nghĩa các hàm thành viên: định nghĩa hàm thành viên ngay trong định nghĩa lớp hoặc
khai báo nguyên mẫu trong lớp, còn định nghĩa bên ngoài lớp.
Định nghĩa hàm ngay trong định nghĩa lớp
Khi đó định nghĩa lớp được viết lại như sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
class Student{
private:
string name;
int age;
string student_code;
public:
// set information
void set_name(string str){ name=str; }
void set_age(int num){ age=num; }
void set_student_code(string str){ student_code=str; }
// get information
string get_name(){ return name; }
int get_age(){ return age; }
string get_student_code(){ return student_code; };
};
Ta nhận thấy các hàm đã được định nghĩa luôn trong nghĩa lớp. Tuy nhiên cách này không
phải là cách tốt. Với bài này thì các hàm còn đơn giản, còn ngắn. Nhưng trong thực tế khi ta
xây dựng các lớp những lớp phức tạp hơn thì số lượng hàm sẽ nhiều hơn và dài hơn. Nếu
định nghĩa trong lớp sẽ làm “mất mĩ quan” và khó kiểm soát. Vì vậy trong định nghĩa lớp ta
chỉ liệt kê các nguyên mẫu hàm, còn khi định nghĩa, ta sẽ định nghĩa ra bên ngoài.
Khai báo hàm trong lớp, còn định nghĩa ngoài lớp
C++ Code:
Lựa chọn code | Ẩn/Hiện code
// class definition
class Student{
private:
string name;
int age;
string student_code;
public:
// set information
void set_name(string);
void set_age(int);
void set_student_code(string);
// get information
string get_name();
int get_age();
string get_student_code();
};
// member function definitions
// set name
void Student::set_name(string str){
name=str;
}
// set age
void Student::set_age(int num){
age=num;
}
// set student code
void Student::set_student_code(string str){
student_code=str;
}
// get name
string Student::get_name(){
return name;
}
// get age
int Student::get_age(){
return age;
}
// get student code
string Student::get_student_code(){
return student_code;
}
Cú pháp để định nghĩa một hàm thành viên bên ngoài lớp là:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
<kiểu_trả_về> <tên_lớp>::<tên_hàm_thành_viên>(danh_sách_tham_số){
// định nghĩa thân hàm ở đây
}
Ví dụ hàm set_name
void Student::set_name(string str){
name=str;
}
Cú pháp này chỉ rõ rằng hàm ta đang định nghĩa là hàm thành viên của lớp Student vì nó
được chỉ định bởi toán tử phân giải phạm vi :: (trong trường hợp này làStudent:: ), nghĩa
là hàm này nằm trong phạm vi lớp Student.
Có một sự khác nhau giữa hai cách định nghĩa hàm này không phải chỉ ở góc độ “thẩm mỹ”.
Theo cách thứ nhất: định nghĩa luôn trong định nghĩa lớp, thì hàm được coi là“hàm nội
tuyến” hay inline. Thông thường khi muốn một hàm làm một việc gì đó thì ta phải làm một
việc là “gọi hàm” (invoke). Việc gọi hàm sẽ phải tốn các chi phí về thời gian như gửi lời gọi
hàm, truyền đối số, … điều này có thể làm chậm chương trình. C++ cung cấp một giải pháp
đó là “hàm nội tuyến” bằng cách thêm vào từ khóa inline trước kiểu trả về của hàm như
sau:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
inline <kiểu_trả_về> <tên_lớp>::<tên_hàm_thành_viên>(danh_sách_tham_số){
// định nghĩa thân hàm ở đây
}
Điều này “gợi ý” cho compiler sinh mã của hàm ở những nơi thích hợp để tránh phải gọi
hàm. Như vậy sẽ tránh được những chi phí gọi hàm nhưng ngược lại nó làm tăng kích thước
của chương trình. Vì cứ mỗi lời gọi hàm sẽ được thay thế bởi một đoạn mã tương ứng. Vì
vậy chỉ khai báo một hàm là inline khi kích thước của nó không quá lớn và không chứa
vòng lặp cũng như đệ quy. Hơn nữa không phải hàm nào khai báo inline đều được hiểu
là inline, vì đó chỉ là “gợi ý” cho compiler. Nếu compiler nhận thấy hàm có kích thước khá
lớn, xuất hiện nhiều lần trong chương trình, hoặc có chứa các cấu trúc lặp, đệ quy thì nó có
thể “lờ đi” yêu cầu inline này. Hiện này các compiler đều được tối ưu rất tốt, vì vậy không
cần thiết phải dùng inline. Đó là sự khác biệt của các hàm thành viên được định nghĩa ngay
trong lớp so với các hàm thành viên được định nghĩa bên ngoài.
Hết bài 5a
__________________
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 : 08-03-2011 lúc 03:17 PM.
#6
28-02-2011, 01:40 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
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ớpStudent 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
đến student_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_namevẫn thế, do đó chương trình không phải chỉnh sửa nhiều.
Hết bài 5b
__________________
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 : 08-03-2011 lúc 03:18 PM.
#7
28-02-2011, 06:30 PM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
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ệuwidth 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(){
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
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 << rectA.area() << endl; // kết quả là 0
cout << rectB.area() << endl; // kết quả là 12
C++ sẽ tự nhận biết để gọi constructor phù hợp. Trong đoạn chương trình trên, câu lệnh
thứ nhất khởi tạo đối tượng rectA nhưng không kèm theo truyền tham số vào, nên
compiler sẽ gọi tới hàm tạo thứ nhất, tức hàm tạo không có tham số. Sau câu lệnh
này rectA đều có width và height đều bằng 0. Câu lệnh thứ hai khởi tạo đối tượng rectB,
nhưng đồng thời truyền vào hai đối số là 3 và 4. Do đó compiler sẽ gọi đến hàm tạo thứ hai.
Sau câu lệnh này rectB có width=3 còn height=4. Và kết quả ta được diện tích
thằng rectA là 0, còn rectB là 12.
Cách thứ hai: dùng đối số mặc định (default arguments)
Chúng ta vẫn làm việc với lớp Rectangle ở trên và sẽ chỉ dùng một hàm tạo nhưng “chế
biến” nó một chút:
C++ Code:
Lựa chọn code | Ẩn/Hiện code
class Rectangle{
private:
int width;
int height;
public:
// constructor
Rectangle(int =0, int =0); // hàm tạo với đối số mặc định
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// 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;
}
/* 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
__________________
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 : 08-03-2011 lúc 03:19 PM.
#8
01-03-2011, 02:35 AM
first_pace
Thành viên chính thức
Ngày gia nhập: 02 2011
Nơi ở: Hà Nội
Bài viết: 67
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