سلسلة ++c للمحترفين الدرس 28: النوع std::string: السلاسل النصية في Cpp


محمد الميداوي

النوع std::string والذي يدعى السلسلة النصية في العربية هو كائن يمثّل سلاسل من المحارف، ويوفر صنف string القياسي خيارًا بسيطًا وآمنًا ومتعدّد الاستخدامات لتخزين سلاسل المحارف ومعالجتها، وذلك موازنة بمصفوفات المحارف، و‎string‎ هو جزء من فضاء الاسم ‎std‎، وقد صار معياريًا في عام 1998.

تقطيع السلاسل النصية

يمكن تقطيع سلسلة نصيَّة إلى سلاسل نصيَّة أصغر تدعى الوحدات أو القطع (tokens)، تُسمّى هذه العمليَّة بالترميز المُقطَّع للسلاسل النصيَّة أو تقطيع السلاسل النصية (String Tokenization).

ولدينا عدّة طرق لتقطيع سلسلة نصّية، وسنُدرجها من الأقل إلى الأكثر تكلفة في وقت التشغيل:

  • الطريقة الأولى: ‎std::strtok‎ 

هي أقل طرق تقطيع السلاسل النصّية تكلفة، كما تسمح بتعديل الفاصل (delimiter) بين القطع (tokens)، غير أن هذه الطريقة تعتريها ثلاثة إشكاليات في C++‎:

  • لا يمكن استخدام ‎std::strtok‎ على عدّة سلاسل نصّية في نفس الوقت (باستثناء بعض التطبيقات implementations التي تمكّن من ذلك، مثل: ‎strtok_s‎)
  • لا يمكن استخدام ‎std::strtok‎ على عدّة خيوط أو عمليات (threads) في وقت واحد (لكن قد يتغيّر ذلك بحسب التطبيق كما هو الحال في Visual Studio)
  • استدعاء ‎std::strtok‎ يُعدّل السلسلة النصية التي يعمل عليها، لذلك لا يمكن استخدامه على السلاسل النصّية من النوع const strings أو ‎‎const char*‎ أو السلاسل النصية المجردة، ويجب نسخ السلاسل النصّية المُدخلة لتقطيع أيٍّ من هذه السلاسل باستخدام ‎std::strtok‎ أو للعمل على السلاسل النصّية التي ينبغي حفظ محتوياتها، ثم يمكن العمل على النسخة بعد ذلك .

ستكون تكلفة هذه الخيارات جزءًا من تكلفة حجز القطع، لكن إذا كنت تريد استخدام خوارزميات سريعة ولم تتمكن من تجاوز إشكاليات ‎std::strtok‎، فربما عليك النظر في خيار hand-spun solution.

// السلسلة النصية المراد تقطيعها
std::string str {
    "The quick brown fox"
};
// المتجه الذي سنخزن فيه القطع.
vector < std::string > tokens;
for (auto i = strtok( & str[0], " "); i != NULL; i = strtok(NULL, " "))
    tokens.push_back(i);

انظر هذا المثال الحي.

  • الطريقة الثانية: ‎std::istream_iterator‎

يستخدم std::istream_iterator‎ هنا عامل الاستخراج من المجرى بشكل تكراري، وإذا كانت السلسلة النصّية المعطاة مفصولة بمسافات فارغة فسنستطيع التوسعة على المعامِل ‎std::strtok‎ بالتخلص من صعوباته، مما يسمح بالترميز المقطَّع المُضمَّن (inline)، ومن ثم دعم إنشاء متجهات السلاسل النصّية الثابتة (‎const vector<string>‎) ودعم محرف مسافات فاصلة متعددة، انظر:

// السلسلة النصية المراد تقطيعها
const std::string str("The  quick \tbrown \nfox");
std::istringstream is(str);
// المتجه الذي سنخزن فيه القطع.
const std::vector<std::string> tokens =    std::vector<std::string>(
std::istream_iterator<std::string>(is),
std::istream_iterator<std::string>());

انظر هذا المثال الحي.

  • الطريقة الثالثة: std::regex_token_iterator‎

تَستخدم ‎std::regex_token_iterator‎ تعبيرًا نمطيًّا أو ‎std::regex‎ للقيام بعملية التقطيع بشكل تكراري، وهذا يسمح بتعريف الفاصل بشكل أكثر مرونة، مثل الفاصلات , والمسافات الفارغة، انظر:

الإصدار ≥ C++‎ 11

