Chức năng
Các chức năng cho phép cấu trúc các chương trình trong các phân đoạn mã để thực hiện các tác vụ riêng lẻ.
Trong C++, hàm là một nhóm các câu lệnh được đặt tên và có thể được gọi từ một số điểm của chương trình. Cú pháp phổ biến nhất để định nghĩa một hàm là: Trong đó:
– là loại giá trị được hàm trả về.
– là mã định danh mà hàm có thể được gọi.
– (nhiều như cần thiết): Mỗi tham số bao gồm một loại theo sau là một mã định danh, với mỗi tham số được phân tách với tham số tiếp theo bằng dấu phẩy. Mỗi tham số trông rất giống một khai báo biến chính quy (ví dụ: ), và trên thực tế hoạt động trong hàm như một biến chính quy cục bộ của hàm. Mục đích của các tham số là cho phép truyền các đối số đến hàm từ vị trí mà nó được gọi đến.
– là cơ thể của chức năng. Nó là một khối các câu lệnh được bao quanh bởi dấu ngoặc nhọn { } chỉ định chức năng thực sự làm gì.
Chúng ta hãy xem một ví dụ: type name ( parameter1, parameter2, ...) { statements }
type
name
parameters
int x
statements
|
The result is 8 |
Chương trình này được chia thành hai hàm: và . Hãy nhớ rằng bất kể thứ tự mà chúng được xác định, một chương trình C ++ luôn bắt đầu bằng cách gọi . Trên thực tế, là hàm duy nhất được gọi tự động và mã trong bất kỳ hàm nào khác chỉ được thực thi nếu hàm của nó được gọi từ (trực tiếp hoặc gián tiếp).
Trong ví dụ trên, bắt đầu bằng cách khai báo biến kiểu , và ngay sau đó, nó thực hiện lệnh gọi hàm đầu tiên: nó gọi . Lệnh gọi đến một hàm tuân theo một cấu trúc rất giống với khai báo của nó. Trong ví dụ trên, lệnh gọi đến có thể được so sánh với định nghĩa của nó chỉ vài dòng trước đó:
Các tham số trong khai báo hàm có sự tương ứng rõ ràng với các đối số được truyền trong lệnh gọi hàm. Cuộc gọi chuyển hai giá trị và , đến hàm; Chúng tương ứng với các tham số và , được khai báo cho hàm .
Tại thời điểm mà hàm được gọi từ bên trong chính, điều khiển được chuyển đến hàm : ở đây, việc thực thi được dừng lại và sẽ chỉ tiếp tục khi hàm kết thúc. Tại thời điểm gọi hàm, giá trị của cả hai đối số ( và ) được sao chép vào các biến cục bộ và trong hàm.
Sau đó, bên trong , một biến cục bộ khác được khai báo (), và bằng biểu thức , kết quả của cộng được gán cho ; Trong đó, đối với trường hợp này, trong đó là 5 và là 3, có nghĩa là 8 được gán cho .
Câu lệnh cuối cùng trong hàm:addition
main
main
main
main
main
z
int
addition
addition
5
3
a
b
addition
addition
main
addition
5
3
int a
int b
addition
int r
r=a+b
a
b
r
a
b
r
|
|
Kết thúc hàm , và trả về điều khiển trở lại điểm mà hàm được gọi; Trong trường hợp này: để chức năng . Tại thời điểm chính xác này, chương trình tiếp tục khóa học quay trở lại chính xác tại cùng thời điểm mà nó bị gián đoạn bởi cuộc gọi đến . Nhưng ngoài ra, vì có kiểu trả về, lệnh gọi được đánh giá là có giá trị và giá trị này là giá trị được chỉ định trong câu lệnh trả về đã kết thúc : trong trường hợp cụ thể này, giá trị của biến cục bộ , tại thời điểm câu lệnh có giá trị là 8.
Do đó, lệnh gọi đến là một biểu thức có giá trị được hàm trả về và trong trường hợp này, giá trị đó, 8, được gán cho . Như thể toàn bộ hàm gọi () đã được thay thế bằng giá trị mà nó trả về (tức là 8).
Sau đó, main chỉ cần in giá trị này bằng cách gọi: addition
main
main
addition
addition
addition
r
return
addition
z
addition(5,3)
|
|
Một hàm thực sự có thể được gọi nhiều lần trong một chương trình và đối số của nó tự nhiên không chỉ giới hạn ở các nghĩa đen:
|
The first result is 5 The second result is 5 The third result is 2 The fourth result is 6 |
Tương tự như hàm trong ví dụ trước, ví dụ này định nghĩa một hàm, chỉ đơn giản trả về sự khác biệt giữa hai tham số của nó. Lần này, gọi hàm này nhiều lần, chứng minh nhiều cách khả thi hơn trong đó một hàm có thể được gọi.
Hãy kiểm tra từng cuộc gọi này, lưu ý rằng mỗi cuộc gọi hàm tự nó là một biểu thức được đánh giá là giá trị mà nó trả về. Một lần nữa, bạn có thể nghĩ về nó như thể lệnh gọi hàm tự nó được thay thế bằng giá trị trả về:addition
subtract
main
|
|
Nếu chúng ta thay thế lệnh gọi hàm bằng giá trị mà nó trả về (tức là 5), chúng ta sẽ có:
|
|
Với quy trình tương tự, chúng ta có thể giải thích:
|
|
như:
|
|
vì 5 là giá trị được trả về bởi .
Trong trường hợp:subtraction (7,2)
|
|
Các đối số được truyền cho phép trừ là các biến thay vì nghĩa đen. Điều đó cũng hợp lệ, và hoạt động tốt. Hàm được gọi với các giá trị và có tại thời điểm gọi: 5 và 3 tương ứng, kết quả là trả về 2.
Cuộc gọi thứ tư lại tương tự:x
y
|
|
Bổ sung duy nhất là bây giờ cuộc gọi hàm cũng là một toán hạng của một phép cộng. Một lần nữa, kết quả giống như khi cuộc gọi hàm được thay thế bằng kết quả của nó: 6. Lưu ý, nhờ thuộc tính giao hoán của phép cộng, ở trên cũng có thể được viết là:
|
|
Với kết quả chính xác tương tự. Cũng lưu ý rằng dấu chấm phẩy không nhất thiết phải đi sau lệnh gọi hàm, nhưng, như mọi khi, ở cuối toàn bộ câu lệnh. Một lần nữa, logic đằng sau có thể dễ dàng được nhìn thấy lại bằng cách thay thế các lệnh gọi hàm bằng giá trị trả về của chúng:
|
|
Chức năng không có loại. Việc sử dụng khoảng trống
Cú pháp hiển thị ở trên cho các hàm:
Yêu cầu khai báo bắt đầu bằng một kiểu. Đây là loại giá trị được trả về bởi hàm. Nhưng nếu hàm không cần trả về giá trị thì sao? Trong trường hợp này, loại được sử dụng là , là một loại đặc biệt để đại diện cho sự vắng mặt của giá trị. Ví dụ: một hàm chỉ cần in thư có thể không cần trả về bất kỳ giá trị nào: type name ( argument1, argument2 ...) { statements }
void
|
I'm a function! |
void
cũng có thể được sử dụng trong danh sách tham số của hàm để xác định rõ ràng rằng hàm không nhận tham số thực tế khi được gọi. Ví dụ: có thể được khai báo là:printmessage
|
|
Trong C++, một danh sách tham số trống có thể được sử dụng thay vì có cùng ý nghĩa, nhưng việc sử dụng trong danh sách đối số đã được phổ biến bởi ngôn ngữ C, trong đó đây là một yêu cầu.
Một cái gì đó trong mọi trường hợp không phải là tùy chọn là dấu ngoặc đơn theo tên hàm, không phải trong khai báo của nó cũng như khi gọi nó. Và ngay cả khi hàm không nhận tham số, ít nhất một cặp dấu ngoặc đơn trống sẽ luôn được gắn vào tên hàm. Xem cách được gọi trong ví dụ trước:void
void
printmessage
|
|
Dấu ngoặc đơn là những gì phân biệt các chức năng với các loại khai báo hoặc tuyên bố khác. Sau đây sẽ không gọi hàm:
|
|
Giá trị trả về của chính
Bạn có thể nhận thấy rằng kiểu trả về là , nhưng hầu hết các ví dụ trong chương này và các chương trước không thực sự trả về bất kỳ giá trị nào từ .
Vâng, có một nhược điểm: Nếu việc thực thi kết thúc bình thường mà không gặp phải một câu lệnh, trình biên dịch giả định hàm kết thúc bằng một câu lệnh trả về ngầm:main
int
main
main
return
|
|
Lưu ý rằng điều này chỉ áp dụng cho chức năng vì lý do lịch sử. Tất cả các hàm khác có kiểu trả về sẽ kết thúc bằng một câu lệnh thích hợp bao gồm giá trị trả về, ngay cả khi giá trị này không bao giờ được sử dụng.
Khi trả về số 0 (ngầm hoặc rõ ràng), môi trường diễn giải nó là chương trình đã kết thúc thành công. Các giá trị khác có thể được trả về bởi , và một số môi trường cung cấp quyền truy cập vào giá trị đó cho người gọi theo một cách nào đó, mặc dù hành vi này không bắt buộc cũng như không nhất thiết phải di động giữa các nền tảng. Các giá trị cho điều đó được đảm bảo được diễn giải theo cùng một cách trên tất cả các nền tảng là:main
return
main
main
main
Bởi vì tuyên bố ngầm cho là một ngoại lệ khó khăn, một số tác giả coi đó là thực hành tốt để viết tuyên bố rõ ràng.return 0;
main
Các đối số được truyền theo giá trị và bằng tham chiếu
Trong các hàm được thấy trước đó, các đối số luôn được truyền theo giá trị. Điều này có nghĩa là, khi gọi một hàm, những gì được truyền đến hàm là các giá trị của các đối số này tại thời điểm của cuộc gọi, được sao chép vào các biến được biểu thị bằng các tham số hàm. Ví dụ: hãy:
|
|
Trong trường hợp này, phép cộng hàm được truyền 5 và 3, là bản sao của các giá trị của và , tương ứng. Các giá trị này (5 và 3) được sử dụng để khởi tạo các biến được đặt làm tham số trong định nghĩa của hàm, nhưng bất kỳ sửa đổi nào của các biến này trong hàm đều không ảnh hưởng đến các giá trị của các biến x và y bên ngoài nó, bởi vì x và y không được chuyển đến hàm trong cuộc gọi, mà chỉ sao chép các giá trị của chúng tại thời điểm đó.
Tuy nhiên, trong một số trường hợp nhất định, có thể hữu ích khi truy cập một biến bên ngoài từ bên trong một hàm. Để làm điều đó, các đối số có thể được truyền bằng tham chiếu, thay vì theo giá trị. Ví dụ: hàm trong mã này sao chép giá trị của ba đối số của nó, khiến các biến được sử dụng làm đối số thực sự được sửa đổi bởi lệnh gọi:x
y
duplicate
|
x=2, y=6, z=14 |
Để có quyền truy cập vào các đối số của nó, hàm khai báo các tham số của nó làm tham chiếu. Trong C++, các tham chiếu được biểu thị bằng dấu và () theo kiểu tham số, như trong các tham số được lấy trong ví dụ trên.
Khi một biến được truyền bằng tham chiếu, những gì được truyền không còn là bản sao nữa, mà chính biến, biến được xác định bởi tham số hàm, bằng cách nào đó được liên kết với đối số được truyền đến hàm và bất kỳ sửa đổi nào trên các biến cục bộ tương ứng của chúng trong hàm được phản ánh trong các biến được truyền dưới dạng đối số trong cuộc gọi.
Trên thực tế, , , và trở thành bí danh của các đối số được truyền vào lệnh gọi hàm (, và ) và bất kỳ thay đổi nào trong hàm thực sự là sửa đổi biến bên ngoài hàm. Bất kỳ thay đổi nào về sửa đổi và bất kỳ thay đổi nào về sửa đổi . Đó là lý do tại sao khi, trong ví dụ, hàm sửa đổi các giá trị của biến , và , các giá trị của , , và bị ảnh hưởng.
Nếu thay vì định nghĩa trùng lặp là:&
duplicate
a
b
c
x
y
z
a
x
b
y
c
z
duplicate
a
b
c
x
y
z
|
|
Nó có được định nghĩa mà không có ký hiệu và dấu hiệu là:
|
|
Các biến sẽ không được truyền bằng tham chiếu, mà theo giá trị, thay vào đó tạo ra các bản sao của các giá trị của chúng. Trong trường hợp này, đầu ra của chương trình sẽ là các giá trị của , và không bị sửa đổi (tức là 1, 3 và 7).x
y
z
Cân nhắc hiệu quả và tham chiếu const
Gọi một hàm với các tham số được lấy bởi giá trị sẽ tạo ra các bản sao của các giá trị. Đây là một hoạt động tương đối rẻ tiền đối với các loại cơ bản như , nhưng nếu tham số thuộc loại hợp chất lớn, nó có thể dẫn đến một số chi phí nhất định. Ví dụ: hãy xem xét chức năng sau:int
|
|
Hàm này lấy hai chuỗi làm tham số (theo giá trị) và trả về kết quả nối chúng. Bằng cách truyền các đối số theo giá trị, hàm buộc và là bản sao của các đối số được truyền đến hàm khi nó được gọi. Và nếu đây là những chuỗi dài, nó có thể có nghĩa là sao chép một lượng lớn dữ liệu chỉ cho cuộc gọi hàm.
Nhưng bản sao này có thể tránh được hoàn toàn nếu cả hai tham số được tạo tham chiếu:a
b
|
|
Các đối số bằng cách tham khảo không yêu cầu một bản sao. Hàm hoạt động trực tiếp trên (bí danh của) các chuỗi được truyền dưới dạng đối số và nhiều nhất, nó có thể có nghĩa là chuyển một số con trỏ nhất định sang hàm. Về vấn đề này, phiên bản lấy tham chiếu hiệu quả hơn phiên bản lấy giá trị, vì nó không cần sao chép các chuỗi đắt tiền để sao chép.
Mặt khác, các hàm có tham số tham chiếu thường được coi là các hàm sửa đổi các đối số được truyền, bởi vì đó là lý do tại sao các tham số tham chiếu thực sự dành cho.
Giải pháp là để hàm đảm bảo rằng các tham số tham chiếu của nó sẽ không bị sửa đổi bởi hàm này. Điều này có thể được thực hiện bằng cách đánh giá các tham số là hằng số:concatenate
|
|
Bằng cách đủ điều kiện chúng là , hàm bị cấm sửa đổi các giá trị của không cũng không , nhưng thực sự có thể truy cập các giá trị của chúng dưới dạng tham chiếu (bí danh của các đối số) mà không cần phải tạo các bản sao thực tế của chuỗi.
Do đó, các tham chiếu cung cấp chức năng tương tự như truyền các đối số theo giá trị, nhưng với hiệu quả tăng lên cho các tham số của các loại lớn. Đó là lý do tại sao chúng cực kỳ phổ biến trong C ++ cho các đối số của các loại ghép. Tuy nhiên, lưu ý rằng đối với hầu hết các loại cơ bản, không có sự khác biệt đáng chú ý về hiệu quả và trong một số trường hợp, các tham chiếu const thậm chí có thể kém hiệu quả hơn!const
a
b
const
Hàm nội tuyến
Gọi một hàm thường gây ra một chi phí nhất định (xếp chồng các đối số, nhảy, v.v.), và do đó đối với các hàm rất ngắn, có thể hiệu quả hơn khi chỉ cần chèn mã của hàm nơi nó được gọi, thay vì thực hiện quá trình gọi chính thức một hàm.
Trước khi khai báo hàm với mã xác định thông báo cho trình biên dịch rằng mở rộng nội tuyến được ưu tiên hơn cơ chế gọi hàm thông thường cho một hàm cụ thể. Điều này hoàn toàn không thay đổi hành vi của một hàm, mà chỉ được sử dụng để gợi ý trình biên dịch rằng mã được tạo bởi phần thân hàm sẽ được chèn vào mỗi điểm mà hàm được gọi, thay vì được gọi bằng một lệnh gọi hàm thông thường.
Ví dụ: hàm concatenate ở trên có thể được khai báo nội tuyến là:inline
|
|
Điều này thông báo cho trình biên dịch rằng khi đượ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 một cuộc gọi thông thường. chỉ được chỉ đị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 hàm nội tuyến khi chúng thấy cơ hội cải thiện hiệu quả, ngay cả khi không được đánh dấu rõ ràng bằng mã xác định. Do đó, specifier này chỉ đơn thuần chỉ ra trình biên dịch mà nội tuyến được ưa thích cho hàm này, mặc dù trình biên dịch có thể tự do không nội tuyến nó và tối ưu hóa khác. Trong C++, tối ưu hóa là một tác vụ được giao cho trình biên dịch, miễn phí tạo bất kỳ mã nào miễn là hành vi kết quả là hành vi được chỉ định bởi mã.concatenate
inline
inline
Giá trị mặc định trong tham số
Trong C++, các hàm cũng có thể có các tham số tùy chọn, trong đó không có đối số nào được yêu cầu trong cuộc gọi, theo cách mà ví dụ, một hàm có ba tham số có thể được gọi chỉ với hai. Đối với điều này, hàm sẽ bao gồm một giá trị mặc định cho tham số cuối cùng của nó, được hàm sử dụng khi được gọi với ít đối số hơn. Chẳng hạn:
|
6 5 |
Trong ví dụ này, có hai cuộc gọi để hàm . Trong cái đầu tiên:divide
|
|
Cuộc gọi chỉ truyền một đối số cho hàm, mặc dù hàm có hai tham số. Trong trường hợp này, hàm giả định tham số thứ hai là 2 (lưu ý định nghĩa hàm, khai báo tham số thứ hai của nó là ). Do đó, kết quả là 6.
Trong cuộc gọi thứ hai:int b=2
|
|
Cuộc gọi chuyển hai đối số cho hàm. Do đó, giá trị mặc định cho () bị bỏ qua và lấy giá trị được truyền làm đối số, nghĩa là 4, mang lại kết quả là 5.b
int b=2
b
Khai báo hàm
Trong C++, định danh chỉ có thể được sử dụng trong các biểu thức khi chúng đã được khai báo. Ví dụ, một số biến không thể được sử dụng trước khi được khai báo với một câu lệnh, chẳng hạn như:x
|
|
Điều tương tự cũng áp dụng cho các chức năng. Các hàm không thể được gọi trước khi chúng được khai báo. Đó là lý do tại sao, trong tất cả các ví dụ về hàm trước đây, các hàm luôn được xác định trước hàm, đó là hàm từ nơi các hàm khác được gọi. Nếu được định nghĩa trước các hàm khác, điều này sẽ phá vỡ quy tắc rằng các hàm phải được khai báo trước khi được sử dụng, và do đó sẽ không biên dịch.
Nguyên mẫu của một hàm có thể được khai báo mà không thực sự định nghĩa hàm hoàn toàn, chỉ cung cấp đủ chi tiết để cho phép các kiểu liên quan đến lệnh gọi hàm được biết đến. Đương nhiên, hàm sẽ được định nghĩa ở một nơi khác, như sau này trong mã. Nhưng ít nhất, một khi tuyên bố như thế này, nó đã có thể được gọi.
Khai báo phải bao gồm tất cả các kiểu liên quan (kiểu trả về và loại đối số của nó), sử dụng cùng cú pháp như được sử dụng trong định nghĩa của hàm, nhưng thay thế phần thân của hàm (khối câu lệnh) bằng dấu chấm phẩy kết thúc.
Danh sách tham số không cần bao gồm tên tham số mà chỉ bao gồm các loại của chúng. Tuy nhiên, tên tham số có thể được chỉ định, nhưng chúng là tùy chọn và không nhất thiết phải khớp với tên trong định nghĩa hàm. Ví dụ, một hàm được gọi với hai tham số int có thể được khai báo với một trong các câu lệnh sau:main
main
protofunction
|
|
Dù sao, bao gồm tên cho mỗi tham số luôn cải thiện tính dễ đọc của khai báo.
|
Please, enter number (0 to exit): 9 It is odd. Please, enter number (0 to exit): 6 It is even. Please, enter number (0 to exit): 1030 It is even. Please, enter number (0 to exit): 0 It is even. |
Ví dụ này thực sự không phải là một ví dụ về hiệu quả. Bạn có thể viết cho mình một phiên bản của chương trình này với một nửa dòng mã. Dù sao, ví dụ này minh họa cách các hàm có thể được khai báo trước định nghĩa của nó: Các dòng sau:
|
|
Khai báo nguyên mẫu của các chức năng. Chúng đã chứa tất cả những gì cần thiết để gọi chúng, tên của chúng, các loại đối số của chúng và loại trả về của chúng (trong trường hợp này). Với các khai báo nguyên mẫu này, chúng có thể được gọi trước khi chúng được định nghĩa hoàn toàn, cho phép ví dụ, đặt hàm từ nơi chúng được gọi là () trước định nghĩa thực tế của các hàm này.
Nhưng khai báo các hàm trước khi được định nghĩa không chỉ hữu ích để tổ chức lại thứ tự các hàm trong mã. Trong một số trường hợp, chẳng hạn như trong trường hợp cụ thể này, ít nhất một trong các tuyên bố là bắt buộc, bởi vì và được gọi là lẫn nhau; Có một cuộc gọi đến trong và một cuộc gọi đến trong . Và, do đó, không có cách nào để cấu trúc mã để được xác định trước và trước .void
main
odd
even
even
odd
odd
even
odd
even
even
odd
Đệ quy
Đệ quy là thuộc tính mà các hàm phải được gọi bởi chính chúng. Nó rất hữu ích cho một số tác vụ, chẳng hạn như sắp xếp các phần tử hoặc tính toán giai thừa của số. Ví dụ, để có được giai thừa của một số(), công thức toán học sẽ là: Cụ thể hơn, (giai thừa của 5) sẽ là: Và một hàm đệ quy để tính toán điều này trong C++ có thể là:
n!
n! = n * (n-1) * (n-2) * (n-3) ... * 1
5!
5! = 5 * 4 * 3 * 2 * 1 = 120
|
9! = 362880 |
Lưu ý cách trong giai thừa hàm, chúng tôi bao gồm một cuộc gọi đến chính nó, nhưng chỉ khi đối số được truyền lớn hơn 1, vì nếu không, hàm sẽ thực hiện một vòng lặp đệ quy vô hạn, trong đó khi nó đến 0, nó sẽ tiếp tục nhân với tất cả các số âm (có thể gây ra tràn ngăn xếp tại một số điểm trong thời gian chạy).