C++Primer读书笔记知识点
C++Primer读书笔记知识点
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++”和“j = ++i”对比,告诉读者虽然i都增了1,但是j却不一样。
这也没办法,因为绝大多数书在讲到++