جلوگیری از integer overflow

هر نوع عددی در زبان ‪C++‬ دارای محدوده خاصی است، بطور مثال برای نوع short int اعداد می‌توانند در بازه‌ای بین ‪-32768‬ و 32767 ذخیره شوند، ولی اگر عددی از این بازه فراتر رفت؟ در اینحالت integer overflow رخ خواهد داد. بطور مثال:

short int x = 32767;
x = x + 1; //x = -32768

با توجه به این مثال، مقدار x حداکثر مقداری است که نوع short int می‌تواند ذخیره کند، اگر عدد ۱ را به این مقدار اضافه کنیم از بازه قابل قبول فراتر خواهد رفت و integer overflow رخ خواهد داد. در نتیجه مقدار x تغییر علامت داده و برابر منفی 32768 می‌گردد. برای جلوگیری از چنین تغییر علامت‌های ناگهانی و برای جلوگیری از محاسبات غلط، باید از integer overflow جلوگیری نمود. روش‌های متفاوتی برای جلوگیری از integer overflow می‌تواند مورد استفاده قرار گیرد.

از یک تابع یا کلاس کمکی استفاده کنید. تابعی ایجاد کنید تا قبل از محاسبات بررسی کند که آیا integer overflow صورت می‌گیرد یا خیر. بطور مثال تابع is_safe_add این کار را انجام می‌دهد:

short int x = 32767, y = 32767, z;
if(is_safe_add(x, y)) z = x + y;

تابع is_safe_add بررسی می‌کند که آیا نتیجهٔ جمع دو عدد x و y بدون integer overflow خواهد بود یا خیر. بنابراین در مثال قبل، هیچگاه عمل جمع صورت نخواهد گرفت زیرا این تابع، مقدار false را برخواهد گرداند. برای پیاده‌سازی تابع is_safe_add می‌توانید از چند شرط ساده برای کنترل بازه‌های اعداد x و y استفاده کنید. به جای ایجاد تابعی به نام is_safe_add می‌توانید یک کلاس جدید برای کار با نوع short int ایجاد کنید:

ShortInt x = 32767;
x = x + 1; //throw an exception

در این نمونه مثال، «عملگر +» در کلاس ShortInt سربارگذاری و پیاده‌سازی شده است تا در صورتیکه integer overflow رخ می‌دهد، یک استثناء برای توقف اجرای کدها، صادر گردد.

مقادیری که در متغیر ذخیره می‌شوند را محدود کنید. ورودی‌های کاربر را محدود کنید، تا زمانیکه مقادیر بزرگی در متغیرها ذخیره نشوند، احتمال رخ‌دادن integer overflow ممکن نیست. این روش یک مزیت نسبت به روش قبل دارد و آن این است که قبل از هر محاسبه‌ای، نیاز به بررسی احتمال وقوع integer overflow نیست. بنابراین سرعت محاسبات در این روش بیشتر از روش قبل خواهد بود. با توجه به اینکه معمولاً کاربر با اعداد بزرگ سر و کار ندارد و عملاً این روش بر روی  نیازهای کاربر تأثیر منفی نخواهد گذاشت. به عنوان یک نکته، تشخیص احتمال integer overflow ساده و قابل جلوگیری خواهد بود اگر هوشیار باشید و این احتمال را نادیده نگیرید. پس اگر محدودیت‌ها بر روی کاربر اعمال‌شده و فقط اشتباه برنامه‌نویس ممکن است علت integer overflow باشد، برای کمک به برنامه‌نویس (خود شما) بررسی امکان وقوع integer overflow را در ماکروی assert قرار دهید، تا درصورتیکه برنامه‌نویس (خود شما) سهواً اشتباه کرد، در مرحله تست و اشکال‌زدایی به او کمک شود تا ایراد را راحت‌تر پیدا کند، برای انتشار نهایی برنامه با تعریف ماکروی NDEBUG همهٔ شرطهای درون ماکروی assert نادیده گرفته خواهند شد.

نادیده‌گیری احتمال وقوع integer overflow می‌تواند صدمات امنیتی جدی را بر روی برنامه‌های مهم و بحرانی وارد کند، همانطور که شما دسترسی به اعضای آرایه را کنترل می‌کنید تا از محدودهٔ آرایه‌ها فراتر نروید، محدوده انواع عددی را نیز نباید فراموش کنید تا از صحت درستی محاسبات انجام‌شده اطمینان حاصل کنید.

