IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 第 4 章 复合类型 -> 正文阅读

[C++知识库]第 4 章 复合类型

目录

4.1 数组

4.1.1 程序说明

4.1.2 数组的初始化规则

4.1.3 C++数组初始化方法

4.2 字符串

4.2.1 拼接字符串常量

4.2.2 在数组中使用字符串

4.2.3 字符串输入

4.2.4 每次读取一行字符串输入

4.2.5 混合输入字符串和数字

4.3 string类简介

4.3.1 C++11字符串初始化

4.3.2 赋值、拼接和附加

4.3.3?string类的其他操作

4.3.4 string类 I/O

4.3.5 其他形式的字符串字面值

4.4 结构简介

4.4.1 在程序中使用结构

4.4.2 C++11结构初始化

4.4.3 结构可以将string类作为成员吗

4.4.4 其他结构属性

4.4.5 结构数组

4.4.6 结构中的位字段

4.5 共用体

4.6 枚举

4.6.1 设置枚举量的值

4.6.2 枚举的取值范围

4.7 指针和自由存储空间

4.7.1 声明和初始化指针

4.7.2 指针的危险

4.7.3 指针和数字

4.7.4 使用new来分配内存

4.7.5 使用delete来释放内存

4.7.6 使用new来创建动态数组

4.8 指针、数组和指针算术

4.8.1 程序说明

4.8.2 指针小结

4.8.3 指针和字符串

4.8.4 使用new创建动态结构

4.8.5 自动存储、静态存储和动态存储

4.9 类型组合

4.10 数组的替代品

4.10.1 模板类vector

4.10.2 模板类array(C ++11)

4.10.3 比较数组、vector对象和array对象

4.11 总结


本章内容包括:

  • 创建和使用数组
  • 创建和使用C-风格字符串
  • 创建和使用string类字符串
  • 使用方法getline()和get()读取字符串
  • 混合输入字符串和数字
  • 创建和使用结构
  • 创建和使用共同体
  • 创建和使用枚举
  • 创建 和使用指针
  • 使用new和delete管理动态内存
  • 创建动态数组
  • 创建动态结构
  • 自动存储、静态存储和动态存储
  • vector和array类简介

4.1 数组

数组(array)是一种数据格式,能够存储多个同类型的值。例如,数组可以存储60个int类型的值或者12个short值,或365个float值。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。

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

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

C++中,可以通过修改简单变量的声明,添加中括号(其中包含元素数目)来完成数组声明。例如 short months[12];

声明数组的通用格式如下:

typeName arrayName[arraySize];

?数组之所以被称为复合类型,是因为它是使用其他类型来创建的。不仅能将某种东西声明为数组,它必须是特定类型的数组。没有通用的数组类型,但存在很多特定的数组类型,如char数组或long数组。可以单独访问数组元素,方法是通过下标或索引来对元素进行编号。C++数组从0开始编号。

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

4.1.1 程序说明

如下程序,创建了一个名为yams的包含3个元素的数组,它们分别编号0~2.

#include <iostream>
int main()
{
  using namespace std;
  int yams[3];
  yams[0]=7;
  yams[1]=8;
  yams[2]=6;

}

4.1.2 数组的初始化规则

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

1,只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组。

2,可以使用下标分别给数组中的元素赋值。
3,初始化数组时,提供的值可以少于数组的元素数目。

4,如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0。因此,将数组中所有元素都初始化为0非常简单-只要显式地将第一个元素初始化为0,然后让编译器将其它元素都初始化 为0即可。

5,如果初始化数组时方括号内([])为空,C++编译器将计算元素个数,例如short things[]={1,5,3,8};编译器将使things数组包含4个元素。但是这种方法可能会因为手误在列表中遗漏值。

int cards[4] ={3,6,8,10}; //okay
int hands[4]; // okay

hands[4] = {5,6,7,9}; //not allowed
hands=cards;  //not allowed

float hotelatips[5]= {5.0,2.5};

long totals[500]={0};

4.1.3 C++数组初始化方法

C++11将使用大括号的初始化(列表初始化)作为一种通用的初始化方法,C++11中的列表初始化新增了一些功能。

首先,初始化数组时,可省略等号(=)。

double earnings[4] {1.2, 1.6, 1.1e4,1.7};

其次,可不在大括号内包含任何东西,这将把所有元素都设置为零

unsigned int counts[10] ={}; // all elements set to 0
float balances [100] {}; //all elements set to 0

