본문 바로가기

C++/C++ 기초 플러스 6판(Book)

[C++] 복합 데이터형

C++의 단순한 기본 데이터형만 가지고는 이러한 데이터를 처리할 수가 없다. 이들 복합 데이터형 중에서 가장 복잡한 것은 클래스이다. 그러나 C++는 C로부터 가져온 몇 가지 다른 복합형들도 제공한다.

 

배열

배열이 여러 가지 용도로 유용한 것은 각각의 배열 원소에 개별적으로 접근할 수 있기 때문이다. 개별적인 접근을 허용하기 위해 인덱스(subscript) 또는 인덱스(index)를 사용하여 배열 원소에 차례로 번호가 매겨진다.

 

정의된 배열을 초기화하지 않으면, 배열 원소의 값들은 미확정 상태로 남는다. 즉, 배열 원소들은 그 메모리 위치에 전부터 우연히 남아 있던 스레기 들을 값으로 취한다.

 

C++11 배열 초기화

C++11에서는 몆 가지가 추가 되었다.

먼저, 배열을 초기화할 때, = 부호를 사용하지 않아도 된다.

두 번째로, 중괄호를 공백하여 모든 배열을 0으로 초기화할 수 있다.

세 번째로, 리스트 초기화시에 narrowing을 방지할 수 있다.

 

문자열

큰따옴표로 묶인 문자열을 문자열 상수(string constant 또는 string literal)이라 한다.

char bird[11] = "Mr. Cheeps";
char fish[] = "Buddles";

 

큰따옴표로 묶인 문자열은 끝내기 널 문자를 암시적으로 가지고 있다. 문자열을 저장할 char형의 배열은, 컴파일러가 알아서 개수를 헤아려 주는것이 더 안전할 수 있다.

 

'S'는 83이다. 그러나 "S"는 두 개의 문자 S와 \0로 구성된 문자열을 나타낸다.

 

string 클래스의 조작

C 라이브러리에 있는 함수들을 사용하여 C스타일의 문자열들을 대상으로 이러한 조작들을 수행하였다.

strcpy(charr3, charr1);
strcpy(charr3, charr2);

 

C++에서는 string 객체 조작을 통해 다음과 같다.

str3 = str1 + str2;

 

문자열에 있는 문자들의 수를 구하는 데 서로 다른 문법이 사용된 것에 주목해라.

int len1 = str1.size();
int len2 = strlen(charr1);

 

strlen() 함수는 C 스타일 문자열의 매개변수로 사용하고, 그 문자열에 들어 있는 문자들의 개수를 리턴하는 표준 함수다. size() 함수도 기본적으로 같은 일을 한다.

 

구조체

C++에서 구조체는 객체 지향 프로그래밍의 핵심인 클래스의 기초가 된다. 구조체는 두 단계를 거쳐 생성된다.

첫 번째는 구조체 서술(structure description)을 정의하는 단계이다.

두 번째는 구조체 서술에 따라 구조체 변수(structure variable)를 생성하는 단계이다.

 

한 가지 중요한 것은 구조체 선언을 두는 위치이다.

첫 번째 방법은 main() 함수의 안에 여는 중괄호 바로 뒤에 선언을 두는 것이다.

두 번째 방법은 main() 함수의 앞에 선언을 두는 것이다. 함수 밖에 선언을 두는 것을 외부 선언(external declaration)이라 한다. 일반적으로 프로그래머들은 모든 함수들이 구조체를 사용할 수 있도록 구조체를 외부적으로 선언하고 있다.

 

공용체

공용체(union)는 서로 다른 데이터형을 한 번에 한가지만 보관할 수 있는 데이터 형식이다. 구문은 구조체와 같지만 의미가 조금 다르다. 여러 가지 데이터형을 사용할 수는 있지만 이들을 동시에 사용할 수 없을때 공용체를 사용하면 메모리를 절약할 수 있다.

 

열거체

C++의 enum기능은 const를 사용하여 기호 상수를 만드는 것에 대한 또 다른 방편을 제공힌다.

struct형 변수를 구조체라 부르듯이 enum형 변수를 열거체(enumeration)이라 부는다.

정수 값을 각각 나타내는 기호 상수로 만든다. 이 상수들은 열거자(enumerator)라 부른다.

 

포인터와 메모리 해제

포인터의 이름이 주소를 나타낸다. 간접값(indirect value) 연산자 또는 간접 참조(dereferencing) 연산자라고 부르는 *를 포인터 이름 앞에 붙이면 그 주소에 저장되어 있는 값이 된다.

