const

이오싶·2025년 12월 21일

변수에서 const

값 변수에서

void const_variable() {
    const int a = 10;
    int const b = 20;

    // a = 15; // 오류: const 변수는 수정할 수 없음
    // b = 25; // 오류: const 변수는 수정할 수 없음
    std::cout << "a: " << a << ", b: " << b << std::endl;
}

위 경우 const intint const의 역할은 사실상 동일하다. 변수를 수정 불가한 상수로 정의하는 것.

class HasConst {
public:
    HasConst(int value);
private:
    const int constMember; // const 멤버 변수
};

HasConst::HasConst(int value) : constMember(value) {
    // constMember = value; // 오류: const 멤버 변수는 초기화 이후 수정할 수 없음
}

한편 const로 정의된 멤버 변수는 생성자의 초기화 리스트를 통해서만 초기화할 수 있다는 특징을 가진다. 즉 다른 멤버 메서드를 통한 변경이 불가할 뿐 아니라 생성자의 body에서도 초기화가 불가능하다.

포인터에서

포인터에서는 조금 복잡해진다.

void const_pointer() {
    int value1 = 10;
    int value2 = 20;

    const int *ptr1 = &value1; // 가리키는 값이 const
    int const *ptr2 = &value1; // 가리키는 값이 const (위와 동일)
    int* const ptr3 = &value1; // 포인터 자체가 const
    const int* const ptr4 = &value1; // 포인터와 가리키는 값 모두 const

    // *ptr1 = 15; // 오류: 가리키는 값이 const
    ptr1 = &value2; // 가능: 포인터 자체는 const 아님
    // *ptr2 = 25; // 오류: 가리키는 값이 const
    ptr2 = &value2; // 가능: 포인터 자체는 const 아님
    *ptr3 = 35; // 가능: 가리키는 값은 const 아님
    // ptr3 = &value2; // 오류: 포인터 자체가 const
    // *ptr4 = 45; // 오류: 가리키는 값이 const

    std::cout << "value1: " << value1 << ", value2: " << value2 << std::endl;
}

일단 const int*int const*const intint const의 경우처럼 동일한 동작을 가진다. 다만, int* const의 경우 다른 의미를 가진다.

  • const int*: 포인터가 가리키는 값이 const; 가리키는 값을 수정할 수 없으나 포인터가 가리키는 주소(즉 ptr1의 값) 자체는 수정 가능하다.
  • int* const: 포인터가 const; 포인터가 가리키는 주소(ptr3의 값)은 수정 불가하나 가리키는 값은 수정 가능하다.
  • const int* const: 포인터의 주소값도, 가리키는 값도 수정 불가하다.

다중 포인터의 const

다중 포인터의 경우 의미가 조금 더 명확해진다.

void const_multiple_pointer() {
    int value = 10;

    const int *ptr = &value;
    const int **ptr1 = &ptr; // int가 const인 이중 포인터
    int const **ptr1b = &ptr; // int가 const인 이중 포인터 (위와 동일)

    int* const ptr_a = &value;
    int* const *ptr2 = &ptr_a; // int*가 const인 이중 포인터

    int* ptr_b = &value;
    int** const ptr3 = &ptr_b; // int**가 const인 이중 포인터

    int new_value = 20;
    const int* new_ptr = &new_value;
    int* const new_ptr_a = &new_value;
    int* new_ptr_b = &new_value;

    // **ptr1 = 15; // 오류: int가 const
    *ptr1 = &new_value; // 가능: int*는 const 아님
    ptr1 = &new_ptr; // 가능: int**는 const 아님
    
    **ptr2 = 25; // 가능: int는 const 아님
    // *ptr2 = &new_value; // 오류: int*가 const
    ptr2 = &new_ptr_a; // 가능: int**는 const 아님

    **ptr3 = 35; // 가능: int는 const 아님
    *ptr3 = &new_value; // 가능: int*는 const 아님
    // ptr3 = &new_ptr_b; // 오류: int**가 const
}

int const**부터 비교한다면, const 키워드의 앞 부분에 해당하는 타입을 const로 보호한다고 하면 이해하기 편할 듯 하다.


    int* const ** trp2 = &ptr2;

    trp2 = &ptr2; // int*** 수정 가능
    *trp2 = &ptr_a; // int** 수정 가능
    // **trp2 = &new_value; // int* 수정 불가
    ***trp2 = 50; // int 수정 가능

const 다중 포인터 간 형변환

한편, C++ 표준은 다중 const 포인터를 선언하고 초기화할 시 타입 안전성을 보장하기 위해 int**const int**int**int* const *의 암묵적 변환을 금지한다. 따라서 다중 포인터를 초기화할 때는 대입되는 포인터 주소의 타입을 주의하여야 한다.

  • const int** / int const**: intconst
    int *ptr_mut = &value;    // &ptr_mut는 int**; const int**로 암묵적 변환 불가
    const int *ptr = &value;  // &ptr는 (const int*)*
    const int **ptr1 = &ptr;
    int const **ptr1b = &ptr;
  • int* const*: int*const
    int *ptr_mut = &value;    // &ptr_mut는 int**; int* const*로 암묵적 변환 불가
    int* const *ptr = &value; // &ptr는 (int* const)*
    int* const* ptr2 = &ptr;
  • int** const: int**const
    int *ptr = &value;    // &ptr는 (int*)*; int** const로 암묵적 변환 가능
    int** const ptr3 = &ptr;

