《C++ Primer Plus》第四章总结

一、数组

数组是一种数据格式,可以存储多个同类型的值。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。

要创建数组,可使用声明语句,数组声明应该指出以下三点:

  • 存储在每个元素中的值的类型;
  • 数组名;
  • 数组中的元素数;

在C++中,可以通过修改简单变量的声明,添加中括号(其中包含元素数目)来完成数组声明。事实上,可以将数组中的每一个元素看为一个简单变量。声明数组的通用格式为

typeName arrayName[arraySize];

表达式arraySize指定元素数目,它必须是整型常量或const值,也可以是常量表达式,即其中所有的值在编译时都是已知的。具体的说,arraySize不能是变量,变量的值是在程序运行时设置的。

数组之所以被称为复合类型,是因为它是使用其他类型来创建的。不能仅仅将某种东西声明为数组,它必须是特定类型的数组。没有通用的数组类型,但存在很多特定的数组类型,如char数组或long数组。

数组的很多用途都基于可以单独访问数组元素。方法是使用下标或索引来对元素进行编号,C++数组从0开始编号,且使用带索引的方括号表示法来指定数组元素。注意,最后一个元素的索引比数组长度小1。因此,数组声明能够使用一个声明创建大量的变量,然后便可以使用索引来标识和访问各个元素。

编译器不会检查使用的下标是否有效,如果将一个值赋给不存在的元素,编译器并不会指出错误。但是程序运行后,这种赋值可能会引发问题,它可能会破坏数据或代码,也可能导致程序异常终止,所以必须确保程序使用有效的下标值。

#include <iostream>
using namespace std;

int main(){
    int yams[3];
    yams[0]=7;
    yams[1]=8;
    yams[2]=6;
    int yamcosts[3]={20,30,5};
        cout<<"Total yams=";
        cout<<yams[0]+yams[1]+yams[2]<<endl;
        cout<<"The package with "<<yams[1]<<" yams costs ";
        cout<<yamcosts[1]<<" cents per yam.\n";
        int total=yams[0]*yamcosts[0]+yams[1]*yamcosts[1];
        total=total+yams[2]*yamcosts[2];
        cout<<"The total yam expense is "<<total<<" cents.\n";
        cout<<"Size of yams array = "<<sizeof yams;
        cout<<" bytes."<<endl;
        cout<<"Size of one element = "<<sizeof yams[0];
        cout<<" bytes.";
    return 0;
}

990294da27ad4f1990e1e60cffb900d6.jpg

 1.程序说明

程序给yam的元素赋值语句比较繁琐,而C++允许在声明语句中初始化数组元素。只需提供一个用逗号分隔的值列表(初始化列表),并将它们用花括号括起即可。如果没有初始化函数中定义的数组,则其元素值将是不确定的,这意味着元素的值为以前驻留在该内存单元中的值。

sizeof运算符返回类型或数据对象的长度(单位为字节),如果将sizeof运算符用于数组名,得到整个数组的字节数。但如果将sizeof用于数组元素,则得到元素的长度。

2.数组的初始化规则

C++有几条关于初始化数组的规则,它们限制了初始化的时刻,决定了数组的元素数目与初始化器中值的数目不相同时将发生地情况。

只有在定义数组时才能使用初始化,此后就不能使用了。然而,可以使用下标分别给数组中的元素赋值。初始化数组时,提供的值可以少于数组的元素数目。如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0。因此,将数组中所有的元素都初始化为0非常简单——只要显式地将第一个元素初始化为0,然后让编译器将其他元素都初始化为0。

如果初始化数组时方括号内为空,C++编译器将计算元素个数。通常,让编译器计算元素个数并不明智,因为其计数可能与你想象的不一样,例如,可能不小心就在列表中遗漏了一个值。但这种方法对于将字符数组初始化为一个字符串来说比较安全。

如果主要关心的问题是程序,而不是自己是否知道数组的大小,则可以

short things[]={1,2,3,4};

int num=sizeof things/sizeof (short);

3.C++11数组初始化方法

C++11将使用大括号的初始化(列表初始化)作为一种通用初始化方法,可用于所有类型。数组以前就可使用列表初始化,但C++11中的列表初始化新增了一些功能。

(1)初始化数组时,可省略等号。

double a[3] {1,2,3};

(2)可不在大括号内包含任何东西,这将把所有元素都设置成0。

float a[10] {};

(3)列表初始化禁止缩窄转换。

long a[]={25,92,3.0};

char b[4] {'h','i',1122011,'\0'};

char c[4] {'h','i',112,'\0'};

在上述代码中,第一条语句不能通过编译,因为将浮点数转换为整数是缩窄操作,即使小数点后面为零;第二条语句也不能通过编译,因为1122011超出了char变量的取值范围;第三条语句可通过编译,虽然112是一个int值,但它在char变量的取值范围内。

C++标准模板库(STL)提供了一种数组替代品——模板类vector,而C++11新增了模板类array。这些替代品比内置复合类型数组更复杂,更灵活。

二、字符串

字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种,第一种来自C语言,称为C-风格字符串,第二种是基于string类库的方法。

存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都存储在自己的数组元素中。字符串提供了一种存储文本信息的便捷方法,而C-风格字符串具有一种特殊性质,即以空字符结尾。空字符被写作\0,其ASCII码为0,用来标记字符串的结尾。

char a[]={'a','b','c'};

char b[]={'a','b','c','\0'};

这两个数组都是char数组,但第二个是字符串。空字符对C-风格字符串而言非常重要,如C++有很多处理字符串的函数,包括cout使用的那些函数,它们都逐个地处理字符串中的字符,直到到达空字符为止。如果使用cout显示上面的b数组,会显示三个字符后遇到空字符而停止;但如果使用cout显示a数组,它不是字符串,cout将打印出数组的3个元素后并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止,空字符在内存中很常见,所以这个过程会很快停止。

在b数组示例中,将数组初始化为字符串的工作看上去很乏味,因为使用了大量单引号且需要在最后加上空字符,但有一种更好、将字符数组初始化为字符串的方法,即使用一个用双引号括起的字符串即可,这种字符串称为字符串常量字符串字面值

用引号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它。另外,各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字符

应确保数组足够大,能存储字符串中所有字符——包括空字符。使用字符串常量初始化字符数组时,让编译器计算元素数目更为安全。让数组比字符串长只会浪费一些空间,没有什么别的害处。这是因为处理字符串的函数是根据字符串的位置,而不是数组长度进行处理。C++对字符串的长度没有限制。在确定存储字符串所需的最短数组时,需要记住加上空字符。

字符串常量(使用双引号)不能与字符常量(使用单引号)互换,字符常量是字符串编码的简写形式,但字符串常量如"S",它表示的是两个字符(字符S和\0)组成的字符串,而且"S"实际上表示的是字符串所在的内存地址。

char ch="S";

这句话尝试将一个内存地址赋给ch,由于地址在C++中是一种独立的类型,因此C++编译器不允许这种不合理的做法。

1.拼接字符串常量

有时字符串很长,无法放到一行里。C++允许拼接字符串字面值,即将两个用引号括起的字符串合并为一个。事实上,任何两个由空白(空格、制表符、换行符)分隔的字符串常量将自动拼接成一个。

cout<<"a b";

cout<<"a " "b";

cout<<"a "

"b";

三条语句等效。

拼接时不会在被拼接的字符串之间添加空格,第二个字符串的第一个字符将紧跟在第一个字符串的最后一个字符(不考虑\0)后面,第一个字符串的\0字符将被第二个字符串的第一个字符取代。

2.在数组中使用字符串

要将字符串存储到数组中,最常用的两个方法是将数组初始化为字符串常量、将键盘或文件输入读入到数组中

#include <iostream>
#include <cstring>
using namespace std;

int main() {
    const int size=15;
    char name1[size];
    char name2[size]="C++owboy";
    cout<<"Howdy! I\'m "<<name2;
    cout<<"! What\'s your name?"<<endl;
    cin>>name1;
    cout<<"Well, "<<name1<<", your name has ";
    cout<<strlen(name1)<<" letters and is stored\n";
    cout<<"in an array of "<<sizeof(name1)<<" bytes!"<<endl;
    cout<<"Your initial is "<<name1[0]<<".\n";
    name2[3]='\0';
    cout<<"Here are the first 3 characters of my name:";
    cout<<name2<<endl;
    return 0;
}

88a5c492b9f44ecdb167f58265a3fa10.jpg

 该程序演示了这两种方法,它将一个数组初始化为用引号括起的字符串,并使用cin将一个输入字符串放到另一个数组中。该程序还使用了标准C语言库函数strlen()来确定字符串的长度。标准头文件cstring提供了该函数以及很多与字符串相关的函数的声明。

sizeof运算符指出整个数组的长度,但strlen()函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度。strlen()计算时不会把空字符计算在内。如果string是字符串,则要存储该字符串,数组的长度不能小于strlen(string)+1。

由于name1和name2是数组,所以可以用索引来访问数组中各个字符。另外,可使用赋值\0来截短字符串。

439fd0175c1646e2854f96e62ee7ef65.jpg

该程序使用符号常量来指定数组的长度,程序常常多条语句使用了数组长度。使用符号常量来表示数组长度后,当需要修改数组长度时,只需要在定义符号常量的地方进行修改即可。

