본문으로 건너뛰기

안동민 개발노트

본문 시작

멤버 함수와 멤버 변수

객체마다 독립적인 비정적 데이터 멤버와 이를 다루는 멤버 함수의 접근·const·static 규칙을 익힙니다.

클래스를 구성하는 가장 기본적인 요소인 멤버 변수(Member Variables)멤버 함수(Member Functions)를 더 깊이 있게 다룹니다.

이들은 클래스가 어떤 데이터를 가지고 있고, 어떤 행동을 할 수 있는지를 정의하며, 객체 지향 프로그래밍의 핵심적인 구성 요소입니다.

호출자는 public 멤버 함수를 통해서만 private 상태를 바꾸고, public 함수는 반지름이 음수가 아니어야 한다는 불변식을 검증한다. c1과 c2는 각각 독립적인 비정적 데이터 멤버를 가지며, const 조회 함수는 호출 대상으로 선택한 객체의 상태만 읽는다.

C++ · encapsulation · object boundary

public 동작이 private 객체 상태의 불변식을 지킨다

객체마다 따로 존재하는 것은 비정적 데이터 멤버입니다. 외부 호출은 public 동작을 통과하고, 그 동작이 검증을 마친 뒤 선택한 객체의 private 상태만 읽거나 바꿉니다.

C++ public 게이트와 객체별 private 상태 호출자 c1.setRadius 7은 c1 객체의 public setRadius 함수로 들어간다. 함수는 반지름이 0 이상인지 검증한 뒤 c1의 private radius만 7로 바꾼다. c2는 radius 10과 파랑 색상을 그대로 유지한다. c2의 const 넓이 조회는 c2의 상태만 읽는다. OBJECT c1 · 독립 경계 OBJECT c2 · 독립 경계 WRITE READ CALLER c1.setRadius(7) private 직접 접근 불가 c1 · PUBLIC GATE setRadius(7) r ≥ 0 검증 뒤 갱신 c1 · PRIVATE STATE radius = 7 color = "빨강" INVARIANT · radius ≥ 0 c2 · PUBLIC CONST calculateArea() const 선택한 c2만 조회 c2 · PRIVATE STATE radius = 10 color = "파랑" c1 변경과 무관

01 · public gate

외부 호출은 public 동작을 통과한다

c1.setRadius(7)은 private 상태에 직접 쓰지 않습니다. setRadius가 입력을 검사하고 반지름이 0 이상이라는 불변식을 지킨 뒤 갱신합니다.

02 · c1 private state

c1의 비정적 데이터 멤버만 바뀐다

radius는 7, color는 빨강입니다. 이 상태는 c1 객체 경계 안에 있으며 public 멤버 함수가 읽거나 바꿉니다.

03 · c2 private state

c2는 자신의 상태를 독립적으로 유지한다

c2의 radius는 10, color는 파랑으로 남습니다. c1의 비정적 데이터 멤버를 바꿔도 c2에는 영향이 없습니다.

04 · const read

const 조회는 선택한 객체의 상태만 읽는다

c2.calculateArea() const는 c2의 일반 비정적 데이터 멤버를 변경하지 않고 읽습니다.

객체별 비정적 데이터 멤버의 시작값은 생성할 때 정하며, 보통 생성자 멤버 초기화 리스트를 사용합니다. 멤버 함수의 코드는 객체마다 복사되지 않습니다. thread_local이 아닌 정적 데이터 멤버는 모든 객체가 하나의 엔터티를 공유합니다.


멤버 변수 (Member Variables)

멤버 함수와 멤버 변수는 객체 경계, 접근 권한, 상태의 불변식을 기준으로 확인합니다.

멤버 변수는 클래스의 속성(Attributes) 또는 상태(State)를 나타내는 변수입니다.

이들은 일반 변수와 동일하게 데이터 타입을 가지며, 클래스 내부에 선언됩니다.

각 객체가 자신만의 복사본을 갖는 것은 **비정적 데이터 멤버(non-static data member)**입니다. 정적 데이터 멤버는 객체의 하위 객체가 아닙니다. thread_local이 아닌 정적 데이터 멤버는 클래스의 모든 객체가 하나의 엔터티를 공유하고, thread_local static은 스레드마다 별도 엔터티를 가집니다. 멤버 함수의 코드도 객체마다 복사되지 않습니다.

특징
  • 객체마다 독립적인 값: 각 객체는 비정적 데이터 멤버 하위 객체를 따로 가지며, 한 객체의 값을 바꿔도 다른 객체의 값에는 영향을 주지 않습니다.
  • 스코프: 멤버 변수는 클래스 스코프 내에서 유효하며, 클래스의 모든 멤버 함수에서 직접 접근할 수 있습니다.
  • 접근 지정자: public, private, protected 접근 지정자를 사용하여 외부로부터의 접근을 제어합니다. (일반적으로 private으로 선언하여 캡슐화를 유지)
  • 초기화: 생성자를 통해 초기화하는 것이 일반적이며, 특히 멤버 초기화 리스트를 사용하는 것이 권장됩니다.
예시: Circle 클래스의 멤버 변수
#include <iostream>
#include <stdexcept>
#include <string>

class Circle {
private: // private 멤버 변수
    double radius; // 원의 반지름
    std::string color; // 원의 색상

public:
    // 생성자 (멤버 초기화 리스트 사용)
    Circle(double r, std::string c) : radius(r), color(c) {
        if (!(r >= 0.0)) { // 음수와 NaN을 모두 거부해 불변식을 세움
            throw std::invalid_argument("반지름은 0 이상이어야 합니다.");
        }
        std::cout << "Circle 객체 생성: 반지름 " << radius << ", 색상 " << color << std::endl;
    }

    // 멤버 함수 (게터/세터)
    double getRadius() const {
        return radius;
    }

    void setRadius(double r) {
        if (r >= 0) {
            radius = r;
        } else {
            std::cerr << "오류: 반지름은 음수가 될 수 없습니다." << std::endl;
        }
    }

    const std::string& getColor() const {
        return color;
    }

    void setColor(std::string c) {
        color = c;
    }

    // 원의 넓이를 계산하는 멤버 함수
    double calculateArea() const {
        return 3.14159 * radius * radius;
    }
};

int main() {
    Circle c1(5.0, "빨강"); // 반지름 5.0, 색상 빨강인 원
    std::cout << "c1의 넓이: " << c1.calculateArea() << std::endl; // 출력: c1의 넓이: 78.5397

    Circle c2(10.0, "파랑"); // 반지름 10.0, 색상 파랑인 또 다른 원
    std::cout << "c2의 넓이: " << c2.calculateArea() << std::endl; // 출력: c2의 넓이: 314.159

    c1.setRadius(7.0); // c1의 반지름만 변경
    std::cout << "변경된 c1의 넓이: " << c1.calculateArea() << std::endl; // 출력: 변경된 c1의 넓이: 153.938

    return 0;
}

위 예시에서 radiuscolorCircle 클래스의 비정적 데이터 멤버입니다.

c1c2 두 개의 Circle 객체는 각자 독립적인 radiuscolor 값을 가집니다.

c1radius를 변경해도 c2radius에는 영향을 미치지 않습니다.


멤버 함수 (Member Functions)

멤버 함수는 클래스의 행동(Behavior) 또는 기능(Functionality)을 나타내는 함수입니다.

이들은 클래스 내부에 선언되며, 해당 클래스의 객체에 속한 멤버 변수에 접근하고 조작할 수 있습니다.

특징
  • 객체의 상태에 접근: 멤버 함수는 해당 객체의 멤버 변수에 직접 접근하여 읽거나 변경할 수 있습니다.
  • 암시적 객체 매개변수와 this: 이 절에서 다루는 일반적인 implicit-object 비정적 멤버 함수를 호출하면 호출 객체가 암시적 객체 매개변수(implicit object parameter)에 결합됩니다. 함수 본문에서 this 표현식은 그 객체를 가리킵니다. this는 사용자가 이름 붙여 선언하는 일반 매개변수가 아니며 다른 객체를 가리키도록 다시 대입할 수 없습니다. C++23의 explicit-object 멤버 함수는 객체 매개변수를 명시적으로 선언하며 본문에 this 표현식이 없습니다.
  • 접근 지정자: public, private, protected 접근 지정자를 사용하여 외부로부터의 호출을 제어합니다. (private 멤버 변수에 접근하기 위한 public 게터/세터가 일반적)
  • 선언과 정의: 클래스 내부에 선언(프로토타입)하고, 클래스 외부에서 클래스이름:: 스코프 연산자를 사용하여 정의하는 것이 일반적입니다.
예시: Account 클래스의 멤버 함수
#include <iostream>
#include <string>

class Account {
private:
    std::string ownerName;
    double balance;

public:
    Account(std::string name, double initialBalance) : ownerName(name), balance(initialBalance) {
        std::cout << "계좌 생성: " << ownerName << "님의 초기 잔액 " << balance << std::endl;
    }

    // 입금 기능 (멤버 함수)
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount; // 멤버 함수는 private 멤버 변수 balance에 직접 접근 가능
            std::cout << amount << "원 입금. 현재 잔액: " << balance << std::endl;
        } else {
            std::cout << "입금액은 양수여야 합니다." << std::endl;
        }
    }

    // 출금 기능 (멤버 함수)
    void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount; // private 멤버 변수 balance에 직접 접근 가능
            std::cout << amount << "원 출금. 현재 잔액: " << balance << std::endl;
        } else {
            std::cout << "출금 실패: 유효하지 않은 금액이거나 잔액 부족. 현재 잔액: " << balance << std::endl;
        }
    }

    // 잔액 조회 기능 (게터 멤버 함수)
    double getBalance() const {
        return balance; // private 멤버 변수 balance 값 반환
    }

    // 소유자 이름 조회 기능
    const std::string& getOwnerName() const {
        return ownerName;
    }
};

int main() {
    Account acc1("김미영", 50000.0);
    acc1.deposit(10000.0);    // 10000원 입금
    acc1.withdraw(5000.0);   // 5000원 출금
    std::cout << acc1.getOwnerName() << "님의 최종 잔액: " << acc1.getBalance() << std::endl; // 잔액 확인

    Account acc2("박지훈", 10000.0);
    acc2.withdraw(12000.0); // 잔액 부족으로 출금 실패
    std::cout << acc2.getOwnerName() << "님의 최종 잔액: " << acc2.getBalance() << std::endl;

    return 0;
}

deposit(), withdraw(), getBalance()Account 클래스의 비정적 멤버 함수입니다. 조회 함수인 getBalance()getOwnerName()에는 const를 붙여 읽기 전용 객체에서도 호출할 수 있게 했습니다.

이 함수들은 ownerNamebalance라는 private 멤버 변수에 직접 접근하여 계좌의 상태를 변경하거나 조회할 수 있습니다.


const 멤버 함수

멤버 함수를 선언할 때 함수 시그니처 뒤에 const 키워드를 붙일 수 있습니다.

const 멤버 함수 선언 형식
class Sample {
public:
    int value() const;
};

const 멤버 함수는 암시적 객체 매개변수를 const로 취급합니다. 클래스가 X라면 본문의 thisconst X* 타입이며, 선택한 객체를 const 접근 경로로 다룹니다. 실제 호출 객체 자체는 변경 가능한 객체일 수도 있지만, 그 경로로 일반 비정적 데이터 멤버를 수정할 수는 없습니다.

즉, 객체의 논리적 상태를 읽기만 하는 조회 함수는 const 멤버 함수로 선언해야 합니다.

특징
  • const 멤버 함수는 this를 통해 일반 비정적 데이터 멤버를 변경할 수 없습니다. 비정적 멤버 함수를 호출한다면 const 객체나 const 참조자·포인터에서는 const로 선언된 오버로드만 후보가 됩니다.
  • 정적 멤버 함수는 암시적 객체 매개변수가 없으므로 객체의 const 여부와 무관합니다. 객체 없이 클래스이름::함수()로 호출하는 표기가 의도를 가장 잘 드러냅니다.
  • mutable로 지정한 데이터 멤버는 const 멤버 함수에서도 바꿀 수 있습니다. 캐시나 진단 횟수처럼 관찰 가능한 논리적 상태가 아닌 구현 세부에 한정하고, 동기화 책임도 별도로 지켜야 합니다.
  • 같은 이름과 매개변수 목록에 const와 비const 오버로드를 둘 수 있습니다. 변경 가능한 객체는 비const 오버로드를, const 객체는 const 오버로드를 선택합니다.
장점
  • 안전성: 객체의 상태를 변경하지 않는 함수를 명확히 하여 코드의 안정성을 높입니다.
  • 유연성: const 객체를 함수에 인자로 전달할 때, 그 객체를 대상으로 하는 비정적 호출을 const 멤버 함수로 제한할 수 있습니다.

const 객체와 const 참조자는 읽기 전용 약속을 지켜야 하므로, 비정적 멤버 함수의 호출 가능성도 const 선언 여부에 따라 달라집니다.

예시: const 멤버 함수 활용
#include <iostream>
#include <string>

class Point {
private:
    int x;
    int y;
    mutable int displayCount{0}; // 좌표가 아닌 진단용 구현 세부

public:
    Point(int _x, int _y) : x(_x), y(_y) {}

    // 값만 반환하는 게터 함수는 const 멤버 함수로 선언하는 것이 좋습니다.
    int getX() const { // const 멤버 함수
        // x = 10; // 컴파일 오류! const 함수 내에서 멤버 변수 변경 불가
        return x;
    }

    int getY() const { // const 멤버 함수
        return y;
    }

    // 객체의 const 여부에 따라 선택되는 두 오버로드
    int& xValue() {
        return x;
    }

    const int& xValue() const {
        return x;
    }

    // 객체의 상태를 변경하는 세터 함수는 const가 될 수 없습니다.
    void setX(int _x) {
        x = _x;
    }

    // Point 정보를 출력하는 함수 (값을 변경하지 않으므로 const가 적합)
    void display() const { // const 멤버 함수
        ++displayCount; // mutable 진단 값만 변경
        std::cout << "좌표: (" << x << ", " << y << ")" << std::endl;
    }

    int getDisplayCount() const {
        return displayCount;
    }
};

// 함수 매개변수로 const 참조자를 사용하는 경우
void printPoint(const Point& p) { // p는 const 객체를 참조
    p.display(); // OK: display()는 const 멤버 함수이므로 호출 가능
    // p.setX(10); // 컴파일 오류: p는 const 참조자이므로 const가 아닌 setX() 호출 불가
}

int main() {
    Point p1(10, 20);
    p1.display(); // 출력: 좌표: (10, 20)

    p1.xValue() = 15; // 비const 오버로드가 int&를 반환
    p1.display(); // 출력: 좌표: (15, 20)

    const Point p2(30, 40); // const 객체 생성
    p2.display(); // OK: display()는 const 멤버 함수이므로 호출 가능
    std::cout << "p2의 x: " << p2.xValue() << std::endl; // const 오버로드 선택
    // p2.setX(35); // 컴파일 오류: p2는 const 객체이므로 const가 아닌 setX() 호출 불가

    printPoint(p1); // p1은 const 참조자로 전달
    printPoint(p2); // p2는 const 참조자로 전달

    return 0;
}

const 멤버 함수는 코드의 의도를 명확히 하고, 잠재적인 버그를 줄이는 데 도움을 줍니다.

객체의 논리적 상태를 변경하지 않는 멤버 함수에는 const를 붙이는 습관을 들이는 것이 좋습니다. 위의 displayCount는 출력 횟수를 기록하는 구현 세부이므로 mutable로 한정했지만, 좌표처럼 사용자에게 보이는 상태까지 mutable로 우회해서는 안 됩니다.


정적 멤버 (Static Members) (심화)

클래스의 멤버 변수나 멤버 함수 앞에 static 키워드를 붙이면 정적 멤버(Static Member)가 됩니다.

정적 멤버는 클래스 스코프에 속하지만 특정 객체의 하위 객체나 동작은 아닙니다.

  • 정적 데이터 멤버: thread_local이 아니라면 객체마다 복사되지 않는 하나의 공유 엔터티입니다. thread_local static은 스레드마다 하나씩 존재합니다. 접근 권한이 허용되면 객체 없이 클래스이름::정적멤버로 접근할 수 있습니다.
  • 정의 규칙: 비인라인 정적 데이터 멤버의 클래스 안 선언은 보통 정의가 아닙니다. 그 멤버가 odr-use되면 네임스페이스 범위의 정의가 필요합니다. 반면 C++17부터 inline static 데이터 멤버는 클래스 안 선언이 곧 정의이며, static constexpr 데이터 멤버도 암시적으로 인라인이므로 별도 정의가 필요하지 않습니다.
  • 정적 멤버 함수: 암시적 객체 매개변수와 this가 없습니다. 그래서 객체를 지정하지 않고 비정적 멤버에 접근할 수는 없지만, 매개변수로 받은 객체나 참조를 명시해 그 객체를 다룰 수 있습니다. 정적 함수도 클래스 멤버이므로 접근 권한이 허용하는 private 멤버를 사용할 수 있습니다.

정적 멤버는 모든 객체가 공유하는 설정이나 객체 없이 호출할 클래스 수준 동작에 적합합니다.

예시: inline static 공유 상태와 명시적으로 받은 객체
#include <iostream>

class Meter {
private:
    int value;
    inline static int scale{1}; // C++17: 클래스 안 선언이 정의

public:
    explicit Meter(int initialValue) : value(initialValue) {}

    int scaledValue() const {
        return value * scale;
    }

    static void setScale(int nextScale) {
        if (nextScale > 0) {
            scale = nextScale;
        }
    }

    static int readRawValue(const Meter& meter) {
        return meter.value; // 전달받은 객체를 명시하면 private 상태도 읽을 수 있음
    }
};

int main() {
    Meter first(3);
    Meter second(5);

    Meter::setScale(2);
    std::cout << first.scaledValue() << ", " << second.scaledValue() << std::endl;
    std::cout << Meter::readRawValue(first) << std::endl;

    return 0;
}

일반 비정적 멤버 함수, const 비정적 멤버 함수, 정적 멤버는 객체 요구와 상태 범위가 서로 다릅니다.

일반 비정적 멤버 함수, const 비정적 멤버 함수, 정적 멤버를 객체 요구, this 표현식, 대상 상태와 저장, 호출 가능성, 변경 범위, 정의 규칙으로 비교한다. 이 표에서 다루는 implicit-object 비정적 함수에는 암시적 객체 매개변수가 있고 정적 함수에는 없으며, 정적 함수도 명시적으로 전달받은 객체는 다룰 수 있다.

C++ · member semantics · comparison

비정적, const, static은 객체 요구와 상태 범위가 다르다

비정적 함수에는 호출 객체가 결합되지만 정적 함수에는 그렇지 않습니다. const는 별도 저장 종류가 아니라 선택한 객체를 읽기 전용으로 다루는 비정적 함수의 한정자입니다.

객체 요구, 접근 경로, 저장과 정의 규칙 비교
기준 일반 비정적 const 비정적 static
객체 필요 필요. 변경 가능한 호출 객체가 결합됩니다. 필요. 변경 가능 객체와 const 객체 모두 결합될 수 있습니다. 불필요. X::f()를 권장하며 객체를 통한 표기도 가능합니다.
this 암시적 객체 매개변수가 있고 this는 선택한 객체를 가리킵니다. 암시적 객체 매개변수가 const로 한정되고 thisconst X* 타입의 접근 경로가 됩니다. 암시적 객체 매개변수와 this가 없습니다.
대상 상태 선택한 객체의 비정적 데이터 멤버를 다룹니다. 저장은 객체마다 독립적입니다. 같은 객체별 저장을 읽기 전용 경로로 다룹니다. thread_local이 아닌 정적 데이터 멤버는 객체 하위 객체가 아니며 하나의 엔터티를 공유합니다.
호출 가능 const 객체에서는 호출할 수 없습니다. 두 오버로드가 있으면 변경 가능 객체가 이쪽을 선택합니다. 변경 가능 객체와 const 객체에서 호출할 수 있습니다. const 객체는 이쪽만 후보입니다. 객체의 const 여부와 무관합니다. 객체 없이 호출할 수 있습니다.
변경 범위 선택한 객체의 일반 비정적 데이터 멤버를 바꿀 수 있습니다. 일반 멤버는 바꿀 수 없습니다. mutable 구현 세부만 예외입니다. 정적 상태를 바꿀 수 있고, 명시적으로 받은 객체도 접근 권한 안에서 다룰 수 있습니다.
정의 규칙 클래스 안에서 정의하거나 밖에서 X::로 정의합니다. 같은 방식이며 선언과 정의의 뒤쪽 const가 일치해야 합니다. 함수의 클래스 밖 정의에는 static을 반복하지 않습니다. 비인라인 정적 데이터는 odr-use 시 밖의 정의가 필요하고, C++17의 인라인 정적 또는 constexpr 정적 데이터는 클래스 안에서 정의됩니다.

01 · non-static

일반 비정적 멤버 함수

객체 요구: 변경 가능한 호출 객체가 필요합니다.

this: 암시적 객체 매개변수가 있고 선택한 객체를 가리킵니다.

대상 상태: 객체마다 독립적인 비정적 데이터 멤버를 다룹니다.

호출: const 객체에서는 호출할 수 없습니다. const 오버로드와 함께 있으면 변경 가능 객체가 이쪽을 선택합니다.

변경: 선택한 객체의 일반 비정적 데이터 멤버를 바꿀 수 있습니다.

정의: 클래스 안에서 정의하거나 밖에서 X::로 정의합니다.

02 · const non-static

const 비정적 멤버 함수

객체 요구: 변경 가능 객체와 const 객체 모두 사용할 수 있습니다.

this: 암시적 객체 매개변수가 const로 한정되고 const X* 타입의 접근 경로가 됩니다.

대상 상태: 같은 객체별 저장을 읽기 전용 경로로 다룹니다.

호출: const 객체는 이 오버로드만 후보로 삼습니다.

변경: 일반 멤버는 바꿀 수 없고 mutable 구현 세부만 예외입니다.

정의: 클래스 밖 정의에서도 뒤쪽 const를 빠뜨리면 안 됩니다.

03 · static

정적 멤버

객체 요구: 객체 없이 X::f()로 호출할 수 있습니다. 객체를 통한 표기도 가능하지만 클래스 한정이 선명합니다.

this: 암시적 객체 매개변수와 this가 없습니다.

대상 상태: thread_local이 아닌 정적 데이터 멤버는 하나의 엔터티를 공유합니다.

호출: 객체의 const 여부와 무관합니다.

변경: 정적 상태를 바꿀 수 있고, 명시적으로 받은 객체도 접근 권한 안에서 다룰 수 있습니다.

정의: 함수의 밖 정의에는 static을 반복하지 않습니다. 비인라인 정적 데이터는 odr-use 시 밖에서 정의하며, C++17의 인라인 정적 또는 constexpr 정적 데이터는 클래스 안 선언이 정의입니다.

이 비교는 implicit-object 비정적 멤버 함수를 기준으로 합니다. C++23 explicit-object 멤버 함수는 객체 매개변수를 명시적으로 선언하며 본문에 this 표현식이 없습니다. 멤버 함수 코드는 객체마다 복사되지 않습니다. const는 객체 접근을 한정하고, static은 객체 없이 사용할 클래스 수준 경계를 만듭니다. mutable은 논리적으로 보이지 않는 구현 세부에만 좁게 사용해야 합니다.

멤버를 설계할 때는 먼저 값이 객체별 상태인지 공유 상태인지 정하고, 변경은 불변식을 지키는 public 동작으로 제한하세요. 읽기 동작에는 const를 붙이고, 객체가 필요 없는 동작만 static으로 분리하면 경계가 명확해집니다.