C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20;// 20 是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一个正确的声明语句
一般来说,如果认定变量是一个常量表达式,那就把它声明成constexpr类型。
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为顶层const。
constexpr int *np = nullptr;// np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; // i的类型时整型常量
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j;// p1是常量指针,指向整数j
类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_item; // SI是Sales_item的同义词
这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
const是对给定类型的修饰,pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。
auto item = val1 + val2; // item初始化为val1和val2相加的结果
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
· 使用引用其实是使用引用的对象,特别是当引用被用作初始值时,auto为引用对象的类型;
· auto一般会忽略掉顶层const,同时底层const则会保留下来。
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略掉)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整型指针
auto e = &ci; // e是一个指向整型常量的指针(对常量对象取地址是一种底层const)
有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype (f()) sum = x; // sum的类型就是函数f的返回类型
decltype处理顶层const和引用的方式与auto有些许不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
需要指出的是,引用从来都作为其所指对象的同义词出现,只有在decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式讲decl返回一个引用类型,一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
int i = 42, *p = &i, &r = i;
decltype (r + 0) b; // 正确,加法的结果时int,因此b是一个(未初始化的)int
decltype (*p) c; // 错误,c时int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关,有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上一对括号,则得到的类型与不加括号时会有不同。
int i;
decltype ((i)) d; // 错误:d是int&,必须初始化
decltype (i) e; // 正确:e是一个(未初始化的)int
切记:decltype((v)) (注意是双层括号)的结果永远是引用,而decltype(v)结果只有当v本身就是一个引用时才是引用。
确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor).它由C++语言从C语言继承而来,预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。
C++程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定预处理变量是否已经定义;#ifdef当且晋档变量已定义时为真,#ifndef当且仅当变量未定义时为真,一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。
预处理变量无视C++语言中关于作用域的规则。
头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要。
C++标准一方面对库类型所提供的操作做了详细规定,另一方面也对库的实现者做出一些性能上的需求。因此,标准库类型对于一般应用场合来说有足够的的效率。
拷贝初始化(copy initialization)和直接初始化(direct initialization)
string s1 = "hihihi'; // 拷贝初始化
string s2 ("ddddd"); // 直接初始化
string s3(10,‘c’); // 直接初始化
getline(cin, strLine)
触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。
auto len = str.size()
如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
建议:使用C++版本的C标准库头文件
C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname,也就是去掉了.h后缀,而在文件名name之前添加了字母c,这里的c表示这是一个属于C语言的彼岸准哭的头文件。
因此,cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中则不然。
一般来说,C++程序应该使用名为cname的头文件而不是使用name.h的形式,标准库中的名字总能在命名空间std中找到。如果使用.h形式的头文件,程序员就不得不时刻牢记那些是从C语言那儿继承过来的,哪些又是C++语言所独有的。
string对象的下标必须大于等于0而小于s.size().
使用超出此范围的下标将引发不可预知的结果,以此推断,使用下标访问空string也会引发不可预知的结果。
提示:注意检查下标的合法性
使用下标时必须确保其在合理范围之内,也就是说,下标必须大于等于0而小于字符串的size()的值。一种简单易行的方法是,总是设下标的类型为string::size_type,因为此类型是无符号数,可以确保下标不会小于0.此时,代码只要保证下标小于size()的值就可以了。
- C++标准并不要求标准库检查下标是否合法。一旦使用了一个超出范围的下标,就会产生不可预知的结果。
C++语言既有类模板(class template),也有函数模板,其中vector是一个类模板。
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要提出编译器应把类或函数实例化成何种类型。
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector
。 某些编译器可能仍需要以老式的圣铭渔具来处理元素未vector的vector对象,如vector
>
关键概念:vector对象能高效增长
C++标准要求vector应该能在运行时高效快速地添加元素。因此既然vector对象能高效地增长,那么在定义vector对象的时候设定其大小也就没什么必要了,事实上如果这么做性能可能更差。只有一种例外情况,就是所有元素的值都一样。一旦元素的值有所不同,更有效的办法是先定一个空的vector对象,再在运行时向其中添加具体值。此外,vector还提供了方法,允许我们进一步提升动态添加元素的性能。
开始的时候创建空的vector对象,在运行时再动态添加元素,这一做法与C语言及其他大多数语言中内置数组类型的用法不同。特别是乳沟用惯了C或者Java,可以预计在创建vector对象时顺便指定了其容量是最好的,然而事实上,通常的情况恰恰相反。
- 范围for语句体内不应该改变其所遍历序列的大小。
- 要使用size_type,需要先指定它是由那种类型定义的。vector对象的类型总是包含着元素的类型。(vector
::size_type) - vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。如下语句:
auto b = v.begin(), e = v.end();
end 成员则负责返回指向容器(或string对象)“尾元素的下一个位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符 | |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指示容器中的下一个元素 |
--iter | 令iter指示容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等 |
iter1 != iter2 | 判断两个迭代器是否不相等 |
因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。
关键概念:泛型编程
原来使用C或Java的程序员在转而使用C++语言之后,会对for循环使用!=而非<进行判断有点奇怪,比如上面的这个程序。C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样;因为这种编程风格在标准库提供的所有容器上都有效。
只有string和vector等一些标准库类型有下标运算符,而并非全部如此。与之类似,所有标准库容器的迭代器都定义了==和!=的习惯,就不用太在意用的到底是那种容器类型。
术语:迭代器和迭代器类型
迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。
重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另一个元素。
每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的的一套操作。
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
iter + n | 迭代器加上一个整数值仍是一个迭代器,向后移动n个元素 |
---|---|
iter-n | 向前移动n各元素 |
iter1 += n | 迭代器加法的复合赋值语句,将iter1加n的结果赋值给iter1 |
iter1 -= n | 迭代器减法的复合赋值语句,将iter1减n的结果赋值给iter1 |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离 |
> >= < <= | 迭代器的关系运算符 |
如果不清楚元素的确切个数,请使用vector。
数组是一种复合类型。数组的声明升入a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0,。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42;// 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[int]; // 错误,cnt不是常量表达式
string strs[get_size()]; // 当get_size()是constexpr时正确,否则错误
默认情况下,数组的暗元素呗默认初始化。
和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
显示初始化数组元素:数组的维度要大于等于初始值的总数量。
字符数组的特殊性:字符串自勉之的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去。
不允许拷贝和赋值:不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a[] = {};// 含有3个整数的数组
int a2[] = a;// 错误,不允许使用一个数组初始化另一个数组
a2 = a;// 错误,不能把一个数组直接赋值给另一个数组
一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正
常工作。
理解复杂的数组声明
和vector一样,数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组,又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一些了。
int *ptrs[10]; // ptrs是含有10个整数指针的数组
int &refs[10] = /* ? */; // 错误,不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
检查下标的值
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类型数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
指针和数组
在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。
标准库函数begin和end
尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、跟安全,C++11新标准引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。
int ia[] = {0,1,2,3,4,5,6,7,8,9};// ia是一个含有10个整数的数组
int *beg = begin(ia); // 指向ia首元素的指针
int *end = end(ia); // 指向ia尾元素的下一位置的指针
// 寻找第一个负值元素,如果已经检查完全部元素则结束循环
while(beg != end && *beg >= 0)
{
++beg;
}
一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与vector的end函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。
内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。
int ia[] = {0,1,2,3,4};
int *p = &ia[2]; // p指向索引为2的元素
int j = p[1];// p[1]等价于 *(p+1),就是ia[3]表示的那个元素
int k = p[-2]; // p[-2]是ia[0]表示的那个元素
C风格字符串
尽管C++支持C冯哥字符串,但在C++程序中最好还是不要使用他们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
字符串字面值是一种通用结构的示例,这种结构即使C++由C继承而来的C风格字符串。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按照习惯书写的字符串存放在字符数组中并以空字符结束。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符('\0')。一般利用指针来操作这些字符串。
C标准库String函数
strlen(p) | 返回p的长度,空字符不计算在内 |
---|---|
strcmp(p1,p2) | 比较p1和p2:相等,返回0;大于,返回正值;否则,负值 |
strcat(p1,p2) | 将p2附加到p1之后,返回p1 |
strcpy(p1,p2) | 将p2拷贝给p1,返回p1 |
上述函数不负责验证其字符串参数。 |
对大多数应用来说,使用标准库string要比使用C风格字符串更安全,更高效。
混用string对象和C风格字符串
如果执行完.c_str()函数后程序想一直都是用其返回的数组,最好将该数组重新拷贝一份。
使用数组初始化vector对象
int int_arr[] = {0,1,2,4,5,6,6,6,6};
vector ivec(begin(int_arr),end(int_arr));
vector subvec(int_arr + 1, int_arr + 4);
建议:尽量使用标准库类型而非数组
使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用语底层操作,因此容易引发一些与繁琐细节有关的错误。其它问题则源于语法错误,特别是声明指针时的语法错误。
现代的C++程序应当尽量使用vector和迭代器,避免使用向量数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
使用范围for语句处理多维数组
size_t cnt = 0;
for(auto &row : ia)
{
for(auto &col : row)
{
col = cnt;
cnt++'
}
}
要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。