一、指针和引用的区别:
1、初始化要求不同:引用在创建的时候必须进行初始化,也就是说必须引用到一个有效的对象;而指针在定义的时候可以不必初始化,也可以在后面的任何地方重新赋值(不是int const*p)
2、可修改性不同:引用一旦被初始化指向某一个对象后,他就不能改变了,也就是说不能指向其他对象的引用;而指针在任何时候都可以指向另一个对象。
3、不存在NULL引用:引用不能指向空的引用,它总是必须指向某一个对象的;而指针则可以指向NULL,任意时刻可以改变指向,指针灵活性较大,但易出错。
4、应用需求:如果你的需求是一旦指向一个对象后就不会改变指向,则用引用。如果存在指向NULL或指向不同的对象时,则应该使用指针。
二、new/delete和malloc/free区别
1、new/delete是C++的运算符,而malloc/free是C++的标准库函数,他们都可以用来申请动态内存和释放内存
2、但是对于非内部对象而言光用malloc/free是无法满足动态对象的要求。因为对象在创建的时候必须执行构造函数,对象在消亡的时候必须执行析构函数。但malloc/free是库函数而不是运算符,不在编译器控制范围之内,不能把构造函数和析构函数强加于malloc/free。因此只能使用new/delete运算符。
三、C++中虚析构函数的作用?
1、大家都知道,析构函数是为了在对象不被使用之后进行释放内存,虚函数为了实现多态。那么虚析构函数(析构函数声明为virtual)有什么作用呢?其实主要的作用就是进行正常的资源释放,为啥这么说呢?如果你父类Base中声明的一个虚函数,父类的子类为Derived。如果我们定义Derived的指针pTest1 , Derived* pTest1 = new Derived()。当我们正常释放delete pTest1是没问题的。但是如果我们定义Base的指针pTest2 , Base* pTest2 = new Derived()。当我们执行delete pTest2时,Derived的析构函数没有被调用,这样的话就造成内存泄漏,因为类的析构函数里面都是释放内存资源的。解决这个问题的技术是在Base类中把析构函数声明虚函数。注意只有当一个类被用来作为基类时才会把析构函数声明虚函数。
四、虚函数和纯虚函数的区别
1、类中声明了虚函数,则这个函数是可以实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译就是可以使用后期绑定来达到多态。纯虚函数知识一个接口,是个函数的声明,不能在基类中实现必须在子类中实现。
2、虚函数在子类中可以不进行重载,但纯虚函数必须在子类中去实现。通常把很多函数加上virtual,这是一个好习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性。
3、虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现,当然我们也可以完成自己的实现。但纯虚函数的类用于“介面继承”,也就说纯虚函数只关注于接口的统一性,实现由子类完成。
4、带纯虚函数的类叫虚基类,这种基类不能直接生成对象,智能被继承,并重写虚函数的实现方法后才能使用。这种类也称为抽象类。
5、定义方式不一样,如虚函数:virtual function() {}; 纯虚函数virtual function()=0 {}
五、面试和笔试考到链表时不外乎就下面这些问题
1、链表反转
2、从末尾开始的打印链表
3、求链表的倒数第k个节点
4、求链表的中间节点(要求值遍历一次)
5、判断两个单向链表是否存在公共节点
6、求两个单向链表的第一个公共节点
7、判断链表是否存在环
8、求取链表中环的长度
9、求链表中环的入口点
10、合并两个有序的单向链表
程序及思想(碰到链表题,一时半会难以搞定时,想都不用想先用两个指针看一下能不能解决问题,链表题基本都会涉及到双指针解决问题)
1、链表反转。思想:首先我们先定义三个指针 pPrev 指向前一个节点; pReverseHead 反转链表的头指针; pNode = pHead指向当前节点; 先断开第一个节点,即pHead,把
pNext = pNode->next的下一个节点保存起来防止链表断开,把pNode->next指向当前节点的前一个节点pPrev,在把pPrev这个指针指向当前节点,把当前节点指向下一个节点pNext
,依次进行处理就看实现链表的反转,具体程序如下:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode* pPrev = NULL;
ListNode* pReverseHead = NULL;
ListNode* pNode = pHead;
while(pNode != NULL)
{
ListNode* pNext = pNode->next;
if(pNode->next == NULL)
{
pReverseHead = pNode;
}
pNode->next = pPrev;
pPrev = pNode;
pNode = pNext;
}
return pReverseHead;
}
};
2、从末尾开始的打印链表,一看到这道题是不是感觉很简单,其实是的。但是如果面试或笔试的时候不做任何限制的话,你可以先把链表进行反转在进行输出,可以参考1的链表反转。另一种方法是在不修改链表的条件下进行处理时。我们可以考虑借助一个栈来辅助实现,因为栈具有后进先出的特性,因为正好符合本题。首先我们先进行压栈,即第一个节点先压进栈底,然后依次处理。然后就进行出栈。程序如下:
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
*/
class Solution {
public:
vector<int> printListFromTailToHead(struct ListNode* head)
{
vector<int> v;
while(head != NULL)
{
v.insert(v.begin(),head->val);
head = head->next;
}
return v;
}
};
3、求链表的倒数第k个节点,常规思想需要遍历两次,肯定拿不到Offer。ok那就来个双指针,先用指针p1从头节点走k-1步,然后在用p2指针指向头节点,同时移动p1和p2指针,当p1指针指向尾节点时,则p2指针指向的结点就是倒数第k个节点。在强调一下,链表有难题请认准双指针。程序如下
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(pListHead == NULL || k<=0)
return NULL;
ListNode* p1 = pListHead;
ListNode* p2 = NULL;
for(int i = 0; i < k-1; i++)
{
if(p1->next == NULL)
return NULL;
p1 = p1->next;
}
p2 = pListHead;
while(p1->next != NULL)
{
p2 = p2->next;
p1 = p1->next;
}
return p2;
}
};
4、求链表的中间节点(要求值遍历一次),思想:你看你看又来了,这种题秒杀它,定义两个指针,一个指针快(p1=p1->next->next),一个指针慢(p2=p2->next)。同时从头节点开始移动,快的指针指向尾节点时,慢的指针就是中间节点。程序省略。
5、判断两个单向链表是否存在公共节点,如果有求第一个公共节点;思想:碰到这个问题你就去想一下连边的公共点存在什么特征,灵光一现,两个链表存在公共点就是从公共点开始以后的结点都是一样的是吧?也就是说只要我们用两个指针分别指向两个链表的头指,并且每移动一步就进行一次比较,判断他们是否相等(前提时当两个链表的长度一致时才可以)。这道题的难题就是如果两张链表长度不一样时,怎么同时移动指针。解决办法是先用指针分别求出链表的长度,在求它们差的绝对值(m-n),在把指向长链表的指针先移动(m-n)。然后同时移动两个指针,每移动一步做一次判断是否相等,相等时则指针指向的结点为公共节点。程序如下:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode* p1 = pHead1;
ListNode* p2 = pHead2;
int m , n ;
while(p1 != NULL)
{
m++;
p1 = p1->next;
}
while(p2 != NULL)
{
n++;
p2 = p2->next;
}
int num = 0;
p1 = pHead1;
p2 = pHead2;
if(m > n)
{
num = m - n;
for(int i = 0; (i < num) && (p1 != NULL) ;i++)
{
p1 = p1->next;
}
while(p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}
}
else
{
num = n - m;
for(int i = 0; (i < num) && (p2 != NULL);i++)
{
p2 = p2->next;
}
while(p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}
}
return p1;
}
};
改良版的程序
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) {
ListNode* p1 = pHead1;
ListNode* p2 = pHead2 ;
while(p1 != p2)
{
p1 = (p1 == NULL)? pHead2 : p1->next;
p2 = (p2 == NULL)? pHead1 : p2->next;
}
return p1;
}
};
6、判断链表是否存在环,如果存在环,则在求取环的长度和入口点,思想:环的特征是什么?我们假设操场是一个环,你在里面以速度v1奔跑,另一个小伙伴以v2速度奔跑(v2 > v1)。你们肯定在某一个时刻相遇的,这点大家肯定都没问题吧?好了,,,判断链表中是否存在一个环,就是以这种方法。我们假设两个指针分别为pFast1和 pNorn2 ,都指向链表的头节点,然后下一步把pFast1=pFast1->next->next (快指针),pNorn2 = pNorn2->next(慢指针)。如果链表存在环,则他们在某个时刻肯定相等,我们记下相遇点,然后在循环一次,每移动一次m++,再次回到相遇的点时,m得值就是环的长度。求环的入口点则是先用pFast1 和pNorn2指向链表的头节点。然后把pFast1先移动环的长度m,然后同时移动指针pFast1->next和指针pNorn2->next。每移动一步做一次判断,当他们相等时,该节点就是链表的入口点。程序如下:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
if(pHead == NULL || pHead->next == NULL)
return NULL;
ListNode* pFast1 = pHead->next;
ListNode* pNorm2 = pHead;
ListNode* pFlag = NULL;
int Lnum = 1;
while(pFast1 != pNorm2)//求取环的相遇节点
{
pFast1 = pFast1->next->next;
pNorm2 = pNorm2->next;
}
pFlag = pFast1;
pFast1 = pFast1->next;
while(pFast1 != pFlag)//求取环的长度
{
Lnum++;
pFast1 = pFast1->next;
}
pFast1 = pHead;
pNorm2 = pHead;
for(int i = 0; i < Lnum; i++)
{
pFast1 = pFast1->next;
}
while(pFast1 != pNorm2)//求取环的入口点
{
pFast1 = pFast1->next;
pNorm2 = pNorm2->next;
}
return pNorm2;
}
};
10、后续将继续更新