Con Trỏ Hàm Trong C++ (Function Pointers)

--- Bài mới hơn ---

  • Con Trỏ (Pointer) Trong C /c++ (Part 1)
  • Khi Nào Sử Dụng Private Protected Và Public
  • Bài 3: Lớp Trong Lập Trình Hướng Đối Tượng
  • Tất Tần Tật Về Lập Trình Hướng Đối Tượng? (Phần 2)
  • Câu Lệnh “return {}” Có Nghĩa Là Gì Trong C++ 11?
  • Dẫn nhập

    Trong bài học này, chúng ta sẽ cùng tìm hiểu về Con trỏ hàm trong C++ (Function pointers).

    Nội dung

    Để đọc hiểu bài này tốt nhất các bạn nên có kiến thức cơ bản về:

    Trong bài ta sẽ cùng tìm hiểu các vấn đề:

    • Đặt vấn đề
    • Con trỏ hàm là gì?
    • Gán địa chỉ của hàm cho con trỏ hàm
    • Gọi một hàm bằng con trỏ hàm
    • Truyền con trỏ hàm vào hàm dưới dạng đối số
    • Đối số mặc định của tham số hàm kiểu con trỏ hàm
    • std::function trong C++11
    • Khai báo con trỏ hàm với từ khóa auto trong C++11

    Đặt vấn đề

    Cùng xem ví dụ sau:

    int func(int a) { // do something return a; } int main() { cout << func << 'n'; // in địa chỉ hàm func trong bộ nhớ cout << func(1) << 'n'; // đi đến địa chỉ hàm func và thực thi hàm return 0; }

    Giống như các biến, hàm cũng được lưu trữ tại một địa chỉ trong bộ nhớ. Khi hàm được gọi, chương trình sẽ đi đến địa chỉ của hàm trong bộ nhớ, sau đó thực thi mã lệnh tại vùng nhớ đó.

    Vì hàm cũng có địa chỉ trong bộ nhớ, nên ta cũng có thể khai báo một con trỏ cho một hàm.

    Con trỏ hàm là gì?

    Con trỏ hàm là một biến lưu trữ địa chỉ của một hàm, thông qua biến đó, ta có thể gọi hàm mà nó trỏ tới.

    Cú pháp khai báo con trỏ hàm:

    int(*fcnPtr)(int); // con trỏ hàm nhận vào 1 biến kiểu int và trả về kiểu int void(*fcnPtr)(int, int); // con trỏ hàm nhận vào 2 biến kiểu int và trả về kiểu void

    Gán địa chỉ của hàm cho con trỏ hàm

    Giống như mọi con trỏ khác, con trỏ hàm phải được định nghĩa giá trị trước khi sử dụng.

    // khai báo prototype int funcA(); int funcB(); void funcC(); double funcD(int a); int main() { int(*fcnPtr)() = funcA(); // lỗi, không dùng dấu ngoặc đơn () sau tên hàm int(*fcnPtrA)() = funcA; // ok, con trỏ fcnPtrA trỏ đến hàm funcA fcnPtrA = funcB; // ok, fcnPtrA có thể trỏ đến một hàm khác có cùng cấu trúc // fcnPtrA = &funcB; tương tự câu lệnh trên int(*fcnPtr1)() = funcA; // ok void(*fcnPtr2)() = funcA; // lỗi, kiểu trả về của con trỏ hàm và hàm không trùng nhau void(*fcnPtr3)() = funcC; // ok double(*fcnPtr4)(int) = funcD; // ok return 0; }

    Không giống như các kiểu dữ liệu cơ bản, C++ sẽ ngầm chuyển đổi một hàm thành một con trỏ hàm nếu cần (vì vậy bạn không cần sử dụng toán tử (&) để lấy địa chỉ của hàm).

    Chú ý: Cấu trúc (tham số và kiểu trả về) của con trỏ hàm phải khớp với cấu trúc của hàm.

    Gọi một hàm bằng con trỏ hàm

    Con trỏ hàm có thể được sử dụng để gọi hàm mà nó trỏ đến. Có hai cách để thực hiện lời gọi hàm:

    Chú ý: Các tham số mặc định của hàm không sử dụng được thông qua con trỏ hàm. Tham số mặc định được compiler xác định tại thời điểm biên dịch (compile) chương trình, còn con trỏ hàm được sử dụng tại thời điểm chương trình đang chạy (run time).

    Truyền hàm vào hàm dưới dạng đối số

    Con trỏ hàm cũng là một biến con trỏ, do đó chúng ta có thể sử dụng con trỏ hàm là tham số của một hàm nào đó. Khi tham số của hàm là con trỏ hàm, đối số chính là địa chỉ của hàm.

    Ví dụ: Viết chương trình thực hiện việc sắp xếp tăng, giảm mảng 1 chiều các số nguyên. Nếu chưa có kiến thức về con trỏ hàm, có thể bạn sẽ thực hiện như bên dưới:

    Chương trình trên sử dụng thuật toán selection sort để sắp xếp mảng. Bạn có thể thấy, 2 hàm selectionSortAsc() và selectionSortDesc() chỉ khác nhau ở câu lệnh so sánh bên trong vòng lặp thứ 2.

    Khi sử dụng con trỏ hàm, bạn có thể tạo ra 1 hàm sắp xếp tổng quát cho 2 hàm trên:

    Chương trình trên sử dụng con trỏ hàm là tham số thứ 3 của hàm selectionSort(). Khi có thêm những nhu cầu sắp xếp khác nhau, chúng ta chỉ cần viết thêm hàm có điều kiện sắp xếp, và thay đổi đối số thứ 3 khi gọi hàm, mà không phải viết lại toàn bộ thuật toán bên trong hàm.

    Đối số mặc định của tham số hàm kiểu con trỏ hàm

    Tương tự như những kiểu dữ liệu cơ bản khác, chúng ta có thể cung cập một đối số mặc định cho tham số hàm kiểu con trỏ hàm.

    // mặc định hàm được sắp xếp tăng dần nếu không truyền vào đối số thứ 3 void selectionSort(int *arr, int n, bool(*comparisonFcn)(int, int) = asc); int main() { int arr[] = { 64, 25, 12, 22, 11 }; int n = sizeof(arr) / sizeof(int); // Sắp xếp tăng selectionSort(arr, n); // Sắp xếp giảm selectionSort(arr, n, desc); return 0; }

    std::function trong C++11

    Việc sử dụng kiểu dữ liệu std::function cũng tương tự như sử dụng con trỏ hàm, chỉ khác nhau về cách khai báo.

    Khai báo con trỏ hàm với từ khóa auto trong C++11

    Từ phiên bản C++11 trở về sau, từ khóa auto được dùng để tự động nhận dạng kiểu dữ liệu thông qua kiểu dữ liệu của giá trị khởi tạo ra nó. Vì vậy, từ khóa auto cũng có thể nhận dạng ra loại con trỏ hàm.

    Từ khóa auto giúp cú pháp đơn giản hơn. Tuy nhiên, nhược điểm là tất cả các chi tiết về các tham số và kiểu trả về của hàm đều bị ẩn, do đó dễ mắc lỗi hơn khi sử dụng con trỏ hàm.

    Chú ý: Từ khóa auto xác định kiểu dữ liệu tại thời gian biên dịch, nên nó không được sử dụng cho tham số hàm. Vì vậy việc sử dụng nó có phần bị hạn chế.

    Kết luận

    Qua bài học này, bạn đã nắm được những kiến thức về Con trỏ hàm trong C++ (Function pointers).

    Con trỏ hàm (function pointers) thường được sử dụng khi chúng ta có các hàm có cùng kiểu trả về và danh sách tham số, hoặc khi bạn cần truyền một hàm cho hàm khác.

    Con trỏ hàm có cú pháp khai báo khó nhớ và dễ gây ra lỗi nếu chưa nắm rõ, bạn có thể đơn giản hóa bằng cách sử dụng kiểu std::function của C++11.

    Trong bài tiếp theo, chúng ta sẽ cùng tìm hiểu về ĐỆ QUY TRONG C++ (Recursion).

    Nếu bạn có bất kỳ khó khăn hay thắc mắc gì về khóa học, đừng ngần ngại đặt câu hỏi trong phần BÌNH LUẬN bên dưới hoặc trong mục HỎI & ĐÁP trên thư viện chúng tôi để nhận được sự hỗ trợ từ cộng đồng.

    --- Bài cũ hơn ---

  • Giải Thích Về Pointer Trong 5 Phút
  • Con Trỏ Cơ Bản Trong C++
  • Hướng Dẫn Khắc Phục Lỗi Invalid Parameter Khi Chơi Lmht Nhanh Nhất
  • Tham Số Và Đối Số Khác Nhau Như Thế Nào
  • (Ado.net) Sqlcommand Truy Vấn Và Cập Nhật Dữ Liệu C# Sql Server
  • Nạp Chồng Hàm Trong C++ (Function Overloading)

    --- Bài mới hơn ---

  • Danh Sách Liên Kết Đơn Trong C++
  • 60 Hợp Âm Piano Cơ Bản Bạn Có Thể Học Ngay Bây Giờ
  • Con Trỏ (Pointer) Trong C++
  • Khai Báo Và Định Nghĩa Hàm
  • Kiểu Tham Chiếu Trong C++
  • Dẫn nhập

    Ở bài học trước, bạn đã nắm được những kiến thức về HÀM NỘI TUYẾN TRONG C++ (Inline functions).

    Trong bài học này, chúng ta sẽ cùng tìm hiểu về Nạp chồng hàm trong C++ (Function overloading) .

    Nội dung

    Để đọc hiểu bài này tốt nhất các bạn nên có kiến thức cơ bản về:

    Trong bài ta sẽ cùng tìm hiểu các vấn đề:

    • Đặt vấn đề
    • Nạp chồng hàm trong C++ (Function overloading)
    • Một số hàm không thể nạp chồng trong C++

    Đặt vấn đề

    Chúng ta có hàm tính tổng 2 số nguyên:

    int add(int a, int b) { return a + b; }

    Vì hàm trên có tham số kiểu số nguyên ( int), nếu chúng ta có nhu cầu tính tổng 2 số chấm động, hàm này sẽ không thực hiện được.

    Để giải quyết vấn đề này, chúng ta có thể tạo ra 2 hàm có tên khác nhau:

    int addInteger(int a, int b) { return a + b; } double addDouble(double a, double b) { return a + b; }

    Như vậy, ta sẽ có nhiều hàm với các tên gọi khác nhau. Tuy nhiên, việc sử dụng tên như vậy sẽ gây bất lợi cho người lập trình khi gọi hàm. Nạp chồng hàm (function overloading) ra đời để giải quyết vấn đề trên.

    Nạp chồng hàm trong C++ (Function overloading)

    Nạp chồng hàm (function overloading) là tính năng của ngôn ngữ C++ (không có trong C). Kỹ thuật này cho phép sử dụng cùng một tên gọi cho nhiều hàm (có cùng mục đích). Nhưng khác nhau về kiểu dữ liệu tham số hoặc số lượng tham số.

    Giải quyết vấn đề trên:

    int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; }

    Chúng ta cũng có thể định nghĩa các hàm add() với số lượng tham số khác nhau:

    int add(int a, int b, int c) { return a + b + c; }

    Chú ý: Việc quyết định cần gọi đến hàm nào phụ thuộc vào đối số truyền vào khi gọi hàm.

    Một số hàm không thể nạp chồng trong C++

    1. Hàm chỉ khác nhau kiểu trả về

    Chú ý: Không thể nạp chồng hàm nếu chúng chỉ khác nhau kiểu dữ liệu trả về.

    i

    int foo() { return 10; } double foo() { // compiler error return 0.5; }

    Hai hàm trên giống nhau về tham số (không có), vì vậy trình biên dịch sẽ báo lỗi.

    2. Tham số hàm kiểu typedef

    Khai báo typedef chỉ là một bí danh (không phải kiểu dữ liệu mới), vì vậy chương trình bên dưới sẽ gặp lỗi:

    typedef int myint; void print(myint value) { cout << value << 'n'; } void print(int value) // compiler error { cout << value << 'n'; }

    3. Tham số hàm kiểu con trỏ * và mảng là tương đương. Lúc này, khai báo mảng ); // compiler error

    Hai hàm trên giống nhau về tham số (int*), vì vậy trình biên dịch sẽ báo lỗi.

    4. Nạp chồng hàm và từ khóa const

    Trước tiên chúng ta hãy xem hai ví dụ sau. Chương trình 1 biên dịch thất bại, nhưng chương trình 2 biên dịch và chạy tốt.

    C++ cho phép nạp chồng hàm với tham số là const chỉ khi tham số const là tham chiếu hoặc con trỏ. Đó là lý do tại sao chương trình 1 gặp lỗi biên dịch, nhưng chương trình 2 hoạt động.

    Trong chương trình 1, tham số ' i' được truyền theo giá trị, vì vậy ' i' trong fun() là bản sao của 'i' trong main(). Do đó, fun()không thể sửa đổi ' i' của hàm main(). Vì vậy, không quan trọng việc ' i' được nhận dưới dạng tham số const hay tham số bình thường.

    Khi tham số là tham chiếu hoặc con trỏ, chúng ta có thể sửa đổi giá trị được tham chiếu hoặc được trỏ tới, do đó chúng ta có thể có hai phiên bản của hàm, một phiên bản có thể sửa đổi giá trị được tham chiếu hoặc trỏ tới, một phiên bản không thể thay đổi.

    Chú ý: C++ cho phép nạp chồng hàm với tham số là const chỉ khi tham số const là tham chiếu hoặc con trỏ.

    Kết luận

    Qua bài học này, bạn đã nắm được những kiến thức về Nạp chồng hàm trong C++ (Function overloading).

    Trong bài tiếp theo, chúng ta sẽ cùng tìm hiểu về HÀM CÓ ĐỐI SỐ MẶC ĐỊNH TRONG C++ (Default parameters).

    Nếu bạn có bất kỳ khó khăn hay thắc mắc gì về khóa học, đừng ngần ngại đặt câu hỏi trong phần BÌNH LUẬN bên dưới hoặc trong mục HỎI & ĐÁP trên thư viện chúng tôi để nhận được sự hỗ trợ từ cộng đồng.

    --- Bài cũ hơn ---

  • Bitwise Operators Trong C/c++ Là Gì?
  • Nạp Chồng Toán Tử Và Nạp Chồng Hàm Trong C++
  • Bài 7: Overload Toán Tử Trong Lập Trình Hướng Đối Tượng C++
  • Phương Thức Thanh Toán L/c
  • Sử Dụng Vector Trong Lập Trình C++
  • Hàm Toán Học (Math Function) Trong C++

    --- Bài mới hơn ---

  • Lý Thuyết Và Bài Tập Các Tập Hợp Số Lớp 10
  • Lý Thuyết Và Bài Tập Về Các Phép Toán Tập Hợp (Phép Giao, Phép Hợp, Phép Hiệu, Phép Lấy Phần Bù)
  • Yield Trong Toán Học Là Gì?
  • Giá Trị Thực Của Bất Động Sản: Giải Bài Toán Khó Cho Chủ Đầu Tư
  • Bài 5,6,7,8,9 Trang 11,12 Sgk Toán 7 Tập 2: Bảng Tần Số Các Giá Trị Của Dấu Hiệu
  • Trong bài học hôm nay chúng ta sẽ cùng tìm hiểu các hàm toán học (Math Function) trong C++. Vậy C++ hổ trợ cho chúng ta các hàm toán học nào? Để sử dụng các hàm toán học trong C++ chúng ta cần khai báo thư viện nào? Chúng ta sẽ cùng tìm hiểu trong nội dung sau đây.

    1. Hàm toán học (Math Function) trong C++

    Trong C++ có hổ trợ các loại toán học sau:

    • Trignometric functions
    • Hyperbolic functions
    • Exponential functions
    • Floating point manipulation functions
    • Maximum,Minimum and Difference functions
    • Power functions
    • Nearest integer operations
    • Other functions
    • Macro functions
    • Comparison macro functions
    • Error and gamma functions

    Trignometric functions

    Ví dụ

    Chúng ta cùng xem ví dụ đơn giản sau đây có sử dụng các hàm toán học như asin, sin, cos, tan, …

    Và kết quả sau khi thực thi chương trình trên:

    Hyperbolic functions

    Ví dụ

    Chúng ta cùng xem xét một ví dụ đơn giản có sử dụng hyperbolic functions trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    Nearest integer operations

    ceil(x)

    Dùng để làm tròn lên giá trị của x.

    floor(x)

    Dùng để làm tròn xuống giá trị của x.

    round(x)

    Dùng để làm tròn giá trị của x.

    lround(x)

    Dùng để làm tròn giá trị của x và chuyển thành số nguyên long.

    llround(x)

    Dùng để làm tròn giá trị của x và chuyển thành số nguyên long long.

    fmod(n,d)

    Dùng để lấy phần dư của phép chia n cho d

    trunc(x)

    Dùng để làm tròn xuống không chữ số thập phân

    rint(x)

    Dùng để làm tròn giá trị của x bằng rounding mode

    lrint(x)

    Dùng để làm trong giá trị của x bằng roungding mode và chuyển đổi thành số nguyên long.

    llrint(x)

    Dùng để làm tròn giá trị x và chuyển thành số nguyên long long

    nearbyint(x)

    Dùng để làm tròn giá trị x thành giá trị tích phân gần đó

    remainder(n,d)

    Dùng để tính phần còn lại của n/d.

    remquo()

    Dùng để tính toán phần còn lại và thương.

    Ví dụ

    Chúng ta cùng xem xét một ví dụ đơn giản có sử dụng nearest integer operations trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    Other functions

    Ví dụ

    Chúng ta cùng xem xét một ví dụ đơn giản có sử dụng fabs, abs và fma trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    Macro functions

    Ví dụ

    Chúng ta cùng xem xét một ví dụ có sử dụng macro functions trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    Comparison macro functions

    Ví dụ

    Chúng ta cùng xem xét một ví dụ đơn giản có sử dụng comparison macro functions trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    Error and gamma functions

    Ví dụ

    Chúng ta cùng xem một ví dụ đơn giản có sử dụng error and gamma functions trong C++ như sau:

    Và kết quả sau khi thực thi chương trình trên như sau:

    2. Kết luận

    Như vậy chúng ta đã tìm hiểu xong về các hàm tính toán (Math Functions) có sẳn trong C++. Bài này rất là dài và có rất nhiều hàm chúng ta không thể nào nhớ hết nổi.

    --- Bài cũ hơn ---

  • Lịch Sử Của Toán Học Trong Kinh Tế Học
  • Lí Thuyết Kì Vọng (Expectations Theory) Trong Đầu Tư Là Gì? Đặc Điểm, Ví Dụ Và Nhược Điểm
  • “toán Học” Là Gì? Nghĩa Của Từ Toán Học Trong Tiếng Việt. Từ Điển Việt
  • Gemses: Great Expectations Trong Toán Học Và Khoa Học
  • Gems: Great Expectations Trong Toán Học Và Khoa Học
  • Hàm Nội Tuyến Trong C++ (Inline Functions)

    --- Bài mới hơn ---

  • Cơ Bản Về File Pointer Trong C
  • Các Chỉ Số Kinh Tế Vĩ Mô
  • Kinh Tế Học (P5: Chỉ Số Kinh Tế Vĩ Mô Trong Dài Hạn)
  • C++ Linked List, Danh Sách Liên Kết C++, Tìm Hiểu Về Linked List Trong C++
  • Hiện Thực Và Sử Dụng Hàm Memcmp
  • Dẫn nhập

    Trong bài học này, chúng ta sẽ cùng tìm hiểu về Hàm nội tuyến trong C++ (Inline functions).

    Nội dung

    Để đọc hiểu bài này tốt nhất các bạn nên có kiến thức cơ bản về:

    Trong bài ta sẽ cùng tìm hiểu các vấn đề:

    • Đặt vấn đề
    • Hàm nội tuyến trong C++ (Inline functions)
    • Những vấn đề cần lưu ý khi dùng inline functions
    • Tổng kết về hàm nội tuyến

    Đặt vấn đề

    Hàm nội tuyến (inline functions) là một trong những tính năng quan trọng của C++. Vì vậy, trước tiên hãy hiểu tại sao các hàm nội tuyến được sử dụng và mục đích của hàm nội tuyến là gì?

    Khi một hàm được gọi, CPU sẽ lưu địa chỉ bộ nhớ của dòng lệnh hiện tại mà nó đang thực thi (để biết nơi sẽ quay lại sau lời gọi hàm), sao chép các đối số của hàm trên ngăn xếp (stack) và cuối cùng chuyển hướng điều khiển sang hàm đã chỉ định. CPU sau đó thực thi mã bên trong hàm, lưu trữ giá trị trả về của hàm trong một vùng nhớ/thanh ghi và trả lại quyền điều khiển cho vị trí lời gọi hàm. Điều này sẽ tạo ra một lượng chi phí hoạt động nhất định so với việc thực thi mã trực tiếp (không sử dụng hàm).

    Đối với các hàm lớn hoặc các tác vụ phức tạp, tổng chi phí của lệnh gọi hàm thường không đáng kể so với lượng thời gian mà hàm mất để chạy. Tuy nhiên, đối với các hàm nhỏ, thường được sử dụng, thời gian cần thiết để thực hiện lệnh gọi hàm thường nhiều hơn rất nhiều so với thời gian cần thiết để thực thi mã của hàm.

    Trong C, chúng ta thường sử dụng hàm Macro, một kỹ thuật tối ưu hóa được sử dụng bởi trình biên dịch để giảm thời gian thực hiện. C++ cung cấp một khái niệm mới tốt hơn, đó là hàm nội tuyến (inline functions) .

    Hàm nội tuyến trong C++ (Inline functions)

    Inline functions (hàm nội tuyến) là một loại hàm trong ngôn ngữ lập trình C++. Từ khoá inline được sử dụng để đề nghị (không phải là bắt buộc) compiler (trình biên dịch) thực hiện inline expansion (khai triển nội tuyến) với hàm đó hay nói cách khác là chèn code của hàm đó tại địa chỉ mà nó được gọi.

    Khi chương trình trên được biên dịch, mã máy được tạo ra tương tự như hàm main() bên dưới:

    Trong trường hợp này, sử dụng inline functions sẽ thực thi nhanh hơn một chút so với hàm thông thường.

    Những vấn đề cần lưu ý khi dùng inline functions

    Hãy nhớ rằng, từ khóa inline (nội tuyến) chỉ là một yêu cầu cho trình biên dịch, không phải là một lệnh bắt buộc.

    Trình biên dịch có thể không thực hiện nội tuyến trong các trường hợp như:

    • Hàm chứa vòng lặp (for, while, do-while).
    • Hàm chứa các biến tĩnh.
    • Hàm đệ quy.
    • Hàm chứa câu lệnh switch hoặc goto.

    Hầu hết các trình biên dịch hiện đại sẽ tự động đặt các hàm nội tuyến nếu cần thiết. Do đó, trong hầu hết các trường hợp, nếu không có nhu cầu cụ thể để sử dụng từ khóa nội tuyến, hãy để trình biên dịch xử lý các hàm nội tuyến cho bạn.

    Tổng kết về hàm nội tuyến

    Ưu điểm:

    • Tiết kiệm chi phí gọi hàm.
    • Tiết kiệm chi phí của các biến trên ngăn xếp khi hàm được gọi.
    • Tiết kiệm chi phí cuộc gọi trả về từ một hàm.
    • Có thể đặt định nghĩa hàm nội tuyến (inline functions) trong file tiêu đề (*.h) (nghĩa là nó có thể được include trong nhiều đơn vị biên dịch, hàm thông thường sẽ gây ra lỗi).

    Nhược điểm:

    • Tăng kích thước file thực thi do sự trùng lặp của cùng một mã.
    • Khi được sử dụng trong file tiêu đề (*.h), nó làm cho file tiêu đề của bạn lớn hơn.
    • Hàm nội tuyến có thể không hữu ích cho nhiều hệ thống nhúng. Vì trong các hệ thống nhúng, kích thước mã quan trọng hơn tốc độ.

    Kết luận

    Qua bài học này, bạn đã nắm được những kiến thức về Hàm nội tuyến trong C++ (Inline functions). Lưu ý rằng trong hầu hết các trường hợp, nếu không có nhu cầu cụ thể để sử dụng từ khóa nội tuyến, hãy để trình biên dịch xử lý các hàm nội tuyến cho bạn

    Trong bài tiếp theo, chúng ta sẽ cùng tìm hiểu về NẠP CHỒNG HÀM TRONG C++ (Function overloading).

    Nếu bạn có bất kỳ khó khăn hay thắc mắc gì về khóa học, đừng ngần ngại đặt câu hỏi trong phần BÌNH LUẬN bên dưới hoặc trong mục HỎI & ĐÁP trên thư viện chúng tôi để nhận được sự hỗ trợ từ cộng đồng.

    --- Bài cũ hơn ---

  • Sử Dụng Mảng Trong C++
  • Interface Trong Lập Trình Hướng Đối Tượng
  • Sự Khác Biệt Giữa ++ I Và I ++ Là Gì?
  • Phạm Vi Giá Trị Các Kiểu Dữ Liệu Trong C/c++
  • Hướng Dẫn Sử Dụng C/c++ String
  • Hàm Bạn(Friend Functions) Và Lớp Bạn Trong C++

    --- Bài mới hơn ---

  • 10.2 Một Số Thao Tác Đọc Dữ Liệu Từ File Trong C++
  • Cách Đọc Và Ghi File Trong Lập Trình C++ – Đào Tạo Lập Trình C++
  • Tìm Hiểu Về File Header Trong C++
  • C — Tôi Đang Cố Gắng Để Hiểu Getchar ()! = Eof
  • Câu Lệnh Goto Trong C++
  • Trong phần lớn thời lượng của chương này, chúng ta đã được học về những ưu điểm của việc giữ kín dữ liệu khi cài đặt các class và các thành viên (biến thành viên, hàm thành viên) bên trong class. Tuy nhiên, đôi khi bạn sẽ gặp phải những tình huống mà trong đó, bạn sẽ thấy có các class và các hàm nằm ở bên ngoài các class đó, hoạt động chặt chẽ cùng với nhau. Ví dụ, bạn có thể có một class để lưu trữ dữ liệu, và một hàm (hoặc một class khác) hiển thị dữ liệu lên màn hình. Mặc dù class thực hiện việc lưu trữ dữ liệu và phần code hiển thị dữ liệu đã được tách biệt để bảo trì dễ dàng hơn, nhưng phần code hiển thị dữ liệu thực sự gắn bó một cách chặt chẽ và gắn kết với các chi tiết của class lưu trữ dữ liệu. Do đó, trong trường hợp này, việc che giấu các chi tiết của class lưu trữ dữ liệu khỏi phần code hiển thị dữ liệu sẽ không có nhiều ý nghĩa.

    Trong những tình huống như thế này, ta có hai lựa chọn:

    1) Cứ để cho phần code hiển thị dữ liệu sử dụng được những hàm được công khai (public) của class lưu trữ dữ liệu. Tuy nhiên, cách này tiềm tàng một số nhược điểm. Đầu tiên, phải xác định rõ những hàm thành viên nào sẽ được công khai, việc này sẽ mất thời gian, và có thể làm lộn xộn giao diện code của class lưu trữ dữ liệu. Thứ hai, khi hiển thị công khai một số hàm cho phần code hiển thị dữ liệu có thể sử dụng được, thì class lưu trữ dữ liệu cũng đồng thời phơi bày những hàm công khai đó cho những đối tượng mà nó không mong muốn thấy được. Không có cách nào để nói được rằng “hàm này được bố trí công khai chỉ để cho phần code hiển thị dữ liệu sử dụng”.

    2) Sử dụng các class bạn và các hàm bạn, bằng cách này, bạn có thể cấp cho phần code hiển thị dữ liệu của mình quyền truy cập tới các chi tiết private (riêng tư) của class lưu trữ dữ liệu. Điều này cho phép phần code hiển thị dữ liệu có thể truy cập trực tiếp tới tất cả các biến thành viên và hàm thành viên private của class lưu trữ dữ liệu, trong khi vẫn đảm bảo các đối tượng không mong muốn không thể truy cập vào được! Trong bài này, chúng ta sẽ tìm hiểu kỹ hơn về class bạn (friend class) và hàm bạn (friend function).

    1. Hàm bạn(Friend functions)

    Hàm bạn là một hàm có thể truy cập đến các thành viên private (gồm cả các biến thành viên và các hàm thành viên) của một class, như thể nó là một thành viên của class đó. Ngoại trừ đặc điểm ở trên ra thì hàm bạn hoàn toàn giống với hàm bình thường. Một hàm bạn có thể chỉ là một hàm bình thường, hoặc là một hàm thành viên của một class khác. Để khai báo một hàm bạn, chỉ cần sử dụng từ khóa friend ở trước phần nguyên mẫu (prototype) của hàm mà bạn muốn nó trở thành bạn của class. Có thể khai báo hàm bạn bên trong phần public (công khai) hoặc phần private (riêng tư) của class đều được.

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Accumulator { private: int m_value; public: Accumulator() { m_value = 0; } void add(int value) { m_value += value; } // Make the reset() function a friend of this class friend void reset(Accumulator &accumulator); }; // reset() is now a friend of the Accumulator class void reset(Accumulator &accumulator) { // And can access the private data of Accumulator objects accumulator.m_value = 0; } int main() { Accumulator acc; acc.add(5); // add 5 to the accumulator reset(acc); // reset the accumulator to 0 return 0; }

    Trong ví dụ này, chúng ta đã khai báo một hàm có tên là reset() nhận vào một đối tượng của class Accumulator, và gán giá trị 0 cho biến m_value. Bởi vì hàm reset() không phải là một thành viên của class Accumulator, nên theo lẽ thường hàm reset() sẽ không thể truy cập tới các thành viên private của class Accumulator. Tuy nhiên, bởi vì class Accumulator đã khai báo cụ thể hàm reset() này là bạn của nó, nên hàm reset() sẽ có được quyền truy cập tới các thành viên private của class Accumulator.

    Lưu ý rằng, chúng ta phải truyền vào một đối tượng của class Accumulator cho hàm reset(). Điều này là do hàm reset() không phải là một hàm thành viên của class Accumulator. Nó không có con trỏ *this, và cũng không sở hữu đối tượng nào của class Accumulator để có thể làm việc cùng, trừ khi ta cung cấp cho nó.

    class Value { private: int m_value; public: Value(int value) { m_value = value; } friend bool isEqual(const Value &value1, const Value &value2); }; bool isEqual(const Value &value1, const Value &value2) { return (value1.m_value == value2.m_value); }

    Trong ví dụ này, chúng ta khai báo hàm isEqual() là bạn của class Value. Hàm isEqual() nhận vào hai đối tượng Value làm tham số truyền vào. Bởi vì hàm isEqual() là bạn của class Value, nên nó có thẻ truy cập tới các thành viên private của tất cả các đối tượng Value. Trong trường hợp này, nó sử dụng quyền truy cập đó để thực hiện một phép so sánh trên hai đối tượng, và trả về true nếu chúng bằng nhau.

    2. Nhiều bạn(Multiple friends)

    Một hàm có thể là bạn của nhiều class cùng một lúc. Ví dụ, cùng xét đoạn code ví dụ sau:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Humidity; class Temperature { private: int m_temp; public: Temperature(int temp=0) { m_temp = temp; } friend void printWeather(const Temperature &temperature, const Humidity &humidity); }; class Humidity { private: int m_humidity; public: Humidity(int humidity=0) { m_humidity = humidity; } friend void printWeather(const Temperature &temperature, const Humidity &humidity); }; void printWeather(const Temperature &temperature, const Humidity &humidity) { std::cout << "The temperature is " << temperature.m_temp << " and the humidity is " << humidity.m_humidity << 'n'; } int main() { Humidity hum(10); Temperature temp(12); printWeather(temp, hum); return 0; }

    Có hai điều đáng lưu ý về ví dụ này. Thứ nhất, bởi vì PrintWeather là bạn của cả hai class, nên nó có thể truy cập tới dữ liệu private từ các đối tượng thuộc cả hai kiểu class. Thứ hai, hãy chú ý tới dòng code nằm ở đầu ví dụ:

    class Humidity;

    Đây là một khai báo nguyên mẫu lớp (class prototype), cho trình biên dịch biết rằng chúng ta sẽ định nghĩa một class có tên Humidity trong tương lai. Nếu không có dòng code này, trình biên dịch sẽ nói với chúng ta rằng nó không biết Humidity là gì, khi phân tích cú pháp (parsing) cái phần nguyên mẫu (prototype) của hàm PrintWeather() bên trong class Temperature. Nguyên mẫu lớp (class prototype) đóng vai trò giống như các nguyên mẫu hàm (function prototype) – chúng nói cho trình biên dịch biết cái gì đó trông như thế nào, để trình biên dịch có thể biết được rằng, à, cái này có thể sử dụng được ngay bây giờ, rồi định nghĩa sau cũng được. Tuy nhiên, không giống như các hàm, các class không có kiểu trả về hoặc tham số truyền vào, vì vậy các class prototypes đều chỉ có cú pháp đơn giản là từ khóa class theo sau bởi tên của class muốn tạo nguyên mẫu, ví dụ: class ClassName, trong đó ClassName là tên của class cụ thể.

    3. Lớp bạn(Friend classes)

    Việc biến toàn bộ một lớp trở thành bạn của một lớp khác là hoàn toàn có thể. Điều này cho phép tất cả các thành viên của lớp bạn có thể truy cập tới các thành viên private của lớp khác. Đây là một ví dụ:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Storage { private: int m_nValue; double m_dValue; public: Storage(int nValue, double dValue) { m_nValue = nValue; m_dValue = dValue; } // Make the Display class a friend of Storage friend class Display; }; class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(const Storage &storage) { if (m_displayIntFirst) std::cout << storage.m_nValue << " " << storage.m_dValue << 'n'; else // display double first std::cout << storage.m_dValue << " " << storage.m_nValue << 'n'; } }; int main() { Storage storage(5, 6.7); Display display(false); display.displayItem(storage); return 0; }

    Bởi vì class Display là bạn của class Storage, nên bất cứ thành viên nào của class Display mà sử dụng một đối tượng của class Storage thì đều có thể truy cập trực tiếp tới các thành viên private của class Storage. Đoạn chương trình trên sẽ in ra kết quả sau:

    6.7 5

    Có một số lưu ý bổ sung về các lớp bạn. Thứ nhất, mặc dù Display là bạn của Storage, nhưng Display không có quyền truy cập trực tiếp vào con trỏ *this của các đối tượng thuộc lớp Storage. Thứ hai, chỉ vì Display là bạn của Storage, điều đó không có nghĩa là Storage cũng là bạn của Display. Nếu muốn hai lớp là bạn của nhau, cả hai lớp đó đều phải khai báo lớp kia là bạn của mình. Cuối cùng, nếu lớp A là bạn của lớp B, và lớp B là bạn của lớp C, điều đó không có nghĩa là lớp A là bạn của lớp C.

    Hãy cẩn thận khi sử dụng các hàm bạn và lớp bạn, bởi vì việc này có thể cho phép các hàm bạn hoặc lớp bạn vi phạm tính đóng gói (encapsulation) của OOP. Nếu các thông tin chi tiết của một lớp bị thay đổi, thì các chi tiết của lớp bạn cũng sẽ bị buộc phải thay đổi. Do đó, hãy hạn chế sử dụng các hàm bạn và lớp bạn ở mức tối thiểu.

    4. Các hàm thành viên là bạn

    Thay vì làm cho cả một class trở thành bạn, ta có thể chỉ biến một hàm thành viên trở thành bạn cũng được. Điều này được thực hiện tương tự như khi biến một hàm thông thường thành bạn, ngoại trừ việc ta phải thêm tiền tố className:: vào phía trước tên hàm thành viên, ví dụ: Display::displayItem.

    Tuy nhiên, trong thực tế, điều này có thể phức tạp hơn một chút so với dự kiến. Hãy thử biến đổi ví dụ trước một chút bằng cách làm cho hàm Display::displayItem trở thành một hàm (thành viên) bạn:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Display; // forward declaration for class Display class Storage { private: int m_nValue; double m_dValue; public: Storage(int nValue, double dValue) { m_nValue = nValue; m_dValue = dValue; } // Make the Display::displayItem member function a friend of the Storage class friend void Display::displayItem(const Storage& storage); // error: Storage hasn't seen the full definition of class Display }; class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(const Storage &storage) { if (m_displayIntFirst) std::cout << storage.m_nValue << " " << storage.m_dValue << 'n'; else // display double first std::cout << storage.m_dValue << " " << storage.m_nValue << 'n'; } };

    Tuy nhiên, đoạn code trên sẽ không hoạt động. Để làm cho một hàm thành viên trở thành hàm bạn, trình biên dịch phải đã nhìn thấy được toàn bộ phần code định nghĩa của class của hàm thành viên bạn này (chứ không chỉ là một forward declaration – một cái khai báo trước). Bởi vì class Storage đã không nhìn thấy được phần code định nghĩa đầy đủ của class Display, nên trình biên dịch sẽ báo lỗi tại đoạn code mà chúng ta đang cố gắng làm cho hàm thành viên trở thành hàm bạn.

    Thật may là điều này có thể được giải quyết dễ dàng bằng cách di chuyển phần code định nghĩa class Display lên trước phần code định nghĩa class Storage:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(const Storage &storage) // error: compiler doesn't know what a Storage is { if (m_displayIntFirst) std::cout << storage.m_nValue << " " << storage.m_dValue << 'n'; else // display double first std::cout << storage.m_dValue << " " << storage.m_nValue << 'n'; } }; class Storage { private: int m_nValue; double m_dValue; public: Storage(int nValue, double dValue) { m_nValue = nValue; m_dValue = dValue; } // Make the Display::displayItem member function a friend of the Storage class friend void Display::displayItem(const Storage& storage); // okay now };

    Tuy nhiên, bây giờ chúng ta lại gặp phải một vấn đề khác. Bởi vì hàm thành viên Display::displayItem() sử dụng đối tượng của class Storage như là một tham số tham chiếu, và chúng ta vừa mới di chuyển phần code định nghĩa class Storage xuống bên dưới phần code định nghĩa class Display, nên trình biên dịch sẽ phàn nàn rằng nó không biết Storage là cái gì. Chúng ta không thể sửa lỗi này bằng cách sắp xếp lại thứ tự các đoạn code định nghĩa class, bởi nếu làm vậy ta sẽ hoàn tác (undo) giải pháp sửa lỗi trước đó.

    Chúng ta lại tiếp tục gặp may khi lỗi này cũng có thể sửa trong một vài bước đơn giản. Đẩu tiên, chúng ta có thể định một forward declaration – khai báo trước cho class Storage. Tiếp theo, chúng ta có thể di chuyển phần code định nghĩa của hàm Display::displayItem() ra nêm ngoài class Display, đặt ở sau phần code định nghĩa đầy đủ của class Storage. Sau khi sửa như trên, ta sẽ được:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Storage; // forward declaration for class Storage class Display { private: bool m_displayIntFirst; public: Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; } void displayItem(const Storage &storage); // forward declaration above needed for this declaration line }; class Storage // full definition of Storage class { private: int m_nValue; double m_dValue; public: Storage(int nValue, double dValue) { m_nValue = nValue; m_dValue = dValue; } // Make the Display::displayItem member function a friend of the Storage class (requires seeing the full declaration of class Display, as above) friend void Display::displayItem(const Storage& storage); }; // Now we can define Display::displayItem, which needs to have seen the full definition of class Storage void Display::displayItem(const Storage &storage) { if (m_displayIntFirst) std::cout << storage.m_nValue << " " << storage.m_dValue << 'n'; else // display double first std::cout << storage.m_dValue << " " << storage.m_nValue << 'n'; } int main() { Storage storage(5, 6.7); Display display(false); display.displayItem(storage); return 0; }

    Nếu bạn cảm thấy việc khai báo các hàm thành viên trở thành hàm bạn này thật là nhảm nhí, thì thật ra nó đúng là như vậy đấy =)). Và thật may là, việc này chỉ cần thiết khi chúng ta cố gắng làm tất cả mọi thứ bên trong một file duy nhất. Có một giải pháp tốt hơn đó là đặt từng phần code định nghĩa của mỗi class vào bên trong các file header riêng biệt, với các phần code định nghĩa các hàm thành viên ở bên trong các file .cpp tương ứng. Theo cách đó, tất cả các phần code định nghĩa class sẽ ngay lập tức có thể được nhìn thấy (visible) bên trong các file .cpp, và chúng ta cũng sẽ không cần phải sắp xếp lại thứ tự của các class hoặc các hàm!

    5. Tổng kết

    Một hàm bạn hoặc class bạn là một hàm hoặc class mà có thể truy cập tới các thành viên private của một class khác như thế nó là thành viên của class đó. Điều này cho phép hàm bạn hoặc class bạn có thể làm việc một cách chặt chẽ và gắn kết với class khác, mà không khiến cho class khác phải phơi bày ra các thành viên private của mình (ví dụ: Thông qua các hàm truy cập).

    Việc kết bạn được sử dụng không phổ biến khi hai hoặc nhiều class cần làm việc với nhau một cách chặt chẽ và gắn kết, hoặc phổ biến hơn nhiều, khi định nghĩa các toán tử nạp chồng – overloading operators

    --- Bài cũ hơn ---

  • Chi Tiết Bài Học Các Loại Hàm Trong C++
  • Flag Là Gì? Định Nghĩa Và Giải Thích Ý Nghĩa
  • Sự Khác Nhau Giữa Std::endl Và “N” Trong C++
  • Giới Thiệu Về Enum Trong C++
  • Kiểu Dữ Liệu Liệt Kê (Enumeration) Trong C#, Từ Khóa Enum
  • Bài 14: Hàm Nội Tuyến (Inline Function) Trong C++

    --- Bài mới hơn ---

  • Con Trỏ This Trong C++
  • Kinh Tế Học (P30: Các Mô Hình Trong Kinh Tế Vĩ Mô)
  • Danh Sách Liên Kết (Linked List) Trong C
  • Biểu Thức Lambda Trong C++ 11 Là Gì?
  • Biểu Thức Lambda Trong C ++ 11 Là Gì?
  • Hàm nội tuyến (tiếng Anh: inline function) là một cấu trúc trong ngôn ngữ lập trình C++ được sử dụng để đề nghị với chương trình biên dịch rằng một hàm cụ thể nào đó là đối tượng của việc khai triển nội tuyến (inline expansion); có nghĩa là, nó đề nghị rằng chương trình biên dịch nên chèn toàn bộ thân hàm vào trong từng ngữ cảnh, nơi hàm đó được sử dụng.

    Vì sao phải sử dụng hàm nội tuyến?

    Khai triển nội tuyến thường được dùng để loại bỏ thời gian quá dụng ( overhead) xảy ra khi gọi một hàm; kỹ thuật này thường được dùng cho các hàm thực thi thường xuyên, vì khi đó thời gian quá dụng chiếm phần đáng kể. Hàm nội tuyến còn có tác dụng giảm không gian bộ nhớ mà các hàm nhỏ chiếm chỗ, đồng thời cho phép các kỹ thuật tối ưu hoá ( optimization) khác biến đổi mã.

    Nếu không có hàm nội tuyến thì bạn sẽ có rất ít hoặc không có quyền quyết định xem hàm nào được là hàm nội tuyến, hàm nào không, mà việc này sẽ hoàn toàn do trình biên dịch quyết định. Việc bổ sung khả năng điều khiển này cho phép bạn khai thác các kiến thức đặc thù về chương trình ứng dụng, chẳng hạn những hàm thường xuyên được thực thi, để lựa chọn xem hàm nào cần là hàm nội tuyến.

    Tuy nhiên, trình biên dịch cho ngôn ngữ C++ giữ quyền quyết định cuối cùng về việc một hàm đã được lập trình đặt chế độ nội tuyến có thể thực sự được là một hàm nội tuyến hay không, quyết định này thường được dựa vào việc nội dung của hàm này có khả thi cho việc khai triển nội tuyến hay không.

    Trong C++ các hàm nội tuyến tương tác gần gũi với mô hình biên dịch ( compilation model), một hàm nội tuyến phải được định nghĩa trong tất cả các module sử dụng hàm đó, trong khi đó chỉ cần định nghĩa các hàm thông thường trong một module mà thôi. Điều này cho phép biên dịch các module một cách độc lập với tất cả các module khác.

    Hàm nội tuyến trong C++

    Việc gọi một hàm thường gây ra hao tổn tài nguyên nhất định (Overhead – Ví dụ: xếp chồng lên nhau (stacking arguments), nhảy (jumps), vv ..), và do đó đối với các hàm rất ngắn, có thể hiệu quả hơn chỉ đơn giản là chèn mã của hàm nơi nó được gọi, thay vì thực hiện quá trình chính thức gọi một hàm.

    Thêm inline vào trước một khai báo hàm sẽ thông báo cho trình biên dịch rằng hàm nội tuyến được ưa thích hơn cơ chế gọi hàm thông thường cho một hàm cụ thể. Điều này không thay đổi ở tất cả các chức năng và cách vận hành của một hàm, nhưng chỉ đơn giản là được sử dụng để đề nghị trình biên dịch rằng mã được tạo ra bởi thân hàm sẽ được chèn vào tại mỗi điểm mà hàm được gọi, thay vì hàm được gọi một cách thông thường.

    Ví dụ, hàm có chức năng nối hai chuỗi sẽ được khai báo là hàm nội tuyến như sau:

    inline string concatenate (const string& a, const string& b) { return a+b; }

    Điều này thông báo trình biên dịch rằng khi hàm concatenate được gọi, chương trình thích hàm được mở rộng nội tuyến, thay vì thực hiện việc gọi hàm một cách thông thường. inline chỉ được xác định trong khai báo hàm, không phải khi nó được gọi.

    Lưu ý rằng hầu hết các trình biên dịch đã tối ưu hóa mã để tạo các chức năng nội tuyến khi nó nhìn thấy một cơ hội để nâng cao hiệu quả, thậm chí nếu không được đánh dấu rõ ràng với từ khoá inline. Vì vậy, từ khoá này chỉ đơn thuần chỉ ra trình biên dịch rằng nội tuyến được ưa thích cho chức năng này, mặc dù trình biên dịch được tự do không inline nó, và tối ưu hóa khác. Trong C ++, tối ưu hóa là một nhiệm vụ được ủy thác cho trình biên dịch, tự do tạo ra bất kỳ mã nào miễn là hành vi kết quả được xác định bởi mã.

    --- Bài cũ hơn ---

  • Khái Niệm Hàm Inline Trong C/c++ Là Gì?
  • Std::string Và Xử Lý Chuỗi Trong C++
  • So Sánh Giữa Abstract Class Và Interface Trong C#.net
  • Interface Trong C++, (Lớp Trừu Tượng)
  • Sự Khác Nhau Giữa ++ I Và I ++ Là Gì?
  • Lập Trình C: Hàm (Function)

    --- Bài mới hơn ---

  • Cơ Bản Về Hàm Và Giá Trị Trả Về (Basic Of Functions And Return Values)
  • Câu Lệnh While Trong C++
  • Chi Tiết Bài Học Vòng Lăp While Và Do…while
  • Phần 22: Class Và Object
  • Tìm Hiểu Về Con Trỏ Null(Null Pointers) Trong C++
  • Đăng ký nhận thông báo về những video mới nhất

    Giới thiệu

    Hầu hết các chương trình viết bằng ngôn ngữ C (cũng như viết bằng những ngôn ngữ khác như C++, Java, C#, Python) thường được phân nhỏ thành các hàm (function) trong đó có một hàm chính là main() và chương trình luôn luôn bắt đầu từ hàm main(). Hàm (Function) là phần kiến thức rất quan trọng trong C, nắm tốt kiến thức về hàm bạn sẽ có được thuận lợi khi tìm hiểu các ngôn ngữ lập trình khác.

    Khái niệm

    Hàm là đoạn chương trình thực hiện trọn vẹn một công việc nhất định (cụ thể). Hàm giúp chia cắt việc lớn thành nhiều việc nhỏ hơn, điều này tương đương với việc chia bài toán lớn thành các bài toán nhỏ hơn để giải, như vậy thì việc giải bài toán sẽ trở nên dễ dàng hơn. Ngoài ra, hàm còn giúp cho chương trình trở nên sáng sủa, dễ sửa, nhất là đối với các chương trình lớn.

    Định nghĩa một hàm

    Để định nghĩa hay tạo một hàm, ta có thể sử dụng cú pháp như sau:

    Kiểu_dữ_liệu Tên_hàm(Khai_báo Các_đối_số) {

      Khai_báo_các_biến_của_hàm;  //Nếu cần

      Khối_lệnh;

      return Giá_trị; //Nếu Kiểu_dữ_liệu là void thì không cần câu lệnh này

    }

    , trong đó:

    + Kiểu_dữ_liệu: Là một trong 5 kiểu dữ liệu cơ bản hoặc một trong các kiểu dữ liệu dẫn xuất hay nâng cao. Nếu Kiểu_dữ_liệu là kiểu int thì không cần khai báo Kiểu_dữ_liệu vì int là kiểu dữ liệu mặc định của ngôn ngữ C.

    + Tên_hàm buộc phải có và việc đặt tên phải tuân theo quy tắc đặt tên. Ví dụ muốn sử dụng tên hàm là “Tinh binh phuong” thì không được vì có dấu cách trong đó, ta có thể thay bằng “Tinh_binh_phuong” hoặc “tinhBinhPhuong”.

    + Các_đối_số: Không bắt buộc, nghĩa là Các_đối_số có thể có hoặc không có tuỳ thuộc vào mục đích dùng hàm đó để làm gì.

    + Cặp () bao ngoài Các_đối_số và sau Tên_hàm là bắt buộc phải có, ngay cả không có Các_đối_số.

    + Cặp {} là bắt buộc phải có đối với mọi định nghĩa hàm.

    + return Giá_trị: Lệnh này dùng để trả về giá trị cho hàm, nếu Kiểu_dữ_liệu của hàm không phải là void thì lệnh này buộc phải có. Giá_trị có thể là một hằng, giá trị của biến, giá trị của biểu thức hoặc giá trị trả về từ một lời gọi hàm khác.

    Một số chú ý đối với hàm:

    – Có thể thể gọi một hàm từ hàm khác nhưng bạn không được định nghĩa hàm bên trong hàm (kể cả trong hàm main()).

    – Kiểu dữ liệu của Giá_trị nên cùng kiểu (nhưng không được lớn hơn) với kiểu dữ liệu của hàm.

    – Bạn có thể định nghĩa hàm nằm trên hoặc nằm dưới hàm main(). Trong trường hợp bạn muốn định nghĩa hàm nằm dưới hàm main() thì bạn cần phải khai báo hàm.

    – Mỗi hàm chỉ có thể trả về được duy nhất một giá trị. Trong trường hợp bạn muốn trả về nhiều hơn một giá trị thì bạn có thể sử dụng phương pháp tham chiếu.

    – Nơi mà hàm trả về giá trị chính là nơi mà nó được gọi.

    float

    Giaithua(

    int

    n) { //định nghĩa hàm Giaithua() có kiểu trả về là float có một đối số kiểu int

    int

    i; //biến cục bộ của hàm

    float

    KQ=

    1.0

    ; //đây cũng là biến cục bộ

    for

    (i=

    1

    ; i<=n; i++) //n ở đây là đối số của hàm Giaithua()

    KQ = KQ*i;

    return

    KQ ; //hàm trả về giá trị lưu trong biến KQ cho nơi gọi

    }

    int

    main() { //mọi chương trình C luôn bắt đầu từ hàm main()

    int

    n; //khai báo biến nguyên n

    printf(

    “Nhap n = “

    ); //in ra màn hình chuỗi

    scanf(

    “%d”

    ,&n); //nhập giá trị cho n

    printf(

    n

    Giai thua cua %d la %f”

    , n, Giaithua(n)); //gọi đến hàm Giaithua() trong đó truyền giá trị của n (thuộc hàm main()) cho đối số n của hàm Giaithua()

    return

    0

    ;

    }

     

    Khai báo hàm

    Việc khai báo một hàm đơn giản hơn so với định nghĩa một hàm. Mục đích của việc khai báo hàm là để cho việc kiểm soát code chương trình của bạn được dễ dàng hơn. Mặt khác, nếu bạn muốn định nghĩa một hàm nằm dưới hàm main() thì bạn bắt buộc phải khai báo hàm. Cú pháp của việc khai báo hàm như sau:

    Kiểu_dữ_liệu Tên_hàm(Khai_báo Các_đối_số)

    ;

    Lưu ý là bạn có thể khai báo hàm nằm bên trong hàm khác, nhưng bạn cần phải khai báo trước khi có lời gọi hàm đến hàm đó. Trong trường hợp khai báo hàm trong hàm main() thì lệnh khai báo phải nằm trên lời gọi hàm clrscr().

    float

    Giai_thua(

    int

    n); //Khai báo nhằm mục đích đặt hàm Giai_thua() ở sau hàm main()

    int

    main(){

    int

    n;

    printf(

    “Nhap n = “

    );

    scanf(

    “%d”

    ,&n);

    printf(

    n

    Giai thua cua %d la %g”

    , n, Giai_thua(n));

    return

    0

    ;

    }

    float

    Giai_thua(

    int

    n) {

    int

    i;

    float

    GT=

    1.0

    ;

    for

    (i=

    1

    ; i<=n ; i++)

    GT = GT * i;

    return

    GT;

    }

    Lời gọi hàm

    Các hàm thường giao tiếp hay gọi đến nhau bằng lời gọi hàm (call function). Việc giao tiếp hay gọi đến nhau của các hàm thông qua cách truyền tham số.

    Các tham số được truyền theo một trong hai cách sau đây:

    Truyền đối số bằng tham trị (hay giá trị)

    Ý nghĩa: Đây là lời gọi hàm mà trong đó đối số thực sự không thay đổi giá trị sau khi hàm được gọi thực hiện xong công việc. Truyền bằng tham trị là lời gọi mặc định của các chương trình C.

    Truyền đối số bằng tham chiếu (hay tham biến)

    * Bản chất của truyền bằng tham chiếu là truyền địa chỉ của biến.

    * Ý nghĩa: cách gọi hàm bằng truyền tham chiếu có thể làm thay đổi giá trị của đối số thực sự. Việc truyền tham chiếu được thực hiện thông qua biện pháp con trỏ. Cú pháp như sau:

    Kiểu_dữ_liệu  Tên_hàm(Kiểu _dữ_liệu *Tên_con_trỏ_1, Kiểu_dữ_liệu *Tên_con_trỏ_2,…) {

        Khối_lệnh;

    }

    void main() {

        Kiểu_dữ_liệu Biến_1;

        Kiểu_dữ_liệu Biến_2;

        …

        Tên_hàm(&Biến_1,&Biến_2,…);

        …

    }

    Nếu không dùng phương pháp tham chiếu thì chương trình sẽ không thực hiện được mục đích, tức là hai số x và y vẫn giữ nguyên giá trị sau lời gọi hàm. Chương trình được viết như sau:

    void

    hoanVi(

    float

    x,

    float

    y) {

    int

    z;

    z=x; //cách khác: x=x+y;

    x=y; //y=x-y; //y= (x+y)-y = x

    y=z; //x=x-y; //x= (x+y)-x = y

    }

    int

    main() {

    float

    x, y;

    printf(

    “Nhap 2 so x, y: “

    );

    scanf(

    “%f%f”

    ,&x,&y);

    hoanVi(x, y);

    printf(

    n

    Sau khi tien hanh hoan vi, ta duoc: x=%g va y=%g”

    , x, y);

    return

    0

    ;

    }

    Nếu dùng phương pháp tham chiếu thì chương trình sẽ thực hiện được mục đích, x và y sẽ hoán đổi giá trị cho nhau. Chương trình được viết như sau:

    void

    DoiGT(

    float

    *x,

    float

    *y) {

    int

    z;

    z=*x; //cách khác: *x=*x+*y;

    *x=*y; //*y=*x-*y; //*y= (*x+*y)-*y = *x

    *y=z; //*x=*x-*y; //*x= (*x+*y)-*x = *y

    }

    int

    main() {

    float

    x, y;

    printf(

    n

    Nhap 2 so x, y: “

    );

    scanf(

    “%f%f”

    , &x, &y);

    DoiGT(&x,&y);

    printf(

    n

    Sau khi tien hanh hoan vi, ta duoc: x=%g va y=%g”

    , x, y);

    return

    0

    ;

    }

    Một kết quả demo thể hiện như hình sau:

    Còn đây là ví dụ viết chương trình nhập vào 3 giá trị kiểu thực (float hoặc double), sau đó tìm giá trị lớn nhất trong 3 giá trị đó. Yêu cầu:

    · Phải viết chương trình dưới dạng các hàm.

    · Không được sử dụng các biến tổng thể (biến toàn cục).

    void

    nhap(

    float

    *a,

    float

    *b,

    float

    *c) { //hàm nhập các số a, b và c

    printf(

    n

    Nhap a: “

    );

    scanf(

    “%f”

    , a);

    printf(

    n

    Nhap b: “

    );

    scanf(

    “%f”

    , b);

    printf(

    n

    Nhap c: “

    );

    scanf(

    “%f”

    , c);

    }

    float

    sosanh(

    float

    a,

    float

    b,

    float

    c) { //hàm tìm giá trị lớn nhất

    float

    max; //biến lưu lại giá trị lớn nhất

    max = a; //giả sử a là số lớn nhất

    if

    (max <

    b

    ) //nếu b lớn hơn max

    max = b; //thì gán b cho max

    if

    (max <

    c

    ) //nếu c lớn hơn max

    max = c; //thì gán c cho max

    return

    max; //trả lại giá trị lớn nhất lưu trong biến max

    }

    int

    main() {

    float

    a, b, c; //khai báo 3 biến a, b, c

    float

    max; //khai báo biến lưu trữ giá trị lớn nhất

    nhap(&a,&b,&c); //Gọi hàm nhap() truyền đối số ở dạng tham chiếu

    max=sosanh(a,b,c); //Gọi và gán giá trị trả về của hàm sosanh() cho biến max

    printf(

    n

    So lon nhat trong 3 so nhap vao la: %g”

    , max);

    return

    0

    ;

    }

    Chú ý: Mọi hàm nhập giá trị cho các biến địa phương (cục bộ) đều được truyền theo phương pháp tham chiếu theo mẫu chương trình như trên.

    --- Bài cũ hơn ---

  • Giới Thiệu Về Iostream: Cout, Cin Và Endl
  • Chi Tiết Bài Học C++ Cout, Cin, Endl
  • Số Dấu Phẩy Động(Float, Double,…) Trong C++
  • Prototype Là Khỉ Gì ?
  • Hàm Templates Trong C++
  • Hàm Ảo(Virtual Functions) Và Đa Hình(Polymorphism) Trong C++

    --- Bài mới hơn ---

  • Cơ Bản Về Lớp Vector
  • Tổng Quan Về Xml
  • Cơ Bản Về Chuỗi Ký Tự Trong C++ (An Introduction To Std::string)
  • Mảng Ký Tự Trong C++ (C
  • Tính Từ Là Gì? Tính Từ Và Cụm Tính Từ
  • Trong bài học trước về con trỏ và tham chiếu đến lớp cơ sở của các đối tượng dẫn xuất, chúng ta đã xem xét một số ví dụ trong đó sử dụng con trỏ hoặc tham chiếu đến lớp cơ sở có hàm đơn giản. Tuy nhiên, trong mọi trường hợp, chúng ta gặp phải vấn đề là con trỏ cơ sở hoặc tham chiếu chỉ có thể gọi lớp cơ sở của hàm, không phải là lớp dẫn xuất.

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Base { public: std::string_view getName() const { return "Base"; } }; class Derived: public Base { public: std::string_view getName() const { return "Derived"; } }; int main() { Derived derived; Base &rBase{ derived }; std::cout << "rBase is a " << rBase.getName() << 'n'; return 0; }

    Ví dụ này in ra kết quả:

    rBase is a Base

    Vì rBase là một tham chiếu của Base, nên nó gọi Base :: getName(), mặc dù nó thực sự tham chiếu tới Base của một đối tượng đã tạo.

    Trong bài học này, chúng ta sẽ chỉ cho bạn cách giải quyết vấn đề này bằng các hàm ảo.

    1. Hàm ảo và đa hình

    Hàm ảo là một loại hàm đặc biệt, khi được gọi, sẽ tự động hiểu và chọn đúng đối tượng gốc để gọi đúng hàm của đối tượng đó giữa lớp cơ sở và lớp dẫn xuất. Khả năng này được gọi là đa hình. Hàm dẫn xuất được coi là khớp với lớp cơ sở nếu nó có cùng tên, loại tham số (cho dù đó là const) và kiểu trả về của hàm trong lớp cơ sở. Các hàm như vậy được gọi là ghi đè(overriding).

    Để tạo một hàm ảo, chỉ cần đặt từ khóa virtual trước khi khai báo hàm.

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Base { public: virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword }; class Derived: public Base { public: virtual std::string_view getName() const { return "Derived"; } }; int main() { Derived derived; Base &rBase{ derived }; std::cout << "rBase is a " << rBase.getName() << 'n'; return 0; }

    Ví dụ này in kết quả:

    rBase is a Derived

    Vì rBase là một tham chiếu đến lớp Base của một đối tượng đã tạo, khi rBase.getName() được thực thi, thông thường nó sẽ gọi Base :: getName(). Tuy nhiên, Base :: getName() là ảo, nên chương trình sẽ xem xét liệu và tìm đúng đối tượng dẫn xuất đã kế thừa hàm ảo đó không. Trong trường hợp này, nó sẽ tìm thấy đối tượng Derived và sẽ gọi Derived :: getName =()!

    class A

    {

    public:

    virtual std::string_view getName() const { return “A”; }

    };

    class B: public A

    {

    public:

    virtual std::string_view getName() const { return “B”; }

    };

    class C: public B

    {

    public:

    virtual std::string_view getName() const { return “C”; }

    };

    class D: public C

    {

    public:

    virtual std::string_view getName() const { return “D”; }

    };

    int main()

    {

    C c;

    A &rBase{ c };

    std::cout << “rBase is a ” << rBase.getName() << ‘n’;

    return 0;

    }

    Bạn nghĩ chương trình này sẽ ra sao?

    Hãy nhìn vào cách thức hoạt động của nó. Đầu tiên, chúng ta khởi tạo một đối tượng lớp C. rBase là một tham chiếu A, mà chúng ta đặt để tham chiếu phần A của đối tượng C. Cuối cùng, chúng ta gọi rBase.getName (). rBase.getName() ước tính thành A :: getName(). Tuy nhiên, A :: getName() là ảo, vì vậy trình biên dịch sẽ tìm và gọi đối tượng gốc nhất giữa A và C. Trong trường hợp này, đó là C :: getName(). Lưu ý rằng nó sẽ không gọi D :: getName(), vì đối tượng ban đầu của chúng ta là C, không phải D, do đó chỉ xem xét các hàm giữa A và C.

    Kết quả là, chương trình của chúng ta sẽ in ra:

    rBase is a C

    2. Một ví dụ phức tạp hơn

    Hãy để một cái nhìn khác về ví dụ Động vật mà chúng ta đã làm việc trong bài học trước. Ở đây, lớp học ban đầu, cùng với một số mã kiểm tra:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Animal { protected: std::string m_name; // We're making this constructor protected because // we don't want people creating Animal objects directly, // but we still want derived classes to be able to use it. Animal(const std::string &name) : m_name{ name } { } public: const std::string &getName() const { return m_name; } std::string_view speak() const { return "???"; } }; class Cat: public Animal { public: Cat(const std::string& name) : Animal{ name } { } std::string_view speak() const { return "Meow"; } }; class Dog: public Animal { public: Dog(const std::string &name) : Animal{ name } { } std::string_view speak() const { return "Woof"; } }; void report(const Animal &animal) { std::cout << animal.getName() << " says " << animal.speak() << 'n'; } int main() { Cat cat{ "Fred" }; Dog dog{ "Garbo" }; report(cat); report(dog); return 0; }

    Bản in này:

    Fred says ??? Garbo says ???

    Ở đây, lớp tương đương với hàm speak() được tạo ảo:

    /** * chúng tôi - Kênh thông tin IT hàng đầu Việt Nam * * @author cafedevn * Contact: [email protected] * Fanpage: https://www.facebook.com/cafedevn * Instagram: https://instagram.com/cafedevn * Twitter: https://twitter.com/CafedeVn * Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/ */ class Animal { protected: std::string m_name; // We're making this constructor protected because // we don't want people creating Animal objects directly, // but we still want derived classes to be able to use it. Animal(const std::string &name) : m_name{ name } { } public: const std::string &getName() const { return m_name; } virtual std::string_view speak() const { return "???"; } }; class Cat: public Animal { public: Cat(const std::string &name) : Animal{ name } { } virtual std::string_view speak() const { return "Meow"; } }; class Dog: public Animal { public: Dog(const std::string& name) : Animal{ name } { } virtual std::string_view speak() const { return "Woof"; } }; void report(const Animal &animal) { std::cout << animal.getName() << " says " << animal.speak() << 'n'; } int main() { Cat cat{ "Fred" }; Dog dog{ "Garbo" }; report(cat); report(dog); return 0; }

    Chương trình này tạo ra kết quả:

    Fred says Meow Garbo says Woof

    Nó hoạt động!

    Khi đánh giá Animal.speak(), chương trình lưu ý rằng Animal :: speak() là một hàm ảo. Trong trường hợp động vật tham chiếu lớp Animal của vật thể Cat, chương trình sẽ xem xét tất cả các lớp giữa Animal và Cat để xem liệu nó có thể tìm thấy một hàm từ đối tượng gốc không. Trong trường hợp đó, nó tìm thấy Cat :: speak(). Trong trường hợp động vật tham chiếu lớp Animal của đối tượng Dog, chương trình sẽ gọi hàm Dog :: speak().

    Lưu ý rằng chúng ta đã làm cho Animal :: getName() ảo. Điều này làm cho getName() không bao giờ bị ghi đè trong bất kỳ lớp dẫn xuất nào, do đó không cần.

    Tương tự, ví dụ mảng sau đây hoạt động như mong đợi:

    Cat fred{ "Fred" }; Cat misty{ "Misty" }; Cat zeke{ "Zeke" }; Dog garbo{ "Garbo" }; Dog pooky{ "Pooky" }; Dog truffle{ "Truffle" }; // Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects Animal *animals[]{ &fred, &garbo, &misty, &pooky, &truffle, &zeke }; for (const auto *animal : animals)

    Tạo ra kết quả:

    Fred says Meow Garbo says Woof Misty says Meow Pooky says Woof Truffle says Woof Zeke says Meow

    Mặc dù hai ví dụ này chỉ sử dụng Cat và Dog, bất kỳ lớp nào khác mà chúng ta lấy từ Animal cũng sẽ hoạt động với hàm report() và mảng animal mà không cần sửa đổi thêm! Đây có lẽ là lợi ích lớn nhất của các hàm ảo – khả năng cấu trúc code của bạn theo cách mà các lớp mới xuất phát sẽ tự động làm việc với code cũ mà không cần sửa đổi!

    Một lời cảnh báo: các từ của hàm lớp dẫn xuất phải khớp chính xác với từ của hàm ảo lớp trong lớp cơ sở để sử dụng hàm trong lớp dẫn xuất. Nếu hàm lớp dẫn xuất có các loại tham số khác nhau, chương trình có thể vẫn sẽ biên dịch tốt, nhưng hàm ảo sẽ không gọi như mong đợi của chúng ta.

    3. Sử dụng từ khóa virtual

    Nếu một hàm được đánh dấu là virtual, tất cả các phần ghi đè phù hợp cũng được coi là virtual, ngay cả khi chúng không được đánh dấu rõ ràng như vậy. Tuy nhiên, việc có từ khóa virtual trên các hàm dẫn xuất không gây hại và nó đóng vai trò như một lời nhắc hữu ích rằng hàm này là một hàm ảo chứ không phải là một hàm bình thường. Do đó, nói chung là một ý tưởng tốt để sử dụng từ khóa virtual cho các hàm ảo trong các lớp dẫn xuất mặc dù nó không thực sự cần thiết.

    4. Trả về các loại hàm ảo

    Trong trường hợp bình thường, kiểu trả về của hàm ảo và hàm ghi đè của nó phải khớp. Hãy xem xét ví dụ sau:

    class Base { public: virtual int getValue() const { return 5; } }; class Derived: public Base { public: virtual double getValue() const { return 6.78; } };

    Trong trường hợp này, Derived :: getValue() không được coi là ghi đè phù hợp cho Base :: getValue() (nó được coi là một hàm hoàn toàn riêng biệt).

    5. Không gọi các hàm ảo từ các hàm tạo hoặc hàm hủy

    Ở đây, Bạn không nên gọi các hàm ảo từ các hàm tạo hoặc hàm hủy. Tại sao?

    Hãy nhớ rằng khi một lớp Derived được tạo, phần Base được xây dựng trước. Nếu bạn đã gọi một hàm ảo từ hàm tạo cơ sở và phần lớp Derived thậm chí chưa được tạo, thì nó không thể gọi hàm của Derived vì không có đối tượng Derived được khởi tạo để gọi hàm. Trong C ++, nó sẽ gọi hàm trong class Base thay thế.

    Một vấn đề tương tự tồn tại cho hàm huỷ. Nếu bạn gọi một hàm ảo trong hàm hủy của lớp Cơ sở, nó sẽ luôn gọi hàm của lớp Cơ sở, bởi vì phần Derived của lớp đã bị hủy.

    Quy tắc: Không bao giờ gọi các hàm ảo từ các hàm tạo hoặc hàm hủy.

    Bài tập thực hành về tính đa hình trong C++

    6. Nhược điểm của các hàm ảo

    Vì hầu hết thời gian bạn sẽ muốn các hàm của mình là ảo, tại sao không làm cho tất cả các hàm trở nên ảo? Câu trả lời là bởi vì nó không hiệu quả – việc giải quyết một cuộc gọi hàm ảo mất nhiều thời gian hơn là giải quyết một cuộc gọi thông thường. Hơn nữa, trình biên dịch cũng phải cấp phát một con trỏ phụ cho mỗi đối tượng lớp có một hoặc nhiều hàm ảo. Chúng tôi sẽ nói về điều này nhiều hơn trong các bài học trong tương lai trong của phần này.

    --- Bài cũ hơn ---

  • Vật Lý – Ngành Học Nhiều Thú Vị
  • Trọn Bộ Công Thức Vật Lý 12 Ôn Thi Thpt Quốc Gia Chọn Lọc
  • Kiểu Void Trong C++
  • Unsigned Integers Là Gì? Và Tại Sao Phải Tránh Dùng Nó
  • Sử Dụng Typedef Và Using Trong C++
  • Functional Interface Là Gì? Functional Interface Api Trong Java 8

    --- Bài mới hơn ---

  • Java Bài 32: Tính Trừu Tượng (Abstraction)
  • 600 Câu Thành Ngữ Việt Trung
  • Công Cụ Tính Lương Gross Sang Net / Net Sang Gross Chuẩn 2022
  • Lương Net, Lương Gross Là Gì? Chuyển Lương Net Sang Gross
  • Lương Gross Là Gì? Người Lao Động Nên Nhận Lương Gross Hay Lương Net
  • Functional Interface là gì? Functional Interface API trong Java 8

    Functional Interface là interface có duy nhất 1 method trừu tượng (có thể có thêm các method không trừu tượng bằng từ khóa default trong Java 8)

    Ví dụ: Comparable là 1 Functional Interface với method trừu tượng duy nhất compareTo; Runnable là 1 Functional Interface với method trừu tượng duy nhất run

    Về annotation @FunctionalInterface: nó được dùng ở trước mỗi interface để khai báo đây là 1 functional interface.

    @FunctionalInterface public interface Runnable { public abstract void run(); }

    Việc dùng annotation @FunctionalInterface là không bắt buộc nhưng nó giúp đảm bảo cho quá trình compile. Ví dụ bạn khai báo @FunctionalInterface nhưng trong interface lại có nhiều hơn 2 method trừu tượng thì nó sẽ báo lỗi.

    Java 8 xây dựng sẵn một số functional interface và nó được dùng nhiều trong các biểu thức lambda:

    2.1. java.util.function.Consumer

    package java.util.function; import java.util.Objects; @FunctionalInterface // Phương thức chấp nhận một tham số đầu vào // và không trả về gì cả. void accept(T t); }

    consumer thường được dùng với list, stream để xử lý với các phần tử bên trong.

    // Sử dụng List.forEach(Consumer) để in ra giá trị của các phần tử trong list

    @Override

    public void accept(String t) {

    System.out.println(t);

    }

    });

    // Sử dụng List.forEach(Consumer) với cú pháp lambda expssion:

    Kết quả:

    stack java stackjava.com ---------------- stack java stackjava.com

    2.2. java.util.function.Predicate

    package java.util.function; import java.util.Objects; @FunctionalInterface // Kiểm tra một tham số đầu vào và trả về true hoặc false. boolean test(T t); }

    pdicate thường được dùng với list, stream để kiểm tra từng phần tử lúc xóa, lọc…

    list.add(-1);

    list.add(1);

    list.add(0);

    list.add(-2);

    list.add(3);

    // lệnh removeIf sẽ thực hiện duyệt từng phần tử,

    // nếu method test của Predicate trả về true thì sẽ remove phần tử đó khỏi list

    @Override

    public boolean test(Integer t) {

    return t < 0;

    }

    });

    // Sử dụng Predicate với cú pháp Lambda Expssion

    // loại bỏ các phần tử lớn hơn 1

    Kết quả:

    1 0 3 ----------------------------- 1 0

    2.3. java.util.function.Function

    package java.util.function; import java.util.Objects; @FunctionalInterface // Method này nhận đầu vào là 1 tham số và trả về một giá trị. R apply(T t); }

    Function thường dùng với Stream khi muốn thay đổi giá trị cho từng phần tử trong stream.

    // chuyển tất cả các phần tử của stream thành chữ in hoa

    @Override

    public String apply(String t) {

    return t.toUpperCase();

    }

    // Function với cú pháp Lambda Expssion

    // chuyển tất cả các phần tử của stream thành chữ thường

    stream = list.stream();// lưu ý là stream ko thể dùng lại nên phải khởi tạo lại

    Kết quả:

    STACK JAVA DEMO FUNCTION --------------- stack java demo function

    Một số Function interface tương tự:

    2.4. java.util.function.Supplier

    package java.util.function; @FunctionalInterface // method này không có tham số nhưng trả về một kết quả. T get(); }

    Random random = new Random(); @Override public Integer get() { return random.nextInt(10); } }).limit(5); // Sử dụng Supplier với cú pháp Lambda Expssion:

    Kết quả:

    4 9 8 5 8 -------------------- 2 2 9 9 6

    References:

    https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

    --- Bài cũ hơn ---

  • Abstract Class Và Interface Trong Java
  • Hướng Dẫn Lập Trình Spring Cho Người Mới Bắt Đầu
  • Từ Khóa Static Và Final Trong Java
  • Sử Dụng Static Và Final Trong Java
  • Java Bài 16: Đối Tượng (Object) & Lớp (Class)
  • Hàm Bạn (Friend Function) Và Lớp Bạn (Friend Class) Trong C++

    --- Bài mới hơn ---

  • Vòng Lặp While Trong C++ (While Statements)
  • Vòng Lặp While Trong C++
  • Lớp(Class) Và Đối Tượng Trong C++
  • Cơ Bản Về Class Trong C++
  • Con Trỏ Null Trong C++ (Null Pointers)
  • Trong bài học hôm nay chúng ta sẽ tìm hiều về hàm bạn ( Friend function) và lớp bạn ( Friend Class) trong C++. Vậy hàm bạn có gì khác so với hàm thông thường khác trong C++. Lớp bạn có khác gì so với lớp thông thường khác trong C++. Chúng ta sẽ cùng tìm hiểu trong nội dung sau đây.

    1. Hàm bạn trong C++

    Nếu một hàm được định nghĩa là một hàm bạn ( Friend function) trong C++, thì dữ liệu được bảo vệ ( protected) và riêng tư ( private) của một lớp có thể được truy cập bằng cách sử dụng hàm.

    Cú pháp để khai báo hàm bạn ( Friend function) trong C++ như sau:

    class TenLop { friend KieuDuLieu TenHam([Tham so]); };

    Ví dụ

    Chúng ta cùng xem xét một ví dụ đơn giản về hàm bạn ( Friend function) trong C++ như sau:

    using namespace std; class HinhChuNhat { private: int chieuDai; int chieuRong; public: HinhChuNhat(int chieuDai, int chieuRong) { } HinhChuNhat(): chieuDai(0) { } friend int HienThiChieuDai(HinhChuNhat); //friend function }; int HienThiChieuDai(HinhChuNhat hcn) { hcn.chieuDai += 10; return hcn.chieuDai; } int main() { HinhChuNhat hcn = HinhChuNhat(10, 20); cout << "Chieu dai cua hinh chu nhat la: " << HienThiChieuDai(hcn)<<endl; return 0; }

    Và kết quả sau khi thực thi chương trình trên như sau:

    Chúng ta cùng xem tiếp một ví dụ nữa về hàm bạn giữa 2 lớp trong C++ như sau:

    using namespace std; class Lop2; class Lop1 { int a; public: void ThietLapGiaTriA(int a) { } void HienThi() { cout << "Lop 1, a = " << a << endl; } friend void TimGiaTriNhoNhat(Lop1, Lop2); }; class Lop2 { int b; public: void ThietLapGiaTriB(int b) { } void HienThi() { cout << "Lop 2, b = " << b << endl; } friend void TimGiaTriNhoNhat(Lop1, Lop2); }; void TimGiaTriNhoNhat(Lop1 l1, Lop2 l2) { cout << "Gia tri nho nhat trong 2 lop la: "; if(l1.a <= l2.b) cout << l1.a << endl; else cout << l2.b << endl; } int main() { Lop1 l1; Lop2 l2; l1.ThietLapGiaTriA(7); l2.ThietLapGiaTriB(9); l1.HienThi(); l2.HienThi(); TimGiaTriNhoNhat(l1, l2); return 0; }

    Và kết quả sau khi thực thi chương trình trên như sau:

    Ở ví dụ trên hàm TimGiaTriNhoNhat là hàm bạn của cả hai lớp Lop1 và Lop2 vì vậy hàm TimGiaTriNhoNhat có thể truy cập dữ liệu thành viên private của cả hai lớp.

    Trong C++ có hổ trợ thêm khái niệm khác nữa đó là lớp bạn (Friend class). Vậy lớp bạn là gì? Chúng ta sẽ cùng tìm hiểu ở phần sau đây.

    2. Lớp bạn (Friend class)

    Một lớp bạn ( friend class) có thể truy cập cả các thành viên riêng tư và được bảo vệ của lớp mà nó đã được khai báo là friend.

    Ví dụ

    Chúng ta cùng xem một ví dụ đơn giản về lớp bạn ( friend class) trong C++ như sau:

    using namespace std; class Lop1 { int a = 10; friend class Lop2; }; class Lop2 { public: void HienThi(Lop1 &l1) { cout << "Gia tri cua a la: "<< l1.a; } }; int main() { Lop1 l1; Lop2 l2; l2.HienThi(l1); return 0; }

    Và kết quả sau khi thực thi chương trình trên như sau:

    Trong ví dụ trên, Lop2 được khai báo là friend trong lớp Lop1. Do đó, Lop2 là bạn của Lop1. Lop2 có thể truy cập các thành viên private của Lop1.

    3. Kết luận

    Như vậy là chúng ta đã tìm hiểu xong về hàm bạn ( friend function) và lớp bạn ( friend class) trong C++ là gì rồi.

    Nếu các bạn muốn truy xuất thành viên của lớp ở dạng private hoặc protected từ một hàm hoặc lớp khác thì chúng ta sử dụng hàm bạn hoặc lớp bạn.

    --- Bài cũ hơn ---

  • 1.4 Biến Và Các Kiểu Dữ Liệu Trong C++
  • Nguyên Mẫu Hàm Trong C/c++ Là Gì? Và Cách Sử Dụng
  • Hàm Year Trong Excel, Hàm Cho Giá Trị Là Năm Của Biểu Thức Số, Ví Dụ V
  • Tổng Quan Về Xml Trong Excel
  • Đối Tượng Worksheet Trong Excel Vba