변수의 유효 범위와 저장 구역
변수는 지역 변수와 전역 변수로 나뉜다. 지역 변수는 중괄호({})로 둘러싸인 코드 블록 내에서만 유효하며, 스택 영역에 할당되어 해당 블록이 종료되면 자동으로 소멸된다. 반면 전역 변수는 프로젝트 전체에서 접근 가능하며, 전역 영역(정적 메모리 영역)에 저장되어 프로그램 종료 시까지 유지된다.
특수한 경우:
- 지역 블록 내에서 전역 변수와 동일한 이름의 변수를 선언하면, 그 유효 기간 동안 전역 변수는 가려진다.
- 전역 변수는 하나의 소스 파일에서만 정의될 수 있으며, 다른 파일에서는
extern키워드를 사용해 참조해야 한다. - 여러 파일에서 공유하는 전역 변수가 필요한 경우, 한 파일에서 정의하고 헤더 파일에서
extern선언을 하며, 다른 파일에서 해당 헤더를 포함하면 된다.
static 키워드의 역할
static은 변수, 함수, 클래스 멤버 등에 적용되어 생명주기와 접근 범위를 조절한다.
- 지역 변수에 적용:
static지역 변수는 전역 영역에 저장되며, 프로그램 종료 전까지 유지된다. 하지만 유효 범위는 여전히 해당 블록 내부이다. - 전역 변수에 적용:
static전역 변수는 해당 파일 내에서만 접근 가능하다. 다른 파일에서 접근하려면 링크 오류 발생. 여러 파일에 같은 이름의static전역 변수가 있어도 충돌 없음. 이는 결합도 감소와 안전성 향상에 기여한다. - 함수에 적용:
static함수는 해당 소스 파일 내에서만 유효하며 외부에서 호출 불가. 헤더 파일에서 선언 시static을 함께 쓰면, 컴파일러는 내부 함수로 간주하여 링크 단계에서 정의되지 않음을 알림. - 클래스 멤버에 적용:
static멤버 변수와 함수는 클래스의 모든 인스턴스가 공유하며, 인스턴스 생성 시마다 복제되지 않는다. 클래스 이름으로 직접 접근 가능(공개 권한 필요). 멤버 함수는static멤버만 접근 가능하며,this포인터가 없기 때문에 비-정적 멤버에 접근할 수 없다. 또한 가상 함수로 선언할 수 없으며, 가상 함수 테이블에 등록되지 않기 때문이다.
메모리 저장 유형
C 언어에는 auto, register, extern, static 네 가지 저장 지정자로 구성된 저장 클래스가 있다. 이들은 자동 저장 기간과 정적 저장 기간으로 분류된다. auto와 register는 자동 저장 기간을 가지며, 블록 진입 시 생성되고 종료 시 소멸된다.
C++11부터 auto는 타입 추론을 위한 키워드로 사용되며, 초기값이 반드시 제공되어야 한다.
std::map<int, int> data;
for (auto it = data.begin(); it != data.end(); ++it) {
// it의 타입은 std::map<int, int>::iterator로 추론됨
}
다형성: 정적 다형성과 동적 다형성
- 정적 다형성: 함수 오버로딩을 통해 구현. 컴파일 시 결정되며, 함수 이름이 같고 매개변수 리스트가 다를 때 발생. 반환 타입은 구분 기준이 되지 않는다.
- 동적 다형성: 상속과 가상 함수를 통한 함수 재정의(오버라이드)에 의해 이루어진다. 자식 클래스에서 부모 클래스의 함수를 재정의하면, 객체가 해당 함수를 호출할 때 실제 실행되는 코드는 자식 클래스의 구현이다. 실행 시점에 결정되므로 동적 다형성이라 한다.
가상 함수와 가상 함수 테이블
클래스에 가상 함수가 존재하면, 각 객체는 가상 함수 테이블 포인터(vptr)를 갖게 된다. 이 포인터는 가상 함수 테이블을 가리키며, 테이블 내에는 해당 클래스의 모든 가상 함수 주소가 저장된다. 테이블 자체는 읽기 전용 데이터 세그먼트(.rodata)에 위치하며, 함수 코드는 코드 세그먼트(.text)에 있다.
자식 클래스가 부모의 가상 함수를 재정의하지 않으면, 자식의 가상 함수 테이블은 부모의 테이블과 동일한 주소를 갖는다. 재정의하면 해당 함수의 주소가 자식의 테이블에 업데이트된다.
class Base {
public:
virtual void func() { std::cout << "Base::func" << std::endl; }
};
class Derived : public Base {
public:
void func() override { std::cout << "Derived::func" << std::endl; }
};
int main() {
Base* b = new Base();
Derived* d = new Derived();
long* vtable_b = (long*)*(long*)b;
long* vtable_d = (long*)*(long*)d;
std::cout << std::hex << vtable_b << std::endl;
std::cout << std::hex << vtable_d << std::endl;
delete b;
delete d;
return 0;
}
매크로 및 조건 컴파일
예상 처리 지시문은 컴파일 전에 처리된다.
#define: 매크로 정의#undef: 매크로 제거#if,#ifdef,#ifndef,#elif,#else,#endif: 조건 컴파일defined: 매크로 정의 여부 확인
const 키워드의 의미
const는 값 변경을 금지한다. 그러나 포인터를 통해 값을 수정할 수 있는 경우도 있다.
const int MAX = 100;
int* p = &MAX;
*p = 101; // 경고 발생, 허용됨
또한, const는 변수 앞뒤에 위치에 따라 의미가 다르지 않으며, 포인터의 경우:
const int*: 포인터가 가리키는 값은 변경 불가 (상수 포인터)int* const: 포인터 자체의 값(주소)은 변경 불가 (포인터 상수)
스마트 포인터
스마트 포인터는 원시 포인터를 감싸는 템플릿 클래스로, 메모리 관리를 자동화한다. 객체가 파괴될 때 할당된 동적 메모리가 자동 해제된다.
익명 함수 (람다 표현식)
람다는 함수 객체이며, 정의 시 스택에 임시 객체가 생성된다. () 연산자 오버로딩을 통해 함수처럼 호출 가능하다.
좌값과 우값
좌값은 메모리 주소를 나타내며, 읽고 쓸 수 있다. 우값은 메모리에 저장된 값만을 의미하며, 읽기만 가능하다.
- 좌값 참조: 좌값의 별칭. 복사 방지를 위해 사용.
- 우값 참조: 임시 객체(우값)의 참조. 함수에서 임시 객체를 반환할 때, 이를 바로 참조할 수 있게 해줌.
프로세스와 스레드
- 프로세스 간 통신: 파이프, 공유 메모리, 메시지 큐, 세마포어
- 스레드 간 통신: 상호배제 자원(뮤텍스), 조건 변수, 세마포어, 원자 연산
스레드 안전성: 여러 스레드가 동시에 호출해도 결과가 항상 올바른 코드.
데드락 조건:
- 상호 배타 조건: 자원은 한 번에 하나의 스레드만 사용 가능
- 요청 및 보유 조건: 자원을 요청하면서 기존 자원을 점유한 상태 유지
- 비강제 해제 조건: 점유한 자원은 스스로 해제해야 함
- 순환 대기 조건: 스레드들이 서로 자원을 요구하며 순환 구조 형성
pragma once vs #ifndef
헤더 파일의 중복 포함을 방지하기 위한 방법. pragma once는 컴파일러 의존적이지만 빠르고 간단. #ifndef는 표준이며 크로스 플랫폼 호환성 좋음.
정적 라이브러리와 동적 라이브러리
정적 라이브러리(.a/.lib)는 링크 시 코드가 프로그램에 복사된다. 동적 라이브러리(.so/.dll)는 실행 시 로드되며, 여러 프로그램이 공유 가능하다.
기타 중요 개념
- 함수 외부에서는 할당이나 실행 문장이 허용되지 않으며, 초기화는 가능하다. 실행 순서가 보장되지 않기 때문이다.