.. Cover Letter

ㅇ 공부#언어/(C++)기초

(기초 C++) 4장. C++에서의 포인터 및 동적 생성 (string 클래스 활용)

BrainKimDu 2023. 2. 26. 15:08

주의 : 이 글은 C의 기초문법에 대해 상세하게 다루지 않습니다. (즉 C언어에서 배울 수 있는 기초내용은 생략합니다)

※ 명품 C++ Programming 의 책을 참고하여 개인적으로 정리한 글입니다. 
이 글의 목적은 해당 책의 내용을 인용하여 더 쉽게 이해하고자 정리하고, 더 쉬운 예제를 통해 이해하는 것을 목표로 하고 있습니다.
명품 C++ Programming의 예제문제와 실습문제가 정말 좋으므로, 깊게 공부하고 싶다면 책을 구매하는 것을 추천드립니다.
책의 저작권 등등 각종 권한은 출판사와 지은이/옮긴이에 있습니다.

- 출판사: (주)생능 출판사
- 지음: 황기태

명품 C++ Programming - YES24


C++ 에서의 객체 포인터

C언어에서 가장 어려운 부분은 역시 포인터입니다. C++에서도 이러한 포인터의 개념이 등장합니다. C++에서는 객체에대한 포인터 변수라고 합니다. 다음의 코드를 보면서 접근을 해봅시다.

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
};

int main() {
	animal dog;
	animal* cat;
}

dog는 animal의 객체이지만, cat은 뭘까요? point형의 객체 cat? 한 번 더 진행을 해봅시다.

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
};

int main() {
	animal dog;
	animal* cat;

	dog.setName("dog");
	cat.setName("cat");
}

이 코드에서 cat.setName("cat"); 은 오류를 발생시킵니다. 

현재 포인터 변수 cat은 아무 객체도 가리키고 있지 않기 때문에 해당 오류가 발생합니다.  cat은 객체에 대한 포인터 선언이라고 하며, 어떤 메모리를 할당 받았는데, 아직 어떤 객체를 가리키고 있지 않기 때문입니다.

일단 그냥 이대로 더 진행을 해봅시다. 포인터 변수 cat 으로 멤버를 접근할때 -> 와 같은 표현으로 접근합니다. 

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
};

int main() {
	animal dog;
	animal* cat;

	dog.setName("dog");
	cat->setName("cat");
}

다음을 실행해봅시다.

다음과 같은 오류가 발생하면서 컴파일이 진행되지 않습니다. 주의할 점이라면, Visual Studio에서는 실행을 하기 전까지는 이 오류를 잡아내지 못합니다. 

현재 메모리는 이런 상황입니다. cat은 아무것도 가리키고 있지 않은 상태입니다.
그래서 다음처럼 사용이 가능합니다. 변수 앞에 & 기호를 붙이면, 주소를 알아낼 수 있습니다. 다음처럼 코드를 수정하면

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};

int main() {
	animal dog;
	dog.setName("dog");

	animal* cat;
	cat = &dog;
	cat->setName("cat");
	cout << cat->getName();
}

cat 이  dog의 객체를 가리키게 됩니다. 그 상태에서 dog객체의 이름을 cat으로 수정했을 때 어떤 것을 출력할까요?

cat을 출력합니다. 그러면 dog 객체의 이름은 어떤 것을 가리킬까요?

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};

int main() {
	animal dog;
	dog.setName("dog");

	animal* cat;
	cat = &dog;
	cat->setName("cat");
	cout << cat->getName() << endl;
	cout << dog.getName();
}

dog 객체또한 cat을 출력합니다. 

 

한 가지 궁금증이 생겼습니다. 지금은 animal* cat 인데, 배운 개념으로는 객체에 대한 포인터 선언이라고 하여 Animal의 포인터 변수를 선언한 것이였습니다. 

그러면 animal이 아니라 다른 객체인데, 거기다가 주소를 넣어주면 어떻게될까요?

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};

class car {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};

int main() {
	animal dog;
	dog.setName("dog");

	car* kia;
	kia = &dog;
	kia ->setName("cat");
	cout << kia->getName() << endl;
	cout << dog.getName();
}

다음과 같은 오류를 뱉기 때문에 객체에 대한 포인터 변수는 그 객체가 아니면 그 객체를 가리키지 않습니다. 

 

 

C++ 에서의 객체 배열

배열과 마찬가지로 객체 또한 배열로 선언할 수 있습니다.

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

class animal {
	string name;
public:
	void setName(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};


int main() {
	animal dog[10];
	dog[0].setName("dog");
	cout << dog[0].getName();
}

 

객체 배열 선언에서 한가지 문제가 되는 것은 다음과 같은 코드일 것입니다.

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

class animal {
	string name;
public:
	animal(string name) {
		this->name = name;
	}
	string getName() {
		return this->name;
	}
};


int main() {
	animal dog[10]("dog");
	cout << dog[0].getName();
}

위의 코드에서는 기본생성자가 아니라 우리가 만든 생성자를 호출해서 10개의 배열 객체를 생성하고 싶은데, 

다음과 같은 오류를 발생하며 컴파일이 되지 않습니다. 그래서 가장 중요한건 객체 배열을 선언할 때는 기본생성자가 있어야합니다. 

그러면 객체 배열을 다루고 싶다면 어떻게 해야하는 걸까요? 이때 포인터를 활용해야합니다.

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

class animal {
	string name;
public:
	animal() {}

	void setName(string name) {
		this->name = name;
	}

