2025. 4. 24. 17:54ㆍC 연습
/*본게시글은 understanding and using c pointer (c포인터 이해와 활용) 2020년 서적을 참고했습니다.
또한 Python Tutor 를 이용하였습니다.*/
1. 포인터란 무엇인가요?
c언어에서의 포인터는 핵심이자 직접적인 메모리 제어 방법입니다.
포인터의 정의와 일상적 비유
일단 포인터의 정의를 보면
Pionter
"무언가를 가리키는 것 또는 가리키는 데 사용되는 물건"
이라는 뜻을 가지고 있습니다.
c언어에서의 포인터 역시 같은 의미를 가지고 있습니다.
그림처럼 포인터또한 변수를 가리키는데 이용됩니다.
정확히는 메모리의 어떤 주소를 가리킨다고 보면됩니다.
자세한 이해를 위해서 아래와 같이 포인터변수를 선언하고 초기화 하는 코드를 작성해봅니다.
2. 포인터의 선언과 초기화
포인터 변수 선언법
#include<stdio.h>
int main() {
int a = 10;
int *pi;
pi= &a;
return 0;
}
아래코드는 다음과 같습니다.
1. int형 변수 a를 선언과 동시에 10으로 초기화
2. int형 포인터변수 pi 선언
3. 포인터 변수 pi는 변수 a의 주소를 가리킴 (역참조)
4. 프로그램종료
Q) a의 주소를 가리키는 것이 무엇인가요? 그냥 a의 내용물(10)을 가리키는것 인가요?
아닙니다. a자체가 가지는 메모리상의 주소값을 가리킵니다.
일반적으로 이것을 아파트 택배에 비유합니다.
택배아저씨가 택배를 배달할때 배송지에 적힌 주소의 현관앞에 택배를 놓으면 됩니다.
집안의 누군가에게 반드시 넘길 필요는 없듯이 말입니다.
아파트 123동 /456호 김아무개로 가는 주소가 있다면
집안에 수령인의 본인이나 가족 혹은 지인등 누가있든 관계없이
456호 현관앞(주소)에다 택배를 놓고 가는것 처럼 생각하시면 됩니다.
포인터 또한 이와 같습니다.
메모리상 변수의 주소를 팻말처럼 가리키는 것 뿐입니다.
3. 포인터와 메모리 구조
여러분들이 쓰시는 주기억장치(Ram메모리)상의 특정 공간을 식별하는 고유한 값입니다. 마치 집 주소처럼, 각각의 메모리 위치는 데이터를 저장하고 접근하기 위한 주소를 가지고 있습니다.
메모리 주소의 개념
메모리의 구조는 프로그램이 실행되기 위해 필요한 여러 종류의 데이터를 효율적으로 관리하기 위해 논리적으로 나뉘어져 있습니다. 각각의 영역은 RAM(주 기억 장치)의 특정 주소 공간에 할당되어 고유한 역할을 수행합니다.
메모리의 구조
3.1. Code Section (코드 영역)
프로그램 명령어: 이 영역은 작성한 프로그램의 실제 명령어들이 저장되는 곳입니다. CPU(중앙 처리 처리 장치)는 이 영역에 있는 명령어들을 순차적으로 읽어와서 실행합니다.
읽기 전용 (Read-only): 일반적으로 코드 영역은 프로그램 실행 중에는 내용이 바뀌지 않아야 하므로 읽기 전용으로 설정됩니다.
해당영역에 저장되는 데이터는
기계어로 이루어진 읽기 전용 실행 파일의 명령어가 저장됩니다.
3.2. Data Section (데이터 영역)
전역 변수, 정적 변수: 이 영역은 프로그램 전체에서 접근할 수 있는 전역 변수(global variables)와 프로그램이 시작될 때 초기화되고 프로그램이 종료될 때까지 메모리에 유지되는 정적 변수(static variables)가 저장되는 곳입니다.
초기화된 변수 (Initialized Variables): 데이터 영역 중에서도 초기화된 값을 가지는 변수들이 이 부분에 저장됩니다.
ex)
int a = 0; 과 같이 선언과 동시에 초기화된 변수 a는 데이터 영역에 생성됩니다.
3.3. BSS Section
초기화되지 않은 전역 변수: 이 영역은 전역 변수나 정적 변수 중에서 초기화되지 않은 변수들이 저장되는 곳입니다. 프로그램이 시작될 때 이 영역의 변수들은 자동으로 0 또는 NULL 값으로 초기화됩니다.
BSS는 "Block Started by Symbol"의 약자로, 초기화되지 않은 데이터를 위한 공간을 효율적으로 관리하기 위해 사용됩니다.
프로그램이 실행되기 전에, 운영체제(os) 또는 링커는 이 BSS 영역의 모든 바이트를 0으로 채웁니다.
#include <stdio.h>
// 초기화되지 않은 전역 변수
int globalVar;
// 초기화되지 않은 정적 변수
static int staticVar;
int main() {
printf("globalVar: %d\n", globalVar); // 출력: 0
printf("staticVar: %d\n", staticVar); // 출력: 0
return 0;
}
3.a BSS 영역에 선언된 초기화되지 않은 포인터 변수는 0으로 채워집니다.
프로그래밍 언어(특히 C/C++)에서 메모리 주소 0번지는 특별한 의미를 가지며, **널 포인터(null pointer)**를 나타냅니다.
따라서 BSS 영역에 있는 초기화되지 않은 포인터 변수는 프로그램 시작 시 널 포인터 값을 가지게 됩니다.
이 널 포인터 값은 "아무것도 가리키지 않음"을 의미하는 Null 값으로 해석될 수 있습니다.
이 0이라는 값이 포인터 변수에 저장될 때, C 언어에서는 이것을 특별하게 NULL 포인터라고 해석합니다. 1
3.4. Heap (힙 영역)
동적 메모리 할당: 힙 영역은 프로그래머가 프로그램 실행 중에 필요에 따라 메모리를 동적으로 할당하고 해제하는 데 사용됩니다. malloc(), free() (C/C++) 또는 new, delete (C++)와 같은 함수를 통해 이 영역의 메모리를 관리합니다.
힙 영역은 프로그램 실행 중에 필요한 만큼의 가변적인 메모리를 확보할 수 있게 해주지만, 메모리 누수(memory leak) 와 같은 문제에 주의해야 합니다. 2
3. 5. Stack (스택 영역)
함수 호출, 지역 변수: 스택 영역은 함수의 호출과 관련된 정보를 저장하고, 함수 내부 에서 선언된 지역 변수(local variables)가 임시로 저장되는 곳입니다.
: 함수가 호출될 때 스택 프레임(stack frame)이라는 블록이 스택에 적재(push)되어 함수의 매개변수, 지역 변수, 반환 주소 등의 정보를 저장합니다. 함수 실행이 끝나면 해당 스택 프레임은 스택에서 제거(pop)됩니다. 이러한 LIFO 구조 덕분에 함수의 호출과 반환 순서를 효율적으로 관리할 수 있습니다.
자동 관리: 스택 영역의 메모리는 함수가 호출되고 종료될 때 자동으로 할당되고 해제되므로 프로그래머가 직접 관리할 필요가 없습니다. 하지만 스택 오버플로우(stack overflow) 와 같은 문제에 주의해야 합니다. 4
4. 메모리 영역별 변수의 특징
4.1 정적(staic)메모리
전역 변수나 정적으로 선언된 변수들 (static 키워드로 생성된 변수들)은 정적인 메모리에 할당됩니다.
해당 변수들은 프로그램이 종료될 떄 까지 메모리 공간에 남아 있습니다.
프로그램 실행동안만 메모리에서 유지됩니다.
4.2 동적 메모리
동적(Dynamic) 메모리는 힙(heap) 메모리 영역에 할당되고 필요한 경우 해제 합니다.
보통 포인터를 이용하여 접근제한을 지정하거나 영역을 참조합니다.
4.3 자동(로컬) 메모리
함수나 블록 안에서 선언되고 해당 블록이 끝날 때 자동으로 메모리가 해제되는 지역 변수입니다.
스택(stack) 메모리 영역에 저장됩니다.
일반적으로 블록({}) 내부에 선언된 변수들의 범위는 해당 블록으로 제한됩니다.
함수에서 실행되는 동안 메모리 공간에 남아있습니다.
5. 포인터 변수의 선언과 주소 연산자(&)
다시 돌아가 아까의 포인터 변수를 봅시다
#include<stdio.h>
int main() {
int a = 10;
int *pi;
pi= &a;
return 0;
}
step 1
stack 메모리 상에서
0xFFFF000BD4 와 0xFFFF000BD8 에 각각 의 주소가 각각의 변수들에 할당되어 있습니다.
해당 프로그램에서a 메모리 주소 바로 다음 포인터 주소를 주었지만 크게 신경쓰지 않아도 됩니다.
step 2
포인터 변수 pi가 변수a의 주소를 참조하는 단계입니다.
포인터변수 pi는 a의 주소인 0xFFFF000BD4를 가리키고 있습니다.
스텝3은 프로그램 종료이후라 따로 적진 않겟습니다.
Q) 근데 주소를 가리켜서 뭐해요? 자바나 c# , 코틀린등은 포인터 안쓰던데?
c언어의 특성입니다.
c언어 자체는 고급언어(인간의 관점의 언어)지만 low 레벨 (기계에 가까운) 특성을 띄는언어입니다.
그렇기에 하드웨어적 관점이나 성향이 더 강해. 메모리의 물리적 주소의 접근을 통한방법을 이용합니다.
이러한 과정에서 포인터를 활용합니다.
c언어는 JAVA의 가상머신(JVM) 이나 c#의 닷넷 처럼 메모리를 별도로 관리해주는 가상머신이 없어 개발자(혹은 작성자)가 직접 메모리 참조하고 해제하는 방법을 취합니다. 5
6. 포인터의 필요성 및 활용 예시
Q) 근데 굳이 포인터를 써야만해요? 그냥 함수처리나 전역변수로 처리할수 있잖아요
전역변수를 사용하는 경우
지역변수와 달리 코드블록 종료후에도 프로그램 종료시까지 유지되므로 리소스 부담이되고,
모든 함수에서 접근 가능하므로 의도치 않은 수정이 발생하기 쉽습니다.
(접근하는 함수마다 전역변수가 수정되는경우)
함수의 경우
재귀함수의 문제
1.스택 오버플로우
재귀 깊이가 커지면 스택 메모리자원이 고갈되어 프로그램이 비정상 종료됩니다.
2.성능 저하
함수 호출 시마다 스택 프레임이 생성되어 반복문보다 속도가 느립니다.
3.디버깅 어려움
재귀 단계가 많아지면 실행 흐름을 추적하기 어렵습니다.
swap 함수 예제(값/포인터 비교)
2개의 코드가 있습니다. 그냥 두 변수를 교환하는 코드가 있습니다.
값에 의한 호출(Call by Value)
#include <stdio.h>
void swap(int a, int b) {
int temp;
temp = a;
a = b;
b = temp; //void 형으로 리턴문이 없지만 블록이 끝나면 종료, 원본 변수의 값이 바뀌지 않는다
}
int main() {
int a = 2;
int b = 10;
swap(a, b);
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
change 함수는 매개변수 a, b를 값으로 받기 때문에 함수 내에서 변수 교환이 일어나도 호출한 쪽 변수 a, b에는 영향이 없습니다.
위의 코드는 swap함수의 결과값이 반영되지 않습니다.
주소에 의한 호출(Call by Reference)
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a; // a가 가리키는 값을 temp에 저장
*a = *b; // b가 가리키는 값을 a가 가리키는 메모리에 저장
*b = temp; // temp 값을 b가 가리키는 메모리에 저장
}
int main() {
int a = 2;
int b = 10;
swap(&a, &b); // a와 b의 주소 전달
printf("%d\n", a); // 10 출력
printf("%d\n", b); // 2 출력
return 0;
}
포인터를 사용한 swap 함수 입니다.
포인터를 사용하면 함수에 변수의 실제 메모리 주소를 전달할 수 있습니다.
앞에서 와달리 변수 a와b를 다른주소로 복사하지 않고 직접 참조하고 값을 변경함으로
함수가 종료가 되어도 함수내부에서 일어난 연산 등이 변수 a,b 에 반영됩니다.
이러한 과정은 주소에 의한 호출(Call by Reference) 이라고도 부릅니다.
이렇게 하면 함수 내에서 변수의 복사본이 아니라 원본 데이터를 직접 수정할 수 있어 결과를 반영할수 있습니다.
또한 복사본대신 주소를 직접 참조 하기 때문에 메모리자원을 절약할수 있게됩니다.
5. 포인터의 변수 선언 그리고 & 연산자
포인터 변수는 데이터 타입(자료형) 과 별표(*) 그리고 변수 명을 순서 대로 나열합니다.
아래 코드는 정수형 변수와 정수형 포인터 변수의 선언을 나타낸 코드입니다.
주소 연산자와 scanf 사용법
/*포인터 선언 하기*/
int alpa; //정수형 변수
int *p1; //정수형 포인터 변수
/모두 유효한 포인터변수 선언방식/
int* p1;
int * p1;
int *p1;
int*pi;
포인터에서 주소연산자(&)는 변수의 주소를 반환합니다.
이말은 포인터를 즉 변수의 주소로 초기화 할 수있습니다.
처음의 작성했던 코드를 다시 보면 다음과 같습니다.
#include<stdio.h>
int main() {
int a = 10;
int *pi;
pi= &a;
return 0;
}
pi= &a;
포인터 변수 pi는 변수 a의 주소를 가리킴(초기화)
주소연산자(&) 와 비트연산자 AND(&)는 서로 다른 개념이니 혼동하지 않아야합니다.
ex)
a =1 , b=0;
a&b
Q) 뭔가 비슷한 걸 본거 같은데 scnaf함수에서 본거 같아요!
네 맞습니다. 주소 연산자 &는 변수의 메모리 주소를 반환합니다.
포인터에서의 주소연산자
#include <stdio.h>
int main() {
int a = 12; // 변수 선언 및 초기화
int *ptr = &a; // 포인터를 a의 주소로 초기화
printf("Value of a: %d\n", *ptr); // 포인터를 통해 변수 a의 값 출력
return 0;
}
포인터 초기화에서 포인터를 특정 변수(num)의 주소로 초기화 하는 데 사용되고
scnaf함수 주소 연산자 예제
#include <stdio.h>
int main() {
int num;
printf("Enter a number: ");
scanf("%d", &num); // num의 주소를 전달
printf("You entered: %d\n", num);
return 0;
}
주소 연산자 예제
scanf 함수에서 입력값을 저장할 변수(num)의 주소를 전달하는 데 사용됩니다. 7
포인터의 개념과 메모리 구조 그리고 포인터 변수선언, 주소연산자를 다루었습니다.
메모리 구조를 해당 게시글에 작성한 이유는 이후 포인터를 활용한 메모리 관리 및 자료구조에 대한 이해를 위해 작성했습니다.
다음 포스팅은 NULL 포인터, 상수 포인터 , 포인터 역참조 , void 함수에 대해 포스팅 해보겟습니다.
- Null은 아스키코드 0번으로 표기됩니다. ( 숫자 '0'은 아스키코드 48번 으로 혼동하지 않아야합니다 ) [본문으로]
- 사용하지 않는 프로그램이 메모리를 계속 점유하는 상황. [본문으로]
- 후입선출 구조형태라 생각하시면 됩니다. [본문으로]
- 보통 buffer 오버플로우라는 개념입니다만 스텍영역에서 자주 발생하기 때문에 스택 오버플로우라고도 부릅니다. 버퍼를 초과하는경우 초과된 데이터가 기존에 적제된 데이터를 덮어쓰는 현상입니다. [본문으로]
- garbage collecter (가비지 컬렉터) 기능 [본문으로]
- C언어는 기본적으로 값에 의한 호출 방식을 사용하기 때문에, 변수 값을 함수에 넘기면 복사본이 만들어집니다. 위코드에서 swap 함수 내부의 지역변수 a,b는 복사본 형태로 전달됩니다. 복사본은 함수의 종료와 함께 삭제 됩니다. [본문으로]
- 스택 메모리 영역에 저장되고 주소는 해당 스택 영역의 시작 주소 [본문으로]
'C 연습' 카테고리의 다른 글
c언어 포인터 2: 포인터 연산자 , 배열 포인터 , void 포인터 , 상수 포인터 , 2중포인터 (1) | 2025.05.09 |
---|