第4章 复合类型(2)剖析指针
创建和使用数组。
创建和使用C-风格字符串,string类字符串。
使用方法getline()和get()读取字符串。
混合输入字符串和数字。
创建和使用结构,共用体,枚举,指针。
使用new和delete管理动态内存。
创建动态数组,动态结构。
自动存储、静态存储和动态存储。
vector和array类简介。
4.4 结构简介
结构是一种比数组更灵活的数据格式,因为同一个结构可以存储多种类型的数据,从而将数据的表示合并到一起。
结构也是C++ OOP堡垒(类)的基石。
结构是用户定义的类型,而结构声明定义了这种类型的数据属性。
定义了类型后,便可以创建这种类型的变量。
因此创建结构包括两步。
首先,定义结构描述,它描述并标记了能够存储在结构中的各种数据类型。然后按描述创建结构变量(结构数据对象)。
struct inflatable
{
char name[20];
float volume;
double price;
};
//C++ 允许在声明结构变量是省略关键字 struct
inflatable hat;
inflatable woopie_cushion;
inflatable mainframe;
在C++中,结构标记的用法与基本类型名相同。这种变化强调的是,结构声明定义了一种新类型。在C++中,省略struct不会出错。
由于hat的类型为inflatable,因此可以使用成员运算符(.)来访问各个成员。hat是一个结构,而hat.price是一个double变量。
访问类成员函数(如cin.getline()) 的方式是从访问结构成员变量(如vincent.price) 的方式衍生而来的。
4.4.1 在程序中使用结构
结构声明的位置很重要。有两种选择。可以将声明放在main()函数中,紧跟在开始括号的后面。
另一种选择是将声明放到main()的前面,位于函数外面的声明被称为外部声明。二者区别在作用域不同。
和数组一样,使用由逗号分隔值列表,并将这些值用花括号括起。在该程序中,每个值占一行,但也可以将它们全部放在同一行中。只是应用逗号将它们分开:
与数组一样, C++11也支持将列表初始化用于结构,且等号(=)是可选的。
其次,如果大括号内未包含任何东西,各个成员都将被设置为零。
最后,不允许缩窄转换。
4.4.3 结构可以将 string类 作为成员吗
答案是肯定的,只要您使用的编译器支持对以 string 对象作为成员的结构进行初始化。
一定要让结构定义能够访问名称空间std。为此,可以将编译指令 using 移到结构定义之前;也可以像前面那样,将 name 的类型声明为std::string。
struct inflatable
{
std::string name;
float volume;
double price;
};
4.4.4 其他结构属性
C++使用户定义的类型与内置类型尽可能相似。
例如,可以将结构作为参数传递给函数,也可以让函数返回一个结构。
另外,还可以使用赋值运算符(=)将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。
struct assgn
{
char name[20];
float volume;
double price;
};
assgn bouquet = {
"sunflowers", 0.20, 12.49
};
assgn choice;
cout << "bouquet: " << bouquet.name << "for $" << bouquet.price << '\n';
choice = bouquet;
cout << "choice: " << choice.name << "for $ " << choice.price << '\n';
从中可以看出,成员赋值是有效的,因为choice结构的成员值与bouquet结构中存储的值相同。
可以同时完成定义结构和创建结构变量的工作。为此,只需将变量名放在结束括号的后面即可:
bouquet: sunflowersfor $12.49
choice: sunflowersfor $ 12.49
注意,最好是将结构定义和变量声明分开,以降低程序耦合度并易于阅读和理解。
还可以声明没有名称的结构类型,方法是省略名称,同时定义一种结构类型和一个这种类型的变量:
strcut //no tag
{
int x; // 2 numbers
int y;
}position; //a structure variable
这样将创建一个名为position的结构变量。可以使用成员运算符来访问它的成员(如position.x),但这种类型没有名称,因此以后无法创建这种类型的变量。
与 C 结构不同, C++结构除了成员变量之外,还可以有成员函数。但这些高级特性通常被用于类中,而不是结构中。
4.4.5 结构数组
inflatable结构包含一个数组(name)。也可以创建元素为结构的数组,方法和创建基本类型数组完全相同。
inflatable gifts[100];
cin >> gifts[0].volume;
cout << gifts[99].price;
要初始化结构数组,可以结合使用初始化数组的规则(用逗号分隔每个元素的值,并将这些值用花括号括起)和初始化结构的规则(用逗号分隔每个成员的值,并将这些值用花括号括起)。由于数组中的每个元素都是结构,因此可以使用结构初始化的方式来提供它的值。
记住, gifts本身是一个数组,而不是结构,因此像gifts.price这样的表述是无效的。
inflatable guests[2] =
{
{"Bambi", 0.5, 21.99},
{"Godzilla", 2000, 565.99}
};
4.4.6 结构中的位字段
与C语言一样, C++也允许指定占用特定位数的结构成员,这使得创建与某个硬件设备上的寄存器对应的数据结构非常方便。
4.5 共用体 union
共用体(union)是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
也就是说,结构可以同时存储int、long 和 double,共用体只能存储int、 long 或 double。
共用体的句法与结构相似,但含义不同。
共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。
共用体常用于操作系统数据结构或硬件数据结构。
4.6 枚举 enum
enum工具提供了另一种创建符号常量的方式,这种方式可以代替 const。它还允许定义新类型,但必须按严格的限制进行。
使用enum的句法与使用结构相似。
enum spectrum {red, orange, yellow, green, blue};
spectrum 是新类型的名称,就像 struct变量被称为结构一样。
将 red、orange、yellow 等作为符号常量,它们对应整数值 0-4,也被称为枚举量。
在默认情况下,将整数值赋给枚举量,第一个枚举量的值为0,第二个枚举量的值为1,依次类推。可以通过显式地指定整数值来覆盖默认值。
可以用枚举名来声明这种类型的变量
spectrum band;
//在不强制类型转换下,只能将定义枚举时使用的枚举量赋给这种枚举的变量
band = blue;
band = 200000; //invalid, 2000 not an enume 工ator
对于枚举,只定义了赋值运算符。具体地说,没有为枚举定义算术运算:
枚举量是整型,可被提升为 int类型,但 int类型不能自动转换为枚举类型:
如果 int值是有效的,则可以通过强制类型转换,将它赋给枚举变量:
- 实际上,枚举更常被用来定义相关的符号常量,而不是新类型。
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};
4.6.2 枚举的取值范围
取值范围的定义
首先,要找出上限,需要知道枚举量的最大值。找到大于这个最大值的、最小的 2 的幂,将它减去 1,得到的便是取值范围的上限。
要计算下限,需要知道枚举量的最小值。如果它不小于 0,则取值范围的下限为 0;否则,采用与寻找上限方式相同的方式,但加上负号。
例如,前面定义的 bigstep 的最大值枚举值是 101。在 2 的幂中,比这个数大的最小值为 128,因此取值范围的上限为 127。
例如,如果最小的枚举量为−6,而比它小的、最大的2的幂是−8(加上负号),因此下限为−7。
4.7 指针和自由存储空间
计算机程序在存储数据时必须跟踪的3种基本属性:
- 信息存储在何处
信息的值为多少
存储的信息是什么类型
一种策略是定义一个简单变量。声明语句指出了值的类型和符号名,还让程序为值分配内存,并在内部跟踪该内存单元。
一种策略是以指针为基础,指针是变量,但存储的是值的地址,而不是值本身。
常规变量的地址。只需对变量应用地址运算符(&),就可以获得它的位置。
使用常规变量时,值是指定的量,而地址为派生量。
下面来看看指针策略,它是C++内存管理编程理念的核心。
面向对象编程与传统的过程性编程的区别在于, OOP强调的是在运行阶段(而不是编译阶段)进行决策。
运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。
处理存储数据的新策略刚好相反,将地址视为指定的量,而将值视为派生量。一种特殊类型的变量,即指针用于存储 值的地址。
因此,指针名表示的是地址。
- 运算符被称为间接值(indirect velue)或解除引用(dereferencing)运算符,将其应用于指针,可以得到该地址处存储的值(这和乘法使用的符号相同;
C++根据上下文来确定所指的是乘法还是解除引用)。
假设 p_updates是一个指针,则 p_updates表示的是一个地址,而 *p_updates表示存储在该地址处的值。
- *p_updates与常规 int变量等效。
int updates = 6;
int* p_updates; //这强调的是: int* 是一种类型,即指向 int 的指针。
//* 运算符两边的空格是可选的。传统上,C程序员使用这种格式:int *p_updates,这强调 *ptr是一个 int类型的值。
//这难道是我一直不懂指针的原因???大悟!!当然,在哪里添加空格对于编译器来说没有任何区别。
p_updates = &updates; //(√)
cout << "Values: updates = " << updates << " , *p_updates = " << *p_updates << '\n';
cout << "Addesses: &updates = " << &updates << " , p_updates = " << p_updates << '\n';
*p_updates += 1;
cout << "Now updates = " << updates << endl;
如何声明指针? 基本类型加个 *
int变量 updates和指针变量 p_updates只不过是同一枚硬币的两面。
变量 updates表示值,并使用 &运算符来获得地址;
而变量 p_updates表示地址,并使用 *运算符来获得值。
Values: updates = 6 , *p_updates = 6
Addesses: &updates = 00000099CBCFFCA4 , p_updates = 00000099CBCFFCA4
Now updates = 7
4.7.1 声明和初始化指针
接前文声明,这表明, * p_updates的类型为int。由于 运算符被用于指针,因此 p_updates变量本身必须是指针。
我们说 p_updates 指向 int 类型,我们还说 p_updates 的类型是指向 int的指针,或int。
可以这样说, p_updates是指针(地址),而 *p_updates 是int,而不是指针。
int* p1, p2; //声明的是指针,还是变量?
对每个指针变量名,都需要使用一个*。
在C++中, int *是一种复合类型,是指向int的指针。
可以忽略空格,方便理解。
double* tax_ptr;
char* str;
将 tax_ptr 声明为一个指向 double 的指针,因此编译器知道 *tax_ptr 是一个 double 类型的值。也就是说,它知道 *tax_ptr 是一个以浮点格式存储的值。
指针变量不仅仅是指针,而且是指向特定类型的指针。 tax_ptr的类型是指向 double的指针(或double *类型)。
和数组一样,指针都是基于其他类型的。
虽然 tax_ptr 和 str 指向两种长度不同的数据类型,但这两个变量本身的长度通常是相同的。也就是说,char 的地址与 double 的地址的长度相同。
这就好比 1016 可能是超市的街道地址,而 1024 可以是小村庄的街道地址一样。
地址的长度 或 值 既不能指示关于变量的长度或类型的任何信息,也不能指示该地址上有什么建筑物。
int* np_updates ;
np_updates = 8; //不能将 int 类型的值分配到 int* 类型的实体。
//添加一组
int* np_updates ;
np_updates = 00000098CBCFFCA4; //无效的八进制数字,无法逆向使用操作系统分配的内存地址。 (×)
cout << "addesses: np_updates = " << np_updates << " , values = " << *np_updates<< '\n';
4.7.2 指针的危险
危险更易发生在那些使用指针不仔细(危险)的人身上。极其重要的一点是:
在 C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。
为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦。
int* np_updates ;
*np_updates =8; //使用未初始化的内存 np_updates (×)
cout << "addesses: np_updates = " << np_updates << " , values = " << *np_updates<< '\n';
np_updates 确实是一个指针,但它指向哪里呢?上述代码没有将地址赋给 np_updates。那么 8 将被放在哪里呢?
我们不知道。由于 np_updates 没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储 8 的地址。如果 np_updates 的值碰巧为1200,计算机将把数据放在地址 8 上,即使这恰巧是程序代码的地址。 np_updates 指向的地方很可能并不是所要存储 8 的地方。这种错误可能会导致一些最隐匿、最难以跟踪的bug。
一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。
4.7.3 指针和数字
指针不是整型,虽然计算机通常把地址当作整数来处理。
从概念上看,指针与整数是截然不同的类型。整数是可以执行加、减、除等运算的数字,而指针描述的是位置,将两个地址相乘没有任何意义。
int* pt;
pt = 0xB8000000; (×)//偏移地址
pt = (int *)0xB8000000; (√)//要将数字值作为地址来使用,应通过强制类型转换将数字转换为**适当的**地址类型:
4.7.4 使用 new 来分配内存
前面我们都将指针初始化为变量的地址;
变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。
指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。
在这种情况下,只能通过指针来访问内存。在 C语言中,可以用库函数 malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法,new运算符。
在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里的关键所在是C++的 new 运算符。
程序员要告诉new,需要为哪种数据类型分配内存; new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针。
比较将变量的地址赋给指针
new int告诉程序,需要适合存储int的内存。 new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。
接下来,将地址赋给pn, pn是被声明为指向int的指针。现在, pn是地址,而 *pn是存储在那里的值。
int* pn = new int;
int higgens;
int* pt = &higgens;
在这两种情况(pn和pt)下,都是将一个 int变量的地址赋给了指针。
在第二种情况下,可以通过名称 higgens 来访问该 int,
在第一种情况下,则只能通过该指针进行访问。
- 这引出了一个问题: pn指向的内存没有名称,如何称呼它呢?
我们说 pn 指向一个数据对象,这里的“对象”不是“面向对象编程”中的对象,而是一样“东西”。
术语“数据对象”比“变量”更通用,它指的是为数据项分配的内存块。因此,变量也是数据对象,但 pn 指向的内存不是变量。
乍一看,处理数据对象的指针方法可能不太好用,但它使程序在管理内存方面有更大的控制权。
需要在两个地方指定数据类型:
用来指定需要什么样的内存和用来声明合适的指针。
当然,如果已经声明了相应类型的指针,则可以使用该指针,而不用再声明一个新的指针。
- 如何将 new 用于两种不同的类型
//new用于两种不同的类型
int nights = 1001;
int* pt = new int;
*pt = 1001;
cout << "nights value = "<< nights << " : location = " << &nights << '\n'; //nights = 1001
cout << "int " << "value = " << *pt << ": location = " << pt << '\n'; //int value = 1001
double* pd = new double;
*pd = 10000001.0;
cout << "Double " << "value = " << *pd << ": location = " << pd << '\n'; //double value = 10000001.0
cout << "location of pointer pd: " << &pd << '\n';
cout << "size of pt = " << sizeof(pt);
cout << ": size of *pt = " << sizeof(*pt) << '\n';
cout << "size of pd = " << sizeof pd;
cout << ": size of *pd = " << sizeof(*pd) << endl;
该程序使用 new 分别为 int 类型和 double 类型的数据对象分配内存。
- 这是在程序运行时进行的。
指针 pt 和 pd 指向这两个数据对象,如果没有他们,将无法访问这些内存单元(如前文的报错)。有了指针就可以像使用变量那样使用 *pt 和 *pd。将值赋给 *pt 和 *pd,从而将这些值赋给新的数据对象。同样,可以通过打印 *pt 和 *pd 来显示这些值。
必须声明指针所指向的的类型的原因之一。
地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。
底层原理是 CPU的寻址方式,即 基地址 + 偏移地址。
从这两个值的地址可以知道,它们只是数字,并没有提供类型或长度信息。另外,指向 int 的指针的长度与指向 double 的指针相同。它们都是地址,但由于程序里声明了指针的类型,因此程序知道 pd 是 8 个字节的 double 值,pt 是 4 个字节的 int 值。
nights value = 1001 : location = 000000E4D5CFFAE4
int value = 1001: location = 00000278479181B0
Double value = 1e+07: location = 000002784791BDC0
location of pointer pd: 000000E4D5CFFB28
size of pt = 8: size of *pt = 4
size of pd = 8: size of *pd = 8
对于指针,另一点是 new 分配的内存块通常与常规变量声明分配的内存块不同。
变量 nights 和 pd 的值都存储在 栈(stack)的内存区域中,而 new 从被称为 堆(heap)或自由存储区(free store)的内存区域分配内存。
4.7.5 使用 delete 释放内存
delete运算符,它使得在使用完内存后,能够将其归还给内存池,这是通向最有效地使用内存的关键一步。
归还或释放(free)的内存可供程序的其他部分使用。使用 delete 时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)。
释放 ps 指向的内存,但不会删除指针 ps 本身。
注意:一定要配对地使用new和delete;否则将发生内存泄漏(memory leak),
也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。
不要尝试释放已经释放的内存块, C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete来释放声明变量所获得的内存:
int* ps = new int;
delete ps;
delete ps; // not ok now
int jugs = 5;
iint* pi = &jugs;
delete pi; // not allowed
警告:只能用delete来释放使用new分配的内存。然而,对空指针使用delete是安全的。
注意:使用 delete 的关键在于,将它用于 new 分配的内存。并不意味着要使用用于 new 的指针,而是用于 new 的地址。(很绕口)
int* ps = new int;
int* pq = ps;
delete pq;
一般来说,不要创建两个指向同一个内存块的指针,这将增加错误地删除同一个内存快两次的可能性。
4.7.6 使用 new 来创建动态数组
通常,对于大型数据(如数组、字符串和结构),应使用 new,这正是 new 的用武之地。
在编译时给数组分配内存被称为静态联编(static binding),意味着数组是在编译时加入到程序中的。
但使用 new 时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编(dynamic binding),意味着数组是在程序运行时创建的。这种数组叫作动态数组(dynamic array)。
使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。
使用new和delete时,应遵守以下规则。
- 不要使用delete来释放不是new分配的内存。
- 不要使用delete释放同一个内存块两次。
- 如果使用new [ ]为数组分配内存,则应使用delete [ ]来释放。
- 如果使用new [ ]为一个实体分配内存,则应使用delete(没有方括号)来释放。
- 对空指针应用delete是安全的。
为数组分配内存的通用格式 type_name * pointer_name = new type_name [num_elements] ;
int* psome = new int[10];
delete []psome;
psome是指向一个int(数组第一个元素)的指针。您的责任是跟踪内存块中的元素个数。(想念Java的第n天)
也就是说,由于编译器不能对psome是指向10个整数中的第1个这种情况进行跟踪,因此编写程序时,必须让程序跟踪元素的数目。
不理解,原话贴出来。
实际上,程序确实跟踪了分配的内存量,以便以后使用 delete []运算符时能够正确地释放这些内存。
但这种信息不是公用的,例如,不能使用 sizeof运算符来确定动态分配的数组包含的字节数。
- 使用动态数组
创建动态数组后,如何使用它呢?首先,从概念上考虑这个问题。下面的语句创建指针psome,它指向包含10个int值的内存块中的第1个元素。
可以将它看作是一根指向该元素的手指。(偏移量与数据类型有关)
假设 int 占4个字节,则将手指沿正确的方向移动4个字节,手指将指向第2个元素。总共有10个元素,这就是手指的移动范围。因此,new语句提供了识别内存块中每个元素所需的全部信息。
实际上,在 C 和 C++ 内部都使用指针来处理数组。数组 基本等价 指针。
由于psome指向数组的第1个元素,因此*psome是第1个元素的值。
只要把指针当作数组名使用即可。也就是说,对于第1个元素,可以使用 psome[0],而不是 *psome;对于第2个元素,可以使用 psome[1],依此类推。
//使用 new 来创建动态数组,关于动态数组的两个基本问题
//1.使用 new 创建动态数组
//只要将数组的元素类型和元素数目告诉 new即可。必须在类型名后加上方括号,其中包含元素数目。
//2.使用动态数组
double* p3 = new double[3];
p3[0] = 0.2;
p3[1] = 0.5;
p3[2] = 0.8;
cout << "p3[1] is " << p3[1] << '\n';
p3 += 1; //数组名和指针之间的根本差别,不能修改数组名的值。但指针是变量,可以修改。
cout << "Now p3[0] is " << p3[0] << " and p3[1] is " << p3[1] << '\n';
p3 -= 1; //如果没有这一行会怎么样?
delete[] p3;
将指针 p3 当作数组名来使用, p3[0]为第1个元素,依次类推。
注意将 p3 加 1 的效果。表达式 p3[0] 现在指的是数组的第2个值。因此,将 p3 加 1 导致它指向第 2 个元素而不是第 1 个。将它减 1 后,指针将指向原来的值,这样程序便可以给delete[]提供正确的地址。
p3[1] is 0.5
Now p3[0] is 0.5 and p3[1] is 0.8
4.8 指针、数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer arithmetic) 和 C++ 内部处理数组的方式。
首先,我们来看一看算术。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。
在多数情况下, C++将数组名解释为数组第1个元素的地址。
double* pw = wages;
wages = &wages[0];
为表明情况确实如此,该程序在表达式 &stacks[0] 中显式地使用地址运算符来将 ps 指针初始化为 stacks 数组的第 1个元素。
//4.8 指针和数组基本等价的原因在于指针算术(pointer arithmetic) 和 C++内部处理数组的方式。
//C++将数组名解释为地址。
double wages[3] = { 10000.0, 20000.0, 30000.0 };
short stacks[3] = { 3, 2, 1 };
//以下是获取数组地址的两种方法
double* pw = wages;
short* ps = &stacks[0];
cout << "pw = " << pw << ", *pw = " << *pw << "\n"; //..,10000
pw += 1;
cout << "add 1 to the pw pointer : \n" << "pw = " << pw << ", *pw = " << *pw << "\n\n"; //..,20000
cout << "ps = " << ps << ", *ps = " << *ps << '\n'; //..,3
ps += 1;
cout << "add 1 to the ps pointer: \n" << "ps = " << ps << ", *ps = " << *ps << "\n\n"; //..,2
cout << "access two elements with array notation\n";
cout << "stacks[0] = " << stacks[0] << ", stacks[1] = " << stacks[1] << '\n'; //3,2
cout << "access two elements with pointer notation\n";
cout << "*stacks = " << *stacks << ", *(stacks + 1) = " << *(stacks + 1) << '\n'; //3,2
cout << sizeof(wages) << " = size of wages array.\n ";
cout << sizeof(pw) << " = size of pw pointer.\n";
前提知识,将指针变量加 1 后,其增加的值等于指向的类型占用的字节数。short占用 2个字节,double占用 8个字节。
程序将 pw加 1。正如前面指出的,这样数字地址值将增加 8,这使得 pw的值为第 2个元素的地址。
程序对 ps加 1。这一次由于ps指向的是 short类型,而short占用2个字节,因此将指针加 1时,其值将增加 2。结果是,指针也指向数组中下一个元素。
*(stacks + 1) 等价于 stacks[1];
arrayname[i] becomes *(arrayname + i)
//如果使用的是指针,而不是数组名,则C++也将执行同样的转换:
pointername[i] becomes *(pointername + i)
因此,在很多情况下,可以相同的方式使用指针名和数组名。对于它们,可以使用数组方括号表示法,也可以使用解除引用运算符(*)。
在多数表达式中,它们都表示地址。
区别之一是,可以修改指针的值,而数组名是常量:
pointername = pointername + 1;
arrayname = arrayname + 1; //not allowed
另一个区别是,对数组应用 sizeof运算符得到的是数组的长度,而对指针应用 sizeof得到的是指针的长度,即使指针指向的是一个数组。
例如,pw和 wages指的是同一个数组,但对它们应用 sizeof运算符得到的结果是:24与 8(64位系统,指针一定是8字节)。
pw = 00000046446FF908, *pw = 10000
add 1 to the pw pointer :
pw = 00000046446FF910, *pw = 20000
ps = 00000046446FF934, *ps = 3
add 1 to the ps pointer:
ps = 00000046446FF936, *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.
总之,使用 new来创建数组以及使用指针来访问不同的元素很简单。只要把指针当作数组名对待即可。然而,要理解为何可以这样做。
4.8.2 指针小结
//1.声明指针
typeName* pointerName;
double* pn;
char* pc;
//其中, pn和 pc都是指针,而 double *和 char *是指向 double的指针和指向 char的指针。
//2.给指针赋值
//应将内存地址赋给指针。可以对变量名应用 &运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址。
double* pn;
double* pa;
char* pc;
double bubble = 3.2;
pn = &bubble;
pc = new char;
pa = new double[30];
3.对指针解除引用
对指针解除引用意味着获得指针指向的值。
对指针应用 解除引用或 间接值运算符(*)来解除引用。
因此,如果像上面的例子中那样,pn 是指向 bubble的指针,则 *pn是指向的值,即3.2。
另一种对指针解除引用的方法是使用数组表示法,例如,pn[0]与 *pn是一样的。决不要对未被初始化为适当地址的指针解除引用。
4.区分指针和指针所指向的值
如果 pt是指向 int的指针,则 *pt不是指向 int的指针,而是完全等同于一个 int类型的变量。
pt 才是指针。
int* pt = new int;
*pt = 5;
5.数组名
在多数情况下,C++将数组名视为数组的第一个元素的地址。
一种例外情况是,将 sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节)。
6.指针算术
C++允许将指针和整数相加。
加 1的结果等于原来的地址值加上指向的对象占用的总字节数。
还可以将一个指针减去另一个指针,获得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数组(也可以指向超出结尾的一个位置)时,这种运算才有意义,这将是两个元素的间隔。
int tacos[10] = {5, 2, 4, 8, 1, 3, 3, 4, 6, 9};
int* pt = tacos;
pt = pt + 1;
int* pe = &tacos[9];
int diff = pe - pt;
7.数组的动态联编和静态联编
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置
使用 new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。
使用完这种数组后,应牢记 delete[] 指针释放内存。
8.数组表示法和指针表示法
使用方括号数组表示法等同于对指针解除引用。
数组名和指针变量都是如此,因此对于指针和数组名,既可以使用指针表示法,也可以使用数组表示法。
int* pt = new int[10];
*pt = 5; //set element number O to 5
pt[0] = 6; //reset element number O to 6
pt[9] = 44; //set tenth element (element number 9) to 44
int coats[10];
*(coats + 4) = 12; //set coats[4] to 12
*(coats[4]) = 12; //???
牢记,基地址与偏移地址。
4.8.3 指针和字符串
char flower[10] = "rose";
cout << flower << " s are red\n"; //rose s are red
数组名是第一个元素的地址,因此 cout语句中的 flower是包含字符 r的 char元素的地址。 cout对象认为char的地址是字符串的地址。
关键在于 flower是一个 char的地址。这意味着可以将指向 char的指针变量作为 cout的参数,因为它也是 char的地址。当然,该指针指向字符串的开头。
为了与cout对字符串输出的处理保持一致,这个用引号括起的字符串也应当是一个地址。
在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。
这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。
//4.8.3 指针和字符串
char flower[10] = "rose";
cout << flower << " s are red\n\n" ;
char animal[20] = "bear";
const char* bird = "wren";
char* ps;
cout << animal << " and " << bird << '\n';
cout << "Enter a kind of animal: ";
cin >> animal;
//获取地址
ps = animal;
cout << ps << '!\n';
cout << "Before using strcpy(): \n";
cout << animal << " at " << (int*)animal << '\n';
cout << ps << " at " << (int*)ps << '\n';
//创建副本
ps = new char[strlen(animal) + 1];
strcpy(ps, animal);
cout << "After using strcpy():\n";
cout << animal << " at " << (int*)animal << endl;
cout << ps << " at " << (int*)ps << endl;
delete[] ps;
4.8.4 使用 new创建动态结构
创建数组时知道运行时优于编译时,结构亦是,由于类与结构非常相似,动态结构也适用于类。
new 用于结构分两步,创建结构和访问成员。要创建结构,要同时使用结构类型和 new。
inflatable* ps = new inflatable;
这将把足以存储 inflatable结构的一块可用内存的地址赋给 ps。这种句法和 C++的内置类型完全相同。
- 难点在访问成员。
创建动态结构时,不能将成员运算符句点用于结构名,因为 new结构没有名称,只知道地址。
C++专门提供了箭头成员运算符 ->,用于指向结构的指针,就像点运算符可用于结构名一样。
区分 .与 ->,看结构标识符是结构名或指向结构的指针。
另一种访问结构成员的方式是,(*ps).price。
- new 未命名的结构,并演示了两种访问结构成员的指针表示法。
struct inflatable
{
char name[20];
float volume;
double price;
};
inflatable* ps = new inflatable;
cout << "Enter name of inflatable item: ";
cin.get(ps->name, 20);
cout << "Enter volume in cubic feet: ";
cin >> (*ps).volume;
cout << "Enter price: $";
cin >> ps->price;
cout << "Name: " << (*ps).name << '\n';
cout << "Volume: " << ps->volume << " cubic feet.\n";
cout << "Price: $" << ps->price << '\n';
delete ps;
Enter name of inflatable item: Fabulous Frodo
Enter volume in cubic feet: 1.4
Enter price: $27.89
Name: Fabulous Frodo
Volume: 1.4 cubic feet.
Price: $27.89
- 自定义函数 getname()
该函数将输入读入到一个大型的临时数组中后使用 new[]创建一个刚好能够存储该输入字符串的内存块,返回一个指向该内存块的指针。
char* getname(void); // 函数原型也可以放在 main内
char* name;
name = getname();
cout << name << " at " << (int*)name << '\n';
delete[]name;
name = getname();
cout << name << " at " << (int*)name << '\n';
delete[]name;
//以下是 main函数之外
char* getname()
{
char temp[80];
cout << "Enter last name: ";
cin >> temp;
char* pn = new char[strlen(temp) + 1];
//strcpy(pn, temp);
return pn;
}
4.8.5 自动存储、静态存储和动态存储
自动存储(变量,栈)
静态存储(严格限制寿命的变量)
动态存储(指针变量,自由存储空间或堆)
在栈中,自动添加和删除机制使得占用的内存总是连续的,但 new和 delete的相互影响可能导致占用的自由存储区不连续。
4.10 数组的替代品
4.10.1 模板类 vector
模板类 vector类似于 string类,也是一种动态数组。
可以在运行时设置 vector对象的长度,可在末尾附加新数据,可在中间插入新数据。
vector是使用 new创建动态数组的替代品
首先,要使用 vector对象,必须包含头文件 vector
其次,vector包含在名称空间 std中
第三,模板使用不同的语法来指出它存储的数据类型
第四,vector类使用不同的语法来指定元素数
vector<typeName> vt(n_elem); //其中参数 n_elem可以是整型常量,也可以是整型变量。
4.10.2 模板类 array(C++11)
vector类的功能比数组强大,但缺点是效率低。数组高效但是不够方便和安全。
模板类 array也位于名称空间std中,与数组一样,array对象的长度固定,也使用栈,而不是自由存储区,因此其效率与数组相同,但更方便和安全。
array<typeName, n_elem> arr; //其中参数 n_elem不能是变量。
4.10.3 比较数组、vector对象和array对象
double a1[4] = { 1.2, 2.4, 3.6, 4.8 };
vector<double> a2(4);
a2[0] = 1.0 / 3.0;
a2[1] = 1.0 / 5.0;
a2[2] = 1.0 / 7.0;
a2[3] = 1.0 / 9.0;
array<double, 4> a3 = { 3.14, 2.72, 1.62, 1.41 };
array<double, 4> a4;
a4 = a3;
cout << "a1[2]: " << a1[2] << " at " << &a1[2] << '\n';
cout << "a2[2]: " << a2[2] << " at " << &a2[2] << '\n';
cout << "a3[2]: " << a3[2] << " at " << &a3[2] << '\n';
cout << "a4[2]: " << a4[2] << " at " << &a4[2] << '\n';
// 等同于 *(a1-2)找到 a1指向的地方,向前移两个 double元素,虽然 C++不会检查错误,但编译器会。
//a1[-2] = 20.2;
//cout << "a1[-1]: " << a1[-2] << " at " << &a1[-2] << '\n';
//cout << "a3[2]: " << a3[2] << " at " << &a3[2] << '\n';
//cout << "a4[2]: " << a4[2] << " at " << &a4[2] << endl;
//对于中括号表示法可能存在的不安全,可用 vector和 array对象的成员函数at()。
return 0;
new运算符允许在程序运行时为数据对象请求内存。
该运算符返回获得内存的地址,可以将这个地址赋给一个指针,程序将只能使用该指针来访问这块内存。
如果数据对象是简单变量,则可以使用解除引用运算符(*)来获得其值;
如果数据对象是数组,则可以像使用数组名那样使用指针来访问元素;
如果数据对象是结构,则可以用指针解除引用运算符(->)来访问其成员。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)