《C++ Primer Plus》第四章

第四章 复合类型

小目录

  • 创建并使用数组
  • 创建并使用C式字符串
  • 创建并使用string类字符串
  • 使用getline和get方法读入字符串
  • 混合输入字符与数字
  • 创建并使用结构(structure)
  • 创建并使用联合(union)
  • 创建并使用枚举(enumeration)
  • 创建并使用指针(pointer)
  • 使用new和delete动态管理内存
  • 创建动态数组
  • 创建动态结构
  • 自动,静态,和动态存储
  • vector和array类

数组介绍

声明数组需要三个要素:

  1. 每个元素的类型
  2. 数组的名字
  3. 数组的元素个数

格式:typename arrayname[arraysize];

举例:short months[12];

数组长度必须是一个整数常量,即数字或一个const值,又或者一个常量表达式比如8*sizeof(int).这个值必须是在这一句声明之前便已知的。特别地,数组长度不能是一个程序执行中赋值的变量。不过,本章迟些会介绍到如何使用new来突破这个限制。

数组中的每个元素可以被单独访问,方法是数组名加上下标。

格式:arrayname[subscripts]

举例:months[0]  //注意,数组下标从0开始

需要注意的是,编译器不会对超出范围的下标发出警告,比如months[111]。但对这个不存在的数组元素操作会导致数据或代码崩溃,导致不可知的结果。

数组初始化规则

C++有几条数组初始化的规则。

初始化形式只能用于声明语句,而不能在之后使用。同样,你不能将一个数组整个赋值给另一个:

int cards[4] = {3, 6, 8, 1};      //行

int hand[4];                           //行

hand[4] = {3, 6, 8, 1};           //不行

hand = cards;                       //不行

不过你可以使用下标对单个元素进行赋值。

在初始化时,允许提供少于数组长度的值,比如:

float height[5] = {1.0, 5.6};

这样编译器会把剩下的元素都初始化为0.所以要把数组所有元素初始化为0可以这样写:

long o[500] = {0};

注意,如果你把0改成1,只是0位是1,剩余依然是0。

如果你不写上数组长度,编译器会帮你数你传入的元素个数,比如:

int stuff[] = {1, 3, 5, 7};

编译器会把stuff长度设为4。

C++11数组初始化

C++11有几种新初始化格式。

  1. 省略等号:int a[4] {1, 2, 3, 4};
  2. 使用空的大括号将所有元素设为0:int a[4] {};
  3. 列表初始化会阻止变量类型缩窄:long p[] = {25, 36, 33.0};     //double→long不行

字符串

字符串就是在存储中连续地byte里存储的一系列字符。C++有两种字符串处理,一种是C式字符串,一种是string类。

在连续byte中存储的一系列字符这个概念让你可以把string存到char的数组里,每个字符一个元素。C式字符串有一个特性:最后一个字符是一个空字符(null character)。这个字符写作\0,ASCII码为0,专用于标记字符串的结尾。比如以下两个char数组:

char dog[] = {'d', 'a', 'v', 'i', 'd'};     //不是字符串

char cat[] = {'s', 't', 'e', 'v', 'e', '\0'};  //字符串

这两个都是char数组,但只有第二个是字符串。C++有很多处理字符串的方法,包括cout使用的。它们都是从字符串的头逐个字符处理直到读到空字符。如果你向这些方法传入一个没有空字符的char数组,方法会一直逐个byte往后读下去,直到在某个其他数据的存储区内读到空字符。

当然C++还有更好的字符串初始化方法,那就是使用被双引号括起来的字符串常量:

char pig[] = "sicca";

被双引号括起的字符串常量总是会隐式地包括空字符,所以不需要特地写出来。同样,多种C++输入工具在读入字符串时也会自动地在末尾加上空字符。

要注意让数组足够大以放下整个字符串,包括空字符,所以最好还是使用字符串常量进行初始化并让编译器帮你数。另外,让数组比字符串长除了浪费空间之外是无害的。因为字符串的长度取决于空字符的位置,和数组长度没有关系。

字符串常量连接

有时一个字符串太长了,一行写不下,C++允许将其拆成两个双引号括起的字符串常量并通过空白格(空格、缩进、回车)进行连接。也就是你可以这样写:

cout << "this string is too long "

"to be written in one line."

注意连接不会在两个字符串之间添加任何字符,第一个字符串的空字符会被第二个字符串的第一个字符替代。

在数组使用字符串

两种把字符串放入数组的方式,一是用字符串常量初始化数组,二是从文件或输入读入到数组。看下面的程序:

#include 
#include   //包含strlen方法
int main()
{
	using namespace std;
	const int Size = 15;
	char name1[Size];
	char name2[Size] = "walawala";
	
	cout << "I am " << name2 <<". And you are?\n";
	cin >> name1;
	cout << "Hello " << name1 << ". Your name is " << strlen(name1) << "characters long.\n";
	name2[3] = '\0';
	cout << "The first 3 characters of my name is:" << name2 << endl;
 } 

这段程序有几个点:

  1. 用const值Size初始化了两个char数组的长。
  2. 通过cin从键盘读入了字符串并存入了name1
  3. 通过将name2第4位(下标3)赋值为空字符,让cout只输出前三位

字符串输入

由于你没法通过键盘输入空字符,所以cin是靠空白格(空格、缩进,回车)来分割每一个字符串。这就意味着你无法从键盘输入空格。看以下的程序:

#include 
int main()
{
	using namespace std;
	char c1[15];
	char c2[15];
	
	cout << "enter first string.\n";
	cin >> c1;
	cout << "enter second string.\n";
	cin >> c2;
	cout << "c1:" << c1 << " c2:" << c2;
 } 

输出:

enter first string.
taylor fall
enter second string.
c1:taylor c2:fall

粗体的是输入。可以看到,一次输入被分成了两个字符串。第一次cin读了taylor,第二次读了fall。第一次读完taylor之后,fall放在了读入队列中。当程序输出了第二句后,再次调用cin读入,就从队列中读入了fall。

每次读入一行

如果需要一次输入一整行,包括其中的空格,就需要面向行的方法getline和get。这两个方法都会一直读到换行符,不同在于,getline读入结束后会将换行丢弃,而get会把换行符留在读入队列中。

getline()

调用方法:cin.getline(name, 20);

name是读入的字符串要存入的数组的名字,20是读入的字符串的长度上限。

get()

get方法的调用和getline相同,也是数组名字与上限。但要注意的是,上文提过,get方法不会丢弃它所遇到的换行符。也即是说,第一个get方法读入了一行后,读入队列的头位便是一个换行符,后续的get方法将全都会在这个换行符面前停下,无法读到任何字符。

这个情况的解决方法是使用没有参数的get方法:cin.get();这个方法会读入单个字符,即便是换行符也会读入。所以我们可以靠它来越过换行符:

cin.get(str1, 12);

cin.get();

cin.get(str2, 12);

而由于get和getline都会将调用它们的cin对象实例作为返回值返回,所以我们可以直接采用如下写法:

cin.get(str1, 12).get();

cin.getline(str1, 12).getline(str2, 11);

空行和其他问题

如果getline和get读入了空行会怎么样?现在实行的是,当get(不是getline)读入了一个空行,它会这个一个失败元(failbit),这会把之后的输入全部阻隔。要恢复输入就要使用以下命令:

cin.clear();

另一个潜在问题是输入的字符串可能长于分配的空间。如果输入的字符串比参数里的输入上限长,get和getline都会把多于的字符留在输入队列。但getline会增加一个失败元(failbit)阻隔后续输入。

字符串与数字混合输入

使用面向行的字符串读入方法进行数字和字符串混合读入可能会导致问题。比如:

int year;
cin >> year;
char name[4];
cin.getline(name, 4);

你先输入年份1996,然后回车,你会发现输入就结束了。year值为1996,而name则什么都没有。

这是因为输入1996后的回车在cin第一次读入1996这个整数后并没有作处理。所以在getline方法调用时,读入序列头位就是换行符。就和上文提到的get方法导致的情况一样,这次同样可以靠cin.get()来解决。

或者,你也可以采用连续的调用。>>操作符和小数点调用一样,也会返回cin对象实例,所以可以采用如下写法:

(cin >> year).get();


string类

先看一段程序:

#include 
#include  
int main()
{
	using namespace std;
	string str1;
	string str2 = "string2";
	
	cout << "enter str1:";
	cin >> str1;
	cout << "str2:" << str2 << endl;
	cout << "the third character of str2: " << str2[2];
 } 

从以上程序可以看出:

  • 可以用C式字符串来初始化string对象
  • 可以用cin将输入的字符串存入string对象
  • 可以用cout来输出string对象
  • 可以用数组下标来单独访问特定位置的字符

