مقدمه¶
تا اینجای درس، هر متغیر برای ما یک نام بود و کامپایلر مدیریت حافظه را خودش به عهده داشت. اما چند سؤال را بیپاسخ گذاشتهایم. چرا scanf حتماً به & نیاز دارد؟ چرا تابعی مثل swap که دو متغیر را جابهجا کند با روشهای فعلی ما کار نمیکند؟ و چرا تابع میتواند عناصر آرایهی ورودی را تغییر دهد ولی متغیر ساده را نه؟ پاسخ هر سه یک کلمه است: آدرس.
این بخش دو قدم دارد. اول اشارهگرها (Pointers) را یاد میگیریم، یعنی متغیرهایی که بهجای یک مقدار، آدرس یک خانهی دیگر را نگه میدارند. بعد با همین ابزار سراغ حافظهی پویا میرویم. آرایهی معمولی باید از قبل اندازه داشته باشد، ولی تعداد دانشجویان یک کلاس را تا موقع اجرا نمیدانیم. با malloc و دوستانش اندازه را هنگام اجرا تعیین میکنیم.
سناریوی بخش قبل، یعنی سیستم مدیریت نمرات دانشجویان، اینجا هم ادامه دارد: کمترین و بیشترین نمره را با اشارهگر بیرون میدهیم و آرایهی نمرات را دقیقاً بهاندازهی تعداد دانشجویان میسازیم.
نقشه کلی بخش:¶
Memory → Address (&) → Pointer (*) → Pointer + Function (swap, min_max)
│
├──► Pointer + Array → Array Decay → Pointer Arithmetic
└──► Pointer to Pointer / void* / Function Pointer
│
Stack vs Heap → malloc / calloc / realloc / free → Dynamic Array
│
Memory Leak / Dangling Pointer / Double Free → AddressSanitizerفصل ۱۴ : Pointers¶
14.1 حافظه و آدرس¶
حافظه را مثل یک خیابان بلند تصور کنید که هر خانهاش شمارهای دارد و یک بایت جا دارد. وقتی متغیری تعریف میکنیم، کامپایلر چند خانهی پشتسرهم برایش برمیدارد: int معمولاً ۴ خانه و char یک خانه. شمارهی اولین خانهی متغیر همان آدرس آن است.
address content variable
+---------+--------------+------------+
| 0x1000 | 20 | age | <- int : 4 bytes
| 0x1004 | 3.5 | price | <- float: 4 bytes
| 0x1008 | 'A' | grade | <- char : 1 byte
+---------+--------------+------------+عملگر & (بخوانید «آدرسِ…») آدرس یک متغیر را میدهد و قالب %p آن را چاپ میکند:
#include <stdio.h>
int main(void)
{
int age = 20;
printf("value : %d\n", age);
printf("address : %p\n", (void *)&age);
return 0;
}خروجی (نمونه):
value : 20
address : 0x7ffd5e3b2a1cآدرس واقعی در هر سیستم و هر بار اجرا فرق میکند و این کاملاً طبیعی است. مهم این است که هر متغیر آدرسی دارد و میتوانیم آن را بگیریم.
14.2 اشارهگر و عملگر *¶
اشارهگر متغیری است که مقدارش یک آدرس است. تعریف int *p; یعنی p آدرسِ یک int را نگه میدارد. نوع اشارهگر مهم است، چون برای خواندن از یک آدرس باید بدانیم چند بایت و به چه شکلی بخوانیم. دو عملگر کار را کامل میکنند: &x آدرسِ x را میدهد و *p محتوای آدرسی را که p نگه میدارد، چه برای خواندن و چه برای نوشتن.
x p
+-----------+ +-------------+
| 10 | <----------- | 0x1000 | p "points to" x
+-----------+ +-------------+
address 0x1000 address 0x2000#include <stdio.h>
int main(void)
{
int x = 10;
int *p = &x; /* p holds the address of x */
printf("x = %d\n", x);
printf("*p = %d\n", *p); /* read through the pointer */
*p = 99; /* write through the pointer */
printf("x = %d\n", x); /* x changed! */
return 0;
}x = 10
*p = 10
x = 9914.3 اشارهگر بیمقصد و NULL¶
اشارهگری که مقدار اولیه نگرفته باشد به یک آدرس تصادفی اشاره میکند و نوشتن در آن رفتار تعریفنشده دارد: ممکن است برنامه همان لحظه کرش کند یا بیصدا دادههای دیگر را خراب کند.
int *p; /* garbage address */
*p = 5; /* undefined behaviour */اشارهگری که هنوز مقصدی ندارد را برابر NULL قرار میدهیم و پیش از استفاده بررسی میکنیم. قاعده ساده است: هر اشارهگر یا به یک آدرس معتبر اشاره میکند یا NULL است.
#include <stdio.h>
int main(void)
{
int *p = NULL;
if (p == NULL)
printf("p points to nothing yet\n");
int x = 8;
p = &x;
if (p != NULL)
printf("now *p = %d\n", *p);
return 0;
}p points to nothing yet
now *p = 8یک نکتهی تاریخی: تونی هوآر، که ایدهی «ارجاع تهی» را در سال ۱۹۶۵ وارد زبان ALGOL W کرد، در سال ۲۰۰۹ آن را «اشتباه یکمیلیارد دلاری» خودش نامید، چون در دههها بیشمار خطا و کرش از آن به وجود آمد.
14.4 اشارهگر و تابع¶
در C پارامترهای تابع بهصورت کپی ارسال میشوند. مثل این است که به همکارتان فتوکپیِ یک برگه را بدهید؛ هرچه روی کپی بنویسد، برگهی اصلی عوض نمیشود. اما اگر آدرسِ برگه را بدهید، همکارتان میتواند خودش سراغ آن برود و آن را تغییر دهد.
#include <stdio.h>
void swap_wrong(int a, int b)
{
int t = a; a = b; b = t;
}
void swap(int *a, int *b)
{
int t = *a; *a = *b; *b = t;
}
int main(void)
{
int x = 3, y = 7;
swap_wrong(x, y);
printf("after swap_wrong: x=%d y=%d\n", x, y);
swap(&x, &y);
printf("after swap : x=%d y=%d\n", x, y);
return 0;
}after swap_wrong: x=3 y=7
after swap : x=7 y=3همین توضیح میدهد که چرا مینویسیم scanf("%d", &x): scanf باید مقدار خواندهشده را داخل خودِ x بگذارد، پس آدرس x را لازم دارد.
کاربرد مهم دیگر این است که تابع بتواند بیش از یک نتیجه برگرداند. return فقط یک مقدار میدهد، ولی با اشارهگر میتوانیم نتیجههای دیگر را «بیرون بفرستیم». در سیستم نمرات، کمترین و بیشترین نمره را همزمان پیدا میکنیم. کلمهی const به خواننده میگوید که تابع آرایه را فقط میخواند و تغییرش نمیدهد.
#include <stdio.h>
void min_max(const int a[], int n, int *min, int *max)
{
*min = *max = a[0];
for (int i = 1; i < n; i++) {
if (a[i] < *min) *min = a[i];
if (a[i] > *max) *max = a[i];
}
}
int main(void)
{
int scores[] = {18, 15, 20, 12, 17};
int lo, hi;
min_max(scores, 5, &lo, &hi);
printf("min = %d, max = %d\n", lo, hi);
return 0;
}min = 12, max = 2014.5 اشارهگر و آرایه¶
نام یک آرایه در بیشتر عبارتها به آدرسِ اولین عنصرش تبدیل میشود (این تبدیل Array Decay نام دارد و در بخش 14.6 کاملتر میشود)، یعنی a همان &a[0] است. نتیجه این است که a[i] و *(a + i) دقیقاً یک چیزند.
index 0 1 2 3 4
+-------+-------+-------+-------+-------+
a | 10 | 20 | 30 | 40 | 50 |
+-------+-------+-------+-------+-------+
address 0x100 0x104 0x108 0x10C 0x110
^
p (p = a = &a[0])#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* same as &a[0] */
printf("a[2] = %d\n", a[2]);
printf("*(a + 2) = %d\n", *(a + 2));
printf("*(p + 2) = %d\n", *(p + 2));
printf("p[2] = %d\n", p[2]);
for (int *q = a; q < a + 5; q++)
printf("%d ", *q);
printf("\n");
return 0;
}a[2] = 30
*(a + 2) = 30
*(p + 2) = 30
p[2] = 30
10 20 30 40 50 یک تفاوت ظریف: p متغیر است و میتوان p++ کرد، ولی نام آرایه ثابت است و a++ خطای کامپایل میدهد.
نکتهی جالب: چون a[i] در واقع *(a + i) است و جمع جابهجاییپذیر است، عبارت 2[a] هم معتبر است و برابر a[2] است. البته برای خوانایی هیچکس چنین نمینویسد.
حساب اشارهگر¶
وقتی به اشارهگر یک واحد اضافه میکنیم (p + 1)، به عنصر بعدی میرویم، نه بایت بعدی. کامپایلر اندازهی نوع را خودش حساب میکند: آدرس جدید برابر است با آدرس فعلی بهعلاوهی حاصلضرب n در اندازهی نوع (sizeof).
#include <stdio.h>
int main(void)
{
int i[2];
double d[2];
char c[2];
int *pi = i;
double *pd = d;
char *pc = c;
printf("int step: %ld bytes\n", (long)((char *)(pi + 1) - (char *)pi));
printf("double step: %ld bytes\n", (long)((char *)(pd + 1) - (char *)pd));
printf("char step: %ld bytes\n", (long)((char *)(pc + 1) - (char *)pc));
return 0;
}int step: 4 bytes
double step: 8 bytes
char step: 1 bytesروی اشارهگر میتوان عدد جمع و تفریق کرد، دو اشارهگرِ همآرایه را از هم کم کرد (حاصل، تعداد عناصر بین آنهاست) و آنها را مقایسه کرد. جمع دو اشارهگر، ضرب و تقسیم معنی ندارند. همچنین حساب اشارهگر فقط داخل محدودهی همان آرایه معتبر است؛ بیرون رفتن از آن و خواندن یا نوشتن، رفتار تعریفنشده دارد.
14.6 تبدیل آرایه به اشارهگر (Array Decay) و ارسال آرایه به تابع¶
در بخش قبل دیدیم که نام آرایه در بیشتر عبارتها به آدرسِ اولین عنصرش تبدیل میشود. این تبدیل خودکار را Array Decay مینامند: آرایهای از نوع int[5] به اشارهگری از نوع int * تبدیل میشود. هر جا بنویسید a + 1، *a یا p = a، یا آرایه را به تابع بدهید، همین اتفاق افتاده است.
سه جایی که Decay رخ نمیدهد¶
نام آرایه در سه حالت به اشارهگر تبدیل نمیشود: وقتی عملوند sizeof باشد، وقتی عملوند & باشد، و وقتی رشتهی ثابتی آرایهی char را مقداردهی کند (مثل char s[] = "abc";). برنامهی زیر تفاوت را نشان میدهد:
#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
printf("a == &a[0] : %d\n", a == &a[0]);
printf("sizeof(a) : %zu\n", sizeof(a));
printf("sizeof(a + 0) : %zu\n", sizeof(a + 0));
printf("step of a + 1 : %ld bytes\n", (long)((char *)(a + 1) - (char *)a));
printf("step of &a + 1 : %ld bytes\n", (long)((char *)(&a + 1) - (char *)&a));
return 0;
}خروجی (روی سیستم ۶۴ بیتی):
a == &a[0] : 1
sizeof(a) : 20
sizeof(a + 0) : 8
step of a + 1 : 4 bytes
step of &a + 1 : 20 bytesدر sizeof(a) آرایه سالم مانده و اندازهاش ۵ × ۴ = ۲۰ بایت است. اما در a + 0 تبدیل به اشارهگر انجام شده و اندازهی یک اشارهگر، یعنی ۸ بایت، به دست میآید. عبارت &a هم آدرسِ کل آرایه است (از نوع int (*)[5]). آدرسش با a یکی است، ولی &a + 1 بهاندازهی کل آرایه، یعنی ۲۰ بایت، جلو میرود و نه ۴ بایت.
ارسال آرایه به تابع¶
وقتی آرایه را به تابع میدهیم، Decay رخ میدهد: کپی آرایه نمیرود و فقط آدرسِ اولین عنصرش میرود. به همین دلیل تابع میتواند آرایهی اصلی را تغییر دهد. در پارامتر تابع سه نوشتار int arr[]، int arr[10] و int *arr کاملاً معادلاند و عددِ داخل کروشه نادیده گرفته میشود.
#include <stdio.h>
void show(int arr[])
{
/* arr is really an int* here */
printf("inside show : sizeof(arr) = %zu\n", sizeof(arr));
}
int main(void)
{
int a[10];
printf("inside main : sizeof(a) = %zu\n", sizeof(a));
show(a);
return 0;
}خروجی (روی سیستم ۶۴ بیتی):
inside main : sizeof(a) = 40
inside show : sizeof(arr) = 8در main، نام a آرایهی ۱۰ تایی از int است و ۱۰ × ۴ = ۴۰ بایت میشود. داخل show فقط یک اشارهگر داریم و اندازهی اشارهگر ۸ بایت است؛ کامپایلر هم هشدار میدهد. نتیجهی عملی این است که تابع اندازهی آرایه را نمیداند و باید تعداد عناصر را جداگانه، مثلاً با پارامتر n، دریافت کند. اگر تابع آرایه را فقط میخواند، نوشتن const پیش از نوع پارامتر این قصد را مشخص میکند.
آرایهی دوبعدی¶
برای آرایهی دوبعدی int m[2][3] هم Decay رخ میدهد، اما نتیجه اشارهگر به یک سطر است، نه به یک int: نوع m به int (*)[3] تبدیل میشود. به همین دلیل در پارامتر تابع باید تعداد ستونها را بنویسیم؛ کامپایلر برای محاسبهی آدرسِ m[i][j] باید بداند هر سطر چند عنصر دارد.
void print_matrix(int m[][3], int rows); /* same as: int (*m)[3] */14.7 اشارهگر به اشارهگر، void * و اشارهگر به تابع¶
اشارهگر به اشارهگر¶
اشارهگر هم یک متغیر است و آدرس دارد، پس میتوان اشارهگری ساخت که آدرسِ یک اشارهگر را نگه دارد. نوع چنین اشارهگری یک ستارهی بیشتر دارد (int **).
pp p x
+---------+ +---------+ +---------+
| &p | -------> | &x | -------> | 5 |
+---------+ +---------+ +---------+
int ** int * int#include <stdio.h>
int main(void)
{
int x = 5;
int *p = &x;
int **pp = &p;
printf("x = %d\n", x);
printf("*p = %d\n", *p);
printf("**pp= %d\n", **pp);
**pp = 42;
printf("x = %d\n", x);
return 0;
}x = 5
*p = 5
**pp= 5
x = 42این ساختار برای یک کار مشخص لازم میشود: تابعی که بخواهد خودِ یک اشارهگر را تغییر دهد. همانطور که برای تغییر یک int آدرسش را میدهیم (int *)، برای تغییر یک int * باید آدرسش را بدهیم (int **). در مثال زیر تابع، اشارهگر best را به بزرگترِ دو متغیر میبرد:
#include <stdio.h>
void set_to_larger(int **pp, int *x, int *y)
{
*pp = (*x > *y) ? x : y;
}
int main(void)
{
int a = 3, b = 9;
int *best = NULL;
set_to_larger(&best, &a, &b);
printf("*best = %d\n", *best);
return 0;
}*best = 9همین الگو در main(int argc, char *argv[]) هم دیده میشود. پارامتر argv آرایهای از اشارهگر به رشته است و طبق Array Decay به char ** تبدیل میشود.
اشارهگر عمومی void *¶
گاهی نوع دادهی مقصد را از قبل نمیدانیم. void * اشارهگری است که آدرسِ هر نوعی را میتواند نگه دارد، اما چون نوع ندارد نمیتوان مستقیم با * به محتوایش رسید؛ باید اول آن را به نوع درست تبدیل (cast) کرد. تابع malloc در فصل بعد همین نوع را برمیگرداند.
int n = 5;
void *vp = &n;
printf("%d
", *(int *)vp); /* cast first, then dereference: prints 5 */اشارهگر به تابع¶
هر تابع هم در حافظه آدرسی دارد و میتوان آن را در یک اشارهگر نگه داشت. عبارت int (*op)(int, int) یعنی «op اشارهگر به تابعی است که دو int میگیرد و int برمیگرداند». پرانتزِ دور *op مهم است، چون بدون آن عبارت تابعی را تعریف میکند که int * برمیگرداند. نام تابع بدون پرانتز (add) همان آدرس تابع است، شبیه نام آرایه. با این ابزار میتوان تابع را بهعنوان ورودی به تابع دیگر داد:
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int apply(int (*op)(int, int), int a, int b)
{
return op(a, b);
}
int main(void)
{
printf("add: %d\n", apply(add, 6, 7));
printf("mul: %d\n", apply(mul, 6, 7));
return 0;
}add: 13
mul: 42مثال واقعی: qsort¶
تابع استاندارد qsort (در <stdlib.h>) هر آرایهای را مرتب میکند: int، double یا هر نوع دیگر. برای اینکه با هر نوعی کار کند، آرایه را بهصورت void * میگیرد، و برای اینکه بداند عناصر را چطور مقایسه کند، تابع مقایسه را بهصورت اشارهگر به تابع میگیرد. ما فقط همین مقایسه را مینویسیم. مثال زیر نمرات را نزولی مرتب میکند:
#include <stdio.h>
#include <stdlib.h>
int by_score_desc(const void *x, const void *y)
{
int a = *(const int *)x;
int b = *(const int *)y;
return (a < b) - (a > b);
}
int main(void)
{
int scores[] = {18, 15, 20, 12, 17};
int n = sizeof(scores) / sizeof(scores[0]);
qsort(scores, n, sizeof(int), by_score_desc);
for (int i = 0; i < n; i++)
printf("%d ", scores[i]);
printf("\n");
return 0;
}20 18 17 15 12 تابع مقایسه باید عددی منفی، صفر یا مثبت برگرداند. عبارت (a < b) - (a > b) ترتیب نزولی میدهد و برخلاف b - a برای عددهای بزرگ سرریز نمیکند.
14.8 تمرین¶
تمرین ۱. خروجی برنامهی زیر را بدون اجرا پیشبینی کنید.
#include <stdio.h>
int main(void)
{
int a = 5, b = 10;
int *p = &a, *q = &b;
*p = *q + 1;
q = p;
*q = *q * 2;
printf("%d %d\n", a, b);
return 0;
}پاسخ
خروجی 22 10 است. خط *p = *q + 1; یعنی a = b + 1 = 11. با q = p; اشارهگر q هم به a اشاره میکند، پس *q = *q * 2; مقدار a را به ۲۲ میرساند. مقدار b هیچوقت تغییر نکرده است.
تمرین ۲. تابع void reverse(int *a, int n) را بنویسید که آرایه را درجا معکوس کند. فقط از دو اشارهگر (یکی از ابتدا، یکی از انتها) استفاده کنید.
پاسخ
#include <stdio.h>
void reverse(int *a, int n)
{
int *left = a, *right = a + n - 1;
while (left < right) {
int t = *left;
*left = *right;
*right = t;
left++;
right--;
}
}
int main(void)
{
int v[] = {1, 2, 3, 4, 5, 6};
reverse(v, 6);
for (int i = 0; i < 6; i++) printf("%d ", v[i]);
printf("\n");
return 0;
}خروجی: 6 5 4 3 2 1
تمرین ۳. تابع int *find_max(int *a, int n) را بنویسید که اشارهگر به بزرگترین عنصر را برگرداند، نه خود مقدار. در main نشان دهید که با همان اشارهگر میتوان مقدار اصلی را تغییر داد و اندیس آن را با m - v به دست آورد.
پاسخ
#include <stdio.h>
int *find_max(int *a, int n)
{
int *best = a;
for (int *p = a + 1; p < a + n; p++)
if (*p > *best) best = p;
return best;
}
int main(void)
{
int v[] = {4, 17, 9, 17, 2};
int *m = find_max(v, 5);
printf("max=%d at index %ld\n", *m, (long)(m - v));
*m = 0; /* modify the original array through it */
printf("v[1]=%d\n", v[1]);
return 0;
}max=17 at index 1
v[1]=0اگر چند عنصر برابر ماکزیمم باشند، این پیادهسازی اولین را برمیگرداند.
تمرین ۴. تابع int elements(int a[]) میخواهد با عبارت sizeof(a) / sizeof(a[0]) تعداد عناصر آرایه را حساب کند. چرا نتیجهاش درست نیست و راه درست چیست؟
پاسخ
چون پارامتر a در تابع یک اشارهگر است (Array Decay)، پس sizeof(a) اندازهی اشارهگر را میدهد، یعنی ۸ بایت، نه اندازهی آرایه. نتیجه همیشه ۸ ÷ ۴ = ۲ میشود، هر اندازه که آرایهی اصلی داشته باشد. راه درست این است که تعداد عناصر را بهصورت یک پارامتر جداگانه به تابع بفرستیم، یا همان عبارت را در تابعی بنویسیم که خودِ آرایه در آن تعریف شده است.
فصل ۱۵ : Dynamic Memory¶
15.1 چرا حافظهی پویا؟¶
آرایهی معمولی دو محدودیت دارد: اندازهاش باید از قبل معلوم باشد، و متغیرهای محلی با پایان تابع از بین میروند. اگر int a[100] بگیریم و کلاس ۳ نفر باشد، حافظه هدر رفته است؛ اگر ۵۰۰ نفر باشد، جا نداریم.
برنامهی در حال اجرا دو ناحیهی اصلی برای داده دارد:
+---------------------------+ high addresses
| STACK | local variables, parameters
| (grows downward) | created and destroyed with the function
+---------------------------+
| ... |
+---------------------------+
| HEAP | malloc / calloc / realloc
| (grows upward) | lives until you call free()
+---------------------------+
| globals / static |
+---------------------------+
| program code |
+---------------------------+ low addresses| پشته (Stack) | هیپ (Heap) | |
|---|---|---|
| محتوا | متغیرهای محلی و پارامترها | حافظهای که با malloc و مشابه آن میگیریم |
| مدیریت | خودکار | دستی، به عهدهی برنامهنویس |
| عمر | تا پایان تابع | تا زمانی که free کنیم |
| اندازه | کوچک و محدود | خیلی بزرگتر |
15.2 تابعهای malloc و free¶
تابع malloc (در <stdlib.h>) تعداد مشخصی بایت از هیپ میگیرد و آدرس اولین بایت را برمیگرداند، یا اگر حافظه کافی نباشد NULL. محتوای حافظهی گرفتهشده مقدار اولیه ندارد. الگوی استفاده همیشه چهار مرحله دارد: گرفتن، بررسی NULL، استفاده، و free.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
if (p == NULL) {
printf("out of memory!\n");
return 1;
}
*p = 2026;
printf("*p = %d\n", *p);
free(p);
p = NULL;
return 0;
}*p = 2026تعداد بایت را همیشه با sizeof بنویسید، نه با عدد ثابت، تا برنامه روی هر سیستمی درست کار کند. نتیجهی malloc را هم حتماً با NULL مقایسه کنید؛ استفاده از اشارهگر NULL باعث کرش میشود.
15.3 آرایهی پویا¶
بعد از malloc، اشارهگر را دقیقاً مثل آرایه با a[i] استفاده میکنیم. حالا اندازهی آرایه میتواند از ورودی کاربر بیاید. در سیستم نمرات، تعداد دانشجویان را میپرسیم و به همان اندازه جا میگیریم:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n;
printf("How many students? ");
if (scanf("%d", &n) != 1 || n <= 0) return 1;
int *a = malloc(n * sizeof(int));
if (a == NULL) {
printf("out of memory!\n");
return 1;
}
long sum = 0;
for (int i = 0; i < n; i++) {
printf("score[%d] = ", i);
scanf("%d", &a[i]);
sum += a[i];
}
printf("sum = %ld, average = %.2f\n", sum, (double)sum / n);
free(a);
return 0;
}نمونهی اجرا:
How many students? 4
score[0] = 18
score[1] = 15
score[2] = 20
score[3] = 12
sum = 65, average = 16.25خودِ اشارهگر a روی پشته است ولی دادهها روی هیپاند. اگر free(a) را فراموش کنیم، آدرس هیپ از دست میرود و دیگر راهی برای آزادکردنش نداریم.
15.4 تابعهای calloc و realloc¶
تابع calloc(count, size) مثل malloc حافظه میگیرد، با این تفاوت که همهی بایتها را صفر میکند و تعداد و اندازهی هر عنصر را جدا میگیرد. خواندن از حافظهای که با malloc گرفته شده، پیش از مقداردهی، خطاست؛ اگر به صفر نیاز دارید از calloc استفاده کنید.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *m = malloc(3 * sizeof(int)); /* garbage inside */
int *c = calloc(3, sizeof(int)); /* guaranteed zeros */
printf("calloc: %d %d %d\n", c[0], c[1], c[2]);
/* m[0..2] are indeterminate: reading them is a bug */
m[0] = m[1] = m[2] = 7;
printf("malloc after filling: %d %d %d\n", m[0], m[1], m[2]);
free(m);
free(c);
return 0;
}calloc: 0 0 0
malloc after filling: 7 7 7تابع realloc(ptr, new_size) اندازهی یک بلوک را بزرگ یا کوچک میکند و محتوای قبلی را نگه میدارد. ممکن است بلوک را به جای دیگری منتقل کند، پس بعد از آن فقط از اشارهگر جدید استفاده کنید. نوشتن a = realloc(a, n) خطرناک است، چون اگر realloc شکست بخورد NULL برمیگرداند و آدرس بلوک قبلی از دست میرود. راه امن این است که نتیجه را اول در یک اشارهگر موقت بگیریم.
در مثال زیر، هر بار که آرایه پر شد ظرفیتش دو برابر میشود. دو برابر کردن باعث میشود برای ۱۰۰۰۰ عنصر فقط حدود ۱۲ بار realloc لازم باشد، نه ۱۰۰۰۰ بار.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int cap = 2, size = 0;
int *a = malloc(cap * sizeof(int));
if (a == NULL) return 1;
for (int v = 1; v <= 10; v++) {
if (size == cap) { /* full: double it */
int *tmp = realloc(a, 2 * cap * sizeof(int));
if (tmp == NULL) { /* keep old block on failure */
free(a);
return 1;
}
a = tmp;
cap *= 2;
printf("grew to capacity %d\n", cap);
}
a[size++] = v * v;
}
for (int i = 0; i < size; i++) printf("%d ", a[i]);
printf("\n");
free(a);
return 0;
}grew to capacity 4
grew to capacity 8
grew to capacity 16
1 4 9 16 25 36 49 64 81 100 15.5 خطاهای حافظه¶
حافظهی پویا قدرت میدهد ولی مسئولیت هم میآورد. قاعدهی کلی: هر malloc یا calloc باید دقیقاً یک free داشته باشد، و بعد از free اشارهگر را NULL کنید (free(NULL) بیخطر است). چهار خطای معروف این قاعده را نقض میکنند.
15.5.1 نشتی حافظه (Memory Leak)¶
حافظه گرفته شده ولی هیچوقت آزاد نمیشود و آدرسش هم گم شده است. در برنامهی کوتاه دیده نمیشود، اما در برنامهای که ساعتها اجرا میشود، حافظه کمکم تمام میشود.
#include <stdlib.h>
int main(void)
{
int *p = malloc(100 * sizeof(int));
p[0] = 1;
return 0; /* forgot free(p) */
}15.5.2 اشارهگر معلق (Dangling Pointer)¶
اشارهگری که به حافظهی آزادشده اشاره میکند. آن حافظه ممکن است هر لحظه به چیز دیگری داده شود، پس نتیجهی استفاده از آن غیرقابل پیشبینی است.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
*p = 5;
free(p);
printf("%d\n", *p); /* use after free */
return 0;
}شکل پنهانترش برگرداندن آدرس متغیر محلی است. آن متغیر روی پشته است و با پایان تابع از بین میرود:
int *bad(void)
{
int x = 42;
return &x; /* x dies when bad() returns */
}15.5.3 آزادسازی دوباره (Double Free)¶
آزاد کردن یک بلوک برای بار دوم ساختار داخلی مدیریت حافظه را خراب میکند و معمولاً به کرش میرسد.
#include <stdlib.h>
int main(void)
{
int *p = malloc(sizeof(int));
free(p);
free(p); /* double free */
return 0;
}15.5.4 نوشتن بیرون از محدوده¶
int *a = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++) /* bug: a[5] is one past the end */
a[i] = i;استفاده از <= بهجای < یک عنصر بیشتر مینویسد. این خطا ممکن است اصلاً علامتی نشان ندهد ولی دادههای دیگر را خراب کند.
چنین خطاهایی فقط آزاردهنده نیستند. باگ معروف Heartbleed در کتابخانهی OpenSSL (سال ۲۰۱۴) از همین خانواده بود: برنامه بیش از اندازهی واقعی یک بافر را از حافظه میخواند و ممکن بود دادههای حساس سرور را لو بدهد.
15.6 پیدا کردن خطاها با AddressSanitizer¶
بیشتر این خطاها موقع اجرا پیام نمیدهند، برای همین ابزار لازم داریم. gcc ابزار آمادهای به نام AddressSanitizer دارد و فقط کافی است موقع کامپایل دو گزینه اضافه کنید:
gcc -g -fsanitize=address -std=c11 prog.c -o prog
./progبرای برنامهی نشتی بخش ۱۵.۵.۱ گزارشی شبیه این (خلاصهشده) میگیرید:
ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 ... in malloc
#1 ... in main leak.c:4
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).برای اشارهگر معلق، گزارش با heap-use-after-free شروع میشود و برای آزادسازی دوباره با attempting double-free. در هر گزارش دو چیز را نگاه کنید: نوع خطا و شمارهی خط در فایل خودتان. این ابزار روی لینوکس و macOS راحت کار میکند؛ روی بعضی نصبهای ویندوز (مثل MinGW) ممکن است در دسترس نباشد و بهتر است از WSL استفاده کنید.
15.7 تابعی که حافظه برمیگرداند¶
حافظهی هیپ با پایان تابع از بین نمیرود، پس تابع میتواند حافظه بگیرد و آدرسش را برگرداند. در این حالت مسئولیت free با فراخواننده است و بهتر است این موضوع در توضیح تابع نوشته شود. قاعدهی ساده: هر کس حافظه را میگیرد، یا خودش آزادش میکند یا صریحاً مسئولیتش را به دیگری میسپارد.
#include <stdio.h>
#include <stdlib.h>
int *make_range(int n)
{
int *a = malloc(n * sizeof(int));
if (a == NULL) return NULL;
for (int i = 0; i < n; i++) a[i] = i;
return a;
}
int main(void)
{
int *r = make_range(5);
if (r == NULL) return 1;
for (int i = 0; i < 5; i++) printf("%d ", r[i]);
printf("\n");
free(r);
return 0;
}0 1 2 3 415.8 تمرین¶
تمرین ۱. برنامهای بنویسید که n را بخواند، با malloc آرایهای به همان اندازه بسازد، n عدد بخواند و آنها را از آخر به اول چاپ کند. در پایان حافظه را آزاد کند.
پاسخ
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n;
if (scanf("%d", &n) != 1 || n <= 0) return 1;
int *a = malloc(n * sizeof(int));
if (a == NULL) return 1;
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = n - 1; i >= 0; i--) printf("%d ", a[i]);
printf("\n");
free(a);
return 0;
}ورودی 4 و 1 2 3 4 خروجی 4 3 2 1 میدهد.
تمرین ۲. در هر قطعهکد یک خطای حافظه هست. نوع خطا و اصلاحش را بگویید.
/* snippet 1 */
for (int i = 0; i < 1000; i++) {
int *p = malloc(100 * sizeof(int));
p[0] = i;
}
/* snippet 2 */
int *p = malloc(sizeof(int));
*p = 7;
free(p);
*p = 8;
/* snippet 3 */
int *get(void)
{
int v = 10;
return &v;
}
/* snippet 4 */
int *a = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++)
a[i] = i;پاسخ
قطعهی ۱، نشتی حافظه: در هر دور یک بلوک ۴۰۰ بایتی گرفته میشود و آدرسش در دور بعد گم میشود. با
free(p); در انتهای بدنهی حلقه درست میشود.قطعهی ۲، اشارهگر معلق: بعد از
freeدر *p نوشته شده است. بعد ازfreeمقدار p = NULL; بدهید و دیگر از آن استفاده نکنید.قطعهی ۳، برگرداندن آدرس متغیر محلی:
vبا پایان تابع از بین میرود. یا باmallocحافظهی هیپ بگیرید یا مقدار را با return v; برگردانید.قطعهی ۴، نوشتن بیرون از محدوده:
i <= 5 یعنی a[5] که وجود ندارد. باید i < 5 باشد.
تمرین ۳. برنامهای بنویسید که تا زمانی که کاربر -1 وارد نکرده عدد بخواند. تعداد ورودیها از قبل معلوم نیست؛ آرایه با ظرفیت ۴ شروع شود و هر بار پر شد با realloc دو برابر شود. در پایان تعداد و میانگین را چاپ کند.
پاسخ
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int cap = 4, size = 0, x;
int *a = malloc(cap * sizeof(int));
if (a == NULL) return 1;
printf("Enter numbers (-1 to stop):\n");
while (scanf("%d", &x) == 1 && x != -1) {
if (size == cap) {
int *tmp = realloc(a, cap * 2 * sizeof(int));
if (tmp == NULL) { free(a); return 1; }
a = tmp;
cap *= 2;
}
a[size++] = x;
}
if (size > 0) {
long sum = 0;
for (int i = 0; i < size; i++) sum += a[i];
printf("count=%d average=%.2f\n", size, (double)sum / size);
}
free(a);
return 0;
}ورودی 10 20 30 40 50 60 -1 خروجی count=6 average=35.00 میدهد.
جمعبندی بخش¶
این بخش حول یک ایدهی ساده میچرخد: هر چیزی در حافظه آدرسی دارد و میتوانیم آن آدرس را در یک متغیر نگه داریم. فصل ۱۴ نشان داد این ایده چه امکانهایی میدهد، و فصل ۱۵ نشان داد وقتی حافظه را خودمان میگیریم چه مسئولیتی داریم.
فصل ۱۴: اشارهگرها¶
آدرس و اشارهگر:
&x آدرسِxرا میدهد و *p به محتوای آدرسی میرسد کهpنگه میدارد. اشارهگری که هنوز مقصد ندارد بایدNULLباشد.ارسال با آدرس: C پارامترها را کپی میکند. برای اینکه تابع بتواند مقدار اصلی را تغییر دهد یا چند نتیجه برگرداند، آدرس میفرستیم. دلیل
& درscanfهم همین است.آرایه و اشارهگر:
a[i] همان *(a + i) است، نام آرایه در بیشتر عبارتها به آدرسِ عنصر اول تبدیل میشود (Array Decay) و p + 1 یعنی عنصر بعدی، نه بایت بعدی. به همین دلیل تابع اندازهی آرایه را نمیداند و باید آن را جداگانه دریافت کند.مباحث تکمیلی:
int ** برای تغییر خودِ یک اشارهگر، void * برای دادهای که نوعش از قبل معلوم نیست، و اشارهگر به تابع برای فرستادن یک رفتار بهعنوان ورودی، مثل تابع مقایسه درqsort.
فصل ۱۵: حافظهی پویا¶
پشته و هیپ: متغیرهای محلی روی پشتهاند و با پایان تابع از بین میروند. حافظهی هیپ تا وقتی
freeنکنیم زنده میماند.چهار تابع:
mallocحافظه میگیرد،callocمیگیرد و صفر میکند،reallocاندازه را عوض میکند وfreeپس میدهد. نتیجه را همیشه باNULLبسنجید وreallocرا با اشارهگر موقت بنویسید.خطاهای حافظه: نشتی، اشارهگر معلق، آزادسازی دوباره و نوشتن بیرون از محدوده. قاعدهی کلی این است که هر
mallocیکfreeدارد و بعد ازfreeاشارهگر راNULLمیکنیم.پیدا کردن خطا: AddressSanitizer با گزینهی
-fsanitize=address این خطاها را هنگام اجرا نشان میدهد.
اشتباههای رایج¶
| اشتباه | چه میشود؟ | راه درست |
|---|---|---|
| استفاده از اشارهگر بدون مقدار اولیه | رفتار تعریفنشده | مقداردهی با آدرس معتبر یا NULL |
فراموشکردن & در scanf | رفتار تعریفنشده، معمولاً کرش | فرستادن آدرس، مثل &x |
شمردن عناصر با sizeof(a) داخل تابع | اندازهی اشارهگر به دست میآید | فرستادن تعداد عناصر بهصورت پارامتر |
نادیدهگرفتن NULL بعد از malloc | کرش در صورت کمبود حافظه | بررسی نتیجه پیش از استفاده |
استفاده از اشارهگر بعد از free | اشارهگر معلق | نوشتن p = NULL; بعد از free |
نوشتن a = realloc(a, n) | در صورت شکست، بلوک قبلی گم میشود | گرفتن نتیجه در اشارهگر موقت |
بخش بعد¶
در بخش بعد دادههای ناهمگن را کنار هم میگذاریم، مثلاً نام، شمارهی دانشجویی و نمرهی یک دانشجو در یک بسته (struct)، و سراغ فایلها میرویم. دو چیزی که در این بخش یاد گرفتیم آنجا دوباره به کار میآید: اشارهگر به ساختار، که با -> به فیلدهایش میرسیم، و تابع fopen که بهجای یک مقدار، یک اشارهگر (FILE *) برمیگرداند.