CPP2-基础部分(1)

时间:2022-08-10 17:04:06

参考自《c++ primer 5th zh》,本系列将会接着将《The C++ Programming Language 4th Ed》《c++ primer plus 6th》加进来,暂时是抄书形式的这种,所以会引起大家的不适吧。初衷主要是为了自己能将厚厚的一本书压缩到几个博客中,这样看书回忆更容易了,因为相比来说,一个版面的,就算再大也比折成一张一张的书籍,更能够宏观的把握,而且对于我来说,忽略了很多不必要的文字,(==!当然现在还是很多,所以其实通过对自己博客的书写可以看出,哪个章节的文字多,那么那个章节就是掌握的较差的一章)。

一、变量和基本内置类型

1、cpp定义了算术类型和空类型,其中算术类型包括字符、整形数、布尔值和浮点数,算术类型在不同机器上所占的比特数是不同的,下表为cpp标准定义的比特最小值,当然编译器可以允许更大的:

CPP2-基础部分(1)

其中cpp语言规定int至少和short一样大;long至少和int一样大;longlong至少和long一样大。通常float以1个字(32bit)表示,double以2个字表示,long double以3或者4个字表示,float和double分别有7和16个有效位;而且对于char、 signed char、unsigned char 来说,char默认为哪一个是编译器决定的。

2、明确知道数值不为负的时候,用无符号类型;一般使用int 。short有时候太小,而long和int一般有着一样的位数,如果数值超过了int,用long long;算术表达式中不要用char或bool,因为不同机器定义是有符号还是无符号不同,或者显式指定;浮点运算用double,float精度不够,而long double运行消耗较大。当对无符号类型赋值超出范围的值时,采取的是将值对无符号类型表示数值最大值取模后的余数,比如-1赋给8bit的unsigned
char,结果为255.

3、如果两个字符串字面值位置紧邻,之间只有空格、缩进和换行符,那么会被自动合并为一个字符串字面值。对于转义字符来说,\ 后面跟的是1-3个数字,那么就是8进制;如果后面是\x那么就是16进制。如果反斜线 \ 后面跟着的八进制数字超过3个,那么只有前3个会被认为八进制;而 \x 会用到后面所有的数字。其中nullptr是指针字面值。

4、通过添加下面的前缀或者后缀,可以改变整型、浮点型和字符型字面值的默认类型,如:L'a'  、42ULL:

CPP2-基础部分(1)

5、对象和变量的称呼,参考python中的详细说明,对象表示的是内存中某种类型的空间,而变量就是作为引用该空间的标识符。而且初始化不是赋值,初始化是创建变量时赋予其初始值;赋值时将对象的当前值擦除,用新值来代替。

6、这四种都是初始化:int a = 0;   int a = {0};    int a{0};   int  a(0);其中{}表示的是列表初始化;而且用内置类型的变量作为其他的初始化时:如果初始值存在丢失信息的风险,编译器会报错:long double ld = 3.1415926536;  int a{ld}, b = {ld}; //这时候会报错 int c(ld), d = ld; //这时候不报错。

7、cpp支持分离式编译(separate compilation),就是程序分成多个文件。为了支持这个机制,可以将声明和定义区分开,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明,而定义赋值创建与名字关联的实体。变量声明规定了变量的类型和名字,定义在这基础上申请了内存空间,或者为变量赋一个初始值(初始化、声明、定义、初始值,都是不同的概念)。如果只想声明一个变量而不定义它,在前面增加extern 关键字,而且不要显式的初始化变量:
extern int i ;//声明未定义;int i ;//声明且定义。而且在函数内部,初始化一个由extern关键字标记的变量,将引发错误。所以如果需要跨文件使用变量,那么定义只能出现在一个文件中,而用到该变量的文件中必须有声明,但却不能重复定义。

8、用户定义的标识符中不能连续出现两个下划线,也不能以下划线紧跟大写字母开头,定义在函数体外的标识符不能以下划线开头。变量名一般小写字母,定义的类名一般是大写字母开头。下面是cpp的关键字和操作符替代名:

CPP2-基础部分(1)

CPP2-基础部分(1)

9、::作用域符号,前面没有域名的时候,就是指的全局作用域;c++11中增加了“右值引用”(在后面介绍),一般说的引用指的是“左值引用”,而且引用必须初始化。而且因为引用本身不是一个对象,所以无法定义引用的引用。除了下面的”const的引用“和书中15.2.3中的两种另外,其他的应用类型都需要与绑定的对象严格匹配。而且引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起;除了下面的"const的引用"和15.2.3节的两种另外,其他所有指针类型都需要和指向的对象严格匹配,因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以必须匹配。c++11增加的nullptr作为指针的字面值常量,可以被解释成各种类型的指针。void*是一种特殊的指针类型,用来存放任意对象的地址。这个类型所能做的比较有限:拿它和别的指针作比较、作为函数的输入或输出、或者赋给另一个void*指针。不能直接操作这个指针指向的对象,因为没解释它指向的到底是什么类型。

10、引用本身不是对象,所以不能定义指向引用的指针。指针是对象,所以有对指针的引用int *&r = p;(p为指针),解读方法:按照结合方式,&r,说明这是一个引用,剩下的int *说明什么类型的引用。

11、const对象一旦创建后就不能改变它的值,所以const对象必须初始化,初始值可以是任意复杂的表达式:const  int i  = function();   const in i =42; 这两种都是可以的。int i = 42;const int ci  = i; int j = ci;这三种都是正确的;const的常量特征只有在执行改变ci操作的时候才会阻止,如果发现并没有改变值,那么就支持这种操作。如果直接用字面值常量来初始化比如const
int bufsize = 512;那么其实在编译阶段,编译器就会把所有这个变量的出现的地方换成512,然后在进行下一步编译操作(常量折叠)。默认情况下const的作用范围只在文件内,多个文件出现同名的const变量,只是不同文件分别设立独立的变量而已。如果想跨文件使用,比如得到函数返回值,那么对于const 变量不管是声明还是定义都添加extern关键字,然后在某个文件中定义一次就行; const int ci = 1024; int &r2 = ci;会报错,因为试图用可以改变的变量来指向常量,const
int &r1 = ci;是正确的。

12、在上面9中说引用的两个例外之一:a)在初始化常量引用时允许任意表达式作为初始值,只要该表达式的结果能转换成引用的类型就行。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:

CPP2-基础部分(1)

要理解过程,最简单的就是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:

CPP2-基础部分(1)

这里ri引用了一个int型的数,对ri的操作是整数运算,但是dval是double,所以为了确保ri绑定到一个整数上,编译器把上述代码变成如下形式:

CPP2-基础部分(1)

所以其实是 ri 绑定到一个临时量对象,这个对象是由编译器检测到需要空间暂存表达式的求值结果时临时创建一个未命名的对象。当ri不是const 类型时,如果执行上面的引用那么就是需要可以对ri 赋值,可是ri 绑定的是临时变量,所以改变的却不是我们想要的,所以这种操作是非法的(即非const 不支持中间转换,必须完全匹配)。下面这种:常量引用仅对引用可参与的操作做出限定,对引用本身是不是常量不做限定,(其实下面的这种情况也可以用上面的临时变量来解释,因为i
是int类型,而r2是const int 类型,中间需要转换):

CPP2-基础部分(1)

13、指向常量的指针不能用于改变其所指对象的值,要存放常量对象的地址,只能使用指向常量的指针:

CPP2-基础部分(1)

指针类型必须与所指对象的类型一致,但有两个另外:一个是允许令一个指向常量的指针指向一个非常量对象:

CPP2-基础部分(1)

指向此昂了的指针仅仅要求不能通过该指针改变对象的值,所以可以用指向常量的指针去指向非常量的对象。

14、将const分成两种说法:顶层const,用于表示自身是常量;底层const,用于表示指向的对象是常量。当进行复制的时候,顶层const不受到影响,底层const就有了,因为对于顶层来说,非常量可以转换成常量,而常量不能转换成非常量。(p58)

15、常量表达式指的是值不变,并且在编译的时候就知道结果的表达式。int sta = 27;这不属于常量表达式,因为sta是个int 类型,对于编译器来说,可能后面会改变值,没法直接固定。在复杂的系统中要求的常量表达式初始化别人,很难做到,c++11允许将变量声明为constexpr类型,让编译器来验证变量值是否是常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

CPP2-基础部分(1)

虽然不能用普通函数作为constexpr变量的初始值,但是如6.5.2部分一样,允许定义一种特殊的constexpr函数,这种函数足够简单而且编译的时候就能计算结果,这样就能用该函数初始化constexpr变量。constexpr指针的初始值必须是nullptr或者0,或者直接是地址值。6.1.1说道函数体内定义的变量一般不存放在固定的地址中,所以constexpr指针不能指向这样的变量,而全局对象一般地址不变,所以可以,而且函数定义的其中一种具有固定地址的变量也可以作为其初始值。在constexpr声明中如果定义了一个指针,限定符constexpr仅仅对指针有效,对其指向的对象无关。所以可以将其当做顶层const指针对待。

16、类型别名(type alias),含有typedef的声明语句定义的不再是变量而是类型别名。而且,新规定引入一种新的方法,使用别名声明(alias declaration)来定义类型的别名:

CPP2-基础部分(1)

这种方法用关键字using作为别名声明的开始,后面紧跟别名和等号,作用是把等号左侧的名字规定成等号右侧类型的别名。在追本溯源的时候往往是将原有的typedef定义放入原来位置考虑,对于其他情况都较好理解,对于指针就不太好了,这里技巧就是,将指针当成一个类型,将* 与p绑定在一起考虑,而不是简单的带入。

17、为了解决编程时将表达式的值赋给变量,而不知道表达式的类型的尴尬,c++11引入了auto类型说明符让编译器去分析表达式所属的类型,显然auto定义的变量必须有初始值:

CPP2-基础部分(1)

而auto支持的一条语句中声明多个变量,这时候这些变量的初始基本数据类型都必须一样。当推断出auto的类型和要求的不太一样时,编译器会适当的转换。

CPP2-基础部分(1)

而且使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的是引用对象的值,此时编译器以引用对象的类型作为auto类型,其次auto一般会忽略掉顶层const,同时底层const会保留下来,比如当初始值时一个指向常量的指针时:

CPP2-基础部分(1)

CPP2-基础部分(1)

如果希望auto明确指出类型是个顶层const,必须明确指出:

CPP2-基础部分(1)

还可以将引用的类型设为auto,这时候之前的初始化规则仍然适用:

CPP2-基础部分(1)

