C++面试题整理

1.写一个string

该题主要考察赋值

class String
{
public:
    String(const char *str = NULL);    //通用构造函数
    String(const String &str);        //拷贝构造函数
    ~String();                        //析构函数

    String operator+(const String &str) const;    //重载+
    String& operator=(const String &str);        //重载=
    String& operator+=(const String &str);        //重载+=
    bool operator==(const String &str) const;    //重载==
    char& operator[](int n) const;                //重载[]

    size_t size() const;        //获取长度
    const char* c_str() const;    //获取C字符串

    friend istream& operator>>(istream &is, String &str);//输入
    friend ostream& operator<<(ostream &os, String &str);//输出

private:
    char *data;        //字符串
    size_t length;    //长度
};
//注意,类的成员函数中,有一些是加了const修饰的,表示这个函数不会对类的成员进行任何修改。一些函数的输入参数也加了const修饰,表示该函数不会对改变这个参数的值。

 

/*下面逐个进行成员函数的实现。
同样构造函数适用一个字符串数组进行String的初始化,默认的字符串数组为空。这里的函数定义中不需要再定义参数的默认值,因为在类中已经声明过了。
另外,适用C函数strlen的时候需要注意字符串参数是否为空,对空指针调用strlen会引发内存错误。*/
 String::String(const char *str)//通用构造函数
{
    if (!str)
    {
        length = 0;
        data = new char[1];
        *data = '\0';
    }
    else
    {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
}
//拷贝构造函数需要进行深复制。
String::String(const String &str)//拷贝构造函数
{
    length = str.size();
    data = new char[length + 1];
    strcpy(data, str.c_str());
}
//析构函数需要进行内存的释放及长度的归零。
String::~String()//析构函数
{
    delete []data;
    length = 0;
}
//重载字符串连接运算,这个运算会返回一个新的字符串。
String String::operator+(const String &str) const//重载+
{
    String newString;
    newString.length = length + str.size();
    newString.data = new char[newString.length + 1];
    strcpy(newString.data, data);
    strcat(newString.data, str.data);
    return newString;
}
//重载字符串赋值运算,这个运算会改变原有字符串的值,为了避免内存泄露,这里释放了原先申请的内存再重新申请一块适当大小的内存存放新的字符串。
String& String::operator=(const String &str)//重载=
{
    if (this == &str)    return *this;

    delete []data;
    length = str.length;
    data = new char[length + 1];
    strcpy(data, str.c_str());
    return *this;
}
//重载字符串+=操作,总体上是以上两个操作的结合。
String& String::operator+=(const String &str)//重载+=
{
    length += str.length;
    char *newData = new char[length + 1];
    strcpy(newData, data);
    strcat(newData, str.data);
    delete []data;
    data = newData;
    return *this;
}
//重载相等关系运算,这里定义为内联函数加快运行速度。
inline bool String::operator==(const String &str) const//重载==
{
    if (length != str.length)    return false;
    return strcmp(data, str.data) ? false : true;
}
//重载字符串索引运算符,进行了一个简单的错误处理,当长度太大时自动读取最后一个字符。
inline char& String::operator[](int n) const//重载[]
{
    if (n >= length) return data[length-1]; //错误处理
    else return data[n];
}
//重载两个读取私有成员的函数,分别读取长度和C字符串。
inline size_t String::size() const//获取长度
{
    return length;
}
//重载输入运算符,先申请一块足够大的内存用来存放输入字符串,再进行新字符串的生成。这是一个比较简单朴素的实现,网上很多直接is>>str.data的方法是错误的,因为不能确定str.data的大小和即将输入的字符串的大小关系。
istream& operator>>(istream &is, String &str)//输入
{
    char tem[1000];  //简单的申请一块内存
    is >> tem;
    str.length = strlen(tem);
    str.data = new char[str.length + 1];
    strcpy(str.data, tem);
    return is;
}
//重载输出运算符,只需简单地输出字符串的内容即可。注意为了实现形如cout<<a<<b的连续输出,这里需要返回输出流。上面的输入也是类似。
ostream& operator<<(ostream &os, String &str)//输出
{
    os << str.data;
    return os;
}
inline const char* String::c_str() const//获取C字符串
{
    return data;
}

2.将字符转化为int

 /*
 * 函数介绍: 将字符串转化为整形数
 * 输入参数:str str :数值字符串
 * 输出参数:
 * 返回值: 整型数结果
 */
 int str2int(const char *str)
 {
     int temp = 0;
     const char *ptr = str;  //ptr保存str字符串开头
 
     if (*str == '-' || *str == '+')  //如果第一个字符是正负号,
     {                      //则移到下一个字符
         str++;
     }
 
     while (*str != 0)
     {
         if ((*str < '0') || (*str > '9'))  //如果当前字符不是数字
         {                       //则退出循环
             break;
         }
 
         temp = temp * 10 + (*str - '0'); //如果当前字符是数字则计算数值
         str++;      //移到下一个字符
     }
 
     if (*ptr == '-')     //如果字符串是以“-”开头,则转换成其相反数
     {
         temp = -temp;
     }
 
     return temp;
 }

2.将字符转化为double

double str2double(const char *str)
 {
 
     size_t pointPos = std::string(str).find_first_of('.');
     long long temp = 0;
     const char *ptr = str;              //ptr保存str字符串开头 
 
     if (*str == '-' || *str == '+')     //如果第一个字符是正负号,
     {
         str++;                          //则移到下一个字符
     }
 
     while (*str != 0)
     {
         if (*str >= '0' && *str <= '9')
         {
             temp = temp * 10 + (*str - '0');
         }
         str++;
     }
 
     if (*ptr == '-')     //如果字符串是以“-”开头,则转换成其相反数
     {
         temp = -temp;
     }
     size_t afterPointlength = str - (ptr + pointPos + 1);    //小数点后有几位小数
     double result = temp*pow(0.1, afterPointlength);
     return result;
 }

3.如何将数字转化为字符串

#define MAX_DIGITS_INT 10
void IntToStr( int num ,char str[] )
{
int i = 0, j = 0;
int isNeg = 0;
char temp[MAX_DIGITS_INT + 2];
if( num<0 )
{
num* = -1;
isNeg = 1;
}

do
{
temp[i++] = (num%10) + '0';
num/ = 10;
}while(num);
if( isNeg )
temp[i++] = '-1';
while(i>0)
str[j++] = temp[--i];
str[j] = '\0';
}

4.如果翻转数字,例如将12345 变成54321

int resevetNumber(int i)
{
    int ret = 0;
    while(i>0)
    {
        int temp = i%10;
        ret = ret*10+temp;
        i = i/10;
    }
    return ret;
}

5.字符串中找出连续最长的数字串,此方法可以延伸到找出所有数字,找出非数字字符串等等

    string str,res,cur;
    cin >> str;
    for(int i = 0; i <= str.length();i++){        // = :处理字符串最后的数字串的情况,不加就丢失处理了
        if(str[i] >= '0' && str[i] <= '9'){
            cur += str[i];    // 数字+=到cur
        }else{
            if(str.size() > res.size()){
                res = str;    //找到更长子串,则更新字符串
            }
            str.clear();
        }
    }
    cout << res;
6.引用和指针有什么区别

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。

(2)可以有const指针,但是没有const引用;

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(7)指针和引用的自增(++)运算意义不一样;

7.智能指针

1. auto_ptr(c++98的方案,cpp11已经抛弃)

采用所有权模式。

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

2.unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

3.shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

4. weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

7.线程同步的方法

1.临界区(用户态,速度快)

2.互斥量对象(内核对象)

3.信号量对象(内核对象)

4.事件对象(内核)

 

9.进程通信方式

1.管道:速度慢,容量有限,只有父子进程能通讯    

2.FIFO:任何进程间都能通讯,但速度慢    

3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题    

4.信号量:不能传递复杂消息,只能用来同步    

5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

6.socket通信

7.内核对象

 

10.引用计数

该题一般主要问保存引用计数对象的类型,一定是指针,可以想象其他类型为啥不行,例如static

 

11.单例模式

1.懒汉式


class CSingleton  
{  
public:  
static CSingleton* GetInstance()  
{  
     if ( m_pInstance == NULL )    
         m_pInstance = new CSingleton();  
     return m_pInstance;  
}  
private:  
    CSingleton(){};  
    static CSingleton * m_pInstance;  

};

class CSingleton    
{    
private:    
    CSingleton()      
    {    
    }    
public:    
    static CSingleton * GetInstance()    
    {    
        static CSingleton instance;     
        return &instance;    
    }    
};  
线程非安全

2.饿汉式

CSingleton *CSingleton::instance = new CSingleton
class CSingleton    
{    
private:    
    CSingleton()      
    {    
    }    
public:    
    static CSingleton * GetInstance()    
    {    

           return   instance;
    }    

static CSingleton    *instance;
}; 

由于静态变量在程序启动前就已初始化,线程安全

3.多线程下的单例模式(懒汉式,双枷锁)

class Singleton  
{  
private:  
    static Singleton* m_instance;  
    Singleton(){}  
public:  
    static Singleton* getInstance();  
};  
  
Singleton* Singleton::getInstance()  
{  
    if(NULL == m_instance)  
    {  
        Lock();//借用其它类来实现,如boost  
        if(NULL == m_instance)  
        {  
            m_instance = new Singleton;  
        }  
        UnLock();  
    }  
    return m_instance;  

};

12.类图的关系
依赖关系
描述一个事物的变化可能会影响到使用它的另一个事物,反之不成立
当描述一个类使用另一个类作为它的函数成员参数时,就使用依赖关系
特征:
一个类的方法的参数的数据类型是另一个类的定义
一个类的方法使用了另一个类的属性
一个类的方法调用了另一个类的方法
例:

作用关系——关联
用于描述一个类的对象和另一个类的对象之间相互作用的连接,用实现来表示的两个类(或用一个类)之间的关联,在线段两端通常包含多重性(或称重数),多重性表示关联另一端类的对象要求与本端类的多少个对象发生作用
特征 均为属性上的关系
1、一个类的属性的数据类型是另一个类的定义;
2、一个类的部分对象与另一个类的部分对象存在属性值上的联系;
3、关联关系一旦建立,系统运行与否它都存在。
例:

重数A决定了B的每个对象与A的多少个对象发生作用
重数B决定了A的每个对象与B的多少个对象发生作用
重数的形式与含义:


包含关系——聚集和组合
是一种特殊的关联
聚集:表示的是整体与部分的关系,分为共享聚集和组成聚集
共享聚集:一个整体包含多个部分,但多个部分又可以是另外一个整体的部分
组合:组成聚集,即整体拥有各部分,部分与整体共存,且部分不为另外的整体所拥有
例:

继承关系——泛化
类之间的继承关系
特征:
一般类定义了共同的属性和方法
特殊类继承了一般类的属性和方法
特殊类还可以定义自己的属性和方法
例:子类1说明单继承,子类2说明多继承

13.常见的设计模式

  请参考https://www.cnblogs.com/chengjundu/p/8473564.html   主要了解 抽象工厂,适配器,观察者,组合,享元,策略,模板,外观,代理,桥接,装饰模式

设计模式的六大原则

总原则:开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。

1、单一职责原则

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。

 

2、里氏替换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

历史替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

 

3、依赖倒转原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。

 

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

 

5、迪米特法则(最少知道原则)(Demeter Principle)

就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

 

6、合成复用原则(Composite Reuse Principle)

原则是尽量首先使用合成/聚合的方式,而不是使用继承。

 

13.详解 TCP 连接的“ 三次握手 ”与“ 四次挥手 

参考:https://baijiahao.baidu.com/s?id=1654225744653405133&wfr=spider&for=pc

 

14.App网络传输协议(Json、XML、protobuf)

xml是一种最早的网络传输协议,常见于Java web开发中,不单单作为网络层的参数协议,还常见于各种配置文件中,在移动开发中也常见但是已不是主流的网络传输协议。

优点:可读性强,解析方便; 
缺点:效率不高,资源消耗过大; 

JSON是在移动端比较常见的网络传输协议,它较xml格式更叫的简单和“小”,因此比xml更适合移动端对流量和内存的控制。

优点:较XML格式更加小巧; 
缺点:传输效率也不是太别高,但相较于xml提高了很多;

ProtoBuf是Google开源的一套二进制流网络传输协议,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、Go 和Python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

优点:传输效率快(比xml和json快10-20倍),文档型协议; 
缺点:使用不太方便;

 

15.网络架构

 

16.SendMessage和PostMessage的区别,SendMessage可以跨线程吗?

一般来讲PostMessage就是把消息放入消息队列,而SendMessage发送的消息则不入队列而是直接处理(多线程会进入消息队列)

Queued Messages 要进消息队列排队等侯处理.当然在队列内也有优先级排序的问题.
而Nonqueued Messages 则不用排队,它们的优先级更高喔.

如果,窗口是由非主线程创建的,那么,那个线程将会跑一个自己的窗口消息队列。调用SendMessage会切换到该线程上去执行。


主线程是个窗口线程,在另外一个工作线程中调用了窗口线程的SendMessage,那么执行的过程是:
1.工作线程挂起
2.切换到窗口线程,执行对应的消息处理
3.切换回工作线程

SendMessage : 如果指定窗口由调用线程创建,那么窗口过程会被当成一个子程序立即调用。如果指定窗口由另外一个线程创建,那么系统会切换到那个线程,并且调用合适的窗口过程。在线程之间传递的消息仅仅当接收线程执行message retrieval code才会被处理。发送线程会被堵塞直到接收线程处理完消息。但是,发送线程在等待的同时会处理收到的nonqueued messages 。为了阻止这一点,使用带有SMTO_BLOCK参数 的SendMessageTimeout .

在多线程的程序里面,最好不要操作CWnd的实例指针,而是用API函数操作HWND窗口句柄。

17.SendMessage是否进入消息队列

      SendMessage发送出来的消息到底进入不进入消息队列,确切的说是有时进入,有时不进入。

    1.当向本线程所建立的窗口SendMessage消息时,它只是调用窗口的消息处理过程。会直接调用到消息处理过程不回进入消息队列。

 

   2.当一个线程向另一个线程所建立的窗口SendMessage时,该消息要添加到接收线程的消息队列,然后发送消息的线程进入等待状态,接收消息的线程处理完该消息后,由系统唤醒发送消息的线程,这时发送线程继续执行

 

17.函数对象

就是一个重载'()'运算符的类的对象。这样就可以直接使用‘对象名()’的方式,这跟调用函数一样,所以称谓函数对象

#include <iostream>
#include <string>

class Printer{
public:
    explicit Printer(){};
    void operator()(const std::string & str)const{
        std::cout<<str<<std::endl;
    }
};

int main(int atgc,char * argv[]){
    Printer print;
    print("hello world!");
    return 0;
}

 

现在来说说函数对象有哪些好处:

  (1)函数对象有自己的状态,即它可以携带自己的成员函数,而且这个函数对象在多次调用的过程中它的那些状态是共享的,而函数则不能做到这点(除非定义函数内部的静态变量或者全局变量)。

  (2)函数对象有自己的类型,而普通函数则没有。在使用STL的容器时可以将函数对象的类型传递给容器作为参数来实例化相应的模板,从而来定制自己的算法,如排序算法。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

class SuccessiveNumGen
{
public:
    SuccessiveNumGen(int origin = 0):m_origin(origin){}
    int operator()(){
        return m_origin++;
    }
private:
    int m_origin;
};

int main(int argc,char * argv[]){
    std::vector<int> dest;
    generate_n(back_inserter(dest),10,SuccessiveNumGen(3));
    for(int i=0;i<10;i++){
        std::cout<<dest[i]<<std::endl;
    } 
    return 0;
}

18.lambda表达式

C++11提供了对匿名函数的支持,称为Lambda函数(也叫Lambda表达式). Lambda表达式具体形式如下:

    [capture](parameters)->return-type{body}

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值
传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编
译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都
会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同

 

Lambda 在C++14可以函数body里引入新的variables,它可以周围的scope中捕获variables,哪些variables能被捕获,以传值或是引用,都需要说明。

  • []:默认不捕获任何变量;
  • [=]:默认以值捕获所有变量;
  • [&]:默认以引用捕获所有变量;
  • [x]:仅以值捕获x,其它变量不捕获;
  • [&x]:仅以引用捕获x,其它变量不捕获;
  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
  • [this]:通过引用捕获当前对象(其实是复制指针);
  • [*this]:通过传值方式捕获当前对象;

从上面可以看出lambda表达式 是一个匿名函数,也就是没有函数名的函数。也叫闭包,闭就是封闭的意思,包就是函数。lambda表达式 其实就是一个函数对象,内部创建了一个重载()操作符的类。

19、构造函数初始化和初始化列表有什么区别

初始化列表先于构造函数初始化(C++基础没啥)
20、怎么加快编译

1、在头文件中使用前置声明,而不是直接包含头文件

2、使用Pimpl模式

Pimpl全称为Private Implementation。传统的C++的类的接口与实现是混淆在一起的,而Pimpl这种做法使得类的接口与实现得以完全分离。如此,只要类的公共接口保持不变,对类实现的修改始终只需编译该cpp;同时,该类提供给外界的头文件也会精简许多。

3、高度模块化

模块化就是低耦合,就是尽可能的减少相互依赖。这里其实有两个层面的意思。一是文件与文件之间,一个头文件的变化,尽量不要引起其他文件的重新编译;二是工程与工程之间,对一个工程的修改,尽量不要引起太多其他工程的编译。这就要求头文件,或者工程的内容一定要单一,不要什么东西都往里面塞,从而引起不必要的依赖。这也可以说是内聚性吧。

以头文件为例,不要把两个不相关的类,或者没什么联系的宏定义放到一个头文件里。内容要尽量单一,从而不会使包含他们的文件包含了不需要的内容。记得我们曾经做过这么一个事,把代码中最"hot"的那些头文件找出来,然后分成多个独立的小文件,效果相当可观。

其实我们去年做过的refactoring,把众多DLL分离成UI与Core两个部分,也是有着相同的效果的 - 提高开发效率。

4、删除冗余的头文件

5、特别注意inline和template

这是C++中两种比较"先进"的机制,但是它们却又强制我们在头文件中包含实现,这对增加头文件的内容,从而减慢编译速度有着很大的贡献。使用之前,权衡一下。

6.预编译头文件
21、程序断点是 什么原理

软件断点

软件断点在X86系统中就是指令INT 3,它的二进制代码opcode是0xCC。当程序执行到INT 3指令时,会引发软件中断。操作系统的INT 3中断处理器会寻找注册在该进程上的调试处理程序。从而像Windbg和VS等等调试器就有了上下其手的机会。

硬件断点

X86系统提供8个调试寄存器(DR0~DR7)和2个MSR用于硬件调试。其中前四个DR0~DR3是硬件断点寄存器,可以放入内存地址或者IO地址,还可以设置为执行、修改等条件。CPU在执行的到这里并满足条件会自动停下来。

22、C++ 调用函数的具体步骤是什么

    栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,
    然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,
    也就是主函数中的下一条指令,程序由该点继续运行。

 

22.怎么判断链表是否有环、若有环找出环的节点(两种方法),求出环的长度

struct ListNode { int val; ListNode* next; };

方法一:快慢指针
定义一个快指针和慢指针都指向链表的head,快指针一次走两步,慢指针一次走一步,这样两个指针之间的举例会每次缩小一,如果该链表带环的话,那么快慢指针必然会相遇,并且相遇是在环中。

bool hasCycle(ListNode *head) 
{
    if(head == nullptr)//空链表直接返回false
	    return false;
	ListNode* fast = head;//快指针
	ListNode* slow = head;//慢指针
	while(fast && fast->next)
	{
	    fast = fast->next->next;
	    slow = slow->next;
	    if(fast == slow)
	        return true;
	}
	return false;
}

方法二:使用STL库中的map表进行映射

我们可以用map建立<ListNode*,int>的映射关系,int为节点出现的次数,将每个节点都放入map中,放入时并将int设置为1,如果链表带环,必然会在遇到之前插入过map的节点,那么如果检测到value为1,那就说明带环。

bool hasCycle(ListNode *head) 
{
    if(head == nullptr)
        return false;
    map<ListNode*,int> m;//定义映射map
    ListNode* cur = head;
    while(cur)
    {
        if(m[cur] == 0)
            m[cur] = 1;
        else
            return true;
        cur = cur->next;
    }
    return false;
}

求环的长度

要求环的长度我们先找到相遇的点,相遇的点必然在环内部,那么定义一个计数器,每走一步计数器++,当再次回到这个节点的时候,计数器就记录了这个环的长度。

ListNode *getmeet(ListNode *head)//找到相遇节点
{
    if(head == nullptr)
        return NULL;
    ListNode* fast = head;
    ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
            return slow;         
    }
    return NULL;
}
int cycleLenth(ListNode *head)
{
	ListNode* meet = getmeet(head);
    if(meet == nullptr)
    	return 0;
    int count = 1;
    ListNode* cur = meet->next;
    while(cur!=meet)
    {
    	count++;
    	cur = cur->next;
    }
    return count;
}