3.字符串输入

#include <iostream>
using namespace std;

int main() {
    const int size=20;
    char name[size];
    char dessert[size];
    cout<<"Enter your name:"<<endl;
    cin>>name;
    cout<<"Enter your favorite dessert:"<<endl;
    cin>>dessert;
    cout<<"I have some delicious "<<dessert;
    cout<<" for you, "<<name<<"."<<endl;
    return 0;
}

747681cdbd92455785588dd380a51db4.jpg

我们并没有对第二个输入进行反应,程序就显示了出来并立即显示了最后一行。由于不能通过键盘输入空字符,因此cin需要用别的方法来确定字符串的结尾位置。cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放入数组中,并自动在结尾添加空字符。

另一个问题是,输入字符串可能比目标数组长,后续介绍。

4.每次读取一行字符串输入

每次读取一个单词通常不是最好的选择,要将整条短语而不是一个单词做为字符串输入,需要采用另一种字符串读取方法。具体地说,需要采用面向行而不是面向单词的方法,幸运的是,istream中的类提供了一些面向行的成员函数:getline()和get()。这两个函数都读取一行输入,直到达到换行符。然而,随后getline()将丢弃换行符,而get()将换行符保留在输入队列中。

(1)面向行的输入getline()

getline()函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用cin.getline()。该函数有两个参数,第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。如果这个参数是20,则函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。getline()成员函数在读取指定数目的字符或遇到换行符时停止读取。

#include <iostream>
using namespace std;

int main() {
    const int size=20;
    char name[size];
    char dessert[size];
    cout<<"Enter your name:"<<endl;
    cin.getline(name,size);
    cout<<"Enter your favorite dessert:"<<endl;
    cin.getline(dessert,size);
    cout<<"I have some delicious "<<dessert;
    cout<<" for you, "<<name<<"."<<endl;
    return 0;
}

57f3f573ef034ae398f5b1c2566e00e6.jpg

该程序现在可以读取完整的姓名和用户喜欢的dessert。getline()函数每次读取一行,它通过换行符来确定行尾,但不保存换行符。相反,在存储字符串时,它用空字符来替换换行符。

d62445cb51a34ceb9b228744473922db.jpg

(2)面向行的输入get()

istream类有另一个名为get()的成员函数,该函数有几种变体。其中一种变体的工作方式与getline()类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但get不再读取并丢弃换行符,而是将其留在输入队列中。

如果两次调用get()

cin.get(name,size);

cin.get(dessert,size);

由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符就是换行符。因此get()认为已到达行尾,而没有发现任何可读取内容。如果不借助帮助,get()无法跨过该换行符。

get()有另一种变体,使用不带任何参数的cin.get()调用可读取下一个字符(即使是换行符),因此可以用来处理换行符,为读取下一行输入做好准备。

cin.get(name,size);

cin.get();

cin.get(dessert,size);

该调用顺序可以处理换行符。

另一种使用get()的方法是将两个类成员函数拼接起来(合并)。

cin.getline(name,size).get();

之所以可以这样做,是因为cin.get(name,size)返回一个cin对象,该对象随后将被用来调用get()函数。

cin.getline(name1,size).getline(name2,size);

这条语句将把输入中连续的两行分别读入到数组name1和name2中,其效果与两次调用cin.getline()相同。

#include <iostream>
using namespace std;

int main() {
    const int size=20;
    char name[size];
    char dessert[size];
    cout<<"Enter your name:"<<endl;
    cin.get(name,size).get();
    cout<<"Enter your favorite dessert:"<<endl;
    cin.get(dessert,size).get();
    cout<<"I have some delicious "<<dessert;
    cout<<" for you, "<<name<<"."<<endl;
    return 0;
}

1274ec71c903493c806a3b175efbee7b.jpg

C++允许函数有多个版本,条件是这些版本的参数列表不同。如果使用的是cin.getline(name,size),则编译器知道要读取一个字符串放入数组中,因而将使用适当的成员函数;如果使用的是cin.get(),则编译器知道是要读取一个字符。

为什么要使用get(),而不是getline()呢?

  • 首先,老式实现没有getline();
  • 其次,get()使输入更仔细;

例如,假设使用get()将一行读入数组中,如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已填满呢?查看下一个输入字符,如果是换行符,说明已经读取了整行;否则,说明该行中还有其他输入。

总之,getline()使用起来简单一点,但get()使得检查错误更简单一点。可以使用其中任何一个来读取一行输入,但是他们的行为稍有不同。

(3)空行和其他问题

当getline()或get()读取空行时,最初的做法是,下一条输入语句将在前一条getline()或get()结束读取的位置开始读取;但当前的做法是,当get()(不是getline())读取空行后将设置失效位。这意味着接下来的输入将被阻断,但可以使用下面的命令来恢复输入:

cin.clear();

另一个潜在的问题是,输入字符串可能比分配的空间长。如果输入行包含的字符数比指定的多,则getline()和get()会把余下的字符留在输入队列中,而getline()还会设置失效位,并关闭后面的输入。

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;
}

26558b729e984945bab26a8efacdf01f.jpg

用户根本没有输入地址的机会,当cin读取年份时,将回车键生成的换行符留在了输入队列中。后面的cin.getline()看到换行符后,将认为是一个空行,并将一个空字符赋给数组。解决方法是,在读取地址之前,先读取并丢弃换行符。

这可以通过多种方式实现,

使用无参数的get():

cin>>year;

cin.get();

使用接受一个参数的get(ch):

char ch;

cin>>year;

cin.get(ch);

返回cin对象后进行拼接:

(cin>>year).get();

3f72d9c09253440e8dedd8a2a5bf3975.jpg

C++程序常使用指针(而不是数组)来处理字符串。

三、string类简介

可以使用string类型的变量而不是字符数组来存储字符串,string类使用起来比数组简单且提供了将字符串作为一种数据类型的表示方法。

要使用string类,需要包含头文件string,string类位于名称空间std中,所以需要使用using编译指令或者指定名称空间std。string定义隐藏了字符串的数组属性,让我们能够像处理普通变量那样处理字符串。

#include <iostream>
#include <string>
using namespace std;

int main() {
    char charr1[20];
    char charr2[20]="jaguar";
    string str1;
    string str2="panther";
    cout<<"Enter a kind of feline:";
    cin>>charr1;
    cout<<"Enter another kind of feline:";
    cin>>str1;
    cout<<"Here are some felines:"<<endl;
    cout<<charr1<<" "<<charr2<<" "<<str1<<" "<<str2<<endl;
    cout<<"The third letter in "<<charr2<<" is "<<charr2[2]<<endl;
    cout<<"The third letter in "<<str2<<" is "<<str2[2]<<endl;
    return 0;
}

cf927a28f7b14f86b10d740122a8d214.jpg

string对象与字符数组的相同点

  • 可以使用C-风格字符串来初始化string对象;
  • 可以使用cin来将键盘输入存储到string对象中;
  • 可以使用cout来显示string对象;
  • 可以使用数组表示法来访问存储在string对象中的字符;

string对象和字符数组的主要区别就是可以将string对象声明为简单变量,而不是数组。

string类设计让程序可以自动处理string的大小,如str的声明创建了一个长度为0的string对象,但程序将输入读入到str时,将自动调整str的长度,从而使其相较于数组更方便、安全。从理论上讲,可以将char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的实体。

1.C++11字符串初始化

C++11允许将列表初始化用于C-风格字符串和string对象。

char a[]={"abcd"};

char b[] {"abcd"};

string c={"abcd"};

string d {"abcd"};

2.赋值、拼接和附加

使用string类时,某些操作比使用数组时要简单。

例如,我们不能将一个数组赋值给另一个数组,但却可以将一个string对象赋值给另一个string。

string类简化了字符串合并操作,可以使用运算符+将两个string对象合并起来,还可以使用运算符+=将字符串附加到string字符串末尾。

可以将C-风格字符串或string对象与string对象相加,或将它们附加到string对象的末尾。

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s1="penguin";
    string s2,s3;
    cout<<"You can assign one string object to another: s2=s1"<<endl;
    s2=s1;
    cout<<"s1: "<<s1<<", s2: "<<s2<<endl;
    cout<<"You can assign a C-style string to a string object."<<endl;
    cout<<"s2= \"buzzard\""<<endl;
    s2="buzzard";
    cout<<"s2: "<<s2<<endl;
    cout<<"You can concatenate strings: s3=s1+s2"<<endl;
    s3=s1+s2;
    cout<<"s3: "<<s3<<endl;
    cout<<"You can append strings."<<endl;
    s1+=s2;
    cout<<"s1 += s2 yields s1 = "<<s1<<endl;
    s2+=" for a day";
    cout<<"s2 += \" for a day\" yields s2="<<s2<<endl;
    return 0;
}

5db769b6386549358e73f6b1d0bb05c8.jpg

3.string类的其他操作

在C++新增string类之前,也需要完成给字符串赋值等操作,对于C-风格字符串,使用C语言库中的函数来完成这些任务。头文件cstring提供了这些函数。

如,可以使用函数strcpy()将字符串复制到字符数组中

strcpy(charr1,charr2);//把charr2复制到charr1中

可以使用strcat()将字符串附加到字符数组末尾

strcat(charr1,charr2);//将charr2的内容附加到charr1后面

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

