C++의 static 및 const 키워드 활용

C++ 프로그래밍에서 staticconst 키워드는 변수, 함수, 그리고 클래스 멤버의 동작 방식을 제어하는 데 핵심적인 역할을 합니다. 이 두 키워드는 각각 저장 방식 및 가시성, 그리고 불변성이라는 중요한 개념을 나타내며, 코드의 효율성과 안정성을 높이는 데 기여합니다.

static 키워드 이해하기

static 키워드는 주로 변수의 저장 수명(storage duration)과 링크(linkage, 가시성)를 제어하는 데 사용됩니다. 이는 프로그램 내에서 객체의 생성 및 소멸 시점, 그리고 접근 범위를 결정합니다.

1. 정적 지역 변수 (Static Local Variables)

함수나 코드 블록 내에서 static으로 선언된 지역 변수는 해당 블록 내에서만 접근할 수 있지만, 프로그램 전체의 실행 기간 동안 메모리에 유지됩니다. 즉, 함수가 종료되어도 값은 사라지지 않고 다음 호출 시에도 이전 값이 유지됩니다.

  • 초기화: 사용자가 명시적으로 정의하며, 프로그램이 시작될 때 또는 해당 변수가 포함된 코드가 처음 실행될 때 단 한 번 초기화됩니다.
  • 스코프(유효 범위): 선언된 함수 또는 블록 내부.
  • 생명 주기: 프로그램 시작부터 종료까지.
  • 저장 영역: 정적 데이터 영역.

다음 예제는 정적 지역 변수의 동작을 보여줍니다.


#include <iostream>

void processCounter() {
    static int callCount = 0; // 이 변수는 첫 호출 시에만 0으로 초기화됩니다.
    int tempCount = 0;        // 이 변수는 매 호출마다 0으로 초기화됩니다.
    
    callCount++;
    tempCount++;
    
    std::cout << "정적 카운트: " << callCount << ", 임시 카운트: " << tempCount << std::endl;
}

int main() {
    processCounter(); // 출력: 정적 카운트: 1, 임시 카운트: 1
    processCounter(); // 출력: 정적 카운트: 2, 임시 카운트: 1
    processCounter(); // 출력: 정적 카운트: 3, 임시 카운트: 1
    return 0;
}

클래스의 정적 멤버 함수 내에 정적 지역 변수를 사용하여 싱글톤(Singleton) 패턴을 구현하는 것도 가능합니다. 이 경우, 객체는 함수가 처음 호출될 때만 생성됩니다.


#include <iostream>
#include <string>

class Logger {
private:
    // 생성자를 private으로 만들어 외부에서 직접 객체 생성을 막습니다.
    Logger() { std::cout << "Logger 인스턴스 생성됨." << std::endl; }
    // 복사 및 할당을 비활성화하여 단일 인스턴스를 보장합니다.
    Logger(const Logger&) = delete; 
    Logger& operator=(const Logger&) = delete; 

public:
    static Logger& getInstance() {
        // 이 정적 지역 변수는 첫 호출 시에만 초기화됩니다.
        static Logger instance; 
        return instance;
    }

    void writeLog(const std::string& message) {
        std::cout << "[LOG] " << message << std::endl;
    }
};

int main() {
    Logger& logger1 = Logger::getInstance(); // 첫 호출, 인스턴스 생성
    logger1.writeLog("애플리케이션 시작.");

    Logger& logger2 = Logger::getInstance(); // 두 번째 호출, 기존 인스턴스 반환
    logger2.writeLog("데이터 처리 중.");

    // logger1과 logger2는 동일한 Logger 객체를 참조합니다.
    return 0;
}

2. 정적 전역 변수 (Static Global Variables)

전역 스코프에서 static으로 선언된 변수는 해당 소스 파일 내에서만 접근 가능하며, 다른 파일에서는 접근할 수 없습니다. 이는 이름 충돌을 방지하고 특정 변수의 가시성을 제한하여 모듈성을 높이는 데 유용합니다.

  • 초기화: 사용자가 명시적으로 정의하며, 프로그램 시작 시 초기화됩니다.
  • 스코프(유효 범위): 선언된 파일 내부 (내부 링크).
  • 생명 주기: 프로그램 시작부터 종료까지.
  • 저장 영역: 정적 데이터 영역.

