第 4 章 复合类型
本章内容包括:
- 创建和使用数组
- 创建和使用C风格字符串
- 创建和使用string类字符串
- 使用方法getline()和get()读取字符串
- 混合输入字符串和数字
- 创建和使用结构
- 创建和使用共同体
- 创建和使用枚举
- 创建和使用指针
- 使用new和delete管理动态内存
- 创建动态数组
- 创建动态结构
- 自动存储,静态类型,动态存储
- vector和array类简介
4.1 数组
数组也是复合类型。
创建数组的三点要素:
- 存储在每个元素中的值的类型
- 数组名
- 数组中的元素数
C++中可以通过修改简单变量的声明,添加中括号来完成数组声明。
通用格式为:
typeName arrayName[arraySize];
arraySize必须为整形常数或者是const值,也可以是常量表达式,总而言之,需要在编译时都是已知的。不能是变量,变量是在运行时设置的。当然,可以使用new运算符来避开这种限制。
sizeof运算符会返回类型或数据对象的长度(单位为字节)。
如果将sizeof运算符用于数组名,会得到整个数组的字节数,用于数组的单个元素,就会获得单个元素的字节数。
4.1.2 数组的初始化规则
只有在定义数组时能够使用初始化。之后就不能使用了,也不能进行数组之间的赋值。
int cards[4] = {1,2,3,4}; //ok
int hand[4];
hand[4]={1,2,3,4}; // no!
hand=cards; //no!
初始化数组时,提供的值可以少于数组元素数目。此时,其他元素会被设置为0。
如果初始化数组时方括号内([])为空,那么C++编译器会计算元素个数。
比如:int things[] = {1,5,3,8};
编译器会赋值给数组4个元素长度。
但是通常情况下,使用编译器去计算元素个数是很糟糕的做法。
4.1.3 C++11数组初始化方法
可以省略=,比如 int a[10]{}
4.2 字符串
字符串是存储在连续字节中的一系列字符。
C风格的字符串以‘\0’为结尾。
声明方式:
char bird[11] = "Mr.Cheeps"
char fish[] = "Buddle"
4.2.1 拼接字符串常量
C++允许拼接字符串字面值,任何两个由空白(空格 制表符 换行符)分开的字符串都将自动拼接成一个。
cout << "my name is" " aiky\n";
4.2.2 在数组中使用字符串
在头文件cstring中,提供了库函数strlen(),可以用来确定字符串的长度。
strlen会读到‘\0’,只会计算可见字符。
4.2.3 字符串输入
cin使用空白(空格 制表符 换行符)作为字符串读取结束的位置。
4.2.4 每次读取一行字符串输入
cin提供了一些面向行的类成员函数:getline()和get()。
两个函数都会读取一行数据,但是getline会抛弃换行符号。
cin.getline(name,20);
表示读取19个字符,最后一个字符用来保存‘\0’。
cin.get()读取到行尾后,并不会读取换行符。
比如使用cin.get(name, 20)后,会读取到行位,但是行尾换行符并没有读取。
使用不带参数的cin.get()可以直接读取下一个字符。
所以可以使用cin.get(name,Size).get(); 来读取完数据后,继续读取换行符。
4.2.5 混合输入字符串和数字
4.3 string类简介
string类型位于std名称空间内。
string和字符数组的区别在于,strnig对象可以声明为简单变量,而不是数组。
4.3.2 赋值、拼接和附加
string类型简化了字符串合并操作。
可以使用 + 来将两个string对象进行合并。也可以使用+=附加到末尾。
4.3.3 string类其他操作
头文件cstring中,可以使用strcpy()函数将字符串复制到字符数组中。也可以使用strcat()将字符串附加到字符数组末尾。
4.3.4 string类IO
输入一行数据到字符数组:
cin.getline(charr,20)
输入一行数据到string:
getline(cin,str);
虽然都叫getline,但是上面的是cin的对象函数,下面的是istream的类方法,以cin为参数,指出到哪里寻找输入。
4.3.5 其他形式的字符串字面值
C++11还有char16_t或者char32_t类型的字符串。可以在赋初值的时候加上对应的前缀。
char32_t car[]=U"Humber Car";
C++11还支持unicode类型,使用前缀u8。
C++11还新增了另一种原始字符串(raw)。使用 "( 和 )" 作为界线符号。不用再繁琐的使用反斜杠做转义。使用前缀R作为原始字符串的标识。
cout << R"(Jim "kinds \n sdhn)" <<endl;
输出的就是Jim "kinds \n sdhn。
如果原始字符串中包含 )" ,会认为到此结束。但是原始字符串给了解决方法,允许在“(之间添加其他字符。比如:
cout << R"+*("(who??)",he!.)+*" <<endl;
那么将会输出"(who??)",he!.
4.4 结构简介
结构是C++ OOP的基石。
使用关键字struct来声明结构体,结构体重的每一项被称为结构成员。
C++允许直接使用结构体名创建变量。
可以使用成员运算符(.)来访问各个成员。
4.4.1 在程序中使用结构
定义在外,对整个文件生效;定义在函数内,只对函数生效。
4.4.2 C++11结构初始化
inflat duck {"Dsaf", 0.12, 9.9 };
使用大括号进行初始化,等号可以省略,如果括号内没有包含东西,则各个成员被设置为0。
结构体也可以使用赋值运算符(=),即使成员中包含数组。
4.4.6 结构中的位字段
C++允许指定占用特定位数的结构成员,使得创建于某个硬件设备上的寄存器对应的数据结构非常方便。可以使用没有名称的字段来提供间距。
比如:
struct torgle_register{
unsigned int SN : 4 ; // 4 bits for SN value
unsigned int : 4 ; // 4 bits unused
bool goodIn : 1 ; // valid input
bool goodTorgle : 1 ; //successful torgling
}
初始化时可以忽略位字段,使用正常初始化方法。
位字段通常用在低级编程中,可以使用整形来代替这种方式。
4.5 共用体
共用体union是一种数据格式。能够存储不同的数据类型,但是只能同时存储其中一种类型。
结构体可以同时存储int long double,但是共用体只能存储其中一个值。
由于共用体只能存储一个值,所以必须有足够大的空间来存储最大的成员。
共用体的用途之一是在使用多种格式,但是不同时使用时,能够节省空间。
共用体还用于嵌入式系统编程,用于节省内存。
4.6 枚举
C++的enum提供了另一种创建符号常量的方法,可以替代const。
比如语句:enum spectrum { red,orange,tellow,blue };
创建了新类型的名称spectrum,将red等作为符号常量,对应整数0,1,2,3。
默认情况下,使用整数值赋给枚举量。默认0开始,也可以显示的指定。
并且只能够使用枚举量为枚举变量赋值。
spectrum band;
band = blue; //ok
bane = 200; //no!
但是band = blue+red;是非法的,计算时会转成int,但是没有办法把int赋值给枚举类型。
除非强制转化,使用band = spectrum(blue+red);
如果只打算使用枚举类型的常量,不打算使用变量,那么也可以省略枚举类型的名称。
4.6.1 设置枚举量的值
可以显示赋值来设置枚举量值。
enum bits{one = 1 ,two=2,four = 4,eight = 8};
指定的值必须为整数,enum中后面的值会比前面的值大1.
enum bigstep{first,second = 100,third};
那么此时third的值会变成101。
枚举量不必不同,可以使用相同的值做枚举。
4.7 指针和自由存储空间
计算机程序在存储数据时必须跟踪三种基本属性:
- 信息存储在何处
- 存储的值为多少
- 存储的信息时什么类型
指针在C++类开发中非常重要,指针是一个变量,存储的是值的地址,不是存储的值本身。
OOP强调的是在运行阶段进行决策,面相过程是在编译阶段进行决策。运行阶段决策提供了灵活性。
C++中使用 & 作为取地址符号,使用 * 作为取值符号或者解除引用符号。
4.7.1 声明和初始化指针
指针声明必须指定指针指向的数据的类型。
定义指针时,每个指针变量名,都需要一个 *。
比如 int* p1, p2;表示定义了一个指针p1和一个整数p2。
可以在定义时赋初值:
int higgrnd = 5;
int *pt = &higgrnd;
4.7.2 指针的危险
在C++中创建指针时,计算机将会分配用来存储地址的内存,但是不会分配用来存储指针纸箱数据的内存 。
所以一定要在对指针应用之前,将其初始化为一个确定且适合的地址。
4.7.3 指针和数字
指针不是整形。虽然计算机会把地址值作为整数来处理。但是不能简单的讲整数赋值给指针。
4.7.4 使用new来分配内存
指针在运行时分配内存才更有用武之地。使用new来分配内存。
typeName * pointer_name = new typeName;
比如: int *pn= new int;
new int会告诉程序,需要适合存储int的内存,new运算符会根据类型来确定需要多少字节的内存。
然后寻找内存并返回地址。
地址本身只是指出了对象存储地址的位置,并没有指出其类型使用字节数,所以在声明指针时需要指定数据类型。
另外,使用new分配的内存块通常情况下和常规变量声明分配的内存块不同,变量和指针变量的值会被存储在栈(stack)内存区域,而new分配的内存在堆(heap)或者自由存储区(free store)。
如果new没有足够的内存用来满足请求,那么通常会引发异常。C+中,new会返回0,被视为空指针。
4.7.5 使用delete释放内存
delete运算符可以在使用完内存后,将其归还给内存池。归还的内存可供其他部分使用。
int *ps = new int;
...
delete ps;
使用delete之后将会删除指针指向的内存,但是不会删除指针变量本身。
此外,一定要配对的使用new和delete,避免出现内存泄漏问题。另外不要尝试释放已经释放过的内存块,这样做会造成不确定结果。delete也不能释放变量声明的内存。
一般来说,不要使用两个指针指向同一个内存块,防止使用delete出现问题。
4.7.6 使用new来创建动态数组
对于大型数据,应该使用new。
如果通过声明来创建数组,那么在程序被编译时,将会分配内存空间,称为静态联编。
在运行时选择数组的长度,被称为动态联编,
为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num_elements];
比如,要创建10个int元素的数组:
int * psome = new int[10];
要删除整个数组时:
delete [] psome;
方括号告诉程序应该释放整个数组,而不是只释放指针指向的元素。new和delete的方括号也应该是匹配的。
总之,使用new和delete的规则:
- 不要使用delete释放不是new分配的内存
- 不要使用delete释放两次同一块内存
- 如果使用new []为数组分配内存,那么也应该使用delete []来释放
- 如果使用new为一个实体分配内存,那么应该是用delete来释放
- 对空指针使用delete是安全的
在创建数组之后,指针实际上指向第一个元素。
如果想访问其他元素,可以使用下表的方式:
比如第一个元素,可以使用psome[0]来访问;第2个元素,可以使用psome[1]来访问。
数组和指针基本等价是C++的优点之一。
指针算数有特殊性,如果使用了psome = psome +1,那么psome就会指向原来的第2个元素。
4.8 指针、数组和指针算术
在多数情况下,C++将数组名截石位数组的第一个元素的地址。
将指针变量加1后,其增加的值等于指向的类型占用的字节数。
对于数组表达式 stacks[1] ,C++编译器将该表达式看做为 *(stacks + 1),意味着要先计算数组第2个元素的地址,然后找到存储的值。(另外,如果不适用括号,那么将会看成给*stacks的值+1)
因此,在很多情况下,可以使用相同的方式运算指针名和数组名。
区别之一是,可以修改指针的值,但是数组名为常量;另一个区别是,对数组sizeof得到数组的长度,对指针sizeof得到指针的长度。
对于数组来说,数组名被解释为第一个元素的地址,而对数组名应用取地址符,得到的是整个数组的地址。
比如 short array[10];
从数字上来说,两个地址是一样的,但是从概念上来说,array 和 &array[0]是一个2字节内存块的地址,而&array是一个20字节内存块的地址。因此,array+1会将地址+2,&array+1会将地址+20。换句话说,&array是一个指针,类型是10元素的short数组。那么,short (*pas)[20] = &array; 是成立的。(*pas)[0]为array数组的第一个元素。
4.8.3 指针和字符串
在cout和多数C++表达式中,char数组名、char指针以及引号括起来的字符串常量都被解释为字符串第一个字符的地址。
4.8.4 使用new创建动态结构
比较棘手的是访问成员。
创建动态结构时,不能将成员运算符号(.)用于结构名,因为没有名称,只知道地址。
C++中使用->,箭头成员运算符,来访问成员。
另一种方式是使用 (*ps).price 的方式来访问成员。
4.8.5 自动存储、静态存储和动态存储
C++有3种管理数据内存的方式:自动存储,静态存储和动态存储。C++11新增了都四种类型,线程存储。
1.自动存储
在函数内部定义的常规变量使用自动存储空间,叫做自动变量,又叫做局部变量。
通常存储在栈中。离开代码块时,将按照相反的顺序释放这些变量,被称为后进先出(LIFO)。
2.静态存储
静态存储是整个程序执行期间都会存在的存储方式,使变量成为静态的方式有两种:在函数外面进行定义;在声明变量时使用关键字static。
自动存储和静态存储的关键在于,严格的限制了变量的寿命,变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。
3.动态存储
new和delete运算符提供了动态存储,管理一个内存池,在C++中被称为自由存储空间或者堆。
该内存池和用于静态变量和自动变量的内存是分开的。
在栈中,自动添加和删除机制是的占用内存总是连续的,但是new和delete相互影响可能导致占用的自由存储区不连续。跟踪新分配的内存位置会更困难。
4.9 类型组合
有时有组合起来的类型会很复杂。
比如: const ante **ppa = arp;
这时候可以使用C++11版本的auto,auto ppb = arp,能更方便。
另外 *ppa -> year 试图将*应用于ppa->year,所以要加上括号,(*ppa) ->year
4.10 数组的替代品
4.10.1 模板类vector
要使用vector需要包含头文件vector。
vector是一种动态数组,可以在运行阶段设置长度, 添加数据或删除数据。
vector内部是使用new和delete来管理内存的,但是会自动完成。
使用方式为:vector<typeName> vt(n_elem);
比如 vector <int> vi(10); 其中n_elem为整型常量,代表初始长度。
4.10.2 模板类array (C++11)
array类需要包含头文件array。
vector的功能要比数组强大,但是付出的代价是效率比较低。
C++11增加了array类,array对象的长度固定,使用栈来进行存储,不使用堆。因此效率和使用数组时相同。
创建方式为:array<typeName, n_elem> arr;
表示创建了一个包含n_elem个类型为typeName的,名为arr的array对象。
与创建vector对象不同的是,n_elem不能是变量。
4.10.3 比较数组、vector对象和array对象
无论是数组、vector对象还是array对象,都可以使用标准数组表示法来访问各个元素。
数组和array对象都在栈内存中,而vector在堆内存。
可以将一个array对象赋值给另一个array对象;而对于数组,必须逐个进行复制。
对于数组来说,使用a1 [-2] = 20.2;编译器是不会报错的,会转换成*(a1-2)=20.1;即找到a1指向的位置,向前移动两个元素长度。所以会不安全。
对于vector和array来说,使用数组下标方式访问也会不安全。
但是如果使用at()的方式,将在运行期间捕获非法索引。但是有额外检查代价,运行时间会更长。
如果要遍历的话,使用begin()和end()也能避免超过边界。
第5章 循环和关系表达式
本章内容包括:
- for循环
- 表达式和语句
- 递增运算符和递减运算符:++、--
- 组合赋值运算符
- 复合语句(语句块)
- 逗号运算符
- 关系运算符:>,>=,==,<=,< 和 !=
- while循环
- typedef工具
- do while循环
- 字符输入方法 get()
- 文件尾条件
- 嵌套循环和二维数组
5.1 for循环
for (initialization ; test-expression ; update-expression)
body
for循环:
1.设置初始值
2.执行测试,看循环是否能够继续执行
3.执行循环操作
4.更新用于测试的值
- for是一个C++关键字,所以要避免将函数命名为for。
- C++表达式是值或值与运算符的组合,每个表达式都有值。
- 可以使用 cout.setf(ios_base::boolalpha); 来将1或者0转化为true或者 false进行输出。
5.1.7 前缀格式和后缀格式
C++的++和--有前缀版本++x和后缀版本x++,两个版本对操作数的影响是一样的,但是影响的时间会有所不同。前缀会在运行完++后加入表达式,后缀会在表达式运行后再执行++。
从逻辑上来说,前缀后缀版本没有区别。
但是实际上执行速度会有细微差别。对于内置类型和当代编译器来说影响不大。
但是对于针对类重载的运算符,使用前缀:将值+1,返回结果。使用后缀:先复制一个副本,将其+1,然后返回副本值。
因此来说,前缀版本的效率会更高。
5.1.8 递增递减运算符和指针
对指针执行++和--,指针会移动一个指向类型的距离。
但是如果和* 一起使用,那么需要考虑优先级。
前缀递增(++x)、前缀递减(--x)和解除引用运算符(*)的优先级相同,从右到左结合。
后缀递增(x++)、后缀递减(x--)优先级相同但比前缀运算符的优先极高。
比如:
*++p 代表先指针移动距离,再取值。
++*p代表先取值,再对值+1
*p++代表先将p+1,再进行取值。
5.1.10 复合语句
使用花括号作为一个语句块。
在外部语句块中定义的变量在内部语句块中也是被定义了的。
如果语句块内外同时包含一个相同名称的变量,那么内部语句块中的新变量将隐藏旧变量。
5.1.11 逗号运算符
逗号运算符允许将两个C++表达式句法放在一个表达式位置。
比如:i++,j--;
逗号运算符还可以在声明变量时将相邻的名称进行分割。
比如:int i,j;
逗号运算符确保先计算第一个表达式,然后计算第二个表达式。
C++规定,逗号表达式的值是第二部分的值。
比如 int a=(12,240); 那么赋值为240
5.1.14 字符串比较
判断两个C风格字符串是否相等:
strcmp(str1,str2) == 0
如果str1在str2前面,那么strcmp(str1,str2)<0为true
如果str1在str2后面,那么strcmp(str1,str2)>0为true
判断两个C++风格字符串是否相等:
由于进行了运算符重载,所以可以直接使用<,>,==等进行比较。
5.2 while循环
可以把while循环理解为没有进行初始化的for循环。
while (test -condition)
body
for循环和while循环基本上是等效的,使用哪种只是风格上的问题。
在设计循环时,需要遵守以下几条指导规则:
- 制定循环终止的条件。
- 首次测试之前初始化条件。
- 条件被再次测试之前更新条件。
5.2.2 程序等待一段时间
C++库中提供了函数clock()。
头文件ctime中定义了符号常量 CLOCKS_PRE_SEC。
该常量等于每秒钟包含的系统时间单位数,将系统时间除以该值,即可得到秒数;秒数乘以该值即可得到系统时间单位数。
ctime中将clock_t作为clock()返回类型的别名。(参考类型别名)
举例说明:
#include <iostream>
#include <ctime>
int main(){
using namespace std;
float secs;
cin >> secs;
clock_t delay = secs * CLOCKS_PRE_SEC; //convert to clock ticks
cout << "startings\n";
clock_t start = clock();
while ( clock() - start < delay ) //wait until time slapses
;
cout << "done\b";
return 0;
}
该程序以系统时间单位为单位,计算延迟时间,避免每轮循环将系统时间转换为秒。
类型别名
C++中建立别名的方式有两种:
一种是使用预处理器:
#define BYTE char // replace BYTE with char
第二种是使用关键字typedef来创建别名。:
typedef typeName aliasName;
例: typedef char * byte_pointer;
但是使用define有时会引发问题,比如:
#define FLOAT_POINTER float *
FLOAT_POINTER pa, pb;
这样在预处理器置换时,会变成float * pa,pb;
和意图不符合。
使用typedef可以避免这样的问题。另外typedef不会创建新类型,而是为已有的类型创建一个新名称。
5.3 do while循环
do while循环为出口循环,这种循环首先执行循环体,然后再判定测试表达式。
do
body
while (test-expression);
通常,编写清晰、容易理解的代码比使用语言的晦涩特性来显示自己的能力更为有用。
5.4 基于范围的for循环(C++11)
C++11的新特性,简化了常见的循环任务:对数组或者容器进行遍历操作。
double prices[5] = {4,5,6,7,8};
for (double &x : prices)
x = x*0.8;
&代表x为一个引用变量,能够修改数组中的值。
5.5 循环和文本输入
5.5.1 使用原始的cin进行输入
如果要使用while循环来读入来自键盘的输入。那么必须要直到何时停止。
一般使用特殊字符作为结束符号,被称为“哨兵字符”(sentinel character)。
cin会忽略掉空格和换行符。
发送给cin的输入被缓冲,只有在用户按下回车键后,输入的内容才会被发送给程序。
5.5.2 使用cin.get(char)进行补救
使用cin.get(ch)读取输入的下一个字符(即使是空格),可以解决cin会忽略空字符的问题。
5.5.4 文件尾条件
使用哨兵字符不太让人满意。可以使用EOF来检测。
读取文件中的信息似乎和使用键盘输入没有什么关系,但是在很多PC变成环境中都是可以使用ctrl+Z来模拟EOF的。
检测到EOF后,cin将两位(eofbit和failbit)都设置为1。
可以通过成员函数eof()来判断eofbit是否被设置。如果检测到EOF,那么cin.eof()将返回bool类型true。同样,如果eofbit或failbit被设置为1,fail()成员函数也会返回true,否则返回false。
eof()和fail()方法报告最近读取的结果,在事后报告,而不是预先报告。
#include <iostream>
int main(){
using namespace std;
char ch;
int count = 0;
cin.get(ch); //attempt to read a char
while ( cin.fail() == false ) // test for EOF
{
cout << ch; // echo character
++count;
cin.get(ch); //attempt to read another char
}
cout << endl << count << "characters read\n";
return 0;
}
istream类提供了可以将istream对象(比如cin)转换为bool值的函数,当cin出现在需要bool的地方,该转换函数将会被调用。如果最后一次读取成功,那么的大道true;否则得到false。
比如 :
while (cin) // while input is successful
这样通常会比!cin.fail()或者!cin.eof()更常用,因为可以检测到其他失败原因,比如磁盘故障。
上面可以简化为:
while (ch = cin.get() != EOF)
5.6 嵌套循环和二维数组
初始化二维数组
int maxtemps[4][5] =
{
{1,2,3,4,5},
{6,7,8,9,10},
{3,4,5,6,7},
{6,5,3,5,3}
};