포인터(pointer): 메모리의 특정 공간을 가리키는 변수이다.
주소를 직접 가리킴으로써 명확한 위치를 지정할 수 있다.
메모리에는 바이트 단위로 주소가 부여되며, 포인터는 해당 주소를 저장하여 데이터를 참조한다.
이러한 변수를 포인터 변수라고 하며, 변수이므로 값이 변할 수 있다.
포인터 변수는 하나의 특정 주소를 가리키는 것뿐만 아니라, 다양한 주소를 저장할 수도 있다.
따라서 포인터도 변수이므로 선언이 필요하다.
포인터 선언 방식:
자료형* 변수명;
예제:
int* p; // 정수형 데이터를 가리키는 포인터 선언
변수를 선언하면 해당 변수는 메모리 공간을 할당받는다. 예를 들어,
int n;
이 경우, n은 특정 메모리 주소(예: 0x1000)에 저장되며, int 자료형이므로 4바이트를 차지한다(0x1000~0x1003).
컴파일러가 실행파일을 생성한 후, 실행하면 해당 실행파일은 하드디스크에 존재하다가 실행 시 메모리로 적재된다.
이때, 커널이 실행파일을 메모리에 탑재하고 주소 관리를 수행한다.
프로그램이 실행되면서 프로세스로 변환되며, 프로세스 내에서 포인터가 가리키는 주소는 가상 메모리 주소이다.
이 가상 메모리는 커널에 의해 물리 메모리로 매핑된다.
포인터 변수도 일반 변수처럼 메모리에 할당된다. 예를 들어,
int *p;
이 경우, p는 포인터 변수이며, 64비트 시스템에서는 8바이트를 차지한다(예: 0x1100~0x1107).
초기화되지 않은 포인터는 쓰레기 값을 가지므로, 반드시 NULL로 초기화하는 것이 권장된다.
int *p = NULL;
포인터에 특정 주소를 할당하려면:
int *p = &n;
이렇게 하면 p는 n의 시작 주소(0x1000)를 가리킨다.
포인터를 사용하여 값을 변경할 수 있다.
*p = 123; // n의 값이 123으로 변경됨
포인터 변수 앞의 자료형은 포인터 자체의 자료형이 아니라, 가리키는 대상의 자료형을 나타낸다.
예를 들어:
int *p; // int 형 데이터를 가리키는 포인터
char *c; // char 형 데이터를 가리키는 포인터
void* 포인터는 특정 자료형을 지정하지 않은 포인터이다. 다양한 자료형의 변수를 가리킬 수 있다. 그래서 라이브러리 함수들을 보면 void*로 인자를 받는 경우가 많다. 다양한 자료형에 대해 받기 위해.
void* ptr;
하지만 void* 포인터를 사용할 때는 명확한 자료형으로 변환해야 한다.
int a = 10;
void* ptr = &a;
int* int_ptr = (int*)ptr;
포인터는 +, - 연산만 가능하며, 포인터 연산은 자료형 크기 단위로 수행된다.
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p = p + 1; // p가 4바이트 증가하여 arr[1]을 가리킴
즉, p + 1은 단순히 1 증가하는 것이 아니라, 포인터가 가리키는 자료형 크기만큼 증가한다.
배열과 포인터는 밀접한 연관이 있다.
배열은 동일한 자료형으로 이루어지며, 연속된 메모리 공간을 차지한다.
int arr[3] = {10, 20, 30};
int *p = arr;
printf("%d", *(p + 1)); // 20 출력
배열의 이름(arr)은 첫 번째 요소의 주소를 나타내며, 포인터처럼 사용될 수 있다.
프로세스는 다음과 같은 메모리 세그먼트로 구성된다:
1. 코드(Code) 세그먼트 - 실행 코드가 저장됨
2. 데이터(Data) 세그먼트 - 전역 변수 및 정적(static) 변수 저장
3. 스택(Stack) 세그먼트 - 지역 변수 및 함수 호출 정보 저장
4. 힙(Heap) 영역 - 동적 할당된 메모리 저장
스택(Stack)은 함수 호출 시 생성되는 메모리 영역이며, 지역 변수 및 매개변수 저장에 사용된다.
힙(Heap)은 동적 할당된 메모리를 저장하는 공간으로, 반드시 개발자가 메모리를 해제해야 한다.
int *p = (int*)malloc(sizeof(int));
free(p); // 메모리 해제
메모리를 해제하지 않으면 메모리 누수(Memory Leak)가 발생할 수 있으며, 시스템 성능에 영향을 미칠 수 있다.
함수로 값을 전달하는 방식은 다음과 같다:
1. Call by Value (값에 의한 전달) - 함수에서 값이 변경되더라도 원본 변수는 변경되지 않음.
2. Call by Address (주소에 의한 전달) - 포인터를 통해 주소를 전달하여 원본 값을 변경할 수 있음.
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
포인터 변수에 const 키워드를 사용하여 변경을 제한할 수 있다.
const int* pa = &a; // a의 값 변경 불가능 (포인터는 변경 가능)
int* const pb = &a; // 포인터 pb 변경 불가능 (값은 변경 가능)
const int* const pc = &a; // 포인터와 값 모두 변경 불가능
배열명은 상수이며, 직접 변경할 수 없다. 반면, 포인터 변수는 값을 변경할 수 있으므로 주의가 필요하다.
컴퓨터 내에서 문자는 숫자로 저장된다. 대표적으로 아스키 코드(ASCII Code)를 사용하여 문자 데이터를 표현한다.
컴퓨터 기술이 미국을 중심으로 발전했기 때문에, 영문자 체계가 먼저 구현되었다.
문자를 입력받을 때 %c 형식 지정자를 사용한다.
scanf 함수는 문자열, 정수값, 실수값 등 다양한 데이터를 입력받을 수 있으며, 입력 시 키보드에서 입력한 내용을 버퍼에 저장한 후, 지정한 자료형에 맞게 데이터를 변환하여 전달한다.
문자와 숫자가 혼합된 입력을 받을 때 scanf를 사용할 경우, 입력 버퍼에 개행 문자가 남아 있어 예상치 못한 입력 오류가 발생할 수 있다.
fflush(stdin) 함수를 사용하여 입력 버퍼를 초기화하는 방법이 있지만, 표준 함수가 아니므로 권장되지 않는다.
대신, getchar() 함수를 활용하여 입력 버퍼를 비우는 것이 바람직하다.
char ch;
scanf("%c", &ch);
getchar(); // 개행 문자 제거
getchar() 함수는 표준 함수이며, 문자 하나만 입력받는다. 이 함수를 이용하면 입력 버퍼에 남아 있는 개행 문자(\n)를 제거할 수 있다.
입력 버퍼에는 사용자가 입력한 데이터가 저장되며, 입력 함수를 호출하면 해당 데이터가 전달된다.
엔터 키(Enter)를 입력하면 개행 문자(\n)가 입력 버퍼에 남아 문제를 발생시킬 수 있다.
지역 변수는 특정 코드 블록({}) 또는 함수 내에서 선언되며, 해당 블록을 벗어나면 소멸된다.
지역 변수는 auto 키워드를 생략할 수 있다.
같은 이름의 지역 변수가 다른 함수 또는 블록에서 선언될 경우, 각각 독립적인 메모리 공간(스택 프레임)을 가지므로 서로 영향을 주지 않는다.
스택 프레임(Stack Frame)에는 매개변수, 지역 변수, 반환 값이 저장된다.
예를 들어, for 루프 내에서 선언된 변수는 해당 루프가 종료될 때 소멸된다.
for (int i = 0; i < 10; i++) {
printf("%d", i); // i는 for문 내부에서만 사용 가능
}
위 i 변수는 for 블록을 벗어나면 제거된다.
전역 변수는 프로그램 전체에서 접근할 수 있으며, 데이터 세그먼트(Data Segment)에 저장된다.
같은 이름의 지역 변수가 존재할 경우, 해당 지역 변수의 범위 내에서는 지역 변수가 우선적으로 사용된다.
전역 변수 사용은 가능한 한 지양해야 한다.
전역 변수는 프로그램 실행 동안 유지되며, 불필요한 메모리 점유를 줄이기 위해서는 전역변수 사용을 지양하되, 사용이 끝난 변수는 해제하는 것이 바람직하다.
static)변수 앞에 static 키워드를 사용하면 정적 변수로 선언된다.
static 변수는 데이터 세그먼트에 저장되며, 프로그램이 종료될 때까지 값이 유지된다.void counter() {
static int count = 0;
count++;
printf("%d", count);
}
위 함수가 여러 번 호출되더라도 count 변수는 값을 유지한다.
전역 변수에 static을 붙이면 해당 파일 내부에서만 접근 가능하게 제한된다.
이렇게 하면 다른 파일에서 같은 이름의 변수를 선언하더라도 충돌을 방지할 수 있다.
register)레지스터 변수는 CPU의 레지스터(Register)에 저장되는 변수이다.
register 키워드를 사용하면 CPU 내부 레지스터를 사용하여 연산 속도를 향상시킬 수 있다.register int fastVar = 10; // CPU 레지스터에 저장 시도
register 키워드를 명시적으로 사용할 필요가 거의 없다.결론적으로, register 키워드는 과거에 성능 최적화를 위해 사용되었지만, 현대 컴파일러의 최적화 기술이 발달하면서 거의 사용되지 않는다.