string类和字符数组最主要的区别就是,你可以将string类声明为一个简单的变量,而不是一个数组。

string类的设计允许它自动重定义大小。比如string str1将str1长度初始化为0.在用cin对其输入时,程序会自动将str1重定义为合适的长度。所以string类是更方便且安全的。

C++11的string初始化

C++11的大括号初始化同样适用于string类:

string str1 = {"wow."};
string str2 {"yeah!"};

赋值、连接和添加

string有很多方面都比字符数组方便。

首先,你可以直接将一个string赋值给另一个string:

string str1;
string str2 = "hey.";
str1 = str2;

string类还简化了字符串合并。你可以直接用加号把两个string合并,也可以用+=把一个数组加到另一个的后面:

string str3;
str3 = str1 + str2;
str1 += str2;

更多的string类操作

ctring头文件里其实也定义了用于操作字符数组的方法:

#include 
#include 
int main()
{
    using namespace std;
    char c1[10];
    char c2[10] = "aaaaaa";
    string str1;
    string str2 = "bbb";
    
    //将第二个赋值给第一个
    str1 = str2;
    strcpy(c1, c2);

    //将第二个加在第一个之后
    str1 += str2;
    strcat(c1, c2);

    //获得长度
    int len1 = str1.size();
    int len2 = strlen(c1);
}

但显然,string类的方法用起来要方便得多。并且,字符数组的操作也存在潜在的问题。比如strcat(c1, c2),把c2加在c1之后再存入c1.c1原本长度为6,c2长度也是6,二者相加长度为12.而c1的空间只有10,这个方法就会导致内存泄漏。strcpy等方法也有同样的问题。相对地,string类因为可以自动重定义自身长度,它的方法就不会出现这种问题。C的库其实也提供了strncat和strncpy这种安全的方法,但写起来就更麻烦了。

更多的string类I/O

先前提过面向行的读入方法cin.get和cin.getline。注意,这两个方法也可以用于string类但用法不完全相同:

cin.getline(c1, 20);   //读入到字符数组
getline(cin, str1);    //读入到string类

从代码可以看出,第二个getline并不是cin的成员方法,所以它需要cin对象作为参数来传入。同时参数里去掉了读入长度上限,因为string类的长度是可以重定义的。

为什么第一个getline是istream里的方法而第二个不是?因为在string类出现之前,istream就已经是C++的一部分了。所以istream只能处理C++的基本类型比如int,double之类的,而无法处理string。

但为什么cin>>str1这种写法又是可以的?因为这是调用了cin的方法,而cin转为调用了来自string类的友邻方法(friend function)。友邻方法这个概念将会在11章讲到。


结构(structure)介绍

结构(structure)是一个用户自定义类型,其结构声明定义了类型的数据属性。在定义类型后,你就可以创建一个这个类型的变量。所以创建一个结构需要两步。首先先写出结构的定义,然后创建一个结构变量。写法如下:

struct player
{
    char name[10];
    int point;
};

player david;

就像对象调用成员方法一样,结构变量通过小数点单独访问它的成员数据,比如david.name;

在程序中使用结构

看如下程序:

#include 

struct player 
{
	char name[10];
	int point;
 };
 
int main()
{
	using namespace std;
	player player1 = 
	{
		"david",  //name值
		300       //point值 
	};
	
	cout << "player1's name:" << player1.name << endl;
	cout << "player1's point:" << player1.point << endl;
 } 

以上程序写明了结构的定义以及结构变量的初始化格式,还有访问结构的成员数据的方法。

C++11结构初始化

出现了大括号初始化,你就懂了,C++11当然也是对它作了更改,让你可以省掉等号。还有你可以让大括号内为空,让编译器把结构内所有成员数据初始化为0:

player player2 {"jack", 500};
player player3 {};

当然还有,数据类型窄化是不行的。

其他结构属性

C++让用户自定义类型尽量和自有类型的相似。比如你可以将结构作为参数传给方法,也可以将其作为返回值。同样你也可以用等号将一个结构变量赋值给另一个同类型的结构变量。

你可以将结构定义和变量声明写到一起:

struct player
{
    char[] name;
    int point;
}player1, player2;

甚至可以直接把变量初始化写上:

struct player
{
    char[] name;
    int point;
}player1 =
{
    "david",
    100
}

不过分开写还是更清楚易读性更高。

另外你还可以定义一种没有名字的结构类型,就直接去除定义时的名字就好:

