C++1-基本数据类型-字面值常量-变量-标识符-作用域-复合类型(引用/指针)-const限定符-对类型的处理(类型别名/auto/decltype)-自定义数据结构(struct/头文件/预处理器)-字符串-容器-迭代器-数组
1,基本数据类型
- C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。
- 其中算术类型包含了整型数、浮点数、字符和布尔值
- 空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
- 为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容。
1-1,算术类型
- 算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
- 算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。C++标准规定了尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围也不一样。
类型 | 含义 | 最小尺寸 |
---|---|---|
short | 短整型 | 16位,2字节 |
int | 整型 | 16位,2字节 |
long | 长整型 | 32位,4字节 |
long long | 长整型 | 64位,8字节 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
char | 字符 | 8位,1字节 |
wchar_t | 宽字符 | 16位,2字节 |
char16_t | unicode字符 | 16位,2字节 |
char32_t | unicode字符 | 32位,4字节 |
bool | 布尔类型 | 未定义 |
- 除字符和布尔类型之外,其他整型用于表示(可能)不同尺寸的整数。C++语言规定一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大(在不同的机器上同一个数据类型的尺寸可能不同)。其中,数据类型long long是在C++11中新定义的。
- 通常,float以1个字(32比特)来表示,double以2个字(64比特)来表示,long double以3或4个字(96或128比特)来表示。一般来说,类型float和double分别有7和16个有效位;类型long double则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同。
- 基本的字符类型是char,一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,一个char的大小和一个机器字节一样。
- 其他字符类型用于扩展字符集,如wchar_t、char16_t、char32_t。wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型char16_t和char32_t则为Unicode字符集服务。
- 除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。类型int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int可以缩写为unsigned。
- 与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
- 切勿混用带符号类型和无符号类型。如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。
- 计算前带符号类型就会自动转换成无符号类型,当带符号类型取值为负时就会出现异常结果。
2,字面值常量
- 一个形如42的值被称作字面值常量(literal),这样的值一望而知(魔数?)。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。(给变量赋值的值)
- 可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。
- 整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的。
- 十进制字面值的类型是int、long和long long中尺寸最小的那个(例如,三者当中最小是int),当然前提是这种类型要能容纳下当前的值。
- 八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和unsigned long long中的尺寸最小者。
- 如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。类型short没有对应的字面值。
- 浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。
- 默认的,浮点型字面值是一个double。
- 由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
- 字符串字面值的类型实际上是由常量字符构成的数组(array),编译器在每个字符串的结尾处添加一个空字符(′\0′),因此,字符串字面值的实际长度要比它的内容多1。
- 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。当书写的字符串字面值比较长,写在一行里不太合适时,就可以采取分开书写的方式。
- 转义序列
- 有两类字符程序员不能直接使用:一类是不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符;另一类是在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下需要用到转义序列(escape sequence),转义序列均以反斜线作为开始。
- 我们也可以使用泛化的转义序列,其形式是\x后紧跟1个或多个十六进制数字,或者\后紧跟1个、2个或3个八进制数字,其中数字部分表示的是字符对应的数值。
- 如果反斜线\后面跟着的八进制数字超过3个,只有前3个数字与\构成转义序列;而\x要用到后面跟着的所有数字。
- 布尔字面值
- true和false是布尔类型的字面值
3,变量
- 每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
- 变量声明和定义的关系
- 为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
- 为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
- 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
- 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量。
- 任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义.
- 在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
- 变量能且只能被定义一次,但是可以被多次声明。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
4,标识符
- C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
- 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
5,作用域
- 不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同实体。
- 作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
- 同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
- 作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。
- 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字
6,复合类型
- 复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,其中比较常见的是:引用、指针。
- 一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
- 声明符如果只是变量名,此时变量的类型也就是声明的基本数据类型。其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
6-1,引用&
- 引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成
&d
的形式来定义引用类型,其中d是声明的变量名。 - 概念:
- 一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。
- 一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
- 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
- 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的.
- 定义:
- 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号
&
开头。 - 几乎所有引用的类型都要和与之绑定的对象严格匹配;而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起;而且,引用必须基于某个对象进行初始化。
- 因为引用本身不是一个对象,所以不能定义引用的引用。
- 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号
- 例子:
#include<iostream> void exchange(int a, int b){ int middle = a; a = b; b = middle; } void exchange2(int &a, int &b){ int middle = a; a = b; b = middle; } int main(){ int a = 10; int b = 20; exchange(a,b); std::cout<<a<<std::endl; std::cout<<b<<std::endl; int c = 30; int d = c; int &e = c; std::cout<<c<<" "<<d<<" "<<e<<" "<<std::endl; c = 40; std::cout<<c<<" "<<d<<" "<<e<<" "<<std::endl; d = 50; std::cout<<c<<" "<<d<<" "<<e<<" "<<std::endl; e = 60; std::cout<<c<<" "<<d<<" "<<e<<" "<<std::endl; int &m = a; int &n = b; std::cout<<m<<" "<<n<<std::endl; exchange2(m, n); std::cout<<m<<" "<<n<<std::endl; std::cout<<a<<" "<<b<<std::endl; }
6-2,指针*
- 指针(pointer)是“指向(point to)”另外一种类型的复合类型。
- 概念:
- 与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
- 定义:
- 定义指针类型的方法将声明符写成
*d
的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*
- 指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符
&
,与引用不同,等号左边是引用,右边是取地址操作符),比如int val=1; int *p = &val;
- 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
- 几乎所有指针的类型都要和它所指向的对象严格匹配
- 指针可以只定义而不初始化,可以用一个指针初始化另一个指针,可以用取地址操作符+对象初始化指针
- 定义指针类型的方法将声明符写成
- 指针值:
- 指针的值(即地址)应属下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
- 试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。
- 尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象,所以试图访问此类指针(假定的)对象的行为不被允许。如果这样做了,后果也无法预计。
- 指针的值(即地址)应属下列4种状态之一:
- 利用指针访问对象:
- 解引用操作仅适用于那些确实指向了某个对象的有效指针。
- 如果指针指向了一个对象,则允许使用解引用符(操作符
*
)来访问该对象:#include<iostream> int main(){ int val=1; int *p = &val; std::cout<<p<<std::endl; std::cout<<*p; }
- 对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值
#include<iostream> int main(){ int val=1; int *p = &val; *p = 2; std::cout<<*p<<" "<<val; }
- 指针的指针
- 指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
- 通过
*
的个数可以区分指针的级别。也就是说,**
表示指向指针的指针,**
表示指向指针的指针的指针,以此类推。 - 对于指针的指针,也需要做两次解引用
**
,以此类推。
6-3,引用与指针对比
- 指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
- 指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
- 引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
#include<iostream>
int main(){
int i = 42;
int *p; //p是一个int型的指针
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0
std::cout<<i<<" "<<*p<<" "<<*r;
}
- 对
*&r
的分析:要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r
的符号&
)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*
说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
7,const限定符
- const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
- 对象的类型决定了其上的操作。与非const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。
- 默认状态下,const对象仅在文件内有效:编译器将在编译过程中把用到const变量的地方都替换成对应的值,为了该替换,编译器必须知道变量的初始值;如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行,要做到这一点,就必须在每一个用到变量的文件中都有对它的定义;当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
- 如果想要只在一个文件中定义const,而在其他多个文件中声明并使用它,那么对于该const变量不管是声明还是定义都添加extern关键字。
extern const int i;
,此时i的赋值是在别的文件中完成的。 - 如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
- 如果想要只在一个文件中定义const,而在其他多个文件中声明并使用它,那么对于该const变量不管是声明还是定义都添加extern关键字。
- 常量与引用
- 非常量引用不能指向常量对象,对常量的引用不能被用作修改它所绑定的常量对象
#include<iostream> int main(){ const int i = 2; const int &j = i; //多个修饰符从右往左看,先是引用,然后int,最后常量 // j = 3; //报错,对常量的引用不能被用作修改它所绑定的常量对象 // int &m = i; //报错,非常量引用不能指向常量对象 }
- 严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
- 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。(底层通过中间临时常量实现,这种情况下常量引用绑定了一个临时量对象,所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。)
const int &i = 2;
等价于const int temp = 2; const int &i = temp;
;但是当引用不是常量时,这种初始化又是非法的int &m = 2;
会报错- 上面这种操作又会引发新的问题:常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。
#include<iostream> int main(){ int i = 0; int &r1 = i; const int &r2 = i; std::cout<<i<<" "<<r1<<" "<<r2<<std::endl; r1 = 1; //也改变了r2的值,只是不允许通过r2进行改变,这个底层还是很混乱啊。。。 std::cout<<i<<" "<<r1<<" "<<r2; }
- 上面这种操作又会引发新的问题:常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值。
- 非常量引用不能指向常量对象,对常量的引用不能被用作修改它所绑定的常量对象
- 常量与指针
- 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针 。
#include<iostream> int main(){ const int i = 1; const int *p = &i; // *p = 2; //报错,常量指针不能用于修改其所指向的对象的值 }
- 允许令一个指向常量的指针指向一个非常量对象。(和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。)
- 指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量-常量指针。
const double pi=3.1415926; const double *const P = π //含义是P永远指向pi(*const P的作用,顶层const),并且不同通过P改变pi的值(const double,底层const)
- 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。
- 指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量。
- 用于声明引用的const都是底层const
- 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针 。
- 常量表达式
- 常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。
- C++11规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
8,对类型的处理
- 随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面。
- 一是一些类型难于“拼写”,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。
- 二是有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文中寻求帮助。
8-1,类型别名:
- 类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
- 定义方法
typedef
,比如:typedef double wages; typedef wages base, *p;
,wages, base是double的类型别名,p为double*的类型别名- 别名声明:
using wages = double
8-2,auto
- 实现编译器帮助自动推定变量的类型。
- auto定义的变量必须有初始值。
- 使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。
8-3,decltype
- 有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。比如
decltype(f()) sum=x; //编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。
9,自定义数据结构
9-1,关键词struct
- 自定义数据结构以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少。
#include<iostream>
struct Test{
std::string name="test";
unsigned age = 18;
};
int main(){
Test test;
std::cout<<test.name<<" "<<test.age<<std::endl;
test.name = "lala";
std::cout<<test.name<<" "<<test.age;
}
9-2,头文件
- 尽管可以在函数体内定义类,但是这样的类毕竟受到了一些限制。所以,类一般都不定义在函数体内。
- 当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。
- 为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
- 头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。
- 头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
9-3,预处理器
- 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。
- 预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。比如一项预处理功能
#include
,当预处理器看到#include
标记时就会用指定的头文件的内容代替#include
。 - C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。
#define
指令把一个名字设定为预处理变量#ifdef
当且仅当变量已定义时为真#ifndef
当且仅当变量未定义时为真- 一旦检查结果为真,则执行后续操作直至遇到
#endif
指令为止。 - 预处理变量无视C++语言中关于作用域的规则
10,字符串
- 可变长的字符序列-string,使用string类型必须包含string头文件,string定义在命名空间std中
- 初始化方法
string i = "my";
string i = string("my");
string i = string(10, 'm');
- 操作
os<<s 将s写出到输出流中,返回os
is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,s) 从is中读取一行赋给s,返回is
s.empty() s为空返回true,否则返回false
s.size() 返回s中字符的个数,size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。比如:假设n是一个具有负值的int,则表达式s.size()<n的判断结果几乎肯定是true,这是因为负值n会自动地转换成一个比较大的无符号值。
s[n] 返回s中第n个字符的引用,下标从0开始
s1+s2 返回s1和s2拼接后的结果
s1=s2 用s2的副本代替s1中原来的字符
s1==s2 s1!=s2 判断s1和s2所含的字符是否完全相同/不同,string对象的相等性判断对字母大小写敏感
< <= > >= 字典序比较大小,对大小写敏感
- 使用范围for语句改变字符串中的字符
- 如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。
- 代码
int main(){ string line = "something"; for(auto &c: line){ c = toupper(c); } cout<<line; }
11,容器
11-1,某种给定类型对象的数据序列-vector
- 标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
- 使用vector类型必须包含vector头文件,vector定义在命名空间std中。
#include <vector>
- 初始化
- vector为类模板,实例化时需要在类模板名字后面跟一对尖括号,在括号内放上信息。以vector为例,提供的额外信息是vector内所存放对象的类型。
vector<T> v1 vector<T> v2(v1) 等价于 vector<T> v2=v1 v2中包含有v1所有元素的副本 vector<T> v3(n,val) v3中包含有n个重复的元素,每个值都是val vector<T> v4(n) v4中包含了n个重复地执行了值初始化的值 vector<T> v5{a,b,c,...} 等价于 vector<T> v5={a,b,c,...} v5包含了初始值个数的元素,每个元素被赋予相应的初始值
- 操作
- 增:
push_back
- 删:
- 改:
v[n]=m
,n可为负值表示倒着计数 - 查:
v[n]
,n可为负值表示倒着计数 v.empty()
,v不包含元素返回真,否则返回假v.size()
,返回v中元素的个数v1=v2 v1={a,b,c...}
,用v2/列表中的元素拷贝替换v1中的元素v1==v2
,当且仅当元素数量相同且对应位置的元素都相同v1!=v2
> < >= <=
,字典序排序,如果两个vector对象的容量不同,但是在相同位置上的元素值都一样,则元素较少的vector对象小于元素较多的vector对象;若元素的值有区别,则vector对象的大小关系由第一对相异的元素值的大小关系决定。
- 增:
- vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素,添加超出已有索引范围的元素需要使用push_back。
- 试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。
- 不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致PC及其他设备上应用程序出现安全问题的一个重要原因。
12,迭代器
- 所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。(string对象不属于容器类型,但是string支持很多与容器类型类似的操作,string对象也同时支持迭代器和下标运算符)
- 获取迭代器
- 有迭代器的类型同时拥有返回迭代器的成员;这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器,end成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器。
auto b = v.begin(), e = v.end(); //返回迭代器类型,可读可写
auto b = v.cbegin(), e = v.cend(); //返回常量迭代器类型,只读不写
- 所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符,这就导致了以下类型的代码:
#include <iostream>
#include <vector>
using namespace std;
int main(){
string s = "some thing";
if(s.begin()!=s.end()){
auto it = s.begin();
while(it!=s.end()){
*it = toupper(*it);
it++;
}
}
cout<<s;
}
#include <iostream>
#include <vector>
using namespace std;
int main(){
string s = "some thing";
for(auto it=s.begin(); it!=s.end()&&!isspace(*it); it++){
*it = toupper(*it);
}
cout<<s;
}
- 运算
*iter 解引用,返回迭代器iter所指对象的引用 iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem,比如 iter->empty() 等价于 (*iter).empty() 判断iter所指元素是否为空 ++iter 令iter指示容器中的下一个元素 --iter 令iter指示容器中的上一个元素 iter1==iter2 判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素或者同一个容器的尾后end迭代器,则相等;否则不等 iter1!=iter2 vector和string迭代器支持的运算 iter+n 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。 iter-n iter+=n iter-=n iter1-iter2 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。 > < >= <= 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
- 注意:没有迭代器与迭代器的加法运算;有的是迭代器与迭代器的减法(得到距离),以及迭代器与整数的加减运算(控制迭代器左右运动)。
- 迭代器类型
- 拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型,其中iterator能够读写,const_iterator只能读不能写;如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator。
- 迭代器的限制
- 不能在范围for循环中向vector对象添加元素。
- 任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
13,数组
- 与vector的对比
- 相同点:存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。
- 不同点:数组的大小确定不变,不能随意向数组中增加元素;而vector大小不固定,可以增加总元素数量。
- 数组的维度在定义时已经确定,如果我们想更改数组的长度,只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组中去
- 也无法像vector那样使用
size
函数直接获取数组的维度。如果是字符数组,可以调用strlen
函数得到字符串的长度;如果是其他数组,只能使用sizeof(array)/sizeof(array[0])
的方式计算数组的维度。
- 如果不清楚元素的确切个数,请使用vector。
- 定义:
a[d]
int a[10]; 含有10个整数的数组 int *a[10]; 含有10个整形指针的数组
- 字符数组的特殊性:
- 注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去
- 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
N1-STL(标准模板库)
行动是治愈恐惧的良药,而犹豫拖延将不断滋养恐惧。