חזור לתיקייה: תרגול

4 - המשך ++C ובפרט עבודה עם קבצים

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • עבודה עם קבצים
  • חלוקה למודולים
  • המודול
  • הידור של מספר קבצים
  • עיבוד מקדים
  • הגדרת מאקרו
  • הוספת תוכן קובץ
  • הידור
  • קישור
  • פונקציות סטטיות גלובליות
  • וידוא הנחות בזמן ריצה
  • מתי משתמשים ב-assert

4 - המשך ++C ובפרט עבודה עם קבצים

02340124

מבוא לתכנות מערכות

רשימות תרגול לקריאה בשבת

המשך ++C ובפרט עבודה עם קבצים

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

עבודה עם קבצים

קבצים הם משאב מערכת בדומה לזיכרון ולכן:

  • צריך לפתוח קובץ לפני השימוש בו
  • צריך לסגור קובץ בסוף השימוש בו
  • אותן בעיות אשר צצות מניהול זכרון לא נכון עלולות לצוץ מניהול קבצים לא נכון

בדומה לעבודה עם הקלט והפלט הסטנדרטיים, קריאה וכתיבה מקבצים מתבצעת באמצעות streamstreamstream-ים המוגדרים בספריה fstreamfstreamfstream.

  • המחלקה ofstreamofstreamofstream משמשת לכתיבה לקובץ
  • המחלקה ifstreamifstreamifstream משמשת לקריאה מקובץ

לשתי המחלקות הנ"ל יש בנאי המקבל את שם הקובץ לפתיחה והורס המוודא שהקובץ נסגר כאשר העצם נהרס.

כתיבה וקריאה מתבצעות ע"י שימוש ב-<<<<<< ו->>>>>> בדומה לקלט והפלט הסטנדרטיים ושאר הפעולות על ערוצי הפלט והקלט מתבצעות כשיטות(מטודות) של המחלקות. (למשל השיטה eof()eof()eof() מחזירה האם הגענו לסוף הקובץ)

הערה

ניתן גם לקרוא מקובץ באמצעות הפקודה getlinegetlinegetline. (שהיא לא שיטה, אלא פונקציה חיצונית)

לא ניתן להעתיק אובייקטים מסוג fstreamfstreamfstream. כדי להעביק קבצים לפונקציה אנו נשתמש במצביע/רפרנס.

דוגמה

#include <fstream>
using namespace std;

int main()
{
	ofstream name("output.txt");
	name << "Hello, World!" << endl;
	return 0;
}
תרגיל

כתבו את התוכנית cpcpcp המקבלת שני קבצים ומעתיקה אחד לשני.

#include <fstream>

int main(int argc, char** argv)
{
	if (argc < 3)
	{
		cerr << "Not enough arguments..." << endl;
		return 1;
	}
	
	ifstream inFile(argv[1]);
	ofstream outFile(argv[2]);
	string s;
	
	while (!in_file.eof())
	{
		getline(in_file, s);
		out_file << s << endl;
	}
	
	return 0;
}
הערה

אם קובץ הפלט קיים אז הוא ידרס.
אם קובץ קלט לא קיים אז הוא יווצר.

חלוקה למודולים

אורך החיים של הקוד יכול להיות עשרות שנים ולאורך תקופה זו יש לתחזק את הקוד:

  • לתקן באגים
  • להכניס תכונות חדשות
  • להתאים להתקנים חדשים

הכנסת שינויים לתוכנה קיימת עלולה ליצור באגים חדשים וככל שהתוכנה מסובכת יותר, כך הסיכוי לבאגים גדול יותר.

בתכנות מודלרי אנו מחלקים את הקוד למודולים לפי תפקידם. זה מאפשר:

  1. שימוש חוזר במודולים עבור תוכנות אחרות
  2. הסתרת פרטי מימוש
  3. הקטנת התלות בקוד עצמו

המודול

מודול תוכנה מחולק לשני חלקים: הממשק והמימוש.

הממשק (interfaceinterfaceinterface):

  • מגדיר את הפעולות שניתן לעשות בעזרת המודול
  • חלק זה חשוף למשתמש במודול

המימוש (implementationimplementationimplementation):

  • מספק את המימוש לפעולות שניתן לבצע דרך ממשק המודול
  • חלק זה (בדרך כלל) אינו חשוף למשתמש

ב-C++C++C++ הדרך בה אנו בונים מודול הוא באמצעות שני סוגי קבצים: קבצי cppcppcpp וקבצי hhh.

קובץ hhh יכל את הממשק:

  • הכרזות על פונקציות
  • הגדרות טיפוסים
  • הכרזות על קבועים שיש בהם עניין למשתמש

קובץ cppcppcpp יכיל את מימוש המודול:

  • מימושי פונקציות הממשק ופונקציות פנימיות נוספות
  • הגדרות טיפוסים לשימוש פנימי
  • הכרזות על קבועים שאין בהם עניין למשתמש

למה שנרצה להפריד את הממשק והמימוש? כדי להסתיר מידע מהלקוח ובנוסף, לתת לנו אפשרות לשנות את המימוש בלי שהלקוח ידע על כך או שזה ישנה את הקוד של הלקוח בגלל שינויים שאנחנו עשינו.

דוגמה

נרצה לכתוב מודול המאפשר עבודה עם וקטורים במישור - vector2Dvector2Dvector2D
המודול יכיל את האלמנטים הבאים:

  • הגדרת מבנה הנקרא vector2D עם שדות אליהם יוכל לגשת המשתמש
  • קבוע בשם zero_vector המייצג את וקטור האפס
  • פונקציה בשם length המקבלת וקטור ומחזירה את האורך שלו
  • פונקציה פנימית בשם dotProduct המחזירה את תוצאת המכפלה הסקלרית של שני וקטורים.

בקובץ hhh יהיה המבנה, הקבוע וההכרזה על length.
בקובץ cppcppcpp יהיה המימוש של length וההכרזה והמימוש של הפונקציה dot_product

הקובץ vector2d.hvector2d.hvector2d.h יהיה:

#pragma once

struct vector2D
{
	double x;
	double y;
};

const vector2D zero_vector = {0, 0};

double length(const vector2D& v);

הקובץ vector2d.cppvector2d.cppvector2d.cpp יהיה:

#include <cmath>
#include "vector2d.h"

using namespace std;

static double dot_product(const vector2D& v1, const vector2D& v2)
{
	return (v1.x * v2.x) + (v1.y * v2.y);
}

double length(const vector2D& v)
{
	return sqrt(dot_product(v, v));
}

הידור של מספר קבצים

כדי להדר מספר קבצים ניתן פשוט לרשום את כל הקבצים כפרמטרים לשורת הפקודה של g++g++g++:

g++ a.c b.c mytest.c

את ההידור ניתן לחלק לשלושה שלבים:

  1. עיבוד מקדים - PreprocessingPreprocessingPreprocessing
  2. הידור - CompilationCompilationCompilation
  3. קישור - LinkingLinkingLinking

​compiling process - mtm - tutorial 4.svg

1. עיבוד מקדים

שלב העיבוד המקדים עור על קובץ הקוד ומבצע את ההוראות עבור ה-preprocessorpreprocessorpreprocessor שהן כל השורות המתחילות ב-#\##.
פעולות העיבוד המקדים הן כולן פעולות גזירה והדבקה פשוטות.

הגדרת מאקרו

#define <macro> <value>

לאחר ההגדרה, בכל מקום שיופיע <macro> בקוד, תבוצע ההחלפה ל-<value>.
ניתן להגדיר יותר מסתם קבועים בעזרת מאקרו אבל חשוב לשים לב להשלכות.

הוספת תוכן קובץ

#include <filename>

מוסיף את תוכן הקובץ לתוך הקוד במיקום הנוכחי.

  • הקוד שמתווסף עובר גם הוא עיבוד מקדים
  • אם שם הקבוץ מופיע בתוך <><><> ההתייחסות היא לקובץ מהספריה הסטנדרטית
  • אם שם הקובץ מופיע במרכאות " " ההתייחסות היא לקובץ שנמצא עם שאר הקוד שכתבנו

2. הידור

שלב ההידור מקבל קבצי קוד מקור שעברו את העיבוד המקדים. הוא עדיין לא מקבל קבצי hhh אלא רק קבצי cppcppcpp (אשר אליהם הודבקו קבצי ה-hhh בעיבוד המקדים)

לכל קובץ נוצר קובץ בינארי אשר מכיל את הקוד המהודר מקובץ הקוד. לקובץ זה קוראים object file ויומתו היא .o.o.o
ייתכן וחלק מהפונקציות שהוכרזו לא מומשו בקובץ הנוכחי ולכן קובץ ה-objectobjectobject יכיל "חורים"

3. קישור

שלב הקישור מקבל כקלט קבצי אובייקט ומקשר את כולם לקובץ הרצה יחיד.
בשלב זה, לכל "חור" שהושאר בקובץ אובייקט מקושרת הפונקציה המתאימה(אם אכן קיים לה מימוש באחד מקבצי האובייקט)

ייתכנו שגיאות בשלב זה הנקראות שגיאות קישור:

  • לא נמצא מימוש עבור פונקציה מסויימת: a.c:11: undefined rerfence to 'my_function'
  • נמצא יותר ממימוש אחד עבור פונקציה מסויימת:
    a.c:10: multiple definition of 'main'
    b.c:10: first defined here
    

בקישור של קובץ הרצה, רק אחד הקבצים צריך להכיל הגדרה של פונקציית mainmainmain.

פונקציות סטטיות גלובליות

כאשר מגדירים פונקציה סטטית מחוץ למחלקה, אנו אומרים בעצם שאפשר להשתמש בפונקציה אך ורק בתוך הקובץ שהיא נכתבה בו.
נשתמש בזה בשביל פונקציות עזר שאינן חלק מהממשק.
פונקציות סטטיות אינן מוכרזות בקובץ hhh.

דוגמה

static int square(int n)
{
	return n * n;
}

וידוא הנחות בזמן ריצה

ישנו המקרו assertassertassert אשר מאפשר לנו בזמן בדיקה בלבד לבדוק האם יש בעיות בקוד שלנו.

assert(<expression>);

המאקרו מוגדר בספרייה <cassert> ובזמן הריצה הביטוי מוערך ונבדק:

  • אם הוא נכון, לא קורה כלום.
  • אם הוא אינו נכון, התוכנית נעצרת ומודפסת הודעה המפרטת את מיקום ההנחה שכשלה.

ניתן לכבות את המקרו assertassertassert ע"י הידור התכנית עם הדגל -DNDEBUG. כך ניתן לשחרר גרסה סופית של הקוד שאינה מואטת ע"י הבדיקות ללא הסרתן ידנית.

הערה

שימו שקוד שבתוך ה-assertassertassert לא יורץ כלל אם המאקרו כבוי.

מתי משתמשים ב-assertassertassert

  • ב-assertassertassert משתמשים לוידאו נכונות של הנחות הנעשות בקוד.
    • אם ההנחות שגויות ייתכן וקיימים באגים
    • נוח לבדוק עם assertassertassert את נכונות הארגומנטים, ערכי החזרה ואינווריאנטות של טיפוסי נתונים.

לא משתמשים ב-assertassertassert כדי לבדוק קלט מהמשתמש או עבור בדיקות שרוצים שיתבצעו גם בתוכנית הסופית.