Monday, January 4, 2016

Con trỏ - Bài 10 : Phép toán số học trên con trỏ (Phần 2)

Chúng ta sẽ xét 1 số ví dụ mình họa cho quy tắc tính toán địa chỉ vừa được trình bày. Hình trên trình bày một ví dụ về tính toán địa chỉ con trỏ và các ký pháp lúc viết chương trình. Con trỏ p kiểu double nhận địa chỉ của biến a[1] là phần tử thứ hai trong mảng năm số. Vì p = &a[1] và đang dùng cùng kiểu double nên theo quy ước ký pháp ở bài trước thì *p, p[0], và a[1] là cùng 1 biến, tức là mặc dù viết khác nhưng chúng tham chiếu đến cùng 1 ô nhớ 8-byte(kích thước biến double) có thể chứa một số thực kiểu double. Địa chỉ p + 1 dời đi so với p một độ dời bằng với 1 x sizeof(double) = 8 byte, nên p + 1 cũng là &a[2], do đó *(p + 1), p[1], a[2] là ba cách viết khác nhau cùng tham chiếu đến cùng một biến. Tương tự *(p + 3), p[3], a[4] cũng tham chiếu đến cùng 1 biến a[4].
Địa chỉ (float*)p + 4 là một con trỏ kiểu float* dời đi so với p một độ dời là 4 x sizeof(float) = 16 byte = 2 x (8 byte). Do đó vì p đang giữ địa chỉ của a[1] nên (float*)p + 4 bằng với: địa chỉ (a[1]) + 16 byte và trùng với địa chỉ của a[3]. Tuy nhiên ký pháp biến *((float*)p + 4) (một biến float kích thước 4-byte) vô nghĩa vì tương đương với phân nửa của biến a[3] (một biến double kích thước 8-byte). Nếu ép kiểu để chuyển về double như là *(double*)((float*)p + 4) thì chính là biến a[3], cũng cùng nghĩa với *(p + 2).

Sunday, January 3, 2016

Con trỏ - Bài 9 : Phép toán số học trên con trỏ (Phần 1)

-Các phép tính cơ bản cho các biến con trỏ có kiểu(tức không phải là void*)
*Phép cộng, trừ với số: con trỏ(địa chỉ bộ nhớ) +(hoặc -) số nguyên. Phép toán này có ý nghĩa là tạo ra một địa chỉ bộ nhớ có độ lệch so với địa chỉ gốc một số byte tùy thuộc vào kiểu con trỏ và số nguyên mà ta cộng hay trừ vào địa chỉ.
*Phép trừ 2 con trỏ: con trỏ(địa chỉ 1) - con trỏ(địa chỉ 2). Phép toán này yêu cầu 2 con trỏ phải có cùng kiểu. Kết quả cho ra độ lệch địa chỉ có đơn vị tính tùy thuộc vào kiểu con trỏ.
 -Riêng đối với kiểu void*(con trỏ vô kiểu) thì không thể thực hiện 2 phép toán nói trên. Khi muốn tính toán địa chỉ trên con trỏ vô kiểu thì ta phải ép kiểu con trỏ. Thông thường nếu muốn tính theo đơn vị byte thì các con trỏ được ép kiểu sang char*.
- Giả sử pVar là biến con trỏ kiểu T* với T là một kiểu dữ liệu nào đó khác kiểu void và n là 1 số nguyên(có thể âm hay dương). Khi đó pVar + n là địa chỉ bộ nhớ sẽ dời đi so với địa chỉ pVar một độ dời là n x sizeof(T).Nghĩa là chúng ta có công thức sau:
 Địa chỉ(pVar + n) = Địa chỉ(pVar) + n x sizeof(T)
