编程已经3年了
我时常觉得, 如果3年前的我由现在的我来指点的话,应该用3天就可以出师了。
我的这篇文章就是冲这个目的来的。在接下来远远少于3天的篇幅中,我将带领读者从菜鸟一直晋升到COM(因为我自己也只会这么多了),初学者读完后,应当小有所成,基本上能胜任通常各种小IT公司的常规开发任务了。如果你不能学成,please email me: oyd_admin@163.com。
准备工作:
首先我想明确一点:编程与语言有关系吗?我的答案是:有!语言是思维的载体。如果你不能认同这一点,那么就不用往下看了,我们的思维方式不同,沟通起来肯定有困难。
因为与语言有关,所以我选定了一种语言--C++。没有兴趣学C++的或者还无法写出C语言版的Hello World的读者也不需要往下读了,这篇文章不是为你们而写的。
OK,到此为止,接下来,我保证不会再淘汰任何读者了。
在开始做程序之前,我希望首先要养成一种代码风格,这是极为必要的。你程序写的烂,人家一眼也看不出来,可是如果你代码没风格,人家一眼就看出你是菜鸟了。对于代码风格的养成,可以借助我推荐的这个软件GC GreatCode,主页在http://perso.club-internet.fr/cbeaudet,现在已经是开源软件了。你可以借助这个软件,选定一种你喜欢的代码风格,随时美化,随时纠正自己,你很快就能适应。
其实开发平台应该是与语言没有太大关系的,不过就我的经验,只能讲述在windows下做程序,至于linux/unix下面,有其特殊性,但是大部分原理都是相通的。
为了入门快速,强烈建议,不要用记事本写程序,你所用的编辑器一定要带语法高亮功能与自动完成功能,程序都是调出来的(比尔盖茨那样的天才除外),所以你也要选择一款调试器。忘了说,编译器也是必须要的。当你成为高手时,你会有需要自己选择这些工具,但是作为入门,我推荐你最省事的工具:VC6.0+Visual Assist 10。VC请务必安装时选中把CRT源代码也装上,这样你以后可以从里面抄很多东西。(有读者问我VC6不标准,为何我还要推荐它,其实真实的原因是为了用上Visual Assist,有了Visual Assist,你写代码的速度会上一个档次,而且极大减轻你的记忆负担,VS2003/2005尽管也能用Visual Assist,不过我嫌它们运行起来速度慢)
做完这些准备后,我们可以开始上路了。
抱歉说一句,我还想淘汰一种类型的读者:女性。我不是歧视,我曾经尝试过教我女朋友C++可是败的很惨,因为心理阴影,所以我没有把握。
另外,不要对本文抱太大希望。想成为编程高手是无法速成的,需要大量的经验积累、理论基础及悟性。可是许多时候你觉得困惑,并非遥不可及,而是有些简单的东西你还不知道而已。本文就是把这些简单的、但是你还不知道的东西告诉你,以达到速成的目的。
一、标准库
学习使用标准库
我记得高中时刚学Basic语言的时候,那时候只会一些简单的流程控制,连数组也不大会用,最高成就也就只编出了一个字符弹球的Demo。现在回想起来,能不能使用数组,可以看作会不会写程序的一个分水岭。为什么这么说呢?我大学时有一个同学,很勤奋好学也有一定思维能力,一次C语言作业要求编程序排序3个数并输出,我作业基本上都是抄的,不过那次抄他的作业差点没把我恶心坏。他的排序方法是把3个数的6种大小情况用if/else全给列举出来,写的还很严谨,一种情况也没漏掉。我当时就想要是30个数排序,全世界生产的作业纸也不够你写的呀,于是自己用数组写了个排序程序(后来知道那是冒泡排序)。像那位同学这样的思维方式,不是在写程序。
数组可以算最简单的一种数据结构,数组之外,还有许多复杂的数据结构。如果不利用一些数据结构,很多程序根本就写不出来,甚至想都没法想。就像是不用数组而要做排序一样。这应该算是许多初学者遇到问题无从下手的原因之一:因为问题的复杂度超出了他能想象的范围。那么,我们需要熟记各种数据结构再来编程吗?不,你可以速成的。只要使用STL标准库,你能写出的程序级别马上能上好几个台阶。
标准库速成
标准库对我们的重大意义不在于它的iostream,cout, <<, >>等等花哨的东西。对于一般的文件读写、控制台输出等任务,我至今没看出iostream与传统的printf等相比有何优势。许多教科书上来就教iostream,结果只是让学生换了一种方式来做控制台输出,没得到什么好处就先消磨了大部分耐心。
我希望大家先学习用string、vector、list、map这四个最常用的类,它们的成员函数的详细使用请参看相关文档,我这里只是做一个入门性的介绍。
string
可以被看作是内置的字符串一样,请在通常程序中放弃掉用char[1024]之类的方式来存储字符串的写法。也请停止用strcpy、strcat来组织你的程序。只要一有机会,就不要用它们。下面是替代用法:
// old
char str[1024] = { 0 };
strcpy(str, "......");
strcat(str, "more");
// new
string str = "......";
str += "more";
string相比C风格的用法来说,除了简洁,还安全的多,旧的方式下总要担心所拷贝的内容超过字符数组的长度的情况。
string还提供了一系列的成员函数可以利用,没什么太多好说的。值得一提的是string的下标运算[],如果没有十分必要和把握,请尽量避免使用。如果要修改,请用其replace函数,要截断,请用erase。要获取其C风格的字符串,请用c_str().
vector
比string要重要的多,string即使不用,许多场合下都有替代物,如MFC中的CString,即使是许多初学者也早就耳熟能详了。vector中文译作矢量,不过最好我们把它称为动态数组有助于理解。
我有一个有5年以上编程经验的同事,他的代码中不用vector。我经常能在他的代码中看到类似这样的语句:
SOMETYPE g_something[10];//最大暂时支持10个
在有一次莫名其妙的Bug中,经过很长时间的Debug,发现是他的代码中是这样写的:
BYTE g_cert[1024];
int g_certlength;
这是他假设的一个数字证书的最大长度,刚巧测试用证书长度不到1024,正式证书超过了1024。于是他把1024改成了2048,Bug就解决了。
我们提醒他2048也许还不够用呢?于是他又做了改动:
BYTE* g_cert;
//使用前
g_cert = (BYTE*)malloc(nCertLen);
g_certlength = nCertLen;
//使用后
free(g_cert);
g_cert = NULL;
g_certlength = 0;
他在他经手过的地方制造了成百上千处类似这样的代码。如果我是他上司,一定只安排他写文档。他这样的代码写得越多造成的损失越大。
让我们来看看vector是如何来简化操作的:
vector<BYTE> vBuf(nCertLen);
OK,现在可以使用这片缓冲区了。
大家注意到,C语言中的数组的声明是不可以用变量的,例如你不能像这样来声明一个数组:
BYTE buf[nLength];
于是当所需要的空间不固定时,需要动态分配空间。动态分配使用起来不方便,还很容易忘记释放。有时候为了释放这些空间,会把代码弄的要多难看有多难看:
bool foo()
{
byte* b1 = new byte[n1];
if (!do_something(b1))
{
delete[] b1;
return false;
}
byte* b2 = new byte[n2];
if (!do_something(b2))
{
delete[] b2;
delete[] b1;
return false;
}
// ...
delete[] b1;
delete[] b2;
// ...
return true;
}
可以看到,本来是非常简单的代码,为了要处理内存的释放,结果加进了许多冗余的代码。
请大家记住vector的用法,它用起来相当于可以用变量来声明的数组,你不再需要为释放内存而发愁。
vector除了初始化时可以指定大小以外,你还可以在使用的过程中增加大小,见下面的示例:
vector<BYTE> vBuf; // 不指定初始大小,这时候大小为0
vBuf.resize(n1); // 将大小增加为n1
// do something
vBuf.resize(vBuf.size() + n2); // 再增加n2的空间
// do something
通常,你完全可以用vector代替传统数组,除非是对性能要求极为苛刻的场合。有读者会问,许多函数需要BYTE*类型的参数,不用new动态分配怎么做呢?这个问题我也遇到过,事实上在我开始用vector之后的相当长一段时间内,我遇到BYTE*类型的参数时我还是用new BYTE[n]来做。直到有一天我实在无法忍受满世界的找指针delete时,我自己写了一个类,在析构函数中自动delete掉分配的空间。又过了不久,我开始意识到自己的愚蠢了:有现成的东西不用,竟然还在重新发明*。现在我举一个简单的例子来说明vector的这个应用,打开一个文件,读取文件中的内容到内存中,如下面的代码所示:
CFile file;
BOOL bRet = file.Open("c://test.bin", CFile::modeRead);
if (bRet)
{
vector<BYTE> vBuf(file.GetLength());
file.Read(&vBuf[0], vBuf.size());
// 现在文件内容已经读入内存中,可以使用了
}
还有其他类似例子,读者可以自己举一反三,但是请相信,没有什么是数组可以做,而vector不能做或者做的不如数组好的。如果你找到反例,欢迎和我一起探讨。
vector还有一类很常见的用法,存储复杂对象的指针。对于复杂对象,将其直接存入vector会引起许多麻烦,比较明智的做法是存指针。我还举上面的例子,读入的文件的内容是存储了一组对象(假设为CDemo),现在需要从文件中将这一组对象还原出来:
class CDemo
{
public:
int Load(BYTE* pBuf, int nSize);// 该函数从内存中装载自己,返回其实际读取的字节数
private:
//...
}
void LoadDemosFromBuffer(BYTE* pBuf, int nSize)
{
// 为了简明起见,这里略去了错误处理
vector<CDemo*> vDemo;
int nStart = 0;
do {
CDemo* pDemo = new CDemo;
nStart += pDemo.Load(pBuf + nStart, nSize - nStart);
vDemo.push_back(pDemo); // push_back用于将元素附加到vector的末尾,vector的大小会自动增长
} while ( nStart < nSize)
// 现在CDemo对象已经加载完毕,开始使用
// do something
...
// 下面是用完后释放
for (int i = 0; i < vDemo.size(); i++)
{
delete vDemo[i];
}
vDemo.clear();
}
上面的例子仅是为了示范vector的用法,在实际的程序中,LoadDemosFromBuffer很可能是某个类的成员函数,vDemo是这个类的成员,于是,释放操作可以在这个类的析构函数中去做。
list
从list开始,我要介绍iterator的概念了。iterator对于陌生的读者来说,好像很不好理解的样子。其实iterator的目的只是要对所有的STL中的容器(string、vector、list、map等等)提供统一的访问和遍历接口罢了。简单的说,当你学会了用iterator,你就一招鲜吃遍天了。前面介绍的string和vector也都支持iterator的访问方式,只是它们同时还支持下标访问。对于list来说,它不支持下标操作,只能用iterator,list相比vector,可以说list能做的vector都能做,只是某些操作(如插入、删除)list比vector的执行效率高。一般情况下,我都是根据兴趣即兴选择是用list还是vector,因为如果你仅用iterator接口的话,list和vector使用上没有什么区别。
现在我们来通过一段标准的遍历list的操作熟悉一下iterator:
void foo(list<CDemo*>& ls)
{
typedef list<CDemo*>::iterator Iter;
for (Iter i = ls.begin(); i != ls.end(); i++)
{
CDemo* pDemo = *i;
// use pDemo
}
}
从上面的例子来看,iterator从用法上就好像是指向容器元素的指针,在本例中,即CDemo*的指针,所以访问时用*i。
iterator的声明无一例外是使用如下的方式:
string::iterator iter_string;
vector<CDemo*>::iterator iter_vector;
map<string, int>::iterator iter_map;
......
由于这种声明很长,我们经常会用一个typedef来换一个短点的名称来简化书写,就像在foo中做的那样。所有的STL的容器也都提供begin()、end()这样的方法,你可以用类似foo中那样的循环语句来遍历任何一个STL容器。
<未完待续>