面试算法整理(1)
堆和栈的区别
一由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时,可以由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。
3、全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
4、文字常量区:常量字符串放在这里,程序结束后由系统释放。
5、程序代码去:存放函数体的二进制代码。
二、例子程序:
int a = 0; //全局初始化区
char* p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456" //123456\0在常量区,p3在栈上
static int c = 0; //全局静态初始化区
p1 = (char*)malloc(10);
p2 = (char*)malloc(20);
//分配来的10和20字节的区域在堆上面
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的“123456”优化成一个地方。
}
三、堆和栈的理论知识
3.1、申请方式:
stack: 由系统自动分配。例如,声明在函数中的一个局部变量int b;系统自动在栈中为b开辟空间。
heap: 需要程序员自己申请,并指明大小,在C中malloc函数:p1 = (char*)malloc(10); 在C++中用new运算符,但是注意p1这些变量本身是在栈中的。
3.2、申请后系统的响应:
栈: 只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆: 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请的时候,会遍历链表,寻找第一个空间大于所申请空间的堆结点,然后将该节点从空闲结点链表中删除,并将该结点的空间分配给程序,另外对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动将多余的那部分重新放入空闲链表中。
3.3、申请大小的限制:
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2M,如果申请的空间超过栈的剩余空间时,将提示overflow。因此能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3.4、申请的效率比较:
栈: 由系统自动分配,速度较快,程序员无法控制。
堆: 由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外在Windows下最好的方式是用VirtualAlloc分配内存,它不是在堆也不是在栈。栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便,但是速度快,也最灵活。
3.5、堆和栈的存储内容:
栈: 在函数调用的时候,第一个进栈的主函数中的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由改点继续运行。
堆: 一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
3.6、存取效率比较:
char s1[] = "aaaaaaaa";
char *s2 = "bbbbbbbb";
这其中,aaaaaaaa是在运行时候赋值的,而bbbbbbbb是在编译时候就确定的;但是,在以后的存取中,在栈上的数组比指针多指向的字符串(例如堆)快。
关于指针和引用
一、指针和引用的区别:
(1)指针是一个变量,存储了指向数据的地址;引用只不过是原变量的别名,实质上跟原变量是一回事。
(2)指针可以多级,引用只能有一级。
(3)指针可以为NULL,引用不能为空,创建的时候就必须初始化。
(4)指针可以有两层const,但是引用只有一层const。
(5)指针的值初始化后可以改变,引用的值初始化后不可以改变。
(6)sizeof引用得到变量的大小,sizeof指针得到指针本身的大小。
(7)指针和引用的自增(++)运算意义不一样。例如:
int a=0;
int b=&a;
int *p=&a;
b++;相当于a++;b只是a的一个别名,和a一样使用。
p++;后p指向a后面的内存
(*p)++;相当于a++
二、指针、引用:
(1)引用必须被初始化,引用并非对象,没有地址空间,所以没有指向引用的指针;
(2)距离变量名最近的符号对变量的类型有最直接的影响,如int *&a;a为引用,是int指针的引用;
(3)常量指针: 即指向常量的指针,指针本身不可改变,指向的对象可以改变。int const p;或 const int p;
(4)指针常量: 即指针本身是一个常量,指针本身不可以改变,指针的对象可以改变。int *const p;
(5)指向常量的指针常量: 即指针本身是常量,不可改变,指向的对象也不可改变。const int *const p;
关于深度优先遍历和广度优先遍历
深度优先遍历(DFS): 从某个顶点出发,首先访问这个顶点,然后找出刚刚访问这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的下一个新的顶点进行访问;重复此步骤,直到所有结点均被访问完为止。
广度优先遍历(BFS): 从某个顶点出发,首先访问这个顶点,然后找出这个结点的所有未被访问的邻结点,访问完后再访问这些节点中第一个邻结点的所有结点;重复此方法,直到所有几点均被访问完为止。
几种进程间的通信方式
管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
信号量(signal):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程之间以及同一进程内不同进程之间的同步手段。
消息队列:消息队列是由消息的链表,存放在内科中并由消息队列标识符标识,消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件以及发生。
共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程之间的同步和通信。
套接字:套接口也是一种进程间通信机制,与其他通信机制不同的是,它可以用于不同进程之间的通信。
一个空类会默认生成哪些函数
定义一个空类
class Empty
{
};
默认生成以下几个函数:
1.无参的构造函数
Empty()
{
}
2.拷贝构造函数
Empty(const Empty& copy)
{
}
3.赋值运算符
Empty& operator = (const Empty& copy)
{
}
4.析构函数(非虚)
~Empty()
{
}
这些函数只有在第一次使用它们的时候它们才会生成,它们都是inline并且public的。如果想禁止这些函数,可以将它们定义成private函数,如果有很多类都有这种需求,那么可以定义一个基类,然后让其他类继承这个类。下面是来自boost库的代码,任何继承了该类的类,都不能进行复制操作。也不能使用赋值运算符。
#ifndef BOOST_NONCOPYABLE_HPP_INCLUDED
#define BOOST_NONCOPYABLE_HPP_INCLUDED
namespace boost
{
namespace noncopyable
{
class noncopyable
{
protected:
noncopyable() {}
~noncopyable() {}
private:
noncopyable(const noncopyable&);
const noncopyable& operator = (const noncopyable&);
};
}
}
#endif
C++中不允许重载的5个运算符:
1、 .*(成员指针访问运算符)
2、 :: 域运算符
3、 sizeof 长度运算符
4、 ?: 条件运算符
5、 .(成员访问符)
运算符重载的三种方式
普通函数、 友元函数、 类成员函数
静态函数存在的意义
- 静态私有成员在类外不能被访问,可通过类的静态成员函数来访问;
- 当类的构造函数是私有时,不像普通类那样实例化自己,只有通过静态成员函数来调用构造函数。
对象之间通过类的静态成员变量来实现数据的共享,静态成员变量占有自己独立的空间,不为某个对象所私有。
函数重载是什么意思?它与虚函数的概念有什么区别?
函数重载是一个同名函数完成不同的功能,编译系统在编译阶段通过函数参数个数、参数类型不同,即实现的是静态的多态性。但是,不能仅仅通过函数返回值不同来实现函数重载。而虚函数实现的是在基类中通过使用关键字virtual来申明一个函数为虚函数,含义就是该函数的功能可能在将来的派生类中定义或者在基类的基础上进行扩展,系统只能在运行阶段才能动态决定该调用哪一个函数,所以说想的是动态的多态性。它提箱的是一个纵向的概念,也即在基类和派生类间实现。
构造函数和析构函数是否可以被重载,为什么?
构造函数可以被重载,析构函数不可以被重载。因为构造函数可以有多个且可以带参数,而析构函数只有一个,且不能带参数。
简单来说,虚函数是使用了虚函数表来实现的
C程序的储存空间布局
C程序由一下几部分组成:
--正文段: CPU执行的机器指令部分,通常可以共享,以使频繁执行的程序在内存中只有一个副本,同时正文段常常是只读的,以防由于意外被修改
--初始化数据段: 通常称为数据段,包含了程序中明确赋初值的变量,如C语言中任何在函数外声明的变量。
--未初始化数据段: 通常称为BSS段,程序开始执行前,内核会将此段初始化为0或空指针。
--栈区:自动变量和每次函数调用时保存的信息保存在其中,函数调用时,返回地址和调用者环境信息都存放在栈中,最近调用的函数在栈上为其所有的自动变量和临时变量分配空间
--BSS段的内容不会存放在硬盘中,因其每次开始运行前都会被内核初始化为0.磁盘中存放的只有正文段数据和初始化数据段数据。