// السلسلة النصية المُراد تقطيعها
const std::string str {
    "The ,qu\\,ick ,\tbrown, fox"
};
const std::regex re {
    "\\s*((?:[^\\\\,]|\\\\.)*?)\\s*(?:,|$)"
};
// المتجه الذي سنخزن فيه القطع.
const std::vector < std::string > tokens {
    std::sregex_token_iterator(str.begin(), str.end(), re, 1),
        std::sregex_token_iterator()
};

انظر هذا المثال الحي، وهذا مثال آخر في درس التعابير النمطية للمزيد من التفاصيل.

التحويل إلى مؤشر ‎ (const)‎ char*‎

استخدم الدالة التابعة ‎c_str()‎ لتمكين مؤشّر ‎const char*‎ من الوصول إلى بيانات سلسلة نصّية، واعلم أنّ المؤشّر يبقى صالحًا ما دامت السلسلة النصّية ضمن النطاق (scope) ولم يمسّها تغيير، هذا يعني أنه لا يمكن استدعاء توابع غير ثابتة على الكائن.

الإصدار ≥ C++‎ 17

استخدم الدالة التابعة ‎data()‎ للحصول على مؤشّر قابل للتغيير ‎char*‎، والذي يمكن استخدامه لمعالجة السلسلة النصّية.

الإصدار ≥ C++‎ 11

ويمكن الحصول على مؤشّر ‎char*‎ قابل للتعديل عن طريق أخذ عنوان الحرف الأول: ‎&s[0‎]‎، سيُعيد ذلك في C++‎ 11 سلسلةً نصّية مُنسّقة جيدًا ومنتهية بالمحرف الفارغ (null-terminated). لاحظ أنّ ‎&s[0‎]‎ ستكون مُنسّقة حتى لو كانت s فارغة، بينما تكون ‎&s.front()‎ غير مُحدّدة إن كانت ‎s‎ فارغة.

الإصدار ≥ C++‎ 17 في المثال التالي، تشير كل من cstr و data إلى "This is a string.\0":

std::string str("This is a string.");
const char* cstr = str.c_str(); 
const char* data = str.data(); 
std::string str("This is a string.");

انسخ محتويات str لفك lifetime من كائن std::string:

std::unique_ptr<char []> cstr = std::make_unique<char[]>(str.size() + 1);
// حل بديل للسطر أعلاه، غير محصن من الاعتراضات.
// char* cstr_unsafe = new char[str.size() + 1];

std::copy(str.data(), str.data() + str.size(), cstr);
cstr[str.size()] = '\0';        //  ينبغي إضافة سلسلة نصّية منتهية بحرف فارغ

// delete[] cstr_unsafe;
std::cout << cstr.get();

استخدام الصنف std::string_view

الإصدار ≥ C++‎ 17

قدّمت C++‎ 17 الصنف ‎std::string_view‎، وهو ببساطة مدىً غير مالك (non-owning range) من المحارف الثابتة (‎const char‎)، والقابلة للتنفيذ إمّا على هيئة زوج من المؤشّرات، أو مؤشّر وطول (length)، وهو نوع معاملات أفضل للدوالّ التي تتطلب سلاسل نصّية غير قابلة للتعديل.

وكان قبل الإصدار C++‎ 17 ثلاثة خيارات لفعل هذا: الأول: وسيط واحد، قد يقوم بالتخصيص إن لم تكن بيانات المستدعي في سلسلة نصية مثل سلسلة نصية مجردة أو <vector<char:

void foo(std::string const& s);

الثاني: وسيطان، يجب أن يمررهما في كل مكان.

void foo(const char* s, size_t len);

والثالث: وسيط واحد، لكن يجب أن يستدعي ()strlen.

void foo(const char* s);

يستطيع المستدعي تمرير مزود بيانات محرفية لكن سيكون على ()foo أن يتواجد في ترويسة.

template < class StringT >
    void foo(StringT const& s);

يمكن استبدال كلّ ما في المثال السابق بالشيفرة التالية، ومزاياها أنها بوسيط واحد، مع ربط أقوى، وبدون نُسخ بغض النظر عن كيفية تخزين المستدعي للبيانات.

void foo(std::string_view s);

لاحظ أنّ std::string_view لا يمكنها تعديل البيانات الأساسية (underlying data) الخاصّة بها.

‎string_view‎ مفيدة في حال أردت تجنّب عمليات النسخ غير الضرورية، كما تقدم مجموعة من وظائف السلاسل النصّية، رغم أنّ سلوك بعض الدوالّ قد يختلف، انظر المثال التالي لسلسلة نصية طويلة بدون داعي:

std::string str = "lllloooonnnngggg sssstttrrriiinnnggg"; // سلسلة نصّية طويلة

استخدام string::subsr سيعيد سلسلة نصية جديدة، وهذا مكلف إن كانت السلسلة طويلة، انظر:

std::cout << str.substr(15, 10) << '\n';

وعليه فلا نحبذ هذا الأسلوب، ومن ناحية أخرى فلن تنشئ الطريقة الصحيحة هنا أي نسخ إضافية، انظر:

std::string_view view = str;

وستعيد string_view::substr سلسلة string_view جديدة:

std::cout << view.substr(15, 10) << '\n';

التحويل إلى std::wstring

تُمثَّل تسلسلات المحارف في C++‎ عبر تخصيص الصنف ‎std::basic_string‎ بنوع محرفي أصلي (native)، والمجموعتان الرئيسيتان المُعرّفتان في المكتبة القياسية هما ‎std::string‎ و ‎std::wstring‎:

  • ‎std::string‎ هي سلسلة مبنية بعناصر من نوع char
  • ‎std::wstring‎ مبنية بعناصر من نوع wchar_t

استخدم لأجل التحويل بين النوعين، ‎wstring_convert‎:

#include <string>
#include <codecvt>
#include <locale>

std::string input_str = "this is a -string-, which is a sequence based on the -char- type.";
std::wstring input_wstr = L"this is a -wide- string, which is based on the -wchar_t- type.";
// التحويل
std::wstring str_turned_to_wstr = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(input_str);
std::string wstr_turned_to_str = std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(input_wstr);

لتحسين قابلية الاستخدام وسهولة القراءة، عرِّف دوالًا لتنفيذ عملية التحويل:

#include <string>
#include <codecvt>
#include <locale>

using convert_t = std::codecvt_utf8 < wchar_t > ;
std::wstring_convert < convert_t, wchar_t > strconverter;

std::string to_string(std::wstring wstr) {
    return strconverter.to_bytes(wstr);
}

std::wstring to_wstring(std::string str) {
    return strconverter.from_bytes(str);
}

مثال تطبيقي:

std::wstring a_wide_string = to_wstring("Hello World!");

هذا أفضل وأوضح بكثير من السطر التالي:

td::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes("HelloWorld!")‎‎‎

لاحظ أنّ ‎char‎ و ‎wchar_t‎ لا تقتضيان الترميز (encoding) ولا تُعطيان أيّ إشارة عن الحجم بالبايتات، على سبيل المثال، تُستخدم ‎wchar_t‎ عادة كنوع بيانات ثنائي البايت، وعادة ما تحتوي على بيانات مُرمّزة بترميز UTF-16 في ويندوز (أو UCS-2 في الإصدارات السابقة لنظام ويندوز 2000) أو كنوع بيانات رباعي البايت مُرمّز باستخدام الترميز UTF-32 في لينكس.

هذا على النقيض من الأنواع الحديثة ‎char16_t‎ و ‎char32_t‎، والتي جاءت في الإصدار C++‎ 11، إذ هي كبيرة بما يكفي لاحتواء أيّ محرف من UTF16 أو UTF32 على الترتيب.

الموازنة المعجمية

يمكن موازنة سلسلتين نصّيتين معجميًا باستخدام المعاملات ‎==‎ و ‎!=‎ و ‎<‎ و ‎<=‎ و ‎>‎ و ‎>=‎:

std::string str1 = "Foo";
std::string str2 = "Bar";

assert(!(str1 < str2));
assert(str > str2);
assert(!(str1 <= str2));
assert(str1 >= str2);
assert(!(str1 == str2));
assert(str1 != str2);

تستخدِم كل هذه الدوالّ التابع ‎std::string::compare()‎ لإجراء المقارنات وإعادة القيمة البوليانية المناسبة.

وفيما يلي شرح عامّ لمعَاملات الموازنة:

  • المُعامل ‎==‎

إذا كانت ‎str1.length() == str2.length()‎، وتطابقت أزواج المحارف (character pairs)، فسيعيد المُعاملُ القيمةَ ‎true‎، وإلا فسَيعيد ‎false‎.

  • المُعامل ‎!=‎

