AcceleratedC++/Chapter9 | AcceleratedC++/Chapter11 |
1. Chapter 10 Managing memory and low-level data structures ¶
지금까지는 vector, string등 STL이 기본적으로 제공하는 자료구조를 통해서 프로그래밍을 하였다.
이제 그 동작원리를 알아볼 차례이다.
low-level이라는 표현은 이런 내용들이 STL구현의 근간을 이루며, 하드웨어의 동작 방식과 매우 흡사하기 때문이다.
이제 그 동작원리를 알아볼 차례이다.
low-level이라는 표현은 이런 내용들이 STL구현의 근간을 이루며, 하드웨어의 동작 방식과 매우 흡사하기 때문이다.
1.1. 10.1 Pointers and arrays ¶
배열(array) vector와 유사하지만 vector만큼 편리하지는 않다.
포인터(pointer) 임의 접근 반복자로서, 배열의 요소들을 접근하기 위해서는 필수, 다른 용도로도 많이 이용된다.
포인터(pointer) 임의 접근 반복자로서, 배열의 요소들을 접근하기 위해서는 필수, 다른 용도로도 많이 이용된다.
1.1.1. 10.1.1 포인터 ¶
포인터(pointer) | 주소를 나타내는 값 |
주소 연산자(address operator) | 객체의 주소를 리턴한다. |
역참조 연산자(dereference operator) | 포인터 p가 가리키는 객체를 리턴한다. |
포인터도 타입을 갖는데 일반적으로 type-name * 으로 정의한다.
~cpp int *p; // *p는 int 타입을 갖는다. int* p; // p는 int*라는 것을 강조하는 표현이다. C컴파일러는 * 주변의 공백을 무시하기 때문에 상기의 2개 표현은 완전히 동일하다.
피해야할 정의 방식
~cpp int* p, q; // p는 int* 인 포인터 형식, q는 int 형을 가리킨다.
예제
~cpp //pointer_example.cpp #include <iostream> using std::cout; using std::endl; int main() { int x = 5; // `p' points to `x' int* p = &x; cout << "x = " << x << endl; // change the value of `x' through `p' *p = 6; cout << "x = " << x << endl; return 0; }
1.1.2. 10.1.2 함수에 대한 포인터 ¶
※ 함수포인터를 처음으로 다루는 사람은 조금 어려울 지도 모르겠습니다. 좀 어렵더라도 C, C++에서 많이 이용되는 테크닉이니 익히는 건 필수이겠지요?
6.2.2 절에서 다른 함수의 인자로 함수를 전달하는 것을 보았다.
함수에 대해서 우리가 할 수 잇는 것은 그 함수의 주소를 취하거나, 실행하는 것 밖에는 없다.
함수에 대한 포인터를 인자로 전달하고 역참조 연산자를 통해서 그 포인터에 접근하면 우리는 원래의 함수에 접근하는 것이 가능하다.
C++은 상기와 같은 표현으로 매개변수를 지정하더라도 이를 자동으로 다음과 같은 형으로 자동 형 변환시킨다.
하지만 이러한 자동 형 변환은 함수의 리턴형에는 적용되지 않는다. 따라서 함수를 리턴하는 경우에는 명식적으로 포인터임을 나타내야할 필요가 있다.
그렇지만 언뜻보기에도 코드가 2번째의 경우 코드가 상당히 복잡하다는 것을 알 수 잇습니다.
이런식의 문법은 많이 사용되지는 않습니다. (Addition 1에서 설명합니다.)
함수 포인터는 흔히 다른 함수의 인자로 이용된다.
6.2.2 절에서 다른 함수의 인자로 함수를 전달하는 것을 보았다.
함수에 대해서 우리가 할 수 잇는 것은 그 함수의 주소를 취하거나, 실행하는 것 밖에는 없다.
함수에 대한 포인터를 인자로 전달하고 역참조 연산자를 통해서 그 포인터에 접근하면 우리는 원래의 함수에 접근하는 것이 가능하다.
~cpp int (*fp)(int); int next(int n) { return n+1; } fp = &next; fp = next; // 상기의 2가지 표현 모두 next함수의 포인터를 fp에 대입하는 것이 가능하다. int i = 0; int n =1; i = (*fp)(i); i = fp(i); // 마찬가지로 이 2개의 표현모두 유효한 표현이된다.6.2.2절에 존재하는 write_ayalysis의 선언구문을 살펴보도록 하자.
~cpp void write_analysis(std::ostream& out, const std::string& name, double analysis(const std::vector<Student_info>&), const std::vector<Student_info>& did, const std::vector<Student_info>& didnt);상기에서 3번째 매개변수를 살펴보도록 하자.
~cpp double analysis(const std::vector<Student_info>&)
C++은 상기와 같은 표현으로 매개변수를 지정하더라도 이를 자동으로 다음과 같은 형으로 자동 형 변환시킨다.
~cpp double (*analysis)(const std::vector<Student_info>&)따라서 우리가 일반적으로 사용하는 함수의 표현만으로도 매개변수로 함수를 전달시키는 것이 가능한 것이다.
하지만 이러한 자동 형 변환은 함수의 리턴형에는 적용되지 않는다. 따라서 함수를 리턴하는 경우에는 명식적으로 포인터임을 나타내야할 필요가 있다.
~cpp //올바른 표현 typedef double (*analysis_fp)(const vector<Student_info>&); analysis_fp get_analysis_ptr(); // 이와 같이 이용하는 것이 가능합니다. //올바르지 않은 표현 double (*get_analysis_ptr()) (const vector<Student_info>&);상기의 코드에서 프로그래머가 원한 기능은 get_analysis_ptr()을 호출하면 그 결과를 역참조하여서 const vector<Student_info>&를 인자로 갖고 double 형을 리턴하는 함수를 얻는 것입니다.
그렇지만 언뜻보기에도 코드가 2번째의 경우 코드가 상당히 복잡하다는 것을 알 수 잇습니다.
이런식의 문법은 많이 사용되지는 않습니다. (Addition 1에서 설명합니다.)
함수 포인터는 흔히 다른 함수의 인자로 이용된다.
~cpp template<class In, class Pred> In find_if(In begin, In end, Pred f) { while (begin != end && if f(*begin)) //여기서 Pred는 Pred(*begin)이 의미를 갖는 모든 타입이 가용합니다. ++begin; return begin; } bool is_negative(int n) { return n<0; } vector<int>::iterator i = find_if(v.begin(), v.end(), is_negative); // &is_negative 를 사용하지 않는 이유는 자동형변환으로 포인터형으로 변환되기 때문임.
1.1.3. 10.1.3 배열 ¶
배열(Array)
표준 라이브러리가 아닌 기본언어의 일부로서 하나 이상의 같은 타입의 객체들로 이루어진 시퀀스입니다. 단 배열 요소의 갯수는 컴파일 시에 알 수 있어야합니다.
또한 배열의 이름은 그 배열의 요소의 첫번째 요소를 가리키는 포인터형으로 초기화 되기 때문에 배열과 포인터는 밀접한 상관 관계를 갖고 잇다.
표준 라이브러리가 아닌 기본언어의 일부로서 하나 이상의 같은 타입의 객체들로 이루어진 시퀀스입니다. 단 배열 요소의 갯수는 컴파일 시에 알 수 있어야합니다.
※ C99 표준에서는 변수로 크기를 받아서 동적으로 할당하는 일이 가능합니다.
배열은 클래스 타입이 아니기 때문에 배열의 크기를 나타내는 size_type과 같은 요소는 없습니다. 대신에 C Standard Definition 이하 <cstddef> 에 size_t로 지정된 unsigned 타입으로 배열의 크기를 사용해야합니다.~cpp const size_t NDim = 3; const size_t PTriangle = 3; double coords[NDim]; double coords1[PTriangle];상기와 같은 표현은 const로 지정된 변수로 변수를 초기화하기 때문에 컴파일시에 그 크기를 알 수 있다. 또한 상기와 같은 방식으로 코딩을 하면 coord에 의미를 부여할 수 잇기 때문에 장점이 존재한다.
또한 배열의 이름은 그 배열의 요소의 첫번째 요소를 가리키는 포인터형으로 초기화 되기 때문에 배열과 포인터는 밀접한 상관 관계를 갖고 잇다.
~cpp *coord = 1.5; //coord 배열의 첫번째 요소를 1.5로 할당한다. 즉, coord[1] = 1.5; 와 동일한 표현이 된다.
1.1.4. 10.1.4 포인터 산술 연산 ¶
포인터도 일종의 반복자이다. 이사실에서 배열의 포인터를 이용한 접근을 생각해볼 수 잇다.
coord가 coord의 0번째 요소를 가르키므로
p, q가 같은 배열의 요소들의 포인터라면, p-q는 p와 q사이에 있는 요소들의 거리를 나타내는 정수가 된다.
그러나 이 차이를 나타내는 값은 구현 시스템 마다 다를 수 잇고, 음수가 나타내는 경우가 있기 때문에 <cstddef>에는 ptrdiff_t라는 약칭을 통해서 그 데이터 형을 제공한다.
a가 n개 요소의 배열이라면, a+i가 유효하기 위해서는 0<=i<=n, a+i가 a의 요소를 가리키기 위한 필요 충분 조건은, 0<=i
coord가 coord의 0번째 요소를 가르키므로
coord+1 | 1번째 요소 |
coord+2 | 2번째 요소 |
coord+3 | 3번째 요소. 배열에서는 유효하지 않지만 포인터 그 자체로는 유효한 포인터이다. |
coord+4 | 유효하지 않은 포인터 |
coord-1 | -1번째 요소. 배열에서는 유효하지 않지만 포인터 그 자체로는 유효한 포인터이다. |
~cpp vector<double> v; copy(coords, coords + NDim, back_inserter(v));coord+NDim 은 coord의 마지막 요소에서 1개가 더 지난 값을 가리키므로 상기의 표현은 유효한 표현식이다.
p, q가 같은 배열의 요소들의 포인터라면, p-q는 p와 q사이에 있는 요소들의 거리를 나타내는 정수가 된다.
그러나 이 차이를 나타내는 값은 구현 시스템 마다 다를 수 잇고, 음수가 나타내는 경우가 있기 때문에 <cstddef>에는 ptrdiff_t라는 약칭을 통해서 그 데이터 형을 제공한다.
a가 n개 요소의 배열이라면, a+i가 유효하기 위해서는 0<=i<=n, a+i가 a의 요소를 가리키기 위한 필요 충분 조건은, 0<=i
1.1.5. 10.1.5 인덱싱(Indexing) ¶
포인터는 임의 접근 반복자이다. 따라서 vector와 같이 임의 접근이 된다.
만약 p가 요소의 m번째 요소를 가리킨다면 pn은 m+n번째 요소를 가리킨다. (요소의 주소가 아니라 요소를 가리킨다.)
만약 p가 요소의 m번째 요소를 가리킨다면 pn은 m+n번째 요소를 가리킨다. (요소의 주소가 아니라 요소를 가리킨다.)
1.1.6. 10.1.6 배열 초기화 ¶
STL의 컨테이너들과는 달리 배열은 초기화에 있어서 다른 문법을 제공한다.
이 문법을 사용함으로써 배열의 초기화시에 사용자가 명시적으로 배열의 크기를 나타내지 않아도 된다.
이 문법을 사용함으로써 배열의 초기화시에 사용자가 명시적으로 배열의 크기를 나타내지 않아도 된다.
~cpp const int month_length[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // 윤년은 무시상기의 예제에서 month_length의 배열의 크기를 명시적으로 나타내지 않았다는 사실에 유의. 대신에 컴파일러가 초기화된 갯수에 맞추어서 배열을 할당한다.
1.2. 10.2 String literals revisited ¶
"hello"와 같은 문자열 리터럴은 사실은 const char형의 배열이다. 이 배열은 실제로 사용하는 문자의 갯수보다 한개가 더 많은 요소를 포함하는데, 이는 문자열 리터럴의 끝을 나타내는 '\0' 즉 널 캐릭터를 넣어야하기 때문이다.
~cpp const char hello[] = { 'h', 'e', 'l', 'l', 'o', '\0' }; //이 표현은 const char hello[] = "hello";와 동일한 표현이다.<cstring> strlen
~cpp size_t strlen(const char* p) { // 쉬우니 기타 설명은 생략 size_t size = 0; while (*p++ != '\0') ++size; return size; }상기의 내용을 바탕으로 string에서 우리는 다음과 같은 초기화를 행하는 것이 가능하다.
~cpp string s(hello); string s("hello"); string s(hello, hello+strlen(hello));상기의 3가지 표현은 모두 같다.
1.3. 10.3 Initializing arrays of character pointers ¶
~cpp //letter_grade.cpp #include <cstddef> #include <string> using std::string; string letter_grade(double grade) { // range posts for numeric grades static const double numbers[] = { 97, 94, 90, 87, 84, 80, 77, 74, 70, 60, 0 }; // names for the letter grades static const char* const letters[] = { "A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D", "F" }; // compute the number of grades given the size of the array // and the size of a single element static const size_t ngrades = sizeof(numbers)/sizeof(*numbers); // given a numeric grade, find and return the associated letter grade for (size_t i = 0; i < ngrades; ++i) { if (grade >= numbers[i]) return letters[i]; } return "?\?\?"; }
static 키워드를 사용하여서 배열의 초기화에 소요되는 시간을 단축한다.
요소 배열은 변화하지 않느 값이므로 const 키워드 이용
array_pointer / sizeof(*array_pointer) 를 이용하면 array가 가지고 있는 요소의 갯수를 알 수 있다. 자주쓰는 표현이므로 잘 익힌다.
요소 배열은 변화하지 않느 값이므로 const 키워드 이용
array_pointer / sizeof(*array_pointer) 를 이용하면 array가 가지고 있는 요소의 갯수를 알 수 있다. 자주쓰는 표현이므로 잘 익힌다.
1.4. 10.4 Arguments to main ¶
대부분의 운영체제는 프로그램이 실행될때 인자로서 문자열을 주는 것이 가능하다. 이것은 main함수가 int, char** 의 2가지의 인자를 가지기 때문이다.
기타의 내용은 간단하므로 생략.
~cpp #include <iostream> using std::cout; using std::endl; int main(int argc, char** argv) { // if there are command-line arguments, write them if (argc > 1) { cout << argv[1]; // write the first argument // write each remaining argument with a space before it for (int i = 2; i != argc; ++i) cout << " " << argv[i]; // `argv[i]' is a `char*' } cout << endl; return 0; }char**는 (char*)를 요소로 갖는 배열이라고 생각하면 된다. 즉 문자열 리터럴을 요소로 갖는 배열이다.
기타의 내용은 간단하므로 생략.
1.5.1. 10.5.1 표준 에러 스트림 ¶
C++에서는 일반적인 표준 출력 스트림 뿐만아니라 표준 에러 스트림을 통해서 프로그램의 주석의 내용들. 즉 프로그램이 실행되는 동안의 상태정보를 출력하는 것이 가능하다.
대부분의 운영체제에서는 이런 2가지의 출력을 분리하여 처리하는 방법을 제공한다.
※ 출력 버퍼링에 관한 내용은 자세하게 나온 책이 많으므로 참고하면 좋을 듯. C책이기는 하지만 터보 C 정복을 보면 출력 스트림의 버퍼링에 관한 자세한 설명이 있다.
대부분의 운영체제에서는 이런 2가지의 출력을 분리하여 처리하는 방법을 제공한다.
cerr | 버퍼링을 사용하지 않고, 즉각적인 출력을 한다. 오버헤드가 크다. |
clog | 버퍼링을 이용하고, 적당한 시기에 출력한다. |
1.5.2. 10.5.2 여러 입력 파일과 출력 파일 다루기 ¶
파일의 입출력을 위해서 iostream을 파일 입출력에 맞도록 상속시킨 ofstream, ifstream객체들을 이용한다. 이들 클래스는 <fstream>에 정의 되어있다.
<fstream>에서 사용하는 파일 이름은 char* 형이며, string 타입이 아니다. 이는 fstream library 가 만들어진 시기가 STL이 만들어진 시기 보다 앞서기 때문이다.
<fstream>에서 사용하는 파일 이름은 char* 형이며, string 타입이 아니다. 이는 fstream library 가 만들어진 시기가 STL이 만들어진 시기 보다 앞서기 때문이다.
~cpp //copy_file.cpp #include <fstream> #include <string> using std::endl; using std::getline; using std::ifstream; using std::ofstream; using std::string; int main() { ifstream infile("in"); ofstream outfile("out"); string s; while (getline(infile, s)) outfile << s << endl; return 0; }만약 파일의 이름으로 string 형을 이용하고 싶다면 string형의 멤버함수인 c_str() 을 이용해서 사용하는 것이 가능하다.
~cpp string file("out"); ifstream infile(file.c_str());
~cpp //concat_files.cpp #include <iostream> #include <fstream> #include <string> using std::cerr; using std::cout; using std::endl; using std::getline; using std::ifstream; using std::string; int main(int argc, char **argv) { int fail_count = 0; // for each file in the input list for (int i = 1; i < argc; ++i) { ifstream in(argv[i]); // if it exists, write its contents, otherwise generate an error message if (in) { string s; while (getline(in, s)) cout << s << endl; } else { cerr << "cannot open file " << argv[i] << endl; ++fail_count; } } return fail_count; }단순한 프로그램이므로 설명은 생략함. 한번 쳐보자.
1.6. 10.6 Three kinds of memory management ¶
~cpp int* invalid_pointer() { int x; return &x; //함수의 종료와 함께 x가 해제되므로 리턴되는 값은 무효한 메모리상의 공간을 가리킨다. }
~cpp int* pointer_to_static() { static int x; return &x; //유효하다. static 으로 함수 안에서 선언하면 함수가 처음 실행될때 메모리 공간이 한번 할당되고 그 함 수가 종료되더라도 해제되지 않고 프로그램이 종료될때 해제된다. 따라서 메모리가 해제되지 않은 static 변수를 리턴하는 것이기에 유효하다. }
자동메모리 관리 | 지역변수, 지역변수를 참조형, 포인터형으로 리턴하면 그 리턴값은 무효한 값이된다. 사용하고 싶다면 static 키워드를 이용해야한다. |
정적메모리 할당 | 정적 변수가 존재하는 블록이 처음 실행되는 동안 할당되어 프로그램이 실행되는 동안 계속 유효한 상태로 남는다. |
동적메모리 할당 | new, delete 키워드를 이용해서 프로그래머가 원하는 시기에 메모리상에 할당하고 해제를 할 수 있다. |
1.6.1. 10.6.1 객체 할당 및 해제 ¶
객체의 타입을 T라고 가장하고, 메모리 상에 동적으로 할당하기 위해서는
이렇게 할당된 메모리 공간은 프로그래머가 반드시 해제해야할 책임을 갖는다.
~cpp new T(args)와 같은 표현을 이용하면 된다.
이렇게 할당된 메모리 공간은 프로그래머가 반드시 해제해야할 책임을 갖는다.
1.6.2. 10.6.2 배열 할당 및 해제 ¶
객체의 타입을 T, 갯수를 음이아닌 정수 n이라 가장하고, 메모리 상에 동적으로 할당하기 위해서는
이렇게 배열로 할당되었을때 리턴되는 값은 T* 형으로 그 배열의 첫번째 요소의 포인터가 된다.
배열의 요소는 디폴트 생성자로 초기화된다.
이렇게 생성된 배열의 요소는 delete[] 키워드로 해제가 가능하다.
~cpp new T[n]와 같은 표현을 이용하면 된다.
이렇게 배열로 할당되었을때 리턴되는 값은 T* 형으로 그 배열의 첫번째 요소의 포인터가 된다.
배열의 요소는 디폴트 생성자로 초기화된다.
이렇게 생성된 배열의 요소는 delete[] 키워드로 해제가 가능하다.
~cpp char* duplicate_chars(const char* p) { size_t length = strlen(p) + 1; char* result = new char[length]; copy(p, p+length, result); return result;간단한 소스이므로 설명생략