int main() {
    char charr1[20];
    char charr2[20]="jaguar";
    string str1;
    string str2="panther";

    str1=str2;
    strcpy(charr1,charr2);

    str1+=" paste";
    strcat(charr1," juice");

    int len1=str1.size();
    int len2=strlen(charr1);

    cout<<"The string "<<str1<<" contains "<<len1<<" characters."<<endl;
    cout<<"The string "<<charr1<<" contains "<<len2<<" characters."<<endl;
    return 0;
}

f7ec6488bc1e439099ba84440a0bd704.jpg

该程序将用于string对象的技术和用于字符数组的技术进行了比较。

处理string对象的语法通常比使用C-风格字符串函数的简单,尤其是执行比较复杂的操作时。

str3=str1+str2;

等价于

strcpy(charr3,charr1);

strcat(charr3,charr2);

另外,使用字符数组时,总是存在目标数组过小,无法存储指定信息的危险。

char site[10]="house";

strcat(site," of pancakes");

函数strcat()试图将全部12个字符复制到数组site中,这将覆盖相邻内存。这可能导致程序终止,也可能导致程序继续运行但数据被破坏。string类具有自动调整大小的功能,所以可以避免这种情况。C函数库提供了与strcat()和strcpy()类似的函数——strncat()和strncpy(),它们接受指出目标数组最大允许长度的第三个参数,因此更为安全,但会增强程序的复杂度。

程序中还使用了两种确定字符串中字符数的方法:

int len1=str1.size();

int len2=strlen(charr1);

函数strlen()接受一个C-风格字符串作为参数,并返回该字符串包含的字符数。函数size()的功能与它类似,但句法不同,str1不是被用作函数参数,而是位于函数名之前,它们之间用句点连接。这种句法表明,str1是一个对象,而size()是一个类方法。方法是一个函数,只能通过其所属类的对象进行调用。

C函数使用参数来指出要使用哪个字符串,而C++string类对象使用对象名和句点运算符来指出要使用哪个字符串。

4.string类I/O

可以使用cin和运算符>>来将输入存储到string对象中,使用cout和运算符<<来显示string对象,其句法与处理C-风格字符串相同,但每次读取一行而不是一个单词时,使用的句法不同。

#include <iostream>
#include <string>
#include <cstring>
using namespace std;

int main() {
    char charr[20];
    string str;

    cout<<"Length of string in charr before input:"<<strlen(charr)<<endl;
    cout<<"Length of string in str before input:"<<str.size()<<endl;
    cout<<"Enter a line of text:"<<endl;
    cin.getline(charr,20);
    cout<<"You entered: "<<charr<<endl;
    cout<<"Enter another line of text:"<<endl;
    getline(cin,str);
    cout<<"You 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;
    return 0;
}

03d4f3bbf36d4c00b22861b0d57b384a.jpg

我们发现在输入之前,该程序指出数组charr中字符串长度为0。首先,未初始化的数组的内容是未定义的;其次,函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符,而对于未初始化的数据,第一个空字符出现的位置是随机的,该答案也是不确定的。另外,用户输入之前,str中的字符串长度为0,这是因为未被初始化的string对象的长度自动被初始化为0。

cin.getline(charr,20);//将一行读取到数组中

这种句点表示法表明,函数getline()是istream类中的一个类方法,第一个参数是目标数组,第二个参数是数组长度,getline()使用它来避免超越数组的边界。

getline(cin,str);//将一行读取到string对象中

这个getline()不是类方法,它将cin作为参数,指出要到哪里去查找输入。另外也没有指出字符串长度的参数,因为string对象将根据字符串长度自动调整自身大小。

那为何两者一个是类方法而另一个不是呢?

在C++引入string类之前,就已经有了istream类,因此istream类的设计考虑到了C++的基本数据类型,比如int等,但没有考虑string类型,所以istream类中,有处理double、int和其他基础类型的类方法,但没有针对string对象的类方法。

但比如cin>>string;还是可以执行的,因为其中使用了string类的友元函数。

5.其他形式的字符串字面值

C++除了有char类型外,还有wchar_t类型,而C++11新增了类型char16_t和char32_t,同样也可以创建这些类型的数组和字符串字面值。对于这些类型的字符串字面值,C++分别使用前缀L、u和U表示。

wchar_t a[]=L"abcd";

char16_t b[]=u"abcd";

char32_t c[]=U"abcd";

C++11还支持Unicode字符编码方案UTF-8,在这种方案中,根据编码的数字值,字符可能存储为1~4个八位组,C++使用前缀u8来表示这种类型的字符串字面值。

C++11新增的另一种类型是原始字符串,在原始字符串中,字符表示的就是自己。

例如,序列\n不表示换行符,而表示这两个常规字符,即右斜杠和n,在屏幕上显示时也将显示这两个字符。可以在原始字符串中使用",不用再使用\"。既然在字符串字面值中包含",那么就无法使用它来表示字符串的开头和结尾,原始字符串将"(和)"作为定界符,并使用前缀R来标识原始字符串。

输入原始字符串时,按回车键不仅会移到下一行,还将在原始字符串中添加回车字符。

一般情况下,编译器见到第一个)"时,会认为字符串到此结束。但原始字符串语法允许我们在表示字符串开头的"和(之间添加其他字符,这意味着表示字符串结尾的"和)之间也必须包含这些字符。因此,使用R"1(标识原始字符串的开头时,必须使用)1"标识原始字符串的结尾。

也就是说,我们使用"1(和)1"替代了默认定界符。自定义定界符时,可以在默认定界符之间添加任意数量的基本字符,但空格、左右括号、斜杠和控制字符(如制表符和换行符等)除外。

也可将前缀R与其他字符串前缀结合使用,以标识wchar_t等类型的原始字符串,可将R放在前面,也可以放在后面,如Ru、UR等。

四、结构体简介

结构体是一种比数组更灵活的数据格式,同一个结构体可以存储多种类型的数据,将数据的表示合并到一起。也可以使用结构体数组来存储多个结构体。结构体是用户定义的类型,而结构体声明定义了这种类型的数据属性,定义了类型后,就可以创建这种类型的变量。

因此创建结构体分为两步,

  • 首先,定义结构体描述,它描述并标记了能够存储在结构体中的各种数据类型;
  • 其次,按描述创建结构体变量(结构体数据对象);
struct abc
{
    char a[20];
    int b;
    float c;
};

关键字struct表明这些代码定义的是一个结构体,标识符abc是这种数据格式的名称,大括号中包含的是结构体存储的数据类型的列表,其中每个列表项都是一条声明语句。列表中的每一项都被称为结构体成员。总之,结构体定义出了新类型的特征。

接下来,可以创建该类型的变量了。

abc A;//C++

struct abc A;//C

C++允许在声明结构体变量时省略关键字struct。

在C++中结构体标识符的用法与基本类型名相同,强调了结构体声明定义了一种新的类型,所以可以省略struct。

当创建完结构体变量后,可以使用成员运算符(.)来访问各个成员,通过成员名就可以访问结构体的成员,就像通过索引可以访问数组的元素一样。访问类成员函数的方法就是从访问结构体成员变量的方式衍生而来的。

1.在程序中使用结构体

#include <iostream> 
using namespace std;
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	inflatable guest={"abcd",1.88,29.99};
	inflatable pal={"ABCD",3.12,32.99};
	cout<<"Expand your guest list with "<<guest.name;
	cout<<" and "<<pal.name<<"!\n";
	cout<<"You can have both for $";
	cout<<guest.price+pal.price<<"!"<<endl;
	return 0;
	
}

结构体声明的位置很重要,一般有两种选择,一是可以将声明放在main()函数中,紧跟在开始的括号后面;另一种是将声明放在main()的前面,位于main()函数外面的声明称为外部声明,在main()内部的称为内部声明。外部声明可以被后面的任何函数使用,而内部声明只能被该声明所属的函数使用。一般使用外部声明,这样所有函数都可以使用这种结构体。

变量也可以在函数内部和外部定义,外部变量由所有的函数共享。C++不提倡使用外部变量,但提倡外部结构体声明,在外部声明符号常量更合理。

inflatable guest={"abcd",1.88,29.99};

该初始化方式和数组一样,使用逗号分隔值列表,并将这些值由花括号括起,可以将结构体的每个成员都初始化为适当类型的数据,同样,也可以将每个结构体成员看作是相应类型的变量。

2.C++11结构体初始化

与数组相同,C++11也支持将列表初始化用于结构体,且等号是可有可无的。

其次,如果大括号内没有包含任何东西,各个成员都将被设置为0.

最后,不允许缩窄操作。

3.结构体可以将string类作为成员

只要使用的编译器支持对以string对象作为成员的结构体进行初始化,结构体就可以将string类作为成员,但需要注意,需要指定名称空间std。

4.其他结构体属性

C++使用户定义的类型与内置类型尽可能相似,比如,可以将结构体作为参数传递给函数,也可以让函数返回一个结构体,另外,还可以使用赋值运算符将结构体赋给另一个同类型的结构体,这样结构体中每个成员都被设置为另一个结构体中相应成员的值,即使成员包含数组。这种赋值被称为成员赋值