ویرایش: بهرحال قبل از هر چیز بهتر است ابتدا محاسبات را طوری تغییر دهید که integer overflow رخ ندهد، اگر ممکن نبود، آنوقت از روشهای قبل استفاده کنید. ویرایش: جناب آقای توکلی، مثال بسیار خوبی را برای این حالت در قسمت دیدگاهها نوشته‌اند.

NULL برای تعیین طول و تعداد

طول آرایه‌ها در زبان C یا ‪C++‬ ذخیره نمی‌شود، پس این وظیفهٔ برنامه‌نویس است که طول آن را در یک متغیر جداگانه ذخیره نماید. اما یک روش جالب! آخرین عضو آرایه را NULL (برابر صفر) قرار دهید، از این پس بسیار ساده می‌توانید طول آرایه را تشخیص داده و به تعداد اعضای آن، با آرایه کار کنید:

MyStruct a, b, c;
...
MyStruct* var_array[] = {&a, &b, &c, NULL};

for(int i = 0; var_array[i] != NULL; ++i)
{
    std::cout << var_array[i]->value << std::endl;
}

.
MyStruct یک نوع ساختاری سازگار با زبان C است و دارای متغیری به نام value است. به کمک حلقهٔ for تمام اعضای آرایهٔ var_array مورد پویش قرار می‌گیرد تا زمانیکه به عضو تهی یا NULL برسد.

همچنین تعداد آرگومان‌های توابع در زبان C یا ‪C++‬ می‌تواند نامشخص باشد که به آنها توابع variadic گفته می‌شود، چنین توابعی می‌توانند به تعداد دلخواه، پارامتر دریافت کنند. برای تشخیص تعداد پارامترهای ارسال‌شده می‌توان از یک پارامتر کمکی استفاده کرد یا به عنوان روشی دیگر، می‌توان آخرین پارامتر ارسال شده را NULL در نظر گرفت:

void my_function(MyStruct* x, ...);

تابع my_function یک تابع variadic است و تعداد پارامترهای متفاوتی را می‌توان به جای علامت سه‌نقطه به این تابع فرستاد. به فرض اگر همهٔ پارامترهایی که این تابع می‌گیرد از نوع MyStruct باشد، برای دریافت پارامترهای ارسال‌شده به این توابع می‌توان از فایل سرآیند stdarg.h (یا cstdarg) کمک گرفت یا بطور مستقیم با آدرس آنها کار کرد:

int main()
{
    MyStruct a, b, c;
    ...
    my_function(&a, &b, &c, NULL);
}

void my_function(MyStruct* x, ...)
{
    for(MyStruct** p = &x; *p != NULL; ++p)
    {
        std::cout << (*p)->value << std::endl;
    }
}

.
استفاده از NULL برای تعیین تعداد پارامترهای ارسال شده به توابع variadic یا برای تعیین تعداد اعضای یک آرایه، می‌تواند روش مناسبی بوده و باعث افزایش خوانایی کدها گردد.

ویرایش: به جای NULL برای مقادیر انواع عددی می‌توانید از منفی یک استفاده کنید، بطور کلی با توجه به نوع داده می‌توانید یک مقدار منحصربه‌فرد را به جای NULL مورد استفاده قرار دهید.

۱۱ باور غلط دربارهٔ زبان سی++

  1. ‪C++‬ کندتر از زبان C است.
    لیست قابلیت‌های زبان ‪C++‬ که در زبان C نیست را می‌توان در سه دسته قرار داد:

    1. قابلیت‌هائی که می‌توانند موجب افزایش کارآیی برنامه شوند.
      مثل template meta-programming و inline function (که در C99 بعداً به زبان C نیز اضافه شد)
    2. قابلیت‌هائی که تأثیری بر روی کاهش کارآیی برنامه ندارند.
      مثل non-virtual inheritance و encapsulation و function overloading و reference type
    3. قابلیت‌هائی که تأثیر ناچیزی بر روی کاهش کارآیی برنامه دارند.
      مثل virtual inheritance و virtual function و exception handling
      مادامیکه از این قابلیت‌ها استفاده نمی‌کنید، برنامهٔ شما کاهش کارآیی نخواهد داشت، حتی می‌توانید توسط کامپایلر، آنها را کاملاً غیر فعال کنید.
  2. ‪C++‬ نسخهٔ جدیدی از زبان C است.
    خیر، ‪C++‬ سازگار با استاندارد C89 طراحی شده است اما نسخهٔ جدیدی از زبان C نیست. کمیتهٔ استاندارد ‪C++‬ و کمیتهٔ استاندارد C همکاری بسیار مشترکی با یکدیگر دارند و در اغلب موارد به آنها کمیتهٔ استاندارد C/‪C++‬ گفته می‌شود. اگر ‪C++‬ نسخهٔ جدیدی از زبان C است، پس چرا استاندارد جدید زبان C یعنی C99 چندین سال پس از ابداع زبان ‪C++‬ معرفی شد؟ و چرا ‪C++‬ با استاندارد C99 در مواردی ناسازگار است؟ جواب: چون ‪C++‬ و C دو زبان جدا از هم هستند.
  3. تفاوت C و ‪C++‬ تنها در شیئ‌گرائی است.
    در ‪C++‬ همه چیز فقط در شیئ‌گرائی خلاصه نشده است، در واقع روش generic programming با کمک abstraction و encapsulation بیشتر از شیئ‌گرائی مورد توجه قرار می‌گیرد (با توجه به اینکه برای شیئ‌گرا بودن باید از polymorphism نیز استفاده کرد؛ یعنی باید تابع مخرب کلاس‌ها virtual باشد). روش template meta-programming هم چیزی است که (تنها) در ‪C++‬ معرفی شده است. به عبارت ساده‌تر، ‪C++‬ یک زبان کاملاً hybrid است، شما به هر روشی که بخواهید می‌توانید در آن برنامه بنویسید حتی به روش functional programming (بخصوص به کمک استاندارد جدید زبان ‪C++‬ موسوم به C++0x که قابلیت توابع بی‌نام نیز اضافه شده است).
  4. برای یادگیری ‪C++‬ ابتدا باید زبان X را آموخت.
    من یک کتاب آموزشی ‪C++‬ را ندیده‌ام که در ابتدای آن و در مقدمه گفته باشد که ابتدا باید زبان X را یاد گرفته باشید. بله، اگر از قبل با زبان X آشنا باشید، می‌تواند یادگیری ‪C++‬ را تسهیل کند اما نه تنها ‪C++‬ بلکه این مورد برای سایر زبان‌های برنامه‌نویسی هم صادق است.
  5. کدهای نوشته‌شده در ‪C++‬ ناخوانا است و امنیت کمی دارد.
    متاسفانه نمی‌دانم این بحث از کجا ناشی می‌شود، اما نوشتن برنامه‌هایی با امنیت کم، ارتباط خاصی با زبان برنامه‌نویسی ندارد، این برنامه‌نویس است که باید وظیفهٔ خود را در قبال تأمین امنیت برنامه انجام دهد. زبان برنامه‌نویسی نمی‌تواند امنیتی که بطور معمول همه در جستجوی آن هستند را به برنامه‌نویس هدیه کند. در مورد نوشتن کدهای ناخوانا، رک حرف می‌زنم، سورس کد برنامه‌های نوشته‌شده در ‪C++‬ ناخوانا می‌شود اگر شما کدهایتان را کثیف بنویسید!
  6. زبان ‪C++‬ منسوخ شده و برای رایانه‌های نسل قدیم است.
    زبان ‪C++‬ همواره با زمان پیشرفت خواهد کرد، استاندارد جدید این زبان موسوم به C++0x نیز به زودی فراگیر خواهد شد و قابلیت‌های بسیار بیشتری را به این زبان قدرتمند اضافه خواهد نمود. در ادامه برای جواب به این مورد، هیچ چیز بهتر از آمارهای سال جاری نیست: آمار شماره یک، آمار شماره دوم.
  7. برای هر سیستم‌عاملی باید کدهای متفاوتی در ‪C++‬ نوشت.
    اگر از کتابخانه‌های cross-platform یا از کتابخانهٔ استاندارد زبان استفاده شود (کتابخانه‌هایی مثل Standard Template Library و GTK و POSIX و Boost و Qt و SDL و OpenGL و …)، تنها با نوشتن یک سورس کد، برنامهٔ مورد نظر در هر سیستم‌عاملی کامپایل و اجرا خواهد شد. همچنین برای سادگی بیشتر می‌توانید از GNU Autotools بهره بگیرید.
  8. با زبان ‪C++‬ نمی‌توان هستهٔ سیستم‌عامل (kernel) نوشت.
    این تفکر نیز اشتباه است، همانطور که می‌دانید برای نوشتن هستهٔ سیستم‌عامل نباید به محیط نرم‌افزاری خاصی وابسته بود، پس چه در زبان C و چه در زبان ‪C++‬ نباید از کتابخانه‌ها (حتی کتابخانهٔ استاندارد) استفاده کرد، اما در ‪C++‬ بعضی قابلیت‌های ذاتی زبان مثل RTTI و exception handling و کلمات کلیدی مثل new و delete و dynamic_cast را یا باید غیر فعال کنید یا باید خود شما آنها را پیاده‌سازی کنید. زیرا این قابلیت‌ها به کد run-time احتیاج دارند (همچنان یک کتابخانه؛ در زبان ‪C++‬ کلمه‌کلیدی new برای اخذ حافظهٔ پویا استفاده می‌شود و در زبان C از تابع malloc برای انجام اینکار استفاده می‌شود، پس در ‪C++‬ همانند C نباید از این قابلیت استفاده کرد، یا آنکه خود شما باید آن را پیاده‌سازی کنید). در اینترنت جستجو کنید، مطمئن هستم kernelهائی را خواهید یافت که تنها توسط ‪C++‬ نوشته شده‌اند.
  9. زبان ‪C++‬ برای نوشتن برنامه‌های عادی، مناسب نیست.
    نوشتن یک برنامه عادی در سی++ با وجود ابزارهای فراوانی که دارد، سهل است. منظور از برنامهٔ عادی چیست؟ بهرحال اگر منظور از مناسب نبودن، همان سخت بودن برای یک برنامه‌نویس مبتدی است، درست است، در غیر اینصورت غلط است.
  10. در ‪C++‬ همواره باید نگران memory leak باشیم.
    خوشبختانه به کمک زبان ‪C++‬ می‌توان از الگوی طراحی RAII استفاده برد و در این مورد، حافظه بطور کامل و بطور خودکار مدیریت می‌گردد (همچون اشیاء تعریف شده در STL)، البته تا زمانیکه بطور مستقیم از new و delete استفاده نکنید.
  11. ‪C++‬ باید زبان X را جایگزین می‌کرد.
    ‪C++‬ لزوماً نباید هیچ زبان دیگری را جایگزین کند، ‪C++‬ راه‌حل‌های خاص خود را ارائه کرده است، در ابتدا هم ‪C++‬ ابداع نشده بود که زبان خاصی را جایگزین کند، بلکه ‪C++‬ ابداع شده بود که راه‌حل مناسبی را برای مسائل موجود ارائه کند. ‪C++‬ یک زبان همه‌منظوره است، برای هر مسئلهٔ خاصی یک یا چندین راه‌حل ارائه کرده است، لزوماً نباید به خاطر وجود چنین راه‌حل‌هایی، سایر زبان‌هایی را جایگزین کند که راه‌حل‌های مشابه (و گاهاً ساده‌تر یا بهتری) را برای آن مسئلهٔ خاص ارائه کرده‌اند.

