관리 메뉴

클라이언트/ 서버/ 엔지니어 "게임 개발자"를 향한 매일의 공부일지

복합 데이터형 1 : 배열과 문자열 본문

프로그래밍 언어/C++

복합 데이터형 1 : 배열과 문자열

huenuri 2024. 8. 25. 11:21

이전 Chapter에서 기본 데이터에 대해서 다루었고 이제는 복합 데이터형을 공부하게 되었다. 여기서부터는 많이 어려워지는 것 같다. C 언어를 공부할 때도 배열에서부터 이해가 잘 안되어 넘어가지 못했는데 이번에는 개념부터 차근차근 학습해볼 생각이다.

기본 데이터형만 가지고는 다양한 종류의 데이터를 처리할 수 없다. 그렇기 때문에 C++에서는 복합 데이터를 제공한다. 복합 데이터형은 기본 정수형과 부동 소수점형으로부터 만들어지다. 이들 데이터형 중 가장 복잡한 것은 클래스이다.
클래스는 가장 마지막으로 점령하게 될 객체 지향 프로그래밍이다. 배열은 동일한 데이터형의 값 여러 개를 연속적으로 저장할 수 있으며, 구조체는 종류가 다른 여러 데이터형의 값을 함께 저장한다.
포인터는 데이터가 저장되어 있는 주소를 컴퓨터에게 알려주는 변수이다. 우선 이 포스트에서는 배열과 문자열에 대해서 학습해볼 예정이다.


 

 

1. 배열

배열은 데이터형이 여러 개인 값을 연속적으로 저장할 수 있는 데이터 구조이다. 예를 들어, 각 달의 날짜 수를 나타내는 12개의 short형 값을 하나의 배열에 저장할 수 있다.
배열에서 각 값은 배열 원소라는 개별 공간에 저장된다. 컴퓨터는 이 모든 원소들을 메모리에 연속적으로 배치한다. 배열은 선언 구문을 사용하여 생성한다. 선언 구문에서는 다음과 같은 3가지를 선언한다.

  1. 각 원소에 저장될 값의 데이터형
  2. 배열의 이름
  3. 배열 원소의 개수

 

C++에서는 간단한 변수의 선언 형식을 수정하여 배열을 선언한다. 즉 배열 원소의 개수를 대괄호 안에 넣어 선언한다.

short months[12];

 

다음은 배열을 선언하는 일반적인 형식이다.

typeName arrayName[arraySize];

 

배열이 여러 가지 용도로 유용한 것은 각각의 배열 원소에 개별적으로 접근할 수 있기 때문이다. 개별적인 접근을 하기 위해 인덱스를 사용하여 배열 원소에 차례로 번호가 매겨진다. 배열 인덱스는 항상 0부터 시작한다.
예를 들어, months[0]은 months 배열의 첫번째 원소를 나타낸다. 배열의 마지막 원소를 나타내는 인덱스는 배열의 크기보다 항상 1만큼 작다.

 

 

예제 1번 : arrayone.cpp

// 정수형의 작은 배열

#include <iostream>
int main()
{
	using namespace std;
	int yams[3];	// 3개의 원소를 가진 배열을 생성한다
	yams[0] = 7;	// 첫 번째 원소에 값을 대입한다
	yams[1] = 8;
	yams[2] = 6;

	int yamcosts[3] = {200, 300, 50};	// 배열을 생성하고 초기화한다
	// 참고: 사용하는 C++ 컴파일러나 번역기가 이 배열을 초기화할 수 없다면
	// int yamcosts[3] 대신에 static int yamcosts[3]을 사용하라

	cout << "고구마 합계 = ";
	cout << yams[0] + yams[1] + yams[2] << endl;
	cout << yams[1] << "개가 들어 있는 포장은 ";
	cout << "개당 " << yamcosts[1] << "원씩입니다.\n";
	int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1];
	total = total + yams[2] * yamcosts[2];
	cout << "세 포장의 총 가격은 " << total << "원입니다.\n";
	cout << "\nyams 배열의 크기는 " << sizeof yams;
	cout << "바이트입니다.\n";
	cout << "원소 하나의 크기는 " << sizeof yams[0];
	cout << "바이트입니다.\n";
	return 0;
}

 

 

이 프로그램은 yams 배열의 세 원소에 값을 일일이 대입하고 있다. 하지만 이렇게 하지 않고 선언 구문에서 배열 원소를 직접 초기화할 수도 있다.