3. 파일 스코프 함수 (File-scope Functions)

C++에서는 전역 함수 앞에 static을 붙여 내부 링크(internal linkage)를 가질 수 있도록 할 수 있습니다. 이는 해당 함수가 선언된 소스 파일 내에서만 호출될 수 있도록 제한하며, 외부 파일에서 동일한 이름의 함수를 선언해도 충돌하지 않습니다.


// file_utility.cpp
#include <iostream>

// 이 함수는 file_utility.cpp 내에서만 유효합니다.
static void performInternalCalculation() {
    std::cout << "내부 파일 계산을 수행합니다." << std::endl;
}

void publicUtilityFunction() {
    performInternalCalculation(); // 같은 파일 내에서는 호출 가능
    std::cout << "공용 유틸리티 함수 실행." << std::endl;
}

// main.cpp (performInternalCalculation() 호출 불가)
// #include "file_utility.h" (가정)
// extern void performInternalCalculation(); // 컴파일 오류 또는 링커 오류 발생
// void appLogic() {
//     performInternalCalculation(); 
// }

4. 정적 클래스 멤버 변수 (Static Class Member Variables)

static으로 선언된 클래스 멤버 변수는 해당 클래스의 모든 객체가 공유하는 단 하나의 복사본만 존재합니다. 이 변수는 특정 객체에 속하지 않고 클래스 자체에 속합니다.

  • 선언: 클래스 내부에서 선언하고, 정의는 반드시 클래스 외부에서 이루어져야 합니다. 이때 초기화할 수 있습니다.
  • 공유: 모든 객체가 하나의 값을 공유하며, 클래스 이름(ClassName::staticVar)으로 직접 접근하거나 객체(object.staticVar)를 통해 접근할 수 있습니다.

#include <iostream>
#include <string>

class InventoryItem {
public:
    static int totalInventoryCount; // 정적 멤버 변수 선언

    InventoryItem(const std::string& name) : itemName(name) {
        totalInventoryCount++; // 객체가 생성될 때마다 총 개수 증가
    }
    
    std::string getItemName() const { return itemName; }

private:
    std::string itemName;
};

// 정적 멤버 변수 정의 및 초기화 (클래스 외부에서만 가능)
int InventoryItem::totalInventoryCount = 0; 

int main() {
    InventoryItem laptop("노트북");
    InventoryItem monitor("모니터");
    InventoryItem keyboard("키보드");

    std::cout << "총 재고 품목 수: " << InventoryItem::totalInventoryCount << std::endl; // 출력: 3
    
    // 정적 멤버는 객체를 통해서도 접근 가능하지만, 클래스 이름을 통한 접근이 권장됩니다.
    laptop.totalInventoryCount = 5; // 모든 객체가 공유하는 값이 변경됩니다.
    std::cout << "모니터 재고 수: " << monitor.totalInventoryCount << std::endl; // 출력: 5

    return 0;
}

5. 정적 클래스 멤버 함수 (Static Class Member Functions)

static 멤버 함수는 특정 클래스 객체에 묶이지 않습니다. 따라서 this 포인터가 없으며, 비정적(non-static) 멤버 변수나 함수에 직접 접근할 수 없습니다. 오직 static 멤버 변수와 다른 static 멤버 함수에만 접근할 수 있습니다.

  • this 포인터 없음: 객체의 특정 인스턴스 상태에 의존하지 않습니다.
  • 비정적 멤버 접근 불가: 비정적 멤버는 특정 객체 인스턴스와 연결되어야 하므로 직접 접근할 수 없습니다.
  • 가상 함수 불가: static 함수는 오버라이딩될 수 없으므로 virtual 키워드를 사용할 수 없습니다.

#include <iostream>

class TaskManager {
private:
    static int completedTasksCount; // 정적 멤버 변수: 완료된 총 작업 수
    int currentTaskProgress;        // 비정적 멤버 변수: 현재 객체의 작업 진행도

public:
    TaskManager() : currentTaskProgress(0) {}

