4.1 数组
数组(array)是一种数据格式,能够存储多个同类型的值。
数组声明应指出:
存储在每个元素中的值的类型;
数组名;
数组的中元素的个数;
在C++中,可以通过修改简单变量的声明,添加中括号(其中包括元素数目)来完成数组声明。
typeName arrayName[arraySize];
short array1[10];
表达式arraySize指定元素数目,它必须是整型常数或const值,也可以是常量表达式(如8 * sizeof(int)),即其中的所有的值在编译时都是已知的。具体地说,arraySize的值不能是变量,变量的值是在程序运行时设置的。
数组的很多用途都是基于以下事实,可单独访问数组元素。方法时使用下标或索引来对元素编号。
C++数组从0开始编号。
C++使用带索引的方括号表示法来指定数组元素。
编译器不会检查下标是否有效,数组越界。
4.1.1 程序说明
C++允许在声明语句中初始化数组元素。
int array1[10] = {20, 3, 0};
只需提供一个用逗号分隔的值列表(初始化列表),并将它们用花括号括起即可。
sizeof运算符返回类型或数据对象的长度(单位为字节)。如果将sizeof运算符作用于数组名,得到的时整个数组的字节数。但如果将sizeof作用于数组元素,则得到的将是元素的长度。array1是数组,而array1[1]只是int变量。
4.1.2 数组的初始化规则
只有在定义数组时才能使用初始化,此后就不能在用了。
也不能将一个数组赋给另一个数组。
然而,可以使用下标分别给数组中的元素赋值。
初始化数组时,提供的值可以少于数组的元素数目。
如果只对数组的一部分进行初始化,则编译器将其他元素设为0.因此,将数组中所有的元素都初始化为0非常简单--只要显式地将第一个元素初始化为0。
如果初始化数组时方括号为空,C++编译器将计算元素个数。
4.1.3 C++11 数组初始化方法
首先,初始化数组时,可省略等号;
其次,可不在大括号内包含任何东西,这将把所有地元素都设置为0;
第三,列表初始化禁止缩窄转换。
4.2 字符串
字符串是存储在内存地连续字节中地一系列字符。C++处理字符串地方法有两种。第一种来自于C语言,第二种是基于String类库的方法。
C风格字符串具有一种特殊的性质:以空子符null character结尾,空字符被协作\0。
只需使用一个引号括起的字符串即可。这种字符串被称为字符串常量(string constant)或字符串字面值(string literal)。
char str[12] = ''Mr,Geek'';
处理字符串的函数根据空字符的位置,而不是数组的长度来进行处理。C++对字符串的长度没有限制。
在确定存储字符串确定的最短数组时,别忘了将结尾的空字符计算在内。
注意字符串常量(使用双引号)不能与字符常量(使用单引号)互换。
“S”不是字符常量,它表示两个字符(字符S与\0)组成的字符串。更糟糕的是,“S”实际上表示的是字符串所在的内存地址。
4.2.1 拼接字符串
有时候,字符串很长,无法放到同一行种。C++允许拼接字符串字面值,即将两个用引号括起的字符串合并成一个。事实上,任何两个由空白(空格、制表符与换行符)分隔的字符串常量将自动拼接成一个。前一个字符串的\0字符会被后一个字符串的第一个取代。
4.2.2 在数组种使用字符串
char str[15] = "AWHKL";
sizeof运算符指出整个数组的长度。strlen()函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度。而且strlen()函数只计算可见字符,而不把空字符计算在内。
4.2.3 字符串输入
cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在获取字符串输入时只获得一个单词。
4.2.4 每次读取一行字符串输入
需要面向行而不是面向单词的方法。cin.get() 或cin.getline()都可以读取一行输入,直到换行符。然后getline()将丢弃换行符,而get()将换行符保留在输入序列中。
cin.getline()函数读取整行,通过回车键输入的换行符来确定输入结尾。该函数有两个参数,第一个参数用来存储输入行的数组的名称,第二个参数是要去读的字符数。该函数在读取指定数目的字符或遇到换行符时停止读取。
cin.get()与getline()不同的是,get()并不在读取并丢弃换行符,而是将其保留在输入队列中。若连续调用两次cin.get(),第二次调用时看到第一个字符便是换行符。因此get()认为以及到达行尾,而没有发现任何可读取的内容。如果不借助帮助,则get()将不能跨过该换行符。
幸运的是,使用不带任何参数的cin.get()调用可读取下一个字符串(即使是换行符)。因此它可以用来处理换行符,为读取下一行做准备。
为什么使用get(),而不是getline()?首先,老式实现没有getline()。其次,get()使输入更仔细。例如,假设用get()将一行读入数组中。如何知道停止读取的原因使由于已经读取了整行,而不是由于数组已填满呢?查看下一个输入字符,如果使换行符,说明已读取了整行;否则,说明该行中还有其他输入。
当get()读取到空行后将设置失效位(failbit)。这意味着接下来的输入将被阻断,但可以使用下面的命令来恢复输入。
另一个潜在的问题是,输入的字符串比可能分配的空间长。则getline()与get()将把余下的字符留在输入队列中,而getline()还会设置失效位,并关闭后面的输入。
4.2.5 混合输入字符串和数字
C++程序常用指针(而不是数组)来处理字符串。
4.3 string类简介
string类使用起来比数组简单,同时提供了将字符串作为一种数据类型的表示方法。
要使用string类,必须在程序中包含头文件string。string类位于名称空间std中,因此我还必须提供一条using编译指令。
string对象和字符数组之间相同点:
可以使用C风格字符串来初始化string对象;
可以使用cin来将键盘输入存储到string对象中;
可以使用cout来显示string对象’
可以使用数组表示法来访问存储在string对象中的字符。
string对象和字符数组之间不同点:
可以将string对象声明为简单变量,而不是数组;
类设计让程序能自动处理string的大小。
4.3.1 C++11 字符串初始化
char str[] = {"suiahsuih"};
string str = {"suiahsuih"};
4.3.2 赋值、拼接和附加
数组不能直接赋值给另一个数组,但可以将一个string对象赋给另一个string对象。
string类简化了字符串合并操作。可以使用运算符+将两个string对象合并起来,还可以使用+=将字符串附加到string对象的末尾。
4.3.3 string类的其他操作
在头文件cstring,使用函数strcpy()将字符串复制到字符数组中,strcat()将字符串附加到字符数组末尾。
4.4 结构简介
数组虽然能存储多个元素,但所有元素的类型必须相同。而结构可以存储多种类型的数据。
结构也是C++ OOP的基石。
结构式用户定义的类型,而结构声明定义了这种类型的数据属性。
创建结构包括两部:首先,定义结构描述--它描述并标记了能够存储在结构中的各种数据类型。然后按照描述创建结构变量。
struct structName
{
typename variableName;
typename variableName;
};
关键字struct表明,这些代码定义了一个结构的布局。标识符structName是这中数据格式的名称。列表中的每一项都被称为结构成员。定义结构后,便可以创建此类型的结构变量。
结构声明定义了一种新类型。在C++中,struct structName variableName声明中省略struct不会出错。
可以用成员运算符.来访问各个成员数据。
总之,通过成员名能偶访问结构的成员,就像通过索引能够访问数组的元素一样。
访问类成员函数的方式是从访问结构成员变量的方式衍生出来的。
4.4.1 在程序中使用结构
外部声明可以可以被之后的任何函数使用,而内部声明只能被该声明所属的函数使用。
变量也可以在函数内部和外部定义,外部变量由所有的函数共享。C++不提倡使用外部的变量,但提倡使用外部结构声明。另外,在外部声明符号常量通常更合理。
结构初始化方式,和数组一样,使用由逗号分隔值列表,并将这些值用花括号括起。
structName variableName = { value1, value2...};
可将结构的每个成员都看作相应类型的变量。
4.4.2 C++11 结构初始化
与数组一样,C++11 也支持将列表初始化用于结构,且等号是可选的。其次,若大括号内位包含任何东西,各个成员都将被设置为零。
4.4.3 结构可以将string类作为成员吗?
答案是肯定的。
4.4.4 其他结构属性
可以将结构作为参数传递给函数,也可以让函数返回一个结构。
另外,也可以使用赋值运算符将结构赋给另一个同类型结构。即使成员是数组,此赋值称为成员赋值(memberwise assignment)。
若将变量名放在结束括号的后面,可同时完成定义结构和创建结构变量的工作。
struct structName
{
};variableName
若上述代码省略structName,则以后无法创建此类型的结构变量。
4.4.5 结构数组
创建结构数组与创建基本类型数组完全相同。
要初始化结构数组,可以结合使用初始化的规则。{{}, {}, {}...}。
4.4.6 结构中的位字段
C++也允许指定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对于的数据结构非常方便。字段的类型应为整型或枚举,接下来是冒号,冒号后面是数字,它指定了使用的位数。可以使用没有名称的字段来提供间距。每个成员都被称为位字段(bit field)。
4.5 共用体
共用体(union) 是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
共用体的用途之一是,当数据项使用两种或更多格式(但不会同时使用)时,可节省空间。
匿名共用体(anonymous union)没有名称,其成员将称为位于相同地址的变量。显然,每次只有一个成员是当前的成员。
4.6 枚举
C++ 的enum工具提供了另一种创建符号常量的方式,这种方式可以代替const。它还允许定义新类型,但必须按照严格的限制进行。使用enum的句法与struct类似。
enum enumerationName {value1,value2...};
enumerationName成为新类型的名称;
enumerationName被称为枚举;
value1等作为符号常量,对应整数0~n;
在不进行强制类型转换,只能将定义枚举时使用的枚举量赋给这个枚举的变量。
对于枚举,只定义了赋值运算符。具体地说,没有为枚举定义算术运算。
4.6.1 设置枚举量的值
可以使用赋值运算符=来显式地设置枚举量的值。
指定的值必须是整数,也可以只显式地定义其中一些枚举量的值。后面没有初始化的枚举量的值比其前面的枚举量大1。
4.7 指针和自由存储空间
存储数据:
信息存储在何处;
存储的值为多少;
存储的信息是什么类型;
定义一个简单变量。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。
另一种是通过指针方式。指针是一个变量,其存储的是值的地址,而不是值本身。
使用常规变量时,值是指定的量。
面对对象编程与传统的过程性编程的区别在于,OOP强调的是在允许阶段(而不是编译阶段)进行决策。运行阶段指的是程序正在运行时,编译阶段是指编译器将程序组合起来时。运行阶段决策好比度假时,选择参观那些景点取决于当时的天气和当时的心情;而编译阶段决策更像不管在声明条件下,都坚持预先设定的日程安排。
处理存储数据的新策略是将地址视为指定的量,而将值视为派生量。一种特殊类型的变量--指针用于存储值的地址。
指针名表示的是地址,*运算符被称为间接值(indirect value)或解除引用(dereferencing)运算符,将其应用于指针,可以得到该地址存储的值。
int value1 = 10;
int* pt1 = &value1;
//*pt1 实际就是value1
变量value1表示值,使用&运算符来获得地址;而变量pt1表示地址, 用*运算符来获取值。
4.7.1 声明和初始化指针
char的地址与double的地址看上去没什么两样,但它们所使用的字节数是不同的,它们存储值时使用的内部格式也不同。因此,指针声明i必须指定指针指向的数据的类型。
对每个指针变量名,都要使用一个*。
int* pt1, pt2; //创建了一个int* 变量pt1, 和一个整型变量pt2。
int* pt1, *pt2; //创建了两个int*变量。
在C++中, int*是一种复合类型,是指向int的指针。
指针变量不仅仅是指针,而且是指向特定类型的指针。
和数组一样,指针都是基于其他类型的。
虽然double*与int*指向两种长度不一样的数据类型,但这两个变量本身的长度通常是相同的。
int value1 = 10;
int* pt1 = &value1; //指针初始化
4.7.2 指针的危险
在C++中创建指针时,计算机分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,忽略这一步是很危险的。
long* fellow;
*fellow = 223333;
fellow确实是一个指针,但它指向哪里呢?上述代码没有将地址赋给fellow。那么223333将被放在那里? 我们不知道。由于fellow没有被初始化,它可能是任何值。不管值是声明,程序都将它解释为存储223333的地址。如果fellow的值碰巧是1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址。这种错误可能会导致一些非常隐匿的bug。
一定要在对指针解引用之前,将指针初始化为一个确定的、适当的地址。
4.7.3 指针和数字
指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上说,指针与整数是截然不同的类型。整数可以算术运算,而指针描述的地址,将两个地址相乘没有任何意义。
若要将整数型变量赋值给指针,可以通过强制类型转化。
int* pt1 = (int*)0x457A41;
4.7.4 使用new来分配内存
变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问内存提供了一个别名。指针正在用武之地在于,在运行阶段分配未命名的内存以存储值。C++可以使用new运算符来分配内存。
程序员要告诉new,需要为那种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。
int* ptn = new int;
new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName* pointer_name = new typeName;
地址本身之指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。
void main()
{
int nights = 1001;
int* pt = new int;
*pt = 1001;
}
变量nights和pt的值都存储在stack区,而new从heap区的内存区域分配内存。
在C++中,值为0的指针被称为空指针(null pointer)。C++确定空指针不会指向有效的数据,因此它常被用运算符或函数失败。
4.7.5 使用delete释放内存
当需要内存时,使用new来分配内存。另一方面是delete运算符,它使得在使用完内存后,能够将其归还给内存池。
int* pt = new int;
delete pt;
这将释放pt所指的内存,但不会删除pt本身。例如,可以将pt重新指向另一个新分配的内存块。
一定要配对的使用new和delete。否则将发生内存泄露(memory leak)。也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。
不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不要使用delete来释放并非new所获的内存。
int* pt = new int;
delete pt;
delete pt;//重新释放pt,会引发未知的后果。
int value1 = 100;
delete value1; // delete删除非new申请的内存,不被允许的。
只能使用delete来释放使用new分配的内存。然而,对空指针使用delete却是安全的。
一般来说,不要创建两个指向同一内存块的指针,这将增加错误地删除同一个内存块两次地可能性。
4.7.6 使用new来创建动态数组
通常,对于大型数据(数组,字符串和结构),应使用new。
在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。但是用new时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫做动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。
在C++中,创建动态数组很容易;只要将数组的元素类型和元素的数目告诉new即可。
typeName* variableName = new typeName[numberOfElements];
delete[] variableName;
new运算符返回第一个元素的地址。
对于new[] 创建的数组,应使用delete []来删除。
方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
总之,使用new和delete时,应遵守以下规则:
不要使用delete来释放不是new分配的内存;
不要使用delete释放同一内存块两次;
如果使用new[]为数组分配内存,则应使用delete[]来释放;
如果使用new为一个实体分配内存,则应使用delete来释放;
对空指针应用delete是安全的;
对于new创建的动态数组,只要把指针当作数组名使用即可。第一个元素,pt[0], 对于第二个元素pt[1]。
数组名是不能修改的。但指针式变量因此可以修改它的值。
int ary[10] = {};
ary += 1; // 不允许
int* pt = new int[10];
pt+=1; //可以的,导致pt指向它原始的第二个元素,pt[1]指向原始的第三个元素。
4.8 指针、数组和指针算术
指针和数组基本等价的原因在于指针算数(pointer arithmetic)和C++内部处理数组的方式。
将指针变量加1,增加的量等于它指向的类型的字节数。
4.8.1 程序说明
C++将数组名解释为数组第1个元素的地址。
double wages[3] = {1, };
wages = &wages[0];
double* pw = wages;
看一看数组stacks[n]。C++编译器将该表达式看作*(stacks + n),这意味着先计算数组第n个元素的地址,然后再找到存储在那处的值。
arrayName[n] = *(arrayName + n);
pointerName[n] = *(pointerName + n);
数组名和指针名的区别之一在于,可以修改指针的值,而数组名式常量。
数组名被解释为其第1个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。
short tell[10];
cout << tell << endl; //输出第1个元素的地址。
cout << &tell << endl; //输出整个数组的地址。
// &tell[0] + 1, 是加2个字节
// &tell + 1, 是加一次整个tell数组地址,即2 * 10个字节,而不是2个字节。
如何创建指针数组?
short (*pt)[20];
4.8.2 指针小结
//声明指针
typeName* pointerName;
//给指针赋值
double* pn;
double bubble = 3.2;
pn = &bubble;
//对指针解引用
*pn = 32.1;
//区别指针和指针所指向的值
int* pt = new int;
*pt = 5; //pt才是指针变量,*pt是完全等同于一个int型变量。
//数组名
//在大多数情况下,C++将数组名视为数组的第1个元素的地址。
//sizeof, &取址运算符作用在数组名时,返回整个数组的长度。
C++ 允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象中元素类型所占的字节数。仅当两个指针指向同一个数组时,指针减法才有意义,返回两个元素之间间隔。
使用数组声明来创建数组时,将采用静态编联。
使用new[]创建数组时,将采用动态编联。将在运行时候为数组分配空间,应使用delete[]来释放所占内存。
使用方括号数组表示法等同于对指针解引用
int tacos[10];
//tacos[3] 意味着 *(tacos + 3)
int* pt = new int[11];
//pt[3]意味着 *(pt + 3)。
4.8.3 指针和字符串
如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇见空字符。一般来说,如果给cout提供一个指针,它将打印指针。但如果指针类型是char*,则cout将显示指向的字符串。如果要显示字符串的地址,则必须将这种指针强制转换为另一种指针类型。
在C++中,用“ ”双引号括起的字符串像数字名一样,也就是第1个元素的地址。
在cout和多数C++表达式中,char数组名,char指针已经双引号括起的字符串常量常都被解释为字符串的第1个字符的地址。
字符串字面值是常量,使用关键字const。
4.8.4 使用new创建动态结构
structTypeName* ps = new structTypeName;
struct inflatable
{
int price;
...
};
inflatable* ps = new inflatable;
ps->price = 10; // ->成员运算符
C++可以用->箭头成员运算符来访问指针所指的结构的成员。
如果结构标识名是结构名,则使用.句点运算符;如果标识符是指向结构的指针,则使用箭头箭头运算符。另一种访问结构指针的成员方法是
(*inflatable).price;
先解引用,获得结构变量,然后在使用.句号成员运算符访问成员。
4.8.5 自动存储、静态存储和动态存储
根据内存分配的方法,C++有三种管理数据内存的方法:自动存储(栈)、静态存储和动态存储(自由存储空间或堆)。C++11 还新增了第四种类型--线程存储。
在函数内部定义的常规变量使用自动存储空间,被称为自动变量,意味着它们在所属的函数被调用时自动产生,在该函数结束后消亡。
实际上,自动变量是一个局部变量,其作用域为包含它的代码块。如果在某个代码块定义了一个变量,则该变量仅在程序执行该代码块中的代码可见。
自动变量通常存储在栈中。意味执行代码块时,其中的变量将以此加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出(LIFO)。因此,在程序运行过程中,栈将不断地增大和缩小。
静态存储时整个程序执行期间都存在地存储方式。使变量成为静态存储方式有两种:一种使子啊函数外面定义它;另一种使在声明变量时使用关键字static。
new与delete运算符提供了一种比自动变量和静态变量更灵活地方法。它们管理了一个内存池,这在C++中被称为自由存储区或heap堆。该内存池同用于静态变量和自动变量地内存时分开的。new和delete让您能够在一个函数中分配内存,而在另一个函数中释放它。因此,数据的生命周期不完全受程序或函数的生存时间控制。
在栈中,自动添加或删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪和分配内存的位置更困难。
若调用new后,却没有用delete删除。结果则是导致内存泄漏。即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由内存空间上动态分配的变量或结构也将继续存在。实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将程序的整个生命周期内都不可使用。这些内存被分配出来,但无法收回。
指针式功能最强大的C++工具之一,但也最危险,因为它们允许执行对计算机不友好操作,如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次。
4.9 类型组合
structTypeName* pt1; // 指向结构的指针
structTypeName pt2[num_elems]; // 结构数组
structTypeName* pt3[num_elems]; // 结构指针数组
4.10 数组的替代品
4.10.1 模板类vector
首先,要使用vector对象,必须包含头文件vector。其次,vector包含在名称空间std中,因此您可使用using编译指令、using声明或std:vector。再次,模板使用不同的语法来指出它存储的数据类型。
vector<typeName> vt(n);
4.10.2 模板类array(C++ 11)
与数组一样,array对象的长度也是固定的,也使用栈(静态内存),而不是自由存储区。
array<typeName, n_elem> arr;
4.10.3 比较数组、vector和array
首先,数组、vector对象和array对象,都可使用标准数组表示法来访问各个元素。其次,从地址可知,array对象和数组存储在相同的内存区域(栈区),而vector对象存储在另一个区域(自由存储区域或堆)。第三,注意到可以将一个array对象赋给另一个array对象;而对于数组,必须逐元素赋值数据。
中括号索引和成员函数at()的差别在于,使用at()时,将在运行期间捕获非法索引,而程序默认将中断。这种额外检查的代价时运行时间更长。类还有成员函数begin()与end()。
4.11 总结
数组、结构和指针是C++的3种复合类型。数组可以在一个数据对象中存储多个同种类型的值。通过使用索引或下标,可以访问数组中各个元素。
结构可以将多个不同类型的值存储在同一个数据对象中,可以使用成员关系运算符(.)来访问其中的成员。
共用体可以存储一个值,但是这个值可以是不同的类型,成员名指出了使用的模式。
指针式被设计用来存储地址的变量。对指针应用解除引用的运算符,将得到指针指向的位置中的值。
字符串是以空字符为结尾的一系列字符。字符串可用引号括起的字符串常量表示,其中隐式包含了结尾的空字符。
new运算符允许在程序运行时为数据对象请求内存。该运算符返回内存的地址,可以将这个地址赋给一个指针,程序将只能使用该指针来访问这块内存。如果数据对象是简单变量,则可以使用解除引用运算符(*)来获得其值;如果数据对象是数组,则可以像使用数组名那样使用指针来访问元素;如果数据对象是结构,则可以用指针解除引用运算->来访问其成员。
指针和数组紧密相连。如果ar是数组名,则表达式ar[i]被解释为*(ar + i),其中数组名被解释为数组第一个元素的地址。
运算符new和delete允许显式的控制何时给数据对象分配内存,何时将内存归还给内存池。
自动变量是在函数中声明的变量,而静态变量是在函数外部或者使用关键字static声明的变量。