이 글은 퍼온 글입니다.
원본은 아래의 주소에 있습니다.
http://yatoyato.tistory.com/1537

C++는 참조변수라는 새로운 복합형을 언어에 추가했다. 참조(reference)는 미리 정의된 어떤 변수의 실제 이름 대신 쓸 수 있는 대용이름이다. 예를 들어, twain을 clemens변수의 참조로 만들면, twain과 clemens는 같은 변수를 나타내는 것으로 사용할 수 있다. 참조의 주된 용도는 함수의 형식 매개변수에 사용하는 것이다. 참조를 전달인자로 사용하면, 그 함수는 복사본 대신 원본데이터를 가지고 작업한다. 덩치 큰 구조체를 처리해야 하는 함수에서 포인터 대신에 참조를 사용할 수 있다. 참조는 클래스를 설계할 때 필수적으로 사용된다.

참조변수의 생성

C와 C++는 변수의 주소를 나타내기 위해 &기호를 사용한다. C++는 &기호에 또하나의 의미를 추가하여 참조선언을 나타내게 하였다. 예를 들어, rodents를 변수 rats의 대용이름으로 만들려면 다음과 같이 할 수 있다.

int rats; 
int &rodents = rats;               // rodents를 rats의 대용이름으로 만든다.

여기에서 &가 주소연산자가 아니라, 데이터형 식별자의 일부로 사용된 것이다. 변수선언에서 char *가 문자를 지시하는 포인터를 의미하는 것처럼, int &는 int에 대한 참조를 의미한다. 이 두 변수는 모두 같은 값과 같은 메모리 위치를 참조한다.

rodents++;                         // rats가 증가한다.

참조는 포인터를 생각나게 한다. 예를 들면 rats를 참조하기 위해 참조와 포인터를 둘다 만들 수 있다.

int *prats = &rats;

그러나 포인터와 참조사이에는 표기방식 외에도 차이가 존재한다. 참조를 선언할 때 그 참조를 초기화해야 한다. 포인터를 선언할 때처럼, 참조를 먼저 선언하고 나중에 값을 지정할 수는 없다. 즉 최초 선언시 가리키는 변수를 변경할 수 없다.

int &rodent; 
rodent = rats;                     // 불가능하다.

참조가 다른 변수를 가리키면 어떻게 되는가?

int rats = 101; 
int bunnies = 50; 
int &rodents = rats; 
rodents = bunnies;                 // 후에 다른 변수를 가리키고자 할 때

이 때 rodents는 bunnies를 가리키는 것이 아니라 rats에 bunnies변수의 값 50을 대입한다. 이런 경우는 어떻게 될까?

int rats = 101; 
int *pt = &rats; 
int &rodents = *pt; 
int bunnies = 50; 
pt = &bunnies;

rodents를 *pt로 초기화하면 rodents가 rats를 참조한다. 이어서 pt가 bunnies를 지시하도록 변경해도 rodents가 rats를 참조한다는 사실은 변하지 않는다.

함수 매개변수로서의 참조

참조는 주로 함수의 매개변수로 사용된다. 그것은 어떤 함수에서 사용하는 변수의 이름을 그 함수를 호출한 프로그램(호출함수)에 있는 어떤 변수의 대용이름으로 만든다. 이러한 방식으로 매개변수를 전달하는 것을 참조로 전달이라 한다. 참조로 전달하면 피호출함수가 호출함수의 변수를 사용할 수 있다. C++에 새로 추가된 이 기능은 오로지 값으로만 전달하는 C로부터 탈피하는 것이다. 물론 C는 포인터를 사용하여 값으로 전달하는 것에 따른 제약을 극복한다.

swapr(x, y);                       // 함수호출 
void swapr(int &a, int &b);        // 함수원형

간단한 함수를 작성할 때에는, 쓸데없이 참조로 전달하려 하지말고, 값으로 전달해야 한다. 곧 알게 되겠지만, 참조 매개변수는 구조체나 클래스와 같이 덩치 큰 데이터를 다룰 때에나 유익하다. 당연히 참조변수는 상수가 포함된 전달인자(예를 들어, x+3.0)를 받아들일 수 없다. 그러나 예외적인 경우도 있는데, C++의 초창기에는 참조변수에 표현식도 전달할 수 있었다. 지금도 그런 경우가 있기는 하다. 그런 경우에는 다음과 같은 일이 일어난다. x+3.0은 double형 변수가 아니므로, 프로그램은 이름없는 임시변수를 만들어 그것을 표현식 x+3.0의 값으로 초기화한다. 그렇게 되었을 때 형식 매개변수는 그 임시변수에 대한 참조가 된다.

C++는 실제 전달인자와 참조 전달인자가 일치하지 않을 때 임시변수를 생성할 수 있다. 최근의 C++는 전달인자가 const참조일 경우에만 이것을 허용한다. 이것은 새로운 제약이다. 참조 매개변수가 const일 경우, 컴파일러는 다음과 같은 두가지 상황에서 임시변수를 생성한다.

  • 실제 전달인자가 올바른 데이터형이지만 lvalue가 아닐 때
  • 실제 전달인자가 잘못된 데이터형이지만 올바른 데이터형으로 변환할 수 있을 때

lvalue전달인자는 참조가 가능한 데이터 객체이다. 예를 들어 변수, 배열의 원소, 구조체의 멤버, 참조, 내용참조 포인터는 lvalue이다. 일반상수와 여러개의 항으로 이루어진 표현식은 lvalue가 아니다. const double형의 참조 형식매개변수가 존재할 때, 데이터형이 일치하지 않거나, 상수, 상수표현식은 임시변수를 생성하고 형식 매개변수가 이를 참조하게 만든다. 임시변수는 함수가 호출되어 있는 동안 유지되지만, 그 후에 컴파일러는 그것을 마음대로 없앨 수 있다.

이러한 행동이 상수참조일 경우에는 옳지만 다른 경우에는 옳지 않다. 예를 들어 두 변수의 값을 교환하는 스왑함수의 경우, 함수 내에서 임시변수의 내용의 교환이 실제 전달된 변수의 교환으로 이루어지지는 않는다. 간단히 말해서, 참조 전달인자를 가진 함수의 목적이 전달인자로 전달되는 변수를 변경하는 것이라면, 임시변수의 생성은 그 목적을 방해한다. 이것을 해결하는 방법은 이러한 상황에서는 임시변수의 생성을 허용하지 않는 것이다. 최근의 C++표준은 그렇게 하고 있다. 그러나 일부 컴파일러들은 여전히 에러메시지 대신 경고메시지를 디폴트로 내보낸다. 따라서 임시변수와 관련된 경고가 나왔을 때, 절대 그것을 무시하면 안된다.

참조 전달인자를 상수데이터에 대한 참조로 선언하는 이유는 다음과 같은 세가지 이점이 있기 때문이다.

  • const를 사용하면, 실수로 데이터변경을 일으키는 프로그래밍 에러를 막을 수 있다.
  • 원형에 const를 사용하면, 함수가 const와 const가 아닌 실제 전달인자를 모두 처리할 수 있지만, 원형에 const를 생략한 함수는 const가 아닌 데이터만 처리할 수 있다.
  • const참조를 사용하면, 함수가 자신의 필요에 따라 임시변수를 생성하여 사용할 수 있다.

따라서 가능하면 참조형식 매개변수를 const로 선언하는 것이 좋다.

구조체에 대한 참조

참조는 C++의 사용자정의 데이터형인 구조체나 클래스를 다루는데 아주 유용하게 사용된다. 참조는 기본 데이터형보다 주로 그러한 사용자정의 데이터형에 사용하기 위해 도입된 것이다.

