수업일: 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]; // 포인터 배열
'시험공부' 카테고리의 다른 글
| (2025-2) Introduction to programming(2) 6. Classes (0) | 2025.12.18 |
|---|---|
| (2025-2) Introduction to programming(2) 5. Functions (0) | 2025.12.18 |
| (2025-2) Introduction to programming(2) 4. Expressions & Statements (0) | 2025.12.18 |
| (2025-2) Introduction to programming(2) 2. Variables, types (pointer, references,string) (0) | 2025.12.18 |
| (2025-2) Introduction to programming (2) 1. Introcudtion (0) | 2025.12.18 |