#include <iostream> 
using namespace std;
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	inflatable guest={"abcd",1.88,29.99};
	inflatable pal;
	cout<<"guest: "<<guest.name<<" for $";
	cout<<guest.price<<endl;
	
	pal=guest;
	cout<<"pal: "<<pal.name<<" for $";
	cout<<pal.price<<endl;
	return 0;
	
}

可以看出,成员赋值是有效的。可以同时完成定义结构体和创建结构体变量的工作,只要将变量名放在结束括号的后面即可。

struct abc
{
    int b;
    float c;
} as,bs;

也可以对这种方式创建的变量进行初始化,

struct abc
{
    int b;
    float c;
} as={1,2.3};

不过建议将结构体定义和变量声明分开,这样可以便于阅读和理解。

还可以声明没有名称的结构体类型,方法是省略名称,同时定义一种结构体类型和一个这种类型的变量。

struct 
{
    int x;
    int y;
} position;

这样创建了一个名为position的结构体变量,可以使用成员运算符来访问它的成员。但这种类型没有名称,所以之后就无法创建这样的变量了。

除了C++程序可以使用结构体标记作为类型名称外,C结构体具有到目前为止除了C++11以外的讨论的C++结构体的所有特性,但C++结构体的特性更多。比如,与C结构体不同,C++结构体除了成员变量以外,还可以有成员函数,但多数被用于类中。

5.结构体数组

C++可以创建元素为结构体的数组,方法与创建基本类型数组完全相同,而这每个元素都可以单独使用成员运算符。

要初始化结构体数组,可以结合使用初始化数组的规则(用逗号分隔元素,并将所有值用花括号括起)和初始化结构体的规则(用逗号分隔成员,并将所有值用花括号括起)。由于数组中的元素都是结构体,所以可以使用结构体初始化的方法,最终结果为一个被括在花括号中、用逗号分隔的值列表,其中每个值又是一个被括在花括号中、用逗号分隔的值列表。可以使用自己喜欢的方式来格式化它们。

#include <iostream> 
using namespace std;
struct inflatable
{
	char name[20];
	float volume;
	double price;
};

int main()
{
	inflatable guest[2]={
	{"abcd",1.3,1.2},{"ABCD",2.3,2.2}
	};
	cout<<"The guests "<<guest[0].name<<" and "<<guest[1].name
		<<"\nhave a combined volume of "<<guest[0].volume+guest[1].volume<<" cubic feet."<<endl;
	return 0;
}

6.结构体中的位字段

与C语言一样,C++也允许指定占用特定位数的结构体成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便。字段的类型应为整型或枚举,接下来是冒号,冒号后面是一个数字,它指定了使用的位数。可以使用没有名称的字段来提供间距,每个成员被称为位字段。

struct abc
{
    int a:4;
    int :4;
    bool b: 1;
};

可以照常初始化这些字段,也可以用标准的结构体表示法来访问位字段。

abc ah={1,true};
if(ah.b)

位字段通常用于低级编程中,一般来说,可以使用整型和按位运算符来代替这种方法。

五、共用体

共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构体可以同时存储int、float和double等,但共用体只能存储int、float或double,共用体的语法与结构体类似,但含义不同。

union abc
{
    int a;
    long b;
    double c;
};

可以使用abc变量来存储int、long和double类型,条件是在不同的时间进行。

abc test;
test.a=15;
cout<<test.a;
test.c=1.2;
cout<<test.c;

因此,test有时是int变量,而有时又可以是double变量。成员名称标识了变量的容量,由于共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度。

共用体的用途之一是,当数据项使用两种或更多种格式,但这些格式不会同时使用时,可节省空间。

struct abc
{
    int a;
    union bc
    {
        float b;
        float c;
    }  bch;//标识符
};

匿名共同体没有名称,其成员将成为位于相同地址处的变量,而这个匿名共同体中的成员在某一时刻只有一个有效。它们的地址相同,所以不需要中间的标识符,程序员需要确定哪个成员是活动的。

struct abc
{
    int a;
    union 
    {
        float b;
        float c;
    }  ;
};

共用体常用于节省内存、操作系统数据结构或硬件数据结构。

六、枚举

C++的enum工具提供了另一种创建符号常量的方法,该方法可代替const。它还允许定义新类型,但必须按严格的限制进行。使用enum的句法与使用结构体类似

enum abcd {ae,be,ce,de};
  • 这条语句使abcd成为新类型的名称,abcd被称为枚举;
  • 将a、b、c、d作为符号常量,它们对应整数值0~3,这些常量叫做枚举量。

在默认情况下,将整数值赋给枚举量,第一个枚举量的值为0,第二个枚举量的值为1,依此类推。可以通过显式地指定整数值来覆盖默认值。

可以用枚举名来声明这种类型地变量:

abcd test;

枚举类型的特殊属性:

  • 在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量。如果试图赋给一个非法值,则有些编译器将出现编译错误,而有一些将发出警告。为获得最大限度的可移植性,应将非enum值赋给enum变量视为错误。
  • 对于枚举。只定义了赋值运算符,而没有定义算术运算。然而,有些实现并没有这种限制,这可能会导致违反类型限制。为获得最大限度的可移植性,应采纳较严格的限制。

枚举量是整型,可被提升为int类型,但int类型不能自动转换为枚举类型。

对于上面已经定义好的abcd枚举类型,

int letter=ae;//有效,枚举量被提升为int,为0

test=3;//不能,int无法转换为枚举量

letter=2+be;//有效,枚举量被提升为int

        虽然,3对应的是de,但将3赋给test会产生类型错误。但将be赋给test是可以的,因为它们都是abcd类型。有些实现方法没有这种限制,而2+be中的加法并非为枚举量定义,但de被转换为int类型,因此结果也是int类型。由于在这种情况下,枚举会被转换为int,因此可以在算术表达式中使用枚举和常规整数,尽管并没有为枚举定义算术运算。

对应非法语句test=ae+be;

没有为枚举定义运算符+,但用于算术表达式中时,枚举将被转换为整数,表达式将被转换为0+1,这是一个合法的表达式,但类型为int类型,不能将其赋值给类型为abcd的变量test。

如果int值是有效的,则可以通过强制类型转换,将它赋给枚举变量:

test=abcd(1);

如果对一个不适当的值进行强制类型转换,结果是不确定的,这意味着这样做不会出错,但不能依赖得到的结果。比如test=abcd(1000);

枚举的规则相当严格,实际上,枚举更常被用来定义相关的符号常量,而不是新类型。例如,可以用枚举来定义switch语句中使用的符号常量,如果打算只使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称,如enum {ae,be,ce,de};

1.设置枚举量的值

可以使用赋值运算符来显式地设置枚举量的值:

enum num {one=1,two=2,three=3};

指定的值必须是整数,也可以只显式地定义其中一些枚举量的值:

enum num {one,two=200,three};

这里的one默认情况下是0,后面没有初始化的枚举量的值将比其前面的枚举量大1,则three为201.

也可以创建多个值相同的枚举量:

enum {zero,null=0,one,num_one=1};

其中前两者为0,而后两者为1。在C++早期版本中,只能将int(或提升为int的值)赋给枚举量,但这种限制取消了,因此可以使用long甚至long long类型的值。

2.枚举的取值范围

最初,对于枚举来说,只有声明指出的那些值是有效的。然而,C++通过强制类型转换,增加了可赋给枚举变量的合法值。每个枚举都有取值范围,通过强制类型转换,可以将取值范围内的任意整数值赋给枚举变量,即使这个值不是枚举值。

取值范围的定义:

首先,要找出上限,需要知道枚举量的最大值,找到大于这个最大值的、最小的2的幂,将它减去1,得到的便是取值范围的上限;比如,某个枚举的枚举量最大值为100,则大于100的最小2的幂为128,则上限为127.其次,要计算下限,需要知道枚举量的最小值,如果它不小于0,则取值范围的下限为0;否则,采用与寻找上限方式相同的方法,但加上负号。比如,某个枚举的枚举量的最小值为-6,而比它小的、最大的2的幂为-8,则下限是-7.

选择用多少空间来存储枚举由编译器决定,对于取值范围较小的枚举,使用一个字节或者更小的空间;而对于包含long类型值的枚举,使用4个字节。

C++11拓展了枚举,增加了作用域内枚举。

七、指针和自由存储空间

计算机程序在存储数据时必须要有3种基本属性:

  • 信息存储在何处;
  • 存储的值为多少;
  • 存储的信息是什么类型;

一种实现策略是定义一个简单变量时,声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。

另一种策略以指针为基础,在开发C++类时非常重要。指针是一个变量,其存储的是值的地址,而不是值本身。而只需对变量应用地址运算符(&),就可以获得它的地址,例如,若a是一个变量,则&a是其地址。

#include <iostream> 
using namespace std;
 
int main()
{
	int a=1;
	double b=2.2;
	cout<<"a的值为:"<<a<<",其地址为"<<&a<<endl;
	cout<<"b的值为:"<<b<<",其地址为"<<&b<<endl;
	return 0;
}

显示地址时,该实现的cout使用十六进制表示法,因为这是常用于内存的表示法。不同的系统给定的地址值可能不同,有些系统先存储a,而有些系统先存储b,而还有些系统不会把两个变量存储在相邻的内存单元中。

指针与C++基本原理

面向对象编程与传统的过程性编程相比,OOP强调的是在运行阶段,而不是编译阶段进行决策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。

运行阶段决策提供了灵活性,可以根据当时的情况进行调整。