struct 
{
    char[] name;
    int point;
}player1;

这个结构变量叫player1,可以用来访问其成员数据。但这个结构类型是没有名字的,所以你无法再创建另一个这个结构类型的变量。

结构数组

player players[10]{
    {"david", 100},
    {"jack", 30}
};
cin >> players[2].name;
cout << players[4].point;

无需细说。

结构里的bit域

C++和C一样,允许你指定每个变量占据的bit数。这一般是为了创建与某些硬件注册信息相对应的数据结构。域的类型应为整形或枚举类型(枚举在本章稍后说到), 冒号之后跟着的数字就是要用的bit数。可以通过未命名的域名来创建空的区域。举例:

struct player
{
    int point : 4;  //point值用4个bit
    int : 4;        //4个bit的空区间
    bool alive : 1; //alive用1个bit
}

这种操作一般还是用于底层编程。


联合(unions)

联合是一种可以容纳多种数据类型但每次只能使用其中之一的数据格式。也即是说,比如一个结构可以容纳一个int 和 一个long 和 一个double,相对的一个联合可以容纳一个int 或 一个long 或 一个double。看如下代码:

union one4all
{
    int i;
    long l;
    double d;
}

one4all o;
o.i = 15;  //存入一个int
cout << o.i;
o.l = 13;  //存入一个long,之前的int丢失
cout << o.l;

因为联合每次只会通纳其中一种类型的值,所以它的大小只要只够容纳它最大的成员类型就行了。

联合的用处是当一个数据可能会用到多种类型但永远不会同时用到不同类型时,联合可以节省空间。比如玩家的id,有的id是数字,有的是字符串,这时就可以将id定义为一个联合:

struct player
{
    char name[10];   
    union id
    {
        long id_l;
        char id_c[20];
    } player_id;
};
...
player player1;
...
cout << player1.player_id.id_c;

匿名联合不需要名字,它的成员会变成共享一个地址的多个变量。当然,特定时刻这几个变量只存在其中之一。举例:

struct player
{
    union
    {
        long id_l;
        char id_c[10];
    };
};
...
player p1;
...
cout << p1.id_l;

因为联合是匿名的,所以id_l和id_c相当于是player的两个共享地址的成员变量。这样在访问时就不需要再写上联合的名字了。


枚举(enumeration)

C++enum是const之外的另一种创建符号常量的方式。看下面这一句:

enum weekday {monday, tuesday, wednesday, thursday, friday, saturday, sunday};

上面这句做了两件事:

  • 它将weekday定义为了一个新的类型;weekday称为一个枚举类型(enumeration)。
  • 它定义了monday到sunday共7个符号常量对应整数值0-7.这些常量称为enumerator。

默认情况下,enumerator的值从0开始往后一一对应。但你也可以给他们赋特定的整型值,这个迟些说。

你可以用枚举类型的名字来声明一个枚举变量:

weekday today;

不发生强转直接赋值给枚举变量的值只能是枚举类型定义中出现过的enumerator值。比如:

today = monday;  //可以,monday是一个enumerator
today = 100;     //不行,100不是一个enumerator

也就是,一个weekday变量的取值只有7种。有些编译器会在你试图给它赋无效值,而另一些则发个警告。为了最大的可移植性,这种无效赋值应被当做error来对待。

enumeration只定义了等号操作,加号等运算符是不适用的。

enumerator是整数类型,可以被升级为int类型,但int不能自动转为枚举类型:

int day = monday; //可以

day = monday + 2;//可以

weekay today = 1; //不行

你还可以用int来初始化一个枚举变量,但这个值要存在于这个枚举类型:

today = weekday(0);

设定enumerator值

enum month {jan, feb = 0, may = 5, jun};

上面这句有几个要点:

  • jan是默认取值,为0;
  • feb指定了取值为0,就是说enumerator的值可以相同
  • may指定了取值5,jun在其后,值是上一个值+1,所以jun = 6

只能给enumerator指定整数值。早先只能指定int类型值,但现在指定long和long long也行。

enumeration值范围

本来枚举变量取值只能是枚举类型中存在的值,但C++靠强转扩展了这个范围。现在每个枚举类型都有一个范围,在范围内的整型都可以赋值给枚举变量,即便enumerator中没有这个值。比如:

enum weekday {monday = 1, friday = 5};
weekday today;