سالاد try…catch با new و delete

روش اصولی و صحیح استفاده از try…catch برای اخذ حافظهٔ پویا از سیستم زمانیکه در ادامه ممکن است یک استثناء صادر شود و ما نمی‌خواهیم memory leak بر جای بماند:

MyType* my_var = 0;
try
{
    my_var = new MyType();
    //در ادامه ممکن است خطا رخ دهد...
}
catch(...)
{
    //متن مناسبی را به کاربر نمایش دهید...
}
delete my_var;

.
اما روش بهتری هم هست، از اشاره‌گرها بطور مستقیم استفاده نکنید، آنها را در پشت کلاس‌ها مدیریت کنید. بطور مثال می‌توانید از Smart Pointerها استفاده کنید:

try
{
    auto_ptr<MyType> my_var(new MyType());
    //در ادامه ممکن است خطا رخ دهد...
}
catch(...)
{
    //متن مناسبی را به کاربر نمایش دهید...
}

.
دلیل آن هم ساده است، عملیات new و delete به ترتیب توسط توابع سازنده و مخرب کلاس auto_ptr مدیریت می‌شود.

محاسبهٔ مدت زمان لازم برای اجرای کدها

در برنامه‌نویسی برای سنجش و آگاهی از میزان کارآیی (performance) قطعه‌ای از کدها، می‌توان مقدار حافظهٔ مصرف‌شده، یا میزان درگیری پردازشگر و یا مدت زمان سپری‌شده برای انجام آن عملیات را در زمان اجرا محاسبه کرد. الآن فقط می‌خواهم در رابطه با نحوهٔ محاسبهٔ مدت زمان سپری‌شده در زبان ‪C++‬ صحبت کنم. زیرا اغلب برنامه‌نویس‌های C و ‪C++‬ به نوشتن کدهای کارآ اهمیت می‌دهند. همچنین از این طریق می‌توانیم کلاس‌ها یا دستورات متفاوت را (از لحاظ کارآیی) با هم مقایسه کنیم.

