4.1数组
数组是一种数据格式,能够储存多个同类型的值(参考汇编语言的dup)。
数组元素数目不能是变量
4.1.2
数组只有在定义时才能使用大括号初始化,此后如果想为数组元素赋值可用下标单独赋值。
C++默认没有赋值的元素为0.
4.2 字符串
字符串是连续储存在内存中的字符。
连续储存意味着可以使用将字符串储存在char数组中。
C风格字符串的特性:以空字符(\0)结尾。
如果用cout打印字符数组时没有遇到空字符,它将把紧随其后的内存里的内容当作要打印的内容,直到数组结束或遇到空字符(0等于空字符,因此cout将很快停止)。
使用双引号括起要赋值给数组的字符串被称为字符串常量,将会隐式地包含一个空字符。使用输入工具将字符串读入字符数组时也是一样。
字符串常量(双引号)不能和字符常量(单引号)互换,二者含义不同:双引号表示字符串的内存地址。
因为地址在C++中是一种独立的类型,因此编译器不会允许将地址赋给数组。(人话:会报错)
使用C++输入工具(如cin)将字符串输入字符数组时,将会自动加上结尾的空字符。
4.2.2
strlen()函数计算的是数组内字符串的长度(不包括空字符) 而sizeof()运算符计算的是整个数组的字节长度。(strlen需要头文件cstring)
可以使用符号常量指定数组长度。
4.2.3
cin通过空白(空格,制表符,换行符)来确定字符串的结束位置(断句)。
4.2.4 cin的类成员函数getline()和get()
1.面向行的输入:getline()
getline()函数通过敲击回车输入的换行符确定输入结尾,并在读入后将换行符替换成空字符\0。
使用格式:cin.getline(数组名,数组元素数)
2.面向行的输入:get()
get()和getline()的工作方式相似,但get不会读入敲击回车产生的换行符,而是把换行符留在输入 里。
这会导致一个问题:连续两次调用get(),第二次调用时会先看到第一次调用时留在输入队列里的 ,从而认为字符串已经结束,导致无法读入。
解决方案:在调用完第一次get()后,再调用一次不带任何参数的get(),或者把不带任何参数的
get()用拼接到第一个get()上
格式:cin.get(数组名,数字元素数).get()
C++允许函数有不同的版本(说人话:C++允许函数使用不同的参数列表)。
使用get而不是使用getline的理由:
get()会将换行符保留在输入队列中,而在第17章介绍的内容中,将会介绍查看输入队列中下一个输入 字符的技术,从而通过这种技术辨别停止读取的原因是已经读取完整行,还是数组被填满导致无法读入。
get()读取空行后将会设置失效位导致接下来的输入被阻断,但可以通过cin.clear()命令恢复输入。
如果要输入的字符数大于分配的空间长,则会把余下的字符留在输入队列中(getline还会设置失效位)
4.2.5
cin在读取要输入的东西后,将会把敲击回车产生的换行符留在输入队列中,导致在后面使用面向行的函数时,会先看到换行符而认为已经读取完成,导致无法输入。
解决方法:
1:cin.get()
2:(cin>>函数名).get() //新的拼接方式
C++一般使用指针而非数组处理字符串
4.3:string类简介
可以使用string类型的变量存储字符串,同时,string类提供了将字符串当作一种数据类型的表示方法。
要使用string类,必须在程序中包含string头文件。
string类位于std名称空间中,注意引用。
string对象(变量)的使用方法与数组相同,与数组的主要区别是,可以将string对象声明为一个简单变量。
格式:
string 变量名
程序能自动处理string对象的大小,从而避免了超出长度,因此比字符数组更安全。
4.3.2
不能将一个数组赋给另一个数组,但可以将一个string对象赋给另一个string对象。
可以使用运算符+将两个string对象拼接起来,也可以使用+=将一个string对象赋给另一个string对象的 末尾。
格式:+:str1+str2(赋值是从左到右,拼接也一样)
+=:str1+=str2
4.3.3
头文件cstring中提供了对字符数组进行操作的函数:
strcpy()和strcat()。
strcpy:将字符串复制到字符数组中
strcat:将字符串拼接到字符数组的末尾
格式:strcpy/cat(数组名,字符串)
上面的两个函数也可以直接复制/拼接数组中的内容。
确定字符串中字符数的两种方法:
int len=str1.size()
int len=strlen(字符串)
str1.size()的这种句法指出,str1是一个对象,而size()是一个类方法。
总之,上面这两种方法中
C函数(strlen)使用参数指出要使用的字符串;
string类对象使用对象名(str1)和句点运算符(.)指出要使用的字符串。
4.3.4
未被初始化的数组内容是未定义的,空字符是随机出现的。
未被初始化的string对象内容默认为0
将成行的字符串读入string类对象的方法格式:
getline(cin,string变量名)
这里没有使用句点(.)表示法,这意味着这里的getline不是类方法,它将cin作为参数去指出在哪里查找输入。
istream类中没有处理string对象的类方法,但使用cin(cin是isream类的对象)处理string类对象是可行的。暂时不必深究原因,这属于第11章的内容。
4.3.5
C++11新增了新类型:原始字符串(Raw),原始字符串可表示换行符(\n)等普通字符串无法表示的字符。
原始字符串格式:
cout<<R"+*("字符串")+*"
4.4 结构
结构是用户定义的类型,定义后便可创建这种类型的变量。
创建结构的步骤:
1:定义结构描述
struct str //str为结构名,struct为结构关键字
{
char name[20]; //大括号内的都是结构的成员
float volume; //定义用分号分隔成员,初始化用逗号
double price;
};
2:按描述创建结构变量
struct str hat; //标准格式,创建一个str类型的变量hat
str hat; //简化格式,在C++中省略struct不会出错
由于hat的类型为str,因此可以使用成员运算符(.)访问各个成员。
hat.price指的是hat变量中的price成员。由于price成员被声明为double类型,所以hat.price相当于一个double类型的变量。
结构初始化的步骤:
str hat=
{
"Glorious Gloria", //字符串用双引号括住
1.88, //成员之间要用逗号隔开
29.99
}; //在结尾处加上;
也可以将所有成员都置于一行中,但一定要用逗号隔开。
C++提倡使用外部结构声明(声明位置那部分知识我以前学过,所以省了)
以及外部声明常量(用const声明)
4.4.2
和数组一样,结构支持使用列表初始化
同样的,未被声明的成员默认为0。
4.4.3
可以使用string类定义成员类型,但要先声明使用std名称空间。
4.4.4
C++使用户定义的类型与自带类型尽量相似(第7章再说)
可以使用赋值运算符(=)将结构中的内容赋给另一个同类型(指结构类型)的结构
这种赋值被称为成员赋值。
可以同时完成定义结构类型与创建结构变量的工作。
4.4.5 结构数组
可以创建元素为结构的数组,格式如下:
inflatable gifts[100]; //inflatable为结构名,gifts为数组名
这样,gifts的每一个元素都是一个inflatable对象。 使用方法如:
cout<<gifts[0].volume;
初始化结构数组时可以使用以下的格式:
inflatable gifts[2]=
{
{"Bambi",0.5,21.99}, //第一个结构初始化(gifts[0])
{"Godzillia",2000,565.99} //第二个结构初始化(gifts[1])
};
这样做时,每一个花括号中括起来的都是一个结构初始化
结构成员与结构成员,和结构初始化与结构初始化之间都用逗号分隔。
4.5 共用体
共用体是一种能在非同一时间存储不同类型数据(不只是书中的int,long和double)的数据格式。
共用体的声明方式与结构相似,如下:
union one4all //union是共用体关键字
{
int int_val; //和结构一样,在声明时使用分号分隔成员
long long_val;
double double_val;
}Data;
可以在共用体定义的末尾,最后一个分号之前,定义一个或多个共用体变量(如上面的变量Data)。
共用体的最小长度为其最大成员长度(上面的是double)。
共用体的使用方式和结构一样。
结构与共用体可以互相包含,当程序中出现这样的包含关系时,引用变量需要用到多个成员运算符(.)。
引用变量的顺序由包含关系决定,包含者比被包含者先引用,
使用匿名共用体可以避免这一点,匿名共用体的成员将成为位于同位置的变量
由于共用体是匿名的,其成员将被视为包含匿名共用体的结构的成员,因此不需要多个成员运算符。
共用体的用途:当数据项使用多种格式(类型)时,节省空间。
共用体节省空间的原理:使用相同的内存存储不同类型的数据。
4.6 枚举
C++的enum工具提供了一种能代替const的创建符号常量的方式,它还允许定义新类型,但要求比较严格。
句法格式:
enum 类型名{符号常量1,符号常量2,符号常量3...... };
这里的类型名被称为枚举。
上面的语句将会导致符号常量1~3被默认赋值为0~2,以此类推。
在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量(即上面格式定义的符号常量)赋给同一种枚举的变量。
枚举类型只有赋值运算符(=),没有算数运算符。(有些实现没有类似限制)
枚举量是整形,可自动转换(提升)为int类型,但int类型不能自动转换为枚举类型。
(同上,有些实现没有类似限制,但为了可移植性考虑,应当采纳较为严格的限制)
band=orange+red; //非法,因为将枚举用于算术表达式时会自动转换为 //int类型,但可以通过强制类型转换将它赋给枚举变量
如果只打算用枚举定义符号常量而不使用变量,则可以省略枚举类型名。
4.6.1
可以使用赋值运算符显式设置枚举量的值,指定的值必须是整数。
4.6.2
枚举量的取值范围=上限:大于最大枚举量的2的幂(2的n次方)-1
下限:不小于0则是0,小于0则和上限一样,但加上负号
4.7 指针
指针是一个变量,存储的是值的地址,而非值本身。
可用地址运算符(&)直接找到常规变量的地址,使用时以前缀的形式出现。
指针与OOP原理重点:
OOP强调的是在运行阶段而非编译阶段进行决策,就好比是
在旅游时根据当时的情况做出选择,而非按照旅游前做出的选择。
使用OOP时,可以在运行阶段确定数组的长度。
指针名表示的是地址,*运算符被称为间接值或解除引用运算符,
将其用于指针可得到该地址处存储的值。
声明指针变量的格式:
数据类型 * 指针变量名;
可用&将普通变量的地址赋给指针变量。
对于每一个指针变量名,都需要一个*
double * tax_ptr; //tax_ptr是一个指向double的指针,double类型的指针
char * str; //str是一个指向char的指针,也就是char类型的指针
虽然tax_ptr和str指向不同的数据类型,但他们本身的长度是一样的。
(*tax_ptr和*str就不一样了)
4.7.2
一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定(且适当)的地址。
4.7.3
从可执行的操作和概念上来看,指针与整数是截然不同的类型。
因此,不能简单地将整数赋给指针,因为程序并不知道这串数字是地址。
更通俗地讲,不能直接使用数字为指针变量赋值,除非使用强制类型转换。
4.7.4
指针真正的用武之地在于,在运行阶段分配未命名的内存以储存值。
在这种情况下,只能用指针储存值。
new运算符将返回分配好的内存块的地址,使用格式如下:
数据类型1 * 指针名=new 数据类型1
我们用“数据对象”来称呼new运算符分配的没有名字的地址
数据对象指的是为数据项分配的内存块。
数据对象比变量更通用,准确地说,变量属于数据对象。
必须声明指针所指向的类型,因为对指针指向的值进行各种操作时,
需要知道读出/入多少,以及如何解释它们。
常规数据对象的值都被存储在栈中,而new分配的则处于堆中(第9章知识)
4.7.5
delete运算符可以释放使用完的内存,而不会删除指针本身。
(目前情况下来看,只能用于释放new分配的内存)
一定要配对地使用new和delete,否则将发生内存泄漏导致程序终止。
使用举例:
int * ps=new int;
......
delete ps;
4.7.6
在编译时给数组分配内存空间被称为静态联编
在运行时根据情况选择是否需要创建数组并且选择数组的长度被称为动态联编
这种数组是在运行时被创建的,叫做动态数组。
1.使用new创建动态数组
举例:
int * psome=new int[10];
new运算符返回第一个元素的地址,也就是说,第一个元素的地址被赋给指针
使用delete释放动态数组
举例:delete [] psome;
2.使用动态数组
只需把指针名当作数组使用即可,不需要带*
指针和真正数组名之间的差别:
不能修改数组名的值,如自加自减,但可以修改指针的值,
如指针+1将会导致指针指向下一个元素。
释放动态数组时要将指针复位会创建时的位置。
4.8 指针、数组和指针算数
指针算数和常规算数的区别:
将常规(如整数)变量加1时,其值将增加1。
将指针变量加1时,将增加指针指向的类型所占的字节数,这使得指针每次都刚好指向数组的下一项。
4.8.1
重点:在多数情况下,C++将数组名[下标]解释为*(数组名+下标),
将数组名解释为*(数组名+0) //(将数组名解释为第一个元素的地址)
指针名[下标]转换为*(指针名+下标)
例如:C++编译器将stacks[0]看做*(stacks+0)
通常,在使用数组(或指针指出数组)表示法时,C++都会执行上面的转换。
使用指针名表达和使用数组名表达在多数表达式中都表示地址,
而二者的区别在于:
一:可以修改指针的值,而数组名是常量。
二:对数组使用sizeof()将会得到数组的长度,而对指向数组的指针使用还是会得到指针的长度。
在对数组名应用sizeof()的时候,C++不会将数组名解释为地址。
short tell[10]; //举个例子
上面说了,数组名被解释为第一个元素的地址
重点:但对数组名应用地址运算符(&)时,得到的是整个数组的地址
这两个地址从数字上来看一样,但从概念上来说:
tell(被转换为&tell[0])是一个2字节内存块的地址;
&tell是一个20字节内存块的地址。
因此,表达式tell+1将会将地址值加2,而表达式&tell+1会将地址值加20。
设置指向&tell的指针的方法如下:
short (*pas)[10]=&tell; //pas是指针名,随便起
这条指令将会使指针pas被设置为&tell
指针指向tell的地址,因此*pas=tell (*pas的意思是指针指向的值)
4.8.2
使用方括号表示法等同于对指针解除引用(*),
数组名和指针变量都是如此,因此可以这样使用数组和指针:
int * pt=new int[10];
*pt=5; //将5赋给数组中的[0]的位置
pt[0]=5; //和上面的指令含义一样
还可以这样:
*(pt+0)=5; //含义同上
4.8.3
char flower[10]="rose";
cout<<flower<<"s are red\n";
这里cout语句中的flower是char元素的地址,
cout对象认为char元素的地址是字符串的地址,
因此,cout打印字符串。
这里的关键不在于flower是数组,而在于flower是char元素的地址。
这意味着可以将指向char元素的,
由flower转化而来的指针变量作为cout的参数,
因为它也是char元素的地址。
重点:
如果给cout提供一个字符的地址,则它将会从该字符开始打印,遇到空字符停
这就是为什么能用字符串的第一个字符的位置打印整个字符串的原因。
上面的用引号括起来的字符串"s are red\n;也是一个地址,
用引号括起的字符串像数组名一样也是第一个元素的地址。
上述代码不会将整个字符串发送给cout,而只是发送了字符串的地址,
与逐个传递所有字符相比,大大节省了工作量。
总结:在大多数C++表达式中,char数组名,char指针以及引号括起的字符串常量
都被解释为字符串中第一个字符的地址。
一般来说,编译器在内存中留出一些空间存放用引号括起的字符串,
并将被存储的字符串与其地址关联起来。(这段话暂时没那么重要,选学)
使用指向字符串字面值的指针进行输入并不合适
原因有二:
1.有些编译器将字符串字面值视为只读常量
2.有些编译器只使用字符串字面值的一个副本来表示程序中的该字面值
这导致指向字符串字面值的指针只是指向一个副本。
综合以上两点,
无论如何,由于bird被声明为const,因此不能改变bird指向的内容
如果要使用char类型指针显示地址,则必须使用强制类型转换。
char animal[20]="bear";
获得该字符串副本的方法(具体见92页程序):
1.分配内存来存储该字符串。
ps=new char[strlen(animal)+1];//节约空间
2.使用strcpy进行复制
strcpy(ps,animal);//将animal赋给ps指向的位置
strncpy的用法:
strncpy(数组名,"字符串",最多复制的数量);//可以限制复制数量
4.8.4 动态结构
本节介绍的结构技术也可用于类。
一,声明结构类型
举例:
struct things
{
int good;
int bad;
};
创建结构
举例:
things * ps=new things;
访问成员的方法:
箭头成员运算符(->)
举例:
things abc;
cout<<abc->good;
如果标识符(用于标识这个结构的符号,也就是名字):
为结构名或数组名,则使用句点运算符(.)
为指向结构的指针名,则使用箭头运算符。
当然也可以对指针名使用句点运算符:
(*ps).price
95、96页程序4.22不懂详见程序分析
可以将new和delete放在不同的函数中。
4.8.5
1.自动存储
在函数内部定义的常规变量被称为自动变量,
自动变量在所属的函数(其中的代码块)的被调用时产生,在结束调用时被释放
自动变量的作用域为包含它的代码块
注:代码块是被包含在花括号内的一段代码。
2.静态存储
静态存储是整个程序执行期间都存在的存储方式
使变量成为静态的两种方式:
1.在函数外声明变量
2.使用关键字static声明变量
如:
static double a=56.60;
有些实现不支持对自动数组和自动结构的初始化(这句话不知道啥意思)
3.动态存储
new和delete运算符管理了一个被称为堆的内存池,该内存池同用于自动存储和静态存储的内存是分开的
动态存储变量的生命周期取决于何时声明和释放。
要避免内存泄漏,最好的方式就是同时使用new和delete运算符
4.9 类型组合
本章介绍的数组、结构和指针可以以各种方式组合:
知道的就不写了,只写我有疑惑的:
antarctica_years_end trio; //anta...是结构名
书中说数组名是一个指针,实际上:
重点:数组名可以转换为指向其指代实体的指针(参考前面4.8.1的数组和指针的转换)
可直接对成员使用赋值运算符进行成员赋值(4.4.4内容)
指针数组(注:这玩意不是指针结构数组常量!!!)
const antarctica_years_end * arp[3]={&s01,&s02,&s03};
分开详细解析这玩意:
第一步,抛开结构名不看,这里有结构名纯粹是因为指针指向的东西是结构变量
(说白了就是为了格式对称)
第二步,抛开常量不看,它的特征不明显
于是就得到了这玩意:
*arp[3]={&s01,&s02,&s03};
这玩意并非是在定义新的变量(往上翻!),而是在定义新的指向这些老变量的指针
重点:它的确切含义是:定义一个存储指向三个老结构变量地址的指针的数组
为什么要这么做呢?因为单纯定义指针无法同时指向三个变量。
重点:为指针赋值时,赋给指针的是地址。
这句话很好:既然arp是一个指针数组,那么arp[1]就是一个指针(慢慢理解这句话的思路)
因此,使用arp访问数据(这里的访问数据的意思是访问结构成员!)的方法:
arp[1]->year;
可创建指向上面数组的指针:
const antarctica_years_end **ppa=arp;
这行代码在声明的时候要注意格式,必须和上面的“指针数组”格式一样(也就是格式对称)。
arp是一个数组的名称,因此它是第一个元素的地址。
重点:在此很明确的告诉你,数组名就是一个指针,指向的就是数组第一个数据的首地址。
编译器知道arp的类型,因此可以使用auto,使其自动推断类型,避免出错。
auto ppa=arp; //甚至连*都不用!编译器知道arp是指针
使用ppa访问数据(访问结构成员)的方法:
(*ppa)->year;//由于ppa是一个指向指针的指针,因此*ppa是一个指针
这里要先详细说下解除引用运算符(*)的含义(我自己的理解):
举个例子:
int vaa;
int * pp=&vaa;
这里通过取地址(&vaa)知道了vaa的内存地址,这属于一种引用
而解除引用的意思是,pp不再表示vaa的地址,而是直接表示vaa。
这里的*ppa的意思就是直接表示arp,
小结(重点):(ppa=arp的地址,*ppa=arp,arp=arp[0],arp[0]=s01,因此*ppa=s01)
而使用指针名被解释为指向第一个元素(s01),因此使用*ppa。
用法:
cout<<(*ppa)->years;
auto ppb=arp;//这行代码除了指针名不同外,效果等同于上面的创建ppa
用法:
cout<<(*(ppb+1))->years;//括号不能缺
4.10 数组的替代品
4.10.1 模板类vector
模板类vector类是一种动态数组,使用方法和数组差不多。
可以代替使用new创建动态数组(vector自动管理new和delete)。
这里只介绍关于vector的实用知识:
要使用vector类,必须在头文件中包含<vector>,而且vector也位于名称空间std中,记得编译。
使用方法:
vector<数据类型>模板名(n);//n表示(数组)元素数可以是整形常量或常量
举例:
vector<int>vi(5)
这将创建一个名为vi的vector对象,它可存储5个类型为int的元素(用法和数组差不多)。
vector类比数组方便安全,但效率低
4.10.2 模板类array(C++11)
array效率等同于数组,且安全方便。
array使用栈(静态存储区)进行分配
使用方法:
包含头文件array,编译空间std。
格式:
array<数据类型,n>模板名;//与vector不同的是,array的长度不能是变量
举例:
array<int,5>,vi;
4.10.3
可将列表初始化用于vector和array,但C++98中不能对vector这样做。
array对象和数组都存储在栈中,但vector存储在自由存储区(堆)中。
可以使用赋值运算符(=)直接将一个array对象赋给另一个array对象。
数组之所以不安全,是因为它不禁止数组名[-2]这种超界错误,
而vector和array可以使用成员函数at()来避免超界错误,但会导致运行时间更长。
格式:
对象名.at(下标);
举例:
a2.at(1)