例如,考虑为数组分配内存的情况。传统的方法是声明一个数组,要在C++中声明数组,需要指定数组长度。因此,数组长度在程序编译时就确定好了,这就是编译阶段决策。有时程序需要处理20个元素的数组,有时还要处理200个元素的数组,为了安全起见,使用了一个包含200个元素的数组,这样程序在大多数情况下浪费了内存,OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活,在程序运行后,可以告诉它这次处理20个元素,下次处理200个元素。

也就是说,使用OOP时,可能在运行阶段确定数组长度,为使用这种方法,语言必须允许在程序运行时创建数组。C++采用的方式是使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存位置。

在运行阶段决策并非OOP独有的,但使用C++编写这样的代码比使用C语言简单。

使用常规变量时,值是指定的量,而地址是派生量。指针策略是C++内存管理编程理念的核心。处理存储的新策略——指针策略刚好相反,地址为指定量,而值是派生量。一种特殊类型的变量——指针用于存储值的地址。因此,指针名表示的是地址。*运算符被称为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值(这也是乘法符号,C++根据上下文来确定是乘法还是解除引用)。

例如,a是一个指针,则a表示的是一个地址,而*a表示存储在该地址处的值,其与常规int变量等效。

#include <iostream>
using namespace std;

int main()
{
	int a=6;
	int* pa;
	pa=&a;
	
	cout<<"值输出:a="<<a<<",*pa="<<*pa<<endl;
	cout<<"地址输出:&a="<<&a<<",pa="<<pa<<endl;
	*pa=*pa+1;
	cout<<"现在a="<<a;
    return 0;
}

从中可知,int变量a和指针变量pa是同一硬币的两面,变量a表示值,使用&来获取地址;而指针pa表示地址,使用*来获取值。由于pa指向p,所以*pa和a完全等价。可以像正常使用int变量一样使用*pa,也可以对它进行修改,结果可以反映在a上。

1.声明和初始化指针

计算机需要跟踪指针指向的值的类型。比如,char类型和double类型的地址看上去没有区别,但char和double使用的字节数是不同的,它们存储时使用的内部格式也是不同的,因此,指针声明必须指定指针指向的数据的类型。

例如,int* pa;

*pa的类型是int,由于*运算符被用于指针,因此pa变量本身必须是指针,并说pa指向int类型,还说pa的类型是指向int的指针,或int*。即pa是指针,而*pa是int。

*运算符两边的空格是可选的,传统上,C程序员使用这种格式:

int *pa;//这强调*pa是int类型。

而很多C++程序员使用这种格式:

int* pa;//这强调的是int*是一种类型,即指向int的指针。在C++中,int*就是一种复合类型,是指向int的指针。

在哪里添加空格对于编译器来说没有任何区别,甚至可以这样写:

int*pa;

但需要知道,int* a,b;该声明语句创建了一个指针(a)和一个int变量(b)。对于每一个指针变量名,都需要使用一个*。

double* b;

char* c;

将b声明为一个指向都double的指针,因此编译器知道*b是一个double值,即一个以浮点格式存储的值,这个值占据8个字节。指针变量不仅仅是指针,还是指向特定类型的指针。b是指向double类型的指针,而c是指向char类型的指针,尽管都是指针,但两者却是不同类型的指针。和数组一样,指针都是基于其他类型的。

虽然两者指向两种长度不同的数据类型,但两个变量本身的长度通常是相同的,即double类型的地址和char类型的地址的长度相同。地址的长度和值既不能指示关于变量的长度或类型的任何信息,也不能指示该地址上有什么建筑物。一般来说,地址需要2个还是4个字节,取决于计算机系统。有些系统可能需要更大的地址,系统可以针对不同的类型使用不同长度的地址。

可以在声明语句中初始化指针,在这种情况下,被初始化的是指针,而不是它指向的值。

#include <iostream>
using namespace std;

int main()
{
	int a=1;
	int* pa=&a;
	
	cout<<"a的值为"<<a<<",地址为"<<&a<<endl;
	cout<<"*pa的值为"<<*pa<<",地址为"<<pa<<endl; 
    return 0;
}

2.指针的危险

如果使用指针不仔细,可能会有危险。在C++创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存,为数据提供空间是一个独立的步骤。

int* a;

*a=1;

a是一个指针,但没有将地址赋给a。由于a没有被初始化,它的值是不确定的,而这个值将被解释为存储1的地址。a指向的地方很可能并不是所要存储1的地方,这种错误很可能会导致一些最隐匿、最难以跟踪的bug。

也就是说,一定要在指针解除引用之前,将指针初始化为一个确定的、适当的地址。

3.指针和数字

指针不是整型,虽然计算机通常把地址当作整数来处理。从概念上看,指针和整数是两种完全不同的类型,整数是可以加减乘除的数字,而指针描述的是位置,将两个地址相乘没有任何意义。从可以对整数和指针执行的操作来看,它们也是不同的。因此,不能简单地将整数赋值给指针:

int* a;
a=0xB8000000;

左边是指向int类型的指针,因此可以把地址赋值给它,但右边是一个整数。0xB8000000是老式计算机系统中视频内存的组合段偏移地址,但这条语句并没有告诉程序,这个数字就是一个地址。在C99标准发布之前,C语言允许这样赋值。但C++在类型一致的方面的要求更加严格,编译器将显示一条错误消息,通告类型不匹配。要将数字值作为地址使用,应通过强制类型转换将数字转换为适当的地址类型:

int* a;
a=(int *)0xB8000000;

这样,赋值语句的两边都是整数的地址,因此这样赋值有效。需要注意,a是int值的地址并不意味着a本身的类型是int。例如,在有些平台中,int类型是个2字节值,而地址是个4字节值。

4.使用new来分配内存

前面我们都将指针初始化为变量的地址:变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C语言中,可以用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法——new运算符。

这里的关键所在是C++的new运算符,程序员要告诉new,需要为哪种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员需要将该地址赋给一个指针。

int* a=new int;

new int告诉程序,需要一个适合存储int类型的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这个内存,并返回其地址。接下来,将地址赋给a,而a是被声明为指向int的指针。现在,a是地址,而*a是存储在那里的值。将这种方法与之前将变量的地址赋给指针进行比较:

int b;

int* pb=&b;

两者都将一个int变量的地址赋给了指针。第二种情况下,可以使用名称b来访问这个int;第一种情况下,只能通过指针进行访问。这引出了一个问题,a指向的内存没有名称,我们如何称呼它呢?我们说a指向了一个数据对象,“数据对象”比“变量”更常用,且和“面向对象编程”的对象不同,它指的是为数据项分配的内存块。因此,变量是数据对象,而数据对象不是变量。看上去处理数据对象的指针方法可能不太好用,但它使程序在管理内存方面有了更大的控制权。

为一个数据对象(可以是结构体,也可以是基本类型)获得并指定分配内存的通用格式如下:

typeName * pointer_name=new typeName;

需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。如果已经声明了相应类型的指针,则可以使用该指针,而不用再声明一个新指针。

#include <iostream>
using namespace std;

int main()
{
	int a=1001;
	int* pa=new int;
	*pa=1001;
	cout<<"a的值为"<<a<<",内存地址为"<<&a<<endl;
	cout<<"int的值为"<<*pa<<",内存地址为"<<pa<<endl;
	double* pb=new double;
	*pb=10000001.0;
	cout<<"double的值为"<<*pb<<",内存地址为"<<pb<<endl; 
	cout<<"size of pa:"<<sizeof(pa)<<",size of *pa:"<<sizeof(*pa)<<endl;
	cout<<"size of pb:"<<sizeof(pb)<<",size of *pb:"<<sizeof(*pb)<<endl;
    return 0;
}

程序说明

该程序使用new分别为int类型和double类型的数据对象分配内存,且这是在程序运行时进行的。指针pa和pb指向这两个数据对象,通过这两个指针来访问它们对应的内存单元。可以像使用变量一样使用*pa和*pb,可以将值赋给它们,从而将这些值赋给新的数据对象,同样也可以打印出这些值。

该程序还指出了必须声明指针所指向的类型的原因之一,地址本身只指出了对象存储地址的开始,而没有指出其类型和其使用的字节数。从这两个值的地址可以知道,它们都只是数字,并没有提供类型和长度信息。另外,两个指针的长度相同,它们都是地址,但由于程序声明了指针的类型,因此程序知道*pb是8个字节的double值,*pa是4个字节的int值。程序打印*pb的值时,cout知道要读取多少字节以及如何解释它们。

对于指针,new分配的内存块与常规变量声明分配的内存块不同。变量a的值都存储在被称为栈的内存区域中,而new从被称为堆或自由存储区的内存区域分配内存。

计算机可能会由于没有足够的内存而无法满足new的请求。在这种情况下,new通常会引发异常;而在较老的实现中,new将返回0.在C++中,值为0的指针称为空指针。C++确保空指针不会指向有效的数据,因此它常被用来表示运算符或函数失败;如果成功,它们将返回一个有用的指针。C++提供了检验并处理内存分配失败的工具。

5.使用delete释放内存

当需要内存时,可以使用new来请求,这只是C++内存管理数据包中的一个方面;另一个方面是delete运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效使用内存的关键一步。归还或释放的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块的指针,这些内存块最初是用new分配的:

int* a=new int;

delete a;

这将释放a指向的内存,但不会删除a本身。例如,可以将a重新指向另一个新分配的内存块。一定要配对地使用new和delete,否则将发生内存泄漏,即被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。

不要尝试释放已经释放的内存块。C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete来释放声明变量所获得的内存:

int* a=new int;

delete a;

delete a;//不可以

int b=6;

int* pb=&b;

delete pb;//不可以,这个内存不是new分配的。

只能用delete来释放使用new分配的内存。然而,对空指针使用delete是安全的。使用delete的关键在于,将它用于new分配的内存。这并不意味着要使用用于new的指针,而是用于new的地址:

int* a=new int;

int* a2=a;//指向同一个内存块

delete a2;

一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。

6.使用new来创建动态数组

如果程序只需要一个值,可直接声明一个简单变量,因为对于管理一个小型的数据对象来说,这样做比new和指针更加简单。通常,对于大型数据(如数组、字符串和结构体),应使用new,这也是new的用武之地。比如,我们要编写一个程序,它是否需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则再程序被编译时将为它分配空间,无论程序是否需要数组,数组都占用了内存。在编译时给数组分配内存被称为静态联编,意味着数组是在编译时加入程序中的。但使用new时,如果在运行阶段需要数组,就创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编,意味着数组是在运行时创建的。这种数组叫动态数组,使用静态联编时,必须在编写程序时指定数组长度;使用动态联编时,程序将在运行时确定数组的长度。

(1)使用new创建动态数组

在C++中,想要创建动态数组,只要将数组的元素类型和元素数目告诉new即可。必须在类型名后加上方括号,其中包含元素数目。

int* a=new int[10];//创建了包含10个int的数组

new运算符返回第一个元素的地址。当程序使用完new分配的内存块时,应使用delete释放它们。然而,对于使用new创建的数组,应使用另一种格式的delete释放。

delete []a;

方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。对于delete和指针之间的方括号,如果使用new时,不带方括号,则使用delete时,也不应带方括号;如果使用new时带方括号,则使用delete时也带方括号。C++的早期版本无法识别方括号表示法,然而,对于ANSI/ISO标准来说,new与delete的格式不匹配导致的后果是不确定的,这意味着程序员不能依赖于某种特定的行为。

总之,使用new和delete时,应遵守以下规则:

  1. 不要使用delete来释放不是new分配的内存;
  2. 不要使用delete来释放同一个内存块两次;
  3. 如果使用new []为数组分配内存,则应使用delete []来释放;
  4. 如果使用new为一个实体分配内存,则应使用delete(没有方括号)来释放;
  5. 对空指针使用delete是安全的。

a指向一个整数数组的第一个元素。同时,程序员需要负责跟踪这个数组中元素的数量。由于编译器无法自动追踪a指向的数组中有多少个元素,因此在编写程序时,需要手动追踪数组中元素的数量。这意味着在使用a指针时,程序员需要确保在其指向的数组中正确地跟踪元素的数量,以免发生错误或越界访问。

实际上,程序确实跟踪了分配的内存量,以便以后使用delete []运算符时能够正确地释放这些内存,但这些信息不是公用的,例如,不能使用sizeof运算符来确定动态分配的数组包含的字节数。

为数组分配内存的通用格式为:

typename* pointername=new typename [element];

使用new运算符可以确保内存块足以存储element个类型为typename的元素,而 pointername将指向第1个元素。

(2)使用动态数组

int* a=new int[10];

假设int占4个字节,则将指针沿正确的方向移动4个字节,指针将指向第2个元素。总共有10个元素,这就是指针的移动范围。因此,new语句提供了识别内存块中每个元素所需的全部信息。

由于a指向数组的第1个元素,因此*a是第1个元素的值。还有9个元素,只要把指针当作数组名使用即可。也就是说,对于第1个元素,可以使用a[0],而不是*a;对于第2个元素,可以使用a[1],依此类推。可以这样做的原因是,C和C++内部都使用指针来处理数组。数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题)。

#include <iostream>
using namespace std;

int main()
{
	int* a=new int[3];
	a[0]=1;
	a[1]=2;
	a[2]=3;
	cout<<"a[0]是"<<a[0]<<",a[1]是"<<a[1]<<endl;
	a=a+1;
	cout<<"现在,a[0]是"<<a[0]<<",a[1]是"<<a[1]<<endl;
	a=a-1;
	delete[] a;
    return 0;
}

从中可知,该程序将指针a当作数组名来使用,a[0]为第1个元素,依次类推。下面的代码行
指出了数组名和指针之间的根本差别:

a=a+1;//数组名无法这样操作

不能修改数组名的值。但指针是变量,因此可以修改它的值。将a加1,表达式a[0]现在指的是数组的第2个值。因此,将a加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给delete[]提供正确的地址。
相邻的int地址通常相差2个字节或4个字节,而将a加1后,它将指向下一个元素的地址,这表明
指针算术有一些特别的地方。

八、指针、数组和指针算术

指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。

#include <iostream>
using namespace std;

int main()
{
	double d[3]={1.1,2.2,3.3};
	short s[3]={1,2,3};
	
	double* pd=d;
	short* ps=&s[0];
	cout<<"pd = "<<pd<<" , *pd = "<<*pd<<endl;
	pd=pd+1;
	cout<<"加1后 pd = "<<pd<<" , *pd = "<<*pd<<endl;
	cout<<"ps = "<<ps<<",*ps = "<<*ps<<endl;
	ps=ps+1;
	cout<<"加1后 ps = "<<ps<<",*ps = "<<*ps<<endl;
	
	cout<<"s[0] = "<<s[0]<<",s[1] = "<<s[1]<<endl;
	cout<<"*s = "<<*s<<",*(s+1) = "<<*(s+1)<<endl;
	
	cout<<sizeof(d)<<"为d数组的长度"<<endl;
	cout<<sizeof(pd)<<"为pd指针的大小"<<endl;
    return 0;
}

1.程序说明

在多数情况下,C++将数组名解释为数组第1个元素的地址。因此,下面的语句将pd声明为指向double类型的指针,然后将它初始化为d,d数组中第1个元素的地址:

double* pd=d;

和所有数组一样,d也存在下面的等式:

d = &d[0] = 第一个元素的地址

该程序在表达式&s[0]中显式地使用地址运算符来将ps指针初始化为s数组的第1个元素。

C++编译器将s[1]看作是*(s+1),这意味着先计算数组第2个元素的地址,然后找到存储在那里的值。最后的结果便是s[1]的含义(运算符优先级要求使用括号,如果不使用括号,将给*s加1,而不是给s加1)。从该程序的输出可知,*(s+1)和s[1]是等价的。同样,*(s+2)和s[2]也是等价的。通常,使用数组表示法时,C++都执行下面的转换:

arrayname[i]等价于*(arrayname+1);

如果使用的是指针,而不是数组名,则C++也将执行同样的转换:

pointername[i]等价于*(pointername+1);

因此,在很多情况下,可以相同的方式使用指针名和数组名。对于它们,可以使用数组方括号表示法,也可以使用解除引用运算符(*)。在多数表达式中,它们都表示地址。区别之一是,可以修改指针的值;而数组名是常量,不能修改。

另一个区别是,对数组应用sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。这种情况下,C++不会将数组名解释为地址。

数组的地址

对数组取地址时,数组名也不会被解释为其地址。数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。

short s[10];

cout<<s<<endl;

cout<<&s<<endl;

从数字上说,这两个地址相同;但从概念上说,&s[0](即s)是一个2字节内存块的地址,而&s是一个20字节内存块的地址。因此,表达式s+1将地址值加2,而表达式&s+1将地址加20。换句话说,s是一个short指针(*short),而&s是一个指向包含20个元素的short数组(short(*)[20])的指针。

首先,可以这样声明和初始化这种指针:

short (*ps) [20] = &s;

如果省略括号,优先级规则将使得ps先与[20]结合,导致ps是一个short指针数组,它包含20个元素,因此括号是必不可少的。

其次,如果要描述变量的类型,可将声明中的变量名删除。因此,ps的类型为short (*)[20]。另外,由于ps被设置为&s,因此*ps与s等价,所以(*ps)[0]为s数组的第一个元素。

总之,使用new来创建数组以及使用指针来访问不同的元素很简单。只要把指针当作数组名对待即可。

2.指针小结

(1)声明指针

要声明指向特定类型的指针

typename* pointername;

(2)给指针赋值

应将内存地址赋给指针。可以对变量名应用&运算符,来获得被命名的内存的地址,new 运算符返回未命名的内存的地址。

(3)对指针解引用

对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符(*)来解除引用。另一种对指针解除引用的方法是使用数组表示法,例如,s[0]与*s是一样的。决不要对未被初始化为适当地址的指针解除引用。

(4)区分指针和指针所指向的值

如果s是指向int的指针,则*s不是指向int的指针,而是完全等同于一个int类型的变量,s才是指针。

(5)数组名

在多数情况下,C++将数组名视为数组的第一个元素的地址。一种例外情况是,将sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节)。

(6)指针算术

C++允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象占用的总字节数。还可以将一个指针减去另一个指针,获得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数组(也可以指向超出结尾的一个位置)时,这种运算才有意义,这将得到两个元素的间隔。

(7)数组的动态联编和静态联编

使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置;使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应使用delete[]释放其占用的内存。

(8)数组表示法和指针表示法

使用方括号数组表示法等同于对指针解除引用;数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。

3.指针和字符串

数组和指针的特殊关系可以扩展到C-风格字符串。

char man[10]="Kawin";

cout<<man<<"is a boy."<<endl;

数组名是第一个元素的地址,因此cout语句中的man是包含字符K的char元素的地址。cout对象认
为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。总之,如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。

这里的关键不在于man是数组名,而在于man是一个char的地址。这意味着可以将指向char的
指针变量作为cout的参数,因为它也是char的地址。

为了与cout对字符串输出的处理保持一致,这个用引号括起的字符串也应当是一个地址。在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。上述代码不会将整个字符串发送给cout,而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符电常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址

#include <iostream>
#include <cstring> 
using namespace std;

int main()
{
	char animal[20]="bear";
	const char* bird="wren";
	char* p;
	
	cout<<animal<<" and "<<bird<<"\n";
	cout<<"Enter a kind of animal:";
	cin>>animal;
	p=animal;
	cout<<p<<endl;
	cout<<"Before using strcpy():"<<endl;
	cout<<animal<<" at "<<(int *)animal<<endl;
	cout<<p<<" at "<<(int *)p<<endl;
	
	p=new char [strlen(animal)+1];
	strcpy(p,animal);
	cout<<"After using strcpy():"<<endl;
	cout<<animal<<" at "<<(int *)animal<<endl;
	cout<<p<<" at "<<(int *)p<<endl;
	delete[] p;
    return 0;
}

程序说明

程序创建了一个char数组和两个指向char的指针变量。该程序首先将animal数组初始化为字符串“bear”,就像初始化数组一样。然后,程序将char指针初始化为指向一个字符串。“wren”实际表示的是字符串的地址,因此这条语句将“wren”的地址赋给了bird指针。一般来说,编译器在内存留出一些空间,以存储程序源代码中所有用引号括起的字符串,并将每个被存储的字符串与其地址关联起来。这意味着可以像使用字符串“wren”那样使用指针bird,比如使用cout进行输出。

字符串字面值是常量,以这种方式使用const意味着可以用bird来访问字符串,但不能修改它。最后,指针p未被初始化,因此不指向任何字符串。对于cout来说,使用数组名animal和指针bird是一样的。毕竟,它们都是字符串的地址,cout将显示存储在这两个地址上的两个字符串。创建未初始化的指针,将可能显示一个空行、一堆乱码或者程序将崩溃,无法控制它将被如何使用。

对于输入,情况有点不同。只要输入比较短,能够被存储在数组中,则使用数组animal进行输入将是安全的。然而,使用bird来进行输入并不合适:

  • 有些编译器将字符串字面值视为只读常量,如果试图修改它们,将导致运行阶段错误。在C++中,字符串字面值都将被视为常量,但并不是所有的编译器都对以前的行为做了这样的修改。
  • 有些编译器只使用字符串字面值的一个副本来表示程序中所有的该字面值。

C++不能保证字符串字面值被唯一地存储。也就是说,如果在程序中多次使用了字符串字面值“wren”,则编译器将可能存储该字符串的多个副本,也可能只存储一个副本。如果是后面一种情况,则将bird设置为指向一个“wren”,将使它只是指向该字符串的唯一一个副本。将值读入一个字符串可能会影响被认为是独立的、位于其他地方的字符串。无论如何,由于bird指针被声明为const,因此编译器将禁止改变bird指向的位置中的内容。

由于指针没有被初始化,因此并不知道信息将被存储在哪里,这甚至可能改写内存中的信息。不要使用字符串常量或未被初始化的指针来接收输入。为避免这些问题,也以使用std::string对象,而不是数组。在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。

一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为char*,则cout将显示指
向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int*。将animal赋给p并不会复制字符串,而只是复制地址。这样,这两个指针将指向相同的内存单元和字符串。

要获得字符串的副本,还需要做其他工作。首先,需要分配内存来存储该字符串,这可以通过声明另一个数组或使用new来完成。后一种方法使得能够根据字符串的长度来指定所需的空间:

p=new char [strlen(animal)+1];

由于字符串不能填满整个数组,浪费了空间。上述代码使用strlen()来确定字符串的长度,并将它加1来获得包含空字符时该字符串的长度。随后,程序使用new来分配刚好足够存储该字符串的空间。

接下来,需要将animal数组中的字符串复制到新分配的空间中。将animal赋给p是不行的,因为这样只能修改存储在p中的地址,从而失去程序访问新分配内存的唯一途径。需要使用库函数strcpy()。strcpy()函数接受2个参数,第一个是目标地址,第二个是要复制的字符串的地址。我们需要分配目标空间,并有足够的空间来存储副本。在这里,我们用strlen()来确定所需的空间,并使用new获得可用的内存。

经常需要将字符串放到数组中。初始化数组时,使用=运算符;否则应使用strcpy()或strncpy()。

strcpy()工作原理为

char food[6]="meat";

strcpy(food,"egg");

如果数组比字符串小,代码可能导致问题。在这种情况下,函数将字符串中剩余的部分复制到数组后面的内存字节中,这可能会覆盖程序正在使用的其他内存。要避免这种问题,请使用strncpy()。该函数还接受第3个参数,即要复制的最大字符数。然而,要注意的是,如果该函数在到达字符串结尾之前,目标内存已经用完,则它将不会添加空字符。

因此,应该这样使用该函数

strncpy(food,"12223333",5);//留一个位置

food[5]='\0';

这样最多将5个字符复制到数组中,然后将最后一个元素设置成空字符。如果该字符串少于5个字
符,则strncpy()将在复制完该字符串之后加上空字符,以标记该字符串的结尾。应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组。

使用C++string类型,不用担心字符串会导致数组越界,并可以使用赋值运算符而不是函数strcpy()和strncpy()。

4.使用new创建动态结构体

在运行时创建数组优于在编译时创建数组,对于结构体也是如此。需要在程序运行时为结构体分配所需的空间,这也可以使用new运算符来完成。通过使用new,可以创建动态结构体。“动态”意味着内存是在运行时,而不是编译时分配的。

将new用于结构体由两步组成:创建结构体和访问其成员。要创建结构体,需要同时使用结构体类型和new。

abcd* p=new abcd;

这将把足以存储abcd结构体的一块可用内存的地址赋给p。这种句法和C++的内置类型完全相同。

创建动态结构体时,不能将成员运算符句点用于结构体名,因为这种结构体没有名称,只是知道它的地址。C++专门为这种情况提供了一个运算符:箭头成员运算符(->)。该运算符由连字符和大于号组成,可用于指向结构体的指针,就像点运算符可用于结构名一样。例如,如果p指向一个
abcd结构体,则p->price是被指向的结构体的price成员。

如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构体的指针,则使用箭头运算符。另一种访问结构体成员的方法是,如果p是指向结构体的指针,则*p就是被指向的值——结构体本身。由于*p是一个结构体,因此(*p).price是该结构体的price成员。C++的运算符优先规则要求使用括号。

#include <iostream>
using namespace std;

struct inflatable
{
	char name[20];
	float volume;
	double price;
 } ;
int main()
{
	inflatable* p=new inflatable;
	cout<<"Enter name of inflatable item:"<<endl;
	cin.get(p->name,20);
	cout<<"Enter volume in cubic feet:"<<endl;
	cin>>(*p).volume;
	cout<<"Enter price: $";
	cin>>p->price;
	cout<<"name:"<<(*p).name<<endl;
	cout<<"volume:"<<p->volume<<endl;
	cout<<"price: $"<<p->price<<endl;
	delete p;
	return 0;
	
}

使用 cin.get(p->name,20)读取输入到 name 成员变量时,cin.get() 会将输入缓冲区中的换行符留在缓冲区中。这意味着接下来的 cin>>(*p).volume 读取操作可能会受到之前的换行符的影响。

然而,在这种情况下,由于 (*p).volume 是一个 float 类型,而不是字符数组,因此它会忽略之前的换行符并继续读取输入。对于 cin>>(*p).volume 这样的输入操作,C++ 会跳过之前的换行符,并尝试读取有效的输入作为浮点数值。

虽然 cin.get() 在读取字符数组时可能会留下换行符,但对于后续的非字符输入操作,C++ 输入流会尽力处理这些情况,以确保正确地读取输入数据。

(1)一个使用new和delete的示例

#include <iostream>
#include <cstring>
using namespace std;
char* getname(void);

int main()
{
	char* name;
	
	name=getname();
	cout<<name<<" at "<<(int*) name<<"\n";
	delete[] name;
	
	name=getname();
	cout<<name<<" at "<<(int*) name<<"\n";
	delete[] name;
	return 0;
}

char* getname()
{
	char temp[80];
	cout<<"Enter last name: ";
	cin>>temp;
	char* p=new char[strlen(temp)+1];
	strcpy(p,temp);
	return p;
}

程序定义了一个函数getname(),该函数返回一个指向输入字符串的指针。该函数将输入读入到一个大型的临时数组中,然后使用new[]创建一个刚好能够存储该输入字符串的内存块,并返回一个指向该内存块的指针。对于读取大量字符串的程序,这种方法可以节省大量内存(实际编写程序时,使用string类将更容易,因为这样可以使用内置的new和delete)。