设置一个类型为auto引用时,初始值中的顶层常量属性仍然保留。而且如下面,auto的多个声明中必须类型统一,因为符号&和×只属于标识符,而不是基本数据类型,所以初始值必须同一种类型:

CPP2-基础部分(1)

18、有时候希望从表达式的类型推断出要定义的变量的类型,但是却不想用该表达式的值初始化变量。c++11引入了第二种类型说明符decltype。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

CPP2-基础部分(1)

decltype处理顶层const和引用的方式与auto有些不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

CPP2-基础部分(1)

这里要注意的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如4.1.1部分,有些表达式将向decltype返回一个引用类型。当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

CPP2-基础部分(1)

上面前两行就不解释了,第三行是如果表达式的内容是解引用操作,则decltype将得到引用类型。解引用指针可以得到指针所指向的对象,而且还能给这个对象赋值。因此第三行的结果类型就是int&,而不是int。decltype和auto的另一个重要区别是,decltype的结果类型与表达式形式密切相关,不过有个另外:对于decltype所用的表达式来说,如果变量名加上一对括号,则得到的类型与不加的时候不同,不加的话得到的是该变量的类型;加一层或多层括号,编译器会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:

CPP2-基础部分(1)

ps:decltype((var))双括号的结果永远是引用,而decltype(var)结果只有当var本身是引用的时候才是引用。

19、c++新标准规定可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将默认初始化。不过对类内初始值的限制与之前类似:放在花括号里,或者等号右边,不过不能用圆括号(上面6)

20、对于分离式编译来说,为了确保各个文件中类的定义一致,类通常定义在头文件中,而且类所在头文件的名字应该与类的名字一样。头文件中通常页用到其他头文件的功能,对于显式包含和间接包含都会造成编译器识别成重定义,所以要做适当处理。也就是预处理器,在编译之前执行的一段程序,比如#include的替换,和这里的头文件保护符,#define 指令将一个名字设定为预处理变量,:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。ps:不要太在乎程序需不需要这个头文件都应该习惯性的加上头文件保护符。

二、字符串、向量和数组

1、c++很多内置类型是计算机硬件本身支持的能力,而标准库定义的一些更高级的类型是未直接实现到计算机硬件中的。

2、使用using声明来指定所需要访问的命名空间的名字,如下形式,不过记得每个名字都需要独立的using声明,而且每个都需要分号结尾:

CPP2-基础部分(1)

CPP2-基础部分(1)

值得注意的是头文件中不应该包含using声明,会引起一些名字冲突,也就是用到才使用(头文件会被其他源文件包含),下面附带在《c++ primer 5th 》中涉及到的标准库名字和头文件以供查阅:

CPP2-基础部分(1)

CPP2-基础部分(1)

CPP2-基础部分(1)

CPP2-基础部分(1)

CPP2-基础部分(1)

3、使用标准库的string类型,需要包含其#include <string>头文件,还要使用using命令usingstd::string。(标准库对库类型提供的操作做了详细的规定;而且也对库的实现作出了一些性能上的需求,所以要想实现自己的库,最好也遵循要求)。

3.1下面是string初始化的方式:

CPP2-基础部分(1)

很多时候,特别是后面的类类型的时候,可以显式的创建一个临时对象来用于复制:string s8 = string(10,‘c’),这个观点在后面的隐式类转换上会用到。下面是string的大部分操作:

CPP2-基础部分(1)

上面操作中:对于string的流读取来说,会自动忽略开头的空白(空格符、换行符、制表符),读取之后,直到下一个空白前停止。(这种空白的讨论是很有必要的,很多时候不但是可读性的问题,也涉及到一些安全问题,比如在c的某些函数上)。因为流读取的结果会返回左侧的对象,所以可以连续流读取,cin>>str1>>str2;对于getline函数来说:会从当前输入流缓冲区中读取数据,直到遇到换行符(也被读取进来),然后赋值给string对象(丢弃最后的换行符),如果当前输入流缓冲区中只有一个换行符,那么当前str就是个空字符串。和上面的流操作一样,getline函数也会返回流参数,所以可以用作whine(getline())中的条件判断。

3.2、对于size()函数来说,其返回的是string:size_type类型(无符号整数类型),标准库中定义自己配套的类型,就是为了保证与机器无关的特性。在c++11中允许使用auto或者decltype来推断变量的类型:auto len = line.size();值得注意的是,如果用一个负值,比如s.size()<n(负数n),那么结果肯定是true,因为负值会转换成无符号值(肯定很大)。

3.3 、标准库允许字符和字符串字面值在运算中转换成string对象,不过与string对象混合使用时,记得每个加法运算两侧至少有一个是string对象。而且因为加法是从左到右的操作,所以如果表达式的第一个就是string对象,那么后面按照连续返回机制,其实后面可以都是字符串或者字符字面值。

3.4、处理string对象中的字符会涉及到语言和库的多方面,在cctype头文件中就定义了标准库函数处理这部分的工作,下面是主要的函数名和含义:

CPP2-基础部分(1)

头文件如果是cname,那么就是从c中继承行为而改写的,并且包含在std中,不然name.h就是c版本的标准库。在c++11中引入新的语句:范围for(range for,估计采用了matlab python的一些特性吧):

CPP2-基础部分(1)

