The C++ Programming Language - 클래스 심화(1)

an_yan_yang·2025년 2월 19일

본 글을 개인적 학습을 위한 글입니다. 틀린 내용이 있을 시 마구 지적해주시면 감사합니다.

이번 글은 클래스의 심화에 대해 다루는 글입니다. 내용이 방대하다보니, 두 개의 글 정도로 나누어 알아보겠습니다.


String Class

  아마 C++에서 사용하는 클래스 중 가장 많이 사용하는 클래스라고 하면 String Class일 것입니다. 문자열을 다루기에 아주 좋은 클래스이죠. 사실상 primitive와 같이 사용된다고 해도 무방할 것입니다. 먼저 이 string에 대해 간단하게나마 알아보며 글을 시작해보겠습니다.

string에는 다음과 같은 주요 기능이 있습니다.

  • 문자열 생성
  • 문자열 출력
  • 문자열 입력
  • 문자열 숫자 변환 : stoi() 전역 함수 이용 (C++ 11 표준부터)

이 외에도

  • 문자열 길이 : .length() 멤버 함수 이용
  • 사전 순 배열 비교 : <, > 연산자를 이용하여 간단하게 비교 가능


함수와 인자 전달

  클래스의 멤버 함수를 이야기하기 전에 먼저 함수 그 자체에 대해 알아볼 필요성이 있습니다. 그 중에서 인자(매개 변수) 전달에 대해 먼저 보겠습니다.


인자 전달 방식

인자 전달 방식은 기본적으로 2가지 범주로 나뉘어집니다.

  • 값에 의한 호출 (Call by Value)

    • 함수가 호출되면 매개 변수가 스택에 생성됨
    • 호출하는 코드에서 값을 넘겨줌
    • 호출하는 코드에서 넘어온 값이 매개 변수에 복사됨
  • 주소에 의한 호출 (Call by Address)

    • 함수의 매개 변수는 포인터 타입(포인터 타입의 매개 변수가 스택에 생성됨)
    • 호출하는 코드에서는 명시적으로 주소를 넘겨줌(기본 타입 변수나 객체의 경우 주소 전달)
    • 호출하는 코드에서 넘어온 주소 값이 매개 변수에 복사됨

이 두 가지 방법에서 객체가 전달되는 것을 포인트로 하여 조금 더 자세히 살펴 보자면,

  • 값에 의한 호출로 객체 전달
    • 함수를 호출하는 쪽에서 객체 전달
    • 함수의 매개변수 객체 생성(스택에 할당, But 생성자는 호출 X) ⇒ 생성자와 소멸자의 비대칭 실행
    • 함수 종료(소멸자 호출)

→ 매개 변수 객체의 생성자가 실행되지 않는 이유

: 호출되는 순간의 실인자의 객체 상태를 매개 변수 객체에 그대로 전달하기 위함


  위의 설명이 잘 이해가 되지 않는다면, 다음과 같은 코드를 확인하면 됩니다. 만일 매개 변수의 생성자가 호출된다고 가정한다면, “Oh!!!”가 총 두 번 출력될 것입니다. 하지만, 실제로 돌려본다면 한 번만 출력되는 것을 확인할 수 있습니다.

class MyClass{
public:
    MyClass(){
        cout << "Oh!!!" << endl;
    }
};

void test(MyClass in){
    cout << "Function!"<<endl;
}

int main(void){
    MyClass tmp;
    test(tmp);
    
    return 0;
}
  • 주소에 의한 호출로 객체 전달
    • 함수 호출 시 객체의 주소만 전달(생성자와 소멸자 모두 실행되지 않는 구조)

그 외에 매개변수의 특성에 대해 조금 더 살펴보겠습니다.

  • 객체 치환
    • 동일한 클래스 타입의 객체끼리 치환 가능
    • 객체의 모든 데이터가 비트 단위로 복사
    • 치환된 두 객체는 현재 내용물만 같을 뿐 독립적인 공간 유지
  • 객체 리턴
    - 함수를 통한 return (리턴 타입 설정)

참조(Referencing)