struct sysop { ... } 
sysop looper { ... , int used; }; 
sysop copycat; 

use(looper); 
copycat = use(looper); 
use(looper).used

const sysop &use(sysop &sysopref) { ... , return sysopref; }

구조체를 참조로 전달하여 함수를 수행하므로 구조체 멤버가 변경된다. 일반적으로 함수의 리턴값은 임시저장영역에 복사되고, 호출프로그램이 그것을 사용한다. 그러나 참조를 리턴하면 호출프로그램은 복사본이 아니라 리턴값을 직접 사용한다. 리턴되는 참조는 처음에 함수에 전달했던 바로 그 참조를 말하므로, 호출프로그램은 실제로는 자기 자신의 변수를 직접 사용하게 된다. 예를 들면, sysopref는 looper에 대한 참조이므로, 리턴값은 main()에 있는 looper변수 자체가 된다.

참조를 리턴할 때 기억해야 할 가장 중요한 것은, 함수가 종료할 때 수명이 함께 끝나는 메모리 위치에 대한 참조를 리턴하지 않도록 조심하는 것이다. 다음과 같은 코드를 사용하지 말아야 한다.

const sysop& clone2(sysop &sysopref) { 
    sysop newguy;                  // 큰 에러를 일으키는 첫걸음 
    newguy = sysopref;             // 정보를 복사한다. 
    return newguy;                 // 복사본에 대한 참조를 리턴한다. 
}

이 코드는 함수가 종료할 때 함께 사라질 운명인 임시변수에 대한 참조를 리턴하는 불행한 실수를 저지르고 있다. 마찬가지로, 그러한 임시변수를 지시하는 포인터를 리턴하는 것도 피해야 한다. 이 문제를 피하는 가장 간단한 방법은, 함수에 전달인자로 전달된 참조를 리턴하는 것이다. 참조 매개변수는 호출함수가 사용하는 데이터를 참조한다. 그러므로 리턴되는 참조도 동일한 그 데이터를 참조한다.

두번째 방법은 new를 사용하여 새로운 기억공간을 만드는 것이다.

const sysop& clone(sysop &sysopref) { 
    sysop *psysop = new sysop; 
    &psysop = sysopref; 
    return *psysop; 
}

이 코드는 언뜻 보면 구조체를 리턴하는 것처럼 보이지만, 함수선언을 살펴보면 실제로는 구조체에 대한 참조를 리턴하는 것이다. 그러면 이제 이 함수를 다음과 같은 방법으로 사용할 수 있다.

sysop &jolly = clone(looper);

여기서 한가지 주의할 점은 할당된 메모리를 delete로 반드시 삭제해 주어야 한다는 점이다.

함수의 리턴형을 const로 지정하라. 이것은 sysop구조체 자체가 const라는 것을 의미하지는 않는다. 이것은 단지 그 구조체를 변경하는데 리턴값을 직접 사용할 수 없다는 것을 의미한다. 예를 들어, const를 생략하면 다음과 같은 코드를 사용할 수 있다.

use(looper).used = 10;

sysop newgal = { ... }; 
use(looper) = newgal;              // looper = newgal;

요약하면, const를 생략하면 짧지만 더 모호해 보이는 코드를 작성할 수 있다.

일반적으로 모호한 코드는 모호한 에러를 일으킬 기회를 높이기 때문에 모호한 코드를 추가하지 않도록 하는 것이 좋다. 리턴형을 const참조로 만드는 것은 프로그래머를 혼미의 유혹으로부터 보호한다. 때로는 const를 생략하는 것이 옳을 때도 있다. <<오버로딩 연산자가 그 예다.

클래스 객체와 참조

일반적으로 C++는, 클래스 객체를 함수에 전달할 때 참조를 사용한다. 예를 들면, string, ostream, istream, ofstream, ifstream클래스의 객체를 전달인자로 취하는 함수들에 참조 매개변수를 사용할 수 있다.

string version1(const string &s1, const string &s2) { 
    string temp; 
    temp = s2 + s1 + s2; 
    return temp; 

const string& version2(string &s1, const string &s2) {      // 참조를 리턴한다. 
    s1 = s2 + s1 + s2; 
    return s1; 
}

const string& version3(string &s1, const string &s2) {      // 나쁜 설계 
    string temp; 
    temp = s2 + s1 + s2; 
    return temp;                                            // 사용되지 않을 메모리를 리턴 
}

const string형 매개변수에 const char *형을 전달인자로 하는 것을 프로그램은 허용한다. 이것에는 두가지 이유가 숨어있다. 첫째, string클래스가 char *형을 string으로 변환하는 것을 정의하고 있다. 이것이 string객체를 C스타일 문자열로 초기화하는 것을 가능하게 한다. 둘째, 앞에서 설명한 const 참조형식 매개변수의 특성때문이다. 살전달인자의 데이터형이 참조 매개변수의 데이터형과 일치하지 않지만, 참조 데이터형으로 변환될 수 있다고 가정하자. 그러면 프로그램은 올바른 데이터형의 임시변수를 만들고, 변환된 값으로 그것을 초기화한고, 임시변수에 참조를 전달한다.

결과적으로 형식 매개변수가 const string &형이면, string객체 또는 큰따옴표로 묶은 문자열 리터럴, 널 문자로 종결된 char형의 배열, char형을 지시하는 포인터변수와 같은 C스타일 문자열이 함수 호출에 사용되는 실전달인자가 될 수 있다. 그러므로 다음과 같은 행은 바르게 동작한다.

result = version1(input, "***");                            // input은 string형 객체

객체, 상속, 참조

ostream과 ofstream클래스는 참조의 흥미있는 특성을 전면에 부각시킨다. ofstream형 객체들은 ostream메서드를 사용할 수 있다. 그들은 콘솔입력 / 출력과 동일한 형식을 파일입력 / 출력에 사용하는 것을 허용한다. 한 클래스에서 다른 클래스로 기능을 전달하는 것을 가능하게 하는 C++언어의 기능을 상속(inheritance)이라 부른다. 간단히 말해서 ostream을 기초클래스(base class)라 하고 (ofstream클래스가 그것에 기초하고 있기 때문에), ofstream을 파생클래스(derived class)라 한다(ostream으로부터 파생되었기 때문에). 파생클래스는 기초클래스의 메서드들을 상속한다. 이것은 ofstream객체가 precision(), setf()등의 포맷팅 메서드와 같은 기초클래스의 기능들을 사용할 수 있다는 것을 의미한다.

상속의 또 한가지 측면은, 데이터형 변환없이 기초클래스 참조가 파생클래스 객체를 참조할 수 있다는 것이다. 이것의 실제적인 결과는, 기초클래스 참조 매개변수를 가지는 함수를 정의하고, 그 함수를 기초클래스에도 사용하고 파생객체에도 사용할 수 있다는 것이다. 예를 들면, ostream &형 매개변수를 가지는 함수는, cout과 같은 ostream객체와 사용자가 선언하는 ofstream객체를 동등하게 받아들일 수 있다.

ofstream fout; 
const char *fn = "ep-data.txt"; 
fout.open(fn);

file_it(fout, objective, eps, LIMIT); 
file_it(cout, objective, eps, LIMIT);

void file_it(ostream &os, double fo, const double fe[], int n) { 
    os << ... 
}

동일한 내용이 파일에, 그리고 모니터에 디스플레이된다. 이 예제의 중요한 점은 ostream &형인 os매개변수가 cout과 같은 ostream객체와 fout과 같은 ofstream객체를 참조할 수 있다는 것이다.

참조 전달인자를 사용하는 주된 이유는 다음 두가지이다.

  • 호출함수에 있는 데이터 객체의 변경을 허용하기 위해
  • 전체 데이터 객체 대신에 참조를 전달하여 프로그램의 속도를 높이기 위해

두번째 이유는 구조체나 클래스 객체와 같이 덩치 큰 데이터 객체를 다룰 때 가장 중요하다. 위의 두가지 이유는 포인터를 전달인자로 사용하는 것과 같은 이유이다. 사실상 참조 전달인자는 포인터를 사용하도록 짜여진 코드의 또 다른 인터페이스이기 때문에, 이것은 이치에 맞는다. 참조와 포인터는 어떤 때 구별하여 사용할 수 있을까? 다음은 이를 위한 몇가지 지침이다.

함수가 전달된 데이터를 변경하지 않고 사용만 하는 경우,

  • 데이터 객체가 기본 데이터형이나 작은 구조체라면 값으로 전달한다.
  • 데이터 객체가 배열이라면 포인터가 유일한 선택이므로 포인터를 사용한다. 포인터를 const를 지시하는 포인터로 만든다.
  • 데이터 객체가 덩치 큰 구조체라면 const포인터나 const참조를 사용하여 프로그램의 속도를 높인다. 이것은 구조체나 클래스 설계를 복사하는데 드는 시간과 공간을 절약한다. 포인터나 참조를 const로 만든다.
  • 데이터 객체가 클래스 객체라면 const참조를 사용한다. 클래스 설계 자체가 흔히 참조를 사용할 것을 요구한다. 이것이 C++에 const기능을 추가한 주된 이유이기도 하다. 클래스 객체 전달인자의 전달은 참조로 전달하는 것이 표준이다.

함수가 호출함수의 데이터를 변경하는 경우

  • 데이터 객체가 기본 데이터형이면 포인터를 사용한다.
  • 데이터 객체가 배열이면 유일한 선택은 포인터를 사용하는 것이다.
  • 데이터 객체가 구조체이면 참조 또는 포인터를 사용한다.
  • 데이터 객체가 클래스 객체이면 참조를 사용한다.

이것은 단지 지침일 뿐이다. 다른 방식으로 선택한다면 그 나름대로의 이유가 있을 것이다. 예를 들어, cin은 cin >> &n 대신에 cin >> n을 사용할 수 있도록 기본형에 대한 참조를 사용한다.

, .

이글은 퍼온 글입니다.

원본은 아래 주소에 있습니다.

http://yatoyato.tistory.com/1534


배열을 처리하는 C++함수는 배열에 들어있는 데이터의 종류, 배열의 시작위치, 배열에 들어있는 데이터의 종류, 배열의 시작위치, 배열에 들어있는 원소 개수에 관한 정보를 넘겨받아야 한다. 전통적으로 C와 C++가 배열을 처리하는 함수에 접근하는 방법은, 첫번째 전달인자로 배열의 시작위치를 지시하는 포인터를 전달하고, 두번째 전달인자로 배열의 크기를 전달하는 것이다. (그 포인터는 배열이 어디에 있는지와 배열에 들어있는 데이터의 종류가 무엇인지를 말해준다.) 그렇게 하면 배열의 모든 데이터를 인식하는데 필요한 정보가 함수에 전달된다.

필요한 정보를 함수에 전달하는 또 하나의 방법이 있다. 원소들의 범위를 지정하는 것이다. 이것은 두 개의 포인터, 즉 배열의 시작을 지시하는 포인터와 배열의 끝을 지시하는 포인터를 전달하여 이루어진다. 예를 들면 C++ 표준 템플릿 라이브러리(C++ Standard Template Library)에서는 이러한 범위접근방법을 일반화한다. STL접근방법은 원소들의 범위를 지정하기 위해 "끝 바로 다음"이라는 개념을 사용한다. 즉, 어떤 배열이 있다고 했을 때, 배열의 끝을 인식하기 위한 전달인자는 배열의 마지막 원소 바로 다음을 지시하는 포인터가 될 것이다. 예를 들어, 다음과 같은 선언이 있다고 가정해보자.

double elbuod[20];

그러면 두 개의 포인터 elbuod와 elbuod+20이 배열의 범위를 지정한다. 먼저 배열의 이름인 elbuod는 첫번째 원소를 지시한다. elbuod+19는 마지막 원소(즉, elbuod[19])를 지시한다. 그러므로 elbuod+20은 배열의 마지막 원소 바로 다음을 지시한다. 범위를 전달하는 것은 어떤 원소들을 처리해야 하는지를 함수에게 알려준다.

아래의 예제는 범위를 지정하는 두 개의 포인터를 사용한다.

int sum = sum_arr(cookies, cookies+ARSIZE);

int sum_arr(const int *begin, const int *end) { 
    const int *pt; 
    int total = 0;

    for (pt=begin; pt!=end; pt++) 
        total = total + *pt; 
    return total; 
}

포인터 값 cookies+ARSIZE는 마지막 원소 바로 다음 위치를 지시한다. 배열이 ARSIZE개의 원소를 가지고 있으므로 cookies[ARSIZE-1]은 마지막 원소이다. 그리고 마지막 원소의 주소는 cookies+ARSIZE-1이다. 따라서 cookies, cookies+ARSIZE는 배열 전체를 지정하는 범위가 된다. 포인터 뺄셈의 규칙에 따르면 sum_arr()와 같은 함수에서 표현식 end-begin은 그 범위에 들어있는 원소의 개수와 동일한 정수값이 된다.

포인터와 const

const라는 키워드는 포인터에 두가지 방법으로 사용된다. 첫번째 방법은 상수 객체를 지시하는 포인터를 만드는 것이다. 상수객체를 지시하는 포인터를 사용하여 그 포인터가 지시하는 값을 변경할 수 없다. 두번째 방법은 포인터 자신을 상수로 만드는 것이다. 상수포인터를 사용하여 그 포인터가 지시하는 장소를 변경할 수 없다.

상수를 지시하는 pt라는 포인터를 선언한다.

const int *pt = &age;

pt에 대한 이 선언은 그것이 지시하는 값이 실제로 상수라는 것을 의미하지는 않는다. 단지 pt가 관계하는 한에서만 그 값이 상수라는 것을 의미한다. 예를 들어 pt는 age를 지시하고 있지만, age는 const가 아니다. 따라서 age변수를 직접 사용하여 age의 값을 변경할 수 있다. 그러나 포인터 pt를 사용해서는 age의 값을 변경할 수 없다.

const변수의 주소를 const를 지시하는 포인터에 대입하는 것은 가능하나, const변수의 주소를 일반포인터에 대입하는 것을 허용되지 않는다. 후자의 것은 변수의 const를 무의미하게 만든다. 그렇기 때문에 C++는 const변수의 주소를 const가 아닌 일반포인터에 대입하는 것을 금지한다. 그래도 필사적으로 꼭 그렇게 하기를 원한다면, const_cast연산자를 이용한 데이터형 변환을 사용하여 이 제한을 무시할 수 있다.

포인터를 지시하는 포인터를 사용할 때 이러한 사정을 더욱 복잡해진다. 앞에서도 살펴보았듯이, const가 아닌 포인터를 const포인터에 대입하는 것은 한다리 건너는 간접지시인 경우에만 사용할 수 있다.

int age = 39;                        // age++은 사용할 수 있다. 
int *pd = &age;                      // *pd = 41은 사용할 수 있다. 
const int *pt = pd;                  // *pt = 42는 사용할 수 없다.

cosnt와 const가 아닌 것을 이런 식으로 섞어서 사용하는 포인터대입은 두다리 건너는 간접지시인 경우에는 더이상 안전하지 않다. const와 const가 아닌 것을 섞어서 사용하는 것이 허용된다면 다음과 같은 것도 가능하게 되기 때문에 안된다.

const int **pp2; 
int *p1; 
const int n = 13; 
pp2 = &p1;                           // 허용되지 않지만 허용된다고 가정하면 
*pp2 = &n;                           // 둘다 const인데 p1이 n을 지시하게 만든다. 
*p1 = 10;                            // const n을 변경하게 만든다.

앞의 코드는 const가 아닌 주소(&p1)를 const포인터(pp2)에 대입한다. 그것은 p1을 사용하여 const데이터를 변경하도록 허용한다. 그러므로 const가 아닌 주소나 포인터를 const포인터에 대입할 수 있다는 규칙은 한다리 건너는 간접지시인 경우에만 유효하다. 예를 들면, 그 포인터가 기본 데이터형을 지시하는 경우에만 유효하다. 데이터형 자체가 포인터가 아니라면, const데이터의 주소이든 const가 아닌 데이터의 주소이든 const를 지시하는 포인터에 모두 대입할 수 있다. 그러나 const가 아닌 데이터의 주소는 const가 아닌 포인터에만 대입할 수 있다.

const데이터들을 원소로 가지는 다음과 같은 배열이 있다고 가정할 때, 상수배열의 주소를 대입할 수 없도록 금지하는 것은, 상수가 아닌 형식 매개변수를 사용하여 배열의 이름을 전달인자로 함수에 전달할 수 없다는 것을 의미한다.

const int months[12] = { ... }; 
int sum(int arr[], int n);           // const int arr[]이어야 한다. 
int j = sum(months, 12);             // 허용되지 않는다.

이 함수호출은 const포인터인 months를 const가 아닌 포인터 arr에 대입하려고 시도한다. 그러나 컴파일러는 이러한 함수호출을 허용하지 않는다. 이 선언에서 const를 사용한 것은 함수가 자신에게 전달되는 배열이 어떤 것이든 간에 그 배열에 들어있는 값을 변경할 수 없다는 것을 뜻한다. 이것은 한다리 건너는 간접지시의 경우에만 동작한다. 예를 들어, 그 배열의 원소들은 기본형이어야 한다. 그 배열의 원소들이 포인터이거나 포인터를 지시하는 포인터인 경우에는 const를 사용할 수 없다.

가능하면 const를 사용해야 한다. 포인터 전달인자를 상수데이터를 지시하는 포인터로 선언하는 이유는 두가지이다. 이것은 실수로 데이터를 변경시키는 프로그래밍 에러를 막을 수 있다. const를 사용하는 함수는 const와 const가 아닌 전달인자를 모두 처리할 수 있지만, 함수원형에서 const를 생략한 함수는 const가 아닌 데이터만 처리할 수 있다. 따라서 가능하다면 형식포인터 전달인자를 const를 지시하는 포인터로 선언해야 한다.

const를 사용하는 또 하나의 방법은 포인터 자신의 값을 변경하지 못하게 막는 것이다.

int* const finger = &sloth;          // int를 지시하는 const포인터

이러한 형태의 선언은 finger가 sloth만을 지시하도록 제한한다. 그러나 finger를 이용하여 sloth의 값을 변경할 수는 있다. 그러나 포인터가 가리키는 주소를 변경할 수는 없다.

const double* const stick = &trouble;

stick은 trouble만을 가리킬 수 있고 stick을 사용하여 trouble의 값을 변경할 수 없다.

함수와 C스타일 문자열

문자열을 리턴하는 함수를 작성하고 싶다고 가정하자. 함수로는 문자열 자체를 리턴할 수 없다. 그 대신에 문자열의 주소를 리턴할 수 있다. 그것이 더 효율적이다.

char *ps = buildstr(ch, times); 
delete []ps;

char* buildstr(char c, int n) { 
    char *pstr = new char[n+1]; 
    pstr[n] = '\0'; 
    while (n-- > 0)                  // 뒤에서부터 문자를 채워, 포인터가 문자열의 시작위치를 가리킨다. 
        pstr[n] = c; 
    return pstr; 
}

변수 pstr은 buildstr()함수의 지역변수이다. 따라서 함수가 종결되면 pstr에 할당되었던 기억장소는 자동으로 해제된다. 그러나 이 함수는 종결하기 전에 pstr의 값을 리턴하기 때문에 main()에 있는 ps포인터를 통하여 그 문자열에 접근할 수 있다.

이 예제는 문자열이 더이상 필요없게 되면 delete를 사용하여 그 문자열에 할당되었던 기억장소를 해제한다. 이 방식의 단점은 프로그래머가 delete사용을 항상 기억해야 한다는 것이다.

, .


아래 글은 퍼온 글입니다.

원본 주소는 아래와 같습니다.

http://yatoyato.tistory.com/878



Constructor / Destructor

생성자의 필요성

개인의 기본정보(이름, 전화번호, 나이)를 담고있는 person이라는 이름의 클래스를 정의해 본다. 단 person클래스의 기본기능은 데이터를 저장하고 출력하는 것으로 제한한다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

const int SIZE=20;

class person { 
    char name[SIZE]; 
    char phone[SIZE]; 
    int age;

public: 
    void showData(); 
};

void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
}