范围定义:

  • 上界。先找到最大的enumerator值,然后找比这个值大的最小的二次幂,这个二次幂-1即是上界
  • 下界。先找到最小的enumerator值,如果这个值大于等于0,则下界为0.如果最小值是负数,则其下界确定方法和上界类似。(如最小值是-6,则比6大的最小的二次幂为8,这个二次幂-1为7.得到的下界为-7.)

指针(pointer)和解放内存

指针是一个存储了值的地址的变量。先看看如何找到一个变量的地址。那就是使用&操作符。看以下代码:

#include 
int main
{
    using namespace std;
    int a = 6;
    int b = 7;

    cout << "value:" << a;
    cout << "address:" << &a << endl;
    cout << "value:" << b;
    cout << "address:" << &b << endl;
}

输出:

value:6address:0x6ffe3c
value:7address:0x6ffe38

cout在输出地址的时候会特别地使用十六进制,因为存储地址通常都是使用十六进制的。可以看到变量b的存储地址比变量a小,二者的差为0x6ffe3c - 0x6ffe38 = 4。这是合理的因为a是int,一个int占4个byte。不同的系统,给出的地址是不一样的,甚至变量存储顺序都不一样。比如原书上是先存a后存b,而我的编译器上是先存b的。

指针和C++哲学

面向对象编程和传统面向过程编程不同在于,面型对象编程着重在运行过程中(runtime)进行选择,而非在编译时选择。运行时选择就像是在假期的时候,根据天气和心情选择要去的景点,而编译时选择则是事先排好时间表,而不管当时的环境。

运行时选择提供了一种适应当前环境的灵活度。比如,给一个数组分配空间。传统的方法是声明一个数组。在C++里声明数组,你必须要给出一个特定的大小。故而在编译完成时,数组的大小就是固定的了,这是编译时选择。也许你觉得20个元素的数组可以满足80%的情况了,但特定情况下会需要处理200个元素。为了安全,你就要声明一个200个元素的数组。这就会使得你的程序大大地浪费空间。面向对象编程会通过将选择延迟到运行时来让程序更灵活。在程序开始运行后,你就可以知道你究竟是需要20元素的数组还是200元素的了。

简单来说,通过面向对象编程你可以在运行的时候决定数组的大小。为了实现这个,这门语言允许你在运行时创建一个数组——或其他类似的对象。C++的方法同样,包含了使用关键词new来请求合适的存储空间和使用指针来定位新分配的存储的位置。

在运行时选择不是面向对象编程独有的。但C++使得编写代码比C更直接一些。

一种特别的变量类型——指针——诞生了,专用于存储值的地址。所以指针的名字就代表着地址。使用*操作符来访问地址内存储的值。(对的这个*就是乘号的那个*。C++会通过上下文来判断这是乘号还是指针操作符。)看以下程序:

#include 
int main()
{
    using namespace std;
    int a = 6;
    int * p_a = &a;
	
	cout << "a:" << a << endl;
	cout << "*p_a" << *p_a << endl;
	
	cout << "&a:" << &a << endl;
	cout << "p_a:" << p_a << endl;
	
	*p_a += 1;
	cout << "a:" << a << endl; 
}

输出:

a:6
*p_a6
&a:0x6ffe34
p_a:0x6ffe34
a:7

声明和初始化指针

上面的程序中的声明语句:

int * p_a;

这个说明了*p_a这个组合是一个int。而p_a自身则是一个指针。我们说法是p_a这个指针指向了一个int类型。p_a的类型是一个指向int的指针,或者更直接一些:int*。重复:p_a是一个指针,而*p_a是一个int。

在*两边的空格是可选的。传统上,C程序员会这么写:

int *p_a;

这个是顺应了*p_a是一个int这个概念。相对应地,许多C++程序员会这么写:

int* p_a;

这个则强调p_a是一个int*类型。甚至,你还可以把空格都去掉:

int*p_a;

不过注意的是,如下写法会创建一个指针p1和一个普通int类型p2:

int* p1,p2;

你需要给每个指针都加一个*。

还有一点是对于一个指向double的指针和一个指向char数组的指针,它们指向的类型的大小是不同的,但它们本身大小是一样的。因为它们是存储地址的,而地址的大小都是一样的。就好像一栋别墅的门牌号是四位的,一栋办公楼的门牌号同样也是四位的。所以指针的大小并不会告诉你它指向的是什么类型。一般来说一个指针大小在2到4个byte之间,取决于你的系统。

指针危险

随意地使用指针会导致危险。最最重要的一点是如果你在C++中创建一个指针,计算机会分配内存来存储一个地址,但它不会分配内存来存储存了这个地址的地址。为数据申请空间需要一个步骤,而如果省略了这个步骤,比如以下代码,就会导致危险:

long * fellow;
*fellow = 2233;

fellow是一个指针,但它指向哪里?这段代码没有写明fellow指向的地址,那2233要存到哪里去?这就会导致计算机产生不可预料的结果。

指针和数字

指针不是整数类型,尽管计算机一般地址地址当成整数来处理。概念上,指针是区别于整数的一个类型。数字是你可以用来运算的,但地址不是。把两个地址加起来或乘起来是不知所谓的举动。同样,概念上,你不能向指针赋值一个数值:

int * pt = 0xb8000000; //不行,类型不符合

你可能知道这个0xb8000000是你系统里面的某个地址,但程序并不能识别出来这是一个地址。如果你想要用数值来给指针赋值,你就要使用到强转:

int * pt = (int8)0xb8000000;

使用new来分配内存

到目前,我们都是用一个变量的地址来初始化指针。指针看起来仅仅是访问变量地址的第二种方式。但实际上,真正的指针应该是你在运行中分配未命名空间用于容纳值时用到的。在这个情况下,指针就成了唯一可以定位那块空间的手段。在C语言里,你可以用malloc来分配空间,现在C++也可以,但C++还有更好的方法:用new操作符。

int * pt = new int;

new int这个部分说明了你想要一块内存来容纳一个int。new操作符用类型来得出需要多少的byte。然后它会找到一块空间并将其返回。然后,你就可以将这个返回的空间赋值给指针pt。

用delete解放内存

用new申请内存只是C++内存管理包的一半,另一半则是delete操作符,用于将申请的内存在使用结束后归还给内存池(memory pool)。这是高效使用内存非常关键的一步。范例如下:

int* pt = new int;
...
delete pt;

delete会将pt指向的内存块擦除,但并不会删除pt本身。你可以再给pt分配一块新的内存块。要注意平衡new和delete的数量,否则就可能会发生内存泄漏(memory leak)。已分配的内存可能无法被使用,导致程序不断申请新的内存空间直到崩溃。

同时你也不能解放一块已解放的内存空间。其结果是未定义的。

用new创建动态数组

int * ps = new int[10];

delete [] ps;

上面两句是用new创建一个动态数组然后将其删除。new操作符会返回数组第一个元素的地址,赋值给你定义的指针。delete和指针名之间的中括号意思是不要只擦除指针指向的地址,而是将整个数组都擦除。

以下是几条使用new和delete的规则:

  • 不要用delete解放还未分配的内存
  • 不要用delete解放同一个内存块两次
  • 要用delete[]如果你是用的new[],如果没用中括号new就不要中括号delete
  • 对空指针使用delete是安全的,什么都不会发生

现在回到动态数组。注意ps指针是指向数组首位元素,也即是一个普通int,而非整个数组。所以记录数组长度需要自己动手。虽然实际上程序有记录数组分配到的空间以用于删除用,但这对我们是不可用的。我们无法通过sizeof来获得数组长度。

动态数组使用

#include 
int main()
{
    using namespace std;
    double * p3 = new double[3];
    p3[0] = 0.1;
    p3[1] = 0.2;
    p3[2] = 0.3;
    
    cout << "p3[0]:" << p3[0] << endl;
    p3 += 1;
    cout << "p3[0]:" << p3[0] << endl;
    p3 -= 1;
    delete [] p3;
}

输出:

p3[0]:0.1
p3[0]:0.2

注意看,p3指针指向了new出来的数组第一位,和普通数组一样,用下标可以单独访问元素。但这个下标代表的是从p3指针指向的元素数起的元素。当p3加一后,p3指向了数组第二位,这时的p3[0]就会访问到原本的p3[1]。最后删除数组时,删除也是从p3指向的位置开始,所以在delete之前要把p3指向的地址移回原处。

另外注意的是加一这个运算可以用在指针身上因为它是个变量,但这是不能用于一个数组的名称的。


指针,数组和指针运算

指针和数组名称的近似等价源于指针算法以及C ++如何在内部处理数组。先来看看算法。给整数变量加一就是让其值加一,但给指针加一则是让其值加它所指向的类型用到的byte的数量。给指向double的指针加一就是让其值加8(假设该系统内一个double是8bytes)。

