软件研发面试题汇总

1.什么是预编译?何时需要预编译?

什么是预编译:
1)预编译又称为预处理,是做些代码文本的替换工作;
2)处理以 # 开头的指令,比如拷贝 #include 包含的文件代码, #define 宏定义的替换, 条件编译等, 就是为编译做的预备工作的阶段。
3)主要处理 # 开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。
4)C编译系统在对程序进行通常的编译之前,首先进行预处理。

何时需要预编译:
1)总是使用不经常改动的大型代码体;
2)程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个“预编译头”.

2.char * const p ; char const * p ; const char * p 这三者有什么区别?

3. 求输出结果

1
2
3
4
char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";

上面是数组变量,他们有各自的内存空间

1
2
3
4
const char * str5 = "abc";
const char * str6 = "abc";
char * str7 = "abc";
char * str8 = "abc";

上面的指针指向相同的常量地址区域

1
2
cout << (str1 == str2) << endl;
cout << (str3 == str4) << endl;

所以数组变量的地址不一样

1
2
cout << (str5 == str6) << endl;
cout << (str7 == str8) << endl;

所以指针的地址是一样的

结果是: 0 0 1 1

4.请问下面的代码有什么问题?

1
2
3
4
5
6
7
8
int main()
{
char a;
char *str = &a;
strcpy(str,"hello");
printf(str);
return 0;
}

答案:没有为str分配内存空间,将会发生异常。
问题出在,将一个字符串复制进一个字符变量指针所指地址,虽然可以正确输出结果,但因为越界进行内在读写而导致程序崩溃。
就是说,你给了指针一个地址,但是这个地址长度不一定够,所以会越界,越界就会产生segmentation fault。

5.代码有何问题?

1
2
3
4
5
char * s = "AAA";
printf("%s",s);

s[0] = 'B';
printf("%s",s);

有什么错?

“AAA”是字符串常量。
s是指针,指向这个字符串常量,所以声明s的时候就有问题。
应该为:const char * s = “AAA”;

然后又因为是常量,所以对s[0]的赋值操作是不合法的。

6.如何用C语言编写死循环?

while(1){}

for(;;)

7.new 和 malloc 的区别?

1) 申请内存所在的位置
new 是 C++语言 从 自由存储区 为对象动态分配内存;
malloc 是 C语言 从 上动态分配内存。

2)返回类型安全性
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void * 指针转换成我们需要的类型。

3)内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;
malloc分配内存失败时返回NULL。

4)是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算;
而malloc则需要显式地指出所需内存的尺寸。

5)是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:

  • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  • 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  • 第一步:调用对象的析构函数。
  • 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

总之来说,
new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。
而malloc则不会。

6)对数组的处理
C++提供了new[]与delete[]来专门处理数组类型:

1
A *ptr = new A[10];//分配10个对象

使用new[]分配的内存必须使用delete[]进行释放。

1
delete [] ptr;

malloc,它并不知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。
所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

1
int * ptr = (int *) malloc ( sizeof(int) * 10);//分配一个10个int元素的数组

7)new 和 malloc 是否可以相互调用
operator new /operator delete的实现可以基于malloc,
而malloc的实现不可以去调用new。

8)是否可以被重载
opeartor new /operator delete可以被重载。
标准库是定义了operator new函数和operator delete函数的8个重载版本:

1
2
3
4
5
6
7
8
9
10
//这些版本可能抛出异常
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete (void * )noexcept;
void * operator delete[](void *0)noexcept;
//这些版本承诺不抛出异常
void * operator new(size_t ,nothrow_t&) noexcept;
void * operator new[](size_t, nothrow_t& );
void * operator delete (void *,nothrow_t& )noexcept;
void * operator delete[](void *0,nothrow_t& )noexcept;

malloc/free 并不允许重载。

9)能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。
realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存

10)客户处理内存分配不足
在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:

1
2
3
4
namespace std
{
typedef void (*new_handler)();
}

指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:

1
2
3
4
namespace std
{
new_handler set_new_handler(new_handler p ) throw();
}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

11)属性不同
new/delete是C++关键字,需要编译器支持。
malloc/free是库函数,需要头文件支持。

8.堆和栈的区别

1)程序的内存分配方式不同
堆(heap):需要程序员自己申请自己释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
//C中使用malloc函数申请
char * p1 = (char *)malloc(10);
cout << (int *)p << endl; //输出:00000000003BA0C0

//使用free()释放
free(p1);

//C++中用new运算符申请
char p2 = new char[10];
cout << (int *)p2 << endl; //输出:00000000003BA0C0

//使用delete运算符释放
delete[] p2;
}

其中p1所指的10字节的内存空间与p2所指的10字节内存空间都是存在于堆的。堆的内存地址生长方向与栈相反,由低到高,但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,即p2指向的地址并不一定大于p1所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。堆中存储的数据的若未释放,则其生命周期等同于程序的生命周期。

栈(stack):编译器自动分配释放
用来存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈。

1
2
3
4
5
6
int main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
}

其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量,比如上面代码中变量s的地址小于变量b的地址,p2地址小于s的地址。栈中存储的数据的生命周期随着函数的执行完成而结束。

2)申请后的响应时间不同
堆:首先应该知道操作系统有一个记录内存地址的链表,当系统收到程序的申请时,遍历该链表,寻找第一个空间大于所申请的空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的 delete 或 free 语句就能够正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会将多余的那部分重新放入空闲链表中。

栈:只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。

3)申请的大小限制不同
堆:堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是由链表在存储空闲内存地址,自然堆就是不连续的内存区域,且链表的遍历也是从低地址向高地址遍历的,堆得大小受限于计算机系统的有效虚拟内存空间,由此空间,堆获得的空间比较灵活,也比较大

栈:在 windows 下,栈是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的,能从栈获得的空间较小
栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量。

4)申请的效率不同
堆:堆是有程序员自己分配,速度较慢容易产生碎片不过用起来方便

栈:栈由系统自动分配,速度快,但是程序员无法控制

5)堆和栈的存储内容不同
堆:一般是在堆的头部用一个字节存放堆的大小,而堆中具体存放内容是由程序员来填充的。

栈:栈存放的内容,函数返回地址相关参数局部变量寄存器内容等。

9.指针与引用的区别

1)指针是一个实体
引用只是一个别名
2)指针需要解引用
引用无需解引用
3)引用只能在定义时被初始化一次,之后不可变
指针可变
4)指针有const,const的指针不可变,
引用没有const
5)指针可以为空
引用不能为空
6)“sizeof(引用)” 得到的是 所指向的变量(对象)的大小
而“sizeof(指针)”得到的是指针本身(所指向的变量或对象的地址)的大小;
7)指针和引用的自增(++)运算意义不一样。

1
引用是C++中的概念,初学者容易把引用和指针混淆在一起。以下程序中,n是m的一个引用(reference),m是被引用物(referent)。

int m;
int &n = m;

1
n 相当于 m 的别名(绰号),对 n 的任何操作就是对 m 的操作。例如,有个人叫尹小丹,他的绰号是“仙女”。说“仙女”怎么怎么样,其实就是对尹小丹说三道四。所以,n 既不是 m 的拷贝,也不是指向 m 的指针,其实 n 就是 m 他自己。

int i = 5;
int j = 6;
int &k = i;
k = j; //k和i的值都变成了6

1
2
3
4
以上示例中,k 被初始化为 i 的引用。 语句 k = j并不能将 k 修改成为 j 的引用, 只是把 k 的值改变成为 6 。由于 k 是 i 的引用,所以 i 的值也变成了 6。

C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
**a)以下是“值传递”的示例程序。**

void func1(int x)
{
x = x + 10;
}
int n = 0;
func1(n);
cout << “n = “ << n << endl; //输出:n = 0

1
2
3
由于func1()函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n,所以n的值仍然是0。

**b)以下是“指针传递”的示例程序。**

void func2(int x)
{
(
x) = (*x) + 10;
}
int n = 0;
func2(&n);
cout << “n = “ << n << endl; //输出:n = 10

1
2
3
由于func2()函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10.

**c)以下是“引用传递”的示例程序。**

void func3(int &x)
{
x = x + 10;
}
int n = 0;
func3(n);
cout << “n = “ << n << endl; //输出:n = 10

1
2
3
由于func3()函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10.

## 10.以下四行代码的区别是什么?

const char * arr = “123”;
char * brr = “123”;
const char crr[] = “123”
char drr[] = “123”;

~
参考回答:
const char * arr = “123”;
字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果不一样。

char * brr = “123”
字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改“123”的值。

const char crr[] = “123”;
这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区。

char drr[] = “123”;
字符串123保存在栈区,可以通过drr去修改。

11.C++的内存管理

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段(text segment):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段(data segment):存储程序中已初始化的全局变量和静态变量。
BBS段(bss segment):存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区(heap):调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区(memory mepping segment):存储动态链接库以及调用mmap函数进行的文件映射。
栈区(stack):使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,linux可以通过ulimit命令指定。

12.什么是内存泄漏

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉、不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类:

  • 堆内存泄漏(heap leak)。堆内存指的是程序运行中根据需要分配通过malloc,realloc,new等堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放掉,那么此后这块内存将不会被使用,就会产生heap leak.

  • 系统资源泄露(resource leak)。主要指程序使用系统分配的资源比如bitmap,handle,socket等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致西戎效能降低,系统运行不稳定。

  • 没有将基类的析构函数定义为虚函数。 当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确被释放,因此造成内存泄漏。

13.