【C++ Primer Plus】学习笔记 5【指针 上】


前言

终于进入最难的指针啦,加油加油


一、指针和自由存储空间

指针是一个变量,其存储的是值的地址,而不是值本身。在讨论指针之前,我们先看一看如何找到常规变量的地址。只需对变量应用地址运算符(&),就可以获得它的位置:例如,如果home是一个变量,则&home是它的地址。

指针与 C++基本原理
面向对象编程与传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。运行阶段决策就好比度假时,选择参观哪些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先设定的日程安排。
运行阶段决策提供了灵活性,可以根据当时的情况进行调整。例如,考虑为数组分配内存的情况。传统的方法是声明一个数组。要在C++中声明数组,必须指定数组的长度。因此,数组长度在程序编译时就设定好了;这就是编译阶段决策。可能在80%的情况下,一个包含20个元素的数组足够了,但程序有时需要处理200个元素。为了安全起见,使用了一个包含200个元素的数组。这样,程序在大多数情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后,可以这次告诉它只需要 20个元素,而还可以下次告诉它需要 205个元素。为使用这种方法,语言必须允许在程序运行时创建数组。C++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。
在运行阶段做决策并非 OOP 独有的,但使用 C++编写这样的代码比使用C语言简单。

将地址视为指定的量,而将值视为派生量。一种特殊类型的变量——指针用于存储值的地址。因此,指针名表示的是地址。* 运算符被称为间接值或解除引用运算符,将其应用于指针,可以得到该地址处存储的值(这和乘法使用的符号相同;C+根据上下文来确定所指的是乘法还是解除引用)。例如,假设 manly 是一个指针,则 manly 表示的是一个地址,而*manly 表示存储在该地址处的值。*manly 与常规 int 变量等效。示例代码如下:

// pointer.cpp--our first pointer variable
#include <iostream>
int main()
{
	using namespace std;
	int updates =6;
	int *p_updates;
	p_updates = &updates;//assign address of int to pointer
	
	cout <<"Values:updates="<<updates;
	cout << ", *p_updates="<<*p_updates << endl;

	cout<<"Addresses:&updates="<<&updates;
	cout<<",p_updates="<<p_updates << endl;
	
	*p_updates =*p_updates +1;
	cout<<"Now updates="<< updates << endl;
	return 0;
}

在这里插入图片描述
从中可知,int 变量 updates 和指针变量p_updates 只不过是同一枚硬币的两面。变量 updates 表示值,并使用&运算符来获得地址;而变量p_updates表示地址,并使用运算符来获得值。由于p_updates 指向 updates,因此p_updates 和 updates 完全等价。可以像使用 int 变量那样使用p_updates,甚至可以将值赋给p_updates。这样做将修改指向的值,即 updates。
在这里插入图片描述

1. 声明和初始化指针

计算机需要跟踪指针指向的值的类型。例如,char的地址与double的地址看上去没什么两样,但 char 和 double 使用的字节数是不同的,它们存储值时使用的内部格式也不同。因此,指针声明必须指定指针指向的数据的类型。
例如,前一个示例包含这样的声明:

int *p_updates;

这表明,* p_updates 的类型为int。由于 * 运算符被用于指针,因此p_updates 变量本身必须是指针。我们说 p_updates 指向 int 类型,我们还说 p_updates 的类型是指向 int 的指针,或 int*。可以这样说,p_updates是指针(地址),而*p_updates是int,而不是指针。
在这里插入图片描述
顺便说一句, * 运算符两边的空格是可选的。
C语言使用这种格式int *ptr; ,这强调 *ptr 是一个int类型的值。
C++使用这种格式 int* ptr;,这强调的是 int * 是一种指向int 的指针类型。

在哪里添加空格对于编译器来说没有任何区别,甚至可以这样写int*ptr;
但要知道的是,下面的声明是创建一个指针(p1)和一个int变量(p2):

int* p1,p2;

对每个指针变量名,都需要使用一个*。

注意:在C++中,int*是一种复合类型,是指向int的指针。
可以用同样的句法来声明指向其他类型的指针。

可以在声明语句中初始化指针。在这种情况下,被初始化的是指针,而不是它指向的值。也就是说,下面的语句将pt(而不是*pt)的值设置为&higgens:

int higgens =5;
int *pt= &higgens;

2. 指针的危险

在C++中创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤,忽略这一步无疑是自找麻烦,如下所示:

long *fellow;//create a pointer-to-long
*fellow = 223323;//place a value in never-never land