大部分情况下,C++将数组的名字理解为其首位元素的地址。所以下列代码:

double wages[3];
double * pw = wages;

让指针pw指向了数组wages的首位。也即是我们有:wages = &wages[0] = 数组首位元素地址。

对于指针pw,pw[0]对系统来说和*(pw + 1)是完全一样的。第二种写法的意思是计算出需要访问位置的地址,然后读取该地址的元素。也就是说,系统会把所有的 数组名[下标] 转为 *(数组名 + 下标)。指针名也同样。所以某些方面来说,你可以把指针名和数组名当成同样的东西来用。但有一个不同的是,指针值可以变,但数组是一个常量:

pointername = pointername + 1;  //行
arrayname = arrayname + 1;      //不行

第二个不同是,对数组用sizeof会返回数组的长度,但对指针用sizeof则会返回指针的大小。

指针用法总结

  • 声明:typeName * pointerName;
  • 赋值:pointerName = &varName(new typeName);
  • 访问:*pointerName;
  • 数组:typeName arrayName[arrayLength];  (arrayName = &arrayName[0])
  • 指针算法:pointerName += integer;
  • 数组的动态与静态绑定:
    typeName arrayName[size];                       //静态
    typeName * pointerName = new typeName[size];    //动态
    delete [] pointerName;                          //归还内存
  • 数组下标和指针下标:arrayName[notation] = *(arrayName + notation)    pointerName[notation] = *(pointerName + notation)

指针和字符串

数组和指针的特殊关系扩展到了C式字符串。看如下程序:

char pet[10] = "cat";
cout << pet << endl;

数组名代表了其首位的元素地址,所以cout语句中的pet是存有字符'c'的元素地址。cout对象假定这个char是一个字符串的开始,cout将会从该字符开始一直向后逐位输出直到遇到字符串的结束——空字符\0.

这当中关键在于pet不是一个数组名而是作为一个字符的地址。这意味着你可以向cout输入一个指向char的指针,因为它也是一个char的地址。

使用new创建动态结构

struct s
{
    int a;
    int b;
};

s * ps = new s;
cin >> ps->a;
cin >> (*ps).b;
delete ps;

上面程序中涉及了两种通过结构指针访问结构内元素的方法。一是通过指针名和箭头(->)操作符访问。二是*和指针名组合,代表指针指向的结构变量,然后通过小数点操作符访问成员元素。

自动存储,静态存储,动态存储

自动存储:

正常在方法里定义的变量就是自动存储,称为自动变量(automatic variables)。这意为这些变量会在包含它们的方法被调用时自动出现,然后在方法结束时被丢弃。实际上,自动变量属于区块(block)自有。区块就是大括号之间的代码称为一个区块。到目前一个区块就是一个方法。但下一章会讲到方法内的区块。

自动变量一般存在栈(stack)内。变量会顺序存入栈内,然后反序取出。也即是先进后出。先定义,后归还。

静态存储:

静态存储会贯穿整个程序的执行过程。有两种使变量静态的方法,一是在方法外将其定义。而是使用关键词static:

static double d = 3.0;

K&R C语言中,只允许初始化静态的数组和结构,但C++2.0和ANSI允许初始化自动数组和结构。但,某些C++接口并未实现自动数组和结构。

第九章将会详细讨论静态存储。

动态存储:

new和delete提供了更灵活的方法。它们管理的内存池在C++意为堆(heap)和自由内存(free store)。这个池和用于静态和自动变量的内存是分开的。new和delete让你可以更好地控制程序使用的内存。但同时也复杂化了内存管理。在栈里,使用中的内存永远都是连续的,但使用new和delete管理的内存则有可能出现空挡,这就让分配内存复杂了。


数组替代方案

本章早些提到了vactor和array模板类,作为基础数组的替代。

vector模板类

vector的大小可以在运行时定义,还可以往内添加新的元素。基本上这就是使用new创建动态数组的一个替代。事实上,在vector内部就是用new和delete来管理内存的。

vector范例:

#include 
...
using namespace std;
vector vi;        //创建一个长为0的int数组
int n;
cin >> n;
vector vd(n);  //创建一个n个double的数组

 vector的大小可以是整数变量或常量。

array模板类(C++11)