    static void markTaskCompleted() {
        completedTasksCount++; // 정적 멤버 접근 가능
        // currentTaskProgress = 100; // 오류: 비정적 멤버는 접근 불가 (this 포인터 없음)
    }

    static int getGlobalCompletedTasks() {
        return completedTasksCount;
    }

    void updateProgress(int progress) {
        currentTaskProgress = progress;
        if (currentTaskProgress >= 100) {
            markTaskCompleted(); // 정적 함수 호출 가능
        }
    }
    
    int getCurrentTaskProgress() const {
        return currentTaskProgress;
    }
};

int TaskManager::completedTasksCount = 0; // 정적 멤버 변수 정의

int main() {
    TaskManager taskA;
    taskA.updateProgress(50);
    taskA.updateProgress(100); // taskA 완료, completedTasksCount 증가

    TaskManager taskB;
    taskB.updateProgress(80);
    taskB.updateProgress(100); // taskB 완료, completedTasksCount 증가

    std::cout << "전체 완료된 작업 수: " << TaskManager::getGlobalCompletedTasks() << std::endl; // 출력: 2
    std::cout << "Task A 진행도: " << taskA.getCurrentTaskProgress() << std::endl; // 출력: 100
    std::cout << "Task B 진행도: " << taskB.getCurrentTaskProgress() << std::endl; // 출력: 100
    
    return 0;
}

const 키워드 활용

const 키워드는 값이 변경될 수 없는 상수를 선언할 때 사용됩니다. 이는 코드의 안정성을 높이고 의도치 않은 값 변경을 방지하는 데 도움을 줍니다.

1. 기본 타입 및 데이터 구조

일반 변수 앞에 const를 붙이면 해당 변수는 초기화된 이후에는 값을 변경할 수 없는 상수가 됩니다.


#include <iostream>

int main() {
    const int maxAttempts = 5; // maxAttempts는 5로 고정됩니다.
    // maxAttempts = 10; // 오류: 상수 변수는 수정할 수 없습니다.

    int const bufferSize = 1024; // 위치가 바뀌어도 동일하게 상수 변수입니다.
    // bufferSize = 2048; // 오류
    
    std::cout << "최대 시도 횟수: " << maxAttempts << std::endl;
    std::cout << "버퍼 크기: " << bufferSize << std::endl;
    return 0;
}

2. 포인터 변수

const가 포인터 선언에서 어디에 위치하는지에 따라 의미가 달라집니다.

가. 상수 데이터를 가리키는 포인터 (Pointer to const data)

const* 앞에 위치하면, 포인터가 가리키는 데이터가 상수임을 의미합니다. 포인터 자체는 다른 주소를 가리키도록 변경할 수 있지만, 가리키는 데이터의 값은 변경할 수 없습니다.


int score1 = 100;
int score2 = 200;

const int* ptrToConstScore = &score1; // int형 상수를 가리키는 포인터
// *ptrToConstScore = 150; // 오류: 가리키는 데이터를 수정할 수 없습니다.
ptrToConstScore = &score2; // 허용: 포인터 자체는 다른 주소를 가리킬 수 있습니다.
// 이제 ptrToConstScore는 score2를 가리키고, 그 값은 200입니다.

나. 상수 포인터 (Const pointer)

const* 뒤에 위치하면, 포인터 자체가 상수임을 의미합니다. 즉, 포인터가 한 번 초기화되면 다른 주소를 가리키도록 변경할 수 없습니다. 하지만 포인터가 가리키는 데이터의 값은 변경할 수 있습니다.


int health = 50;
int* const constHealthPtr = &health; // int형을 가리키는 상수 포인터
*constHealthPtr = 45; // 허용: 가리키는 데이터의 값을 수정할 수 있습니다.
// int newHealth = 70;
// constHealthPtr = &newHealth; // 오류: 상수 포인터는 다른 주소를 가리킬 수 없습니다.

다. 상수 데이터를 가리키는 상수 포인터 (Const pointer to const data)

const*의 앞뒤 모두에 위치하면, 포인터 자체도 상수이고 포인터가 가리키는 데이터도 상수임을 의미합니다. 즉, 포인터도 변경할 수 없고, 가리키는 데이터의 값도 변경할 수 없습니다.