fellow 确实是一个指针,但它指向哪里呢?上述代码没有将地址赋给fellow。那么 223323 将被放在哪里呢?我们不知道。由于fellow 没有被初始化,它可能有任何值。不管值是什么,程序都将它解释为存储223323的地址。如果fellow的值碰巧为1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址。fellow指向的地方很可能并不是所要存储223323的地方。这种错误可能会导致一些 bug。
一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。

3. 指针和数字

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

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

左边是指向int的指针,因此可以把它赋给地址,但右边是一个整数,这条语句并没有告诉程序,这个数字就是一个地址。要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型:

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

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

4. 使用 new 来分配内存

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

int *pn = new int;

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

int higgens;
int *pt= &higgens;

在这两种情况(pn 和 pt)下,都是将一个int 变量的地址赋给了指针。在第二种情况下,可以通过名称 higgens 来访问该 int,在第一种情况下,则只能通过该指针进行访问。这引出了一个问题:pn指向的内存没有名称,如何称呼它呢? 我们说 pn指向一个数据对象,这里的“对象”不是“面向对象编程”中的对象,而是一样“东西”。术语“数据对象”比“变量”更通用,它指的是为数据项分配的内存块。因此,变量也是数据对象,但pn指向的内存不是变量。乍一看,处理数据对象的指针方法可能不太好用,但它使程序在管理内存方面有更大的控制权。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:

typeName *pointer_name = new typeName;

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

using namespace std;
int nights=1001;
int *pt =new int;//allocate space for an int
*pt = 1001;//store a value there

cout <<"nights value ="<<nights<<":location"<< &nights << endl;
cout << "int value="<<*pt<<":location="<< pt << endl;

double *pd=new double;//allocate space for a double
*pd = 10000001.0;//store a double there
cout << "double value ="<<*pd<<":location="<< pd << endl;
cout <<"location of pointer pd:"<< &pd << endl;
cout <<"size of pt="<< sizeof(pt);
cout <<":size of *pt="<< sizeof(*pt)<< endl;
cout<<"size of pd="<< sizeof pd;
cout<<":size of *pd="<< sizeof(*pd)<< endl;
return 0;

在这里插入图片描述
该程序指出了必须声明指针所指向的类型的原因之一:地址本身只指出了对象存储地址的开始,而没有指出其类型(使用的字节数)。从这两个值的地址可以知道,它们都只是数字,并没有提供类型或长度信息。另外,指向int的指针的长度与指向 double 的指针相同。它们都是地址,但由于声明了指针的类型,因此程序知道*pd是8个字节的double值,*pt是4个字节的int值。
对于指针, new分配的内存块通常与常规变量声明分配的内存块不同。变量nights和 pd 的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。第9章将更详细地讨论这一点。

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

5. 使用 delete 释放内存

当需要内存时,可以使用new来请求,在使用完内存后用delete运算符能够将其归还给内存池。归还或释放(free)的内存可供程序的其他部分使用。使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的):

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

这将释放 ps指向的内存,但不会删除指针 ps本身。例如,可以将 ps 重新指向另一个新分配的内存块。一定要配对地使用new和delete;否则将发生内存泄漏,也就是说,被分配的内存再也无法使用了。如果内存泄漏严重,则程序将由于不断寻找更多内存而终止。
不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。另外,不能使用delete 来释放声明变量所获得的内存:

int *ps =new int;// ok
delete ps;//ok
delete ps;//not ok now
int jugs =5;// ok
int *pi = &jugs;// ok
delete pi;// not allowed,memory not allocated by new

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

int *ps =new int;//allocate memory
int *pq = ps;//set second pointer to same block
delete pq;//delete with second pointer

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

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

如果程序只需要一个值,则可能会声明一个简单变量,因为对于管理一个小型数据对象来说,这样做比使用 new 和指针更简单。而对于大型数据(如数组、字符串和结构),应使用 new。例如,假设要编写一个程序,它是否需要数组取决于运行时用户提供的信息。如果通过声明来创建数组,则在程序被编译时将为它分配内存空间。不管程序最终是否使用数组,数组都在那里,它占用了内存。在编译时给数组分配内存被称为静态联编,意味着数组是在编译时加入到程序中的。但使用 new 时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编,意味着数组是在程序运行时创建的。这种数组叫作动态数组(dynamic array)。使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。
下面来看一下关于动态数组的两个基本问题:如何使用 C++的 new 运算符创建数组以及如何使用指针访问数组元素。

1.使用new创建动态数组

在 C++中,创建动态数组很容易;只要将数组的元素类型和元素数目告诉new即可。必须在类型名后加上方括号,其中包含元素数目。例如,要创建一个包含10个int元素的数组,可以这样做:

int *psome =new int[10];//get a block of 10 ints

new运算符返回第一个元素的地址。在这个例子中,该地址被赋给指针psome。当程序使用完 new 分配的内存块时,应使用 delete 释放它们。然而,对于使用 new 创建的数组,应使用另一种格式的 delete 来释放:

delete []psome;
//free a dynamic array

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

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

现在我们回过头来讨论动态数组。psome是指向一个int(数组第一个元素)的指针。由于编译器不能对psome 是指向10个整数中的第1个这种情况进行跟踪,不能使用 sizeof运算符来确定动态分配的数组包含的字节数。为数组分配内存的通用格式如下:

type_name *pointer_name = new type_name [num elements];

使用 new 运算符可以确保内存块足以存储num_elements 个类型为type_name 的元素,而 pointer_name将指向第1个元素。下面将会看到,可以以使用数组名的方式来使用pointer_name。

2.使用动态数组

下面的语句创建指针psome,它指向包含10个int值的内存块中的第1个元素:

int *psome =new int[10];//get a block of 10 ints

可以将它看作是一根指向该元素的手指。假设int占4个字节,则将手指沿正确的方向移动4个字节,手指将指向第2个元素。总共有10个元素,这就是手指的移动范围。因此,new语句提供了识别内存块中每个元素所需的全部信息。
对于第1个元素,可以使用 psome[0],而不是*psome;对于第2个元素,可以使用psome[1],依此类推。这样,使用指针来访问动态数组就非常简单了,虽然还不知道为何这种方法管用。可以这样做的原因是,C和 C++内部都使用指针来处理数组。数组和指针基本等价是C和C++的优点之一(这在有时候也是个问题,但这是另一码事)。

using namespace std;
double *p3=new double[3];//space for 3 doubles
p3[0]=0.2;
p3[1]=0.5;
p3[2]=0.8;
cout << "p3[1] is " << p3[1] << ".\n";
p3 = p3 + 1;// increment the pointer
cout <<"Now p3[0] is"<<p3[0]<<" and ";
cout << "p3[1] is "<< p3[1]<< ".\n";
p3 = p3 - 1;// point back to beginning
delete [] p3;// free the memory
return 0;

在这里插入图片描述
以上代码将指针p3当作数组名来使用,p3[0]为第1个元素,依次类推。下面的代码行指出了数组名和指针之间的根本差别:p3=p3+1;不能修改数组名的值,但指针是变量,因此可以修改它的值。请注意将p3加1的效果。表达式 p3[0]现在指的是数组的第2个值。因此,将p3加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给 delete[]提供正确的地址。
相邻的 int 地址通常相差2个字节或4个字节,而将p3加1后,它将指向下一个元素的地址,这表明指针算术有一些特别的地方.

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

指针和数组基本等价的原因在于指针算术和 C++内部处理数组的方式。算术将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。
将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short 的指针加1后,如果系统对 short 使用2个字节存储,则指针值将增加 2。
C++将数组名解释为地址,因此,在很多情况下,可以相同的方式使用指针名和数组名。对于它们,可以使用数组方括号表示法,也可以使用解除引用运算符(*)。在多数表达式中,它们都表示地址。区别之一是,可以修改指针的值,而数组名是常量。另一个区别是,对数组应用 sizeof运算符得到的是数组的长度,而对指针应用sizeof得到的是指针的长度,即使指针指向的是一个数组。

1. 指针小结

1.声明指针

要声明指向特定类型的指针,请使用下面的格式:

typeName *pointerName;

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是指向的值,下面是一些示例:

cout <<*pn;//print the value of bubble
*pc = 'S';//place 's'into the memory location whose address is pc

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

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

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

5.数组名

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

6.指针算术

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

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

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

2.指针和字符串

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

数组名是第一个元素的地址,因此cout语句中的 flower 是包含字符r的char 元素的地址。cout 对象认为 char 的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。总之,如果给 cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。
这里的关键不在于 flower是数组名,而在于flower 是一个char 的地址。这意味着可以将指向 char 的指针变量作为 cout 的参数,因为它也是 char 的地址。当然,该指针指向字符串的开头,稍后将核实这一点。前面的 cout 语句中最后一部分的情况如何呢?如果flower 是字符串第一个字符的地址,则表达式“s are red\n"是什么呢?为了与 cout对字符串输出的处理保持一致,这个用引号括起的字符串也应当是一个地址。在 C++中,用引号括起的字符串像数组名一样,也是第一个元素的地址。上述代码不会将整个字符串发送给cout,而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。与逐个传递字符串中的所有字符相比,这样做的工作量确实要少。
注意:在cout和多数 C++表达式中,char数组名、char 指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组。


指针这章知识点太多,分成两章发吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值