יום שבת

Intro to STL, Containers, Vector

ה-STL היא ספריה של C++  המכילה classes מאד שימושיים. היא מחולקת ל-4 חלקים עיקריים:
1. Containers
2. Iterators
3. Algorithms
4. functors

אני מתכנן לכתוב מספר פוסטים בנושאים הנ"ל.
בפוסט הזה נתחיל  עם הסבר כללי על Containers, ובהמשך נתייחס ונתין דוגמא ל-vector שהוא אחד ה-classes השימושיים מסוג container.
בכל אופן, מומלץ להכיר את נושא ה-templates - הנה קישור לפוסט שדן ב-templates.




אז מי הם ה-containers?  זהו class שמאחסן קבוצה של אובייקטים - בדומה למערך. ה-containers בנויים כ-templates, כך ש-container יכול להכיל כל סוג של data type.
ה-containers מחולקים לשתי קבוצות משנה: sequence containers  ו-associative containers.
הסבר על שני סוגי ה-containers:
sequence containers - כאן האובייקטים שב-container מאופיינים ע"י מיקומם ברשימה.
דוגמאות: vector. הוקטור מסודר בדיוק כמו מערך: רשימה של איברים, לכל אחד מספר סידורי, שמבטא את מיקומו בקבוצה.
דוגמא נוספת: list. גם כאן מדובר בקבוצה מסודרת. המיקום של כל איבר נקבע בזמן הכנסתו. הסדר יכול כמובן להשתנות ע"י פעולות שיעשו לאחר הכנסת האלמנט. למשל: הכנסה של אלמנטים נוספים, מיון, היפוך סדר וכו.
associative containers - כאן סדר האובייקטים ברשימה נקבע ע"י פעולת מיון הקשורה לתוכן של האובייקטים. לכל אובייקט כאן  שני ערכים המאפיינים אותו: value type - שהוא התוכן של האובייקט. key_type - זהו המפתח על פיו ממוינת הרשימה. ישנם containers בהם ה-key type וה-value_type הם אותה ישות.


הנה רשימה של ה-containers הסטנדרטים:

Sequence Containers:
vector
list
dequeu

:Associative Containers
map
multimap
set 
multiset
bitset

בנוסף יש containers נוספים שאינם שייכים לרשימה הסטנדרטית:
hash_map
hash_multimap
hash_set
hash_multiset


ה-containers הם כאמור classes והם כוללים מתודות המשמשות לפעולתם, למשל להכנסת אלמנטים ל-container, למחיקה, לסידור מחדש וכו,. חלק גדול מהמתודות נתמכות ע"י רב ה-containers, ויש כמובן מתדודות יחודיות לcontainers מסוימים או לקבוצה של containers. בהמשך נצלול לתוך חלק מה-containers ונבין את פעולתם. במידה שלא נכסה כאן את כולם, יהיה די קל להכנס אליהם אחרי הכרות עם ה-containers שמציג כאן.

אז ניגש כאן להכרות עם אחד ה-containers הנפוצים - vector.
כולנו מכירים את המערך (array) בשפת c.
אפשר להגדיר מערך בצורה סטטית, כמו בדוגמא הבאה:
int a[10];
החיסרון הבולט: גודל המערך נקבע בזמן הקומפילציה. הוא כמובן לא ניתן להגדלה באופן דינמי.
אפשר גם להקצות את המערך באופן דינאמי. הנה דוגמא:
int array_size = 100;
int *a = new int[array_size];
כאן יש לדאוג להחזרת הזכרון בסיום (delete), ועדיין לא ניתן להגדיל\להקטין את גודל הזיכרון המוקצה.

נראה בהמשך שה-vector container מאפשר הקצאה דינאמית, והגדלת\הקטנת הקצאת הזיכרון אפשרית בכל שלב. אלו רק חלק מהיכולות של vector.

כמו כל ה-container, ה-vector הוא template class. ההגדרה שלו נראית כך:


template <class T, class Allocator = allocator<T>>
class vector{

..
..
..
}


ה-template של vector כולל שני ארגומנטים:
T: זהו ה-type של האלמנטים של ה-vector.
Allocator: ה-type של האובייקטים שמוגדרים עבור ה-storage allocation. זהו פרמטר אופציונלי, וברירת המחדל היא T.


