학습목표

  • 배열의 생성과 사용
  • string 클래스 문자열의 생성과 사용
  • 문자열과 수치의 혼합 입력
  • 공용체의 생성과 사용
  • 포인터의 생성과 사용
  • 동적 배열의 생성
  • 자동 기억 공간, 정적 기억 공간, 동적 기억 공간
  • C 스타일 문자열의 생성과 사용
  • 문자열을 읽는 getline()get() 메소드의 사용
  • 구조체의 생성과 사용
  • 열거체의 생성과 사용
  • newdelete를 사용한 동적 메모리 관리
  • 동적 구조체의 생성
  • 벡터(vector)와 배열에 대한 소개

4.1 배열

배열(array) : 데이터형이 같은 여러 개의 값을 연속적으로 저장할 수 있는 데이터형

  • 각각의 값은 배열 원소(element)라는 개별 공간에 저장되며, 메모리 상에도 연속적으로 배치됨
  • typeName arrayName[arraySize]; 형식의 선언 구문을 사용하여 생성됨
    • typeName : 각 원소에 저장될 값의 데이터형
    • arrayName : 배열의 이름
    • arraySize : 배열 원소의 개수, 정수 상수 혹은 상수 수식이어야 하며 변수는 사용할 수 없음
      단, new연산자를 사용하여 제약을 피하는 방법이 존재함
  • 각각의 배열 원소에는 개별적인 접근을 허용하기 위한 인덱스(index)가 부여되며, C++에서 배열 인덱스는 항상 0부터 시작함
// arrayone.cpp

#include <iostream>

int main()
{
	using namespace std;
	int yams[3];
	yams[0] = 7;
	yams[1] = 8;
	yams[2] = 6;

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

	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와 yamcosts는 각각 원소가 3개인 배열이며, 원소들의 인덱스는 0, 1, 2가 됨
각 배열 원소는 int형의 모든 특성을 그대로 가짐

  • yams와 같이 각 원소들을 직접 초기화할 수 있음
  • yamcosts와 같이 선언 구문에서 중괄호를 사용하여 직접 초기화할 수도 있음
  • 배열을 초기화하지 않을 경우 원소의 값들은 미확정 상태로 남아 쓰레기값을 취함

배열 초기화 규칙

초기화 형식은 배열을 정의하는 곳에서만 사용 가능
단, 인덱스를 사용하여 원소마다 개별적으로 값을 대입하는 것은 언제든 가능
배열을 부분적으로 초기화할시 나머지 원소들은 모두 0으로 초기화됨
초기화시 배열의 개수를 지정하지 않으면 초기화 값의 개수에 맞추어 배열 원소의 개수가 결정됨


C++에서의 배열 초기화

배열에도 중괄호를 이용한 초기화인 리스트 초기화가 적용됨

  • 배열 초기화시 = 부호를 생략해도 됨
  • 중괄호 안을 공백으로 처리시 모든 배열이 0으로 초기화됨
  • 리스트 초기화시에 narrowing이 일어나는 경우는 허용되지 않음


4.2 문자열

문자열(string) : 메모리에 바이트 단위로 연속적으로 저장되어있는 문자

C 스타일의 문자열은 반드시 마지막 문자가 널 문자(NULL character)여야 함

  • 널 문자는 \0로 표기하며 ASCII코드에서 0값을 가지고있음
  • cout의 경우 배열에 들어있는 문자들을 처리하다 널 문자를 만나면 출력을 중단함
    만약 널 문자가 없다면 이후 메모리 내용을 한 바이트씩 문자로 간주하여 계속 출력하며, 이는 우연히 널 문자를 만날때까지 지속됨

문자열 상수(string constant 또는 string literal) : 큰따옴표로 묶인 문자열, 암시적으로 널 문자를 가지고있음

  • char형 배열에 문자열을 저장할시 널 문자까지 고려하여 최소 크기를 지정해주어야 함
  • char형 배열을 문자열 상수로 초기화할시 컴파일러가 배열 원소의 개수를 대신 헤아려주므로 더 안전할 수 있음
  • 선언한 배열의 크기가 문자열 상수 뒤에 널 문자를 붙인것보다 클 경우 나머지 공간들 역시 모두 널 문자로 채워짐
  • 문자 상수(작은따옴표 사용)와 문자열 상수(큰따옴표 사용)는 다름
    • 문자 상수 : 해당 문자에 해당하는 수치 코드를 나타냄
    • 문자열 상수 : 내부적으로 그 문자열이 저장되어있는 메모리 주소를 나타냄

문자열 상수의 결합

C++에서는 큰따옴표로 묶인 두 문자열 상수를 결합할 수 있음
화이트스페이스로 분리된 문자열 상수는 하나의 문자열 상수로 결합되며, 결합된 문자열에는 어떠한 공백도 추가되지 않음
즉, 앞 문자열의 \0이 두번째 문자열의 첫 문자로 대체되어 이어짐


배열에 문자열 사용

// strings.cpp

#include <iostream>
#include <cstring>

int main()
{
	using namespace std;
	const int Size = 15;
	char name1[Size];
	char name2[Size] = "C++owboy";

	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;
}

배열에 문자열을 넣기 위해서는 배열을 큰따옴표로 묶인 문자열 상수로 초기화하거나, cin을 통해 키보드로 입력한 문자열을 배열에 저장하는 방법이 있음
strlen()함수는 널 문자를 제외한 눈에 보이는 문자들의 길이를 리턴함
문자열 중간에 널 문자가 있을 경우 널 문자 이후의 문자들은 무시됨
배열 크기를 기호 상수로 나타낼 경우 배열 크기를 바꾸어야할 때 수정이 쉬워짐


문자열 입력

// 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은 단어 단위로만 문자열을 읽을 수 있다는 단점이 있음
또한 배열의 크기를 넘는 문자열을 집어넣는 실수를 방지할 수 없음


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

행 단위의 문자열 입력에는 iostream 클래스에 속하는 getline() 함수와 get()함수를 사용

// instr2.cpp

#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;
}

