最近更新: 2007-07-18

STL Vector/Map 的使用練習, 附 Ruby 對照程式碼

最近在練習使用 C++ STL 中的 Container 功能。嗯,寫著寫著,覺得很不順手啊。例如不能用 Vector/Set/Map 直接建表。Stack 的 pop() 方法沒有回傳值;我用 C 寫的 stack 功能, pop() 是會推一個值出來的。

挑了兩個 STL Container 的練習程式碼,再用 Ruby 寫一段相同的。兩相比較,也算在吐槽吧。

Vector 練習

把字元陣列的內容放入 Vector 容器 vec 中。將 vec 中的每個字元都轉換成大寫字母,最後倒著顯示。

輸出結果如下:

H
e
l
l
o
O
L
L
E
H
C++: 21-3.cc

不接受 vector vec("Hello") 的寫法。每次要建立一個容器時,總少不了重覆的存入動作程式碼。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<char> vec;
    char* ps;
    
    for (char* ps = "Hello"; *ps != '\0'; ++ps) {
        vec.push_back(*ps);
    }
    
    vector<char>::iterator iter;
    for (iter = vec.begin(); iter != vec.end(); ++iter) {
        cout << *iter << endl;
    }
    
    int n = vec.size();
    for (int i = 0; i < n; ++i) {
        if (vec[i] >= 'a' && vec[i] <= 'z')
            vec[i] -= 0x20;
    }

    vector<char>::reverse_iterator riter;
    for (riter = vec.rbegin(); riter != vec.rend(); ++riter) {
        cout << *riter << endl;
    }
    return 0;
}
Ruby: 21-3.rb
vec = "Hello"

vec.split('').each do |c|
    puts c
end

vec.upcase!
vec.split('').reverse_each do |c|
    puts c
end
Map 練習

用 Map 建立一個顏色名稱與 RGB 代碼的對照表。輸出結果如下列:

blue       => #0000ff   
red        => #ff0000   
green      => #00ff00   
black      => #ffffff   
white      => #000      
'blue' Find! It's value is #0000ff
C++: 22-2.cc

原本用 C 字串陣列,後來改用 String 陣列。然而這個陣列 ss 只是「初始值」,還要重覆將這些初始值存入 Map 容器 myMap 中的程式碼。

#include <iostream>
#include <iomanip>
#include <string>
#include <map>

using namespace std;

int main() {
    /*char* ss[][2] = {
        {"black",   "#ffffff"},
        {"white",   "#000"},
        {"blue",    "#0000ff"},
        {"red",     "#ff0000"},
        {"green",   "#00ff00"},
        {NULL, NULL}
    };*/
    string ss[][2] = {
        {"black",   "#ffffff"},
        {"white",   "#000"},
        {"blue",    "#0000ff"},
        {"red",     "#ff0000"},
        {"green",   "#00ff00"},
        {"", ""}
    };

    map<string, string> myMap;
    map<string, string>::iterator iter;
    
    //for (int i = 0; ss[i][0]; ++i)
    for (int i = 0; !ss[i][0].empty(); ++i)
        myMap.insert(map<string,string>::value_type(ss[i][0], ss[i][1]));

    for (iter = myMap.begin(); iter != myMap.end(); ++iter) {
        cout << setiosflags(ios::left) << setw(10) << (*iter).first
            << " => " << setw(7) << (*iter).second << endl;
    }

    if ((iter = myMap.find("blue")) != myMap.end()) {
        cout << "'blue' Find! It's value is " << (*iter).second << endl;
    }

    return 0;
}
Ruby: 22-2.rb

Ruby 可以直接建立對照表,不需要另外建立容器。

ss = {
    :black  =>  "#ffffff",
    :white  =>  "#000",
    :blue   =>  "#0000ff",
    :red    =>  "#ff0000",
    :green  =>  "#00ff00"
};

ss.each do |key, value|
    printf "%-10s => %-10s\n", key, value
    # Data type is dynamic, so printf in Ruby is safe.
