Open rainit2006 opened 7 years ago
extern全局变量和static、const的的区别
作用二:当它不与”C”在一起修饰变量或函数时,如在头文件中,extern int g_nNum;,它的作用就是声明函数或变量的作用范围的关键字,其声明的函数和变量可以在本编译单元或其他编译单元中使用。 即B编译单元要引用A编译单元中定义的全局变量或函数时,B编译单元只要包含A编译单元的头文件即可,在编译阶段,B编译单元虽然找不到该函数或变量,但它不会报错,它会在链接时从A编译单元生成的目标代码中找到此函数。
注意使用static修饰变量,就不能使用extern来修饰,static和extern不可同时出现.
static修饰的全局变量的作用域只能是本身的编译单元。在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。
静的クラス (Static Class) 「new させない」という意図を示すために、C# では静的クラスという機能を使用します。 静态类相当于一个sealed abstract类,主要的一个优点是写在栈中,安全高速稳定,而且在执行的时候,十分优先。
静态方法、静态变量,方便了开发中的操作,不需要实例则可以调用,但是往往却破坏了一些属性的封装,使得在安全性方面大大降低,在内存使用上和实例变量有着不同的地方,静态变量或方法他是在程序一运行类一加载的时候就会为他分配了一块内存地址,相当于初始化了这些静态变量,而实例变量或属性是只有当对象被实例的时候才会为这些属性分配地址,也就是说在程序运行的时候,如果是小程序,那么在使用过程中占用的内存会随着你实例的创建而逐步增加,对象需要被共享的时候,这时候可以考虑单例模式(只有一个实例),保证不会浪费内存地址,当然这要符合实际,但是如果是一个比较大的项目,重用性强,变量需要被共享的时候,就可以考虑用static来解决。
访问类的静态成员(包含静态函数和静态变量)时,只能通过类名访问,而不能用类的对象访问。
class Dogs
{
//静态成员,狗的数量
public static int Count =0;
//非静态成员,狗的名称
public string Name;
//创建一条狗,数量加1
public Dogs()
{
Count++;
}
//静态方法,只能访问静态字段
public static int GetCount()
{
//string nm = this.Name; //错误,不能访问非静态成员
return Count;
}
}
class Program
{
static void Main(string[] args)
{
System.Console.WriteLine("Dogs.Count={0}",Dogs.Count);
Dogs aDog = new Dogs();
System.Console.WriteLine("Dogs.Count={0}",Dogs.Count);
//直接通过类名调用静态成员
Dogs.Count=5;
System.Console.WriteLine("Dogs.Count={0}",Dogs.Count);
Dogs bDog =new Dogs();
//调用静态成员函数
System.Console.WriteLine("Dogs.Count={0}",Dogs.GetCount());
//aDog.GetCount(); //错误,静态成员只能通过类名访问。
}
}
静态构造函数用于初始化任何 静态 数据,或用于执行仅需执行一次的特定操作。 在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。 静态构造函数具有以下特点: •静态构造函数既没有访问修饰符,也没有参数。 •在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数来初始化类。 •无法直接调用静态构造函数。 •在程序中,用户无法控制何时执行静态构造函数。 •静态构造函数的典型用途是:当类使用日志文件时,将使用这种构造函数向日志文件中写入项。 •静态构造函数在为非托管代码创建包装类时也很有用,此时该构造函数可以调用 LoadLibrary 方法。 •如果静态构造函数引发异常,运行时将不会再次调用该构造函数,并且在程序运行所在的应用程序域的生存期内,类型将保持未初始化。
编译单元:一个.cc或.cpp文件作为一个编译单元,生成.o
Marshal クラス Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code.
成员变量 -- AllocHGlobal函数:内存分配 -- AllocCoTaskMem(メモリ割り当て)、ReAllocCoTaskMem(メモリサイズの再割り当て)、 --在托管和非托管间复制:Copy 在托管和非托管内存空间之间复制值类型数组。支持CLI整型,包括64位整型。支持单精度和双精度浮点数。有14个重载的方法(7个用来复制到托管内存空间;7个用来复制到非托管内存空间) -- 复制到非托管内存空间 StructureToPtr:复制托管对象到非托管内存空间 WriteByte:写入一个字节(byte)到非托管内存空间 --复制到托管内存空间 PtrToStringUni:在非托管内存空间中创建一个托管的字符串 PtrToStructure:在非托管内存空间中创建一个对象
分配非托管内存空间 我们称之为“非托管”内存是因为运行时的垃圾收集器不会管理内存。而你必须管理你分配的内存,这就意味着当你不再使用它的时候,需要释放这些内存。没有释放内存空间会导致内存泄漏。当内存的泄露到一定程度的时候,你的程序或操作系统本身可能会崩溃。
*Marshal在C#中的应用(void 指针到IntPtr的转化)**
StructLayout マネージ環境で用いられる構造体はアンマネージ環境のものと互換性がありません。相互運用では構造体の定義にStructLayoutアトリビュートを付加することでアンマネージ互換の構造体を定義します。
アンマネージ互換の構造体は構造体定義の頭に以下の定義を付けます。 [StructLayout(必要なディレクティブをカンマで区切りながら羅列)]
ディレクティブには以下の種類があります。
C/C++
typedef struct mmtime_tag
{
UINT wType; /* indicates the contents of the union */
union
{
DWORD ms; /* milliseconds */
DWORD sample; /* samples */
DWORD cb; /* byte count */
DWORD ticks; /* ticks in MIDI stream */
/* SMPTE */
struct
{
BYTE hour; /* hours */
BYTE min; /* minutes */
BYTE sec; /* seconds */
BYTE frame; /* frames */
BYTE fps; /* frames per second */
BYTE dummy; /* pad */
BYTE pad[2];
} smpte;
/* MIDI */
struct
{
DWORD songptrpos; /* song pointer position */
} midi;
} u;
} MMTIME;
C#
[StructLayout(LayoutKind.Explicit)]
public struct MmTime
{
[FieldOffset(0)]
public uint wType; // indicates the contents of the union
[FieldOffset(4)]
public uint ms; // milliseconds
[FieldOffset(4)]
public uint sample; // samples
[FieldOffset(4)]
public uint cb; // byte count
[FieldOffset(4)]
public uint ticks; // ticks in MIDI stream
// SMPTE
[FieldOffset(4)]
public byte hour; // hours
[FieldOffset(5)]
public byte min; // minutes
[FieldOffset(6)]
public byte sec; // seconds
[FieldOffset(7)]
public byte frame; // frames
[FieldOffset(8)]
public byte fps; // frames per second
[FieldOffset(9)]
public byte dummy; // pad
[FieldOffset(10)]
public byte pad0;
[FieldOffset(11)]
public byte pad1;
// MIDI
[FieldOffset(4)]
public uint songptrpos; // song pointer position
}
※union(ユニオン)とは、複数の型が同一のメモリ領域を共有する構造のことです。共用体(きょうようたい)ともいいます。うまく使用すると処理系に依存するような情報を入れないで一つのメモリ領域で異なる種類のデータを処理できます。
Pack Pack=N:アライメントをNにします。デフォルトでは8の様に大きな値になっていることがあります。実装しようとする構造体のメンバーに隙間ができないように適切な値を設定します。
CharSet CharSet=CharSet.Ansi :構造体で用いる文字をAnsiとします。 CharSet=CharSet.Unicode:構造体で用いる文字をUnicodeとします。
IEnumerable 実はforeachはIEnumerableインターフェースを実装しているクラスしか処理することができません。
配列やListなどはすでにIEnumerableインターフェースを実装しているので「C#の配列とかはforeachで処理できるものだ」と認識しちゃいますがIEnumerableインターフェースがあってこそのforeachです。
程序集(Assembly)
①什么是程序集? 可以把程序集简单理解为你的.NET项目在编译后生成的.exe或.dll文件. 嗯,这个确实简单了些,但我是这么理解的.详细: http://blog.csdn.net/sws8327/archive/2006/09/21/1244642.aspx
②程序集和命名空间的区别? 一个程序集可以跨越n个命名空间,一个命名空间也可以包含n个程序集.(估计你该晕了)
如果说命名空间是类库的逻辑组织形式,那么程序集就是类库的物理组织形式。只有同时指定类型所在的命名空间及实现该类型的程序集,才能完全限定该类型。(摘抄自《精通.NET核心技术--原来与架构》 电子工业出版社)
也就是说,你要创建一个类的实例,必须知道该类的 命名空间(这个一般都知道)+程序集(这个很容易被我们忽略,因为一般我们不需要引用外部的程序集,如果你用C#做过Excel文件的导入导出,就会知道必须添加一个Excel相关的程序集引用)
③怎样通过命令行创建程序集? 我对命令行向来反感,如果你想知道,look here: http://www.cnblogs.com/3echo/archive/2006/02/14/330579.html
④我怎么把项目和程序集联系起来理解? 一个项目对应一个程序集.项目名与程序集名相同(03版,05版乱七八糟,随机生成的程序集名). 一般的我们每创建一个.NET项目(ASP.NET(VS2005里没有),WinForm,类库,控制台等),IDE都会自动生成一个AssemblyInfo.cs的文件,打开看看(03版)
-BindingList [C#] DataGridViewにバインドするリストにはBindingListを使用すると良い DataGridViewにはデータベースから取得したDataTable以外にも任意のカスタムクラスの配列やリストもバインドすることができてとても便利です。
ただ、Listをバインドした場合、例えばListにオブジェクトを追加しても、DataGridViewにはその値はすぐに反映されません。 一度DataSourceをnullにして再度割り当てれば反映されますが、色々な設定も元に戻ってしまい面倒です。 また、Listをバインドした場合は、DataGridView上での行の追加や削除操作も行えません。
そんな時には、バインドするのをListではなく、BindingListというリストにすると、変更内容がリアルタイムに反映されるようになります。
(リアルタイムに行うためにはバインドする対象がIBindingListというインタフェースを実装している必要があるようです。)
Stopwatch sw = Stopwatch.StartNew();
for(int index = 0; index < 10; index++) { DoSomething(); Console.WriteLine(sw.ElapsedMilliseconds); }
sw.Stop();
C++创建对象时的new与不new C++在创建对象时有两种方式(例如类名为Test): 1,Test test; //此时Test类的构造函数会被调用。在栈上创建。函数局部变量就被创建在栈上,函数结束后内存空间被收回。 2,Test *pTest = new Test(); //在堆上创建,C++程序必须要程序员手动去管理变量对象的内存释放。
#region
値型,参照型, ref, out
C#には値型と参照型という2つの種類があり、
値型のオブジェクトは数値(int)や構造体(struct)を指す。この種類のオブジェクトを関数へ渡すとき、そのコピーが渡される。
一方の参照型は、クラスにあたるオブジェクトで、stringやList
値型のオブジェクトの3パターンの作例がこちら。
public static void Main(string[] args)
{
// For value type object.
int valueTarget = 1;
// [修飾子なし] 関数へはvalueTargetのコピーが渡されるから1のまま。
AddOne(valueTarget);
Console.WriteLine("valueTarget : " + valueTarget.ToString());
// [ref] refをつけるとvalueTargetの参照を渡せるため、2に書き換わる
AddOne(ref valueTarget);
Console.WriteLine("valueTarget : " + valueTarget.ToString());
// [out] outは、別関数内で初期化する場合に使う。
int valueTarget2;
Initialize(out valueTarget2);
Console.WriteLine("valueTarget : " + valueTarget2.ToString());
}
private static void AddOne(int target)
{
++target;
}
private static void AddOne(ref int target)
{
++target;
}
private static void Initialize(out int target)
{
target = 100;
}
参照型オブジェクトだとこうなる。
public static void Main(string[] args)
{
// For reference type object.
var referenceTargets = new List<string>{ "a", "b", "c" };
// [修飾子なし] referenceTargetsの参照が関数に渡されるから、新たな要素が追加される。
AddText(referenceTargets);
const string separator = ", ";
Console.WriteLine(
"referenceTarget : " +
string.Join(separator, referenceTargets.Select(target => target.ToString())));
// [ref] 参照型のオブジェクトは関数へ参照を渡すようになっているから、refを付ける意味はない。
AddText(ref referenceTargets);
Console.WriteLine(
"referenceTarget : " +
string.Join(separator, referenceTargets.Select(target => target.ToString())));
// [out] 参照型のオブジェクトで一番使うのがout。別関数内でオブジェクトを初期化したいときに使う。
Initialize(out referenceTargets);
Console.WriteLine(
"referenceTarget : " +
string.Join(separator, referenceTargets.Select(target => target.ToString())));
}
private static void AddText(List<string> targets)
{
targets.Add("No ref and out.");
}
private static void AddText(ref List<string> targets)
{
targets.Add("ref");
}
private static void Initialize(out List<string> targets)
{
targets = new List<string>{ "Initialized by external method" };
}
呼び出し元のオブジェクトを書き換える場合、参照型は修飾子なしでもrefでも同じ動作になる(どちらも書き換えられる)
ref 和 out : どちらも参照渡しのためのパラメーター修飾子です。
out out修飾子はreturn以外でメソッド内からメソッド外へデータを受け渡す場合で使用されます。 よく使われるものとしてはTryParseメソッドがあります。
// outの受け皿
int number;
// int.TryParseは結果として成否を返すが、成功の場合は変換結果がnumberへ格納される
bool result = int.TryParse("1234", out number)
out修飾子のついたパラメーターはメソッド内で必ず代入をする必要があります。
ref ref修飾子はメソッド外からメソッド内へデータを渡し、変更を外部へ反映させる必要がある場合に使用します。
void Main()
{
int x = 10;
Console.WriteLine(x); // 10;
Hoge(x);
Console.WriteLine(x); // 20;
}
void Hoge(ref int x)
{
x = x + 10;
}
ref修飾子のついたパラメーターはメソッドに渡す前に必ず初期化する必要があります。
volatile 組み込み系の場合:IOポートなど外部デバイスと通信するケースで、メモリアドレスを介して入出力を行うときにvolatileが必要となります。 上記以外の普通のOSの場合:(少々乱暴な言い方ですが)volatileの正しい意味を理解するまでは、それを使わないでください。通常のプログラムでは必要にならない機能です。
また、Intenet上には「マルチスレッドプログラムではvolatileが必要だ」という情報もいくつかみられますが、C言語においては誤った情報です。 C言語ではなくJava言語の話をしている可能性があります。CとJavaではvolatileは意味が全く異なります。(=Javaでは正しい情報ですが、Cには適用できない) 古いC言語仕様と古いコンパイラでは、volatileが必要な時代もありました。(=当時は正しかったが、今となっては陳腐化してしまった) https://teratail.com/questions/39986
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー reinterpret_cast ポインタ型を他のポインタ型に強制的に変換します。dynamic_castと違いポインタの型変換が安全に行えるかは考慮されません。 また整数型(int, long, long longなど)を任意の型のポインタに変換するのにも利用できます。
static_cast ある型からある型への暗黙の変換が存在する時に(たとえばintからdoubleなど)、そこで暗黙の変換が行われることを明示する場合に行います。 多くの場合はstatic_castは省略することが可能です。
dynamic_cast 親クラスの型のポインタを子クラスのポインタにキャストするときに利用します。
dynamic_castを行うためには、型情報がポインタから得られる必要があります。つまりクラスはpolymorphicである必要があります。 つまり親クラスは最低でも1つのvirtualな関数が親クラスに定義されていてvtableが存在しなくてはなりません。 http://www.yunabe.jp/docs/cpp_casts.html
将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理.
LPCTSTR、LPTSTR、LPSTR、LPCSTR http://www.usefullcode.net/2006/11/lpctstrlptstrlpstrlpcstr.html
LPSTR = char*
LPCSTR = const char*
LPTSTR = TCHAR*
LPCTSTR = const TCHAR*
LPWSTR = WCHAR*
LPCWSTR = const WCHAR*
つまり
LP = *(ポインタ)
C = const
TSTR = TCHAR
STR = char
WSTR = WCHAR
非ユニコードビルド環境でプログラミングをしているときは、WSTR系は使うことがほとんどないので、何も考えずにchar*でプログラミングして、エラーがでたらconstを付加するという方法でとりあえず動く。
しかしユニコードビルド環境の場合はTSTRとSTRの間の区別をきちんとしておかないと大変なことになるし、1つのソースコードで非ユニコードとユニコードビルドの両方に対応させようと思ったらいつも違いを意識してプログラミングをしなければならない。
作者:张乃潇 链接:https://www.zhihu.com/question/19745718/answer/12830645 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
先记最原始的 ANSI 字符串 LPSTR, 被定义成 char .LPCSTR 比 LPSTR 多了个 C, 意思是 const, 所以 LPCSTR 是 const char .
后来 XP 后 微软又把所有 API 增加了 Unicode 版本(实际是重新开发), 于是在 LPSTR 基础上加了个 W ,即 LPWSTR, 被定义成 wchat_t.同理 LPCWSTR 被定义成 const wchar_t .
可是怎么兼容 以前的 ANSI 版本的 API 呢? 微软用 带 T 的宏 来解决的。
如果 定义了Unicode那么
TCHAR 被定义成 WCHAR (就是 wchar_t)
LPTSTR 被定义成 LPWSTR (wchar_t )
LPCTSTR 被定义成 LPCWSTR (const wchar_t *)
否则 / 那就是用了 ANSI 版本了 /
TCHAR 被定义成 CHAR (就是 char 了)
LPTSTR 被定义成 LPSTR (char )
LPCTSTR 被定义成 LPCSTR (const char )对应的,实际的函数名 以 A 结尾的 对应 ANSI 版本,而以 W 结尾的 对应的 Unicode 版本.
最后我们用的 MessageBox, 其实也是宏:如果 定义了Unicode那么 MessageBox 就是 MessageBoxW 否则 MessageBox 就是 MessageBoxA
在平时的时候,char 与 const char 之间的显式转换很少, 即使用到也很容易转. 偶尔麻烦的就是 ANSI 和 Unicode 之间的转换,有俩API: Unicode- > ANSI: WideCharToMultiByte ANSI- > Unicode: MultiByteToWideChar https://www.zhihu.com/question/19745718
[VC++] std::string から LPCTSTRへの変換
std::string str;
str.c_str();
程序员应该知道的关于Windows API、CRT和STL二三事 http://www.cnblogs.com/menggucaoyuan/archive/2011/06/09/2075910.html
[C/C++]プログラムにコマンドラインからUNIX風のオプションを渡す方法 getopt関数 https://qiita.com/Shitimi_613/items/1b0eb36ca6413a521ec2
内联函数 在C语言中,我们使用宏定义函数这种借助编译器的优化技术来减少程序的执行时间。 C++中则使用内联函数。
当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
C++内联函数提供了替代函数调用的方案,通过inline声明,编译器首先在函数调用处使用函数体本身语句替换了函数调用语句,然后编译替换后的代码。因此,通过内联函数,编译器不需要跳转到内存其他地址去执行函数调用,也不需要保留函数调用时的现场数据。
static 它被用来控制变量的存储方式和可见性。
为什么要引入static? 函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现? 最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。
什么时候用static? 需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。
static的优势: 可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
静态成员函数 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。
数值转换成16进制保存到文件
std::cout << std::hex << 15 << std::endl;
ofstream of(filename);
of <<std::hex << 15 << std::endl;
std::cout << "0x" << std::setw(8) << std::setfill('0') << std::hex << 0x1111 << std::endl;
of << "0x" << std::setw(8) << std::setfill('0') << std::hex << 0x1111 << std::endl;
//setw和setfill需要引用头文件 #include <iomanip>
数值变换
http://code-mynote.blogspot.jp/2014/01/c2.html 一番手っ取り早い方法はstd::bitsetでキャストする方法
#include <iostream>
#include <bitset>
int main() {
unsigned x = 11;
std::cout << static_cast<std::bitset<8> >(x) << std::endl;
}
// 出力結果: 00001011
時間処理
現在の日付時刻に数時間加算した時の日付時刻 CTime t0(2010,12,16,23,32,56); CTimeSpan tp0( 86 60 60 ) CTime t1 = t0 + tp; CString s1 = t1.Format( "%Y/%m/%d %H:%M:%S" );
VC++で使用可能な時刻関連処理 https://qiita.com/litencatt/items/711d719382edeea9c51e
CTime time(CTime::GetCurrentTime());
SYSTEMTIME timeDest;
time.GetAsSystemTime(timeDest);
//SYSTEMTIME -> FILETIME FILETIME fileTime; ::SystemTimeToFileTime(&timeDest, &fileTime);
C++回调函数的用法
第一步,在动态库类A的头文件中,定义一个函数指针类型,这里主要是规定了参数的类型,将需要传递的参数定义好:
typedef void (*CallBackPtr)(CDataSet ,vector
以上就实现了消息的通知。
C++11 新特性 defaulted 和 deleted 函数 该特性巧妙地对 C++ 已有的关键字 default 和 delete 的语法进行了扩充,引入了两种新的函数定义方式:在函数声明后加 =default 和 =delete。通过将类的特殊成员函数声明为 defaulted 函数,可以显式指定编译器为该函数自动生成默认函数体。通过将函数声明为 deleted 函数,可以禁用某些不期望的转换或者操作符。 https://www.ibm.com/developerworks/cn/aix/library/1212_lufang_c11new/
nullptr C++03まで、ヌルポインタを表すために0数値リテラルやNULLマクロを使用していた。C++11からは、nullptrキーワードでヌルポインタ値を表すことを推奨する。
C# 装箱(boxing)和拆箱(unboxing) 实现值类型与引用类型的互相转换.
C#中定义的值类型包括原类型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct),引用类型包括:类、数组、接口、委托、字符串等。 值型就是在栈中分配内存,在申明的同时就初始化,以确保数据不为NULL; 引用型是在堆中分配内存,初始化为null,引用型是需要GARBAGE COLLECTION来回收内存的,值型不用,超出了作用范围,系统就会自动释放!
int i=0; Syste.Object obj=i; 这个过程就是装箱!就是将i装箱!
拆箱就是将一个引用型对象转换成任意值型!(注意:拆箱操作只能针对装过箱的数据,没装过箱的要是拆箱的话后果不堪设想!) 比如: int i=0; System.Object obj=i; int j=(int)obj; 这个过程前2句是将i装箱,后一句是将obj拆箱!
在C#中,将类和数组等都归为了引用型的,那么值类型和引用型有什么区别呢? 值类型的变量包含自身的数据,而引用类型的变量是指向数据的内存块的,并不是直接存放数据。对于值类型,每个变量都有一份自己的数据复制,对另一个值类型变量的操作并不影响这一个变量的值。 而对于引用类型,两个变量有可能引用同一对象,因此对一个变量的操作会影响到另一个变量。 http://www.cnblogs.com/xiaoshi/archive/2008/05/28/1208902.html