נתחיל עם רשימת המתודות של vector.  לאחר הרשימה נציג דוגמאות לשימוש במתודות, ונפרט את  הפעולה של כל אחת מהן.

רשימת המתודות של vector לפי סדר אלפאבתי:



assign();
at();
back();
begin();
capacity();
clear();
data();
empty();
end();
erase();
front();
insert();
max_size();
operator =();
operator []();
pop_back();
push_back();
rbegin();
rend();
reserve();
resize();
size();
swap();




והאופרטורים:

 ==
!=
 <
>
 <=
 >=



לפני שנציג דוגמא שמפעילה את המתודות הנ"ל, נציג אובייקט נוסף- iterator.
ה-iterator הוא התחליף לשימוש ב-pointer ומשמש לצורך גישה לאלמנטים של ה-containers ושל ה-vector בפרט.
(בסוף הדוגמא כמה מילים על סיבוכיות.)

הנה דוגמא ל-iterator:

  1. vector<int> vec;
  2. vector<int>::iterator iter;
  3. iter = vec.begin();
  4. iter++;
  5. *iter = 5;

הסבר קטע הקוד הנ"ל:
שורה 1: יצירת וקטור מסוג int.
שורה 2: יצירת iterator מסוג int.
שורה 3: המתודה begin מחזירה iterator מצביע על תחילת הוקטור.
שורה 4: הגדלת ה-iterator. כעת יצביע על האלמנט השני בוקטור.
שורה 5: כתיבת הערך 5 לאלמנט השני בוקטור.

כעת, עם מושג כלשהו על iterators, נראה דוגמא המשתמשת במתודות של vector.

לפנינו תוכנית שמשתמשת בכל המתודות של vector. התוכנית אורגנה כך שסדר ההופעה של המתודות  הוא  לפי הסדר האלפביתי (פרט ל begin ו-end שהסתננו קודם, כי התוכנית דרשה זאת...). 
ליד כל מתודה בהופעתה הראשונה מצורף הסבר על פעולתה- ראה רקע צהוב. מעבר לכך, ה-comments בתוכנית מספקים הסבר מפורט. כדי להמנע מסרבול לא הוספתי פרוט בעברית.
אפשר כמובן להעתיק את התוכנית ולהריץ אותה (כרגיל).




  1. /*
  2.  * vector.cpp
  3.  *
  4.  *  Created on: Mar 19, 2012
  5.  *      Author: ronen
  6.  */



  7. #include <vector>        // Must include the vector header file
  8. #include <iostream>
  9. using namespace std;


  10. int main(){
  11. vector<int> vec; // here vec is created with size 0


  12. /* assign elements to the vector with size and value*/
  13. vec.assign(20,7); // here vec size is set to 20, objects value are all 7

  14. vector<int>::iterator iter1; // create an iterator
  15. vector<int>::iterator iter2; // create an iterator
  16. vector<int> new_vec; // create another int vector

  17. /** begin()
  18.   *  Returns a read/write iterator that points to the first
  19.   *  element in the %vector.  Iteration is done in ordinary
  20.   *  element order.
  21.   */
  22. iter1 = vec.begin() + 4; // iter1 points to the 5th element of vec.
  23. /** end()
  24.   *  Returns a read/write iterator that points one past the last
  25.   *  element in the %vector.  Iteration is done in ordinary
  26.   *  element order.
  27.   */
  28. iter2 = vec.end(); // iter2 points to the place after the last element of vec
  29. /* assign - iterator first ,iterator last */
  30. new_vec.assign(iter1, iter2);
  31. /* assign iterators anonymously: */
  32. new_vec.assign(vec.begin(), vec.end());//  now new_vec[] will have 20 elements, contents copied from vec

  33. /**at(n)
  34.   *  @brief  Provides access to the data contained in the vector.
  35.   *  @param n -The index of the element for which data should be
  36.   *  accessed.
  37.   *  @return  Read/write reference to data.
  38.   *  @throw  std::out_of_range  If @a n is an invalid index.
  39.   *
  40.   *  This function provides for safer data access.  The parameter
  41.   *  is first checked that it is in the range of the vector.  The
  42.   *  function throws out_of_range if the check fails.
  43.   */
  44. int a = vec.at(6); // a=vec[6] which is now 7.
  45. cout << " check result of vec.at. vec.at(6)= " << vec.at(6) << " vec[6] = " << vec[6]<<endl;

  46. /** back()
  47.    *  Returns a read/write reference to the data at the last
  48.    *  element of the %vector.
  49.    */
  50. a = vec.back(); // a will get value of last element of vec which is vec[19] equals 7.

  51.  /** capacity()
  52.   *  Returns the total number of elements that the %vector can
  53.   *  hold before needing to allocate more memory.
  54.   */
  55. a = vec.capacity(); // here the capacity is according to the size set by assign. it equals vec.size(). This may change if we use vec.reserve.
  56.  /** clear()
  57.   *  Erases all the elements.  Note that this function only erases the
  58.   *  elements, and that if the elements themselves are pointers, the
  59.   *  pointed-to memory is not touched in any way.  Managing the pointer is
  60.   *  the user's responsibility.
  61.   */
  62. vec.clear();

  63. /** empty()
  64.   *  Returns true if the %vector is empty.  (Thus begin() would
  65.   *  equal end().)
  66.   */
  67.    if(!vec.empty())
  68.   cout << "vec is not empty" << endl;
  69.    else
  70.   cout << " vec is empty"<<endl;


  71. vec.assign(20,7); // // After the vec.clear. vec is surely empty!. Let's assign it again with 20 elements
  72. /** end()
  73.   *  Returns a read/write iterator that points one past the last
  74.   *  element in the %vector.  Iteration is done in ordinary
  75.   *  element order.
  76.   */
  77. iter1 = vec.end(); // note: iter1 point to the entry beyond the vector's last element.
  78. /* erase vector at iterator position */
  79.    vec.erase(vec.begin()+5); // the 6th element of the vector is erased. The vector is now shrinked to 19 elements.
  80. /* erase range between iterators positions */
  81.    vec.erase(vec.begin(), vec.begin() + 10);// 1st to 10th elements are erased (range is always [ist, last) i.e. last element not included.
  82.    //  vector has now only 9 elements
  83.    cout << "size of vector: " << vec.size() << endl;
  84. /** front
  85.    *  Returns a read/write reference to the data at the first
  86.    *  element of the %vector.
  87.    */
  88.    a = vec.front(); // a=vec[0];
  89.    vec.front() = 4;// same as saying: vec[0] = 4;

  90. /* insert: insert new elements to the vector */
  91.    vec.insert(vec.begin() + 3, new_vec.begin(), new_vec.begin()+5); // here 6 elements form new_vec are inserted after vec's 4th element.

  92.    vec.insert(vec.begin(),20);// 20 is inserted after the first element
  93.    vec.insert(vec.begin(),10, 5);// 10 elements with value of 5 are inserted after first element
  94. /** max_size() Returns the size() of the largest possible %vector.  */
  95.    vec.max_size();// have no idea which is the max possible size. let's print it
  96.    cout << " vec.max size = " << vec.max_size()<< endl;
  97. //vec.operator =();
  98.    vec = new_vec; // vec is assigned with new_vec's value
  99. //vec.operator []();
  100.     a = vec[4]; // can read and write directly to the nth element.

  101.     /*  pop_back() @brief  Removes last element.
  102.   *
  103.   *  This is a typical stack operation. It shrinks the %vector by one.
  104.   *
  105.   *  Note that no data is returned, and if the last element's
  106.   *  data is needed, it should be retrieved before pop_back() is
  107.   *  called.
  108.   */
  109.     vec.pop_back();// last element is popped. Vector shrinks by 1. Can't get the popped value from this method.
  110. /** push_back(n)
  111.    *  @brief  Add data to the end of the %vector.
  112.    *  @param  x  Data to be added.
  113.    *
  114.    *  This is a typical stack operation.  The function creates an
  115.    *  element at the end of the %vector and assigns the given data
  116.    *  to it.  Due to the nature of a %vector this operation can be
  117.    *  done in constant time if the %vector has preallocated space
  118.    *  available.
  119.    */
  120.     vec.push_back(11); // 11 is added to the end of vec. vec.size() is incremented by 1
  121.     vec.push_back(12); // 12 is added to the end of vec. vec.size() is incremented by 1
  122.     vec.push_back(13); // 13 is added to the end of vec. vec.size() is incremented by 1
  123.     vec.push_back(14); // 14 is added to the end of vec. vec.size() is incremented by 1
  124. /** rbegin()
  125.    *  Returns a read/write reverse iterator that points to the
  126.    *  last element in the %vector.  Iteration is done in reverse
  127.    *  element order.
  128.    */
  129.     /* rbegin() returns a reverse iterator, so let's create one:
  130.      *
  131.      */
  132.     vector<int>::reverse_iterator riter1;
  133.     vector<int>::reverse_iterator riter2;
  134.     riter1 = vec.rbegin(); // now riter1 points to vec's last element. Note:Remember that vec.end() points one place after that.
  135. /** rend()
  136.    *  Returns a read/write reverse iterator that points to one
  137.    *  before the first element in the %vector.  Iteration is done
  138.    *  in reverse element order.
  139.    */
  140.     riter2 = vec.rend(); // riter2 points on the place before vec's first element.
  141. // let's print vec in reverse order:
  142.     vector<int>::reverse_iterator riter;
  143.     cout << " vec in reverse order: " << endl;
  144.     for ( riter=riter1 ; riter <riter2; ++riter )
  145.        cout << *riter << ", ";
  146.   // as a reference: here is the non reverse order:
  147.     cout << "\nsame vec in regular order: " << endl;
  148.     iter1 = vec.begin();
  149.     iter2 = vec.end();
  150.     vector<int>::iterator iter;
  151.     for ( iter=iter1 ; iter <iter2; ++iter )
  152.        cout << *iter << ", ";



  153.  /* reserve(n) Set the allocated storage of the vector. It doesn't change the size of the vector, but the reserved capacity can avoid the expensive memory
  154.   * relocation when expending the vector's size
  155.   */

  156.     vec.reserve(2); // here it will do nothing because capacity is more than 2
  157.     vec.reserve(200); // the vector is preserved with a total of 200 elements. size is not changed.
  158.     cout << " 200 places were resreved without affecting  size:\n";
  159. cout << "vec.capacity() = " << vec.capacity() << ", vec.size() = " << vec.size() << endl;

  160. /** resize(n)
  161.    *  @brief  Resizes the %vector to the specified number of elements.
  162.    *  @param  new_size  Number of elements the %vector should contain.
  163.    *  @param  x  Data with which new elements should be populated.
  164.    *
  165.    *  This function will %resize the %vector to the specified
  166.    *  number of elements.  If the number is smaller than the
  167.    *  %vector's current size the %vector is truncated, otherwise
  168.    *  the %vector is extended and new elements are populated with
  169.    *  given data.
  170.    */

  171.     vec.resize(50); // here size of vector is changed to 50 elements. No relocation is needed, as the vector is reserved with 200 elements.
  172.     vec.resize(40, 3); // size is changed to 40 entries with value = 3
  173. /**  Returns the number of elements in the %vector.  */
  174.     vec.size(); // size is 40 now
  175.     cout << "\ncurrent vec size is " << vec.size();
  176. /** swap(vector)
  177.   *  @brief  Swaps data with another %vector.
  178.   *  @param  x  A %vector of the same element and allocator types.
  179.   *
  180.   *  This exchanges the elements between two vectors in constant time.
  181.   *  (Three pointers, so it should be quite fast.)
  182.   *  Note that the global std::swap() function is specialized such that
  183.   *  std::swap(v1,v2) will feed to this function.
  184.   */
  185.   // let print vectors before and after swap:
  186.    cout << "\nprint vec before swap" << endl;
  187.      for ( iter=vec.begin() ; iter < vec.end(); ++iter )
  188.         cout << *iter << ", ";
  189.      cout << "\nprint new_vec before swap" << endl;
  190.          for ( iter=new_vec.begin() ; iter < new_vec.end(); ++iter )
  191.             cout << *iter << ", ";


  192.     vec.swap(new_vec); // here vec and new_vec are swapped.

  193.     cout << "\nprint vec after swap" << endl;
  194.        for ( iter=vec.begin() ; iter < vec.end(); ++iter )
  195.           cout << *iter << ", ";
  196.        cout << "\nprint new_vec after swap" << endl;
  197.            for ( iter=new_vec.begin() ; iter < new_vec.end(); ++iter )
  198.               cout << *iter << ", ";


  199. }



