印象笔记同步分享:《程序员面试宝典》学习记录3
《程序员面试宝典》学习记录3
第7章 指针与引用
7.1 指针的基本问题
考点1:指针与引用的差别?
1)非空区别。在任何情况下都不能使用指向空值的引用。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高
2)合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空
3)可修改区别。指针与引用的另一个重要的区别是指针可以被重新赋值以指向另一个不同对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
补充区别的具体表现:
1)引用必须初始化,指针则不必
int &reiv; //无效int *p; //有效
2)引用初始化后不能被修改,指针可以改变所指向的变量
3)不存在指向空值的引用,但是存在指向空值的指针
4)引用没有const,指针有const
这里需要切记,使用const声明的同时必须要初始化
const double si; //无效
5)指针指向一块内存,它的内容是所指向存的地址,引用时某块内存的别名
6)sizeof 引用 得到的是所指向的变量的大小;sizeof 指针得到的是指针本身的大小
7)指针和引用的自增运算意义不一样
考点2:定义指针的时候一定要注意必须知道指针指向哪里?
例如:
int *pi;
*pi = 5;//这是不对的,因为我们不知道指向哪里
例如:
int *temp;
*temp = *p; //同理无效
例如:
int ptr;
ptr = (int)0x8000;
*ptr = oxaabb;
7.2 传递动态内存
考点1:两个数交换的两种形式
1)使用指针交换
void swap(int *p, int *q)
{
int temp;
temp = *p;
*p = *q;
*q = temp;
}
2)使用引用传递
void swap(int &p, int &q)
{
int temp;
temp = p;
p = q;
q = temp;
}
考点2:字符传递值和整型传递值
1)字符传递
#include <iostream>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并不能传递动态内存,str一直是NULL,而且每执行一次GetMemory就会申请一块内存,但申请的内存却不能被有效释放,一直独占着,最终找出内存泄露。
解决方案1:
如果要用指针参数去申请内存,我们可以采用指向指针的指针,传递str地址给函数
void GetMemory(char **p, int num);
GetMemory(&str,100);
解决方案2:
采用返回值比较方便,直接修改函数加上
return p;
str = GetMemory(str,100);
2)整型传递
#include<iostream>using namespace std;
void GetMemory2(int *z)
{
*z=5;
};
int main()
{
int v;
GetMemory2(&v);
cout << v << endl;
return 0;
}
GetMemory2函数把v的地址传进来,*z
是地址里的值,是v的副本;通过直接修改地址里的值,不需要有返回值,也把v给修改了,因为v所指向地址的值发生了改变。
考点3:区分数组字符串和指针字符串初始化
这个函数有什么问题?该如何修改?
char *strA()
{
char str[] = "hello world";
return str;
}
解析:这个str里存在的地址是函数strA栈里“hello world”的首地址。函数调用完成,栈帧恢复调用strA之前的状态,临时空间被重置,堆栈“回缩”,strA栈帧不再属于应该访问的范围。这段程序可以正确输出结果,但是这种访问方法违背了函数的栈帧机制。
但是只要另外一个函数调用的话,你就会发现,这种方式的不合理及危险性。
如果想获得正确的函数,改成下面这样就可以:
char *strA()
{
char *str = "hello world";
return str;
}
首先要搞清楚char *str
和 char str[]
:
char str[] = "hello world";
是分配一个局部数组。局部数组是局部变量,它所对应的是内存中的栈。局部变量的生命周期结束后该变量不存在了。
char *str = "hello world";
是指向了常量区的字符串,位于静态存储区,它在程序生命期内恒定不变,所以字符串还在。无论什么时候调用 strA,它返回的始终是同一个“只读”的内存块。
另外想要修改,也可以这样:
char *strA()
{
static char str[] = "hello world";
return str;
}
通过static开辟一段静态存贮空间。
答案:
因为这个函数返回的是局部变量的地址,当调用这个函数后,这个局部变量str就释放了,所以返回的结果是不确定的且不安全,随时都有被收回的可能。
考点4:深入字符串指针变量和字符数组的区别
1)分配内存char *pc ,str[100];
字符数组str分配100个字节的内存单元,用于存放100个字符。而系统只为指针变量pc分配4个存储单元,用于存放一个内存单元的地址。
2)初始化含义对于字符数组,是将字符串放到为数组分配的存储空间去,属于局部变量,而对于字符型指针变量,是先将字符串存放到内存,然后将存放字符串的内存起始地址送到指针变量pc中。3)赋值方式字符数组只能逐个元素赋值,字符串地址可以直接赋给字符指针变量。
str="I love China! "; 字符数组名str不能直接赋值,该语句是错误的。pc="I love China! "; 指针变量pc可以直接赋字符串地址,语句正确
4)输入方式将字符串直接输入字符数组,但是不能将字符串直接输入指针变量,但是可将指针变量所指字符串直接输出。
cin >> str 正确
cin >> pc 错误
cout<<pc 正确
5)值的改变字符数组的首地址不能改变,字符串指针变量的首地址可以改变
str=str+5; 错误
pc=str+5; 正确
考点5:理解指针和地址的关系
#include<iostream>using namespace std;
int main ()
{
int a[3];
a[0]=0; a[1]=1; a[2]=2;
int *p , *q;
p=a;
q=&a[2];
cout << a[q-p] <<endl;
}
解析:本程序的结构如下:
1)先声明了一个整型数组a[3],然后分别给数组赋值。
2)又声明了两个整型指针 p、q,但是并没有定义这两个指针所指向的地址。
3)使整型指针 p 的地址指向 a(注意 a 就是a[0]),使整型指针 q 的地址指向 a[2]。
考点6:理解内存偏移
#include<iostream>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;
};
void main ()
{
A a;
B *p = (B *)(&a);
p->fun();
}
首先可以肯定的是上面的代码是非常槽糕的,无论是可读性还是安全性都很差。写这种代码的人,按照Jarne Stroustrup(C++标志化制定者)的说法,应该“斩立决”。但是不得不说这也是一道很好考察你对内存偏移的理解的题:B *p = (B *)(&a);
这是一个野蛮的转化,强制把 a 地址内容看成是一个B类对象,p 指向的是 a 类的内存空间。
B类只有一个元素m_c 但是 A类的内存空间存放第一个元素的位置是 m_a, p指向的是对象的内存首地址,比如:0x22ff58,但p->fun()
调用B::fun()
来打印m_c
时,编译器对m_c
的认识就是m_c
距离对象的偏移量是0,于是打印了对象A首地址的偏移量 0x22ff58+0
变量值,即就是m_a
的值1.
考点7:类之间的覆盖
#include<iostream>using namespace std;
class A
{
public:
int m_a;
A()
{
m_a = 1;
}
void print ()
{
printf("%d",m_a);
}
};
class B : public A
{
public:
int m_a;
B()
{
m_a = 2;
}
};
int main ()
{
B b;
b.print();
printf("%d\n",b.m_a);
}
解析:B类中的 m_a
把 A 类中的 m_a
覆盖掉了。在构造 B 类时,先调用 A 类的构造函数,所以 A 类中的 m_a
是1,b.print()打印的是 A 类中的m_a
而B类中的 m_a
是2。
答案:12
考点8:形参和实参
1)函数的形参在调用到实参才会分配空间
2)函数需要在它被调用之前被声明,与主函数无关
3)若一个函数没有return语句,编译器会隐式返回0
4)一般来说函数的形参和实参的类型要一致
7.3 函数指针
考点1:区分函数指针、函数返回指针、const指针、指向const的指针、指向const的const指针
void(*f)() 函数指针,也叫指向函数的指针
void* f() 函数返回指针
int * const const指针 const位于*右侧,指针本身为常量
const int * 指向const的指针 const位于*左侧,指针指向为常量
float(**def)[10] 二级指针,指向一个一维数组指针,数组元素为floatdouble*(*gh)[10] gh是一个指针,它指向一个一维数组,数组元素都是double*
Long(*fun)(int) 函数指针
Int (*(*F)(int,int))(int) F是一个函数的指针,指向的函数类型是有两个int参数并且返回一个函数指针的函数
考点2:函数指针
1)float(**def)[10]
def是二级指针,它指向的是一个一维数组的指针,数组元素float
2)double*(*gh)[10]
gh是一个指针,它指向一个一维数组,数组元素都是double*
3)double(*f[10])()
f是一个数组,f有10个元素,元素都是函数的指针,指向的函数类型是没有参数且返回double的函数。
4)int*((*b)[10])
是一个一维数组的指针,数组元素都是int*
5)Long(*fun)(int)
函数指针
6)Int(*(*F)(int,int))(int)
F是一个函数指针,指向的函数的类型是有两个int参数,且返回一个函数指针的函数
7.4 指针数组和数组指针
考点1:引入指针数组和数组指针,并区分
数组指针(也成为行指针)int(*p)[n]
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
如要将二维数组赋给一指针,应这样赋值:
int a[3][4];
int (*p)[4]; 该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a; 将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; 该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]
所以数组指针也称指向一维数组的指针,亦称行指针。
指针数组定义 int *p[n];
[]优先级高,先与p结合成为一个数组,再由int*
说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1是错误的,这样赋值也是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a;
这里*p
表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:
int *p[3];
int a[3][4];
for(i=0;i<3;i++)
p[i]=a[i];
这里int *p[3]
表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]
所以要分别赋值。
这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。
还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
考点2:理解二维数组的真正含义且与*
比如要表示数组中i行j列一个元素:
*(p[i]+j)
*(*(p+i)+j)
(*(p+i))[j]
p[i][j]
优先级:()>[]>*
其实这很好记忆 p[i][j]
之后记住p[i]=*(p+i)
牢记这个就OK了.上面三种形式都是它的一个转换
举例:
#include<stdio.h>#include<iostream>using namsespace std;
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; v[0][0] 1
cout << **(a+1) << endl; v[1][0] 11
cout << *(*a+1) << endl; v[0][1] 2
cout << *(a[0]+1) << endl; v[0][1] 2
cout << *(a[1]) << endl; v[1][0] 11
return 0;
}
考点3:二维数组与&
int a[] = {1,2,3,4,5};
int *ptr = (int*)(&a+1);
printf("%d %d", *(a+1), *(ptr-1));
//说明:
*(a+1) 直接就为2 简单
&a+1 由于本身a就为数组名也就是指针,加上& 相当于双指针 也就相当于**(a+1) 所以加1 就是数组整体加一行,ptr指向a的第6个元素,所以*(ptr-1)为5
7.5 迷途指针
考点1:理解迷途指针
C中的野指针,C++中的迷途指针(失控指针或悬浮指针)都是不为NULL的指针,也不是指向常量的指针,而是指向“垃圾”内存的指针。“垃圾”的意思是未知区域、未知内存。
例如,当我们定义一个指针的时候:
void main()
{
int *p;
}
p的值为0xcccccccc。也就是说,任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。这也就是为什么我们在指针一开始创建时就给它赋一个值总是没有错的。
指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。free和delete只是把指针所指的内存给释放掉,但并没有把指针本身干掉。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。
int main()
{
int *pInt = new int;
*pInt=10;
cout<<pInt<<endl;
cout<<"*pInt: "<<*pInt<<endl;
delete pInt; //pInt为迷途指针!
//pInt = NULL; 加上
int *pLong = new int;
cout<<pInt<<endl;
cout<<pLong<<endl;
*pLong=90000;
cout<<"*pLong: "<<*pLong<<endl;
*pInt=20; //再次使用pInt!
cout<<"*pInt: "<<*pInt<<endl;
cout<<"*pLong: "<<*pLong<<endl;
delete pLong;
return 0;
}
//使用迷途指针并重新赋值,会照成系统崩溃。
如果我们加上pInt = NULL,那么就是对的
考点2:区别迷途指针和空指针
#include <iostream>using namespace std;
int main()
{
int* p = 0;//初始化时应该将指针赋值为空指针
p = new int;
*p = 9;
cout<<"*p = "<<*p<<endl;
delete p; 这个时候p为迷途指针,编译器只是释放了内存,但是指针本身还是存在的。
p = 0;//or p = NULL; 这个时候p为空指针
delete p; 不会发生错误,因为删除一个空指针很安全,如果删除一个迷途指针则会照成系统崩溃
int n;//为了不让屏幕一闪而消失
cin>>n;
return 0;
}
考点2:C++中有了 malloc/free ,为什么还需要 new/delete 呢?
1)malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2)对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
总结,不要企图用malloc/free来完成对象的内存管理
3)因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new ,以及一个能完成清理与释放内存工作的运算符delete。new/delete 不是库函数而是运算符。
7.6 指针和句柄
考点1:句柄和指针的区别和联系是什么?
指针和句柄是两个完全不同的东西,windows系统用句柄标记系统资源,隐藏系统消息,指针是用来标记某个物理内存地址
考点2:智能指针的使用auto_ptr
std::auto_ptr <Object> pObj(new Object);
auto_ptr好处在于在析构时会自动删除此指针,但是不要误用
1)不能共享所有权,即不要让两个auto_ptr指向同一个对象
2)不能指向数组,因为在析构时候调用delete,而不是deletr[]
3)不能作为容器的对象
考点3:this指针
this指针记录每个对象的内存地址,之后通过运算符->访问该对象成员
当你进入一个房子后,你可以看到桌子、椅子、地板等,但是房子你是看不到全貌的,对于一个类的实例来说,你可以看到它的成员函数,成员变量,但是实例本身你是看不到的,this指针是这样的一个指针,时时刻刻指向这个实例本身。
1)this只在成员函数中使用,全局函数、静态函数都不能使用this
2)this在成员函数开始之前创造,在成员的结束后清除
3)this不占用对象的空间
4)this可能存放在堆、栈、也可能是寄存器
5)this在成员函数中定义,所以获取一个对象之后,也不能通过对象使用this指针,所以我们没法知道一个对象this指针的位置,只有在成员函数中我们才知道,利用&this就知道了,并且可以使用它。