其中declaration是变量,expression是个可迭代的序列,当执行一次,变量就变成了序列中的下一个值。比如string str(“abcdef”);for (auto c : str)/*操作部分*/(个人:朝着python近了一步);而且如果想这种方式改变string对象中的每个值,可以采用之前的auto &c的方式:for(auto &c :str) c = toupper(c);ps:对字符串操作前检测当前字符串是否为空是个好习惯。

4、标准库类型vector表示对象的集合,其中所有对象的类型都相同。如之前一样需要#include<vector>然后还有 using std::vector;编译器根据模板创建类或函数的过程称为实例化。而vector就是类模板,需要实例化的时候提供存放对象的类型:vector<int> ivec;vector能容纳绝大多数类型的对象做元素,不过引用不是对象,所以不存在包含引用的vector(个人:估计就是在实例化的时候没法采取<int &>的形式,因为引用首先不是对象,其次必须初始化),而且相比早期的标准,vector<vector<int>
空格>;现在的可以vector<vector<int>>,最后两个相邻了。

4.1、下面是vector对象的常用初始化方法:

CPP2-基础部分(1)

根据新标准的形式,列表初始化可以如下:vector<string> articles = {“a”,”an”,”the”};也就是vector对象包含三个元素:每个元素对应着不同的字符串。其中()这种初始化形式只支持1个元素,而多个值的初始化需要用到{};上表中的默认初始化就是按照不同的类型会有不同的初始化,内置类型或者调用类的构造函数。不过这里有个问题值得注意的:

CPP2-基础部分(1)

CPP2-基础部分(1)

对比可以发现上面的v8看着怪怪的,可是还是允许的,v6是违反了vector容器的()规定,而v7,v8是当花括号无法执行的时候,考虑使用圆括号规则代替。Ps:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。具体原因在本书的5.4.3中(个人:应该是vector为了支持动态增长,在改变容器大小的时候,内存位置会变化的,所以读取的迭代器会可能失效)。

4.2 vector的一些重要的操作:

CPP2-基础部分(1)

只有当元素的值支持可比较时,才能比较,比如vector<string>,而比如vector<Myclass>,自己定义的类中并没有运算符重载,那么就不支持比较了。

5、除了vector之外,标准库定义了其他几种容器,所有都支持迭代器,只有少数才能使用下标运算符。string不是容器类型,不过却支持与容器类似的操作,比如迭代器。Begin()指向第一个元素,end()指向最后一个元素的下一个位置,如果容器为空,则begin()==end()。为了方便,可以尽可能的使用auto b = vec.begin();让编译器自己决定迭代器的类型。下面就是标准容器迭代器的运算符:

CPP2-基础部分(1)

解引用迭代器的前提是该迭代器指向某个元素,试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。Ps:对于cpp老程序员来说习惯在比较的时候使用!=和==,而不是使用小于<来观察是否到了最后,因为这两个是所有容器都有效的,其他的不保证有效。

5.1下面是非常量和常量迭代器的类型的例子:

CPP2-基础部分(1)

没const 的支持读写,有const 的只能读取(类似于指针常量一样,不能通过该渠道去改变对象)。迭代器名词混淆点:可能是迭代器概念本身;容器定义的迭代器类型;某个迭代器对象。比如下面的:vector<int> v;

CPP2-基础部分(1)

上面的一个是表示vector内存放的是否是常量,从而返回对应的迭代器,不过更多时候就算是可改变的容器,我们也想在某个部分使用迭代器常量,C++11引入新的两个函数:cbegin()和cend()用以返回const_iterator类型。

5.2、迭代器本身的运算,支持的操作有iter++;++iter;iter+n;iter-n;iter+=n;iter-=n;iter1-iter2;>;>=;<;<=;其中必须保证得到的结果是有效迭代器,也就是在范围内或者尾后迭代器。其中iter1-iter2的结果的类型为difference_type类型,是带符号的整型数,表示两个迭代器的差分距离,可正可负。

6、对于数组的维度大小初始化来说,如果变量或者函数是constexpr的话可以作为其值:string strs[get_size()];当get_size是constexpr时正确,否则出错。而且定义数组时,必须指定数组的类型,不允许用auto由初始值的列表推断类型,而且也不存在引用的数组。一些编译器支持数组的赋值,这是编译器扩展,不过这对于需要移植的代码来说,是不好的。

6.1、定义指针数组、数组指针、数组引用(没有引用数组):假设int arr[10];那么对应为int *ptr[10];int (*parray)[10] = &arr;int (&arrRef)[10]= arr;对于这种嵌套的来说,最好从数组的名字开始按照由内向外的顺序阅读。对于数组下标来说,通常将大小定义为size_t类型,该类型在cstddef头文件中。

6.2 在很多地方使用数组类型的对象其实就是使用一个指向该数组首元素的指针。所以当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:

CPP2-基础部分(1)

编译器在这其中执行的是auto ia2(&ia[0])这样的操作;而使用decltype关键字的时候却不会转换成指针:

CPP2-基础部分(1)

对于int *e = &arr[10];来说,也能做一个尾后指针,不过该指针只能当作哨兵的作用,不能解引用或者递增。,可以如for(int *be = arr,*en = arr+10; be!=en;++be){};为了更安全,c++11引入两个新的函数begin和end,int *beg =begin(ia);int *last = end(ia);这两个函数在<iterator>头文件中。对应的指针相减:auto n = end(arr) – begin(arr);相减的类型为ptrdiff_t类型,也是定义在cstddef头文件中。数组中的指针和迭代器一样,范围为头和“尾后”之间。标准库类型string和vector的下标必须是无符号的,而数组的下标无这个要求。