vector比基础array类型有更大的容量,但这会略微地降低效率。如果你只想要一个固定大小的数组,更好的还是用C++自有类型。但那并不方便也不安全。C++11为此新加了array模板类,作为std命名空间的一员。就像自有类型一样,array对象有固定的大小且使用栈而非自由存储。所以它和自有类型效率一样。而它又有着更方便的使用和更高的安全度。

范例:

#include 
...
using namespace std;
array ai;
array ad = {1.2, 2.1, 1.0, 3.3};

array的大小只能是常量。

数组,vector对象,array对象比较

#include 
#include 
#include   
int main()
{
    using namespace std;
    // C,原版C++ 
    double a1[4] = {1.0, 1.1, 1.2, 1.3};
    // C++98 STL
    vector a2(4);  //创建有4个元素的vector
	// 98里没有方便的初始化方法 
	a2[0] = 1.0;
	a2[1] = 1.1;
	...
	// C++11 创建并初始化array对象
	array a3 = {1.0, 1.1, 1.2, 1.3}; 
	array a4;
	a4 = a3;     //同大小的可以相互赋值
	//使用数组下标
	cout << "a[1]:" << a[1] << "at" << &a[1] << endl;
	//越界
	a1[-2] = 2.0;
	cout << "a1[-2]:" << a1[-2] << "at" << &a1[-2]  << endl;
	cout << "a3[2]:" << a3[2] << endl;
}

注意到最后的下表越界。a1[-2]其实是*(a1 - 2),也就是数组首位元素地址往前两个double的地址。这其实就不在数组范围内了,但C++并不对这种行为识别为错误(error)。

那么vector和array类有针对这种情况的应对吗?有的,那就是at方法:

a2.at(1);  //相当于a2[1]

调用at方法将会捕捉越界的下标,然后报错并默认终止程序。


总结

  • 数组,结构和指针是C++的三种复合类型。数组可以在单个数据对象中容纳相同类型的复数的值。使用索引或下标可以单独访问元素
  • 结构可以在单个数据结构容纳复数的不同类型的值,然后使用小数点(.)来访问其成员。使用结构第一步是创建一个结构的模型来定义结构容纳的成员。结构的名字就成为了一个新的类型标识。接着就可以使用这个名字来声明一个这个类型的结构变量。
  • 一个联合可以容纳单个值,但这个值可以是多个类型,其成员名标识当前它是哪个类型。
  • 指针是一个设计用于容纳地址的变量。我们说指针指向它容纳的地址。指针声明一定会包含其指向的对象的类型。对指针使用*操作符会显示指针指向的地址内的值。
  • 字符串是空字符(null character)\0结尾的一连串字符。一个字符串可以是双引号括起的字符串常量,在这个情况下结尾的空字符是隐含的。可以把字符串存入字符数组,然后通过将指针初始化为指向字符串首位字符来让这个指针代表这个字符串。strlen这个方法返回字符串的长度,不算空字符。strcpy方法将字符串从一个地址复制到另一个。当使用这两个方法要include头文件cstring或string.h
  • string头文件支持的C++string类是一个对用户更友好的处理字符串的替代方法。特别地,string类会自动重定义大小来适应需要存储的字符串,且你可以用等号来复制一个字符串。
  • new操作符可以在程序运行中为数据对象申请空间。这个操作符会返回申请到的空间的地址,然后可以将这个地址赋值给指针。唯一的访问这个地址的方法就是通过这个指针。如果数据对象是一个简单的变量,可以用*操作符到指代它的值。如果数据对象是一个数组,你可以把指针当做数组名来访问其元素。如果数据对象是一个结构,你可以用箭头(->)操作符来访问机构的成员。
  • 指针和数组是高度相关的。如果ar是一个数组的名字,那么ar[i]等价于*(ar + i),数组的名字等价于数组首位元素的地址。所以数组名的作用和指针是相同的。同样的,你可以用指针名和数组下标来访问new分配的数组的元素。
  • new和delete操作符让你可以显式地管理数据对象的空间的分配与归还至内存池。自动变量,是在方法内声明的变量。静态变量是定义在方法之外的变量或用关键字static定义的变量,这灵活度较低。自动变量在包含它的区块(一般是方法定义)开始时出现,然后在区块结束时失效。静态变量则持续整个程序的运行过程。
  • 标准模板库(STL)是在C++98标准添加的,提供了vector模板类,这模板类提供了自定义动态数组的替代。C++11提供了array模板类,则是一个定长数组的替代。

你可能感兴趣的:(C++,Primer)