Con trỏ trong lập hình hướng đối tượng C++
1.4 Pointer - Con trỏ
Các biến sử dụng trong một chương trình được coi như những chiếc hộp không bao giờ rỗng. Chúng được lấp đầy một số nội dung do người lập trình viên hoặc nếu chưa được khởi tạo, thì nó được khởi tạo bởi hệ điều hành. Một biến như vậy có ít nhất hai thuộc tính: giá trị và ví trị của biến trong bộ nhớ máy tính. Giá trị này có thể là một số, một kí tự hoặc một thành phần phức tạp hơn như là cấu trúc. Tuy nhiên, giá trị này cũng có thể là vị trí của một biến khác, các biến có giá trị như vậy được gọi là con trỏ. Con trỏ thường là biến phụ trợ cho phép chúng ta truy cập vào giá trị của biến khác một cách gián tiếp. Một con trỏ tương tự như một đèn báo giao thông chỉ chúng ta đến một địa điểm nhất định hoặc một tờ giấy được ghi địa chỉ trên đó:
Ví dụ, khai báo sau:
int i = 15, j, *p, *q;
i
, j
là các biến số và p
, q
là các con trỏ chỉ đến biển có dữ liệu kiểu số. Dấu sao phía trước p
và q
cho biết chức năng của chúng. Giả sử địa chỉ của các biến i
, j
, p
và q
lần lượt là 1080, 1082, 1084 và 1086, sau đó gán giá trị 15 cho biến i
trong khai báo, vị trị và giá trị của biến ở trong bộ nhớ máy tính ở trong hình 1.1a.
Bây giờ, chúng ta có thể thực hiện gán p = i
(hoặc p = (int*) i
nếu trình biên dịch báo lỗi), nhưng biến p
được tạo ra để lưu địa chỉ của một biến số nguyên, chứ không phải giá trị của nó. Do đó, gán biến đúng cách là p = &i
, trong đó dấu và ở phía trước i
có nghĩa là địa chỉ của i
chứ không phải là nội dung của nó. Hình 1.1b minh họa cho trường hợp này. Ở hình 1.1c, mũi tên chỉ từ p
đến i
là một con trỏ chưa địa chỉ của i
.
Chúng ta có khả năng phân biệt được giá trị của p
, là một địa chỉ, với giá trị của vị trị có địa chỉ mà con trỏ đang trỏ đến. Ví dụ, để gán giá trị 20 cho biến được trỏ bới p
, câu lệnh gán là:
*p = 20;
Dấu sao (*) là một toán tử điều hướng buộc hệ thống phải truy xuất nội dung của p
trước tiên, sau đó truy cập vào vị trí có địa chỉ được lấy từ p
, sau đó gán giá trị 20 cho vị trí này (hình 1.1d). Hình 1.1e đến 1.1n cung cấp thêm một số ví dụ về các câu lệnh gán và cách lưu giá trị trong bộ nhớ máy tính.
Trong thực tế, con trỏ nó cũng giống như tất cả các biến cũng có 2 thuộc tính: nội dung và địa chỉ. Địa chỉ này có thể lưu ở biến khác, sau đó trở thành một con trỏ đến một con trỏ.
Trong hình 1.1, địa chỉ của biến đã được gán cho một con trỏ. Tuy nhiên, con trỏ được xem là các địa chỉ ẩn danh, chỉ có thể truy cập thông qua địa chỉ của chúng chứ không phải như biến, truy cập bằng tên của chúng. Các địa chỉ này phải được bởi hệ thống quản lí bộ nhớ. Xuyên suốt thời gian chạy của chương trình, không giống như biến, các địa chỉ được phân phát tại thời điểm biên dịch.
Để cấp phát động và phân bố bộ nhớ, hai hàm được sử dụng. Hàm đầu tiên là new
, lấy từ bộ nhớ nhiều không gian cần thiết để lưu trữ một đối tượng có kiểu theo sau new
. Ví dụ:
p = new int;
chương trình sẽ yêu cầu đủ không gian để lưu trữ một số nguyên, từ hệ thống quản lí bộ nhớ, và địa phần đầu của bộ nhớ này được lưu trử trong p
. Bây giờ giá trị có thể gán cho khối bộ nhớ được trỏ tới bởi p
một cách gián tiếp thông qua con trỏ khác, hoặc con trỏ p
hoặc con trỏ q
bất kì nào khác đã được gán địa chỉ lưu trữ trong p
với phép gán q = p
.
Nếu ô nhớ đã bị chiếm giữ bới một số nguyên có thể truy cập từ p
nhưng không còn cần thiết nữa, nó có thể trả về vùng bộ nhớ trống do hệ điều hành quản lý bằng cách thực hiện lệnh sau:
delete p;
Tuy nhiên, sau khi thực hiện câu lệnh delete
, địa chỉ của khối bộ nhớ vẫn còn trong p
, mặc dù khối không còn tồn tại nữa. Nó giống như địa chỉ của một ngôi nhà đã bị phá hủy. Tương tự, nếu sau khi thực hiện câu lệnh delete
chúng ta không xóa địa chỉ biến con trỏ, dẫn đến sự tiềm tàng nguy hiểm về kết quả của chương trình, và chúng ta có thể làm lỗi chương trình trình khi cố cắng truy cập vào địa chỉ không tồn tại, đặc biệt đói với các đối tượng phức tạp hơn là cái giá trị số. Vấn đề này gọi là con trỏ lơ lững. Để tránh vấn đề này, một địa chỉ phải được gán cho một con trỏ, nếu nó không phải là địa chỉ của bất kỳ vị trí nào, nó phải là một giá trị rỗng (NULL), đơn giản là 0. Sau đó thực hiện nhiệm vụ
p = 0;
Chúng ta có thể không nói rằng p
tham chiếu đến NULL hoặc trỏ đến NULL nhưng p
có giá trị là NULL hoặc 0.
Một vấn đề khác liên quan đến việc xóa bộ nhớ là rò rỉ bộ nhớ. Xét hai mã lệnh sau:
p = new int;
p = new int;
Sau khi phân bố bộ nhớ cho một số nguyên, cùng một con trỏ p
được dùng đẻ cấp phát cho một ô khác. Sau khi thực hiện nhiệm vụ thứ hai, ô đầu tiên không thể truy cập được và cũng không khả dụng trong lần cấp phát tiếp theo xuyên suốt chương trình. Vấn đề là không giải phóng bộ nhớ với lệnh delete, bộ nhớ có thể truy cập từ biến p trước đó khi thực hiện nhiệm vụ thứ 2. Đoạn code nên cài đăt như sau:
p = new int;
delete p;
p = new int;
Vấn đề rò rỉ bộ nhớ có thể trở nên nghiêm trọng khi một chương trình sử dụng nhiều và nhiều hơn nữa bộ nhớ không giải phóng nó, cuối cùng làm hết bộ nhớ và dẫn đến kết thúc chương trình đột ngột. Điều này đặt biệt quan trọng trong các chương trình được thực thi trong một thời gian dài, chẳng hạn như các chương trình ở servers.
1.4.1 Pointers and Arrays - Con trỏ và mảng
Trong phần trước, con trỏ p
tham chiếu đến một khối bộ nhớ chứa một số nguyên. Một tình huống thú vị hơn là khi một con trỏ tham chiếu đến một cấu trúc dữ liệu động đã được tạo và sửa đổi. Đây là một tình huống mà chúng ta cần phải vượt qua các hạn chế do mảng áp đặt. Mảng trong C++ và hầu hết trong các ngôn ngữ lập trình, phải được khai báo trước, do đó, kích thước của mảng cần phải biết trước khi chương trình bắt đầu. Điều này có nghĩa là người lập trình cần có kiến thức tốt về vấn đề sẽ được lập trình để chọn kích thước mảng phù hợp. Nếu kích thước quá lớn, thì mảng sẽ chiếm không gian bộ nhớ một cách không cần thiết, đơn giản là lãng phí bộ nhớ. Nếu kích thước quá nhỏ, thì dẫn đến việc tràn mảng và chương trình sẽ bị hủy bỏ. Đôi khi việc dự đoán kích thước của mảng không hề đơn giản, do đó quyết định trì hoãn cho đến thời gian chạy, và sau đó cấp phát đủ bộ nhớ để giữ mảng.
Vấn đề này được giải quyết khi sử dụng con trỏ. Xét hình 1.1b, trong hình này, con trỏ p
trỏ đến vị trí 1080. Nhưng nó cũng cho phép truy cập vị trí 1082, 1084 và vân vân bởi vì các vị trí cách đều nhau. Ví dụ, để truy cập giá trị của biến j
, đứng bên cạnh i
, lấy địa chỉ của ô hiện tại cộng với kích thước của ô hiện tại để sang ô nhớ tiếp theo. Và về cơ bản thì đây là cách C++ xử lí mảng.
Xét khai báo sau:
int a[5], *p;
Khai báo trên xác định a
là một con trỏ đến khối bộ nhớ và có thể chứa 5 số nguyên. Con trỏ a
là cố định, nghĩa là, a
phải được xem là một hằng số để bất kì nổ lực nào gán một giá trị cho a
, như là:
a = p;
Hoặc
a++;
được xem như là một lỗi biên dịch. Bời vì a
là một con trỏ, ký hiệu con trỏ có thể sử dụng truy cập các ô của mảng a
. Ví dụ, một kí hiệu mảng được sử dụng trong vòng lặp cộng tất cả các số trong mảng a
:
for (sum = a[0], i = 1; i < 5; i++)
sum += a[i];
có thể được thay thế bằng kí hiệu con trỏ
for (sum = *a, i = 1; i < 5; i++)
sum += *(a + i);
hoặc
for (sum = *a, p = a+1; p < a+5; p++)
sum += *p;
Lưu ý rằng a+1
là vị trí của ô nhớ tiếp theo của mảng a
có nghĩa là a+1
tương đương &a[1]
. Do đó, nếu a
bằng 1020, thì a+1
khong phải là 1021 mà là 1022 bởi vì con trỏ số học phụ thuộc vào loại thực thể con trỏ. Ví dụ, khai báo sau:
char b[5];
long c[5];
giả định b
bằng 1050 và c
bằng 1055, b+1
bằng 1051 bởi vì một kí tự chiếm 1 byte, và c+1
bằng 1059 bởi vì một số chiếm 4 byte. Lý do cho kết quả này của con trỏ số học là biểu thức c+i
là biểu diễn địa chỉ bộ nhớ c+i*sizeof(long)
.
Trong cuộc thảo luận này, mảng a
được khai báo tĩnh bằng cách khai báo 5 ô nhớ. Kích thước của mảng được cố định trong thời gian mà chương trình chạy. Nhưng một mảng có thể được khai báo động. Để khai báo mảng động, phải sử dụng biến con trỏ. Ví dụ, khai báo sau:
p = new int[n];
phân bố đầy đủ chỗ để lưu trữ n
số nguyên. Con trỏ p
có thể xem như là một biến mảng để các kí hiệu mảng có thể sử dụng. Ví dụ, tổng các số trong mảng p
có thể được tìm thấy với đoạn mã sử dụng kí hiệu mảng:
for (sum = p[0], i = 1; i < n; i++)
sum += p[i];
một kí hiệu con trỏ là sự biểu diễn của vòng lắp trước đó,
for (sum = *p, i = 1; i < n; i++)
sum += *(p+i);
hoặc kí hiệu con trỏ sử dụng với 2 con trỏ
for (sum = *p, q = p+1; q < p+n; q++)
sum += *q;
Bởi vì p
là một biến, nó có thể được gán cho một mảng mới. Nhưng nếu mảng hiện tại được trỏ tới p
mà không còn cần thiết nữa, nó nên được xử lý theo hướng dẫn sau
delete [] p;
Lưu ý việc sử dụng dấu ngoặc vuông trong hướng dẫn. Dấu ngoặc [] cho biết p
trỏ đến một mảng. Ngoài ra, delete nên được sử dụng với các con trỏ đã được gán một giá với từ khóa new. Vì lý do này, hai ứng dụng của delete sau đây có thể dẫn đến sự cố của chương trình:
int a[10], *p = a;
delete [] p;
int n = 10, *q = &n;
delete q;
Một loại mảng rất quan trọng đó là chuỗi, hoặc một mảng kí tự. Nhiều hàm được định nghĩa trước ở trên chuỗi. Tên của các hàm này bắt đầu với từ str
, như là strlen(s)
để tìm độ dài của chuỗi s
hoặc strcpy(s1,s2)
để sao chép chuỗi s2
đến chuỗi s1
. Điều quan trọng cần nhớ là tất cả các hàm đều giả định chuỗi kết thúc bằng kí tự null
‘\0’. Ví dụ, strcpy(s1,s2)
sẽ tiếp tục sao chép cho đến khi tìm thấy kì tự \0
ở s2
. Nếu một người lập trình không đưa kí tự này vào trong s2
, việc sao chép sẽ dừng lại khi lần xuất hiện đầu tiền của kí tự này được tìm thấy trong bộ nhớ máy tính sau vị trí s2
. Điều này có nghĩa là việc sao chép được thực hiện đến các vị trí bên ngoài s1
, điều này có thể dẫn đến sự cố của chương trình.
1.4.2 Poninter and Copy Constructors
Một số vấn đề có thể nảy sinh khi các thành viên dữ liệu con trỏ không được xử lý đúng cách khi sao chép dữ liệu từ đối tượng này qua đối tượng khác. Xét định nghĩa sau:
struct Node {
char *name;
int age;
Node(char *n = "", int a = 0) {
name = strdup(n);
age = a;
}
};
Mục đích của khai báo:
Node node1("Roger",20), node2(node1); //or node2 = node1;
là tạo đối tượng node1
, gán giá trị cho hai thành viên dữ liệu trong node1
, và sau đó tạo node2
có các thành viên dữ liệu giống như node1
. Các đối tượng này phải là các thực thể độc lập để gán giá trị cho một trong số chúng sẽ không bị ảnh hướng đến các giá trị khác. Tuy nhiên, sau mệnh đề
strcpy(node2.name,"Wendy");
node2.age = 30;
lệnh in màn hình:
cout<<node1.name<<' '<<node1.age<<' '<<node2.name<<' '<<node2.age;
Hình 1.2
đầu ra chung: “Wendy 30 Wendy 20”
Tuổi khác nhau, nhưng tên ở trong hai đối tượng đều giống nhau. Điều gì sẽ xảy ra? Vấn đề là định nghĩa của Node
không cung cấp hàm sao chép constructor
Node (const Node&);
điều này vô cùng cần thiết để thực hiện khai báo node2(node1)
để khởi tạo node1
. Nếu hàm constructor tạo bản sao người dùng bị thiếu, thì constructor sẽ được tạo tự đổng bởi trình biên dịch. Nhưng trình biên dịch tạo ra constructor thực hiện sao chép từng thành viên. bởi vì name
là một con trỏ, constructor sap chép địa chỉ chuỗi node1.name
đến node2.name
, không phải là nội dung chuỗi, do đó ngay sai khi thực hiện khai báo, dẫn đến tình trạng như trong hình 1.2a. Bây giờ nếu mệnh đề
strcpy(node2.name,"Wendy");
node2.age = 30;
được thực hiện, node2.age
được cập nhật đúng cách, nhưng chuỗi “Roger” được trỏ bởi thành viên name
của cả hai đối tượng được ghi đè bởi “Wendy”, đều được trỏ bởi hai con trỏ (hình 1.2b). Để ngăn chặn điều này xảy ra, người dùng phải định nghĩa một hàm constructor thích hợp, như dưới đây:
struct Node {
char *name;
int age;
Node(char *n = 0, int a = 0) {
name = strdup(n);
age = a;
}
Node(const Node& n) { // copy constructor;
name = strdup(n.name);
age = n.age;
}
};
Với constructor, khai báo node2(node1)
tạo ra một bản sao khác của "Roger"
được trỏ đến bởi node2.name
(hình1.2c), và các nhiệm vụ cho thành viên viên dữ liệu trong một đối tượng không có ảnh hướng đến các thành viên trong các đối tượng khác, do đó sau khi thực hiện các lệnh
strcpy(node2.name,"Wendy");
node2.age = 30;
đối tượng node1
vẫn không thay đổi, hình minh họa 1.2d.
Lưu ý rằng một vấn đề tương tự được đưa ra bởi phép toán gán. Nếu một định nghĩa của toán tử gán không được cung cấp bởi người dùng, phép gán
node1 = node2;
thực hiện copy từng thành viên, dẫn đến vấn đều giống như trong hình 1.2a-b. Để tránh vấn đề này, toán tử gán phải được thực hiện bởi người dùng. Đối với Node
, việc thực hiện toán tử gán như sau:
Node& operator=(const Node& n) {
if (this != &n) { // no assignment to itself;
if (name != 0)
free(name);
name = strdup(n.name);
age = n.age;
}
return *this;
}
Trong đoạn code này, một con trỏ đặc biệt this đã được sử dụng. Mỗi đối tượng có thể truy cập địa chỉ của chính nó thông qua con trỏ this.
1.4.3 Pointers and Destructors
Điều gì sẽ xảy ra với các đối tượng được định nghĩa cục bộ của kiểu Node
? Giống như tất cả các mục cục bộ, chúng bị hủy bỏ theo nghĩa là chúng trở nên không khả dụng bên ngoài khối mà chúng được định nghĩa, và các vùng nhớ bị chiếm giữ cũng được giải phóng. Nhưng mặc dù một bộ nhớ bị chiếm giữ bởi một đối tượng của kiểu Node
được giải phóng, không phải là tất cả bộ nhớ liên quan đến đối tượng này đều trở nên khả dụng. Một trong những thành viên dữ liệu của đối tượng là một con trỏ trỏ đến một chuỗi, do đó, bộ nhớ bị chiếm giữ bởi con trỏ thành viên dữ liệu được giải phóng, nhưng bộ nhớ được lấy bởi chuổi thì không. Sau khi đối tượng được hủy bỏ, chuỗi khả dụng có sẵn trước đó từ thành viên dữ liệu name
trở nên không thể truy cập được (nếu không gán name
cho một số đối tượng khác hoặc một biến chuỗi) và bộ nhớ bị chiếm giữ bởi chuỗi này không thể giải phóng được nữa, dẫn đến rò rĩ bộ nhớ. Đây là vấn đề với các đối tượng có các thành viên dữ liệu trỏ đến các vị trí được phân bố động. Để tránh vấn đề này, một lớp nên bao gồm một định nghĩa về destructor. Destructor là một hàm tự động được gọi khi một đối tượng bị hủy bỏ, diễn ra khi thoát khỏi khối bộ nhớ trong đó đối tượng được định nghĩa hoặc theo lệnh gọi delete
. Destructors không nhận các tham số và không trả về giá trị nào chỉ có thể có một destructor cho một lớp. Ví dụ lớp Node
, Destructor có thể được định nghĩa như sau:
~Node() {
if (name != 0)
free(name);
}
1.4.4 Poniters and Reference Variables
Xét các khác báo sau:
int n = 5, *p = &n, &r = n;
Biến p
được khai báo kiểu int*
, một con trỏ trỏ đến một số nguyên, và r
là kiểu int&
, một biến số nguyên được tham chiếu. Một biến tham chiếu phải được khởi tạo trong khai báo của nó như một tham chiếu đến một biến cụ thể, và tham chiếu này không thể thay đổi. Điều này có nghĩa là biến tham chiếu không được trống. Một biến tham chiếu r
có thể được coi là một tên khác của một biến tham chiếu n
để nếu n
thay đổi thì r
cũng thay đổi theo. Điều này là do một biến tham chiếu được trình bày như một con trỏ hằng.
Sau ba khai báo, thực hiện in
cout << n << ' ' << *p << ' ' << r << endl;
Đầu ra là là 5 5 5. Sau đó gán
n = 7;
cùng một câu lệnh in đầu ra là 7 7 7. Ngoài ra, một phép gán
*p = 9;
đầu ra 9 9 9, và thực hiện phép gán
r = 10;
nó cũng dẫn đến đầu ra là 10 10 10. Những lệnh trên chỉ ra rằng về mặt kí hiệu, những gì chúng ta có thể thực hiện với việc bỏ tham chiếu các biến con trỏ được thi hành mà không cần bỏ tham chiếu với các biến tham chiếu. Điều này không phải là tình cờ, như đã đề cập, các biến tham chiếu được triển khai dưới dạng con trỏ hằng. Thay vì khai báo
int& r = n
chúng ta có thể sử dụng khai báo
int *const r = &n;
trong đó r
là một con trỏ hằng đến một số nguyên, có nghĩa là phép gán
r = q;
trong đó q
là một con trỏ khác, là một lỗi vì giá trị của r
không thể thay đổi. Tuy nhiên, phép gán
*r = 1;
được chấp nhận nếu n
là một số nguyên không đổi.
Điều quan trọng là cần lưu ý sự khác biệt giữa kiểu int *const và kiểu const int *. Cái sau là một kiểu con trỏ tới một số nguyên không đổi.
const int *s = &m;
sau đó gán
s = &m;
trong đó m
là mốt số nguyên (cho dù là hằng số hay không) là có thể chấp nhận được, nhưng phép gán
*s = 2;
là sai, ngay cả khi m là một hằng số.
Các biến tham chiếu được dử dụng để truyền các tham số bằng cách tham chiếu đến cái lời gọi hàm. Truyền bằng tham chiếu là yêu cầu bắt buộc nếu một tham số thực tế sẽ được thay đổi vĩnh viên trong quá trình thực thi hàm. Điều này có thể thực hiện đối với các con trỏ (và trong C, đây là cơ chế có sẵn duy nhất để chuyển qua tham chiếu) hoặc với tham chiếu biến. Ví dụ, sau khi khai báo một hàm
void f1(int i, int* j, int& k) {
i = 1;
*j = 2;
k = 3;
}
giá trị của các biến
int n1 = 4, n2 = 5, n3 = 6;
sau khi thực hiện lời gọi
f1(n1, &n2, n3);
là n1 = 4, n2 =2, n3 = 3.
Kiểu tham chiếu cũng được sử dụng để chỉ ra kiểu trả về của hàm. ví dụ, chúng ta có định nghĩa hàm
int& f2(int a[], int i) {
return a[i];
}
và khai bảo mảng
int a[] = {1,2,3,4,5};
chúng ta có thể sử dụng hàm f2() ở bất khì phía nào của toán tử. Ví dụ, ở phía bên phải
n = f2(a,3);
hoặc phía bên trái
f2(a,3) = 6;
khi gán 6 cho a[3]
có nghĩa là a = [1 2 3 6 5]
. Lưu ý rằng chúng ta có thể thực hiện giống như một con trỏ, nhưng không được tham chiếu một cách rõ ràng
int* f3(int a[], int i) {
return &a[i];
}
và sau đó
*f3(a,3) = 6;
Các biến tham chiếu và kiểu trả về phải được sử dụng một cách thận trọng bởi vì nó có khả năng ảnh hưởng đến nguyên tắc che giấu thông tin khi chúng được sử dụng không đúng cách. Xét class C:
class C {
public:
int& getRefN() {
return n;
}
int getN() {
return n;
}
private:
int n;
} c;
và có các lệnh:
int& k =c.getRefN();
k = 7;
cout<<c.getN();
Mặc dù n
được khai bảo private, sau phép gán đầu tiên nó có thể truy cập theo ý muốn từ bên ngoài thông qua k và được gán bất kì giá trị nào. Một hành vi cũng có thể được thực hiện thông qua getRefN();
c.getRefN() = 9;
1.4.5 Pointer to Functions
Như đã thảo luận trong phần 1.4.1
, một trong những thuộc tính của của một biến là một địa chỉ của nó cho biết vị trí của nó trong bộ nhớ máy tính. Điều này cũng đúng với các hàm. Một trong những thuộc tính của fhafmunction là địa chỉ của nó cho biết vị trí của phần thân hàm trong bộ nhớ. Khi gọi một hàm, hệ thống chuyển quyền điều khiển đến vị trí này để thực hiện hàm. Vì lý do này, nó có khả năng sử dụng con trỏ trỏ đến các hàm. Những con trỏ này vô cũng hữu ích trong việc thực thi các hàm (nghĩa là các hàm nhận các đối số) chẳng hạn như một tích phân.
Xét một hàm đơn giản sau:
double f(double x) {
return 2*x;
}
Với định nghĩa này, f
là một con trỏ tới function f()
, *f
là một function của chính nó, và (*f)(7)
là lời gọi hàm.
Bây giờ hãy xem xét viết một hàm C++ để tính tổng sau:
\[\sum_{i=n} ^ {m} f(i) \]
Để tính tổng, chúng ta phải cung cấp n
và m
không giới hạn, nhưng còn cung cấp thêm hàm f
. Do đó, mong muốn thực hiện nên cho phép truyền số không chỉ ở dạng tham số, mà còn ở dạng hàm. Điều này được thực hiên trong C++ theo cách sau:
double sum(double (*f)(double), int n, int m) {
double result = 0;
for (int i = n; i <= m; i++)
result += f(i);
return result;
}
Trong định nghĩa của sum()
, khai báo tham số chính đầu tiên
double (*f)(double)
có nghĩa là f
là một con trỏ đến một hàm với một tham số double và trả về giá trị double. Lưu ý rằng cần dấu ngoặc đơn ()
xung quanh *f
. Bởi vì dấu ngoặc đơn được ưu tiên hơn toán tử tham chiếu *
, biểu thức
double *f(double)
khai báo một hàm và trả về giá trị double.
Bây giờ, có thể gọi hàm sum()
bằng bất kì cách xây dựng nào hoặc người dùng xác định các hàm double nhận các đối số trả về kiểu double, như
cout << sum(f,1,5) << endl;
cout << sum(sin,3,7) << endl;
Một ví dú khác về một hàm tìm root của một hàm liên tục trong một khoảng. Root được tìm thấy bằng cách lặp đi lặp lại chia đôi một khảng và tìm một điểm giữa của khoảng hiện tại. Nếu giá trị hàm của điểm ở giữa bằng 0 hoặc khoảng nhỏ hơn một số giá trị, điểm ở giữa được trả về. Nếu giá trị của hàm ở giới hạn bên trái của khoảng hiện tại và điểm ở giữa có dấu hiệu ngược lại, tiếp tục tìm kiếm trong nửa khoảng bên trái hiện tại, nếu không, khoảng hiện tại sẽ là nửa khoảng bên phải. Đây là một triên khai của thuật toán này:
double root(double (*f)(double), double a, double b, double epsilon) {
double middle = (a + b) / 2;
while (f(middle) != 0 && fabs(b - a) > epsilon) {
if (f(a) * f(middle) < 0) // if f(a) and f(middle) have
b = middle; // opposite signs;
else a = middle;
middle = (a + b) / 2;
}
return middle;
}