}
main()
{
int a[10] ; // mảng a chứa tối đa 10 phần tử
nhap(a,7); // vào 7 phần tử đầu tiên cho a
in(a,3); // ra 3 phần tử đầu tiên của a
}
Truyền mảng 2 chiều cho hàm
Đối với mảng 2 chiều khai báo đối cũng như lời gọi là phức tạp hơn nhiều so với mảng 1 chiều. Ta có hai cách khai báo đối như sau:
Khai báo theo đúng bản chất của mảng 2 chiều float x[m][n] do C++ qui định, tức x là mảng 1 chiều m phần tử, mỗi phần tử của nó có kiểu float[n]. Từ đó, đối được khai báo như một mảng hình thức 1 chiều (khồng cần số phần tử - ở đây là số dòng) của kiểu float[n]. Tức có thể khai báo như sau:
float x[][n] ; // mảng với số phần tử không định trước, mỗi phần tử là n số
float (*x)[n] ; // một con trỏ, có kiểu là mảng n số (float[n])
Để truy nhập đến đến phần tử thứ i, j ta vẫn sử dụng cú pháp x[i][j]. Tên của mảng a được viết bình thường trong lời gọi hàm. Nói chung theo cách khai báo này việc truy nhập là đơn giản nhưng phương pháp cũng có hạn chế đó là số cột của mảng truyền cho hàm phải cố định bằng n.
Xem mảng float x[m][n] thực sự là mảng một chiều float x[m*n] và sử dụng cách khai báo như trong mảng một chiều, đó là sử dụng con trỏ float *p để truy cập được đến từng phần tử của mảng. Cách này có hạn chế trong lời gọi: địa chỉ truyền cho hàm không phải là mảng a mà cần phải ép kiểu về (float*) (để phù hợp với p). Với cách này gọi k là thứ tự của phần tử a[i][j] trong mảng một chiều (m*n), ta có quan hệ giữa k, i, j như sau:
k = *(p + i*n + j)
i = k/n
j = k%n
trong đó n là số cột của mảng truyền cho hàm. Điều này có nghĩa để truy cập đến a[i][j] ta có thể viết *(p+i*n+j), ngược lại biết chỉ số k có thể tính được dòng i, cột j của phần tử này. Ưu điểm của cách khai báo này là ta có thể truyền mảng với kích thước bất kỳ (số cột không cần định trước) cho hàm.
Sau đây là các ví dụ minh hoạ cho 2 cách khai báo trên.
: Tính tổng các số hạng trong ma trận
float tong(float x[][10], int m, int n) // hoặc float tong(float (*x)[10], int m, int n)
{ // m: số dòng, n: số cột
float t = 0;
int i, j ;
for (i=0; i
for (j=0; j
return t;
}
main()
{
float a[8][10], b[5][7] ;
int i, j, ma, na, mb, nb;
cout << "nhập số dòng, số cột ma trận a: " ; cin >> ma >> na;
for (i=0; i
for (j=0; j
{ cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ; }
cout << "nhập số dòng, số cột ma trận b: " ; cin >> mb >> nb;
for (i=0; i
for (j=0; j
{ cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ; }
cout << tong(a, ma, na); // in tổng các số trong ma trận
cout << tong(b, mb, nb); // sai vì số cột của b khác 10
}
: Tìm phần tử bé nhất của ma trận
void minmt(float *x, int m, int n) // m: số dòng, n: số cột
{
float min = *x; // gán phần tử đầu tiên cho min
int k, kmin;
for (k=1; k
if (min > *(x+k)) { min = *(x+k) ; kmin = k; }
cout << "Giá trị min la: " << min << " tại dòng " << k/n << " cột " << k%n;
}
main()
{
float a[8][10], b[5][7] ;
int i, j ;
for (i=0; i<8; i++) // nhập ma trận a
for (j=0; j<10; j++)
{ cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ; }
for (i=0; i<5; i++) // nhập ma trận b
for (j=0; j<7; j++)
{ cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ; }
minmt((float*)a, 8, 10) ; // in giá trị và vị trí số bé nhất trong a
minmt((float*)b, 5, 7) ; // in giá trị và vị trí số bé nhất trong b
}
: Cộng 2 ma trận và in kết quả.
void inmt(float *x, int m, int n)
{
int i, j;
for (i=0; i
{
for (j=0; j
cout << endl;
}
}
void cong(float *x, float *y, int m, int n)
{
float *t = new float[m*n]; // t là ma trận kết quả (xem như dãy số)
int k, i, j ;
for (k = 0; k < m*n; k++) *(t+k) = *(x+k) + *(y+k) ;
inmt((float*)t, m, n);
}
main()
{
float a[8][10], b[5][7] ;
int i, j, m, n;
cout << "nhập số dòng, số cột ma trận: " ; cin >> m >> n;
for (i=0; i
for (j=0; j
{
cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ;
cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ;
}
cong((float*)a, (float*)b, m, n); // cộng và in kết quả a+b
}
Xu hướng chung là chúng ta xem mảng (1 hoặc 2 chiều) như là một dãy liên tiếp các số trong bộ nhớ, tức một ma trận là một đối con trỏ trỏ đến thành phần của mảng. Đối với mảng 2 chiều m*n khi truyền đối địa chỉ của ma trận cần phải ép kiểu về kiểu con trỏ. Ngoài ra bước chạy k của con trỏ (từ 0 đến m*n-1) tương ứng với các toạ độ của phần tử a[i][j] trong mảng như sau:
k = *(p + i*n + j)
i = k/n
j = k%n
từ đó, chúng ta có thể viết các hàm mà không cần phải băn khoăn gì về kích thước của ma trận sẽ truyền cho hàm.
Giá trị trả lại của hàm là một mảng
Không có cách nào để giá trị trả lại của một hàm là mảng. Tuy nhiên thực sự mỗi mảng cũng chính là một con trỏ, vì vậy việc hàm trả lại một con trỏ trỏ đến dãy dữ liệu kết quả là tương đương với việc trả lại mảng. Ngoài ra còn một cách dễ dùng hơn đối với mảng 2 chiều là mảng kết quả được trả lại vào trong tham đối của hàm (giống như nghiệm của phương trình bậc 2 được trả lại vào trong các tham đối). Ở đây chúng ta sẽ lần lượt xét 2 cách làm việc này.
Giá trị trả lại là con trỏ trỏ đến mảng kết quả. Trước hết chúng ta xét ví dụ nhỏ sau đây:
int* tragiatri1() // giá trị trả lại là con trỏ trỏ đến dãy số nguyên
{
int kq[3] = { 1, 2, 3 }; // tạo mảng kết quả với 3 giá trị 1, 2, 3
return kq ; // trả lại địa chỉ cho con trỏ kết quả hàm
}
int* tragiatri2() // giá trị trả lại là con trỏ trỏ đến dãy số nguyên
{
int *kq = new int[4]; // cấp phát 3 ô nhớ nguyên
*kq = *(kq+1) = *(kq+2) = 0 ; // tạo mảng kết quả với 3 giá trị 1, 2, 3
return kq ; // trả lại địa chỉ cho con trỏ kết quả hàm
}
main()
{
int *a, i;
a = tragiatri1();
for (i=0; i<3; i++) cout *(a+i); // không phải là 1, 2, 3
a = tragiatri2();
for (i=0; i<3; i++) cout *(a+i); // 1, 2, 3
}
Qua ví dụ trên ta thấy hai hàm trả giá trị đều tạo bên trong nó một mảng 3 số nguyên và trả lại địa chỉ mảng này cho con trỏ kết quả hàm. Tuy nhiên, chỉ có tragiatri2() là cho lại kết quả đúng. Tại sao ? Xét mảng kq được khai báo và khởi tạo trong tragiatri1(), đây là một mảng cục bộ (được tạo bên trong hàm) như sau này chúng ta sẽ thấy, các loại biến "tạm thời" này (và cả các tham đối) chỉ tồn tại trong quá trình hàm hoạt động. Khi hàm kết thúc các biến này sẽ mất đi. Do vậy tuy hàm đã trả lại địa chỉ của kq trước khi nó kết thúc, thế nhưng sau khi hàm thực hiện xong, toàn bộ kq sẽ được xoá khỏi bộ nhớ và vì vậy con trỏ kết quả hàm đã trỏ đến vùng nhớ không còn các giá trị như kq đã có. Từ điều này việc sử dụng hàm trả lại con trỏ là phải hết sức cẩn thận. Muốn trả lại con trỏ cho hàm thì con trỏ này phải trỏ đến dãy dữ liệu nào sao cho nó không mất đi sau khi hàm kết thúc, hay nói khác hơn đó phải là những dãy dữ liệu được khởi tạo bên ngoài hàm hoặc có thể sử dụng theo phương pháp trong hàm tragiatri2(). Trong tragiatri2() một mảng kết quả 3 số cũng được tạo ra nhưng bằng cách xin cấp phát vùng nhớ. Vùng nhớ được cấp phát này sẽ vẫn còn tồn tại sau khi hàm kết thúc (nó chỉ bị xoá đi khi sử dụng toán tử delete). Do vậy hoạt động của tragiatri2() là chính xác.
Tóm lại, ví dụ trên cho thấy nếu muốn trả lại giá trị con trỏ thì vùng dữ liệu mà nó trỏ đến phải được cấp phát một cách tường minh (bằng toán tử new), chứ không để chương trình tự động cấp phát và tự động thu hồi. Ví dụ sau minh hoạ hàm cộng 2 vectơ và trả lại vectơ kết quả (thực chất là con trỏ trỏ đến vùng nhớ đặt kết quả)
int* congvt(int *x, int *y, int n) // n số phần tử của vectơ
{
int* z = new int[n]; // xin cấp phát bộ nhớ
for (int i=0; i
return c;
}
main()
{
int i, n, a[10], b[10], c[10] ;
cout << "n = " ; cin >> n; // nhập số phần tử
for (i=0; i> a[i] ; // nhập vectơ a
for (i=0; i> b[i] ; // nhập vectơ b
c = congvt(a, b, n);
for (i=0; i
}
Chú ý: a[i], b[i], c[i] còn được viết dưới dạng tương đương *(a+i), *(b+i), *(c+i).
Trong cách này, mảng cần trả lại được khai báo như một tham đối trong danh sách đối của hàm. Tham đối này là một con trỏ nên hiển nhiên khi truyền mảng đã khai báo sẵn (để chứa kết quả) từ ngoài vào cho hàm thì mảng sẽ thực sự nhận được nội dung kết quả (tức có thay đổi trước và sau khi gọi hàm - xem mục truyền tham đối thực sự theo dẫn trỏ). Ở đây ta xét 2 ví dụ: bài toán cộng 2 vectơ trong ví dụ trước và nhân 2 ma trận.
: Cộng 2 vectơ, vectơ kết quả trả lại trong tham đối của hàm. So với ví dụ trước giá trị trả lại là void (không trả lại giá trị) còn danh sách đối có thêm con trỏ z để chứa kết quả.
void congvt(int *x, int *y, int *z, int n) // z lưu kết quả
{
for (int i=0; i
}
main()
{
int i, n, a[10], b[10], c[10] ;
cout << "n = " ; cin >> n; // nhập số phần tử
for (i=0; i> a[i] ; // nhập vectơ a
for (i=0; i> b[i] ; // nhập vectơ b
congvt(a, b, c, n);
for (i=0; i
}
: Nhân 2 ma trận kích thước m*n và n*p. Hai ma trận đầu vào và ma trận kết quả (kích thước m*p) đều được khai báo dưới dạng con trỏ và là đối của hàm nhanmt(). Nhắc lại, trong lời gọi hàm địa chỉ của 3 mảng cần được ép kiểu về (int*) để phù hợp với các con trỏ tham đối.
void nhanmt(int *x, int *y, int *z, int m, int n, int p) // z lưu kết quả
{
int i, j, k ;
for (i=0; i
for (j=0; j
{
*(z+i*p+j) = 0; // tức z[i][j] = 0
for (k=0; k
*(z+i*p+j) += *(x+i*n+k)**(y+k*p+j) ; // tức z[i][j] += x[i][k]*y[k][j]
}
}
main()
{
int a[10][10], b[10][10], c[10][10] ; // khai báo 3 mảng a, b, c
int m, n, p ; // kích thước các mảng
cout << "m, n, p = " ; cin >> m >> n >> p ; // nhập số phần tử
for (i=0; i
for (j=0; j
cout << "a[" << i << "," << j << "] = " ; cin >> a[i][j] ;
for (i=0; i
for (j=0; j
cout << "b[" << i << "," << j << "] = " ; cin >> b[i][j] ;
nhanmt((int*)a, (int*)b, (int*)c, m, n, p); // gọi hàm
for (i=0; i
{
for (j=0; j
cout << endl;
}
}
Đối và giá trị trả lại là xâu kí tự
Giống các trường hợp đã xét với mảng 1 chiều, đối của các hàm xâu kí tự có thể khai báo dưới 2 dạng: mảng kí tự hoặc con trỏ kí tự. Giá trị trả lại luôn luôn là con trỏ kí tự. Ngoài ra hàm cũng có thể trả lại giá trị vào trong các đối con trỏ trong danh sách đối.
Ví dụ sau đây dùng để tách họ, tên của một xâu họ và tên. Ví dụ gồm 3 hàm. Hàm họ trả lại xâu họ (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng mảng. Hàm tên trả lại xâu tên (con trỏ kí tự) với đối là xâu họ và tên được khai báo dạng con trỏ kí tự. Thực chất đối họ và tên trong hai hàm họ, tên có thể được khai báo theo cùng cách thức, ở đây chương trình muốn minh hoạ các cách khai báo đối khác nhau (đã đề cập đến trong phần đối mảng 1 chiều). Hàm thứ ba cũng trả lại họ, tên nhưng cho vào trong danh sách tham đối, do vậy hàm không trả lại giá trị (void). Để đơn giản ta qui ước xâu họ và tên không chứa các dấu cách đầu và cuối xâu, trong đó họ là dãy kí tự từ đầu cho đến khi gặp dấu cách đầu tiên và tên là dãy kí tự từ sau dấu cách cuối cùng đến kí tự cuối xâu.
char* ho(char hoten[]) // hàm trả lại họ
{
char* kq = new char[10]; // cấp bộ nhớ để chứa họ
int i=0;
while (hoten[i] != '\40') i++; // i dừng tại dấu cách đầu tiên
strncpy(kq, hoten, i) ; // copy i kí tự của hoten vào kq
return kq;
}
char* ten(char* hoten) // hàm trả lại tên
{
char* kq = new char[10]; // cấp bộ nhớ để chứa tên
int i=strlen(hoten);
while (hoten[i] != '\40') i--; // i dừng tại dấu cách cuối cùng
strncpy(kq, hoten+i+1, strlen(hoten)-i-1) ; // copy tên vào kq
return kq;
}
void tachht(char* hoten, char* ho, char* ten)
{
int i=0;
while (hoten[i] != '\40') i++; // i dừng tại dấu cách đầu tiên
strncpy(ho, hoten, i) ; // copy i kí tự của hoten vào ho
i=strlen(hoten);
while (hoten[i] != '\40') i--; // i dừng tại dấu cách cuối cùng
strncpy(ten, hoten+i+1, strlen(hoten)-i-1) ; // copy tên vào ten
}
main()
{
char ht[30], *h, *t ; // các biến họ tên, họ, tên
cout << "Họ và tên = " ; cin.getline(ht,30) ; // nhập họ tên
h = ho(ht); t = ten(ht);
cout << "Họ = " << h << ", tên = " << t << endl;
tachht(ht, h, t);
cout << "Họ = " << h << ", tên = " << t << endl;
}
Đối là hằng con trỏ
Theo phần truyền đối cho hàm ta đã biết để thay đổi biến ngoài đối tương ứng phải được khai báo dưới dạng con trỏ. Tuy nhiên, trong nhiều trường hợp các biến ngoài không có nhu cầu thay đổi nhưng đối tương ứng với nó vẫn phải khai báo dưới dạng con trỏ (ví dụ đối là mảng hoặc xâu kí tự). Điều này có khả năng do nhầm lẫn, các biến ngoài này sẽ bị thay đổi ngoài ý muốn. Trong trường hợp như vậy để cẩn thận, các đối con trỏ nếu không muốn thay đổi (chỉ lấy giá trị) cần được khai báo như là một hằng con trỏ bằng cách thêm trước khai báo kiểu của chúng từ khoá const. Từ khoá này khẳng định biến tuy là con trỏ nhưng nó là một hằng không thay đổi được giá trị. Nếu trong thân hàm ta cố tình thay đổi chúng thì chương trình sẽ báo lỗi. Ví dụ đối hoten trong cả 3 hàm ở trên có thể được khai báo dạng const char* hoten.
: Đối là hằng con trỏ. In hoa một xâu kí tự
void inhoa(const char* s)
{
char *t;
strcpy(t, s);
cout << s << strupr(t); // không dùng được strupr(s)
}
main()
{
char *s = "abcde" ;
inhoa(s); // abcdeABCDE
}
Chia sẻ với bạn bè của bạn: