C++ Primer读书笔记1(经典收藏)
来源:互联网 发布:永恒之塔淘宝双倍礼包 编辑:程序博客网 时间:2024/06/04 18:58
C++ Primer读书笔记
注:本文转自www.Eachfun.com
(整理说明:本资料是我在网上无意间找到的,读起来感觉不错,但由于原文是每章一个网页的格式,读起来不是很习惯,而且也不方便保存,所以我花了2个多小时的时间将所有网页的内容综合整理了一下,但最后才发现,文章的顺序颠倒了,所以各位如果愿意阅读本文的话,请从后面向前读,每个红色“标题”代表一章,如有不便还请各位见谅,或到原文网站阅览)
标题::类型转换之隐式转换
对于我们来说,3+1.5=4.5。但是对于计算机来说,这两个数的相加可不这么简单。因为3与3.0是不同的数据类型,3.0与1.5是可以相加的,3却不能与1.5相加。于是,C++在对上面的表达式进行处理时,有必要对其中一个(或两者)进行转换。
因为这个转换是“隐式”的,也就是说这个转换不让程序员知道,那么,系统就不能必须保证不产生损失,这个损失指的是精度。为了不损失精度,数据总是向精度高的类型转换。惟一的例外是当某个变量用作条件时,它被转换为bool型。
对算术类型的转换是这样的:所有比int小的都转为int或unsigned int,即使没有必要也这么转。原因很简单,因为int的长度正好等于字长。对CPU来说,一次处理一个字是最快的。如果int或unsigned int无法达到要求,则往long、double转化。
如果每一个转换都能不造成损失,那自然是好事。可是世间的事总有不随人愿的时候。对于同一种算术类型,其signed和unsigned所能表达的范围是一样大,但却是互不重叠的两个范围。就像“妇联”和“工会”一样,往哪边转换都可能会产生损失。这是无法解决的问题,所以,在VC中,试图比较int和unsigned int变量时会显示警告。
奇怪的是,据我测试,在VC++.net中,只有进行“<”和“>”比较的时候才会显示警告,而进行“==”和“!=”比较的时候却不显示警告。实际上这两个比较也会有同样的问题产生,比如以下代码:
int a = -3;
unsigned b = 4294967293;
if (a == b) cout << "yes" << endl;
测试运行以上代码会发现表达式a==b的值为true。这是从int转为unsigned int过程中的副作用,这个副作用我们应该知道,但是VC++.net不进行任何警告似乎也有些与理不通。——难道我关闭了某些警告?
其它隐式转换还包括数组名转换为指针、算术值用作条件时转换为bool,枚举被转换为整数,非const对象转换为const对象等。其中枚举转换为整数没什么要提的,枚举值本来就是整数的别名,非const对象转为const对象只是临时声明它的保护级别,通常用于作为参数传递时。
因为这个转换是“隐式”的,也就是说这个转换不让程序员知道,那么,系统就不能必须保证不产生损失,这个损失指的是精度。为了不损失精度,数据总是向精度高的类型转换。惟一的例外是当某个变量用作条件时,它被转换为bool型。
对算术类型的转换是这样的:所有比int小的都转为int或unsigned int,即使没有必要也这么转。原因很简单,因为int的长度正好等于字长。对CPU来说,一次处理一个字是最快的。如果int或unsigned int无法达到要求,则往long、double转化。
如果每一个转换都能不造成损失,那自然是好事。可是世间的事总有不随人愿的时候。对于同一种算术类型,其signed和unsigned所能表达的范围是一样大,但却是互不重叠的两个范围。就像“妇联”和“工会”一样,往哪边转换都可能会产生损失。这是无法解决的问题,所以,在VC中,试图比较int和unsigned int变量时会显示警告。
奇怪的是,据我测试,在VC++.net中,只有进行“<”和“>”比较的时候才会显示警告,而进行“==”和“!=”比较的时候却不显示警告。实际上这两个比较也会有同样的问题产生,比如以下代码:
int a = -3;
unsigned b = 4294967293;
if (a == b) cout << "yes" << endl;
测试运行以上代码会发现表达式a==b的值为true。这是从int转为unsigned int过程中的副作用,这个副作用我们应该知道,但是VC++.net不进行任何警告似乎也有些与理不通。——难道我关闭了某些警告?
其它隐式转换还包括数组名转换为指针、算术值用作条件时转换为bool,枚举被转换为整数,非const对象转换为const对象等。其中枚举转换为整数没什么要提的,枚举值本来就是整数的别名,非const对象转为const对象只是临时声明它的保护级别,通常用于作为参数传递时。
标题::内存管理之new和delete
林锐博士曾将内存管理比喻为“雷区”(《高质量C++/C编程指南》第44页),内存管理这块难不难?恐怕不好说。“会者不难难者不会”嘛。但是说内存管理这块难以成为“会者”,应该是没有错的。
程序时时刻刻与内存打交道,只不过以往我们不用考虑,甚至不用知道。所以,所谓“内存管理”,是特指堆内存。
如果把堆内存和栈内存的使用放在一起考虑,可以降低对内存管理恐惧。
一、内存的分配:
int i(100);//栈上分配内存
int *pi = new int(100);//堆上分配内存
以上两种分配,都使用了100作为初始值进行初始化,如果是进行类对象的分配,它们还可以指定使用哪个构造函数,比如:
CString s(s1);//栈上分配内存
CString *ps = new CString(s1)//堆上分配内存
这里的s可以是char*指针,也可以是另一个CString对象,它的类型将决定上面这两行语句调用哪一个构造函数。
在这里,有一点要特别说明,如果要使用默认构造函数,则new语句后面可以用空括号对,而栈内分配的语句切不可用空括号对。如果写成“CString s();”,则并不是定义一个CString对象s,而是定义一个返回值为CString的函数s。
上面两种分配,也都可以分配对象数组,不同的是,用new操作符在堆内存分配数组时,只能调用默认构造函数。而在栈上分配却可以指定对象成员的初始值。如:
int a[3] = {1,2,3};//栈上分配内存,int可以换成其它类型名,后面的初始值可作相应调整。
int *p = new int[3];//不能指定这三个对象的初始值
二、内存的访问:
栈内存可以通过对象访问,也可以通过指针访问,堆内存通过指针访问。方法完全相同。
三、内存的释放:
栈内存在对象作用域结束后自动释放,堆内存要用delete。
delete pi;//释放内存
delete []p;//释放对象数组
对于释放对象数组,那个空的[]对不可以丢,否则将只释放数组的第一个元素。导致内存泄露。
有了以上对比,堆内存似乎没有了任何难度。那么内存管理的玄机究竟在哪儿呢?在进行内存分配与释放的时候,有几个注意点要记住:
1、new操作有可能失败,当系统无法分配需要的内存块时,将返回NULL值,所以在new操作之后,要立即判断pi的值是否为NULL。
int *pi = new int(100);
if (pi = NULL) {...}
2、堆上分配的内存必须delete,而且只可以delete一次。为了保证内存只被delete一次,请务必记住delete以后立即将指针设为NULL:
delete pi;
pi = NULL;
虽然“pi=NULL;”不是必须的,但这是个好习惯。将指针设为NULL既可以防止继续读写该内存,也可以防止再次释放该内存。
老的C程序员可能忘不了malloc和free函数,它们也可以进行内存的分配与释放。但是C++时代它们已经落伍了。它们只是按请求的字节数进行分配,而不管你用这块内存来干什么。这样做,就等于放弃了类对象的构造与析构。对于很多类来说,这样做是很危险的。
程序时时刻刻与内存打交道,只不过以往我们不用考虑,甚至不用知道。所以,所谓“内存管理”,是特指堆内存。
如果把堆内存和栈内存的使用放在一起考虑,可以降低对内存管理恐惧。
一、内存的分配:
int i(100);//栈上分配内存
int *pi = new int(100);//堆上分配内存
以上两种分配,都使用了100作为初始值进行初始化,如果是进行类对象的分配,它们还可以指定使用哪个构造函数,比如:
CString s(s1);//栈上分配内存
CString *ps = new CString(s1)//堆上分配内存
这里的s可以是char*指针,也可以是另一个CString对象,它的类型将决定上面这两行语句调用哪一个构造函数。
在这里,有一点要特别说明,如果要使用默认构造函数,则new语句后面可以用空括号对,而栈内分配的语句切不可用空括号对。如果写成“CString s();”,则并不是定义一个CString对象s,而是定义一个返回值为CString的函数s。
上面两种分配,也都可以分配对象数组,不同的是,用new操作符在堆内存分配数组时,只能调用默认构造函数。而在栈上分配却可以指定对象成员的初始值。如:
int a[3] = {1,2,3};//栈上分配内存,int可以换成其它类型名,后面的初始值可作相应调整。
int *p = new int[3];//不能指定这三个对象的初始值
二、内存的访问:
栈内存可以通过对象访问,也可以通过指针访问,堆内存通过指针访问。方法完全相同。
三、内存的释放:
栈内存在对象作用域结束后自动释放,堆内存要用delete。
delete pi;//释放内存
delete []p;//释放对象数组
对于释放对象数组,那个空的[]对不可以丢,否则将只释放数组的第一个元素。导致内存泄露。
有了以上对比,堆内存似乎没有了任何难度。那么内存管理的玄机究竟在哪儿呢?在进行内存分配与释放的时候,有几个注意点要记住:
1、new操作有可能失败,当系统无法分配需要的内存块时,将返回NULL值,所以在new操作之后,要立即判断pi的值是否为NULL。
int *pi = new int(100);
if (pi = NULL) {...}
2、堆上分配的内存必须delete,而且只可以delete一次。为了保证内存只被delete一次,请务必记住delete以后立即将指针设为NULL:
delete pi;
pi = NULL;
虽然“pi=NULL;”不是必须的,但这是个好习惯。将指针设为NULL既可以防止继续读写该内存,也可以防止再次释放该内存。
老的C程序员可能忘不了malloc和free函数,它们也可以进行内存的分配与释放。但是C++时代它们已经落伍了。它们只是按请求的字节数进行分配,而不管你用这块内存来干什么。这样做,就等于放弃了类对象的构造与析构。对于很多类来说,这样做是很危险的。
标题::优先级、结合性和求值顺序
说到优先级,我能熟练背出“先乘除,后加减”,之于C++列出的整整19个优先级,每个优先级又包含若干个操作符,我总是看了就头皮发麻。以我的记性,连军旗里哪个大哪个小都背不出来,这几十个操作符——还是饶了我吧。
记住林锐博士的话:“如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。”(《高质量C++/C编程指南》第26页)这样做最直接的作用是不用记忆了复杂的优先级了,不用记忆并不是因为懒,而是为了更清晰。毕竟程序不只是编给计算机运行的,当我们处在一个多人协作的团体中时,程序的清晰度和精确性比性能要高得多。再说,多加几对括号是不影响运行效率的。
结合性和求值顺序是容易混淆的两个概念。每一个操作符都规定了结合性,但是只有极少数操作符规定求值顺序。结合性是说如果有多个同级别的操作符,这些操作数该如何分组。比如“1+2+3”究竟分成“(1+2)+3”还是“1+(2+3)”,虽然这两种分组最终没有区别,但不等于所有操作符都不产生区别。即使不产生区别,计算机毕竟是计算机,它只能按死的规范做事,于其给它灵活机制,还不如规定了结合性让它遵守。
C++只有四个操作符规定了求值顺序,它们是“&&”、“||”、“?:”和“,”,记住这四个操作符并不难。反过来记住其它操作符也不难,难的是在写程序中是否有这个意识。那么多网友讨论“j = i++ + i++ + i++;”的结果,正说明了还有好多人不了解“未定义”的威力。如果不小心使用了依赖于未定义求值程序的语句,将是一个不容易发现并改正的问题。比如“if (a[index++] < a[index]);”
记住林锐博士的话:“如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。”(《高质量C++/C编程指南》第26页)这样做最直接的作用是不用记忆了复杂的优先级了,不用记忆并不是因为懒,而是为了更清晰。毕竟程序不只是编给计算机运行的,当我们处在一个多人协作的团体中时,程序的清晰度和精确性比性能要高得多。再说,多加几对括号是不影响运行效率的。
结合性和求值顺序是容易混淆的两个概念。每一个操作符都规定了结合性,但是只有极少数操作符规定求值顺序。结合性是说如果有多个同级别的操作符,这些操作数该如何分组。比如“1+2+3”究竟分成“(1+2)+3”还是“1+(2+3)”,虽然这两种分组最终没有区别,但不等于所有操作符都不产生区别。即使不产生区别,计算机毕竟是计算机,它只能按死的规范做事,于其给它灵活机制,还不如规定了结合性让它遵守。
C++只有四个操作符规定了求值顺序,它们是“&&”、“||”、“?:”和“,”,记住这四个操作符并不难。反过来记住其它操作符也不难,难的是在写程序中是否有这个意识。那么多网友讨论“j = i++ + i++ + i++;”的结果,正说明了还有好多人不了解“未定义”的威力。如果不小心使用了依赖于未定义求值程序的语句,将是一个不容易发现并改正的问题。比如“if (a[index++] < a[index]);”
标题::sizeof和逗号操作符
把sizeof说成操作符可能有些不合习惯,因为sizeof的用法与函数没区别。但是sizeof与函数有着本质的区别:它是编译时常量。也就是说,在程序编译时,就会求出它的值,并且成为程序中的常量。
sizeof本身比较简单,惟一要提的就是它对数组名和指针进行操作的结果。
int a[10];
sizeof(a);
该操作返回的是数组所有元素在内存中的总长度。但是如果对指针进行操作,返回的则是指针本身的长度,与指针所指类型无关。
正因为数组名与指针有着千丝万缕的关系,所以有时候这个特性会让人摸不着头脑:
int function(int a[10])
{
sizeof(a);
...
}
以上sizeof返回的不是数组所有成员的大小,而是指针的大小,因为数组在参数传递中弱化为指针。
逗号操作符除了在for语句中应用以外,我没发现在哪儿还有用处。因为在一般情况下,逗号改成分号肯定是可以的,在for语句中因为分号的作用另有定义,所以不能随便改。这才有了逗号的用武之地。
sizeof本身比较简单,惟一要提的就是它对数组名和指针进行操作的结果。
int a[10];
sizeof(a);
该操作返回的是数组所有元素在内存中的总长度。但是如果对指针进行操作,返回的则是指针本身的长度,与指针所指类型无关。
正因为数组名与指针有着千丝万缕的关系,所以有时候这个特性会让人摸不着头脑:
int function(int a[10])
{
sizeof(a);
...
}
以上sizeof返回的不是数组所有成员的大小,而是指针的大小,因为数组在参数传递中弱化为指针。
逗号操作符除了在for语句中应用以外,我没发现在哪儿还有用处。因为在一般情况下,逗号改成分号肯定是可以的,在for语句中因为分号的作用另有定义,所以不能随便改。这才有了逗号的用武之地。
标题::条件操作符
我觉得条件操作符的存在就是为了简化if-else语句。第一,它与if-else语句的功能完全一致;第二,它虽然是一行语句,但是它规定了求解顺序,这个顺序保证了有些表达式不被求值。
条件操作符是有一定的危险性的,危险的原因在于它的优先级特别底,还容易漏掉括号。它的优先级仅仅高于赋值和逗号运算符,也就是说,只有在与赋值或逗号共存时,才可以免去括号,其它情况下都得加上括号。漏加括号的BUG是很难发现的。
比如“cout << (i < j) ? i : j;”这句的实际作用是将表达式“(i<j)”的值输出,然后测试一下cout的状态(<<操作符的返回值是cout),整个表达式的值不管是i还是j,都被丢弃。
条件操作符是有一定的危险性的,危险的原因在于它的优先级特别底,还容易漏掉括号。它的优先级仅仅高于赋值和逗号运算符,也就是说,只有在与赋值或逗号共存时,才可以免去括号,其它情况下都得加上括号。漏加括号的BUG是很难发现的。
比如“cout << (i < j) ? i : j;”这句的实际作用是将表达式“(i<j)”的值输出,然后测试一下cout的状态(<<操作符的返回值是cout),整个表达式的值不管是i还是j,都被丢弃。
标题::箭头操作符(->)
箭头操作符是C++发明的全新操作符,但却不是C++才用到的功能。早期的C语言虽然没有类,却有结构体,也允许有指向结构体对象的指针。不同的只是没有发明“->”这个符号来进行简化操作。说到底,“->”的出现只是代替原来就可以实现的功能。
引用:C++语言为包含点操作符和解引用操作符的表达式提供了一个同义词:箭头操作符(->)。
笔记:这一同义词的出现,不仅仅使程序简化而且更易于理解,更重要的是,它降低了出错的可能性。出什么错呢?这就跟操作符的优先级有关了:
p->a();
(*p).a();
以上两行等价,但是第二行却很容易写成“*p.a();”,由于点操作符的优先级高,就成了“*(p.a());”,这里至少包含了两个错误:一是p不是对象,点操作无效;二是试图对类成员解引用(只有当该成员返回指针才有效)。
也许有人要说了,第一个错误已经导致了编译不通过,还要说第二个错误干什么?这样理解就错了。VC++为程序员提供了一个十分强大的库,其中有些类的对象,既可以进行点操作也可以进行解引用操作的,如果上例中的p是那种类的对象,而且p.a()刚好又返回指针,那么上面这句将可以通过编译,最终换来难以查找的BUG。
记住,尽量多用箭头操作符。
引用:C++语言为包含点操作符和解引用操作符的表达式提供了一个同义词:箭头操作符(->)。
笔记:这一同义词的出现,不仅仅使程序简化而且更易于理解,更重要的是,它降低了出错的可能性。出什么错呢?这就跟操作符的优先级有关了:
p->a();
(*p).a();
以上两行等价,但是第二行却很容易写成“*p.a();”,由于点操作符的优先级高,就成了“*(p.a());”,这里至少包含了两个错误:一是p不是对象,点操作无效;二是试图对类成员解引用(只有当该成员返回指针才有效)。
也许有人要说了,第一个错误已经导致了编译不通过,还要说第二个错误干什么?这样理解就错了。VC++为程序员提供了一个十分强大的库,其中有些类的对象,既可以进行点操作也可以进行解引用操作的,如果上例中的p是那种类的对象,而且p.a()刚好又返回指针,那么上面这句将可以通过编译,最终换来难以查找的BUG。
记住,尽量多用箭头操作符。
标题::++的陷阱
自增和自减符作符是如此常用,以至于没有必要提了。但是任何一本书都会下重手来提它,原因是它虽然简单,却含有玄机。
讲到前自增和后自增时,几乎所有的书都是这样讲的:用“j = i++”和“j = ++i”对比,告诉读者虽然i都增了1,但是j却不一样。
这也没办法,因为绝大多数书在讲到++时还没有提到“表达式的值”这个概念,有些书本可能从头到尾都不提。对于“j = i++;”来说,它是一个表达式没错,但是等号右侧也是一个表达式,对于表达式“i++”来说,它是有值的,该表达式的值赋予了j,而整个表达式也有一个值,只是这个值被丢弃了。类推:“i = j = k = 1;”语句中单独的“1”就是一个表达式,叫常量表达式,它的值就是1,它给了k,“k=1”这个表达式也有值,它的值就是k的值,它给了j,……最后给i赋值的表达式也有值,它的值就是i的值,这个值被丢弃。
弄明白了“表达式的值”后,就可以科学地讲解“++i”和“i++”了,因为这两个表达式的值是这样定义的:前置自增/减表达式的值是修改后的变量值,后置则为修改前的值。
那么,在不关心表达式的值的时候——即只是给某变量自增或自减一下——究竟用哪个好呢?当然是前置好,因为前置只要用一个指令进行一下自增自减运算,然后直接返回即可。而后置却要先保存好这个初始值,再自增减,然后再返回以前保存的值。写过操作符重载的程序员应该更能体会这里面的区别。
“++i”或“i++”是这样简洁,所以它们比“i = i + 1;”要美得多,所以书上说“简洁就是美”。但是,在美的同时,一个美丽的陷阱正在招手,程序员们必须理解并小心。
“if (ia[index++] < ia[index])”这样的表达式是危险的。前文中已经提到了,C++中只有极少的几个操作符是规定了求值顺序的,“<”号没有规定,那么,这两个数要比较大小,系统究竟先求前者还是后者?如果先求前者,那么求后者的时候index是值有没有增一?
经常在论坛上看到有人讨论“j = i++ + i++ + i++;”的结果,还有人把自己的实践结果贴出来证实自己的理论。这正是初学者令人悲哀与同情的地方:对于应该好好掌握的知识没有足够的热情,却把一腔热血放在这些无须讨论的话题上,C++初学者要走的路不仅长,而且充满了荆棘。
讲到前自增和后自增时,几乎所有的书都是这样讲的:用“j = i++”和“j = ++i”对比,告诉读者虽然i都增了1,但是j却不一样。
这也没办法,因为绝大多数书在讲到++时还没有提到“表达式的值”这个概念,有些书本可能从头到尾都不提。对于“j = i++;”来说,它是一个表达式没错,但是等号右侧也是一个表达式,对于表达式“i++”来说,它是有值的,该表达式的值赋予了j,而整个表达式也有一个值,只是这个值被丢弃了。类推:“i = j = k = 1;”语句中单独的“1”就是一个表达式,叫常量表达式,它的值就是1,它给了k,“k=1”这个表达式也有值,它的值就是k的值,它给了j,……最后给i赋值的表达式也有值,它的值就是i的值,这个值被丢弃。
弄明白了“表达式的值”后,就可以科学地讲解“++i”和“i++”了,因为这两个表达式的值是这样定义的:前置自增/减表达式的值是修改后的变量值,后置则为修改前的值。
那么,在不关心表达式的值的时候——即只是给某变量自增或自减一下——究竟用哪个好呢?当然是前置好,因为前置只要用一个指令进行一下自增自减运算,然后直接返回即可。而后置却要先保存好这个初始值,再自增减,然后再返回以前保存的值。写过操作符重载的程序员应该更能体会这里面的区别。
“++i”或“i++”是这样简洁,所以它们比“i = i + 1;”要美得多,所以书上说“简洁就是美”。但是,在美的同时,一个美丽的陷阱正在招手,程序员们必须理解并小心。
“if (ia[index++] < ia[index])”这样的表达式是危险的。前文中已经提到了,C++中只有极少的几个操作符是规定了求值顺序的,“<”号没有规定,那么,这两个数要比较大小,系统究竟先求前者还是后者?如果先求前者,那么求后者的时候index是值有没有增一?
经常在论坛上看到有人讨论“j = i++ + i++ + i++;”的结果,还有人把自己的实践结果贴出来证实自己的理论。这正是初学者令人悲哀与同情的地方:对于应该好好掌握的知识没有足够的热情,却把一腔热血放在这些无须讨论的话题上,C++初学者要走的路不仅长,而且充满了荆棘。
标题::回忆十几年前的编程入门
本来不想为这段写读书笔记,不过突然想起十几年前的一件趣事来,还是记下来吧。
1993年的时候,学校开设了“劳技”课,讲的是BASIC语言。对于当时连电脑都没看过一眼的我们来说,学校开设这样的课,真是让我们无比感动。我至今仍然感谢我的母校,在片面追求升学率、大量缩减副课的全局下,我的母校居然开设了音乐、美术、劳技等一系列副之又副的课。这让我至今难忘。而令一方面,我今天能够在程序界打拼,完全是从那时候开始陪养的兴趣。如果我的高中没有开设这门课,我未必就不进入程序界,但是入门至少要晚三年。
这三年,我学的编程东西少之又少,对于整个BASIC来说,简直连皮毛都不如,而且学校只提供了一次上机机会,练的是开机与关机。我所谓的“调试程序”是在自己的小霸王学习机上进行的。但是,在电脑没有普及的年份,懂得一点点就是很先进的了。进入大学后,堂堂一个大学的班级,居然只有两个人碰过电脑,大家的基础可想而知。在同学们拼命学习DOS命令的时候,我已经遥遥地走在了前面。之后学习True Basic自然一日千里,之后自学VB、自学C也就顺理成章。如果我的高中没有开设这门课,我未必就不进入程序界,但是我在进大学的头一段时间肯定会与其他同学一起拼命记DOS命令。要知道,其他同学至所以学得比我慢,并不是比我笨,而是没有习惯电脑的思维模式。
该说“赋值操作符”了。高中开设的BASIC语言课,其课本是不到半厘米厚的小书。但是我爱不释手地提前阅读了。读得一知半解就去做后面的习题,发现一句“P=P+1”,心想:这怎么可能嘛?P怎么可能等于P加一呢?移项一减,不就相当于0=1了吗?一定是书上印错了。于是,我将其中一个P改成了R,而且是用黑钢笔描的。我描得是如此细致,以至于根本看不出那个R是P改的。等到老师讲到这一节的时候,我虽然已经懂了这里的“=”与数学上的“=”不一样,但是由于把P看成了R,这题还是做错了。
当时的情况是这样的:老师喊了几个同学上去做题,没有一个会。老师说“有没有谁会做的?主动上来?”好几个同学立即喊我的名字,把我逼上去了。结果我这么一做就做错了。老师表扬了我的勇气,但是同学却说我虽然平时捧着这本书不放,原来也是个“菜鸟”。
赋值操作符就是在那个时候给我留下深刻印像的,那天我知道了,“=”号不表示左右相等。
复合赋值操作符也比较简单,理解了它的用法就好了。C++至所以有了赋值操作符以后还要复合赋值操作符,不仅仅是为了简化代码,还可以加快处理速度。“i=i+j”和“i+=j”相比,前者的i求值了两次。不过,这点性能差别对整个程序性能来说不大。还有一个区别就是优先级方面的了。“i*=j+1”如果不写成复合赋值,就要加上括号:i=i*(j+1)。
1993年的时候,学校开设了“劳技”课,讲的是BASIC语言。对于当时连电脑都没看过一眼的我们来说,学校开设这样的课,真是让我们无比感动。我至今仍然感谢我的母校,在片面追求升学率、大量缩减副课的全局下,我的母校居然开设了音乐、美术、劳技等一系列副之又副的课。这让我至今难忘。而令一方面,我今天能够在程序界打拼,完全是从那时候开始陪养的兴趣。如果我的高中没有开设这门课,我未必就不进入程序界,但是入门至少要晚三年。
这三年,我学的编程东西少之又少,对于整个BASIC来说,简直连皮毛都不如,而且学校只提供了一次上机机会,练的是开机与关机。我所谓的“调试程序”是在自己的小霸王学习机上进行的。但是,在电脑没有普及的年份,懂得一点点就是很先进的了。进入大学后,堂堂一个大学的班级,居然只有两个人碰过电脑,大家的基础可想而知。在同学们拼命学习DOS命令的时候,我已经遥遥地走在了前面。之后学习True Basic自然一日千里,之后自学VB、自学C也就顺理成章。如果我的高中没有开设这门课,我未必就不进入程序界,但是我在进大学的头一段时间肯定会与其他同学一起拼命记DOS命令。要知道,其他同学至所以学得比我慢,并不是比我笨,而是没有习惯电脑的思维模式。
该说“赋值操作符”了。高中开设的BASIC语言课,其课本是不到半厘米厚的小书。但是我爱不释手地提前阅读了。读得一知半解就去做后面的习题,发现一句“P=P+1”,心想:这怎么可能嘛?P怎么可能等于P加一呢?移项一减,不就相当于0=1了吗?一定是书上印错了。于是,我将其中一个P改成了R,而且是用黑钢笔描的。我描得是如此细致,以至于根本看不出那个R是P改的。等到老师讲到这一节的时候,我虽然已经懂了这里的“=”与数学上的“=”不一样,但是由于把P看成了R,这题还是做错了。
当时的情况是这样的:老师喊了几个同学上去做题,没有一个会。老师说“有没有谁会做的?主动上来?”好几个同学立即喊我的名字,把我逼上去了。结果我这么一做就做错了。老师表扬了我的勇气,但是同学却说我虽然平时捧着这本书不放,原来也是个“菜鸟”。
赋值操作符就是在那个时候给我留下深刻印像的,那天我知道了,“=”号不表示左右相等。
复合赋值操作符也比较简单,理解了它的用法就好了。C++至所以有了赋值操作符以后还要复合赋值操作符,不仅仅是为了简化代码,还可以加快处理速度。“i=i+j”和“i+=j”相比,前者的i求值了两次。不过,这点性能差别对整个程序性能来说不大。还有一个区别就是优先级方面的了。“i*=j+1”如果不写成复合赋值,就要加上括号:i=i*(j+1)。
标题::关系、逻辑和位操作符
关系操作符本身没什么好提的,它们与我们平时身边的逻辑一样,所以不难理解。有两点可以略提一下:
一、因为ASC字符中没有“≥”这些符号,所以只好用“>=”代替。于是产生了BASIC和C++的两种不同符号集:BASIC用“<>”表示不等于,C++则用“!=”。
二、程序设计时不能用“if (i < j < k)”这样的写法。原因很简单,因为这种写法另有含义,或者说正因为不能这样写,才给这种写法另外赋了一种含义。
BASIC和C++的逻辑操作符也有完全不同的写法,BASIC用比较直观的关键字“And”、“Or”之类,C++则用“&&”和“||”。这也没什么,记住就行了。
逻辑操作符中的“&&”和“||”是C++标准中为数不多的指定了求值顺序的操作符(除此以外还有条件操作符“?:”和逗号操作符“,”,关于求值顺序,后面还将提及)。这样的规定惟一的缺点是需要额外的记忆,优点则是很明显的:它可以让“危险”的操作变得不危险。如“while (i<MaxSize && Array[i]>0)”,对于“Array[i]”来说,指针越界是很可怕的,但是“&&”操作符的求值顺序保证了指针不越界。从另一方面说,要保证指针不越界,就必须记住该操作符的求值顺序(不然就只能分成两个语句写喽)。
又要提到bool值的比较了,“if (i < j < k)”的实际就是进行了bool值的比较。本书在讲解的时候,虽然提到了“将k与整数0或1做比较”,但是我宁可提醒大家不去记住这个。记住bool值只有true和false两个值,比记住1和0要好得多,因为虽然false就是0,但是true却不仅是1。
C++中如果只有逻辑操作符也就算了,它偏偏还有“&”和“|”这样的位操作符。而位操作正是BASIC不提供的功能。这就给初学C/C++的人带来了难度。难点不在于理解,而在于记忆。位操作符的作用是进行某一Bit位的设定,在一个字节掰成八份用的年代,它非常常用,—比如Turbo C中的屏幕设置,就是3位表示背景色、4位表示前景色、一位表示闪烁,用一个字节完全存放了屏幕字体的信息。现在的海量存储与高速处理中,大可不必这么节约了(从处理速上看,非但没有节约,反而浪费了),所以,不会位操作也没什么大不了的。
不过,不熟悉位操作的用户却都知道“<<”和“>>”的另一用处。这事不能怪程序员,几乎所有的C++书本都会从“cin >> i;”和“cout << i;”入手。不用知道这两个操作符原来是干什么的,甚至不用知道“重载”是怎么回事。C++来到这个世界,发展了C语言,使得“知其然不知道其所以然”的程序员也能好好工作。也许这是它的一个进步吧。
一、因为ASC字符中没有“≥”这些符号,所以只好用“>=”代替。于是产生了BASIC和C++的两种不同符号集:BASIC用“<>”表示不等于,C++则用“!=”。
二、程序设计时不能用“if (i < j < k)”这样的写法。原因很简单,因为这种写法另有含义,或者说正因为不能这样写,才给这种写法另外赋了一种含义。
BASIC和C++的逻辑操作符也有完全不同的写法,BASIC用比较直观的关键字“And”、“Or”之类,C++则用“&&”和“||”。这也没什么,记住就行了。
逻辑操作符中的“&&”和“||”是C++标准中为数不多的指定了求值顺序的操作符(除此以外还有条件操作符“?:”和逗号操作符“,”,关于求值顺序,后面还将提及)。这样的规定惟一的缺点是需要额外的记忆,优点则是很明显的:它可以让“危险”的操作变得不危险。如“while (i<MaxSize && Array[i]>0)”,对于“Array[i]”来说,指针越界是很可怕的,但是“&&”操作符的求值顺序保证了指针不越界。从另一方面说,要保证指针不越界,就必须记住该操作符的求值顺序(不然就只能分成两个语句写喽)。
又要提到bool值的比较了,“if (i < j < k)”的实际就是进行了bool值的比较。本书在讲解的时候,虽然提到了“将k与整数0或1做比较”,但是我宁可提醒大家不去记住这个。记住bool值只有true和false两个值,比记住1和0要好得多,因为虽然false就是0,但是true却不仅是1。
C++中如果只有逻辑操作符也就算了,它偏偏还有“&”和“|”这样的位操作符。而位操作正是BASIC不提供的功能。这就给初学C/C++的人带来了难度。难点不在于理解,而在于记忆。位操作符的作用是进行某一Bit位的设定,在一个字节掰成八份用的年代,它非常常用,—比如Turbo C中的屏幕设置,就是3位表示背景色、4位表示前景色、一位表示闪烁,用一个字节完全存放了屏幕字体的信息。现在的海量存储与高速处理中,大可不必这么节约了(从处理速上看,非但没有节约,反而浪费了),所以,不会位操作也没什么大不了的。
不过,不熟悉位操作的用户却都知道“<<”和“>>”的另一用处。这事不能怪程序员,几乎所有的C++书本都会从“cin >> i;”和“cout << i;”入手。不用知道这两个操作符原来是干什么的,甚至不用知道“重载”是怎么回事。C++来到这个世界,发展了C语言,使得“知其然不知道其所以然”的程序员也能好好工作。也许这是它的一个进步吧。
标题::可爱的算术操作符
算术操作符是最容易理解的符号了,因这它与平时做的数学是完全相同的规则。就连小学的知识“先乘除后加减”都完全适用。
不过,就像因为与生活的逻辑完全一样导致易于理解一般,与生活逻辑不一致的问题就比较难以理解了。比如“有9个苹果,3个小朋友分,平均每个小朋友可以分到几个苹果?”,现实中既不可能是-9个苹果,也不可能给-3个小朋友分。而是C/C++中的除法和求余却不得不面对这种情况。它们非但难于理解,而且还有不确定因素。
引用:如果两个操作数都为正,除法和求模操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数是负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零)。
笔记:以上这一大堆似乎有些费口舌。这么说吧,如果两个数同号,则一切都那么简单。即使是两个负数相除,只要把它们当成两个正数就可以了——商肯定是正的,余数只要对应调整一下符号即可;如果是一正一负两个数相除,那么商肯定是负的,但是究竟是负多少可不一定,余数就更难说了,连是正是负都不能确定。
21 % 6 = 3;
21 % 7 = 0;
-21 % -8 = -5;
21 % -5 = ?;//与机器相关,可能是1可能是-4
21 / 6 = 3;
21 / 3 = 3;
-21 / -28 = 2;
21 / -5 = ?;//与机器相关,可能是-4或-5
引用:当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。
10个苹果分给-3个朋友,平均每个小朋友可以分到几个?还剩下几个?
不过,就像因为与生活的逻辑完全一样导致易于理解一般,与生活逻辑不一致的问题就比较难以理解了。比如“有9个苹果,3个小朋友分,平均每个小朋友可以分到几个苹果?”,现实中既不可能是-9个苹果,也不可能给-3个小朋友分。而是C/C++中的除法和求余却不得不面对这种情况。它们非但难于理解,而且还有不确定因素。
引用:如果两个操作数都为正,除法和求模操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数是负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零)。
笔记:以上这一大堆似乎有些费口舌。这么说吧,如果两个数同号,则一切都那么简单。即使是两个负数相除,只要把它们当成两个正数就可以了——商肯定是正的,余数只要对应调整一下符号即可;如果是一正一负两个数相除,那么商肯定是负的,但是究竟是负多少可不一定,余数就更难说了,连是正是负都不能确定。
21 % 6 = 3;
21 % 7 = 0;
-21 % -8 = -5;
21 % -5 = ?;//与机器相关,可能是1可能是-4
21 / 6 = 3;
21 / 3 = 3;
-21 / -28 = 2;
21 / -5 = ?;//与机器相关,可能是-4或-5
引用:当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。
10个苹果分给-3个朋友,平均每个小朋友可以分到几个?还剩下几个?
标题::操作符
第五章开始了,看得出来,从这章才真正开始讲解C++的基本内容。没有这里的内容,前四章都是狗屎。
操作符就是我们平时理解的“运算符”了,不过因为C++是计算机语言,它与我们平时生活有着不一样的逻辑,所以,在我们平时看来简单的“3+4”,到了C++里,就得分成一个操作符和两个操作数了。
操作符的含义以及它能得到的结果,不仅仅取决于操作符本身,还同时取决于操作数。当初学C语言的时候,发现做除法要用“10/3.0”而不用“10/3”着实惊讶了一回。特别是从BASIC走过来的人,BASIC里没有这么强的类型,所以10/3就是浮点数,要整除还得用“Int()”函数或改用“/”运算符(不是每个VB程序员都知道这个运算符的哦。)
从C/C++中弄明白了“10/3.0”和“10/3”,反过来再去理解C与BASIC的区别,不难发现C/C++这样做的确比BASIC高明得多。而进一步了解了硬件的运算机制后,则可以理解这样做不仅仅是高明,而且是必须。
引用:有些符号既可以表示一元操作也可以表示二元操作。例如*……,这种两用法相互独立、各不相关,如果将其视为两个不同的符号可能会更容易理解些。……需要根据该符号所处的上下文来确定它代表一元操作还是二元操作。
笔记:这是一个大家都明白,但是大家都不会去想的问题。细想起来,我们不去想,正是因为我们早已熟知。然而我们读程序可以上下关联,计算机要做到这点就不容易——比如拼音输入法的自动选词。由此看来,C++编译器是十分优秀的人工智能软件。
操作符就是我们平时理解的“运算符”了,不过因为C++是计算机语言,它与我们平时生活有着不一样的逻辑,所以,在我们平时看来简单的“3+4”,到了C++里,就得分成一个操作符和两个操作数了。
操作符的含义以及它能得到的结果,不仅仅取决于操作符本身,还同时取决于操作数。当初学C语言的时候,发现做除法要用“10/3.0”而不用“10/3”着实惊讶了一回。特别是从BASIC走过来的人,BASIC里没有这么强的类型,所以10/3就是浮点数,要整除还得用“Int()”函数或改用“/”运算符(不是每个VB程序员都知道这个运算符的哦。)
从C/C++中弄明白了“10/3.0”和“10/3”,反过来再去理解C与BASIC的区别,不难发现C/C++这样做的确比BASIC高明得多。而进一步了解了硬件的运算机制后,则可以理解这样做不仅仅是高明,而且是必须。
引用:有些符号既可以表示一元操作也可以表示二元操作。例如*……,这种两用法相互独立、各不相关,如果将其视为两个不同的符号可能会更容易理解些。……需要根据该符号所处的上下文来确定它代表一元操作还是二元操作。
笔记:这是一个大家都明白,但是大家都不会去想的问题。细想起来,我们不去想,正是因为我们早已熟知。然而我们读程序可以上下关联,计算机要做到这点就不容易——比如拼音输入法的自动选词。由此看来,C++编译器是十分优秀的人工智能软件。
标题::多维数组
引用:严格地说,C++中没有多维数组。
笔记:不只是C++啦,C中就是这样。不过,正因为C++中没有多维数组,而提供了“数组的数组”,所以C/C++在数组使用上更灵活。
多维数组的定义和使用没什么要多提的,用过就懂了。无非是多一对括号而已。不过,如果把它跟指针一起用,倒是要注意的:二维数组名对应的是“指向指针的指针”,所以,如果要在函数间传递多维数组,指针类型一定要正确:
int a[3][4];
int *p1 = &a[0][0];//a[][]是一个int,对其取地址就是int*
int *p2 = a[0];//a[0]虽然是a有一个元素,但它也是另一个数组的数组名
int **p3 = a;//a是一个二维数组的数组名
int **p4 = &a[0];//a[0]是一个数组名,它是a数组的一个成员
另外,有一个比较难记、容易混淆的用法:
int (*p5)[4] = a;
说它容易混淆,是因为它与“int *p5[4];”有着截然不同的意义。前者是指定义一个指向数组的指针,后者则是定义一个指针数组。——头昏ing...
本人在实际使用中,经常避开多维数组,而用其它途径来使用一大堆数值。比如可以这样用:
int a, b;//两维的元素个数
int *p = new int[a*b];
for (int i=0; i<a; ++i)
for (int j=0; j<b; ++j)
p[i*a+j].....;
delete []p;
用这种方法,就是三维、四维也不用考虑“指向指针的指针”这么复杂的东西。不是我不会,而是不高兴去想。
笔记:不只是C++啦,C中就是这样。不过,正因为C++中没有多维数组,而提供了“数组的数组”,所以C/C++在数组使用上更灵活。
多维数组的定义和使用没什么要多提的,用过就懂了。无非是多一对括号而已。不过,如果把它跟指针一起用,倒是要注意的:二维数组名对应的是“指向指针的指针”,所以,如果要在函数间传递多维数组,指针类型一定要正确:
int a[3][4];
int *p1 = &a[0][0];//a[][]是一个int,对其取地址就是int*
int *p2 = a[0];//a[0]虽然是a有一个元素,但它也是另一个数组的数组名
int **p3 = a;//a是一个二维数组的数组名
int **p4 = &a[0];//a[0]是一个数组名,它是a数组的一个成员
另外,有一个比较难记、容易混淆的用法:
int (*p5)[4] = a;
说它容易混淆,是因为它与“int *p5[4];”有着截然不同的意义。前者是指定义一个指向数组的指针,后者则是定义一个指针数组。——头昏ing...
本人在实际使用中,经常避开多维数组,而用其它途径来使用一大堆数值。比如可以这样用:
int a, b;//两维的元素个数
int *p = new int[a*b];
for (int i=0; i<a; ++i)
for (int j=0; j<b; ++j)
p[i*a+j].....;
delete []p;
用这种方法,就是三维、四维也不用考虑“指向指针的指针”这么复杂的东西。不是我不会,而是不高兴去想。
标题::动态数组
我晕,本书才讲了个开头,居然讲到new和delete了。我“偷窥”了一下:下一章开始才讲到操作符,而且下章将有专门的一节讲new和delete。看来这里提到它们的目的只是为了说明string这样的类为什么可以自动适应大小。
new的返回值是一个指针,不过本书暂时没有提到new也会返回NULL的。是的,暂时还不用提内容不够这么复杂的情况。
引用:在自由存储区中创建的数组对象数组是没有名字的,程序员只能通过其地址间接地访问堆中的对象。
笔记:这里有两个问题,一是“数组的名字”其实也是个指针,指针当然也可以看成“数组的名字”,这本来就可以互换的,正如我前面说的一样:“5[a]”完全等价于“*(5+a)”。至于a是静态的数组名还是动态的指针,没有区别。第二个问题是这里提到了“堆”,在没有讲解内存之前,这样说毕竟理解上有难度。还是那句话,这本书是给有一定基础的人看的。
令我耳目一新的是:new还能创建const数组。——不是我不懂,而是实在没想到。再说了,创建一大堆const的内容,而且只能初始化为同一个值。那这有什么用?正如本书提到的一样:“这样的数组实际上用处不大”。依我看,不是用处不大,而是根本没用。
动态空间的释放应该用“delete []p;”而不是“delete p;”,这是一个只要记住就可以的问题,我之所以提上一句,是因为有人没有注意过这个细节,包括很多自以为很了不起的程序员。
引用:如果遗漏了方括号对,这是一个编译器无法发现的错误,将导致程序在运行时出错。
new的返回值是一个指针,不过本书暂时没有提到new也会返回NULL的。是的,暂时还不用提内容不够这么复杂的情况。
引用:在自由存储区中创建的数组对象数组是没有名字的,程序员只能通过其地址间接地访问堆中的对象。
笔记:这里有两个问题,一是“数组的名字”其实也是个指针,指针当然也可以看成“数组的名字”,这本来就可以互换的,正如我前面说的一样:“5[a]”完全等价于“*(5+a)”。至于a是静态的数组名还是动态的指针,没有区别。第二个问题是这里提到了“堆”,在没有讲解内存之前,这样说毕竟理解上有难度。还是那句话,这本书是给有一定基础的人看的。
令我耳目一新的是:new还能创建const数组。——不是我不懂,而是实在没想到。再说了,创建一大堆const的内容,而且只能初始化为同一个值。那这有什么用?正如本书提到的一样:“这样的数组实际上用处不大”。依我看,不是用处不大,而是根本没用。
动态空间的释放应该用“delete []p;”而不是“delete p;”,这是一个只要记住就可以的问题,我之所以提上一句,是因为有人没有注意过这个细节,包括很多自以为很了不起的程序员。
引用:如果遗漏了方括号对,这是一个编译器无法发现的错误,将导致程序在运行时出错。
标题::C风格字符串
给这篇文章定下这个标题:,是因为书中就是这样说的。本书是讲解C++的,所以它推荐读者尽量使用C++的内容,而实际上像我这样从C过来的人,还是习惯于使用C风格的字符串。——我又想起了那句话:“原来我只是一个‘古代’的C++程序员。”(见《数组》一文)
C语言是用字符数组来做字符串的(当然这个字符数组必需要有一个NULL结尾),因为字符串是如此常用,C语言还专门开发了一套库函数来处理这个特殊的数组。于是,我们进行字符串操作时,可以忘记指针、忘记循环、还可以忘记char这个内置类型。
正是因为如此,林锐博士的《高质量C++/C编程指南》中还特别强调,不可以用指针的赋值和比较来进行字符串的赋值和比较。这个警告对于从VB转过来的人尤其重要。
使用C风格的字符串有两点是必须保证的:一是要给这个数组开劈足够长度的空间;二是一定不要忘了NULL;其中第二点一般程序员不会犯错,因为毕竟没几个人用“chat s[3] = {'a', 'b', '/0'}”这种方式来定义字符串。第一点就成了重中之重。我们在strcpy之前,有没有考虑过目标字符串可能的空间不足?
“strn”风格的函数既救了大家也可能害了大家,说它救了大家,因为大家在strncpy和strncat时可以控制字符个数,即使源字符串太长,也可以避免内存溢出。但是它存在的危险性是它不会为目标字符串添加NULL。
所以,书写到这里再次做了一个提醒:“尽可能使用标准库类型string”——我都忘了这是第几次提醒了,本书一而再再而三地提醒读者不要做“古代”的C++程序员。
C语言是用字符数组来做字符串的(当然这个字符数组必需要有一个NULL结尾),因为字符串是如此常用,C语言还专门开发了一套库函数来处理这个特殊的数组。于是,我们进行字符串操作时,可以忘记指针、忘记循环、还可以忘记char这个内置类型。
正是因为如此,林锐博士的《高质量C++/C编程指南》中还特别强调,不可以用指针的赋值和比较来进行字符串的赋值和比较。这个警告对于从VB转过来的人尤其重要。
使用C风格的字符串有两点是必须保证的:一是要给这个数组开劈足够长度的空间;二是一定不要忘了NULL;其中第二点一般程序员不会犯错,因为毕竟没几个人用“chat s[3] = {'a', 'b', '/0'}”这种方式来定义字符串。第一点就成了重中之重。我们在strcpy之前,有没有考虑过目标字符串可能的空间不足?
“strn”风格的函数既救了大家也可能害了大家,说它救了大家,因为大家在strncpy和strncat时可以控制字符个数,即使源字符串太长,也可以避免内存溢出。但是它存在的危险性是它不会为目标字符串添加NULL。
所以,书写到这里再次做了一个提醒:“尽可能使用标准库类型string”——我都忘了这是第几次提醒了,本书一而再再而三地提醒读者不要做“古代”的C++程序员。
标题::指针(三)指针与数组
指针和数组之间是什么关系呢?书中曰“密切相关”。其实,那简真就是同一回事嘛。用到指针的时候,你未必会用到数组;但是只要你用到数组,你就必要然用到指针(即使你不知道)。
正是因为指针可以用加或减运算来移动它所指的位置,而且每加一或减一正好移动到相邻一个同类型的变量(不管这个变量占内存是多少),那么我有意将一堆同类型的变量放在一起,拿一个指针指向它们中的第一个,再记住它们的个数,这就成了数组。
数组用一组方括号来解引用其中的某个成员,这也只是指针运算的简化。比如:
int a[10];
a[5] = 5;
以上这种代码谁都用过,谁都能理解。那么下面这行代码呢?
5[a] = 5;
这种用法恐怕很少有人知道,即使现在知道了,恐怕也很难理解。实际上知道了数组运算的实质,这行代码的迷雾就会立即消失:C/C++语言处理括号的方法很简单,将方括号前面的值和方括号内的值相加,得到一个新的指针,再取指针所指的对象值。“a[5]”就完全等价于“*(a+5)”,“5[a]”就完全等价于“*(5+a)”。
那么“*(a+5)”是什么运算呢?指针运算。因为在编译器处理“int a[10];”的时候,就等于定义了一个“int * const a;”同时将它初始化为指向栈内存的某处。
实际上,正是因为“*(a+5)”这种用法实在太常用了,C才规定了它的替代用法,后来这个替代用法被广为接受,而它的实际却被人遗忘。
以上内容本书未有提及,这是我看书看到这里的一点心得,作为读书笔记写下来。不是为了炫耀。本书虽然是给有一定基础的人读的,但是毕竟它只是按步就章地写下C++的语法规则,没有必要提及这些技巧性高而又实用性少的内容。我之所以要写下来,目的是为了便于理解指针运算。
正是因为指针可以用加或减运算来移动它所指的位置,而且每加一或减一正好移动到相邻一个同类型的变量(不管这个变量占内存是多少),那么我有意将一堆同类型的变量放在一起,拿一个指针指向它们中的第一个,再记住它们的个数,这就成了数组。
数组用一组方括号来解引用其中的某个成员,这也只是指针运算的简化。比如:
int a[10];
a[5] = 5;
以上这种代码谁都用过,谁都能理解。那么下面这行代码呢?
5[a] = 5;
这种用法恐怕很少有人知道,即使现在知道了,恐怕也很难理解。实际上知道了数组运算的实质,这行代码的迷雾就会立即消失:C/C++语言处理括号的方法很简单,将方括号前面的值和方括号内的值相加,得到一个新的指针,再取指针所指的对象值。“a[5]”就完全等价于“*(a+5)”,“5[a]”就完全等价于“*(5+a)”。
那么“*(a+5)”是什么运算呢?指针运算。因为在编译器处理“int a[10];”的时候,就等于定义了一个“int * const a;”同时将它初始化为指向栈内存的某处。
实际上,正是因为“*(a+5)”这种用法实在太常用了,C才规定了它的替代用法,后来这个替代用法被广为接受,而它的实际却被人遗忘。
以上内容本书未有提及,这是我看书看到这里的一点心得,作为读书笔记写下来。不是为了炫耀。本书虽然是给有一定基础的人读的,但是毕竟它只是按步就章地写下C++的语法规则,没有必要提及这些技巧性高而又实用性少的内容。我之所以要写下来,目的是为了便于理解指针运算。
标题::指针(二)
指针的初始化与赋值:指针是一个变量,它可以被赋值,也可以被求值。指针可以接受的值只有以下几种:
1、编译时可求值的0值常量。(必须是0,其实就是NULL啦)
2、类型匹配的对象的地址。(也就是用&运算符取一个变量的地址)
3、另一对象末的下一地址。(这种用法主要用在循环里,其实当指针取这个值时,对其所指的内存进行存取往往会导致灾难)
4、同类型的另一个有效指针(如“p=q;”)。
其中第1点,将0值赋给指针,主要是为了有一个状态表示这个指针是“空的”。C/C++通常约定0为NULL,虽然的确存在地址为0的内存,但是别指望用指针来访问这个内存。
初学者怎样才能消除对指针的恐惧?我觉得首要的一点是清醒地认识并且时刻提醒自己“指针也是一个变量”。比如以下两行程序:
int i;
int *p = &i;
看到这儿的人几乎无一例外把p和i联系起来(这当然不是坏事),但是,我觉得更重要的是将p和i分离,心里记住,p是一个变量,该变量是有它的值的,这个值与i的唯一关系是:目前该值正好等于变量i在内存中的位置。两种情况下p与i将毫无关系:
1、p值被改变,如“p = &j;”或“p++;”
2、i变量被释放,如离开了i的作用域。
1、编译时可求值的0值常量。(必须是0,其实就是NULL啦)
2、类型匹配的对象的地址。(也就是用&运算符取一个变量的地址)
3、另一对象末的下一地址。(这种用法主要用在循环里,其实当指针取这个值时,对其所指的内存进行存取往往会导致灾难)
4、同类型的另一个有效指针(如“p=q;”)。
其中第1点,将0值赋给指针,主要是为了有一个状态表示这个指针是“空的”。C/C++通常约定0为NULL,虽然的确存在地址为0的内存,但是别指望用指针来访问这个内存。
初学者怎样才能消除对指针的恐惧?我觉得首要的一点是清醒地认识并且时刻提醒自己“指针也是一个变量”。比如以下两行程序:
int i;
int *p = &i;
看到这儿的人几乎无一例外把p和i联系起来(这当然不是坏事),但是,我觉得更重要的是将p和i分离,心里记住,p是一个变量,该变量是有它的值的,这个值与i的唯一关系是:目前该值正好等于变量i在内存中的位置。两种情况下p与i将毫无关系:
1、p值被改变,如“p = &j;”或“p++;”
2、i变量被释放,如离开了i的作用域。
标题::指针
指针是C/C++的精华,也是最难的部分。——所有学习C/C++的人都明白这点,当年我初学的时候也是这样。但是,现在再回想指针,我却很难回忆它究竟难在哪儿。应该说这就叫“难者不会,会者不难”吧。“饱汉不知饿汉饥”是有一定的道理的,即使饱汉曾经饿过。
本书中规中矩地讲解了指针的概念、定义与初始化、操作等。正如上面提到的“饱汉不知饿汉饥”,我似乎很健忘,以至于不记得指针的难点在哪儿了。
指针的灵活性可以把大量的工作化繁为易,前提是必须首很把足够繁的指针弄懂。听起来有点像绕口令,事实就是这样,你现在把难懂的东西弄懂了,日后可以把难事化简,大事化小。
从VB过来的人一定会熟悉“值传递”和“地址传递”这两个概念,实际上,“地址传递”这种说法正是为了弥补VB没有指针却有类似的需要才发明的。我认为C/C++程序员要想深入理解指针,首先要抛弃这个概念。在C/C++程序中,即使在函数调用中传递指针,也不能说“地址传递”,还应该说是值传递,只不过这次传递的值有点特殊,特殊在于借用这个值,可以找到其它值。就好像我给你一把钥匙一样,你通过钥匙可以间接获得更多,但是我给你的只不过是钥匙。
我前阵子曾写过一篇关于指针的文章,之所以写那篇文章,是因为看到一大堆初学者在论坛上提问。通过对他们提的问题的分析,我总结了几点。下面,首先就先引用我自己写的《关于指针》中的片段吧(完整的文章请到我的个人主页查找):
一、指针就是变量:
虽然申明指针的时候也提类型,如:
char *p1;
int *p2;
float *p3;
double *p4;
.....
但是,这只表示该指针指向某类型的数据,而不表示该指针的类型。说白了,指针都是一个类型:四字节无符号整数(将来的64位系统中可能有变化)。
二、指针的加减运算很特殊:
p++、p--之类的运算并不是让p这个“四字节无符号整数”加一或减一,而是让它指向下一个或上一个存储单元,它实际加减的值就是它所指类型的值的size。
比如:
char *型指针,每次加减的改变量都是1;
float *型的指针,每次加减的改变量都是4;
void *型指针无法加减。
还要注意的是:指针不能相加,指针相减的差为int型。
正是因为指针有着不同于其它变量的运算方式,所以,在任何时候用到指针都必须明确“指针的类型”(即指针所指的变量的类型)。这就不难理解为什么函数声明时必须用“int abc(char *p)”而调用的时候却成了“a = abc(p);”这样的形式了。
三、用指针做参数传递的是指针值,不是指针本身:
要理解参数传递,首先必须把“形参”与“实参”弄明白。
函数A在调用函数B时,如果要传递一个参数C,实际是在函数B中重新建立一个变量C,并将函数A中的C值传入其中,于是函数B就可以使用这个值了,在函数B中,无论有没有修改这个C值,对于函数A中的C都没有影响。函数B结束时,会将所有内存收回,局部变量C被销毁,函数B对变量C所做的一切修改都将被抛弃。
以上示例中,函数A中的变量C称为“实参”,函数B中的变量C被称为“形参”,调用函数时,会在B函数体内建立一个形参,该形参的值与实参的值是相同的,但是形参的改变不影响实参,函数结束时,形参被销毁,实参依然没有发生变化。
指针也是一个变量,所以它也符合以上的规定,但是,指针存放的不仅仅是一个值,而是一个内存地址。B函数对这个地址进行了改动,改动的并不是形参,而是形参所指的内存。由于形参的值与实参的值完全相同,所以,实参所指的内存也被修改。函数结束时,虽然这个形参会被销毁,指针的变化无法影响实参,但此前对它所指的内存的修改会持续有效。所以,把指针作为参数可以在被调函数(B)中改变主调函数(A)中的变量,好像形参影响了实参一样。
注意:是“好像”。在这过程中,函数B影响的不是参数,而是内存。
下面再来看刚才的例子:“int abc(char *p)”和“a = abc(p);”。为什么申请中要用*号,因为函数必须知道这是指针;为什么调用时不加*号,因为传递的是“指针值”,而不是“指针所指内存的值”。
四、指向指针的指针:
正因为指针也是一个变量,它一样要尊守形参与实参的规定。所以,虽然指针做参数可以将函数内对变量的修改带到函数外,但是,函数体内对指针本身作任何修都将被丢弃。如果要让指针本身被修改而且要影响函数外,那么,被调函数就应该知道“该指针所在的内存地址”。这时,指针不再是指针,而是“普通变量”。作为参数传递的不是这个“普通变量”,而是指向这个“普通变量”的指针。即“指向指针的指针”。
如果p是一个指向指针的指针,那么*p就是一个指针,我们不妨就把它看成q。要访问q指针所指的内存,只要*q就是了。用初中数学的“等量代换”一换就知道,*q就是**p。
五、指针数组。
之所以要把“指针数组”单独提出来,是因为数组本身就与指针有着千丝万缕的关系。即使你不想用指针,只要你使用了数组,实际就在与指针打交道了。
只要理解了指针本身就是变量,就不难理解“指针数组”,我们可以暂且把它当成普通数组来处理,a[0]、a[1]、a[2]……就是数组的元素,只是,a[0]是一个指针,a[1]、a[2]也是一个指针。那a呢?当然也是指针,但这是两码事。你可以完全无视a的存在,只去管a[0]等元素。*a[0]与*p没有什么本质的区别。
还有一个东西不得不提一下,它比较重要:
指针的定义有两个可取的方式,它们各有优缺点:“int *p;”和“int* p;”是完全等价的,后者的好处是让人体会到p是一个“指向int的”指针,前者会让人误解为*p是一个int型变量(这里没有定义int型变量);但是前者的好处是不会产生混淆,如“int *p, *q;”让人一眼就看出定义了两个指针,而“int* p,q;”会让人误解成定义了两个指针(实际上q不是指针)。
本书中规中矩地讲解了指针的概念、定义与初始化、操作等。正如上面提到的“饱汉不知饿汉饥”,我似乎很健忘,以至于不记得指针的难点在哪儿了。
指针的灵活性可以把大量的工作化繁为易,前提是必须首很把足够繁的指针弄懂。听起来有点像绕口令,事实就是这样,你现在把难懂的东西弄懂了,日后可以把难事化简,大事化小。
从VB过来的人一定会熟悉“值传递”和“地址传递”这两个概念,实际上,“地址传递”这种说法正是为了弥补VB没有指针却有类似的需要才发明的。我认为C/C++程序员要想深入理解指针,首先要抛弃这个概念。在C/C++程序中,即使在函数调用中传递指针,也不能说“地址传递”,还应该说是值传递,只不过这次传递的值有点特殊,特殊在于借用这个值,可以找到其它值。就好像我给你一把钥匙一样,你通过钥匙可以间接获得更多,但是我给你的只不过是钥匙。
我前阵子曾写过一篇关于指针的文章,之所以写那篇文章,是因为看到一大堆初学者在论坛上提问。通过对他们提的问题的分析,我总结了几点。下面,首先就先引用我自己写的《关于指针》中的片段吧(完整的文章请到我的个人主页查找):
一、指针就是变量:
虽然申明指针的时候也提类型,如:
char *p1;
int *p2;
float *p3;
double *p4;
.....
但是,这只表示该指针指向某类型的数据,而不表示该指针的类型。说白了,指针都是一个类型:四字节无符号整数(将来的64位系统中可能有变化)。
二、指针的加减运算很特殊:
p++、p--之类的运算并不是让p这个“四字节无符号整数”加一或减一,而是让它指向下一个或上一个存储单元,它实际加减的值就是它所指类型的值的size。
比如:
char *型指针,每次加减的改变量都是1;
float *型的指针,每次加减的改变量都是4;
void *型指针无法加减。
还要注意的是:指针不能相加,指针相减的差为int型。
正是因为指针有着不同于其它变量的运算方式,所以,在任何时候用到指针都必须明确“指针的类型”(即指针所指的变量的类型)。这就不难理解为什么函数声明时必须用“int abc(char *p)”而调用的时候却成了“a = abc(p);”这样的形式了。
三、用指针做参数传递的是指针值,不是指针本身:
要理解参数传递,首先必须把“形参”与“实参”弄明白。
函数A在调用函数B时,如果要传递一个参数C,实际是在函数B中重新建立一个变量C,并将函数A中的C值传入其中,于是函数B就可以使用这个值了,在函数B中,无论有没有修改这个C值,对于函数A中的C都没有影响。函数B结束时,会将所有内存收回,局部变量C被销毁,函数B对变量C所做的一切修改都将被抛弃。
以上示例中,函数A中的变量C称为“实参”,函数B中的变量C被称为“形参”,调用函数时,会在B函数体内建立一个形参,该形参的值与实参的值是相同的,但是形参的改变不影响实参,函数结束时,形参被销毁,实参依然没有发生变化。
指针也是一个变量,所以它也符合以上的规定,但是,指针存放的不仅仅是一个值,而是一个内存地址。B函数对这个地址进行了改动,改动的并不是形参,而是形参所指的内存。由于形参的值与实参的值完全相同,所以,实参所指的内存也被修改。函数结束时,虽然这个形参会被销毁,指针的变化无法影响实参,但此前对它所指的内存的修改会持续有效。所以,把指针作为参数可以在被调函数(B)中改变主调函数(A)中的变量,好像形参影响了实参一样。
注意:是“好像”。在这过程中,函数B影响的不是参数,而是内存。
下面再来看刚才的例子:“int abc(char *p)”和“a = abc(p);”。为什么申请中要用*号,因为函数必须知道这是指针;为什么调用时不加*号,因为传递的是“指针值”,而不是“指针所指内存的值”。
四、指向指针的指针:
正因为指针也是一个变量,它一样要尊守形参与实参的规定。所以,虽然指针做参数可以将函数内对变量的修改带到函数外,但是,函数体内对指针本身作任何修都将被丢弃。如果要让指针本身被修改而且要影响函数外,那么,被调函数就应该知道“该指针所在的内存地址”。这时,指针不再是指针,而是“普通变量”。作为参数传递的不是这个“普通变量”,而是指向这个“普通变量”的指针。即“指向指针的指针”。
如果p是一个指向指针的指针,那么*p就是一个指针,我们不妨就把它看成q。要访问q指针所指的内存,只要*q就是了。用初中数学的“等量代换”一换就知道,*q就是**p。
五、指针数组。
之所以要把“指针数组”单独提出来,是因为数组本身就与指针有着千丝万缕的关系。即使你不想用指针,只要你使用了数组,实际就在与指针打交道了。
只要理解了指针本身就是变量,就不难理解“指针数组”,我们可以暂且把它当成普通数组来处理,a[0]、a[1]、a[2]……就是数组的元素,只是,a[0]是一个指针,a[1]、a[2]也是一个指针。那a呢?当然也是指针,但这是两码事。你可以完全无视a的存在,只去管a[0]等元素。*a[0]与*p没有什么本质的区别。
还有一个东西不得不提一下,它比较重要:
指针的定义有两个可取的方式,它们各有优缺点:“int *p;”和“int* p;”是完全等价的,后者的好处是让人体会到p是一个“指向int的”指针,前者会让人误解为*p是一个int型变量(这里没有定义int型变量);但是前者的好处是不会产生混淆,如“int *p, *q;”让人一眼就看出定义了两个指针,而“int* p,q;”会让人误解成定义了两个指针(实际上q不是指针)。
标题::数组
进入本书第四章,开始讲“数组”了。数组难不难?这不好说。但是数组非常重要这是肯定的,有许多基本的算法就是与数组一起出现的——比如冒泡排序法。而离开了那些算法,数组本身也失去了价值。
注意:阅读本章时要对“维数”概念加以小心,按平时的理解,“维数”是多维数组中的概念,但是本书中的“维数”指的是元素个数。为了避免干扰,我在阅读笔记中用“个数”来取代“维数”。
引用:在出现标准库之前,C++程序大量使用数组保存一组对象。而现代的C++程序则更多地使用vector来取代数组,数组被严格限制于程序内部使用,只有当性能测试表明使用vector无法达到必要的速度要求时,才使用数组。
笔记:我汗一个先!原来我只是一个“古代”的C++程序员。
数组的定义必需指明元素的个数,而且必须是“常量表达式”。这一点想必理解数组的人都会操作(即使弄错了,编译器也会立即报错),但是真正去思考它的人也许不多。这里的“常量”概念是指程序在编译阶段就能求值的量。
在C时代,我们并不会过多地理会“常量”这个概念,那是因为C时代没有const,而define这个东西,谁都知道它是在编译前就直接替换的。但是到了C++时代,由于const的存在,使“常量”这个概念变得更普杂了。
const size1 = 5;//这是个编译时就可以求值的常量,它可以在数据的定义中使用。它的作用仅仅相当于“define”。
const size = getsize();//这个常量是运行时才能求值的。
除此之外,“常量求达式”的概念还指明这可是以一个算术式,只要编译时能求值。如“char username[max_name_size + 1];”这种用法非常常用,因为它可以避免在定义数组时忘掉NULL。
数组成员的初始化可以显式指定,也可以不指定。显式指定时指定成员的个数可以小于等于实际个数,但不可以比实际个数大,如果小于,则其它元素视为未指定。对于未指定的元素的初始值,它们遵循变量的初始化原则,请看《变量初始化》一文。
绝大多数书本在介绍“字符串”时都会提到,字符串其实是字符数组,只是因为它们太常用,才会有专门的使用方法。本书也不例外。
本书没有提数组的实质,因为“指针”还没有进入读者的眼睛。
引用:数组的显著缺点在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。
笔记:其实这正在程序员心中的一个痛。当我把数组作为参数传送给函数时,函数该怎么处理这个东西?要么是对大小有个事先的约定——这样的程序将失去很多通用性,要么连大小一起传递给人家。
注意:阅读本章时要对“维数”概念加以小心,按平时的理解,“维数”是多维数组中的概念,但是本书中的“维数”指的是元素个数。为了避免干扰,我在阅读笔记中用“个数”来取代“维数”。
引用:在出现标准库之前,C++程序大量使用数组保存一组对象。而现代的C++程序则更多地使用vector来取代数组,数组被严格限制于程序内部使用,只有当性能测试表明使用vector无法达到必要的速度要求时,才使用数组。
笔记:我汗一个先!原来我只是一个“古代”的C++程序员。
数组的定义必需指明元素的个数,而且必须是“常量表达式”。这一点想必理解数组的人都会操作(即使弄错了,编译器也会立即报错),但是真正去思考它的人也许不多。这里的“常量”概念是指程序在编译阶段就能求值的量。
在C时代,我们并不会过多地理会“常量”这个概念,那是因为C时代没有const,而define这个东西,谁都知道它是在编译前就直接替换的。但是到了C++时代,由于const的存在,使“常量”这个概念变得更普杂了。
const size1 = 5;//这是个编译时就可以求值的常量,它可以在数据的定义中使用。它的作用仅仅相当于“define”。
const size = getsize();//这个常量是运行时才能求值的。
除此之外,“常量求达式”的概念还指明这可是以一个算术式,只要编译时能求值。如“char username[max_name_size + 1];”这种用法非常常用,因为它可以避免在定义数组时忘掉NULL。
数组成员的初始化可以显式指定,也可以不指定。显式指定时指定成员的个数可以小于等于实际个数,但不可以比实际个数大,如果小于,则其它元素视为未指定。对于未指定的元素的初始值,它们遵循变量的初始化原则,请看《变量初始化》一文。
绝大多数书本在介绍“字符串”时都会提到,字符串其实是字符数组,只是因为它们太常用,才会有专门的使用方法。本书也不例外。
本书没有提数组的实质,因为“指针”还没有进入读者的眼睛。
引用:数组的显著缺点在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。
笔记:其实这正在程序员心中的一个痛。当我把数组作为参数传送给函数时,函数该怎么处理这个东西?要么是对大小有个事先的约定——这样的程序将失去很多通用性,要么连大小一起传递给人家。
标题::C++标准库,想说爱你不容易
第三章就这样结束了,本章介绍了三个标准库类型:string、vector和bitset。
可惜的是,整个第三章我都是草草读过的。一方面因为它们不属于严格意义上的C++内容,另一方面C时代的东西在不经意间抵触着它们。
确切地说,它们C时代的那些东西的替代品。它们存在的理由就是它们更优秀。然而优秀是一回事,动不动心又是一回事。
C语言在类与对象方面的缺失,使C程序员更多地掌握了底层的操作。面对C++标准库中的string和bitset这些东西,C程序员们都知道,在C++出来以前自己是怎样想方法解决过问题的。刻苦才有刻骨铭心,转到C++以后,是使用更简单安全的标准库,还是使用自己曾熟悉的老办法,这是一个取舍的问题。
引用:程序员应优先使用标准库类类型。
笔记:本书在这里放置了这样一个提示,大概是想告诫我这样的顽固派:“别再死纠着陈腐旧套不放了,回头吧。”其实,早在本书的前言就有了类似的内容。可是,我依然不能让自己静下心来细读并熟记第三章。
也许有一天我会回头重读第三章,当然,也许永远不会。因为像string和vector这样的东西,在MFC中还有更好的替代品。
可惜的是,整个第三章我都是草草读过的。一方面因为它们不属于严格意义上的C++内容,另一方面C时代的东西在不经意间抵触着它们。
确切地说,它们C时代的那些东西的替代品。它们存在的理由就是它们更优秀。然而优秀是一回事,动不动心又是一回事。
C语言在类与对象方面的缺失,使C程序员更多地掌握了底层的操作。面对C++标准库中的string和bitset这些东西,C程序员们都知道,在C++出来以前自己是怎样想方法解决过问题的。刻苦才有刻骨铭心,转到C++以后,是使用更简单安全的标准库,还是使用自己曾熟悉的老办法,这是一个取舍的问题。
引用:程序员应优先使用标准库类类型。
笔记:本书在这里放置了这样一个提示,大概是想告诫我这样的顽固派:“别再死纠着陈腐旧套不放了,回头吧。”其实,早在本书的前言就有了类似的内容。可是,我依然不能让自己静下心来细读并熟记第三章。
也许有一天我会回头重读第三章,当然,也许永远不会。因为像string和vector这样的东西,在MFC中还有更好的替代品。
标题::标准库bitset类型
每当使用到布尔变量数组时,总是有点心疼。因为布尔变量只需要0和1两种值,然而编译器动辄使用一个字节——甚至四个字节来存放一个布尔变量。面对1/32的使用效率,叫人怎能不心疼?
要想节约空间也不是没有办法,代价是写更多的代码:用“&”操作将变量的某一个bit取出来,一个字节就可以存放8个布尔变量。但是,这个代价是比较重的,重到足以让程序员望而生畏的地步。
bitset应运而生,它可以方便地管理一系列的bit位而不用程序员自己来写代码。
更重要的是,bitset除了可以访问指定下标的bit位以外,还可以把它们作为一个整数来进行某些统计,如:
b.any();//b中是否存在置为1的二进制位?
b.count();//b中置为1的二进制位的个数
……
不过,话说回来,我还是不习惯用bitset,原因在于我是从C语言转到C++的。详细问题留到后一篇文章中讨论。
要想节约空间也不是没有办法,代价是写更多的代码:用“&”操作将变量的某一个bit取出来,一个字节就可以存放8个布尔变量。但是,这个代价是比较重的,重到足以让程序员望而生畏的地步。
bitset应运而生,它可以方便地管理一系列的bit位而不用程序员自己来写代码。
更重要的是,bitset除了可以访问指定下标的bit位以外,还可以把它们作为一个整数来进行某些统计,如:
b.any();//b中是否存在置为1的二进制位?
b.count();//b中置为1的二进制位的个数
……
不过,话说回来,我还是不习惯用bitset,原因在于我是从C语言转到C++的。详细问题留到后一篇文章中讨论。
标题::迭代器:指针与数据库杂交的后代
迭代器与数据库的相似之处在于end()函数返回值为“指向末元素的下一个”。跟数据记录集的eof这么相似。话说回来,熟练于C/C++的程序员一定不会忘了,利用下标访问数组时用的总是用“半开半闭区间”。就拿“int a[10]; for(int i=0; i!=10; i++)”来说,下标为10就可以视为“最后一个元素的下一个”。只是以前不会有这么明显的思考。
迭代器与指针的相似之处在于它的“解引操作符”,居然就是一个“*”号。而且存在着“ivec[n]”和“*ivec”这两个完全等价的操作。而且还支持算术运算来移动要访问的元素。
迭代的const用法与指针有个区别:
“vector<int>::const_iterator ivec = vec.begin();”定义的ivec并不是常量,只是它指向的元素会得到保护。如果要定义本身为const的迭代器,要用“const vector<int>::ivec = vec.begin();”。
指针是这样处理的:
const int *p = &i;//p指向的变量是const
int* const p = &i;//p是const
地雷:任何改变vector长度的操作都会使已存在的迭代器失败。例如,在调用push_back之后,就不能再信赖指向vector的迭代器的值了。
笔记:我不得不相信所谓的迭代器其本质就是一个指针了。因为vector支持动态增长,而且保证内存的连续。所以,每次改变它的长度都会导致释放已有内存、重新申请内存。指针当然得失效。
迭代器与指针的相似之处在于它的“解引操作符”,居然就是一个“*”号。而且存在着“ivec[n]”和“*ivec”这两个完全等价的操作。而且还支持算术运算来移动要访问的元素。
迭代的const用法与指针有个区别:
“vector<int>::const_iterator ivec = vec.begin();”定义的ivec并不是常量,只是它指向的元素会得到保护。如果要定义本身为const的迭代器,要用“const vector<int>::ivec = vec.begin();”。
指针是这样处理的:
const int *p = &i;//p指向的变量是const
int* const p = &i;//p是const
地雷:任何改变vector长度的操作都会使已存在的迭代器失败。例如,在调用push_back之后,就不能再信赖指向vector的迭代器的值了。
笔记:我不得不相信所谓的迭代器其本质就是一个指针了。因为vector支持动态增长,而且保证内存的连续。所以,每次改变它的长度都会导致释放已有内存、重新申请内存。指针当然得失效。
标题::for语句的条件思考
对于for语句,我几乎总是这样写的:“for(int i=0; i<Max; i++);”。但是,按本书的说法,我这样写似乎同时犯了两个错误。
1、本书中写for语句中的第二个表达式(条件表达式)时总是用!=,而不用<。这几天来虽然心里觉得奇怪,但是一直没去思考这里面的含义。本书没有急着告诉我“所以然”,只是提醒我读完本书的第二部分后就会明白。我等着吧:)
2、我总是觉得用Max这样一个变量去代替某个需要用函数才能返回的值可以增加运行效率,但是本书的建议与我的理解相反。它建议用“i<ivec.size()”,“因为数据结构可以动态增长,……如果确实增加了新元素的话,那么测试已保存的size值作为循环结束条件就会有问题,因为没有将新加入的元素计算在内。”
同时,本书为了打消运行效率的顾虑,还提到了“内联函数”这个概念。现在还没有到介绍内联的时候,但是C++必须是一个有机体,在讲述某一个知识点的时候,不可能完全避免另一个知识点。所以,我还是建议初学者不要急着阅读本书。
1、本书中写for语句中的第二个表达式(条件表达式)时总是用!=,而不用<。这几天来虽然心里觉得奇怪,但是一直没去思考这里面的含义。本书没有急着告诉我“所以然”,只是提醒我读完本书的第二部分后就会明白。我等着吧:)
2、我总是觉得用Max这样一个变量去代替某个需要用函数才能返回的值可以增加运行效率,但是本书的建议与我的理解相反。它建议用“i<ivec.size()”,“因为数据结构可以动态增长,……如果确实增加了新元素的话,那么测试已保存的size值作为循环结束条件就会有问题,因为没有将新加入的元素计算在内。”
同时,本书为了打消运行效率的顾虑,还提到了“内联函数”这个概念。现在还没有到介绍内联的时候,但是C++必须是一个有机体,在讲述某一个知识点的时候,不可能完全避免另一个知识点。所以,我还是建议初学者不要急着阅读本书。
标题::标准库vector类型
终于让“类模板”上场了,可惜的是,在本书的第三章,还不能彻底让类模板浮出水面,只能将就着提一下。
引用:使用类模板可以编写一个类定义或函数定义,而用于多个不同的数据类型。……vector并不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。
笔记:“vector<int> ivec;”中,“vector<int>”是一个数据类型,C++支持类模板,以后大量接触类模板时,这个一定要时刻注意。
引用:在元素值已知的情况下,最好是动态地增加元素。……虽然可以对给定元素个数的vector对象预先分配内存,但是更有效的方法是先初始化一个空的vector对象,然后再动态地增加元素。
将vector与string对比,可以轻松地记住empty()、size()函数和[]、=、==、!=等运算符。不过这似乎有个前提:对C语言比较熟并且摈弃VB的String变量类型。因为,string虽然是一个变量,但是如果缺少对“char []”的理解,将注定不能理解它。——所以,我很反对某些人说的“可以不学C语言直接学C++”论调。
引用:使用类模板可以编写一个类定义或函数定义,而用于多个不同的数据类型。……vector并不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。
笔记:“vector<int> ivec;”中,“vector<int>”是一个数据类型,C++支持类模板,以后大量接触类模板时,这个一定要时刻注意。
引用:在元素值已知的情况下,最好是动态地增加元素。……虽然可以对给定元素个数的vector对象预先分配内存,但是更有效的方法是先初始化一个空的vector对象,然后再动态地增加元素。
将vector与string对比,可以轻松地记住empty()、size()函数和[]、=、==、!=等运算符。不过这似乎有个前提:对C语言比较熟并且摈弃VB的String变量类型。因为,string虽然是一个变量,但是如果缺少对“char []”的理解,将注定不能理解它。——所以,我很反对某些人说的“可以不学C语言直接学C++”论调。
标题::标准库string类型
习惯了VC++的CString类,而此前用的又是C语言,所以,压根没有看一眼string。现在既然书中专门讲它,我就看一看吧。
定义与初始化:
string s1;
string s2(s1);
string s3("value");
string s4(n, 'c');//由n个c组成的一串
string对象的输入除了可以“cin >> s1;”以外,还可以将cin和string对象一起作为getline()函数的参数“getline(cin, s1);”,而且这个函数的返回值还是cin。
除此之外,string类还有empty()、size()等函数和[]、+、=、==等运算符。
地雷:empty()函数的功能并不是将对象置空,而是测试是否为空。这可是与CString类的Empty()函数不一样的哦!
本书还详细介绍了empty::size_type类型存在的原因。在使用VC++.NET的时候,我已经见过了size_t类型——strlen()函数的返回值就是size_t类型对象。当时我也没有细究这个东西,想来应该是为了支持64位机而设的吧。不过现在我知道了,原来我想得还不够。
引用:通过这些配套类型,库类型的使用就能与机器无关。
关于“+运算必须至少包含一个string类型”,这可能让初学者摸不着头脑。比如“s1 + s2”、“s1 + "hello"”、“"hello" + s1”是合法的,唯独“"hello" + "world"”不合法。我在看懂“运算符重载”之前也没有明白这个。本书由于刚开头,离“运算符重载”还远着呢,所以只告诉读者“然”、没有告诉读者“所以然”。我也不写了。
引用:下标操作可作左值。
笔记:人类一思考,上帝就发笑。这就是一个例证。下标操作可作左值有什么好说的?早就用过啊?比如“char s[]="abcde"; s[2]='t';”。可是现在的不是指针变量,而是类成员,对于“string s("abcde");”来说,“s[n]”就不是简单操作一个内存了,而是从“[]重载函数中返回了一个值”。这个值可以做左值,并不是想当然的事。
定义与初始化:
string s1;
string s2(s1);
string s3("value");
string s4(n, 'c');//由n个c组成的一串
string对象的输入除了可以“cin >> s1;”以外,还可以将cin和string对象一起作为getline()函数的参数“getline(cin, s1);”,而且这个函数的返回值还是cin。
除此之外,string类还有empty()、size()等函数和[]、+、=、==等运算符。
地雷:empty()函数的功能并不是将对象置空,而是测试是否为空。这可是与CString类的Empty()函数不一样的哦!
本书还详细介绍了empty::size_type类型存在的原因。在使用VC++.NET的时候,我已经见过了size_t类型——strlen()函数的返回值就是size_t类型对象。当时我也没有细究这个东西,想来应该是为了支持64位机而设的吧。不过现在我知道了,原来我想得还不够。
引用:通过这些配套类型,库类型的使用就能与机器无关。
关于“+运算必须至少包含一个string类型”,这可能让初学者摸不着头脑。比如“s1 + s2”、“s1 + "hello"”、“"hello" + s1”是合法的,唯独“"hello" + "world"”不合法。我在看懂“运算符重载”之前也没有明白这个。本书由于刚开头,离“运算符重载”还远着呢,所以只告诉读者“然”、没有告诉读者“所以然”。我也不写了。
引用:下标操作可作左值。
笔记:人类一思考,上帝就发笑。这就是一个例证。下标操作可作左值有什么好说的?早就用过啊?比如“char s[]="abcde"; s[2]='t';”。可是现在的不是指针变量,而是类成员,对于“string s("abcde");”来说,“s[n]”就不是简单操作一个内存了,而是从“[]重载函数中返回了一个值”。这个值可以做左值,并不是想当然的事。
标题::命名空间的using声明
虽然实际工作中绝少用到cin和cout,但是我还是记得要用它们必先“using namespace std;”,至于为什么这样做,三个字:不知道。
本书从一开头就用到了cin和cout,但是它没有using,而是每一次用到它们都写成“std::cin”和“std::cout”,同时提醒说这两个字名是来自std命名空间的。于是,我似乎明白了“using namespace std;”的作用。
也许是C++实在太难,作者迟迟不介绍更深入的内容,到了第一部分、第三章,还没有打算深入using,但又不得不讲一点,于是提了这样两行:
using std::cin;
using std::cout;
至此,究竟什么是命名空间,命名空间是干什么的,我还是不懂。也许书后面会提吧。
本书从一开头就用到了cin和cout,但是它没有using,而是每一次用到它们都写成“std::cin”和“std::cout”,同时提醒说这两个字名是来自std命名空间的。于是,我似乎明白了“using namespace std;”的作用。
也许是C++实在太难,作者迟迟不介绍更深入的内容,到了第一部分、第三章,还没有打算深入using,但又不得不讲一点,于是提了这样两行:
using std::cin;
using std::cout;
至此,究竟什么是命名空间,命名空间是干什么的,我还是不懂。也许书后面会提吧。
标题::头文件
头文件的用处主要是代码重用——重用不仅仅是为了减少工作量,还可以保证每一次重用都是完全相同的内容。
正因为头文件可以多次重用,所以要防止有些只能出现一次的代码放进头文件中。比如变量的定义只能有一次,声明(含extern且不含初始化)却可以有多次。函数也是。
引用:一些const对象定义在头文件中。
笔记:看到这里,总算想通了前面的困惑:为什么const常量的作用域仅为一个文件。正如我前面的估计,const常量是不开劈内存空间的,代码在编译的时候就被直接替换成常量值。而且,编译器对多个CPP是分开编译的。所以,它必须要编译的时候知道该常量的值。如果常量的作用域也是全局的,那么我告诉编译器“该常量的值在另一个CPP中”将使其无可适从。
避免头文件被重复包含是个比较有岐义的说法,头文件既然可以被多次包含,为什么又要避免重复包含?还是因为CPP文件的单独编译。在编译任一个CPP文件时,都要知道文件的内容,所以,如果两个CPP都包含了同一个头文件,那么头文件就要被编译两次。但是同一个CPP如果重复调用——甚至可能循环调用——了某一个头文件,则要及时避免。#ifndef...#define...#endif可以起到这个作用。
对了,我一直没想通VC++.NET的“#pragma once”是怎么工作的。它为什么不需要用变量来标记不同的头文件?为什么只需要一行也能标记出整个头文件的所有内容?
第一部分、第二章结束。
正因为头文件可以多次重用,所以要防止有些只能出现一次的代码放进头文件中。比如变量的定义只能有一次,声明(含extern且不含初始化)却可以有多次。函数也是。
引用:一些const对象定义在头文件中。
笔记:看到这里,总算想通了前面的困惑:为什么const常量的作用域仅为一个文件。正如我前面的估计,const常量是不开劈内存空间的,代码在编译的时候就被直接替换成常量值。而且,编译器对多个CPP是分开编译的。所以,它必须要编译的时候知道该常量的值。如果常量的作用域也是全局的,那么我告诉编译器“该常量的值在另一个CPP中”将使其无可适从。
避免头文件被重复包含是个比较有岐义的说法,头文件既然可以被多次包含,为什么又要避免重复包含?还是因为CPP文件的单独编译。在编译任一个CPP文件时,都要知道文件的内容,所以,如果两个CPP都包含了同一个头文件,那么头文件就要被编译两次。但是同一个CPP如果重复调用——甚至可能循环调用——了某一个头文件,则要及时避免。#ifndef...#define...#endif可以起到这个作用。
对了,我一直没想通VC++.NET的“#pragma once”是怎么工作的。它为什么不需要用变量来标记不同的头文件?为什么只需要一行也能标记出整个头文件的所有内容?
第一部分、第二章结束。
标题::class与struct
首次考虑class与struct的关系源自我对类对象占用内存数的观察。我发现VC++6的CString——这么强大的类——它占内存居然是4字节(sizeof(CString)为4)。那时我就认为,类对象仅仅在内存中存放成员变量(《C++ Primer》称作“数据成员”)而不存放成员函数。后来,读林锐博士的《高质量C/C++编程指南》时,才看到了比较正规的说法:C++中class与struct没有本质的区别。——当时我将这句话给我朋友看时,他还表现出不相信的神色。
读《C++ Primer》时,我已经熟知了class与struct的关系。但是我还是细细地没有放过书中的任何一个字,还是作点记录吧:
引用:用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private。
笔记:不过,一般情况下公司与公司间规定协议时,喜欢将类定义写成struct,我想这可能是源于程序员从C语言继承来的习惯,还有就是协议内容一般只包含数据,不包含函数,也没有必要规定安全的访问级别。
引用:编程新手经常会忘记类定义后面的分号,这是个很普通的错误!
笔记:书中将这段文字标作一个地雷,我还是引用一下吧。
读《C++ Primer》时,我已经熟知了class与struct的关系。但是我还是细细地没有放过书中的任何一个字,还是作点记录吧:
引用:用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private。
笔记:不过,一般情况下公司与公司间规定协议时,喜欢将类定义写成struct,我想这可能是源于程序员从C语言继承来的习惯,还有就是协议内容一般只包含数据,不包含函数,也没有必要规定安全的访问级别。
引用:编程新手经常会忘记类定义后面的分号,这是个很普通的错误!
笔记:书中将这段文字标作一个地雷,我还是引用一下吧。
标题::枚举
枚举是我向来不太喜欢用的东西,几乎我见过的每本书都是这样介绍枚举的:
enum weekday {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday};
我看到这里,总是想:多浪费啊,与其这样,还不如直接用0-6这些数呢。以后要写“weekday Today = Sunday;”,哪有“int Today = 0;”舒服。
本书似乎早看透了我的心理,于是,讲枚举不从枚举入手,偏从const常量入手:
引用:
const int input = 0;
const int output = 1;
const int append = 2;
虽然这种方法也能奏效,但是它有个明显的缺点:没有指出这些值是相关联的。枚举提供了一种替代方法,不但定义了整数常量集,而且还把它们聚集成组。
笔记:大师就是大师,他们写书能切中要害,让读者明白标准制定者的苦心。
另外,本书在这儿冷不丁地提了一个冷冰冰的概念:常量表达式。
引用:常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常表达式,……
笔记:让我先汗一个。常量表达式必需是整数?我怎么不知道?那“double pi = 3.14159;”后面的字面值不属于常量表达式吗?这是个疑问,先记下来。
引用:枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其它对象来进行。
笔记:这跟现实生活中的“做人要专一”有点相似,呵呵。简单地说,你选择了用名字来代替数值,那就得始终如一地使用名字,不可以用数值或其它表达式。比如“weekday Today = Sunday;”不可写成“weekday Today = 0;”,虽然Sunday就是0。
enum weekday {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday};
我看到这里,总是想:多浪费啊,与其这样,还不如直接用0-6这些数呢。以后要写“weekday Today = Sunday;”,哪有“int Today = 0;”舒服。
本书似乎早看透了我的心理,于是,讲枚举不从枚举入手,偏从const常量入手:
引用:
const int input = 0;
const int output = 1;
const int append = 2;
虽然这种方法也能奏效,但是它有个明显的缺点:没有指出这些值是相关联的。枚举提供了一种替代方法,不但定义了整数常量集,而且还把它们聚集成组。
笔记:大师就是大师,他们写书能切中要害,让读者明白标准制定者的苦心。
另外,本书在这儿冷不丁地提了一个冷冰冰的概念:常量表达式。
引用:常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常表达式,……
笔记:让我先汗一个。常量表达式必需是整数?我怎么不知道?那“double pi = 3.14159;”后面的字面值不属于常量表达式吗?这是个疑问,先记下来。
引用:枚举类型的对象的初始化或赋值,只能通过其枚举成员或同一枚举类型的其它对象来进行。
笔记:这跟现实生活中的“做人要专一”有点相似,呵呵。简单地说,你选择了用名字来代替数值,那就得始终如一地使用名字,不可以用数值或其它表达式。比如“weekday Today = Sunday;”不可写成“weekday Today = 0;”,虽然Sunday就是0。
标题::typedef
长期以来,我一直在疑惑:typedef这个词要它干什么?因为没有它我照样可以完成所有任务。而有了它我反而觉得无法适应。比如“UINT i;”,为什么不写作“unsigned int i;”?本书用简短的三句话告诉我它存在的意义:
引用:typedef通常被用于以下三种目的:为了隐藏特定类型的实现,更强调使用类型的目的;简化复杂的类型定义,使其更易理解;允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
笔记:第三种目的对我的启发特大。以后就“typedef unsigned int age;”,呵呵。
引用:typedef通常被用于以下三种目的:为了隐藏特定类型的实现,更强调使用类型的目的;简化复杂的类型定义,使其更易理解;允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
笔记:第三种目的对我的启发特大。以后就“typedef unsigned int age;”,呵呵。
标题::引用
引用是C++的特色,一般用在函数的参数中。按有些书本的说法,叫“普通变量的用法,指针变量的效果”。书中本节没有讲诉引用在函数参数中的用法,只提了“给变量起个别名”这一个用处(毕竟本书才开头)。说实在的,如果撇开函数参数,还真想不到引用有什么用处。
引用这个概念本身也不难理解(除了对C程序员来说有些不习惯以外),但是引用的符号却增加了理解它的难度,我经常在论坛上看到有初学者对“&”和“*”两个符号的疑惑,他们问的问题可以说非常基础,但却表现出了这个问题的难以理解的特点:
C语言中的指针已经够复杂的了,加再上一个引用,引用与指针有着千丝万缕的联系,这就算了,而且还用了“&”这个符号。真让初学者忙昏了头。呵呵。下面四行程序,用到了两个“&”和两个“*”,但是它们的意义却全然不同:
int a;
int &b = a;//&用在定义中仅表示变量的性质为引用
int *c = &a;//*用在定义中仅表示变量的性质为指针,&用在表达式中表示取地址
int d = *c;//*用天表达式中表示取指针变量所指的变量的值
写下以上文字,我觉得有些越权了。这些内容估计在本书后面会详谈的,我心急了点。
引用这个概念本身也不难理解(除了对C程序员来说有些不习惯以外),但是引用的符号却增加了理解它的难度,我经常在论坛上看到有初学者对“&”和“*”两个符号的疑惑,他们问的问题可以说非常基础,但却表现出了这个问题的难以理解的特点:
C语言中的指针已经够复杂的了,加再上一个引用,引用与指针有着千丝万缕的联系,这就算了,而且还用了“&”这个符号。真让初学者忙昏了头。呵呵。下面四行程序,用到了两个“&”和两个“*”,但是它们的意义却全然不同:
int a;
int &b = a;//&用在定义中仅表示变量的性质为引用
int *c = &a;//*用在定义中仅表示变量的性质为指针,&用在表达式中表示取地址
int d = *c;//*用天表达式中表示取指针变量所指的变量的值
写下以上文字,我觉得有些越权了。这些内容估计在本书后面会详谈的,我心急了点。
标题::const常量的作用域仅为一个CPP文件
如果将变量定义放在任何{}的外面,则该变量是全局的,这个规则不适用于const常量。
以前虽然心里隐隐约约有这个感觉,但是从未正面考虑过这个问题。之所以隐隐约约有此感觉,是因为我认为编译器并不为const常量开劈内存空间。
我曾经专门做过测试:程序如下:
const int i = 5;
int *p;
p = (int *)&i;
cout << *p << "/t" << i << endl;
(*p)++;
cout << *p << "/t" << i << endl;
测试结果发现const常量也是可以通过某些途径改变其值的,但是改变不起作用。我的解释是,编程器在生成机器码时将所有的i直接替换成了5。使得上面两个cout语句都成了“cout << *p << "/t" << 5 << endl;”。
回想起以前做过的测试,便不难理解“const常量的作用域仅仅为定义它的CPP文件”,因为编译器是对每个CPP文件单独编译的,只有在连接时才会去理会CPP之间的关系。虽然本书才看了个开头,书中还说“我们将会在2.9.1节看到为何const对象局部于文件创建”。我不打算跳跃式地阅读本书,所以,我还是先保留我自己的理解吧。
const常量也还是可以成为全局常量的,方法是在定义的时候就加上extern。看到这里,我终于明白了《extern的困惑》中的困惑:原来,那种“有些多余,而且增加了出错的可能性”的做法在常量的处理上派上了用场。
以前虽然心里隐隐约约有这个感觉,但是从未正面考虑过这个问题。之所以隐隐约约有此感觉,是因为我认为编译器并不为const常量开劈内存空间。
我曾经专门做过测试:程序如下:
const int i = 5;
int *p;
p = (int *)&i;
cout << *p << "/t" << i << endl;
(*p)++;
cout << *p << "/t" << i << endl;
测试结果发现const常量也是可以通过某些途径改变其值的,但是改变不起作用。我的解释是,编程器在生成机器码时将所有的i直接替换成了5。使得上面两个cout语句都成了“cout << *p << "/t" << 5 << endl;”。
回想起以前做过的测试,便不难理解“const常量的作用域仅仅为定义它的CPP文件”,因为编译器是对每个CPP文件单独编译的,只有在连接时才会去理会CPP之间的关系。虽然本书才看了个开头,书中还说“我们将会在2.9.1节看到为何const对象局部于文件创建”。我不打算跳跃式地阅读本书,所以,我还是先保留我自己的理解吧。
const常量也还是可以成为全局常量的,方法是在定义的时候就加上extern。看到这里,我终于明白了《extern的困惑》中的困惑:原来,那种“有些多余,而且增加了出错的可能性”的做法在常量的处理上派上了用场。
标题::变量的作用域
作用域这个概念是程序员都耳熟能详的知识点。这部分知识我几乎可以跳过,不过我还是认真阅读了相关内容。阅读过程中还是有体会的:
有无数书本曾经提醒过我:少用(尽量不用)全局变量,多用局部变量。其实,即使是局部变量,也还有作用域大小的,局部变量的作用域也是越小越好。原因自然和少用全局变量一个道理。正如书中所言:“通常把一个对象定义放在它首次使用的地方是一个很好的办法。”
以往,我使用局部变量总是把它放在函数的开头,表示这些变量在本函数中起作用。唯一的例外是for语句的循环变量(for (int i=0; i<Max; i++);)。以后我就改改,把局部变量的作用域缩小到仅仅用到它的语句块。
有无数书本曾经提醒过我:少用(尽量不用)全局变量,多用局部变量。其实,即使是局部变量,也还有作用域大小的,局部变量的作用域也是越小越好。原因自然和少用全局变量一个道理。正如书中所言:“通常把一个对象定义放在它首次使用的地方是一个很好的办法。”
以往,我使用局部变量总是把它放在函数的开头,表示这些变量在本函数中起作用。唯一的例外是for语句的循环变量(for (int i=0; i<Max; i++);)。以后我就改改,把局部变量的作用域缩小到仅仅用到它的语句块。
标题::extern的困惑
extern用来告诉程序:你不用为我的变量开劈内存空间,你只要知道这个变量别处已经声明过了。所以,我总是在程序包含多个CPP文件时才这样用:
1、在某一个CPP文件中直接定义变量,如int i = 0;
2、其它CPP文件中声明变量,如extern int i;
但是,书中介绍exturn还可以用来定义,如:extern double pi = 3.1416;特点是该extern语句包含变量的初始化。
我觉得C++标准这样做有些多余,而且增加了出错的可能性。因为“extern double pi = 3.1416;”完全可以用“double pi = 3.1416;”来代替,这样做可以让定义与声明划清界线。毕竟定义只能有一次,而声明可以无数次。如果没有这个特性,可以让程序员简单记为“不带extern的只能有一次,带extern的可以有无数次,而且extern同时不能指定初始值。”这一特性的支持,使原本简单的规则变得复杂,但没有带来灵活(众所周知,C++的复杂是以高度灵活为补尝的)。
这个段落还让我认识到了我以前使用extern的不足。我以往在同一个CPP文件中只使用一次extern,所以我往往是在文件头部用extern语句来声明一下变量,这样做虽然没有什么错,但却会导致上下翻查:有时在文件的某一处用到变量时,想看一下它的声明,不得不把滚动条拖到顶上去查看。如果在用到它的段落开头处再声明,明显比顶部声明要好一些。
1、在某一个CPP文件中直接定义变量,如int i = 0;
2、其它CPP文件中声明变量,如extern int i;
但是,书中介绍exturn还可以用来定义,如:extern double pi = 3.1416;特点是该extern语句包含变量的初始化。
我觉得C++标准这样做有些多余,而且增加了出错的可能性。因为“extern double pi = 3.1416;”完全可以用“double pi = 3.1416;”来代替,这样做可以让定义与声明划清界线。毕竟定义只能有一次,而声明可以无数次。如果没有这个特性,可以让程序员简单记为“不带extern的只能有一次,带extern的可以有无数次,而且extern同时不能指定初始值。”这一特性的支持,使原本简单的规则变得复杂,但没有带来灵活(众所周知,C++的复杂是以高度灵活为补尝的)。
这个段落还让我认识到了我以前使用extern的不足。我以往在同一个CPP文件中只使用一次extern,所以我往往是在文件头部用extern语句来声明一下变量,这样做虽然没有什么错,但却会导致上下翻查:有时在文件的某一处用到变量时,想看一下它的声明,不得不把滚动条拖到顶上去查看。如果在用到它的段落开头处再声明,明显比顶部声明要好一些。
标题::变量初始化
int ival(1024);//直接初始化
int ival = 1024;//复制初始化
以前的我常用第二种用法,原因很简单:从来没见过第一种用法。直到后来学习了林锐博士的《高质量C/C++编程指南》。
那本书在讲类的构造时说道:CMyClass b = a;这种形式看起来像赋值,实际上调用的是拷贝构造函数。
那本书给我的感觉仅仅停留在类变量的初始化中,一直没有过渡到内置变量类型。直到后来,我在用VC++.NET的向导功能编程序时,才发现向导帮我产生了类似于“int i(100);”这种语法来初始化。
本书重点强调:初始化不是赋值,
引用:当定义没有初始化的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。……内置类型变量是否初始化取决于变量定义的位置。在函数体外定义的变量都初始化为0,在函数体内定义的内置类型变量不进行自动初始化。……(类类型)通过定义一个特殊的构造函数即默认构造函数来实现的。这个构造函数被称作默认构造函数,是因为它是“默认”运行的。……不管变量在哪里定义,默认构造函数都会被使用。
笔记:林锐博士的《高质量C/C++编程指南》中说,如果类没有定义无参数构造函数或拷贝构造函数,系统会自动产生这两个构造函数,它们采用最简单的“值传递”和“位拷贝”来完成构造。
int ival = 1024;//复制初始化
以前的我常用第二种用法,原因很简单:从来没见过第一种用法。直到后来学习了林锐博士的《高质量C/C++编程指南》。
那本书在讲类的构造时说道:CMyClass b = a;这种形式看起来像赋值,实际上调用的是拷贝构造函数。
那本书给我的感觉仅仅停留在类变量的初始化中,一直没有过渡到内置变量类型。直到后来,我在用VC++.NET的向导功能编程序时,才发现向导帮我产生了类似于“int i(100);”这种语法来初始化。
本书重点强调:初始化不是赋值,
引用:当定义没有初始化的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。……内置类型变量是否初始化取决于变量定义的位置。在函数体外定义的变量都初始化为0,在函数体内定义的内置类型变量不进行自动初始化。……(类类型)通过定义一个特殊的构造函数即默认构造函数来实现的。这个构造函数被称作默认构造函数,是因为它是“默认”运行的。……不管变量在哪里定义,默认构造函数都会被使用。
笔记:林锐博士的《高质量C/C++编程指南》中说,如果类没有定义无参数构造函数或拷贝构造函数,系统会自动产生这两个构造函数,它们采用最简单的“值传递”和“位拷贝”来完成构造。
标题::变量和变量名
引用:对象是内存中具有类型的区域。
笔记:这句话说得很直白,也只有这样面向C++熟练工的书才可以这样说,毕竟初学者不知道内存与变量的关系,或者还没考虑到。
笔记:这句话说得很直白,也只有这样面向C++熟练工的书才可以这样说,毕竟初学者不知道内存与变量的关系,或者还没考虑到。
引用:C++还保留了一些词用作各操作符的替代名。这些替代名用于支持某些不支持标准C++操作符号集的字符集。它们也不能用作标识符(此处指变量名,偷猫标)。
一般的书只提到C++变量名不可以使用关键字,本书还额外提了这样一句,然后列出一个表,有and、bitand、compl、not_eq、or_eq、xor_eq、and_eq、bitor、not、or、xor。不过据我在VC++.NET上测试,这些是可以用作变量标识符的。
一般的书只提到C++变量名不可以使用关键字,本书还额外提了这样一句,然后列出一个表,有and、bitand、compl、not_eq、or_eq、xor_eq、and_eq、bitor、not、or、xor。不过据我在VC++.NET上测试,这些是可以用作变量标识符的。
都说变量名可以含“_”,还可以用“_”开头,但我一直没有试过光光用一个“_”来做变量名,正好习题里有,我就试了一下,果然可以的。
标题::cout << (wchar_t类型变量)体验
《C++ Primer》说了,字符常量或字符串常量前加L,表示wchat_t类型,于是我试了一下:
程序如下:
char a = "a";
wchar_t b = L"a";
cout << a << endl;
cout << b << endl;
结果如下:
a
0012FEBC
晕,怎么出现这个结果?
让我再试。
char a = "a";
wchar_t b = L"a";
cout << a << endl;
cout << b << endl;
结果如下:
a
0012FEBC
晕,怎么出现这个结果?
让我再试。
程序如下:
char a = 'a';
wchar_t b = L'a';
cout << a << endl;
cout << b << endl;
结果如下:
a
97
char a = 'a';
wchar_t b = L'a';
cout << a << endl;
cout << b << endl;
结果如下:
a
97
由此可见,cout的<<运算符没有对wchat_t的重载(或不健全)。
标题::内置类型之精度选择
引用:……大多数通用机器都是使用和long类型一样长的32位来表示int类型。整型运算时,用32位表示int类型和用64位表示long类型的机器会出现应该选择int类型还是long类型的难题。在这些机器上,用long类型进行计算所付出的运行时代价远远高于用int类型进行同样计算的代价。……决定使用哪种浮点型就容易多了:使用double类型基本上不会有错。在float类型中隐式的精度损失是不能忽视的,而double类型精度代价相对于float类型精度代价可以忽略。事实上,有些机器上,double类型比float类型的计算要快和多。long double类型提供的精度通常没有必要,而且还需要承担额外的运行代价。
标题::内置类型之int和bool
第一部分,第二章
引用:C++标准规定了每个算术类型的最小存储空间,但它并不阻止编译器使用更大的存储空间,事实上,对于int类型,几乎所有的编译器使用的存储空间都比所要求的大。
笔记:确实如此,VC++中int和long是一样大。VC++.NET增加了对_int64的支持。
引用:字符类型有两种:char和wchar_t,wchar_t类型用于扩展字符集,比如汉字和日语。
笔记:我怎么不知道wchar_t?我自己用包含汉字的字符串时用的也是char。:(
看到bool型,我心里对VC++有些气愤。因为VC++里有一个BOOL宏。它的原型为“typedef int BOOL”。既然C++标准已经有bool型,VC++加入BOOL的用意很明显:迎合更多的编程习惯。但是,即使非要增加对BOOL的支持,我认为原型应该这样:“typedef bool BOOL”,这样更易于理解。
当然了,长期以来我一直没有注意到bool确实也不应该。但是正是BOOL的存在,阻碍了我对bool的理解。
引用:C++标准规定了每个算术类型的最小存储空间,但它并不阻止编译器使用更大的存储空间,事实上,对于int类型,几乎所有的编译器使用的存储空间都比所要求的大。
笔记:确实如此,VC++中int和long是一样大。VC++.NET增加了对_int64的支持。
引用:字符类型有两种:char和wchar_t,wchar_t类型用于扩展字符集,比如汉字和日语。
笔记:我怎么不知道wchar_t?我自己用包含汉字的字符串时用的也是char。:(
看到bool型,我心里对VC++有些气愤。因为VC++里有一个BOOL宏。它的原型为“typedef int BOOL”。既然C++标准已经有bool型,VC++加入BOOL的用意很明显:迎合更多的编程习惯。但是,即使非要增加对BOOL的支持,我认为原型应该这样:“typedef bool BOOL”,这样更易于理解。
当然了,长期以来我一直没有注意到bool确实也不应该。但是正是BOOL的存在,阻碍了我对bool的理解。
标题::第一章:快速入门
第一章:快速入门
本章的存在使本书变得不像一本“规范书”,似乎成了“入门书”,这可能是后来版本新加入的内容。以至于这一章节被排除在任何一个“部分”之外。
本章“无厘头”地简要介绍了cin、cout、注释、while、for、if等概念。这么多东西,每一个都介绍点皮毛,然后组合成一个综合实例。
我称其为“无厘头”有以下原因:如果本书面对不了解C++的读者,那么这一章似乎是有用的,但是C++的入门者使用本书显然很难入门,我不了解国外的情况怎样,至少我身边的人是这样;如果本书面对已经了解C++的读者,那么,这些东西都不用介绍,读者也可以看懂那个“综合实例”,而且所谓的“综合实例”也没有存在的必要。
本章的存在使本书变得不像一本“规范书”,似乎成了“入门书”,这可能是后来版本新加入的内容。以至于这一章节被排除在任何一个“部分”之外。
本章“无厘头”地简要介绍了cin、cout、注释、while、for、if等概念。这么多东西,每一个都介绍点皮毛,然后组合成一个综合实例。
我称其为“无厘头”有以下原因:如果本书面对不了解C++的读者,那么这一章似乎是有用的,但是C++的入门者使用本书显然很难入门,我不了解国外的情况怎样,至少我身边的人是这样;如果本书面对已经了解C++的读者,那么,这些东西都不用介绍,读者也可以看懂那个“综合实例”,而且所谓的“综合实例”也没有存在的必要。
不过有一个小小的收获:
int main()
{
return -1;
}
如果返回值为-1,Windows的CMD中运行也没有任何额外信息,说明Windows并不报告运行失败。
int main()
{
return -1;
}
如果返回值为-1,Windows的CMD中运行也没有任何额外信息,说明Windows并不报告运行失败。
标题::《〈C++ Primer〉阅读笔记》前言
在读本书之前,我已经有过一段编写C++程序的历史,如果连C语言也算在内,可以追溯到十年前。用BASIC语言编程序的历史则有十四年(1992-2006)。
长期编程序中所使用的参考书无非有两种:介绍算法的书和介绍语法的书。我所买的参考书往往是同时介绍两者的。而对语法的介绍,则只是基于某一个编译器。
于是,这十年来,我所学习的“C/C++”,从本质上说只是Turbo C和Visual C++,对C/C++本身的理解也是被编译器过滤的内容。不是我不想去了解C/C++的本质,只是我对C/C++的“法典”有着与生俱来的恐惧。
这个恐惧直到我发现了《C++ Primer中文版》,当我捧起这本C++的“圣经”时,我终于能理解为什么每有几十万人冒着被踩死的危险前去朝圣。
请允许我用“圣经”来比喻这本书,我知道这样并不合适,因为绝大多数中国人并不知道“圣经”的地位,但是我搜遍大脑的每一个角落也找不到其它合适的比喻。这源于中国人缺乏信仰。
在半年前,我曾经带着无限的敬仰阅读了林锐博士的《高质量C/C++编程指南》,并且及时培养/修正了我的编程习惯。当然了,正如《C++ Primer》所言:“什么是C或C++程序的正确格式存在着无休止的争论……”。所以,我所修正的只是“自由体”,而不是与林锐矛盾的方面。令我感到欣慰的是,《C++ Primer》居然也用一定的笔默来讲格式与风格,不同的是,它同时介绍几种风格,然后作出一个略带倾向性的建议。同时,书中还告诫读者:“一旦选择了某种风格,就要始终如一地使用。”
从现在起,我就要捧起这一本四五厘米厚、七百多页的圣经,在阅读过程中,难免会有重点、要点要记录。我比较爱书,不愿在书上做标记,只好选择了BLOG这种形式来做读书笔记。
谨以此作为我的BLOG开篇说明吧。
长期编程序中所使用的参考书无非有两种:介绍算法的书和介绍语法的书。我所买的参考书往往是同时介绍两者的。而对语法的介绍,则只是基于某一个编译器。
于是,这十年来,我所学习的“C/C++”,从本质上说只是Turbo C和Visual C++,对C/C++本身的理解也是被编译器过滤的内容。不是我不想去了解C/C++的本质,只是我对C/C++的“法典”有着与生俱来的恐惧。
这个恐惧直到我发现了《C++ Primer中文版》,当我捧起这本C++的“圣经”时,我终于能理解为什么每有几十万人冒着被踩死的危险前去朝圣。
请允许我用“圣经”来比喻这本书,我知道这样并不合适,因为绝大多数中国人并不知道“圣经”的地位,但是我搜遍大脑的每一个角落也找不到其它合适的比喻。这源于中国人缺乏信仰。
在半年前,我曾经带着无限的敬仰阅读了林锐博士的《高质量C/C++编程指南》,并且及时培养/修正了我的编程习惯。当然了,正如《C++ Primer》所言:“什么是C或C++程序的正确格式存在着无休止的争论……”。所以,我所修正的只是“自由体”,而不是与林锐矛盾的方面。令我感到欣慰的是,《C++ Primer》居然也用一定的笔默来讲格式与风格,不同的是,它同时介绍几种风格,然后作出一个略带倾向性的建议。同时,书中还告诫读者:“一旦选择了某种风格,就要始终如一地使用。”
从现在起,我就要捧起这一本四五厘米厚、七百多页的圣经,在阅读过程中,难免会有重点、要点要记录。我比较爱书,不愿在书上做标记,只好选择了BLOG这种形式来做读书笔记。
谨以此作为我的BLOG开篇说明吧。
- C++ Primer读书笔记1(经典收藏)
- C++ Primer读书笔记1(经典收藏)
- C++ Primer读书笔记2(经典收藏)
- C++ Primer读书笔记2(经典收藏)
- C primer 读书笔记1
- 【旧】C++Primer读书笔记(1)
- C++Primer读书笔记:1 开始
- C primer 读书笔记 结构1
- c primer plus 读书笔记1
- C primer plus 读书笔记 (1)
- 《C++primer》读书笔记(1)
- C++Primer 读书笔记 第1章 开始
- <<C++Primer PLus 第五版>>读书笔记1
- C++Primer读书笔记(1)-声明和定义
- <<C++Primer PLus 第五版>>读书笔记1
- C-PRIMER PLUS读书笔记
- C-PRIMER PLUS读书笔记
- C++Primer读书笔记(一)
- container_of 理解 2
- 查看端口所用的应用程序
- 系统自带的UIBarButtonSystemItem样式
- 登陆SQL Server 2008时提示评估期已过的解决办法
- window和linux下查询oracle的em端口、isql*plus端口
- C++ Primer读书笔记1(经典收藏)
- 使用Navicat 8.0管理mysql数据库(导入导出数据)
- 模型驱动技术
- ArcGIS Runtime 本地API介绍(五)
- 算法导论学习笔记(11)——贪心算法之哈夫曼树
- Win7笔记本设置成Wifi热点
- class_create(),device_create自动创建设备文件结点
- C++ Primer读书笔记2(经典收藏)
- Sunday算法