포인터와 C++의 철학

객체 지향 프로그래밍은 컴파일 시간(compile time)이 아닌 실행 시간(run time)에 어떤 결정을 내린다는 것을 강조하는 점에 재래의 절차적 프로그래밍과는 다르다.

 

C++에서 배열을 선언하려면 배열의 크기를 미리 정해야 한다. 즉, 컴파일할 때 배열의 크기가 미리 결정된다. 이것이 바로 컴파일 시간이 결정된다.

 

객체 지향 프로그래밍에서는 실행 시간까지 이러한 결정을 미룸으로써 프로그램에 융통성을 부여한다.

 

C++에서는 new라는 키워드를 사용하여 원하는 만큼의 메모리를 대입하고, 이렇게 대입된 메모리의 위치를 포인터를 사용하여 추적할 수 있다.

 

포인터의 선언과 초기화

int * p_updates;

 

p_updates는 포인터(주소)이고, * p_updates는 포인터가 아니라 int형 변수다. * 연산자의 앞뒤에는 빈칸이 있어도 되고 없어도 된다. 전통적으로 C프로그래머들은 다음과 같은 형식을 사용한다.

int *ptr;

 

이것은 *ptr가 int형이라는 것을 강조한다. 그러나 C++ 프로그래머들은 다음과 같은 형식을 사용한다.

int* ptr;

 

이것은 int* 자체가 하나의 데이터형임을 강조한다.

double * tax_ptr;
char * str;

 

*tax_ptr가 double형 값이라는 사실을 컴파일러가 알 수 있다. 즉, 컴파일러는 *tax_ptr가 부동 소수점형으로 저장되는 수이고, (대부분의 시스템에서) 8바이트를 차지한다.

 

tax_ptr와 str가 서로 다른 크기의 데이터형을 지시하지만, tax_ptr와 str변수 자체의 크기는 같다. char형의 주소와 double형의 주소는 크기가 같다.

 

선언 구문을 사용하여 포인터를 초기화할 수 있다. 이러한 경우에 포인터에 의해 지시되는 값이 아니라 포인터가 초기화된디. 즉, 다음과 같은 구문은

int higgens = 5;
int * pt = &higgens;

 

포인터에 의해 지시되는 값인 *pt가 초기화 되는 것이 아니라, 포인터인 pt가 &higgens 값으로 초기화된다.

 

포인터의 위험

포인터는 매우 조심스럽게 사용해야 한다. C++에서 포인터를 생성하면 컴퓨터는 주소를 저장하기 위한 메모리를 대입한다. 이것은 그 포인터가 지시하는 데이터를 저장하기 위한 메모리를 대입하는 것이 아니다. 데이터를 저장하기 위한 메모리를 대입하는 것은 이와는 다른 단계이다.

long * felow; // long형을 지시하는 포인터 생성
*felow = 23; // 어디인지 알 수 없는 곳에 값을 저장

 

이포인터가 어디를 지시하고 있는지 알 수가 없다. 왜냐하면 fellow에 주소를 대입하는 단계를 빠뜨렸기 때문이다. 그렇다면 도대체 23은 어디에 저장되었을까? 이것은 아무도 모른다. fellow를 초기화하지 않았기 때문에 fellow에는 지금 엉뚱한 값이 들어 있을 것이다.

 

포인터와 수

일반적으로 컴퓨터가 주소를 정수로 다루고 있지만, 포인터는 정수형이 아니다. 포인터는 개념적으로 정수형과는 다른 데이터형이다. 

 

new를 사용한 메모리 대입

어떤 데이터형의 메모리를 원하는지 new연산자에게 알려 주면, new 연산자는 그에 알맞은 크기의 메모리 블록을 찾아내고, 그 블록의 주소를 리턴한다.

int * pn = new int;

 

두방식 모두 int형의 주소가 포인터(pn과 pt)에 대입된다.여기서 pn이 지시하는 메모리는 이름이 없는데, 어떻게 이 메모리에 접근할 수 있을까? 이러한 경우에는 pn이 데이터 객체(data object)를 지시하고 있다고 말한다. 이때의 객체는 객체 프로그래밍에서 말하는 객체(object)가 아니라 단순히 어떤 대상(thing)을 의미한다. 데이터 객체라는 말은 어떤 데이터를 저장하기 위해 대입된 메모리 블록을 의미하기 때문에 변수라는 말보다 더 일반적이다. 변수는 하나의 데이터 객체이지만, pn이 지시하는 메모리 블록은 변수가 아니다. 포인터를 사용하여 이름이 없는 데이터 객체를 다루는 첫 번째 방식이 처음에는 약간 어렵게 느껴진다. 그러나 이 방식은 메모리 제어권을 우리에게 더 많이 제공한다.

 