Công thức nói trên cũng lý giải tại sao con trỏ kiểu void* không thể cộng hay trừ với số nguyên bởi vì sizeof(void) không xác định. Đối với con trỏ kiểu char* vì sizeof(char) = 1 nên khi T = char thì pVar + n dời đi so với pVar đúng n byte.
 Quy ước ký pháp: Với điều kiện địa chỉ hợp lệ thì ký hiệu *(pVar + n) được đồng nhất với biến kiểu T tại địa chỉ pVar + n, biến này cũng có thể được truy xuất đến bằng cách dùng ký hiệu mảng pVar[n].
 - Giả sử 2 biến con trỏ pVar1 và pVar2 cùng có kiểu là T* với T là 1 kiểu dữ liệu nào đó khác void. Khi đó pVar1 - pVar2 là độ lệch được tính bằng sizeof(T), nói 1 cách đơn giản hơn là độ lệch này được tính bằng số lượng các phần tử có kiểu là T. Tức là ta có công thức sau:
                             Địa chỉ(pVar1) - Địa chỉ(pVar2) 
 pVar1 - pVar2 = ---------------------------------------
                                          sizeof(T) 
-> Áp dụng công thức khi T = char thì (char*)pVar1 - (char*)pVar2 là độ lệch địa chỉ giữa 2 con trỏ được tính bằng đơn vị là byte.

Con trỏ - Bài 8 : Gán con trỏ bằng địa chỉ bộ nhớ xin cấp phát

Các chương trình viết bằng C/C++ chuẩn có thể xin cấp phát bộ nhớ từ hệ điều hành, địa chỉ đầu của vùng nhớ được cấp phát sẽ được trả về thông qua biến con trỏ. Sau khi vùng nhớ sử dụng xong, người lập trình cần phải giải phóng vùng nhớ, để trả về bộ nhớ cho hệ thống.

void main()
{
int n; float *a = NULL; // Khai báo con trỏ a kiểu float*
printf("\nEnter array size: ");
scanf("%d", &n);
if(n < 1)
{
printf("\nInvalid size!"); return;
}
a = (float*)malloc(n * sizeof(float)); // gán đến địa chỉ vùng nhớ xin được cấp phát
if(a == NULL)
{
printf("Memory error!"); return;
}
for(int i = 0; i < n; i++)
{
printf("a[%d] = ", i);
scanf("%f", &a[i]);
}
printf("\nData: ");
for(int i = 0; i < n; i++)
{
printf("%f", a[i]);
// do something here
// Finally freeing memory
}
free(a);
getch();
}

void main()
{
int n; float *a = NULL; // Khai báo con trỏ a kiểu float*
cout << "Enter array size: ";
cin >> n;
if(n < 1)
{
cout << "Invalid size!"; return;
}
a = new float[n]; // gán đến địa chỉ vùng nhớ xin được cấp phát
if(a == NULL)
{
cout << "Memory Error!"; return;
}
for(int i = 0; i < n; i++)
{
cout << "a[" << i << "] = ";
cin >> a[i];
}
cout << "Data: ";
for(int i = 0; i < n; i++)
{
cout << a[i] << " ";
// do something here
// Finally freeing memory
}
delete[]a;
getch();
}
Mã nguồn minh họa việc gán con trỏ bằng 1 vùng nhớ được cấp phát, được viết bằng ngôn ngữ C và C++. Chương trình này chỉ làm 1 việc đơn giản là xin cấp phát 1 mảng n số thực kiểu float, nhập n số thực, xuất ra màn hình và sau cùng giải phóng bộ nhớ. Biến con trỏ a kiểu float* được khai báo, sau đó gán đến địa chỉ vùng nhớ xin được cấp phát. Đối với C++, việc gọi new float[n] nghĩa là xin 1 vùng nhớ liên tục chứa được n số float, thực chất thì vùng nhớ này gồm 4n byte bởi vì mỗi số float có kích thước là 4 byte theo chuẩn IEEE. Đối với C, bởi vì hàm malloc() cần nhận vào 1 tham số quy định số byte được cấp phát và trả về con trỏ kiểu void* nên việc gọi hàm cấp phát có phức tạp hơn. Chỉ thị (float*)malloc(n*sizeof(float)) có nghĩa là xin 1 vùng nhớ có kích thước là n*sizeof(float) = 4n byte, bởi vì địa chỉ vùng nhớ trả về kiểu void* nên phải ép kiểu sang float* mới có thể gán được vào biến con trỏ a có kiểu là float*.