int playerID = 12345;
const int* const constIDPtr = &playerID; // int형 상수를 가리키는 상수 포인터
// *constIDPtr = 54321; // 오류: 가리키는 데이터를 수정할 수 없습니다.
// int newPlayerID = 67890;
// constIDPtr = &newPlayerID; // 오류: 포인터 자체를 수정할 수 없습니다.

3. 참조 (References)

const 참조는 참조하는 변수의 값을 변경할 수 없도록 보장합니다. 이는 주로 함수 인자로 전달할 때 원본 데이터를 보호하는 데 사용됩니다. 참조 자체가 다른 대상을 참조하도록 변경될 수 없으므로(참조는 한 번 바인딩되면 바뀔 수 없음), const 키워드는 오직 참조 대상의 불변성을 제어합니다.


#include <iostream>

int main() {
    int originalPrice = 25000;
    const int& constPriceRef = originalPrice; // const 참조

    // constPriceRef = 30000; // 오류: const 참조를 통해 값을 변경할 수 없습니다.
    
    std::cout << "원본 가격: " << originalPrice << ", const 참조 가격: " << constPriceRef << std::endl;
    originalPrice = 28000; // 원본 변수는 변경 가능
    std::cout << "원본 가격 변경 후: " << originalPrice << ", const 참조 가격: " << constPriceRef << std::endl; 
    return 0;
}

4. 함수

가. const 매개변수

함수 매개변수에 const를 사용하면, 함수 내부에서 해당 매개변수의 값을 변경할 수 없도록 합니다. 이는 특히 포인터나 참조로 객체를 전달할 때 원본 데이터의 불변성을 보장하는 데 중요합니다.


#include <iostream>
#include <vector>
#include <string>

void displayGreeting(const std::string& name) {
    // name = "New Name"; // 오류: const 참조를 통해 값을 변경할 수 없습니다.
    std::cout << "안녕하세요, " << name << "님!" << std::endl;
}

void printVector(const std::vector<int>* dataVec) {
    if (dataVec) {
        for (int val : *dataVec) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }
    // dataVec->push_back(100); // 오류: const 포인터를 통해 값을 변경할 수 없습니다.
}

int main() {
    std::string userName = "김철수";
    displayGreeting(userName);

    std::vector<int> numbers = {10, 20, 30, 40, 50};
    printVector(&numbers);
    return 0;
}

나. const 반환 값

함수의 반환 타입에 const를 붙이면, 반환된 값을 변경할 수 없게 만듭니다. 이는 주로 참조나 포인터를 반환할 때 유용하며, 반환된 객체가 수정되지 않도록 보호합니다.


#include <iostream>
#include <string>

const std::string& getSystemVersion() {
    static const std::string versionString = "v1.2.3-release"; // 정적 const 문자열
    return versionString; // const 참조 반환
}

int main() {
    const std::string& currentVersion = getSystemVersion();
    // currentVersion = "v2.0.0"; // 오류: const 참조는 수정할 수 없습니다.
    std::cout << "시스템 버전: " << currentVersion << std::endl;
    return 0;
}

5. 클래스 내에서의 const 사용

가. const 멤버 변수

클래스 멤버 변수에 const를 붙이면, 해당 객체가 생성될 때 한 번만 초기화되고 이후에는 변경할 수 없습니다. const 멤버 변수는 반드시 생성자의 초기화 리스트(initializer list)에서 초기화되어야 합니다.

  • 초기화: 생성자의 초기화 리스트에서만 가능합니다.
  • 객체별 값: static const와 달리, 각 객체는 고유한 const 멤버 변수 값을 가질 수 있습니다.

#include <iostream>
#include <string>

class UserProfile {
private:
    const int userId; // const 멤버 변수: 사용자 ID는 한 번 설정되면 변경 불가
    std::string userName;

public:
    // 생성자 초기화 리스트에서 const 멤버 변수 초기화
    UserProfile(int id, const std::string& name) : userId(id), userName(name) {} 

    int getUserId() const {
        return userId;
    }

    std::string getUserName() const {
        return userName;
    }