int main() { 
    person p={"KIM", "013-113-1113", 22}; 
    p.showData();

    return 0; 
}

메인함수 내에서 p라는 이름의 객체를 "생성과 동시에 초기화"하려고 한다. 구조체변수 초기화는 가능했지만 여기에서는 컴파일러가 에러를 발생시킨다. 문제는 초기화하고자 하는 멤버들이 private로 선언되어 있다는 것이다. 즉 클래스의 내부가 아니라 외부에 해당이 되므로 접근이 허용되지 않는다.

문제를 해결하는 가장 쉬운 방법은 멤버들을 public으로 선언하는 것이다. 그러나 이는 정보은닉에 위배되므로 다음과 같은 방법을 택하기로 결정내렸다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

const int SIZE=20;

class person { 
    char name[SIZE]; 
    char phone[SIZE]; 
    int age;

public: 
    void showData(); 
    void setData(char *_name, char *_phone, int _age); 
};

void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
}

void person::setData(char *_name, char *_phone, int _age) { 
    strcpy(name, _name); 
    strcpy(phone, _phone); 
    age=_age; 
}

int main(void) { 
    person p; 
    p.setData("KIM", "013-333-5555", 22); 
    p.showData();

    return 0; 
}

멤버변수를 초기화하기 위한 멤버함수가 선언되어 있다. 함수의 전달된 인자를 통해서 멤버변수에 단순히 복사만 하고 있지만, 필요한 경우에는 데이터의 유효성 검사에 관련된 코드도 삽입이 가능하다. 메인함수 내에서는 p라는 이름의 객체를 생성만 하고 있다. 따라서 멤버변수들은 쓰레기 값으로 초기화될 것이다. 그리고 그 다음 줄에서는 원하는 값으로 멤버들을 초기화하고 있다.

이는 궁극적으로 우리가 하고자 했던 "생성과 동시에 초기화"라는 것과는 다소 거리가 멀어진 것이다. 위와 같은 클래스의 정의는 프로그래머에게 제약을 가하는 형태가 된다. 프로그래머는 다음과 같은 사항을 반드시 기억해야 한다. 그러나 이는 바람직한 형태가 아니다.