找环的入口节点

当快慢指针相遇时说明有环,并且这个相遇点一定在环内部,此时记录这个相遇节点,然后让一个指针从这个带环链表的头开始走,那么这两个指针相遇的点就是环的入口点。所以我们写代码的时候只需要找到相遇节点,再让一个指针从头开始走即可。

ListNode *getmeet(ListNode *head)//找到相遇节点
{
    if(head == nullptr)
        return NULL;
    ListNode* fast = head;
    ListNode* slow = head;
    while(fast && fast->next)
    {
        fast = fast->next->next;
        slow = slow->next;
        if(fast == slow)
            return slow;         
    }
    return NULL;
}
ListNode *detectCycle(ListNode *head)//查找入环点
{ 
    ListNode* meet = getmeet(head);
    if(meet == nullptr)
        return NULL;
    ListNode* cur = head;
    while(meet && cur)
    {
        if(cur == meet)
            return meet;
        meet = meet->next;
        cur = cur->next;
    }
    return NULL;
}

23.二叉树先关

<1>求结点总数 
size_t BTreeSize(BTNode* root){
    if(root == NULL){
        return 0;
    }
    //划分子问题
    return 1 + BTreeSize(root->_lchild) + BTreeSize(root->_rchild);
}<2>求叶子结点数 
size_t BTreeLeafSize(BTNode* root) {
    if(root == NULL){
        return 0;
    }
    //该结点的左右子树为空,返回1
    if(root->_lchild == NULL && root->_rchild == NULL){
        return 1;
    }
    return BTreeLeafSize(root->_lchild) + BTreeLeafSize(root->_rchild);
}
<3>求二叉树的深度 //选左右子树深度较大的递归size_t BTreeDepth(BTNode* root) {
    if(root == NULL){
        return 0;
    }
    
    size_t leftDepth = BTreeDepth(root->_lchild);
    size_t rightDepth = BTreeDepth(root->_rchild);
    if(leftDepth > rightDepth){   //选深度
        return leftDepth + 1;
    }
    return rightDepth + 1;
}
<4>求第K层二叉树的结点数 
//转化为k -1层结点数子问题 
size_t BTreeKLevelSize(BTNode* root, size_t k){
    if(root == NULL || k <= 0){
        return 0;
    }
    if(root && k == 1){  //到根结点
        return 1;
    }
    
    return BTreeKLevelSize(root->_lchild, k-1 ) + 
           BTreeKLevelSize(root->_rchild, k-1 ); 
} 
<5>二叉树查找 
BTNode* BTreeFind(BTNode* root, BTDataType x) {
    if(root == NULL){
        return NULL;
    }
    
    if(root->_data == x) return root;
    //定义变量来保存结果,这样如果在左子树找到了,就不用在去右子树查找,提高了效率 
    BTNode* lChildRet = BTreeFind(root->_lchild , x);  
    if(lChildRet){
        return lChildRet;
    }
    BTNode* rChildRet = BTreeFind(root->_rchild , x);
    if(rChildRet){
        return rChildRet;
    } 
}

