배열과 포인터 관계를 왜 알아야 할까?

'혼자 공부하는 C언어' 8-2강 도전 실전예제 / 10-1강 예제 코딩

by Younggi Seo






도전 | 대소문자 변환 프로그램

키보드로부터 문장을 입력받은 후에 대문자를 찾아 소문자로 바꾸는 프로그램을 작성합니다. 바뀐 문장과 바뀐 문자의 수도 함께 출력합니다.


issupper() 함수와 tolower() 함수 쓰면 쉽다.


앞의 소스코드처럼 아스키코드 값을 이용하면 ctype 함수를 사용해서 출력했을 때 연산속도를 3초가량 줄일 수도 있다. 하지마 배열 요소를 인자로 함수호출했을 때의 연산속도는?
gets 함수 없어졌다(deprecated). 그래서 fgets() 함수를 써서 시큐어 코딩할 수 있다. fgets( str, sizeof(str), stdin );





챕터 10 | 배열과 포인터


AI 시대에 C를 배워서 얻을 수 있는 게 있을까? (from ChatGPT)



① 컴퓨터가 실제로 작동하는 방식

C는 추상화를 제거한 언어입니다.


챗! GPT가 답변한 추상성이라는 개념을 모르면 C에서 포인터와 배열의 관계를 알 수 있을까? 변수의 집합(set)인 배열(List)의 이면에 '메모리 주소 값'이라는 추상적인 개념을 그릴 수 없으면, 쉽사리 배열과 포인터의 관계를 연결시킬 수 없음을 알게 된다. 정처기 실기 문제에서 가장 많은 비중을 차지하는 C언어 코딩 유형도 '포인터와 배열'이다.


예제 소스 코드

...

int ary[3] = {10, 20, 30}; // 배열 선언
int *pa = ary; // 포인터에 배열명 저장
int i;

*pa = 10; // 첫 번째 배열 요소에 10 대입
*(pa + 1) = 20; // 두 번째 배열 요소에 20 대입
pa[2] = pa[0] + pa[1]; // 대괄호를 써서 pa를 배열명처럼 사용

...


이때 배열명이 곧 배열의 첫 번째 요소의 '주소 값'이고, 포인터를 이용한 배열에서도 역시 포인터 변수가 곧 배열의 첫 번째 '주소값'이다. 이 말은 즉슨 포인터에서 주소를 반환하는 연산자 &가 배열명 내부에 이미 포함되어 있다는 것이다.


그럼에도 불구하고 배열명과 포인터의 차이는 도드라진다.


1) sizeof 연산(크기)의 결과가 다르다. 포인터는 32비트 시스템인 경우 포인터 하나의 크기인 4바이트이고, 배열은 선언한 사이즈에 따라 배열 전체 크기다.


2) 또 중요한 차이점은 포인터는 값을 바꿀 수 있는 *변수(l-value)이고, 배열명은 값을 바꿀 수 없는(다시 저장할 수 없는) 상수(r-value)다.


그래서 아래와 같이 ary(변수가 아니라 상수)에 증가 연산자 ++를 후위연산자로 넣으면 상수(constant)를 변경시키려고 하기 때문에 에러가 뜬다.

ary++;


쉽게 얘기하면 대입연산자 '='를 사이에 두고 왼편에는 변수가, 오른편에는 상수(프로그램 실행 간에 변하지 않는 값)만이 올 수 있다.

int ary[0] = 10;

ary 배열의 첫 번째 값에 상수 10을 넣었다.


*pa = 10;

포인터 변수 pa가 가리키는 배열명(ary)의 첫 번째 주소 값에 10을 넣었다. 앞의 문법과 동일한 의미.


ary = 10;

배열의 첫 번째 주소값을 의미하는 배열명 ary(상수)에 10을 넣으려고 하니, [상수 = 상수]처럼 대입이 불가다.

int 형으로 선언한 배열명 ary는 값이 변하지 않는 상수(변하지 않는 값)이므로 처음 초기화 이후 변경할 수 없다.


