상속(Inheritance)


클래스라는 것을 배우면서 Object 클래스에 대해서 잠깐 소개한 적이 있었습니다. 그 이야기를 하면서 상속에 대해 언급한 적이 있었습니다. 자바에서는 이 상속의 사전적인 뜻을 그대로 반영합니다.


inheritance

1. 상속받은 재산, 유산; 상속   2. (과거·선대로부터 물려받는) 유산, 유전(되는 것)


내가 500억을 상속받았으면 이 포스팅을 하지 않았다.


사전을 보니 물려받는 것으로 간단하게 정의할 수 있겠군요.

우리는 이제부토 상속이란 클래스 관점으로 보면 됩니다. 클래스에는 변수와 메소드가 있죠. 클래스 입장에서는 이것이 재산이라고 생각하면 됩니다. 그래서 자식이 되는 클래스는 이런 재산을 코드의 추가없이 그대로 사용하거나, 변경해서 사용할 수 있습니다.




여기서 왜 우리는 상속을 해야하는 지 알 수 있습니다. 바로 코드의 재사용성을 더욱 확대하는 것이지요. 그렇다면 그냥 복사, 붙여넣기를 하면 되는 것이 아닌가? 라고 생각할 수도 있지만, 코드의 변경 역시 신경써야합니다. 그리고 관리도 복잡하게 되구요.


가장 중요한 것은 상속을 통해서 다형성을 만족시킬 수 있다는 점입니다. 이 부분에 대해서는 이 다음에 알아보도록 하지요.


extends 

상속을 하는 방법 아래와 같습니다.


class 자식클래스 extends 부모클래스


어떤 클래스를 정의함과 동시에 extends 예약어로 다른 클래스를 포함하게 된다면 extends 이후에 나오는 클래스를 상속하게 되는 것입니다.


어떤 클래스는 자동적으로 Object클래스를 상속했다고 했는데, 그럼 이런 형태일까요?


class 자식클래스 extends 부모클래스 extends Object


아닙니다. 자바에서는 클래스의 다중상속(여러 부모클래스를 두는 것)을 허용하지 않습니다. 이런 경우에는 이미 부모클래스가 Object클래스를 이미 상속받고 있기 때문에 자식클래스는 부모클래스를 상속받음으로써 Object클래스의 메소드를 사용할 수 있는 것입니다.


이렇게 되는 것이죠.


class 부모클래스 extends Object { ... }

class 자식클래스 extends 부모클래스 { ... }





코드를 통해 알아보자.

이쯤되면 코드를 볼 타이밍이겠군요.



class Parent{
	int age=45;
	String name="Parent";
	public Parent(){
		System.out.println("Parent Default Constructor");
	}
	public Parent(int _age,String _name){
		age=_age;
		name=_name;
		System.out.println("Parent Constructor");
	}
	public void showInfo(){
		System.out.println("Name:"+name+", age:"+age);
	}
}

class Child extends Parent{
	public Child(int _age, String _name){
		System.out.println("Child Constructor");
	}
}
public class Main {

	public static void main(String[] args){
		Child a=new Child(25,"REAKWON");
		a.showInfo();
	}
}


우선 Child 클래스는 Parent 클래스를 상속받고 있군요. 


코드에 대해서 간단하게 요약하면 Parent 클래스는 아래와 같은 특징이 있군요.


Default 생성자를 갖고 있다.

매개변수를 갖는 생성자도 있다.

showInfo라는 메소드가 있다.


Child클래스는 그저 매개변수를 받는 생성자가 다에요.


main메소드에서는 Child 객체를 초기화값을 넣어 생성자를 호출합니다. 어떤 결과가 나올까요? 결과를 봅시다.


Parent Default Constructor

Child Constructor

Name:Parent, age:45


어? 부모클래스의 생성자를 우선 호출하는 군요. Default 생성자를 호출하네요.

아아, 우리가 아무 클래스를 상속하게 된다면 부모클래스의 디폴트 생성자를 우선 호출하는 군요.


그리고 우리는 Child클래스에서 showInfo라는 메소드를 만들지 않았음에도 불구하고 main함수에서는 사용하고 있습니다. Parent 클래스를 상속 했기 때문이죠.


근데 좀 부족해요. 뭔가 2%만족이 되지 않는군요.


Child a=new Child(25,"REAKWON");


우리는 생성자를 통해서 age와 name을 바꾸고 싶다는 거죠. 어떻게 해야될까요?




super, this

우리는 위의 코드를 조금 변경해서 age와 name 변수의 데이터를 바꿀 수 있습니다. 바로 부모클래스의 데이터를 직접 변경하는 것이죠.


class Child extends Parent{
	public Child(int _age, String _name){
                age=_age;
                name=_name;
		System.out.println("Child Constructor");
	}
}

우리는 이미 Parent클래스를 상속받은 상태입니다. 그렇기 때문에 Parent클래스의 변수를 접근할 수도 있습니다(물론 부모클래스가 허용한다면).


그런데 우리는 이미 정의된 부모 클래스의 생성자를 다시 활용해보고 싶습니다. 


자식클래스에서 부모클래스를 super클래스라고 부릅니다. 그렇기 때문에 부모클래스의 변수나 메소드, 생성자에 접근할땐 super라는 키워드를 사용합니다.


class Child extends Parent{
	public Child(int _age, String _name){
                super(_age,_name);
		System.out.println("Child Constructor");
	}
}

이런식으로 부모클래스의 생성자(super( args))를 호출함으로써 구현해낼 수도 있습니다.

super. 을 눌러 어떤 변수와 메소드들이 있는지 확인해보세요. 부모클래스가 허락한다면 super클래스의 변수와 메소드를 직접 사용할 수도 있습니다.


이제 코드를 조금 더 변형시켜보도록 해서 실험해 봅시다.


class Child extends Parent{
	public Child(int age, String name){
                age=age;
                name=name;
		System.out.println("Child Constructor");
	}
}


이제 매개변수와 클래스 멤버 변수의 이름이 같습니다. 이런 경우에는 어떤 일이 벌어질까요?


결론은 클래스의 멤버 변수는 변하지 않습니다. 메소드가 호출할때는 매개변수나 그 안의 지역변수가 항상 우선순위를 갖기 때문입니다.


클래스에서 자기 자신을 가리킬 때는 this라는 키워드를 사용합니다. 그렇기 때문에 이런 코딩을 해야합니다.

class Child extends Parent{
	public Child(int age, String name){
                this.age=age;
                this.name=name;
		System.out.println("Child Constructor");
	}
}


this.age는 자기 자신의 변수 age를 명시적으로 나타냅니다. 어? 우리는 Child에는 age와 name이라는 변수가 없는데요?


아까 말했다시피 상속을 받았기 때문에 슈퍼클래스의 변수도 이제 Child 클래스의 것입니다. 그래서 super.age이건 this.age이건 age접근하는 것은 결론적으로 같습니다.




생성자는 어떨까요?

생성자 역시 다르지 않습니다.

this(args)를 통해서 자기 자신의 생성자를 호출 할 수 있습니다.


class Child extends Parent{
        public Child(){
                System.out.println("Child Default Constructor");
        }
	public Child(int age, String name){
                this();
                this.age=age;
                this.name=name;
		System.out.println("Child Constructor");
	}
}


this()를 보세요. 자신의 Default 생성자를 호출하고 있습니다.


다만, 한가지 주의해야할 점은 자기 자신의 생성자나 슈퍼클래스의 생성자를 다른 생성자 메소드에서 호출해야 될 때에는 호출할 생성자 메소드의 최상위에 코딩을 해야합니다. 


overriding

우리는 슈퍼클래스의 메소드를 우리 나름대로 다시 정의해보고 싶습니다. 그것도 역시 가능합니다.

우리는 부모가 정의한 메소드를 우리 입맛에 맞게 바꾸는 것을 Overriding이라고 합니다.




코드와 함께 보도록 합시다.


class Parent{
	int age=45;
	String name="Parent";
	public Parent(){
		System.out.println("Parent Default Constructor");
	}
	public Parent(int _age,String _name){
		age=_age;
		name=_name;
		System.out.println("Parent Constructor");
	}
	public void showInfo(){
		System.out.println("Name:"+name+", age:"+age);
	}
}

class Child extends Parent{
	public Child(){}
	public Child(int _age, String _name){
		super(_age,_name);
		System.out.println("Child Constructor");
	}
	@Override
	public void showInfo(){
		System.out.println("Child showInfo");
		super.showInfo();
	}
}
public class Main {

	public static void main(String[] args){
		Child a=new Child(25,"REAKWON");
		a.showInfo();
	}
}

Child클래스의 showInfo()를 보세요. 부모 클래스의 메소드를 오버라이딩한다는 것을 알리기 위해 @Override annotation으로 명시하고 있습니다. @Overriding이라는 annotation을 써주지 않아도 오류는 없지만, 우리는 컴파일러에게 부모클래스의 메소드를 오버라이딩한다는 것을 알려주어 원치않는 코딩(오타)을 막을 수 있습니다.


또한 Child클래스의 showInfo()는 super클래스의 showInfo() 역시 호출하며 코드를 줄이고 있습니다.


그 결과입니다.


Parent Constructor

Child Constructor

Child showInfo

Name:REAKWON, age:25



여기까지 상속에 대해 주저리 주저리 떠들어봤습니다.

아 힘들었다...

반응형
블로그 이미지

REAKWON

와나진짜

,

클래스(Class)


객체지향언어에서는 현실세계를 반영하기 위해 객체(Object)라는 개념을 도입하게 됩니다. 현실세계에서 보는 사람들, 자동차, TV 등이 객체지향언어에서는 객체로 표현이 됩니다. 


클래스란 객체를 생성하기 위해 그 객체가 어떤 데이터를 갖고 어떤 연산을 하는지에 대해 정의합니다.




클래스 정의

사람(Human)이라는 클래스가 있다고 칩시다. 사람은 눈, 코, 입이 있고, 손과 다리가 있겠죠. 이런 것이 클래스에서는 데이터입니다. 그리고 눈으로는 사물을 보고, 코로는 냄새를 맏고, 입으로는 말을 하거나 음식을 먹겠죠. 이것이 클래스 관점으로 보면 연산입니다.


간단히 이야기하면 멤버 변수와 멤버 메소드라고 기억하시기 바랍니다. 


코드로 정의를 해본다면 이렇게 될겁니다.


class Human{ String eyes="눈"; String ears="귀"; String nose="코"; String mouth="입"; void useEyes(){ System.out.println(eyes+"으로 봄"); } void useEars(){ System.out.println(ears+"로 소리를 들음"); } void useNose(){ System.out.println(nose+"로 냄새를 맡음"); } void useMouth(){ System.out.println(mouth+"으로 욕을 함"); } }


class라는 키워드로 Human이라는 클래스를 정의하고 있습니다. 클래스 안에는 데이터가 있고, 이 데이터들을 활용하여 행동을 정의하고 있습니다. 하지만 이렇게 클래스를 정의했다고 해서 바로 써먹을 수 있는 것은 아닙니다.




객체생성

자바에서는 모든 데이터를 객체로 취급합니다. 클래스를 정의한 이후에 클래스를 통해서 객체를 생성해야합니다. 객체를 생성하는 방법은 new라는 키워드로 메모리에 할당을 해야하는데요. 간단합니다.


Human human=new Human();


클래스를 통해서 메모리에 생성을 했습니다. 이제 human이라는 객체!가 생성이 된것이죠. 

C++을 배웠다면 new라는 키워드가 익숙할 겁니다. 네, C++에서의 new와 거의 동일한 역할을 합니다. 하지만 우리는 객체를 쓰고 닫을때 C++에서와 같이 메모리를 해제하지 않아도 됩니다. 왜냐하면 Garbage Collector가 알아서 메모리를 해제해 주거든요. C++에 대해 배우지 않았어도 상관없습니다. 


우리는 앞서 클래스를 정의할때 메소드 들을 정의했었습니다. 그렇기 때문에 이 메소드들을 사용할 수 있는 것입니다. 물론 그 객체의 변수들도 바꾸어 줄 수 있습니다. 


아래처럼요.


public class Main { public static void main(String[] args){ Human human=new Human(); human.eyes="쌍꺼풀 수술한 눈"; human.useEyes(); human.useEars(); human.useNose(); } }


물론 데이터가 바뀌었으니, 메소드의 연산도 바뀔 수 있습니다.




생성자

우리는 new라는 키워드를 통해서 객체를 생성할 때 


Human human=new Human();


라는 표현으로 객체를 생성했습니다. 하지만 눈 여겨 보면 괄호가 있는 것을 확인 할 수 있지요.

분명 괄호를 쓰는 이유가 있을 겁니다. 메소드처럼 그 안에 데이터를 매개변수로 전달하거나 하는 이유가 있지 않을까요?


우리는 클래스로부터 객체를 생성할때 초기 데이터를 전달해줄 수 있습니다. 그것이 바로 생성자라고 합니다.


생성자는 객체가 생성될때 가장 처음 호출하는 메소드라고 보면 됩니다. 이 생성자는 꼭 호출이 됩니다. 그리고 객체가 생성되지요. 하지만 우리는 생성자를 선언하지 않았습니다. 하지만 객체를 만들 수는 있었죠.


왜 일까요? 우리가 명시적으로 생성자를 선언하지 않아도 자동으로 default 생성자를 알아서 자바가 정의해줍니다. 물론 아무 기능을 하지 않는 생성자로요.


생성자의 정의는 일반 메소드를 정의하는 것과 같지만


1. 반환값이 없습니다.

2. 생성자의 이름은 클래스의 이름과 정확히 같아야합니다.

3. 생성자는 매개변수에 따라 여러개가 정의 될 수 있습니다.

4. public이라는 접근제어자여야 합니다.


위의 Human은 아무 매개변수가 없는 Default 생성자가 자동으로 호출이 된 것입니다. 




아래 코드는 생성자를 통해서 객체를 초기화 시키는 방법을 보여줍니다. 

class Human{
	String eyes="눈";
	String ears="귀";
	String nose="코";
	String mouth="입";
	public Human(){ 
		//default 생성자
	}
	public Human(String _eyes, String _ears, String _nose, String _mouth){
		eyes=_eyes;
		ears=_ears;
		nose=_nose;
		mouth=_mouth;
	}
	public void useEyes(){
		System.out.println(eyes+"으로 봄");
	}
	public void useEars(){
		System.out.println(ears+"로 소리를 들음");
	}
	public void useNose(){
		System.out.println(nose+"로 냄새를 맡음");
	}
	public void useMouth(){
		System.out.println(mouth+"로 욕을 함");
	}
}


public class Main {

	public static void main(String[] args){
		Human human=new Human("라식한 눈","귀","코털 삐져나온 코","험악한 입");
		human.useEars();
		human.useEyes();
		human.useMouth();
		human.useNose();
	}
}


위에서 생성자를 정의한 규칙대로 생성자를 정의해주었습니다. 그리고 default 생성자를 명시적으로 정의해주었습니다. 


그래서 생성자가 2개인 것이군요.


객체를 생성할때 2번째있는 생성자로 객체를 초기화시켰습니다. 그렇다면 초기화된 데이터로 객체가 생성됐을까요?

결과를 보고 확인해보죠.

 


제가 설명한 대로 군요.

생성자 어렵지 않지요?


Object 클래스

우리는 단지 위 4개의 메소드만을 정의했습니다. 하지만 우리가 정의하지 않은 메소드들도 리스트에 나옵니다. 우리는 분명이 아래와 같은 hashCode, notify, wait 과 같은 메소드들은 정의하지 않았습니다. 이것들은 무엇일까요? 우리는 이 정체를 밝혀내기 위해서 우선 Object 클래스를 알아야합니다.



사실 위의 메소드들은 Object클래스의 메소드들입니다. 위의 메소드들이 Object클래스에 그대로 존재하고 있다는 것을 확인하고 있지요.





허나 왜 Object클래스의 메소드가 왜 내가 만든 클래스에 딸려 나오는 것이냐?

모든 클래스는 Object클래스를 자동으로 상속받게 됩니다.




상속(Inheritance)?

아직 이야기하지는 않았습니다. 간단히 말해서 어떤 클래스의 변수와 메소드들을 물려받는다고 지금은 그렇게 이해하시기 바랍니다. 상속을 쓰게 되면 코드의 재활용이나 유지보수가 더욱 더 쉬워집니다. 상속을 사용하는 방법은 클래스를 정의하기 전 중괄호( { )에서 extends 클래스명 을 통해 이루어 집니다.


그렇기 때문에 extends Object를 명시해주지 않아도, 우리는 Object클래스의 메소드를 사용할 수 있는 겁니다.


모든 클래스입니다. 모든 클래스는 Object 클래스를 상속받는다는 것을 기억하세요.


아직은 Object 클래스의 메소드들을 일일이 설명하는 것은 좀 오바인것 같습니다.

포스팅을 하면서 중간중간에 설명하는 것으로 하거나, 정 궁금하다면 자바 Doc를 보아주세요.



반응형
블로그 이미지

REAKWON

와나진짜

,

이름공간(namespace)


C언어에서 한 단계 더 발전한 언어, 바로 C++이 C언어와 차이점을 두고 있는 것은 무엇일까요?


우리는 그 차이점 중에서 한가지를 이야기해보려 합니다.

우선 바로 코드를 보면서 어떤 주제에 대해서 설명할 지 감을 잡아보겠습니다.



#include <iostream>

int main() {
        char input[50];
	std::cin >> input;
	std::cout << input << std::endl;
}



이 짧은 코드를 보며 C언어에서 보지 못한 것이 보이시나요?


몇가지 보입니다. 우선 iostream이라는 파일을 include하고, std::in과 std::cout이 보이네요. std::endl라는 것도 보입니다.




하나씩 보도록 하지요.


● #include <iostream>

원래는 iostream.h 헤더 파일 입니다만 c++에서는 .h확장자를 붙이지 않아도 됩니다. 그래서 파일명만 써주도록 하는 거죠. 우리는 이 헤더파일을 include 해주어야 콘솔 출력, 입력을 할 수 있습니다(물론 다른 헤더파일을 써도 할 수는 있습니다..). (아, iostream은 input, output stream이라는 뜻이라는 거~)



● std::cin

cin은 콘솔 입력을 담당하는 객체입니다. >>은 오퍼레이터로 연산자를 의미합니다. cin에서 >>은 콘솔로 입력할때 쓰이는 연산자입니다. 음 그냥 cin>>은 이 후에 데이터에 콘솔로 입력하여라라는 것입니다(in앞에 c는 console의 c입니다.).  C언어에서 scanf와 아주 비슷한 역할을 하는 녀석입니다.

그 앞의 std는 우리가 오늘 이야기할 네임스페이스라고 합니다.


● std::cout

cin을 알았으니 cout도 대충 눈치채셨겠죠? <<역시 연산자를 의미합니다. 

cout은 콘솔출력을 담당하는 객체입니다. 누군가가 cout 보고 카우트라고 하는데 그럼 cin은 킨이냐?


● std::endl

endl은  endline으로 줄을 바꾸어 줍니다. 


namespace

여기서 자주 등장하는 키워드는 무엇일까요? 바로 std입니다. 

std는 네임스페이스의 이름입니다. 네임스페이스는 이름 공간으로 그 변수를 구분해주는 역할을 합니다. 


왜 쓰일까요?

어떤 개발자 A, B 두명이 있습니다. 서로 협동하여 프로젝트를 진행하고 있지요. 

프로젝트를 진행하던 중 개발자A는 함수를 func(int a,int b)해서 함수를 선언했습니다. 개발자 B는 생각없이 func(int a,int b)로 다른 기능을 하지만 함수 이름은 A가 정한것과 같이 정의해버렸습니다.


나중에 프로젝트가 진행될때 같은 함수이름과 같은 매개변수 때문에 오류가 나게 되지요.


그렇게 등장한게 네임스페이스입니다. 개발자 A는 namespace의 이름을 namespaceA, 그리고 개발자 B는 namespaceB로 자신의 함수를 선언합니다. 이렇게 하면 충돌할 확률이 줄어들겠죠??


와 그런데도 namespace이름마저 같으면 답없다 너네 둘은


네임스페이스를 정의하는 것은 너무 쉽습니다. 그저 namespace 키워드를 써주고 namespace의 이름을 정해 중괄호({, })로 묶어주기만 하면 끝입니다.


namespace 네임스페이스 이름{

...

변수나 함수

...

}


어때요? 참 쉽지 않나요? 그래서 네임스페이스에 존재하는 함수와 이름을 사용하려면 네임스페이스이름::함수 또는 변수 로 써주면 됩니다. 


예를 한번 볼까요?





#include <iostream>

namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}
int main() {
	myNamespace::a = 30;
	myNamespace::b = 40;
	std::cout << "30+40="<< myNamespace::sum(myNamespace::a, myNamespace::b) << std::endl;
}


myNamespace라는 네임스페이스에서는 a,b라는 변수, sum이라는 함수가 있네요. 얘네들을 써먹어서 a+b를 sum함수로 값을 도출해냅니다. 그러니 myNamespace::가 앞에 붙어있죠.



원하는 결과가 나오죠?


네임스페이스안에 네임스페이스를 지정할 수도 있습니다. 그렇게 되면 충돌확률은 현저히 줄어들겠네요. 하지만 쓸 일이 거의 없습니다. 이름도 길어지고.


namespace 네임스페이스 이름{

namespace 네임스페이스 이름1{

...

변수나 함수

...

}

namespace 네임스페이스 이름2{

...

변수나 함수

...

}

namespace 네임스페이스 이름3{

...

변수나 함수

...

}

}



접근하는 방법도 역시 똑같습니다.

네임스페이스::네임스페이스이름n::함수나 변수명



좋은것 같은 데 귀찮아

근데요. 사실 우리는 공부하는데 일일이 std::붙어주는 게 여간 귀찮은게 아니죠.


그런 경우에는 using namespace 네임스페이스이름 을 써주기만 하면 namespace의 이름으로 구분할 것 없이 바로 변수, 함수 등의 데이터를 사용할 수 있습니다.


이렇게 말입니다.


#include <iostream>

using namespace std;
namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}

using namespace myNamespace;
int main() {
	a = 30;
	b = 40;
	cout << "30+40="<< sum(a, b) << endl;
}


바로 위의 코드를 네임스페이스를 사용하지 않고 구현했습니다. 코드가 확 줄고 편해졌지요?


이제까지 네임스페이스를 알아보았습니다.


다음에 봐요~



반응형
블로그 이미지

REAKWON

와나진짜

,

구조체와 포인터


지난 번에 구조체에 대한 이야기를 쬐~~~끔 했었죠? 이번에도 구조체를 가지고 놀아봐요.


구조체를 통해서 ,이야기했다 시피 여러가지 자료형을 통합적이고 효율적으로 작업을 할 수 있는 것이 장점입니다.


자료형이라...

우리는 이제까지 int, char, double, float 같은 자료형을 많이 봐왔지요. 하지만 구조체 자체도 자료형이 될 수 있습니다. 즉, 변수로 선언이 가능하다는 것이죠.




한번 되짚어 봅시다. int 자료형 변수 a를 선언하고 10이라는 값을 집어 넣어 보아라 한다면 우리는 식은 죽 먹기로 해낼 수 있습니다.


int a;

a = 10;


이렇게 자료형을 통한 변수는 값을 대입하는 것 외에도 


1. 매개변수로 쓰일 수 있다.

2. 포인터로 참조할 수 있다.

3. 배열로 쓰일 수 있다.

4. 구조체의 변수로 쓰일 수 있다.


뭐 이밖에도 여러분이 더 잘 알거에요.


그래서 무슨 말이 하고 싶은 거냐 넌?


구조체도 자료형이 될 수 있다고 했습니다. 그러니까 위와 같이 쓰일 수도 있다는 이야깁니다. 똑같습니다. 위의 순서대로 구조체를 갖고 놀아 봅시다.


일단 매개변수로 쓰이는 경우를 보도록 하지요.


#include <stdio.h>

typedef struct student{
	char *name;
	int math;
	int kor;
	int eng;
} student;

float avg(student person);
student getHonorStudent(student me, student you);

int main() {
	student reakwon = { "REAKWON",40,50,40 };
	student seonmi = { "선미",90,95,100 };

	student honorStudent = getHonorStudent(reakwon, seonmi);
	printf("우등생은 %s입니다.\n", honorStudent.name);
	return 0;
}

float avg(student person) {
	return (person.math + person.kor + person.eng) / 3.0;
}
student getHonorStudent(student me, student you) {
	if (avg(me) > avg(you))
		return me;
	if (avg(me) < avg(you))
		return you;
}

전교생이 두 명인 학교에서 우열을 참 가리기 쉬운 학생 두명이 있습니다(그러니 평균이 같은 학생은 없다는 가정을 하겠습니다). 누가 우등생인가를 구하는 코드입니다.




위 코드에서 reakwon이라는 학생 성적은 제 고등학교 시절과 정확히 똑같군요.


우리는 avg나 getHonorStudent함수에서 매개변수로 쓰인 student 구조체를 주목해야합니다. 변수가 매개변수로 함수로 전달하는 것과 일치하죠. 그 매개변수 앞에는 student라는 자료형이 있는 것과 같은 겁니다.


결과를 보죠.



예상했다 싶이 선미라는 아이가 우등생이네요.

오~ 매개변수로 쓰일 수 있구나!


두번째, 포인터로 참조할 수도 있습니다.

우리는 포인터를 통해 그 주소에 접근할때 *를 이용해서 접근했었죠.

포인터도 역시 똑같습니다.


(*구조체 변수).변수이름


구조체는 이와 같은 접근 방법외에도 다른 방법으로도 포인터를 통해 참조할 수 있습니다. "->" 이와 같은 표시로 말이죠.


구조체 변수->변수이름


마치 화살표 같은게 포인터 티가 나죠?


이제 코드로 한번 확인해보도록 합시다.


#include <stdio.h>

typedef struct student{
	char *name;
	int math;
	int kor;
	int eng;
} student;


int main() {
	student reakwon = { "REAKWON",40,50,40 };
	student *me = &reakwon;

	printf("me의 크기:%d\n", sizeof(me));
	printf("reakwon의 주소:%p, me가 가리키는 주소:%p\n", &reakwon, me);
	printf("\n");
	printf("포인터를 통해서 값을 읽어오는 방법 1");
	printf("수학:%d, 국어:%d, 영어:%d\n", (*me).math,(*me).kor,(*me).eng);

	printf("\n");
	printf("포인터를 통해서 값을 읽어오는 방법 2");
	printf("수학:%d, 국어:%d, 영어:%d\n", me->math, me->kor, me->eng);
	
      return 0;
}



다음 결과 사진을 보고서 다시 이야기해 보도록 합시다.




포인터를 배울때와 같이 변수 me는 reakwon의 주소를 값으로 갖고 있습니다. 그리고 접근하는 방법 두가지 역시 같은 값을 나타내고 있습니다.


하지만 크기를 보세요. 구조체 포인터는 역시 포인터 크기(4바이트)와 같은 크기입니다. 주소만 갖고 있으면 되기 때문이죠.


그림으로 그려보면 이런 그림이겠군요.





우리는 한가지 생각해볼 점이 있습니다. 

구조체의 크기가 크고 함수 매개변수로 쓰일 경우 어떻게 넘겨주는 것이 더 효율적일까요?

값을 복사하는 normal한 매개변수로 쓴다면 구조체의 크기만큼 복사해야합니다.

그러나 포인터를 사용한다면 단지 주소값만 넘겨주면 되기 때문에 시간 면에서나 효율 면에서 유리할 수 있습니다.


물론 포인터는 매개변수의 변형을 일으킬 수도 있지만, 그런 원치않는 조작을 막기위해서 const라는 키워드가 존재하는 겁니다.



이제 구조체를 배열로 관리해보겠습니다. 역시 쉽습니다. 바로 코드로 봅시다.


#include <stdio.h>


typedef struct student{
	char name[30];
	int math;
	int kor;
	int eng;

} student;

float avg(student who) {
	return (who.math + who.kor + who.eng) / 3.0;
}

int main() {
	
	student students[3];

	for (int i = 0; i < 3; i++) {
		printf("이름:");
		scanf("%s", students[i].name);

		printf("수학 점수:");
		scanf("%d", &students[i].math);

		printf("국어 점수:");
		scanf("%d", &students[i].kor);

		printf("영어 점수:");
		scanf("%d", &students[i].eng);
		printf("\n");
		
	}


	for (int i = 0; i < 3; i++) {
		printf("%s의 점수\n",students[i].name);
		printf("수학 %d, 국어 %d, 영어 %d\n",
			students[i].math, students[i].kor, students[i].eng);
		printf("평균 %.1lf\n",avg(students[i]));
		printf("\n");
	}

	return 0;
}



세명의 학생의 이름과 점수를 입력받고 점수와 평균을 출력해주는 코드입니다. 


배열을 포인터 연산으로 나타낼 수 있듯이 구조체 배열 역시 포인터 연산으로 나타낼 수 있습니다. 위의 코드를 (*(students+i)).kor 과 같이 코드를 한 번 바꾸어 실행해보세요.


위 코드의 결과가 아래의 캡처화면입니다.



구조체는 이렇게 편리합니다. 




이제 마지막, 구조체는 구조체를 포함할 수 있습니다. 이것도 역시 변수와 같은 성격이죠. 구조체를 변수로 쓰는 방법은 아래와 같습니다.


typedef struct person {

char name[30];

int age;

char sex[10];

struct person friends[3];

} person;



구조체 안에 같은 구조체 타입의 변수가 배열로 들어가 있습니다. 구조체는 자료형이라고 했기 때문에 뭐 놀랍지도 않군요.


물론 다른 구조체 타입의 변수까지 멤버로 가질 수도 있습니다. 이런 엉뚱한 구조체는 어디서 쓰일까요?


나중에 자료구조에서나 알고리즘에서 트리의 노드와 같은 것으로 쓰일 수가 있습니다.



이제까지 구조체에 대해서 공부해봤습니다. 바이바이~

반응형
블로그 이미지

REAKWON

와나진짜

,

구초제(Structure)

 

우리는 같은 자료형을 여러개 쓸 때는 배열이라는 것을 썼었죠. 아주 유용합니다. 배열과 반복문을 통해서 조금 더 쉽게 프로그래밍을 할 수 있었습니다.

 

하지만 같은 자료형이 아닌, 다른 자료형을 하나로 통합해서 관리해야할때는 어떻게 할까요??

 

통합적이게 데이터를 묶고 쉽게 접근할 수 있는 기법이 바로 구조체라고 합니다.

 

만약 구조체가 없이 사람들의 데이터를 관리한다고 칩시다.

 

char name[MAX_N][30];

int age[MAX_N];

char phone[MAX_N][30];

 

자료형이 서로 다르니, 서로 같아도 쓰임새가 다르니, 여러개의 변수를 관리할 수 밖에 없습니다. 프로그래밍의 가독성과 효율성을 떨어뜨리게 되는 것이죠.

 

 

하지만 구조체는 우리의 작은 소원을 들어줍니다. 하나로 묶는 것이죠. 

구조체를 정의하는 방법은 상당히 간단합니다. 이렇게요.

 

struct User{

char name[30];

int age;

char phone[30];

}

 

User가 바로 구조체의 이름이며, 그 안의 변수들은 멤버변수라고 합니다.

 

이렇게 데이터를 묶어서 배열로 정의하게 되면 저 위의 코드를 조금 더 편하게 관리하고 접근할 수 있습니다.

 

struct User user[MAX_N];

 

대충 이런 느낌입니다. 쉽죠?

 

그럼 오늘 포스팅을 이걸로 마치겠습니다.

 

이제 본격적으로 구조체를 어떻게 사용하는 지 알아보도록 하겠습니다.

구조체 정의는 위에서 본것과 같이 함수밖에서(전역) 구조체 안의 멤버 변수들을 선언해줍니다. 

struct 구조체이름{

...

변수

...

};

 

그리고 함수안에서는 다음과 같이 사용할 수 있습니다.

 

struct 구조체이름 구조체변수명={ ..., 멤버 변수 값, ...};

 

이렇게 한꺼번에 구조체에 선언된 변수 순서대로 값을 초기화 할 수도 있고

또는

 

struct 구조체이름 구조체변수명;

구조체변수명.멤버변수명=변수값

...

 

하나하나 명시적으로 변수에 값을 할당할 수 있습니다.

 

변수에 접근하려면 어떻게 할까요??

그것도 역시 간단합니다. 구조체변수에 .(dot)을 찍어서 멤버변수명을 입력해서 접근할 수 있습니다.

 

구조체변수명.멤버변수명

 

 

이제까지 설명했던 내용을 코드로 간단하게 사용해보도록 합시다.

#include <stdio.h>
struct User {
        char name[30];
        int age;
        char phone[30];
};
int main() {
        struct User user = { "REAKWON",2018,"010-????-????" };
        printf("이름:%s\n", user.name);
        printf("나이:%d\n", user.age);
        printf("전화번호:%s\n", user.phone);
}

 

원하는 대로 결과가 나오죠?

 

그런데 우리는 구조체를 typedef로 조금 더 간단하게 선언할 수 있습니다. 

 

typedef은 자료형을 조금 더 편하게 관리하기 위해서 쓰입니다.

typedef 원래의 자료형 내가 정의한 자료형;

이렇게 하면 변수를 원하는 대로 정의해줄 수 있습니다.

아래와 같이요.

typedef unsigned int u_int;u_int val=30;

 

typedef struct 구조체명{

구조체 정의부

} 정의할 구조체명;

 

그러면 선언에서 struct를 빼고

 

정의한 구조체명 구조체변수

 

이렇게 조금 더 편하게 쓸 수도 있습니다.

 

아래의 코드는 바로 위의 코드를 typedef를 사용해서 바꾼 코드랍니다.

 

 

#include <stdio.h>
typedef struct User {
        char name[30];
        int age;
        char phone[30];
} _User;
int main() {
        _User user = { "REAKWON",2018,"010-????-????" };
        printf("이름:%s\n", user.name);
        printf("나이:%d\n", user.age);
        printf("전화번호:%s\n", user.phone);
}

 

구조체 크기

구조체의 크기를 한번 알아보도록 할까요?

아까와 같은 구조체를 한번 변경해서 보도록하겠습니다.

struct User { 
    char name[10]; 
    int age; 
    char phone[10]; 
}

char자료형은 1바이트이고 10개의 배열을 가졌으니, 10바이트

int자료형은 4바이트입니다. 

또 char배열 10개로 10바이트해서 구조체의 크기는 총 24바이트겠네요.

 