6.3、对于string对象,可以用字符串字面值或者空字符结尾的字符数组来初始化,逆向是str.c_str(),该函数返回指向空字符结尾的字符数组,而且如果后续对str改变了,那么该函数的返回值也会改变,从而前面的会失效,所以最好深度复制需要的str返回的字符串数组。

6.4、对于数组的理解:a)从数组名出发,比如intia[2][3],那么就是ia往后结合,最后观察数组元素,即是一个2维数组,每个元素是一个3维数组,其中每个具体元素是int值。如果绑定int (&row)[4] = ia[1];就是只引用ia数组第2行:

CPP2-基础部分(1)

使用范围for循环处理多维数组,除了最内层的循环外,其他所有循环的控制变量都必须是引用类型,这是为了避免数组被自动转成指针,否则外层的ia首先转换成大小为4的数组类型int (*p)[4],然后接着再次转换成数组内部的首地址类型,也就是int*类型,所以内层就只能在第一个int*内循环了,也就是第一行(编译器也会报错),而且对于ia来说,其在使用的时候会自动转换成第一个内层数组的指针。如果上面都没有引用,那么row是int*类型。

CPP2-基础部分(1)

如果通过类型别名的话,可以让上面的工作更简化。

CPP2-基础部分(1)

三、表达式

1、左值右值好绕;逻辑运算符和关系运算符的运算对象和求值结果都是右值,而且返回值(即比较结果)都是布尔类型。

2、优先级规定了运算对象的组合方式,但是没说对象按照什么顺序求值,而且大多数情况下,不会明确指定求值的顺序,所以不要在同一个运算符上使用依赖同一个变量或者参数的函数,因为无法知道最后的求值顺序,所以下面这种:cout<<i<<” “<<++i<<endl;的结果是依赖于编译器的。只有四种:&&、||、?:、,。这四种的求值顺序是可以保证的(个人:注意p123和p133页,不要在所有的运算对象中使用修改同一个对象的表达式)。

3、在算术运算中,%为取余,也就是其中的运算对象必须都是整型。在c++11中规定在除法运算中,如果两个对象符号为正,则小数直接删除,如果有一个负值,则结果有小数的,也是一样直接切除小数。而且新标准中(m/n)*n+m%n==m;(-m)/n==m/(-n) ==-(m/n);m%(-n)==m%n;(-m)%n==-(m%n)。

CPP2-基础部分(1)

对于if(var==true)这个来说,会先把true提升到int类型的1,然后在var与1比较,也就是如果var的值是2的话,也是不相等的。所以老手一般写成if(!var)或者直接if(var)。

4、int k=0;然后k={3.14};//错误:窄化转换。如果左侧是内置类型,那么初始值列表最多只有一个值,而且该值即使转换,其所占空间也不能大于目标类型的空间,(个人:double的空间比int的大)。无论左侧的类型是是什么,初始值列表为空的话,编译器都会创建一个初始化的临时值,然后在赋给左侧运算对象(比如类类型)。

5、对于前置自增和后置自增,这两种都必须用于左值运算对象,前置将对象本身作为左值返回,后置的将对象原始值副本作为右值返回。而且对于*p++和*p—来说,是先进行自增自减操作,然后在解引用,不过后置的会返回改变前的值,其实就是相当于先取p指向的值,然后自增自减。

6、要非常注意<<和>>与不同的预算符之间的优先级。比如下面三个就完全不同:

CPP2-基础部分(1)

7、对于符号位如何处理没有明确规定,所以位运算最好都用来处理无符号类型。对于移位来说,右侧的运算对象必须不为负的,而且值必须小于结果的位数。几乎位操作在运算的时候,都会先提升。而且记得移位运算符的优先级就是用作输入输出运算符的优先级。

8、sizeof运算符满足右结合律,所得的值为size_t类型的常量表达式,有两种形式:sizeof (type);sizeof expr。后者是返回表达式结果类型的大小,而且sizeof并不实际计算运算对象的值。所以其expr是否是个有效的指针,是否合法解引用,它并不关心。新标准允许使用作用域运算符获取类成员的大小,所以可以直接sizeof Myclass:ivar(数据成员)。Sizeof 不会将数组转换成指针处理,所以得到的是整个数组的占空间大小。Ps:对string对象或vector对象执行sizeof,只返回该类型固定部分大小,不会计算对象中元素占了多少空间(个人:难道和opencv中固定的部分指的就是矩阵头,而元素对应数据部分,所以这里的结果就类似矩阵头那种固定部分?)。而且返回值可以用作数组的维度声明因为它的返回值是常量。

9、隐式转换:a)大多数表达式中,比int小的整型首先提升到较大的整型;b)条件中,非布尔值转换成布尔值;c)初始化过程中,初始值转换成变量的类型;在赋值中,右侧运算对象转换成左侧运算对象的类型;d)算术运算或关系运算对象有多种类型,先提升到精度较高的同一类型;e)函数调用时,也会发生类型转换。

9.1、算术转换中运算对象将转换成最宽类型,比如有个是long double,那么都会转换到这个类型,如果有浮点数和整型,那么直接转换到浮点类型。整型的提升:bool、char、signed char、unsigned char、short和unsigned short等类型,只要所有可能的值能存放在int中,就会提升成int,否则提升成unsigned int类型;较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned
long、long long和unsigned long long中最小的能够容纳原类型所有可能的值的类型。

