زبان برنامهنویسی سلام و C هر دو زبانهایی کامپایلی با گونههای ایستا و بدون زبالهروب (Garbage Collector) هستند و در سنجش سرعت پروژه در یک رده تمام میکنند. تفاوت در چیزی است که مینویسید و در چیزهایی که کامپایلر برایتان بررسی میکند. C زبانی کوچک، اکوسیستمی عظیم و کنترل کامل به شما میدهد، به بهای رفتار تعریفنشده (Undefined Behavior) و حسابوکتاب دستی. سلام مدیریت دستی حافظه را نگه میدارد، ولی ظرفهای رشدپذیر، دیرکن (همان defer)، بررسی مرز اندیس در ساختهای توسعه، استنتاج گونه و کامپایلری را اضافه میکند که بسیاری از اشتباههایی را که C میپذیرد رد میکند.
این مقایسهی سلام و C ابتدا برنامههای یکسان را کنار هم میگذارد و سپس حافظه، رشتهها، ایمنی، همکاری با C و سرعت را مرور میکند. سلام میتواند C را مستقیم صدا بزند، پس در بسیاری از پروژهها پرسش این نیست که «سلام یا C»، بلکه این است که «کدام بخش به کدام زبان».
مقایسهی زبان سلام و C در یک نگاه
| C | سلام | |
|---|---|---|
| نخستین عرضه | ۱۹۷۲، استانداردشده از ۱۹۸۹ (C23 نسخهی کنونی است) | ۲۰۲۴ بنا بر سرآیندهای کد منبع، هنوز پیش از نسخهی ۱٫۰ |
| کامپایل | بومی، با gcc، clang، MSVC و بسیاری کامپایلرهای دیگر | بومی، با پشتیبانهای C، LLVM یا x86-64/ARM خود سلام |
| حافظه | malloc/free، دستی | مقدارهای پشته بهعلاوهی ظرفهای هیپ که با .آزادکن() و دیرکن آزاد میشوند |
| آرایهها و نگاشتهای رشدپذیر | خودتان بنویسید یا کتابخانهای انتخاب کنید | وکتور<T> و نگاشت<K, V> درون زبان، و مجموعه (Set) و ظرفهای دیگر در کتابخانهی استاندارد |
| رشتهها | char * با یک بایت صفر در پایان | رشته، متن UTF-8 با عملیات بایتی و نویسهای |
| دسترسی بیرون از مرز آرایه | رفتار تعریفنشده | خطای کامپایل وقتی اثباتپذیر باشد، توقف (Panic) هنگام اجرا در ساختهای توسعه |
| سرریز عدد علامتدار | رفتار تعریفنشده | در ساخت توسعه چرخشی است؛ با --release پشتیبان LLVM فرض میکند رخ نمیدهد |
| برنامهنویسی عام (Generics) | ماکروها و void * | تابعها و گونههای عام |
| مدیریت خطا | کدهای برگشتی و errno | خطادار، تلاش و گرفتن خطا، یا جداشمارهای نتیجه |
| کتابخانهی استاندارد | کوچک؛ برای بقیه POSIX و کتابخانههای بیرونی | بزرگ: HTTP، پایگاه داده، JSON، رمزنگاری، فشردهسازی و بیشتر |
| اکوسیستم و جامعه | عظیم، با چند دهه سابقه | کوچک و جوان |
یک کار، به دو زبان
یک خط از عددهای جداشده با ویرگول را بگیرید و تعداد، جمع، کوچکترین و بزرگترین آنها را گزارش کنید. در C ابزارهای معمول strtok_r و strtol هستند. strtok_r در همان رشتهای که میشکافد مینویسد، پس رشتهی ثابت باید اول کپی شود:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
const char *line = "42,7,19,88,3,61";
char *copy = strdup(line);
if (copy == NULL) {
return 1;
}
long sum = 0, lo = 0, hi = 0;
int count = 0;
char *save = NULL;
for (char *tok = strtok_r(copy, ",", &save); tok != NULL;
tok = strtok_r(NULL, ",", &save)) {
long n = strtol(tok, NULL, 10);
sum += n;
if (count == 0 || n < lo) lo = n;
if (count == 0 || n > hi) hi = n;
count++;
}
free(copy);
printf("count: %d sum: %ld min: %ld max: %ld\n", count, sum, lo, hi);
return 0;
}strdup در C23 و POSIX هست و strtok_r در POSIX، پس روی ویندوز باید سراغ strtok_s بروید. نسخهی سلام از رشته.تفکیک کردن (در انگلیسی str.Split) استفاده میکند که یک وکتور<رشته> برمیگرداند:
واردسازی رشته
تابع ریشه:
خط := "42,7,19,88,3,61"
تکهها := رشته.تفکیک کردن(خط، ",")
دیرکن تکهها.آزادکن()
ناپایا جمع := ۰
ناپایا کمینه := ۰
ناپایا بیشینه := ۰
هر ش، تکه در تکهها:
عدد := تکه.به صحیح()
جمع += عدد
اگر ش == ۰ یا عدد < کمینه: کمینه = عدد پایان
اگر ش == ۰ یا عدد > بیشینه: بیشینه = عدد پایان
پایان
سرچاپ "تعداد:"، تکهها.طول()، "جمع:"، جمع، "کمینه:"، کمینه، "بیشینه:"، بیشینه
پایانهر دو همان نتیجه را چاپ میکنند. نسخهی سلام بیشتر از آن رو کوتاهتر است که ظرف و شکافتن رشته از پیش وجود دارند. به آنچه تغییر نکرده هم دقت کنید: هنوز یک آزادسازی هست، که یک بار و درست پس از تخصیص به شکل دیرکن تکهها.آزادکن() نوشته شده و هر طور که تابع تمام شود اجرا میشود. سلام مدیریت حافظه را پنهان نمیکند؛ الگوی رایج را کوتاه میکند.
ساختارها، آرایههای پویا و حافظه
C آرایهی رشدپذیر ندارد، پس هر برنامهنویس C دیر یا زود یکی مینویسد. این نسخهای کوچک و دقیق است که خوانشهای دمای هوا را نگه میدارد و میانگین و گرمترین شهر را گزارش میکند:
#include <stdio.h>
#include <stdlib.h>
struct reading {
const char *city;
double celsius;
};
struct readings {
struct reading *items;
size_t len;
size_t cap;
};
static int push(struct readings *v, struct reading r)
{
if (v->len == v->cap) {
size_t cap = v->cap ? v->cap * 2 : 4;
struct reading *items = realloc(v->items, cap * sizeof *items);
if (items == NULL) {
return -1;
}
v->items = items;
v->cap = cap;
}
v->items[v->len++] = r;
return 0;
}
int main(void)
{
struct readings v = {0};
if (push(&v, (struct reading){"Tehran", 31.5}) != 0 ||
push(&v, (struct reading){"Tabriz", 24.0}) != 0 ||
push(&v, (struct reading){"Shiraz", 29.5}) != 0 ||
push(&v, (struct reading){"Rasht", 26.5}) != 0) {
free(v.items);
return 1;
}
double total = 0.0;
struct reading hottest = v.items[0];
for (size_t i = 0; i < v.len; i++) {
total += v.items[i].celsius;
if (v.items[i].celsius > hottest.celsius) {
hottest = v.items[i];
}
}
printf("readings: %zu\n", v.len);
printf("average: %g\n", total / (double)v.len);
printf("hottest: %s\n", hottest.city);
free(v.items);
return 0;
}همین برنامه به سلام، که در آن وکتور<خوانش> کار رشد کردن را انجام میدهد:
ساختار خوانش:
همگانی شهر: رشته
همگانی دما: اعشار۶۴
پایان
تابع ریشه:
ناپایا خوانشها := وکتور {} برگردان وکتور<خوانش>
دیرکن خوانشها.آزادکن()
خوانشها.بیفزا(خوانش { شهر = "تهران"، دما = ۳۱.۵ })
خوانشها.بیفزا(خوانش { شهر = "تبریز"، دما = ۲۴.۰ })
خوانشها.بیفزا(خوانش { شهر = "شیراز"، دما = ۲۹.۵ })
خوانشها.بیفزا(خوانش { شهر = "رشت"، دما = ۲۶.۵ })
ناپایا مجموع := ۰.۰
ناپایا گرمترین := خوانشها.بگیر(۰)
هر خ در خوانشها:
مجموع += خ.دما
اگر خ.دما > گرمترین.دما:
گرمترین = خ
پایان
پایان
سرچاپ "تعداد خوانشها:"، خوانشها.طول()
سرچاپ "میانگین:"، مجموع / (خوانشها.طول() برگردان اعشار۶۴)
سرچاپ "گرمترین:"، گرمترین.شهر
پایانمدل حافظهی زیرین به مدل C نزدیک است. عددها، مقدارهای منطقی و ساختارهای ساده روی پشته زندگی میکنند و هنگام انتساب کپی میشوند. یک وکتور، یک نگاشت و رشتهای که هنگام اجرا ساخته شود روی هیپ زندگی میکنند و کسی باید آزادشان کند. تابعی که یک ظرف یا یک رشتهی تازه برمیگرداند، مالکیت را به فراخواننده میسپارد. کپی کردن یک متغیر وکتور سرآیند آن را کپی میکند نه عنصرهایش را، پس دو متغیر میتوانند یک فضای ذخیره را شریک باشند، درست مثل دو اشارهگر C به یک میانگیر. .رونوشت() یک کپی مستقل میسازد.
آنچه سلام اضافه میکند مجموعهای از بررسیهای زمان کامپایل روی همین مدل است. استفاده از اشارهگر پس از mem.Free خطای E139 است، آزاد کردن دوباره E140، برگرداندن مقداری که دیرکن خود تابع آزادش میکند E141، و نگه داشتن اشارهگری به درون یک وکتور در طول یک بیفزا که ممکن است فضای آن را جابهجا کند، هشدار W144. اینها بررسی الگوهای رایجاند، نه اثبات اینکه برنامه از نظر حافظه ایمن است؛ نوشتهی مدیریت حافظه در سلام و درس حافظه آنها را مرور میکنند. برای آنچه این بررسیها از دست میدهند، salam memcheck برنامه را با AddressSanitizer میسازد و اجرا میکند.
رشتهها: اشارهگر به char در برابر متن UTF-8
رشتهی C اشارهگری به بایتهایی است که با یک بایت صفر تمام میشوند. طولش با پیمایش پیدا میشود، فضای ذخیرهاش مسئولیت کسی است و «یک نویسه» یعنی یک بایت. رشته در سلام متن UTF-8 است با متدهایی برای هر دو نگاه:
s.طول()وstr.Substrبایتها را میشمارند، مانندstrlenدر C؛s.شمارنویسه()،str.CharSubstrوهر ن در sبا نقطهکدهای یونیکد کار میکنند، و این برای فارسی، عربی یا هر متنی بیرون از ASCII اهمیت دارد؛+رشتهها را به هم میچسباند و عددها را تبدیل میکند ("n=" + 3)، وstr.NewBuilderمتنهای بلند را با کارایی میسازد.
در زیر، رشتهی سلام هنوز با صفر تمام میشود، و برای همین میشود آن را مستقیم به C داد؛ و باز برای همین، دادهی دودوییای که بایت صفر دارد جایش در میانگیرهای بایتی است نه در رشته. هر + یک رشتهی تازه روی هیپ میسازد، پس حلقهای طولانی که رشته به هم میچسباند باید از یک سازندهی رشته (Builder) استفاده کند و نتیجه را آزاد کند؛ همان انضباطی که C با snprintf و میانگیرها از شما میخواهد.
ایمنی: رفتار تعریفنشده و آنچه سلام بررسی میکند
بزرگترین هزینهی C رفتار تعریفنشده است: خواندن بیرون از مرز آرایه، سرریز یک عدد صحیح علامتدار یا استفاده از حافظهی آزادشده به کامپایلر اجازه میدهد فرض کند هرگز رخ نمیدهد، و نتیجه میتواند هر چیزی باشد. ابزارهایی مانند پاکسازها (Sanitizers)، تحلیلگرهای ایستا و بازبینی دقیق کد، این مشکل را در پروژههای خوب C مهار میکنند، ولی خود زبان کمکی نمیکند.
سلام چند تا از این درها را میبندد:
- بررسی مرز اندیس. وقتی کامپایلر بتواند ثابت کند اندیسی بیرون از محدوده است، برنامه کامپایل نمیشود. برای آرایهی ثابت
آ := [۱، ۲، ۳]، نوشتنآ[۳]خطایE021است؛ برایوکتوری که کامپایلر طولش را دنبال میکند، خواندن اندیسی برابر با طول آن خطایE146است با پیام «اندیس خارج از محدوده است: 'ای' برابر ۲ است، اما 'و۱' ۲ عضو دارد». وقتی اندیس فقط هنگام اجرا معلوم باشد، ساخت توسعه آن را بررسی میکند و به جای خواندن حافظهی دیگران باsalam panic: Vector.get: index out of boundsمتوقف میشود. - سرریز عدد صحیح. در ساخت توسعه، حساب علامتدار به شکل قابل پیشبینی میچرخد. بررسیهای بازه در زمان کامپایل، سرریز، تقسیم بر صفر یا دسترسی به اشارهگر تهیای را که همیشه رخ میدهد خطا، و آن را که ممکن است رخ دهد هشدار گزارش میکنند.
- مقداردهی اولیه و گونهها. متغیرها همراه با مقدار تعریف میشوند، تغییرناپذیرند مگر با
ناپایاتعریف شوند، و تبدیل میان گونهها صریح است.
در عوض، --release بررسیهای مرز اندیس هنگام اجرا را برای سرعت برمیدارد و به بهینهساز اجازه میدهد فرض کند حساب علامتدار سرریز نمیکند؛ یعنی رفتار C را در ازای سرعت C برمیگرداند. بسیاری از تیمها با ساخت توسعه آزمایش میکنند و ساخت انتشار را منتشر میکنند، و تا وقتی آزمونها خوب باشند این تقسیم کار معقولی است.
صدا زدن C از سلام
سلام طوری طراحی شده که کنار C زندگی کند، نه اینکه یکباره جای آن را بگیرد. یک بلوک درونداد: (در انگلیسی extern:) تابعهای C را اعلام میکند و شما آنها را مثل تابعهای سلام صدا میزنید. نامهای C در کد فارسی هم همان نامهای C میمانند:
درونداد:
تابع strlen(s: رشته): اندازه مثبت
تابع labs(n: صحیح۶۴): صحیح۶۴
تابع sqrt(x: اعشار۶۴): اعشار۶۴
پایان
تابع ریشه:
درود := "سلام"
سرچاپ "تعداد بایت به گفتهی C:"، strlen(درود)
سرچاپ "تعداد حرف به گفتهی سلام:"، درود.شمارنویسه()
سرچاپ "labs(-42):"، labs(-۴۲ برگردان صحیح۶۴)
سرچاپ "sqrt(2):"، sqrt(۲.۰)
پایانواژهی «سلام» ۸ بایت و ۴ حرف است؛ یادآوری کوچکی از اینکه چرا دو نگاه به رشته وجود دارد. پیوند پویا "name" و پیوند ایستا "name" کتابخانهها را اضافه میکنند، ساختارها طبق ABI زبان C روی آن سکو به شکل مقدار منتقل میشوند، و یک بلوک برونداد: (در انگلیسی export:) به یک تابع سلام نام ثابتی در C میدهد تا کد C بتواند آن را صدا بزند، مثلاً به عنوان تابع مقایسهی qsort. در جهت برعکس، salam build --lib یک کتابخانهی اشتراکی یا ایستا همراه با یک سرآیند C برای تابعهایی که صادر میکنید میسازد. درس همکاری با C و نوشتهی FFI در سلام این موضوع را به تفصیل بررسی میکنند.
سرعت و ابزارها: سلام در برابر C
در سنجش پروژه روی ۳۹ برنامهای که در همهی زبانها الگوریتم یکسان دارند، میانگین سلام ۱۰٫۶ میلیثانیه و میانگین C ۱۱٫۷ میلیثانیه بود، با C ساختهشده با gcc -O3 و سلام ساختهشده با پشتیبان LLVM و -O3. این یعنی یک رده، نه برد: بیشتر برنامهها چند میلیثانیه اجرا میشوند، پس راهاندازی فرایند بخشی از هر عدد است، و فاصلههای کمتر از حدود ۵٪ از اجرایی به اجرای دیگر تغییر میکنند. C در برنامههایی مانند n وزیر (۳۵٫۷ میلیثانیه در برابر ۴۱٫۱)، فیبوناچی بازگشتی (۲٫۴ در برابر ۳٫۱) و یک حلقهی عدد تصادفی LCG (۱۷٫۹ در برابر ۱۹٫۴) جلوتر بود. صفحهی سرعت همهی عددها را دارد و خوانش دقیق سنجش سرعت توضیح میدهد چرا نباید میانگینها را رتبهبندی خواند.
از نظر ابزار، C مجموعهای از کامپایلرها، خطایابها، نمایهگیرها و تحلیلگرهای درجهیک در اختیارتان میگذارد که با یک سیستم ساخت مانند Make یا CMake کنار هم میگذاریدشان. سلام یک فایل اجرایی دارد که میسازد، آزمون اجرا میکند (salam test)، قالببندی میکند، مستندات میسازد، یک سرور زبان برای ویرایشگرها اجرا میکند، gdb یا lldb را باز میکند (salam debug) و AddressSanitizer را اجرا میکند (salam memcheck). ابزارهای C عمیقترند؛ ابزارهای سلام یکجا جمعاند.
کی C و کی زبان سلام را انتخاب کنیم؟
C را انتخاب کنید وقتی:
- برای یک میکروکنترلر، یک پردازندهی کمیاب یا سکویی مینویسید که فقط یک کامپایلر C از آن پشتیبانی میکند؛
- در یک پایگاه کد، هسته یا کتابخانهی موجود C مشارکت میکنید؛
- به یک ABI پایدار و استاندارد زبانی نیاز دارید که دههها زیر پایتان عوض نشود؛
- گواهینامه، ممیزی یا تجربهی تیم آن را لازم میکند.
سلام را انتخاب کنید وقتی:
- کد بومی و کنترلی در سطح C میخواهید، ولی ترجیح میدهید ظرفها، کار با رشته و لولهکشی مدیریت خطا را خودتان ننویسید (نوشتهی گشتی در کتابخانهی استاندارد سلام را ببینید)؛
- برنامه یک نرمافزار کاربردی است (ابزار خط فرمان، سرور، سرویس) که در آن یک کتابخانهی استاندارد بزرگ هفتهها صرفهجویی میکند؛
- بررسیهای زمان کامپایل و بررسی مرز اندیس در ساخت توسعه را میخواهید، بدون اینکه به زبانی با زبالهروب بروید؛
- میخواهید به فارسی برنامه بنویسید یا به فارسی آموزش دهید، با همان کامپایلر و همان کتابخانهها (یک زبان، دو خط).
به یاد داشته باشید که سلام هنوز به نسخهی ۱٫۰ نرسیده است. زبان میان نسخهها هنوز تغییر میکند، جامعهاش کوچک است و نمونههای استقرار در محیط عملیاتی که بشود از آنها آموخت کماند. یک راه میانهی عملی این است که کتابخانههای آزمودهی C را به C نگه دارید و برنامهی دور آنها را به سلام بنویسید.
پرسشهای پرتکرار
آیا زبان سلام جایگزین C است؟
برای نرمافزارهای کاربردی، اغلب بله؛ برای همهی کاربردهای C، نه. سلام همان ردهی سرعت بومی را با کمک بیشتری از سوی کامپایلر پوشش میدهد، ولی C همچنان زبان سیستمعاملها، سفتافزارهای نهفته و ABIهای پایدار کتابخانههاست، و خود سلام روی بیشتر سکوها به محیطهای اجرای C تکیه دارد.
آیا زبان سلام از نظر حافظه ایمن است؟
نه به آن شکلی که Rust هست. سلام در ساختهای توسعه بررسی مرز اندیس دارد، در زمان کامپایل استفاده پس از آزادسازی، آزادسازی دوباره و اشتباههای مشابه را بررسی میکند، و AddressSanitizer را از راه salam memcheck در اختیار میگذارد؛ ولی حافظهی هیپ را هنوز خودتان آزاد میکنید و کامپایلر ایمنی برنامه را اثبات نمیکند.
آیا سلام میتواند از کتابخانههای موجود C استفاده کند؟
بله. تابعها را در یک بلوک درونداد: اعلام کنید و کتابخانه را با پیوند پویا یا پیوند ایستا پیوند دهید. ساختارها و تابعهای بازگشتی (Callback) از ABI زبان C روی سکوی هدف پیروی میکنند.
آیا سلام به C کامپایل میشود؟
میتواند. پشتیبان C کد C تولید میکند و آن را به یک کامپایلر C میسپارد، و --emit-c آن کد C را بیرون مینویسد تا بررسیاش کنید یا با ابزارهای خودتان کامپایلش کنید. سلام یک پشتیبان LLVM و تولیدکنندهی کد بومی خودش را هم دارد.