01/10/2018, 00:28

Cách thay đổi biến const trong C

Xét đoạn code sau với 4 biến const:

  1. INIT_GLOBAL_VAR: phạm vi global, có khởi tạo giá trị
  2. UNINIT_GLOBAL_VAR: phạm vi global, không khởi tạo giá trị
  3. INIT_VAR: phạm vi local, có khởi tạo giá trị
  4. UNINIT_VAR: phạm vi local, không khởi tạo giá trị
#include <stdio.h>

const int INIT_GLOBAL_VAR = 123;
const int UNINIT_GLOBAL_VAR;

int main(void) {
    const int INIT_VAR = 456;
    const int UNINIT_VAR;
    int *p = NULL;
    
    /*
     * Cách 1
     */
    *(&INIT_GLOBAL_VAR) = 789; 
    INIT_GLOBAL_VAR = 789;
    *(&UNINIT_GLOBAL_VAR) = 789;
    UNINIT_GLOBAL_VAR = 789;
    *(&INIT_VAR) = 789;
    INIT_VAR = 789;
    *(&UNINIT_VAR) = 789;
    UNINIT_VAR = 789;
    
    /*
     * Cách 2
     */
    p = &UNINIT_GLOBAL_VAR; // compile warning: assignment discards ‘const’ qualifier from pointer target type
    *p = 789;
    p = &INIT_VAR; // compile warning: assignment discards ‘const’ qualifier from pointer target type
    *p = 789;
    p = &UNINIT_VAR; // compile warning: assignment discards ‘const’ qualifier from pointer target type
    *p = 789;
    p = &INIT_GLOBAL_VAR; // runtime error: Segmentation fault
    *p = 789;
    
    return 1;
}

Xét 2 cách thay đổi sau:

  1. Cách 1: thay đổi trực tiếp chúng ta sẽ gặp compile error: assignment of read-only variable, không cần giải thích nhiều vì điều này đã được quy định rõ trong C biến const không thể thay đổi được.
  2. Cách 2: dùng 1 con trỏ trung gian để thay đổi, con trỏ không nhất thiết phải là trỏ hằng. Cách này pass được compile error chỉ còn lại compile warning: assignment discards ‘const’ qualifier from pointer target type và 1 runtime error: Segment fault.
  • INIT_VARUNINIT_VAR thay đổi được là vì đây là 2 biến local khi hàm main chạy thì chúng mới được khởi tạo và khởi tạo trên vùng stack (RAM region)
  • Biến ‘UNINIT_GLOBAL_VAR’ tuy là biến global nhưng do không được được khởii tạo giá trị nên nó thuộc về vùng .bss mà đây là vùng nhớ modify được, phân tích map file sẽ thấy:
.bss            0x000000000060103c        0xc
 *(.dynbss)
 .dynbss        0x0000000000000000        0x0 /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o
 *(.bss .bss.* .gnu.linkonce.b.*)
 .bss           0x000000000060103c        0x0 /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o
 .bss           0x000000000060103c        0x0 /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crti.o
 .bss           0x000000000060103c        0x1 /usr/lib/gcc/x86_64-linux-gnu/4.8/crtbegin.o
 .bss           0x000000000060103d        0x0 /tmp/ccTcj2bs.o
 .bss           0x000000000060103d        0x0 /usr/lib/x86_64-linux-gnu/libc_nonshared.a(elf-init.oS)
 .bss           0x000000000060103d        0x0 /usr/lib/gcc/x86_64-linux-gnu/4.8/crtend.o
 .bss           0x000000000060103d        0x0 /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crtn.o
 *(COMMON)
 *fill*         0x000000000060103d        0x3 
 COMMON         0x0000000000601040        0x4 /tmp/ccTcj2bs.o
                0x0000000000601040                UNINIT_GLOBAL_VAR
                0x0000000000601048                . = ALIGN ((. != 0x0)?0x8:0x1)
 *fill*         0x0000000000601044        0x4 
  • Biến INIT_GLOBAL_VAR là biến global và có khởi tạo giá trị nên nó được xếp vào vùng rodata, đây là vùng nhớ đặt biệt được protect bởi hệ thống nên nếu có gắng thay đổi vùng nhớ này bằng cách nào đó thì hệ thống sẽ quăng exception Segment fault như trên và dừng chương trình, phân tích map file sẽ thấy:
.rodata         0x00000000004005e0        0x8
 *(.rodata .rodata.* .gnu.linkonce.r.*)
 .rodata.cst4   0x00000000004005e0        0x4 /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../x86_64-linux-gnu/crt1.o
                0x00000000004005e0                _IO_stdin_used
 .rodata        0x00000000004005e4        0x4 /tmp/ccTcj2bs.o
                0x00000000004005e4                INIT_GLOBAL_VAR

Kết luận

  • Từ khóa const có 2 tác dụng, giúp compiler kiểm tra syntax khi build và báo cho compiler biết nên đặt biến nào phải đặt vào vùng read-only data.
  • Nếu 1 biến toàn cục không được khởi tạo giá trị lúc khai báo thì nó luôn được đặt trong vùng .bss bất chấp có từ khóa const khi khai báo, lúc này const chỉ còn tác dụng kiểm tra syntax.
  • Nếu bạn vẫn muốn ngoan cố thay đổi biến global const có khởi tạo giá trị, tham khảo thêm cách dùng __attribute__ để ép compiler đặt biến đó vào vùng nhớ khác không phải là read-only data, nhưng đến đây thì đã đi xa vấn đề ban đầu quá rồi
  • Việc vô tình hay cố ý thay đổi biến const đều không được recomment vì nó tạo ra những undefined behaviour như trong ví dụ trên là Segment fault

P/S: trong bài viết, mình dùng máy ảo của cloud9.io để test code,tùy vào từng platform và compiler mà vị trí đặt các biến ở những vùng nhớ sẽ khác nhau, bên dưới là cấu hình hệ thống và thông tin compiler mình dùng trong bài test

>> ddt_1793:~/workspace (master) $ uname -a # system information
Linux ddt_1793-test_machine-3790314 4.2.0-c9 #1 SMP Fri Nov 20 14:49:01 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
>> ddt_1793:~/workspace (master) $ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Tuấn viết 02:35 ngày 01/10/2018

Rất hay! Cảm ơn bạn đã chia sẻ!

Bài liên quan
0