Tải bản đầy đủ
Một số con trỏ đặc biệt

Một số con trỏ đặc biệt

Tải bản đầy đủ

TopTaiLieu.Com | Chia Sẻ Tài Liệu Miễn Phí
© Dương Thiên Tứ

www.codeschool.vn

Không thể thực hiện số học con trỏ trên con trỏ kiểu void* vì không xác định được kích thước dữ liệu mà nó chỉ đến (nhưng
thực hiện được với con trỏ void**). Không thể deference con trỏ kiểu void* vì không xác định được kiểu dữ liệu của nó. Chỉ
có thể ép kiểu tường minh nó thành kiểu con trỏ khác.
Ép kiểu con trỏ có kiểu bất kỳ thành con trỏ có kiểu void* rồi truyền nó như đối số đến toán tử << sẽ trả về địa chỉ vùng nhớ
chứa trong con trỏ đó dưới dạng hex.
#include
using namespace std;
int main()
{
char* p = "OOP with C++";
void* q;
// con trỏ void* có thể chỉ đến dữ liệu có kiểu bất kỳ, ý tưởng:
// lưu trữ một con trỏ kiểu bất kỳ trong con trỏ void*
q = p;
// con trỏ void* q quản lý chuỗi "OOP with C++", có thể gán p bằng NULL
p = NULL;
// phải ép kiểu con trỏ void* để sử dụng được dữ liệu nơi nó chỉ đến
p = ( char* )q;
cout << p << endl;
// in chuỗi "OOP with C++"
cout << ( void* )p << endl;
// in địa chỉ chứa trong p
return 0;
}
Con trỏ void* dùng xây dựng các phương thức, template, nạp chồng toán tử new, … trong đó cần con trỏ chỉ đến một kiểu
chung do chưa xác định được kích thước kiểu dữ liệu chỉ đến.
c) Con trỏ hàm (function pointer - functor)
Khác với con trỏ thông thường dùng chỉ đến dữ liệu, con trỏ hàm là một loại con trỏ dùng chỉ đến code. Trong C++, tên một
hàm là con trỏ hằng chỉ đến hàm đó. Có nhiều cách dùng con trỏ hàm: dùng gọi hàm thay cho tên hàm, dùng như đối số truyền
đến hàm khác, lưu vào mảng để truy xuất theo chỉ số một hàm trong tập các hàm giống nhau.
#include
using namespace std;
int
{
int
{

add( int
return a
mul( int
return a

a, int b )
+ b; }
a, int b )
* b; }

// định nghĩa một số hàm có đặc điểm giống nhau
// nhận hai đối số int và trả về kiểu int

// khai báo con trỏ hàm nhận hai đối số int và trả về kiểu int
int ( *pFunc )( int, int );
// khai báo và gán một mảng các con trỏ hàm (các tên hàm)
int ( *apFunc[] )( int, int ) = { add, mul };
// khai báo một hàm nhận đối số là một con trỏ hàm (tên hàm)
int caller( int, int, int ( *p )( int, int ) );
int main()
{
// gán cho con trỏ hàm một địa chỉ hàm để nó ủy nhiệm đến khi gọi hàm
pFunc = &add;
// dereference một con trỏ hàm nghĩa là ủy nhiệm lời gọi đến hàm cần gọi, trong trường hợp này là hàm add()
cout << ( *pFunc )( 4, 5 ) << endl;
// nên dùng cú pháp sau, linh động và dễ sử dụng hơn, ủy nhiệm đến hàm mul(),
pFunc = mul;
// tên một hàm xem như là một con trỏ hàm!
// dùng con trỏ hàm như một hàm! (ủy nhiệm gọi hàm), tên con trỏ thay cho tên hàm
cout << pFunc( 4, 5 ) << endl;
// dùng một con trỏ hàm trong mảng các con trỏ hàm
cout << apFunc[0]( 4, 5 ) << endl;
// một phần tử của mảng con trỏ hàm cũng là một con trỏ!
cout << ( *( apFunc + 1 ) )( 4, 5 ) << endl;

}

// chuyển lời gọi hàm mul() đến hàm caller(), truyền con trỏ hàm như đối số
cout << caller( 4, 5, mul ) << endl;
return 0;

int caller( int a, int b, int ( *p )( int, int ) )
{
32

TopTaiLieu.Com | Chia Sẻ Tài Liệu Miễn Phí
© Dương Thiên Tứ

www.codeschool.vn

return p( a, b );

}
Dùng typedef sẽ làm cho chương trình dễ đọc hơn nhiều, xem lại ví dụ trên:
#include
using namespace std;
int add( int a, int b ) { return a + b; }
int mul( int a, int b ) { return a * b; }
typedef int ( *VPF )( int, int );
VPF v[] = { add, mul };
// mảng các con trỏ hàm
int caller( int, int, VPF );
// truyền con trỏ hàm như đối số
int main()
{
VPF pFunc = mul;
cout << pFunc( 4, 5 ) << endl;
cout << v[0]( 4, 5 ) << endl;
cout << caller( 4, 5, mul ) << endl;
return 0;
}

//
//
//
//

dùng tên hàm như con trỏ hàm
dùng con trỏ hàm giống như một hàm
dùng con trỏ hàm trong mảng các con trỏ hàm
gọi hàm với đối số là con trỏ hàm

int caller( int a, int b, VPF p )
{
return p( a, b );
}
Con trỏ chỉ đến phương thức thành viên (pointer to member function) cũng được sử dụng trong trường hợp lớp có nhiều phương
thức thành viên có đặc điểm giống nhau:
- Truy xuất con trỏ chỉ đến phương thức thành viên thông qua con trỏ chỉ đến đối tượng, dùng toán tử ->*
- Truy xuất con trỏ chỉ đến phương thức thành viên thông qua đối tượng, dùng toán tử .*
Khi truy xuất con trỏ chỉ đến dữ liệu thành viên cũng sử dụng hai toán tử trên theo cách tương tự.
#include
#include
using namespace std;
class Greetings {
public:
void hello( const string & name ) const
{ cout << "Hello, " << name << endl; }
void byebye( const string & name ) const
{ cout << "Byebye, " << name <};
typedef void ( Greetings::*VPF )( string ) const;
void greet( const Greetings* p, string name, VPF pFunc = &Greetings::hello )
{
( p->*pFunc )( name );
}
int main()
{
Greetings* p = new Greetings();
greet( p, "Albert Einstein", &Greetings::byebye );
return 0;
}
Con trỏ hàm thường được dùng để thực hiện các hàm callback, cơ chế kết nối động (dynamic binding), các ứng dụng hướng sự
kiện, cơ chế ủy nhiệm (delegate), …
II. Tham chiếu (Reference)
1. Kiểu tham chiếu
a
iint a = 7;
aa
7
// khai báo tham chiếu, ký hiệu &
// không phải là toán tử lấy địa chỉ
chỉ là khai báo một "tên khác"
int& aa = a;
cout << aa << endl;
// đọc GIÁN TIẾP trị của a thông qua "tên khác" aa
aa = 5;
// a bây giờ là 5, bị thay đổi GIÁN TIẾP thông qua aa
aa là một tên khác của a, một bí danh (alias) của a (a gọi là referent của aa). Ta truy xuất đến aa cũng giống như truy xuất
đến a. Nhận xét: có thể truy xuất GIÁN TIẾP đến biến a thông qua bí danh của nó là biến tham chiếu aa.
Tham chiếu cũng truy xuất GIÁN TIẾP đến một biến như con trỏ nhưng cú pháp ít phức tạp hơn. C++ có khuynh hướng sử
33

TopTaiLieu.Com | Chia Sẻ Tài Liệu Miễn Phí
© Dương Thiên Tứ

www.codeschool.vn

dụng tham chiếu nhiều hơn con trỏ.
Tham chiếu không phải là một biến riêng biệt, biến tham chiếu aa thật sự đồng nhất với biến a. Áp dụng toán tử & lên tham
chiếu giống như áp dụng toán tử & lên biến mà nó tham chiếu đến, kết quả cho cùng một địa chỉ là địa chỉ của biến. Việc định
nghĩa một tham chiếu không tốn thêm bộ nhớ như con trỏ.
cout << &aa << ' ' << &a; // kết quả cho cùng một địa chỉ, ví dụ 0x25FDA8
Như vậy, tham chiếu chính là một tên khác của một biến đã tồn tại, nhưng dùng tham chiếu có nghĩa là ta đã truy xuất đến biến
đó một cách gián tiếp. Vì bản chất tham chiếu khác con trỏ nên tham chiếu có rất điểm khác con trỏ:
- Tham chiếu cần được khởi gán tại thời điểm nó được tạo ra, sau đó không thể thay đổi tham chiếu chỉ đến một đối tượng khác.
Nghĩa là không dùng một tham chiếu cho nhiều biến khác nhau, không gán lại tham chiếu.
- Không có biến tham chiếu NULL. Không có tham chiếu đến một con trỏ.
- Không có đặc tính cộng trừ như con trỏ. Không thể gán chỉ số cho tham chiếu.
- Không thể tạo một mảng các tham chiếu.
- Con trỏ được dereference bằng toán tử * hoặc -> một cách tường minh. Tham chiếu được dereference tự động, không dùng
toán tử. Vì vậy dùng tham chiếu có vẻ "tự nhiên" hơn, giống với dùng biến trực tiếp.
// con trỏ
// tham chiếu
// trực tiếp
int x;
int x;
int x;
int* p = &x;
int& y = x;
*p = 10;
y = 10;
x = 10;
cout << *p;
cout << y;
cout << x;
2. Chú ý khi dùng tham chiếu
Biến tham chiếu hoàn toàn không có ý nghĩa cho đến khi nó được khởi tạo bằng cách gắn (attach) với một referent nào đó. Gán
cho aa trị của b, nghĩa là gán cho a trị của b, không phải thay đổi tham chiếu aa cho nó chỉ (kết nối - bound) đến b.
int a = 2, b = 3;
int& aa = a;
// khởi gán tham chiếu aa là một alias của a
int c = aa;
// gán c với tham chiếu aa, tương đương gán c với a (mà aa là alias)
aa = b;
// thay đổi a với trị của b, không phải gán aa là alias của b
int& bb = 10;
// SAI. Tham chiếu phải là alias của biến hoặc đối tượng
Có thể tham chiếu đến một đối tượng cấp phát động.
#include
class Point
{
int x, y;
public:
Point( int a = 0, int b = 0 )
{ x = a; y = b; }
void show()
{ std::cout << "( " << x << ", " << y << " )\n"; }
};
int main()
{
// dùng tham chiếu của một đối tượng cấp phát động
Point& p = *new Point( 3, 4 );
p.show();
// dùng con trỏ chỉ đến đối tượng cấp phát động
Point* q = new Point( 5, 6 );
q->show();
return 0;
}
III. Truyền đối số cho một hàm
1. Truyền bằng trị (Pass By Value)
void DoubleIt( int x )
{ x *= 2; }
int main()
{
int a = 8;
DoubleIt( a );
cout << a << endl; // kết quả: 8, a không đổi sau khi gọi hàm DoubleIt
return 0;
}
Khi dùng truyền bằng trị biến a đến hàm DoubleIt(), đối số thực a sẽ được COPY đến đối số hình thức x: int x = a;
Hàm DoubleIt() sẽ thao tác trên biến cục bộ x này giống như thao tác trên một bản sao của a.
Hàm DoubleIt() không làm thay đổi được biến a do biến a ở tầm vực (scope) khác: thuộc về hàm main().
Truyền một mảng đến hàm tương đương với truyền con trỏ, hàm nhận đối số là mảng có thể thay đổi các phần tử của mảng.
Truyền con trỏ hằng (const con trỏ truyền) để bảo vệ mảng tránh thay đổi nếu cần.
34

TopTaiLieu.Com | Chia Sẻ Tài Liệu Miễn Phí
© Dương Thiên Tứ

www.codeschool.vn

2. Truyền bằng tham chiếu thông qua con trỏ (Pointer Based Pass By Reference)
Ta hiểu là truyền bằng trị một đối số kiểu con trỏ.
void DoubleIt( int* x )
{ *x *= 2; }
int main()
{
int a = 8;
DoubleIt( &a );
cout << a << endl; // kết quả: 16, a thay đổi sau khi gọi hàm DoubleIt
return 0;
}
Địa chỉ của đối số thực a được COPY đến đối số hình thức là con trỏ *x như sau: int* x = &a;
Như vậy, rõ ràng x là con trỏ chỉ đến biến a.
Hàm DoubleIt() không thể truy xuất TRỰC TIẾP biến a của hàm main(); nhưng vẫn có thể truy xuất GIÁN TIẾP và làm
thay đổi đối số thực a thông qua con trỏ x chỉ đến nó.
Ta nhận thấy cú pháp sử dụng phức tạp và dễ nhầm lẫn cho cả người gọi hàm lẫn người cài đặt hàm, do dùng nhiều toán tử *
và &. Tuy nhiên, đây là cách ngôn ngữ C dùng.
Ta thường dùng con trỏ để truy xuất đọc và ghi đến một đối tượng. Khi dùng chỉ với tác vụ đọc, ta có thể dùng từ khóa const
để gắn "nhãn chống ghi" cho đối tượng do con trỏ chỉ đến. Con trỏ trong trường hợp này gọi là con trỏ chỉ đọc (read-only pointer).
#include
using namespace std;
// mô phỏng các hàm xử lý chuỗi của C trả về số ký tự có trong chuỗi
size_t _strlen( const char *str )
{
const char* p = str;
while ( *p ) ++p;
return ( p - str );
}
// so sánh len ký tự của chuỗi s1 với s2
int _strncmp( const char *s1, const char *s2, size_t len )
{
if ( len > _strlen( s1 ) ) len = _strlen( s1 );
if ( len > _strlen( s2 ) ) len = _strlen( s2 );
for ( int i = 0; i < len; ++i )
if ( s1[i] != s2[i] ) return 1;
return 0;
}
// tìm chuỗi s2 trong chuỗi s1
char* _strstr( const char *s1, const char *s2 )
{
size_t len = _strlen( s2 );
for ( ; *s1 != '\0'; ++s1 )
if ( _strncmp( s1, s2, len ) == 0 )
return ( char * )s1;
return NULL;
}
// trả về vị trí các chuỗi s2 trong s1
int main()
{
char* s = "hom qua qua khong qua";
char* p = s;
while ( ( p = _strstr( p, "qua" ) ) != NULL )
{
std::cout << p - s << ' ';
p++;
}
return 0;
}
3. Truyền bằng tham chiếu (Pass By Reference)
Ta hiểu là truyền bằng trị một đối số kiểu tham chiếu.
void DoubleIt( int& x ){ x *= 2; }
int main()
{
int a = 8;
DoubleIt( a );
cout << a << endl; // kết quả: 16, a thay đổi sau khi gọi hàm DoubleIt
35