코드로 확인해보도록 하지요.

 

#include <stdio.h>

struct User {
    char name[10];
    int age;
    char phone[10];
};

int main() {
    printf("User 구조체 사이즈:%d\n",sizeof(struct User));
}

 

 

 

 

??????

제가 틀렸군요. 28바이트가 나오네요.

 

왜일까요??

구조체의 사이즈는 그 구조체 멤버중에서 가장 큰 자료형에 따라 결정이 됩니다. 가장 큰 자료형은 int로 4바이트가 되죠? 그래서 4의 배수로 크기가 결정됩니다.

이해를 돕기 위해 그림을 첨부합니다. 

 

 

사각형 한 칸의 크기는 1바이트를 나타냅니다. name과 phone은 10바이트를 차지할 것 같지만, char형은 1바이트이고, int형인 age는 4바이트니까 큰 자료형에 의해서 4의 배수로 메모리가 할당이 됩니다.

name과 같이 12바이트를 맞추기 위해 여분의 바이트를 붙여주는 것을 우리는 패딩(padding)이라고 합니다.

 

아~ 그래서 28바이트가 나오는 구나. 

그렇다면 메모리가 낭비될텐데 조금 더 아낄수 있는 방법은 없을까요?

 

우리는 변수명을 단지 바꾸어 주어 이런 메모리 낭비를 막을 수 있습니다. 이렇게요.

 

#include <stdio.h>

struct User {
    char name[10];
    char phone[10];
    int age;
};

int main() {
    printf("User 구조체 사이즈:%d\n",sizeof(struct User));
}

 

이때의 메모리를 보면 아래의 그림과 같습니다.

 

이렇게 4바이트를 아낄 수 있지요.

 

왜 이렇게 메모리를 큰 자료형에 맞출까요?? 네트워크 통신에서 관련이 있습니다. 우리는 아직 그 크기에 그렇게 신경써도 되지 않아도 됩니다.

 

이렇게 구조체가 무엇인지, 어떻게 정의하는지 알아보았는데요. 다음 시간에는 구조체에 대해서 조금 더 세세하게 알아보고 구조체 포인터에 대해서도 이야기해보도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

동적 메모리 할당


우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면


int arr[100];


이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요.


하지만 이런 상황은 조금 불편할 수 있겠죠.


● 처음 int배열 100개가 필요하다고 생각했는데 프로그램을 실행하다 보니 int배열이 500개만 필요한 경우


● int배열 500개가 모자라 배열 500개를 메모리에 더 할당해야 할 경우


우리는 메모리를 효율적으로 사용하기 위해서 너무 남거나, 너무 모자란 메모리 할당을 피해야할텐데요. 그 목적을 달성하기 위해서 이런 코딩은 어떨까요?




#include <stdio.h> int main() { int size, i; scanf("%d", &size); int arr[size]; for (i = 0; i < size; i++) scanf("%d", &arr[i]); for (i = 0; i < size; i++) printf("arr[%d] = %d\n",i,arr[i]); }


size를 입력으로 받고, 배열의 크기를 size만큼 할당하는 겁니다. 그리고 그 size만큼 for루프를 돌아 정수를 입력받고 있네요.

이 코드는 분명 얼핏 보기에는 문제가 없어보입니다. 그러나 메모리에 할당하는 시점을 고려한다면 이 코드는 실행조차 안되고 컴파일 에러가 발생하게 됩니다.


왜요?


stack에서 메모리는 컴파일 시점에서 결정됩니다. 우리는 실행중에 입력을 받고, 실행 중에 메모리를 할당해야하는데, 그 앞의 단계인 컴파일단계에서는 얼마만큼의 메모리를 할당할지 알 수 있는 방법이 없죠. 컴파일러는 모릅니다.


우선 우리는 메모리 구조에 대해서 조금 알아야 할 필요가 있습니다. 





스택영역

우리는 이제껏 스택영역에 메모리를 할당해 왔습니다. 컴파일 시점에 결정되는 영역입니다.


함수의 지역변수, 매개변수등이 이 메모리에 할당이 됩니다. 그리고 함수가 종료되었을때 할당된 메모리를 반환하게 됩니다.


그러니까 메인 함수안의 변수들은 모두 스택영역에 할당이 된거죠. 




힙 영역

이 영역의 메모리는 실행시점(Run Time)에 결정됩니다. 프로그래머에 의해서요. 이 영역을 힙영역이라고 합니다.


한가지 더 보충설명을 하자면 스택영역은 높은 주소에서 낮은 주소로 할당이 되고, 힙영역은 낮은 주소에서 높은 주소로 할당이 됩니다. 그래서 재귀함수를 통해 함수를 계속호출하게 되면 힙영역을 침범해 스택오버플로우가 발생하게 됩니다. 그 반대의 경우는 힙오버플로우가 발생합니다.


데이터 영역

이 영역의 메모리는 정적변수, 전역변수, 구조체 등 함수 외부에서 선언되는 변수들이 이 메모리에 할당됩니다.


코드 영역

코드영역에는 프로그램의 실행 명령어들이 존재합니다.


우리가 이번에 주목해야할 영역은 힙영역입니다. 위의 코드를 정상적이게 동작시키기 위해서는요.


그 목적을 달성하기 위해서 나온 함수가 바로 malloc함수입니다. malloc함수는 stdlib헤더에 선언되어 있으며 malloc함수를 사용하기 위해서는 stdlib.h를 include해야합니다.


void *malloc(size_t size)


이 함수는 size만큼의 메모리를 힙영역에 할당합니다. 어떤 자료형일지 모르니 반환형 데이터는 void포인터입니다. 


하지만 그냥 메모리만 할당하고 해제하지 않으면 메모리가 누출됩니다. 우리는 메모리를 이제 쓰지 않을 경우(거의 함수 반환 직전)에 free함수를 써서 메모리를 해제해야합니다.


void free(void *ptr)


이제 malloc함수와 free함수를 사용해서 위의 코드를 오류없이 실행시켜보도록 하지요.



#include <stdio.h> #include <stdlib.h> int main() { int size, i; scanf("%d", &size); int *arr=(int*)malloc(sizeof(int)*size); for (i = 0; i < size; i++) scanf("%d", &arr[i]); for (i = 0; i < size; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }



무리없이 실행도 되고, 원하는 결과를 얻을 수 있습니다.


malloc함수 외에도 calloc, realloc함수가 있습니다.


void *calloc(size_t n, size_t size)


calloc은 malloc과 힙영역에 할당하는 것을 똑같습니다. 사용법과 초기값이 다른데요.

calloc은 할당된 메모리를 전부 0으로 초기화합니다. malloc은 0으로 전부 초기화 시키지 않죠. 

쓰임새는 아래의 코드와 같습니다.


#include <stdio.h> #include <stdlib.h> int main() { int n, i; scanf("%d", &n); int *arr=(int*)calloc(n,sizeof(int)); printf("calloc 0으로 초기화\n"); for (i = 0; i < n; i++) printf("arr[%d]=%d ", i, arr[i]); printf("\n"); for (i = 0; i < n; i++) scanf("%d", &arr[i]); for (i = 0; i < n; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }





realloc함수는 할당된 메모리를 다시 할당할때 쓰입니다. 기존에 할당했던 포인터와 다시 할당할 size를 매개변수로 전달합니다. 기존에 있던 값은 변함이 없습니다.


void *realloc(void *memblock, size_t size)


realloc은 아래처럼 쓰면 됩니다.

#include <stdio.h> #include <stdlib.h> int main() { int n,m,i; printf("처음 크기 입력\n"); scanf("%d", &n); int *arr=(int*)malloc(sizeof(int)*n); for (i = 0; i < n; i++) scanf("%d", &arr[i]); for (i = 0; i < n; i++) printf("arr[%d]=%d\n", i, arr[i]); printf("다시 할당될 크기 입력\n"); scanf("%d", &m); //realloc함수도 다시 할당 arr = (int*)realloc(arr, sizeof(int)*m); for (i = n; i < m;i++) scanf("%d", &arr[i]); for (i = 0; i < m; i++) printf("arr[%d]=%d\n", i, arr[i]); free(arr); }





2차원 배열은 어떻게 할당할까요?

우선 1차원 배열의 메모리를 힙에 할당하고, 1차원배열 각각 메모리를 할당하면 됩니다. 


메모리 해제할때도 일차원배열 메모리를 해제하고, 2차원배열 메모리를 하제하면 됩니다. 조금 복잡할 수도 있어요.



#include <stdio.h>
#include <stdlib.h>

int main() {
	int n, m, i;
	int **arr;
	scanf("%d %d", &n,&m);

	arr = (int**)malloc(sizeof(int *)*n);


	for (i = 0; i<n; i++)
		arr[i] = (int *)malloc(sizeof(int)*m);

	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			scanf("%d", &arr[i][j]);
	printf("\n\n");
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++)
			printf("arr[%d][%d]=%d\n", i, j, arr[i][j]);
		printf("\n");
	}

	for (i = 0; i<n; i++)
		free(arr[i]);
	free(arr);

}
	




이제까지 메모리 할당 함수를 사용해서 동적메모리를 할당하는 방법을 알아보았습니다. 조금 어렵죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

void 포인터


void 포인터??

이런 포인터는 처음 들어봤네요~ 분명 void라는 것은 함수 앞에서 반환형이 없을 때 쓰이는 키워드로 아는뎁;

void main은 많이 봤는데...


지금부터 이야기할 주제가 바로 void포인터라고 합니다. 우리는 이제껏 자료형이 정해져있는 포인터 예를 들면


int *a


라는 형태는 봐왔잖아요?


그리고 그 포인터에 주소를 할당하는 방법은 이런 것이죠.


int a=100;

int *b=&a; 


이걸 말로 풀어서 설명한다고 하면

b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 int형 데이터가 있다!

라고 말이에요.


우리는 이 말속에서 힌트를 얻을 수 있습니다. 

그곳에는 int(정수형)형 데이터가 있다 라는 말을 집중해주세요.


위의 int *를 void *로만 바꾸어 써보고 읽어볼게요.


int a=100;

void *b=&a;


b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 void형 데이터가 있다.




void 형 데이터가 있다....(?) void는 "빈공간"이라는 뜻을 내포하고 있는데요. 컴퓨터는 자료형을 모르기 때문에 빈공간처럼 보는 겁니다.

그러기 때문에 앞으로 우리는 이렇게 읽어야 할 겁니다.


b는 a의 주소를 가지고 있고, b를 통해 a를 참조할 수 있는데 그곳에는 알 수 없는 자료형 데이터가 있다.


이 형태 그대로 데이터를 참조하면 컴퓨터는 "아 몰랑!" 합니다.


printf("%d\n", *b);     //오류

 

얼마나 참조해야하는지 알 수 없기 때문입니다. void는 단순히 주소값만을 가지고 있습니다.


우리는 void가 가리키고 있는 데이터의 형태를 알고 있습니다. 우리는 똑똑하니까요(?). 그래서 우리는 *b가 무엇이냐 라고 질문할때 100이라고 대답할 수 있습니다. 우리는 똑똑하기 때문이죠.


하지만 단순히 주소값만!알고 있는 우리 void형은 그 형태가 int형이든 char 형이든 구조체든 문자열을 가리키고 있는 포인터이든 상관없이 단순히 주소값만이요. 그러니 void포인터는 자료형이 무엇이든 간에 주소값만 바라봅니다.


주소만 갖으면 되기 때문에 포인터의 크기(4바이트)만 갖고 있고, 포인터 연산조차 할 수 없습니다.


정말 읽을 수 없는 지 한번 코드로 살펴봅시다.

#include <stdio.h>

int main() {
	int a = 10;
	void *b = &a;
	printf("%d\n", *b);
}
	


실행시킬 수 조차없이 빨간줄로 


Error:Expression must be a pointer to a complete object type


라는 에러를 보게 됩니다. 

완전한 형태의 포인터로 바뀌어야한다 라고요.




우리는 컴퓨터에게 "너가 가리키고 있는 데이터 자료형은 int형이야" 라고 명확히 알려주어야합니다. "그러니까 넌 묻지도 따지지도 말고 4바이트만 읽으면 돼!" 라고요.

어떻게 알려줄까요??


우리는 형변환을 알고 있습니다. 그걸 사용하는 것이죠.


printf("%d\n", *(int*)b); 


이렇게 하면 void*는 int형을 읽을 수 있는 int*로 변환되게 됩니다.

정말 빨간 줄 없이 int형 데이터를 읽을 수 있는 지 코드로 볼까요?

#include <stdio.h>

int main() {
	int a = 10;
	void *b = &a;
	printf("%d\n", *(int*)b);
}


빨간 밑줄은 없어졌군요~ 





실행도 정상적으로 되는 것을 확인할 수 있고, 10을 정확히 읽는 것도 확인할 수 있습니다.


char형은 어떻게 변환할까요? 그것도 간단합니다. 바로 char*로만 바꾸어주면 됩니다.

#include <stdio.h>

int main() {
	char a= 'c';
	void *b = &a;
	printf("%c\n", *(char*)b);
}





네, 'c'라는 문자를 제대로 읽을 수 있군요.




문자열 역시 다르지 않습니다.

#include <stdio.h>

int main() {
	char *str= "문자열";
	void *b = str;
	printf("%s\n", (char*)b);
}

str자체가 문자열을 가리키고 있는 포인터이기 때문에 str변수 앞에 &(amp)를 붙여주지 않습니다. 이해하죠?




문자열도 잘 읽어오는군요.



이렇게 void포인터는 만능입니다. 어떤 자료형이건 바로 참조할 수있죠. 알맞은 자료형으로만 변경한다면 말이죠.


우리는 void포인터를 동적할당할때 유용하게 사용합니다. 동적할당에 사용하는 malloc과 같은 함수들이 void*로 반환하기 때문입니다.

malloc은 특정한 size의 크기로 메모리를 할당하고나서 우리들에게 알맞게 변환해서 써라 라는 의미로 void*를 내뱉게 됩니다.


어차피 나중에 배울 malloc함수의 원형 한번 볼까요?


void *malloc(size_t size)


앞서 말한대로 void*를 토하고 있습니다. 너네가 맘대로 바꾸라고 말이죠.


한번 보세요. 아~ 이런 변태같은 함수도 있구나~ 라고 기억하시기 바랍니다.



이것으로 void포인터에 대해 설명해보았습니다. 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

함수포인터

우리는 이제까지 어떤 변수를 가리키는 포인터, 배열을 가리키는 포인터를 사용해보았어요. 물론 이정도를 알아도 상당히 훌륭하다고 생각합니다.

그렇지만 변수뿐만 아니라 함수도 포인터로 가리킬 수 있다는 것을 알게 된다면 조금 더 간지나는 프로그래밍을 시도해 볼 수 있겠습니다. 이제부터 설명할 것이 무엇이냐, 그 이름하여 함수포인터랍니다. 

함수포인터라... 그냥 포인터도 극혐인데 말이죠

함수포인터란 함수를 가리킬 수 있는 포인터를 의미합니다. 아니, 근데 그러면 주소를 알아야하는데, 함수에도 주소가 있나? 네, 있습니다. 함수가 주소를 가지고 있다구요? 제말이 구라가 아님을 보여드리겠습니다.

#include <stdio.h>
int sum(int a, int b) { 
        return a + b;
} 

int main() { 
        printf("함수 sum의 주소 : %p\n", &sum);
}


이 코드의 결과는 어떻게 될까요? 우리는 변수명 앞에 &(amp)를 붙여 변수의 주소를 알게 됩니다. 그렇다면 &함수명은 무엇을 의미하게 될까요? 아무리 눈치없어도 진짜 이정도면 눈치채야지~

신기하네요. 16진수로 어떤 수가 나오네요. 눈치채셨겠지만 함수의 주소를 의미하게 됩니다. 바늘가는데 실간다는데 주소가 있으면 포인터가 있겠죠. 그렇다면 함수포인터를 선언하는 방법을 알아보도록 하겠습니다. 간단합니다.

①int ②(*ptrSum)③(int a,int b)

 

일반 포인터와 마찬가지로 주소를 가리킬때는 *을 사용해서 포인터라고 알려줍니다.

①은 함수의 반환형을 의미합니다.

②는 함수포인터의 이름을 의미합니다. (변수명과 같이 임의로 정해줍니다.)

③은 매개변수를 의미합니다. 매개변수가 없을 때는 빈 괄호나 void를 사용합니다.

 

네, 위 세가지만 지켜주면 됩니다.

그러니까 ptrSum이라는 함수포인터는 반환형이 int형이고 매개변수 2개를 갖는데, 둘 다 int형 매개변수인 함수포인터가 되겠습니다.

그렇다면 다음과 같은 함수포인터는 무엇을 의미할까요?

void (*ptrFunc)()

함수명이 ptrFunc인데, 반환값이 없고(void), 매개변수도 없는 함수포인터를 의미합니다. 이제 응용가능하시겠죠? 함수포인터에서 반환형과 매개변수가 일치하는 함수만 함수포인터에 할당이 가능합니다. 기억하세요.

함수포인터를 선언하는 방법은 알게 되었으니, 다음 코드를 통해서 함수포인터의 값이 바로 그 함수의 주소인지 확인도 해보고, 사용도 해봅시다.

#include <stdio.h>
int sum(int a, int b) { 
        return a + b;
}

int main() {
        int(*ptrSum)(int a,int b); 
        ptrSum = sum; 
        printf("sum의 주소: %p\n", &sum); //&sum은 sum과 같음 
        printf("ptrSum의 값: %p\n", ptrSum);
        printf("ptrSum의 주소: %p\n", &ptrSum);
        printf("ptrSum(%d, %d) = %d\n", 3, 4, ptrSum(3, 4));
}

 

우리는 sum이라는 함수의 주소가 0x00081023번지라는 것과 ptrSum이 0x00081023을 가리키는 것을 확인할 수 있습니다(실제 sum도 함수의 주소, &sum도 함수의 주소입니다).

그리고 그 함수포인터의 주소는 0x0026FB04번지네요.

함수포인터를 통해서 호출(ptrSum(3,4)) 확인해볼 수 있군요. 

(코드에서 나오지는 않았지만 함수포인터의 크기는 역시 4바이트입니다. 포인터이기 때문에요.)

조금 더 쉽게 그림을 통해서 보도록할게요.

 

포인터를 설명할때 아주 많이 사용하는 그림이죠? 지겹죠?

0x0026FB04번지에 있는 ptrSum이라는 함수포인터는 0x00081023번지에 있는 함수 sum의 시작주소를 가리키게 됩니다.

sum은 함수이기 때문에 시작주소를 갖고 있고, 그 주소를 기점으로 매개변수를 할당, 그리고 함수 내부의 변수들을 스택에 따라 쌓아올립니다. 그러니까 함수의 시작주소를 통해 변수의 주소를 알게 됩니다. 함수의 시작주소는 중요하단 거죠. (더 자세하게 함수호출과정을 아는 것도 도움이 됩니다. 구글형님께 물어보세요.)

그런 sum의 시작주소를 ptrSum은 알고 있기 때문에 sum함수를 호출할 수 있는 겁니다.

아니, 그렇다면 sum함수만 호출하면 되지, 왜 굳이 포인터를 써서 호출하는 거야?

저도 무척이나 이런 생각을 하고 하고 또 하면서 그냥 함수포인터를 흘려 넘겼었죠.

근데 쓰임새는 꽤나 많습니다. 예를 들면 함수 자체를 매개변수로 받고 싶을때가 있을 겁니다.

프로젝트를 진행할때 한 사람만이 진행한다면 물론 상관이 없지만, 여러 사람과 협업을 해야하거나, 라이브러리를 제공할때, 함수에 함수자체를 매개변수로 받아야할 때가 있습니다. 누가 어떤 함수를 필요로 할지 모르니, 어떤 형식으로 함수를 정의해서 매개변수로 전달하게 되면 그 함수를 내부에서는 호출하게 되는 식으로 말이죠.

void func(void (*ptr)()) {
...      ptr();       ... 
}

그리고 또 구조체에서 멤버함수를 커스터마이징할때도 사용할 수가 있습니다. 

struct animal{     
...       void (*walk)();       ... 
}

 

위에서 walk라는 멤버함수를 우리가 원하는 대로 기능을 정의해줄 수 있습니다. 물론, 함수포인터의 형식과 맞는다면 말이죠. 이런 방식은 자바에서 interface라고 하여 메소드를 구현하는 방식과 비슷해보입니다. 함수포인터를 더 연습해보고 개념을 확실히 이해하세요.

이것으로 함수포인터에 대해 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

문자열과 char 포인터

오늘은 심심한데 문자열에 대해서 이야기 해볼까 해요~ 문자열과 포인터는 C언어에서 너무나 귀찮은 놈들인데,,, 그래도 꼭 쓰이니까요. char 자료형은 문자를 변수로 갖는 건 모두 아는 사실이죠?

근데~ 우리는 문자열을 쉽게 할당하고 싶단 말이에요. 우리는 배열이라는 아주 편리한 변수 선언 법을 알고 있답니다.

배열로 문자열을 표현하는 방법을 알아보겠습니다.

char hello[6] = { 'h','e','l','l','o','\0' };

이 표현은 char이 자료형을 배열로 문자열을 표현한 방법이랍니다. '\0' 라는 문자는 NULL문자라는 뜻입니다. 문자열의 끝을 알려줍니다.

그래서 hello가 다섯글자임에도 불구하고 배열 크기를 6으로 잡은 겁니다.

또 이런 선언법도 가능합니다.

char hello[6] = "hello";

그렇다면 우리가 문자열의 길이를 알고 싶다면 널문자가 나타나기 전까지만 세어주면 문자열의 길이를 알 수 있겠네요.

#include <stdio.h>

int main() {
        char *ptr = "ABCDEF";
        int len = -1;
       
        while (*(ptr+(++len)));
        printf("문자열 길이: %d\n", len);
}

그 결과는 이렇겠네요.

 

len=-1인 이유는 null문자 이전까지만 세어주기 위함입니다. while의 조건절은 null이면 멈추어 버립니다. 여기까지는 쉽네요. 포인터로는 어떻게 표현할까요?

사실 문자열("~~~~")은 그 문자열이 시작되는 주소를 가리키게 됩니다. 주소를 가리킨다!?  그러면 포인터가 생각나지 않나요?

왜냐면 주소를 포인터로 가리키면 문자열을 찾을 수 있으니까요.

그러면 이렇게 선언할 수 있을까요?

char *ptr = "hello";

포인터 ptr은 "hello"라는 문자열을 가리키는 포인터입니다. 

그림에서 보는 것과 같이 ptr은 문자열 "hello"의 주소를 가리키고 있고, 그렇기 때문에 참조가 가능한 상태가 됩니다. 그렇다면 어떤 포인터 역시 hello를 가리킨다면 그 주소는 같을까요?

코드와 결과로 확인해보도록 합시다.

 

#include <stdio.h>  int main() {   	char *ptr1 = "hello"; 	char *ptr2 = "hello"; 	printf("%s, %s\n", ptr1, ptr2); 	printf("%p, %p\n", ptr1, ptr2); } #include <stdio.h> 

int main() {   
        char *ptr1 = "hello"; 
        char *ptr2 = "hello"; 

        printf("%s, %s\n", ptr1, ptr2);
        printf("%p, %p\n", ptr1, ptr2);
}

 

같다는 것을 알 수 있습니다. 우리는 이런 그림을 그려볼 수 있겠네요.

ptr1과 ptr2는 서로 같은 문자열을 가리킵니다.  배열과 포인터에 대해서 선언방법은 그렇게 차이가 없어보이죠?

그렇다면 문자열 배열과 포인터는 서로 같은 성질을 갖고 있을까요?

만약 아래와 같은 코드를 입력한다 arr에 ptr가 가리키는 문자열을 넣으라는 거겠죠??

char arr[10] = "world";

char *ptr = "hello";

arr = ptr;

 "hello"라는 문자열의 길이는 배열크기보다 작기 때문에 들어갈 것입니다. 이렇게 생각하셨다면 다시 생각해봅시다. 오류나니까요.

문자열 "hello" 그 주소 자체를 반환합니다. 그러니까 "hello"의 시작주소가 되는 것이죠. 그것이 ptr이 갖고 있는 값입니다.

arr자체는 arr[0]의 주소, 즉 배열의 시작 위치를 말합니다. 이러한 시작 주소를 마음대로 ptr이 가리키고 있는 주소로 바꿀 수 없습니다.

이 의미는 더 쉽게 풀어서 이야기하면

int a = 0;

int b = 30;

&a = b;

랑 유사한 짓거리를 하는 것이라는 거죠. 마치 a의 주소를 b의 값으로 변경하라는 것과 유사하게 되어버립니다.

하지만 그 반대는 가능합니다. 이렇게요.

char arr[10] = "world" ;

char *ptr = "hello";

ptr = arr;

ptr은 주소를 갖을 수 있는 포인터, arr은 arr[0]의 주소! 말이 되죠 이건??

그러니 ptr은 arr과 동일한 곳을 가리키게 되는 겁니다.

만약

arr = ptr;

이걸 죽어도 써야겠다. 난 arr에다가 ptr의 문자열을 진짜 안쓰면 디질거 같다.  하시는 분들은 ptr의 문자열을 복사해서 쓰는 방법밖에 없습니다.

strcpy(arr, ptr)

이렇게 하시면 arr에 ptr이 가리키는 문자열을 그대로 복사해서 arr에 쑤셔 넣습니다. 주의 할 사항은 arr의 크기는 ptr이 가리키고 있는 문자열의 길이 이상으로 커야한다는 겁니다.

그렇지 않으면 런타임 오류납니다. 컴파일에서 문자열의 길이를 검사하지 않습니다!

이 오류가 바로 그 유명한 버퍼오버플로우(buffer overflow)가 됩니다. 취약점인거죠. 버퍼오버플로우를 통해 해커는 함수의 return 주소를 변경하여 자신의 실행코드를 실행합니다. 별짓을 다할 수가 있게 됩니다그래서 그 대안으로 strncpy, strncat 이런것이 나오게 된겁니다.

그리고 또!

포인터와 배열에는 다른 차이점이 있습니다.

문자열을 초기화 할때 배열은 배열 원소를 변경할 수 있지만, 포인터는 배열의 원소를 바꿀 수 없습니다. 즉, 포인터로 초기화 한다면 상수적인 성격을 띈다라는 것입니다.

가령, 아래 코드가 있다면 ptr[0] 변경시 오류가 발생합니다.

char hello[10] = "hello";

char *ptr = "hello";

ptr[0] = 'H';  //오류

hello[0] = 'H';

하지만

char hello[10] = "hello";

char *ptr = hello; 

ptr[0] = 'H'; 

hello[0] = 'H';

이건 오류가 나지 않습니다. 왜냐면 hello는 배열이거든요. 배열은 그 원소의 값이 변경가능합니다. ptr은 배열의 시작주소를 참조하고 있는 포인터이기 때문입니다.

 

그래서 이러한 strcat를 써먹을때도

#include <stdio.h>
#include <string.h>  

int main() {
        char *hello = "hello, ";  
        strcat(hello, "world");
        printf("%s\n", hello);
}

가 아닌

#include <stdio.h>
#include <string.h>

int main() { 
        char hello[20] = "hello, ";  
        strcat(hello, "world");
        printf("%s\n", hello);
}

 

이런 형태나

#include <stdio.h>
#include <string.h>  

int main() {  
        char hello[20] = "hello, ";
        char *ptr = hello;  
        strcat(ptr, "world");  
        printf("%s\n", ptr);
}

이런식으로 쓰여야 한다는 겁니다. 이렇게 간단하게 문자열과 포인터에 대해서 알아보았습니다. 부족한 점은 나중에 또 보충 설명해보도록 할게요 ㅎㅎ

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

배열과 포인터

 

배열과 포인터는 아주 유사한 성격을 갖고 있습니다. 배열은 뭘까요?

 

우선 배열에 대해서 알아봅시다. 배열은 같은 자료형이 연속된 공간으로 나열되있는 것을 뜻합니다. 배열에 대해서 조금 더 쉽게 접근하기 위해서 index를 갖는데요. index를 통해 빠르게 배열 원소에 접근하게 됩니다.

 

int a[5]={5, 3, 1, 2, 4};

 

이와 같은 배열이 있다면 메모리의 구조는 바로 이렇게 구성될겁니다.

 

 

왜 이런식으로 배열이 구성이 될까요??

 

왜 배열의 원소는 4바이트나 되는 걸까요.

그 이유는 int가 4바이트이기 때문입니다. 따라서 4바이트씩 주소가 증가하고 있는 겁니다.

그렇다면 char형의 배열은 한 원소의 크기가 1바이트이고, long long배열의 한 원소 크기는 8바이트가 되는 겁니다. got it?

 

 

사실 a라는 변수는 a[0]의 주소값을 갖고 있습니다. 배열의 시작주소를 가지고 있다는 거지요.

 

따라서 a는 &a[0]와 같은 값을 나타냅니다. a는 a[0]의 주소를 가리키고 있다는 겁니다. 

a가 주소값을 가지고 있다고??

그렇다면 a가 가지고 있는 주소를 참조한다면 *a로 a가 참조하는 값을 구할 수 있겠네요. 그러면 정말 a[0]의 값을 참조할까요? 

코드로 확인해보세요.

 

 

이제부터는 다른 배열의 다른 연산법을 알아보겠습니다. a는 a[0]을 가리키고 있는 포인터라고 했습니다. a[0]은 5의 값을 가지고 있습니다. 

헌데, a가 a[0]를 가리키고 있으니, *a는 a[0]의 값을 나타내게 되는 것이지요.

 

그렇다면 이렇게 변형해볼까요?

*(a+0) 는 *a와 같은 뜻이 될까요? 아마 그렇겠죠?

그렇다면 *(a+1)은 어떤 값을 나타나게 될까요?? 결론부터 말씀드리면 a[1]의 값을 갖게 됩니다. 어떤 포인터의 +1은 그것이 참조하고 있는 자료형의 크기만큼 주소를 더하라는 의미가 됩니다. 

 

그렇다면 *(a+i)는 a[i]가 되겠네요!! 와우!!

 

이제껏 지껄인 말이 확실한지 코드로 확인해보도록 하겠습니다.

 

#include <stdio.h>

int main() {
	int i;
	int a[5] = { 5,3,1,2,4 };
	
	
	
	printf("a의 주소 &a[0] = %p, a = %p \n", &a[0],a);
	printf("a의 값: %d\n", *a);
	for (i = 0; i < 5; i++)
		printf("\t 주소 %p  a[%d] : %d, *(a+%d) : %d\n",(a+i),i,a[i],i,*(a+i));

}

 

 

다음 사진은 결과를 보여줍니다.

 

 

 

제가 설명한 내용이 맞나요??

a의 값은 a[0]의 주소와 같네요!! 주소 값은 역시 4바이트씩 증가합니다. 

아까 포인터 연산 역시 제가 설명한 대로 나오고 있습니다.

포인터로 배열을 참조할 수 있습니다.

 

방금 전 배열 a를 포인터 ptr로 참조해보도록 하지요.

 

int *ptr = a;

 

로 간단하게 a라는 배열을 포인터로 참조할 수 있습니다.

a 역시 배열의 주소를 가지고 있고 ptr 역시 배열 a를 가리키고 있습니다. 그래서 a와 ptr은 같은 곳을 바라보게 되는 거죠.

 

 

그러면 ptr 역시 a가 가지는 특성을 그대로 가지고 있을까요?

코드로 확인해봅시다. 컴퓨터는 거짓말을 하지 않으니까

 

 

#include <stdio.h>

int main() {
	int i;
	int a[5] = { 5,3,1,2,4 };
	int *ptr = a;
	
	
	
	printf("a의 주소 &a[0] = %p, a = %p \n", &a[0],a);
	printf("a의 값: %d\n", *a);
	
	for (i = 0; i < 5; i++)
		printf("\t 주소 %p  a[%d] : %d, *(a+%d) : %d\n",(a+i),i,a[i],i,*(a+i));

	printf("\n");

	printf("ptr이 가리키는 주소 : %p\n", ptr);
	for (i = 0; i < 5; i++)
		printf("\t 주소 %p  p[%d] : %d, *(p+%d) : %d\n"
			,(ptr+i),i,ptr[i],i,*(ptr+i));
	
	
}

그 결과입니다.
 

 

정확히 똑같은 성질을 갖고 있다는 것을 알 수 있습니다.

 

그러면 둘의 차이는 없을까요??

 

이 둘의 차이는 크기에서 차이가 납니다.

sizeof연산을 한 번 해보도록 하지요.

 

 

#include <stdio.h>
int main() {
	int i;
	int a[5] = { 5,3,1,2,4 };
	int *ptr = a;

	printf("a의 사이즈 :%d, ptr의 사이즈 :%d\n", sizeof(a), sizeof(ptr));
}

 

결과

 

음.....

a의 사이즈는 20이고, ptr의 사이즈는 4바이트네요.

a는 배열의 크기를 갖고 있습니다.

a는 int형(4바이트) 배열로 5개가 할당되어있으니 20바이트이지요.

 

하지만 ptr은 단순히 그 배열을 참조해야하기 때문에 주소값만 저장할 수 있는 공간이면 충분합니다. 따라서 4바이트면 충분합니다.

 

그리고 배열은 상수화가 된다는 것이 특징입니다. 배열의 원소는 변경가능합니다. 하지만 배열 변수를 다른 값으로 변경할 수는 없습니다.

 

int a[5];

int b[5] = {5, 3, 1, 2, 4} ;

 

a=b;

 

a는 b와 크기가 같고 자료형이 같음에도 불구하고 이와 같은 코드는 컴파일 에러를 불러일으키게 되는 겁니다.

 

a는 a[0]의 주소입니다. b 역시 b[0]의 주소입니다.

주소는 read-only입니다. 즉, 값을 대입할 수가 없습니다 절대로!

 

 

위의 코드는 이것과 같은 얘기가 됩니다. 

&a[0] = &b[0] 이라는 거죠.

이제까지 이야기한 내용을 이해하셨다고 한다면 이해가 갈겁니다.

 

주소는 포인터만이 참조할 수 있는 포인터만의 특성입니다.

 

그러니까 위의 코드는 이렇게 바뀌어야합니다.

 

int *a;

int b[5] = {5, 3, 1, 2, 4} ;

 

a=b; //a=&b[0]과 같음.

 

배열과 포인터의 관계 이해하셨는지요??

이것으로 배열과 포인터의 관계를 마치겠습니다~~~

반응형
블로그 이미지

REAKWON

와나진짜

,