참조(레퍼런스)에서

참조에서 const는 포인터의 경우처럼 다양한 위치에 가진 않는다.

void const_reference() {
    int value = 10;
    int& ref = value; // 일반 참조
    const int& ref1 = value; // 가리키는 값이 const인 참조
    int const& ref2 = value; // 가리키는 값이 const인 참조 (위와 동일)
    // int & const ref3 = value; // 참조 자체가 const (의미 없음, 컴파일러에 따라 컴파일 불가)

    ref = 15; // 가능: 일반 참조이므로 값 수정 가능
    // ref1 = 15; // 오류: 가리키는 값이 const
    // ref2 = 20; // 오류: 가리키는 값이 const

    std::cout << "value: " << value << std::endl;
}

const int&, int const& 모두 참조를 가진 값의 변경을 금지하는 것을 의미한다. int& const는 참조 자체가 const라는 의미가 되겠으나, 참조 자체가 기존 변수와 동일한 메모리 주소를 가리키는 변수, 즉 alias를 나타내고 자체로 어떤 값을 가지지 않으므로 의미가 없다.

컴파일러에 따라 T& const 따위의 정의는 아예 막아버리기도 한다.

함수에서 const

const 반환형

함수 왼쪽에 const가 위치하는 경우 해당 함수의 반환형이 const임을 의미하며, 상응하는 타입을 가진 변수에 대입할 수 있다.

const int return_const_value() {
    return 100;
}

const int* return_const_pointer() {
    int* ptr = new int(150);
    return ptr;
}

int* const return_const_pointer_2() {
    int* ptr = new int(200);
    return ptr;
}

void test_return_const() {
    const int val = return_const_value();
    std::cout << "Returned const value: " << val << std::endl;
    // val = 200; // 오류: const 변수는 수정할 수 없음
    
    const int* ptr = return_const_pointer();
    std::cout << "Returned const pointer value: " << *ptr << std::endl;
    // *ptr = 250; // 오류: 가리키는 값이 const
    
    int* const ptr2 = return_const_pointer_2();
    std::cout << "Returned const pointer 2 value: " << *ptr2 << std::endl;
    *ptr2 = 300; // 가능: 가리키는 값은 const 아님
    // ptr2 = nullptr; // 오류: 포인터 자체가 const
    delete ptr;
}

반환형에 const가 제일 많이 쓰이는 곳은 반환값이 참조인 경우인데, 다음 예시를 보자.

int& HasConst::getMemberReference() {
    return regularMember;
}

const int& HasConst::getMemberConstReference() {
    return regularMember2;
}

void test_ReturnMemberReference() {
    HasConst obj(10, 20, 30, 0);  // regularMember = 20, regularMember2 = 30
    std::cout << "Const Ref: " << obj.getMemberConstReference() << std::endl;
    
    obj.getMemberReference() = 40; // const가 아닌 참조 반환형은 수정 가능; 위험!
    // obj.getMember() = 30; // 참조 반환형을 const로 선언함으로서 수정 불가하게 방지
    std::cout << "Member after modification: " << obj.getMemberReference() << std::endl;
}
  • 실행 결과
    Const Ref: 30
    Member after modification: 40

함수 스택 종료 시 자동 반환되는 지역 변수의 ref를 반환하는 것은 금기인 것이 당연하다. 따라서 ref를 반환하는 경우는 대부분 메서드에서 값을 복사하기 부담스러운 필드의 참조를 반환하는 경우이다.

그러나 반환되는 참조가 const가 아닌 경우, 반환된 참조를 수정하는 것이 가능해져버린다. 그렇게 되면 객체 외부에서 필드의 값을 변경할 수 있게 되어버리므로 캡슐화에 실패한 코드가 되어버리는 것.

따라서 필드의 참조를 반환하는 경우 const 참조의 형태로 반환하여 외부에서의 수정을 방지하면서 복사 비용도 아낄 수가 있게 되는 것이다.

const 매개변수

매개변수 또한 const로 선언하는 것이 가능하다.

void getConstRef(const int& value) {
    // value += 3; const 참조값 변경 불가
    int newValue = value + 3;
    std::cout << newValue << std::endl;
}

const 함수(메서드)

void Method() const;

메서드가 객체의 멤버를 변경하지 않음(mutable 제외)을 말한다.

class HasConst {
public:
    HasConst(int constValue, int regularValue, int regularValue2, int mutableValue);

    int& getMemberReference();
    const int& getMemberConstReference();

    void constMethod() const;

private:
    const int constMember; // const 멤버 변수
    int regularMember;
    int regularMember2;
    int mutable mutableMember; // mutable 멤버 변수
};

void HasConst::constMethod() const {
    int temp = regularMember + 3;
    // regularMember2 += 10; // 멤버 변경 불가
    // int temp2 = getMemberReference();  // const가 아닌 메서드 호출 불가

    mutableMember += 1; // mutable 멤버 수정 가능
}

const로 정의된 메서드는 객체가 const로 선언되었을 때 해당 객체에서 호출될 수 있다.

void callConstMethod() {
    const HasConst instance(0, 1, 2, 3);

    // auto a = instance.getMemberReference(); // const가 아닌 메서드 호출 불가
    instance.constMethod();
}
profile
Iosif2510

0개의 댓글