Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 12.215 phút đọc

Toán tử lấy địa chỉ

Sau bài này bạn sẽ làm được

  • Dùng toán tử lấy địa chỉ đúng cách
  • Xác định kiểu của biểu thức lấy địa chỉ
  • Biết những gì không lấy địa chỉ được và vì sao
  • In địa chỉ đúng định dạng

Toán tử lấy địa chỉ là toán tử đơn giản nhất trong C: nó trả lời câu hỏi biến này nằm ở đâu. Bài này ngắn, nhưng nó chốt lại một chi tiết mà nếu không nắm thì mọi bài về con trỏ sau đây đều mù mờ: kiểu của kết quả.

#Cú pháp và ý nghĩa

Toán tử lấy địa chỉ
Ký hiệu & đặt trước tên biến, cho ra địa chỉ của biến đó. Đây là toán tử một ngôi, khác hoàn toàn với toán tử và trên bit cũng viết là & nhưng có hai toán hạng.
int n = 42;

&n              /* địa chỉ của n */

printf("%p\n", (void *)&n);      /* in ra ví dụ 0x7ffd3a2c1e4c */

Bạn đã dùng toán tử này từ Bài 3.3 mà chưa biết tên nó. Mọi lời gọi scanf đều cần nó.

int n;

scanf("%d", &n);      /* đưa cho scanf địa chỉ của n để nó ghi vào */
scanf("%d", n);       /* SAI: đưa giá trị rác của n, scanf coi đó là địa chỉ */

Toán tử này áp dụng được cho gì

int    n;
int    a[5];
struct S { int x; } s;

&n           /* biến đơn */
&a[2]        /* một phần tử của mảng */
&a           /* cả mảng, kiểu khác hẳn, xem Bài 13.4 */
&s           /* một struct */
&s.x         /* một trường của struct */

Điều kiện chung là toán hạng phải là một vùng nhớ có tên, thuật ngữ chuẩn gọi là lvalue. Mục dưới liệt kê những thứ không thỏa điều kiện đó.

#Kiểu của kết quả

Đây là chi tiết quyết định. Kết quả của &x không phải là một số nguyên chung chung, mà là một con trỏ có kiểu cụ thể.

Khai báo biếnBiểu thứcKiểu của kết quả
int n;&nint *
double d;&ddouble *
char c;&cchar *
int a[5];&a[0]int *
int a[5];&aint (*)[5]
struct S s;&sstruct S *
int *p;&pint **
kieu.c
#include <stdio.h>

int main(void)
{
    int    n = 42;
    double d = 3.14;

    int    *pn = &n;      /* đúng: int * nhận int * */
    double *pd = &d;      /* đúng */

    int    *sai = &d;     /* cảnh báo: gán double * cho int * */

    return 0;
}
terminal
gcc -std=c17 -Wall kieu.c -o kieu
kieu.c:11:19: warning: initialization of 'int *' from incompatible pointer type 'double *' [-Wincompatible-pointer-types]

#Những gì không lấy địa chỉ được

ThứVí dụVì sao không lấy được
Hằng số&42Số 42 không nằm ở đâu cả, nó là một giá trị trong lệnh máy
Biểu thức&(a + b)Kết quả nằm trong thanh ghi, không có địa chỉ bộ nhớ
Biến register&r với register int rChuẩn cấm, vì biến đó có thể nằm hẳn trong thanh ghi
Trường bit của struct&s.coTrường bit không chiếm nguyên byte nên không có địa chỉ
Giá trị trả về của hàm&f()Kết quả lời gọi hàm không phải vùng nhớ có tên nên không có địa chỉ
&42;              /* error: lvalue required as unary '&' operand */
&(a + b);         /* lỗi tương tự */

register int r;
&r;               /* error: address of register variable requested */

struct { unsigned co : 1; } s;
&s.co;            /* error: cannot take address of bit-field */

#In địa chỉ đúng cách

