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