    void setUserName(const std::string& newName) {
        userName = newName; // 비-const 멤버는 변경 가능
    }

    // void setUserId(int newId) { // 오류: const 멤버는 변경할 수 없습니다.
    //     userId = newId;
    // }
};

int main() {
    UserProfile userA(101, "Alice");
    UserProfile userB(102, "Bob");

    std::cout << "User A ID: " << userA.getUserId() << ", 이름: " << userA.getUserName() << std::endl;
    std::cout << "User B ID: " << userB.getUserId() << ", 이름: " << userB.getUserName() << std::endl;

    userA.setUserName("Alicia"); // 사용자 이름은 변경 가능
    std::cout << "User A 변경된 이름: " << userA.getUserName() << std::endl;
    return 0;
}

나. const 멤버 함수

멤버 함수 선언 뒤에 const를 붙이면, 해당 함수는 클래스의 멤버 변수들을 수정하지 않겠다는 것을 컴파일러에게 보장합니다. 이는 객체의 상태를 변경하지 않는 "읽기 전용" 함수임을 나타냅니다.


#include <iostream>

class SensorData {
private:
    int temperature;
    mutable int readCount; // mutable: const 멤버 함수 내에서도 변경 가능

public:
    SensorData(int temp) : temperature(temp), readCount(0) {}

    int getTemperature() const { // const 멤버 함수: temperature를 수정할 수 없습니다.
        // temperature = 25; // 오류: const 함수는 멤버 변수를 수정할 수 없습니다.
        readCount++; // mutable 변수는 const 함수 내에서도 수정 가능
        return temperature;
    }

    void calibrate(int offset) { // 비-const 멤버 함수: temperature 수정 가능
        temperature += offset;
    }
    
    int getReadCount() const {
        return readCount;
    }
};

int main() {
    SensorData sensor(20);
    std::cout << "현재 온도: " << sensor.getTemperature() << std::endl; // const 함수 호출
    std::cout << "현재 온도: " << sensor.getTemperature() << std::endl;
    std::cout << "센서 판독 횟수: " << sensor.getReadCount() << std::endl; // 출력: 2

    sensor.calibrate(5); // 비-const 함수 호출
    std::cout << "보정 후 온도: " << sensor.getTemperature() << std::endl; // 출력: 25
    return 0;
}

다. const 객체

const로 선언된 객체는 오직 const 멤버 함수만 호출할 수 있습니다. 이는 const 객체의 상태가 변경되지 않음을 컴파일러가 보장하도록 합니다. 비-const 객체는 const 함수와 비-const 함수 모두 호출할 수 있습니다.


#include <iostream>

class ImmutableValue {
private:
    int dataValue;
public:
    ImmutableValue(int v) : dataValue(v) {}

    int getData() const { // const 멤버 함수
        return dataValue;
    }

    void setData(int v) { // 비-const 멤버 함수
        dataValue = v;
    }
};

int main() {
    ImmutableValue mutableObject(50); // 일반 (비-const) 객체
    std::cout << "일반 객체 값: " << mutableObject.getData() << std::endl;
    mutableObject.setData(75); // 비-const 객체는 비-const 함수 호출 가능
    std::cout << "일반 객체 수정 후 값: " << mutableObject.getData() << std::endl;

    const ImmutableValue constObject(100); // const 객체
    std::cout << "const 객체 값: " << constObject.getData() << std::endl;
    // constObject.setData(120); // 오류: const 객체는 비-const 함수를 호출할 수 없습니다.
    return 0;
}

const 객체의 멤버 함수 호출 규칙은 내부적으로 this 포인터의 타입과 관련이 있습니다. 일반 객체의 this 포인터는 ClassName* const 타입인 반면, const 객체의 this 포인터는 const ClassName* const 타입입니다. const 멤버 함수는 const ClassName* const this를 받도록 선언되어 있으므로 const 객체에서 호출할 수 있습니다. 비-const 멤버 함수는 ClassName* const this를 받도록 선언되어 있으므로, const ClassName* const this 타입의 const 객체는 이를 호출할 수 없습니다.

태그: C++ static const keywords storage-duration

7월 8일 08:27에 게시됨