(欧) 第7章 指针与引用

来源:互联网 发布:mac刺客信条2闪退 编辑:程序博客网 时间:2024/05/30 04:11

指针是C++提供的一种颇具特色的数据类型,允许直接获取和操纵数据地址,实现动态存储分配、

一个数据对象的内存地址称为该数据对象的指针。指针可以表示各种数据对象,如简单变量、数组、数组元素、结构体,甚至函数。


1 指针和引用的区别

(1)非空区别。任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。而指针可以指向空值。 不存在指向空值的引用说明使用引用的代码效率比使用指针高。

(2)合法性区别。在使用引用前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。

(3)可修改区别。指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。

(4)应用区别。以下情况应该使用指针:1)考虑到存在不指向任何对象的可能,在这种情况下,能够设置指针为空。2)需要能够在不同时刻指向不同对象,可以改变指针指向。

如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,应该使用引用。


int iv3;//trueint &recv;//false,声明一个引用,但不能为空们必须同时初始化 int *pi;//true*pi = 5;//false,整数指针pi并没有指向实际的地址,这种情况下就给它赋值是错误的。因为赋的值不知道放在哪。 pi = &iv3;//true


3 下述5个函数哪个能够成功进行两个数的交换?

void swap1(int p,int q) {//传递值的副本,当函数生命周期结束,p、q所在的栈也就被删除了。 int temp;temp=p;p=q;q=temp; } void swap2(int *p,int *q) {//传的个是地址 int *temp; //新建了一个指针,但是没有分配内存 。*temp=*p;// 这2行不符合逻辑,这行代表复制而不是指向。把*p所指向的内存里的值(实参值)复制到*temp所指向的内存。 //于是系统在复制时临时给了一个随机地址,让它存值。分配的 随机地址是个意外,且函数结束后不收回,造成内存泄露。 *p=*q;*q=*temp; } //swap2到底能否实现两数交换,需要看编译器而定。若在DEV-C++可以通过,但是在严格的VS2008就不能。 void swap3(int *p,int *q) {int *temp;//新建了一个指针,但是没有分配内存 。temp=p; //注意此处是指向,而不是赋值。temp指向*p所指向的地址 (实参) p=q;q=temp; } //swap3不能实现两数转换,因为函数体内只是指针的变化,而对地址中的值却没有改变。 void swap4(int *p,int *q) {int temp;temp=*p;*p=*q;*q=temp; } void swap5(int &p,int &q) {int temp;temp=p;p=q;q=temp; } 

4  分析下列程序

#include<iostream>//#include<string.h>//#include<stdlib.h>void GetMemory(char *p,int num) {p=(char *)malloc(sizeof(char)*num);} int main(){char *str=NULL;GetMemory(str,100);strcpy(str,"hello");return 0;}

结果:程序崩溃。因为GetMemory不能传递动态内存,main函数中的str一直是NULL。

分析:void GetMemory(char *p,int num)中的*p实际上是主函数中str的一个副本,编译器总是要为函数的每个参数制作临时副本。在本例中,p申请了新的内存,只是把p所指的内存地址改变了,但是str丝毫未变。因为函数void GetMemory(char *p,int num)并不能输出任何东西。 事实上,每执行一次GetMemory就会申请一块内存,但申请的内存却不能有效释放,结果是内存一直被独占,最终造成内存泄露


如果一定要用指针参数去申请内存,应该采用指向指针的指针,传str的地址给函数GetMemory。

#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;void GetMemory(char **p,int num) {*p=(char *)malloc(sizeof(char)*num);} int main(){char *str=NULL;GetMemory(&str,100);strcpy(str,"hello");cout<<*str<<endl;//字符串中某一个字符的值,默认是首字符cout<<str<<endl;//字符串的值cout<<&str<<endl;//字符串的地址值return 0;}


指向指针的指针不同理解,用函数返回值来传递动态内存。

#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;char *GetMemory(char *p,int num) {p=(char *)malloc(sizeof(char)*num);return p;} int main(){char *str=NULL;GetMemory(str,100);strcpy(str,"hello");return 0;}


#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;void GetMemory(int *z) {*z=5;} int main(){int v;GetMemory(&v);cout<<v<<endl;return 0;}

5  分析下列程序
char *strA() {char str[]="hello world";return str;}
解析:str存的地址是函数strA栈帧里“hello world”的首地址。函数调用完成,栈帧恢复到调用strA之前的状态,临时空间被重置,堆栈回缩,str栈帧不再属于应该访问的范围。存在于strA栈帧里的“hello world”也不应该访问了。这段程序可以正确输出结果,但是这种访问违背了函数的栈帧机制。

答案:因为函数返回的是局部变量的地址,当调用这个函数后,这个局部变量str就释放了,所以返回的结果是不确定的且不安全,随时都有被收回的可能。


应该修改如下:

const char *strA() {char *str="hello world";return str;}

注意,字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区)。

char *c= "hello world";*c = 't';//false
c占用一个存储区域,但是局部区的数据是可以修改的。

char c[]= "hello world";c[0]='t';//true


6 分析下列程序:输出结果是1.

#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;class A{public:A() {m_a=1;m_b=2; }~A() {}void fun() {printf("%d%d",m_a,m_b);}private:int m_a;int m_b;};class B{public:B() {m_c=3;}~B() {}void fun() {printf("%d",m_c);}private:int m_c;};int main(){A a;B *pb=(B*)(&a);pb->fun() ;return 0;}
分析:考察内存偏移。 B *pb=(B*)(&a);,强制把a地址内容看出是一个B对象,pb指向的是a类的内存空间:pb->fun();

正常情况下,B类只有一个元素是int m_c,但是a类的内存空间中存放第一个元素的位置是m_a,pb指向的是对象的内存首地址,比如0x22ff58,当pb->fun()调用B::func()来打印m_c时,编译器对m_c的认识就是m_c距离对象的偏移量0,于是打印了对象a首地址的编译量0x22ff58+0变量值。


7 分析下列代码

int *ptr;ptr=(int *)0x8000;*ptr=0xaabb;
运行错误,这种做法会给一个指针分配一个随意的地址,很危险。不管这个指针是否被使用过,这么做是不允许的。


8  分析下列程序:函数指针错误使用问题。

#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;int max(int x,int y) {return x>y?x:y;}int main(){int x,y;int max(x,y);//int max(int ,int);int *p=&max;//int *p(int ,int)=&max;int a,b,c,d;printf("input three numbers:");scanf("%d%d%d",a,b,c);//scanf("%d%d%d",&a,&b,&c);d=(*p)((*p)(a,b),c);printf("%d,%d,%d,%d",a,b,c,d);return 0;}


9  重点辨析:

long (*fun)(int)//函数指针,指向函数的指针,这个指针返回值是long,所带参数是int。

long *fun(int)//指针函数,是一个带有整数参数并返回一个长整形变量的指针的函数。

float(**def)[10];//def是一个二级指针,它指向的是一个一维数组的指针,数组的元素都是floatdouble*(*gh)[10];//gh是一个指针,指向一个一维数组,数组元素都是double*double(*f[10])();//f是一个数组,f有10个元素,元素都是函数的指针,指向的函数类型是没有参数且返回double的函数int*((*b)[10]);//一维数组指针Long (*fun)(int);//函数指针int (*(*F)(int,int))(int)//F是一个函数的指针,指向的函数的类型是有两个int参数并且返回一个函数指针函数,返回函数指针指向有一个int参数且返回int函数


10 分析下列程序输出:1 11 2 2 11

#include<iostream>#include<string.h>#include<stdlib.h>using namespace std;int max(int x,int y) {return x>y?x:y;}int main(){int v[2][10]={{1,2,3,4,5,6,7,8,9,10},{11,12,13,14,15,16,17,18,19,20}};int (*a)[10]=v;//数组指针cout<<**a<<endl;cout<<**(a+1)<<endl;cout<<*(*a+1)<<endl;cout<<*(a[0]+1)<<endl;cout<<*(a[1])<<endl;return 0;}
分析:定义了一个指针指向一个10个int型元素的数组。a+1表明a指针向后移动1*sizeof(数组大小),a+1向后移动40个字节。 *a+1仅对这一行向后移动4个字节。


11 分析下列程序输出:2 5

int a[]={1,2,3,4,5};int *ptr=(int*)(&a+1);printf("%d %d", *(a+1),*(ptr-1));
分析:首先a表示一个一行五列的数组,在内存中表示为一个5个元素的序列。int *ptr=(int*)(&a+1);表示,指向a数组的第6个元素,尽管这个元素不存在。那么显然,(ptr-1)所指向的数据就是a数组的第5个元素即5。

如果存在如下的数组:

int b[2][5]={1,2,3,4,5,6,7,8,9,10};int (*a)[5]=b;int *ptr=(int*)(&a+1);
那么显然 int *ptr=(int*)(&a+1)=b[1]=6.
本质上,b的数据分布还是按照1、2、3、4、5、6、7、8、9、10分布的,所谓b[0]和b[1]实际上只是指向其中一个元素的指针。


数组名本身就是指针,再加&,就变成双指针,这里双指针是指二维数组,加1,就是数组整体江上一行,ptr指向a的第6个元素。


12 野指针,也叫迷途指针,悬浮指针,失控指针,是当对一个指针进行delete操作后,这样会释放它所指向的内存,并没有把它设置为空时产生的。而后,如果没有重新赋值就试图再次使用该指针,引起的结果是不可预料。

删除指针后不要再使用它。虽然这个指针仍然指向原来的内存区域,但是编译器已经把这块内存区域分配给了其他的数据。再次使用这个指针会导致程序崩溃。更糟糕的是,程序可能表明运行的很好,过不了几分钟就崩溃了。为了安全起见,删除一个指针后,把它设置尾空指针。这样可以消除危害。


13 空指针 和 迷途指针区别

当delete一个指针时候,实际上仅仅是让编译器释放内存,但指针本身依然存在。这时它是一个迷途指针。

当使用以下语句时,可以把迷途指针改为空指针。MyPtr=0;


