- 创建和使用数组。
- 创建和使用C-风格字符串。创建和使用string类字符串。
- 使用方法getline()和get()读取字符串。混合输入字符串和数字。
- 创建和使用结构。
- 创建和使用共用体。创建和使用枚举。创建和使用指针。
- 使用new和delete管理动态内存。创建动态数组。
- 创建动态结构。
- 自动存储、静态存储和动态存储。
- vector和array类简介。
复合类型:
- 类(不做介绍)
- 数组:存储多个同类型的值
- 字符串:
- 可以使用字符数组存储一系列字符,形成字符串,字符串以
\0
结尾 - 可以使用string类,构造字符串
- 可以使用字符数组存储一系列字符,形成字符串,字符串以
- 结构体与联合体:存储多个不同类型的值
- 枚举
- 指针:将数据所处位置告诉计算机变量
4.1 数组 array
数组array
:存储多个同类型的值,并且在计算机内存中依次存储(有序)数组的各个元素。数组中每个值叫做数组元素。
创建数组的声明语法:
- 存储在每个元素中的值的类型;
- 数组名;(其和指针有千丝万缕的联系。数组名当作指针,指向当前数组中的第一个元素)
- 数组中的元素个数(编译器在编译阶段要提前开辟内存空间)。
表达式// typeName arrayName[arraySize]; // 数组声明语法格式
short months[12]; // 创建一个包含12个short类型元素的months数组
arraySize
指定元素数目,它必须是整型常数(如10)或const
值,也可以是常量表达式:8*sizeof(int))
,即其值在编译时必须是已知的。
后续可以使用new
运算符创建数组,可以不指定arraySize
。
数组声明能够使用一个声明创建大量的变量。而且数组可以使用索引下标访问任意数组元素,时间复杂度为O(1)
。注:C++数组是从0
开始索引。
:::warning
编译器不会检查使用的下标是否有效,必须确保程序只使用有效的下标值。
如果将一个值赋给不存在的元素months[101]
,编译器并不会指出错误。但是程序运行后,这种赋值操作会正常执行,会覆盖months[101]所在地址的数据,这可能破坏数据或代码,甚至导致程序异常终止。
:::
数组之所以被称为复合类型,是因为它是使用其他类型来创建的(C语言中数组为派生类型,但由于C++对类使用术语派生,所以C++中的数组不能在成为派生类型,叫做复合类型)。
没有通用的数组类型,但存在很多特定的数组类型,如:char数组
或long数组
。例如,请看下面的声明:
float loans [20];
loans
的类型不是数组
,而是 float数组
。这强调了loans数组
是使用float类型
创建的。
/***
* @Author : DarrenZhang
* @Date : 2022-03-13 16:07:52
* @LastEditTime : 2022-03-13 16:07:52
* @LastEditors : DarrenZhang
* @Description : array in c++
* @FilePath : /CPlus/C++_Primer_Plus/chapter04/4.1_arrayone.cpp
* @RunCode : g++ 4.1_arrayone.cpp & ./a.out
*/
#include <iostream>
int main()
{
using namespace std;
int yams[3]; // 创建一个包含三个元素的数组
yams[0] = 7; // 给数组的第一个元素赋值
yams[1] = 8;
yams[2] = 6;
int yamcosts[3] = {20, 30, 5}; // create and initialize array
cout << "Total yams = ";
cout << yams[0] + yams[1] + yams[2] << endl;
cout << "The package with " << yams[1] << " yams costs " << yamcosts[1] << " cents per yam." << endl;
int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1] + yams[2] * yamcosts[2];
cout << "The total yam expense is " << total << " cents." << endl;
// 计算数组的内存占用 12
cout << "Size of yams array = " << sizeof(yams) << " bytes." << endl;
// 计算数组中单个元素的内存占用 4
cout << "Size of one element = " << sizeof(yams[0]) << " bytes." << endl;
// sizeof 对于变量可以使用括号或者不使用括号,但是对于 数据类型 来说,必须加括号
cout << "Size of int type = " << sizeof(int) << " bytes." << endl;
cout << "Size of short type = " << sizeof(short) << " bytes." << endl;
cout << "Size of float type = " << sizeof(float) << " bytes." << endl;
cout << "Size of double type = " << sizeof(double) << " bytes." << endl;
// cout << sizeof int << endl; // error
// ============ C++中数组初始化方式 ============ //
int cards[4] = {3, 6, 8, 10}; // ok
int hand[4]; // ok,这样初始化之后,如果要为元素赋值,只能按照元素位置(索引)赋值
// hand[4] = {5, 6, 7, 8}; // not allowed
// hand = cards; // not allowed
// 数组名表示数组在内存中的地址,将地址赋值给地址,是说不通的
float hotelTips[5] = {5.0, 2.5}; // 如果初始化时,提供的值少于数组的元素数目,编译器会将其他元素设置为0
cout << "hotelTips 的第一个元素为: " << hotelTips[0] << endl;
cout << "hotelTips 的第四个元素为: " << hotelTips[3] << endl;
long totals[500] = {0}; // 将数组中所有元素全部初始化为0,只需显式地将第一个元素初始化为0
long totals_[500] = {1};
cout << "totals 的第一个元素为" << totals[0] << ", totals_ 的第一个元素为" << totals_[0] << endl;
cout << "totals 的第四个元素为" << totals[3] << ", totals_ 的第四个元素为" << totals_[3] << endl;
// ============ 计算数组中元素个数 ============ //
short things[] = {1, 5, 3, 8}; // 如果初始化没有指定元素个数,编译器会计算元素个数
int num_elements = sizeof(things) / sizeof(short);
cout << "Number of things' element = " << num_elements << endl;
// =========== C++ 11 中数组初始化方式 =========== //
// 1. 初始化数组,可省略 =
double earnings[4]{1.2e4, 1.6e4, 1.1e4, 1.7e4}; // ok with C++11
// 2. 如果大括号中没有任何内容,将会把所有元素初始化为0
unsigned int counts[10]{}; // all elements set to 0
float balances[100]{}; // all elements set to 0
// 3. 列表初始化禁止 缩窄转换
// long plifs[] = {25, 92, 3.0}; // not allowed, 浮点数转换为整型是缩窄操作,编译不能通过
// char slifs[4] {'h', 'i', 1122011, '\0'}; // not allowed, 1122011超出了char变量的取值范围(假设char变量的长度为8位,则取值范围为[0, 255])
char tlifs[4]{'h', 'i', 112, '\0'}; // allowed, 112在char变量的取值范围内,char类型也是一种整型类型,所以编译可以通过
return 0;
}
该程序首先创建一个名为yams
,包含3个元素,且每个元素都是int类型
的数组,元素下标为[0, 2]
,因此可以用索引[0, 2]
分别给这三个元素赋值。在给yams
元素赋值时,分别为每个元素进行赋值操作,这样过于繁琐。
C++允许在声明语句中初始化数组元素。int yamcosts[3] = {20, 30, 5};
。只需提供一个用逗号分隔的值列表(初始化列表),并将它们用花括号括起即可。列表中的空格是可选的。
:::warning
如果没有初始化函数中定义的数组,则其元素值将是不确定的,这意味着元素的值为以前驻留在该内存单元中的值。
:::
接下来,程序使用数组值进行了计算。
**sizeof**
运算符返回类型或数据对象的长度(单位为字节)。sizeof
运算符对于变量可以使用括号或者不使用括号,但是对于数据类型来说,必须加括号,否则会报错。
:::info
- 如果将sizeof运算符用于数组名,得到的将是整个数组中的字节数;
- 如果将sizeof 用于数组元素,则得到的将是元素的长度(单位为字节)。
:::
yams
是一个数组,而yams[1]
只是一个int类型
变量。
接下来是C++初始化数组的规则,它们限制了初始化的时刻,决定了数组的元素数目与初始化器中值的数目不相同时将发生的情况。我们来看看这些规则。
只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组。
int cards [4] = {3, 6, 8, 10}; //okay int hand [4]; // okay hand [4] = {5,6,7,9}; // not allowed,只能使用下标分别给数组中的元素赋值。 hand = cards; // not allowed
而且数组名表示数组在内存中的地址,将一个地址赋值给另外一个地址语法上是说不通的。
初始化数组时,提供的值可以少于数组的元素数目。
例如,下面的语句只初始化 hotelTips
的前两个元素:
float hotelTips [5] = { 5.0, 2.5 } ;
如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0。因此,将数组中所有的元素都初始化为0:
只要显式地将第一个元素初始化为0,然后让编译器将其他元素都初始化为0即可:
long totals [500] = {0} ;
:::info
如果初始化为{1}
而不是{0}
,则第一个元素被设置为1
,其他元素仍然都被设置为0
。
:::
如果初始化数组时方括号内
[]
为空,C++编译器将计算元素个数。例如,对于下面的声明:short things [] = {1, 5, 3, 8} ;
编译器将使
things
数组包含4个元素。short things [] = { 1, 5, 3, 8 } ; int num_elements = sizeof things / sizeof (short) ;
C++11 的初始化方法
C++11将使用大括号的初始化(列表初始化)作为一种通用初始化方式,可用于所有类型。数组以前就可使用列表初始化,但C++11中的列表初始化新增了一些功能。
首先,初始化数组时,可省略等号
=
:double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4}; //okay with C++11
其次,可不在大括号内包含任何东西,这将把所有元素都设置为零
unsigned int counts [10]= {0}; // all elements set to o float balances [100]{}; //all elements set to 0
第三,列表初始化禁止缩窄转换
long plifs[] = {25, 92, 3.0}; // not allowed char slifs[4] { 'h','i', 1122011, '\0'} ; // not allowed char tlifs[4] { 'h', 'i', 112,'\0'}; // allowed
- 第一条语句不能通过编译,因为将浮点数转换为整型是缩窄操作,即使浮点数的小数点后面为零。
- 第二条语句也不能通过编译,因为
1122011
超出了char变量
的取值范围(这里假设char变量
的长度为8位,范围是[0, 255]
)。 - 第三条语句可通过编译,因为虽然
112
是一个int类型的值
,但它在char变量
的取值范围内。char
类型也是一种整型。
4.2 字符串
:::info 字符串是存储在内存的连续字节中的一系列字符,C++处理字符串的两种方式:
- C-style string
- 基于string类库
:::
存储在连续字节中的一系列字符意味着可以将字符串存储在 char 数组中,其中每个字符都位于自己的数组元素中。
C-style string
以空字符(null character)结尾,空字符被写作\0
,其ASCII 码为0,用来标记字符串的结尾。例如,请看下面两个声明:
这两个数组都是char数组,但只有第二个数组是字符串。空字符对C-风格字符串而言至关重要。应确保数组足够大,能够存储字符串中所有字符,包括空字符。是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理。C++对字符串长度没有限制。char dog[8] = { 'b', 'e', 'a', 'u', 'x ',' ', 'I', 'I'}; // not a string ! char cat [8] = { 'f', 'a', 't ', 'e', 's', 's', 'a ', '\0'}; // a string!
例如,C++有很多处理字符串的函数,其中包括cout 使用的那些函数。它们都逐个地处理字符串中的字符,直到到达空字符为止。如果没有空字符,cout等函数会继续将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。
上面代码将数组初始化为字符串会使用大量单引号,且必须记住加上空字符。这太烦了😒。所以引入了字符串常量(string constant)或字符串字面值(string literal)。只需使用一个用引号括起的字符串即可,
char bird[11] = "Mr. cheeps" ; // the \0 is understood
char fish[] = "Bubbles"; // let the compiler count number of elements
用引号" "
括起的字符串默认包括结尾的空字符。另外各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字符。
注意,字符串常量(使用双引号)不能与字符常量(使用单引号)互换。
字符常量(如
'S'
)是字符串编码的简写表示。在ASCII系统上,字符常量'S'
只是int类型常量83
的另一种写法,字符常量也是一种整型。因此,下面的语句将83赋给shirt_size
是正确的:char shirt_size = 83; // shirt_size is 'S' char shirt_size_2 = 'S'; // shirt_size is 'S' int shirt_size_3 = 'S'; // shirt_size is 83
但
"S"
不是字符常量,它表示的是两个字符(字符S
和\0
)组成的字符串。"S"
是一个字符类型的数组,数组名表示字符串所在的内存地址。因此下面的语句试图将一个内存地址赋给shirt_size
是错误的。char shirt_size = "S"; // illegal type mismatch
由于地址在C++中是一种独立的类型,因此C++编译器不允许这种不合理的做法。
4.2.1 拼接字符串常量
C++允许拼接字符串字面值,将两个用引号括起的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。因此,下面所有的输出语句都是等效的:
cout << "I'd give my right arm to be" " a great violinist. \n" ;
cout << "I'd give my right arm to be a great violinist. \n" ;
cout << "I'd give my right ar"
"m to be a great violinist . \n" ;
4.2.2 在数组中使用字符串
要将字符串存储到数组中,最常用的方法有两种:
- 将数组初始化为字符串常量
- 将键盘或文件输入读入到数组中。
程序4.2使用了这两种方法,它将一个数组初始化为用引号括起的字符串,并使用cin将一个输入字符串放到另一个数组中。该程序还使用了标准C语言库函数strlen()
来确定字符串的长度。标准头文件cstring
提供了该函数以及很多与字符串相关的其他函数的声明。
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
// 1. 将一个数组初始化为用双引号括起来的字符串
const int Size = 15;
char name1[Size]; // empty array
char name2[Size] = "C++owboy"; // initialized array 8+1=9个字符足够存放到15个字符的数组中
// 2. 使用cin将输入字符串放置到数组中
cout << "Howdy~ I'm " << name2 << "! What`s your name?" << endl;
cin >> name1;
cout << "Well, " << name1 << ", your name has " << strlen(name1) << " letters." << endl; // strlen() 统计字符串长度
cout << "And is stored in an array of " << sizeof(name1) << " bytes." << endl; // 数组 name1 占用的内存空间大小
cout << "Your initial is " << name1[0] << "." << endl;
// strlen() 返回存储在数组中的字符串长度,而不是数组本身的长度,另外其只计算可见的字符,并不把空字符长度计算在内。
// 如果 cosmic 是字符串,存储其的数组长度不能短于 strlen(cosmic) + 1
name2[3] = '\0'; // set to null character
cout << "Here are the first 3 characters of my name: " << name2 << endl;
return 0;
}
sizeof
运算符返回的是整个数组的长度:15字节;strlen()
函数返回的是存储在数组中的字符串的长度。strlen()
只计算可见的字符,而不把空字符计算在内。如果要存储字符串,数组的长度不能短于strlen(cosmic)+1
。
4.2.3 字符串输入
#include <iostream>
using namespace std;
int main() {
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name: " << endl;
cin >> name;
cout << "Enter your favorite dessert: " << endl;
cin >> dessert;
cout << "I have some delicious " << dessert << " for you, " << name << "." << endl;
return 0;
}
// 输出
// Enter your name :Alistair Dreeb
// Enter your favorite dessert :
// T have some delicious Dreeb for you,Alistair.
cin是如何确定已完成字符串输入呢?由于不能通过键盘输入空字符,因此cin
使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着**cin**
在获取字符数组输入时只读取一个单词。读取该单词后, cin将该字符串放到数组中,并自动在结尾添加空字符。
这个代码例子的实际结果是,cin
把 Alistair
作为第一个字符串,并将它放到name
数组中。这把Dreeb
留在输入队列中。当继续使用cin
输入时,会首先在输入队列中搜索,这是Dreeb
仍在输入队列中,因此 cin 读取Dreeb
,并将它放到dessert
数组中。
如果输入字符串有30个字符,但是目标数组只能容纳20个字符,那么目标数组只能存放20个字符。
4.2.4 每次读取一行字符串输入
要将整条短语而不是一个单词作为字符串输入,需要采用istream
中的类提供的一些面向行的类成员函数。
getline()
:丢弃换行符get()
:将换行符保留在输入序列中
这两个函数都读取一行输入,直到到达换行符。
1. getline()
getline()
函数读取整行,它通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用cin.getline()
。该函数有两个参数。
- 第一个参数是用来存储输入行的数组的名称;
- 第二个参数是要读取的字符数。如果这个参数为20,则函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。
:::info
getline()成员函数在读取指定数目的字符或遇到换行符时停止读取。
:::
例如,假设要使用getline()将姓名读入到一个包含20个元素的name数组中。可以使用这样的函数调用:
这将把一行不超过19个的字符读入到cin.getline (name , 20) ;
name
数组中。 ```cppinclude
using namespace std;
int main() { const int ArSize = 20; char name[ArSize]; char dessert[ArSize];
cout << "Enter your name: " << endl;
// geline(数组名称, 要读取的字符数)
cin.getline(name, ArSize); // reads through newline
cout << "Enter your favorite dessert: " << endl;
cin.getline(dessert, ArSize);
cout << "I have some delicious " << dessert << " for you, " << name << "." << endl;
return 0;
}
// 输出 // Enter your name: // Dirk Hammernose // Enter your favorite dessert: // Radish Torte // I have some delicious Radish Torte for you,Dirk Hammernose.
`getline()`函数每次读取一行。它通过换行符来确定行尾,但不保存换行符。相反,在存储字符串时,它用空字符来替换换行符。<br />
<a name="YncnV"></a>
#### 2. get()
`get()`与`getline()`接受参数相同,并且都读取到行尾。但是`get()`不会读取并丢弃换行符,而是将其留在输入队列中。这种情况下,
```python
cin.get(name, Arsize); // input is : DarrenZhang
cin.get(dessert, Arsize);
此时,由于第一行代码执行过后,将换行符留在输入队列中;当执行第二行代码时,会遇到队列中的换行符,此时get()方法认为已经到达一行的行尾,所以不会继续读取任何内容。那么应该如何解决呢?
可以在中间加入cin.get()
,使其读取换行符,就可以使下一行的get()
方法继续读取键盘输入,代码如下:
cin.get(name, Arsize);
cin.get();
cin.get(dessert, Arsize);
为了方便使用,可以将两个类成员函数拼接合并使用
cin.get(name, ArSize).get(); // concatenate member functions
由于cin.get(name, ArSize)
返回的是一个cin
对象,该对象仍然可以继续调用其成员函数get()
方法。
cin.getline(name1, ArSize).getline(name2, ArSize);
cin.get(name1, ArSize).get()
cin.get(name2, ArSize).get()
#include <iostream>
using namespace std;
int main()
{
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
// cin.get() 正确用法:读取下一个字符
cout << "Enter your name: " << endl;
// cin.get(name, ArSize); // reads first line,返回的仍然是cin对象
// cin.get(); // read newline; read 'enter' line
cin.get(name, ArSize).get(); // concatenate member functions
cout << "Enter your favorite dessert: " << endl;
cin.get(dessert, ArSize).get(); // read second line
cout << "I have some delicious " << dessert << " for you, " << name << "." << endl;
return 0;
}
4.2.5 混合输入字符串和数字
#include <iostream>
using namespace std;
int main()
{
cout << "What year was your house built?" << endl;
int year;
cin >> year;
cout << "What is its street address?" << endl;
char address[80];
cin.getline(address, 80);
cout << "Year built: " << year << endl;
cout << "Address: " << address << endl;
cout << "Done!" << endl;
return 0;
}
程序没有输入地址的机会。当cin读取年份,将回车生成的换行符号留在了输入队列。当cin.getline()读取输入时,遇到的额是换行符,直接将空字符串赋给address数组。
解决方案:
使用cin.get()在读取地址之前先读取换行符号。
cin >> year; cin.get();
利用表达式
cin>>year
返回 cin 对象,将调用拼接起来(cin >> year).get();
:::info C++通常使用指针(而不是数组)来处理字符串 :::
4.3 String类
String类可以使用String类型的变量(C++的说法叫做对象)而不是字符数组来存储字符串。string类隐藏了字符串的数组性质,可以能够像处理普通变量一样处理字符串。String类将字符串作为一种数据类型。要使用string类需要包含头文件。 ```cpp
include
include
// make string class acailable
using namespace std;
int main() { char charr1[20]; // create an empty char array char charr2[20] = “jaguar”; // create an initialized char array string str1; // create an empty string object string str2 = “panther”; // create an initialized string
cout << "Enter a kind of feline:";
cin >> charr1;
cout << "Enter another kind of feline:";
cin >> str1; // use cin for input
cout << "Here are some felines: " << endl;
cout << charr1 << " " << charr2 << " " << str1 << " " << str2 << endl; // use cout for output
cout << "The third letter in " << charr2 << " is " << charr2[2] << endl;
cout << "The third letter in " << str2 << " is " << str2[2] << endl; // use array notation
return 0;
}
string对象和字符数组相同之处:
- 可以使用C风格字符串来初始化string对象;
- 可以使用cin来键盘输入,存储到string对象中;
- 可以使用cout来显示string对象;
- 可以使用数组表示法来访问存储再string对象中的字符;
string对象和字符数组的主要区别:**可以将string对象声明为简单变量,而不是数组。**
```cpp
string str1; // create an empty string object
string str2 = "panther"; // create an initialized string
程序可以自动处理string的大小,声明一个长度为0的string对象str1
,但程序将输入读到str1中时,会自动调整str1的长度
cin >> str1; // str1 resized to fit input
:::info char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串实体对象。 :::
4.3.1 C++11 字符串初始化
// 1. C++11 字符串初始化:允许将列表初始化用于C-style 和 string对象
char first_data[] = {"Le Chapon Dodu; "};
char second_data[]{"The Elegant Plate; "};
string third_data = {"The Bread Bowl; "};
string fourth_data{"Hank's Fine Eats; "};
cout << first_data << second_data << third_data << fourth_data << endl;
4.3.2 赋值、拼接、附加
前面中提到:不能将一个数组赋值给另外一个数组。但是对于string类来说,可以将一个string对象赋值给另一个string对象。
// 2. 赋值、拼接、附加
char charr1[20]; // create an empty array
char charr2[20] = "jaguar"; // create an initialized array
string str1; // create an empty strting object
string str2 = "panther"; // create an initialized string
// charr1 = charr2; // not allowed, no array assignment
str1 = str2; // valied, object assignment ok
#include <iostream>
#include <string>
using namespace std;
int main()
{
// 字符串合并拼接操作
string s1 = "penguin";
string s2, s3;
// string类对象间的赋值
cout << "Your can assign one string object to another: s2 = s1" << endl;
s2 = s1;
cout << "s1: " << s1 << ", s2: " << s2 << endl;
// 将C-style的字符串赋值给string类
cout << "You can assign a C-style string to a string object." << endl;
cout << "s2 = \"buzzard\"" << endl;
s2 = "buzzard";
cout << "s2: " << s2 << endl;
// string类对象之间的拼接
cout << "You can concatenate strings: s3 = s1 + s2" << endl;
s3 = s1 + s2;
cout << "s3: " << s3 << endl;
// string类对象的附加操作
cout << "You can append strings." << endl;
s1 += s2;
cout << "s1 += s2 yields s1 = " << s1 << endl;
// 将C-style字符串和string对象拼接
s2 += " for a day";
cout << "s2 += \" for a day\" yields s2 = " << s2 << endl;
return 0;
}
// 程序输出:
// Your can assign one string object to another: s2 = s1
// s1: penguin, s2: penguin
// You can assign a C-style string to a string object.
// s2 = "buzzard"
// s2: buzzard
// You can concatenate strings: s3 = s1 + s2
// s3: penguinbuzzard
// You can append strings.
// s1 += s2 yields s1 = penguinbuzzard
// s2 += " for a day" yields s2 = buzzard for a day
4.3.3 string类的其他操作
strcpy()
:将字符串复制到字符数组strcat()
:将字符串附加到字符数组末尾 ```cppinclude
include
// make string class available include
// C-style string library
using namespace std;
int main() { char charr1[20]; char charr2[20] = “jaguar”; string str1; string str2 = “panther”;
// assignment for string objects and character arrays
str1 = str2; // copy str2 to str1
strcpy(charr1, charr2); // copy charr2 to charr1
// appending for string objects and character arrays
str1 += " paste"; // add paste to end od str1
strcat(charr1, " juice"); // add juice to end of charr1
// finding the length of a string object and a C-sytle string
int len1 = str1.size(); // .size() 成员函数,obtain length of str1
int len2 = strlen(charr1); // obtain length of charr1
cout << "The string " << str1 << " contains " << len1 << " characters." << endl;
cout << "The string " << charr1 << " contains " << len2 << " characters." << endl;
return 0;
}
将str1 和 str2拼接,并赋值给str3,string对象的做法`str3 = str1 + str2`:<br />将charr1和charr2拼接,并赋值给charr3,C-style方式做法:
- `strcpy(charr3, charr1); // 将charr1赋值给charr3`
- `strcpy(charr3, charr2); // 将charr2拼接到charr3后面`
使用字符数组最大的风险:目标数组过小,无法存储全信息。
```cpp
char site[10] = "house";
strcat(site, " of pancakes"); // memory problem
函数strcat尝试将全部12个字符复制到site数组中,但是site字符数组最大容量是10,无法存储全部信息,这将会覆盖相邻的内存,导致数据损坏,甚至程序错误。
string类能够自动调整大小,从而避免字符数组的风险。
两种确定字符串中字符数的方法
int len1 = str1.size();
strlen()
接受C-Style字符串作为参数,返回该字符串包含的字符数;
int len2 = strlen(charr1);
语法与C-style字符串相同,但是每次读取一行而不是一个单词时,语法却不相同。
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main()
{
char charr[20]; // 只开辟了20个字节的内存大小,并没有初始化,还是保留了内存中原始的值
string str;
// strlen(charr): 统计charr中从第一个元素到'\0'的元素个数
cout << "Length of string in charr before input: " << strlen(charr) << endl; // is not 20
cout << "Length of string in str before input: " << str.size() << endl;
cout << "Enter a line of text:" << endl;
cin.getline(charr, 20); // indicate maximum length, 是istream类的成员函数,类方法
cout << "You entered: " << charr << endl;
cout << "Enter another line of text:" << endl;
getline(cin, str); // cin now an argument; no length specifier,不是类方法,单纯的一个方法
cout << "Your entered: " << str << endl;
cout << "Length of string in charr after input: " << strlen(charr) << endl;
cout << "Length of string in str after input " << str.size() << endl;
cin >> str; // read a word into the str string object
cout << str << endl;
int x;
cin >> x; // read a value into a basic C++ type
cout << x << endl;
return 0;
}
因为charr并没有被初始化,所以charr数组中还保留原来的值,strlen()会从数组的第一个元素开始计算字节数,一直到空字符。对于string类来说,未被初始化的string对象的长度会被自动设置为0。
第17行代码是将一行输入读取到charr数组中,使用的是cin.getline()方法,cin是一个istream对象,getline是istream类的一个类方法。其参数为:目标数组,参数数组的长度
第21行代码是将一行输入读取到string对象中,使用的是getline()方法,这个方法不是类方法,其将cin作为参数,指出应该在哪里查找输入。也没有指定字符串长度的参数,是因为string可以根据字符串长度自动调整大小。
istream类中有处理double int和其他基本数据类型的类方法,但是没有处理string对象的类方法。但是在上述代码中:cin >> str
是可行的。WHY?!!! 这是使用的string类的一个友元函数
4.4 结构体
结构体是用户定义的类型,同一种结构体可以同时存储多种不同类型的数据。结构声明定义了结构体类型的数据属性。定义了类型后,就可以创建这种类型的变量。
创建结构体的两步:
- 定义结构描述:描述并标记了能够存储在结构中的各种数据类型
- 按照描述创建结构变量(结构数据对象)
创建一个结构体类型,存储产品名称、容量和售价,代码如下:
// structure declaration
struct inflatable
{
char name[20];
float volume;
double price;
}; // 结束结构声明
关键字struct
表明:定义了一个结构体;inflatable
表明:这种数据格式的名称,所以新类型的名称为inflatable
。大括号内部包含的是结构存储的数据类型的列表,其中每个列表项都是一条声明语句。列表中的每一项都被成为结构成员。结构定义指出了新类型(inflatable)的特征。
定义好结构后,就可以创建这种类型的变量了,C++允许在声明结构变量时省略关键字struct
struct inflatable goose; // keyword struct required in C
inflatable vincent; // keyword struct not required in C++
结构体可以使用成员运算符.
来访问结构体成员
:::info
访问类成员函数cin.getline()
的方式是从访问结构体成员变量的方式衍生而来的
:::
#include <iostream>
using namespace std;
// structure declaration
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable guest =
{
"Glorious Gloria", // name value
1.88, // volume value
29.99 // price value
}; // guest is a structure variable of type inflatable
inflatable pal =
{
"Audacious Arthur",
3.12,
32.99
}; // pal is a second variable of type inflatable
cout << "Expand your guest list with " << guest.name << " and " << pal.name << "!" << endl;
cout << "You can have both for $" << guest.price + pal.price << "!" << endl;
return 0;
}
结构体声明的位置很重要,有两种方式:
- 声明放在main函数中,紧跟在开始括号的后面
- 将声明放在main()函数的外部:外部声明
外部声明可以被其后面的任何函数使用,而内部声明只能被该声明所属的函数使用,通常应该使用外部声明。变量也可以再函数内部和外部定义,外部变量可以被所有函数使用,C++不提倡使用外部变量,但是提倡使用外部结构声明。
可以将结构体每个成员都初始化为适当类型的数据,name成员是一个字符数组,可以将其初始化为一个字符串,并且具有和字符数组相同的用法:pal.name[0] = A
。
C++11结构体初始化方法
inflatable duck {"DarrenZhang", 0.12, 9.98}; // can omit = in C++ 11
如果大括号内没有任何东西,各个成员都会被置为0。同样也不允许有缩窄转换
将成员name指定为string对象而不是上述代码中的字符数组,可以这样改写
#include <string>
struct inflatable
{
std::string name;
float volume;
double price;
};
4.4.4 其他结构属性
- C++ 使用户定义的类型与内置类型尽可能相似。可以将结构作为参数传递给函数,也可以让函数返回一个结构
- 使用赋值运算符
=
将结构赋给另一个同类型的结构。这样结构中的每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值 memberwise assignment。 ```cppinclude
using namespace std;
struct inflatable { char name[20]; float volume; double price; };
int main() { inflatable bouquet = { “sunflowers”, 0.20, 12.49 };
inflatable choice;
cout << "bouquet: " << bouquet.name << " for $" << bouquet.price << endl;
choice = bouquet; // assign one structure to another
cout << "choice: " << choice.name << " for $" << choice.price << endl;
return 0;
}
可以同时完成定义结构和创建结构变量的工作,只需将变量名放在结束括号的后面。
```cpp
struct perks
{
int key_numbers;
char car[12];
} mr_smith, ms_jones; // two perks variables
还可以初始化以这种方式创建的变量
struct perks
{
int key_numbers;
char car[12];
} mr_glitz =
{
7, // value for mr_glitz.key_numbers member
"Packard" // value for mr_glitz.car member
}
实际上并不推荐这种写法,不容易代码阅读。
还可以声明没有名称的结构类型,同时定义一种结构类型和这种类型的变量;
struct // no tag
{
int x,
int y
} position; // a structure variable
这样就创建了一个名为position的结构变量,可以使用成员运算符来访问他的成员position.x
,但是这种类型没有名称,无法继续创建这种类型的变量。
4.4.5 结构数组
inflatable结构包含一个数组(name)。也可以创建元素为结构体的数组,方法和创建基本类型数组完全相同
infaltable gifts[100]; // array of 100 inflatable structures
gifts 就是一个inflatable类型的数组,其中每个数组元素都是inflatable对象,可以和成员运算符一起使用
cin >> gifts[0].volume;
cout << gifts[99].price << endl;
gifts 本身是一个数组,而不是结构,因此像gifts.price这样的语法是无效的。
初始化结构数组可以结合初始化数组的规则(用逗号分割每个元素的值,并用花括号括起来)和初始化结构体的规则(用逗号分隔每个成员的值,并用花括号括起来)。
#include <iostream>
using namespace std;
struct inflatable
{
char name[20];
float volume;
double price;
};
int main()
{
inflatable guests[2] =
{
{"Bambi", 0.5, 21.99}, // first structure in array
{"Godzilla", 2000, 565.99}, // next structure in array
};
cout << "The guests " << guests[0].name << " and " << guests[1].name
<< " have a combined volume of "
<< guests[0].volume + guests[1].volume << " cubic feet." << endl;
return 0;
}
4.4.6 结构体中的位字段
4.5 共用体
共用体union也是一种数据格式,能够存储不同的数据类型,但是只能同时存储其中的一种类型。
结构体可以同时存储int、long、double类型的数据,而共用体只能存储int或long或double中的一种。
// union 声明
union one4all
{
int int_val;
long long_val;
double double_val;
}
可以使用one4all变量存储int、long、double类型的数据,条件是再不同的时间进行:
#include <iostream>
using namespace std;
union one4all
{
/* data */
int int_val;
long long_val;
double double_val;
};
int main()
{
one4all pail;
pail.int_val = 15; // store an int
cout << pail.int_val << endl;
pail.double_val = 1.38; // store a double
cout << pail.int_val << endl; // int value is lost
cout << pail.double_val << endl;
}
// 15
// -515396076
// 1.38
由于共用体每次只能存储一个值,因此它必须要有足够的空间来存储最大的成员,所以共用体的长度为其最大的成员长度。
共用体的用途之一:当数据项使用两种或者多种数据格式(但不会同时使用),可节省空间。例如,管理一个小商品目录,其中一些商品的ID为整数,而另一些的ID为字符串,可以这样做:
struct widget
{
char brand[20];
int type;
union id
{
long id_num;
char id_char[20];
} id_val;
};
widget prize;
if (prize.type == 1)
cin >> prize.id_val.id_num;
else
cin >> prize.id_val.id_char;
匿名共用体anonymous union 没有名称,其成员将共享内存地址。每次只有一个成员是当前的成员。
struct 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 枚举
C++的enum提供了另一种创建符号常量的方式,可以替代const。还允许定义新类型,但必须按严格的限制进行。enume的语法如下:
enum spectrum {red, orange, yellow, green};
- spectrum 称为新类型的名称;spectrum被称为枚举enumeration
- 将red、orange、yellow等作为符号常量,他们对应0, 1, 2。这些常量叫做枚举量 enumerator。
在默认情况下,将整数值赋值给枚举量,枚举量是从0开始,以此类推
// 用枚举名来声明这种类型的变量
spectrum band;
在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋值给枚举的变量
band = blue; // valid
band = 2000; // invalid, 2000 is not an enumerator
spectrum变量会受到限制,只有8个可能的值。不要把非enum值赋值给enum变量。
对于枚举,只定义了赋值运算符,没有为枚举定义算数运算
band = orange + green; // invalid
++band; // invalid
因为枚举类型没有算数运算符+,但用与算术表达式中时,枚举被转为整数,进行整型的算数运算,但结果为int类型,不能将int类型转换为枚举类型。所以报错。
枚举量是整型,可被提升为int类型,但是int类型不能转为枚举类型
int color = orange;
band = 3; // invalid, int类型不能转为枚举类型
color = 3 + red; // invalid,red转换成int类型,然后在进行算数运算
实际上,枚举更常被用来定义相关的符号常量,而不是新类型。可以用枚举定义switch语句中使用的符号常量。如果只使用常量,而不创建枚举类型的变量,可以省略枚举类型的名称
enum {red, orange, yellow, green};
4.6.1 设置枚举量的值
可以使用赋值运算符来显示地设置枚举量的值
enum spectrum {red = 1, orange = 3, yellow = 5, green= 7};
指定的值必须是整数,可以值显示地定义其中一些枚举量的值
enum bigstep {first, second = 100, third};
// first:0, third:101
可以创建多个值相同的枚举量
enum {zero, null= 100, one, numero_uno = 1};
zero 和 null都为0, one 和 numero_uno都为1。
4.6.2 枚举的取值范围
最初,对于枚举来说,只有声明中的值是有效的。现在通过强制类型转换,增加了可赋给枚举变量的合法值。每个枚举都有取值范围,可以将范围内任何整数数据赋值给枚举变量,即使这个值不是枚举值。
enum bits{one = 1, two = 2, four = 4, eight = 8};
bits myflag;
myflag = bits(6); // valid
6 不是枚举值,但是它位于枚举定义的取值空间中。
取值范围定义:enum bigstep {first, second = 100, third};
- 找出上限:需要知道枚举量的最大值,找到大于这个最大值、最小的2次幂然后减一;dou
- 最大值为101, 在2次幂中,大于101的最小值为128, 所以上限为128 - 1 = 127
- 计算下限:枚举量的最小值,如果该值不小于0,则取值范围下限为0;如果小于0,找到小于该枚举量的最大2次幂,然后加一。
- 如果最小的枚举值为-6, 比它小的是最大2次幂为-8,所以下限为-8 + 1 = -7
需要多少空间来存储枚举是由编译器决定。对于取值范围较小的枚举,使用一个字节或者更少的空间;而对于包括long类型值的枚举,则使用4个字节。
4.7 指针和自由存储空间
计算机程序在存储数据时必须知道的三种属性
- 信息存储在何处;
- 存储的值为多少;
- 存储的信息时什么类型
指针是一个变量,存储的是值的地址,而不是值本身,程序员可以使用地址运算符&
获取内存地址
#include <iostream>
int main()
{
using namespace std;
int donuts = 6;
double cups = 4.5;
cout << "double value = " << donuts << " and donuts address = " << &donuts << endl;
cout << "cups value = " << cups << " and cups address = " << &cups << endl;
return 0;
}
// donuts value = 6 and donuts address = 0x0065fd40
// cups value = 4.5 and cups address = 0x0065fd44
十六进制表示法是长用于描述内存的表示法。在该实现中, donuts 的存储位置比 cups要低。两个地址的差为0x0065fd44 -0x0065fd40(即4)。这是有意义的,因为donuts 的类型为int,而这种类型使用4个字节。当然,不同系统给定的地址值可能不同。有些系统可能先存储cups,再存储donuts,这样两个地址值的差将为8个字节,因为cups的类型为double。另外,在有些系统中,可能不会将这两个变量存储在相邻的内存单元中。
使用常规变量时,值是指定的量,而地址为派生量。下面来看看指针策略,它是C++内存管理编程理念的核心。
:::info
指针与C++基本原理
面向对象编程与传统的过程性编程的区别在于:OOP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段决策可以根据当时的情况进行调整,更加灵活。
:::
运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。运行阶段决策就好比度假时,选择参观哪些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先设定的日程安排。
考虑为数组分配内存的情况。传统的方法是声明一个数组。要在C++中声明数组,必须指定数组的长度。因此,数组长度在程序编译时就设定好了:这就是编译阶段决策。您可能认为,在80%的情况下,一个包含20个元素的数组足够了,但程序有时需要处理200个元素。为了安全起见,使用了一个包含200个元素的数组。这样,程序在大多数情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后,可以这次告诉它只需要20个元素,而还可以下次告诉它需要205个元素。
总之,使用OOP时,您可能在运行阶段确定数组的长度。为使用这种方法,语言必须允许在程序运行时创建数组。稍后您看会到,C++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。
指针名表示地址,*
运算符被称为间接值或解除引用运算法,将其应用于指针,可以得到该地址存储的值
#include <iostream>
using namespace std;
int main() {
int updates = 6;
int *p_updates; // 生明一个整型指针,指针变量名字叫p_updates
p_updates = &updates; // 指针里面永远是地址,assign address of int to pointer
cout << "Values: updates = " << updates << ", *p_updates = " << *p_updates << endl;
cout << "Address: updates = " << &updates << ", *p_updates = " << p_updates << endl;
*p_updates = *p_updates + 1;
cout << "Now, updates = " << updates << endl;
return 0;
}
// 输出
// Values: updates = 6, *p_updates = 6
// Address: updates = 0x7ffcea931a8c, *p_updates = 0x7ffcea931a8c
// Now, updates = 7
变量 updates表示数值,并使用&
运算符来获得地址;变量p_updates
表示地址,并使用*
运算符来获得地址存储的值。*p_updates == updates == 6
、&updates == p_updates
。
4.7.1 声明和初始化指针
计算机需要跟踪指针指向的值的类型。char的地址和double地址看上去没什么两样,但char 和 double 使用的字节数是不同的,它们存储值时使用的内部格式也不同。所以指针声明必须指定指针指向的数据的类型。
int * p_updates; // 指针声明
*p_updates
的类型为int类型
的数据。p_updates
指向int类型
的指针,p_updates
的类型指向int的指针。
int *p1, p2; // 声明创建了一个指针p1和一个int类型的变量p2
:::info 注意:在C++中,int* 是一种复合类型,是指向int的指针 :::
double * tax_ptr; // tax_ptr points to type double
char * str; // str points to type char
上面中的指针声明中,已将tax_ptr声明为一个指向double的指针,因此编译器知道tax_ptr是一个double类型(以浮点格式存储)的值。指针变量不仅仅是指针,而且是指向特定类型的指针。tax_ptr和str虽然都是指针,但却是不同类型的指针,和数组一样,指针都是基于其他类型的。虽然他们是指向两种长度不同的数据类型,但是这两个指*针变量本身的长度通常是相同的。
char地址与double地址的长度相同,但是就好比是1016是超市的街道地址,1024是小村庄的街道地址。地址的长度或值不能指示关于变量的长度或类型的任何信息,也不能指示地址上有什么建筑物。
可以在声明语句中初始化指针,在这种情况下,被初始化的是指针,而不是它指向的值。
int higgens = 5;
int * pt = &higgens;
// 将指针初始化为一个地址
#include <iostream>
using namespace std;
int main()
{
int higgens = 5;
int *pt = &higgens;
cout << "Value of higgens = " << higgens << "; Address of higgens = " << &higgens << endl;
cout << "Value of *pt = " << *pt << "; value of pt = " << pt << endl;
return 0;
}
// Value of higgens = 5; Address of higgens = 0x7ffdd857ce1c
// Value of *pt = 5; value of pt = 0x7ffdd857ce1c
4.7.2 指针的危险
C++创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据内存。
long * fellow; // 创建一个指向long类型的指针
*fellow = 23333;
fellow 是个指针,但是上述代码中并没有将地址赋值给fellow(fellow没有被初始化),所以将现有的值解释为存储23333的地址。可能会引发冲突
:::warning
一定要再对指针应用*****
之前,将指针初始化为一个确定的、适当的地址。
:::
4.7.3 指针和数字
指针不是整型,但是计算机通常把地址当作整数来处理。整数可以执行算数运算操作,而指针描述的是位置,不能简单的将整数赋值给指针:
int * pt;
pt = 0xB8000000; // 类型不匹配
左边是指向int类型的指针,右边是一个16进制的整数,通常在计算机中使用这种格式来表示地址,但是程序并不知道这个数字是一个地址。可通过强制类型转换将数字转换为适当的地址类型。
int * pt ;
pt = (int*) 0xB8000000;
4.7.4 使用new来分配内存
指针的最大的作用:在运行阶段分配未命名的内存以存储值。这种情况下,只能通过指针来访问内存。
int * pn = new int;
程序员要告诉new,需要为哪种数据类型分配内存,new将找到一个长度正确的内存块,并返回该内存块的地址,程序员的责任就是将该地址赋值给一个指针pn,pn是被声明为指向int的指针。
int higgens;
int * pt = &higgens;
都是将int变量的地址赋值给了指针。第二种方式:可以通过名称higgens访问该int类型的值;第一种情况下,则只能通过该指针进行访问。指针pn指向的是一个数据对象(非OOP的对象),数据对象比变量更加通用,它指的是为数据项分配的内存块。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式为:
typeName * pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。
#include <iostream>
using namespace std;
int main()
{
int nights = 1001;
cout << "nights value = " << nights << ": location " << &nights << endl;
int *pt = new int;
*pt = 1001;
cout << "int value = " << *pt << ": location = " << pt << endl;
double *pd = new double;
*pd = 10000001.0;
cout << "double value = " << *pd << ": location = " << pd << endl;
cout << "location of pointer pd: " << &pd << endl; // 取出指针的地址
cout << "size of pt = " << sizeof(pt) << ": size of *pt = " << sizeof(*pt) << endl;
cout << "size of pd = " << sizeof pd << ": size of *pd = " << sizeof(*pd) << endl;
return 0;
}
// nights value = 1001: location 0x7ffe50a35c54
// int value = 1001: location = 0x563846e34280
// double value = 1e+07: location = 0x563846e342a0
// location of pointer pd: 0x7ffe50a35c58
// size of pt = 8: size of *pt = 4
// size of pd = 8: size of *pd = 8
上述代码说明了必须声明指针所指向的类型的原因之一:地址本身只指出了对象存储地址的开始,而没有指出其类型占用(使用的字节数)。程序中因为声明了指针所指向的数据类型,所以*pd
是8个字节的double值,*pt
是4个字节的int值。在使用cout打印*pd
时,就知道要读取多少字节以及如何解释他们。
new分配的内存块存储在堆heap或自由存储区free store,常规变量声明分配的内存块再栈stack的内存区域。
内存会被耗尽,计算机可能会由于没有足够的内存而无法满足new的请求。在这种情况下,new通常会引发异常,在较老的实现中,new将返回0。在C++中,值为0的指针被称为空指针( null pointer )。C++确保空指针不会指向有效的数据,因此它常被用来表示运算符或函数失败(如果成功,它们将返回一个有用的指针)。C++提供了检测并处理内存分配失败的工具。
4.7.5 使用delete释放内存
当需要内存时,可以使用new来请求;当使用完内存之后,使用delete运算符归还给内存池。释放的内存可以供其他使用。在使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)
int * ps = new int;
delete ps;
这将释放ps所指向的内存,但是不会删除指针ps本身,所以还可以将ps指针指向另一块内存地址。一定要配对使用new和delete,否则会发生内存泄漏memory leak(被分配的内存再也无法使用),如果内存泄漏严重,程序将由于不断地寻找更多的内存而终止。
不要释放已经释放的内存,可能会造成意想不到的后果。不能使用delete来释放声明变量所获得的内存。
int * ps = new int;
delete ps;
delete ps; // not ok now
int jugs = 5;
int * pi = &jugs;
delete pi; // not allowed, memory not allocated by new
:::info 只能用delete来释放使用new来申请的内存,对空指针使用delete是安全的。 :::
int *ps = new int; // allocate memory
int * pq = ps; // set second pointer to same block
delete pq; /// delete with second pointer
一般来说,不要创建两个指向同一个内存块的指针,可能会错误地删除同一个内存块两次。
4.7.6 使用new来创建动态数组
- 静态联编static binding:在编译时给数组分配内存,需要指定数组长度
- 动态联编 dynamic binding:在程序运行时选择数组长度,在运行时确定数组长度
使用new创建动态数组
int * psome = new int[10]; // 创建一个包含10个int元素的数组
new运算符返回第一个元素的地址。应使用delete释放new创建的数组,
delete [] psome; // 释放动态数组
[]
应该告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
int * pt = new int;
short *ps = new short[500];
delete []pt;
delete ps;
使用new和delete时,应遵循的规则
- 不要使用delete释放不是new分配的内存
- 不要使用delete释放同一个内存块两次
- 如果使用new[] 为数组分配内存,应该使用delete[] 来释放
- 如果使用new[] 为一个实体分配内存,则应使用delete(没有[]) 来释放
- 对空指针应用delete是安全的
psome
是指向一个int(数组中的第一个元素)的指针,无法确定数组元素个数。为数组分配内存的通用格式为:
type_name * pointer_name = new type_name[num_elements];
使用new运算符
可以确保内存块足以存储num_elements
个类型为 type_name
的元素,而pointer_name
将指向第1个元素。可以以使用数组名的方式来使用pointer_name。
使用动态数组
指针psome指向包含10个int值的内存块中的第一个元素。对于第一个元素,使用psome[0]
访问,而不是通过*psome
;对于第二个元素,使用psome[1]
来访问。所以使用指针来访问动态数组就非常简单。数组和指针基本等价是C和C++的优点之一。
#include <iostream>
using namespace std;
int main()
{
double *p3 = new double[3]; // space for 3 doubles
p3[0] = 0.2; // treat p3 like an array name
p3[1] = 0.5;
p3[2] = 0.8;
cout << "p3[1] is " << p3[1] << "." << endl;
p3 = p3 + 1; // increment the pointer
cout << "Now p3[0] is " << p3[0] << "." << endl;
cout << "Now p3[1] is " << p3[1] << "." << endl;
p3 = p3 - 1; // point back to beginning
cout << "Now p3[0] is " << p3[0] << "." << endl;
cout << "Now p3[1] is " << p3[1] << "." << endl;
delete[] p3; // free the memory
return 0;
}
// output:
// p3[1] is 0.5.
// Now p3[0] is 0.5.
// Now p3[1] is 0.8.
// Now p3[0] is 0.2.
// Now p3[1] is 0.5.
指针p3当作数组名来使用,p3[0] 为第一个元素。如果p3是数组名,p3 = p3 + 1
是错误的,但是p3是数组指针,指针是变量,因此可以修改指针变量的值。
4.8 指针、数组和指针算术
:::info
指针和数组基本等价的原因在于指针算数**pointer arithmetic**
和 C++内部处理数组的方式。
:::
整数:将整数变量加1,值直接加1;
指针:将指针变量加1,增加的量等于它指向的类型的字节数,如果数据类型为int,将指针加1后,数值会加4。
:::info
C++将数组名解释为指针。
:::
#include <iostream>
using namespace std;
int main() {
double wages[3] = {10000.0, 20000.0, 30000.0};
short stacks[3] = {3, 2, 1};
// here are two ways to get the address of an array
double *pw = wages; // name of an array = address
short *ps = &stacks[0]; // or use address operator with array element
cout << "pw = " << pw << ", *pw = " << *pw << endl;
pw = pw + 1;
cout << "add 1 to the pw pointer: " << endl;
cout << "pw = " << pw << ", *pw = " << *pw << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl;
ps = ps + 1;
cout << "add 1 to the ps pointer: " << endl;
cout << "ps = " << ps << ", *ps = " << *ps << endl;
cout << "access two elements with array notation" << endl;
cout << "stacks[0] = " << stacks[0] << ", stacks[1] = " << stacks[1] << endl;
cout << "access two elements with pointer notation" << endl;
cout << "*stacks = " << *stacks << ", *(stacks + 1) = " << *(stacks + 1) << endl;
cout << sizeof(wages) << " = size of wages array" << endl;
cout << sizeof(pw) << " = size of pw pointer" << endl;
/**** 勘误109页数组的地址 ****/
short tell[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "tell = " << tell << endl; // 第一个元素的起始地址
cout << "tell + 1 = " << tell + 1 << endl;
cout << "&tell = " << &tell << endl; // 整个数组的起始地址
cout << "&tell + 1 = " << &tell + 1 << endl; // &tell + 1跨过了20个字节
return 0;
}
// output
// pw = 0x7ffffed51c10, *pw = 10000
// add 1 to the pw pointer:
// pw = 0x7ffffed51c18, *pw = 20000
// ps = 0x7ffffed51bea, *ps = 3
// add 1 to the ps pointer:
// ps = 0x7ffffed51bec, *ps = 2
// access two elements with array notation
// stacks[0] = 3, stacks[1] = 2
// access two elements with pointer notation
// *stacks = 3, *(stacks + 1) = 2
// 24 = size of wages array
// 8 = size of pw pointer
// tell = 0x7ffffed51bf0
// tell + 1 = 0x7ffffed51bf2
// &tell = 0x7ffffed51bf0
// &tell + 1 = 0x7ffffed51c04
在多数情况下,C++将数组名解释为数组第一个元素的地址。将pw声明为指向double类型的指针,然后将它初始化为wages
:wages数组中的第一个元素的地址 wages = &wages[0] = address of first element of array
。
:::info
将指针变量加1后,其增加的值等于指向的类型占用的字节数。
:::
***(stacks + 1)**
和 **stack[1]**
是等价的,通常,使用数组表示法时,C++都执行下面的转换
arrayname[i] -> *(arrayname + i)
如果使用的是指针,而不是数组名,C++都执行同样的转换
pointname[i] -> *(pointername + i)
指针名和数组名的区别
- 指针是变量,可以修改其值,但数组名是常量
- 对数组应用
sizeof
运算符得到的是数组长度,而对指针应用sizeof
得到的是指针的长度,即使指针指向的是一个数组。 :::info 数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。 ::: 虽然tell
和&tell
数值相同,但是与tell
对应的是&tell[0]
:对应的是一个2字节内存块的地址,而&tell
是一个20字节内存块的地址。tell
是一个short类型的指针(*short
),而&tell指针
指向的是包含20个元素的short数组short (*) [20]
。
可以这样声明和初始化这种指针
如果省略括号,优先级规则将使得short (*pas) [20] = &tell; // pas points to array of 20 shorts
pas
先于[20]
结合,导致pas是一个包含20个元素的short指针数组,所以不可省略括号。其次,如果要描述变量类型,可将声明中的变量名删除,因此pas的类型为short(*)[20]
。由于pas
被设置为&tell
,因此*pas
与tell
等价,所以(*pas)[0]
为tell
数组的第一个元素。
使用new来创建数组以及使用指针来访问不同的元素非常简单,只需要把指针当作数组名对待。
4.8.2 指针小结
声明指针
// 语法格式: typeName * pointerName; double * pn; // 指向double的指针 char * pc;
给指针赋值
应将内存地址赋值给指针。可以对变量名应用&运算符(取地址),new运算符返回未命名的内存地址。
double * pn; // 指针pn可以指向一个double类型的值
double * pa; // pa和pn的作用相同
char * pc; // pc可以指向一个char类型的值
double bubble = 3.2;
pn = &bubble; // 为pn设置为bubble的地址
pc = new char; // 使用new运算符分配了char类型的内存地址赋值给pc
pa = new double[30]; // 将一个包含30个元素的double类型的数组的第一个元素的地址赋值给了pa
- 对指针解除引用
获取指针指向的值,要使用*
取值运算符来解除引用。
// pn 是指向double类型数据的指针,*pn是当前指针所指向的值
cout << *pn; // print the value of bubble
*pc = 'S'; //
也可以使用数组表示法,pn[0] == *pn
。
:::warning
不要对未被初始化为适当地址的指针解除引用。
:::
- 区分指针和指针所指向的值
如果pt是指向int类型的指针,则*pt不是指向int类型的指针,而是一个int类型的变量。
int *pt = new int; // 为pt指针分配一个地址
*pt = 5; // 在分配的地址处储存一个值 5
- 数组名
多数情况下,数组名视为数组的第一个元素的地址。但是当sizeof运算符用于数组名时,会返回整个数组的长度(单位:字节)
int tacos[10];
cout << "tacos array's length " << sizeof(tacos) << endl; // 4 * 10 = 40
- 指针算术
- C++允许将指针和整数相加。加1的结果为原来地址加上所指向对象占用的总字节数。
- 还可以将一个指针减去另一个指针,得到的是两个元素的间隔。前提是两个指针指向同一个数组(也可以指向超出结尾的一个位置)。
- 数组的动态联编和静态联编
- 使用数组声明创建数组,将采用静态联编,即数组长度在编译时设置
- 使用
new[]
运算符创建数组,将采用动态联编,即在程序运行时为数组分配空间和数组长度。使用完之后,应使用delete[]释放其占用的内存。 ```cpp int tacos[10]; // 静态联编,需要指定数组长度
int size; cin >> size; int *pz = new int[size]; // 动态联编,在程序运行时设置数组长度 delete [] pz; // 当这个数组无用时,要及时使用delete []释放掉
8. **数组表示法和指针表示法**
使用方括号数组表示法等同于对指针解除引用
```cpp
int *pu = new int[10];
*pu = 5;
pu[0] = 6;
pu[9] = 44;
int coats[10];
*(coats + 4) = 12;
4.8.3 指针和字符串
数组和指针的关系可以扩展到C-style字符串
char flower[10] = 'rose';
cout << flower << " s are red\n";
数组名是第一个元素的地址,因此cout语句中的flower是字符**r**
的地址。但是如果给cout提供一个字符的地址,其会打印此字符,并继续寻找并打印下一字符,直到遇到空字符为止。
:::info
char数组名、char指针以及用引号括起来的字符串常量都被解释为字符串的第一个字符的地址。
:::
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char flower[10] = "rose";
cout << flower << "s are red" << endl; // 当遇到字符的时候,从该字符开始打印,直到遇到空字符
char animal[20] = "bear";
const char *bird = "wren";
char *ps;
cout << animal << " and ";
cout << bird << endl;
cout << "Enter a kind of animal: ";
cin >> animal;
ps = animal;
cout << ps << "!" << endl;
cout << "Before using strcpy():" << endl;
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
ps = new char[strlen(animal) + 1]; // get new storage
strcpy(ps, animal);
cout << "After using strcpy(): " << endl;
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
delete[] ps;
return 0;
}
// output
// bear and wren
// Enter a kind of animal : fox
// fox !
// Before using strcpy () :
// fox at 0x0065fd30
// fox at 0x0065fd30
// After using strcpy () :
// fox at 0x0065fd30
// fox at 0x004301c8
程序创建了一个char数组(animal)
和 两个指向char
的指针变量(bird和ps)。程序将animal数组初始化为字符串bear
,就像初始化数组一样。然后,程序执行了一些新的操作,将char指针初始化为指向一个字符串:const char * bird = "wren" ; // bird holds address of string
**wren**
实际表示的是字符串的地址,因此这条语句将**wren**
的地址赋给了bird 指针。
:::info
一般来说,编译器在内存留出一些空间,以存储程序源代码中所有用引号括起的字符串,并将每个被存储的字符串与其地址关联起来。
:::
这意味着可以像使用字符串wren
那样使用指针bird
,如下面的示例所示:cout << "Aconcerned " << bird << " speaks\n" ;
字符串字面值是常量,这就是为什么代码在声明中使用关键字const
的原因。以这种方式使用const意味着可以用bird
来访问字符串,但不能修改它。最后,指针 ps 未被初始化,因此不指向任何字符串(要杜绝此类操作)。
对于cout 来说,使用数组名animal
和指针bird
是一样的,它们都是字符串的地址,cout将打印存储在这两个地址上的两个字符串(bear
和wren
)。
创建未初始化的指针有点像签发空头支票:无法控制它将被如何使用。
:::info
在将字符串读入程序时,应使用已经分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。
:::
ps = animal;
cout << animal << " at " << (int *)animal << endl;
cout << ps << " at " << (int *)ps << endl;
// fox at 0x0065fd30
// fox at ox0065fd30
一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为**char***
,则cout将显示指向的字符串。如果要显示的是字符串的地址,则必须使用强制类型转换,如 **int***
。因此,ps 显示为字符串fox
,而(int *) ps
显示为该字符串的地址。
:::info
注意,将animal赋给ps并不会复制字符串,而只是复制地址。这样,这两个指针将指向相同的内存单元和字符串。
:::
那么如何获得字符串副本呢?
首先,需要分配内存来存储该字符串,这可以 通过声明另一个数组 或 使用new 来完成。后一种方法使得能够根据字符串的长度来指定所需的空间:
ps = new char [strlen(animal) + 1]; // get new storage
字符串
fox
不能填满整个animal数组,上述代码使用strlen()
来确定字符串的长度(而非整个数组的长度),并将它加1来获得包含空字符时该字符串的长度。随后,程序使用new来分配刚好足够存储该字符串的空间。接下来,需要将animal数组中的字符串复制到新分配的空间中。将animal赋给ps是不可行的,因为这样只能修改存储在ps中的地址,从而失去程序访问新分配内存的唯一途径。需要使用库函数
strepy()
strcpy(ps, animal); // copy string to new storage
strcpy()
函数接受2个参数。第一个是目标地址,第二个是要复制的字符串的地址。
您应确定,分配了目标空间,并有足够的空间来存储副本。在这里,我们用strlen( )来确定所需的空间,并使用new获得可用的内存。
通过使用strcpy()和new,将获得“fox”的两个独立副本:fox at ox0065fd30
fox at ox004301c8
另外,new在离animal数组很远的地方找到了所需的内存空间。
经常需要将字符串放到数组中。初始化数组时,请使用=运算符;否则应使用strepy()或strnepy()。strcpy()在前面已经介绍过,其工作原理如下: