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

const với con trỏ

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

  • Phân biệt bốn tổ hợp const với con trỏ
  • Đọc khai báo const bằng quy tắc từ phải sang trái
  • Chọn đúng dạng const cho tham số hàm
  • Hiểu vì sao const không phải bảo đảm tuyệt đối

Từ khóa const với con trỏ gây nhầm lẫn vì có hai thứ có thể là hằng: bản thân con trỏ, và dữ liệu nó trỏ tới. Bài cuối của Phần 5 dạy bạn phân biệt chúng bằng một quy tắc đọc duy nhất.

#Bốn tổ hợp

int              *a;      /* 1. con trỏ ghi được, dữ liệu ghi được */
const int        *b;      /* 2. con trỏ ghi được, dữ liệu CHỈ ĐỌC */
int const        *c;      /* 2. giống hệt b */
int       *const d;      /* 3. con trỏ CHỈ ĐỌC, dữ liệu ghi được */
const int *const e;      /* 4. cả hai đều chỉ đọc */
Khai báoĐổi p đượcĐổi *p đượcTên gọi thông dụng
int *pCon trỏ thường
const int *pKhôngCon trỏ tới hằng
int *const pKhôngCon trỏ hằng
const int *const pKhôngKhôngCon trỏ hằng tới hằng
bon-to-hop.c
int x = 1, y = 2;

/* 1 */
int *a = &x;
*a = 10;      /* hợp lệ */
a  = &y;      /* hợp lệ */

/* 2 */
const int *b = &x;
*b = 10;      /* LỖI: assignment of read-only location */
b  = &y;      /* hợp lệ */

/* 3 */
int *const c = &x;
*c = 10;      /* hợp lệ */
c  = &y;      /* LỖI: assignment of read-only variable */

/* 4 */
const int *const d = &x;
*d = 10;      /* LỖI */
d  = &y;      /* LỖI */

#Quy tắc đọc từ phải sang trái

Quy tắc
Đọc khai báo từ phải sang trái, bắt đầu từ tên biến. Từ khóa const áp lên thứ đứng ngay bên trái nó, trừ khi nó đứng ở đầu thì áp lên thứ bên phải.
const int *p;

/* Đọc: p là con trỏ tới int hằng
   const đứng đầu nên áp lên int bên phải */

int const *p;

/* Đọc: p là con trỏ tới int hằng
   const áp lên int bên trái. Cùng nghĩa với dòng trên. */

int *const p;

/* Đọc: p là con trỏ hằng tới int
   const áp lên dấu sao bên trái, tức lên chính con trỏ */

const int *const p;

/* Đọc: p là con trỏ hằng tới int hằng */

Với nhiều tầng con trỏ

Khai báoĐọc là
const char **pp là con trỏ tới con trỏ tới char hằng
char *const *pp là con trỏ tới con trỏ hằng tới char
char **const pp là con trỏ hằng tới con trỏ tới char
const char *const *const pcả ba tầng đều hằng
/* Ví dụ thực tế: hàm so sánh cho qsort trên mảng chuỗi */
static int so_sanh(const void *x, const void *y)
{
    const char *const *a = x;      /* con trỏ tới con trỏ hằng tới char hằng */

    return strcmp(*a, *(const char *const *)y);
}

#Chọn đúng dạng cho tham số hàm

Hàm làm gìNên khai báoVí dụ trong thư viện chuẩn
Chỉ đọc dữ liệuconst T *psize_t strlen(const char *s)
Ghi vào dữ liệuT *pchar *strcpy(char *d, const char *s)
Đổi con trỏ của người gọiT **pplong strtol(const char *, char **het, int)
Chỉ đọc mảng con trỏconst T *const *phiếm gặp, nhưng đúng nhất về mặt ngữ nghĩa
Thiếu const
size_t do_dai(char *s)
{
    size_t n = 0;

    while (s[n] != '\0') ++n;

    return n;
}

do_dai("abc");      /* biên dịch qua nhưng ngữ nghĩa sai, và có cảnh báo
                       nếu bật -Wwrite-strings */
Có const
size_t do_dai(const char *s)
{
    size_t n = 0;

    while (s[n] != '\0') ++n;

    return n;
}

do_dai("abc");      /* đúng hoàn toàn */

char bo_dem[16] = "abc";

do_dai(bo_dem);     /* cũng đúng: T sao chuyển được sang const T sao */

#Quy tắc chuyển đổi

int       x = 1;
int      *p = &x;
const int *q;

q = p;      /* HỢP LỆ: thêm const luôn được */
p = q;      /* LỖI: bỏ const cần ép kiểu rõ ràng */

p = (int *)q;   /* biên dịch qua, nhưng bạn đang tự chịu trách nhiệm */
Chuyển từSangĐược không
int *const int *Được, tự động
const int *int *Không, cần ép kiểu
int **const int **KHÔNG, dù trông có vẻ hợp lý
int **const int *const *Cũng KHÔNG trong C, dù an toàn. Chỉ C++ mới cho

Khi nào ép bỏ const là chấp nhận được

/* Trường hợp hợp lệ: hàm thư viện chuẩn nhận const nhưng trả về không const. */
char *strchr(const char *s, int c);

const char *s = "abc";
char       *p = strchr(s, 'b');      /* p ghi được dù s là const */

*p = 'X';      /* Nếu s thật sự trỏ vào hằng chuỗi thì đây là lỗi. */

#const không phải bảo đảm tuyệt đối

gioi-han.c
#include <stdio.h>

int main(void)
{
    int        x = 42;
    const int *p = &x;

    printf("%d\n", *p);      /* 42 */

    x = 100;                  /* hợp lệ: x không phải const, chỉ p là const */

    printf("%d\n", *p);      /* 100, giá trị đã đổi qua đường khác */

    return 0;
}
Tình huốngGhi qua con trỏ đã ép bỏ const
Đối tượng gốc không phải constHợp lệ, dù xấu về mặt phong cách
Đối tượng gốc là constHành vi không xác định
Hằng chuỗiHành vi không xác định, thường làm chương trình sập

Bảng tổng kết Phần 5

Khái niệmKý hiệuNhớ gì
Lấy địa chỉ&xCho ra T sao nếu x có kiểu T
Giải tham chiếu*pCùng vùng nhớ với biến gốc
Con trỏ tới con trỏint **Dùng khi cần sửa con trỏ của người gọi
Số học con trỏp + 1Nhảy theo phần tử, không theo byte
Hiệu hai con trỏp - qCho ra ptrdiff_t, in bằng %td
Phân rã mảnga thành &a[0]Trừ ba chỗ: sizeof, dấu và, khởi tạo chuỗi
Con trỏ hàmint (*f)(int)Ngoặc bắt buộc, dùng typedef
Con trỏ voidvoid *Không giải tham chiếu, không số học
const trái saoconst int *pDữ liệu chỉ đọc
const phải saoint *const pCon trỏ chỉ đọc

Tự làm thử

  1. Viết cả bốn tổ hợp const, thử gán vào con trỏ và vào dữ liệu, ghi lại bốn thông báo lỗi.
  2. Đọc bảy khai báo const nhiều tầng trong bài bằng quy tắc từ phải sang trái.
  3. Viết typedef int *ConTroInt rồi thêm const, chứng minh nó tạo ra con trỏ hằng chứ không phải con trỏ tới hằng.
  4. Thử gán int ** cho const int **, đọc cảnh báo, rồi sửa thành const int *const *.
  5. Tái hiện ví dụ ghi vào biến const qua con trỏ đã ép kiểu, chạy với -O0-O2 rồi so kết quả.
  6. Rà một chương trình cũ của bạn, thêm const vào mọi tham số con trỏ chỉ đọc và xác nhận vẫn biên dịch được.
  7. Viết hàm nhận const char * và trả về const char *, so với cách strchr làm và giải thích khác biệt.

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

  • const bên trái dấu sao bảo vệ dữ liệu, bên phải dấu sao bảo vệ con trỏ.
  • Dùng const T * cho mọi tham số con trỏ mà hàm chỉ đọc. Đó vừa là tài liệu vừa là thứ trình biên dịch kiểm tra.
  • Thêm const luôn được, bỏ const cần ép kiểu rõ ràng. int ** không chuyển sang const int ** được.
  • const nói về quyền truy cập qua một con trỏ cụ thể, không nói rằng đối tượng bất biến.
  • Ghi vào một đối tượng thật sự khai báo const là hành vi không xác định, và trình biên dịch tối ưu sẽ cho ra kết quả bất ngờ.