오늘은 C++에서 참조와 문자열에 대해서 공부한 내용을 정리해 보고자 한다.
참조는 값을 매개변수에 전달하는 방식이다. 이를 할 때 C에서는 포인터를 이용한 연산이 많이 있었다. 성능적인 면에서는 우수했을지 모르지만 연산이 복잡해지고 안전하지 않은 경우가 생긴다는 단점이 있었고, 그래서 자바에서는 포인터를 없애면서 쉽게 참조를 하게 되었다. 하지만 그러다보니 성능적으로 아쉬움이 많이 생겼다. 그래서 C++에서는 포인터도 사용하되 포인터가 필요 없을 때는 훨씬 더 안전하게 참조를 하는 방법도 생겼다.
어떤 데이터를 불러오는 것을 호출이라고 하는데, 참조할 때도 이 개념이 쓰인다. 호출은 크게 두 가지로 나누어서 생각할 수 있는데 값에 의한 호출(Call By Value)과 참조에 의한 호출(Call By Reference)이 있다. 이 두 가지 개념은 중요하니 차근차근 설명을 해 보려고 한다.
값에 의한 호출(C, C++, JAVA)
void swap(int arg1, int arg2)
{
int tmp = arg1;
arg1 = arg2;
arg2 = tmp;
}
int main()
{
int num1 = 10;
int num2 = 20;
swap(num1, num2);
}
두 변수에 들어있는 값을 바꾸는 예제이다. 값에 의한 호출은 매개변수의 값을 다른 메모리에 먼저 복사를 한다. 그리고 바꾼 뒤, 그 바꾼 값은 함수가 끝나면 메모리에서 사라지게 된다. 아래의 그림은 스택 메모리를 단순하게 도식화하여 값에 의한 호출이 어떤 식으로 일어나는지를 나타내보았다.
참조에 의한 호출(C)
void swap(int* arg1, int* arg2)
{
int tmp = *arg1;
*arg1 = *arg2;
*arg2 = tmp;
}
int main()
{
int num1 = 10;
int num2 = 20;
swap(&num1, &num2);
}
참조에 의한 호출은 C의 포인터 개념을 사용한다. 따라서 JAVA에는 없다. 그리고 C++에도 포인터는 있지만 뒤에서 더 좋은 방법으로 소개하고자 한다. 참조에 의한 호출을 하게 되면 해당 메모리의 주소값을 따로 저장하고, 그 주소값을 가지고 값의 원본을 바꾼다. 따라서 이렇게 값이 바뀌면 함수가 끝나도 메모리에는 바뀐 상태가 유지되는 것이다. 자바의 경우 원시타입은 값에 의한 호출, 객체 타입은 참조에 의한 호출을 한다.
한 문장으로 정리하면 값에 의한 복사는 메모리에서 사본을 고치고, 참조에 의한 복사는 메모리에서 원본을 고친다.
C++에는 참조(reference)를 통한 더 좋은 해결책이 있다.
// 참조는 별칭이다.
int number = 100;
int& reference = number;
// NULL이 될 수 없다.
int& reference = NULL; // error
// 초기화 중에 반드시 선언되어야 한다.
int& reference; // error
// 참조하는 대상을 바꿀 수 없다.
int number1 = 100;
int number2 = 200;
int& reference = number1;
reference = number2; // number1, number2, reference 모두 200이 됨.
참조를 사용하여 위의 swap 연산을 하게 되면 다음과 같이 할 수 있다. 이처럼 C++에서는 포인터를 쓰지 않고도 참조에 의한 호출을 쉽게 할 수가 있다는 장점이 있다.
void swap(int& number1, int& number2)
{
int tmp = number1;
number1 = number2;
number2 = tmp;
}
스트링(String)
기존에 우리가 보았던 문자열을 저장하는 방식은 다음과 같았다.
char line[256];
cin.getline(line, 256);
하지만 이러한 코드는 아래 두 경우에 대해서 작동하지 않는다.
- 아무것도 읽지 못했을 때
- 한 줄에 문자가 256자 이상일 때
따라서 이러한 문제에 대한 대안으로 std::string 클래스를 사용한다. std:string 클래스를 이용한 문자열은 길이가 증가할 수 있다.
std::string firstName;
std::cin >> firstName;
string 클래스를 사용하면 대입(Assignment)과 덧붙이기(Appending)가 가능하다. 문자열 합치기(Concatenation)와 비교 연산자(Relational)도 가능하다. C에서는 복잡하고 신경써야 할 것들이 많은 연산이 되게 단순하게 바뀌었다.
string firstName = "POPE";
string fullName = "John Doe";
// 대입
fullName = firstName;
// 덧붙이기
fullName += " Kim";
string 클래스에는 다양한 함수들이 내장되어서 유용하게 쓸 수가 있다.
- size(), length() : 문자열의 길이를 반환
- at() : 해당 문자열에서 주어진 위치에 있는 문자를 참조로 반환
- c_str() : 해당 string이 가지고 있는 문자 배열의 시작 주소를 가리키는 포인터를 반환
string line;
cin >> line;
const char* cLine = line.c_str();
이번에는 <sstream>에 대해서 알아보자. sstream은 string stream을 의미한다. string으로 들어와서 string으로 나가는 스트림이다. std::istringstream은 cin과 비슷하고, std::ostringstream은 cout과 비슷하다. 차이점은 키보드 대신, 콘솔 대신 string으로 입출력을 받는다는 점이다. 엄청 자주 쓰이지는 않는다. 현업에서는 C++ 어플리케이션에서 성능상의 이유로 C 함수를 사용한다.
그렇다면 왜 string 클래스는 이렇게 문자열의 길이를 지정해 주지 않아도 문제가 없는 것일까?
위의 그림을 보자. line이란 이름을 가진 string 매개변수를 하나 지정해 준다. 그러면 지역변수는 스택에, 메모리는 힙에 저장이 된다. 그리고 처음에 초기값은 "" 이므로 길이는 0, 그리고 길이가 4인 char 배열을 지정해 주었으므로(이는 언어마다 컴파일러마다 다르다) 용량은 15(하나는 NULL)라고 지정되었다.
여기서 어떤 값을 대입(녹색)하면, 필요한 만큼(이 예제에선 4 byte) 메모리를 복사해서 검은색 구간의 첫 번째 칸에 넣어준다. 그리고 어떤 값을 덧붙이면(파랑), 이와 같이 메모리가 부족한 경우 새로운 메모리를 잡는다. 기존의 한 칸 메모리를 새로운 메모리에 넣어준다. 그리고 포인터를 새로운 메모리로 향하게 해준다. 용량은 31(32-1)로 늘어났고, 길이도 더해준 문자열 길이만큼 추가해 준다. 이와 같이 처리를 해주기 때문에 문자열의 길이를 정해주지 않고도 메모리 할당을 자동적으로 할 수 있는 것이다.
참고자료
- C++ 언매니지드 프로그래밍, 포큐 아카데미
'Prog. Langs & Tools > C++' 카테고리의 다른 글
[C++] Ch06. OOP 3 (0) | 2020.10.19 |
---|---|
[C++] Ch05. OOP 2 (0) | 2020.10.01 |
[C++] Ch04. OOP 1 (0) | 2020.09.22 |
[C++] Ch03. 파일 입출력(I/O) (0) | 2020.08.26 |
[C++] Ch01. 입출력(I/O) (0) | 2020.08.03 |