int yamcosts[3] = {200, 300, 50};

 

중괄호 안의 리스트에 반칸이 있어도 되고 없어도 된다. 함수 안에서 정의된 배열을 초기화하지 않으면, 배열 원소의 값은 미확정 상태로 남는다. 즉, 배열 원소들은 그 메모리 위치 전부터 우연히 남아 있던 쓰레기들을 값으로 취한다.


 

 

 

배열 초기화 규칙

초기화 형식은 배열을 정의하는 곳에서만 사용할 수 있다. 인덱스를 사용하여 배열 원소에 개별적으로 값을 대입하는 것은 언제든지 가능하다. 배열을 초기화할 때, 초기화 값의 개수를 배열 원소의 개수보다 모자라게 제공할 수도 있다.
배열을 부분적으로 초기화하면, 컴파일러가 나머지 원소들을 모두 0으로 설정한다. 명시적으로 첫번째 원소만 0으로 초기화하면 된다.

int cards[4] = { 3,6,8,10 };
int hand[4];
hand[4] = { 5,6,7,9 };   // 틀린 배열

 


 

 

2. 문자열

문자열이란 메모리에 바이트 단위로 연속적으로 저장되어 있는 문자들을 말한다. C++은 두 가지 방법으로 문자열을 처리한다.

  1. C 스타일의 문자열 처리 방법
  2. string 클래스 라이브러리에 기반

 

문자들이 메모리에 바이트 단위로 연속적으로 저장된다는 것은, 문자열을 char형의 배열에 저장할 수 있다는 것을 의미한다. 이때 문자열을 구성하는 각 문자들은 배열의 원소에 하나씩 저장된다.
C 스타일의 문자열은 특별한 특징을 가지고 있다. 즉, 모든 문자열의 마지막 문자가 반드시 널 문자여야 한다. 널 문자 없는 것은 문자열이 아니다.

char bird[11] = "Mr. cheeps";
char fish[] = "Bubbles";

 

큰 따옴표로 묶인 문자열은 널 문자를 암시적으로 가지고 있다. 그러므로 문자열 끝에 널 문자를 명시적으로 넣을 필요가 없다. 또한 키보드 입력에서 문자열을 읽어 char형의 배열에 저장하는 C++의 다양한 입력 기능들은 끌내기 널 문자를 자동으로 추가한다.

문자열을 저장할 char형의 배열은 그 크기가 끝내기 널 문자까지 포함하여 모든 문자들을 다 넣을 만큼 충분히 커야 한다.


 

배열에 문자열 사용

 

배열에 문자열을 넣는 가장 일반적인 방법은 2가지이다. 배열을 문자열 상수로 초기화하는 방법과, 키보드 입력이나 파일 입력을 배열에 저장하는 방법이다.

예제 2번 : strings.cpp

// 배열에 문자열을 저장한다

#include <iostream> #include <cstring>      // strlen() 함수를 사용하기 위해
int main()
{
    using namespace std;
    const int Size = 15;
    char name1[Size];				// 비어 있는 배열
    char name2[Size] = "C++owboy";		// 문자열 상수로 초기화된 배열
// 참고: 어떤 C++에서는 name2 배열을 초기화하기 위해
// static 키워드를 사용해야 한다

    cout << "안녕하세요! 저는 " << name2;
    cout << "입니다! 실례지만 성함이?\n";
    cin >> name1;
    cout << "아, " << name1 << " 씨! 당신의 이름은 ";
    cout << strlen(name1) << "자입니다만 \n";
    cout << sizeof(name1) << "바이트 크기의 배열에 저장되었습니다.\n";
    cout << "이름이 " << name1[0] << "자로 시작하는군요.\n";
    name2[3] = '\0';                        // 널 문자
    cout << "제 이름의 처음 세 문자는 다음과 같습니다: ";
    cout << name2 << endl;
    return 0;
}

 

프로그램 분석

 

sizeof 연산자가 배열의 전체 크기를 15바이트라고 알려준다. 그러나 strlen() 함수는 널 문자를 제외하고 눈에 보이는 문자들만 헤아려 basicman의 길이를 9가 아닌 8로 리턴한다.

name1과 name2는 배열이기 때문에, 인덱스를 사용하여 그 배열에 저장되어 있는 문자들에 개별적으로 접근할 수 있다.


 

 

문자열 입력

 

다음의 예시는 결함을 가지고 있다.