end

if (ss['blue'.to_sym])
    puts "'blue' Find! It's value is #{ss[:blue]}"
end
樂多舊網址: http://blog.roodo.com/rocksaying/archives/3679815.html

樂多舊回應
未留名 (#comment-11428123)
Wed, 18 Jul 2007 20:44:36 +0800
C++ 和 ruby 設計理念的不同, 所以程式寫起來也不會一樣, 不過我很好奇的是, 像第一個例子明明就應該是用 std::string 為什麼一定要用 vector (vec 是 std::string, vec.split('') 之後才算是 std::vector), 當然要做也是可以, 我自己改的程式如下:
-------------
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

static void make_upper(char& c)
{
c = (c >= 'a' && c <= 'z')? (c - 0x20): c;
}

static void print_char(const char& c)
{
std::cout << c << "\n";
}

int main()
{
std::string ps("Hello");
std::vector<char> vec(ps.begin(), ps.end());
//上面兩行在這例子中也可以直接換成下面這個
//std::string vec("Hello");
for_each(vec.begin(), vec.end(), print_char);
for_each(vec.begin(), vec.end(), make_upper);
for_each(vec.rbegin(), vec.rend(), print_char);
return 0;
}
未留名 (#comment-11429197)
Wed, 18 Jul 2007 20:56:21 +0800
至於第二個, 因為語法的限制, C++ 沒定義也無法做這種神奇的"擴充"(就算是 macro 也做不到), 所以得乖乖的一個一個加進去, 不過 map 可以使用 map[key] = value 的方式來設定值和 map[key] 來取值, 所以上例我會改成這樣子:
---------------------
#include <iostream>
#include <iomanip>
#include <map>
#include <algorithm>
#include <string>

static void print_map(const std::map<std::string, std::string>::value_type& data)
{
//printf("%-10s => %-10s\n", data.first.c_str(), data.second.c_str());
std::cout << std::setiosflags(std::ios::left) << std::setw(10) << data.first
<< " => " << std::setw(7) << data.second << std::endl;
}

int main()
{
char* ss[] = {
"black", "#ffffff",
"white", "#000",
"blue", "#0000ff",
"red", "#ff0000",
"green", "#00ff00",
NULL
};
std::map<std::string, std::string> myMap;

for(char **q = ss; *q; q += 2)
myMap[*q] = *(q+1);

for_each(myMap.begin(), myMap.end(), print_map);

//這地方必需要用 find, 要不然會多建一個 myMap["blue"], 如果 blue 不存在的話
if(myMap.find("blue") != myMap.end())
std::cout << "'blue' Find! It's value is " << myMap["blue"] << std::endl;
}
未留名 (#comment-11433135)
Wed, 18 Jul 2007 21:21:49 +0800
我是覺得在這兩個語言的設計理念就有差別, 所以在 C++ 裡面必需要用一些比較仔細(或者是囉哩吧嗦)的方式來處理, 好處是可以想的更仔細一點, 壞處也是要想的很仔細(像什麼型態處理等等, 一個錯就各個錯), 而 ruby(perl, php 等等)裡就可以用一些語言上先天的優勢來簡化, 但壞處就有可能是會有地方想不周延或是有一些誤解的可能性, 各有各的好, 各有各的差. 對我來說, 能夠依情況找到適合自己用的語言就好了, 我也不會想要去計較哪一個比較麻煩哪一個比較簡單 :p
cfchou_at_gmail(cf) (#comment-11558653)
Thu, 19 Jul 2007 11:00:38 +0800
我想第一個例子可以寫的更STL
string s("hello");
vector<char> vec(s.begin(), s.end());
copy(vec.begin(), vec.end(), ostream_iterator<char>(cout, "\n"));
transform(vec.rbegin(), vec.rend(), ostream_iterator<char>(cout, "\n"), ptr_fun(::toupper));



第二個例子可以這樣做

map<string, string> clr_tbl; //註一
clr_tbl["black"] = "#ffffff";
clr_tbl["white"] = "#000000";
clr_tbl["blue"] = "#0000ff";
clr_tbl["red"] = "#ff0000";
clr_tbl["green"] = "#00ff00";

transform(clr_tbl.begin(), clr_tbl.end(), ostream_iterator<string>(cout, "\n"), select2nd<map<string, string>::value_type>()); //註二
if(clr_tbl.find("blue") != clr_tbl.end()) cout << "blue is " << clr_tbl["blue"] << endl;


註一
像這樣也可以啊
pair<string, string> tbl[] = { make_pair("black", "#ffffff"), make_pair("white", "#000000") };
map<string, string> clr_tbl(tbl, tbl + 2);

註二
假如不是用SGI STL的話很可能沒有他的select2nd, 但是可以自己仿造一個
template <typename T1, typename T2> T2 simu_sgi_select2nd(pair<T1, T2> const & tmp)
{
return tmp.second;
}
transform(clr_tbl.begin(), clr_tbl.end(), ostream_iterator<string>(cout, "\n"), ptr_fun(simu_sgi_select2nd<string, string>));

註三
其實拿STL map來跟純粹的table來比並不太適合
map是sorted的,如果Ruby的這樣一個table沒有sorted(如果啦,目前還沒時間研究Ruby)
那麼我會覺得直接拿vector<pair<string, string>> or list<pair<string, string>>甚至
直接string[][]配合STL的algorithms來做比較會更適合



老實說寫這短短幾行東西還搞蠻久的(下班還賴著不走吹冷氣是怎樣......)
因為我跟STL還不熟
那就是他接受度不高的原因(你得先跟它裝熟)
但是一旦了解它的觀念(literally, Concept, 就是"Generic Programming and the STL"這本看懂的意思啦)
就會發現用它真的跟組裝樂高很像, 而STL定義軟體元件的方式真讓人大開眼界......
STL和template可以達到generic
指的不單針對c++ built-in type而是還有自訂型別可以共用所有組件
STL有的不只是效能高的container還有其他組件iterator, function object, algorithm, adaptor......
又可以擴充或抽換它們
從這些面向定義軟體元件
我還沒有在其他的地方看過


我不是了解很多其他的語言
動態語言用一個general object來承接object
在runtime的時候再去依照物件(late binding)的實際支援method與否來決定結果(runtime error?)
仍然是polymorphism的味道較重
並不算是generic
至少不算是靜態語言中的generic
我覺得STL(C++ template)比較像是不失去靜態語言優點(solid)為前提追求靜態語言中最大的彈性

但是這個是動態語言和靜態語言基本的特性不同, 無關優劣
cfchou_at_gmail(cf) (#comment-11566317)
Thu, 19 Jul 2007 11:24:21 +0800
另外pop() 方法沒有回傳值這碼子事
算是STL兼顧效能和彈性的一點,十分合理喔
"Generic Programming and the STL"的9.2.3 Back Insertion Sequence有講到
未留名 (#comment-11577761)
Thu, 19 Jul 2007 13:33:07 +0800
我本來也是用 copy, 不過要硬要和 ruby 對照的話, 就全改為 for_each 了
不過 cf 的第二個例子輸出是沒有合要求的哦(當然, 寫一個 function 來做是最簡單的) :p
未留名 (#comment-11585741)
Thu, 19 Jul 2007 16:37:31 +0800
關於第一個例子為何用 vector ?
理由很簡單,因為 "Hello" 是一個「陣列」。

一個陣列,我們可以直接建表:
int ar = {1,2,3,4,5};

一個 vector,頂多只能指定每個元素的初值。
vector<int> vec(5,0);
然後重複:
vec[0] = 1;
vec[1] = 2;
...

cf 所提供的第二個例子也一樣:
map<string, string> clr_tbl;
clr_tbl["black"] = "#ffffff";
clr_tbl["white"] = "#000000";

這是一個令我覺得煩人的程式碼重複動作。

至於鍵值的排序,這點倒不在我的需求中。所以用 vector + pair 也成。但 vector 沒有 find() ,所以還要再給它一個 find 的 Algorithms 。但這牽涉到搜尋的效率問題。

Ruby 中的 table ,鍵值不排序,但搜尋是用雜湊法。考量搜尋與插入效率,在 STL 中,最接近的就是 Map 。List的話,少了個鍵值雜湊表,搜尋效率力有未逮。

關於 pop()。在 C 語言中,我實作的方式是給一個參數,如 pop(void*elm)。如果引數為 NULL ,直接丟棄;否則存入並回傳。這種寫法在 STL 中被視為不安全的。如果要回傳,那麼還要考慮複製或參照等事,確實沒效率。

其實,我用的例子應該算敲在 STL 的弱項上。對老練的 C 程序員而言,不用 STL 反而寫起來更簡單。就像 cf 說「直接string[][]配合STL的algorithms來做比較會更適合」。

關於polymorphism與generic。

從語法與語意看,動態語言的語法其實就是泛型的。它們只是因為沒有編譯動作,而不得不 late binding 。並不得不仰賴其他的處理策略。參考《Use C/C++ with Dynamic Languages is Easier Than Pure C++》及《個體之間協議互動行為的多種形式》。

我也可以換另一種說法,若從泛型與編譯器的關係來看,沒有編譯器的動態語言環境不需要像 template 這類的泛型功能。See also: Ruby versus Smalltalk versus .... 所以 cf 說「我還沒有在其他的地方看過」是很合理的,因為 Ruby, Perl, JavaScript 等動態語言中用不著。
未留名 (#comment-11644671)
Fri, 20 Jul 2007 13:56:25 +0800
我不知道為什麼一定要把 C 的觀念硬套在 STL 上, STL 裡沒有陣列這種東西, 比較接近的是 Random Access Container 這類的 Container, 依照要求的話, 我會選用 std::basic_string<char> 而不是 std::vector<char>(這兩者都是 Random Access Container), 再來對於 "Hello" 來說, 它可以看做是一個陣列, 但是也是可以看做一個字串, 既然是字串的話, 當然用 std::basic_string<char> 下去處理會是最好的選擇

而第二個例子, 要達到 key => value 這個要求, 對映到 STL 上的就是 Pair Associative Container, 底下只有 map/multimap 可以選(在 SGI STL 裡, 還有 hash_map 和 hash_multimap 可用), 所以當然 map 是首選

至於不能很簡單的設值, 就只是語法的問題, 因為 C++ 先天的限制, 不可能很簡單的做到像是 std::vector<char> tmp = { 1, 2, 3 }; 這種事, 要嘛就是建立出來一個慢慢設, 要嘛就是先生出一個有初始值的 char[], 然後透過 std::vector(InputIterator begin, InputIterator end) 來建立, 那這是不是算弱項, 可能吧, 因為要多寫一堆東西, 看起來就不好看

至於 pop 的問題, 如果真的需要回傳的話, 也可以自己寫一個 function 來做相同的事:
template<class C> C::value_type pop(C& c) {
C::value_type tmp = c.back();
c.pop_back();
return tmp;
}
也不是不行啊, 不可能每一個函式庫都能滿足所有的需求的
未留名 (#comment-11652443)
Fri, 20 Jul 2007 15:01:43 +0800
maniac said: '對於 "Hello" 來說, 它可以看做是一個陣列, 但是也是可以看做一個字串.'

雖然我第一個例子用的是 "Hello" ,但我原本就是要練習用容器建表、查表和走訪,而不是練習字串處理。如果我用 string 就沒有這個練習效果了。

關於直接建表的事。在我接觸 STL 之前,我原本習慣用陣列建表,然後直接把陣列作為建構子的初始值。以前的做法,類似用陣列配 Algorithms 實作一個 class 。而不是先建構容器,再一一指派。所以一開始用 STL 的 Container 時,我對它不能直接用陣列作初始值這點感到不可思議。我一直以為它可以這麼做。
未留名 (#comment-12263725)
Sun, 22 Jul 2007 21:53:08 +0800
std::basic_string 也是一個容器, 和 std::vector 同樣都是有著 Random Access Container, Sequence 的容器, 通常我是視為同一類型的東西來處理, 所以我在我自己的應用上, 會選擇符合我需要特性的來處理, 像
char *tmp_string = "Hello";
std::vector<char> vec(tmp_string, tmp_string + strlen(tmp_strlen));

std::basic_string<char> vec("Hello");
兩個比起來, 我會選擇用後者的方式來建立, 而且這兩個處理方式也很類似, 不用因為操作的對象是 vector 或是 string 而有太大的差別(除了用到 string 特有的方式例外)

其實, 在給的 ruby 範例裡, 也是必需把 string 切割成 array(string) 才能對每一個字元做處理(就我所用過的 script 語言裡, 只有 php 可以直接對字串取出一個字元的動作: $str[$pos]), 比擬成用 STL 的話, 就是先從字串生成暫時的 std::vector<char> 物件, 再使用 for_each 方法對每一個 vector 所包含的 char 做處理, 這兩種本質其實是相去不遠的

至於不能用陣列作初始值? 當然那只是語法的問題, C++ 沒有方法可以用"匿名"陣列做出來類似 vector<char> vec = "Hello"; 或是 vector<int> vec = { 1, 2, 3 }; 這種東西, 所以 STL 也不可能超越這種限制, 但是如果是從已經有值的"具名"陣列來建立的話, 可以參考 http://www.sgi.com/tech/stl/Vector.html, std::vector 有一個方法是 template <class InputIterator> vector(InputIterator, InputIterator) [Creates a vector with a copy of a range.]
這個就是說, 我可以給一個陣列的起始值和它的終點來建立一個新的 vector, 比方說
int initial_array[3] = { 1, 2, 3 };
std::vector<int> vec(initial_array, initial_array + 3);
來做出內容是 { 1, 2, 3 } 的 vector, 至於不能用"匿名"陣列來做是好是壞, 對我來說就只是麻煩一點要多寫一些東西而已, 倒也沒什麼
未留名 (#comment-12682515)
Tue, 24 Jul 2007 16:46:02 +0800
類推,我的例子可以寫成:
char* ps = "Hello";
std::vector vec2(ps, ps+strlen(ps));

嗯,用指標做為容器建構初始值的寫法,應該是最直接的方式了。如果要讓容器建表像陣列一樣,那就要在語法上做修改了。算了,C++沒有必要這麼做。

Ruby 也可以用 str[pos] 的方式取出一個字元(char)。只是別忘了, char 的意義就是一個整數,所以 Ruby 的 str[pos] 之值是一個整數,而不是字串(string)。
未留名 (#comment-15349937)
Sat, 22 Dec 2007 17:22:48 +0800
InitUtil:An STL Container Initialization library
http://www.bdsoft.com/tools/initutil.html

可能是你要的.
未留名 (#comment-15349953)
Sat, 22 Dec 2007 17:25:57 +0800
未留名 (#comment-22359922)
Thu, 15 Mar 2012 13:39:42 +0800
pop沒有回傳值可以較容易的做到exception safe,exceptional C++詳細解釋

這兩個例子以C++11可以這樣寫
#include
#include
#include
#include
#include
#include

void simple_str()
{
std::string str("Hello");

std::copy(std::begin(str), std::end(str), std::ostream_iterator(std::cout, "\n"));

std::for_each(std::begin(str), std::end(str), [](char &fir) { fir = toupper(fir); });
std::copy(str.rbegin(), str.rend(), std::ostream_iterator(std::cout, "\n"));
std::cout
未留名 (#comment-22360074)
Thu, 15 Mar 2012 14:30:23 +0800
to GP:

roodo回應系統會過濾字元。
我看看,你想表達的是 C++ 11 新增的 lambda (anonymous function) 語法嗎?
[](char &fir) { fir = toupper(fir); }