第三,列表初始化禁止缩窄转换

long pilfs[] ={25,92,3.0}; //not allowed,浮点数转换整型是缩窄操作
char slifs[4] {'h','i','1122011,}; //not allowed,1122011超出了char变量取值范围
char tilifs[4] {'h','i',112,'\0'} //allowed, 虽然112是int值,但在char的取值范围内

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

4.2 字符串

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

存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都位于自己的数组元素中。C-风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0, 其ASCII码为0,用来标记字符串的结尾。例如,如下两个声明:

char dog[8]={'b','e','a','u','x', ' ', 'I', 'I'};  //not a string
char dcat[8]={'f','a','t','e','s', 'a', 'a', '\0'};  //a string

这两个数组都是char数组,但只有第二个数组是字符串。空字符对C-风格字符串而言至关重要。例如,C++有很多处理字符串的函数,其中包括cout显示上面的cat这样的字符串,则将显示前7个字符,发现空字符后停止。但是,如果使用cout打印dog数组(它不是字符串),cout将打印出数组中的8个字母,并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。

另外一种更好的将字符数组初始化为字符串的方法-只需使用一个用引号括起的字符串,这种字符串被称为字符串常量或字符串字面值,如下:

char bird[11]="Mr. Cheeps";
char fish[]="Bubbles";

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

当然,应该确保数组足够大,能够存储字符串中所有字符-包括空字符。使用字符串常量初始化字符数组是这样的一种情况,即让编译器计算元素数据更为安全。让数组比字符串长没有什么害处,只是会浪费一些空间而已。C++对字符串长度没有限制。

在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内。

4.2.1 拼接字符串常量

有时候,字符串很长,没法放在一行中。C++允许拼接字符串面值,即将两个用引号括起的字符串合并为一个。

cout << "I'd give my right arm to be" "a greate violinist.\n";

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

4.2.2 在数组中使用字符串

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

4.2.3 字符串输入

C++的输入一般使用cin指令, cin使用空白(空格,制表符和换行符)来确定字符串的结束位置,这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。

4.2.4 每次读取一行字符串输入

每次读取一个单词通常不是最好的选择。iostream中的类(如cin)提供了一些面向行的类成员函数:getline()和get()。这两个函数都读取一行输入,直到到达换行符。然而,随后getline()将丢弃换行符,而get()将换行符保留在输入序列中。

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

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

假如要使用getline()将姓名读入到一个包含20个元素的name数组中。可以如下调用,(getline()成员函数还可以接受第三个可选参数,后面将讨论)

cin.getline(name, 20);

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

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

const int ArSize;
char dessert[ArSize];

cin.get(name,ArSize);
cin.get(dessert, ArSize); //a problem

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

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

cin.get(name, ArSize);   //read first line
cin.get();               //read newline
cin.get(dessert,ArSize); //read second line

还可以合并上面的两条命令:

cin.getline(name,ArSize).get(); //concatenate member functions

之所以可以这样做,是由于cin.get(name,ArSize)返回一个cin对象,该对象随后将被用来调用get()函数。同样可以把连续的两行输入分别读入到数组name1和name2中,其效果与两次调用cin.getline()相同:

cin.getline(name1,ArSize).getlinee(name2,ArSize);

?为什么要使用get(),而不是getline()呢?首先,老式实现没有getline();其次,get()使输入更仔细。例如假设用get()将一行读入数组中。如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已填满呢嗯?查看下一个输入字符,如果是换行符,说明已经读取了整行,否则,说明该行中还有其他输入。

3,空行和其他问题

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

cin.clear();

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

4.2.5 混合输入字符串和数字

混合输入数字和面向行的字符串会导致问题。

#include<iostream>
using namespace std;

int main() 
{
	const int ArSize=20;
	cout<<"What year was your house built?\n";
	int year;
	cin>>year;
	cout<<"What is its street address?\n";
	char address[80];
	cin.getline(address,80);
	cout<<"Year built: "<<year<<endl;
	cout<<"Address: "<<address<<endl;
	cout<<"Done!\n";
	return 0;
	
}

运行该程序可发现,用户根本没有输入地址的机会。问题在于,当cin读取年份,将回车键生成的换行符留在了输入队列中。后面的cin.getline()看到换行符后,将认为是一个空行,并将一个空字符串赋给address数组。解决之道是,在读取地址之前先读取并丢弃换行符。这可以通过几种方式完成,其中包括使用没有参数的get()和使用接受一个char参数的get(),如前面的例子所示。可以单独进行调用:

cin>>year;
cin.get(); //or cin.get(ch);

也可以利用表达式cin>>year返回cin对象,将调用拼接起来:

(cin>>year).get(); //or (cin>>year).get(ch);

按照上面的方法修改程序后,便可以正常工作:

4.3 string类简介

ISO/ANSI C++98标准通过添加string类扩展了C++库,因此可以使用string类型的变量而不是字符数组来存储字符串。要使用string类,必须在程序总包含头文件string。string类位于命名空间std中。

string str1;
string str2="panther";

类设计让程序能够自动处理string大小。例如,str1的声明创建一个长度位0的string对象,但程序将输入读取到str1中时,将自动调整str1的长度。

cin >> str1; //str1 resized to fit input

这使得与使用数组相比,使用string对象更方便,也更安全。从理论上说,可以将char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的实体。

4.3.1 C++11字符串初始化

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

char fisrst_data[] = {"Le Chapon Dodu"};
char second_data[] = {"second data"};
string third_data[] = {"the third data"};
string fourth_data {"the fourth data"};

4.3.2 赋值、拼接和附加

使用string类时,某些操作比使用数组时更加简单。例如,不能将一个数组赋给另一个数组,但可以将一个string对象赋给另一个string对象:

char charr1[20];
char charr2[20]="jaguar";
string str1;
string str2 ="pather";

charr1=charr2; //invalid, no array asignment
str1=str2; //valid, object asignment ok

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

string str3;
str3=str1+str; //assign str3 the joined strings
str1+=str2; //add str2 to the end str1

4.3.3?string类的其他操作

在C++新增string类之前,程序员完成给字符串赋值等工作,借助于头文件cstring (string.h)。 函数strcpy()将字符串复制到字符数组中,函数strcat()将字符串附加到字符数组末尾:

strcpy(charr1,charr2); //copy charr2 to charr1
strcat(charr1, charr2); //append contents of charr2 to charr1

当使用字符数组的复制工作时,总存在目标数组过小,无法存储指定信息的危险,如果强制将过大的字符复制到不足的数组中,将覆盖相邻的内存,可能导致程序终止,或者程序继续运行,但数据被损坏。string类具有自动调整大小的功能,从而能够避免着这种问题的发生。C函数库确实提供了与strcat(), strcpy() 类似的函数——strncat(), strncpy(),它们接受指出目标数组最大允许长度的第三个参数,因此更为安全,但是也进一步增加了编写程序的复杂度。

int len1= str1.size(); //obtain length of str1
int len2= strlen(charr1); //obtain length of str1

函数strlen()是一个常规函数,它接受一个C-风格字符串作为参数,并返回该字符串包含的字符数。函数size()的功能基本上与此相同,但句法不同。str1是一个对象,而size()是一个类方法。方法是一个函数,只能通过其所属类的对象进行调用。这里str1是一个对象,size()是string类的一个方法。 C++string类对象使用对象名和句点运算符来指出要使用哪个字符串。

4.3.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:\n";
	
	cin.getline(charr,20);
	cout <<"Yor entered: "<<charr <<endl;
	
	cout <<"Enter another line of text:\n";
	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;
	 
 } 