עוד על constructors של vector. הנה הצורות הקימות:
יצירת וקטור ריק.
  1. vector<T>vect;                                      
יצירת וקטור עם n אלמנטים

  1. vector<T>vect(size_type n);
יצירת וקטור עם n אלמנטים שערכם t
  1. vector<T>vect(size_type n, const T &t);
יצירת וקטור שגודלו ותוכנו בהתאם לתחום המוגדר ע"י שני ה-iterators:
  1. vector<T>vect(begin_iterator, end_iterator);


כל 4 סוגי ה-constructors הנ"ל משולבים בקטע הקוד הבא.

מודגשים בצהוב - ה-constructors.
מודגשות בירוק - פקודות שליחה ל-output.
מתחת לסוף התוכנית - העתק הפלט שודפס על המסך.

  1. /*
  2.  * vector_const.cpp
  3.  *
  4.  *  Created on: Mar 20, 2012
  5.  *      Author: ronen halevy
  6.  */

  7. #include <vector>
  8. #include <iostream>
  9. using namespace std;
  10. int main(void){
  11. vector <string>vect1;
  12. vector<string> vect2(5);
  13. string str = "start";
  14. vector<string> vect3(5, str);
  15. vector<string>vect_source(10, "asd");
  16. vector<string>vect4(vect_source.begin(), vect_source.end());
  17. vector<string>::iterator iter; // create an iterator
  18. cout << "\nvector <string>vect1; " << endl;
  19. for ( iter=vect1.begin() ; iter < vect1.end(); ++iter )
  20. cout << *iter << ", ";
  21. cout << "\nvector<string> vect2(5); " << endl;
  22. for ( iter=vect2.begin() ; iter < vect2.end(); ++iter )
  23. cout << *iter << ", ";
  24. cout << "\n vector<string> vect3(5, str);" << endl;

  25. for ( iter=vect3.begin() ; iter < vect3.end(); ++iter )
  26. cout << *iter << ", ";
  27. cout << "\nvector<string>vect4(vect_source.begin(), vect_source.end()); " << endl;

  28. for ( iter=vect4.begin() ; iter < vect4.end(); ++iter )
  29. cout << *iter << ", ";
  30. cout << endl;
  31. }

הפלט לשהודפס על המסך:

  1. vector <string>vect1; 

  2. vector<string> vect2(5); 
  3. , , , , , 
  4. vector<string> vect3(5, str);
  5. start, start, start, start, start, 
  6. vector<string>vect4(vect_source.begin(), vect_source.end()); 
  7. asd, asd, asd, asd, asd, asd, asd, asd, asd, asd, 


הסבר הפלט (מספרי השורות לא שייכים לפלט המקורי): 
הפלט מציג את תוכנם 4 הוקטורים שנוצרו ע"י 4 ה-constructors.
שורות 1.3.5.7 - הן רק כותרות.
 שורות 2,4,6,8 - הדפסה של 4 הוקטורים שאותחלו ע"י 4 ה-constructors:
שורה 2: ריקה כצפוי.
שורה 4: 5 אלמנטים לא מאותחלים בערך - הם blanks.
שורה 6: 5 אלמנטים עם הערך "start"
שורה 8: 10 אלמנטים עם הערך "asd"


כמה מילים על סיבוכיות:








2 תגובות:

  1. יפה מאוד !!
    שימושי מאוד !!

    השבמחק
  2. יש מצב שאתה מסיר איך לעשות השמה : נניח יש לי מערך א" של פוינטרים (הוגדר ע"י וקטור) ואני מגדיר עוד מערך של פוינטרים ב"
    איך אני עושה השמה ביניהם.

    השבמחק