Ba cách in sai
printf("%d\n", &n);           /* sai kiểu, và cắt cụt trên máy 64 bit */
printf("%p\n", &n);           /* thiếu ép kiểu, hành vi không xác định */
printf("%lu\n", &n);          /* cũng sai kiểu */
Cách đúng
printf("%p\n", (void *)&n);   /* cách chuẩn */

#include <inttypes.h>
printf("%" PRIxPTR "\n", (uintptr_t)&n);   /* khi cần in dạng số nguyên */
KiểuĐịnh dạngGhi chú
void *%pCách chuẩn, in dạng cài đặt tự chọn
uintptr_tPRIxPTR trong inttypes.hKiểu nguyên đủ rộng chứa một con trỏ, có từ C99
ptrdiff_t%tdHiệu hai con trỏ, dùng ở Bài 12.8
in-dia-chi.c
#include <inttypes.h>
#include <stdio.h>

int main(void)
{
    int n = 42;

    printf("dang chuan   : %p\n", (void *)&n);
    printf("dang so nguyen: %" PRIuPTR "\n", (uintptr_t)&n);
    printf("dang thap luc : 0x%" PRIxPTR "\n", (uintptr_t)&n);

    return 0;
}
terminal
./in-dia-chi
dang chuan   : 0x7ffd3a2c1e4c
dang so nguyen: 140725579423308
dang thap luc : 0x7ffd3a2c1e4c

#Ba chỗ dễ nhầm với toán tử khác

Nhầm 1: và một ngôi so với và hai ngôi

int a = 12, b = 10;

int *p = &a;         /* một ngôi: lấy địa chỉ */
int  c = a & b;      /* hai ngôi: phép và trên từng bit, cho ra 8 */

Trình biên dịch phân biệt bằng số toán hạng. Nếu có thứ gì đứng bên trái dấu & thì đó là phép toán bit, ngược lại là lấy địa chỉ.

Nhầm 2: và trong khai báo tham chiếu

int &r = a;      /* C++ có tham chiếu, C KHÔNG CÓ. Đây là lỗi cú pháp trong C. */

Nếu bạn đọc mã C++ hoặc tài liệu C++, dấu & trong khai báo có nghĩa hoàn toàn khác. C không có tham chiếu, và đó là lý do C bắt buộc dùng con trỏ ở những chỗ C++ dùng tham chiếu.

Nhầm 3: và và trong biểu thức logic

if (p != NULL && *p > 0)     /* && là và logic, hai dấu và */
if (a & b)                    /* & là và trên bit, một dấu */

Tự làm thử

  1. In địa chỉ của năm biến kiểu khác nhau bằng cả ba định dạng trong bảng.
  2. Cố tình quên dấu và trong scanf, đọc cảnh báo, rồi chạy và quan sát chương trình sập.
  3. Gán &d của một double vào int *, đọc cảnh báo, rồi ép kiểu và in giá trị nhận được.
  4. Thử lấy địa chỉ của cả năm thứ trong bảng không lấy được, chép lại thông báo lỗi của từng cái.
  5. In &a&a[0] của cùng một mảng, xác nhận giá trị giống nhau rồi in sizeof của cả hai và giải thích chênh lệch.
  6. Viết chương trình có cả bốn cách dùng dấu và trong bài kiểm tra nhỏ, in kết quả từng dòng.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Toán tử & một ngôi cho ra địa chỉ của một vùng nhớ có tên.
  • Nếu x có kiểu T thì &x có kiểu T *. Kiểu của con trỏ quyết định cách đọc vùng nhớ đó.
  • Hằng số, biểu thức, biến register và trường bit đều không lấy địa chỉ được, vì chúng không phải vùng nhớ có tên.
  • In địa chỉ bằng %p và luôn ép về (void *).
  • Dấu & có hai nghĩa tùy số toán hạng: một ngôi là lấy địa chỉ, hai ngôi là phép và trên bit.