시험공부

(2025-2) Introduction to programming(2) 3. Strings, Vectors, and Arrays

norepinephrine 2025. 12. 18. 14:47

수업일: 2025-09-16

목표: 문자열 다루기(문자 분류/대소문자 변환), 안전한 인덱싱(size_type), 범위기반 for(값/참조), vector(동적 배열/반복자/용량), 배열·포인터 선언 해석, C-스타일 문자열 상호운용을 직관적으로 이해하고 안전하게 코딩한다.


0) 큰 그림 한 장 요약

  • string = 안전한 “글자 상자(동적 길이)”; 길이는 부호 없는 정수
  • 문자 처리 = <cctype>: isalpha/isdigit/ispunct + tolower/toupper
  • 반복 = 범위기반 for (값 복사 ↔ 참조) / 전통 for (인덱스는 size_type)
  • vector = “늘어나는 배열”; push_back, 용량(capacity), 반복자
  • 배열/포인터 = 선언 해석(괄호 결합), 포인터 산술, end 관용구
  • 상호운용 = string.c_str() ↔ const char* (수명 주의)

1) 문자열(string)과 문자 처리: “문장을 재료로 가공하기”

1-1. string이 안전한 이유

  • C 문자열(char*)은 길이/경계 오류에 취약.
  • std::string은 길이 정보를 내부에 갖고 있어 경계 검사자동 메모리 관리가 쉬움.
+---------------------+
|  'H' 'e' 'l' 'l' 'o' |
+---------------------+
 size()=5, empty()? false

1-2. 문장부호 세기 & 대문자 변환 (두 가지 순회법 비교)

#include <iostream>
#include <string>
#include <cctype>   // 문자 분류/변환
using namespace std;

int main() {
    string s = "Hello, World!!!";

    // [A] 값으로 순회(원본 불변)
    size_t punct_cnt = 0;
    for (char c : s) { // c는 복사본
        if (ispunct(static_cast<unsigned char>(c)))
            ++punct_cnt;
    }
    cout << "punct=" << punct_cnt << "\\n"; // 4

    // [B] 참조로 순회(원본 수정)
    for (char& c : s) {
        c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
    }
    cout << s << "\\n"; // HELLO, WORLD!!!
}

왜 unsigned char로 캐스팅하나요?

  • toupper/ispunct는 문자 코드가 음수일 때 동작이 미정의일 수 있어요.
  • 관례적으로 static_cast<unsigned char>(c)로 안전 보정!

1-3. s[i] vs s.at(i) (안전성 차이)

string s = "hi";
cout << s[5];     // ❌ 미정의동작(크래시/쓰레기 출력)
cout << s.at(5);  // 예외 throw (out_of_range) → 잡아서 처리 가능

  • 성능이 중요하지 않다면 초보 단계에서 at()가 더 안전.

2) 안전한 인덱싱: decltype(s.size())와 string::size_type

2-1. 왜 굳이 size_type를 쓰나?

  • string::size()는 부호 없는 정수(보통 size_t)를 반환.
  • 인덱스/길이 변수도 같은 족보로 쓰면 경고/오류/비교함정을 줄임.
string s = "hello";
if (!s.empty()) {
    decltype(s.size()) i = 0;  // string::size_type과 동일
    s[i] = static_cast<char>(toupper((unsigned char)s[i]));
}

2-2. 빈 문자열 안전 패턴

if (!s.empty()) {
    // s[0] 접근 OK
}

  • 빈 문자열에서 s[0]은 절대 금지 → 미정의동작.

3) 범위기반 for: “값 복사 vs 참조” 눈으로 보기

3-1. 값 복사 → 원본 불변

string s = "abc";
for (char c : s) c = 'X'; // 복사본만 바뀜
cout << s;                // "abc"

3-2. 참조 → 원본 수정

string s = "abc";
for (char& c : s) c = 'X'; // 원본 수정
cout << s;                 // "XXX"

규칙:

  • 읽기만 할 거면 const char& (불필요한 복사 방지)
  • 수정할 거면 char&
  • 값 복사는 간단하지만 원본 안 바뀜에 유의

4) 전통 for + 인덱스: hexdigits 예제로 감 잡기

#include <iostream>
#include <string>
using namespace std;

