1학년 / 2학기 / C++ / 3강

참조, 배열, 문자 배열, vector

참조, 배열, 문자 배열, vector

학습 흐름

참조 변수(별명) → 참조 초기화 규칙 → 참조 매개변수 (값 전달과 비교)
        ↓
1차원 배열(초기화, 크기, 함수 전달) → 배열 패턴(누적, 뒤집기, 이동)
        ↓
문자 배열과 '\0' → 2차원 배열 → 정렬과 탐색 → vector

1. 참조 변수

int score = 80;
int& ref = score;      // ref는 score의 별명
ref = 95;              // score도 95

참조 초기화 규칙 (시험 최빈출)

코드판정이유
int& r = a;OK변수에 연결
int& r;컴파일 오류참조는 선언할 때 반드시 대상을 지정
int& r = 5;컴파일 오류일반 참조는 리터럴(임시 값)에 연결 불가
int& r = a + 1;컴파일 오류식의 결과도 임시 값
const int& r = 5;OKconst 참조는 임시 값에도 연결 가능
const int c = 3; int& r = c;컴파일 오류const 변수를 수정 가능한 참조로 연결 불가
const int& r = a; r = 10;컴파일 오류const 참조로는 수정 불가
int& r = arr[1];OK배열 원소에도 연결 가능

참조는 다시 연결되지 않는다

int a = 1, b = 2;
int& r = a;
r = b;          // r을 b에 다시 연결하는 것이 아님! a에 b의 값(2)을 대입

2. 참조 매개변수

선언복사원본 수정넘길 수 있는 인수
void f(int x)함불가변수, 리터럴, 식 모두
void f(int& x)안 함가능수정 가능한 변수만
void f(const int& x)안 함불가변수, 리터럴, 식, const 변수 모두
void inc(int& n) { n++; }
 
int a = 1;
inc(a);        // OK, a = 2
inc(5);        // 컴파일 오류: 리터럴
inc(a + 1);    // 컴파일 오류: 식의 결과
const int c = 3;
inc(c);        // 컴파일 오류: const 변수

참조 매개변수는 값을 바꾸는 swap이나 여러 결과를 돌려줄 때 씁니다.

void swapValues(int& a, int& b) { int t = a; a = b; b = t; }
 
void divide(int x, int y, int& q, int& r) { q = x / y; r = x % y; }

다음 코드에서 컴파일 오류가 나는 줄을 모두 고르시오. 오류 줄을 지운 뒤의 출력을 쓰시오.

int a = 1, b = 2;
int& r1 = a;           // (1)
int& r2;               // (2)
int& r3 = 5;           // (3)
const int& r4 = 5;     // (4)
const int c = 3;
int& r5 = c;           // (5)
const int& r6 = a;     // (6)
r6 = 10;               // (7)
r1 = b;                // (8)
cout << a << ' ' << b << ' ' << r1 << ' ' << r4 << ' ' << r6;
정답 확인

오류: (2), (3), (5), (7).

  • (2) 참조는 초기화가 필수입니다.
  • (3) 일반 참조는 리터럴에 연결할 수 없습니다. (4)처럼 const 참조는 됩니다.
  • (5) const 변수를 수정 가능한 참조로 가리킬 수 없습니다.
  • (7) const 참조로는 값을 바꿀 수 없습니다.

출력: 2 2 2 5 2 — (8)의 r1 = b는 a = 2와 같습니다. r6도 a를 가리키므로 2입니다.

다음 출력을 쓰시오.

void hi(int& x, int y) {
    x = x * 2;
    y = y * 2;
}
 
int main() {
    int x = 3, y = 4;
    int& rx = x;
    hi(rx, y);
    cout << x << ' ' << y << '\n';
 
    int arr[3] = {1, 2, 3};
    int& e = arr[1];
    e = 9;
    for (int v : arr) cout << v << ' ';
}

그리고 x를 const int x = 3;으로 바꿀 수 있는지 설명하시오.

정답 확인
6 4
1 9 3

x는 참조로 전달되어 6이 되고, y는 값으로 전달되어 함수 안의 복사본만 8이 됩니다. e는 arr[1]의 별명입니다.

const int x = 3;으로 바꿀 수 없습니다. int& rx = x;는 const 변수를 수정 가능한 참조로 연결하므로 오류이고, hi의 첫 매개변수도 int&라서 const 변수를 넘길 수 없습니다.


3. 1차원 배열

선언과 초기화

코드결과
int a[5]; (함수 안)다섯 칸 모두 쓰레기 값
int a[5] = {}; 또는 {0}모두 0
int a[5] = {1, 2};{1, 2, 0, 0, 0} (나머지는 0)
int a[] = {4, 5, 6};크기 3으로 자동 결정
int a[3] = {1, 2, 3, 4};컴파일 오류 (초기값이 너무 많음)
int n = 5; int a[n];표준 C++에서는 불가(크기는 상수여야 함)
  • 인덱스는 0부터 크기 − 1까지입니다. a[5](크기 5)는 범위 초과 → UB입니다. 컴파일 오류가 아니며 실행 중 오류가 난다고 보장되지도 않습니다(족보에서는 "runtime error"로 분류하기도 함).
  • 배열 원소 개수 = sizeof(a) / sizeof(a[0]). sizeof(a)는 바이트 수입니다(int 5개면 20).
  • 배열끼리 대입할 수 없습니다. a = b;는 컴파일 오류입니다. 원소를 하나씩 복사해야 합니다.

배열을 함수에 넘기면 원본이 바뀐다

배열 매개변수 int a[]는 배열 전체를 복사하지 않고 원본 배열을 가리킵니다. 그래서 함수 안에서 a[i]를 바꾸면 호출한 쪽 배열이 바뀝니다. 대신 함수는 배열 크기를 알 수 없으므로 크기를 따로 넘깁니다.

void mod(int a[], int n) {               // 원본 배열을 바꿈
    for (int i = 1; i < n; i++)
        a[i] += a[i - 1];
}
void modv(int x) { x = 100; }            // 원소 하나를 값으로 받으면 복사본
 
int a[] = {1, 2, 3, 4};
mod(a, 4);
modv(a[0]);
// a = {1, 3, 6, 10}

다음 출력을 쓰시오.

int a[5] = {1, 2};
int b[] = {4, 5, 6};
int c[5] = {};
for (int v : a) cout << v << ' ';
cout << "| " << sizeof(a) / sizeof(a[0]) << ' '
     << sizeof(b) / sizeof(b[0]) << ' ' << sizeof(a) << ' ' << c[4] << '\n';

그리고 다음 두 줄을 각각 판정하시오. int d[3] = {1, 2, 3, 4}; / int x[3], y[3]; x = y;

정답 확인
1 2 0 0 0 | 5 3 20 0

int가 4바이트인 환경에서 sizeof(a)는 20입니다. 두 줄은 모두 컴파일 오류(초기값이 너무 많음 / 배열은 대입 불가)입니다.

자주 나오는 배열 패턴

// 최댓값 (첫 원소로 초기화!)
int mx = a[0];
for (int i = 1; i < n; i++) if (a[i] > mx) mx = a[i];
 
// 내적
int dot = 0;
for (int i = 0; i < n; i++) dot += a[i] * b[i];
 
// 뒤집기: 앞뒤를 바꾸며 가운데까지
for (int i = 0; i < n / 2; i++) { int t = a[i]; a[i] = a[n-1-i]; a[n-1-i] = t; }
 
// 왼쪽으로 한 칸 회전
int first = a[0];
for (int i = 0; i < n - 1; i++) a[i] = a[i + 1];
a[n - 1] = first;

int a[] = {3, 8, 1, 9, 4};에 "왼쪽으로 한 칸 회전"을 실행한 결과를 쓰시오. 반복 조건을 i < n으로 잘못 쓰면 어떤 문제가 생기는가?

정답 확인

8 1 9 4 3. 조건을 i < n으로 쓰면 마지막 반복에서 a[i + 1]이 a[5]가 되어 **범위를 벗어난 읽기(UB)**가 됩니다.


4. 문자 배열과 C 문자열

C 문자열은 마지막에 널 문자 '\0'(코드 0)이 있는 char 배열입니다. 문자열 리터럴 "Hi"는 'H', 'i', '\0' 세 칸입니다.