إذا كانت ‎str1.length() != str2.length()‎ أو لم تتطابق أزواج المحارف، فسَيعيد المُعامل القيمة ‎true‎، وإلا فسيعيد ‎false‎.

  • المُعامل ‎<‎ أو المُعامل ‎>‎

 يبحثان عن أول زوج غير متطابق من المحارف، ويقارن بينهما ثم يعيد النتيجة البوليانية تبعًا لنتيجَة الموازنة.

  • المُعامل ‎<=‎ أو المُعامل ‎>=‎

 يبحث عن أول زوج غير متطابق من المحارف، ويقارن بينهما ثم يعيد النتيجة البوليانية.

ملاحظة: يشير مصطلح زوج المحارف (character pair) إلى المحارف المتقابلة في كلا السلسلتين، أي الحرفين الذين يوجَدين في نفس الموضع من السلسلتين النّصّيتين. مثلًا، لنفترض أنّ لدينا سلسلتين نصّيتين ‎str1‎ و ‎str2‎، وطولاهما ‎n‎ و ‎m‎ على التوالي، في هذه الحالة ستكون أزواج المحارف في كلتَي السلسلتين النصّيتين هي الأزواج ‎str1‎ و ‎str2‎ حيث i = 0, 1, 2, …, max(n,m)‎. في حال عدم وجود حرف يقابل الفهرس i، أي عندما يكون i أكبر من أو يساوي ‎n‎ أو ‎m‎، فسيتم اعتبارُه القيمةَ الأصغر.

فيما يلي مثال على استخدام ‎<‎:

std::string str1 = "Barr";
std::string str2 = "Bar";

assert(str2 < str1);

خطوات الموازنة هي كالتالي:

  1. موازنة الحرفين الأوّلين، ‎'B' == 'B'‎ - ثم متابعة.
  2. موازنة بين الحرفين الثانيين، ‎'a' == 'a'‎ - ثم متابعة.
  3. موازنة بين الحرفين الثّالثين، ‎'r' == 'r'‎ - ثم متابعة.
  4. استُنفِذ نطاق ‎str2‎ الآن، في حين أنّ نطاق ‎str1‎ ما يزال فيه محارف زائدة. وبالتالي يكون لدينا: ‎str2 < str1‎.

تقليم المحارف في بداية أو نهاية السلسلة النصية

يتطلب هذا المثال الترويسات و و .

الإصدار ≥ C++‎ 11

تقليم (trim) تسلسل أو سلسلة نصّية يعني إزالة جميع العناصر الأولى أو الأخيرة التي تحقق شرطًا معينًا، ونقلّم أولًا العناصر الأخيرة لأنها لا تتطلب تحريك أيّ عنصر، ثم نقلّم العناصر الأولى.

لاحظ أنّ التعميمات أدناه تعمل مع جميع أنواع السلاسل النصّية الأساسية ‎std::basic_string‎، مثل ‎std::string‎ و ‎std::wstring‎، وأيضًا على الحاويات مثل ‎std::vector‎ و ‎std::list‎.

template < typename Sequence, // list أو  vector أي سلسلة نصّية أساسية، مثل     
    typename Pred > // شرط
    Sequence& trim(Sequence& seq, Pred pred) {
        return trim_start(trim_end(seq, pred), pred);
    }

يعتمد تقليم العناصر الأخيرة على إيجاد العنصر الأخير غير المطابق للشّرط، وبدء عملية المحو من هناك:

template < typename Sequence, typename Pred >
    Sequence & trim_end(Sequence & seq, Pred pred) {
        auto last = std::find_if_not(seq.rbegin(),
            seq.rend(),
            pred);
        seq.erase(last.base(), seq.end());
        return seq;
    }

أمّا تقليم العناصر الأولية، فيعتمد على تحديد العنصر الأوّل الذي لا يحقّق الشرط وبدء عملية المحو من بداية السلسلة وحتى هناك:

template < typename Sequence, typename Pred >
    Sequence & trim_start(Sequence & seq, Pred pred) {
        auto first = std::find_if_not(seq.begin(),
            seq.end(),
            pred);
        seq.erase(seq.begin(), first);
        return seq;
    }

استخدم الدالّة ‎std::isspace()‎ كشرط إذا أردت تقليم المسافات الفارغة في سلسلة نصّية :

std::string& trim(std::string& str, const std::locale& loc = std::locale()) {
return trim(str, [&loc](const char c){ return std::isspace(c, loc); });
}