getline() : 엔터키에 의해 전달되는 개행 문자를 입력의 끝으로 간주하여 한 행 전체를 읽음

  • cin.getline()을 함수 호출로 사용하여 메소드를 호출할 수 있음
  • 개행문자는 저장되지 않으며, 이는 널 문자로 대체됨
  • 첫 번째 매개변수로는 입력한 행을 저장할 배열의 이름을 받음
  • 두 번째 매개변수로는 입력받을 문자열의 한계를 받으며, 이는 널 문자를 포함한 수치가 됨
// instr3.cpp

#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;
}

get() : getline()과 같은 방식으로 매개변수를 사용하고 해석할 수 있으나 개행 문자를 입력 큐에 그대로 남겨둠
매개변수를 사용하지 않으면 무조건 문자 하나를 읽어 처리하고 다음 행의 입력으로 넘어가도록 만듬

  • 함수의 오버로딩 : 매개변수 리스트를 다르게 하여 같은 이름의 함수를 여러 종류로 사용하는 것

cin.get(name, size).get();의 형식으로 사용 할 경우 앞의 cin.get(name, size)cin 객체를 리턴한 후, 이렇게 리턴된 객체가 뒤에 결합된 get()함수를 호출하는 객체로써 사용됨

getline()은 사용하기 편리한 반면, get()은 함수 호출 후의 입력문자가 개행문자인 경우 정상 실행된 것으로 간주할 수 있기 때문에 에러 체킹에 장점이 있음

get()이 빈 행을 읽거나, getline()이 지정된 문자 수보다 긴 입력을 받는 경우 failbit가 설정되어 계속되는 입력을 막음
입력을 복원하기 위해서는 cin.clear(); 명령을 사용해야 함


문자열과 수치의 혼합 입력

// 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으로 입력을 받은 후 엔터로 만들어진 개행 문자가 입력 큐에 남겨짐
뒤의 cin.getline()은 입력 큐에 남겨진 개행 문자를 빈 행으로 읽어들여 배열에 널 문자열을 대입함
위와 같은 문제는

  • cin >> year; 뒤에 cin. get();을 호출하거나
  • (cin >> year).get();으로 결합하여 해결할 수 있음


4.3 string 클래스

C++에서는 문자열을 저장하는데 문자 배열 대신 string형 변수(C++ 용어로는 객체)를 사용할 수 있음
string 클래스를 사용하기 위해서는 string 헤더 파일을 포함시켜야 하며, 이는 std 이름 공간에 속해있음

// strtype1.cpp

#include <iostream>
#include <string>

int main()
{
	using namespace std;
	char charr1[20];
	char charr2[20] = "jaguar";
	string str1;
	string str2 = "panther";

	cout << "고양이과의 동물 한 종을 입력하시오 : ";
	cin >> charr1;
	cout << "고양이과의 또 다른 동물 한 종을 입력하시오 : ";
	cin >> str1;
	cout << "아래 동물들은 모두 고양이과입니다 : \n";
	cout << charr1 << " " << charr2 << " "
		 << str1 << " " << str2 << endl;
	cout << charr2 << "에서 세번째 글자 : "
		 << charr2[2] << endl;
	cout << str2 << "에서 세번째 글자 : "
		 << str2[2] << endl;

	return 0;
}

string은 배열이 아닌 단순한 변수로 선언되어 크기 문제를 프로그램이 자동으로 처리하도록 허용함
따라서 입력에 맞게 크기가 조절되며, 배열을 사용하는 것보다 편리하고 안전함

C+11에서는 string a = {"abc def ghi"};와 같이 문자열 객체에도 리스트 초기화가 가능함

대입 / 결합 / 추가

// strtype2.cpp

#include <iostream>
#include <string>

int main()
{
	using namespace std;
	string s1 = "penguin";
	string s2, s3;

	cout << "string 객체를 string 객체에 대입할 수 있다 : s2 = s1\n";
	s2 = s1;
	cout << "s1 : " << s1 << ", s2 : " << s2 << endl;
	cout << "string 객체에 C스타일 문자열을 대입할 수 있다.\n";
	cout << "s2 = \"buzzard\"\n";
	s2 = "buzzard";
	cout << "s2 : " << s2 << endl;
	cout << "string 객체들을 결합할 수 있다 : s3 = s1 + s2\n";
	s3 = s1 + s2;
	cout << "s3 : " <<s3 << endl;
	cout << "string 객체들을 추가할 수 있다.\n";
	s1 += s2;
	cout << "s1 += s2 --> s1 = " << s1 << endl;
	s2 += " for a day";
	cout << "s2 += \" for a day\" --> s2 = " << s2 << endl;

	return 0;
}

string 객체는 다른 string 객체에 간단하게 대입할 수 있음

  • + 연산자를 사용하여 두 개 객체의 결합 가능
  • += 연산자를 사용하여 기존의 string 객체의 끝에 또다른 객체를 덧붙일 수 있음

