TMI + Tip for ->, 메모리 유출, 레퍼런스, 디폴트 생성자, 벡터(vector)
* <C++로 배우는 자료구조와 알고리즘>(범한서적, 2013)을 참고하여 작성됨
1. TMI of "->" 연산자
p를 Passenger 구조체에 대한 포인터라 하자. 이러한 경우 *p는 실제 구조체를 참조하고, mealPref 필드와 같은 멤버들에 접근할 경우 (*p).meaPref와 같이 나타낼 수 있다. 이러한 구조체와 같은 복잡한 객체는 종종 동적으로 할당되기 때문에, C++에선 -> 연산자를 이용해 좀 더 간편하게 표현할 수 있게 했다.
pointer_name->member는 (*pointer_name).member와 같은 의미다.
2. delete&new와 메모리 유출
delete 연산자는 new 연산자로 생성된 객체에만 적용되어야 한다(원리를 생각해보면 당연함). Java가 자동 가비지 콜렉션을 제공하는 것과 대조된다. 동적으로 할당된 객체는 명시적으로 삭제해야 함!
만약 해당 객체를 미리 삭제하지 않고 p의 (주소)값을 바꾸면, 우리가 그 객체에 다시 접근할 수 있는 방법이 없다. 그러나 그 객체는 프로그램이 종료할 때까지 계속 존속하여 메모리를 차지하고 있다. 이렇게 동적 메모리에 접근할 수 없는 객체가 있는 것을 메모리 유출(memory leak)이라고 한다. 특히 많은 양의 메모리를 할당하고 삭제하는 프로그램에서 메모리 유출을 조심해야 한다. 메모리를 유출하는 프로그램은 종종 실제 메모리가 충분해도 메모리 부족으로 실행을 멈출 수 있다.
배열이 new로 할당되면 시스템 할당기(system allocator)라는 애가 배열의 첫 번째 원소를 가리키는 포인터를 반환한다. 그런거였구나... 코드는 아래와 같다. delete 연산자를 그냥 쓰지 않고 []를 끼워서 쓰는 것 주의.
char* buffer = new char[500];
buffer[3] = 'a';
delete [] buffer;
3. 레퍼런스
레퍼런스는 단순히 객체에 대한 다른 이름이다. 다만 포인터가 null도 될 수 있는 것과 달리 레퍼런스는 반드시 실제 변수를 가리켜야 한다. 또한 선언될 때 반드시 그 값이 초기화되어야 한다.
string author = "Samuel";
string& penName = author;
penName = "Mark Twain";
레퍼런스는 함수에 매개변수를 전달하는 데 많이 사용되고 함수로부터 결과를 반환하는 데도 많이 사용된다. 함수에서 값이 아닌 레퍼런스로 전달할 경우, 값 수정이 가능하다. 반면 by value일 경우엔 그렇지 않음.
함수로부터 정보를 반환받는 일은 권장되는 일이지만 함수의 매개변수를 변경하는 일은 그렇지 않다. 매개변수 자체를 전달하기보단 매개변수의 주소를 전달(즉 포인터를 전달한 뒤 진짜 값에 액세스하는 것)하여 변경하는 것이 더 좋다. 다만 레퍼런스 매개변수는 표기상의 부담이 적어서 좋고, 둘은 실질적으로 같은 결과를 만드는 방법이다.
레퍼런스 전달은 대형 구조체나 클래스를 전달하려고 할 때 가장 효율적이다(주소만 넘겨주면 되기 때문). 이런 것들은 복사본을 만들어 전달하려고 하는 방법으로 하는 것이 더 비효율적이기 때문.
대부분의 함수 매개변수들은 수정되지 않기 때문에 더 좋은 방법은 매개변수를 상수 레퍼런스로 전달하는 것이다. 이러한 선언은 컴파일러에게 매개변수가 레퍼런스로 전달되지만 함수는 그 값을 변경하지 못한다는 사실을 통보하는 것이다.
배열의 경우는 포인터를 명시적으로 반환하거나 벡터 타입 객체를 반환하는 것이 좋다. 배열은 함수에 전달될 때 그 첫번쨰 원소에 대한 포인터로 변환되기 때문에 어차피 값 변경 연산을 하면 실제값이 변경된다. 어차피 배열은 값으러 전달되지 않는다.
4. 디폴트 생성자(개념은 알지? 파이썬에서도 봤고)
다음과 같이 선언할 수 있다.
Passenger(const string& nm, MealType mp, const string& ffn = "NONE");
복사 생성자 개념도 있다. 이를 코드로 구현하면 아래와 같다.
Passenger::Passenger(const Passenger& pass) {
name = pass.name;
mealPref = pass.mealPref;
isFreqFlyer = pass.isFreqFlyer;
freqFlyerNo = pass.freqFlyerNo;
}
5. 벡터(STL)
크기를 바꿀 수 있는(resizeable) 배열.
STL은 각 객체가 어떠한 타입의 객체라도 저장할 수 있다.
벡터는 많은 점에서 표준 C++ 배열보다 우수하다.
먼저 배열에서처럼, 보통 색인 연산자를 사용해 개별 원소들을 색인할 수 있다. 보통 멤버 함수를 통해 접근할 수 있다. 특히 이렇게 할 경우 색인 범위를 확인하여 범위를 벗어났을 경우 에러 예외를 생성한다는 점이 장점이다.
두번째로, 표준 배열은 크기도 모르고 범위 확인도 불가능하다. 그러나 벡터 객체 크기는 멤버 함수 size()에 의해 주어지며 resize() 멤버 함수로 크기를 바꿀 수도 있다.
int i = //...
cout << scores[i]; // 인덱스, 범위가 확인되지 않음
buffer.at(i) = buffer.at(2*i); // 인덱스, 범위 확인
vector<int> newScores = scores; // scores를 newScores에 배정
scores.resize(scores.size() + 10); // 10개 점수 추가
6. 행렬을 벡터로 구현하기
2차 배열의 크기가 알려져 있지 않다면, 배열을 동적으로 할당해야 한다. n x m 행렬을 만드려고 하면, 동적 배열은 첫 번쨰 원소를 포인터로 제공하기에 각 행은 int* 타입으로 선언되어야 한다. 이 행렬은 행의 포인터로 이뤄진 배열이다. 따라서 행렬은 int**타입, 즉 정수의 포인터를 가리키는 포인터다.
int** M = new int*[n]; // 행 포인터의 배열을 할당
for (int i=0; i<n; i++)
M[i] = new int[m]; // i번째 행을 담당
(위 코드가 아직 이해 안되기는 함)
공간이 할당된 후에는 각 원소에 접근할 수 있다. 그리고 할당된 행렬을 반환하는 것은 위 과정의 역순이다. 먼저 할당된 각 행을 차례로 반환한다. 그 후 할당된 행 포인터 배열을 반환한다. 배열을 반환하므로 delete[] 명령어를 사용한다.
for (int i=0; i<n; i++)
delete[] M[i];
delete[] M;
그러나 이런 과정은 다소 복잡하다. 벡터 클래스는 행렬을 처리할 수 있는 더 우아한 방법을 제공한다. 위 방법과 동일한 접근법으로, 벡터의 벡터로써 행렬을 구현할 수 있다. 행렬의 각 행은 vector<int>로 선언된다. 행렬 전체는 행의 벡터 즉 vector<vector<int>>이다. 벡터의 각 원소를 초기화할 떄 사용할 값을 나타내는 m개 정수의 벡터 "vector<int>(m)".
vector< vector<int> > M(n, vector<int>(m));