chunlanse2014

导航

面试、笔试题记录

强制类型转换:

C++类型转换分为:隐式类型转换和显式类型转换

第1部分. 隐式类型转换

又称为“标准转换”,包括以下几种情况:
1) 算术转换(Arithmetic conversion) : 在混合类型的算术表达式中, 最宽的数据类型成为目标转换类型。

2)一种类型表达式赋值给另一种类型的对象:目标类型是被赋值对象的类型

3)将一个表达式作为实参传递给函数调用,此时形参和实参类型不一致:目标转换类型为形参的类型

4)从一个函数返回一个表达式,表达式类型与返回类型不一致:目标转换类型为函数的返回类型

第2部分. 显式类型转换

被称为“强制类型转换”(cast)
C     风格: (type-id)
C++风格: static_castdynamic_castreinterpret_cast、和const_cast..

标准C++中有四个类型转换符:static_castdynamic_castreinterpret_cast、和const_cast

 

dynamic_cast:动态类型转换   

static_cast:静态类型转换   

reinterpret_cast:重新解释类型转换   

const_cast:常量类型转换   

专业的上面很多了,我说说我自己的理解吧:   

dynamic_cast一般用在父类和子类指针或引用的互相转化;   

static_cast一般是普通数据类型(如int m=static_cast<int>(3.14));   

reinterpret_cast很像c的一般类型转换操作   

const_cast是把const或volatile属性去掉 

 

static_cast
用法:static_cast < type-id > ( expression )
说明:该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
 
dynamic_cast
用法:dynamic_cast < type-id > ( expression )
说明:该运算符把expression转换成type-id类型的对象。type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。

dynamic_cast具有类型检查的功能,比static_cast更安全。

reinterpret_cast
用法:reinterpret_cast<type-id> (expression)
说明:type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
 
const_cast
用法:const_cast<type_id> (expression)
说明:该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
 

多态性:

 语言题:
1、阅读程序,写出结果
#include<iostream>

class Animal
{
public:
    virtual char *getType() const {return "Animal";}
    virtual char *getVoice() const {return "Voice";}
};
class Dog : public Animal
{
public:
    virtual char *getType() const
    {
        return "Dog";
    }
    virtual char *getVoice() const
    {
        return "Woof";
    }    
};
void type(Animal &a)
{
    std::cout<<a.getType(); 
}
void speak(Animal a)
{
    std::cout<<a.getVoice(); 
}
int main()
{
    Dog d;
    type(d);
    std::cout<<"speak";
    speak(d);
    std::cout<<std::endl;
    return 0;
}

/************************************************************************/
/*正确答案是:
DogspeakVoice
请按任意键继续. . .

解析:在类Base中加了virtual关键字的函数就是虚拟函数,于是在Base的派生类Derived
通过重写虚拟函数来实现对基类虚拟函数的覆盖,这是面向对象中的多态性的体现。
*/
/************************************************************************/
运行结果是: DogspeakVoice    
我想问的是,为什么不是输出   DogspeakWoof   
也就是说   void type(Animal &a) {cout<<a.getType();} 
               void speak(Animal a)  {cout<<a.getVoice();}          
这里上下两行有什么区别? Animal为什么第一个函数实现了多态性,而第二个却没有呢?  引用类对象在其中有什么特别作用吗?
 
 由于Dog是Animal的子类,而Animal的getType() 和 getVoice() 都是虚函数,而实现了多态性,而void type(Animal &a)函数 中的参数是Animal的引用对象,由于Animal是Dog的父类,而父类的对象可以接收子类的对象,所以当调用type(d);这个函数时,type(Animal &a)函数中的&a就是d的别名,也就是说a中的地址和d的地址相同,所以能实行多态性,void type(Animal &a) {cout<<a.getType();}所以这里a.getType();调用的是Dog的成员函数.
void speak(Animal a)  {cout<<a.getVoice();}这函数由于a是Animal的对象,地址也是Animal类的,由于Animal是Dog的父类,而父类的对象可以接收子类的对象,但是由于a是Animal的对象,当d作为参数内赋给a时,会调用Animal的复制构造函数来用对象d来初始化a的成员变量,所以a的地址还是Animal类的,所以void speak(Animal a)  {cout<<a.getVoice();}这里的a.getVoice()调用的是Animal的成员函数.
 

赋值操作符:

 

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class MyClass
 5 {
 6 private:
 7     int m_iNumber;
 8     double m_dNumber;
 9     char acName[10];
10 public:
11     MyClass(int inumber,double dnumber,char* acnumber);
12 
13     ///////////////////////////////声明部分//////////////////////////////////////////
14     MyClass& operator=(const MyClass& val);
15     ///////////////////////////////////////////////////////////////////////////////
16 
17     void Show();
18 };
19 MyClass::MyClass(int inumber,double dnumber,char* acnumber)
20 {
21     m_iNumber=inumber;
22     m_dNumber=dnumber;
23     strcpy(acName,acnumber);
24 }
25 
26 //////////////////////////实现部分///////////////////////////////////////////////
27 MyClass& MyClass::operator=(const MyClass& val)
28 {
29     if (this != &val)
30     {
31         this->m_iNumber=val.m_iNumber;
32         this->m_dNumber=val.m_dNumber;
33         strcpy(this->acName,val.acName);
34     }
35     return *this;
36 }
37 //////////////////////////////////////////////////////////////////////////////////
38 
39 void MyClass::Show()
40 {
41     cout<<m_iNumber<<endl;
42     cout<<m_dNumber<<endl;
43     for (int i=0;i<10;++i)
44     {
45         cout<<acName[i]<<" ";
46     }
47     cout<<endl;
48 }
49 
50 int main()
51 {
52     MyClass a(5,6.8,"0123456789");
53     MyClass b(0,0.0,"0000000000");
54     b=a;
55     b.Show();
56     return 0;
57 }
58 
59 
60 /************************************************************************/
61 /* 
62 5
63 6.8
64 0 1 2 3 4 5 6 7 8 9
65 请按任意键继续. . .
66 */
67 /************************************************************************/

 

拓展:

算术和关系运算符:

算术运算符+:

Sales_data operator+(const Sales_data &lhs,const Sales_data &rhs)
{
    Sales_data sum=lhs;         //把lhs的数据成员拷贝给sum
    sum+=rhs;                    //将rhs加到sum中
    return sum;
}

相等运算符==

bool operator==(const Sales_data &lhs,const Sales_data &rhs)
{
    return lhs.isbn()==rhs.isbn()&&
             lhs.units_sold==rhs.units_sold&&
             lhs.revenue==rhs.revenue;
}

复合赋值运算符+=

Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
    units_sold+=rhs.units_sold;              //this->可以舍去不写
    revenue+=rhs.revenue;                    //this->可以舍去不写
    return *this;
}

递增(++)和递减(--)运算符

//声明
StrBlobPtr& operator++();                //前置运算符
StrBlobPtr& operator--();                
StrBlobPtr operator++(int);               //后置运算符
StrBlobPtr operator--(int);             

 

Socket原理与编程基础

一、Socket简介

Socket是进程通讯的一种方式,即调用这个网络库的一些API函数实现分布在不同主机的相关进程之间的数据交换。

几个定义:

(1)IP地址:即依照TCP/IP协议分配给本地主机的网络地址,两个进程要通讯,任一进程首先要知道通讯对方的位置,即对方的IP。

(2)端口号用来辨别本地通讯进程,一个本地的进程在通讯时均会占用一个端口号,不同的进程端口号不同,因此在通讯前必须要分配一个没有被访问的端口号

(3)连接:指两个进程间的通讯链路。

(4)半相关:网络中用一个三元组可以在全局唯一标志一个进程:

协议,本地地址,本地端口号

这样一个三元组,叫做一个半相关,它指定连接的每半部分。

(4)全相关:一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:

协议,本地地址,本地端口号,远地地址,远地端口号

这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。

 

二、客户/服务器模式

在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器(Client/Server, C/S)模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:

(1)首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。

(2)其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务器模式的TCP/IP。

服务器端:

其过程是首先服务器方要先启动,并根据请求提供相应服务:

(1)打开一通信通道并告知本地主机,它愿意在某一公认地址上的某端口(如FTP的端口可能为21)接收客户请求;

(2)等待客户请求到达该端口;

(3)接收到客户端的服务请求时,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。

(4)返回第(2)步,等待另一客户请求。

(5)关闭服务器

客户端:

(1)打开一通信通道,并连接到服务器所在主机的特定端口;

(2)向服务器发服务请求报文,等待并接收应答;继续提出请求......

(3)请求结束后关闭通信通道并终止。

 

从上面所描述过程可知:

(1)客户与服务器进程的作用是非对称的,因此代码不同。

(2)服务器进程一般是先启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。

 

介绍完基础知识,下面就介绍一些API函数:

 

创建套接字──socket()

应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:

 

SOCKET PASCAL FAR socket(int af, int type, int protocol);

 

该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。这里分三种:

(1)一是TCP流式套接字(SOCK_STREAM)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。

(2)二是数据报式套接字(SOCK_DGRAM)提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。

(3)三是原始式套接字(SOCK_RAW)该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。

参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。

 

指定本地地址──bind()

当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:

 

int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);

 

参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。如果没有错误发生,bind()返回0。否则返回SOCKET_ERROR。

 

建立套接字连接──connect()与accept()

这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接accept()用于使服务器等待来自某客户进程的实际连接。

 

connect()的调用格式如下:

 

int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

 

参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。

 

如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。

 

由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。

 

accept()的调用格式如下:

 

SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

 

参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。

 

accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。

 

四个套接字系统调用,socket()、bind()、connect()、accept(),可以完成一个完全五元相关的建立。socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind(),若采用面向连接,则可以不调用bind(),而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一的地址。

 

监听连接──listen()

此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:

 

int PASCAL FAR listen(SOCKET s, int backlog);

 

参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。

 

listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。

 

调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。

 

数据传输──send()与recv()

当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。

send()调用用于s指定的已连接的数据报或流套接字上发送输出数据,格式如下:

 

int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);

 

参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。

recv()调用用于s指定的已连接的数据报或流套接字上接收输入数据,格式如下:

 

int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);

 

参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。

 

输入/输出多路复用──select()

select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:

 

int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);

 

参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。

 

参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数writefds 指向要做写检测的套接字描述符集合的指针。exceptfds指向要检测是否出错的套接字描述符集合的指针。timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。

 

关闭套接字──closesocket()

closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:

 

BOOL PASCAL FAR closesocket(SOCKET s);

 

参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。

 

以上就是SOCKET API一些常用的API函数,下面是一段代码:

 

//客户端代码:

#include <WINSOCK2.H>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")

int main()
{
       int err;
       WORD versionRequired;
       WSADATA wsaData;
       versionRequired=MAKEWORD(1,1);
       err=WSAStartup(versionRequired,&wsaData);//协议库的版本信息

       if (!err)
       {
              printf("客户端嵌套字已经打开!\n");
       }
       else
       {
              printf("客户端的嵌套字打开失败!\n");
              return 0;//结束
       }

       SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0);
       SOCKADDR_IN clientsock_in;
       clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
       clientsock_in.sin_family=AF_INET;
       clientsock_in.sin_port=htons(6000);
       connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//开始连接
       char receiveBuf[100];
       recv(clientSocket,receiveBuf,101,0);
       printf("%s\n",receiveBuf);
       send(clientSocket,"hello,this is client",strlen("hello,this is client")+1,0);
       closesocket(clientSocket);
       WSACleanup();

       return 0;
}
客户端代码

 