string 클래스의 조작

// strtype3.cpp

#include <iostream>
#include <string>
#include <cstring>

int main()
{
	using namespace std;
	char charr1[20];
	char charr2[20] = "jaguar";
	string str1;
	string str2 = "panther";

	str1 = str2;
	strcpy(charr1, charr2);

	str1 += " paste";
	strcat(charr1, " juice");

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

	cout << str1 << " 문자열에는 "
		 << len1 << "개의 문자가 들어있다.\n";
	cout << charr1 << " 문자열에는 "
		 << len2 << "개의 문자가 들어있다.\n";	

	return 0;
}

C++ string 클래스가 추가되기 전에도 strcpy(), strcat()함수 등 C 문자열 함수들을 가지고 문자열을 조작했음
일반적으로는 string 객체를 사용하는 것이 C 문자열 함수를 사용하는 것보다 훨씬 간단함
또한 배열 크기가 작아 정보를 모두 담을 수 없는 경우, C 문자열 함수의 경우 인접 메모리를 침범하여 문제가 생기지만 string 클래스는 크기를 자동으로 조절하여 문제를 피할 수 있음
물론 strncpy() 함수나 strncat()함수 등으로 위험을 줄일 수 있으나 대신 함수가 더 복잡해짐

  • C 함수들은 매개변수를 사용하여 어느 문자열을 사용할 것인지를 나타냄
  • C++ string 클래스 객체는 객체 이름과 도트 연산자를 사용해 어느 문자열을 사용할 것인지를 나타냄

string 클래스의 입출력

// strtype4.cpp

#include <iostream>
#include <string>
#include <cstring>

int main()
{
	using namespace std;
	char charr[20];
	string str;

	cout << "입력 이전에 charr에 있는 문자열의 길이 : "
		 << strlen(charr) << endl;
	cout << "입력 이전에 str에 있는 문자열의 길이 : "
		 << str.size() << endl;
	cout << "텍스트 한 행을 입력하시오 : \n";
	cin.getline(charr, 20);
	cout << "입력한 텍스트 : " << charr <<endl;
	cout << "또 다른 텍스트 한 행을 입력하시오 : \n";
	getline(cin, str);
	cout << "입력한 텍스트 : " << str <<endl;
	cout << "입력 이후에 charr에 있는 문자열의 길이 : "
		 << strlen(charr) << endl;
	cout << "입력 이후에 str에 있는 문자열의 길이 : "
		 << str.size() << endl;

	return 0;
}

string 객체에 한 행을 읽어서 넣는데는 istream 클래스 메소드가 아닌 getline()을 사용
이는 istreamstring 클래스가 추가되기 전부터 만들어진 클래스이기 때문


다른 형태의 문자열 상수

  • wchar_t형은 접두사 L을 붙여서 사용
  • char16_t형은 접두사 u를 붙여서 사용
  • char32_t형은 접두사 U를 붙여서 사용
  • C++11에서는 UTF-8이라고 불리는 유니코드 문자를 위한 인코딩을 지원하며, 이는 접두사로 u8을 붙여서 사용

C++11에서는 raw 문자열을 지원함

  • raw문자열에서는 \, "등 모든 문자가 문자 그대로 인식됨
  • R"( )"의 형태이며, "( 사이에 문자를 추가할 경우 마무리도 동일하게 설정되어야 함
  • 엔터키나 리턴키를 치면 모니터상의 커서 및 raw문자열 내의 문자도 다음 라인에 자리잡게 됨


4.4 구조체

struct inflatable
{
	char name[20];
	float volume;
	double price;
};

struct inflatable goose;
inflatable vincent;

구조체(structure) : 데이터형이 서로 다른 정보들을 하나로 묶어서 쓰기 위해 사용
서로 관련된 정보를 하나의 단위로 묶어서 저장할 수 있음
구조체 리스트의 각 항목은 멤버(memeber)라고 부름
structName.memberName 의 형식으로 해당 멤버 변수에 접근함

  • 구조체 서술(structure description) 정의 : 사용자가 정의할 수 있는 데이터형으로 데이터형의 특성을 정의하는 구조체의 선언이 필요함
  • 데이터형을 정의한 후에 해당하는 데이터형의 변수(구조체 데이터 객체)를 생성할 수 있음
    C++에서는 변수 생성시 키워드 struct를 생략할 수 있음

프로그램에 구조체 사용하기

// structure.cpp

#include <iostream>
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	using namespace std;
	inflatable guest =
	{
		"Glorious GLoria",
		1.88,
		29.99
	};

	inflatable pal = 
	{
		"Audacious Arthur",
		3.12,
		32.99
	};

	cout << "지금 판매하고있는 모형풍선은\n" << guest.name;
	cout << "와 " << pal.name << "입니다.\n";
	cout << "두 제품을 $";
	cout << guest.price + pal.price << "에 드리겠습니다!\n";
	return 0;
}
  • 외부 선언(external declaration) : 함수 밖에 두어 선언 이후에 나오는 모든 함수들이 구조체를 사용할 수 있음, 일반적으로 사용
  • 내부 선언(internal declaration) : 해당 선언이 들어있는 함수에서만 사용할 수 있음

배열과 마찬가지로 중괄호 안에 초기화 값들을 콤마로 구분하여 초기화 리스트를 생성함
구조체의 각 멤버는 해당 데이터형의 변수처럼 취급됨

C++11에서는 초기화시 =을 생략 가능하며, 중괄호 내부가 공백이라면 각각의 멤버에 대해 0으로 초기화함