3) 아래와 같이 포인터 배열을 초기화하면, 각 문자열 상수를 선언한 포인터형 자료(비연속적인 메모리 영역)에 따로 보관할 수 있다.

char* pary[5] = {"dog", "elephant", "horse", "tiger", "lion");
// 포인터(형) 배열(명) pary(문자 자료형의 상수를 가리키는) 선언과 초기화

pary[0] = "dog";

pary[1] = "elephant";

pary[2] = "horse";

pary[3] = "tiger";

pary[4] = "lion"; // 이렇게 포인터 배열 요소마다 상수를 저장시키면, 각 포인터 배열마다 저장 // 된 상수의 크기에 맞춰 메모리 공간이 할당된다.


*pary라는 배열을 굳이 포인터를 사용해서 각 번지별 값을 출력시키는 까닭은 포인터는 변수이기 때문에, C언어에서 많이 쓰는 증가 연산자(++)를 써서 아래와 같이 한 번에 출력할 수 있기 때문이기도 하다. 하지만, 2차원 배열 요소들을 단순히 반복문에 통해 출력시키면, 아래의 처럼 출력된 문자 사이의 쓰레기 값(padding)처럼 다차원 배열의 낭비(*Memory Wastage)를 확인할 수 있다.

2차원 배열명을 캐릭터형 포인터 변수(pary)에 저장시킨 후, 중첩 반복연산문을 통해 출력하면 위와 같이 각 배열의 요소 사이에 메모리 낭비(padding)를 확인할 수 있다.






구글 제미나이를 통해 알아본 메모리 낭비와 메모리 누수 차이 답변


* Memory Wastage or Padding(메모리 낭비) vs. Memory Leakage(메모리 누수)


Memory Leak(누수)는 "메모리를 빌리고 반납하지 않아 영원히 잃어버린 상태"(이전 편)이고, Memory Wastage(낭비)는 "필요한 것보다 더 많은 공간을 차지하고 있는 비효율적인 상태"로 서로 다름.


1. Memory Wastage (메모리 낭비): 비효율적 구조


메모리는 정상적으로 관리되고 있지만, 시스템 아키텍처의 특성이나 잘못된 설계로 인해 실제 데이터보다 더 많은 공간을 잡아먹는 현상. 대표적인 케이스는 구조체 패딩(Padding)과 메모리 내부 단편화(Fragmentation).


1) 구조체 패딩 (Structure Padding)

CPU가 메모리를 더 빨리 읽기 위해(Alignment), 데이터 사이에 **의미 없는 빈 공간(Dummy Bytes)**을 끼워 넣는 현상입니다.


코드 예시 :

struct Wastage {

char a; // 1 byte

// --- 3 bytes padding (낭비) ---

int b; // 4 bytes

};

// 실제 데이터는 5바이트지만, 메모리에서는 8바이트를 차지함.

Use code with caution.


2) 내부 단편화 (Internal Fragmentation)

운영체제(macOS는 Unix)가 메모리를 특정 크기(예: 4KB) 단위로만 잘라서 줄 때 발생합니다.


상황: 100 바이트만 필요한데 OS가 최소 단위인 4096 바이트(4KB)를 줌.

결과: 3996 바이트는 쓰지도 못하고 낭비됨.



2. Memory Leak (메모리 누수): 통제 불능


프로그래머의 실수로 힙(Heap) 영역에 할당된 메모리의 주소(포인터)를 잃어버린 상황입니다. 프로그램이 종료되기 전까지 이 메모리는 시스템에 반환되지 않습니다.


코드 예시 :

void leak_function() {

int *ptr = (int*)malloc(sizeof(int) * 100); // 400바이트 할당

// free(ptr); // 실수로 해제하지 않음.

return;

// 함수가 끝나면서 지역변수 ptr(주소값)은 사라지지만,

// 힙 영역의 400바이트는 여전히 '사용 중'으로 표시됨. 접근 불가.

}

Use code with caution.




2차원 배열 vs. 포인터 배열의 차이를 원서(데닉스 리치 저, The C Programming Language)로 읽기


매거진의 이전글배열 예제 완성