본문 바로가기

Software Engineering/Computer Science

[C++, C#] C#과 다른 컴퓨터 언어의 차이점들5~7(完)

이번 글의 코드는 기초적인 구현에 시간낭비하지 않기위해  ChatGPT를 활용해 작성된 코드가 많다. 

목   차

     

     

     


    5. 배열

    배열의 선언

    c와 cpp는 선언을 다음과 같이 한다:

    int intArr[5];

     c#과 java는 선언을 다음과 같이 한다:

    int[] intArr = new int[5];

    new int는 cpp에서 동적배열을 할 때 사용했지만, 여기서는 오히려 5 대신 변수를 넣어버리면 C#은 오류가 난다. java는 그대로 이용하면 되지만 c#에서 동적배열을 쓰고 싶다면 cpp에서 vector와 비슷한 기능을 하는 List<T>를 써야한다. 여기서 T는 type을 의미하며, T 대신 int 등 자료형을 입력하면 된다.

     한편, 파이썬은 정적배열과 동적배열의 분리가 딱히 없다.

    my_list = []
    my_list.append(1)
    my_list.append("hello")

     파이썬은 이와 같은 형식으로 사용하면 되며, 1 대신 다른 자료형이 들어가도 상관없다. 심지어 보통의 경우와 달리 서로 다른 자료형이 공존할 수도 있다. 이것은 다른 배열과 달리 파이썬은 자료형의 선언이 필요없기 때문이다.


    6. 함수

    함수의 기본틀

    많은 언어들이 기본 함수 형식을 공유한다. 

    int swap(bool a); //함수의 전방선언
    int b = swap(1); //함수의 활용

    라인 1부터 살펴보면, int 는 반환할 자료형을 뜻한다. 이 자료형이 void가 아닌 모든 함수는 반드시 return문을 필요로 한다. 여기서 return되는 것은 수학에서의 y값과 같다고 할 수 있다. swap은 함수의 이름이며, swap이 아니라 다른 이름이어도 상관없다. () 안에 들어갈 것은 변수의 선언이다. 반드시 변수를 선언할 필요는 없지만, 함수를 활용할 때 특정 값에 따라 다른 결과를 나타나게 하고 싶다면 사용한다. 이것을 매개변수라 부르며, 인수라 하기도 한다. 함수는 보통 파스칼 케이스-대문자로 시작-로 이름을 짓는 것이 일반적이다.

    def add_num(x, y):
    	result = x + y
        return result

    한편, python은 다음과 같이 앞에 def를 붙여 함수를 만든다. 또한 자료형의 제한이 없다.

     

    함수를 처음 설명한 코드블럭에서 '전방선언'이라는 것을 볼 수 있다. C#이나 Java, Python은 이것을 필요로 하지 않지만, C와 C++은 함수를 사용할 때 전방선언이 필요한 경우가 있다. 컴퓨터가 우리가 작성한 스크립트를 읽을 때 만약 맨 위에서부터 아래의 순서로 차근차근 명령을 실행해 나가는 언어가 있고, 일단 다 읽고 main의 명령어를 실행하는 경우가 있다. 전자의 경우가 바로 전방선언이 필요한 언어이다. 하는 법은 위의 예시처럼 함수를 만드는 첫 줄을 그대로 옮겨쓰고 세미콜론(;)을 붙이면 된다. 또 변수명(a)은 생략해도 된다. 


    7. 클래스와 객체

    C는 절차지향 프로그래밍에서 객체지향 프로그래밍으로 넘어가는 과도기적인 언어이며, 클래스 대신 struct밖에 없으므로 생략한다. 또 현 python의 클래스를 사용한 경험이 적기 때문에 이 장에서는 C++, C#, Java에 관해서만 이야기한다.

    클래스 형식

    <C++>

    #include <iostream>
    
    class Circle {
    private:
        double radius;
    
    public:
        // 생성자
        Circle(double r) : radius(r) {}
    
        // 면적 계산 메서드
        double area() {
            return 3.14 * radius * radius;
        }
    };
    
    int main() {
        Circle circle(5.0);
        std::cout << "Circle area: " << circle.area() << std::endl;
        return 0;
    }

    <C#>

    using System;
    
    class Circle {
        private double radius;
    
        // 생성자
        public Circle(double r) {
            radius = r;
        }
    
        // 면적 계산 메서드
        public double Area() {
            return 3.14 * radius * radius;
        }
    }
    
    class Program {
        static void Main(string[] args) {
            Circle circle = new Circle(5.0);
            Console.WriteLine("Circle area: " + circle.Area());
        }
    }

    <JAVA> 

    class Circle {
        private double radius;
    
        // 생성자
        public Circle(double r) {
            radius = r;
        }
    
        // 면적 계산 메서드
        public double area() {
            return 3.14 * radius * radius;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Circle circle = new Circle(5.0);
            System.out.println("Circle area: " + circle.area());
        }
    }

    세 언어의 클래스를 보면 C#과 Java가 상당히 유사한 것을 볼 수 있다. 하지만 눈에 보이지 않는 차이를 설명하자면, 만약 두 언어에서 앞의 private/public등의 접근제어자를 빼버린다면 C#은 자동으로 private 접근 제어자가 지정되는 반면, Java는 default 접근 제어자가 지정이 된다. C++역시 기본 접근 제어자가 private이며, C++에서는 이 제어자를 묶어서 한 번만 쓰면 된다. 또 C++는 헤더(클래스의 선언)와 정의(클래스의 구현)을 분리하는 것이 일반적으로, .h로 끝나는 파일에 따로 클래스의 선언부를 작성한다. 반면 C#과 Java는 주로 한 클래스 당 하나의 파일이 되게 구현한다. 즉, 우리가 콘솔앱으로 프로그래밍을 하는데 새로운 클래스가 필요하다면, 새로운 .cs 파일을 하나 새로 만들어서 그곳에 클래스를 만들면 된다.

     

    참고로 접근제어자는 "캡슐화"를 위해 필요한데, 쉽게 말하자면 같은 반 친구는 우리 반 변수나 함수를 쓸 수 있지만 옆 반 애는 못건들게 하는 용도다.

    생성자와 소멸자

    class에 대한 설명이 거의 없다시피 해서 추가한다. 생성자는 객체의 "초기화"를 해주는 함수이고, 소멸자는 말 그대로 객체에서 없어져야만 하는 요소를 "소멸"시켜주는 함수이다. 소멸자는 평시에는 필요없으나, 클래스의 멤버로 동적 배열 등이 있을 때 반드시 필요하다. 하지만! 우리의 C#은 소멸자가 필요없다. 동적 배열을 쓰면 다시 그 공간을 돌려주는 명령어가 원래는 필요하지만, c#은 메모리 관리를 알아서 해주기 때문이다. 따라서 생성자만 보여주겠다.

    //1. 생성자
    public Circle(double r) {
            radius = r;
        }
        
    //2. main에서의 객체 선언
    Circle circle = new Circle(5.0);

     위의 코드에서 필요한 부분만 떼어왔다. 1이 생성자의 선언 및 정의이며, 맴버 변수인 radius에 값을 2의 Circle에서 입력받은 r값, 즉 5.0으로 초기화한다. 이에 따라 후에 클래스 맴버를 사용할 때 잘못된 값을 불러오는 것을 막아준다.

     

    만약 생성자를 만들어주지 않는다면, 똑똑한 컴파일러가 알아서 생성자를 만든다. 생성자는 반환 자료형이 없어야 하며(심지어 void 조차도!) 그에 따라 return 함수를 사용할수도 없다. 또 처음 객체를 선언하는 상황 외에는 써서도 안된다. 만약 위의 예에서 radius 값을 바꾸고 싶다면, 생성자를 불러오는게 아니라 다른 함수를 따로 만들어서 radius에 접근하거나 radius를 public으로 선언해서 직접 접근해야 한다는 뜻이다.

    추상 클래스와 인터페이스

    노션에 설명이 되어있지는 않지만 추상클래스 라는것도 존재한다. 가상클래스는 접근제어자 뒤에 abstract가 붙는다. 간단하게 예시를 보여주겠다.

    using System;
    
    // 가상 클래스 Animal 정의
    public abstract class Animal
    {
        // 추상 메서드 Speak를 선언
        public abstract void Speak();
    }
    
    // Animal 클래스를 상속하는 구체적인 하위 클래스 Dog 정의
    public class Dog : Animal
    {
        // Speak 메서드를 재정의하여 구현
        public override void Speak()
        {
            Console.WriteLine("Woof!"); // 개가 짖는 소리 출력
        }
    }
    
    // Animal 클래스를 상속하는 구체적인 하위 클래스 Cat 정의
    public class Cat : Animal
    {
        // Speak 메서드를 재정의하여 구현
        public override void Speak()
        {
            Console.WriteLine("Meow!"); // 고양이가 울어 대는 소리 출력
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // Dog 클래스의 인스턴스 생성
            Dog dog = new Dog();
            // Dog 클래스의 Speak 메서드 호출
            dog.Speak(); // 출력: "Woof!"
    
            // Cat 클래스의 인스턴스 생성
            Cat cat = new Cat();
            // Cat 클래스의 Speak 메서드 호출
            cat.Speak(); // 출력: "Meow!"
        }
    }

     이처럼 가상클래스는 일종의 "서식" 역할이다. 예시로 나온 하위 클래스 Dog와 Cat에서 처음 Animal 클래스에 선언된 요소들 중 하나라도 빠지면 컴파일 오류가 난다. 가상클래스는 자세한 구현 없이 선언만 한다.

     

    또 override 라는 것을 볼 수 있을텐데, 이것은 이미 존재하는 함수(메서드)를 재정의하는 데에 사용된다.

     

    이건 인터페이스다.

    public interface IShape
    {
        // 면적을 계산하는 메서드
        double CalculateArea();
    }

     이것도 일반 클래스가 계승을 받아 사용하게 된다. 하지만, 변수의 선언은 불가능하고 오직 함수만 선언할 수 있다. 또 추상클래스처럼 계승을 받으면 반드시 보유한 모든 함수를 사용해야 한다. 이때 override 키워드는 필요없다.

     

    가장 큰 차이점은 인터페이스가 추상클래스와 달리 다중상속이 가능하다는 것이다. 즉, 인터페이스A와 인터페이스B를 모두 계승하는 클래스C가 존재할 수 있다는 것이다.

     

    비유하자면 추상클래스는 '카테고리'가 되며, 이 아래의 모든 항목은 이 카테고리와 특성을 공유한다. 하지만 '기계'에 속하는 자동차가 '생물'이 동시에 될 수는 없다.

    인터페이스는 '기능'이 되며, 하나의 개체일지라도 여러 기능을 공유할 수 있다. 스마트폰은 '음악재생기'인 동시에 '전화기' 일 수 있는 것이다.

     

    상속 클래스

    // Dog 클래스를 상속하는 구체적인 하위 클래스 Puppy 정의
    public class Puppy : Dog
    {
        // Puppy 클래스는 Dog 클래스를 상속하므로, Dog 클래스에서 정의된 메서드를 모두 상속받음
        // 따라서 Dog 클래스에서 정의된 Speak와 Move 메서드를 오버라이드할 필요가 없음
    }​

    다음 코드는 위의 코드에서 이어지는 상속클래스이다. 상속은 오직 하나의 클래스로부터만 받을 수 있다. 상속하는 클래스 Dog를 부모라 부르고 받는 클래스 Puppy를 자식이라 부른다면, 하나의 부모 아래 여러 자식은 있을 수 있어도 모든 자식은 반드시 하나의 부모만 둘 수 있다는 것이다.

     

    한편, C++는 다중상속을 지원한다. Dog 클래스의 특징과 Cat 클래스의 특징을 모두 지닌 Chimera 클래스가 있다고 쳐보자. 그럼 Speak 함수를 작동시켰을 때 뭘 출력할까? 답은 "컴파일 오류" 다.

     


    보너스.

    포인터

    보통 개발 처음 배우는 사람이 제일 어려워하는 부분이다. 대충 데이터를 가리키는 변수, 즉 화살표 그 자체라 이해하면 된다. 조금 더 깊게 가면 포인터의 포인터 뭐 그런 것도 있다... 근데 C#은 보통 안쓴다네?? 허허. 어쩐지 배열 선언 생긴 꼬라지도 다르더라.

    예외 처리

    암기할 것 천지라 내가 제일 싫어하는 부분이다. 보통 try~catch 구문을 이용하며, 이걸 제대로 쓰려면 오류 이름을 알아야 한다... 

    try
            {
                // 0으로 나누는 예외 발생
                int result = DivideByZero(10, 0);
                Console.WriteLine("Result: " + result); // 이 줄은 실행되지 않음
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine("Error: " + ex.Message); // 예외 메시지 출력
            }

     여기 보면 "divideByZeroException" 이 그 오류 이름의 예시다. 어떤 오류이냐에 따라 이름이 당연히 다 다르며, 평상시에는 구글링하면 끝이지만 시험을 볼때는...

    Static-정적 변수/함수

    정적 변수는 함수 밖에 선언되는 변수이다. 그에 따라 여러 함수에서 접근해도 그 값이 항상 유지된다. 정적 함수는 객체의 선언 없이도 사용될 수 있는 함수다. 예시는 다음과 같다:

    using System;
    
    public class Calculator
    {
        // 정적 변수로 시작값을 유지합니다.
        public static int value = 10;
    
        // 숫자를 더하는 정적 메서드
        public static void Plus(int number)
        {
            value += number;
        }
    
        // 숫자를 빼는 정적 메서드
        public static void Minus(int number)
        {
            value -= number;
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // 출력: Initial value: 10
            Console.WriteLine("Initial value: " + Calculator.value); 
            
    	// 출력: Value after adding 7: 17
            Calculator.Plus(7);
            Console.WriteLine("Value after adding 7: " + Calculator.value); 
    
            // 출력: Value after subtracting 4: 13
            Calculator.Minus(4);
            Console.WriteLine("Value after subtracting 4: " + Calculator.value); 
        }
    }

    참고로 메서드=함수 라고 생각하면 된다. (변수랑 함수를 뜻하는 이름이 엄청 많다. 오죽하면 다음 중 변수를 칭하는 말이 아닌 것은? 이란 객관식 문제가 있을 정도ㄷㄷ) 물론 위의 경우에는 Main 함수 안에 변수를 선언해도 되는 일이지만, 종종 Main이 아닌 다른 곳에서도 변수에 접근해야 할 필요가 있기 때문에 정적 변수를 쓴다.

     

    정적함수는 비슷한 계열의 여러 함수를 묶는 데에 쓰이며, 일반적인 클래스보단 위의 경우처럼 "계산"이라는 카테고리에 들어가는 두 함수를 하나의 클래스로 묶은 후 쓰는 것과 비슷한 모양으로 쓰인다.

    제네릭 프로그래밍

    일전에 <T>를 사용한 걸 본 적이 있나? 바로 배열에서 사용했던 것인데, 이 T 자리에 원하는 자료형을 넣으면 된다고 했다. 클래스를 선언할 때 클래스 이름 뒤에 <T>를 붙이면 필요한 자료형을 그때그때 대입해서 쓸 수 있다! 언어에서 미리 제공하는 템플릿(C++의 Vector, C#의 List)도 있지만, 원할 때 만들어 써도 된다.


    마무리

    일반적으로 프로그래밍을 공부할 때 봤던 목차를 기준으로 보너스 장을 추가했다. 제일 충격적인건 역시 C#에... 포인터가 없다니... 해킹에 취약한건 들었지만..