9.2、某个运算对象的类型是无符号类型,那么转换的结果会依赖于机器中各个整数类型的相对大小。如果两个运算对象提升后的类型要么都是带符号的,要么都是无符号的,则小类型的运算对象转换成较大的类型。如果一个是无符号,一个是有符号,则无符号的类型不小于带符号类型,那么带符号的运算对象转换成无符号的。比如一个是unsigned int一个是int,那么int转换成unsigned int类型。如果int类型值恰好是负的,那么会带来副作用;当带符号的大于无符号的,此时转换依赖于机器,如果无符号类型的所有值都能存放在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别为long和unsigned
int,并且int和long的大小一样,那么long转换成unsigned int;如果long占用的比int多,那么unsigned int类型转换成long类型。

9.3、当数组用在decltype、取地址(&)、sizeof和typeid中都不会转换成指针。指向任意非常量的指针能转换成void*,指向任意对象的指针能转换成const void*。在强制类型转换中,形式如下:cast-name<type>(expression);type是转换的目标类型,expression是要转换的值,如果type是引用类型,则结果是左值。Cast-name可以是:static_cast、dynamic_cast、const_cast、reinterpret_cast中的一种。第二个支持运行时类型识别。任何具有明确定义的类型转换,只要不包含底层const,则可以使用第一个。而且在编译器无法自动执行的类型转换也很有用,比如:double
d = 1.0;void *p = &d;double *dp = static_cast<double*>(p);对于第三个来说,只能改变运算对象的底层const:

CPP2-基础部分(1)

如果对象本身不是一个常量,使用这个可以获得写权限是合法行为;如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,不能用const_cast改变表达式的类型:

CPP2-基础部分(1)

这个常常用于函数重载的上下文中;而对于最后一个转换,通常为运算对象的位模式提供较低层次的重新解释。假设有如下转换:

CPP2-基础部分(1)

我们必须记得pc指向的真实对象其实是个int而非字符,如果将pc当初字符指针使用就存在运行时发生错误:string str(pc);使用这个转换是非常危险的,因为类型变了,而编译器没提示,而且这其实是不对的,使用这个转换必须对类型和编译器实现转换的过程都非常了解才行。早期的cpp标准中,有两种显示转换形式:type (expr);(type)expr。旧的对应着const_cast、static_cast、reinterpret_cast相似的行为。如果换成前面两个合法,则行为对应,如果不合法就是对应第三个reinterpret_cast。比如char*pc
= (char*) ip;//ip是指向整数的指针.这时候使用的类似第三个。

10、运算符优先表:

CPP2-基础部分(1)

CPP2-基础部分(1)

四、语句

1、对于switch,语句首先对括号里面的表达式求值,该表达式紧跟switch后面,可以是一个初始化过的变量声明,表达式的值转换成整数类型,然后与每个case 标签的值比较:switch (ch){case  ‘a’: /*操作*/;break;default:break;}。case关键字和他对应的值一起被称为case标签。Case标签必须是整型常量表达式;case 3.14://错误,case标签不是个整数;case ival(前面int ival =42)://错误,case标签不是个常量。。。标签不应该孤零零的出现,后面必须有一条语句或者另外一个case标签,所以default后面必须有东西,如果没有也得写个空语句(;)或者一个空块({})。

CPP2-基础部分(1)

1.1、如果在switch中某处一个带有初值的变量位于作用域之外,另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法的:

CPP2-基础部分(1)

上面的语句是非法的,下面的才是合法情况:

CPP2-基础部分(1)

类似于自产自销,不外流给其他case。

2、传统的for语句,中间的条件部分,如果第一次求值结果为false、,那么for语句一次都不会执行。对于新式的for,其中for(declaration:expression)statement;中expression可以是花括号的初始值列表、数组、vector或者string等类型的对象。它们的共同点就是拥有能返回迭代器的begin和end成员。

3、do{   statement   } while( condition);    。记得最后的分号。使用的判定循环变量必须定义在do外面,不能在里面。

4、goto语句:goto label;很多行 label: 语句;。这里的“label:语句”中的label叫做标签标识符,独立于变量和其他标识符的名字,所以何以和程序中其他实体的标识符使用相同的名字,而且不会互相干扰。goto语句和控制权转向的那条带标签的语句必须位于同一个函数之内。同样和switch一样,不能往前跳过定义语句而直接使用,不过往后还是可以的,因为这表示销毁该变量(所以不是重定义):

CPP2-基础部分(1)

CPP2-基础部分(1)

5、异常处理机制为程序中异常检测和异常处理这两个部分:a)throw表达式,异常检测部分使用throw表达式来表示遇到的问题,也就是throw抛出了异常;b)try语句块,异常处理部分使用try语句块处理异常。以关键字try开始,然后一个或多个catch子句结束。Try语句块中代码抛出的异常通常会被某个catch子句处理。C)一套异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。

5.1、throw表达式包含关键字throw和紧跟的一个表达式,其中表达式的类型就是抛出的异常类型。表达式后面紧跟一个分号,构成一条语句:

CPP2-基础部分(1)

runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。必须初始化这个对象,所以这里传递了个字符串。

5.2、try语句块的通用语法形式:

CPP2-基础部分(1)

比如上面的是Sales_item的对象加法代码,在某个地方,而在与用户交互的代码中,负责处理发生的异常:

CPP2-基础部分(1)

CPP2-基础部分(1)

比如上面的try中,就可以调用之前的item相加的函数代码,然后抛出异常返回上一层,也就是try。每个异常类的都有一个成员函数what(),返回的类型是const char*,也就是c风格的字符串。不过runtime_error的what返回的是string对象的副本。当异常抛出时,首先搜索抛出该异常的函数,如果没有匹配的catch子句,则终止该函数,并在调用该函数的函数中继续寻找,如果还没有,则终止该函数,接着往上,最后到了上无可上的时候,调用terminate的标准库函数,启动系统异常。

5.3、标准异常。通常在4个头文件中有标准库定义好的一组类:a)exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外的信息;b)stdexcept头文件定义了几种常用的异常类;c)new头文件定义了bad_alloc异常类型;d)type_info头文件定义了bad_cast异常类型:

CPP2-基础部分(1)

标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。对于exception、bad_alloc和bad_cast对象,不允许为这些对象提供初始值;而其他的需要使用string或者c风格的字符串初始化,但不允许使用默认初始化的方式。所以对于前者,what的返回内容由编译器决定。

五、函数

1、执行函数的第一步就是(隐式的)定义并初始化它的形参为实参的值。其中实参的求值顺序并没有规定,而且实参的类型必须与对应的形参类型匹配。

2、函数最外层作用域中的局部变量不能使用与函数形参一样的名字。形参和函数体内部定义的变量统称为局部变量。函数的返回类型不能是数组类型或者函数类型,不过可以是指向数组或函数的指针。而且在函数内部,内置类型的未初始化局部变量产生未定义的值。

3、函数声明也叫做函数原型。其中形参的名字可以省略。记得含有函数声明的头文件应该被包含到定义函数的源文件中。在参数传递的时候,有引用传递和值传递两种,前者是会改变实参本身,后者只是复制实参的值而已。而且当参数是指针的时候,指针作为一个类型,如果不加引用,也是会重新创建一个实参指针的副本,只是这个副本中的地址值为实参指针中的地址值。而且在传递例如类类型的时候,使用引用是极好的,这样避免了较大的复制操作,如果确定不会改变该实参,可以将该形参设定为常量引用。

4 、正如本书的2.4.3节的顶层const的讨论,顶层const作用于对象本身:

CPP2-基础部分(1)

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const,也就是形参的顶层const被忽略掉了,当形参有顶层const时,传递给它常量对象或者非常量都是可以的。(这里针对的是const与否,没有结合引用的时候,结合了引用的const就不一样了,因为用于声明引用的const都是底层const。)。

CPP2-基础部分(1)

所以上面的例子就成了重定义的形式了,因为参数上忽略了const,所以完全相同的函数原型。只是对于内部来说第一个参数内部无法改变i的值,而第二个可以,不过在接收实参上是一视同仁的。

4.1、在对形参的初始化方式上其实和变量的初始化方式是一样的,可以使用非常量初始化一个底层const对象,可是反过来就不行了,而且对于一个非常量的引用来说,也必须使用同类型的对象初始化:

CPP2-基础部分(1)

对应到形参的初始化上为:

CPP2-基础部分(1)

其中函数原型为:void reset(int &i );

要想调用引用版本的reset只能使用int类型的对象,而不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象,因为都不匹配。所以要想调用指针版本的reset,也只能使用int*。

4.2、c++允许将变量定义成数组的引用,即int (&arr)[10],记得这里的圆括号不能省,不然就是非法的引用数组,这并不存在。同样的在传递参数多维数组的时候可以void print(int matrix[][10]),其中除了第一维,后面的所有维度都不能省略,这里表达的是形参为指向含有10个整数的数组的指针。

4.3、为了能编写处理不同数量实参的函数,新标准提供了两种主要的方法:a)如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;b)如果实参的类型不同,可以编写一种特殊的函数,也就是可变参数模板,在后面介绍。不过c++还有一种特殊的形参类型,即省略符,可以用它传递可变数量的实参,不过这种功能一般只用于与c函数交互的接口程序。Initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,该类型定义在同名的头文件中,它的操作如下:

CPP2-基础部分(1)

和vector一样,它也是个模板类型,定义的时候必须说明所含元素的类型:不过与vector不同的是,它对象中的元素永远是常量值,所以无法改变。可以如下来编写错误信息输出的函数:

CPP2-基础部分(1)

当然了,如果想要调用这个函数,也就是给形参赋值,那么必须把序列放在一对花括号中,比如:error_msg({“functionX”,”okay”,”no error”})。对于省略符形参来说,是为了便于c++程序访问某些特殊的c代码而设置的,使用了名为varargs的c标准库功能。通常,省略符形参不应该用于其他目的,仅仅用于c和c++通用的类型。特别值得注意的是,大多数类类型的对象在传递给省略符形参时都无法正确的复制。这种类型的函数形式差不多都是这两种:

CPP2-基础部分(1)

具体的可查阅c语言的这个标准库资料。

5、对于函数的返回值来说,返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果,最后将该临时量赋值给调用该函数的左值。C++新标准规定,函数可以返回花括号包围的值的列表,此次的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。不过,如果函数返回的是内置类型,而不是类类型,那么花括号包围的列表最多包含一个值,而且该值所占空间不应大于目标类型的空间(比如上面的窄化转换错误)。对于main函数,cstdlib头文件定义了两个预处理变量,EXIT_FAILURE;EXIT_SUCCESS,用来作为return的表达式参数。