24.链表求和

求和(将两个链表中的值相加到一个新的链表中)(3->4->6 + 2->3->4  = 5->8->0)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
{
       ListNode preHead(0), *p = &preHead;
       int extra = 0;
       while (l1 || l2 || extra) 
       {
          int sum = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + extra;
          extra = sum / 10;
          p->next = new ListNode(sum % 10);
          p = p->next;
          l1 = l1 ? l1->next : l1;
          l2 = l2 ? l2->next : l2;
       }
    
       return preHead.next;
}

25.求数组连续子数组最大和

  int getMaxSum(int[] arr)
	{
		if(arr == null)
			return -1;
		if(arr.length == 0)
			return 0;
		int max = arr[0];
		int result = arr[0];
		for(int i = 1;i < arr.length;i++)
		{
			max = max+arr[i];
			max = Math.max(max, arr[i]);
			result = Math.max(max, result);
		}
		return result;
	}

26.无序的数组(vector),求出数组最大的连续子集

int[] getMaxSumStartAndEnd(int[] arr)
	{
		int[] result = new int[2];
		int start = 0;//开始下标
		int end = 0;//结束下标
		if(arr == null)
			return null;
		if(arr.length == 0)
		{
			result[0] = 0;
			result[1] = 0;
			return result;
		}
		int max = arr[0];//临时最大值
		int tempMax = arr[0];//临时值
		int realMax = arr[0];//最后真正的最大值
		for(int i = 1;i < arr.length;i++)
		{
			tempMax = max+arr[i];
			max = Math.max(tempMax, arr[i]);
			if(max > realMax && max == tempMax)
			{
				//表示当前最大值是加上当前元素后得到的,那么就需要修改end下标值了
				end = i;
			}else if(max > realMax && max == arr[i])
			{
				//表示当前最大值是当前元素,那么需要修改start下标值
				start = i;
			}
			realMax = Math.max(max, realMax);
		}
		result[0] = start;
		result[1] = end;
		return result;
	}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值