int main() {
    const string hex = "0123456789ABCDEF"; // 길이 16
    int n;
    while (cin >> n) {
        if (0 <= n && n < static_cast<int>(hex.size()))
            cout << hex[n] << ' ';
        else
            cout << "(?) ";
    }
}

핵심

  • 인덱스 범위 체크는 반드시
  • hex.size()는 부호 없는 정수 → 비교 시 캐스팅으로 의도를 명확히

5) vector 한 번에 잡기: “늘어나는 배열”

5-1. 핵심 조작

#include <vector>
using namespace std;

vector<int> v;        // 크기 0
v.push_back(10);      // [10]
v.push_back(20);      // [10,20]

vector<int> a(3, 7);  // [7,7,7]  (크기 지정 + 채우기)
vector<int> b{1,2,3}; // 리스트 초기화

5-2. size vs capacity (그림)

logical size (size):  [  0  1  2  ]  = 3
physical capacity:    [  0  1  2  3  4  5  ]  = 6 (추가 여유)

  • size() = 실제 원소 수
  • capacity() = 재할당 없이 담을 수 있는 최대 원소 수
  • 많이 넣을 계획이면 reserve로 미리 확보
vector<int> v;
v.reserve(1000);   // 1000개 정도 들어갈 예정
for (int i=0;i<1000;++i) v.push_back(i);

5-3. 반복자와 수정(값/참조)

vector<int> v{1,2,3};
for (int x : v) x *= 2;     // 복사 → 원본 안 변함
for (int& x : v) x *= 2;    // 참조 → [2,4,6]

5-4. 반복자 무효화(invalidation) 주의

  • push_back/insert 중 재할당(capacity 증가) 이 발생하면 기존 반복자/참조/포인터가 무효화될 수 있음.
  • 안전 패턴:
    • 가능한 한 reserve()로 재할당을 줄임
    • 재할당 가능성이 있는 조작 후에는 반복자 다시 얻기

6) 반복자(iterator) 제대로 사용하기

#include <vector>
#include <string>
#include <iostream>
using namespace std;

int main() {
    vector<string> words = {"a", "bb", "ccc"};

    // 쓰기 가능한 반복자
    for (auto it = words.begin(); it != words.end(); ++it) {
        if (!it->empty()) {              // (*it).empty()와 같음
            (*it)[0] = toupper((unsigned char)(*it)[0]);
        }
    }

    // 읽기 전용 반복자
    for (auto it = words.cbegin(); it != words.cend(); ++it) {
        cout << *it << ' ';
    }
}

요점

  • it : 원소에 대한 참조(역참조)
  • it->member : (*it).member 축약
  • cbegin/cend : 읽기 전용 뷰 (함부로 바꾸지 않겠다는 약속)

7) 임의 접근(랜덤 엑세스) 반복자: +n, n, 차이 계산

vector<int> v{10,20,30,40,50};
auto it = v.begin();
cout << *(it + 2) << "\\n"; // 30
*(it + 3) = 999;
auto dist = (v.end() - v.begin()); // 5 (원소 개수)

  • string/vector는 랜덤 엑세스 가능(배열처럼 더하기/빼기)
  • list는 불가(양방향만)

8) 배열과 포인터 선언 해석: 괄호 결합 규칙만 알면 끝

8-1. 두 선언의 차이 (자주 낚이는 포인트)

int (*pa)[10];  // 'int[10]' 배열을 가리키는 '포인터'
int* ap[10];    // 'int*' (포인터) 10개로 이루어진 '배열'

  • 읽는 법: 식별자 주변에서 괄호 우선 → 그 다음 []/ 결합

8-2. 배열 ↔ 포인터 decay와 산술

int arr[3] = {10,20,30};
int* p = arr;           // = &arr[0]
*(p + 1) = 99;          // arr[1] = 99

8-3. end 관용구 (마지막 다음 위치)

int* beg = arr;               // &arr[0]
int* last = arr + 3;          // end(마지막 다음)
for (int* it = beg; it != last; ++it) {
    // *it 사용
}


9) string ↔ C-스타일 문자열: 다리 놓기

#include <string>
#include <cstdio>
using namespace std;

int main() {
    string s = "hello";
    const char* c = s.c_str();      // C 함수에 전달 가능
    // printf("%s\\n", c);

    // 반대 방향
    const char* name = "minji";     // 리터럴(읽기 전용)
    string t(name);                 // string으로 복사
}