	string getName() {
		return this->name;
	}
};


int main() {
	animal dog[10];
	animal* p;
	p = dog;

	for (int i = 0; i < sizeof(dog)/sizeof(dog[0]); i++) {
		p[i].setName("dog");
	}
	for (int i = 0; i < sizeof(dog) / sizeof(dog[0]); i++) {
		cout << p[i].getName() <<endl;
	}
}

다음처럼 p = dog 로 포인터 변수가 객체 배열을 가리키게 합니다. 한가지 다른 점은 변수일 때는 주소값을 넘겨주지만 변수일 때는 주소로 넘기는게 아닙니다. 

그리고 for문에서 sizeof(dog)/sizeof(dog[0]) 는 dog 객체배열의 크기를 dog 객체 한개의 크기로 나눠라 입니다. 결국 dog 객체의 length 가 나오게 됩니다.

그 후로부터는 p를 배열로을 사용하듯이 상용하면 됩니다.  

소멸될때는 가장 높은 인덱스부터 소멸됩니다.

 

객체배열에 서로 다른 생성자를 먹이는 방법으로는 다음과 같은 방법이 있습니다.

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

class animal {
	string name;
public:
	animal() {}

	animal(string name) {
		this->name = name;
	}

	string getName() {
		return this->name;
	}
};


int main() {
	animal dog[3] = {animal(), animal("dog"), animal("cat")};
	cout << dog[0].getName() << dog[1].getName() << dog[2].getName();
}

 

뭐 이런식이면 다차원 배열로 가능하죠.

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

class animal {
	string name;
public:
	animal() {}

	animal(string name) {
		this->name = name;
	}

	string getName() {
		return this->name;
	}
};


int main() {
	animal dog[2][3] = { {animal(), animal("dog"), animal("cat")},
						{animal(), animal("bird"), animal("human")}, };
	cout << dog[0][0].getName() << dog[0][1].getName() << dog[0][2].getName() << endl;
	cout << dog[1][0].getName() << dog[1][1].getName() << dog[1][2].getName() << endl;
}

 

 

C++ 에서의 동적 메모리 할당

C언어와 C++에서는 중요한 점이 하나 있습니다.  파이썬 같은 언어의 예시를 들면

list = []
list.append("hi")
list.append("hello")

print(list)

다음처럼 list의 길이를 정해주지 않아도 됩니다. 하지만 C와 C++의 경우 배열의 길이를 정해주지 않으면 컴파일이 되지 않습니다.  (C와 C++에서 후반부에 이러한 기능을 하는 Vector를 배웁니다. ) 

만약 배열의 길이를 모르는 상황이라면 어떻게 해야할까요? 그래서 등장한 개념이 동적 메모리 할당입니다. C언어에서는 memory allocate 의 약자인 malloc()을 사용합니다. C++에서는 new와 delete를 사용합니다. 이를 활용하는 코드를 한 번 확인해보겠습니다. 자바를 해보신 분이라면 친근한 모습일 겁니다.

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

class animal {
	string name;
public:
	animal() {
		this->name = "dog ";
	}

	animal(string name) {
		this->name = name;
	}

	string getName() {
		return this->name;
	}
};


int main() {
	animal* p = new animal[5];
	for (int i = 0; i < 5; i++) {
		cout << p[i].getName();
	}
}

(예시로는 animal 클래스의 객체로 했지만 동적할당은 대부분의 데이터 타입(int, float, 등등)이 가능합니다.)

하나 이상한 점이 있을 겁니다. 배열의 길이가 변한다고 했는데, 그건 도대체 어떤 경우인가요?

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

class animal {
	string name;
public:
	animal() {
		this->name = "dog ";
	}

	animal(string name) {
		this->name = name;
	}

	string getName() {
		return this->name;
	}
};


int main() {
	int n;
	cin >> n;

	animal* p = new animal[n];
	for (int i = 0; i < n; i++) {
		cout << p[i].getName();
	}
}

다음 코드를 보시면 배열의 개수를 입력받습니다. 입력받은 개수만큼 메모리를 할당하고 객체를 생성합니다.

그래서 우리가 수를 입력하면 그만큼 출력하는 것입니다. 이를 위해 만들어진게 동적 할당이라고 생각하시면됩니다.
(혹시나 코딩테스트를 생각하고 있다면 동적메모리 할당을 사용하지 말고 바로 Vector를 배우고 활용하는 것이 맞습니다.)

 

또한 동적으로 할당받은 객체를 지울 때는 delete로 지우면됩니다. 

animal* p = new animal;
delete p;

animal* p_list = new animal[100];
delete [] p_list;

배열일 경우 지우는 방법이 다르다는 것만 알고계시면됩니다. 

 

한가지 주의해야할 점은 다음과 같습니다.

int main() {
	animal* p = new animal[512];
	animal n;
	p = &n;
}

다음처럼 메모리를 할당 받은 후에 p가 n을 가리키게 하면 메모리가 누수됩니다. 그러므로 꼭 delete를 해주어야합니다. 

 

 

 

C++ string 클래스 활용

내가 모르는 것만 추가하면

- append로 문자열 뒤에 붙일 수 있음

- 문자열 삽입에는 insert를 사용함 (삽입할 인덱스, 문장)

- 문자열 대체는 replace를 사용함 (삽입할 인덱스, 어디까지, 문장)

- 문자열 삭제는 erase를 사용함 (시작 인덱스, 끝인덱스)

- 문자열 일부분 발췌는 substr()을 사용합 (시작 인덱스, 끝인덱스)

- 문자열 검색은 find를 사용함 (문장을 넣으면 검색, 뒤에 숫자는 검색 인덱스위치)

- 문자열을 int로 전환 stoi 를 사용함 

 

 


참고 문헌
Scott Douglas Meyers(2015). Effective C++. Protec Media(프로텍 미디어)
Robert C. Martin(2021).UML 실전에서는 이것만 쓴다(UML for Java Programmers). 인사이트
황기태(2021). 명품 C++ Programming. (주)생능출판사