extern const trong C++ và các khái niệm liên quan

Liên kết const giữa nhiều file

Trong C++, khi cần chia sẻ biến const giữa các file, cần hiểu rõ cơ chế liên kết (linkage). Mặc định, biến const có liên kết nội bộ (internal linkage), nghĩa là chỉ visible trong file định nghĩa nó. Để chia sẻ xuyên file, ta dùng từ khóa extern để thay đổi thành liên kết ngoại (external linkage).

Cơ chế thực tế: trình biên dịch thường thay thế trực tiếp giá trị const tại chỗ sử dụng (constant folding). Khi chia sẻ xuyên file, việc khởi tạo ở nhiều nơi sẽ gây xung đột nếu không dùng extern đúng cách.

Ví dụ đúng: định nghĩa một nơi, khai báo nhiều nơi

// config.cpp - file nguồn duy nhất chứa định nghĩa
extern const int maxConnections = 100;

// config.hpp - file header để include
extern const int maxConnections;  // chỉ khai báo, không khởi tạo

Lỗi thường gặp: thiếu extern trong file định nghĩa

// main.cpp
#include <iostream>
extern const int threshold;  // khai báo từ file khác

int main() {
    std::cout << threshold;
    return 0;
}

// utils.cpp
const int threshold = 50;  // ❌ LỖI: thiếu extern, biến chỉ visible trong utils.cpp

Sửa lại utils.cpp:

// utils.cpp
extern const int threshold = 50;  // ✅ Đúng: extern đặt trước, visible toàn cục

Thứ tự khai báo quan trọng

// app.cpp
extern const int version;      // (1) Khai báo external trước
const int version = 1;       // (2) Định nghĩa sau → HỢP LỆ

// hoặc ngược lại cũng được trong cùng file:
const int version = 1;       // Định nghĩa trước
extern const int version;      // Khai báo sau (redundant nhưng hợp lệ)

Nếu đảo ngược thứ tự không đúng giữa các file hoặc định nghĩa trùng lặp, linker sẽ báo lỗi "multiply defined symbols".

Tham chiếu hằng (const reference)

Tham chiếu đến đối tượng const phải là tham chiếu hằng:

const int limit = 100;
const int& refLimit = limit;   // ✅ Hợp lệ
int& badRef = limit;           // ❌ Lỗi: bỏ qua const qualifier

Đặc tính đặc biệt của const reference

Khác với tham chiếu thường, const T& chấp nhận:

  • Chuyển đổi kiểu ngầm định
  • Biểu thức tạm (rvalue)
double rate = 3.14;
const int& intRef = rate;      // Tạo temporary int = 3, intRef bind đến temporary
std::cout << intRef;            // In: 3

const double& exprRef = rate * 2;  // Bind đến kết quả temporary
std::cout << exprRef;          // In: 6.28

Quan trọng: const reference có thể bind đến đối tượng non-const, nhưng không thể dùng reference để thay đổi giá trị:

double mutableVal = 1.5;
const double& constRef = mutableVal;

std::cout << constRef;   // 1.5
mutableVal = 2.5;        // Thay đổi qua bản gốc
std::cout << constRef;   // 2.5 (phản ánh thay đổi)

Con trỏ và const - hai khái niệm phân biệt

1. Con trỏ đến hằng (pointer to const)

Địa chỉ trỏ đến có thể thay đổi, nhưng giá trị trỏ đến không đổi:

const double pi = 3.14159;
const double* ptrToConst = &pi;   // ✅ Hợp lệ

double* normalPtr = &pi;          // ❌ Lỗi: discarding const qualifier

double regular = 2.5;
const double* flexible = &regular;  // ✅ Hợp lệ: non-const → const*

// *flexible = 3.0;  // ❌ Lỗi: không thể modify qua const*
regular = 3.0;       // ✅ OK: modify trực tiếp

2. Con trỏ hằng (const pointer)

Địa chỉ lưu trong con trỏ cố định, nhưng giá trị trỏ đến có thể đổi:

int alpha = 10, beta = 20;
int* const fixedPtr = &alpha;   // Con trỏ hằng, luôn trỏ đến alpha

// fixedPtr = &beta;  // ❌ Lỗi: cannot reseat const pointer
*fixedPtr = 15;       // ✅ OK: giá trị trỏ đến thay đổi được

3. Con trỏ hằng đến hằng

const int* const locked = &alpha;  // Cả địa chỉ và giá trị đều không đổi

Phân biệt top-level const và low-level const

Khái niệmÁp dụng choÝ nghĩa
Top-level constBản thân đối tượng/con trỏĐối tượng không thể thay đổi
Low-level constĐối tượng được trỏ đếnDữ liệu trỏ đến không đổi
int value = 100;

// Top-level const: p1 là hằng, không thể trỏ chỗ khác
int* const p1 = &value;  

// Low-level const: *p2 là hằng, không thể đổi giá trị
const int* p2 = &value;
int const* p3 = &value;   // Tương đương p2

// Cả hai: p4 hằng và *p4 cũng hằng
const int* const p4 = &value;

Quy tắc đọc: Từ phải sang trái. int* const → "const pointer to int". const int* → "pointer to const int".

Quy tắc chuyển đổi khi copy: Có thể thêm low-level const (non-const → const), nhưng không thể bỏ.

constexpr và biểu thức hằng

Biểu thức hằng (constant expression): Giá trị xác định tại compile-time, không đổi ở runtime.

constexpr: Bảo đảm biến/hàm là constant expression, compiler kiểm tra tại compile-time.

Sự khác biệt const vs constexpr

int dynamic = 5;

// const: chỉ read-only, có thể phụ thuộc runtime
const int& refDyn = dynamic;      // dynamic có thể đổi, refDyn phản ánh thay đổi

// constexpr: compile-time evaluation bắt buộc
constexpr int compileVal = 10;      // ✅ Literal
// constexpr int bad = dynamic;     // ❌ Lỗi: không phải constant expression

constexpr với con trỏ

constexpr áp dụng cho bản thân con trỏ (top-level), không phải điểm trỏ đến:

constexpr int* nullPtr = nullptr;   // Con trỏ hằng, tương đương: int* const

// Để có pointer to const với constexpr:
constexpr const int* ptrToConstInt = nullptr;  // const int* const

Cách viết tên kiểu thay thế

typedef (C-style)

typedef unsigned long ulong;
typedef ulong* ulongPtr;           // ulongPtr là con trỏ đến ulong

typedef char* charPtr;
const charPtr constStr = nullptr;  // char* const, KHÔNG PHẢI const char*

using alias (C++11)

using ulong = unsigned long;
using ulongPtr = ulong*;

auto: suy luận kiểu tự động

const int magic = 42;
auto inferred = magic;        // int (bỏ qua top-level const)
const auto preserved = magic; // const int

auto& refInferred = magic;    // const int& (giữ const vì reference)

Lưu ý: auto bỏ qua top-level const. Dùng const auto hoặc auto& để giữ.

decltype: lấy kiểu từ biểu thức

Khác auto, decltype giữ nguyên top-level const và reference:

int x = 0;
const int cx = 0;

decltype(x)   a;   // int
decltype(cx)  b = 1;  // const int, phải khởi tạo
decltype((x)) c = x;  // int& (vì (x) là lvalue expression)

Quy tắc đặc biệt: decltype((variable)) luôn cho reference type.

Thẻ: cpp const extern constexpr decltype

Đăng vào ngày 9 tháng 8 lúc 07:45