通常,如果删除一个指针后又把它删除一次,程序就会变得非常不稳定,任何情况都有可能发生。但是如果只是删除了一个空指针,则什么都不会发生,这样做很安全。

使用野指针或空指针是非法的,而且有可能造成程序崩溃。如果是空指针,尽管同样是崩溃,但它比野指针造成的崩溃相比是一种可预料的崩溃。这样调试起来比较方便。


14 C++有了malloc/free,为什么还需要new/delete ?

malloc/free是C++/C语言的标准库函数,new/delete是C++运算符。都是应用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。


因此C++语言需要一个能完成动态内存分配和初始化构造的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new/delete不是库函数,而是运算符。


15 分析下列程序:输出the

#include<iostream>using namespace std;int main(){char *a[]={"hello","the","world"};char **pa=a;pa++;cout<<*pa<<endl;return 0;}
指针的指针。


16 句柄和指针的区别联系?

分析:句柄是一个32位的整数,实际是Windows在内存中维护的一个对象内存物理地址列表的整数索引。因为Windows的内存管理经常会将当前空闲对象的内存释放掉,当需要时访问再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。程序将像访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及其物理地址。

句柄是一种指向指针的指针。Windows操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而这个地址本身是不变的。Windows内存管理器移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接知道对象具体在内存中的哪个位置。


Windows系统用句柄标记系统资源,隐藏系统的信息。只要知道有这个东西,然后去调用就行,它是个32bit的unit。指针则标记某个物理内存地址,两者是不同的概念。


 17  智能指针auto_ptr用法

A. std::auto_ptr <Object> pObj(new Object);//true,正确格式B. std::vector<std::auto_ptr<Object*> > object_vector;//false, auto_ptr放在vector是不合适的。因为auto_ptr的复制并不等价。C. std::auto_ptr<Object*> pObj (new Object);//falseD. std::vector<atd::auto_ptr<Object> > object_vector;//false,auto_ptr放在vector是不合适的。E. std::auto_ptr<Object> source() {return new Object;}//true,从new Object构造出一个auto_ptr<Object>
分析:auto_ptr放在vector是不合适的。因为auto_ptr的复制并不等价。当auto_ptr被复制时,原来的那一份会被删除。 《Exceptional C++》特别提到:尽管编译器不会对此给出任何警告,把auto_ptr放在container仍是不安全的。这个是因为我们无法告知这个container关于auto_ptr具有特殊的语义的情况。

解析:auto_ptr是安全指针。最初动机是使得下面的代码更安全:

void f() {T* pt(new T);/*...more code...*/delete pt;}
如果f()从没有执行delete语句(因为过早的return  或者是在函数体内部抛出了异常),动态分配的对象将没有被delete,一个典型的内存泄露。使其安全的一个简单方法是用一个“灵巧”的类指针对象包容这个指针,在其析构时自动删除此指针:

void f() {auto_ptr<T> pt(new T);/*...more code...*/}
现在,这个代码不再泄露T对象,无论是函数正常结束还是因为异常,因为pt的析构函数总在退栈过程中被调用。类似地,auto_ptr可以被用来安全地包容指针:

class C{public:C();/*...*/private:auto_ptr<CImpl> pimpl_; };// file c.ppC::C() : pimpl_(new CImpl) {}
现在析构函数不再需要删除pimpl_指针,因为auto_ptr将自动处理它。


18 this指针

对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this指针是这样的一个指针,它时时刻刻指向这个实例本身。

this指针易混的几个问题如下:

(1)This指针本质是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数。this指针只能在成员函数中使用,全局函数,静态函数不能使用this。

实际上成员函数默认第一个参数为T* const this。

如:

class A{ public: int func(int p) {}}
其中,func的原型在编译器看来是:int func(A* const this, int p);

(2) this在成员函数的开始前构造,在成员的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。

A a;a.func(10);
此处编译器将会编译成

A::func(&a,10);
this指针的传递效率高。


(3)this指针不占用对象的空间。

this相当于非静态成员函数的一个隐函的参数,不占对象的空间。它跟对象之间没有包含关系,只是当前调用函数的对象被它指向而已。

所有成员函数的参数,不管是否是隐含的,都不会占用对象的空间,只会占用参数传递时的栈空间,或者之间占用一个寄存器。

(4)this指针是什么时候创建的?
this在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当作C的struct使用。


(5)this指针存放在何处?堆、栈、还是其他?

this指针因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。

C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。

语法上,this是个指向对象的“常指针”,因此无法改变。它是一个指向相应对象的指针。

在实际应用的时候,this应该是个寄存器参数。这个不是语言规定的,而是“调用约定”。该约定规定参数自右向左入栈,由调用方负责平摊堆栈。


(6)this指针是如何传递给类中的函数的?绑定?还是在函数参数的首参数就是this指针?

大多数编译器通过ecx寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配。


(7)只有获得一个对象后,才能通过对象使用this指针。如果我们知道应该对象this指针的位置,可以直接使用吗?

this指针只有在成员函数中才有定义。因此你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道应该对象的this指针的位置(只有在成员函数里才有this指针的位置。)



《程序员面试宝典》 欧立奇 P65~ P88


原创粉丝点击