"person클래스는 객체생성후 초기화를 해야 하는데, 이때 사용되는 함수가 setData이다."

뿐만 아니라, 대부분의 객체는 생성과 동시에 특정한 값을 지니는 것이 좋은 형태이다. 왜냐하면, 생성과 동시에 초기화한다는 개념은 객체지향에서 또다른 의미를 지니기 때문이다. 그렇다면 객체를 생성과 동시에 초기화할 수 있는 메커니즘이 제공되어야 한다고 결론내릴 수 있다. 그래서 제공되는 메커니즘이 바로 생성자(Constructor)이다.

다음과 같은 형태의 초기화는 허용되지 않는다.(JAVA와 C#과 같은 언어의 클래스 선언에서는 허용이 된다. 그래서 많이 헷갈려 한다.)

class AAA { 
    int a=10; 
    int b=20; 
};

클래스를 선언하는데 있어서 멤버를 초기화하는 것은 문법적 오류이다. 이는 구조체를 선언하면서 멤버를 초기화하지 못하는 것과 같은 이치이다.

생성자와 객체의 생성과정

c++에서의 객체 생성과정은 두단계로 나눠서 이야기할 수 있다. 그 두 단계는 다음과 같으며, 모든 객체는 반드시 이 과정을 거치게 되어 있다.

  1. 메모리 할당
  2. 생성자 호출

생성자에 대한 외형적 특징은 다음과 같다.

  • 함수이다.
  • 클래스의 이름과 같은 이름을 지닌다.
  • 리턴하지도 않고, 리턴타입도 선언되지 않는다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

const int SIZE=20;

class AAA { 
    int i, j;

public: 
    AAA() { 
        cout<<"생성자 호출"<<endl; 
        i=10, j=20; 
    }

    void showData() { 
        cout<<i<<' '<<j<<endl; 
    } 
};

int main(void) { 
    AAA aaa; 
    aaa.showData(); 
    return 0; 
}

클래스 안에 정의되어 있는 한 함수는 독특하게도 리턴타입이 선언되어 있지도 않고, 리턴하지도 않는다. 뿐만 아니라 클래스의 이름과 함수이름이 같다. 따라서 이 함수는 생성자이다. 앞에서 이야기한 생성자의 외형적 특징과 일치한다.

실행결과를 통해서 알 수 있는 사실은 생성자가 호출되었다는 사실이다. 이는 두가지 측면에서 확인이 가능한데, 첫번째로 "생성자 호출"이라는 메시지가 출력되었다는 것이고, 두번째는 멤버변수 i와 j값이 각각 10과 20으로 출력되었다는 것이다. 생성자 내를 보면 i와 j값을 각각 10과 20으로 초기화하고 있다는 것을 알 수 있다.

메인함수 내에서는 클래스 AAA의 객체 aaa를 생성하고 있다. 따라서 제일 먼저 메모리 공간이 할당되고 aaa라는 이름이 붙게 될 것이다. 물론 메모리공간만 할당이 되고 초기화는 이뤄지지 않는 상태이므로 멤버변수는 '쓰레기값'을 지니게 된다. 두번째 단계는 생성자의 호출이다. 따라서 생성자가 호출되면서 i와 j는 각각 10과 20으로 초기화된다.

다음 예제에서 위의 예제와 다른 부분은 정의되어 있는 생성자의 형태이다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

const int SIZE=20;

class AAA { 
    int i, j;

public: 
    AAA(int _i, int _j) { 
        i=_i, j=_j; 
    } 
    void showData() { 
        cout<<i<<' '<<j<<endl; 
    } 
};

int main(void) { 
    AAA aaa(111, 222); 
    aaa.showData(); 
    return 0; 
}

위 예제의 생성자는 함수의 리턴타입이 없고, 클래스와 이름이 같으므로 분명히 생성자다. 다만 매개변수 선언이 있을 뿐이다. 즉 "AAA클래스의 객체를 생성하되 이름은 aaa라 하고, 생성자 호출시 111과 222를 인자로 전달받을 수 있는 생성자를 호출하라"라는 의미이다. 따라서 aaa객체의 멤버변수 i와 j는 각각 111과 222로 초기화될 것이다.

위의 예제는 상당히 의미있는 예제이다. 왜냐하면 객체를 생성과 동시에 초기화하되, 원하는 값으로 초기화했기 때문이다. 이렇듯 생성자는 객체생성시 원하는 값으로 초기화하기 위한 용도로 사용이 된다. 물론 다른 용도로 사용할 수도 있지만, 그렇게 되면 적절한 형태의 생성자와는 거리가 멀어질 수도 있다. 가급적이면 생성자는 멤버변수를 초기화하는 용도로만 사용해야 한다.

객체생성문법은 크게 세 개의 영역으로 나뉘어진다. 제일 먼저 클래스의 이름이 나오고, 그 다음으로 객체의 이름이 등장한다. 그리고 마지막으로 객체생성시 호출하고자 하는 생성자에 대한 정보가 등장한다. 그러나 호출하고자 하는 생성자에 대한 정보가 없다면, void생성자, 즉 인자값을 받지않는 생성자를 호출하라는 의미로 받아 들여지게 된다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

const int SIZE=20;

class person { 
    char name[SIZE]; 
    char phone[SIZE]; 
    int age;

public: 
    person(char *_name, char *_phone, int _age); 
    void showData(); 
};

person::person(char *_name, char *_phone, int _age) { 
    strcpy(name, _name); 
    strcpy(phone, _phone); 
    age=_age; 
}

void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
}

int main(void) { 
    person p("KIM", "013-333-5555", 22); 
    p.showData();

    return 0; 
}

객체생성과정에서 첫번째, 두번째 인자로 문자열을, 세번째 인자로 int형 정수를 받을 수 있는 생성자의 호출을 요구하고 있다. 이러한 형태의 생성자가 존재한다면 무리없이 객체가 생성되겠지만, 존재하지 않는다면 컴파일오류가 발생하게 된다.

클래스 내에서는 생성자가 선언되어 있다. 그런데 이 생성자는 첫번째, 두번째 인자로 문자열을, 세번째 인자로 int형 정수를 받겠다고 선언되어 있다. 따라서 메인함수 내에서 선언하고 있는 객체 p가 생성되는 과정에서 요구하는 형태의 생성자가 된다.

사실 C++에서는 두가지 형태의 객체생성방법을 제시하고 있다. 첫번째 방법은 이미 앞에서 언급한 방법이다.

person p("KIM", "013-333-5555", 22);

두번째 방법은 명시적으로 생성자를 호출하는 방법이다.

person p = person("KIM", "013-333-5555", 22);

위의 두 문장은 같은 의미를 지닌다. 그러나 상대적으로 간결한 첫번째 방법이 주로 사용된다. 그래서 첫번째 방법을 대표적으로 소개한다.

public생성자, private생성자

지금까지 보아온 생성자들은 모두 public으로 선언되어 있다. 그렇지 않으면 클래스 외부에서 객체생성을 할 수 없기 때문이다.

class AAA { 
private: 
    AAA(){} 
};

int main() { 
    AAA aaa; 
    return 0; 
};

aaa라는 이름의 객체를 생성하되, 인자값을 받지않는 void생성자의 호출을 요구하고 있다. 그런데 void생성자의 호출요구가 들어간 곳은 main함수 영역이다.(main함수 내에 객체생성 문장이 존재하므로) 이는 AAA클래스의 외부에 해당하므로 만약에 생성자가 public으로 선언되어 있지 않다면 문제가 된다. 왜냐하면 접근이 불가능하기 때문이다.

