728x90

C++에서 배열을 선언하고 초기화를 해주지 않으면 배열은 쓰레기 값으로 채워져있다.

 

아래의 코드를 실행해보면

#include <iostream>
using namespace std;

int main() {
	int arr[5];
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

 

이렇게 쓰레기 값으로 채워져있는 것을 볼 수 있다.

따라서 배열을 선언하면 초기화를 꼭 해주는 것이 좋다.

 

배열 arr의 원소를 모두 0으로 초기화하고 싶다면

int arr[5] = {};

이렇게 해주면 된다.

#include <iostream>
using namespace std;

int main() {
	int arr[5] = {};
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

위 코드의 실행 결과

배열 arr의 원소 5개가 모두 0으로 초기화된 것을 볼 수 있다.

 

만약에 배열의 0번째 원소만 10으로 초기화하고 나머지는 다 0으로 초기화하고 싶다면?

int arr[5] = { 10 };

또는

int arr[5] = { 10, };

이렇게 해주면 된다.

여기에서 주의할 점은 위 코드는 모든 원소를 10으로 초기화하는 것이 아니라 첫 번째 원소만 10으로 초기화하는 것이라는 것이다. 이 부분을 잘못 알고 있기 쉬우므로 꼭 기억하자.

#include <iostream>
using namespace std;

int main() {
	int arr[5] = { 10 };
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

위 코드의 실행 결과

첫 번째 원소만 10으로 초기화된 것을 볼 수 있다.

 

첫 번째 원소를 10으로, 두 번째 원소를 3으로, 나머지는 0으로 초기화하고 싶다면

int arr[5] = { 10, 3 }

이렇게 하면 된다.

 

#include <iostream>
using namespace std;

int main() {
	int arr[5] = { 10, 3 };
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

위 코드의 실행 결과

첫 번째 원소가 10, 두 번째 원소가 3으로, 나머지는 0으로 초기화 된 것을 볼 수 있다.

 

그런데 만약 모든 원소를 0이 아닌 특정 값으로 초기화하고 싶다면?

배열의 크기가 5 정도로 작다면

int arr[5] = { 10, 10, 10, 10, 10 }

이렇게 직접 초기화 해줄 수 있지만 배열의 크기가 1000, 10000, 100000 등 큰 수라면 이렇게 직접 초기화 하기에는 무리가 있다.

 

그럴 때 일반적으로 이렇게 반복문으로 초기화를 시킨다.

#include <iostream>
using namespace std;

int main() {
	int arr[5];
	for (int i = 0; i < 5; i++) {
		arr[i] = 10;
	}
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

이렇게 10으로 초기화된 것을 볼 수 있다.

 

하지만 함수를 이용해서 더 편리하게 초기화할 수 있는 방법이 있다.

바로 <algorithm> 헤더에 있는 fill 함수이다.

http://www.cplusplus.com/reference/algorithm/fill/

 

fill - C++ Reference

12345678 template void fill (ForwardIterator first, ForwardIterator last, const T& val) { while (first != last) { *first = val; ++first; } }

www.cplusplus.com

fill 함수를 사용하려면 일단 <algorithm> 헤더를 포함해줘야 한다.

사용 방법은

fill(초기화 시키고 싶은 부분의 시작 주소, 초기화시키고 싶은 부분의 끝 주소, 초기화할 값);

이렇게 사용하면 된다.

배열이라면

fill(arr, arr + 5, 10);

이렇게 하면 되고

 

벡터라면

fill(v.begin(), v.end(), 10);

이렇게 해주면 된다.

 

아래 코드를 실행하면

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

int main() {
	int arr[5];
	fill(arr, arr + 5, 10);
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

이렇게 모든 원소가 10으로 초기화되어 있는 것을 볼 수 있다.

 

for(int i = 0; i < 5; i++) {
	arr[i] = 10;
}

이 for문을 사용한 코드와

fill(arr, arr + 5, 10);

이 한 줄은 같은 역할을 한다.

 

fill(arr, arr + 2, 10);
fill(arr + 2, arr + 5, 20);

이렇게 부분 부분씩 초기화시켜줄 수도 있다.

 

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

int main() {
	int arr[5];
	fill(arr, arr + 2, 10);
	fill(arr + 2, arr + 5, 20);
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

전체 코드이다.

위 코드의 실행 결과

 

벡터도 마찬가지이다.

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

int main() {
	vector<int> v(5);
	fill(v.begin(), v.end(), 10);
	for (int i = 0; i < 5; i++) {
		cout << v[i] << " ";
	}
	cout << endl;
	return 0;
}

이 코드를 실행하면

이렇게 10으로 초기화된다.

 

하지만 벡터는 굳이 fill 함수를 안 쓰고

vector<int> v(5, 10);

이렇게 선언과 동시에 10으로 초기화하는 것이 더 편할 것이다.

(벡터 초기화 및 사용법↓)

https://breakcoding.tistory.com/139

 

[C++] 라이브러리, 벡터 클래스, 동적 할당

만약에 입력으로 n을 입력받아 크기가 n인 배열을 만들고 싶다면 어떻게 해야 할까? int n; cin >> n; int arr[n]; 이렇게 하면 될까? 이렇게 선언하면 컴파일 에러가 나는 것을 볼 수 있다. 배열의 크기를 나타..

breakcoding.tistory.com

 

728x90
728x90

map은 데이터를 key-value 형태로 저장하는 자료구조이다.

http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

 

map을 선언하는 방법은 다음과 같다.

map<int, int> m;

map은 템플릿 클래스이기 때문에 선언할 때 <> 사이에 key 값의 타입, value 값의 타입을 순서대로 적어줘야 한다.

key 타입과 value 타입이 같지 않아도 된다.

map<int, string> m;

이렇게 타입이 달라도 상관없다.

 

이렇게 선언된 map에 데이터를 저장하고 싶다면

m[1] = "one";

이렇게 하면 된다.

크기가 확보된 것도 아닌데 빈 map 객체인데도 이렇게 인덱스로 저장이 가능하다.

벡터의 경우에 데이터를 추가하고 싶다면 push_back() 함수를 써야 하고 set의 경우에는 insert() 함수를 써서 데이터를 추가할 수 있었는데 map은 원래 1이라는 인덱스가 있는 것처럼 저렇게 key 값을 [ ] 사이에 적고 그 키 값에 해당하는 value 값을 대입해주면 된다.

다만 주의할 것은 map이라는 자료구조는 key를 통해서 원하는 값을 꺼내오는 것이기 때문에 key 값은 유일한 값이어야 한다. 즉 key 값들 끼리는 중복이 없어야 한다. 다음 코드를 보자.

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


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	return 0;
}

이런 코드가 있을 때 이 코드를 실행하면

이렇게 철수와 영희의 나이가 제대로 출력된 것을 볼 수 있다.

 

하지만 10살짜리 김철수라는 동명이인이 있어서 map에 이 김철수의 데이터를 추가하면 어떻게 될까?

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


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	m["김철수"] = 10; //새로 추가된 10살짜리 김철수
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	cout << "철수의 나이: " << m["김철수"] << endl;
	return 0;
}

 

새로운 김철수가 추가된 것이 아니라 원래 있던 15짜리 철수의 나이가 10살로 바뀐 것을 볼 수 있다.

따라서 map을 사용할 때 key는 중복될 수도 있는 데이터가 아니라 unique한 데이터를 key 값으로 하는 것이 좋다. 만약 이 예제처럼 했다면 나도 모르게 이런 실수를 하고는 이상한 결과가 나온다고 코드를 붙잡고 끙끙댈 수가 있다.

 

이제 map 객체 선언 방법과 map에 데이터를 추가하는 방법을 알았으니 유용한 함수들을 알아보자.

+size(): size_type map에 저장된 쌍의 개수를 반환한다.
+empty(): bool map이 비어있는지 아닌지 반환한다.
+find(k: key_type): iterator 키값인 key가 있으면 그 iterator 반환, 없으면 end() 반환
+count(k: key_type): size_type k라는 키값을 가진 key의 개수를 반환한다. (0또는 1 반환)
+insert(pair<key_type, value_type>) pair의 first 값을 key로, second 값을 value로 가지는 쌍을 map 객체에 추가
+erase(k: key_type): size_type 해당 키 값을 가진 데이터를 삭제한다.
+erase(position: iterator): iterator 해당 iterator 위치에 있는 원소를 삭제한다.
+begin(): iterator map 객체의 시작 주소를 반환한다.
+end(): iterator map 객체의 가장 끝 원소의 다음 주소를 반환한다.

count() 함수는 사실 find() 함수로 대체할 수 있다. count() 함수는 해당 키 값을 가진 key의 개수를 반환한다고는 하지만 key 값들은 서로 중복이 없는 유일한 값이기 때문에 많아봤자 최대 1을 반환한다.

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


int main() {
	map<string, int> m;
	cout << "m의 크기: " << m.size() << endl; // 0 출력
	cout << "m은 비어있습니까? " << (m.empty() ? "true" : "false") << endl; //true 출력
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "m의 크기: " << m.size() << endl; //2 출력
	m.insert(make_pair("홍길동", 40));
	cout << "홍길동의 나이: " << m["홍길동"] << endl; //40 출력
	cout << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	map<string, int>::iterator iter = m.find("김철수");
	if (iter != m.end()) cout << "김철수의 나이: " << iter->second << endl; // 주소는 -> 연산자를 통해 접근. (*iter).second도 가능
	cout << ((m.find("김민지") != m.end()) ? "m에 김민지 있습니다." : "m에 김민지 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	m.erase(iter); //iterator를 이용해 삭제
	cout << "삭제 후: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	m.erase("홍길동"); //key 값으로 삭제
	cout << "삭제 후: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	return 0;
}

이 코드를 실행하면 다음과 같은 결과가 나온다.

 

함수들의 사용법은 매우 간단하기 때문에 이 예제 코드와 실행 결과만 봐도 알 수 있을 것이라 생각된다.

 

그럼 이제 map에 저장된 모든 데이터 쌍을 순회하는 법을 알아보자.

배열이나 벡터 같은 경우는 for(int i = 0; i < n; i++) 이 문장으로 모든 것이 가능했다. 이렇게 하고 for문 안에서 i를 인덱스로 해서 그 인덱스에 해당하는 원소에 접근할 수 있었다. 하지만 map은 인덱스로 접근이 불가능하고 key 값으로 접근이 가능한 자료구조이기 때문에 순회를 하려면 iterator를 사용해야 한다.

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


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	m["홍길동"] = 40;
	for (map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++) {
		cout << "이름: " << iter->first << ", 나이: " << iter->second << endl;
	}
	return 0;
}

for(int i = 0; i < n; i++) 대신에 for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)를 쓰는데

for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)

이 코드 꼭 알아두자.

iterator는 주소이기 때문에 iter.first가 아니라 iter->first 또는 (*iter).first로 접근이 가능하다.

 

예를 들어 하나의 원소가 8바이트인 map 객체가 있는데 3개의 쌍이 들어있고 map의 시작 주소는 1000이라고 하자.

그러면 첫 번째 원소는 1000번지~1007번지 이렇게 8바이트에 걸쳐서 저장되어 있다.

두 번째 원소는 1008번지~1015번지에, 세 번째 원소 즉 마지막 원소는 1016번지~1023번지에 저장되어 있을 것이다.

iterator 변수인 iter를 iter++를 하면 iter가 가지고 있는 주소는 8바이트씩 증가한다.

즉 처음에 iter=m.begin() 이렇게 선언되었을 때 iter는 1000, for문을 한 번 돌고 나면 iter++에 의해 1008이 되고, 그 다음 for문을 돌면서 1016이 된다. 그리고 iter++에 의해서 1024가 된다. 이 1024가 바로 m.end()이다.

그렇게 for문을 돌면서 순회하다가 for문에서 조건 체크를 한다. iter != m.end()이어야 for문의 body 부분으로 들어갈 수 있는데 1024는 m.end()이다. 따라서 for문을 빠져나가게 되는 것이다.

포인터에 대해 익숙치 않은 분이라면 https://breakcoding.tistory.com/81

 

[C++] 포인터

포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다. 하지만 전혀 어렵지 않다는 것을 알게 되었고 다른 사람들에게도 포인터가 어렵지 않다는 것을 알려주고 싶다. 일반적으로 변수는 정수, 실수,..

breakcoding.tistory.com

이 글을 먼저 보고 오시는 것을 추천드린다.

 

그래서 아까 find() 함수를 사용할 때에도

if(m.find("홍길동") != m.end())

이런 식으로 사용했던 것이다. find() 함수는 iterator가 map을 순회하면서 find()의 매개변수로 들어온 그 key 값을 가진 원소가 있는지 찾는 것인데 m.end()까지 탐색해보고 없으면 멈추는 것이기 때문이다.

(선형 탐색을 하는 것처럼 말했지만 사실 map은 항상 정렬되어 있기 때문에 선형탐색이 아니라 이진탐색을 해서 find() 함수의 시간 복잡도는 로그 복잡도를 가진다.)

 

STL 라이브러리를 알아두면 굉장히 유용하므로 자주 쓰이는 함수, 순회하는 방법은 꼭 알아두자.

더 많은 함수를 알고 싶다면 http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

이 사이트에 직접 들어가서 보는 것을 추천한다.

728x90
728x90

-플랫폼 독립적이다 (Platform Independent)

 

플랫폼이란? 운영체제 + 하드웨어를 말함

플랫폼 독립적이라는 것의 의미는 한 번 짠 그 코드는 이 컴퓨터에서도 돌아가고 운영체제와 CPU와 전혀 상관없이 다른 컴퓨터에서도 똑같이 잘 돌아간다는 것을 의미한다. CPU와 운영체제가 서로 다른 컴퓨터에서도 말이다.

 

따라서 플랫폼 종속적인 프로그래밍 언어들은 운영체제에 따라, 하드웨어(CPU)에 따라서는 그 코드가 돌아가지 않을 수도 있다는 불편함이 있다. 이것을 이식성(portability)이 낮다고 말한다. (이식성이 높은 프로그램은 이 컴퓨터에서도, 저 컴퓨터에서도 잘 돌아가는 것을 말한다.)

 

①하드웨어에 따라 왜 다르냐?

 

하드웨어 아키텍처마다 사용하는 기계어 종류가 다름. ex) 인텔 CPU와 AMD CPU는 사용하는 기계어가 다름.

그런데 실행코드는 기계어로 되어 있음. 기계(CPU)에게 일을 시켜야 하는데 CPU는 기계어 밖에 못 알아들으니까.

따라서 당연히  다를 수밖에 없음

 

②운영체제에 따라 왜 다르냐?

 

 ②-⑴운영체제가 사용하는 API 형식이 다름.

 API(Application Programming Interface)란? 응용프로그램이 운영체제의 기능을 사용하고 싶을 때 (운영체제에게 일을 시키고 싶을 때) 프로그래밍 언어를 통해서 운영체제에게 특정 기능을 요청할 수 있는 함수라고 할 수 있음.

 응용프로그램도 운영체제가 제공하는 API 함수를 이용해서 간접적으로 하드웨어에 접근할 수 있는거임. 운영체제만이  하드웨어를 직접 제어하기 때문.

 

따라서 컴퓨터 하나는 운영체제가 리눅스이고 또 다른 하나는 윈도우즈라면 당연히 운영체제가 제공하는 API 형식이 다르므로 프로그램에서 사용하는 함수 이름이 달라지고 (운영체제가 달라도 API 함수 이름이 같으면 상관없겠지만.) 코드도 달라질 수밖에 없다.

 

 ②-⑵운영체제마다 메모리를 관리하는 기법이 다름.

 프로그램을 실행하려면 운영체제는 메모리(메인메모리, RAM)를 사용하게 되는데 운영체제마다 메모리를 관리하는 기법이 다르다.

 

이러한 이유들 때문에 대부분의 프로그래밍 언어들은 플랫폼 종속적인 것이다. 그러나 자바는 그렇지 않다.

 

자바 프로그램을 자바 컴파일러가 컴파일 하면 바이트코드가 만들어지는데 이 바이트코드는 자바 플랫폼(JVM)에서 돌아간다.

여기서 헷갈리지 말아야 할 것은 '자바 플랫폼'은 플랫폼 '종속적'이다. 따라서 실제로 오라클 홈페이지에서 JDK를 다운받을 때에 mac OS용, Windows용, Linux용이 따로 있다. 하지만 이렇게 플랫폼 종속적인 JVM만 설치되어 있으면 자바 프로그램은 플랫폼 독립적이다. (JVM도 사실상 운영체제라고 볼 수 있다. 메모리를 관리하는 능력이 있기 때문) Windows용이든 mac OS용이든 Linux용이든 자바 플랫폼 위에서는 똑같은 코드가 멀쩡히 다 돌아갈 수 있는 것이다.

 

 

-C/C++ vs Java

①실행 환경

C/C++ 프로그램을 작성하여 컴파일러를 통해서 목적코드가 만들어지면 목적코드 안에 printf나 cout 같은 함수가 있을 수 있다. 그런데 이러한 함수는 내가 만든 함수가 아니다. 따라서 이 함수 호출 부분을 라이브러리 안에 있는 함수와 연결시켜줘야 하는데 그것을 링크(link)라고 한다. C/C++의 경우 프로그램 실행 전에 링크를 하므로 정적인 링킹(static linkng)이다. 그 후 운영체제 위에서 프로그램이 돌아갈 때 printf나 cout 같은 함수들이 링크되어있기 때문에 그 함수가 있는 라이브러리가 프로그램 안에 포함되어 있는 것이다. 따라서 자연히 프로그램의 크기도 커지게 된다.

 

그러나 자바는 다르다. 자바는 정적인 링킹이 아니라 동적인 로딩을 한다.

자바는 자바 컴파일러가 바이트코드로 만들면(컴파일 방식) 링크 과정 없이 그 바이트 코드를 JVM에서 바로 실행한다. (인터프리터 방식으로) 그 프로그램을 실행하는 동안에 printf 메소드가 필요하다면 JVM의 클래스로더가 그 때 링크를 시켜주는 것이다. 필요할 때에 가져오고 반납하고 하는 것이다. 실행 도중에. 그렇다고 자바 프로그램의 크기가 작은 것은 아니다. 방식이 달라서 크기 비교는 어렵다.

 

 

②메모리 관리 기법

C/C++에서는 응용프로그램에서 메모리가 필요하면 함수를 사용해 메모리를 직접 할당할 수 있다. C에서의 malloc 함수, C++에서의 new 함수가 그것이다. 운영체제의 도움을 받아서 프로그램에서 필요한 만큼의 메모리를 할당받는 것이다.

참고로 운영체제의 가장 큰 역할은 자원 관리자(resource manager)이다. CPU, 메모리 등의 자원을 관리하는 것이다.

따라서 C 프로그램에서 이만큼의 메모리가 필요하니까 메모리를 할당해달라고 운영체제에게 요청을 하면 운영체제가 메모리에 여유가 있는지 확인하고 여유가 있다면 그만큼의 메모리를 할당해준다. 그러면 그 할당 받은 메모리를 사용해서 프로그램이 실행되는 것이다. 또한 그렇게 할당받은 메모리를 다 사용했다면 C에서는 free 함수, C++에서는 delete 함수를 호출하여 운영체제에게 자원을 반납해야 한다.

 

그러나 자바는 그렇지 않다. 자바 프로그램은 JVM 위에서 돌아가는데 JVM은 운영체제로부터 메모리를 미리 할당받아 놓고 자기가 관리를 한다. 따라서 자바에서 new 함수를 호출하면 운영체제의 도움이 아니라 JVM의 도움으로 메모리를 할당받아서 사용하는 것이다. 그런데 여기서 또 하나 C/C++과 다른 점이 있다. 

 

자바에는 new 함수는 있지만 free(또는 delete) 함수는 없다는 것이다. 자바에는 가비지 컬렉터(Garbage Collector)가 있기 때문에 알아서 다 쓰고 난 메모리 자원을 수거한다. 따라서 자바에서는 메모리 누수 걱정을 하지 않아도 된다는 장점이 있다. 하지만 가비지 컬렉터가 가비지를 모으려고 돌아다니면 그 때 프로그램의 속도가 느려질 수 있다는 단점도 있다. 그래서 JDK 11부터는 속도를 개선시키기 위해서 ZGC(A Scalable Low-Latency Garbage Collector)라는 새로운 가비지 컬렉터를 사용한다. (약자가 왜 ZGC냐면 처음에는 Zero Latency Garbage Collector로 하려고 했으나 현실적으로 Zero Latency는 불가능하기 때문에 약자는 ZGC이지만 풀이는 다르다고 한다.)

 

728x90

+ Recent posts