1차원/다차원 배열, 문자열, 함수와 배열 인수
학습 목표
- 배열(array) 의 메모리 특성과 선언/초기화/순회 방법을 정확히 이해한다.
- 2차원/3차원 배열 의 행·열(면) 개념과 초기화 규칙을 익힌다.
- 문자열과 char 배열 의 관계(널 문자 '\0')를 정확히 다룬다.
- 함수 매개변수로 배열 전달 시의 규칙(첫 차원 생략, 포인터로의 decay)을 이해한다.
1. 배열 개념 핵심
- 배열(array): 동일 자료형의 값들을 연속적 메모리에 저장하고 하나의 이름으로 관리하는 변수.
- 요소 접근: 0부터 시작하는 인덱스로 접근 (a[0], a[1], …).
- 연속 저장이므로 순차 처리(합계, 최댓값, 탐색, 정렬)에 캐시 친화적이고 효율적.
주의: 범위를 벗어난 접근(Out-of-Bounds) 은 미정의 동작(UB) 이며, 다른 데이터 파손/보안 취약점으로 이어질 수 있음.
2. 1차원 배열
2.1 선언 형식
자료형 배열명[개수]; // 개수는 양의 정수 상수식
- 첨자 범위: 0 ~ 개수-1
- 배열명만 사용할 때는 배열 시작 주소의 의미(표현식 맥락에서 포인터로 decay)
예:
int a[5]; // a[0]..a[4]
double v[100]; // v[0]..v[99]
2.2 초기화 방법
(1) 선언 후 대입
int arr[4];
arr[0]=10; arr[1]=20; arr[2]=30; arr[3]=40;
(2) 선언과 동시에 초기화
int a1[4] = {10, 20, 30, 40}; // 정확히 4개
int a2[] = {10, 20, 30, 40}; // 크기 생략 → 4로 결정
int a3[4] = {10, 20}; // 나머지는 0으로 초기화 → {10,20,0,0}
int a4[4] = {0}; // 전부 0
// int e[3] = {1,3,5,7}; // 컴파일 에러: 원소 초과
(3) 전역/정적 배열의 디폴트 초기화
- 파일 전역 또는 static 배열은 명시 초기화 없으면 0으로 초기화.
2.3 순회·사용 예
#include <stdio.h>
int main() {
int data[10] = {10,23,5,9,22,48,12,10,55,31};
int max = data[0];
for (int i=1; i<10; ++i) {
if (max < data[i]) max = data[i];
}
printf("max=%d\n", max); // 55
}
3. 다차원 배열
3.1 2차원 배열
선언 형식
자료형 이름[행][열];
- 총 크기 = 행 × 열
- C는 행 우선(row-major): 한 행(row)이 메모리에서 연속.
예:
int a[2][3]; // 2행 3열
int b[2][3] = {1,2,3,4,5,6}; // 평평하게 나열해도 OK
int c[2][3] = {{1,2},{4,5,6}}; // 부족분은 0 채움 → {{1,2,0},{4,5,6}}
int d[][3] = {{1,2,3},{4,5,6}}; // 행 수 생략 가능(초깃값 개수로 결정)
사용 예
#include <stdio.h>
int main() {
int score[4][3] = {
{90,90,90},{80,80,80},{70,70,70},{60,60,60}
};
for (int i=0;i<4;i++){
int sum=0;
for (int j=0;j<3;j++){ sum += score[i][j]; }
printf("%d행 합계=%d\n", i, sum);
}
}
3.2 3차원 배열
선언 형식
자료형 이름[면][행][열];
- 총 크기 = 면 × 행 × 열
- 생략 규칙: 첫째 차원(면)만 생략 가능.
int a[2][3][4] = { /* ... 24개 값 ... */ };
int b[][3][4] = { /* ... */ }; // OK
// int c[2][][4] = { ... }; // 에러
// int d[2][3][] = { ... }; // 에러
4. 문자열과 char 배열
4.1 문자열 표현
- C 문자열 = 문자들의 연속 + 종료 널문자 '\0'
- 배열 크기는 문자열 길이 + 1 이상이어야 함.
4.2 선언·초기화
문자열 리터럴:
char name[] = "HONG GIL DONG"; // 자동으로 \0 포함, 크기는 14
char name2[16] = "HONG GIL DONG"; // 여유 공간 확보 권장
문자 나열:
char addr1[] = {'S','E','O','U','L','\0'}; // 문자열
char addr2[] = {'S','E','O','U','L'}; // \0 없음 → 문자열 아님(주의)
잘못된 예(버퍼 부족):
// char name[10] = "HONG GIL DONG"; // 문자열 길이 13 + '\0' → 14 필요 → 에러/오버런
4.3 입력과 출력 주의
#include <stdio.h>
int main() {
char s[50];
// scanf("%s", s); // 공백에서 끊김, 버퍼 오버런 위험
// 안전 대안(한 줄): fgets
if (fgets(s, sizeof(s), stdin)) {
// 개행 제거 등 후처리 가능
printf("%s", s);
}
}
- scanf("%s", s) 는 공백 전까지만 읽고 길이 제한 없음 → 오버플로우 위험.
- 실무에선 fgets 권장(필요 시 개행 처리).
5. 함수와 배열 인수 전달
5.1 1차원 배열 전달
- 함수 매개변수에서 배열은 첫 요소 포인터로 decay.
- 보통 배열 크기 n 을 함께 전달.
#include <stdio.h>
double average(int arr[], int n) { // int *arr 와 동일 시그니처
int sum = 0;
for (int i=0; i<n; ++i) sum += arr[i];
return sum / (double)n;
}
int main() {
int score[4] = {90,85,100,88};
printf("평균=%.1f\n", average(score, 4));
}
5.2 2차원 배열 전달
- 두 번째 차원(열의 크기) 는 반드시 고정되어야 함(컴파일러가 오프셋 계산 가능해야 하므로).
void score_sum(int gr[][5], int row, int column) { // 열=5 고정
int sum[2] = {0};
for (int i=0;i<row;i++)
for (int j=0;j<column;j++)
sum[i] += gr[i][j];
for (int i=0;i<row;i++) printf("sum[%d]=%d\n", i, sum[i]);
}
int main() {
int score[2][5] = {{10,20,30,40,50},{10,10,10,10,10}};
score_sum(score, 2, 5);
}
5.3 요약 규칙
- 1차원: int f(int a[], int n) 또는 int f(int *a, int n)
- 2차원: void g(int a[][C], int r, int c) 처럼 두 번째 차원 C는 상수
- 3차원: double h(double a[][Y][X], int n) 처럼 뒤쪽 차원들은 상수
- 범위 체크 책임: C는 배열 크기를 몰라서 호출/함수 내부에서 직접 체크해야 함.
6. 실전 주의사항·베스트 프랙티스
- 경계 체크: 모든 인덱스 접근 전후로 범위 확인 습관화.
- 초기화: 자동변수 배열은 초기화하지 않으면 쓰레기 값. 필요 시 {0} 로 초기화.
- 문자열 버퍼: 항상 +1(널 문자) 고려, 입력은 fgets 선호.
- 다차원 전달: 마지막 차원부터 크기 확정(컴파일러 오프셋 계산 규칙).
- 성능: 중첩 루프는 행 우선(row-major) 순서로 접근(캐시 효율 ↑).
7. 확인 문제(짧게)
- 아래 선언에서 합법/불법?
int a[][3] = {{1,2,3},{4,5,6}}; // ?
int b[2][] = {{1,2},{3,4}}; // ?
- 문자열 버퍼 길이 문제를 찾고 고치기:
char name[8] = "SEOUL KOREA"; // ?
- 2차원 배열 인수를 받는 올바른 시그니처는?
- void f(int m[][], int r, int c);
- void f(int (*m)[4], int r, int c);
- void f(int m[][4], int r, int c);
정답 힌트:
- 첫 줄 OK, 둘째 줄 에러. 2) 길이 12+1 필요 → 버퍼 확장. 3) 두 번째/세 번째가 올바름.
포인터 개념, 선언/참조, 포인터 연산
학습 목표
- 포인터(pointer) 를 “값이 아니라 주소를 저장하는 변수”로 정확히 이해한다.
- 주소 연산자 &, 간접참조(역참조) * 의 의미와 사용 위치를 구분한다.
- 초기화되지 않은 포인터의 위험과 자료형 일치의 중요성을 익힌다.
- 배열 ↔ 포인터 관계, 포인터 산술(증감·차) 규칙을 체화한다.
1) 포인터 기본 개념
- 일반 변수: “데이터 값”을 저장.
- 포인터 변수: “데이터가 저장된 메모리 주소”를 저장.
- 포인터가 가리키는 대상에 접근하려면 *pointer (간접참조, dereference).
int n; // 일반 변수: 값 저장
int *p = &n; // 포인터: n의 주소 저장
*p = 200; // p가 가리키는 주소에 200 저장 (즉, n = 200)
printf("%d\n", n); // 200
포인터를 사용하면 “주소를 통해 간접 접근”을 하므로, 참조 대상이 정확히 무엇인지(어떤 주소인지) 초기화가 매우 중요합니다.
2) 선언/참조와 연산자
2-1. 선언 형식
자료형 *이름; // 이 포인터는 '자료형' 크기/방식으로 메모리를 해석
- 포인터의 자료형은 “가리키는 대상의 자료형”과 일치해야 안전하게 동작합니다.
예:
int *pi; // int를 가리키는 포인터
float *pf; // float를 가리키는 포인터
void *pv; // 형 미정(범용) 포인터
2-2. 주소 연산자 & 와 간접참조 *
int a = 10;
int *p = &a; // &a: a의 주소
*p = 20; // *p: p가 가리키는 "그 메모리 칸" (a)에 기록
printf("%d\n", a); // 20
주의: * 는 선언에서 “포인터형”을 뜻하고, 표현식에서 “간접참조”를 뜻합니다.
3) 안전한 포인터 사용 패턴
3-1. 반드시 초기화
초기화되지 않은 포인터는 쓰레기 주소를 가리켜 미정의 동작(UB) 을 유발합니다.
int *p; // 초기화 안 함: 위험
// *p = 10; // UB: 임의 메모리 파손 가능
int x = 0;
p = &x; // 안전한 초기화
*p = 10; // OK
3-2. 자료형 일치
포인터의 형과 대상의 형이 다르면 읽기/쓰기 크기와 정렬이 달라져 오동작합니다.
float c;
int *ip = (int*)&c; // 피치 못할 캐스팅은 피하자 (정렬/표현 차이로 위험)
정상 예시:
float c, b = 20.0f;
float *fp = &c;
int a = 10;
*fp = (float)a * b; // 포인터형과 대상형 일치 → 안전
3-3. void * (범용 포인터)
- 어떤 형의 주소든 담을 수 있지만 직접 역참조 불가. 역참조 시 명시적 캐스팅 필요.
int a = 100;
char ch = 'b';
void *pv = &a;
printf("%d\n", *(int*)pv);
pv = &ch;
printf("%c\n", *(char*)pv);
4) 배열과 포인터의 관계
- 1차원 배열에서 배열명은 대부분의 표현식에서 첫 요소의 주소로 decay(자동 변환)합니다.
int a[5] = {10,20,30,40,50};
int *p = a; // == &a[0]
printf("%d\n", p[2]); // == *(p+2) == a[2] == 30
배열명 자체는 좌변이 될 수 없고(상수 주소), 함수 인수로 넘어갈 때는 대개 포인터로 변환됩니다.
5) 포인터 산술(Arithmetic)
5-1. 증감/덧셈/뺄셈
- p+1 은 바이트 1이 아니라 “가리키는 형의 크기”만큼 주소가 이동합니다.
- char* 는 1바이트씩, int* 는 sizeof(int) 바이트씩 이동.
char *pc = (char*)0x100; // 가정
pc+1 → 0x101
int *pi = (int*)0x100; // 가정, 32-bit int
pi+1 → 0x104
5-2. 배열 순회와 증감 연산자의 차이
int a[] = {10,20,30,40,50};
int *p = &a[0];
printf("%d\n", *p); // 10
printf("%d\n", *p++); // 10 (사용 후 p가 다음 요소로 이동)
printf("%d\n", *++p); // 30 (p를 먼저 이동한 뒤 역참조)
- *p++ : 후위 — *(p++) (역참조 후 포인터 증가)
- *++p : 전위 — *(++p) (포인터 증가 후 역참조)
5-3. 포인터 간 차이(difference)
- 같은 배열 내 요소를 가리키는 포인터끼리만 차이를 구하는 것이 정의됨.
int a[10], *p=&a[6], *q=&a[2];
printf("%ld\n", p - q); // 4 (요소 개수 단위의 차이)
- 포인터 합산은 불가, 전혀 무관한 영역끼리의 차이도 미정의 동작/의미 없음.
6) 자주 틀리는 포인트(실전 체크리스트)
- 초기화 안 된 포인터 사용 금지
- 선언 즉시 NULL 또는 유효한 주소로 초기화.
- 자료형 일치
- int* 로 float 를 다루지 말 것. 꼭 필요한 캐스팅은 최소화하고 구조를 재설계.
- 배열 경계 준수
- p+i 사용 시 항상 0 <= i < 길이를 보장. 범위 체크는 호출자/피호출자 중 한쪽에서 확실히.
- postfix vs prefix
- *p++와 *++p의 의미 차이를 정확히 구분.
- void*
- 역참조 전 정확한 형으로 캐스팅. (라이브러리 콜백/메모리 풀/제너릭 컨테이너에서 흔히 사용)
7) 예제 모음(슬라이드 재구성)
7-1. 포인터로 값 복사/쓰기
#include <stdio.h>
int main(void) {
int a=5000, b=0, c=0;
int *p = &a; // p → a
b = *p; // b = a
p = &c; // p → c
*p = 100; // c = 100
printf("a=%d, b=%d, c=%d\n", a,b,c); // 5000,5000,100
}
7-2. 포인터와 형 일치
#include <stdio.h>
int main(void) {
int a = 10; float b = 20.0f, c;
float *pf = &c;
*pf = (float)a * b; // OK
printf("%f\n", c); // 200.000000
}
7-3. void* 사용
#include <stdio.h>
int main(void) {
int n = 100; char ch = 'b';
void *pv = &n;
printf("%d\n", *(int*)pv);
pv = &ch;
printf("%c\n", *(char*)pv);
}
7-4. 배열 + 포인터 산술
#include <stdio.h>
int main(void) {
int a[] = {10,20,30,40,50};
int *p = a; // &a[0]
printf("%d\n", *p); // 10
printf("%d\n", *p++); // 10, p → a[1]
printf("%d\n", *++p); // p → a[2], 출력 30
printf("%ld\n", (&a[6])-(&a[2])); // 정의된 동작 아님(경계 밖) → 실제 코드는 금지
return 0;
}
위 마지막 줄처럼 배열 경계 밖 주소로 연산/비교는 금지(실전 코드에서는 하지 마세요).
8) 용어 메모
- UB: Undefined Behavior(미정의 동작)
- dereference: 간접참조(포인터가 가리키는 주소의 메모리에 접근)
- decay: 배열이 표현식 맥락에서 첫 요소 포인터로 변환되는 규칙
- row-major: 행 우선 메모리 배치
9) 빠른 확인 문제
- 아래 중 정상 동작만 고르세요.
a. 초기화 안 한 int *p; *p=7;
b. int a; int *p=&a; *p=7;
c. float f; int *p=(int*)&f; *p=7; - 결과를 예측하세요.
int a[]={1,2,3}; int *p=a;
printf("%d ", *p++); // ?
printf("%d ", *++p); // ?
- void *pv = &x; 를 역참조하려면 어떤 조치가 필요한가?
정답 체크:
- b만 안전. a는 UB, c는 형 불일치로 위험.
- *p++ → 1 출력 후 p가 a[1]로, *++p → p가 a[2]로 이동 후 3 출력.
- 정확한 형으로 캐스팅 후 역참조(예: *(int*)pv).
포인터·배열 심화, 포인터 배열, 이중 포인터
학습 목표
- 문자열 리터럴과 char* 의 불변성을 명확히 이해한다.
- 1차원/2차원 배열을 포인터 산술로 순회·접근하는 법을 익힌다.
- 포인터 배열(배열의 각 요소가 포인터) 의 활용처(“가변 길이 행들”)를 이해한다.
- 이중 포인터(T**) 의 의미와 대표적 사용 패턴(출력 매개변수, argv, 2D 가변 컨테이너)을 익힌다.
1) 문자열과 포인터
핵심
- "TEXT" 는 문자열 리터럴(읽기 전용 영역) 입니다. 이를 가리키는 char *p = "TEXT"; 에서 *p = 't'; 와 같은 수정은 금지(미정의 동작).
- 수정 가능한 문자열이 필요하면 배열에 담아야 합니다.
// (권장 X) 리터럴을 가리키는 포인터: 읽기 전용
const char *p = "COMPUTER"; // const 붙이면 의도가 명확
// *p = 'c'; // 금지!
// (권장) 수정 가능 버전: 배열에 복사
char s[] = "COMPUTER"; // 스택에 'C','O',...,'\0' 저장
s[0] = 'c'; // OK
용어 메모
- literal: 소스에 직접 적힌 상수 데이터
- read-only segment: 실행 중 수정 불가 메모리 영역
2) 1차원 배열과 포인터
배열명 ↔ 포인터
- 1차원 배열에서 배열명은 첫 요소의 주소로 decay 합니다(표현식 맥락).
- a[i] ≡ *(a + i) ≡ *(p + i) ≡ p[i] (단, p == a 또는 p == &a[0] 인 경우)
#include <stdio.h>
int main(void) {
int a[] = {10,20,30,40,50};
int *p = a; // == &a[0]
int b, c, d;
b = *p + *(p + 3); // 10 + 40 = 50
p++; // p → a[1]
c = *p + *(p + 3); // 20 + 50 = 70
d = *p + 3; // 20 + 3 = 23
printf("b=%d, c=%d, d=%d\n", b, c, d);
}
안전 규칙
- 경계 체크: 0 <= i < len 보장.
- 형 일치: int* 로 float 영역을 읽거나 쓰지 않기(미정의 동작).
- 초기화: int *p = NULL; 후 유효한 주소로 세팅.
3) 2차원 배열과 포인터
물리 메모리 배치
- C의 2차원 배열은 행 우선(row-major) 으로 1차원처럼 연속 배치됩니다.
- int a[rows][cols]; 에서 &a[i][j] == (&a[0][0]) + (i*cols + j).
#include <stdio.h>
int main(void) {
static int a[2][3] = { {1,2,3}, {4,5,6} };
int *p = &a[0][0]; // 평탄화 시작 주소
int rows = 2, cols = 3;
// 포인터 산술로 2D 순회
for (int i=0; i<rows; ++i) {
for (int j=0; j<cols; ++j) {
printf("%d ", *(p + i*cols + j));
}
printf("\n");
}
}
올바른 포인터 초기화 예
- int *pt = &a[0][0]; 또는 int *pt = a[0]; 는 OK
- pt = (int*)a; 는 경고 유발(타입 의도 모호). 명확한 주소로 초기화 권장.
4) 포인터와 배열의 관계 (호환적 표기와 차이)
- 호환적 표기: a[i] 와 *(a+i) 는 동치, p[i] 와 *(p+i) 도 동치.
- 차이: “배열명”은 좌변 불가(상수 주소), “포인터 변수”는 증감 가능.
int A[5] = {1,2,3,4,5};
int *p = A;
p++; // OK (다음 요소로)
*A; // OK (읽기)
A++; // 에러: 배열명은 상수 주소
- 메모리 확보 관점
- 배열: 정적/고정 크기를 한 번에 확보.
- 포인터: 필요 시점에 malloc/free 등으로 유동적 확보가 가능.
5) 포인터 배열 (Array of Pointers)
개념과 장점
- “포인터들을 요소로 가지는 배열”.
- 서로 길이가 다른 문자열들을 담을 때 2차원 배열보다 메모리 효율↑.
#include <stdio.h>
int main(void) {
// (2D 배열) 모든 행이 10칸씩 고정 → 짧은 문자열도 10칸 점유
char fixed[3][10] = {"KNOU", "Computer", "123456789"};
// (포인터 배열) 각 문자열 길이만큼만 실제 메모리 사용
const char *dyn[3] = {"KNOU", "Computer", "123456789"};
for (int i=0; i<3; ++i) {
// '\0' 만나면 종료
for (int j=0; dyn[i][j]; ++j) putchar(dyn[i][j]);
putchar('\n');
}
}
언제 쓰나
- “행 길이 가변” 데이터(문자열 목록, 파일 경로 목록 등).
- 함수로 문자열 리스트를 반환(또는 전달)할 때 인터페이스가 단순.
6) 이중 포인터(T**)
의미
- “포인터를 가리키는 포인터”.
- char **pp → pp는 “char* 를 저장하는 메모리”의 주소, *pp는 char*, **pp는 char.
#include <stdio.h>
int main(void) {
char a = 'A';
char *p = &a; // p: &a
char **pp = &p; // pp: &p
printf("**pp = %c\n", **pp); // A
}
대표 사용처 3가지
- 출력 매개변수(out-param): 함수가 포인터를 “설정”해 주어야 할 때
#include <stdlib.h>
#include <stdbool.h>
// 역할: 버퍼를 할당하여 결과 포인터를 돌려준다
// 매개변수:
// out_buf (out) : 할당된 메모리의 주소를 되돌려줄 포인터의 주소 (char**)
// out_n (out) : 길이
// 반환: 성공 여부
bool make_buffer(char **out_buf, size_t *out_n) {
const char *src = "hello";
size_t n = 6; // "hello\0"
char *buf = (char*)malloc(n);
if (!buf) return false;
for (size_t i=0;i<n;i++) buf[i]=src[i];
*out_buf = buf; // 호출자 포인터를 '설정'
*out_n = n;
return true;
}
- argv 스타일: 프로그램 인자 목록
- int main(int argc, char **argv) 에서 argv[i] 는 i번째 문자열(포인터).
- 즉, “문자열들에 대한 포인터들의 배열”을 가리키는 포인터.
- 가변 2D 컨테이너: char **lines 처럼 “행 포인터 배열”을 동적으로 구성.
용어 메모
- out-param: 호출자가 넘긴 포인터의 내용을 함수가 채워서 결과를 “돌려주는” 매개변수
- heap: 동적 메모리 영역(malloc/free)
7) 실전 체크리스트(오류 방지)
- 문자열 리터럴 수정 금지
- const char *p = "abc"; 로 선언해 의도 명확히. 수정 필요 시 char s[] = "abc";.
- 포인터 초기화 필수
- T *p = NULL; → 유효 주소로 세팅 후 사용.
- 경계 검증
- 1D: 0 <= i < n, 2D: 0 <= i < rows, 0 <= j < cols.
- 형 일치
- int* 로 float 메모리 접근 금지. 필요 시 안전한 변환·복사를 설계.
- 이중 포인터 out-param
- 함수 내부에서 *out = malloc(...) 했다면 호출자가 free(*out) 책임.
8) 빠른 연습 문제
- 다음 중 정상/권장만 고르세요.
a) char *p = "ABC"; p[0] = 'a';
b) const char *p = "ABC";
c) char s[] = "ABC"; s[0] = 'a'; - 2차원 배열 int a[3][4]; 에서 &a[2][1] 과 동치인 포인터 산술 식을 쓰시오.
(단, p = &a[0][0]; 로 가정) - 이중 포인터를 out-param 으로 받아 버퍼를 할당하는 함수의 “호출자 코드”에서 빠지기 쉬운 실수를 적고, 올바른 호출 예를 간단히 쓰시오.
정답 예시
- b, c (a는 리터럴 수정 시도라 금지)
- p + 2*4 + 1
- 실수: free를 안 함, 또는 NULL 검사를 안 함
정답 호출 예:
char *buf = NULL;
size_t n = 0;
if (make_buffer(&buf, &n)) {
// 사용
free(buf);
}
'C' 카테고리의 다른 글
| 함수와 기억 클래스 (0) | 2025.11.11 |
|---|---|
| 선택 제어문과 반복 제어문 (0) | 2025.11.11 |
| 입.출력 함수와 연산자 (0) | 2025.10.22 |
| 자료형과 선행처리기 (0) | 2025.10.13 |
| C 언어의 개요 (1) | 2025.08.27 |