delete를 사용한 메모리 해제

메모리가 해제된다. 그러나 ps 자체가 없어지는 것은 아니다. 예를 들어, ps는 새로 대입한 메모리를 치시하는 데 다시 사용할 수 있다.

int * ps = new int;
...
delete ps;

 

delete를 사용할 때 가장 중요한 요구 사항은, new로 대입한 메모리에만 그것을 사용하라는 것이다. 이것은 new가 사용한 것과 동일한 포인터 변수에 사용해야 한다는 뜻이 아니라, 동일한 주소에 사용해야 한다는 뜻이다.

 

new를 사용한 동적 배열의 생성

선언에 의해 배열을 생성하면 프로그램이 실행될 때 이 배열은 사용이 되든 안 되는 항상 메모리를 차지한다. 이 방식을 정적 바인딩(static bindeing)이라 한다. new를 사용하면 배열을 실행 시간에 생성할 수 있고, 필요 없으면 생성하지 않을 수도 있다. 또는 프로그램을 실행하는 동안에 배열을 크기를 결정할 수 있다. 이 방식을 동적 바인딩(dynamic bindeing)이라 한다.

int * pt = new int;
short * ps = new short [500];

 

new와 delete를 사용할 때 다음과 같은 규칙을 지켜야 한다.

- new로 대입하지 않은 메모리는 delete로 해제하지 않는다.

- 같은 메모리 블록을 연달아 두 번 delete로 해제하지 않는다.

- new []로 메모리를 데입한 경우에는 delete[]로 헤제한다.

- new를 대괄호 없이 사용했으면 delete도 대괄호 없이 사용한다.

