C++ Primer Plus 学习记录(第四章节-包含练习题答案)
4.1 数组
1、数组(array)是一种数据格式,能够存储多个同类型的值。
2、创建数组,声明语句。数组声明指出三点:
a、存储在每个元素中的值的类型
b、数组名
c、数组中的元素数
3、声明数组的通用格式如下:
typeName arrayName[arraySize];
arraySize指定元素数目,必须是整型常数或const值,也可以是常量表达式,其中所有的值在编译时必须是已知的。
4、arraySize不能是变量,变量的值是在程序运行时设置的,使用new可以避开这种限制。
5、比如
float loans[20];// 20是长度
loans的类型不是“数组”,而是“float数组”。这强调了loans数组是使用float类型创建的。
6、从0开始编号C++,无容置疑
7、编译器不会检查使用的下标是否有效。赋值给不存在的元素,编译不会出错,但是可能破坏数据和代码。
8、如果想检验数组的总共长度
sizeof用于数组名即可
int yams[3];
cout << sizeof yams << endl;
yams是一个数组,yams[1]只是一个int变量。
12
4.1.1 程序说明
1、C++允许在声明语句的时候初始化数组元素。
使用初始化列表
int yamconsts[3] = {20, 30, 5};
4.1.2 数组的初始化规则
1、只有定义数组时才能使用初始化,此后不可用,也不能将一个数组赋给另一个数组。
int cards[4] = {3, 6, 8, 10};//可以
int hand[4];//可以
hand[4] = {3, 6, 8, 10};//不可以
hand = cards;//不可以
2、可以使用下标分别给数组中的元素赋值。
3、初始化数组时,提供的值可以少于数组的元素数目。
float hotelTops[5] = {5.0, 2.5};
只初始化hotelTips的前两个元素。
只对一部分初始化,编译器把其他的元素设置为0,那么
long totals[500] = {0};
实现了把所有的元素初始化为0。
4、初始化数组时方括号内为([])为空,C++编译器将计算元素个数,比如
short things[] = {1, 5, 6, 8};
编译器使things数组包含4个元素。但是让编译器计算元素个数是种很糟糕的做法。
4.1.3 C++11数组初始化方法
1、C++11将使用大括号的初始化(列表初始化)作为通用的初始化方式,可用于所有类型。
2、初始化数组时,可省略等号(=):
double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4};
3、可不在大括号内包含任何东西,这将把所有的元素设置为0。
4、列表初始化禁止缩窄转换,第三章讲过。
5、C++也提出了数组替代品,模板类vector,C++11新增了模板类array,下面简要讨论,16章将更详细讨论。
4.2 字符串
1、C++处理字符串方式两种。第一种C语言,称为C-风格字符串(C-style string);还有基于string类库的方法。
2、C-风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0,其ASCII码为0,用来标记字符串的结尾。
char dog[8] = {'b','e','a','u','x',' ','I','I'}//是char数组,但不是字符串
char cat[8] = {'f','a','t','e','s','s','a','\0'};//是字符串
使用cout打印字符串,对于cat数组将打印前7个字符,对于dog数组打印出8个字符后,接着打印直到遇到空字符为止。空字符常见,但是还是不应该如此。
3、上述初始化麻烦-使用了大量的单引号,而且还必须加上空字符。
所以使用字符串常量(string constant)或字符串字面值(string literal)更好。
char bird[11] = "Mr. Cheeps";//\0被隐藏
char fish[] = "Bubbles";
y用引号括起的字符隐式地包括结尾的空字符,因此不用显式地包括他。空值字符自动加到结尾。
应确保数组足够大,能够存储字符串中所有的字符–包括空字符。
使用字符串常量初始化字符数组是这样的一种情况,即让编译器计算元素数目更为安全。
因为让数组比字符串长没有什么坏处,只是会浪费一些空间而已。处理字符串的函数是根据遇到的第一个空字符位置,而不是数组长度进行处理。
C++对字符串长度没有限制。
4、
'S’只是83的一种写法
“S”不是字符常量,他表示两个字符(字符S和\0)组成的字符串。“S”实际上表示的是字符串所在的内存地址。
这个问题在讨论指针后会再次讨论
4.2.1 拼接字符串常量
1、C++允许拼接字符串字面值,即将两个用引号括起的字符串合并为一个。
事实上,任何两个由空白(空格、制表符、换行符)分割的字符串常量都将自动拼接成一个。
cout << "I am" " dage.\n";
cout << "I am dage.\n";
cout << "I am"
" dage.";
拼接时不会在被连接的字符串之间添加空格,第二个字符串的第一个字符将紧跟在第一个字符串的最后一个字符(不考虑\0)后面。第一个字符串中的\0字符将被第二个字符串的第一个字符取代。
4.2.2 在数组中使用字符串
1、将字符串存储到数组中,常用的方法有两种–将数组初始化为字符串常量、将键盘或文件输入读入到数组中。
2、使用标准C语言库函数strlen()来确定字符串的长度。标准头文件cstring提供了该函数以及很多与字符串相关的其他函数的声明。
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
const int Size = 15;
char name1[Size];
char name2[Size] = "C++ owboy";
cin >> name1;//bupohongchen
cout << name1 << endl;
cout << name2 <<endl;
cout << strlen(name2) << endl;
return 0;
}
输出结果为:
bupohongchen
bupohongchen
C++owboy
9
15
sizeof运算符指出整个数组的长度:15字节
strlen()函数返回存储在数组中的字符串的长度,不是数组本身的长度。且strlen()只计算可见的字符,不把结尾后面的空字符计算在内。
数组的长度不能短于strlen()+1。
4.2.3 字符串输入
1、cin确定以完成字符串输入?
键盘不能输入空字符,cin使用空白(空格、制表符、换行符)确定字符串的结束位置。
cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该单词放到数组中,并自动在结尾添加空字符。
2、输入字符串长过目标数组,17章介绍。
4.2.4 每次读取一行字符串输入
1、istream中的类提供了一些面向行的类成员函数:getline()和get()。读取一行输入,直到到达换行符。
其中,getline()丢弃换行符;get()将换行符保留在输入序列中。
1、面向行的输入:getline()
1、通过回车键的换行符来确定输入结尾
2、调用这种方法,使用cin.getline()
。
3、会丢弃读取到的第一个换行符
有两个参数:第一个参数是用来存储输入行的数组的名称;第二个参数是读取的字符数。第三个参数在17章讨论。
这个参数为20,函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。
其在读取指定数目的字符或遇到换行符时停止读取。
cin.getline(name,20);//将姓名读入到一个包含20个元素的name数组中
2、面向行的输入:get()
1、cin.get()有好几种变体。一种与geline()类似,接受的参数相同,解释参数方式也相同,并且都读取到行尾。但get()不再丢弃换行符,而是将其留在输入队列中。
cin.get(name,Arsize);
cin.get(dessert,Arsize);
这会导致,连续使用cin.get()时,第一行行尾的换行符保留,第二个get读取的第一个字符就是遗留的换行符,直接结束读取,没有数据。
2、get()另一个变体,使用不带任何参数的cin.get()调用可读取下一个字符(即使是换行符),因此可以用它处理换行符,为读取下一行输入做准备。
cin.get(name,Arsize);
cin.get();
cin.get(dessert,Arsize);
3、另一个get()是将两个类成员函数拼接起来(合并)。
cin.get(name,Arsize).get();//相当于调用了两次get(),因为cin.get(name,Arsize)返回的是一个cin对象,之后再次被调用
4、C++允许函数有多个版本,条件是这些版本的参数列表不同。第八章探索这种特性–函数重载。
5、为什么要使用get(),而不是getline()呢?
老式实现没有getline();get()使输入更仔细。
6、如何知道已经读取到了行,而不是由于数组已填满?
查看下一个输入字符,如果是换行符,说明已经读取到了整行;否则,说明该行中还有其他输入。17章将介绍这种技术。
3、空行和其他问题
1、当getline()或get()读取空行时,发生什么情况?
get()(不是getline())读取空行后将设置失效位(failbit)。这意味着接下来的输入将被阻断,但可以使用下面的命令来恢复输入:
cin.clear();
2、当输入字符串可能比分配的空间长。则getline()和get()将把余下的字符留在输入队列中,而getline()还会设置失效位,并关闭后面的输入。
4.2.5 混合输入字符串和数字
1、当两行使用cin时(第一行是cin,第二行是cin.geline()
),需要在第一行输入之后使用cin.get();
,否则用户对于第二行根本没有输入的机会。
这是因为第一行输入回车键生成的换行符留在了输入队列中。后面的cin.getline()
看到换行符之后,认为这是一个空行,并将一个空字符串赋给后面这个数组。
解决:在第一行cin之后加入:
cin >> year;//第一行
cin.get();
或者利用表达式cin >> year返回cin对象,将调用拼接起来:
(cin >> year).get();//or (cin >> year).get(ch);
2、C++常用指针(而不是数组)处理字符串。
4.3 string类简介
1、要使用string类,必须在程序中包含头文件string。
2、string类位于名称空间std中,因此您必须提供一条using编译指令,或者使用std::string来引用它。
3、string和字符数组的相同之处:
a、可以使用C-风格字符来初始化string对象。
b、可以使用cin来将键盘输入存储到string对象中。
c、可以使用cout来显示string对象。
d、可以使用数组表示法来访问存储在string对象中的字符。
4、string对象和字符数组之间的主要区别是:
a、可以将string对象声明为简单变量,而不是数组。
b、类程序让程序能够自动处理string的大小。虽然声明的是长度为0的string的对象,但程序将输入读取到str1中时,将自动调整str1的长度。
string str1;//创建一个空的string
string str2 = "panther";//创建一个初始化好的string
cin >> str1;//str1 重置大小为了适应输入
5、可以将char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的字体。
4.3.1 C++11字符串初始化
1、C++11也允许将列表初始化用于C-风格字符串和string对象:
char first_data[] = {"zhao"};
char second_data[] {"qian"};
string third_data[] = {"sun"};
string fourth_data[] = {"li"};
4.3.2 赋值、拼接和附加
1、可以将一个string对象赋值给另一个string对象,但是不能将一个数组赋给另一个数组。
2、string类简化了字符串合并操作,可以使用运算符+将两个string对象和并起来。还可以使用运算符+=将字符串附加到string对象的末尾。
4.3.3 string类的其他操作
1、对于C-风格字符串,头文件cstring(以前是string.h)提供这些函数。
2、函数strcpy()将字符串复制到字符数组中
3、使用函数strcat()将字符串附加到字符数组末尾
strcpy(charr1, charr2);//复制charr2数组给charr1数组
strcat(charr1, charr2);//把charr2数组的内容加到charr1数组上
4、string类的大小可以使用 ,确定字符串字符数的方法
string str1 = "panther";
int len = str1.size();
char charr1[20] = "dog";
int len1 = strlen(charr1);
len为7
len1为3
计算 长度只计算能够看得到的字符,也就是最后的空字符不算,但是字符中间的空格是算的
strlen()接受一个C-风格字符串作为参数,并且返回该字符串包含的字符数。
str1是一个string对象,而size()是string类的一个方法。
5、使用strncat()和strncpy()可以避免目标数组过小,数据损坏和程序终止等问题,但是会进一步增加编写程序的复杂度。
4.3.4 string类I/O
1、可以使用cin运算符<<输入存储到string对象中
使用cout和运算符>>来显示string对象
虽然与处理C-风格字符相同,但是每次读取一行而不是一个单词时,使用的句法不同。
2、数组charr[20]
中的字符串长度为27,要比该数组的长度要大。这是因为:
a、为初始化的数组的内容是未定义的;
b、函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符。这里可能在数组末尾的后几个字节才遇到空字符。对于未初始化的数据,第一个空字符出现的位置是随机的。
char charr[20];
int len = strlen(charr);//这个值不一定是20
3、用户输入之前,定义的string类的字符串长度为0,因为未被初始化的string对象的长度被自动设置为0。
4、cin是一个istream对象,getline()是istream类的一个类方法。
5、将一行输入读取到string对象中的代码:
getline(cin, str);
这里没有使用句点表示法,说明getline()不是类方法,他将cin作为参数,指出到哪里去查找输入。也没有指出字符串长度的参数,因为string对象将根据字符串的长度自动调整自己的大小。
4.3.5 其他形式的字符串字面值
1、除char类型外,C++还有类型wchar_t;而C++11新增了类型char16_t和char32_t。
可创建这些类型的数组和这些类型的字符串字面值,C++分别使用前缀L、u和U表示。
wchar_t title[] = L"zhao qian";
char16_t name[] = u"sun li";
char32_t car[] = U"Humber Suber";
2、原始字符
前面带R"( )"
4.4 结构简介
1、同一个结构可以存储多种类型的数据。
2、创建结构的两步:
a、定义结构描述----它描述并标记了能够存储在结构中的各种数据类型。
b、然后按描述创建结构变量
2、例子:公司创建一种类型描述生产线上充气产品的成员。这种类型应存储产品名称、容量和售价。下面结构的描述可以满足这些要求:
struct inflatable
{
char name[20];
float volume;
double price;
}
关键字struct表明,这些代码定义的是一个结构的布局。
标识符inflatable是这种数据格式的名称,因此新类型的名称为inflatable。
大括号内包含的是结构存储的数据类型的列表,其中每个列表项都是一条声明语句。列表中每一项都称为结构成员。
结构定义指出了新类型的特征,使用:
inflatable hat;
inflatable woopie_cushion://是inflatable类型
inflatable mainframe;
C++允许在声明结构变量时省略关键字struct:
struct inflatable goose;//完整的应该是这样
可以使用成员运算符(.)来访问各个成员。通过成员名能够访问结构的成员。
总之,hat是一个结构,price是一个变量。
其实访问类成员函数(如cin.getline())的方式是从访问结构成员变量(如hat.price)的方式衍生而来的
4.4.1 在程序种使用结构
1、初始化
#include <iostream>
struct inflatable
{
char name[20];
float volume;
double price;
};
int main
{
using namespace std;
inflatable guest =
{
"Glorious Gloria",
1.88,
29.99
};
}
结构声明的位置很重要。对于上述而言,有两种选择。
可以将声明放在main()函数中,紧跟在开始括号的后面。
也可以将声明放在main()的前面,这里采用的这种方式,称为外部声明。
外部声明可以被后面的任意函数使用,而内部声明只能被该声明所属的函数使用。通常使用外部声明。
变量也可以在函数内部和外部定义,外部变量由所有的函数共享(第九章详细介绍)。但是C++不提倡使用外部变量,但是提倡使用外部结构声明。
4.4.2 C++11结构初始化
1、与数组一样,C++11也支持将列表初始化用于结构,且等号(=)是可选的。
inflatable duck {"Daphne", 0.12, 9.98};
2、如果大括号内未包含任何东西,各个成员都将被设置成为零。
3、不允许缩窄转换
4.4.3 结构可以将string类作为成员吗
1、可以将成员name指定为string对象
2、一定要让结构定义能够访问名称空间std,为此,要将编译指令using移到结构定义之前。也可以将name的类型声明为std::string
4.4.4 其他结构属性
1、C++使用自定义的类型与内置类型尽可能相似。可以将结构作为参数传递给函数,也可以让函数返回一个结构。
2、还可以使用赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员函数都将被设置为另一个结构相应成员的值,即使成员是数组。这种被称为成员赋值(memberwise assignment),(第七章详细介绍如何传递和返回结构)。
3、可以同时完成定义结构和创建结构变量的工作,只需要将变量名放在结束括号的后面即可:
struct perks
{
int key_number;
char car[12];
}mr_smith,ms_jones;//两个结构体
甚至可以初始化以这种方式创建的变量:
struct perks
{
int key_number;
char car[12];
}mr_glitz=
{
7,
"Packard"
}
但是,将结构体定义和变量声明分开,可以使程序更易于阅读和理解。
4、也可以声明没有名称的的结构类型,方法是省略名称,同时定义一种结构类型和一种这种类型的变量:
struct
{
int x;
int y;
}position;
这样创建一个名为position的结构变量。可以使用成员运算符来访问它的成员(如position.x),但是这种类型没有名称,因此以后无法创建这种类型的变量。本书不会使用。
4.4.5 结构数组
1、可以创建元素为结构的数组。
创建一个包含100个inflatable结构的数组:
inflatable gifts[100];
可以和运算符一起使用
cin >> gifts[0].volume;
此时初始化为
inflatable guests[2] =
{
{"Bambi", 0.5, 21.99},
{"Godzilla", 2000, 565, 99};
};
4.4.6 结构中的位字段(不懂,有空可以看下)
4.5 共用体
1、共用体(union)是一种数据格式,他能够存储不同的数据类型,但只能同时存储其中的一种类型。
结构可以同时存储int、long和double,但是共用体只能存储int、long或double。其语法与结构相似,但是含义不同。
union one4all
{
int int_val;
long long_val;
double double_val;
};
可以使用one4all变量来存储int、long或double,条件是在不同的时间进行。
one4all pail;
pail.int_val = 15;
cout << pail.int_val;
pail.double_val = 1.38;
cout << pail.double_val;
从上述代码中可以看出pail有时可以是int变量,而有时可以是double变量。
成员名称标识了变量的容量。
由于其每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以共用体的长度为其最大成员的长度。
2、共用体的用途:
a、当数据使用两种或者更多种格式时,可节省空间。
3、匿名共同体没有名称,其成员将成为位于相同地址处的变量。每次只有一个成员是当前的成员:
stuct widget
{
char brand[20];
int type;
union
{
long id_num;
char id_char[20];
};
};
...
widget prize;
...
if(prize.type == 1)
cin >> prize.id_num;
else
cin >> prize.id_char;
共同体匿名,因此id_num和id_char被视为prize两个成员,他们的地址相同,所以不需要中间标识符id_val。自己确定哪个是活动的。
共同体主要用于节省内存。
4.6 枚举
1、C++的enum工具提供了另一种创建符号常量的方式,这种方式可以代替const。
它还允许定义新类型,但必须按严格的限制进行。
使用enum的句法与使用结构相似。
2、例子
enum spectrum {red, orange, yellow,green,blue,violet,indigo,ultraviolet};
a、让spectrum成为新类型的名称:spectrum被称为枚举(enumeration),就像struct变量被称为结构一样。
b、将red、orange、yellow等作为符号常量,它们对应整数值0~7.这些常量叫做枚举量(enumerator)。
3、通常情况下,将整数值赋给枚举量,第一个枚举量的值为0,第二个枚举量的值为1,依此类推,可以通过显式地指定整数值来覆盖默认值。
4、可以使用枚举名来声明这种类型的变量:
spectrum band;
在不进行强制转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量:
band = blue;// 合法
band = 2000;//不合法
spectrum变量受到限制,只有8个可能的值。如果赋给它一个非法值,可能会出现编译错误或警告。
5、为获得最大限度的可移植性,应将把非enum值赋给enum变量视为错误。
6、对于枚举,只定义了赋值运算符,没有算术运算。
7、枚举量是整型,可被提升为int类型,但是int类型不能自动转换为枚举类型:
int color = blue;//合法
band = 3;//不合法
color = 3 + red;//合法
8、如果int值是有效的,可以通过强制类型转换了,把它赋给枚举变量:
band = spectrum(30);
band = sprctrum(40003);不适应的值,结果将不确定
9、枚举更常被用来定义相关的符号常量,而不是新类型。(第六章)
如果只打算使用常量,而不是创建枚举类型的变量,可以省略枚举类型的名称:
enum {red, orange, yellow, green, bule, violet, indigo, ultraviolet};
4.6.1 设置枚举量的值
1、使用赋值运算符显式地设置枚举量的值:
enum bits{one = 1, two = 2. four = 4, eight = 8};
指定的数必须是整数。
2、也可以只显式地定义其中一些枚举量的值:
enum bigstep {first, second = 100, third};
这里first在默认情况下为0,后面没有初始化的枚举量将比前面的枚举量大1.因此,third的值为101.
3、可以创建多个值相同的枚举量:
enum {zero, null = 0,one, numero_uno = 1};
其中,zero和null都为0,one和umero_uno都为1。
4、只能使用int值得限制取消了,可以使用long或者long long类型的值。
4.6.2 枚举的取值范围
1、最初,只有生命中指出的那些值是有效的,但是,C++可以通过强制类型转换,增加了可赋给枚举变量的合法值。
2、每个枚举都有取值范围,通过强制类型转换,可以将取值范围种的任何整数值赋给枚举变量,即使这个值不是枚举值。
enum bits[one =1, two = 2. four = 4, eight =8];
bits myflag;
myflag = bits(6);//合法
6不是枚举值,但是它位于枚举定义的取值范围内
3、找上限,找到最小的包含所有的枚举量的2的次方,然后减一。
比如101,最小的2的次方是128,所以上限是127
4、找下限,如果不小于0,取值范围的下限就是0,否则和上限的方法一样。
5、C++11扩展了枚举,增加了作用域内枚举(scoped enumeration)。
4.7 指针和自由存储空间
1、计算机程序在存储数据时必须跟踪的的3种基本属性:
a、信息存储在何处
b、存储的值为多少
c、存储的信息是什么类型
使用一种策略来达到上述目的:定义了一个简单变量,声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。
2、指针也可以实现上述功能:指针是一个变量,其存储的是值的地址,而不是值本身。
3、只需要对变量应用地址运算符(&),就可以获得它的位置。
4、显示地址时,该实现的cout使用十六进制表示法,因为这是常用于描述内存的表示法(有些实现可能使用十进制表示法)。
5、指针与C++基本原理
面向对象编程与传统的过程性编程的区别在于:OOP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。
运行阶段决策提供了灵活性,可以根据当时的情况进行调整。
6、处理存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生量。
一种特殊类型的变量–指针用于存储值的地址。
指针名表示的是地址,*运算符被称为间接值(indirect velue)或解除引用(dereferencing)运算符。用于指针,可以得到该地址处存储的值。
4.7.1 声明和初始化指针
1、指针声明必须指定指针指向的数据的类型。例如
int * p_updates;
这表明,* p_updates的类型为int。 星号运算符被用于指针,因此p_updates变量本身必须是指针,其指向int类型。p_updates的类型是指向int的指针,或int 星号
2、小插曲
*运算符两边的空格是可选的。
传统上,C++程序员使用这种格式:
int *ptr;
这强调*ptr是一个int类型的值。
但是现在很多C++程序员使用这种格式:
int* ptr;
这强调的是,int*是一种类型–指向int的指针。
3、误区
下面的声明创建一个指针(pl)和一个int变量(p2):
int* p1, p2;
因为对于每个指针变量名,都需要使用一个*。
4、注意在C++中,int*是一种复合类型,是指向int的指针。
5、地址的长度或值不能指示关于变量的长度或者类型的任何信息,也不能指示改地址上有什么建筑物。
6、可以在声明语句中初始化指针。此时,初始化的是指针,而不是它指向的值。下面的例子将pt(而不是*pt)的值设置为&higgens。
int higgens = 5;
int * pt = &higgens;
4.7.2 指针的危险
1、极其重要的一点是:在C++创建指针时,计算机将分配用来存储地址的内存,但不是分配用来存储指针所指向数据的内存。为数据提供空间是一个独立的步骤,忽略这一步是自找麻烦,如下图所示:
long * fellow;// 创建一个long型指针
*fellow = 222333;//
fellow没有被初始化,他可能有任何值,不管什么值,程序都将它解释成为存储222333的地址。
2、一定要对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是使用指针的金科玉律。
4.7.3 指针和数字
1、不能简单地将整数赋值给指针:
int * pt;
pt = 0xB8000000;
应通过强制类型转换将数字转换为适当的地址类型:
int * pt;
pt = (int *) 0xB8000000;
4.7.4 使用new来分配内存
1、指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。
2、在C语言中,使用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法–new运算符。
3、为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName * pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。
4、必须声明指针所指向的类型的原因之一:
地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。
5、C++提供了检测并处理内存分配失败的工具。
因为计算机可能会由于没有足够的内存而无法满足new’的请求。new会引发一场–一种在15章讨论的错误处理技术。
4.7.5 使用delete释放内存
1、在使用new结束后,使用delete运算符,使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一步。
归还和释放(free)的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的):
int * ps = new int;
...
delete ps;
一定要配对的使用new和delete,否则将发生严重的内存泄漏(memory leak)。
2、不要尝试释放已经释放的内存块,C++标准指出,这样做的结果是不确定的,这意味着什么情况都可能发生。不能使用delete来释放声明变量所获得的内存:
int * ps = new int; //ok
delete ps; //ok
delete ps; //未知结果
int jugs = 5; //ok
int * pi = &jugs; //ok
delete pi; //不允许
只能用delete来释放使用new分配的内存,然而,对空指针使用delete是安全的。
4.7.6 使用new来创建动态数组
1、对于小数据,使用一个简单变量
对于大型数据(如数组、字符串和结构),应使用new。
2、静态数组和动态数组
3、使用new创建动态数组
将数组的元素类型和元素数组告诉new,在类型名之后加上方括号,其中包含元素数目。创建一个包含10个int元素的数组,可以这样做:
int * psome = new int [10];
new运算符返回第一个元素的地址,该地址被赋给指针psome。
使用delete释放的时候,对于new创建的数组,应使用另一种格式的delete来释放:
delete [] psome;
方括号意思是应当释放整个数组,而不仅仅是指针指向的元素。
4、使用new和delete时,应遵守以下规则
a、不要使用delete释放不是new分配的内存
b、不要使用delete释放用一个内存块两次
c、如果使用new[]为数组分配内存,则应使用delete[]来释放
d、如果使用new[]为一个实体分配内存,则应使用delete(没有方括号)来释放
e、对空指针应用delete是安全的
为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num+elements];
pointer_name将指向第一个元素。
4、使用动态数组
1、只要把指针当作数组名使用即可,看例子
int * psome = new int [10];
第一个元素不是问题,由于psome指向数组的第1个元素,因此*psome是第一个元素的值。一般使用psome[0]。
对于第二个元素,使用psome[1]。C和C++内部都可以使用指针处理数组。数组和指针基本等价是C和C++的优点之一。
2、指针和真正的数组名之间的根本差别:ppt106页
4.8 指针、数组和指针算术
1、指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。
2、使用数组名可以使用
int a,b;
double wages = {10000.0,20000.0,30000.0};
short stacks[3] = {3,2,1}
short * ps =&stacks[0];
a = *(stacke+1);// 2
b = sizeof(stacks);//24
4.8.1 程序说明
1、C++将数组名解释为数组第1个元素的地址,下面的语句将pw声明为指向double类型的指针,然后将它初始化为wages—wages数组中的第一个元素的地址:
double * pw = wages;
和所有数组一样,wages也存在下面的等式:
wages = &wages[0] = address of first element of array
2、将指针变量增加1后,其增加的值等于指向的类型占用的字节数。
3、数组表达式stacks[1],C++编译器将该表达式看作是*(stacks+1),这意味着先计算数组第二个元素的地址,然后找到存储在那里的值。最后的结果便是stacks[1]的含义,这里必须使用小括号。
*(stacks+1)和stacks[1]是等价的。
4、在很多情况下,可以使用相同的方式使用指针名和数组名。
区别之一是可以修改指针的值,而数组名是常量。
另一个区别是,对数组应用sizeof运算符得到的是数组的长度,而对指针应用sizeof的得到的是指针的长度,即使指针指向的是一个数组。在这种情况下,C++不会将数组名解释为地址。
5、数组的地址
对数组取地址时,数组名也不会被解释成其地址。数组名解释为其第一个元素的地址,而对数组名应用地址运算时,得到的是整个数组的地址:
short tell[10];
cout << tell << endl;
cout << &tell << endl;
这两个地址相同,但是&tell[0](即tell)是一个2字节内存块的地址,而&tell是一个20字节内存块的地址。
因此,tell+1将地址值加2,而表达式&tell+2将地址值加20。(这里我一直不知到为啥,地址不应该是加一吗,为什么后面加2是地址值加20,是不是书上写错了)
声明和初始化指针:
short (*pas)[20] = &tell;
不可省略括号,否则优先级将使得pas先于[20]结合,导致pas是一个short指针数组,包含20个元素,因此括号必不可少。
如果描述变量的类型,可将声明中的变量名删除,因此pas的类型为short(*)[20]。
由于pas被设置为&tell,因此*pas与tell等价,所以(*pas)[0]为tell数组的第一个元素。
4.8.2 指针小结
1、声明指针
要声明指向特定类型的指针,请使用下面的格式:
typeName * pointerName;
实例:
double * pn;
char * pc;
pn和pc都是指针,而double和char是指向double的指针和指向char的指针。
2、给指针赋值
可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址。
double * pn;
double * pa;
char * pc;
double bubble = 3.2;
pn = &bubble;
pc = new char;
pa = new double[30];
3、对指针解除引用
对指针接触引用意味着获得指针指向的值。
对指针应用解除引用或间接值运算符(*)来解除引用。
cout << *pn;
*pc = 'S';
4、区分指针和指针所指向的值
pt是指向int的指针,则*pt不是指向int的指针,而是完全等同于一个int类型的变量。pt才是指针。
int * pt = new int;
*pt = 5;
5、数组名
在多数情况下,C++将数组名视为数组的第一个元素的地址。
int tacos[10];
特比的是,将siezof运算符用于数组名时,此时放回整个数组的长度(单位为字节)。
6、指针算术
C++允许将指针和整数相加。
加1的结果=原来的地址+指向的对象占用的总字节数。
也可以将一个指针将去另一个指针,获得两个指针的差,仅当两个指针指向同一个数组时,才有意义,这将得到两个元素的间隔。
int tacos[10] = {5,2,8,4,1,2,2,4,6,8};
int * pt = tacos;
pt = pt + 1;
int *pe = &tacos[9];
pe = pe - 1;
int diff = pe - pt;
7、数组的动态联编和静态联编
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置:
int tacos[10];
使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应使用delete[]释放其占用的内存。
int size;
cin >> size;
int * pz = new int [size];
...
delete [] pz;
8、数组表示法和指针表示法
使用方括号数组表示法等同于对指针解除引用:
数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。
int * pt = new int [10];
*pt = 5;
pt[0] = 6;
pt[9] = 44;
int coats[10];
*(coats + 4) = 12;
4.8.3 指针和字符串
1、如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。
char flower[10] = "rose";
cout << flower << "s are red\n";
数组名是第一个元素的地址,因此cout语句中的flower是包含字符r的char元素的地址。cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。
2、在cout和多数C++表达式中,char数组名,char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
3、函数strlen()返回字符串的长度
函数strcpy()将字符串从一个位置复制到另一个位置
这两个函数的原型都位于头文件cstring。
4、若想查看数组名的地址,则需要
char animal[20] = "bear";
const char * bird = "wren";//将char指针初始化为指向一个字符串,“wren”实际表示的是字符串的地址,因此这条语句将“wren”的地址赋给了bird指针。(一般来说,编译器在内存中留出一些空间,以存储程序源代码中所有引号括起的字符串,并将每个被存储的字符串与其地址关联起来)
char * ps;
cout << animal << "and";
cout bird << "\n";//这里输出的是bear and wren,因为cout数组名是把数组内容输出,直到遇到空字符,详看4.8.3.1
cout << animal << " at " << (int*)animal << endl;//这里输出的是
//fox at 0x0064fd30
注意:使用bird来进行输入并不合适:
- 有些编译器将字符串字面值视为只读常量,如果尝试修改它们,将导致运行阶段错误。在C++中,字符串字面值都将被视为常量,但并不是所有的编译器都对以前的行为做了这样的修改。
- 有些编译器只使用字符串字面值的一个副本来表示程序中所有的该字面值。C++不能保存字符串字面值被唯一地存储,也就是说,如果在程序中多次使用了字符串“wren”,则编译器将可能存储该字符串的多个副本,也可能只存储一个副本。
- 不要使用字符串常量或未被初始化的指针来接受输入。在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。
5、指针的类型是char*,则cout将显示指向的字符串。
如果显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int*。
因此,ps显示为字符串“fox”,而(int*)ps显示为该字符串的地址
将animal赋给ps并不会复制字符串,只是复制地址,这两个指针将指向相同的内存的单元和字符串。
6、要获得字符串的副本,首先需要分配内存来存储该字符串,这可以通过声明另一个数组或使用new来完成。后一种方法使得能够根据字符串的长度来指定所需的空间:
ps = new char[strlen(animal) + 1];
字符串“fox”不能填满整个animal数组,浪费空间。上述使用strlen()来确定字符串的长度,并将它加1来获得包含空字符时该字符串的长度。之后程序使用new来分配刚好足够存储该字符串的空间。
7、接下来,需要将animal数组中的字符串复制到新分配的空间中,将animal赋给ps是不可行的,因为这样只能修改存储在ps的地址中,会失去程序访问新分配内存的唯一途径。需要使用库函数strcpy():
strcpy(ps, animal);//第一个是目标地址,第二个是要复制字符串的地址。首先确定分配了目标空间,并由足够的空间存储副本。使用strlen()确定所需的空间,并使用new获得可用的内存。
获得了“fox”的两个独立副本
fox at 0x0065fd30
fox at 0x004301c8
8、初始化数组,使用=运算符,否则使用strcpy()或strncpy():
char food[20] = "carrots";
strcpy(food, "flan");//确保food数组有足够的空间,否则函数将字符串中多余的部分复制到数组后面的内存字节中,
//这可能会覆盖程序正在使用的其他内存。
strcnpy(food, "a picnic basket filled with many goodies", 19);//该函数接受第三个参数,
//要复制的最大字符数。
food[19] = '\0';//注意,如果该函数在到达字符串结尾之前,目标内存已经用完,则它将不会添加空字符,
//要加上这句。就是复制19个字符,最后一个设置为空字符。将在复制该字符串之后加上空字符,
//以标记该字符串的结尾。
//如果该字符串少于19个字符,则strcnpy()
注意:应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组。
4.8.4 使用new创建动态结构
1、使用new创建结构分两步:创建结构和访问其成员。
inflatable * ps = new inflatable;//创建一个 未命名的inflatable类型,并将其赋给一个指针,对于inflatable可查看4.4.1节定义的。
2 、对于->
的使用说明
创建动态数组时,不能将成员运算符句点用于结构名,因为其没有名称,只知道其地址,箭头运算符可用于指向结构的指针。
若ps指向一个inflatable结构,则ps->price是指被指向的结构的price成员。
3、区分句点运算符(.)和箭头运算符(->)的使用场景
如果结构标识是结构名,则使用句点运算符;
如果标识符是指向结构的指针,则使用箭头运算符。
4、访问结构成员方法(*ps).price
如果ps是指向结构的指针,则*ps就是被指向的值--结构本身,由于*ps是一个结构,因此(*ps).price是该结构的price成员。
5、一个使用的new 和delete的实例
p115
演示了如何动态节省内存
子函数返回指针
分配内存记得给空字符留一个位置
C++不保证新释放的内存就是下次使用new时选择的内存
要记得有new就有delete,且不在一个函数里也可以
cout << name << (int * )name << endl;//name是指针,输出内容和地址
4.8.5 自动存储、静态存储和动态存储
1、自动存储
自动变量是一个局部变量,其作用域为包含它的代码块。
函数内也可以有代码块,目前所接触到的所有的代码块就是整个函数。
自动变量存储在栈中,依次加入,相反顺序释放,即后进先出(LIFO)
2、静态存储(第九章详细介绍)
使变量成为静态的方式有两种:
一种是在函数外面定义它;
一种是在声明变量时使用关键字static:
static double fee = 56.50;
自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。
变量可能存在于程序的整个生命周期(静态变量)
也可能只是在特定函数被执行时存在(自动变量)
3、动态存储
new和delete运算符提供更灵活
被称为自动存储空间和堆。
使得数据的生命周期不完全受程序或函数的生存时间控制。
在栈中,自动添加和删除机制使得占用的内存总是连续的
但是new和delete的相互影响可能导致占用的自由存储区不连续。
4、栈、堆和内存泄漏(没用调用delete后果)
没有调用delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。
实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。
这将导致内存泄漏,被泄露的内存将在程序的整个生命周期都不可使用;这些内存被分配出去, 但是无法收回。
极端的情况下,应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序错误。
养成成对使用new和delete的习惯,在自由存储空间上动态分配内存,随后编释放它。
C++智能指针自动完成此任务,16章节介绍。
4.9 类型组合(可以看这个)
4.9.1、下面是组合使用数组、结构和指针:
1、从结构定义开始:
struct antarctica_years_end
{
int year;
/* ect */
}
2、同时创建多个结构类型的变量:
antarctica_years_end s01,s02,s03;//s01,s02,s03都是结构
3、使用成员运算符访问结构成员:
s01.year = 1998;
4、创建指向这种结构的指针:
antarctica_years_end * pa = &s02;
5、将该指针设置为有效地址后,就可使用间接成员运算符(->)来访问成员:
pa->year = 1999;
6、创建结构数组:
antarctica_years_end trio[3];
7、使用成员运算符(.和->)访问结构数组中结构元素的成员:
trio[0].year = 2003;
其中trio是一个数组,trio[0]是一个结构,而trio[0].year是该结构的一个成员。由于数组名是一个指针,因此也可使用间接成员运算符:
(trio+1)->year = 2004;//等同于 trio[1].year = 2004;
8、创建指针数组:
const antarctica_years_end * arp[3] = {&s01, &s02. &s03};
9、如何使用指针数组访问数据
既然arp是一个指针数组,arp[1]就是一个指针,可将间接成员运算符应用于它,以访问成员:
std::cout << arp[1]->year << std::endl;
10、创建指向上述指针数组的指针:
const antarctica_years_end **ppa = arp;
其中arp是一个数组的名称,他是第一个元素的地址。第一个元素为指针,因此,ppa是一个指针,指向一个指向antarctica_years_end的指针。
这种声明不可遗漏const,不能忘记*,搞错顺序或结构类型。
11、使用auto创建指向指针数组的指针
对于此,auto很好的解决。编译器直到arp的类型,能够正确的推断出ppb的类型:
auto ppb = arp;
12、如何使用11步中的ppb访问数据(我有疑问??)
由于ppa是一个指向结构指针的指针,因此*ppb是一个结构指针,可将间接成员运算符应用于它:
std::cout << (*ppa)->year << std::endl;
std::cout << (*(ppb+1))->year << std::endl;//为什么不是((*ppb)+1)->year
由于ppa指向arp的第一个元素,因此* ppa为第一个元素,即&s01。所以(*ppa)->year为s01的year成员。
第二语句中,ppb+1指向下一个元素arp[1],即&s02。其中的括号必不可少,这样才能正确的结合。
*ppa->year试图将运算符✳作用于ppa->year,导致错误,因为成员year不是指针。
4.10 数组的替代品
4.10.1 模板类 vector
1、vector介绍
模板类vector类似于string类,是一种动态数组。
可以运行阶段设置vector对象的长度,可在末尾附加新数据,还可在中间插入新数据。
基本上,他是使用new创建动态数组的替代品。
vector确实使用new和delete管理内存,但是这种工作自动完成的
2、使用vector(有例子)
第一、必须包含头文件vector。
第二、vector包含在名称空间std中,可以使用using编译指令、using声明或std::vector。
第三、模板使用不同的语法来指出它存储的数据类型
第四、vector类使用不同的语法来指定元素数
实例:
#include <vector>
...
using namespace std;
vector<int> vi;//创建一个0大小的int数组
int n;
cin >> n;
vector<double> vd(n);//创建一个n大小的double数组
vi是一个vector< int >对象,vd是一个vector< double >对象。
vector对象在插入或者添加值时自动调整长度,因此可以将vi的初始长度设置为零。调整长度,需要使用vector包中的各种方法。
3、创建vector类模板语句
vector<typeName> vt(n_elem);
创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素。其中,n_elem可以是整型变量,也可以是整型变量。
4.10.2 模板类array(C++11)
1、array介绍
array对象的长度是固定的,使用栈,效率与数组相同,更安全方便
2、创建array对象
创建array对象,需要包含头文件array。
创建语法与vector稍有不同:
#include <array>
...
using namespace std;
array<int, 5> ai; //创建一个5个int元素的对象
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};
3、创建array对象模板语句
array<typeName, n_elem> arr;
创建一个名为arr的array对象,它包含n_elem个类型为typename的元素。但是这里n_elem不能是变量。
4.10.3 比较数组、vector对象和array对象
1、三者区别
第一、无论是数组、vector对象还是array对象,都可以使用标准数组来访问各个元素。
第二、从地址可知,array对象和数组都存储在相同的内存区域(即栈),而vetor对象存储在另一个区域(自由存储区或者堆)中。
第三、可以将一个array对象赋值给另一个array对象;而对于数组,必须逐元素赋值数据。
2、对于数组a1[-2] = 20.2的含义
找到a1指向的地方,向前移两个double元素,并将20.2存储到目的地。也就是说,将信息存储到数组的外面。C++不会检查这种错误。不安全
3、避免错误的使用数组索引的方法之一
不安全的代码如下:
a2[-2] = .5;//允许,但是不安全,会把0.5存放在不安全的地方,vector对象
a3[200] = 1.4;//array对象
解决方法之一:
一种选择使用成员函数at()
。可以使用vector和array对象的成员函数at()
:
a2.at(1) = 2.3;//赋值a2[1]为2.3
中括号表示法和成员函数at()的差别在于:
使用at()时,将在运行期间捕获非法索引,而程序默认将中断。时间长。
4.11 总结
4.12 复习题
答案如下:
1、
a、
char actors[30];
b、
short betsie[100];
c、
float chuck[13];
d、
long double dipsea[64];
2、
a、
array<char, 30> actors;
b、
array<short,100> betsie;
c、
array<float,13> chuck;
d、
array<long double, 64> dipsea;
3、
int oddly[5] = {1,3,5,7,9};
4、
int even = oddly[0] + odlly[4];
5、
cout << ideas[1] << endl;
6、
char lunch[13] = "cheeseburger"; //字母长度+1
或者
char lunch[] = "cheeseburger";
7、
string lunch = "Waldorf Salad";
没有using,则
std::string lunch = "Waldorf Salad";
8、
struct fish
{
char kind[20];
int weight;
float length;
};
9、
fish petes =
{
"trout",
12,
26.25
};
10、
enum Response {No, Yes, Maybe};
11、
double * pd = &ted;
cout << *pd << endl;
12、
float * pf = treacle;
cout << pf[0] << " " << pf[9] << endl;
13、假设已经包含头文件iostream和vector,并且有一条using编译指令:
unsigned int size;
cin >> size;
int * dyn = new int [size];
vector<int> dv(size);
14、有效的,表达式”home … bytes“ 是一个字符串常量,因此,它将判定为字符串开始的地址。cout对象将char地址解释为打印字符串,但类型转换(int*)将地址转换为int类型,然后作为地址被打印。总之,该语句打印字符串的地址,只要int类型足够宽,能够存储该地址。
15、
struct fish
{
char kind[20];
int weight;
float length;
};
fish * pole = new fish;
cout << pole->kind;
16、
使用cin>>address将使得程序跳过空白,直到找到非空白字符为止,然后它将读取字符,直到再次遇到空白为止。因此,它将跳过数字输入后的换行符,从而避免这种问题。另一方方面,它只拂去一个单词,而不是整行。
17、
#include <string>
#include <vector>
#include <array>
const int Str_num {10};
...
std::vector<std::string> vstr(Str_num);
std::array<std::string, Str_num> astr;