예제 3번 : instr1.cpp

// 여러 개의 문자열을 읽는다

#include <iostream>
int main()
{
	using namespace std;
	const int ArSize = 20;
	char name[ArSize];
	char dessert[ArSize];

	cout << "이름을 입력하십시오:\n";
	cin >> name;
	cout << "좋아하는 디저트를 입력하십시오:\n";
	cin >> dessert;
	cout << "맛있는 " << dessert;
	cout << " 디저트를 준비하겠습니다. " << name << " 님!\n";
	return 0;
}

 

이 프로그램은 디저트를 입력하라는 답을 응할 기회를 주지 않는다. 이렇게 되는 이유는 cin이 문자열의 끝을 인식하는 방법 때문이다. cin에게 문자열의 끝을 알려 주는 다른 수단이 필요하다.
cin은 빈칸, 탭, 캐리지 리턴과 같은 화이트스페이스가 있으면 그 위치에서 문자열이 끝난 것으로 간주한다. 즉, char형의 배열에 저장하기 위해 키보드로 입력을 받을 때 cin은 하나의 단어만 읽어 그것을 배열에 저장하고 널 문자를 끝에 추가한다.

이 예제에는 또 다른 문제 소지가 잠복해있다. 입력한 문자열이 그 문자열을 저장할 배열보다 더 길 수도 있다. 이 방법으로는 20개의 문자를 저장할 수 있는 배열에 30개의 문자로 이루어진 문자열을 집어 넣는 실수를 막을 방도가 없다.


 

한 번에 한 행의 문자열 입력 읽기

 

전체 어구를 하나의 문자열로 입력하려면 다른 방법이 필요하다. 즉 단어 단위의 문자열 입력이 아닌 행 단위의 문자열 입력이 있어야 한다. istream 클래스는 행 단위로 문자열을 입력하는 클래스 멤버 함수인 getline()과 get()을 제공한다.
둘 다 전체 입력 행을 읽는다. 즉 개행 문자가 나올 때까지 읽는다.

  • getline() : 개행 문자를 읽어서 폐기
  • get() : 입력 큐에 개행 문자를 남겨둠

 

getline()을 이용한 행 단위 입력

 

getline() 함수는 Enter 키에 의해 전달되는 개행 문자를 입력의 끝으로 간주하여 한 행 전체를 읽는다. cin.getline()을 함수 호출로 사용함으로써 이 메서드를 호출할 수 있다.
이 함수는 두 개의 매개변수를 사용한다.

  • 첫번째 매개변수 : 입력한 행을 저장할 배열의 이름
  • 두번째 매개변수 : 입력 받을 문자들의 한계
    한계가 20이면 이 함수는 최대 19개까지 문자들을 입력받을 수 있다.

cin.getline(name, 20);

 

예제 4번 : instr2.cpp

// getline() 함수로 한 행을 읽는다

#include <iostream>
int main()
{
    using namespace std;
    const int ArSize = 20;
    char name[ArSize];
    char dessert[ArSize];

    cout << "이름을 입력하십시오:\n";
    cin.getline(name, ArSize);  // 개행 문자가 있는 곳까지 읽는다
    cout << "좋아하는 디저트를 입력하십시오:\n";
    cin.getline(dessert, ArSize);
    cout << "맛있는 " << dessert;
    cout << " 디저트를 준비하겠습니다. " << name << " 님!\n";
    return 0;
}

 

이 프로그램은 이름이 빈칸으로 분리되어 있어도 온전하게 하나로 읽는다. 이 함수는 행의 끝을 표시하는 개행 문자까지 읽는다. 그러나 개행 문자는 저장하지 않고, 문자열을 배열에 저장할 때 널 문자로 대체된다.

 

 

get()을 이용한 행 단위 입력

이 함수는 개행문자를 읽어서 버리지 않고 입력 큐에 그대로 남겨둔다. 첫번째 호출의 입력 큐에 개행 문자를 그대로 남겨 두기 때문에, 두번째 호출은 그 개행 문자를 첫 문자로 만나게 된다.
get()은 읽을 것도 없이 곧바로 행의 끝에 도달했다고 결론을 내린다. 따라서 특별한 조치를 취해 주지 않는 한 get()은 개행 문자를 넘어갈 수가 없다.

cin.get() 호출은 그것이 개행 문자이든 아니든 무조건 문자 하나를 읽는다. 이것을 사횽하면 개행 문자르 읽어서 처리하고, 다음 행의 입력으로 넘어가게 할 수 있다.