Con trỏ - Bài 7 : Kết hợp sử dụng tham số con trỏ và giá trị trả về kiểu con trỏ

Trong nhiều tình huống, chúng ta có thể cần viết các hàm kết hợp sử dụng cả tham số con trỏ và giá trị trả về kiểu con trỏ. Chằng hạn hàm trả về chuỗi lớn hơn khi so sánh theo thứ tự alphabet. Hàm này có thể được mô tả là char* strmax(char* str1, char* str2); khi chuỗi str1 lớn hơn str2 (tức là đứng sau str2 theo thứ tự từ điển) thì hàm trả về str1, ngược lại trả về str2. Trường hợp này ta thấy hàm strmax() nhận vào các tham số là 2 con trỏ kiểu char* và cũng trả về con trỏ kiểu char*.

char* strmax(char* str1, char* str2)
{
if(strcmp(str1, str2) > 0)
{
return str1;
}
return str2;
}
Một trong những lợi thế của hàm trả về con trỏ là chúng ta có thể gọi lồng các hàm để viết mã gọn hơn. Chẳng hạn sau khi tìm chuỗi lớn nhất chúng ta có nhu cầu chuyển hết các ký tự trong chuỗi đó thành chữ hoa bằng cách gọi hàm strupr().

int main()
{
const int maxLen = 256;
char s1[maxLen], s2[maxLen];
printf("\nInput s1 = "); gets(s1);
printf("\nInput s2 = "); gets(s2);
printf("\nResult: %s", strupr(strmax(s1, s2))); // Gọi lồng hàm
getch();
return 0;
}
Bản thân hàm printf() cũng cho phép nhận vào thàm số con trỏ kiểu char* khi chúng ta dùng định dạng %s trong chuỗi định dạng in. Một điểm cần lưu ý thêm là 2 biến s1 và s2 được khai báo trong hàm main() dưới dạng 2 mảng ký tự, nhưng ý nghĩa sử dụng lại là các chuỗi ký tự. Khi truyền s1 và s2 đến hàm strmax() thì s1 tương đương &s1[0], còn s2 tương đương &s2[0]. Vì vậy cả 2 đối số s1, s2 là 2 địa chỉ bộ nhớ có kiểu là char*.

Con trỏ - Bài 6 : Hàm trả về địa chỉ của biến bộ nhớ (Phần 2)

-Kiểu tham chiếu (reference) của C++ thực chất được cài đặt bằng kỹ thuật con trỏ, nhưng cú pháp lập trình lại tương tự như biến thông thường.Dưới đây là hàm refMinAbs() cài đặt tương tự như 2 hàm indexMinAbs() và pointerMinAbs() ở bài trước và chỉ khác ở 2 chỗ: giá trị trả về và chỉ thị return.

float& refMinAbs(float a[], int n)
{
int i = 1, idx = 0;
while(i < n)
{
if(fabs(a[i]) < fabs(a[idx]))
{
idx = i;
}
i++;
}
return a[idx];
}
- Trong hàm main() của chương trình chạy thử, việc gọi hàm với chỉ thị float& refMin = refMinAbs(B, n) nhằm để refMin tham chiếu đến phần tử tìm được. Lưu ý dấu & trong float& là thực sự cần thiết vì nếu ta thay bằng lời gọi hàm không có dấu & như là float refMin = refMinAbs(B, n) thì chương trình vẫn chạy nhưng biến refMin được gán bằng giá trị của phần tử tìm được chứ không tham chiếu(xem như đồng nhất) với phần tử tìm được. Trong hàm main() thì cả ba địa chỉ &B[id], pMin, &refMin đều cùng 1 giá trị địa chỉ bộ nhớ.

