יסודות השפה: טיפוסים, משתנים, פונקציות ואופרטורים
סדנה בתכנות C ו-C++ — הרצאה 1 — HUJI
היסטוריה של C ו-C++
- C (שנות ה-70): פותחה במקביל ל-UNIX על ידי Ritchie & Kernighan במעבדות Bell של AT&T. פשוטה להמרה לשפת מכונה, מהירה ויעילה, תכנות ברמה נמוכה.
- C++ (שנות ה-80): הרחבה מונחית-עצמים של C על ידי Stroustrup במעבדות Bell. תוכננה לפרויקטים גדולים ויעילים.
- Python (שנות ה-90): שפה מפורשת (interpreted) למטרות כלליות. ברמה גבוהה אך איטית יותר.
היררכיית שפות תכנות
מרמה נמוכה לרמה גבוהה: Assembly -> C -> C++ -> Python
C היא שפה רזה וממוקדת -- יש בה רק 32 מילות מפתח. שוו את כולן!
למה משמשת C?
C משמשת לתכנות מערכות:
- מערכות הפעלה (למשל Linux)
- מיקרו-בקרים: מכוניות ומטוסים
- מעבדים משובצים: טלפונים, אלקטרוניקה ניידת
- מכשור רפואי
- דרייברים
- כל מקום עם מגבלות הדוקות על זיכרון וזמן מעבד
ב-C אין בדיקות בזמן ריצה (חריגה מגבולות מערך, גישה לא חוקית לזיכרון) ואין ניהול זיכרון אוטומטי -- המתכנת חייב לנהל את הזיכרון ידנית.
C++ -- הרחבה מונחית-עצמים של C
- מחלקות ומתודות: עיצוב מונחה-עצמים (OO)
- תכנות גנרי (יותר): Templates מאפשרים שימוש חוזר בקוד
- מערכת טיפוסים מחמירה יותר
- בדיקות מסוימות בזמן ריצה ושליטה בזיכרון
סגנון כתיבת קוד
- סגנון כתיבה הוא אישי (אם כי לא תמיד בקורס)
- CamelCase:
aRatherLongSymbolName - snake_case:
a_rather_long_symbol_name - פרויקטים גדולים דורשים תקני סגנון כתיבה
- סגנון כתיבה טוב הופך את הקוד להרבה יותר קריא
Hello World
תוכנית ראשונה ב-C
#include <stdio.h>
int main()
{
printf("Hello class!\n");
return EXIT_SUCCESS;
}
EXIT_SUCCESS (מוגדר ב-<stdlib.h>) ברור יותר מאשר להחזיר 0.
כתבו קוד נקי ומתועד-עצמית
- שמות ברורים, עקביים ותמציתיים
- בלוקי קוד קצרים
- תקשרו את המטרה של כל בלוק (באופן אידיאלי מטרה אחת לבלוק)
- הערות קצרות שמסבירות למה, במיוחד להחלטות לא טריוויאליות
- תעדו את הממשק שלכם
קומפילציה והרצה
קומפילציה ברירת מחדל
gcc hello.c # generates executable a.out
./a.out # run the program
קומפילציה עם אפשרויות
gcc -std=c99 -Wall hello.c -o hello
./hello
| דגל | מטרה |
|---|---|
-std=c99 | שימוש בתקן C99 |
-Wall | הפעלת רוב אזהרות הקומפיילר |
-o hello | קביעת שם קובץ הפלט |
-g | הפעלת debugging |
טיפוסים ומשתנים
טיפוס סטטי מול טיפוס דינמי
# Python (dynamically typed)
x = 4
x = "I am a string!" # OK in Python
// C (statically typed)
int x = 0;
x = "I am a string!"; // Error!
הכרזה על משתנים
C היא שפה בעלת טיפוסים סטטיים -- לכל משתנה יש טיפוס שחייבים להכריז עליו בזמן קומפילציה:
<type> <name>;
int x; // declaration
int x, y; // multiple declarations
int x = 0; // declaration with initialization (recommended!)
משתנים מקומיים שלא אותחלו מכילים ערכי זבל -- תמיד אתחלו את המשתנים שלכם.
טיפוסי משתנים ב-C
טיפוסים פשוטים:
- טיפוסים אריתמטיים (int, char, float וכו')
- מצביעים (Pointers)
- טיפוסי מניה (Enumeration)
טיפוסים מורכבים:
- מערכים (Arrays)
- מבנים (Structs)
המשתמש יכול להגדיר טיפוסים חדשים עם struct וליצור כינויים עם typedef.
טיפוסי נתונים אריתמטיים
טיפוסים שלמים (Integer Types)
char c = 'A';
short s = 0;
int x = 1;
long y = 9;
// Unsigned variants
unsigned char c = 'A';
unsigned short s = 0;
unsigned int x = 1;
unsigned long y = 9;
צורות אתחול נוספות:
char a = 'A', b = 'B';
char c = 65; // char is also a number!
unsigned char d = 0x3A; // hexadecimal number
int x = (y = 1); // (y = 1) returns the value assigned to y
int x = y = 1; // equivalent -- assignment evaluated right to left
טיפוסי נקודה צפה (Floating-Point)
float x = 0.0;
double y = 1.0; // approximately double precision
y = y * 1.2342f; // multiply by float literal
מצביעים (תצוגה מקדימה)
int y = 10;
int *x = &y; // x is a pointer to int, pointing to y's memory address
Enum -- טיפוס מוגדר-משתמש דמוי מספר שלם
enum { SUNDAY = 1, MONDAY, TUESDAY /* ... */ };
enum Color { BLACK, RED, GREEN, YELLOW, BLUE, WHITE = 7, GRAY };
typedef enum Seasons_tag
{
E_WINTER, // = 0 by default
E_SPRING, // = E_WINTER + 1
E_SUMMER, // = E_WINTER + 2
E_AUTUMN // = E_WINTER + 3
} Season_type;
int day = MONDAY; // 2
enum Color ca = BLUE; // 4
enum Color cb = GRAY; // 8
Season_type s = E_SUMMER; // 2
השתמשו ב-enum כדי להיפטר ממספרים קסומים (magic numbers). מספרים קסומים מקשים על קריאת הקוד ותחזוקתו.
המרת טיפוסים וקיצוץ (Type Casting and Truncation)
int x = 75;
int y = 100;
float z = x / y; // = 0.0 (integer division!)
float z = (float) x / y; // = 0.75 (cast x to float first)
int z = (float) x / y; // = 0 (truncation towards zero)
Typedef -- יצירת טיפוסים מותאמים אישית
typedef unsigned char byte;
byte x = 'A';
x++; // x will equal 'B'
x = 0xFA; // hexadecimal number
unsigned char y;
y = x; // works -- byte is just unsigned char
מגדיר const
const int x = 3; // initialization is OK
x = 5; // ERROR -- cannot assign a new value to a const type
משתנה עם טיפוס מוגדר const לא יכול להופיע בצד שמאל של אופרטור השמה.
תחום הכרה (Scope) של משתנים
ניתן להכריז על משתנים:
- בתוך בלוק -- נראים רק בתוך אותו בלוק
- מחוץ לכל הבלוקים (גלובלי) -- נראים בכל מקום
int x = 0; // global
int main()
{
int x = 1; // local, hides global
{
int x = 2; // local, hides outer scope
// x is 2
}
// x is 1 again
}
כללי תחום הכרה
- בלוקי קוד מוגדרים עם
{ו-} - אפשר לקנן בלוקים
- רק הכרזות בתחום הנוכחי או בתחומים חיצוניים נראות
- הכרזה בתחום פנימי מסתירה הכרזה בתחום חיצוני
- התחום החיצוני ביותר (גלובלי) אינו עטוף בסוגריים
- גוף פונקציה הוא גם תחום הכרה
משפטי תנאי
if / else if / else
if (conditional_expression)
{
// ...
}
else if (conditional_expression)
{
// ...
}
else
{
// ...
}
אופרטור טרנרי ? :
y = (x > 0) ? 10 : 100;
// Equivalent to:
if (x > 0)
{
y = 10;
}
else
{
y = 100;
}
Switch ו-Enum
משפט Switch
switch (n) // integer types only (int, char, short, _Bool, etc.)
{
case 1:
// code executed if n == 1
break;
case 2:
// code executed if n == 2
break;
default:
// code executed if n doesn't match any case
}
לעולם אל תשכחו break! בלי break, הביצוע "נופל" ל-case הבא. כל הפקודות שאחרי case תואם מתבצעות עד שנתקלים ב-break.
כללי ביטוי case
ביטוי ה-case חייב להיות מטיפוס שלם (integral) שניתן לחשב בזמן קומפילציה:
int a = 1, b = 2;
const int c = 3;
case (1 + 2): // OK (can be evaluated in advance)
case (a + b): // INVALID (known only at run time)
case (c): // INVALID (const means read-only, not a real constant!)
case (3.14159): // INVALID (not integral type)
case 'A': // OK (a char is an integral type!)
Enum ו-Switch ביחד
int main()
{
enum Color { RED, GREEN };
enum Color my_color;
scanf("%d", &my_color);
switch (my_color)
{
case RED:
printf("Your favorite color is red\n");
break;
case GREEN:
printf("green\n");
break;
default:
printf("Unknown color\n");
}
return 0;
}
int x = 2;
switch (x)
{
case 1: printf("Choice is 1\n");
case 2: printf("Choice is 2\n");
case 3: printf("Choice is 3\n");
default: printf("Choice other than 1, 2 and 3\n");
}
פלט (בלי break!):
Choice is 2
Choice is 3
Choice other than 1, 2 and 3
משפטי לולאה
לולאת for
for (initialization; test_condition; update)
{
// loop body
}
for (int i = 0; i < 5; i++)
{
printf("%d\n", i);
}
// Output: 0 1 2 3 4
לולאת for עם אופרטור פסיק
int i, j;
for (i = 0, j = 0; i < 10 && j < 5; i++, j += 2)
{
printf("%d %d\n", i, j);
}
// Output: 0 0 / 1 2 / 2 4
לולאות while / do-while
while (condition)
{
// ...
}
do
{
// ...
} while (condition);
אופרטורים
סוגי אופרטורים ב-C
| קטגוריה | אופרטורים |
|---|---|
| אונרי | ++ -- |
| אריתמטי | + - * / % |
| יחסי (השוואה) | < <= > >= == != |
| לוגי | && || ! |
| ביטואי (Bitwise) | & | ^ << >> ~ |
| השמה | = += -= *= /= %= <<= >>= &= ^= |= |
| טרנרי | ? : |
| גודל | sizeof() |
| מצביע | * (dereference) |
| כתובת | & |
אופרטורים אריתמטיים
סדר הקדימויות עוקב אחר האלגברה הסטנדרטית: סוגריים קודם, אחר כך * ו-/, אחר כך + ו--, חישוב משמאל לימין.
float x = 3 / 2; // = 1 (integer division!)
float y = 3.0 / 2; // = 1.5
int z = 3.0 / 2; // = 1 (truncated)
int r = 3 % 2; // = 1 (remainder)
הגדלה והקטנה (Increment and Decrement)
x++; // equivalent to x = x + 1
y = x++; // y = x; then x = x + 1 (post-increment)
y = ++x; // x = x + 1; then y = x (pre-increment)
השמה מול אתחול
int x = 5; // Initialization (during declaration)
x = 3; // Assignment (after declaration)
x *= 5; // Compound assignment
const int y = 5; // OK -- initialization
y = 10; // ERROR -- const cannot be assigned
כל אופרטורי ההשמה מחזירים ערך: int x = (y = 3); מציב גם ב-y וגם ב-x את הערך 3.
פונקציות
הכרזה על פונקציה (Prototype)
int power(int a, int b); // return type, name, parameters
הגדרת פונקציה (Definition)
int power(int a, int b)
{
int p = 1;
for (int i = 0; i < b; ++i)
{
p *= a;
}
return p;
}
אם פונקציה לא הוכרזה קודם, ההגדרה משמשת גם כהכרזה.
פרוצדורות (פונקציות void)
void do_something(int a, int b)
{
// ...
return; // optional -- return without a value
}
סדר הכרזת פונקציות
ב-C99, פונקציה מכירה רק הכרזות קודמות (אלו שמעליה בקובץ המקור).
// ERROR: func_c not declared yet
void func_b()
{
func_c(); // Error!
}
void func_c()
{
func_b(); // OK -- func_b was declared above
}
הכרזה מקדימה (Forward Declaration)
השתמשו בהכרזות מקדימות כדי לפתור בעיות סדר:
void func_c(int param); // Forward declaration
void func_b()
{
func_c(7); // OK now
}
void func_c(int param)
{
// definition
}
הכרזה ";" מול הגדרה "{}"
- הכרזה: מספקת לקומפיילר את שם הפונקציה, טיפוסי הפרמטרים, וטיפוס הערך המוחזר.
void func_c(int param); - הגדרה: מקצה "זיכרון קוד" לגוף הפונקציה.
void func_c(int param) { // body }
קבצי Header וקבצי Source
// power.h -- interface
#ifndef _POWER_H
#define _POWER_H
/**
* Computes integer power
* @return base to the power of n
*/
int power(int base, int n);
#endif // _POWER_H
// power.c -- implementation
#include "power.h"
int power(int base, int n)
{
int p = 1;
for (int i = 0; i < n; ++i)
{
p *= base;
}
return p;
}
// main.c -- using the power interface
#include <stdio.h>
#include "power.h"
int main()
{
for (int i = 0; i < 10; i++)
{
printf("2 ^ %d = %d\n", i, power(2, i));
}
return EXIT_SUCCESS;
}
פונקציות ללא ארגומנטים
void foo(); // In a declaration: undetermined number of arguments (obsolescent, don't use)
void foo(void); // In a declaration: explicitly no arguments
// In a definition, both mean "no arguments":
void foo() { }
void foo(void) { }
טיפוסים בוליאניים
בוליאניים ב-C הם פשוט מספרים שלמים
_Bool הוא מספר שלם ללא סימן שיכול לאחסן 0 ו-1. כל טיפוס שלם אחר ניתן להמרה ל-_Bool:
- אפס = false
- שונה מאפס = true
while (1) { } // infinite loop (1 is true)
if (-1974) { } // executes (non-zero is true)
int i = (3 == 4); // i = 0 (false)
stdbool.h
מגדיר bool, false ו-true בתור _Bool, 0 ו-1:
#include <stdbool.h>
bool loves_me = false;
while (true)
{
loves_me = !loves_me;
printf(loves_me ? "loves me\n" : "loves me not\n");
}
int loves_me = 5;
if (loves_me == true) // Won't work! 5 != 1
if (loves_me) // Works! Any non-zero is true
כשמשימים ערך ל-bool/_Bool, כל ערך שונה מאפס הופך ל-1. אבל int רגיל שומר על הערך שלו.
assert() -- ניפוי באגים עם ביטויים בוליאניים
#include <assert.h>
int main()
{
const int b = 6;
assert(b != 3); // makes sure we're not dividing by zero
float c = 1.0f / (b - 3);
return 0;
}
assert() משמש לבדיקות שפיות במהלך פיתוח, ובדרך כלל מושבת במצב production באמצעות NDEBUG.
הנחיית #define (מאקרו)
מאקרו מסוג אובייקט (Object-like Macros)
מזהה פשוט שמוחלף בקטע קוד בשלב העיבוד המקדים (preprocessing):
#define PI 3.141256f
double circumference(double R)
{
return 2 * PI * R; // preprocessor substitutes PI for 3.141256f
}
מאקרו מסוג פונקציה (Function-like Macros)
#define SQUARE(X) ((X) * (X)) // parentheses keep you safe!
double square_difference(double x, double y)
{
return SQUARE(x - y); // expands to ((x-y) * (x-y))
}
בלי סוגריים, הרחבת המאקרו עלולה לייצר תוצאות שגויות:
#define SQUARE(X) X*X
SQUARE(x - y) // expands to x-y*x-y (wrong!)
תמיד עטפו את פרמטרי המאקרו ואת הביטוי כולו בסוגריים.
מאקרו רב-שורתי
השתמשו ב-\ להמשך שורה:
#define x (5 + \
5)
// x == 10
חלופות למאקרו
- קבועים:
enum { FOO = 1 };אוconst int FOO = 1; - פונקציות inline (C99, C++): יידונו בהמשך הקורס