5.1、而且因为数组不能被复制,所以函数无法返回数组,不过可以返回数组的指针或引用:比如int (*func(int i))[10],表示的就是一个参数为int的函数,该函数返回的是一个维度为10的数组指针。C++11标准引入了新的叫做尾置返回类型,任何函数的定义都能使用尾置返回,不过这种形式对返回类型比较复杂的函数最有效,比如数组的指针或者数组的引用:

CPP2-基础部分(1)

因为把函数的返回类型放在了形参列表之后,所以可以很清楚的看到func函数返回的是一个指针,该指针指向含有10个整数的数组。或者使用decltype,这是知道函数返回的指针将指向哪个数组的时候,比如下面:

CPP2-基础部分(1)

arrPtr使用关键字decltype表示它的返回类型是个指针,该指针所指的对象与odd的类型一致,不过decltype并不负责把数组类型转换成对应的指针,所以返回的结果是个数组,要想表达返回的是指针,还需要显式的加个*。

6、重载函数只有形参的数量和类型上不同才能定义。而且顶层const不影响传入函数的对象,所以这个是无法区分的。而对于形参是某种类型的指针或引用,那么区分其是常量对象还是非常量对象可以实现重载,这时候const是底层的。

6.1、对于之前的const_cast在重载函数的情景中最有用,比如下面的函数:

CPP2-基础部分(1)

如果想写它的重载版本,也就是形参是非const的,可是想返回的结果是个普通的引用,而不是const引用:

CPP2-基础部分(1)

上面这个重载函数中,调用了之前的那个版本。

6.2、在cpp中,名字查找发生在类型检查之前。

7、对于函数的声明来说,通常放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的,不过在给定的作用域中一个形参只能被赋予一次默认实参。也就是函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右边所有形参必须都有默认值。用作默认实参的名字在函数声明所在的作用域内解析,而且名字的求值过程发生在函数调用时。比如在开始先int  wd = 80; char def =‘ ’;int ht();然后是函数原型string screen(int = ht(), int
= wd, char = def);按照上面的原则在一个函数中(下面using sz = int;):

CPP2-基础部分(1)
CPP2-基础部分(1)
CPP2-基础部分(1)

7.1、constexpr函数是只能用于常量表达式的函数,这种函数的定义的要求有:函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句:

CPP2-基础部分(1)

编译器把对constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,该函数被隐式的指定为内联函数。该函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行,比如空语句、类型别名以及using声明等。当然可以返回的不是常量:

CPP2-基础部分(1)

只是这时候对于intarr[scale[i]]这个就会报错,如果是int arr[scale[2]],这是不会报错的。Ps:内联函数和constexpr函数可以在程序中多次定义,不过这多次定义必须完全一致,所以一般来说是完整的定义放在头文件中,不声明和定义分离的。

8、assert是一种预定义宏,行为就不说了,这个宏依赖于NDEBUG这个预处理变量的状态,如果定义了NDEBUG那么assert宏就什么都不做,默认状态下是没定义的,这个可以用来在代码发布的时候进行屏蔽测试代码。编译器为每个函数都定义了变量__func__用以输出当前的函数的名称:直接在函数中cout<<__func__(个人:在vs2010中未通过),预处理器还定义了另外4个有用的名字:

CPP2-基础部分(1)

9、对于能够容易区分的函数匹配来说,就不介绍了,对于那些形参可以转换的函数匹配来说,还是不那么容易的。a)首先,选定调用对应的重载函数集,集合中的函数称为候选函数,只要当前可见,同名就都收集起来;b)宣传形参数量与实参数量相等而且类型相同或者能够转换的可行函数集合;c)寻找最匹配函数;d)在没有最匹配的时候,寻找某个函数的每个实参的匹配都不劣于其他可行函数或者至少有一个实参的匹配优于其他可行函数;e)编译器报二义性错误。其中在最匹配阶段,编译器将按照下面的顺序来决定哪个函数:a)实参类型和形参类型完全相同、数组类型或函数类型转换成指针类型、添加或删除顶层const;b)通过const转换实现的匹配;c)类型提升实现的匹配;d)算术类型转换(所有的算术类型的转换级别都一样,不会因为这个转换成int,那个long就不一样)或者指针转换实现的匹配;e)类类型转换实现的匹配。

10、指向不同函数的指针之间不能转换,这里涉及的有形参的数量,类型和返回值类型。函数指针类型与函数之间必须精确匹配,这里不支持形参的类型的升级或者转换。虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,也就是形参是函数类型,实际上是转换成指针的:

CPP2-基础部分(1)

上面的函数原型中第三个参数看起来太繁琐了,可以采用类型别名的方式:

CPP2-基础部分(1)

CPP2-基础部分(1)

其中lengthCompare是个函数名。decltype不会自动转换成指针,所以后面需要显式加上*。不过要注意的是在返回值部分,编译器不会自动的将函数类型转换成指针,所以这个位置必须显式的转换成函数指针作为新函数的返回值类型声明:

CPP2-基础部分(1)

CPP2-基础部分(1)

上面的f1可以直接写出下面形式:

CPP2-基础部分(1)

CPP2-基础部分(1)

2015年09月05日 第0次修改!