//服务器端代码:

#include <WINSOCK2.H>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")

int main()
{
       //创建套接字
       WORD myVersionRequest;
       WSADATA wsaData;
       myVersionRequest=MAKEWORD(1,1);
       int err;
       err=WSAStartup(myVersionRequest,&wsaData);

       if (!err)
       {
              printf("已打开套接字\n");
       }
       else
       {
              //进一步绑定套接字
              printf("嵌套字未打开!");
              return 0;
       }
       SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//创建了可识别套接字
       //需要绑定的参数
       SOCKADDR_IN addr;
       addr.sin_family=AF_INET;
       addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址
       addr.sin_port=htons(6000);//绑定端口
       bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//绑定完成
       listen(serSocket,5);//其中第二个参数代表能够接收的最多的连接数
       //////////////////////////////////////////////////////////////////////////
       //开始进行监听
       //////////////////////////////////////////////////////////////////////////
       SOCKADDR_IN clientsocket;
       int len=sizeof(SOCKADDR);
       while (1)
       {
              SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//如果这里不是accept而是connection的话。。就会不断的监听
              char sendBuf[100];
              sprintf(sendBuf,"welcome %s to bejing",inet_ntoa(clientsocket.sin_addr));//找对对应的IP并且将这行字打印到那里
              send(serConn,sendBuf,strlen(sendBuf)+1,0);
              char receiveBuf[100];//接收
              recv(serConn,receiveBuf,strlen(receiveBuf)+1,0);
              printf("%s\n",receiveBuf);
              closesocket(serConn);//关闭
              WSACleanup();//释放资源的操作
       }
       return 0;
}
服务器端代码

 

 

 

简言之:socke相当于进行网络通信两端的插座,只要对方的socket和自己的socket有通信联接,双方就可以发送和接收数据了。

如果是服务器端的程序:先调用socket()创建一个套接字,调用bind()绑定IP地址和端口,然后启动一个死循环,循环中调用accept()接受连接。对于每个接受的连接,可以启动多线程方式进行处理,在线程中调用send()、recv()发送和接收数据。

如果是客户端的程序:先调用socket()创建一个套接字,然后调用connect()连接服务器,之后调用send()、recv()发送和接收数据。

 

 TCP的3次握手4次挥手全过程

 

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用3次握手建立一个连接。

第1次握手:建立连接时,客户端发送SYN(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认。

第2次握手:服务器收到SYN包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。

第3次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器端进入ESTABLISHED状态,完成3次握手。

完成3次握手,客户端与服务器开始传送数据。

SYN+ACK重传次数:服务器发送完SYN+ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。
3次握手

 

 

稳定排序和不稳定排序

      首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

      其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序在高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。

回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

(1)冒泡排序(稳定)

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

(2)选择排序(不稳定)

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个位置选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

(3)插入排序 (稳定)
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的

(4)快速排序 (不稳定)
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

(5)归并排序(稳定) 
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

(6)基数排序 (稳定)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

(7)希尔排序(shell) (不稳定)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

(8)堆排序 (不稳定)
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法

 

new/delete、 malloc/free

内存泄漏是指堆内存(heap memory)的泄漏(memory leak)。
堆内存指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。
应用程序一般使用malloc,realloc,new等函数从堆中分配内存,使用完后,程序必须负责相应的调用free或delete释放该内存,否则,这块内存就不能被再次使用,即这块内存泄漏了。 
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。
它们都可用于申请动态内存和释放内存。

 

malloc()到底从哪里得来了内存空间:

malloc()到底从哪里得到了内存空间?答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

free()到底释放了什么

free()释放的是指针指向的内存!注意!释放的是内存,不是指针!这点非常非常重要!指针是一个变量,只有程序结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在!只不过现在指针指向的内容是垃圾,是未定义的,所以说是垃圾。因此,释放内存后把指针指向NULL,防止指针在后面不小心又被引用了。非常重要啊这一点!

 

堆和栈的区别

 

什么是堆:堆是大家共有的空间,分全局堆和局部堆。
        全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。
        堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,
        但是记得用完了要还给操作系统,要不然就是内存泄漏。

什么是栈:栈是线程独有的,保存其运行状态和局部自动变量的。
        栈在线程开始的时候初始化,每个线程的栈互相独立。
        每个函数都有自己的栈,栈被用来在函数之间传递参数。
        操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。
        栈空间不需要在高级语言里面显式的分配和释放。 
什么是堆?什么是栈?

 

 

内存分配

a、栈区(stack)
向下生长,由编译器自动分配释放 ,存放函数的参数值,局部变量的值等,内存的分配是连续的,类似于平时我们所说的栈,如果还不清楚,那么就把它想成数组,它的内存分配是连续分配的,即,所分配的内存是在一块连续的内存区域内.当我们声明变量时,那么编译器会自动接着当前栈区的结尾来分配内存.

b、堆区(heap)
向上生长(从低地址开始存放),一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收.类似于链表,在内存中的分布不是连续的,它们是不同区域的内存块通过指针链接起来的.一旦某一节点从链中断开,我们要人为的把所断开的节点从内存中释放.

c、全局/静态存储区(static)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放

d、只读区

存放程序和常量,比如const int a = 2;    char *p = "abc"; 这里的a和“abc”都存放在可读写区。
栈区、堆区、全局/静态存储区、只读区

 

 

指针以及相关问题

指针是存放地址的变量。指针所占的存储空间和操作系统有关,32位操作系统占4个字节,64位操作系统占8个字节。

例题:64位系统上定义的变量int* a[2][3]占据 2*3*8 个字节

&运算符

在C语言中有两种含义:1、位操作符,表示‘位与’;2、取地址运算符,比如int a = 10,*p = &a;

在C++中除了上述两种含义外还有一种含义:引用,比如
int a, &b = a;//a和b等价,共享一个存储空间,a,b的值同时变化。

常量指针和指针常量

常量指针:指针所指向的对象只能被读取,而指针本身的值可以修改,
比如:int a,b;   
const int *p = &a;   p = &b;

指针常量:不能修改指针本身的值,但可以修改指针所指向的对象的值,
比如int a = 10;
int * const p = &a; *p = 20;

常量

常量是不能修改的,存放在内存的只读区,
比如const int a = 10; 
char *p = "abc";
这里的a和字符串abc都是常量,只能读,不能修改。

函数指针和指针函数

函数指针也是一个指针变量,它指向函数的入口地址。
int (*f)(int a, int b); 
// 声明函数指针, 函数指针定义为一个指向一个返回值为整型,有两个参数并且两个参数的类型都是整型的函数。

指针函数是一个函数,这个函数的返回值是一个指针。
int *f(int a, int b); 
//指针函数声明,该函数有两个整形参数,返回值是一个指针
指针相关问题

 

 数据结构链表部分

 

//创建单链表
typedef struct node
{
    char data;
    struct node* next;
}LNode;

//创建单链表时一定要保证尾结点的next为空
LNode* create(LNode* head)
{
    if(NULL==head)
    {
        head=(LNode*)malloc(sizeof(LNode));      //创建头结点
        head->next=NULL;                
    }
    LNode* cur=head;
    LNode* tmp;
    char elem;
    while(1)
    {
        cin>>elem;
        if(elem=='#')                                    //结束条件
        {
             break;
        }
        tmp=(LNode*)malloc(sizeof(LNode));     //申请临时结点为插入结点
        tmp->data=elem;
        tmp->next=NULL;                           //尾结点的next为空
        cur->next=tmp;
        cur=tmp;
    }
    return head;
}

//用法
LNode* head=NULL;
head=create(head);
单链表的建立

 

 

 

 

 

posted on 2015-09-19 11:07  chunlanse2014  阅读(363)  评论(0编辑  收藏  举报