- 널 포인터에는 delete를 사용하는 것이 안전하다.(아무일도 일어나지 않는다.

 

배열 이름

대부분의 경우에는 C++는 배열 이름을 그 배열의 첫 번째 원소의 주소와 동등한 것으로 취급한다.

 

한 가지 예외가 있다. 배열 이름에 sizeof 연산자를 적용할 때에는 배열의 첫 번쨰 원소의 크기가 아니라 배열의 전체 크기가 바이트 단위로 리턴된다.

 

배열 표기와 포인터 표기

대괄호를 사용하는 배열 표기는 포인터를 사용하는 간접 참조와 의미상 동등하다. 예로 tacos[3]은 *(tacos + 3)과 동등하다.

 

new를 사용한 동적 구조체의 생성

지금까지 컴파일 시간보다는 실행 시간에 배열을 생성하는 것이 훨씬 유리하는 것을 알아보았다. 이것은 구조체에도 그대로 해당된다.

 

new를 구조체에 사용할 때에는 두 가지를 알아야 한다. 하나는 구조체를 생성하는 것이고, 다른 하나는 구조체의 멤버에 접근하는 것이다.

 

멤버에 접근하는 것은 약간 까다롭다. 동적 구조체에는 도트(.) 멤버 연산자를 사용할 수 없다. 그 이유는 동적 구조체에는 이름이 없기 때문이다. 우리는 동적 구조체의 주소만 알 수 있다. C++는 이 문제를 해결하기 위해 화살표 멤버 연산자(->)를 제공한다.

 

구조체 멤버에 접근하는 다소 보기 흉한 또 하나의 방법이 있다. ps가 구조체를 지시하는 포인터이면, *ps는 그 포인터가 지시하는 값, 즉 구조체가 된다. *ps가 구조체이기 때문에 (*ps).price는 그 구조체의 price 멤버이다.

 

new와 delete를 사용하는 예제 프로그램 분석

방금 해제한 메모리가 new에 의해 곹바로 다시 대입된다는 것을 C++는 보장하지 않는다. new와 delete를 서로 다른 함수에서 사용하는 것은 좋은 생각이 아니다. 그 이유는 delete를 사용하는 일을 잊어버리기 쉽기 때문이다.

 

자동 공간, 정적 공간, 동적 공간

C++에서는 데이터를 저장해 두기 위한 메모리를, 대입하는 방법에 따라 자동공간(automatic storage), 정적 공간(static storage), 동적 공간(dynamic storage)으로 구분한다. 동적 공간은 자유 공간 또는 힙(heap)이라고도 부른다.

 

자동 공간

자동 공간(automatic storage)을 사용하는 함수 안에서 정의되는  보톤의 변수들을 자동 변수(automatic variable)라고 한다.

 

예를 들어 임시 배열 temp는 getname()함수가 작동하는 동안에만 존재한다. 프로그램의 제어가 main()으로 다시 넘어가면, temp가 사용하던 메모리는 자동으로 해제가 된다. 만약에 getname()이 temp의 주소를 리턴했다면, main()에 있는 name 포인터는 곹 다른 용도로 사용될 운명을 지닌 메모리 위치를 지시하는 꼴이 되었을 것이다.

 

자동변수는 자신들을 포함하고 있는 블록 안에서만 유효하다. 자동 변수는 스택에 저장된다. 이것은 메모리사으이 스택에 그 값이 순차적으로 저장되고, 역순으로 해제되는 것을 의미한다.

 

정적 공간

변수를 정적으로 만드는 방법은 두 가지이다. 하나는 함수의 외부에서 변수를 정의하는 것이고, 다른 하나는 변수를 선언할 때 static이라는 키워드를 붇이는 것이다.

 

정적 변수는 프로그램이 실행되는 동안에 지속적으로 존재하고, 자동 변수는 특정 함수나 블록이 실행되는 동안에만 존재한다.

 

동적 공간

new와 delete 연산자는 보다 융통성 있는 방법을 제공한다. 이들은 자유 공간(free store)이라 부르는 메모리 풀(memory pool)을 관리한다. 이 풀은 자동 변수와 정적 변수가 사용하는 메모리와 분리되어 있다.

 

실전 프로그래밍: 스택, 힙, 메모리 누수

new 연산자로 자유 공간(힙)에 변수를 생성한 후 delete를 사용하여 이를 해제하지 않으면 무슨일이 벌어질까? 자유 공간에 동적으로 대입된 변수나 데이터 객체는, 그것을 지시하는 포인터를 포함하고 있는 메모리가 사용 범위와 객체 수명 규칙 때문에 해제된다고 하더라도, delete가 호출되지 않는 한 계속해서 살아남게 된다. 이러한 경우에 자유 공간에 생존하고 있는 변수나 데이터 객체에 접근할 수 있는 방도가 전혀 없다. 왜냐하면 변수나 데이터 객체가 저장되어 있는 메모리 위치를 지시하는 포인터가 이미 사라졌기 때문이다. 이것을 메모리 누수(memory leak)이라고 한다.

 

Vector 템플릿 클래스

기본적으로 동적 배열을 생성하기 위해 new를 사용하는 것을 대체할 수 이싿. 실제로 vector 클래스는 메모리를 관리하기 위해 new와 delete를 사용하지만, 그 과정은 자동으로 진행된다.

 

array 템플릿 클래스

vector 클래스는 내재 배열 형 보다 많은 기능을 가지고 있지만 다소 비효율적인 면이 있다. 만약 사용자가 고정된 크기의 배열만 필요하다면, 내재 배열형을 사용하는 것이 매우 흥미로운 일이 될 것이다. 그러나, 그럴 경우에 안정성과 편리성은 다소 줄어들 수 있다. C++11은 그 array 탬플릿 클래스를 더해 줌으로써 이러한 경우에 대하여 해결방안을 제시하고 있는데, 이것은 std 이름 공간의 일부분에 해당된다. 내재 배열형과 마찬가지로, array 객체는 자유 저장 대신에 고정된 크기와 고정 메모리 대입을 사용하여 내재 배열이 지닌 것과 동일한 수준의 효율성을 지닌다.

 

배열, Vector 객체, Array 객체 비교

내재 배열의 불완전한 행위(a[-2])를 방지하기 위해 vector 객체와 array 객체를 보호하고 있나? 사용자가 보호하도록 하면 보호될 수 있다.

 

예를 들면, 이 클래스는 사용자로 하여금 사고로 한계 범위를 초과할 수 있는 경우 방지하여 범위를 다시 조정할 수 있게 해 주는 begin()과 end() 멤버 함수를 지닌다.

'C++ > C++ 기초 플러스 6판(Book)' 카테고리의 다른 글

[C++] 분기 구문과 논리 연산자  (0) 2025.03.04
[C++] 루프와 관계 표현식  (0) 2025.03.03
[C++] 데이터 처리  (0) 2025.03.01
[C++] C++ 시작하기  (0) 2025.03.01
[C++] C++ 첫걸음  (0) 2025.03.01