程序输出示例:

Length of string in charr before input: 1
Length of string in str before input: 0
Enter a line of text:
peanut buffer
Yor entered: peanut buffer
Enter another line of text:
blueberry jam
You entered: blueberry jam
Length of string in charr after input: 13
Length of string in str after input: 13

在用户输入之前,该程序指出charr的字符串长度时1,比该数组长度小,也可能会输出一个比实际长度大的值,这是因为,为初始化的数组的内容是未定义的,其次,函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符。对于未被初始化的数据,第一个空字符的出现位置是随机的,因此,您在第一次运行该程序时,得到的数组长度很可能与此不同。

另外,用户输入之前,str中的字符串长度为0,这是因为未被初始化的string对象的长度被自动设置为0。

4.3.5 其他形式的字符串字面值

除char类型外,C++还有类型char_t, 而C++11新增了类型char16_t和char32_t。可创建这些类型的数组和这些类型的字符串字面值。对于这些类型的字符串字面值,C++分别使用前缀L,u和U表示,下面是一个如何使用这些前缀的例子:

wchar_t title[] = L"Chief Astrogator";
char16_t name[] = u"Felonia Ripova";
char32_t car[] = U"Humber Super Snipe";

C++h新增的另一种类型是原始(raw)字符串。在原始字符串中,字符表示的就是自己,例如,序列 \n 不表示换行符,而表示两个常规字符 —— 斜杠和 n, 因此在屏幕上显示时,将显示这两个字符。