수명 이슈(중요)

  • c_str()는 s가 살아 있고, 내용이 바뀌지 않는 동안만 유효.
  • s.push_back('!') 같은 변경이 일어나면, c는 더 이상 유효하지 않을 수 있음.

10) 입출력 자주 터지는 함정: >> 다음 getline

string token, line;
cin >> token;  // 여기서 개행문자(\\n)가 버퍼에 남음
// 바로 getline을 호출하면 빈 줄을 읽을 수 있음
cin.ignore(numeric_limits<streamsize>::max(), '\\n'); // 남은 줄 비우기
getline(cin, line);

외워두기: >> 뒤엔 ignore(...) 하고 getline.


11) 의사코드로 감각 잡기

11-1. 문장부호 세기 (의사코드)

count := 0
문자열 s의 각 문자 c에 대해:
    c가 문장부호면 count := count + 1
count 출력

11-2. 대문자 변환 (의사코드)

문자열 s의 각 문자 c에 대해 (참조로 순회):
    c := 대문자(c)
s 출력


12) 미니 실습(손으로 쳐보기)

(A) 각 단어의 첫 글자를 대문자로

#include <iostream>
#include <string>
#include <cctype>
using namespace std;

int main() {
    string line;
    getline(cin, line);

    bool newWord = true;
    for (char& c : line) {
        if (isspace((unsigned char)c)) {
            newWord = true;
        } else {
            if (newWord) c = (char)toupper((unsigned char)c);
            newWord = false;
        }
    }
    cout << line << "\\n";
}

(B) 숫자만 추출해 합계 구하기

#include <iostream>
#include <string>
#include <cctype>
using namespace std;

int main() {
    string s; getline(cin, s);
    long long sum = 0, cur = 0; bool inNum = false;

    for (char ch : s) {
        if (isdigit((unsigned char)ch)) {
            inNum = true;
            cur = cur * 10 + (ch - '0');
        } else {
            if (inNum) { sum += cur; cur = 0; inNum = false; }
        }
    }
    if (inNum) sum += cur;
    cout << sum << "\\n";
}


13) 예상문제(시험형) & 빠른 답

Q1. vector에서 push_back 후 기존 반복자가 무효화될 수 있는 이유?

A. 재할당(capacity 확장) 시 내부 버퍼가 새로 할당되어, 이전 메모리를 가리키던 반복자/참조/포인터가 죽기 때문.

Q2. 아래 선언을 말로 설명하라: int (*p)[10];

A.int 10개짜리 배열을 가리키는 포인터.”

Q3. 왜 toupper(c) 전에 (unsigned char)c로 캐스팅하나?

A. 음수 코드값으로 호출 시 미정의 가능 → 안전 보정.

Q4. string 인덱스 변수 타입은 무엇을 권장?

A. decltype(s.size()) 또는 string::size_type (부호 없는 정수, 구현 독립).

Q5. >> 뒤 getline이 빈 줄을 읽는 이유와 해결책?

A. 입력 버퍼에 남아 있던 개행문자 때문. cin.ignore(...)로 비우고 getline.


14) 체크리스트 (스스로 점검)

  • [ ] string 길이/인덱스는 부호 없는 정수로.
  • [ ] toupper/ispunct 호출 전 unsigned char 캐스팅.
  • [ ] 범위기반 for: 값/참조 차이 체감.
  • [ ] vector: size/capacity/reserve 개념 이해.
  • [ ] 재할당 후 반복자 무효화 주의.
  • [ ] 배열/포인터 선언을 괄호 결합 규칙으로 해석 가능.
  • [ ] c_str() 수명 규칙 숙지.
  • [ ] >> + getline 섞을 땐 ignore 패턴.

15) 연습예제

// 1) 안전한 대문자 변환
for (char& c : s) c = (char)toupper((unsigned char)c);

// 2) 안전한 인덱스
for (decltype(s.size()) i = 0; i < s.size(); ++i) { /* ... */ }

// 3) vector 미지수 입력 모으기
vector<int> v; v.reserve(1000);
for (int x; cin >> x; ) v.push_back(x);

// 4) 반복자로 접근/수정
for (auto it = v.begin(); it != v.end(); ++it) *it += 1;

// 5) 배열의 end 관용구
int arr[5]; int* beg = arr; int* last = arr + 5;

// 6) 선언 해석 예
int (*pa)[10]; // 배열 포인터
int* ap[10];   // 포인터 배열