낡은 컴파일러를 사용하지 않는다면 구조체에 string 클래스 멤버를 사용하는 것도 가능함
단, 구조체 정의가 std 이름 공간에 접근할 수 있는지 여부를 확인해야 함


구조체의 기타 특성

// assign_st.cpp

#include <iostream>
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	using namespace std;
	inflatable bouquet =
	{
		"sunflowers",
		0.20,
		12.49
	};
	inflatable choice;
	
	cout << "bouquet : " << bouquet.name << " for $";
	cout << bouquet.price << endl;

	choice = bouquet;
	cout << "choice : " << choice.name << " for $";
	cout << choice.price << endl;
	return 0;
}

C++에서는 사용자가 정의한 데이터형을 내장 데이터형과 동일한 방식으로 다룰 수 있음
따라서 구조체를 다른 구조체에 대입하거나, 함수에 매개변수로 전달하거나, 리턴값으로 사용하는 등의 활용이 가능함
구조체 대입 시에는 각 멤버 단위로 대입되는 멤버별 대입(memberwise assignment)이 발생함

//1
struct perks
{
	int key_number;
	char car[12];
} mr_smith, ms_jones;

//2
struct perks2
{
	int key_number;
	char car[12];
} mr_glitz =
{
	7,
	"Packard"
};

//3
struct
{
	int x;
	int y;
} position;

1번 구조체와 같이 구조체 선언과 변수의 생성을 하나로 결합할 수 있음
2번 구조체와 같이 변수 생성시 초기화도 한번에 처리할 수 있으나, 분리하는 것이 프로그램 이해에 더 도움이 됨
3번 구조체와 같이 데이터형 이름이 없는 구조체를 생성할 경우 이후에 같은 형의 다른 변수를 생성할 수는 없음


구조체의 배열

// arrstruc.cpp

#include <iostream>
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	using namespace std;
	inflatable guests[2] =
	{
		{"Bambi", 0.5, 21.99},
		{"Godzilla", 2000, 565.99}
	};
	
	cout << guests[0].name << "와 " << guests[1].name
		 << "의 부피를 합하면\n"
		 << guests[0].volume + guests[1].volume
		 << " 세제곱피트입니다.\n";
	return 0;
}

원소가 구조체인 배열을 생성할 수도 있음
구조체 배열의 초기화시 구조체 초기화 규칙과 배열 초기화 규칙을 하나로 결합해서 사용


구조체 안의 비트 필드

C++에서도 C와 같이 구조체 멤버들이 각각 일정 비트 수를 차지하도록 지정할 수 있음
이는 어떤 하드웨어 장치에 들어있는 레지스터에 대응하는 데이터 구조를 만들 때 매우 편리함
정수자료형 멤버이름 : 비트수;로 표현



4.5 공용체

union one4all
{
	int int_val;
	long long_val;
	double double_val;
};

공용체(union) : 서로 다른 데이터형을 한번에 한가지만 보관할 수 있는 데이터

  • 어느 시점에 한가지 데이터형만 보관할 수 있기 때문에 메모리를 절약할 수 있음
  • 어떤 종류의 멤버라도 보관할 수 있어야 하기 때문에 공용체의 크기는 가장 큰 멤버의 크기가 됨
  • 익명 공용체(anonymous union)의 경우 멤버들이 동일한 주소를 공유하는 변수가 됨


4.6 열거체

enum spectrum {red, orange, yellow, green, blue, violet, indigo};
spectrum band;
band = blue;

열거체(enumerator) : 상호 관련이 있는 기호 상수들을 정의하는 용도로 주로 쓰임

  • 열거자(enumerator)들은 기본적으로 0부터 순서대로 정수값들이 대입되고, 명시적 대입시 기본값을 무시할 수 있음
  • 열거자들은 같은 값을 가질 수 있음
  • 대입 연산자만 사용하도록 정의되어있으나, 일부 C++에서는 제한을 위반할 수 있음
  • 열거자들은 정수형이며 현재는 int형 및 int형으로 승급될 수 있는 값만이 아닌 long형 값도 열거자에 대입할 수 있음

열거체의 값 범위

enum bits {one = 1, two = 2, four = 4, eight = 8};
bits myflag;
myflag = bits(6);

각 열거체는 값 범위를 가지며, 어떤 정수값이 해당 범위 안에 들어있으면 그것이 열거자 값이 아니더라도 열거체 변수에 대입할 수 있음

  • 열거체 값 범위의 상한 : 열거자 값 중 최대값보다 큰 2의 최소 거듭제곱 - 1
  • 열거체 값 범위의 하한 : 열거자 값 중 최소값이 0 이상이면 0, 음수이면 상한과 같이 구한 후 마이너스 부호를 붙임

C++11에서는 범위가 정해진 열거(scoped enumeration)으로 열거체를 확장함



4.7 포인터와 메모리 해제

포인터(pointer) : 값 자체가 아닌 값의 주소를 저장하는 변수

// address.cpp

#include <iostream>

int main()
{
	using namespace std;
	int donuts = 6;
	double cups = 4.5;

	cout << "donuts의 값 = " << donuts;
	cout << ", donuts의 주소 = " << &donuts << endl;
	cout << "cups의 값 = " << cups;
	cout << ", cups의 주소 = " << &cups << endl;
	return 0;
}

주소 연산자 &를 변수 앞에 붙이면 변수의 주소를 알아낼 수 있음
주소값은 보통 16진수 표기를 통해 출력됨

// pointer.cpp

#include <iostream>

int main()
{
	using namespace std;
	int updates = 6;
	int * p_updates;

	p_updates = &updates;

	cout << "값 : updates = " << updates;
	cout << ", *p_updates = " << *p_updates << endl;

	cout << "주소 : &updates = " << &updates;
	cout << ", p_updates = " << p_updates << endl;

	*p_updates = *p_updates + 1;
	cout << "변경된 updates = " << updates << endl;
	return 0;
}

포인터 변수의 이름은 주소를 나타내고, 간접값(indirect value) 또는 간접 참조(dereferencing) 연산자 *를 포인터 앞에 붙여 그 주소의 저장되어 있는 값을 나타냄

포인터의 선언과 초기화

// init_ptr.cpp

#include <iostream>

int main()
{
	using namespace std;
	int higgens = 5;
	int * pt = &higgens;

	cout << "higgens의 값 = " << higgens
		 << ", higgens의 주소 = " << &higgens << endl;
	cout << "*pt의 값 = " << *pt
		 << ", pt의 값 = " << pt << endl;
	return 0;
}

데이터형마다 저장되는 바이트 수와 저장 형식이 다르기 때문에 포인터 선언시 포인터가 지시하는 데이터형이 무엇인지 서술해야 함

  • * 연산자의 앞뒤에는 공백이 있어도 되고 없어도 됨
  • 전통적으로 C에서는 int *ptr;의 형식을 사용했으나, C++에서는 int* ptr;의 형식을 사용하여 int* 자체가 하나의 데이터형임을 강조함
  • 단, int* p1, p2;의 경우 p1은 int*형, p2는 int형이 되기 때문에 각각의 포인터 변수 이름 앞에 * 연산자를 따로 붙여주어야 함
  • 서로 다른 데이터형을 지시하는 포인터 변수라도 포인터 변수 자체의 크기는 모두 동일함
    • 시스템에 따라 2바이트, 4바이트 혹은 그 이상의 주소를 사용하며 데이터형에 따라 다른 크기의 주소를 사용할 수도 있음

포인터의 위험

long * fellow;
*fellow = 223323;

위와 같이 포인터 변수를 선언한 후 해당 포인터에 주소를 대입하지 않았을 경우, 해당 변수에는 어떠한 값이 들어있을지 알 수 없음
따라서 223323이라는 값은 사용자가 알수 없는 메모리 주소 어딘가에 저장이 되며 이 때문에 에러가 발생시 찾기가 매우 어려움


포인터와 수

int* pt;
//pt = 0xB8000000;
pt = (int *) 0xB8000000;

컴퓨터가 주소를 정수로 다루더라도, 포인터는 개념적으로 정수형과는 다른 데이터임
C에서는 포인터에 정수를 직접 대입하는 것이 가능했으나, C++에서는 어떤 수를 주소로 사용하려면 데이터형 변환자를 통해 적당한 주소형으로 반드시 변환해주어야 함


new를 사용한 메모리 대입

C에서 malloc()을 통해 이름이 없는 메모리를 대입한 것처럼 C++에서는 더 좋은 도구인 new연산자를 사용할 수 있음
typeName * pointer_name = new typeName; 의 형식을 사용

//1
int * pn = new int;

//2
int higgens;
int * pt = &higgens;

1번의 경우 포인터 pn이 int형 값에 접근할 수 있는 유일한 통로가 되며, 데이터 객체(data object)를 지시하고 있다고 말함

  • 데이터 객체 : 어떤 데이터를 저장하기 위해 대입된 메모리 블록을 의미

2번의 경우 pt를 통하지 않고 변수 이름 higgens를 통하여 int형 값에 접근할 수 있음
변수는 하나의 데이터 객체임

// use_new.cpp

#include <iostream>

int main()
{
	using namespace std;
	int nights = 1001;
	int * pt = new int;
	*pt = 1001;

	cout << "nights의 값 = ";
	cout << nights << " : 메모리 위치 " << &nights << endl;
	cout << "int형";
	cout << " 값 = " << *pt << " : 메모리 위치 " << pt << endl;

	double * pd = new double;
	*pd = 10000001.0;

	cout << "double형";
	cout << " 값 = " << *pd << " : 메모리 위치 = " << pd << endl;
	cout << "포인터 pd의 메모리 위치 : " << &pd << endl;
	cout << "pt의 크기 = " << sizeof(pt);
	cout << " : *pt의 크기 = " << sizeof(*pt) << endl;
	cout << "pd의 크기 = " << sizeof pd;
	cout << " : *pd의 크기 = " << sizeof(*pd) << endl;
	return 0;
}

데이터 객체를 저장할 메모리를 new를 사용하여 대입함
이 때의 메모리 대입은 컴파일시가 아닌 프로그램이 실행되는 동안에 일어나며, 이는 객체 지향 프로그래밍의 특징으로 그때그때 상황에 적절하게 대처할 수 있는 융통성이 있음

컴퓨터의 메모리가 부족하여 new의 메모리 대입 요청을 허용할 수 없는 경우 new는 0을 반환하며 이를 널 포인터(null pointer)라고 부름


delete를 사용한 메모리 해제

메모리 누수가 발생하지 않도록 사용한 메모리를 다시 메모리 풀로 환수하기 위해 사용
단, 메모리만 해제하는 것이지 포인터 자체를 삭제하는 것은 아님
이미 해제한 메모리 블록을 다시 해제하는 것은 undefined behavior이기 때문에 결과를 예측할 수 없음
일반적으로 선언된 변수에 대입된 메모리는 delete로 해제할 수 없으며, 오직 new로 대입된 메모리에만 사용할 수 있음


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

어떠한 배열이(혹은 변수, 문자열, 구조체 등) 컴파일 시간에 생성될 경우를 정적 바인딩(static binding)이라고 하며, 이 경우 해당 배열의 실제 사용 여부와 관계없이 항상 메모리를 차지하게 됨
반대로 실제로 실행중 사용되는 배열만을 생성하거나, 프로그램이 실행되는 동안에 배열의 크기를 결정하는 것을 동적 바인딩(dynamic binding)이라고 하며 이때의 배열을 동적 배열(dynamic array)라고 함

int * psome = new int [10];
delete [] psome;

int * pt = new int;
delete pt;

new를 사용한 동적 배열의 생성시에는 대괄호 []에 생성할 배열 원소의 개수를 지정함
delete로 배열을 해제할때 역시 해당 배열 전체를 해제한다는 의미로 대괄호를 사용해야함
즉, newdelete사이에는 짝을 맞추어주어야 함
동적 배열을 가리키는 포인터 변수는 해당 배열의 첫번째 원소의 주소를 지시함

// arraynew.cpp

#include <iostream>

int main()
{
	using namespace std;
	double * p3 = new double [3];

	p3[0] = 0.2;
	p3[1] = 0.5;
	p3[2] = 0.8;
	cout << "p3[1]은 " << p3[1] << "입니다.\n";
	p3 = p3 + 1;
	cout << "이제는 p3[0]이 " << p3[0] << "이고, ";
	cout << "p3[1]이 " << p3[1] << "입니다.\n";
	p3 = p3 - 1;
	delete [] p3;
	return 0;
}

포인터명[원소 인덱스]로 동적 배열의 원소에 접근할 수 있으며, 이는 C와 C++이 배열을 구현할 때 내부적으로 포인터를 사용하기 때문에 가능함
포인터 변수에 + 1을 할 경우 포인터가 다음 원소를 가리키게 됨
이 때 포인터 변수의 값(포인터가 가리키는 메모리 주소)은 포인터가 지시하는 데이터형의 바이트 수만큼 증가함



4.8 포인터, 배열, 포인터 연산

// addpntrs.cpp

#include <iostream>

int main()
{
	using namespace std;
	double wages[3] = {10000.0, 20000.0, 30000.0};
	short stacks[3] = {3, 2, 1};

	double * pw = wages;
	short * ps = &stacks[0];

	cout << "pw = " << pw << ", *pw = " << *pw << endl;
	pw = pw + 1;
	cout << "pw 포인터에 1을 더함 :\n";
	cout << "pw = " << pw << ", *pw = " << *pw << "\n\n";

	cout << "ps = " << ps << ", *ps = " << *ps << endl;
	ps = ps + 1;
	cout << "ps 포인터에 1을 더함 :\n";
	cout << "ps = " << ps << ", *ps = " << *ps << "\n\n";

	cout << "배열 표기로 두 원소에 접근\n";
	cout << "stacks[0] = " << stacks[0]
		 << ", stacks[1] = " << stacks[1] << endl;
	cout << "포인터 표기로 두 원소에 접근\n";
	cout << "*stacks = " << *stacks
		 << ", *(stacks + 1) = " << *(stacks + 1) << endl;
	
	cout << sizeof(wages) << " = wages 배열의 크기\n";
	cout << sizeof(pw) << " = pw 포인터의 크기\n";
	return 0;
}

C++에서 배열 이름은 일반적으로 그 배열의 첫 번째 원소의 주소로 해석됨
배열[index]*(배열 + index)는 완벽히 같은 것으로 취급
포인터도 마찬가지이며, 따라서 포인터 이름과 배열 이름을 같은 방식으로 사용할 수 있음

short tell[10];
cout << tell << endl;
cout << &tell << endl;
cout << tell + 1 << endl;
cout << &tell + 1<< endl;

위 코드에서 배열 이름인 tell은 &tell[0]값을 뜻하고, &tell은 전체 배열의 주소를 뜻함
따라서 tell + 1은 short 자료형의 크기인 2바이트만큼이 더해진 주소값을 나타내지만, &tell + 1은 tell 배열의 전체 크기인 20바이트만큼이 더해진 주소값을 나타냄

포인터와 문자열

char flower[10] = "rose";
cout << flower << "s are red\n";

배열의 이름은 첫번째 원소의 주소이기 때문에, cout구문의 flower는 문자 r이 들어있는 char형 원소의 주소가 됨
즉, cout의 매개변수로 char형을 지시하는 포인터 변수를 사용할 수 있음
큰따옴표로 둘러싸인 문자열 상수 역시 그 문자열의 첫번째 문자의 주소를 나타냄

// ptrstr.cpp

#include <iostream>
#include <cstring>

int main()
{
	using namespace std;
	char animal[20] = "bear";
	const char * bird = "wren";
	char * ps;

	cout << animal << " and ";
	cout << bird << "\n";
	// cout << ps << "\n";

	cout << "동물의 종률 입력하십시오 : ";
	cin >> animal;
	// cin >> ps;

	ps = animal;
	cout << ps << "s!\n";
	cout << "strcpy() 사용 전 : \n";
	cout << (int *) animal << " : " << animal << endl;
	cout << (int *) ps << " : " << ps << endl;

	ps = new char[strlen(animal) + 1];
	strcpy(ps, animal);
	cout << "strcpy() 사용 후 : \n";
	cout << (int *) animal << " : " << animal << endl;
	cout << (int *) ps << " : " << ps << endl;
	delete [] ps;
	return 0;
}

포인터 bird에는 문자열 상수 “wren”이 초기화되어있음
C++에서는 문자열 상수들이 유일하게 저장된다고 보장하지 않기 때문에 동일한 문자열 상수가 프로그램에 여러번 사용될 경우 한개로 저장될 수도 있고, 여러개로 저장될 수도 있음
이 때 “wren”은 상수이기 때문에 포인터 bird 역시 const 포인터로 선언되었으며, 이 경우 포인터로 해당 문자열에 접근할 수는 있으나 문자열을 변경할 수는 없음

cout에 포인터가 전달되는 경우 일반적으로는 주소가 출력되지만, 포인터가 char *형일 경우에는 그 포인터가 지시하는 문자열이 출력됨
ps = animal은 단순히 ps에 저장된 주소를 배열 이름 animal이 가리키는 배열의 첫번째 원소의 주소로 대입시킴
따라서 문자열을 복사하기 위해서는 strcpy() 함수를 사용해야 하며, 이때 문자열이 수용될 메모리가 new나 또다른 배열 선언 등으로 지정되어야함


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

// newstrct.cpp

#include <iostream>
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	using namespace std;
	inflatable * ps = new inflatable;

	cout << "모형풍선의 이름을 입력하십시오 : ";
	cin.get(ps->name, 20);
	cout << "부피를 세제곱 피트 단위로 입력하십시오 : ";
	cin >> (*ps).volume;
	cout << "가격을 달러 단위로 입력하십시오 : $";
	cin >> ps->price;
	cout << "이름 : " << (*ps).name << endl;
	cout << "부피 : " << ps->volume << "cubic feet\n";
	cout << "가격 : $" << ps->price << endl;
	delete ps;
	return 0;
}

new 연산자를 사용하여 동적 구조체(컴파일 시간이 아닌 실행 시간이 메모리를 대입받음)를 생성할 수 있음
동적 구조체는 익명으로 생성되기 때문에 구조체 이름에 .연산자를 사용하여 멤버에 접근하는 것이 아닌, 포인터에 ->연산자를 사용하여 멤버에 접근
또는 (*pointer).member와 같이 포인터에 * 연산자를 붙여 구조체를 나타낸 후 .을 사용하는 방법도 가능함

// delete.cpp

#include <iostream>
#include <cstring>
using namespace std;
char * getname(void);

int main()
{
	char * name;

	name = getname();
	cout << (int *) name << " : " << name << endl;
	delete [] name;

	name = getname();
	cout << (int *) name << " : " << name << endl;
	delete [] name;
	return 0;
}

char * getname()
{
	char temp[80];
	cout << "이름을 입력하십시오 : ";
	cin >> temp;
	char * pn = new char[strlen(temp) + 1];
	strcpy(pn, temp);

	return pn;
}

getname() 함수에서는 입력 문자열을 임시 배열에 읽어들인 다음, new []를 사용하여 해당 문자열의 크기에 맞는 메모리 블록을 대입하고 그 메모리 블록을 지시하는 포인터를 리턴함
이렇게 하면 메모리 공간을 대폭 절약할 수 있음

C++에서는 delete로 메모리를 해제한 후 다시 new를 해도 같은 메모리가 대입될 것이라고 보장하지는 않음
newdelete를 서로 다른 함수에서 사용하는 것은 권장되지 않음


자동 공간 / 정적 공간 / 동적 공간

자동 공간(automatic storage)을 사용하는 함수 안에서 정의되는 보통의 변수들을 자동 변수(automatic variable)이라고 부름

  • 자동 변수 : 변수가 정의되어있는 함수가 호출되는 순간에 자동으로 생겨나며, 함수 종료시 자동으로 해제됨
    • 해당 변수를 포함하고있는 블록 안에서만 유효함
    • 스택(stack)에 저장되어 후입선출(last-in, first-out)의 규칙을 따름

정적 공간(static storage)는 프로그램이 실행되는 동안에 지속적으로 존재하는 공간임

  • 정적 변수는 함수 외부에서 변수를 정의하거나, static 키워드를 사용하여 만들 수 있음
  • C에서는 정적 배열 및 정적 구조체만을 초기화할 수 있었으나, C++에서는 자동 배열 및 구조체를 초기화하는 것도 허용함

동적 공간(dynamic storage)은 자유 공간(free store) 또는 힙(heap)이라고도 하며, 자동 변수 및 정적 변수가 사용하는 메모리와 분리되어있음
동적 공간에 저장된 데이터의 수명은 프로그램 또는 함수의 수명에 얽매이지 않음
new로 동적 공간에 변수를 생성한 후 delete로 해제하지 않는 경우 해당 데이터 객체는 메모리에 계속해서 남게되고, 해당 위치를 지시하는 포인터가 사라진 경우 접근마저 불가능해지는 메모리 누수(memory leak)이 발생함



4.9 변수형의 조합

// mixtypes.cpp

#include <iostream>

struct antarctica_years_end
{
	int year;
};

