# C언어 (8)

## 1\. 객체 배열

### 1-1. 객체 배열이란?

C에서 `int arr[5]`로 정수 5개를 묶었듯이, C++에서는 **객체 여러 개를 배열로 묶을 수 있다.** 학생 100명의 정보를 관리해야 한다면 `Student` 객체를 100개 일일이 선언하는 대신 배열로 만들면 된다.

### 1-2. 객체 배열의 선언 형태

```cpp
class Student {
private:
    string name;
    int score;

public:
    Student() : name("없음"), score(0) {}
    Student(string n, int s) : name(n), score(s) {}

    void print() {
        cout << name << ": " << score << "점" << endl;
    }
};

int main() {
    Student students[3];  // 기본 생성자가 3번 호출됨
    return 0;
}
```

객체 배열을 선언하면 각 요소마다 **기본 생성자(매개변수 없는 생성자)** 가 호출된다. 기본 생성자가 없으면 컴파일 에러가 발생하므로, 객체 배열을 사용하려면 반드시 기본 생성자를 정의해야 한다.

초기화 리스트로 선언과 동시에 값을 넣을 수도 있다.

```cpp
Student students[3] = {
    Student("홍길동", 90),
    Student("김철수", 85),
    Student("이영희", 95)
};

for (int i = 0; i < 3; i++) {
    students[i].print();
}
```

배열의 각 요소는 독립적인 객체다. `students[0]`, `students[1]`, `students[2]` 각각이 자신만의 `name`과 `score`를 가진다. 일반 배열과 마찬가지로 인덱스는 0부터 시작하고, 점(`.`) 연산자로 멤버에 접근한다.

동적으로 객체 배열을 만들 수도 있다. 이때는 `new[]`로 할당하고 `delete[]`로 해제한다.

```cpp
Student *students = new Student[5];  // 기본 생성자 5번 호출

for (int i = 0; i < 5; i++) {
    students[i].print();
}

delete[] students;  // 반드시 delete[]로 해제
```

`delete`가 아니라 `delete[]`를 써야 한다. `delete[]`는 배열의 각 객체에 대해 소멸자를 호출한 뒤 메모리를 해제한다. `delete`를 쓰면 첫 번째 객체의 소멸자만 호출되어 메모리 누수나 정의되지 않은 동작이 발생한다.

* * *

## 2\. 객체 포인터

### 2-1. 객체 포인터란?

객체 포인터는 **객체의 메모리 주소를 저장하는 포인터** 다. C에서 구조체 포인터를 사용했던 것과 같은 개념이다.

```cpp
Student s1("홍길동", 90);
Student *p = &s1;

p->print();        // 화살표 연산자로 멤버 접근
(*p).print();      // 역참조 후 점 연산자 (같은 의미)
```

구조체 포인터에서 배웠던 것처럼, 포인터로 멤버에 접근할 때는 **화살표(**`->`**) 연산자** 를 사용한다. `p->print()`는 `(*p).print()`와 같다.

`new`로 힙에 객체를 동적 생성하면 포인터로 받아야 한다.

```cpp
Student *p = new Student("홍길동", 90);
p->print();
delete p;  // 사용 후 반드시 해제
```

객체 포인터 배열을 사용하면 각 객체를 독립적으로 동적 생성하고, 다형성을 활용할 수도 있다.

```cpp
Student *students[3];
students[0] = new Student("홍길동", 90);
students[1] = new Student("김철수", 85);
students[2] = new Student("이영희", 95);

for (int i = 0; i < 3; i++) {
    students[i]->print();
    delete students[i];
}
```

* * *

## 3\. this 포인터

### 3-1. this 포인터가 필요한 이유

클래스의 멤버함수는 여러 객체가 공유한다. `Student` 객체가 100개 있어도 `print` 함수의 코드는 메모리에 **하나만** 존재한다. 그러면 `print` 함수가 호출될 때 "지금 나를 호출한 객체가 누구인지"를 어떻게 알까?

바로 **this 포인터** 가 그 역할을 한다. `this`는 **자기 자신(현재 객체)을 가리키는 포인터** 로, 멤버함수가 호출될 때 컴파일러가 자동으로 전달한다.

```cpp
class Student {
private:
    string name;
    int score;

public:
    void setName(string name) {
        this->name = name;  // this->name은 멤버, name은 매개변수
    }
};
```

매개변수 이름과 멤버변수 이름이 같을 때 `this`가 특히 유용하다. `this->name`은 "이 객체의 `name` 멤버"를 명확히 가리키고, 그냥 `name`은 매개변수를 가리킨다. `this`가 없으면 둘 다 매개변수를 가리키게 되어 멤버변수에 값이 대입되지 않는다.

JavaScript의 `this`와 개념적으로 같다. JS에서 `this.name = name` 하는 것과 C++에서 `this->name = name` 하는 것은 같은 맥락이다. 다만 JS의 `this`는 호출 방식에 따라 바인딩이 달라지는 반면, C++의 `this`는 항상 해당 멤버함수를 호출한 객체를 가리킨다.

`this`는 포인터이므로 `this->` 또는 `(*this).`으로 멤버에 접근한다. `*this`는 객체 자체를 의미하며, 메서드 체이닝에 활용할 수 있다.

```cpp
class Builder {
private:
    int width;
    int height;

public:
    Builder() : width(0), height(0) {}

    Builder& setWidth(int w) {
        width = w;
        return *this;  // 자기 자신을 반환
    }

    Builder& setHeight(int h) {
        height = h;
        return *this;
    }
};

int main() {
    Builder b;
    b.setWidth(100).setHeight(200);  // 메서드 체이닝
    return 0;
}
```

`return *this`로 자기 자신의 참조를 반환하면, 반환된 객체에서 다시 메서드를 호출할 수 있다. JavaScript에서 jQuery의 `$('#id').css('color', 'red').show()` 체이닝과 같은 원리다.

* * *

## 4\. 전달인자가 객체인 함수

### 4-1. 객체 전달 방식

함수에 객체를 넘기는 방법은 크게 **값 전달**, **포인터 전달**, **레퍼런스 전달** 세 가지가 있다.

### 4-2. 객체에 대한 값 전달 방식

값으로 전달하면 객체의 **복사본** 이 만들어진다. C에서 구조체를 함수에 넘길 때와 같은 원리다.

```cpp
void printStudent(Student s) {  // 복사본이 생성됨
    s.print();
}

int main() {
    Student s1("홍길동", 90);
    printStudent(s1);  // s1의 복사본이 함수에 전달됨
    return 0;
}
```

함수 안에서 `s`를 수정해도 원본 `s1`에는 영향이 없다. 하지만 문제가 있다.

**복사 비용이 크다.** 객체가 크면(멤버가 많거나 문자열 등을 포함하면) 복사하는 데 시간과 메모리가 든다.

**복사 생성자가 호출된다.** 값으로 넘기면 복사 생성자가 호출되어 새 객체가 만들어지고, 함수가 끝나면 소멸자가 호출된다. 동적 메모리를 가진 객체라면 깊은 복사/얕은 복사 문제까지 신경 써야 한다.

### 4-3. 레퍼런스 형식

레퍼런스(참조)로 전달하면 복사 없이 **원본 객체를 직접 사용** 한다. 포인터와 비슷하지만 문법이 더 깔끔하다.

```cpp
void printStudent(const Student &s) {  // 복사 없이 원본 참조
    s.print();
}
```

`&`를 붙이면 레퍼런스로 전달된다. 내부적으로는 포인터와 비슷하게 동작하지만, 사용할 때는 일반 객체처럼 `.`으로 멤버에 접근한다. `->` 연산자를 쓸 필요가 없다.

`const`를 붙이면 함수 안에서 객체를 수정할 수 없다. 읽기만 하는 함수에서는 `const &`로 받는 것이 가장 효율적이고 안전하다.

원본을 수정해야 하는 경우에는 `const` 없이 레퍼런스로 받는다.

```cpp
void addBonus(Student &s, int bonus) {  // 원본 수정 가능
    s.addScore(bonus);
}
```

정리하면, 객체를 함수에 넘길 때는 **값 전달보다 레퍼런스 전달이 낫다.** 수정이 필요 없으면 `const &`로, 수정이 필요하면 `&`로 받는다. 값 전달은 불필요한 복사가 발생하므로 특별한 이유가 없는 한 피하는 것이 좋다.

포인터로 전달하는 것도 가능하지만, C++에서는 레퍼런스가 더 자연스러운 선택이다. 포인터는 `NULL`이 될 수 있고 `->` 연산자를 써야 하지만, 레퍼런스는 반드시 유효한 객체를 가리키고 `.`으로 접근할 수 있기 때문이다.

* * *

## 5\. const 멤버함수와 const 객체

### 5-1. const 멤버함수

`const` 멤버함수는 **객체의 멤버변수를 수정하지 않겠다고 약속하는 함수** 다. 함수 선언 뒤에 `const`를 붙인다.

```cpp
class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {}

    void print() const {  // const 멤버함수
        cout << name << ": " << score << "점" << endl;
    }

    void setScore(int s) {  // 일반 멤버함수 (멤버 수정)
        score = s;
    }
};
```

`print()` 뒤에 `const`가 붙어있다. 이 함수 안에서는 멤버변수의 값을 바꿀 수 없다. 만약 `const` 함수 안에서 멤버를 수정하려 하면 컴파일 에러가 발생한다.

#### 읽기 전용 함수

`const` 멤버함수는 곧 **읽기 전용 함수** 다. 값을 읽기만 하고 변경하지 않는 함수에는 `const`를 붙이는 것이 좋다. `getScore()`, `print()`, `getName()` 같은 함수들이 여기에 해당한다.

`const`를 붙이는 것이 중요한 이유는 `const` 객체 때문이다. `const`로 선언된 객체는 `const` **멤버함수만 호출할 수 있다.**

```cpp
const Student s1("홍길동", 90);
s1.print();        // OK — const 멤버함수
// s1.setScore(95);  // 에러! — const 객체에서 non-const 함수 호출 불가
```

함수 매개변수로 `const &`를 받을 때도 마찬가지다. `const`로 받은 객체에서는 `const` 멤버함수만 호출할 수 있다.

```cpp
void display(const Student &s) {
    s.print();        // OK — print()이 const 함수이므로
    // s.setScore(100);  // 에러!
}
```

`print()` 함수에 `const`를 붙이지 않았다면, `const &`로 받은 객체에서 `print()`를 호출할 수 없다. 실제로 값을 수정하지 않더라도 컴파일러는 `const`가 없으면 수정할 수도 있다고 판단하기 때문이다. 읽기 전용 함수에는 반드시 `const`를 붙이는 습관을 들이는 것이 좋다.

* * *

## 6\. static 멤버

### 6-1. 은행 예금 계좌의 예

은행 시스템을 생각해보자. 계좌 객체가 여러 개 있는데, "총 계좌 수"나 "전체 이자율"같은 정보는 어디에 저장해야 할까?

```cpp
class Account {
private:
    string owner;
    int balance;

public:
    Account(string name, int money)
        : owner(name), balance(money) {}
};
```

`owner`와 `balance`는 계좌마다 다르다. 하지만 "총 계좌 수"는 특정 계좌에 속하는 정보가 아니라, 모든 계좌가 공유하는 정보다.

### 6-2. 전역변수의 문제점

간단한 해결책은 전역변수를 쓰는 것이다.

```cpp
int totalCount = 0;  // 전역변수

class Account {
public:
    Account(string name, int money) {
        totalCount++;
    }
};
```

동작은 하지만 문제가 있다. `totalCount`는 `Account` 클래스와 논리적으로 관련된 데이터인데, 클래스 바깥에 따로 떨어져 있다. 누구든 `totalCount = -999` 같이 마음대로 바꿀 수 있고, 캡슐화가 깨진다.

### 6-3. 우리가 원하는 것

"모든 객체가 공유하면서도, 클래스 안에 묶여 있는 변수"가 필요하다. 이것이 바로 **static 멤버** 다.

`static` 멤버변수는 **클래스에 속하지만 모든 객체가 공유하는 변수** 다. 객체를 아무리 많이 만들어도 `static` 변수는 메모리에 **하나만** 존재한다.

```cpp
class Account {
private:
    string owner;
    int balance;
    static int totalCount;    // static 멤버변수 선언
    static double interestRate;

public:
    Account(string name, int money)
        : owner(name), balance(money) {
        totalCount++;
    }

    ~Account() {
        totalCount--;
    }

    static int getTotalCount() {  // static 멤버함수
        return totalCount;
    }

    static void setInterestRate(double rate) {
        interestRate = rate;
    }
};

// static 멤버변수는 클래스 외부에서 반드시 초기화해야 함
int Account::totalCount = 0;
double Account::interestRate = 0.05;
```

```cpp
int main() {
    cout << Account::getTotalCount() << endl;  // 0

    Account a1("홍길동", 10000);
    Account a2("김철수", 20000);

    cout << Account::getTotalCount() << endl;  // 2

    Account::setInterestRate(0.03);

    return 0;
}
```

`static` 멤버변수는 클래스 안에 선언하지만, 실제 메모리 할당과 초기화는 **클래스 외부에서** 해야 한다. `int Account::totalCount = 0;` 이 줄이 없으면 링크 에러가 발생한다.

`static` 멤버함수는 **객체 없이 클래스 이름으로 직접 호출** 할 수 있다. `Account::getTotalCount()`처럼 사용한다. `static` 멤버함수 안에서는 `this` 포인터가 없으므로 일반 멤버변수에 접근할 수 없고, `static` 멤버변수만 사용할 수 있다.

C에서 정적변수가 함수 안에서만 접근 가능하면서 값이 유지되었던 것처럼, `static` 멤버변수는 클래스 안에서만 접근 가능하면서(private이면) 모든 객체가 공유한다. 전역변수의 공유 기능과 캡슐화의 보호 기능을 동시에 얻을 수 있다.

* * *

## 7\. 프렌드 (friend)

C++에서 `private` 멤버는 클래스 외부에서 접근할 수 없다. 하지만 특정 함수나 클래스에게만 예외적으로 접근을 허용하고 싶을 때가 있다. 이때 사용하는 것이 `friend`다.

`friend`로 지정된 함수나 클래스는 해당 클래스의 `private` **멤버에 접근할 수 있다.**

```cpp
class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {}

    friend void printInfo(const Student &s);  // friend 함수 선언
};

void printInfo(const Student &s) {
    // Student의 private 멤버에 직접 접근 가능
    cout << s.name << ": " << s.score << "점" << endl;
}
```

`printInfo`는 `Student`의 멤버함수가 아니라 **외부 함수** 다. 그런데 `friend`로 선언되었기 때문에 `private` 멤버인 `name`과 `score`에 직접 접근할 수 있다.

클래스끼리도 `friend`를 선언할 수 있다.

```cpp
class Engine {
private:
    int horsepower;

public:
    Engine(int hp) : horsepower(hp) {}

    friend class Car;  // Car 클래스에게 접근 허용
};

class Car {
public:
    void showEngine(const Engine &e) {
        // Engine의 private 멤버에 접근 가능
        cout << "마력: " << e.horsepower << endl;
    }
};
```

`friend class Car`로 선언하면, `Car` 클래스의 모든 멤버함수가 `Engine`의 `private` 멤버에 접근할 수 있다.

`friend`는 편리하지만 **캡슐화를 약화시킨다** 는 점을 알아야 한다. `private`으로 보호한 데이터를 외부에 열어주는 것이므로, 남용하면 데이터 은닉의 의미가 퇴색된다. `friend`는 두 클래스가 밀접하게 협력해야 하거나, 연산자 오버로딩에서 외부 함수가 `private` 멤버에 접근해야 할 때 등 명확한 이유가 있을 때만 사용하는 것이 좋다.

`friend` 관계의 특징을 정리하면 이렇다. **단방향** 이다 — `A`가 `B`를 `friend`로 선언해도, `B`가 `A`의 `private`에 접근할 수 있는 것이지 그 반대는 아니다. **비전이적** 이다 — `A`가 `B`의 `friend`이고 `B`가 `C`의 `friend`여도 `A`가 `C`의 `friend`가 되지는 않는다. **상속되지 않는다** — 부모가 `friend`라고 자식까지 `friend`가 되지는 않는다.

* * *

## 마무리

이번 글에서 다룬 내용들은 C++에서 객체를 실제로 다루는 실전적인 기법들이다. 객체 배열과 포인터로 여러 객체를 관리하고, `this`로 자기 자신을 참조하고, `const`로 읽기 전용 보장을 하고, `static`으로 클래스 전체가 공유하는 데이터를 관리하고, `friend`로 필요한 곳에만 접근을 열어준다. 이 개념들은 이후 상속, 연산자 오버로딩, 복사 생성자를 배울 때 기반이 된다.