참조는 C++에서 도입된 강력한 기능 중 하나입니다. 아래의 설명과 함께 이해해보겠습니다.

  • 참조 변수 선언
    • 참조자 ‘&’ 의 도입
    • 이미 존재하는 변수에 대한 다른 이름(별명)을 선언
      • 참조 변수는 이름만 생기며
      • 참조 변수에 새로운 공간을 할당하지 않는다.
      • 초기화로 지정된 기존 변수를 공유한다.

→ 직관적으로, 포인터의 개념과 매우 유사합니다.

참조는 값만 보내는 것이 아니라 변수 자체를 보내는 것이다.

함수로 예를 들어보자면,

int & getA() { return g_a; }

  이 경우에, getA() = a와 같이 이용하면 g_a의 값을 바꿀 수 있게 되는 것이다. 사실 상, 포인터를 레퍼런스로 포장한 것일 뿐이다. 주의할 점은 로컬 변수를 reference로 return하지 않도록 하는 것이다.


참조에 의한 호출(Call by Reference)

값에 의한 호출과 주소에 의한 호출에 더해진 새로운 호출 방식입니다.

  • 참조를 가장 많이 활용하는 사례
  • Call by Reference라 불린다.
  • 함수 형식
    • 함수의 매개 변수를 참조 타입으로 선언
      • 참조의 매개변수(reference parameter)라고 부름(참조 매개변수는 실인자 변수를 참조)
      • 참조 매개 변수의 이름만 생기고 공간이 생기지 않음
      • 참조 매개 변수는 실인자 변수 공간 공유
      • 참조 매개 변수에 대한 조작은 실인자 변수 조작 효과

→ Reference를 함수의 인자로 사용할 때, 무조건 포인터를 가질 수 있는 변수를 넣어야 합니다.


참조 리턴(Returning of Reference)

  • C언어의 함수 리턴
    • 함수는 반드시 값만 리턴 (포인터도 일종의 값임)
  • C++의 함수 리턴
    • 함수는 값 외에 참조를 리턴할 수 있다.
    • 참조 리턴
      • 변수 등과 같이 현존하는 공간에 대한 참조 리턴(변수의 값을 리턴하는 것이 아님)


생성자 추가

  C++에서는 다양한 형태의 생성자를 지원합니다. 이들을 잘 이용하고, 구현한다면 훨씬 좋은 형태의 클래스를 만들어 사용할 수 있을 것입니다. 가장 대표적인 복사 생성자를 살펴 보기 전에, 복사란 무엇인지에 대해서 먼저 살펴 보겠습니다.


C++에서 얕은 복사와 깊은 복사

  보통 복사라 함은 크게 얕은 복사와 깊은 복사로 나뉘어 집니다. 이 둘의 차이를 확실히 알아야 복사 생성자 등을 구현할 때 깊은 이해가 생길 것입니다.

  • 얕은 복사(Shallow Copy) : 동적 메모리(힙)은 복사되지 않는다.
    • 객체 복사 시, 객체의 멤버를 1 : 1로 복사
    • 객체의 멤버 변수에 동적 메모리가 할당된 경우
      • 사본은 원본 객체가 할당 받은 메모리를 공유하는 문제 발생
        그 메모리는 포인터 형태로 연결되어 있기 때문이다.

  • 깊은 복사(Deep Copy) : 동적 메모리(힙)까지 복사된다. (이에 대한 정의는 사용자가 해야 한다.)
    • 객체 복사 시, 객체의 멤버를 1 : 1로 복사
    • 객체의 멤버 변수에 동적 메모리가 할당된 경우
      • 사본은 원본이 가진 메모리 크기만큼 별도로 동적 할당
      • 원본의 동적 메모리에 있는 내용을 사본에 복사
    • 완전한 형태의 복사
      • 사본과 원본은 메모리를 공유하는 문제가 발생하지 않음


복사 생성자(Copy Constructor)

  복사 생성자는 사용자 정의 자료형(클래스)의 융통성을 수직으로 상승시켜주는 장치입니다. 이는 객체의 복사 생성 시 호출되는 특별한 생성자입니다.

  • 특징
    • 한 클래스에 오직 한 개만 선언 가능
    • 복사 생성자는 보통 생성자와 클래스 내에 중복 선언 가능
    • 클래스에 대한 참조 매개 변수를 가지는 독특한 생성자

사실, 컴파일러가 자동으로 생성하는 생성자가 2개 존재한다.

  C++을 공부한 사람이면 알다시피, 컴파일러가 자동으로 생성하는 생성자는 Default 생성자입니다. 이는 생성자가 존재하지 않을 때, 아무것도 하지 않는 생성자를 컴파일러가 묵시적으로 선언하는 것입니다. 하지만 컴파일러는 복사 생성자를 기본으로 생성합니다. 둘의 극명한 차이는 Default 생성자는 다른 생성자가 존재하면 컴파일러가 생성하지 않는다 즉, 존재하지 않을 수도 있다는 것입니다. 하지만, 복사 생성자는 항상 반드시 언제나 존재합니다. 이는 복사 생성자는 개발자에 의해 선언되더라도 바꿔치기 당하는 것에 불과하다는 것을 이야기합니다.

MyClass(const MyClass &in){ } // 복사 생성자의 기본적인 형태

  그렇다면, 복사 생성자를 어떨 때 사용해야 하는 것일까요? 바로 Class 내에서 동적 할당이 발생할 때입니다. 이 때, 깊은 복사(Deep Copy)를 위해 반드시 복사 생성자를 선언해야 합니다. 그러나, 만일 여의치 않은 상황이거나 깊은 복사를 발생하게 하고 싶지 않다면, 오류를 방지하기 위해 복사 생성자를 private로 선언해주어 애초에 사용하는 것을 방지해야 합니다.


기본 복사 생성자(Default Copy Constructor)

  위에서 언급한 컴파일러가 클래스 내에 명시적 복사 생성자가 존재하지 않을 때, 묵시적으로 선언하는 기본 복사 생성자에 대한 설명입니다.

  • 묵시적 복사 생성에 의해 복사 생성자가 자동 호출되는 경우
    • “값에 의한 호출(Call by Value)”로 객체가 전달될 때
    • 객체로 초기화하여 객체가 생성될 때
    • 함수에서 객체를 리턴할 때


함수 추가

  클래스에 대한 더 얘기를 하기 전에 함수에 대해 조금 더 알아볼 것이 있습니다. 함수 중복(Function Overloading)이라고 불리우는 이 문법은 C++이 더 강력한 기능과 다형성을 제공할 수 있도록 도와줍니다.

  • 동일한 이름의 함수가 공존
    • 다형성 (C언어에서는 불가능)
  • 함수 중복이 가능한 범위
    • 보통 함수들 사이, 클래스의 멤버 함수들 사이
    • 상속 관계에 있는 기본 클래스와 파생 클래스의 멤버 함수들 사이

함수 중복 성공 조건 → “컴파일러가 구분할 수 있는가?”

  • 중복된 함수들의 이름 동일
  • 중복된 함수들의 매개 변수 타입이 다르거나 개수가 달라야 함
  • 리턴 타입이 다르다고만 해서 함수 중복이 되지 않는다.

  함수 중복을 왜 사용하느냐라고 하면 당연히 편리성이 가장 큰 이유가 될 것입니다. 동일한 이름의 함수들을 사용하면 함수 이름을 구분하여 기억할 필요가 없고, 함수 호출을 잘못하는 실수를 줄일 수 있습니다.


생성자 함수 중복

  생성자 함수도 중복이 가능합니다. 객체 생성 시, 매개 변수를 통해 사용자가 정의한 대로 다양한 형태의 초깃값을 전달할 수 있습니다.

MyClass() { }

MyClass(int age) { this->age = age; }

MyClass(int age, string name) { this -> age = age; this->name = name; }

소멸자 함수 중복

  하지만 소멸자는 함수 중복이 불가합니다. 첫째로 소멸자는 매개 변수를 가지지 않습니다. 이는 컴파일러가 구분할 수 있는 최소한의 기준조차 가지지 않았음을 뜻합니다. 둘째로 C++에서는 한 클래스 내에 소멸자는 오직 하나만 존재하도록 하고 있습니다. 문법적으로도 불가하다는 이야기이죠.


디폴트 매개 변수(Default Parameters)

  디폴트 매개 변수도 함수 사용의 편의성과 다형성을 제공할 수 있는 기능입니다. 매개 변수에 값이 넘어오지 않는 경우, 디폴트 값을 받도록 선언하는 것입니다. 물론 값을 넘겨주면 디폴트 값이 아닌 인자로 들어온 값이 넘어옵니다.

  디폴트 매개변수는 사용할 때 이의 제약 조건을 주의하며 사용해야 합니다. 디폴트 매개변수는 보통 매개변수 앞에 선언될 수 없다는 것입니다. 즉, 끝 쪽에 몰아서 선언해야 한다는 것이죠. 아래 간략한 코드를 보며 확인해보겠습니다.

void func1(int a = 10, int b, int c = 20) // int a로 인해 컴파일 오류

void func2(int b, int a = 10, int c = 20) // 사용 가능

  또 주의할 점은 함수 선언에만 default 값이 들어가고, 구현부에는 들어가지 않는다는 것입니다. 이는 C++에만 존재하는 특징적인 기능으로 설명됩니다. 더하여 인자가 다른 같은 이름의 함수를 선언할 때, Default Parameters를 사용한다면, 굉장히 주의해야 합니다. 이는 동적 바인딩에 대한 설명과 함께 추후에 다시 알아보겠습니다.


함수 중복 간소화

  디폴트 매개 변수를 사용할 때의 장점이라고 할 수 있는 것은 함수 중복의 간소화일 것입니다. 그렇기에 중복 함수들과 디폴트 매개 변수를 가진 함수를 함께 사용하는 것에 제한됩니다. 이유라 함은 당연히 컴파일러가 어떤 함수를 호출할 지 결정할 수 없다는 것입니다. 이러한 현상을 모호성(Ambiguity)이라고 표현하기도 합니다.

    void show(int x) {
        cout << "show(int x): " << x << endl;
    }

    void show(int x, int y = 20) { // 디폴트 매개변수 제공
        cout << "show(int x, int y): " << x << ", " << y << endl;
    }
    
    show(10); // 이 때, 어떤 함수를 호출할지 컴파일러는 정할 수 없습니다.

함수 중복의 모호성(Ambiguity)

  위에서 얘기했듯이, 함수 중복이 모호하여 컴파일러가 어떤 함수를 호출하는지 판단하지 못하는 경우에 모호성이 존재한다고 합니다. 이에 대한 대표적인 케이스를 살펴보겠습니다.

  • 형 변환으로 인한 모호성
class Example {
public:
    void show(int x) {
        cout << "show(int): " << x << endl;
    }

    void show(double x) { 
        cout << "show(double): " << x << endl;
    }
};

int main() {
    Example obj;
    obj.show('A'); // 'A'는 char인데, int와 double로 둘 다 변환 가능 -> 모호성 발생

    return 0;
}
  • 참조 매개 변수로 인한 모호성
class Example {
public:
    void show(int& x) { // 비const 참조
        cout << "show(int&): " << x << endl;
    }

    void show(const int& x) { // const 참조
        cout << "show(const int&): " << x << endl;
    }
};

int main() {
    Example obj;
    int a = 10;
    const int b = 20;

    obj.show(a); // 비const 참조 가능
    obj.show(b); // const 참조 사용

    obj.show(30); // 어떤 함수가 호출될까? (모호성 발생)

    return 0;
}
  • 디폴트 매개 변수로 인한 모호성
    • 이는 위에서 살펴본 예시에 해당한다.


  오늘은 클래스의 활용 심화 일부에 대해서 다루어 보았습니다. 추가적인 내용은 다음 글에서 다루어보도록 하겠습니다.


참고 자료

profile
개발자가 되고 싶은 공대생

0개의 댓글