cin.get(name, ArSize);		// 첫번째 행을 읽는다
cin.get();					// 개행 문자를 읽는다
cin.get(dessert, ArSize);	// 두번째 행을 읽는다

cin.get(name, ArSize).get();	// 멤버 함수들을 결합한다
cin.getline(name1, ArSize).getline(name2, ArSize);	

 

두 행의 입력을 연속해서 읽어 name1과 name2 배열에 각각 저장한다. 이것은 cin.getline()을 두 번 호출하는 것과 같다.

 

예제 5번 : instr3.cpp

// get() & get()으로 여러 단어를 읽는다

#include <iostream>
int main()
{
    using namespace std;
    const int ArSize = 20;
    char name[ArSize];
    char dessert[ArSize];

    cout << "이름을 입력하십시오:\n";
    cin.get(name, ArSize).get();        // 문자열, 개행 문자를 읽는다
    cout << "좋아하는 디저트를 입력하십시오:\n";
    cin.get(dessert, ArSize).get();
    cout << "맛있는 " << dessert;
    cout << " 디저트를 준비하겠습니다. " << name << "님!\n";
    return 0;
}

 

C++에서는 매개변수 리스트를 다르게 하면 같은 이름을 가진 함수의 여러 변종을 사용할 수 있다. 예를들어, cin.get(name, Arsize)를 사용하면, 컴파일러는 문자열을 읽어 배열에 저장하라는 뜻으로 알아듣고 그에 알맞은 멤버 함수를 호출한다. 그렇지 않고 매개변수 리스트가 없는 cin.get()을 사용하면, 컴파일러는 문자 하나를 읽으라는 뜻으로 알아듣는다.

함수 오버로딩 : 같은 이름의 함수를 여러 벌 만드는것

 

 

빈 행과 기타 문제점

빈 행을 읽으면 failbit라는 것이 설정된다. 이 행동은 계속되는 입력을 막고 입력을 복원하려면 다음과 같은 명령을 사용해야 한다.

cin.clear();

입력 문자열이 대입된 공간보다 더 길면, getline()과 get()은 나머지 문자들을 입력 큐에 그대로 남겨둔다.


 

 

문자열과 수치의 혼합 입력

 

행 단위의 문자열 입력과 수치 입력을 혼합하여 사용하면 문제가 발생할 수 있다.

예제 6번 : numstr.cpp

// 수치 입력 뒤에 오는 문자열 입력

#include <iostream>
int main()
{
    using namespace std;
    cout << "지금 사시는 아파트에 언제 입주하셨습니까?\n";
    int year;
    cin >> year;
    cout << "주소를 말씀해 주시겠습니까?\n";
    char address[80];
    cin.getline(address, 80);
    cout << "아파트 입주 연도: " << year << endl;
    cout << "도시: " << address << endl;
    cout << "등록이 완료되었습니다!\n";
    return 0;
}

 

이 프로그램은 주소를 입력할 기회를 주지 않는다. cin이 입주 연도를 읽어들이고, Enter 키가 만들어 내는 개행 문자를 입력 큐에 남겨 두기 때문에 발생하는 문제이다.

cin.getline()은 입력 큐에 남겨진 개행문자를 빈 행으로 있어들이고, address 배열에 널 문자열을 대입한다. 이 문제를 해결하는 방법은 주소를 읽기 전에 개행 문자를 읽어 허공에다 버리는 것이다.
C++ 프로그램은 문자열을 처리하기 위해 배열 대신에 포인터를 더 많이 사용한다.


 

오늘의 학습을 마치고

4장은 어렵기도 하고 진도가 잘 나가지 않았다. 배열과 문자열은 그동안 공부를 별로 열심히 하지 않았던 것 같다.
무엇보다도 나를 언짢게 했던 건 글을 쓸 때마다 계속해서 너무나도 많은 오타가 난다는 거였다. 한 문장을 칠 때마다 오타가 없는 적이 없고 한 10번은 오타가 나서 두 단어에 한번은 고치게 되는 것 같다.

 

세벌식을 사용한지 15년도 훨씬 넘었지만, 아무리 쳐도 익숙해지지 않는 것 같다. 타자 연습을 하지 않고 그냥 쳐서 그런지 매일 매일 이런 오타 때문에 신경이 많이 거슬린다. 아무래도 매일 10분 이상 타자연습을 해야 할 것 같다.