4.4 结构简介

假设要存储有关篮球运动员的信息,则可能需要他的姓名,工资,身高,体重,平均得分等,希望有一种数据格式可以将所有这些信息都存储在一个单元中。数组不能完成这种任务,虽然数组可以存储多个元素,但是所有的类型必须相同。

C++中的结构可以满足要求。结构是一种比数组更灵活的数据格式,因为同一个结构可以存储多个类型的数据,这使得能够将有关篮球运动员的信息放在一个结构中,从而将数据的表示合并到一起。

结构是用户定义的类型,而结构声明定义了这种类型的数据属性。定义了类型后,便可以创建这种类型的变量。因此创建结构包括两步。首先,定义结构描述—它描述并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。

struct inflatable
{
    char name[20];
    float volume;
    double price;
 };

关键字struct表明,这些代码定义的是一个结构的布局。标识符inflatable是这种数据格式的名称,因此新类型的名称为inflatable。这样,便可以像创建char或int类型的变量那样创建inflatable类型的变量了。接下来的大括号中包含的是结构存储的数据类型的列表,其中每个列表项都是一条声明语句。总之,结构定义指出了新类型(这里是inflatable)的特征。

定义结构后,便可以创建这种类型的变量了,在C++中,结构标记的用法与基本类型名相同。在C++中,省略struct不会出错。

inflatable hat;
inflatable woope_cushion;

struct inflatable gose; // keyword struct required in C
inflatable vincent; //keyword struct not required in C++

?由于hat的类型为inflatable, 因此可以使用成员运算符(.) 来访问各个成员。例如,hat.volume, hat.price。 访问类成员函数(如cin.getline())的方式是从访问结构成员变量的方式衍生而来的。

4.4.1 在程序中使用结构

程序中使用结构,需要对结构进行声明,结构声明的位置很重要。有两种选择,可以将声明放在main()函数中,紧跟在开始括号的后面。另一种选择是将声明放在main()的前面。对于当前程序来讲,两种选择没有实际区别。但对与包含两个或更多函数的程序来说,差别很大。外部声明可以被其后面任何函数使用,而内部声明只能被该声明所属的函数使用。通常应该使用外部声明。

4.4.2 C++11结构初始化

与数组一样,C++11也支持将列表初始化用于结构,且等号(=)是可选的:

inflatable duck {"Daphne",0.12,9.98};

其次,如果大括号内未包含任何东西,各个成员都将被设置为零。

最后,不允许缩窄转换。

4.4.3 结构可以将string类作为成员吗

可以将成员name指定为string对象而不是字符数组吗?答案是可以的,只要您使用的编译器支持对以string对象作为成员的结构进行初始化。

一定要让结构定义能够访问名称空间std。为此,可以将编译指令using移到结构定义之前,也可以像下面一样,将name的类型声明为std::string。

#include <string>
struct inflatable
{
    std::string name;
    float volume;
    double price;
};

4.4.4 其他结构属性

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

还可以同时完成定义结构和创建结构变量的工作。为此,只需要将变量名放在结束括号的后面即可,但是将结构定义和声明分开,可以使程序更容易阅读和理解。

strcut perks
{
    int key_numer;
    char car[12];

}mr_smith, ms_jones;  //two perks variables


4.4.5 结构数组

?inflatable结构包含一个数组(name)。也可以创建元素为结构的数组,方法和创建基本类型数组完全相同。例如创建一个包含100个inflatable结构的数组,可以这样做:

infltable gifts[100];

记住gifts本身是一个数组,而不是结构,每个元素可以通过gits[0].price的方式访问。初始化结构数组,可以结合使用初始化数组的规则(用逗号分割每个元素的值,并将这些值用花括号括起)和初始化结构的规则(用逗号分割每个成员的值,并将这些值用花括号括起)。

inflatable guests[2] =
{
    {"Bambi", 0.5, 21.99},
    {"Godzilla", 2000, 564.99}
};

4.4.6 结构中的位字段

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

struct torgle_register
{
    unsigned int SN : 4; //4bits for SN value
    unsigned int : 4; //4bits unused
    bool goodIn :1; //valid input (1 bit)
    bool goodTorgle :1; //successful torgling
}

4.5 共用体

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

	union one4all
	{
		int int_val;
		long long_val;
		double double_val;
	};
	
	one4all pail;
	pail.int_val=15;
	cout<<pail.int_val<<endl;
	
	pail.double =1.38; //stroe a double, int value is lost
	cout<<pail.double_val<<endl;

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

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

匿名共用体(anonymous union)没有名称,其成员将称为位于相同地址处的变量。显然,每次只有一个成员是当前变量:
?

	struct widget
	{
		char brand[20];
		int type;
		union //anonymous 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。程序员负责确定当前哪个成员是活动的。

共用体常用于(但并非只用于)节省内存。C++还用于嵌入式系统编程,如控制烤箱,MP3播放器等,对于这些应用程序来说,内存可能非常宝贵。另外共用体常用于操作系统数据结构或硬件数据结构。

4.6 枚举

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

enum spectrum {red, orang, yellow, green, blue, violet, indigo, ultraviolet};

这条语句完成了两项工作:

1, 让spectrum成为新类型的名称: spectrum被称为枚举,就像struct变量被称为结构一样

2,将red, orange, yellow等作为符号常量,它们对应整数值 0~7。这些常量叫作枚举量(enumerator)。

可以用枚举名来声明这种类型的变量,在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量。

spectrum band;

band = blue;   //band a variable of type spectrum
band = 2000;  //invalid, 2000 not an enumerator

对于枚举之定义了赋值运算符,具体来说没有为枚举定义算术运算。

4.6.1 设置枚举量的值

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

enum bits {one=1,two=2, four=4, eight =8};

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

enum bigstep {first, second=100, third};

这里,first在默认情况下为0。后面没有被初始化的枚举量的值将比其前面的枚举量大1。因此,third的值为101。

最后,可以创建多个值相同的枚举量:

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

?其中,zero和null都为0, one和umero_uno都为1。在C++早期的版本中,只能将int值赋给枚举量,但这种限制取消了,因此可以使用long甚至long long类型的值。

4.6.2 枚举的取值范围

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

enum bits {one =1, two =2, four =4, eight =8 };
bits myflag;

myflag=bits(6); //valid, because 6 is in bits range

其中6不是枚举值,但它位于枚举定义的取值范围内。

取值范围的定义如下。首先,要找出上限,需要知道枚举量的最大值。找到大于这个最大值的、最小的2的幂,将它减去1,得到的便是取值范围的上限。例如前面定义的bigstep的最大枚举值是101, 在2的幂中,比这个数大的最小值是128,因此取值范围的上限是127。要计算下限,需要知道枚举量的最小值。如果它不小于0,则取值范围的下限为0;否则,采用与寻找上限方式相同的方式,但加上负号。

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

4.7 指针和自由存储空间

在第3章的开头,提到了计算机程序在存储数据时必须跟踪的3中基本属性。这里再次列出这些属性:

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

您使用过一种策略来达到上述目的:定义一个简单变量。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。

下面来看另一种策略,它在开发C++类时非常重要。这种策略以指针为基础,指针是一个变量,其存储的是值的地址,而不是值本身。在讨论指针之前,我们先看一看如何找到常规变量的地址。只需对变量应用地址运算符(&),就可以获得它的位置,例如,如果home是一个变量,则&home是它的地址。

-----------------------------------------------------------------------------------

指针与C++基本原理

面向对象编程(OOP)与传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。运行阶段决策就好比度假时,选择参观哪些景点取决于天气和当时的心情;而编译决策更像不管在什么条件下,都坚持预先设定的日程安排。

运行阶段决策提供了灵活性,可以根据当时的情况进行调整。例如,为考虑数组分配内存的情况。传统的方法时声明一个数组。要在C++中声明数组,必须指定数组的长度。因此,数组长度在程序编译时就设定好了;这就是编译阶段决策。您可以认为,在80%的情况下,一个包含200个元素的数组足够了,但程序有时需要处理200个元素。为了安全起见,使用了一个包含200个元素的数组。这样,程序在大多数情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后,可以这次告诉它只需要20个元素,而还可以下次告诉它需要205个元素。

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

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

-----------------------------------------------------------------------------------

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

4.7.1 声明和初始化指针

计算机需要跟踪指针指向的值的类型。因此,指针声明必须指定指针指向的数据的类型。

int *ptr;

*运算符两边的空格是可选的,这里强调的 *ptr是一个int类型的值。 int* prt则强调 int*是一种类型——指向int的指针。

在C++中,int*是一种复合类型,是指向int的指针。

4.7.2 指针的危险

危险更易发生在那些使用指针不仔细的人身上。及其重要的一点是: 在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间时一个独立的步骤,忽略这一步无疑是自找麻烦。

long *fellow;  //create a pinter-to-long
*fellow =23333; //place a value in never-never land

fellow确实是一个指针,但它指向哪里呢?上述代码并没有将地址赋给fellow。那么23333被放在哪里我们并不知道。由于fellow没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储2333的地址。如果fellow的值碰巧为1200,计算机将把数据放在1200上,即使这恰巧是程序代码的地址。fellow指向的地方很可能并不是所要存储333的地方。这种错误可能会导致一些最隐匿,最难以跟踪的bug。

警告: 一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。

4.7.3 指针和数字

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

int *pt;
pt=0xB8000000;  //type mismatch

在C99标准发布之前,C语言允许这样赋值。但C++在类型一致方面的要求更加严格,编译器将显示一条错误信息,通告类型不匹配。要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型:

int *pt;
pt = (int*) 0xB8000000; //types now match

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

4.7.4 使用new来分配内存

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

在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。示例如下:

int *pn=new int;

new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来,将地址赋给pn,pn是被声明为指向int的指针。现在pn是地址,而*pn是存储在那里的值。

int higgens;
int *pt = &higgens;

示例是将变量的地址赋给指针的操作,这两种情况下,都是将一个int变量的地址赋给了指针。第二种情况下,可以通过名称higgens来访问该int,第一种情况下,则只能通过该指针进行访问。这里pn指向的内存没有名称,我们说pn指向一个数据对象,为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:

typeName *pointer_name =new typeNae;

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

对于指针,需要指出的另一点是,new分配的内存块通常与常规变量声明分配的内存块不同变量nights和pd的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区的内存区域分配内存。

如果计算机没有足够的内存而无法满足new的请求,这种情况下,new通常会引发异常;在较老的实现中,new将返回0。在C++中,值为0的指针被称为空指针。C++确保空指针不会指向有效的数据,因此它被常用来表示运算符或函数失败(如果成功,将返回一个有用的指针)。

4.7.5 使用delete来释放内存

当需要内存时,可以使用new来请求,另外一个对应的delete运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向有效使用内存的关键一步。归还或释放(free)的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块额指针(这些内存块最初使用new分配的):

int *ps= new int; //allocate memory with new
...   //use memory
delete ps; //free memory with delete when done

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

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

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

4.7.6 使用new来创建动态数组

如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做比使用new和指针更简单。通常,对于大型数据(如数组,字符串和结构),应使用new,这正是new的用武之地。

例如,假设要编写一个程序,它是否需要数据取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。再编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。 但使用new时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的这种数组叫做动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度,使用动态联编时,程序将在运行时确定数组的长度。

1. 使用new创建动态数组

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

int *psome= new int [10];

new 运算符返回第一个元素的地址。在这个例子中,该地址被赋给指针psome。

当程序使用完new分配的内存块时,应使用delete释放它们。然而,对于使用new创建的数组,应使用另一种格式的delete来释放:

delete [] psome;

方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。使用new和delete时,应遵守以下规则。

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

2. 使用动态数组

创建动态数组后,如何使用它呢?首先,从概念上考虑这个问题。下面的语句创建指针psome,它指向包含10个int值的内存块中的第1个元素:

int *psome =new int [10] 

实际中,通过psome[0], psome[1]来分别访问每个指针变量指向 的元素。

4.8 指针、数组和指针算术

指针和数组基本等价的原因在于指针算术和C++内部处理数组的方式。首先看一下算术,将整数加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数

4.8.1 程序说明

多数情况下,C++将数组名解释为数组第1个元素的地址。

double *pw=wges;
// wges= &wges[0] = address of first of array

*(stacks+1)和stacks[1]是等价的。多数情况下,可以相同的方式使用指针名和数组名。它们都表示地址,区别之一是,可以修改指针的值,而数组名是常量。

pointername =pointername +1 //valid
arrayname = arrayname +1; //not allowed

另一个区别是,对数组应用sizeof运算符得到的是数组的长度,而对指针得到的是指针的长度,即使指针指向的是一个数组。?

4.8.2 指针小结

1,声明指针

typeName *pointerNmae;

2,给指针赋值

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

double *pn;
double bubble =3.2;
pn =&bubble;

3,对指针解除引用

对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符(*)来解除引用。

cout << *pn;
*pc ='S';

另一种对指针解除引用的方法是使用数组表示法,例如pn[0]与*pn是一样的。绝不要对未被初始化为适当地址的指针解除引用。

4,区分指针和指针所指向的值

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

int *pt = new int;
*pt =5;

5,数组名

多数情况下,C++将数组名视为数组的第一个元素的地址。

6,指针算术

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

int tacos[10]={5,2,8,4,1,2,2,4,6,8};
int *pt =tacos;  //suppose pt and tacos are the address 3000
pt =pt +1;   //now pt is 004 if a int is 4 bytes
int *pe =&tacos[9]; //pe is 3036 if an int is 4bytes
pe =pe-1;    //now pe is 3032, the address of tacos[8]
int diff =pe -pt;  //tacos[8] and tacos[1]

7,数组的动态联编和静态联编

使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置:

int tacos[10];

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

8,数组表示法和指针表示法

使用方括号数组表示法等同于对指针解除引用:
?

tacos[0] means *tacos means the value at address tacos

数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。

int *pt =new int [10];
*pt =5;
pt[0] =6;
pt[9] = 44;
int coats[10];
*(coats+4) =12;

4.8.3 指针和字符串

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

char flower[10] = "rose";
cout << flower <<"s are red \n";

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

这里的关键不在于flower是数组名,而在于flower是一个char的地址。这意味着可以将指向char的指针变量作为cout的参数,因为它也是char的地址。在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。

注意: 在cout和多数C++表达式中,char数组名,char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。

4.8.4 使用new创建动态结构

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

将new用于结构由两步组成: 创建结构和访问其成员。

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

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

?

4.8.5 自动存储、静态存储和动态存储

根据用于分配内存的方法,C++有3中管理数据内存的方式: 自动存储、静态存储和动态存储(也叫做自由存储空间或堆)在存在时间的长短方面,以这3中凡是分配的数据对象各不相同。

1,自动存储

?2,静态存储

3, 动态存储

?栈、堆和内存泄漏

如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,将发生什么情况呢?如果没有调用delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。实际上将会无法访问自由存储空间的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将在程序的整个生命周期内都不可使用。这些内存被分配出去,但无法收回。极端情况是,内存泄漏可能会非常严重,以至于程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃。另外,这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响,导致它们崩溃。

4.9 类型组合

本章介绍了数组、结构和指针。可以各种方式组合它们。

struct antarctica_year_end
{
    int year;
};

antarctica_year_end s01,s02,s03;  //s01,s02,s03 are structures
s01.year =1998;
 

antarctica_year_end *pa=&s02;
pa->year =1999;

antarctica_year_end trio[3];
trio[0].year =2003;

(trio+1)->year =2004;

const antarctica_year_end *arp[3] ={ &s01, &s02, &s03};

4.10 数组的替代品

模板类vector和array是数组的替代品。

4.10.1 模板类vector

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

#include <vector>

using namespace st;
vector<int> vi;  //create a zero-size array of int
int n;
cin >>n;
vector<double> vd(n); //create an array of n doubles

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

一般而言,下面的生命创建一个名为vt的vector对象,它可存储n_elem个类型为typeName的元素,n_elem可以是整型常量,也可以是整型变量。

vector<typeName> vt(n_elem);

4.10.2 模板类array(C ++11)

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

#include <array>
 
using namesapce std;
array<int,5> ai;
array<double,4> ad ={1.2, 2.1, 3.43, 4.3};

下面声明了一个arr的array对象,包含n_elem个类型为typename的元素, 与vector对象不同,n_elem不能是变量。

array<typeName, n_elem> arr;

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

?

4.10.3 比较数组、vector对象和array对象

1)无论是数组、vector对象还是array对象,都可使用标准数组表示法来访问各个元素。

2)从地址可知,array对象和数组存储在相同的内存区域(栈)中,而vector对象存储在自由存储区(堆)中。

3)可以将一个array对象赋给另一个array对象,而对于数组,必须逐元素赋值数据。

a1[-1] =20.2;
 //等价于下面
*(a1-2) =20.2;

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

4.11 总结

?

?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-10 13:14:33  更:2021-08-10 13:15:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/9 18:26:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码