char s[] = "Hi C++";      // 문자 6개 + '\0' = 7칸
char t[10] = "ab";        // 10칸: 'a' 'b' '\0' 그리고 나머지는 '\0'
char w[5] = "Hello";      // 컴파일 오류: '\0'까지 6칸 필요
sizeof(s)문자 수(길이)
char s[] = "Hi C++";76
char t[10] = "ab";102

문자열 순회: '\0'을 만날 때까지

int len = 0;
while (s[len] != '\0') len++;          // 직접 구한 길이
 
for (int i = 0; s[i] != '\0'; i++) {   // 한 글자씩 처리
    char c = s[i];
    if (c >= 'a' && c <= 'z') s[i] = c - 'a' + 'A';   // 소문자 → 대문자
}

알파벳 빈도 세기 (Homework 3 패턴)

int count[26] = {0};                     // 반드시 0으로 초기화
for (int i = 0; s[i] != '\0'; i++) {
    if (s[i] >= 'a' && s[i] <= 'z') count[s[i] - 'a']++;
    else if (s[i] >= 'A' && s[i] <= 'Z') count[s[i] - 'A']++;
}

다음 출력을 쓰시오.

// (1)
char s[] = "Hi C++";
int len = 0;
while (s[len] != '\0') len++;
char t[10] = "ab";
cout << sizeof(s) << ' ' << len << ' ' << sizeof(t) << '\n';
 
// (2) 대문자, 소문자, 숫자, 그 밖의 문자 개수
char u[] = "Hello World 2026!";
 
// (3)
char v[] = "banana";
int cnt[26] = {0};
for (int i = 0; v[i] != '\0'; i++) cnt[v[i] - 'a']++;
for (int i = 0; i < 26; i++)
    if (cnt[i]) cout << char('a' + i) << cnt[i] << ' ';
 
// (4) 문자열 뒤집기
char w[] = "abcde";
int n = 0;
while (w[n] != '\0') n++;
for (int i = 0; i < n / 2; i++) { char c = w[i]; w[i] = w[n-1-i]; w[n-1-i] = c; }
cout << w;
정답 확인

(1) 7 6 10

(2) 대문자 2(H, W), 소문자 8, 숫자 4, 그 밖의 문자 3(공백 2개, !).

(3) a3 b1 n2

(4) edcba — 마지막 인덱스는 n - 1입니다. '\0'(인덱스 n)과 바꾸면 문자열이 빈 문자열이 됩니다.


5. 2차원 배열

2차원 배열은 행 × 열 표입니다. a[행][열]로 접근합니다.

int m[2][3] = {
    {1, 2, 3},      // 행 0
    {4, 5, 6}       // 행 1
};
cout << m[1][0];    // 4  (행 1, 열 0)

초기화 규칙

코드결과
int m[2][3] = {1, 2, 3, 4};행 순서로 채움: {1, 2, 3}, {4, 0, 0}
int m[2][3] = {{1}, {4, 5}};행마다 채움: {1, 0, 0}, {4, 5, 0}
int m[][3] = {{1}, {2, 3}, {4, 5, 6}};행 수만 생략 가능 → 3행
int m[2][] = {…};컴파일 오류: 열 수는 생략 불가

메모리는 행 우선(row-major)

m[2][3]은 메모리에 m[0][0] m[0][1] m[0][2] m[1][0] m[1][1] m[1][2] 순서로 한 줄로 저장됩니다. 그래서 중괄호 없이 {1, 2, 3, 4}를 쓰면 0행부터 채워집니다.

  • 행 수 = sizeof(m) / sizeof(m[0])
  • 열 수 = sizeof(m[0]) / sizeof(m[0][0])

순회 패턴

const int R = 3, C = 4;
int m[R][C] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
 
// 행 합: 바깥 = 행, 안쪽 = 열
for (int i = 0; i < R; i++) {
    int sum = 0;
    for (int j = 0; j < C; j++) sum += m[i][j];
    cout << sum << ' ';                 // 10 26 42
}
 
// 열 합: 바깥 = 열, 안쪽 = 행
for (int j = 0; j < C; j++) {
    int sum = 0;
    for (int i = 0; i < R; i++) sum += m[i][j];
    cout << sum << ' ';                 // 15 18 21 24
}
 
