面试题27 typedef用于函数指针定义
下面的定义有什么作用?
typedef int (*pfun)(int x, int y);
【解析】
这里的pfun是一个使用typedef自定义的数据类型。它表示一个函数指针,其参数有两个,都是int类型,返回值也是int类型。可以按如下步骤使用:
typedef int (*pfun)(int x, int y);
int fun(int x, int y);
pfun p = fun;
int ret = p(2, 3);
简单说明:
第1行定义了pfun类型,表示一个函数指针类型。
第2行定义了一个函数。
第3行定义了一个pfun类型的函数指针p,并赋给它fun的地址。
第4行调用p(2,3),实现fun(2,3)的调用功能。
【答案】
定义了一个函数指针类型,表示指向返回值为int,且同时带2个int参数的函数指针。可以用这种类型定义函数指针来调用相同类型的函数。
面试题28 什么是“野指针”
【解析】
“野指针”不是NULL指针,而是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。
但是“野指针”是很危险的,if语句对它不起作用。“野指针”的成因主要有两种:
1,指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的默认值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
2,指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
【答案】
“野指针”不是NULL指针,而是指向“垃圾”内存的指针。其成员主要为:指针变量没有被初始化,或指针p被free或者delete之后,没有置为NULL。
面试题29 看代码查错——“野指针”的危害
下面的程序片断有什么重大的bug?
short *bufptr; //声明了一个short*类型的指针,并且没有对它初始化。
short bufarray[20]; //声明了一个20个元素的数组,每个元素都是short类型。
short var = 0x20; //声明 了一个short类型的变量var,并且把它初始化为0x20.
*bufptr = var; //错误。将bufptr指针指迥的内容赋为var变量的值。因为bufptr没有被初始化,是个“野指针”。
bufarray[0] = var; //把bufarray的第一个元素赋值为var的值。
【解析】
代码第4行,错误。将bufptr指针指向的内容赋为var变量的值。因为bufptr没有被初始化,是个“野指针”,因此对它所指向的内容操作是址分危险的,会导致程序崩溃。为了杜绝这种错误,可以将bufptr正确地进行初始化。代码第一行改为:short *bufptr = (short*)malloc(sizeof(short));
【答案】
“第4行存在重大buf,bufptr是“野指针”,会导致程序运行崩溃。
面试题30 有了malloc/free,为什么还要new/delete
【解析】
malloc与free是C++/C的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造
函数和析构函数的任务强加于malloc/free。
因此,C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意:new/delete不是库函数。请看下面的例子:
#include <iostream>
using namespace std;
class Obj
{
public:
Obj(void)
{
cout << "Initialization===" << endl;
}
~Obj(void)
{
cout << "Destroy==========" << endl;
}
};
void UseMallocFree(void)
{
cout << "in UseMallocFree()..." << endl;
Obj *a = (Obj *)malloc(sizeof(Obj));
free(a);
}
void UseNewDelete(void)
{
cout << "in UseNewDelete....." << endl;
Obj *a = new Obj;
delete a;
}
int main()
{
UseMallocFree();
UseNewDelete();
return 0;
}
在这个示例中,类Obj只有构造函数和析构函数的定义,这两个成员函数分别打印一句话。函数UseMallocFree()中调用malloc/free申请和释放堆内存;函数UseNewDelete()中调用new/delete申请和释放堆内存。可以看到函数UseMallocFree()执行时,类Obj的构造函数和析构函数都不会被调用;而函数UseNewDelete()执行时,类Obj的构造函数和析构函数都会被调用。执行结果如下:
【答案】
对于非内部数据类型的对象而言,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free,因此只有使用new/delete运算符。
面试题31 程序改错——指针的初始化
【解析】
TNode是一个结构体类型,它有left和right两个成员指针,分别代表链接左,右两个元素,还有value成员表示元素节点的数据。在append函数中,它想把数据从左到右按降序排列。因此在第45、46、48和50行使用while循环来查找合适的位置。这里有一个问题,在这4行都采用temp的left或right与NULL进行判断,然而对堆中分配的内存只做了成员value的初始化,没有把left和right初始化为NULL,因此指针left和指针right与NULL进行的判断没有作用。结果是程序中会对野指针指向的地址进行赋值,从而导致程序崩溃。
改正后的代码如下:
#include <stdio.h>
#include <malloc.h>
struct Tag_Node
{
struct Tag_Node *left;
struct Tag_Node *right;
int value;
};
typedef struct Tag_Node TNode;
TNode *root = NULL;
void append(int N);
void print();
int main()
{
append(63);
append(45);
append(32);
append(77);
append(96);
append(21);
append(17);
printf(" head: %d\n", root->value);
print();
return 0;
}
void append(int N)
{
TNode *NewNode = (TNode *)malloc(sizeof(TNode));
NewNode->value = N;
NewNode->left = NULL; //初始化left
NewNode->right = NULL; //初始化right
if(root == NULL)
{
root = NewNode;
return;
}
else
{
TNode *temp;
temp = root;
printf(" N = %d temp->value = %d temp->left = %p temp->right = %p\n", N, temp->value, temp->left, temp->right);
while((N >= temp->value && temp->left != NULL) ||
(N < temp->value && temp->right != NULL))
{
while(N >= temp->value && temp->left != NULL)
temp = temp->left;
while(N < temp->value && temp->right != NULL)
temp = temp->right;
}
if(N >= temp->value)
{
temp->left = NewNode;
NewNode->right = temp; //形成双向链表
}
else
{
temp->right = NewNode;
NewNode->left = temp; //形成双向链表
}
return;
}
}
void print()
{
TNode *leftside = NULL;
if(root == NULL)
{
printf("There is not any element\n");
return;
}
leftside = root->left;
while(1)
{
if(leftside->left == NULL)
break;
leftside = leftside->left;
}
while(leftside != NULL)
{
printf(" %d ", leftside->value);
leftside = leftside->right;
}
}
如上面的程序刀示,在第34、35行添加了成员指针left和right的初始化,这样就杜绝了野指针的产生。第60、65行的目的是为了使链表是双向链表。这样在遍历链表时就会比较方便。print函数是从左到右打印链表中所有元素value成员的。执行结果为:
可以看到,root节点是第一个插入到链表的,其数据其为63.链表是按照从左到或聊序排序的。
【答案】
没有对新增加的节点成员指针left和right做初始化,它们都是野指针,在随后与NULL比较时不起判断的作用。最终对野指针指向的内存块赋值导致程序崩溃。
面试题32 各种内存分配和释放的函数的联系和区别
【解析】
C语言的标准分配函数:malloc, calloc, realloc, free等。
malloc与calloc的区别为1块与n块的区别。
malloc的调用形式为(类型*)malloc(size):在内存的动态存储区中分配一块长度为”size“字节的连续区域,返回该区域的首地址,此时内存中的值没有初始化,是个随机数。
calloc的调用形式为(类型*)calloc(n, size):在内存的动态存储区中分配n块长度为”size“字节的连续区域,返回首地址,此时内存中的值都被初始化为0.
realloc的调用形式为(类型*)realloc(*ptr, size):将ptr内存大小增加到size,新增加的内存块没有初始化。
free的调用开式为free(void *ptr):释放ptr所指向的一块内存空间。
C++中,new/delete函数可以调用类的构造函数和析构函数。
面试题33 程序找错——动态内存的传递
【解析】
这个程序有Base和Subclass两个类,其中Subclass是Base的子类。在本题里,Subclass只是被用来向Base的构造函数传递字符串参数,以达到为Base的私有成员赋值的目的。
代码第11行,在Base的构造函数中用new分配了堆内存。其内存大小为传入的字符串长度,这里有个Bug。由于字符串是以0作为结速符的,应该多分配一个字节存放0.
Base的成员函数copyName()中,返回其内数组的地址。由于数组处于栈中,当copyName调用结束后,栈就会被销毁。这里应该返回堆内存地址。
【答案】
代码第11行改为:
name = new char[strlen(className) + 1];
代码第20行改为:
char *newname = new char[strlen(name) + 1];
修改后的程序如下:
#include <iostream>
using namespace std;
class Base
{
private:
char *name;
public:
Base(char *className)
{
name = new char[strlen(className) + 1];
strcpy(name, className);
}
~Base()
{
delete name;
}
char *copyName()
{
char *newname = new char[strlen(name) + 1];
strcpy(newname, name);
return newname;
}
char *getName()
{
return name;
}
};
class Subclass: public Base
{
public:
Subclass(char *className):Base(className)
{
}
};
int main()
{
Base *pBase = new Subclass((char*)"test");
printf("name: %s\n", pBase->getName());
printf("new name: %s\n", pBase->copyName());
return 0;
}
面试题34 动态内存的传递
分析 下面代码:
【解析】
这里的GetMemory函数有问题。GetMemory函数体内的p实际上是main函数中的str变量在Getmemory函数栈中的一个备份,因为编译器总是为函数的每个参数制作临时的变量。因此,虽然在代码第6行中p申请了堆内存,但是返回到main函数时,str还是NULL,并不指向那块堆内存。在代码第14行,调用strcpy时会导致程序崩溃。
实际上,GetMemory并不能做任何有用的事情。这里还要注意,由于从GetMemory函数返回时不能获得堆中内存的地址,那块堆内存就不能被继续引用,也就得不到释放,因此调用一次GetMemory函数就会产生num字节的内存泄漏。
可以采用3种方法来解决上面的动态内存不能传递的问题。
1,在C语言中,可以通过采用指迥指针的指会解决这个问题,可以把str的地址传给函数GetMemory。
2,在C++中,多了一种选择,就是传递 str指针的引用。
3,使用函数返回值来传递动态内存。
#include <iostream>
using namespace std;
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void GetMemory2(char **p, int num)
{
*p = (char*)malloc(sizeof(char)*num);
}
void GetMemory3(char* &p, int num)
{
p = (char*)malloc(sizeof(char)*num);
}
char *GetMemory4(int num)
{
char *p = (char*)malloc(sizeof(char)*num);
return p;
}
int main()
{
char *str1 = NULL;
char *str2 = NULL;
char *str3 = NULL;
char *str4 = NULL;
//GetMemory(str1, 20);
GetMemory2(&str2, 20);
GetMemory3(str3, 20);
str4 = GetMemory4(20);
strcpy(str2, "GetMemory 2");
strcpy(str3, "GetMemory 3");
strcpy(str4, "GetMemory 4");
cout << "str1 == NULL ? " << (str1 == NULL ? "yes" : "no") << endl;
cout << "str2: " << str2 << endl;
cout << "str3: " << str3 << endl;
cout << "str4: " << str4 << endl;
free(str2);
free(str3);
free(str4);
str2 = NULL;
str3 = NULL;
str4 = NULL;
return 0;
}
在上面的代码中,GetMemory2()函数采用二维指针作为参数传递;Getmemory3()函数采用指针的引用作为参数传递;GetMemory4()函数采用返回堆内存指针的方式。可以看到这3个函数都能起到相同的作用。
另外注意第47~52行,这里在主函数推出之有把指针str2, str3和str4指向的堆内存释放并把指针赋为NULL。每当问心有愧定不再使用堆内存时,应该把堆内存释放,并把指针赋为NULL, 这样能避免内存泄漏以及产生野指针,是良好的编程习惯。
程序运行结果如下所示:
【答案】
调用strcpy(str, "hello")时程序崩溃。因为GetMemory不能传递动态内存,str始终都是NULL。
面试题35 比较分析两个代码段的输出——动态内存的传递
【解析】
程序1的GetMemory()返回的是指向栈内存的指针,该指针的地址不是NULL,但是当栈退出后,内容不定,有可能会输出乱码。
程序2的GetMemory()没有返回值,这个函数不能传递动态内存在Test函数中,str变量的值通过参数传值的方式赋给GetMemory()的局部变量p。
此外,由于堆内存在GetMemory()执行之后没有指针引用它,因此会产生内存泄露。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *GetMemory()
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(" str====%s\n", str);
}
void GetMemory2(char *p)
{
p = (char*)malloc(100);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(str);
strcpy(str, "hello world");
printf(" str====%s\n", str);
}
int main()
{
Test();
Test2();
return 0;
}
【答案】
程序1输出结果可能是乱码。
程序2有内存泄露,在Test函数调用strcpy时程序崩溃。
面试题36 程序查错——“野指针”用于变量值的互换
【解析】
在代码第3行,声明了一个指针p,由于没有对p初始化,p是个野指针,它可能指向系统区。因此在代码第4行,对p指向的内存区赋值非常危险,会导致程序运行时崩溃。
程序应改为:
swap(int *p1, int *p2)
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
【答案】
swap函数内的指针变量p没有初始化是野指针,野指针可能乱指一气,导致程序运行时崩溃。
面试题37 内存的分配方式有几种
【解析】
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量。
(2)在栈上创建。在执行函数,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。处理器的指令集中有关于栈内存的分配运算,因此效率很高,但是分配的内存容量有限。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自已负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
面试题38 什么是句柄
【解析】
句柄在Windows编程中是一个很重要的概念,在许多地方都扮演着重要的角色。在Windows环境中,句柄是用来标项目的,这些项目包括:
模块(module)。
任务(task)。
实例(instance)。
文件(file)。
内存块(block of memory)。
菜单(menu)。
控制(control)。
字体(font)。
资源(resource),包括图标(icon)、光标(cursor)、字符串(string)等。
GDI对象(GDI object),包括位图(bitmap)、画刷(brush)、元文件(metafile)、调色板(palette)、画笔(pen)、区域(region),以及设备描述表(device context)。
Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。由于地址总是如此变化,所以Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这地址(存储单元的位置)本身是不变的。Windows内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配的,当系统卸载时(Unload)又释放给系统。
因此,Window程序中并不是用物理地址来标识一个内存块、文件、任务或动态装入模块的,相反,WINDOWS API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。
在Windows编程中会用到大量的句柄,比如HINSTANCE(实例句柄)、HBITMAP(位图句柄)、HDC(设备描述表句柄)、HICON(图标句柄)等。这当中还有一个通用的句柄,就是HANDLE,比如下面的语句:
HINSTANCE hInstance;
HANDLE hInstance;
句柄地址(稳定)-->记载丰对象在内存中的地址-->对象在内存中的地址(不稳定)-->实际对象。但是,必须注意的,程序每次重新启动,系统不能保证分配
这个程序的句柄还是原来的那个句柄,而且绝大多数情况的确是不一样的。
面试题39 指针与句柄有什么区别
【解析】
指针对应着一个数据在内存中的地址,得到了指针就可以自由地修改该数。Windows并不希望一般程序修改其内部数据结构,因为这样太不安全。所以Windows给每个使用GlobalAlloc等函数声明的内存区域指定一个句柄,句柄是一种指向指针的指针。句柄和指针都是地址,不同之处在于:
(1)句柄所指的可以是一个很复杂的结构,并且很有可能是与系统有关的。比如说线程的句柄,它指向的就是一个类或者结构,
它和系统有很密切的关系。当一个线程由于不可预料的原因而终止时,系统就可以返回它所占用的资料,如CPU、内存等。反过来想可以知道,这个句柄中的某一些项是与系统进行交互的。由于Windows系统是一个多任务的系统,它随时都可能要分配内存
回收内存,重组内存。
(2)指针也可以指向一个复杂的结构,但是通常是由用户定义的,所以必需的工作都要用户完成,特别是在删除的时候。