결국 위의 예제는 컴파일 오류를 발생시킨다. 호출할 만한 적절한 형태의 생성자를 AAA클래스가 지니고 있지 않기 때문이다. 클래스 내의 생성자는 private로 선언되어 있다. 따라서 생성자는 일반적으로 public멤버로 선언되기 마련이다.

디폴트 생성자와 생성자의 특징

생성자가 지니는 특징은 다음과 같다.

  • 생성자를 하나도 정의하지 않으면 디폴트(default) 생성자가 자동삽입된다.
  • 생성자도 함수이므로 오버로딩이 가능하다.
  • 생성자도 함수이므로 디폴트 매개변수의 설정이 가능하다.

객체의 생성순서는 첫번째가 메모리 할당이고, 두번째가 생성자 호출이다. 실제로 C++의 모든 객체들은 생성과정에서 반드시 한번 생성자가 호출되게끔 되어있다. 따라서 첫번째 순서에 의해서 메모리만 할당된 경우 이는 객체라고 부를 수 없다. 반드시 두번째 순서에 의해서 생성자가 호출되어야만 객체라고 부를 수 있는 것이다.

생성자가 명시되지 않은 클래스의 경우에도 생성자의 호출은 있다. 다만 우리가 정의한 생성자가 아니라, 컴파일러에 의해서 자동으로 삽입된 생성자가 호출되었던 것이다. 이러한 생성자를 가리켜 디폴트(Default) 생성자라 한다.

class point { 
    int x, y; 
public: 
    point() {} 
}

위 클래스에는 생성자가 정의되어 있다. 이 생성자는 인자값을 받지 않는 void생성자인 동시에 아무런 기능도 지니지 않는다. 이것이 바로 디폴트 생성자의 형태이다. 즉 클래스를 정의하는데 있어서 프로그래머가 생성자를 하나도 정의하지 않으면, 인자값도 받지 않고, 아무런 기능도 지니지 않는 디폴트 생성자가 자동으로 삽입된다. 따라서 다음의 두 클래스의 정의는 완전히 같은 것이다.

class point { 
    int x, y;

public: 
    void print() {...} 
}

class point { 
    int x, y;

public: 
    point() {} 
    void print() {...} 
}

다음 예제는 컴파일하는 과정에서 에러메시지를 출력한다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class point { 
    int x, y;

public: 
    point(int _x, int _y) { 
        x=_x; 
        y=_y; 
    } 
    void showData() { 
        cout<<x<<' '<<y<<endl; 
    } 
};

int main(void) { 
    point p1(10, 20); 
    p1.showData();

    point p2; 
    p2.showData();

    return 0; 
}

main함수 내를 보면 10과 20을 인자로 전달받을 수 있는 생성자를 호출하면서 객체를 생성하려고 한다. point클래스의 정의를 보면 두 개의 int형 정수를 인자로 받을 수 있는 생성자가 정의되어 있다. 따라서 문제없이 객체가 생성된다.

그러나 다음의 객체생성은 문제가 된다. 명시적으로 어떠한 생성자를 호출하겠다는 표현이 없으므로, 인자값을 받지 않는 void생성자를 호출하려고 할 텐데, 문제는 void생성자가 존재하지 않는다는 것이다. 앞에서도 이야기했듯이 프로그래머가 정의해놓은 생성자가 하나라도 존재하면 default생성자가 자동으로 삽입되지 않기 때문이다. 따라서 컴파일시 에러메시지를 출력하게 된다.

위 예제의 문제점은 void생성자를 하나 더 넣어주면 간단히 해결된다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class point { 
    int x, y;

public: 
    point() { 
        x=y=0; 
    } 
    point(int _x, int _y) { 
        x=_x, y=_y; 
    }

    void showData() { 
        cout<<x<<' '<<y<<endl; 
    } 
};

int main(void) { 
    point p1(10, 20); 
    p1.showData();

    point p2; 
    p2.showData();

    return 0; 
}

위 예제는 void생성자가 존재하고, int형 정수 두개를 인자로 받는 생성자가 존재한다. 이렇듯 생성자도 오버로딩이 가능하다. 생성자도 함수이기 때문에 함수의 특징을 그대로 지니고 있다. point클래스는 두 개의 생성자를 지니고 있으므로, 두 가지 형태로 객체생성이 가능하다. 각각의 객체는 매개변수에 따라 각각 다른 생성자를 호출한다.

위의 예제말고도 또다른 해법이 존재한다. 생성자도 함수이므로 오버로딩뿐만 아니라, 디폴트 매개변수를 설정할 수 있기 때문이다. 즉 위의 예제는 다음과 같은 형태로 달리 표현될 수 있다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class point { 
    int x, y;

public: 
    point(int _x=0, int _y=0) { 
        x=_x, y=_y; 
    }

    void showData() { 
        cout<<x<<' '<<y<<endl; 
    } 
};

int main(void) { 
    point p1(10, 20); 
    p1.showData();

    point p2; 
    p2.showData();

    point p3(10); 
    p3.showData();

    return 0; 
}

생성자에 디폴트 매개변수가 설정되어 있음을 알 수 있다. 따라서 int형 정수 두개를 인자로 전달하는 방법이외에 다음과 같은 형태의 객체생성이 가능하다.

생성자와 동적할당

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class person { 
    char *name; 
    char *phone; 
    int age;

public: 
    person(char *_name, char *_phone, int _age); 
    void showData(); 
};

person::person(char *_name, char *_phone, int _age) { 
    name=new char[strlen(_name)+1]; 
    strcpy(name, _name);

    phone=new char[strlen(_phone)+1]; 
    strcpy(phone, _phone); 
    age=_age; 
void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
}

int main(void) { 
    person p("KIM", "013-333-5555", 22); 
    p.showData();

    return 0; 
}

main함수 내에서 p라는 이름의 객체를 생성하고 있다. 따라서 객체생성 순서에 의해서 제일 먼저 메모리 공간이 할당되고, p라는 이름을 부여하게 된다. 그 다음으로 생성자를 호출하면서 문자열들과 정수가 인자로 전달된다. 생성자 내에서는 전달된 문자열의 길이를 계산해서 정확한 크기만큼 메모리 공간을 할당하고 문자열을 복사하고 있다.

.. strlen()은 문자열의 길이를 계산하여 값을 반환하는데, '\0'은 포함하지 않는다.

결과적으로 객체 p는 main함수 내에서 생성되었으므로, 스택stack영역에 할당이 되겠지만, 생성자 내에서 메모리공간을 동적할당하고 있기 때문에, 멤버변수 name과 phone이 가리키는 메모리공간은 힙heap이 된다. 
이러한 형태의 초기화가 주는 이점은 메모리 공간을 효율적으로 사용할 수 있다는 것이다.

이러한 동적할당은 메모리공간을 효율적으로 활용한다는 이점은 있지만, 이렇게 클래스를 디자인할 경우 생각해 봐야 할 문제가 많아지기 때문이다. 문자열의 길이가 크게 유동적이지 않다면 동적할당을 사용하지 않는 클래스가 더 좋은 디자인에 속한다. 그럼에도 불구하고 생성자 내에서 메모리 공간을 동적할당해야 할 상황은 앞으로 많이 등장하게 된다.