int main()
{
float B[] = {-9, 12.7F, 2.3F, 11, -10, -3.6F};
int n = sizeof(B)/sizeof(B[0]);
int id = indexMinAbs(B, n);
float* pMin = pointerMinAbs(B, n);
float& refMin = refMinAbs(B, n);
printf("Values: %f %f %f\n", B[id], *pMin, refMin);
printf("Memory Adresses: 0x%x 0x%x 0x%x", &B[id], pMin, &refMin);

getch();
return 0;
}

Con trỏ - Bài 5 : Hàm trả về địa chỉ của biến bộ nhớ (Phần 1)

- 2 cách cài đặt hàm tìm phần tử có giá trị tuyệt đối nhỏ nhất trong mảng 1 chiều gồm n số thực kiểu float

int indexMinAbs(float a[], int n)
{
int i = 1, idx = 0;
while(i < n)
{
if(fabs(a[i]) < fabs(a[idx]))
{
idx = i;
}
i++;
}
return idx;
}

float *pointerMinAbs(float a[], int n)
{
int i = 1, idx = 0;
while(i < n)
{
if(fabs(a[i]) < fabs(a[idx]))
{
idx = i;
}
i++;
}
return &a[idx];
}
- Địa chỉ của phần tử tìm được là &a[idx] được trả về thay vì là chỉ số của mảng idx

int main()
{
float B[] = {-9, 12.7F, 2.3F, 11, -10, -3.6F};
int n = sizeof(B)/sizeof(B[0]);
int id = indexMinAbs(B, n);
float* pMin = pointerMinAbs(B, n);
printf("Values: %f %f\n", B[id], *pMin);
printf("Memory Adresses: 0x%x 0x%x", &B[id], pMin);

getch();
return 0;
}
- Chương trình chính minh họa cho việc gọi thử 2 hàm indexMinAbs() và pointerMinAbs(). Khi chạy chương trình ta sẽ thấy B[id] và *pMin in tra cùng giá trị, hơn nữa các địa chỉ bộ nhớ &B[id] và pMin cũng in ra cùng giá trị.

Saturday, January 2, 2016

Con trỏ - Bài 4: Truyền địa chỉ biến qua nhiều hàm

Đôi khi địa chỉ biến được truyền qua một chuỗi các hàm trong 1 quy trình xử lý nào đó. Chẳng hạn ta muốn viết hàm ajust() để sau khi gọi ajust(&a, &b) thì nếu cần thiết ta phải hoán chuyển giá trị của a và b sao cho |a| <= |b| Trường hợp này, ajust() hàm nhận tham số địa chỉ biến và trong quá trình xử lý lại tiếp tục truyền các địa chỉ biến cho hàm swap() khi gọi hàm.

void swap(float *x, float *y)
{
float u = *x;
*x = *y;
*y = u;
}
void adjust(float *x, float *y)
{
if(fabs(*x) > fabs(*y))
//swap(&(*x), &(*y));
swap(x, y);
}
int main()
{
float a = -1845.3F, b = 234.3F;
adjust(&a, &b);
printf("\nNew values of a and b now are a = %.2f, b = %.2f", a, b);

getch();
return 0;
}
Hàm ajust() nhận 2 tham số x và y là địa chỉ của 2 biến số thực sẽ được gửi đến, tức là *x và *y có thể xem như chính là 2 biến số thực đó. Khi |*x| > |*y| thì chúng ta cần gọi hàm swap() để hoán chuyển nội dung của 2 biến gửi đến. Dòng mã thực hiện điều này là swap(&(*x), &(*y)) với ý nghĩa là đưa địa chỉ của các biến *x và *y đến hàm swap(). Tuy nhiên nếu diễn dịch kỹ thì &(*x) chính là "địa chỉ của biến mà x đang lưu địa chỉ": cụm từ này cho thấy &(*x) = x.

Pages - Menu