در مواقعی لازم است که مدت زمان لازم برای اجرای قطعه‌ای از کدها را (بطور تقریبی) بدانیم. برای محاسبهٔ این مدت زمان؛ قبل از آن کدها باید زمان جاری سیستم را بگیریم و بعد از آن کدها باید دوباره زمان جاری سیستم را (که تغییر کرده است) بگیریم. اختلاف این دو زمان (اختلاف زمان قبل و بعد از اجرای آن کدها)، همان مدت زمانی است که برای اجرای آن کدها سپری‌شده است. برای گرفتن زمان جاری سیستم، در استاندارد ‪C++‬ (منتشر شده در سال 2003) تابعی نداریم که بر مبنای هزارم ثانیه کار کند. پس نتایج آنچنان دقیق نخواهد بود و احتیاج است تا از توابع اختصاصی هر سیستم‌عامل یا بعضی از کتابخانه‌های آماده استفاده کنیم.

در اینجا من از تابع gettimeofday استفاده کردم، این تابع در سیستم‌عامل گنو/لینوکس (دبیان) در فایل سرآیند sys/time.h قرار دارد و یک تابع استاندارد نیست (بلکه جزئی از کتابخانهٔ GNU C است)، برای استفادهٔ راحت پیشنهاد می‌کنم از این کلاس استفاده کنید:

#include <cstdlib>
#include <sys/time.h>

class Timer
{
    timeval timer [2];

    public:

    timeval start ()
    {
        gettimeofday (&this->timer [0], NULL);
        return this->timer [0];
    }

    timeval stop ()
    {
        gettimeofday (&this->timer [1], NULL);
        return this->timer [1];
    }

    int duration () const
    {
        int secs (this->timer [1].tv_sec - this->timer [0].tv_sec);
        int usecs (this->timer [1].tv_usec - this->timer [0].tv_usec);
        if (usecs < 0) { --secs; usecs += 1000000; }
        return static_cast <int> (secs * 1000 + usecs / 1000.0 + 0.5);
    }
};

.
نحوهٔ استفاده از این کلاس برای محاسبهٔ مدت زمان سپری‌شده، بسیار ساده است، بطور مثال:

#include <iostream>
#include <string>
#include <sstream>

int main ()
{
    Timer tm;
    std::ostringstream ooo;
    std::string str;

    tm.start ();
    for (int i = 0; i < 10000000; ++i)
    {
        ooo << "This is a string. ";
    }
    tm.stop ();
    std::cout << "std::ostingstream -> " << tm.duration () << std::endl;

    tm.start ();
    for (int i = 0; i < 10000000; ++i)
    {
        str += "This is a string. ";
    }
    tm.stop ();
    std::cout << "std::string -> " << tm.duration () << std::endl;
}

.
همانطور که ملاحظه می‌کنید، من در این مثال، از یک حلقه تکراری (۱۰۰۰۰۰۰۰ بار)، برای مقایسه دو عملگر >> و ‪+=‬ بترتیب بر روی اشیائی از نوع std::ostringstream و std::string استفاده کرده‌ام. قبل از شروع حلقهٔ تکرار توسط تابع start زمان جاری سیستم گرفته می‌شود و در انتها پس از پایان حلقهٔ تکرار توسط تابع stop زمان جاری جدید از سیستم اخذ می‌شود و توسط تابع duration مدت زمان سپری‌شده از زمان صدازدن تابع start تا زمان صدازدن تابع stop در خروجی نمایش داده می‌شود.

نکته: همانطور که مشاهده کردید، برای افزودن مقادیر به یک رشته، استفاده از std::string سریعتر از std::ostringstream است.

نکته: استفاده از std::ostringstream برای قالب‌بندی (formatting) رشته متن‌های طولانی،‌ روش خوبی است زیرا کاهش کارآیی آن در مقایسه با std::string محسوس نیست.

نکته: از کلاس Timer می‌توانید برای هر نوع مقایسه‌ای استفاده کنید و مدت زمان سپری‌شدهٔ ناشی از هر عملی را بدست آورید.

دنبال‌کردن

هر نوشتهٔ تازه‌ای را در نامه‌دان خود دریافت نمایید.