std::string& trim_start(std::string& str, const std::locale& loc = std::locale()) {
return trim_start(str, [&loc](const char c){ return std::isspace(c, loc); });
}

std::string& trim_end(std::string& str, const std::locale& loc = std::locale()) {
return trim_end(str, [&loc](const char c){ return std::isspace(c, loc); });
}

وبالمثل، يمكننا استخدام الدالّة ‎std::iswspace()‎ مع السلاسل النصّية ‎std::wstring‎.

إذا كنت ترغب في إنشاء تسلسل جديد عبر إنشاء نسخة مُقلَّمة، فيمكنك استخدام دالّة منفصلة على النحو التالي:

template < typename Sequence, typename Pred >
    Sequence trim_copy(Sequence seq, Pred pred) { // بالقيمة seq  تمرير
        trim(seq, pred);
        return seq;
    }

استبدال السلاسل الصّية

الاستبدال بالموضع

استخدم التابع ‎replace‎ الخاص بالصنف ‎std::string‎ لاستبدال جزء من سلسلة نصّية. أيضًا، التابع ‎replace‎ له الكثير من التحميلات الزائدة (overloads) المفيدة:

// عرِّف سلسلة نصّية
std::string str = "Hello foo, bar and world!";
std::string alternate = "Hello foobar";

//1)
str.replace(6, 3, "bar"); //"Hello bar, bar and world!"

//2)
str.replace(str.begin() + 6, str.end(), "nobody!"); //"Hello nobody!"

//3)
str.replace(19, 5, alternate, 6, 6); //"Hello foo, bar and foobar!"

الإصدار ≥ C++‎ 14

//4)
str.replace(19, 5, alternate, 6); //"Hello foo, bar and foobar!"

//5)
str.replace(str.begin(), str.begin() + 5, str.begin() + 6, str.begin() + 9);
//"foo foo, bar and world!"

//6)
str.replace(0, 5, 3, 'z'); //"zzz foo, bar and world!"

//7)
str.replace(str.begin() + 6, str.begin() + 9, 3, 'x'); //"Hello xxx, bar and world!"

الإصدار ≥ C++‎ 11

//8)
str.replace(str.begin(), str.begin() + 5, { 'x', 'y', 'z' }); //"xyz foo, bar and world!"

استبدال سلسلة نصّية بأخرى

استبدل بالظهور الأول فقط للسلسلة النصّية ‎with‎ السلسلة ‎replace‎ في ‎str‎:

std::string replaceString(std::string str,
    const std::string& replace,
        const std::string& with) {
    std::size_t pos = str.find(replace);
    if (pos != std::string::npos)
        str.replace(pos, replace.length(), with);
    return str;
}

استبدال السّلسلة النصّية ‎with‎ بالسلسلة ‎replace‎ أينما ظهرت في ‎str‎:

std::string replaceStringAll(std::string str,
    const std::string& replace,
        const std::string& with) {
    if (!replace.empty()) {
        std::size_t pos = 0;
        while ((pos = str.find(replace, pos)) != std::string::npos) {
            str.replace(pos, replace.length(), with);
            pos += with.length();
        }
    }
    return str;
}

التحويل إلى سلسلة نصية

استخدم std::ostringstream لتحويل أيّ نوع قابل للإجراء (streamable type) إلى تمثيله النصي -أي إلى سلسلة نصّية-، عن طريق إدراج الكائن المُراد تحويله في كائن ‎std::ostringstream‎ (عبر معامل إدراج المجرى ‎<<‎)، ثم تحويل ‎std::ostringstream‎ بأكملها إلى سلسلة نصّية.

مثلًا، بالنسبة للأعداد الصحيحة ‎int‎:

#include <sstream>

int main() {
    int val = 4;
    std::ostringstream str;
    str << val;
    std::string converted = str.str();
    return 0;
}

اكتب دالّة التحويل الخاصة بك:

template < class T >
    std::string toString(const T & x) {
        std::ostringstream ss;
        ss << x;
        return ss.str();
    }

هذه الدالّة ستعمل بنجاح، لكنّ أداءها أقلّ كفاءة. يمكن أن تنفذ الأصناف المُعرَّفة من قبل المستخدم عاملَ إدراج المجرى، انظر المثال التالي حيث نكتب تمثيلًا نصيًا ل a في out:

std::ostream operator << (std::ostream & out,
    const A & a) {
    return out;
}

الإصدار ≥ C++‎ 11

بصرف النظر عن مجاري التدفق، أصبح ممكنًا منذ الإصدار C++‎ 11 أن نستخدم الدالّة ‎std::to_string‎‎std::to_wstring‎) والتي حُمِّلت تحميلًا زائدًا في جميع الأنواع الأساسية، وصارت تعيد تمثيلًا نصيًّا للمعامل المُمرّر إليها.

std::string s = to_string(0x12f3); // "4851" القيمة s بعد هذا ستحتوي

التقسيم (Splitting)

استخدم ‎std::string::substr‎ لتقسِيم السلاسل النصّية. هناك نوعان من هذا التابع، يأخذ الأول موضع البداية، والذي ستبدأ منه السلسلة النصّية الفرعية (substring) المُعادة. يجب أن يكون موضع البداية جزءًا من النطاق (0, str.length()]‎:

std::string str = "Hello foo, bar and world!";
std::string newstr = str.substr(11); // "bar and world!"

فيما يأخذ التابع الثاني موضع البداية والطول الإجمالي للسّلسلة النصّية الفرعية الجديدة. ولن تتجاوز السلسلة النصّية الفرعية نهاية السلسلة النصّية المصدرية مهما كان الطول المُمرَّر:

std::string str = "Hello foo, bar and world!";
std::string newstr = str.substr(15, 3); // "and"

لاحظ أنّك تستطيع استدعاء ‎substr‎ بدون أي وسائط، وفي هذه الحالة ستُعاد نسخة مضبوطة من السلسلة النصية:

std::string str = "Hello foo, bar and world!";
std::string newstr = str.substr(); // "Hello foo, bar and world!"

الوصول إلى محرف من السلسلة النصية

هناك عدة طرق لاستِخراج الحروف من السلاسل النصّية، ولكلّ منها خصوصياتها.

std::string str("Hello world!");

operator‎

يعيد مرجعًا إلى الحرف الموجود عند الفهرس n. لا يتحقّق المُعامل td::string::operator[]‎ من حدود السلسلة النصّية، ولا يرفع اعتراضًا (exception) في حال تجاوزها، لذلك فالمُستدعي هو المسؤول عن التحقق من أنّ الفهرس يقع في حدود السلسلة النصّية:

char c = str[6]; // 'w'

at(n)‎

يعيد مرجعًا إلى الحرف الموجود عند الفهرس n. يتحقق المُعامل ‎std::string::at‎ من الحدود، ويرفع اعتراض ‎std::out_of_range‎ إذا لم يكن الفهرس ضمن حدود السلسلة النصّية:

char c = str.at(7); // 'o'

الإصدار ≥ C++‎ 11

ملاحظة: في كلا المثالين أعلاه، سيكون السّلوك غير محدد في حال كانت السلسلة النصية فارغة.

front()‎

يعيد مرجعًا إلى الحرف الأول:

char c = str.front(); // 'H'

back()‎

يعيد مرجعًا إلى الحرف الأخير:

char c = str.back(); // '!'

التحقق من كون سلسلة نصية سابقة (prefix) لسلسلة أخرى

الإصدار ≥ C++‎ 14

في الإصدار C++‎ 14، يمكن إنجاز ذلك بسهولة عبر ‎std::mismatch‎، والذي يُعيد الزوج الأوّل غير المتطابق من النطاقين:

std::string prefix = "foo";
std::string string = "foobar";
bool isPrefix = std::mismatch(prefix.begin(), prefix.end(),
string.begin(), string.end()).first == prefix.end();

قبل الإصدار C++‎ 14، كان هناك ما يُسمّى إصدار النطاق ونصف (range-and-a-half version) من التابع ‎mismatch()‎، ولكنه لم يكن آمنًا في حال كانت السلسلة النصّية الثانية أقصر من الأولى.

الإصدار < C++‎ 14

 

++‎>

لا زال بإمكاننا استخدام إصدار النطاق ونصف من التابع ‎std::mismatch()‎، لكن سيكون عليك التحقّق أولًا من أنّ السلسلة النصّية الأولى أقصر من الثانية أو تعادلها:

bool isPrefix = prefix.size() <= string.size() &&
std::mismatch(prefix.begin(), prefix.end(),
string.begin(), string.end()).first == prefix.end();

الإصدار ≥ C++‎ 17

يمكننا كتابة الموازنة التي نريدها مباشرة مع ‎std::string_view‎، دون الحاجة إلى القلق بشأن الحِمل الزائد في الذاكرة (allocation overhead) أو إنشاء النُّسخ:

bool isPrefix(std::string_view prefix, std::string_view full)
{
return prefix == full.substr(0, prefix.size());
}

التكرار على المحارف

الإصدار ≥ C++‎ 17

تدعم السلاسل النصّية المُكرّرات، وعليه تستطيع استخدام حلقة نطاقية (ranged based loop) للتكرار على المحارف:

std::string str = "Hello World!";
for (auto c: str)
    std::cout << c;

يمكنك أيضًا استخدام حلقة ‎for‎ "تقليدية" للتكرار على المحارف:

std::string str = "Hello World!";
for (std::size_t i = 0; i < str.length(); ++i)
    std::cout << str[i];

التحويل إلى الأعداد الصحيحة أو العَشرية

يمكن تحويل سلسلة نصّية تحتوي على عدد إلى نوع عددي صحيح أو عشري باستخدام دوالّ التحويل.

ملاحظة: تتوقف جميع هذه الدوالّ عن تحليل السلسلة النصّية المُدخلة بمجرد أن تصادف محرفًا غير رقمي. مثلاً، ستُحوّل السلسلة النصّية ‎"123abc"‎ إلى 123.

تحوّل مجموعة الدوالّ ‎std::ato*‎ السلاسل النصّية من نمط لغة C (مصفوفات المحارف) إلى أنواع عددية صحيحة أو عشرية:

std::string ten = "10";
double num1 = std::atof(ten.c_str());
int num2 = std::atoi(ten.c_str());
long num3 = std::atol(ten.c_str());

الإصدار ≥ C++‎ 11

long long num4 = std::atoll(ten.c_str());

إلا أنّه يفضَّل تجنّب استخدام هذه الدوالّ لأنها تُعيد ‎0‎ في حال فشلت في تحليل السلسلة النصّية. وهذا سيء لأنّ القيمة ‎0‎ قد تُفسَّر على أنّها نتيجة عددية صالحة. مثلًا، إن كانت السلسلة النصية المُدخلة هي "0"، فسيكون من المستحيل تحديد ما إذا كان التحويل قد فشل أم لا بغضّ النظر عن النتيجة.

وتحوّل الدوالّ الحديثة ‎std::sto*‎ السلاسل النصّية إلى أعداد صحيحة أو عشرية، وتطلق استثناءً في حال فشلت في تحليل السلسلة النصية. استخدم هذه الدّوال كلما أمكن:

الإصدار ≥ C++‎ 11

std::string ten = "10";
int num1 = std::stoi(ten);
long num2 = std::stol(ten);
long long num3 = std::stoll(ten);

float num4 = std::stof(ten);
double num5 = std::stod(ten);
long double num6 = std::stold(ten);

كذلك، فإنّ هذه الدوالّ يمكنها أن تتعامل أيضًا مع السلاسل النصّية الثُمانية (octal) والست عشرية (hex)، وذلك على عكس دوالّ ‎std::ato*‎. والمعامل الثاني هو مؤشّر يشير إلى أوّل حرف غير مُحوَّل في السلسلة النصية المُدخَلَة، أمّا المعامل الثالث فيمثّل الأساس الرقمي (base) الذي يجب استخدامه. يُستخدَم الحرف ‎0‎ للرصد التلقائي للأعداد الثُمانية (تبدأ بـ ‎0‎) والست عشرية (تبدأ بـ ‎0x‎ أو ‎0X‎)، أما القيم الأخرى فتمثّل الأساس الذي يجب استخدامه.

std::string ten = "10";
std::string ten_octal = "12";
std::string ten_hex = "0xA";

int num1 = std::stoi(ten, 0, 2); // 2
int num2 = std::stoi(ten_octal, 0, 8); // 10
long num3 = std::stol(ten_hex, 0, 16); // 10
long num4 = std::stol(ten_hex); // 0
long num5 = std::stol(ten_hex, 0, 0); // 0x تعيد 10 لأنها رصدت

ضم السلاسل النصّية

يمكنك ضمّ (concatenate) السلاسل النصّية باستخدام المُعاملين المُحمَّلين تحميلًا زائدًا ‎+‎ و ‎+=‎.

استخدام المُعامل ‎+‎:

std::string hello = "Hello";
std::string world = "world";
std::string helloworld = hello + world; // "Helloworld"

استخدام المُعامل ‎+=‎:

std::string hello = "Hello";
std::string world = "world";
hello += world; // "Helloworld"

يمكنك أيضًا ضمّ السلاسل النصّية من نمط لغة C، بما في ذلك السلاسل النصّية المجردة:

std::string hello = "Hello";
std::string world = "world";
const char *comma = ", ";
std::string newhelloworld = hello + comma + world + "!"; // "Hello, world!"

يمكنك أيضًا استخدام ‎push_back()‎ لإضافة حرف واحد إلى السلسلة النصية:

std::string s = "a, b, ";
s.push_back('c'); // "a, b, c"

لدينا يوجد أيضًا تابع ‎append()‎، والذي يشبه إلى حد كبير ‎+=‎:

std::string app = "test and ";
app.append("test"); // "test and test"

التحويل بين ترميزات المحارف

التحويل بين الترميزات (encodings) أمر سهل في C++‎ 11، ويمكن لمعظم المصرّفات إنجازه بطريقة مستقلّة عن المنصة (cross-platform) عبر الترويستين و .

#include <iostream>
#include <codecvt>
#include <locale>
#include <string>

using namespace std;
int main() {
    // wstring  و utf8 يحول بين سلاسل
    wstring_convert < codecvt_utf8_utf16 < wchar_t >> wchar_to_utf8;
    // utf16 وسلاسل utf8 يحول بين سلاسل
    wstring_convert < codecvt_utf8_utf16 < char16_t > , char16_t > utf16_to_utf8;

    wstring wstr = L"foobar";
    string utf8str = wchar_to_utf8.to_bytes(wstr);
    wstring wstr2 = wchar_to_utf8.from_bytes(utf8str);

    wcout << wstr << endl;
    cout << utf8str << endl;
    wcout << wstr2 << endl;

    u16string u16str = u"foobar";
    string utf8str2 = utf16_to_utf8.to_bytes(u16str);
    u16string u16str2 = utf16_to_utf8.from_bytes(utf8str2);

    return 0;
}

لاحظ أنّ Visual Studio 2015 يدعم مثل هذه التحويلات، ولكن بسبب خلل موجود في مكتبتها، فينبغي استخدام قالب مختلف لـ ‎wstring_convert‎ عند التعامل مع الترميز ‎char16_t‎:

using utf16_char = unsigned short;
wstring_convert<codecvt_utf8_utf16<utf16_char>, utf16_char> conv_utf8_utf16;
void strings::utf16_to_utf8(const std::u16string& utf16, std::string& utf8)
{
std::basic_string<utf16_char> tmp;
tmp.resize(utf16.length());
std::copy(utf16.begin(), utf16.end(), tmp.begin());
utf8 = conv_utf8_utf16.to_bytes(tmp);
}

البحث عن محرف أو أكثر في السلسلة النصية

استخدام التابع ‎std::string::find‎لإيجاد حرف أو سلسلة نصّية أخرى، والذي يعيد موضع الحرف الأوّل من التطابق الأوّل. أما إن لم يُعثر على أيّ تطابق، فستُعاد القيمة ‎std::string::npos‎

std::string str = "Curiosity killed the cat";
auto it = str.find("cat");
if (it != std::string::npos)
std::cout << "Found at position: " << it << '\n';
else
std::cout << "Not found!\n";

ستكون النتيجة 21.

تُوسَّع فرص البحث بالدوال التالية:

find_first_of     // إيجاد أول ظهور للمحارف
find_first_not_of // إيجاد أول غياب للمحارف
find_last_of      // إيجارد آخر ظهور للمحارف
find_last_not_of  // إيجاد آخر غياب للمحارف

تتيح لك لك هذه الدوالّ أن تبحث عن المحارف من نهاية السلسلة النصّية، وكذلك البحث عن الحالات السلبية (أي المحارف غير الموجودة في السلسلة). انظر المثال التالي:

std::string str = "dog dog cat cat";
std::cout << "Found at position: " << str.find_last_of("gzx") << '\n';

ستكون النتيجة 6.

ملاحظة: لا تبحث الدوالّ المذكورة أعلاه عن السلاسل النصّية الفرعية (substrings)، بل عن المحارف الموجودة في سلسلة البحث، ففي المثال أعلاه عُثِر على الظهور الأخير لـ ‎'g'‎ في الموضع ‎6‎ بينما لم يُعثر على المحارف الأخرى.

هذا الدرس جزء من سلسلة مقالات عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 47: std::string من كتاب C++ Notes for Professionals

 





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن