本章内容概览:
- 数组
- C风格字符串
- string类字符串
- getline()和get()读取字符串
- 结构体
- 共用体
- 枚举
- 指针
- new和delete管理动态内存
- 动态数组
- 动态结构
- 自动存储、静态存储和动态存储
- vector和array简述
数组
要创建数组可使用声明语句,数组声明应指出一下三点:
- 存储在每个元素中的值的类型
- 数组名
- 数组中的元素数
例如:
short months[12];
通用格式为:
typeName arrayName[arraySize];
arraySize必须是整型常数或const值,也可以是常量表达式(如sizeof()),具体来说就是不能使用变量,这是因为常量的值是在编译时已知的,而变量值是在运行时设置的,不过使用new来动态设置可以避开这种限制。
C++允许使用列表初始化数组:
int yamcosts = {20, 30, 5}
如果没有初始化函数中定义的数组,则其元素值是不确定的,这意味着元素的值为以前驻留在内存单元的值。
只有在定义数组时才能使用初始化,同时不能使用赋值语句直接将一个数组赋给另一个数组:
int cards[4] = {3,6,8,10}; //valid
int hand[4]; //valid
hand[4] = {5,6,7,9}; //invalid
hand = cards; //invalid
如果只对数组的一部分初始化,剩下的元素会用零来填充。
float totals[500] = {0}; // all the elements are 0
如果初始化数组时方括号内不填常量,那么C++自动根据后面的元素数计算元素个数:
short things[] = {1,5,3,8}; //size: 4
C++11的列表初始化可以省略等号:
double earnings[4] {1,2,3,4};
同时可以在大括号中省略元素来全部零初始化:
float balances[100] {};
同时,列表初始化禁止缩窄转换:
long a[] = {25,92,3.0}; //in valid
char b[4] = {'a','b',123456,'\0'}; //invalid
C++STL提供了数组的替代品——vector,更加的灵活,C++11还提供了array。
字符串
C++有两种字符串:C风格字符串和strinig类型字符串。
C风格字符串用char数组定义,以空字符即'\0'结尾,如:
char b[4] = {'a','b','c','\0'};
不过这种方式很麻烦,我们可以使用双引号来定义字符串,这种字符串成为字符串常量或字符串字面值:
char bird[11] = "Mr. Cheeps"; //未初始化位置处用'\0'填充
char fish[] = "Bubbles";
用括号扩起的字符串隐式地包括结尾地空字符,所以不用特地包括。
字符串常量和字符常量不能互换,如:
char shirt_size = 'S'; //valid
//-----------------------
char shirt_size = "S"; //invalid
因为下面的语句尝试将一字符串地址赋给shirt_size,C++编译器不允许这一操作。
字符串操作
字符串拼接时,前一个字符串的末尾空字符会被下一个字符串的首字符代替。
sizeof()方法可以获得数组长度,而<cstring>头文件中的strlen()方法可以获得字符串的长度,而不是数组本身的长度,不考虑空字符。
字符串输入
字符串输入时,我们考虑使用cin:
char name[20];
char dessert[20];
cin << name; // 输入Alistair Dreed
cin << dessert;
使用上述这种方式会发现,如果我们在输入名字时包括空格,我们来不及对第二个字符串输入进行操作时程序就结束了。由于我们不能输入空字符,所以cin使用空白(空格,制表和换行)来确定字符串的结束位置,所以说cin只读取了Alistari,就将其存储在name数组中了,并在末尾添加空字符,然后Dreed放在输入队列中,然后进入下一个cin时,直接使用输入队列中的Dreed,并将其存储在dessert中,并在末尾添加空字符。
还有的问题是,如果输入字符串长度过长,那么就很难存储在数组中。
因此,我们考虑改为每行读取,cin对象的getline()和get()方法可以实现这个目的,这两个函数都读取一行输入,直到换行符,getline()会丢弃换行符,将其转为一个空字符,get()将换行符保留在输入序列中。
cin.getline()的第一个参数是用来存储字符串的数组名,第二个参数是读取的字符数,同时getline()会自动在末尾填充空字符,在获得所需字符数后会停止读取。如:
cin.getline(name, 20);
cin.get()参数和用法与getline()一致,只是将换行符保留在输入队列中,如:
cin.get(name, 20);
cin.get(dessert, 20);
第一次输入并以回车结尾后,第二次输入的第一个字符遇到的是换行符,此时认为达到末尾,则不会读取内容。不过cin.get()有一种变体,不带参数的cin.get()可以读取下一个字符,所以下面的代码是可用的:
cin.get(name, 20);
cin.get();
cin.get(dessert, 20);
另外,还可以将两个方法拼接起来:
cin.get(name, 20).get();
这是因为cin.get()方法会返回一个cin对象。
如果遇到空行的话,get()方法会设置失效位,接下来的输入会阻断,可以用cin.clear()来恢复输入。
如果输入字符数量多余指定的空间,getline()和get()会把余下的字符留在输入队列中,getline()还会设置失效位,阻断接下来的输入。
string类
string类在std名称空间中。
- 可以使用C风格字符串来初始化string对象
- 可以使用cin来将输入存储到string对象
- 可以使用cout来显示string对象
- 可以使用数组表示法访问string对象中的字符
string对象创建方式:
std::string str1;
std::string str2 = "panther";
同样string对象也可以使用列表初始化:
std::string str {"lalala"};
数组间不能直接赋值,但string对象可以直接赋值:
std::string str1;
std::string str2 = "panther";
str2 = str1;
string字符串合并可以直接加在一起:
std::string str3;
str3 = str1 + str2;
string类的输入可以使用cin,也可以使用getline(cin, str),getline()是一个类方法,用于string对象的输入。
结构体
结构体样式:
struct inflatable
{
char name[20];
float volume;
double price;
};
定义结构体后就可以使用结构体名称定义结构体变量:
inflatable hat;
C++定义结构体变量可以省略struct。
定义后就可以使用.
运算符来访问成员:
hat.name;
hat.volume;
hat.price;
同样C++11还可以使用列表初始化:
inflatable duck {"Daphne", 0.12, 9.98};
另外,还可以使用赋值运算符将结构赋给另一个同类型的结构体,这被称为成员赋值:
inflatable duck {"Daphne", 0.12, 9.98};
inflatable cat;
cat = duck;
还可以同时完成结构体定义和变量声明:
struct perks
{
int key_number;
char cat[12];
} mr_smith, ms_jones;
还可以像下面这样加上结构体变量初始化:
struct perks
{
int key_number;
char cat[12];
} mr_glitz =
{
7,
"Packard"
};
同时还可以省略结构体名称,只是之后不能创建同类结构体:
struct
{
int x;
int y;
}position;
结构体也可以作为一个类型用来创建数组,同时使用列表初始化:
inflatable gifts[2]=
{
{"Bambi", 0.5, 21.99},
{"Godzilla", 2000, 565.99}
};
共用体
共用体可以存储不同的数据类型,但只能同时存储其中的一种类型,即结构体可以同时存储int、long、double,共用体只能存储int或long或double:
union one4all
{
int int_val;
long long_val;
double double_val;
};
这样使用:
one4all pail;
pail.int_val = 15;
pail.double_val = 1.38; //lost int_val
共用体每次只能存储一个值,因此必须有足够的空间来存储空间最大的成员。
枚举
枚举可用来替代const常量,定义:
enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
spectrum作为新类型名称,red~ultrviolet作为符号常量,存储的整型值对应0-7。使用这个枚举名称创建枚举变量:
spectrum band;
在不进行强制转换的情况下,只能将枚举常量赋给该变量:
band = blue; //valid
band = 2000; //invalid
枚举只能使用赋值运算符:
band = orange; //valid
++band; //invalid
枚举量可以被提升为int类型,反之不能自动完成:
int color = blue; //valid
band = 3; //invalid
可以使用强制转换将int值转化为枚举类型:
band = spectrum(3);
枚举值在定义时可以显式赋值:
enum bite {one = 1, two = 2, four = 4, eight = 8};
可以只指定部分值:
enum bigstep {first, second = 100, third};
第一个值默认为0, second后面的值默认比前面的值大1.
还可以创建多个值相同的枚举量:
enum {zero,null = 0, one, number_uno = 1};
这样,zero为0,one为1。
指针和自由存储空间
指针是一个变量,其存储的是值得地址,而不是值本身。使用&
运算符可以获得变量的地址。
使用常规变量时,值是指定的量,而地址为派生量。
面向对象编程和传统的过程式编程的区别在于,OOP强调在运行阶段进行决策,而后者在编译阶段决策。例如,考虑对数组分配内存的情况,传统的方法是声明一个数组,在C++中,必须指定数组的长度,这在编译时就决定好了,假如大多数情况下我们只需要20个元素,但有时我们需要200个元素,这样就必须定义200长度的数组,很显然,这浪费了许多内存,而OOP将这一决策推迟到运行阶段,可以动态地告诉程序需要多少元素。C++采用的策略是使用new来请求正确数量的内存,并使用指针来跟踪新分配的内存的位置。
就像上面提到的新策略,变量将地址视为指定的量,将值视为派生量,这就是指针。可以使用*
运算符获得指针存储地址处的值。例如:
int a = 6;
int* p_a;
p_a = &a;
cout << *p_a; //6
声明指针使用类型加*
,如int*
被称为指向int的指针,是一种复合类型。
注意C++在创建指针时,计算机分配用来存储地址的内存,但不会分配用来存储指针所指向数据的内存,如下面是不合法的:
long* fellow;
*fellow = 223323;
fellow创建后,不知道它指向哪里,那么223323就不知道存放在哪里了,所以其可能存储在任何地址处。
一定要在使用*
前让指针指向某一值的地址。
new
C++可以使用new来为指针分配内存:
int* pn = new int;
new将找到指定类型的长度正确的内存块,并返回该内存块的地址。
比较一下传统方法:
int higgens;
int* pt = &higgens;
这一方法的指针指向了某一具体变量的地址,但使用new来分配内存的话,指针指向的是某一未命名的内存,这一内存可被称为数据对象。
delete
在用new为指针分配内存后,需要在合适的时候使用delete释放内存空间:
int* ps = new int;
...
delete ps;
delete只会释放ps指向的内存,但不会删除ps。如果不使用delete,可能会发生内存泄露。
用new创建动态数组
例如:
int* psome = new int[10];
这将创建一个包含10个元素的数组,new运算符返回该数组地址,并赋予psome指针,同时最后需要释放该内存:
delete[] some;
psome指向数组的首地址。同时,不能使用sizeof运算符来确定动态分配的数组包含的字节数。
创建动态数组通用格式:
type_name* pointer_name = new type_name[num_elements];
我们可以直接使用[]
来使用这一动态数组,如psome[1]
,即C和C++中数组和指针大致等价。
我们不能修改数组名的值,但可以修改指针的值,对于psome
,它是数组首地址,如果psome+1
,那么就是数组第二个元素的地址,以此类推,也就是指针加减对应指向类型的字节数。
指针、数组和指针运算
指针运算就如我们上面所说的,同时,数组在C++中被解释为地址,数组名解释为数组首元素地址,即:
int[10] =wages;
int* pw = wages; // 将数组首地址赋予指针
wages = &wages[0]; // 等价
还是上面的数组,对于wages[1],C++解释为*(wages+1)
,即首地址加上字节数后获得第二个元素地址,然后*
取值,依此类推。但对于sizeof运算符,C++不将数组名解释为地址。
对数组取地址时,数组名也不会被解释为其地址:数组名被解释为首地址,&数组名被解释为整个数组的地址。对于
short tell[10]
,tell+1
将首地址加2,&tell+1
将地址加20,换句话说,tell
是*short
,'&tell'是short(*)[20]
,即指向包含20个元素的short数组。>
使用new创建动态结构体
inflatable* ps = new inflatable;
创建很简单,C++为这结构体指针提供了->
运算符来访问成员。
ps是指向结构的指针,*ps
是被指向值本身——结构本身,所以可以(*ps).price
这么使用。
自动存储、静态存储和动态存储
自动存储
在函数内部定义的常规变量使用自动存储空间,被称为自动变量,即在函数调用时创建,函数结束时消亡。换句话说,自动变量是一个局部变量,作用域是包含它的代码块。自动变量常存储在栈中,按后进先出的方式。静态存储
静态存储是整个程序执行期间都存在的存储方式。有两种方式:在函数外定义;使用static关键字声明。
static double fee = 56.50;
- 动态存储
使用new和delete运算符。它们管理一个内存池,这在C++中称为自由存储空间或堆,和静态变量、自动变量的内存池分开。我们可以在一个函数中new,另一个函数中delete。
栈、堆和内存泄漏:如果使用new运算符创建内存空间后,没有调用delete,即使包含指针的内存由于作用域规则和对象声明周期的原因被释放,在自由存储空间上的动态分配的变量或结构也存在着,由于无法访问,即指向这些内存的指针无效了,这将导致内存泄漏,被泄露的内存在程序的整个声明周期内都不可使用,最糟糕的情况就是内存耗尽,无法继续分配内存。
C++之后添加了智能指针,它可以自动管理内存,在合适的时候释放内存。如果C++使用者不能确定何时使用delete,建议使用智能指针,只是占用的内存大一点。
数组替代品
vector
vector是一种动态数组,自动使用new和delete来管理内存。创建一个vector对象的方式:
std::vector<typeName> vt(n_elem);
n_elem可以是0,同时可以使用索引[]
来访问元素。
array
使用vector效率稍低,如果是固定长度数组,使用数组更好,但不安全,这时候可以使用array。array对象长度固定,也是用栈(静态内存分配),和数组效率相同,但更方便安全。创建一个array对象方式:
array<typeName, n_elem> arr;