// 주대각선 (정사각 부분): m[i][i]
// 반대 대각선 (N×N): m[i][N-1-i]
// 아래 삼각형 (대각선 포함): i >= j
// 전치(행과 열 바꿔 출력): 바깥 j, 안쪽 i로 m[i][j]

함수에 넘기기: 열 크기는 반드시 적는다

const int COLS = 3;
void printM(int m[][COLS], int rows) {     // 첫 번째 [] 만 비울 수 있음
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) cout << m[i][j] << ' ';
        cout << '\n';
    }
}

다음 출력을 쓰시오.

// (1)
int m[2][3] = {1, 2, 3, 4};
int b[2][3] = {{1}, {4, 5}};
int c[][3] = {{1}, {2, 3}, {4, 5, 6}};
printM(m, 2);
printM(b, 2);
cout << sizeof(c) / sizeof(c[0]) << ' ' << sizeof(c[0]) / sizeof(c[0][0]) << '\n';
 
// (2)
int a[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int s = 0, t = 0;
for (int i = 0; i < 3; i++)
    for (int j = 0; j < 3; j++)
        if (i >= j) s += a[i][j];
for (int i = 0; i < 3; i++) t += a[i][2 - i];
cout << s << ' ' << t << '\n';
정답 확인

(1)

1 2 3
4 0 0
1 0 0
4 5 0
3 3

(2) 34 15 — i >= j는 대각선 포함 아래 삼각형(1 + 4 + 5 + 7 + 8 + 9 = 34), a[i][2 - i]는 반대 대각선(3 + 5 + 7 = 15).

int m[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};일 때

  1. 행 합, 열 합, m[i][i](i = 0~2)의 합을 쓰시오.
  2. 행과 열을 바꿔(전치하여) 출력하는 코드를 쓰고 출력 결과를 쓰시오.
  3. 다음 코드의 cnt를 쓰시오.
int k[2][3] = {{1, 2, 3}, {4, 5, 6}};
int cnt = 0;
for (int i = 0; i < 2; i++)
    for (int j = 0; j < 3; j++) {
        if (j == 1) continue;
        if (k[i][j] > 4) break;
        cnt += k[i][j];
    }
정답 확인

1. 행 합 10 26 42, 열 합 15 18 21 24, 대각선 합 1 + 6 + 11 = 18.

2.

for (int j = 0; j < 4; j++) {          // 원래의 열이 새 행
    for (int i = 0; i < 3; i++) cout << m[i][j] << ' ';
    cout << '\n';
}
1 5 9
2 6 10
3 7 11
4 8 12

3. 8 — 행 0: 1 더함, j = 1 건너뜀, 3 더함(4). 행 1: 4 더함(8), j = 1 건너뜀, 6 > 4라서 break(안쪽 반복만 종료).

2×3 행렬 A와 3×2 행렬 B의 곱 C(2×2)를 구하는 3중 반복문을 쓰고 C를 구하시오.

int A[2][3] = {{1, 2, 3}, {4, 5, 6}};
int B[3][2] = {{1, 0}, {0, 1}, {1, 1}};
int C[2][2] = {};
정답 확인
for (int i = 0; i < 2; i++)
    for (int j = 0; j < 2; j++)
        for (int k = 0; k < 3; k++)
            C[i][j] += A[i][k] * B[k][j];

C = {{4, 5}, {10, 11}}. 예: C[0][0] = 1·1 + 2·0 + 3·1 = 4. C를 {}로 0 초기화하지 않으면 쓰레기 값에 더해집니다.


6. 정렬과 탐색

버블 정렬: 이웃끼리 비교해 큰 값을 뒤로

한 패스가 끝날 때마다 가장 큰 값이 맨 뒤로 확정됩니다. 교환이 한 번도 없으면 이미 정렬된 것입니다.

for (int pass = 1; pass < n; pass++) {
    bool swapped = false;
    for (int j = 0; j < n - pass; j++)
        if (a[j] > a[j + 1]) { int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; swapped = true; }
    if (!swapped) break;
}

선택 정렬: 남은 것 중 최솟값을 앞으로

i번째 패스에서 a[i..n-1]의 최솟값을 찾아 a[i]와 한 번만 교환합니다.

for (int i = 0; i < n - 1; i++) {
    int mi = i;
    for (int j = i + 1; j < n; j++)
        if (a[j] < a[mi]) mi = j;
    int t = a[i]; a[i] = a[mi]; a[mi] = t;
}

이진 탐색: 정렬된 배열에서 절반씩

int lo = 0, hi = n - 1;
while (lo <= hi) {
    int mid = (lo + hi) / 2;
    if (a[mid] == key) return mid;
    if (a[mid] < key) lo = mid + 1;
    else hi = mid - 1;
}
return -1;
  1. {5, 1, 4, 2, 8}을 위 버블 정렬로 정렬할 때 패스마다의 배열을 쓰시오. 몇 번째 패스에서 끝나는가?
  2. {64, 25, 12, 22, 11}을 선택 정렬할 때 i마다 교환 후의 배열을 쓰시오.
  3. {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}에서 23을 이진 탐색할 때 [lo, hi]와 mid를 차례로 쓰시오.
정답 확인

1.

pass 1: 1 4 2 5 8
pass 2: 1 2 4 5 8
pass 3: 1 2 4 5 8   ← 교환이 없어 여기서 종료

2.

i=0: 11 25 12 22 64
i=1: 11 12 25 22 64
i=2: 11 12 22 25 64
i=3: 11 12 22 25 64

3. [0, 9] mid=4(16 < 23) → [5, 9] mid=7(56 > 23) → [5, 6] mid=5(23 발견). 비교 3번, 인덱스 5.

퀵 정렬과 힙 정렬은 Exercise 05에서 단계별로 추적합니다.


7. vector

#include <vector>
vector<int> v = {90, 85};
v.push_back(100);         // 끝에 추가
v.size();                 // 3
v.pop_back();             // 끝 원소 제거
v[0];                     // 범위 검사 없음
v.at(0);                  // 범위를 벗어나면 예외(out_of_range)
vector<int> z(5);         // 0으로 채운 5칸

다음 출력을 쓰시오.

void addV(vector<int> v)  { v.push_back(9); v[0] = 0; }
void addR(vector<int>& v) { v.push_back(9); v[0] = 0; }
 
vector<int> v = {1, 2, 3};
addV(v);
cout << v.size() << ' ' << v[0] << ' ';
addR(v);
cout << v.size() << ' ' << v[0] << '\n';
 
int a[] = {1, 2, 3};
for (int x : a) x *= 10;
cout << a[0];
for (int& x : a) x *= 10;
cout << ' ' << a[0];
정답 확인
3 1 4 0
1 10

addV는 복사본을 바꾸므로 원본은 그대로이고, addR은 원본에 9를 추가하고 첫 원소를 0으로 바꿉니다. 범위 기반 for도 int x는 복사본, int& x는 원본입니다.


핵심 정리

  1. 참조는 선언과 동시에 연결해야 하고, 다시 연결되지 않는다. r = b는 대입이다.
  2. 일반 참조(int&)는 리터럴·식·const 변수에 연결할 수 없다. const int&는 모두 가능하다.
  3. int& 매개변수는 원본을 바꾸고, 값 매개변수는 복사본만 바꾼다.
  4. 배열: 부분 초기화하면 나머지는 0, 초기화하지 않은 지역 배열은 쓰레기 값, 초기값이 크기보다 많으면 컴파일 오류, 범위 초과는 UB.
  5. 원소 개수 = sizeof(a) / sizeof(a[0]) (함수 매개변수에서는 불가). 배열끼리 대입 불가.
  6. 배열을 함수에 넘기면 원본이 바뀐다. vector는 값으로 넘기면 복사된다.
  7. C 문자열은 '\0'으로 끝난다. 배열 크기는 문자 수 + 1. 빈도는 count[c - 'a']++.
  8. 2차원 배열은 행 우선으로 저장되고, 함수 매개변수에서 열 크기는 필수다. a[i][j]의 i가 행, j가 열.
  9. 버블 정렬은 패스마다 최댓값이 뒤로, 선택 정렬은 최솟값을 앞으로 보낸다.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석