int main()
{
	antarctica_years_end s01, s02, s03;
	s01.year = 1998;
	antarctica_years_end * pa = &s02;
	pa->year = 1999;
	antarctica_years_end trio[3];
	trio[0].year = 2003;
	std::cout << trio->year << std::endl;

	const antarctica_years_end * arp[3] = {&s01, &s02, &s03};
	std::cout << arp[1]->year << std::endl;
	const antarctica_years_end ** ppa = arp;
	auto ppb = arp;
	std::cout << (*ppa)->year << std::endl;
	std::cout << (*(ppb+1))->year << std::endl;
	return 0;
}

배열에 대한 포인터 생성시 const를 사용해야함
C+11의 auto를 사용하면 변수형 선언시 매우 편리함



4.10 배열의 대안

Vector 탬플릿 클래스

동적 배열에서 속하는 string 클래스와 유사

  • 프로그램이 실행되는 동안 vector 객체의 크기를 세팅할 수 있음
  • 새로운 데이터를 마지막에 추가하거나 중간에 데이터를 삽입할 수 있음
  • new의 사용을 대체할 수 있으며, 내부적으로 newdelete의 사용 과정을 자동으로 진행함
  • vector 헤더 파일을 선언해야하며, std 이름 공간의 일부분임
  • 템플릿은 저장된 데이터 형태를 지시하기 위해 다른 구문을 사용해야 하며, vector 클래스는 원소의 개수를 지칭하기 위해 다른 구문을 사용해야 함
# include <vector>
...
using namespace std;
vector<int> vi;
int n;
cin >> n;
vector<double> vd(n);

vector<typeName> vt(n_elem);의 형식으로 vector 객체를 생성함


array 템플릿 클래스

사용자가 고정된 크기의 배열만 필요할 경우 vector클래스는 내재 배열형보다 비효율적일 수 있음
그러나 내재 배열형은 안전성과 편리성이 더 낮기 때문에, C++11에서는 이에 array 템플릿 클래스를 더해 줌으로써 해결함

  • array 템플릿 클래스는 std 이름 공간의 일부분에 해당함
  • 내재 배열형과 마찬가지로 array 객체는 자유 저장 대신 고정된 크기와 고정 메모리 대입을 사용하여 동일한 수준의 효율성을 가짐
  • array 헤더 파일을 포함시켜야 함
#include <array>
...
using namespace std;
array<int, 5> ai;
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};

array<typeName, n_elem> arr;의 형식으로 array 객체를 생성함
이 때 n_elem은 변수가 될 수 없음


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

// choicess.cpp

#include <iostream>
#include <vector>
#include <array>

int main()
{
	using namespace std;
	
	double a1[4] = {1.2, 2.4, 3.6, 4.8};

	vector<double> a2(4);
	a2[0] = 1.0/3.0;
	a2[1] = 1.0/5.0;
	a2[2] = 1.0/7.0;
	a2[3] = 1.0/9.0;

	array<double, 4> a3 = {3.14, 2.72, 1.62, 1.41};
	array<double, 4> a4;
	a4 = a3;

	cout << "a1[2] : " << a1[2] << " at " << &a1[2] << endl;
	cout << "a2[2] : " << a2[2] << " at " << &a2[2] << endl;
	cout << "a3[2] : " << a3[2] << " at " << &a3[2] << endl;
	cout << "a4[2] : " << a4[2] << " at " << &a4[2] << endl;

	a1[-2] = 20.2;
	cout << "a1[-2] : " << a1[-2] << " at " << &a1[-2] << endl;
	cout << "a2[2] : " << a3[2] << " at " << &a3[2] << endl;
	cout << "a4[2] : " << a4[2] << " at " << &a4[2] << endl;
	return 0;
}

array 객체는 스택, vector 객체는 힙에 저장됨
하나의 array 객체를 또 다른 array 객체에 대입할 수 있음
범위를 벗어나서 발생하는 에러는 at()멤버 함수를 사용하여 보호할 수 잇음

  • at() 멤버 함수는 vector.at() 형식으로 vector`객체 또는 배열 타입에 사용할 수 있음
  • 유효하지 않은 인덱스를 사용할경우 런타임과 프로그램 실행 도중에 디폴트에 의해 잡히게 되어 전격 취소됨


연습문제

  1. a) char actors[30];
    b) short betsie[100];
    c) float chuck[13];
    d) long double dipsea[64];

  2. a) array<char, 30> actors;
    b) array<short, 100> betsie;
    c) array<float, 13> chunk;
    d) array<long double, 64> dipsea;

  3. int a[5] = {1, 3, 5, 7, 9};

  4. int even = a[0] + a[4];

  5. cout << ideas[1];

  6. char str[] = "cheeseburger";

  7. string str = "Waldorf Salad";

  8.  struct fish
     {
         char kind[100];
         int weight;
         double length;
     };
    
  9. fish fish1 = {"salmon", 10, 10};

  10. enum Response {No, Yes, Maybe};

  11. double* ptr = &ted;
    cout << *ptr;
    
  12. float* ptr = treacle;
    cout << *ptr << ", " << *(pt + 9);
    
  13. int num;
    cin >> num;
    int* ptr = new int[num];
    vector<int> vi(num);
    
  14. 올바름, 상수 문자열의 시작주소가 출력됨

  15. fish* ptr = new fish;
    cin >> ptr->kind;
    
  16. 화이트스페이스가 아닌 문자가 나타날 때까지 화이트스페이스를 무시, 다음번 화이트스페이스까지 문자를 읽음
    따라서 한 단어만 읽을 수 있음

  17. #include <string>
    #include <vector>
    #include <array>
    
    const int num = 10;
    std::vector<std::string> vi(num);
    std::array<std::string, num> arr;
    

Tags:

Categories:

Updated: