Skip to main content

Command Palette

Search for a command to run...

C언어 (7)

Updated
6 min read

1. 구조적 프로그래밍

구조적 프로그래밍(Structured Programming)은 프로그램을 함수 단위로 분해 하여 구성하는 방식이다. C언어가 대표적인 구조적 프로그래밍 언어다.

기본 단위는 함수 다. 프로그램의 전체 흐름을 작은 함수들로 쪼개고, 각 함수가 하나의 작업을 담당한다. main에서 시작해서 필요한 함수를 호출하고, 그 함수가 또 다른 함수를 호출하는 식으로 프로그램이 진행된다.

// 구조적 프로그래밍 방식
void inputData(int *arr, int size);
void sortData(int *arr, int size);
void printData(int *arr, int size);

int main(void) {
    int arr[10];
    inputData(arr, 10);
    sortData(arr, 10);
    printData(arr, 10);
    return 0;
}

이 방식은 직관적이고 이해하기 쉽다. 하지만 프로그램이 커지면 문제가 생긴다.

데이터와 함수가 분리되어 있다. 데이터는 변수에, 로직은 함수에 따로 존재한다. arr라는 데이터를 다루는 함수가 여러 개인데, 이 함수들이 arr와 논리적으로 묶여 있다는 것이 코드 구조에 드러나지 않는다.

전역변수 의존 이 심해지기 쉽다. 여러 함수가 같은 데이터를 써야 할 때 전역변수로 공유하게 되고, 프로그램이 커질수록 누가 어떤 데이터를 수정했는지 추적하기 어려워진다.

코드 재사용이 어렵다. 비슷한 기능을 하는 프로그램을 만들 때 함수를 가져다 쓸 수는 있지만, 데이터 구조가 달라지면 함수도 전부 수정해야 한다.

이런 한계를 극복하기 위해 등장한 것이 객체지향 프로그래밍 이다.


2. 객체지향 프로그래밍

객체지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 객체(Object) 단위로 구성하는 방식이다. 객체는 데이터(속성)와 그 데이터를 다루는 함수(동작)를 하나로 묶은 것 이다.

C에서는 구조체에 데이터만 넣을 수 있었다. C++에서는 구조체(그리고 클래스)에 함수까지 함께 넣을 수 있다. 데이터와 로직이 하나의 단위로 묶이는 것이다.

// C 방식 — 데이터와 함수가 분리
struct Student {
    char name[20];
    int age;
};
void printStudent(struct Student *s);

// C++ 방식 — 데이터와 함수가 하나로 묶임
class Student {
    string name;
    int age;
public:
    void print() {
        cout << name << ", " << age << endl;
    }
};

객체지향의 핵심 개념은 추상화, 데이터 은닉(캡슐화), 다형성, 그리고 상속 이다.

2-1. 추상화

추상화(Abstraction)는 복잡한 내부 구현을 숨기고, 핵심적인 기능만 외부에 드러내는 것 이다.

자동차를 운전할 때 엔진 내부의 폭발 과정을 알 필요 없다. 핸들, 페달, 기어라는 인터페이스 만 알면 운전할 수 있다. 프로그래밍에서도 마찬가지다. 객체를 사용하는 쪽에서는 "무엇을 할 수 있는지"만 알면 되고, "어떻게 하는지"는 몰라도 된다.

class Calculator {
public:
    int add(int a, int b) { return a + b; }
    int subtract(int a, int b) { return a - b; }
};

int main() {
    Calculator calc;
    cout << calc.add(3, 5) << endl;  // 내부 구현을 몰라도 사용 가능
    return 0;
}

사용자는 add가 내부적으로 어떻게 동작하는지 신경 쓸 필요 없다. "두 수를 넘기면 합을 돌려준다"는 것만 알면 된다. 이것이 추상화다.

클래스를 설계할 때는 "이 클래스를 사용하는 사람이 알아야 할 것은 무엇인가?"를 기준으로 public 인터페이스를 결정하고, 나머지 세부 구현은 감추는 것이 좋다.

2-2. 데이터 은닉

데이터 은닉(Data Hiding)은 객체 내부의 데이터에 외부에서 직접 접근하지 못하게 막는 것 이다.

C의 구조체는 모든 멤버가 외부에 공개되어 있다. 누구든 s.age = -5 처럼 비정상적인 값을 넣을 수 있고, 막을 방법이 없다.

C++에서는 접근 제어 지시자 로 이 문제를 해결한다. private으로 선언한 멤버는 클래스 외부에서 접근할 수 없다. public으로 선언한 멤버만 외부에서 접근할 수 있다. protected는 상속 관계에서 자식 클래스까지만 접근을 허용한다.

class Student {
private:
    string name;
    int age;

public:
    void setAge(int a) {
        if (a >= 0 && a <= 150) {
            age = a;
        } else {
            cout << "유효하지 않은 나이입니다." << endl;
        }
    }

    int getAge() {
        return age;
    }
};

age에 직접 접근하는 대신 setAge를 통해 값을 설정한다. 이 함수 안에서 유효성 검사를 할 수 있으므로, 잘못된 값이 들어가는 것을 방지할 수 있다.

캡슐화

캡슐화(Encapsulation)는 데이터 은닉과 밀접한 개념으로, 데이터와 그 데이터를 조작하는 함수를 하나의 캡슐(클래스)로 묶는 것 이다.

데이터 은닉이 "외부 접근을 차단한다"는 방어적인 측면이라면, 캡슐화는 "관련된 것들을 하나로 묶는다"는 구조적인 측면이다. 둘은 함께 작동한다.

class BankAccount {
private:
    string owner;
    int balance;

public:
    BankAccount(string name, int initial)
        : owner(name), balance(initial) {}

    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    void withdraw(int amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
        }
    }

    int getBalance() {
        return balance;
    }
};

balance를 직접 건드릴 수 없고, 반드시 deposit이나 withdraw를 통해서만 변경할 수 있다. 입출금 로직과 잔액 데이터가 하나의 클래스 안에 캡슐화되어 있으므로, 잔액이 음수가 되는 등의 비정상적인 상태를 방지할 수 있다.

JavaScript에서는 클로저나 # 프라이빗 필드로 비슷한 효과를 낸다. C++에서는 private 키워드로 언어 차원에서 명확하게 지원하는 것이다.

2-3. 다형성

다형성(Polymorphism)은 같은 이름의 함수가 상황에 따라 다르게 동작하는 것 이다. "poly(여러)" + "morph(형태)"라는 뜻 그대로, 하나의 인터페이스가 여러 형태를 가질 수 있다.

C++에서 다형성은 크게 오버로딩오버라이딩 으로 나뉜다.

오버로딩 (Overloading)

오버로딩은 같은 이름의 함수를 매개변수의 타입이나 개수를 다르게 하여 여러 개 정의하는 것 이다. 컴파일 시점에 어떤 함수를 호출할지 결정되므로 컴파일 타임 다형성 이라고도 한다.

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

int main() {
    cout << add(3, 5) << endl;          // int 버전 호출
    cout << add(3.14, 2.72) << endl;    // double 버전 호출
    cout << add(1, 2, 3) << endl;       // 3개짜리 버전 호출
    return 0;
}

C에서는 이것이 불가능했다. 함수 이름이 같으면 컴파일 에러가 난다. add_int, add_double, add_three 처럼 이름을 전부 다르게 만들어야 했다. C++에서는 컴파일러가 인자의 타입과 개수를 보고 어떤 함수를 호출할지 알아서 결정해준다.

주의할 점은 반환 타입만 다른 것으로는 오버로딩이 되지 않는다 는 것이다. 컴파일러는 호출 시점에 반환값을 보고 함수를 구분할 수 없기 때문이다.

int getValue();
double getValue();  // 에러! 매개변수가 같으면 반환 타입만으로 구분 불가

오버라이딩 (Overriding)

오버라이딩은 부모 클래스의 함수를 자식 클래스에서 재정의하는 것 이다. 상속 관계에서 동작하며, 런타임에 어떤 함수를 호출할지 결정되므로 런타임 다형성 이라고도 한다.

class Animal {
public:
    virtual void speak() {
        cout << "..." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout << "멍멍!" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout << "야옹!" << endl;
    }
};

int main() {
    Animal *animals[3];
    animals[0] = new Animal();
    animals[1] = new Dog();
    animals[2] = new Cat();

    for (int i = 0; i < 3; i++) {
        animals[i]->speak();
    }
    // 출력:
    // ...
    // 멍멍!
    // 야옹!

    for (int i = 0; i < 3; i++) {
        delete animals[i];
    }
    return 0;
}

animals 배열은 전부 Animal * 타입이지만, 실제로 가리키는 객체가 Dog인지 Cat인지에 따라 다른 speak 함수가 호출 된다. 이것이 런타임 다형성이다.

여기서 핵심은 부모 클래스의 함수에 virtual 키워드를 붙이는 것이다. virtual이 없으면 포인터의 타입(Animal *)에 따라 부모의 speak 가 호출된다. virtual이 있으면 실제 객체의 타입에 따라 적절한 speak 가 호출된다.

자식 클래스에서 override 키워드를 붙이는 것은 필수는 아니지만, "이 함수는 부모의 함수를 재정의한 것이다"라는 의도를 명확히 하고, 오타 등으로 인한 실수를 컴파일 타임에 잡아주므로 붙이는 것이 좋다.

오버로딩과 오버라이딩을 헷갈리기 쉬운데, 핵심 차이를 정리하면 이렇다.

오버로딩 은 같은 이름, 다른 매개변수로 함수를 여러 개 만드는 것이다. 같은 클래스 안에서 일어나고, 컴파일 타임에 결정된다. 오버라이딩 은 부모의 함수를 자식이 재정의하는 것이다. 상속 관계에서 일어나고, 런타임에 결정된다.


마무리

구조적 프로그래밍에서 객체지향 프로그래밍으로의 전환은 "함수 중심"에서 "객체 중심"으로의 사고 전환이다. 추상화로 복잡성을 숨기고, 캡슐화로 데이터를 보호하고, 다형성으로 유연한 코드를 작성한다. C의 구조체와 함수 포인터로 흉내냈던 것을 C++에서는 언어 차원에서 깔끔하게 지원하는 것이다. 이 개념들은 이후 상속, 가상 함수, 연산자 오버로딩을 배우면서 더 구체적으로 활용하게 된다.

More from this blog

C언어 (13)

1. STL의 개념 1-1. 배경 C++로 프로그래밍을 하다 보면 동적 배열, 연결 리스트, 정렬, 검색 같은 자료구조와 알고리즘을 반복적으로 구현하게 된다. 프로젝트마다 매번 새로 만들면 시간도 낭비되고, 버그가 생길 가능성도 높아진다. 이런 문제를 해결하기 위해 자주 사용되는 자료구조와 알고리즘을 미리 만들어서 표준 라이브러리에 포함 시킨 것이 STL이

Apr 1, 202610 min read3

chamdom's tech

16 posts