(2)程序说明

函数getname()使用cin将输入的单词放到temp数组中,然后使用new分配新内存,以存储该单词。程序需要strle(temp)+1个字符(包括空字符)来存储该字符串,因此将这个值提供给new。获得空间后,getname()使用标准库函数strcpy()将temp中的字符串复制到新的内存块中。该函数并不检查内存块是否能够容纳字符串,但getname()通过使用new请求合适的字节数来完成了这样的工作。最后,函数返回p,这是字符串副本的地址。

在 main()中,返回值(地址)被赋给指针 name。该指针是在main()中定义的,但它指向 getname()
函数中分配的内存块。然后,程序打印该字符串及其地址。接下来,在释放name指向的内存块后,main()再次调用getname()。C++不保证新释放的内存就是下一次使用new时选择的内存。

在这个程序中,getname()分配内存,而main()释放内存。将new和delete放在不同的函数中通常并不好,因为这样很容易忘记使用delete。不过把new和delete分开放置也是可以的。

5.自动存储、静态存储和动态存储

根据用于分配内存的方法,C++有3种管理数据内存的方式:自动存储、静态存储和动态存储(有时
也叫作自由存储空间或堆)。在存在时间的长短方面,以这3种方式分配的数据对象各不相同。C++11新增了第四种类型——线程存储。

(1)自动存储

在函数内部定义的常规变量使用自动存储空间,被称为自动变量,这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。实际上,自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。如果在其中的某个代码块定义了一个变量,则该变量仅在程序执行该代码块中的代码时存在。

自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出。因此,在程序执行过程中,栈将不断地增大和缩小。

(2)静态存储

静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用关键字 static。

在K&R C中,只能初始化静态数组和静态结构体,而C++Release2.0(及后续版本)和ANSI C中,也可以初始化自动数组和自动结构体。然而,有些C++实现还不支持对自动数组和自动结构体的初始化。

自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)。

(3)动态存储

new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被称为自由存储空间或堆。该内存池同用于静态变量和自动变量的内存是分开的。程序表明,new和delete让我们能够在一个函数中分配内存,而在另一个函数中释放它。因此,数据的生命周期不完全受程序或函数的生存时间控制。与使用常规变量相比,使用new和delete让程序员对程序如何使用内存有更大的控制权。然而,内存管理也更复杂了。在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。

栈、堆和内存泄漏

指针是功能最强大的C++工具之一,但也最危险,因为它们允许执行对计算机不友好的操作,如使用未经初始化的指针来访问内存或者试图释放同一个内存块两次。

如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构体也将继续存在,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将在程序的整个生命周期内都不可使用,这些内存被分配出去,但无法收回。

极端情况是,内存泄漏可能会非常严重,以致于应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃。另外,这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响,导致它们崩溃。

要避免内存泄漏,要同时使用new和dclete运算符,在自由存储空间上动态分配内存,随后便释放它。C++智能指针可以自动完成这种任务。

九、类型组合

可以使用各种方式将数组、结构体和指针组合起来

struct abcd

{

        int a;

};

abcd s1,s2,s3;

s1.a=1;//使用成员运算符访问成员

abcd* p=&s2;//声明指针

p->a=2;//使用间接成员运算符访问成员

abcd h[3];//结构体数组

h[0].a=3;//成员运算符

(h+1)->a=4;//间接成员运算符

cosnt abcd* pa[3]={&s1.&s2.&s3};//指针数组,每一个元素是指针

cout<<pa[1]->a<<endl;

const abcd** ppa=pa;

其中pa是一个数组的名称,因此它是第一个元素的地址。但其第一个元素为指针,因此pa是一个指针,指向一个指向const abcd的指针。这种声明很容易出错,例如,可能遗漏const,忘记*,搞错顺序或结构体类型。编译器知道arp的类型,所以可以使用auto正确地推断出pb的类型:

auto pb=pa;

在以前,编译器利用它推断的类型来指出声明错误,而现在,可利用它的这种推断能力。

由于ppa是一个指向结构指针的指针,因此*ppa是一个结构体指针,可将间接成员运算符应用于它。

cout<<(*ppa)->a<<end;

cout<<(*(pb+1))->a<<endl;

由于ppa指向pa的第一个元素,因此*ppa为第一个元素,即&s1。所以,(*ppa)->a为s1的a成员。在第二条语句中,pb+1 指向下一个元素pa[1],即&s2。其中的括号必不可少,这样才能正确地结合。例如,*ppa->a试图将运算符*应用于ppa->a,这将导致错误,因为成员a不是指针。

#include <iostream>
using namespace std;

struct abcd
{
	int a;
};

int main()
{
	abcd s1,s2,s3;
	s1.a=1;
	abcd* p=&s2;
	p->a=2;
	abcd h[3];
	h[0].a=3;
	cout<<h->a<<endl;
	const abcd * pa[3]={&s1,&s2,&s3};
	cout<<pa[1]->a<<endl;
	const abcd ** ppa=pa;
	auto pb=pa;
	cout<<(*ppa)->a<<endl;
	cout<<(*(pb+1))->a<<endl;
	return 0; 
}

十、数组的替代品

1.模板类vector

模板类vector类似于string类,也是一种动态数组。您可以在运行阶段设置vector 对象的长度,可在末尾附加新数据,还可在中间插入新数据。基本上,它是使用new创建动态数组的替代品。实际上,vector类确实使用new和delete来管理内存,但这种工作是自动完成的。

要使用vector对象,必须包含头文件 vector。其次,vector 包含在名称空间 std 中,因此您可使用 using 编译指令、using 声明或std :: vector。第三,模板使用不同的语法来指出它存储的数据类型。第四,vector类使用不同的语法来指定元素数。

vector<int> vi;

int n;

cin>>n;

vector<double> vd(n);

其中,vi是一个vector<int>对象,vd是一个vector<double>对象。由于vector对象在您插入或添加值时自动调整长度,因此可以将vi的初始长度设置为零。但要调整长度,需要使用vector包中的各种方法。

一般而言,下面的声明创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素:

vector<typename> vt(n_elem);

其中参数n_elem可以是整型常量,也可以是整型变量。

2.模板类array(C++11)

vector 类的功能比数组强大,但付出的代价是效率稍低。如果需要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便和安全。C++11新增了模板类array,它也位于名称空间std中。与数组一样,array 对象的长度也是固定的,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同,但更方便、更安全。要创建array对象,需要包含头文件array。array 对象的创建语法与vector稍有不同:

array<int, 5> ai={1,2,3,4,5};

推而广之,下面的声明创建一个名为arr的array对象,它包含 n_elem个类型为 typename的元素:

array<typeName, n_elem> arr;//与创建 vector 对象不同的是,n_elem不能是变量。

在C++11中,可将列表初始化用于vector和array 对象,但在C++98中,不能对vector对象这样做。

3.比较数组、vector对象和array对象

#include <iostream>
#include <vector>
#include <array> 
using namespace std;

int main()
{
	double d[4]={1.1,2.2,3.3,4.4};
	vector<double> v(4);
	v[0]=1.0/3.0;
	v[1]=1.0/5.0;
	v[2]=1.0/7.0;
	v[3]=1.0/9.0;
	array<double,4> a1={3.14,2.72,1.62,1.41};
	array<double,4> a2;
	a2=a1;
	
	cout<<"d[2]: "<<d[2]<<" at "<<&d[2]<<endl;
	cout<<"v[2]: "<<v[2]<<" at "<<&v[2]<<endl;
	cout<<"a1[2]: "<<a1[2]<<" at "<<&a1[2]<<endl;
	cout<<"a2[2]: "<<a2[2]<<" at "<<&a2[2]<<endl;
	
	d[-2]=20.2;
	cout<<"d[-2]: "<<d[-2]<<" at "<<&d[-2]<<endl;
	cout<<"a1[2]: "<<a1[2]<<" at "<<&a1[2]<<endl;
	cout<<"a2[2]: "<<a2[2]<<" at "<<&a2[2]<<endl;
	return 0; 
}

程序说明

首先,注意到无论是数组、vector对象还是array对象,都可使用标准数组表示法来访问各个元素。其次,从地址可知,array对象和数组存储在相同的内存区域(即栈)中,而vector 对象存储在另一个区域(自由存储区或堆)中。第三,注意到可以将一个array对象赋给另一个array对象;而对于数组,必须逐元素复制数据。

d[-2]=20.2;

它将被转换为

*(d-2)=20.2;

找到d指向的地方,向前移两个double元素,并将20.2存储到目的地。也就是说,将信息存储到数组的外面。与C语言一样,C++也不检查这种超界错误。这表明数组的行为是不安全的。

vector和array对象能够禁止这种行为。也就是说,仍可编写不安全的代码;然而,还有其他选择。一种选择是使用成员函数at()。就像可以使用cin对象的成员函数getline()一样,可以使用vector和 array对象的成员函数at():

a.at(1)=2.3;

中括号表示法和成员函数at()的差别在于,使用at()时,将在运行期间捕获非法索引,而程序默认将中断。这种额外检查的代价是运行时间更长,这就是C++允许使用任何一种表示法的原因所在。另外,这些类还能够降低意外超界错误的概率。例如,它们包含成员函数begin()和end(),能够确定边界,以免无意间超界。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值