1. Trang chủ
  2. » Kỹ Thuật - Công Nghệ

Tìm hiểu lập trình hướng đối tượng với c++

66 496 0

Đ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

Thông tin cơ bản

Định dạng
Số trang 66
Dung lượng 495,79 KB

Nội dung

Tìm hiểu lập trình hướng đối tượng với c++

Trang 1

VIỆN ĐIỆN TỬ VIỄN THÔNG - -

ĐỒ ÁN 1

Chủ đề: Xử Lí Ảnh

Phần 1: Tìm hiểu lập trình hướng đối tượng với C++

Hà Nội, tháng 09/2016

Trang 2

VIỆN ĐIỆN TỬ VIỄN THÔNG

Trang 3

Mục lục

Trang 4

1 Lập trình hướng đối tượng C++

1.1 Mục tiêu

Giúp người đọc hiểu về lập trình hướng đối tượng và ngôn ngữ lập trình C++

1.2 Các khái niệm lập trình hướng đối tượng

1.2.1 Khái niệm lập trình hướng đối tượng

Khái niệm hướng đối tượng được xây dựng trên nền tảng của khái niệm lập trình

có cấu trúc và sự trừu tượng hóa dữ liệu Sự thay đổi căn bản ở chỗ, một chương trình hướng đối tượng được thiết kế xoay quanh dữ liệu mà chúng ta có thể làm việc trên

đó, hơn là theo bản thân chức năng của chương trình Điều này hoàn toàn tự nhiên một khi chúng ta hiểu rằng mục tiêu của chương trình là xử lý dữ liệu

Lập trình hướng đối tượng cho phép chúng ta tổ chức dữ liệu trong chương trình theo một cách tương tự như các nhà sinh học tổ chức các loại thực vật khác nhau Theo cách nói lập trình đối tượng, xe hơi, cây cối, các số phức, các quyển sách đều được gọi là các lớp (Class)

Một lớp là một bản mẫu mô tả các thông tin cấu trúc dữ liệu, lẫn các thao tác hợp lệ của các phần tử dữ liệu Khi một phần tử dữ liệu được khai báo là phần tử của một lớp thì nó được gọi là một đối tượng (Object) Các hàm được định nghĩa hợp lệ trong một lớp được gọi là các phương thức (Method) và chúng là các hàm duy nhất cóthể xử lý dữ liệu của các đối tượng của lớp đó Mộtthực thể (Instance) là một vật thể

có thực bên trong bộ nhớ, thực chất đó là một đối tượng (nghĩa là một đối tượng được cấp phát vùng nhớ)

1.2.2 Khái niệm sự đóng gói

Sự đóng gói là cơ chế ràng buộc dữ liệu và thao tác trên dữ liệu đó thành một thểthống nhất, tránh được các tác động bất ngờ từ bên ngoài Thể thống nhất này gọi là đối tượng

Trong một đối tượng, dữ liệu hay thao tác hay cả hai có thể là riêng (Private) hoặc chung (Public) của đối tượng đó Thao tác hay dữ liệu riêng là thuộc về đối tượng đó chỉ được truy cập bởi các thành phần của đối tượng, điều này nghĩa là thao tác hay dữ liệu riêng không thể truy cập bởi các phần khác của chương trình tồn tại ngoài đối tượng Khi thao tác hay dữ liệu là chung, các phần khác của chương trình cóthể truy cập nó mặc dù nó được định nghĩa trong một đối tượng Các thành phần chung của một đối tượng dùng để cung cấp một giao diện có điều khiển cho các thành thành riêng của đối tượng

Trang 5

1.2.3 Khái niệm tính kế thừa

Chúng ta có thể xây dựng các lớp mới từ các lớp cũ thông qua sự kế thừa Một lớp mới còn gọi là lớp dẫn xuất (derived class), có thể thừa hưởng dữ liệu và các phương thức của lớp cơ sở (base class) ban đầu Trong lớp này, có thể bổ sung các thành phần dữ liệu và các phương thức mới vào những thành phần dữ liệu và các phương thức mà nó thừa hưởng từ lớp cơ sở Mỗi lớp (kể cả lớp dẫn xuất) có thể có một số lượng bất kỳ các lớp dẫn xuất Qua cơ cấu kế thừa này, dạng hình cây của các lớp được hình thành Dạng cây của các lớp trông giống như các cây gia phả vì thế các lớp cơ sở còn được gọi là lớp cha (parent class) và các lớp dẫn xuất được gọi là lớp con (child class)

1.2.4 Khái niệm tính đa hình

Đó là khả năng để cho một thông điệp có thể thay đổi cách thực hiện của nó theolớp cụ thể của đối tượng nhận thông điệp Khi một lớp dẫn xuất được tạo ra, nó có thể thay đổi cách thực hiện các phương thức nào đó mà nó thừa hưởng từ lớp cơ sở của

nó Một thông điệp khi được gởi đến một đối tượng của lớp cơ sở, sẽ dùng phương thức đã định nghĩa cho nó trong lớp cơ sở Nếu một lớp dẫn xuất định nghĩa lại một phương thức thừa hưởng từ lớp cơ sở của nó thì một thông điệp có cùng tên với phương thức này, khi được gởi tới một đối tượng của lớp dẫn xuất sẽ gọi phương thức

đã định nghĩa cho lớp dẫn xuất

Trang 6

return 0;

}

Bên cạnh những dòng lệnh, chúng ta còn có những dòng comment

// This comment is located outside main function

/* We can put comment everywhere in a C++ file */

1. cout << exp ; // output content of expression

Hoặc cho nhiều biểu thức:

2. cout << exp_1 << exp_2 << … << exp_n ; // output content of expression 1, 2,

Trang 7

Hoặc cho nhiều biến:

9. 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 tanhậ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: cin >> a >> b;

Trang 8

1.4 Đối tượng và Lớp

1.4.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 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 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

1.4.2 Lớp (Class)

- Định nghĩa 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

8. string name; // tên sinh viên

9. int age; // tuổi

10. string student_code; // mã số sinh viên

11. public:

12. // set information

13. void set_name(string) // nhập tên

14. void set_age(int) // nhập tuổi

15. void set_student_code(string) // nhập mã sinh viên

16.

17. // get information

18. string get_name(); // lấy tên

19. int get_age(); // lấy tuổi

20. string get_student_code(); // lấy mã sinh viên

21. };

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,

Trang 9

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:

- Đị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:

8. void set_name(string str){ name=str; }

9. void set_age(int num){ age=num; }

Trang 10

10. void set_student_code(string str){ student_code=str; }

11.

12. // get information

13. string get_name(){ return name; }

14. int get_age(){ return age; }

15. string get_student_code(){ return student_code; };

16. };

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ượnghà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

Trang 11

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

1. inline <kiểu_trả_về> <tên_lớp>::<tên_hàm_thành_viên>(danh_sách_tham_s

ố){

2. // định nghĩa thân hàm ở đây

3. }

Trang 12

Đ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

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student

2. studentA.set_name(“Bill Gates”) // gán tên cho studentA là “Bill Gates”

3. 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:

1. Student studentA; // khai báo đối tượng studentA thuộc lớp Student

2. studentA.name=”Bill Gate”; // error

Trang 13

8. int width; // chiều rộng

9. int height; // chiều cao

10. public:

11. // set width & height

12. void set_width(int) // nhập chiều rộng

13. void set_height(int) // nhập chiều cao

14.

15. // get width & height

16. int get_width(); // lấy chiều rộng

17. int get_height(); // lấy chiều cao

Trang 14

đó là hàm tạo (constructor)

1.5.2 Hàm tạo

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:

Trang 15

sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0

1.5.3 Thiết lập giá trị 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á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ạmthờ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:

1. class Rectangle{

2. private:

3. int width;

Trang 16

4. int height;

5. public:

6. // constructor

7. Rectangle(); // hàm tạo không có tham số

8. Rectangle(int, int) // hàm tạo với hai tham số

9.

10. /* các hàm khác khai báo ở chỗ này */

11. };

12.

13. // member function definitions

14. // constructor with no parameters

Bây giờ ta sẽ test bằng chương trình sau:

1. Rectangle rectA; // gọi hàm tạo không tham số

2. Rectangle rectB(3,4) // gọi hàm tạo có tham số

3.

4. cout << rectA.area() << endl; // kết quả là 0

5. 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ởitạ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à

Trang 17

12. // member function definitions

13. // constructor with default arguments

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:

1. Rectangle rectA; // sẽ gán width=0, height=0

2. Rectangle rectB(4) // sẽ gán width=4, height=0

3. 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

1. // lỗi đặt đối số mặc định khi định nghĩa hàm

2. Rectangle::Rectangle(int a=0, int b=0){ // error

Trang 18

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ố:

12. // member function definitions

13. // constructor with 2 parameters

2. 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à xong

Trang 19

1.6 Hàm (Functions)

1.6.1 Tại sao phải dùng hàm

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 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 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ânthứ 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 <string.h> của C được viết để tính chiềudài của một xâu bất kỳ, vì vậy khi muốn tính độ dài của một xâu nào đó ta chỉ việc gọihà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

1.6.2 Khai báo và định nghĩa một hàm

Một nguyên tắc 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 chotrình biên dịch biết điều đó 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)

1. <kiểu_trả_về> <tên_hàm>(danh_sách_tham_số)

Ví dụ:

2. 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 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.”

- Đị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ịchtiếp Giả sử nó gặp một câu lệnh như sau:

Trang 20

1. 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:

1. int square(int n){

2. return n* ;

3. }

Đị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ỏ

o 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ụ:

1. int min(int a, int b) // a và b là tham số

2. minimum=min(x,y) // x, y đối số được truyền vào cho hàm

o 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ụ

1. int min(int, int) // khai báo nguyên mẫu không có tham số hình thức mà chỉ

1.6.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ẳnghạ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

- Truyền hằng (passing constants)

Xét hàm square ở trên, câu lệnh:

1. 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à

Trang 21

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:

1. n=10;

2. 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

o Truyền bằng tham trị (pass-by-value)

Xét đoạn chương trình sau:

1. #include <iostream>

2. using namespace std;

3.

4. int min(int a, int b){

5. return (a<b?a:b) // trả về số nhỏ nhất trong hai số nguyên

Chúng ta đều đoán được kết quả là màn hình hiển thị min= 5, nhưng thực

sự thì chương trình trên hoạt động như thế nào? Ta để ý vào câu lệnh:

1. int z=min(x,y)

Khi gặp câu lệnh này, compiler sẽ gọi đến hàm min và thực hiện truyền x

và y làm đối số Tuy nhiên, đây là truyền theo tham trị Tức là x, y không được truyền trực tiếp vào trong hàm min mà compiler thực hiện một công đoạn như sau: đầu tiên nó tạo ra hai biến tạm a, b có kiểu int, rồi copy giá trị của x, y vào hai biến đó Sau đó hai biến tạm đó được tống vào trong hàm min và thực tế hàm min đang thao tác trên “bản sao” của x và y chứ không phải trực tiếp trên x, y Điều này có cái lợi mà cũng có cái hại Cái

Trang 22

lợi là do không bị thao tác trực tiếp nên các biến ban đầu (ở đây là x và y)

sẽ không có khả năng bị "dính" những sửa đổi không mong muốn do hàm min gây ra Còn cái hại là nếu như ta muốn sửa đổi giá trị của biến ban đầu thì lại không được (ví dụ muốn hoán đổi nội dung của hai biến x,

y cho nhau) vì mọi thao tác là trên bản sao của x, y chứ không phải trên x,

y Thêm nữa, khi tạo bản sao cần phải tạo ra những biến tạm copy dữ liệu

từ biến gốc sang biến tạm Điều này gây ra những chi phí về bộ nhớ cũng như về thời gian, đặc biệt khi kích thước của các đối số lớn hoặc được truyền nhiều lần

Trang 23

o Truyền theo tham chiếu (pass-by-reference)

Như đã nói ở trên truyền theo tham trị không truyền bản thân biến vào mà chỉ truyền bản sao cho hàm Do đó có những hạn chế nhất định của nó Bây giờ mời bà con và cô bác ngâm cứu cách truyền thứ hai, truyền theo tham chiếu (passing-by-reference) Có hai cách để truyền theo tham chiếu là truyền tham chiếu thông qua tham chiếu (pass-by-reference-with-references), và truyền tham chiếu thông qua con trỏ (pass-by-reference-with-pointers) Nghe có vẻ hơi lằng nhằng nhưng mình sẽ giải thích ngay bây giờ

Truyền tham chiếu thông qua con trỏ

Chắc chắn các bạn đã quen thuộc với con trỏ rồi nên mình sẽ không nói nhiều về phần này Tuy nhiên có thể mình sẽ dành ra một bài để viết riêng

về mục con trỏ nếu thấy cần thiết để đảm bảo tính hệ thống Nhắc lại, con trỏ là một biến đặc biệt lưu trữ địa chỉ của một biến mà nó trỏ tới Cú phápkhai báo con trỏ cũng như cách sử dụng nó được mình họa trong chương trình sau:

1. #include <iostream>

2. using namespace std;

3.

4. int main(){

5. int x; // khai báo một biến nguyên

6. int *ptr; // khai báo một con trỏ kiểu nguyên

7. ptr=&x; // ptr trỏ tới x hay gán địa chỉ của x cho ptr

8.

9. *ptr=10; // gán giá trị 10 cho vùng nhớ mà ptr trỏ tới, cụ thể ở đây là x

10. cout << x << endl; // in giá trị của x, bây giờ là 10

Trang 24

13. int y=7;

14.

15. // trước khi gọi swap

16. cout << "Before calling swap" << endl;

23. // sau khi gọi swap

24. cout << "After calling swap" << endl;

Câu lệnh này truyền địa chỉ của x và y chi hàm swap, và hàm swap cứ thế

mò thẳng đến vùng nhớ của x và y mà thao tác Điều này nghĩa mọi mọi thao tác trong hàm swap có thể làm thay đổi biến ban đầu, và do đó nó chophép hoán đổi nội dung của x, y cho nhau Truyền tham chiếu thông qua con trỏ cũng có cái lợi và cái hại Cái lợi thứ nhất là nó cho phép thao tác trực tiếp trên biến ban đầu nên có thể cho phép sửa đổi nội dung của biến nếu cần thiết (như ví dụ hàm swap trên) Thứ hai, cũng do thao tác trực tiếp trên biến gốc nên ta không phải tốn chi phí cho việc tạo biến phụ hay copy các giá trị sang biến phụ Cái hại là làm giảm đi tính bảo mật của dữ liệu Ví dụ trong trường hợp hàm min ở trên ta hoàn toàn không mong muốn thay đổi dữ liệu của biến gốc mà chỉ muốn biết thằng nào bé hơn Nhưng nếu truyền theo kiểu con trỏ như thế này có khả năng ta “lỡ” sửa đổi biến gốc và do đó gây ra lỗi (sợ nhất vẫn là những lỗi logic, nó không chạy thì còn đỡ, nó chạy sai mới đểu)

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++

1. #include <iostream>

Trang 25

2. using namespace std;

3.

4. int main(){

5. int x; // khai báo biến nguyên x

6. int &ref=x; // tham chiếu ref là nickname của x

7.

8. ref=10; // gán ref=10, nghĩa là x cũng bằng 10

9. cout << x << endl; // in giá trị của x, tức là 10, lên màn hình

10. return 0;

11. }

Một lưu ý về tham chiếu là nó phải được khởi tạo ngay khi khai báo Câu lệnh như sau sẽ báo lỗi:

1. int &ref; // lỗi không khởi tạo ngay khi khai báo

Mọi thay đổi về trên tham chiếu cũng gây ra những thay đổi tương tự trên biến vì bản chất nó là hai cái tên cho cùng một biến Vì vậy ta cũng có thể dùng tham chiếu để truyền đối số cho hàm với tác dụng giống hệt con trỏ Bây giờ ta sẽ cải tiến lại hàm swap bên trên bằng cách dùng tham chiếu

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

Trang 26

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:

1. #include <iostream>

2. using namespace std;

3.

4. int min(const int& a, const int& b){

5. return (a<b?a:b) // trả về giá trị nhỏ hơn

Chú ý vào header của hàm:

1. int min(const int& a, const int& b)

Việc đặt từ khóa const trước kiểu của tham số a và b như trên được gọi là truyền theo tham chiếu hằng Với từ khóa const này, ta vẫn truyền trực tiếpbiến x, y vào cho hàmmin nhưng hàm min không có quyền “sửa đổi” giá trị của x, y mà chỉ được dùng những thao tác không làm ảnh hưởng đến x,

y như so sánh, lấy giá trị của x, y để tính toán, … Nếu cố tình sửa đổi x,

y sẽ gây lỗi Xét một ví dụ như sau:

1. int example(const int& a){

2. a=20; // lỗi vì cố tình sủa đổi tham chiếu hằng

3. return a;

4. }

Việc sử dụng tham chiếu hằng như trên là một ví dụ về nguyên tắc “quyền

ưu tiên tối thiểu” (the principle of least privilege), một nguyên tắc nền tảngtrong lập trình Trong trường hợp này nghĩa là chỉ trao cho hàm min nhữngquyền ưu tiên tối thiểu thao tác trên dữ liệu để nó đủ thực hiện nhiệm vụ, không hơn Rõ ràng hàm min chỉ cần so sánh hai đối số truyền vào để xemthằng nào nhỏ hơn rồi trả về giá trị Vì vậy truyền theo tham chiếu hằng là phương án đảm bảo nguyên tắc trên

Trang 27

- Truyền cấu trúc dữ liệu (passing data structures)

Tạm thời mình chỉ giới thiệu cấu trúc đơn giản nhất là mảng (arrays) Còn nhữngcấu trúc dữ liệu phức tạp hơn, nếu có điều kiện mình sẽ nói trong dịp khác Như

ta biết tên mảng là một con trỏ hằng, trỏ đến phần tử đầu tiên của mảng Vì vậy truyền mảng giống như truyền con trỏ vậy Chương trình sau gọi hàm input để nhập các phần tử vào một mảng, vàoutput để xuất các phần tử của mảng:

1. #include <iostream>

2. using namespace std;

3.

4. void input(int*, int) // nguyên mẫu hàm input

5. void output(int*, int) // nguyên mẫu hàm output

6.

7. int main(){

8. int num; // biến lưu số lượng phần tử mảng

9. int *ptr; // con trỏ quản lý mảng

10.

11. cout << "Enter number of elements: " << endl;

12. cin >> num; // nhập số lượng phần tử mảng

13. ptr=new int[num] // cấp phát bộ nhớ động cho con trỏ ptr

14.

15. cout << "Enter elements: " << endl;

16. input(ptr, num) //nhập mảng

17.

18. cout << "Here are elements of the array: " << endl;

19. output(ptr, num) // xuất mảng

25. void input(int* a, int n){

26. for(int i=0; i<n; i++){

27. cout << "element "<< i+1 << "= ";

33. void output(int* a, int n){

34. for(int i=0; i<n; i++){

35. cout << a[i] << " ";

36. }

37. }

Trang 28

1.6.4 Trả về giá trị của hàm (returning value from functions)

Khi hoàn tất nhiệm vụ, hàm có thể trả về một giá trị nào đó cho tên hàm Ví dụ

hàm square trả về giá trị là bình phương của đối số truyền vào Kiểu trả về của hàm

quyết định kiểu của giá trị được trả về Nó có thể là bất cứ kiểu built-in nào

(như char, in, long, double, … ) hoặc các kiểu người dùng định nghĩa

như Rectangle hay Student mà ta đã xây dựng ở những bài trước Hàm cũng có thể

trả về giá trị là một con trỏ hoặc một tham chiếu Phần này mình sẽ tập trung vào

những vấn đề cần lưu ý khi trả về một con trỏ hay hoặc một tham chiếu cho tên hàm

- Trả về một con trỏ (returning a pointer)

Khi nào ta dùng hàm để trả về con trỏ? Có rất nhiều trường hợp bạn trả lại con trỏ cho lời gọi hàm Nhắc lại, xâu ký tự là một con trỏ hằng Bây giờ mình sẽ viếtmột chương trình convert một xâu ký tự thành chữ hoa Trong chương trình có hàm to_upper nhận vào một xâu ký tự ASCII-8 bit, đổi hết các ký tự thành ký tự hoa, rồi trả về xâu viết hoa:

8. for(int i=0; i<length; i++){ // duyệt hết xâu

9. if(str[i]>=97 && str[i]<=122){ // nếu là chữ thường

10. str[i] =32; // đổi thành chữ hoa

21. s2=new char[strlen(s1)]; // cấp phát bộ nhớ động cho con trỏ s2

22. s2=strcpy(s2,to_upper(s1)); // copy kết quả đổi xâu s1 thành chữ hoa cho

Trang 29

Hey, baby ! you are crazy

HEY, BABY ! YOU ARE CRARY

Đây là một ví dụ về việc trả về con trỏ cho hàm, tránh nhầm lẫn hàm trả về con trỏ với con trỏ hàm (function pointer) Bởi vì con trỏ hàm là một vấn đề tương đối phức tạp nên mình sẽ viết riêng một bài

- Trả về tham chiếu (returning a reference)

Hàm trả về tham chiếu tức là giá trị của hàm trả về là một tham chiếu đến một biến nào đó Hàm trả về tham chiếu có dạng:

Trang 30

17. }

Mình đã test chương trình trên, kết quả nó vẫn chạy ngon lành, cho kết quả đúng.Thực sự mình cũng không hiểu thế này là thế nào? Về nguyên tắc thì việc trả về tham chiếu tới biến sqr như trong hàm dangling_square ở trên là “không ổn” , nhưng có thể là mấy cái compiler bây giờ nó được tối ưu tinh vi nên nhận biết được "ý định" của chúng ta Nó issue một cái warning như sau (IDE mình dùng

là Dev C++ 4.9.9.2)

[Warning] reference to local variable `sqr' returned

Rõ ràng thằng compiler cũng nhận ra điều gì đó “không ổn” Tại sao nó lại

“không ổn”? Để ý hàm dangling_square ta thấy biến sqr được khai báo trong hàm, do đó nó là một biến cục bộ (local variable) Nó chỉ “sống” khi

hàm dangling_square thực thi Khi hàm kết thúc nó cũng “die” theo hàm luôn Ở chương trình trên của ta, trước khi chết nó kịp return một cái tham chiếu Trong hàm main câu lệnh:

1. int y=dangling_square(x)

sẽ gán giá trị của dangling_square(x) cho y, nhưng giá trị này là một tham chiếu tới một thằng đã "chết", vì vậy ta hoàn toàn không thể dự đoán được hành vi của chương trình

1.6.5 Đối số mặc định (default arguments)

Cái này mình đã nói qua trong bài nói về constructor, bây giờ mình sẽ nói rõ hơn Khi khai báo một hàm ta có thể chỉ định những giá trị mặc định cho các tham số Nếu như khi gọi hàm, những đối số tương ứng với những vị trí này bị khuyết (không được truyền) thì những giá trị mặc định sẽ được thay thế vào đó Do đó chúng được gọi là đối mặc định (default arguments) Xét chương trình sau:

12. // truyền đủ đối, các giá trị mặc dịnh không được dùng

13. cout << default_arg(2, 3, ) << endl;

14.

15. // khuyết đối ở vị trí thứ 3, giá trị mặc định c=2 được dùng

16. cout << default_arg(2, 3) << endl;

Trang 31

18. // khuyết đối ở vị trí thứ 2 và thư 3, giá trị mặc định b=1, c=2 được dùng

19. cout << default_arg(2) << endl;

1. cout << default_arg(2, ) << endl;

chỉ truyền vào hai đối là 2 và 3, trong khi hàm yêu cầu ba đối, vì vậy vị trí thứ 3 là vị

trí khuyết, và được sử dụng mặc định c=2.

1.6.6 Quá tải hàm (Function Overloading)

C++ cho phép nhiều hàm trùng tên nhau trong cùng một phạm vi, miễn là danh sách tham số của chúng khác nhau (khác về số lượng tham số hoặc nếu cùng số lượng thì các tham số ở những vị trí tương ứng phải khác kiểu) Khả năng này được gọi

là “quá tải hàm” (function overloading) Giả sử một hàm có tên overloaded_func đượcquá tải thành tầm chục cái hàm cùng tên thì khi bắt gặp lời gọi hàm overloaded_func, compiler sẽ xem xét qua chục hàm này để tìm ra hàm phù hợp nhất dựa vào việc so sánh các đối số truyền vào với danh sách tham số ở header hàm (về số lượng cũng nhưkiểu ở các vị trí tương ứng) Ví dụ như chương trình sau:

1. #include <iostream>

2. using namespace std;

3.

4. // khai báo ba hàm cùng tên

5. int min(int, int)

6. int min(int, int, int)

7. double min(double, double)

8.

9. // hàm tính min 2 số nguyên

10. int min(int a, int b){

11. cout << "Call function 1: int min(int a, int b)" << endl;

Trang 32

20. int min(int a, int b, int c){

21. cout << "Call function 2: int min(int a, int b, int c)" << endl;

32. // hàm tính min hai số double

33. double min(double a, double b){

34. cout << "Call function 3: double min(double a, double b)" << endl;

Kết quả như sau:

Call function 1: int min(int a, int b)

Trang 33

Khi quá tải hàm có tham số mặc định thì cần phải hết sức chú ý bởi vì rất dễ dẫn đến sự nhập nhằng Ví dụ ta có hàm min được quá tải thành hai hàm như sau:

1. int min(int a, int b)

2. int min(int a, int b, int c=0)

nếu bắt gặp câu lệnh

1. x=min(4, )

thì compiler không biết phải gọi hàm nào vì xét cả hai hàm đều hòan toàn hợp lệ.Nếu gọi hàm thứ nhất, min sẽ được truyền đủ 2 đối số và kết quả z=4 Còn nếu gọi hàm thứ hai, đối thứ 3 bị khuyết và được mặc định là 0, kết quả z=0 Điều này gây ra lỗi biên dịch

1.6.7 Hàm nội tuyến (inline function)

Khi tổ chức chương trình thành các hàm chương trình sẽ sáng sủa hơn, thuận tiện hơn cho debug và bảo trì Tuy nhiên trong lúc thực thi ta phải “gọi hàm” Việc gọihàm tốn những chi phí về không gian và thời gian nhất định Vì vậy C++ cung cấp khái niệm hàm nội tuyến (inline) để giảm bớt chi phí gọi hàm, đặc biệt là với những hàm có kích thước nhỏ Để khai báo một hàm là nội tuyến, ta chỉ cần đặt từ

khóa inline phía trước kiểu trả về của hàm Ví dụ

1. inline int sqr(int x) // khai báo hàm sqr là inline

Khai báo một hàm inline gợi ý cho compiler sinh mã của hàm vào những nơi tương ứng mà nó được gọi Điều này giúp tránh những lời gọi hàm, nhưng ngược lại

nó làm tăng kích thước chương trình vì mỗi nơi có lời gọi hàm sẽ được chèn vào một bản copy mã đã được dịch của hàm Compiler có thể lờ đi những yêu cầu 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

1.6.8 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

- Biến cục bộ (Local Variables)

Biến cục bộ là những biến được khai báo bên trong một khối lệnh Một khối lệnh(code block) là một nhóm các câu lệnh được nhóm lại bên trong cặp ngoặc móc {} Ví dụ:

Ngày đăng: 07/02/2017, 22:16

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN

w