위 예제는 한가지 중요한 문제를 지니고 있다. 그것은 생성자 내에서 동적할당한 메모리 공간을 어디서도 해제해주지 않고 있다는 것이다. 이러한 경우 메모리 누수(유출)현상이 발생하게 된다. 다음 예제는 문제를 해결한 것이다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class person { 
    char *name; 
    char *phone; 
    int age;

public: 
    person(char *_name, char *_phone, int _age); 
    void showData(); 
    void delMemory(); 
};

person::person(char *_name, char *_phone, int _age) { 
    name=new char[strlen(_name)+1]; 
    strcpy(name, _name);

    phone=new char[strlen(_phone)+1]; 
    strcpy(phone, _phone); 
    age=_age; 
void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
void person::delMemory() { 
    delete []name; 
    delete []phone; 
}

int main(void) { 
    person p("KIM", "013-333-5555", 22); 
    p.showData(); 
    p.delMemory();

    return 0; 
}

이전 예제와 차이를 보이는 부분은 생성자에서 동적할당한 메모리공간을 해제하는 함수를 멤버로 포함시킨 것이다. 언뜻 보면 문제가 해결된 듯하다. 그러나 이러한 클래스는 프로그래머에게 제약을 가하는 형태가 된다. 프로그래머는 다음과 같은 사항을 반드시 기억해야 한다.

"person클래스의 객체는 소멸되기 전에 반드시 delMemeory함수를 호출해야 한다."

이는 바람직한 형태가 아니다. 이러한 형태의 클래스가 대략 5개 정도가 된다고 할 때 신경쓸 일이 한두가지가 아니다. 그래서 등장한 메커니즘이 바로 소멸자(Destructor)이다.

소멸자의 특징과 필요성

경우에 따라서 객체는 소멸되기 전에 다양한 형태의 정리작업이 필요하다.(동적할당한 메모리 공간의 해제) 이러한 일들이 객체소멸시 자동적으로 처리된다면 프로그래머는 그만큼 부담을 덜 수 있다. 이러한 목적으로 사용되는 것이 소멸자이다.

객체 소멸과정도 객체의 생성과정과 마찬가지로 두단계를 거친다. 그 두 단계는 다음과 같으며, 모든 객체는 반드시 이 과정을 거쳐서 소멸된다.

  1. 소멸자 호출
  2. 메모리 반환(해제)

소멸자에 대한 외형적 특징은 다음과 같다.

  • 함수이다.
  • 클래스의 이름 앞에 ~가 붙은 형태의 이름을 지닌다.
  • 리턴하지도 않고, 리턴타입도 선언되지 않는다.
  • 매개변수를 받을 수 없다. 따라서 오버로딩도 불가능하고, 디폴티 매개변수 선언도 불가능하다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class AAA { 
public: 
    AAA() { 
        cout<<"생성자 호출"<<endl; 
    } 
    ~AAA() { 
        cout<<"소멸자 호출"<<endl; 
    } 
};

int main(void) { 
    AAA aaa1; 
    AAA aaa2;

    return 0; 
}

클래스 이름앞에 ~가 붙어서 구성이 되어 있는 함수가 존재한다. 물론 리턴타입도 없으며, 인자도 받지않는 void함수이다. 따라서 이 함수는 소멸자의 조건에 충족되고, 그러므로 객체소멸시 자동적으로 호출된다.

위 예제의 실행결과를 통해서 객체가 소멸되기 이전에 소멸자가 먼저 호출된다는 것을 확인할 수 있다. 메인함수 내에서 두 개의 객체가 생성될 것이다. 따라서 두 번의 생성자 호출에 의해 "생성자 호출"이라는 메시지가 두번 출력된다. 그러고 나서 main함수가 종료되면서 지역적으로 선언된 객체 aaa1과 aaa2가 소멸되므로,(사실 main함수호출이 종료되면 프로그램 자체가 종료되지만) 두번의 소멸자 호출에 의해 "소멸자 호출"이라는 메시지가 두번 출력된다.

소멸자의 가장 중요한 특징은 객체소멸시 반드시 한번 호출된다는 것이다.

#include <iostream> 
using std::cout; 
using std::cin; 
using std::endl;

class person { 
    char *name; 
    char *phone; 
    int age;

public: 
    person(char *_name, char *_phone, int _age); 
    ~person(); 
    void showData(); 
};

person::person(char *_name, char *_phone, int _age) { 
    name=new char[strlen(_name)+1]; 
    strcpy(name, _name);

    phone=new char[strlen(_phone)+1]; 
    strcpy(phone, _phone);

    age=_age; 
person::~person() { 
    delete []name; 
    delete []phone; 
void person::showData() { 
    cout<<"name: "<<name<<endl; 
    cout<<"phone: "<<phone<<endl; 
    cout<<"age: "<<age<<endl; 
}

int main(void) { 
    person p("KIM", "013-333-5555", 22); 
    p.showData();

    return 0; 
}

클래스 내에 정의되어 있는 생성자를 보면 메모리를 동적할당하고 있으며, 그 아래 정의되어 있는 소멸자를 보면 동적할당된 메모리 공간을 해제하고 있다. 따라서 객체생성시 동적할당이 이뤄지고, 객체소멸시 할당된 메모리 공간이 해제될 것이다.

일반적으로 클래스를 정의할 때에는 멤버변수의 초기화를 위해서 생성자를 항상 정의하기 마련이다. 그러나 소멸자는 다르다. 객체소멸시 처리해야 할 일이 있을 때에만 프로그래머가 정의한다. 좀더 구체적으로 말하면 다음과 같다.

"생성자 내에서 메모리를 동적할당하는 경우, 이를 해제하기 위해서 반드시 소멸자를 정의해야 한다."

디폴트 소멸자

소멸자를 정의하지 않더라도 디폴트 소멸자가 자동으로 삽입된다. 디폴트 생성자는 아무런 일도 하지 않는다. 따라서 다음의 두 코드는 서로 완전히 같은 것이다.

class point { 
    int x, y;

public: 
    void print() {...} 
}

class point { 
    int x, y;

public: 
    point() {} 
    ~point() {} 
    void print() {...} 
}

첫번째 코드를 보면 생성자와 소멸자가 정의되어 있지 않다. 따라서 디폴트 생성자와 디폴트 소멸자가 삽입될 것이다. 반면에 오른쪽에 있는 코드는 생성자와 소멸자를 정의해 주고 있는데, 그 형태가 디폴트 생성자와 디폴트 소멸자와 같다. 따라서 이 둘은 완전히 같은 클래스의 정의에 해당한다.

, .

memset 
메모리를 지정한 문자로 채웁니다. 

Declaration 
void *memset( void *dest, int c, size_t count ) 

Return value 
성공 - 채워진 메모리 주소(dest 매개 변수) 반환 
실패 - 없음 


Parameters 
dest  - 채울 메모리의 주소 
c    - 채울 문자 
count - 채우려고 하는 문자 개수 


Detail descriptions 
memset()는 버퍼를 똑같은 "문자"로 채울 때 사용합니다. 명확하게 문자라고 표현했듯이 단위는 바이트입니다. memset()는 문자를 저장하는 char 배열뿐만 아니라 모든 종류의 배열에 대해서 사용할 수 있습니다. 그러나, 단위가 바이트이기 때문에 배열 요소의 자료형에 따라 세심한 주의가 필요합니다. 

대부분의 배열은 초기 상태로 0을 갖습니다. 그래서, {} 초기화를 사용해서 모든 요소에 대해 0을 지정하도록 합니다. 그러나, {} 초기화는 배열을 선언할 때 한번만 사용할 수 있고, 이후에는 일일이 모든 요소에 대해 0을 넣어줘야 합니다. 이와 같이 모든 요소를 0으로 채울 때, 배열 종류에 상관없이 사용하는 함수가 memset()입니다. 
  

20개짜리 double 자료형을 요소로 갖는 배열은 다음과 같이 처리합니다. 


    double array[20]; 
    memset( array, 0, 20*sizeof(double) ); 


반드시 요소 개수(20)에 요소 크기(sizeof(double))를 곱해야 합니다. 단위가 바이트이므로 20을 전달하면 double 자료형 3개(24바이트)에도 미치지 못하는 영역만 0으로 채워집니다. 요소 개수와 요소 크기를 곱할 때, 비로소 배열이 사용하는 전체 크기를 바이트 단위로 얻을 수 있습니다. 


char 배열에 대해서는 원하는 문자로 채워넣는 작업이 가능합니다. 모든 요소를 'h' 문자로 채우는 코드는 다음과 같습니다. 


  char str[256]; 
    memset( str, 'h', 256 ); 

결과를 출력해 보면, str 버퍼가 모두 'h'로 채워진 것을 알 수 있습니다. 그러나, 마지막 문자까지도 'h'이기 때문에 null 문자가 없어서 문자열이 되지는 못합니다. 문자열을 구성하려면 다음과 같이 합니다. 

    memset( str, 'h', 255 ); 
    str[255] = '\0'; 


마지막 이전까지만 원하는 문자로 채우고, 마지막 요소에는 null 문자를 직접 넣습니다. 

그러나, memset()를 사용해서 int 배열의 모든 요소를 5로 채울 수는 없습니다. int 자료형은 4바이트이고 memset()는 바이트 단위로 동작하기 때문에, 다음과 같은 코드는 올바로 동작하지 않습니다. 


    int array[10]; 
    memset( array, 5, 10*sizeof(int) ); 


초기화 영역의 크기는 정확하게 40바이트를 지정했지만, int 자료형이 갖는 4바이트에 포함된 바이트마다 5가 들어가기 때문에 16진수로 표현하면 0x05050505가 됩니다. 이 값을 출력해 보면 10진수로는 84215045가 됩니다. 단위가 바이트이기 때문에 char 요소가 아닌 배열을 원하는 값으로 초기화할 수 없다는 것을 명심하기 바랍니다. 

Remarks 
앞에서 0 이외의 원하는 값으로 int 배열을 초기화할 수 없다고 했는데, 실제로는 -1로 채울 수 있습니다. 0은 모든 비트가 꺼져있는 숫자를 말하고, -1은 모든 비트가 켜져있는 숫자를 말합니다. 모든 비트가 켜져있는 1바이트 숫자인 0xff가 -1인 것처럼 모든 비트가 켜져있는 4바이트 숫자 0xffffffff도 -1입니다. 0xff를 네 번 쓰면 0xffffffff가 되고 모든 요소를 -1로 초기화시키는 것이 가능합니다. 그러나, 배열 전체를 -1로 초기화할 때가 있을지는 모르겠습니다. 

"Example codes" 항목에 배열을 -1로 채우는 코드가 있습니다. 참고하기 바랍니다. 

Header files 
<string.h>  <memory.h> 

Example codes 
memset()로 int 배열을 초기화시키는 코드를 보여줍니다. 정수에 적용할 수 있는 0 또는 -1의 두 가지 패턴을 사용합니다. 
FillArray()는 sw 매개 변수에 전달된 값을 이용해서 배열 전체를 0 또는 -1로 채웁니다. 0을 사용할 때는 OFF 상수를, -1을 사용할 때는 ON 상수를 사용합니다. 
FillArray()에 포함된 if문은 ON도 아니고 OFF도 아닌 경우에 사용합니다. 그래서는 안되겠지만, 엉뚱한 값이 들어오면 ON으로 처리한다고 가정합니다. 
출력 결과를 보면, 모든 비트가 0이거나 1이라는 것을 알 수 있습니다. 
#include <stdio.h> 
#include <string.h> 

enum { ON = -1, OFF }; 

void FillArray( int* array, int size, int sw ); 
void ShowArray( int* array, int size ); 

void main() 

    int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 

    printf( "원본 : " ); 
    ShowArray( array, 10 ); 

    ///////////////////////////////////// 

    FillArray( array, 10, OFF ); 

    printf( "OFF  : " ); 
    ShowArray( array, 10 ); 

    FillArray( array, 10, ON ); 

    printf( "ON  : " ); 
    ShowArray( array, 10 ); 


void FillArray( int* array, int size, int sw ) 

    if( sw != OFF ) 
        sw = ON; 

    memset( array, sw, size*sizeof(int) ); 


void ShowArray( int* array, int size ) 

    int i; 
    for( i = 0; i < size; i++ ) 
        printf( "%2d ", array[i] ); 

    printf( "\n" ); 


[출력 결과] 
원본 :  0  1  2  3  4  5  6  7  8  9 
OFF  :  0  0  0  0  0  0  0  0  0  0 
ON  : -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 
  

memset()를 사용해서 구조체의 모든 멤버를 0으로 초기화합니다. 
PERSON 구조체는 이름(name)과 나이(age), 전화번호(phone) 멤버를 갖습니다. 더 많은 멤버가 있으면 좋겠지만, 지면 관계상 세 개만 넣었습니다. 멤버들의 자료형으로는 배열과 int 자료형을 사용했지만, 이들 외의 어떤 자료형이 멤버로 있어도 결과는 똑같습니다. 
출력 결과에서, 두 번째로 출력한 내용이 memset() 호출 후의 결과입니다. 모든 멤버가 0이 되었다는 것은 문자열은 null 문자로, int 변수는 0으로 채워졌다는 것을 뜻합니다. 문자열은 null 문자밖에 없으므로 아무 것도 출력되지 않았습니다. 
memset()를 배열이 아닌 구조체 변수에 대해 호출합니다. 구조체뿐만 아니라 int나 double 자료형에도 이처럼 사용할 수 있지만, 오히려 번거롭기 때문에 호출하지 않을 뿐입니다. 다만 구조체는 모든 멤버에 대해 일일이 0을 넣어줘야 하기 때문에 memset()를 사용하는 것이 훨씬 좋습니다. 
#include <stdio.h> 
#include <string.h> 

struct PERSON 

    char name[32]; 
    int  age; 
    char phone[16]; 
}; 

void InputPerson( struct PERSON* pPerson ); 
void OutputPerson( const struct PERSON* pPerson ); 

void main() 

    struct PERSON person; 

    InputPerson( &person ); 
    OutputPerson( &person ); 

    memset( &person, 0, sizeof(person) ); 
    OutputPerson( &person ); 


void InputPerson( struct PERSON* pPerson ) 

    printf( "[입력]\n" ); 
    printf( "이름 : " );    scanf( "%s",  pPerson->name  ); 
    printf( "나이 : " );    scanf( "%d", &pPerson->age  ); 
    printf( "전화 : " );    scanf( "%s",  pPerson->phone ); 


void OutputPerson( const struct PERSON* pPerson ) 

    printf( "\n[출력]\n" ); 
    printf( "이름 : %s\n", pPerson->name  ); 
    printf( "나이 : %d\n", pPerson->age  ); 
    printf( "전화 : %s\n", pPerson->phone ); 


[출력 결과] 
[입력] 
이름 : 김정훈 
나이 : 25 
전화 : 010-2730-8282 

[출력] 
이름 : 김정훈 
나이 : 25 
전화 : 010-2730-8282 

[출력] 
이름 : 
나이 : 0 
전화 :

, .