এই পর্বে যা শিখবেন
- যেকোনো
a[i][j]-এর ঠিকানা হাতে হিসাব করা, আর কেন লুপের ক্রম গতি বদলে দেয় - Nested loop দিয়ে সারির যোগফল, কলামের গড়, transpose আর ম্যাট্রিক্স গুণ
- ফাংশনে 2D অ্যারে পাঠানোর সঠিক উপায় — আর
int **-এর ফাঁদ চেনা - চলার সময় মাপ জেনে heap-এ 2D অ্যারে বানানো ও ঠিক ক্রমে free করা
- AI-এর লেখা ম্যাট্রিক্স-কোডে সাধারণ ভুল ধরার চেকলিস্ট
১ · মেমরি একটি লম্বা রাস্তা, টেবিল শুধু নম্বর দেওয়ার নিয়ম
পর্ব ১৬-এ দেখেছেন, অ্যারে মানে মেমরিতে পাশাপাশি সাজানো ঘর। সেখানে শেষের দিকে এক ঝলক 2D array-ও দেখেছিলেন। এই পর্বে সেই ধারণাটি পুরো গভীরে নিয়ে যাব — কারণ পরীক্ষার ফলাফল থেকে শুরু করে AI মডেলের ভেতরের হিসাব পর্যন্ত, বাস্তবের বেশিরভাগ ডেটাই আসে টেবিলের আকারে।
কয়েকটি টেবিলের কথা ভাবুন:
- ক্লাসের ফলাফলের খাতা: প্রতি সারিতে একজন শিক্ষার্থী, প্রতি কলামে একটি বিষয়।
- ঢাকা–চট্টগ্রাম বাসের আসনের নকশা: ১০টি সারি, প্রতি সারিতে ৪টি আসন — কোনটি বুক হয়েছে, কোনটি খালি।
- লুডো বা দাবার বোর্ড: লুডো ১৫ × ১৫ ঘরের, দাবা ৮ × ৮ ঘরের।
- মোবাইলে তোলা ছবি: হাজার হাজার সারি, প্রতি সারিতে হাজার হাজার pixel.
C-তে এমন টেবিল লেখা হয় দুটি বন্ধনী দিয়ে। প্রথম সংখ্যাটি সারি, দ্বিতীয়টি কলাম:
int marks[3][4] = { /* ৩ জন শিক্ষার্থী × ৪টি বিষয় */
{78, 65, 82, 90}, /* সারি 0: বাংলা, ইংরেজি, গণিত, বিজ্ঞান */
{55, 72, 68, 61}, /* সারি 1 */
{88, 91, 95, 84} /* সারি 2 */
};
int seat[10][4] = {0}; /* বাসের ৪০টি আসন, সব খালি (0) */
int board[][8] = {{1, 2}, {3}}; /* প্রথম মাপ বাদ দেওয়া যায়: compiler গুনে 2 ধরে */
ভেতরের বন্ধনীগুলো (nested braces) প্রতিটি সারি আলাদা করে দেখায়। কোনো সারিতে কম মান দিলে বাকি ঘরগুলো 0 হয়ে যায়, যেমন উপরের board-এর বেশিরভাগ ঘর। খেয়াল করুন, শুধু প্রথম মাপটি বাদ দেওয়া যায়; কলামের সংখ্যা (8) সবসময় লিখতে হয়। কেন — তার উত্তর এই পর্বের মূল রহস্য, যা পরের অংশেই খুলবে।
পুরান ঢাকার একটি লম্বা গলির কথা ভাবুন। গলির দুই পাশে নয়, একপাশে সারি দিয়ে ১২টি বাড়ি — বাড়ি নম্বর ০ থেকে ১১. পৌরসভা ঠিক করল, প্রতি ৪টি বাড়ি মিলে একটি “ব্লক।” এখন কেউ যদি বলেন “ব্লক ১-এর ২ নম্বর বাড়ি”, ডাকপিয়ন মনে মনে হিসাব করেন: ব্লক ০-এর ৪টি বাড়ি পার হলাম, তারপর আরও ২টি — অর্থাৎ বাড়ি নম্বর ৬. গলিটি কিন্তু বদলায়নি! “ব্লক” শুধু একটি নম্বর দেওয়ার নিয়ম। C-এর 2D অ্যারে ঠিক এই — মেমরি হলো গলি, সারি হলো ব্লক।
RAM-এর ঠিকানা একমাত্রিক: 1000, 1001, 1002… কোনো “উপরের সারি” বা “পাশের কলাম” নেই। Multidimensional array হলো সেই একমাত্রিক রাস্তার ওপর একটি চুক্তি — কোন (সারি, কলাম) জোড়া কোন ঘর নম্বরে যাবে। এই চুক্তি বুঝলেই এই পর্বের বাকি সবকিছু — ফাংশনে পাঠানো, heap-এ বানানো, গতি, ভুল — নিজে থেকেই বোঝা যায়।
২ · Row-major — a[i][j] আসলে কোন ঘরে থাকে
C যে চুক্তি মানে তার নাম row-major order: প্রথমে সারি 0-এর সব ঘর, তার ঠিক পরে সারি 1-এর সব ঘর, তারপর সারি 2 — মাঝে কোনো ফাঁক ছাড়া। নিচের ডায়াগ্রামে উপরের marks[3][4] টেবিলটি মেমরিতে বসিয়ে দেখি। ধরা যাক অ্যারেটি শুরু হয়েছে ঠিকানা 1000-এ (একে বলে base address), আর প্রতিটি int ৪ বাইটের।
col 0 col 1 col 2 col 3 row 0 : 78 65 82 90 row 1 : 55 72 68 61 row 2 : 88 91 95 84
offset : 0 1 2 3 4 5 6 7 8 9 10 11 memory : [78][65][82][90][ ][ ][ ][ ][ ][ ][ ][ ] address: 1000 ... 1012
offset : 0 1 2 3 4 5 6 7 8 9 10 11
memory : [78][65][82][90][55][72][68][61][88][91][95][84]
|--- row 0 ----||--- row 1 ----||--- row 2 ----|
size : 12 x 4 = 48 bytes, one unbroken block
offset : 0 1 2 3 4 5 6 7 8 9 10 11 memory : [78][65][82][90][55][72][68][61][88][91][95][84] ^ offset = 1 * 4 + 2 = 6 address = 1000 + 6 * 4 = 1024 value = 68
&a[i][j] = base + (i * COLS + j) * sizeof(int) marks[i] -> type int[4], a whole row (16 bytes) marks + 1 -> jumps 16 bytes, to the start of row 1 marks[1] + 2 -> 1016 + 2 * 4 = 1024 /* ROWS never appears in the formula; COLS always does */
- আমাদের মনের ছবি। তিনটি সারি, চারটি কলাম — কাগজের খাতার মতো। কোডে লেখা নেস্টেড বন্ধনী ঠিক এই ছবিটিই দেখায়। কিন্তু মনে রাখুন, মেমরির কাছে “উপর-নিচ” বলে কিছু নেই।
- সারি 0 আগে। প্রথম সারির চারটি মান ঠিকানা 1000, 1004, 1008, 1012-তে পাশাপাশি বসে — একেবারে সাধারণ 1D অ্যারের মতো। বাকি ঘরগুলো এখনো ফাঁকা দেখাচ্ছি।
- তারপর সারি 1, তারপর সারি 2. দ্বিতীয় সারি শুরু হয় 1016-তে, প্রথম সারির শেষ ঘরের গা ঘেঁষে। পুরো টেবিলটি আসলে ১২টি
int-এর একটি একটানা সারি — ৪৮ বাইট। পর্ব ১৬-এর contiguous memory এখানেও খাটে। marks[1][2]কোথায়? সারি 1-এ পৌঁছাতে আগের পুরো একটি সারি, অর্থাৎ ৪টি ঘর পার হতে হবে, তারপর আরও ২টি ঘর। ঘর নম্বর (offset) = 1 × 4 + 2 = ৬, ঠিকানা = 1000 + ৬ × ৪ = 1024, মান 68. ঠিক গলির “ব্লক ১-এর ২ নম্বর বাড়ি”-র হিসাব।- সূত্রটি খেয়াল করুন। এতে কলামের সংখ্যা (COLS) আছে, কিন্তু সারির সংখ্যা (ROWS) কোথাও নেই! তাই compiler-কে কলাম সংখ্যা জানাতেই হয়, সারি সংখ্যা না জানালেও চলে। আরও একটি সূক্ষ্ম কথা:
marks[1]নিজেই একটি পুরো সারি, যার টাইপint[4]. তাইmarks + 1এক লাফে ১৬ বাইট এগোয় — পর্ব ১৮-এর pointer arithmetic, শুধু এক ধাপের মাপ এখন একটি সারি।
সারির সংখ্যা সূত্রে নেই, কলামের সংখ্যা সবসময় আছে
এই সূত্রের একটি সরাসরি ফল হলো গতি। পর্ব ১৬-এর অংশ ৮-এ দেখেছেন, CPU মেমরি থেকে একবারে একটি cache line (সাধারণত ৬৪ বাইট) আনে, আর পাশাপাশি ঘর পরপর ছুঁলে তার পুরো সুবিধা পাওয়া যায় — যাকে বলে spatial locality. Row-major বিন্যাসে a[i][0], a[i][1], a[i][2]… পাশাপাশি থাকে। তাই:
বাইরে সারি, ভেতরে কলাম
মেমরিতে ঘর ০, ১, ২, ৩… ক্রমে হাঁটা। একবার আনা cache line-এর সব ঘর কাজে লাগে। গলির বাড়িগুলো একটার পর একটা ঘুরে চিঠি বিলি করার মতো।
for (i…) for (j…) a[i][j]বাইরে কলাম, ভেতরে সারি
ঘর ০, ৪, ৮, ১, ৫, ৯… প্রতিবার একটি পুরো সারি লাফ। বড় টেবিলে প্রায় প্রতিটি ঘরের জন্য নতুন করে RAM-এ যেতে হয়। একই গলিতে বারবার এ-মাথা ও-মাথা দৌড়ানো।
for (j…) for (i…) a[i][j]দুটি লুপ ঠিক একই উত্তর দেয়, একই সংখ্যক যোগ করে। ছোট টেবিলে পার্থক্য চোখেই পড়বে না। কিন্তু হাজার × হাজার মাপের টেবিলে দ্বিতীয়টি কয়েক গুণ ধীর হতে পারে — কত গুণ, তা কম্পিউটার আর টেবিলের মাপের ওপর নির্ভর করে। নিজে মাপতে চাইলে clock() দিয়ে দুটি লুপের সময় তুলনা করে দেখতে পারেন — সময় মাপার এই কৌশল পর্ব ৩৯-এ বিস্তারিত আসবে।
সব ভাষা এই চুক্তি মানে না। Fortran, MATLAB আর R টেবিল রাখে কলাম ধরে — column-major order. সেখানে উল্টো ক্রমের লুপই দ্রুত। এক ভাষার ডেটা আরেক ভাষায় পাঠাতে গেলে এই পার্থক্য কী গোলমাল বাধায়, তা ভাবনার প্রশ্ন ১-এ দেখব।
৩ · Nested loop-এর চারটি কাজ — যোগফল, গড়, transpose, গুণ
2D অ্যারের প্রায় সব কাজ হয় nested loop দিয়ে: বাইরের লুপ একটি মাত্রা ধরে, ভেতরের লুপ আরেকটি। চারটি কাজ শিখলে বাকিগুলো এদেরই রূপান্তর।
ক. ফলাফলের খাতা — সারির যোগফল, কলামের গড়, সর্বোচ্চ নম্বর
নিচের প্রোগ্রামটি আমাদের ৩ জন শিক্ষার্থী × ৪ বিষয়ের খাতা থেকে প্রত্যেকের মোট নম্বর, প্রতিটি বিষয়ের গড় আর সর্বোচ্চ মোট নম্বর পাওয়া শিক্ষার্থীকে বের করে। চালিয়ে দেখুন, তারপর নিচের ব্যাখ্যা পড়ুন।
#include <stdio.h>
#define ST 3 /* students */
#define SB 4 /* subjects */
int main(void) {
int m[ST][SB] = {{78, 65, 82, 90}, {55, 72, 68, 61}, {88, 91, 95, 84}};
int best = 0, bestTotal = -1;
for (int i = 0; i < ST; i++) { /* one row = one student */
int total = 0;
for (int j = 0; j < SB; j++) total += m[i][j];
printf("Student %d total: %d\n", i, total);
if (total > bestTotal) { bestTotal = total; best = i; }
}
for (int j = 0; j < SB; j++) { /* one column = one subject */
int sum = 0;
for (int i = 0; i < ST; i++) sum += m[i][j];
printf("Subject %d average: %.2f\n", j, (double)sum / ST);
}
printf("Top: student %d with %d\n", best, bestTotal);
return 0;
}প্রথম জোড়া লুপে বাইরের লুপ সারি (শিক্ষার্থী), ভেতরের লুপ কলাম (বিষয়) — মেমরিতে পাশাপাশি হাঁটা। ফল: 315, 256, 358. দ্বিতীয় জোড়ায় উল্টো: বাইরে কলাম, ভেতরে সারি, কারণ এখানে প্রশ্নটাই কলাম ধরে। এত ছোট টেবিলে গতির পার্থক্য নেই, কিন্তু বড় টেবিলে কলামের গড়ও সারি ধরে ঘুরে বের করা যায় (অনুশীলন ৪ দেখুন)। গড়ের লাইনটি খেয়াল করুন: (double)sum / ST. শুধু sum / ST লিখলে দুটিই int, ভাগফলের দশমিক অংশ কাটা পড়ত — 221 / 3 হতো 73, 73.67 নয়। সর্বোচ্চ খুঁজতে bestTotal শুরু হয়েছে -1 দিয়ে, যাতে প্রথম শিক্ষার্থীর মোট নম্বর সবসময় তার চেয়ে বড় হয়। শেষ লাইনে আসে Top: student 2 with 358.
খ. Transpose — সারিকে কলাম বানানো
Matrix মানে সংখ্যার একটি আয়তাকার টেবিল — গণিতের ভাষায় 2D অ্যারে। একটি matrix-এর transpose মানে সারিগুলোকে কলাম বানিয়ে দেওয়া: ২ × ৩ matrix হয়ে যায় ৩ × ২. নিয়ম একটাই — T[j][i] = A[i][j]. নিচের debugger-সিমুলেটরে প্রতিটি ঘর কোথা থেকে কোথায় যায় দেখুন:
1int A[2][3] = {{1, 2, 3},2 {4, 5, 6}};3int T[3][2];4for (int i = 0; i < 2; i++)5 for (int j = 0; j < 3; j++)6 T[j][i] = A[i][j];7/* T is now 3 rows x 2 cols */
| i | |
| j | |
| A[i][j] | |
| লেখা হলো |
- শুরু।
A২ সারি × ৩ কলাম।T-এর মাপ উল্টো — ৩ সারি × ২ কলাম।T-এ এখনো আবর্জনা মান (?), কারণ local অ্যারে নিজে থেকে 0 হয় না। - i = 0, j = 0.
A[0][0]= 1 যায়T[0][0]-এ। কোণের ঘর, জায়গা বদলায় না — matrix-এর কর্ণ (diagonal) বরাবর ঘরগুলো transpose-এ একই থাকে। - i = 0, j = 1.
A[0][1]= 2 যায়T[1][0]-এ। খেয়াল করুন, index দুটি অদলবদল হলো — সারি 0-এর দ্বিতীয় মান এখন কলাম 0-এর দ্বিতীয় মান। - i = 0, j = 2.
A[0][2]= 3 যায়T[2][0]-এ। A-এর পুরো সারি 0 এখন T-এর পুরো কলাম 0. - i = 1, j = 0.
A[1][0]= 4 যায়T[0][1]-এ। এবার A-এর দ্বিতীয় সারি T-এর দ্বিতীয় কলাম ভরাতে শুরু করল। - i = 1, j = 1.
A[1][1]= 5 যায়T[1][1]-এ — আরেকটি কর্ণের ঘর। - i = 1, j = 2.
A[1][2]= 6 যায়T[2][1]-এ। লুপ শেষ। মোট ২ × ৩ = ৬টি ঘর কপি হলো। - ফল।
T= {{1, 4}, {2, 5}, {3, 6}}. একটি সাধারণ ভুল: কেউ যদি লুপের সীমাi < 3,j < 2লেখেন, তাহলেA[2][…]পড়তে গিয়ে অ্যারের বাইরে চলে যাবেন। বর্গাকার (৩ × ৩) matrix-এ এই ভুল ধরাই পড়ে না — তাই transpose সবসময় অবর্গাকার matrix দিয়ে পরীক্ষা করুন।
গ. Matrix গুণ — তিনটি লুপ
Matrix multiplication-এ C = A × B হলে C[i][j] হলো A-এর সারি i আর B-এর কলাম j-এর ঘরগুলো জোড়ায় জোড়ায় গুণ করে যোগফল। শর্ত একটাই — A-এর কলাম সংখ্যা আর B-এর সারি সংখ্যা সমান হতে হবে। ছোট একটি ২ × ২ উদাহরণে দেখি:
- দুটি matrix. A = {{1, 2}, {3, 4}}, B = {{5, 6}, {7, 8}}. A-কে সারি হিসেবে আর B-কে কলাম হিসেবে দেখানো হয়েছে, কারণ গুণে A-এর সারি আর B-এর কলাম জোড়া বাঁধে।
C[0][0]. A-এর সারি 0 (1, 2) আর B-এর কলাম 0 (5, 7): 1×5 + 2×7 = 19. এটাই ভেতরের তৃতীয় লুপের কাজ —kচলে 0 থেকে 1, যোগ হয়A[i][k] * B[k][j].C[0][1]. একই সারি 0, এবার B-এর কলাম 1 (6, 8): 1×6 + 2×8 = 22.C[1][0]. A-এর সারি 1 (3, 4) × B-এর কলাম 0: 3×5 + 4×7 = 43.C[1][1]. 3×6 + 4×8 = 50. ফল C = {{19, 22}, {43, 50}}. খেয়াল করুন, B × A হিসাব করলে উত্তর আলাদা আসত — matrix গুণে ক্রম গুরুত্বপূর্ণ।- খরচ। প্রতিটি ঘরের জন্য n-টি গুণ, আর ঘর আছে n × n টি — মোট n³. পর্ব ৩৯-এর ভাষায় O(n³). ১০০০ × ১০০০ matrix-এ প্রায় ১০০ কোটি গুণ! এজন্যই AI-এর জগতে matrix গুণকে দ্রুত করা একটি বিশাল শিল্প (ভাবনার প্রশ্ন ৩)।
কোডে এটি তিনটি নেস্টেড লুপ:
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
int sum = 0;
for (int k = 0; k < N; k++)
sum += A[i][k] * B[k][j]; /* B পড়া হচ্ছে কলাম ধরে — লাফিয়ে লাফিয়ে */
C[i][j] = sum;
}
মন্তব্যটি খেয়াল করুন: ভেতরের লুপে B[k][j] প্রতিবার একটি পুরো সারি লাফায় — cache-এর জন্য খারাপ। একটি পরিচিত কৌশল হলো লুপের ক্রম i, k, j করে দেওয়া (আগে C শূন্য করে নিয়ে): তখন ভেতরের লুপে B[k][j] আর C[i][j] দুটিই সারি ধরে হাঁটে। উত্তর একই, কিন্তু বড় matrix-এ অনেক দ্রুত। সাধারণ নিয়ম: শেষ index-টি সবচেয়ে ভেতরের লুপে বদলাক।
৪ · ফাংশনে 2D অ্যারে পাঠানো — কলাম সংখ্যা কেন লাগে
পর্ব ১৮-এ দেখেছেন, ফাংশনে অ্যারে পাঠালে পুরো অ্যারে কপি হয় না — শুধু প্রথম ঘরের ঠিকানা যায় (array decay). 2D অ্যারের বেলাতেও একই কথা, শুধু “প্রথম ঘর” এখন একটি পুরো সারি। তাই int marks[3][4] পাঠালে ফাংশন পায় একটি pointer to array — টাইপ int (*)[4], অর্থাৎ “৪টি int-এর একটি সারির ঠিকানা।”
এখন অংশ ২-এর সূত্রটি মনে করুন: a[i][j] খুঁজতে compiler-কে জানতে হবে এক সারিতে কয়টি ঘর। সারির সংখ্যা সূত্রে নেই। এজন্যই:
void print_sheet(int rows, int a[][4]) { /* প্রথম মাপ বাদ, কলাম 4 বাধ্যতামূলক */
for (int i = 0; i < rows; i++)
for (int j = 0; j < 4; j++)
printf("%d ", a[i][j]);
}
/* int a[][4], int a[3][4], int (*a)[4] — তিনটি লেখাই এখানে হুবহু এক জিনিস */
সহজ ও নিরাপদ, কিন্তু ফাংশনটি শুধু ৪-কলামের টেবিলের জন্যই কাজ করবে। ৫-কলামের টেবিল পাঠালে compiler আপত্তি করবে।
void print_any(int rows, int cols, int a[rows][cols]) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
printf("%d ", a[i][j]);
}
/* rows আর cols আগে, অ্যারে পরে — ক্রমটা জরুরি */
C99 থেকে মাপ প্যারামিটার হিসেবে পাঠানো যায় — একে বলে VLA (variable length array) প্যারামিটার। এক ফাংশনই যেকোনো মাপের টেবিলে চলে। শর্ত: মাপের প্যারামিটারগুলো অ্যারের আগে লিখতে হবে, নইলে compiler সেগুলো তখনো চেনে না। GCC আর Clang এটি পুরোপুরি সমর্থন করে, তবে Microsoft-এর MSVC compiler VLA সমর্থন করে না।
void print_flat(int rows, int cols, const int *a) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
printf("%d ", a[i * cols + j]); /* সূত্রটি নিজের হাতে */
}
/* ডাকা: print_flat(3, 4, &marks[0][0]); */
অ্যারেটিকে সোজা ১২টি ঘরের সারি হিসেবে পাঠিয়ে সূত্রটি নিজে লেখা। যেকোনো compiler-এ চলে, অন্য ভাষার library-র সাথেও এভাবেই ডেটা দেওয়া-নেওয়া হয়। অসুবিধা: a[i][j]-এর সহজ লেখাটি হারায়, ভুল সূত্র লিখলে compiler ধরতে পারে না।
সবচেয়ে বড় ফাঁদ: int ** আর int[3][4] এক নয়
নতুনরা (এবং AI-ও!) প্রায়ই ভাবেন, “1D অ্যারে মানে int *, তাহলে 2D মানে নিশ্চয়ই int **.” এই যুক্তি শুনতে ঠিক, কিন্তু সম্পূর্ণ ভুল:
void show(int **a); /* ভুল প্রত্যাশা */
int marks[3][4];
show(marks);
warning: passing argument 1 of 'show' from incompatible pointer type
note: expected 'int **' but argument is of type 'int (*)[4]'
GCC ১৪ থেকে এটি আর শুধু warning নয়, ডিফল্টভাবে error. কেন মেলে না, দুটি ছবি পাশাপাশি রাখলেই বোঝা যায়:
int marks[3][4]
১২টি int একটানা, ৪৮ বাইট। কোথাও কোনো pointer জমা নেই। marks[1][2] মানে শুধু হিসাব: base + ৬ × ৪.
int **p
p একটি ঠিকানার ঠিকানা (pointer to pointer). p[1][2] মানে আগে p[1] থেকে একটি pointer পড়া, তারপর সেই ঠিকানায় গিয়ে ২ ঘর এগোনো।
জোর করে cast করে marks-কে int ** হিসেবে পাঠালে কী হয়? ফাংশন p[1] পড়ার সময় মনে করবে সেখানে একটি ঠিকানা আছে — অথচ সেখানে আছে নম্বর 82 আর 90-এর বাইট! সেই “ঠিকানায়” যেতে চাইলে প্রায় নিশ্চিত segmentation fault. নিয়মটি মনে রাখুন: অ্যারে একবারই pointer-এ decay হয়, শুধু বাইরের মাত্রায়। int[3][4] হয় int (*)[4], কখনোই int ** নয়।
ফাংশনের ভেতরে sizeof(a) / sizeof(a[0]) দিয়ে সারির সংখ্যা বের করার চেষ্টা করবেন না। প্যারামিটার a আসলে একটি pointer, তাই sizeof(a) দেয় pointer-এর মাপ (সাধারণত ৮), অ্যারের নয়। সারির সংখ্যা সবসময় আলাদা প্যারামিটারে পাঠান।
৫ · Heap-এ 2D অ্যারে — তিনটি উপায়, তিনটি free-নিয়ম
এতক্ষণ মাপ আগে থেকে জানা ছিল। কিন্তু ধরুন একটি স্কুলের ফলাফল সফটওয়্যার — কতজন শিক্ষার্থী আর কয়টি বিষয়, তা জানা যাবে ফাইল পড়ার পরে। তখন পর্ব ২০-এর malloc দিয়ে heap থেকে জায়গা নিতে হবে। 2D অ্যারের জন্য এর তিনটি প্রচলিত উপায় আছে।
উপায় ১ — একটি একটানা block, সূত্র নিজের হাতে
int *p = malloc((size_t)rows * cols * sizeof *p); /* rows × cols ঘর, একবারে */
if (p == NULL) { /* সামলান */ }
p[i * cols + j] = 90; /* "a[i][j]" */
free(p); /* একটি malloc, একটি free */
স্থির অ্যারের মতোই সব ঘর পাশাপাশি, cache-বান্ধব। একটি malloc, একটি free — ভুলের সুযোগ সবচেয়ে কম। (size_t) cast-টি গুণফলকে বড় টাইপে করায়, যাতে বড় মাপে int-এর overflow না হয়। সূত্রটি বারবার লিখতে না চাইলে একটি ছোট ফাংশন বানিয়ে নিন: size_t idx(size_t i, size_t j, size_t cols) { return i * cols + j; }. NumPy, OpenCV-র মতো বড় library-গুলোও ভেতরে ঠিক এভাবেই ডেটা রাখে।
উপায় ২ — সারির pointer-এর অ্যারে (jagged)
প্রথমে একটি অ্যারে নিন, যার প্রতিটি ঘরে একটি সারির ঠিকানা থাকবে; তারপর প্রতিটি সারির জন্য আলাদা malloc. এতে সারিগুলোর দৈর্ঘ্য আলাদা হতে পারে — একে বলে jagged array. যেমন একটি কলেজে বিভাগভেদে শিক্ষার্থীর সংখ্যা আলাদা। ধাপে ধাপে দেখুন, আর শেষের free-এর ক্রমটি বিশেষভাবে খেয়াল করুন:
- শুধু একটি pointer.
int **rows;— stack-এ একটি ভ্যারিয়েবল, যে পরে একটি “ঠিকানার অ্যারে”-র ঠিকানা রাখবে। এখনো heap-এ কিছু নেওয়া হয়নি। - ঠিকানার অ্যারে।
rows = malloc(3 * sizeof *rows);— heap-এ ৩টি pointer-এর জায়গা (৬৪-বিট কম্পিউটারে প্রতিটি ৮ বাইট, তাই 5000, 5008, 5016). এখনো এগুলো কোনো সারি দেখায় না। - প্রতিটি সারির আলাদা malloc. লুপে
rows[i] = malloc(len[i] * sizeof *rows[i]);— তিনটি আলাদা block, তিনটি আলাদা জায়গায় (7100, 8240, 6320). দৈর্ঘ্যও আলাদা: ৪, ২, ৩. খেয়াল করুন, সারিগুলো আর পাশাপাশি নেই — cache-এর সুবিধা কমে গেল। rows[1][1]পড়া। লাল দাগের পথ: প্রথমেrowsথেকে 5000, তারপর 5008 থেকেrows[1]= 8240 পড়া, তারপর 8240 + ১ × ৪ = 8244. দুইবার মেমরি থেকে ঠিকানা পড়া — স্থির অ্যারের মতো একবার হিসাব নয়। আর সারি 1-এ মাত্র ২টি ঘর, তাইrows[1][3]লিখলে সোজা অন্যের মেমরিতে।- ফেরত দেওয়া, ধাপ ১ — আগে সারিগুলো।
for (i…) free(rows[i]);— তিনটি সারির block ছাড়া হলো। এই কাজের জন্য ঠিকানাগুলো দরকার, যা আছেrows[]-এ। তাইrowsঅ্যারেটি এখনো ছাড়া যাবে না। - ধাপ ২ — শেষে ঠিকানার অ্যারে।
free(rows); rows = NULL;— নেওয়ার উল্টো ক্রমে ফেরত। উল্টো করলে কী হতো? আগেfree(rows)করলে সারির ঠিকানাগুলো হারিয়ে যেত — তিনটি block চিরতরে memory leak. আর তারপরrows[i]পড়তে গেলে use-after-free. নিয়ম: যতবার malloc, ঠিক ততবার free — নেওয়ার উল্টো ক্রমে।
এই উপায়ে আরেকটি ঝামেলা আছে: ৩টি সারির মাঝখানে, ধরুন দ্বিতীয় সারির malloc ব্যর্থ হলো। তখন আগের সারিটি আর ঠিকানার অ্যারেটি ছেড়ে তারপর বের হতে হবে — নইলে leak. মোট malloc সংখ্যা rows + 1, তাই ভুলের জায়গাও তত বেশি।
উপায় ৩ — pointer to array: একটি block, তবু a[i][j]
কলামের সংখ্যা যদি জানা থাকে (ধরুন সবসময় ৪টি বিষয়), কিন্তু সারির সংখ্যা চলার সময় ঠিক হয়, তাহলে সবচেয়ে সুন্দর উপায় এটি:
#define COLS 4
int (*a)[COLS] = malloc(rows * sizeof *a); /* sizeof *a = 4 × 4 = 16 বাইট, একটি সারি */
a[i][j] = 90; /* স্বাভাবিক লেখা, সূত্র compiler করে */
free(a); /* একটি malloc, একটি free */
int (*a)[COLS] পড়ুন ভেতর থেকে বাইরে: “a একটি pointer, যা ৪টি int-এর একটি অ্যারেকে দেখায়।” বন্ধনী না দিলে int *a[COLS] হয়ে যেত সম্পূর্ণ অন্য জিনিস — ৪টি pointer-এর অ্যারে। এই পড়ার কৌশল পর্ব ৪৫-এ বিস্তারিত দেখবেন। C99 থেকে কলামের সংখ্যাও ভ্যারিয়েবল হতে পারে — নিচের প্রোগ্রামে ঠিক সেটাই, সাথে VLA প্যারামিটার দিয়ে transpose:
#include <stdio.h>
#include <stdlib.h>
void transpose(int r, int c, int a[r][c], int t[c][r]) {
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++)
t[j][i] = a[i][j];
}
int main(void) {
int r = 2, c = 3; /* sizes known only at run time */
int (*a)[c] = malloc(sizeof(int[r][c]));
int (*t)[r] = malloc(sizeof(int[c][r]));
if (a == NULL || t == NULL) { free(a); free(t); return 1; }
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++)
a[i][j] = i * c + j + 1; /* 1 2 3 / 4 5 6 */
transpose(r, c, a, t);
for (int i = 0; i < c; i++) printf("%d %d\n", t[i][0], t[i][1]);
free(a); free(t);
return 0;
}int (*a)[c] মানে “cটি int-এর সারিকে দেখায় এমন pointer”, আর sizeof(int[r][c]) হলো পুরো টেবিলের মাপ — ২ × ৩ × ৪ = ২৪ বাইট, একটি একটানা block. ফলে heap-এ থেকেও a[i][j] স্বাভাবিকভাবে লেখা যায়, compiler চলার সময় c-এর মান দিয়ে সূত্রটি হিসাব করে। transpose-এর প্যারামিটারে int a[r][c] আর int t[c][r] — মাপগুলো আগে এসেছে বলেই অ্যারের ঘোষণায় ব্যবহার করা গেছে। একটি malloc ব্যর্থ হলে দুটিই free করা নিরাপদ, কারণ free(NULL) কিছুই করে না। আউটপুট তিনটি লাইন: 1 4, 2 5, 3 6 — ঠিক স্টেপারের transpose. দুটি malloc, দুটি free — কোনো leak নেই।
কোনটি কখন?
| উপায় | malloc / free সংখ্যা | মেমরিতে একটানা? | লেখার ধরন | কখন বেছে নেবেন |
|---|---|---|---|---|
| একটানা block + সূত্র | ১ / ১ | হ্যাঁ | p[i*cols + j] | সাধারণ ক্ষেত্রে প্রথম পছন্দ; অন্য library-তে ডেটা পাঠাতে |
| pointer to array | ১ / ১ | হ্যাঁ | a[i][j] | সব সারি সমান দৈর্ঘ্যের, আর আপনার compiler VLA বা স্থির কলাম সমর্থন করে |
| সারির pointer (jagged) | rows + ১ / rows + ১ | না | rows[i][j] | সারির দৈর্ঘ্য সত্যিই আলাদা, বা সারি আলাদাভাবে বদলাতে/অদলবদল করতে হয় |
অনেক অভিজ্ঞ প্রোগ্রামারের পরামর্শ: সব সারি সমান হলে একটানা block-ই নিন। Jagged অ্যারে তখনই, যখন তার নমনীয়তা সত্যিই দরকার — যেমন প্রতিটি শিক্ষার্থীর আলাদা সংখ্যক বিষয়, বা দুটি সারি অদলবদল করতে চান শুধু দুটি pointer বদলে।
৬ · তিন মাত্রা আর স্ট্রিংয়ের অ্যারে
3D অ্যারে — ভবন × তলা × কক্ষ
একটি হাসপাতালের কথা ভাবুন: ২টি ভবন, প্রতিটিতে ৬টি তলা, প্রতি তলায় ২০টি কক্ষ। কোন কক্ষে কতজন রোগী, তা রাখতে লাগবে একটি 3D array:
int patients[2][6][20]; /* ২ × ৬ × ২০ = ২৪০টি int */
patients[1][3][7] = 2; /* ভবন 1, তলা 3, কক্ষ 7 */
/* ঘর নম্বর = (b × 6 + f) × 20 + r → (1 × 6 + 3) × 20 + 7 = 187 */
সূত্রটি একই চিন্তার সম্প্রসারণ: আগে আগের পুরো ভবনগুলো পার হতে হয় (প্রতিটি ৬ × ২০ ঘর), তারপর আগের তলাগুলো (প্রতিটি ২০ ঘর), তারপর কক্ষ। শেষ মাত্রাটি সবচেয়ে দ্রুত বদলায়। ফাংশনে পাঠাতে প্রথমটি ছাড়া সব মাপ লাগবে: void f(int b, int p[][6][20]). বাস্তবে সবচেয়ে পরিচিত 3D অ্যারে হলো রঙিন ছবি — unsigned char img[1080][1920][3]: সারি × কলাম × রঙ (লাল, সবুজ, নীল)। মাপটি দেখুন — ১০৮০ × ১৯২০ × ৩ = ৬২,২০,৮০০ বাইট, প্রায় ৬ MB. Local ভ্যারিয়েবল হিসেবে ঘোষণা করলে Windows-এ (stack সাধারণত ১ MB) জায়গাই হবে না, আর Linux-এর সাধারণ ৮ MB stack-এরও প্রায় পুরোটা চলে যাবে — stack overflow-এর ঝুঁকি। এত বড় অ্যারে heap-এ নিন।
স্ট্রিংয়ের অ্যারে — char names[5][20] বনাম char *names[5]
পর্ব ১৯-এ দেখেছেন, স্ট্রিং মানে char-এর অ্যারে। তাহলে অনেকগুলো নাম রাখার array of strings দুইভাবে বানানো যায়, আর এই দুটির পার্থক্য পরীক্ষা ও ইন্টারভিউর প্রিয় প্রশ্ন:
char names[5][20]
একটি আসল 2D অ্যারে: ৫ সারি × ২০ অক্ষর = ১০০ বাইট, একটানা। প্রতিটি নাম সর্বোচ্চ ১৯ অক্ষর (শেষে '\0'). “Rafi” রাখলেও পুরো ২০ বাইট খরচ। কিন্তু অক্ষর বদলানো যায় — names[0][0] = 'R'; নিরাপদ।
char *names[5]
৫টি pointer-এর অ্যারে (৬৪-বিটে ৪০ বাইট), প্রতিটি অন্য কোথাও রাখা একটি স্ট্রিং দেখায় — একরকম jagged অ্যারে। = {"Rafi", "Nusrat"} লিখলে pointer-গুলো string literal দেখায়, যা বদলানো undefined behavior.
main-এর char *argv[] ঠিক দ্বিতীয় ধরনের — প্রতিটি কমান্ড-লাইন আর্গুমেন্টের দৈর্ঘ্য আলাদা। পর্ব ২৫-এ এটি নিয়ে কাজ করবেন।
৭ · সীমার বাইরে — পাশের সারি চুপচাপ নষ্ট হয়
পর্ব ১৬-এ দেখেছেন, C অ্যারের সীমা পাহারা দেয় না। 2D অ্যারেতে বিপদটা আরও সূক্ষ্ম, কারণ সারিগুলো গায়ে গা লাগিয়ে থাকে। int marks[3][4]-এ ভুল করে marks[0][4] লিখলেন (কলাম 4 নেই, আছে 0–3). সূত্রে বসান: 0 × 4 + 4 = ৪ — যা আসলে marks[1][0]-এর ঘর! অর্থাৎ শিক্ষার্থী 0-এর “পঞ্চম বিষয়ের” নম্বর বসাতে গিয়ে আপনি শিক্ষার্থী 1-এর বাংলার নম্বর মুছে দিলেন। কোনো crash নেই, কোনো error নেই — শুধু ভুল ফলাফল।
marks[0][4] “আসলে marks[1][0]” — এটি ঘটনার ব্যাখ্যা, অনুমতি নয়। C-এর নিয়মে প্রতিটি index তার নিজের মাপের ভেতরে থাকতে হবে; বাইরে গেলে সেটি undefined behavior, আর optimizer সেই ধরে নিয়ে কোড বদলে দিতে পারে। সবচেয়ে বেশি ঘটে যে দুটি ভুল: লুপে j <= COLS (একটি বেশি — off-by-one), আর সারি-কলামের সীমা অদলবদল (i < COLS, j < ROWS). দ্বিতীয়টি বর্গাকার টেবিলে কখনো ধরা পড়ে না।
আরেকটি ভুল আসে Python বা MATLAB থেকে আসা মানুষের হাত থেকে: marks[i, j]. C-তে এটি ভুল নয়, আরও খারাপ — বৈধ কোড! কমা এখানে একটি অপারেটর, যা বাঁ দিকেরটা ফেলে দিয়ে ডান দিকেরটা রাখে। ফলে marks[i, j] হয়ে যায় marks[j] — একটি পুরো সারি। -Wall চালু থাকলে GCC সাধারণত সতর্ক করে: left-hand operand of comma expression has no effect.
যন্ত্র দিয়ে ধরা
পর্ব ৩১-এ ডিবাগিংয়ের যন্ত্রগুলো বিস্তারিত দেখবেন। এখানে শুধু 2D অ্যারের জন্য একটি গুরুত্বপূর্ণ সূক্ষ্মতা:
- AddressSanitizer (
-fsanitize=address): পুরো অ্যারের বাইরে গেলে — যেমনmarks[3][0]— হাতেনাতে ধরে। কিন্তুmarks[0][4]তখনো অ্যারের নিজের ৪৮ বাইটের ভেতরে, তাই ASan-এর চোখে এটি বৈধ মেমরি! - UBSan (
-fsanitize=bounds): স্থির মাপের অ্যারেতে প্রতিটি index তার ঘোষিত মাপের সাথে মিলিয়ে দেখে, তাইmarks[0][4]-এ বলেindex 4 out of bounds for type 'int [4]'. Heap-এর একটানা block-এ (p[i*cols + j]) কিন্তু এই সুবিধা নেই — সেখানে সূত্রের ভুল আপনাকেই ধরতে হবে।
তাই পরীক্ষার সময় দুটোই একসাথে চালান: gcc -Wall -Wextra -g -fsanitize=address,undefined. UBSan-এর undefined গ্রুপের ভেতরেই bounds আছে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি (প্রোগ্রামার বা ব্যাংকের IT পদ) আর কোডিং ইন্টারভিউর প্রস্তুতিতে এই বিষয়ে যে ধরনের প্রশ্নের জন্য তৈরি থাকা ভালো:
int a[3][4], base 1000, int ৪ বাইট —a[2][3]-এর ঠিকানা? 1000 + (2 × 4 + 3) × 4 = 1044. Column-major ধরে চাইলে সূত্র হয় base + (j × ROWS + i) × size.- ফাংশনের প্যারামিটারে কলাম সংখ্যা কেন বাধ্যতামূলক? ঠিকানার সূত্রে
i × COLSআছে; সারির সংখ্যা সূত্রে লাগে না, তাই শুধু প্রথম মাপ বাদ দেওয়া যায়। char s[5][20]আরchar *s[5]-এর পার্থক্য? প্রথমটি ১০০ বাইটের আসল 2D অ্যারে; দ্বিতীয়টি ৫টি pointer, যারা অন্য জায়গার স্ট্রিং দেখায়।- দুটি matrix কখন গুণ করা যায়, আর খরচ কত? A-এর কলাম = B-এর সারি হলে; সাধারণ পদ্ধতিতে n × n matrix-এ O(n³).
- Jagged 2D অ্যারে free করার ক্রম? আগে প্রতিটি সারি, শেষে সারির pointer-এর অ্যারে।
৮ · বাস্তব জগতে — ছবি, স্প্রেডশিট, গেম, AI আর DP
এই পর্বের ধারণাগুলো কোনো পাঠ্যবইয়ের খেলনা নয়। আপনার ফোনের প্রায় প্রতিটি অ্যাপ ভেতরে কোথাও না কোথাও 2D বা 3D অ্যারে নিয়ে কাজ করছে।
ছবি
একটি রঙিন ছবি = সারি × কলাম × ৩টি রঙ। ফটো এডিটরের “উজ্জ্বলতা বাড়ান” মানে প্রতিটি ঘরে একটি সংখ্যা যোগ। ছবি ঘোরানো মানে প্রায় একটি transpose. Instagram-এর ফিল্টার থেকে মুখ চেনার সফটওয়্যার — সব এখান থেকে শুরু।
img[row][col][rgb]স্প্রেডশিট
Excel-এর প্রতিটি শিট সারি আর কলামের বিশাল টেবিল — আধুনিক Excel-এ ১০,৪৮,৫৭৬টি সারি আর ১৬,৩৮৪টি কলাম। তবে বেশিরভাগ ঘর ফাঁকা থাকে বলে ভেতরে সব ঘরের জন্য জায়গা রাখা হয় না (ভাবনার প্রশ্ন ৪)।
cell[row][col]গেমের ম্যাপ
লুডো, দাবা, সুডোকু, পুরোনো দিনের Snake বা Pac-Man — সব একটি 2D গ্রিড। প্রতিটি ঘরে একটি সংখ্যা: দেয়াল, খালি পথ, খেলোয়াড়। “পাশের ঘর” মানে [i-1][j], [i+1][j], [i][j-1], [i][j+1] — আর সীমা যাচাই না করলে বোর্ডের কিনারায় বাগ।
AI ও মেশিন লার্নিং
একটি neural network-এর প্রতিটি স্তর আসলে একটি বড় matrix গুণ। বহুমাত্রিক অ্যারেকে AI-এর জগতে বলে tensor. PyTorch, TensorFlow-এর ভেতরের দ্রুত অংশগুলো C/C++-এ লেখা, আর তারা ঠিক এই পর্বের সূত্রেই ডেটা রাখে।
weights[out][in]আরও একটি জায়গা, যা এই কোর্সেই আসছে: পর্ব ৪২-এর dynamic programming-এ প্রায়ই একটি 2D টেবিল dp[i][j] ভরাতে হয় — যেমন দুটি শব্দ কতটা আলাদা (edit distance), যা বানান-সংশোধক আর DNA মেলানোর সফটওয়্যারে লাগে। পর্ব ৩৮-এর গ্রাফেও একটি পরিচিত রূপ হলো adjacency matrix — g[u][v] = 1 মানে u থেকে v-তে রাস্তা আছে।
Matrix-এর গণিত গভীরে বুঝতে চাইলে Math for AI কোর্সের linear algebra অংশ, আর matrix কীভাবে neural network হয়ে ওঠে তা দেখতে Deep Learning কোর্স দেখুন।
৯ · AI-এর যুগে — matrix-কোডে AI কোথায় ভুল করে
“একটি matrix গুণের C প্রোগ্রাম লিখে দাও” — যেকোনো AI কয়েক সেকেন্ডে লিখে দেবে, আর ৩ × ৩ উদাহরণে উত্তরও মিলবে। সমস্যা হলো, এই পর্বের প্রায় সব ভুল ঠিক বর্গাকার, ছোট উদাহরণেই লুকিয়ে থাকে। AI সাধারণত অবর্গাকার matrix, বড় মাপ বা sanitizer দিয়ে নিজের কোড চালিয়ে দেখে না।
AI সাধারণত যেসব 2D-অ্যারে ভুল করে
| AI-এর ভুল | কোডে কেমন দেখায় | কীভাবে ধরবেন |
|---|---|---|
স্থির 2D অ্যারের জন্য int ** প্যারামিটার | void f(int **m), ডাকা হচ্ছে f(marks) দিয়ে | Compiler-এর incompatible pointer type; প্যারামিটার হওয়া উচিত int m[][COLS] বা VLA |
| সারি-কলামের index বা সীমা অদলবদল | for (i < cols) for (j < rows) a[i][j] | ২ × ৫-এর মতো অবর্গাকার matrix দিয়ে চালান; UBSan |
Jagged অ্যারেতে শুধু free(rows) | লুপে সারি malloc, কিন্তু সারিগুলোর free নেই | malloc আর free গুনুন; Valgrind-এ definitely lost |
| Cache-বিরোধী লুপের ক্রম | বাইরে j, ভেতরে i, অথচ পড়া হচ্ছে a[i][j] | শেষ index ভেতরের লুপে আছে কি না দেখুন; বড় মাপে সময় মাপুন |
Python-এর অভ্যাসে a[i, j] | কম্পাইল হয়, কিন্তু মানে a[j] | -Wall-এর comma-warning |
| বড় অ্যারে local হিসেবে | double m[2000][2000]; ফাংশনের ভেতরে (৩২ MB) | শুরুতেই crash; heap-এ নিতে বলুন |
AI-কে matrix-কোড চাইলে শুরুতেই বিন্যাস আর পরীক্ষার শর্ত বলে দিন। শেষ লাইনটি খেয়াল করুন — অবর্গাকার পরীক্ষা চাওয়া হয়েছে, কারণ ভুলগুলো সেখানেই ধরা পড়ে।
AI-এর 2D-অ্যারে কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
চুক্তি বুঝলে AI-এর ভুলও চোখে পড়ে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Fortran আর MATLAB কেন column-major, আর C কেন row-major? দুই জগতের মধ্যে ডেটা পাঠালে কী গোলমাল হয়?
এর পেছনে কোনো “সঠিক” উত্তর নেই — আছে ইতিহাস আর একবার নেওয়া সিদ্ধান্তের দীর্ঘ ছায়া। Fortran তৈরি হয় ১৯৫০-এর দশকের শেষে, IBM-এ John Backus-এর দলের হাতে, বিজ্ঞানী ও প্রকৌশলীদের হিসাবের জন্য। গণিতে matrix-কে প্রায়ই কলাম-ভেক্টরের সমষ্টি হিসেবে দেখা হয়, আর Fortran সেই নিয়মে অ্যারে রাখে কলাম ধরে। পরে linear algebra-র বিখ্যাত library-গুলো — BLAS, LINPACK, পরে LAPACK — Fortran-এ লেখা হয়, ফলে column-major হয়ে যায় বৈজ্ঞানিক গণনার মান। ১৯৭০-এর দশকের শেষে Cleve Moler MATLAB বানান মূলত ছাত্রদের সহজে LINPACK ও EISPACK ব্যবহার করতে দেওয়ার জন্য — তাই MATLAB-ও column-major.
C এসেছে ভিন্ন পথে — B আর BCPL থেকে, সিস্টেম প্রোগ্রামিংয়ের জন্য। C-তে 2D অ্যারে আলাদা কোনো ধারণা নয়, শুধু “অ্যারের অ্যারে”: a[i] নিজেই একটি অ্যারে। এই সংজ্ঞা থেকে row-major আপনা-আপনি চলে আসে। C++, Java, Python-এর NumPy (ডিফল্টভাবে) এই পথ ধরেছে।
গোলমাল কোথায়? C প্রোগ্রাম থেকে একটি Fortran-ভিত্তিক BLAS ফাংশনে ৩ × ২ matrix-এর ৬টি সংখ্যা পাঠালেন। Fortran সেই ৬টি সংখ্যা কলাম ধরে পড়বে — আপনার matrix তার চোখে transpose হয়ে গেল! কোনো error নেই, শুধু ভুল উত্তর। এজন্য CBLAS ইন্টারফেসে প্রতিটি ফাংশনে বলে দিতে হয় বিন্যাস row-major না column-major. NumPy-তে np.array(..., order='C') বা order='F' দিয়ে এটি বেছে নেওয়া যায়, আর arr.T (transpose) অনেক সময় ডেটা কপিই করে না — শুধু “এখন থেকে কলাম ধরে পড়ো” বলে দেয়।
একটি সুন্দর গাণিতিক কৌশলও আছে: row-major-এ রাখা A আসলে column-major-এর চোখে Aᵀ. আর (AB)ᵀ = BᵀAᵀ. তাই C থেকে Fortran-এর গুণ ফাংশনে A আর B-এর ক্রম অদলবদল করে পাঠালে কোনো কপি ছাড়াই সঠিক উত্তর আসে। অনেক উচ্চ-দক্ষতার library ভেতরে এই কৌশলই খাটায়।
মূল উপলব্ধি: ডেটার অর্থ শুধু বাইটে থাকে না, থাকে বাইট পড়ার চুক্তিতেও। দুটি সিস্টেম জোড়া দেওয়ার সময় সবচেয়ে বিপজ্জনক ভুল হলো যেগুলো নীরবে ভুল উত্তর দেয় — endianness, একক (মিটার বনাম ফুট), আর এই row/column বিন্যাস।
প্র ০২আপনার ফোনে তোলা একটি ছবি মেমরিতে আসলে কীভাবে থাকে?
ফাইলে (JPEG, PNG) ছবি থাকে সংকুচিত অবস্থায়। কিন্তু খোলার পরে, স্ক্রিনে দেখানো বা এডিট করার জন্য, ছবি হয়ে যায় একটি সাধারণ বহুমাত্রিক অ্যারে — সারির পর সারি pixel, row-major. প্রতিটি pixel-এ সাধারণত ৩টি সংখ্যা: লাল, সবুজ, নীল (RGB), প্রতিটি ০ থেকে ২৫৫ — অর্থাৎ ১ বাইট। স্বচ্ছতার জন্য চতুর্থ একটি “alpha” মানও থাকতে পারে।
একটি ১৯২০ × ১০৮০ (Full HD) ছবি তাই খোলা অবস্থায় প্রায় ৬ MB, যদিও JPEG ফাইলটি হয়তো কয়েকশো KB. ফোনের ১২ মেগাপিক্সেলের ছবি খোলা অবস্থায় ৩৬ MB-এর মতো! এজন্যই ফোনের গ্যালারি অ্যাপ পুরো ছবি না খুলে ছোট “thumbnail” দেখায়।
কয়েকটি বাস্তব জটিলতা:
- রঙ কোথায়? RGB একসাথে পাশাপাশি রাখা যায় (RGBRGBRGB…, “interleaved”), অথবা আগে সব লাল, তারপর সব সবুজ, তারপর সব নীল (“planar”). AI-এর জগতে এই দুইয়ের নাম HWC আর CHW — TensorFlow ঐতিহ্যগতভাবে প্রথমটি, PyTorch দ্বিতীয়টি পছন্দ করে। ভুল বিন্যাসে পড়লে ছবি রঙ-গোলানো বা ভাঙা দেখায়।
- Stride: অনেক ফরম্যাট আর হার্ডওয়্যার প্রতিটি সারির শেষে কয়েক বাইট ফাঁকা রাখে, যাতে পরের সারি একটি সুবিধাজনক ঠিকানায় শুরু হয়। যেমন BMP ফরম্যাটে প্রতিটি সারি ৪ বাইটের গুণিতকে মেলানো হয়, আর সারিগুলো সাধারণত নিচ থেকে উপরে রাখা। তখন সূত্র হয়
base + i × stride + j × 3, যেখানে stride ≥ কলাম × ৩. Stride ভুলে গেলে ছবি তির্যকভাবে বেঁকে যায় — ইমেজ প্রসেসিংয়ে এটি একটি পরিচিত বাগ। - ভিডিও: সময়ের মাত্রা যোগ হয় — ফ্রেম × সারি × কলাম × রঙ, একটি 4D অ্যারে।
মূল উপলব্ধি: “ছবি” বলে মেমরিতে আলাদা কিছু নেই — আছে সংখ্যার একটি লম্বা সারি আর তা পড়ার একটি চুক্তি (মাপ, রঙের ক্রম, stride). Computer vision-এর প্রথম শিক্ষা এটাই।
প্র ০৩AI-এর জন্য GPU এত গুরুত্বপূর্ণ কেন? এর সাথে matrix-এর সম্পর্ক কী?
একটি neural network-এর একটি স্তর মূলত এই কাজ করে: ইনপুটের একটি matrix-কে ওজনের (weights) একটি matrix দিয়ে গুণ, তারপর প্রতিটি ঘরে একটি সহজ ফাংশন। বড় ভাষা-মডেলে এমন স্তর শত শত, আর matrix-গুলোর মাপ হাজার × হাজার। ফলে AI চালানোর সময়ের সিংহভাগ যায় matrix গুণে।
এবার স্টেপারটি মনে করুন: C[i][j]-এর হিসাব অন্য কোনো C-ঘরের ওপর নির্ভর করে না। ১০ লাখ ঘর মানে ১০ লাখটি স্বাধীন কাজ — একসাথে করা যায়। CPU-তে কয়েকটি শক্তিশালী core, প্রতিটি জটিল কাজে দক্ষ। GPU-তে হাজার হাজার সহজ core, যারা একই নির্দেশ একসাথে অনেক ডেটায় চালায়। মূলত ভিডিও গেমের জন্য বানানো (প্রতিটি pixel-এর রঙ আলাদাভাবে হিসাব), কিন্তু matrix গুণের জন্যও একদম মানানসই।
ইতিহাসের মোড়: ২০১২ সালে AlexNet নামের একটি neural network ছবি চেনার প্রতিযোগিতা ImageNet-এ আগের সব পদ্ধতিকে বড় ব্যবধানে হারায় — আর সেটি প্রশিক্ষণ দেওয়া হয়েছিল দুটি সাধারণ গেমিং GPU-তে। এর পর থেকেই deep learning-এর বিস্ফোরণ। পরে NVIDIA ২০১৭ সালে Volta প্রজন্মে “Tensor Core” আনে — শুধু ছোট matrix-এর গুণ-যোগ দ্রুত করার জন্য বিশেষ সার্কিট। Google-এর TPU-ও মূলত একটি বিশাল matrix-গুণের যন্ত্র।
আরেকটি গভীর কারণ: n × n matrix গুণে ডেটা মাত্র n² টি, কিন্তু কাজ n³. প্রতিটি সংখ্যা মেমরি থেকে একবার এনে প্রায় n বার ব্যবহার করা যায়। মেমরি যেখানে সবসময় ধীর, সেখানে এমন কাজ হার্ডওয়্যারের জন্য স্বপ্নের মতো — যদি ডেটা ঠিক বিন্যাসে থাকে আর cache ঠিকমতো ব্যবহার হয়। গণিতবিদরাও বসে নেই: ১৯৬৯ সালে Volker Strassen দেখান যে matrix গুণ n³-এর চেয়েও কম গুণে করা সম্ভব।
মূল উপলব্ধি: AI-এর অগ্রগতি শুধু নতুন অ্যালগরিদমের গল্প নয় — এটি এই পর্বের মতো সাধারণ একটি কাজ (matrix গুণ) কে হার্ডওয়্যার, মেমরি-বিন্যাস আর সমান্তরাল চিন্তা দিয়ে অবিশ্বাস্য দ্রুত করার গল্পও।
প্র ০৪যে matrix-এর বেশিরভাগ ঘর শূন্য, সেটি 2D অ্যারেতে রাখা কি বুদ্ধিমানের কাজ?
প্রায়ই নয়। একটি ই-কমার্স সাইটের কথা ভাবুন: ১০ লাখ গ্রাহক × ১ লাখ পণ্য, ঘরে লেখা কে কোন পণ্য কিনেছে। পূর্ণ 2D অ্যারে মানে ১০,০০০ কোটি ঘর — প্রতিটি ৪ বাইট হলে ৪০০ GB! অথচ একজন গ্রাহক হয়তো কিনেছেন গড়ে ২০টি পণ্য — অর্থাৎ ঘরের প্রায় ৯৯.৯৮% শূন্য। এমন matrix-কে বলে sparse matrix. একটি পরিচিত বাস্তব উদাহরণ Netflix Prize-এর ডেটা (২০০৬): প্রায় ৪.৮ লাখ ব্যবহারকারী × প্রায় ১৮ হাজার সিনেমা, কিন্তু রেটিং মাত্র প্রায় ১০ কোটি — ঘরের ১%-এর মতো।
সমাধান: শুধু অশূন্য ঘরগুলো রাখা।
- COO (coordinate): প্রতিটি অশূন্য ঘরের জন্য তিনটি সংখ্যা — (সারি, কলাম, মান)। সহজ, বানানো সহজ।
- CSR (compressed sparse row): তিনটি 1D অ্যারে — সব অশূন্য মান সারি ধরে পরপর, প্রতিটির কলাম নম্বর, আর প্রতিটি সারি কোথায় শুরু তার তালিকা। এক সারির সব মান পাশাপাশি থাকে বলে সারি ধরে গুণে দ্রুত — আবার row-major-এর চিন্তা!
দাম আছে: a[i][j] আর এক লাফে পাওয়া যায় না, খুঁজতে হয়; কোনো ঘর শূন্য থেকে অশূন্য হলে কাঠামো নতুন করে সাজাতে হতে পারে। তাই নিয়মটা হলো ঘনত্ব দেখে বেছে নেওয়া। পর্ব ৩৮-এ গ্রাফের জন্য ঠিক এই সিদ্ধান্তই নেবেন — adjacency matrix (ঘন গ্রাফ) বনাম adjacency list (sparse graph). Spreadsheet, সার্চ ইঞ্জিনের শব্দ × পাতার টেবিল, সামাজিক নেটওয়ার্কের বন্ধুত্ব — সব জায়গায় sparse কাঠামো।
মূল উপলব্ধি: ডেটার আকৃতি দেখে কাঠামো বাছাই করুন, অভ্যাস দেখে নয়। “সবকিছু 2D অ্যারে” চিন্তা ছোট সমস্যায় চলে, বড় সমস্যায় মেমরি আর সময় দুটোই খেয়ে ফেলে।
প্র ০৫C99-এ যে VLA আনা হলো, C11-এ তা “ঐচ্ছিক” হয়ে গেল কেন?
১৯৯৯ সালের C99 মানদণ্ড VLA আনে — চলার সময়ের মাপে local অ্যারে (int buf[n];), আর এই পর্বে দেখা VLA প্যারামিটার (int a[rows][cols]). বৈজ্ঞানিক গণনার প্রোগ্রামাররা খুশি হন, কারণ Fortran-এ এমন সুবিধা আগেই ছিল। কিন্তু ২০১১ সালের C11 মানদণ্ডে VLA-কে বাধ্যতামূলক থেকে ঐচ্ছিক করা হয় — কোনো compiler সমর্থন না করলে সে __STDC_NO_VLA__ ম্যাক্রো দিয়ে জানিয়ে দেয়।
কারণগুলো:
- Stack overflow-এর ঝুঁকি:
int buf[n];-এ n যদি ব্যবহারকারীর ইনপুট থেকে আসে আর অনেক বড় হয়, প্রোগ্রাম stack ছাড়িয়ে যায় — কোনো NULL ফেরত নেই, কোনো সতর্কতা নেই, সরাসরি crash বা আরও খারাপ, অন্য মেমরিতে চড়াও। malloc ব্যর্থ হলে অন্তত NULL দেয়; VLA-র ব্যর্থতা ধরার কোনো উপায়ই নেই। - Compiler-নির্মাতাদের অনীহা: Microsoft-এর MSVC কখনো VLA সমর্থন করেনি। ছোট embedded compiler-এর জন্যও এটি বাস্তবায়ন করা ঝামেলার। মানদণ্ড বাধ্যতামূলক রাখলে অনেক compiler “মানদণ্ড-বহির্ভূত” হয়ে থাকত।
- নিরাপত্তা-সচেতন প্রকল্পের প্রত্যাখ্যান: Linux kernel ২০১৮ সালের দিকে নিজের কোড থেকে সব VLA সরিয়ে ফেলে, আর এখন
-Wvlaদিয়ে নতুন VLA আটকায় — কারণ kernel-এর stack খুব ছোট, আর VLA নিরাপত্তা-ঝুঁকি।
মজার মোড়: বিপদটা আসলে stack-এ বড় অ্যারে বানানোয়, VLA টাইপে নয়। এই পর্বে দেখা int (*a)[c] = malloc(...) বা void f(int r, int c, int a[r][c]) stack-এ কোনো বড় জায়গা নেয় না — শুধু compiler-কে মাপ জানায়, যাতে a[i][j]-এর সূত্র সে নিজে হিসাব করতে পারে। এই পার্থক্য স্বীকার করে সর্বশেষ C23 মানদণ্ডে এই “variably modified” টাইপগুলো আবার বাধ্যতামূলক করা হয়েছে, যদিও stack-এ VLA বানানো এখনো ঐচ্ছিক।
মূল উপলব্ধি: একটি ভাষার বৈশিষ্ট্য ভালো না খারাপ, তা নির্ভর করে কোথায় ব্যবহার হচ্ছে। VLA প্যারামিটার পরিষ্কার কোডের বন্ধু; ইনপুট-নির্ভর মাপের stack-অ্যারে নিরাপত্তার শত্রু। পর্ব ৪৭-এ আধুনিক C-এর এমন আরও বিবর্তন দেখবেন।
অনুশীলন
প্রথম তিনটিতে কোড লিখতে হবে না — খাতা-কলমে হিসাব আর ভাবাই আসল কাজ।
-
ঠিকানা হিসাব:
int a[4][5], base address 2000,int৪ বাইট। (ক) row-major-এa[2][3]-এর ঠিকানা কত? (খ) একই অ্যারে column-major হলে কত হতো? (গ) পুরো অ্যারের মাপ কত বাইট?(ক) ঘর নম্বর = 2 × 5 + 3 = ১৩, ঠিকানা = 2000 + ১৩ × ৪ = 2052.
(খ) Column-major-এ আগে পুরো কলাম পার হতে হয়, আর প্রতি কলামে ৪টি ঘর (সারির সংখ্যা)। ঘর নম্বর = 3 × 4 + 2 = ১৪, ঠিকানা = 2000 + ১৪ × ৪ = 2056. মাত্র ৪ বাইটের পার্থক্য, কিন্তু সম্পূর্ণ ভিন্ন ঘর।
(গ) ৪ × ৫ × ৪ = ৮০ বাইট.
-
নীরব ভুল খুঁজুন: ফলাফলের প্রোগ্রামে (
result_sheet.c) ভেতরের লুপের শর্তj < SB-এর বদলেj <= SBকরলে প্রথম দুই শিক্ষার্থীর মোট নম্বর সম্ভবত কত দেখাবে? কেন “সম্ভবত”? আর শেষ শিক্ষার্থীর বেলায় বিপদ কেন বেশি?প্রতিটি সারিতে একটি বাড়তি ঘর পড়া হবে —
m[i][4], যা মেমরিতে আসলে পরের সারির প্রথম ঘর। শিক্ষার্থী 0-এর মোট হবে সম্ভবত 315 + 55 = 370, শিক্ষার্থী 1-এর 256 + 88 = 344.“সম্ভবত”, কারণ এটি undefined behavior — C কোনো নিশ্চয়তা দেয় না; optimizer চাইলে অন্যরকম কোড বানাতে পারে। শেষ শিক্ষার্থীর
m[2][4]পুরো অ্যারের বাইরে — সেখানে অন্য কোনো ভ্যারিয়েবল বা আবর্জনা।-fsanitize=addressশুধু এই শেষটি ধরবে; প্রথম দুটি ধরবে-fsanitize=bounds. কোনো crash ছাড়াই প্রোগ্রাম ভুল ফলাফল দেবে — এটাই সবচেয়ে বিপজ্জনক ধরনের বাগ। -
নকশা করুন: একটি বাস কোম্পানির টিকিট সফটওয়্যার — ১০ সারি, প্রতি সারিতে ৪টি আসন (কলাম 0 আর 3 জানালার পাশে)। কোন টাইপের অ্যারে নেবেন? সব খালি জানালার আসন গুনতে লুপ কেমন হবে? আর যদি একেক বাসে একেক সংখ্যক সারি থাকে?
প্রতিটি আসন শুধু “খালি/বুক” — তাই ছোট টাইপই যথেষ্ট:
unsigned char seat[10][4] = {0};(0 = খালি, 1 = বুক)। জানালার খালি আসন গুনতে সব কলাম ঘোরার দরকার নেই:int count = 0; for (int r = 0; r < 10; r++) { if (seat[r][0] == 0) count++; if (seat[r][3] == 0) count++; }বাসভেদে সারির সংখ্যা আলাদা হলেও কলাম সবসময় ৪ — অংশ ৫-এর উপায় ৩ একদম মানানসই:
unsigned char (*seat)[4] = malloc(rows * sizeof *seat);. একটি malloc, একটি free, আরseat[r][c]লেখাও অটুট থাকে। -
Cache-বান্ধব কলাম-যোগফল (কোড লিখুন): একটি ফাংশন লিখুন
void col_sums(int r, int c, int a[r][c], long out[c]), যা প্রতিটি কলামের যোগফলout-এ রাখবে — কিন্তু বাইরের লুপ হবে সারি ধরে, যাতে মেমরি পাশাপাশি পড়া হয়।কৌশল: আগে সব যোগফল 0 করুন, তারপর প্রতিটি সারি একবার পড়ে প্রতিটি ঘর তার কলামের যোগফলে যোগ করুন।
void col_sums(int r, int c, int a[r][c], long out[c]) { for (int j = 0; j < c; j++) out[j] = 0; for (int i = 0; i < r; i++) /* বাইরে সারি */ for (int j = 0; j < c; j++) /* ভেতরে কলাম: a[i][0], a[i][1]… পাশাপাশি */ out[j] += a[i][j]; }aপড়া হচ্ছে পাশাপাশি, আরoutছোট একটি অ্যারে যা পুরোটাই cache-এ থাকে। ফল একই, কিন্তু বড় টেবিলে কলাম-ধরে-ঘোরা সংস্করণের চেয়ে সাধারণত অনেক দ্রুত।longনেওয়া হয়েছে যাতে অনেক সারির যোগফলেintoverflow না হয়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমHeap-এ jagged 2D অ্যারের জীবনচক্র সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সারির pointer-এর অ্যারের জন্য malloc (
rows = malloc(...)) - লুপে প্রতিটি সারির জন্য আলাদা malloc
rows[i][j]দিয়ে ডেটা পড়া-লেখা- লুপে প্রতিটি সারি free
- সারির pointer-এর অ্যারে free (
free(rows)) rows = NULL— ঝুলন্ত pointer না রাখা
int a[3][4] অ্যারেতে a[1][2] শুরু থেকে কত নম্বর ঘরে (offset) থাকে?
- 5
- 6
- 7
- 9
কোন ফাংশন-ঘোষণা int marks[3][4] অ্যারেটি সরাসরি নিতে পারে?
void f(int **m)void f(int m[][4])void f(int m[3][])void f(int m[][])
C-তে একটি বিশাল 2D অ্যারের সব ঘর যোগ করতে কোন লুপ-ক্রম সাধারণত দ্রুততম?
- বাইরে সারি (
i), ভেতরে কলাম (j) - বাইরে কলাম (
j), ভেতরে সারি (i) - দুটিই হুবহু সমান দ্রুত
- নির্ভর করে, কারণ C-এর অ্যারে column-major
int **rows দিয়ে বানানো jagged অ্যারে ছাড়ার সঠিক উপায় কোনটি?
- শুধু
free(rows); - আগে
free(rows);, তারপর লুপেfree(rows[i]); - আগে লুপে
free(rows[i]);, তারপরfree(rows); - কিছুই করতে হবে না, প্রোগ্রাম শেষে সব ফেরত যায়
char a[5][20] আর char *b[5] সম্পর্কে কোনটি সত্য?
- দুটি হুবহু একই জিনিস, শুধু লেখার ধরন আলাদা
b১০০ বাইট জায়গা নেয়b[0][0] = 'X'সবসময় নিরাপদa১০০ বাইটের একটানা ব্লক;b-এর প্রতিটি ঘরে একটি ঠিকানা
AI-এর লেখা transpose ফাংশন ৩ × ৩ matrix-এ ঠিক উত্তর দেয়, কিন্তু ২ × ৫ matrix-এ ভুল বা crash. সবচেয়ে সম্ভাব্য কারণ কী?
- দশমিক সংখ্যার হিসাবে precision হারানো
- Stack overflow
- লুপের সীমা বা index-এ সারি আর কলাম অদলবদল হয়েছে
- Compiler-এর বাগ
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- মেমরি একমাত্রিক; 2D অ্যারে হলো row-major চুক্তি —
a[i][j]থাকেi × COLS + jনম্বর ঘরে। - শেষ index সবচেয়ে ভেতরের লুপে বদলাক — তাতে মেমরি পাশাপাশি পড়া হয়, প্রোগ্রাম দ্রুত চলে।
- ফাংশনে পাঠাতে কলাম সংখ্যা লাগে (
int a[][4]বাint a[r][c]);int **আরint[3][4]এক নয়। - Heap-এ প্রথম পছন্দ একটানা একটি block; jagged অ্যারেতে আগে সারি, শেষে ঠিকানার অ্যারে free.
- Index-এর ভুল পাশের সারি নীরবে নষ্ট করে — অবর্গাকার মাপে পরীক্ষা আর
-fsanitize=address,undefined.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২২ · struct ও union — নিজের ডেটা টাইপপরবর্তী পর্বভিন্ন টাইপের তথ্য এক নামে বাঁধা, মেমরিতে তার বিন্যাস আর padding.
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Math for AIগভীরে যানMatrix, ভেক্টর আর linear algebra — AI কেন matrix-এর ভাষায় কথা বলে।
- Computer Visionপাশের ট্র্যাকPixel-এর অ্যারে থেকে ছবি চেনা — এই পর্বের 3D অ্যারের বাস্তব প্রয়োগ।
- Data Structures & Algorithmsপাশের ট্র্যাকMatrix, গ্রিড, DP টেবিল আর sparse কাঠামো — সমস্যা সমাধানের অনুশীলন।