C++面试题

 

1.C++中的强制转换

1. const_cast,将转换掉表达式的const性质。
1. 用于去除指针变量的常属性,将它转换为一个对应指针的普通变量。

2. 反过来也可以将一个非常量指针转换为一个常量指针变量。

        3. 它无法将一个非指针的常量转化为普通变量。
 e.g:const  char  *p;

     Char  *s=string_copy(const_cast<char *>(p));

2.static_cast:编译器隐式执行的任何类型转换都可以由static_cast显示完成。
           1.用于转换基本类型和具有继承关系的类型之间的转换。
           2.static_cast不太用于指针类型的之间的转换,它的效率没有reinterpret_cast的效率高。
          e.g:double d=97.0
      Char ch=static_cast<char>(d);
       较大的类型===》小:使用强制转换,告诉程序的者和编译器:我们知道并且不关心潜在精度损失。
3.reinterpret_cast:通常为操作数的位模式提供较低层次的重新解释。
                 1.将一个类型的指针转换为另一个类型指针,这种在转换不修改指针变量值数据存放格式。
                 2.只需在编译时重新解释指针的类型,他可以将指针转换为  一个整型数但不能用于非指针的转换。
4.dynamic_cast: 1.只能在继承类对象的指针之间或引用之间进行类型转换
                  2.这种转换并非在编译时,而是在运行时,动态的

                  3.没有继承关系,但被转换的类具有虚函数对象的指针进行转换

 
 
 
 

2.什么叫多态

基类指针(同引用)指向不同的派生类对象,调用派生类和基类的同名覆盖方法,基类指针指向哪个派生类对象,调用的就是它的覆盖方法!

多态就是通过动态绑定来实现的 =》 动态绑定通过vfptr和vftable来实现的

3.什么叫符号解析?

符号解析是发生在连接阶段的,符号表合并的时候,符号重定向是重点,每个obj(目标文件)都有符号表,有定义和引用,合并时(“UND”--------》符号引用),引用符号时,要知道它定义的地方,定义只有一次,引用可以有多次,解析完之后,给符号分配虚拟地址。

4.什么叫做符号的重定向?

   发生在连接阶段,是核心,符号解析完,都有地址,然后再返回到代码段,把代码段上还没有分配地址的,需要重定向。

5.单例模式?

1.概念:单例模式的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例,即一个类只有一个对象实例
2. 饿汉式 是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变
          懒汉式 如果在创建实例对象时不加上synchronized (同步/锁) 则会导致对对象的访问不是线程安全的
          推荐使用第一种 
从实现方式来讲他们最大的区别就是懒汉式是 延时加载, 他是在 需要的时候才创建对象 ,而饿汉式在虚拟机启动的时候就会创建
版本一: 使用懒加载(快加载),只有在使用时才实例化
class Singleton
{public:
    static Singleton* getIntance() 
    {
        if (pobject == NULL)//懒加载,只有在使用时才生成
        {
            pobject = new Singleton();
        }
        return pobject;
    }private:
    Singleton(){}
    static Singleton *pobject;
};
Singleton* Singleton::pobject = NULL;//懒加载,只有在使用时才生成
版本二: 考虑多线程安全问题,使用互斥锁
·  判断是否是线程安全的——是否存在竞态条件
·  判断是否存在竞态条件——随着线程调度顺序的不同,代码执行的结果也会不同。
·  存在竞态条件的代码称为临界区,临界区代码需要是原子操作,实现原子操作需要使用互斥锁,if语句不是原子操作
class Singleton

{public:

    static Singleton* getIntance()

    {

        pthread_mutex_lock(&mutex);//多线程线程安全问题
        if (pobject == NULL)//懒加载,只有在使用时才生成
        {
            pobject = new Singleton();
        }
        pthread_mutex_lock(&mutex);
        return pobject;
    }private:
    Singleton(){}
    static Singleton *pobject;
};
Singleton* Singleton::pobject = NULL;//快加载 懒加载
版本三: 由于可能存在单线程或多线程共存的使用场景,降低单线程获取释放锁的效率,使用双重if判断
·  单线程时,只需要执行一次获取释放互斥锁操作,之后第一个if语句都为false
·  多线程时,依旧考虑线程安全与竞态条件问题
class Singleton

{public:

    static Singleton* getIntance()

    {

        if (pobject == NULL)//懒加载,只有在使用时才生成

        {

            pthread_mutex_lock(&mutex);//多线程线程安全问题

            if (pobject == NULL)//单线程时效率问题

            {

                pobject = new Singleton();

            }

            pthread_mutex_lock(&mutex);

        }

        return pobject;

    }private:

    Singleton(){}

    static Singleton *pobject;

};

Singleton* Singleton::pobject = NULL;//快加载 懒加载
版本四: 考虑编译器的指令优化和CPU的动态指令优化
·  volatile关键字阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回内存
·  volatile关键字阻止编译器调整操作volatile变量的指令操作
·  barrier指令会阻止CPU对指令进行动态换序优化
class Singleton

{public:

    volatile static Singleton* getIntance()

    {
        if (pobject == NULL)//懒加载,只有在使用时才生成
        {
            pthread_mutex_lock(&mutex);//多线程线程安全问题

            if (pobject == NULL)//单线程时的效率问题
            {
                Singleton* temp = new Singleton;
                barrier();//防止CPU对指令进行动态换序优,使对象的构造一定在barrier完成,因此赋值给pobject的对象是完好的
                pobject = temp;
            }
            pthread_mutex_lock(&mutex);
        }
        return pobject;
    }private:
    Singleton(){}
    volatile static Singleton *pobject;
};volatile Singleton* Singleton::pobject = NULL;//快加载 懒加载

6.一个类,写了构造函数,还写了一个虚构造函数,可不可以,会发生什么?

    构造函数不能实现成虚构造函数的,我们知道一个函数能构造成为虚函数,那么它需要依赖两个条件:
1.  这个函数能生成符号
      因为虚函数的地址要记录在虚函数表 vftable 中,如果连函数的符号都不生成(如 inline 函数),那么它不能在虚函数表当中记录地址。
2.  函数调用依赖于对象
      vfptr 存储在对象的内存中,通过 vfptr 才能找到 vftabe, 进而获取虚函数的地址,所以,虚函数必须依赖于对象。基于以上的描述,像 inline 函数, static 成员方法,构造函数等都是不能实现 virtual 函数的,也就是不能使用 virtual 来修饰这些函数,编译器会报错,编都编不过。
    所以构造函数不可以声明为虚函数,析构函数可以实现成虚析构函数(此时的对象还在)。

7.为什么在构造和析构函数中都不要调用虚函数?

          在构造函数中不要调用虚函数,在基类构造的时候,虚函数是非虚的(此时还没有生成对象),不会走到派生类中,既是采用的静态绑定,显然的是:当为我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父亲对象部分的时候调用子类的虚函数实现,但是不是说你不可以那么写程序,你那么写,编译器也不会报错,只是你如果那么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。

       在析构函数中也不要调用虚函数,在析构的时候会首先调用子类的析构函数,析构掉对象中的子类部分,然后在调用基类的析构函数析构基类部分,如果在基类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。

8.宏定义和内联函数的区别?

1.    宏定义不是函数,但是使用起来像函数,预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。
       内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句, while,switch, 并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
2.     宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串的替换
       内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
3.    宏定义是没有类型检查的,无论对还是错都是直接替换。
      内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值,参数列表等。
4.     宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入,这样可以省去函数压栈退栈,提高了效率。

9.内联函数和普通函数的区别?

1.     内联函数和普通函数的参数传递的机制相同,但是编译器会在每处调用内联函数的地方将内联函数展开,这样既避免了函数的开销又没有宏机制的缺陷。
2.     普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。
       内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有 N 次调用了内联函数则会有 N 次展开函数代码。
3.     内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句 ,如果 内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执性。

10..你有什么办法解决资源泄露(内存泄露)?

引入智能指针
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放
利用栈上的智能指针对象出作用域会自动析构这一特点
裸指针  =》  堆内存
不带引用计数的智能指针   =》 浅拷贝
auto_ptr   // C++
独占所有权,所有权转移
Auto_ptr版本的智能指针采用的是管理权转移的方法,即由a构造出b对象之后,a对象置为NULL,即之后不能再访问a对象,只有b一个对象维护该空间
模板auto_ptr是C++98提供的解决方案,C+11已将其摒弃,不推荐使用
 
有很多问题。不支持复制(拷贝构造函数)和赋值(operato=),但是复制或赋值的时候不会提示出错,因为不能被复制,所以不能被放入容器中
scoped_ptr// Boost 不允许拷贝构造( unique_ptr // c++11
 Scoped_ptr的实现原理是防止对象间的拷贝和赋值,即将拷贝和赋值运算符重载放入保护或私有的访问限定符,只声明不定义防止他人在类外拷贝,简单粗暴地解决了auto_ptr的缺点,提高了代码的安全性,但是导致功能不完整。

unique_ptr shared_ptr 都提供了删除器功能
带引用计数的智能指针:
1.  shared_ptr : 强智能指针(可以改变资源的引用计的)
  Shared_ptr的实现原理是通过引用计数(int *count)来实现,拷贝或赋值时将引用计数加1,析构时只只有当引用计数减到0才释放空间,否则只需将引用计数减1即可。
2.  weak_ptr : 弱智能指针(只能观察对象存活与否,不会引起对象资源计数的改变)
weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。 
带有引用计数的智能指针使用规则:
1.定义对象的时候用强智能指针持有
2.其它地方一律采用弱智能指针持有,当想访问对象成员的时候,先通过弱智能指针的lock方法,

把弱智能指针 =》 强智能指针  =》  访问对象的成员

11. 定义一个空的类型,里面没有任何成员变量和成员函数,对该类型求sizeof,得到的结果是多少?

   答案是1,空类型的实例不包含任何信息,本来求sizeof应该是0,但是当我们声明该类型的实例的时候,它必须在内存中占一定的空间,否则无法使用这些实例,,至于占用内存多少,由编译器决定,vs中每个空类型的实例占用1字节的空间。

12.如果在该类型中添加一个构造函数和析构函数,再对该类型求sizeof,得到的结果又是多少?

   还是1,调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例内添加任何额外的信息。

13.那如果把析构函数标记为虚函数呢?

     C++编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位的机器上,一个指针占4字节的空间,因此求sizeof得到4,如果是64位的机器,一个指针占8字节空间,因此求sizeof是8 

14.为什么在构造函数中,我们传引用而不是传值?

 

      如下实例中,拷贝构造函数A(A other)传入的参数是A的一个实例,由于是传值参数,我们把形参复制到实参会调用复制构造函数,就会形成永无休止的递归调用从而导致栈溢出,因此c++的标准中不允许复制构造函数传值参数,解决这个问题,我们把传值,该为传(常量)引用。

 

15.数组和指针的区别?

   

结果为 20  4   4

Data1是一个数组,sizeof(data1)是求数组的大小,这个数组包含5个整数,每个整数占字节是4,因此总共是20字节,data2声明为指针,尽管它指向了数组data1的第一个数字,但它的本质仍然是一个指针,在32位系统上,对任意指针求sizeof,得到的结果都是4.在c/c++中,当数组作为函数进行传递时,数组就自动的退化为同类型的指针,因此尽管函数GetSize的参数data被声明为数组,但它会退化为指针,size3的结果仍然是4。 

16.字符串

 

为了节内存,c/c++把字符串放在单独的一个内存区域,当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址

Str1和str2是两个字符创数组,为它们分配12字节的空间,把“hello world”的内容分别复制到数组中去。这是两个初始地址不同的数组,str1和str2的值是不同的。

 Str3和str4是两个指针,我们无须为它们分配内存以存储字符串的内容,只需把他们两个指向”hello word”在内存中的地址就可以了,由于hello world是常量字符串,它在内存中只有一个拷贝,因此str3和str4指向的是同一个地址

17.C++中为什么已经有个构造函数负责初始化,为什么还需要构造函数初始化表呢?

 

三种情况要使用初始化成员列表:

1. 需要初始化的数据成员是对象的情况。

2. 需要初始化const修饰的类成员

3. 需要初始化引用成员数据

原因 :对于类成员是const修饰,或是引用类型的情况,是不允许赋值操作的(const就是防止被错误赋值,引用类型必须定义赋值在一起),因此只能用初始化列表对齐进行初始化。

        由于对象赋值比初始化要麻烦的多,因此也带来的性能上的消耗,这是对成员数据类型是对象成员采用初始化列表进行初始化的主要原因。

C++必须带有初始化列表的情况:

1. 成员函数是没有默认构造函数的类,若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。

2. const成员或引用类型的成员,因为const对象或引用类型只能初始化,不能对他们赋值。

3. 在一个类中使用另一个类构造时,类是一个抽象的概念,并不是一个实体,并不能包含属性值(这里来说也就是构造函数的参数了)只有对象才占一定的内存空间,含有明确的属性值。所以我们在构造函数的后面加上冒号并指定调用那个类成员的构造函数来解决问题。

Teacher是另一个类,teacher是一个它的实例

注意:数据成员被初始化的顺序与构造函数初始化列表中的次序无关,而是与成员的定义顺序有关

初始化列表,严格来说,应该叫做构造函数的初始化列表,一个类的完整构造顺序,应该是先按照成员变量定义顺序依次构造成员变量,最后再调用当前类的构造函数构造一个新的对象,因此当构造函数调用时,说明成员变量(或者成员对象)早已初始化(或构造)完成,那么成员变量(或者成员对象)如果没有默认的初始化或者构造方式的话,就在当前类构造函数的初始化列表当中。

18.sharedptr引用计数存在哪里?

 

 

     Shared_ptr是一种带有引用计数的智能指针,智能指针的一种功能就是在作用域到期后,智能指针自动析构,在析构函数中自动释放被托管的资源,以防止资源泄漏的各种问题。

     一个资源可以对应有很多个share_ptr,不能每一个智能指针都去把资源释放一遍,因此必须给没有资源都添加一个相应的引用计数,只有当资源的引用计数为0时,,智能指针才能去释放它所管理的资源,否则什么也不做

     一个资源只能对应一个引用计数,因此引用计数肯定是在创建的,引用同一个资源的所有智能指针是共享这个引用计数的,才不至于在增减资源引用计数时导致错乱。

 

19.什么是纯虚函数?为什么要有纯虚函数?纯虚函数的表放在哪里的?

  在下面这个类中talk成员函数就是纯虚函数。

  

   在继承结构设计中,基类一般是所有派生类共有属性的一个集合,不代表任何实体,比如人这个基类,没有代表具体的某一个人,那么在这个基类里面实现talk(说话)这个方法就显得不明确了,不同的人,说话方式不同,talk这个方法无法实现,所以就被实现成纯虚函数(基类的方法需要依赖具体实体的都必须实现成纯虚函数)

虚函数表存放的位置:虚函数表是在编译过程中生成的,一个类只要有虚函数,或者是从基类继承来了虚函数,那么它就会有一个虚函数表生成,虚函数表运行是放在.rodata段,只能读,不能写,和常量字符串时放在同一段的,它的生命周期是整个应用程序的生命周期。

使用纯虚函数的情况:

1. 当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化。

2. 这个方法必须在派生类中被实现。

20.malloc和new区别?

 

1. malloc和new都是在堆上开辟内存的

     malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化

2. malloc是函数,开辟内存需要传入字节数,如malloc(100);表示在堆上开辟100个字节的内存,返回void*,表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址;new是运算符,开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转。

3. malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或是失败,new运算符其实是operator  new函数的调用,它底层调用的也是malloc来开辟内存的,new它比malloc多的就是初始化功能,对于类类型来说,所谓初始化,就是调用相应的构造函数。

4. malloc开辟的内存永远是通过free来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是delete []来释放内存的。

5. malloc开辟的内存永远只有一种方式,而new有四种,分别是普通的new(内存开辟失败抛出bad_alloc异常),nothrow版本的new,   const  new以及定位new.

21.map/set的实现原理?

 

 

C++中的map 和set容器底层都是由红黑树来实现的,其中map是用来存储键值映射对的,它把[key,value]打包成pair对象存储在红黑树结构上,元素都是经过排序的,因此可以在O(log2 n)的时间复杂度内对set和map进行增删查操作,效率非常高。

在c++STL中,map和multimap,set和multiset这四种关联容器的底层都是红黑树实现的,因此如果要把自定义类类作为set和map的元素类型的话,一定要给自定义类型提供operator<  或者operator>比较运算符的重载函数,因为红黑树是一颗二叉排序树,入set和map的元素都是要经过排序的

 

22. 详细解释deque的底层的原理?

 

  deque是C++STL顺序容器的一种,叫双端队列,底层是动态开辟的二维数组(一维数组放的全是指针,指向二维的分段连续的缓冲区buffer)

   deque的底层内存是连续的吗?

        deque底层是一个动态开辟的二维数组,一维里面放的是指针,指向动态开辟的第二维数组,因此deque底层的内存是分段连续的,整体是不连续的,deque底层的数据结构类似与链地址法实现的哈希表数据结构,只不过哈希表桶里面放的是链表,deque放的还是数组。

deque和vector容器的区别?

     vector提供了push_back和pop_back尾端的删除,时间复杂度是O(1),但是vector首端插入删除O(n),效率比较底;deque是双端队列,提供push_back,pop_back,push_front,pop_front尾端和首端的插入删除,时间复杂度都是O(1)。

 但是如果从中间进行插入,删除等操作,由于vector底层内存是绝对连续的,因此效率要比deque高,所以我们一般在使用队列的场景下,首尾增删比较多的情况下选择deque,否则一般的情况下都选择vector;还有就是数据量不大的情况下,由于deque的第二维数组是 事先分配好的内存,可以直接使用,初始操作效率高,二vector默认构造底层空间是0,待添加数据的时候,底层内存才从1开始以2倍的速度开始扩容,因此vecor的初始操作的效率比较低,好在Vector提供了一个reserve方法,可以给vector容器预留足够的空间,STL库里面的容器适配器stack和queue默认就是依赖deque容器来实现功能的,而优先级队列priority_queue是依赖vector来实现的,因为优先级队列底层默认是一个大根堆,因此在一个内存连续的容器vector(底层是一维数组)上来构建效率高。

23.迭代器失效的问题?

 

 

1. erase的时候,迭代器会失效,在STL中,我们不能以指针来看待迭代器,指针是与内存绑定的,而迭代器是与容器里的元素绑定的,删除之后,迭代器就失效了,在对其重新赋值之前,不能在访问此迭代器。

解决:

 

 

1. 增加元素到容器中

   当capacity么有变化时:

   

当capacity有变化时:

 

24.堆和栈的区别?

 

 

 

 

1. 管理方式:栈是由编译器自动管理的;堆是由程序员控制,使用方便,但是产生内存泄漏。

2. 生长方向:栈向底地址扩展(即向下生长),是连续的内存区域;堆向高地址扩展(即向上生长),是不连续的内存区域,系统用链表来存储空间内存地址,自然不连续,而链表从底地址向高地址遍历

3. 空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存

4. 存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆通常在头部有一个字节存放其大小,堆用于储存生存期和函数调用无关的数据,具体内容由程序员安排。

5. 分配方式:栈可静态分配或动态分配,静态分配由编译器完成,如局部变量的分配,动态分配由alloca函数在栈上申请自动释放。堆只能动态分配且手工释放。

6. 分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压榨出栈由专门的指令执行,因此效率较高,堆由函数库提供,机制复杂,效率比栈底得多

7. 分配系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出 

操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链 表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。                             

     此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。                                               

 

8. 碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效 率低                         
    (1) 可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用 也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆 。          
   (2)使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。       
 

25.早绑定和晚绑定

 

 

 

早绑定或者静态绑定=====》编译时期的绑定,就是这样的函数调用在编译时期就是明确的调用。

 

晚绑定或者动态绑定=======》运行时期的绑定,就是函数调用在编译阶段不确定,只有在运行时才知道到底调用的是哪一个函数。

26.静态数据成员的特点?

 

 

 

 

 

 

1.对于非static数据成员,每个对象都有自己的拷贝。而static数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。

2.静态数据成员存储在全局数据区,静态数据成员定义时才分配空间,所以不能在类声明中定义。

3.静态数据成员和普通函数成员一样的访问规则;除了定义,定义不要管访问规则。

4.因为静态数据成员在全局数据区分配内存,属于本类的所有对象共存,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它。

5.静态数据成员初始化与一般数据成员初始化不同,静态数据成员初始化格式为:

 

6. 类的静态数据成员有两种访问形式:

26.inline内联函数和普通函数的区别?

 

 

 

 

 

 

 

普通函数的调用在汇编上有标准的push压实参指令,然后call指令调用函数,给函数开辟栈帧,函数运行完成,有函数退出栈帧的过程,而inline内联函数是在编译阶段,在函数的调用点将函数的代码展开,省略了函数栈帧开辟回退的调用开销,效率比较高。

 

 

27.Const和static的应用?

 

 

 

 

 

Static关键字的作用:

1. 函数体内的static变量的作用范围是为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用的时候仍维持上次的值。

2. 在模块内static全局变量可以被模块内所用函数访问,但是不能被模块外其他函数访问;

3. 在模块内的static的函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。

4. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝

5. 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

Const关键字的作用:

1. 欲阻止一个变量被改变,可以使用关键字const,在定义const变量时,通常需要对它进行初始化,因为以后就没有机会去修改它了。

2. 对指针来说,可以定义指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const.

3. 在一个函数声明中,const可以修饰形参,表明其是一个输入函数,在函数内部不能改变其值;

4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;

操作 (a * b) = c显然不符合编程者的初衷,也没有任何意义。 

 

 

 

 

 

5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。例如:  
const classA operator*(const classA& a1,const classA& a2);  
operator*的返回结果必须是一个const对象。如果不是,这样的变态代码也不会编译出错:  
classA a, b, c;  
(a * b) = c; // 对a*b的结果赋值  

28.输入www.xxxxx.com敲回车键这期间发生了什么浏览器如何处理的讲一下

 

 

 

 

 

http://www.baidu.com/

http://mail.163.com/

以上均不是域名,域名只是其一部分。首先对http://www.baidu.com/http://mail.163.com/进行解析:

1.http://为超文本传输协议

2.www,mail为服务器名,代表服务器

3.baidu.com,163.com是域名

4.mail.163.com是网站名。即服务器名+域名

5.末尾的 / 为根目录。通过网站名找到服务器,然后在服务器下存放网页的根目录

6./之后为根目录下默认的网页

7.http://mail.163.com/ 为URL,统一资源定位符,全球性地址,用于定位网上的资源 Uniform Resource Locator

 

点击回车后;

1.协议解析,一般浏览器中都支持多种协议,例如http,https,ftp。

2.缓存查询,协议解析之后做缓存查询,看这个URL是否被浏览过。如果浏览过,又不做强制刷新,则不会再次请求。

3.浏览器对域名进行解析,将域名转换为IP

4.浏览器 请求-处理-响应

5.显示

这其中涉及的还有很多知识。跳转、重定向、ajax、dns、请求处理方式。

29.什么是死锁?

 

 

 

 

 

 

所谓死锁,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,他们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力的协助下,永远分配不到必须的资源而无法继续运行,就产生了一种特殊现象死锁。

30.递归?

 

 

 

 

 

 

 

 

   递归虽然有简洁的优点,但它同时也有显著的缺点。递归由于是函数调用自身,而函数调用是有时间和空间的消耗的;每一次函数调用,都需要在内存栈中分配空间以保存参数,返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。

另外,递归中有可能很多计算都是重复的,从而对性能带来很大的负面影响。递归的本质是把一个问题分解成两个或者多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。

除了效率之外,递归还有可能引起更严重的问题:调用栈溢出,每一次函数调用在内存栈中分配空间,每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致调用栈溢出。

 

31.简述HTTP协议。HTTP状态码404 200 304 500都是什么含义?

 

 

 

 

 

 

 

 

 

http协议:超文本传输协议http规定了浏览器与服务器之间的请求和响应的格式与规则,它是万维网上能够可靠地交换文件的重要基础。

1.http协议是应用层的协议

2.中文是超文本传输协议

3.http是一个客户端和服务端请求和应答的标准,客户端是终端用户,服务器是网站。

4.http是客户端浏览器或其他程序与web服务器之间的应用层通信协议。

http的操作过程:

1. 浏览器分析指向页面的URL

2. 浏览器向DNS系统请求解析域名所对应的服务器IP地址

3. DNS系统解析出服务器的IP,并返回给主机

4. 浏览器与该服务器的进程建立TCP连接(三次握手,默认端口为80)

5. 浏览器发出HTTP请求:GET/article/index.html

6. 服务器收到请求并作出相应处理,把文件index.html发送给浏览器

7. 释放TCP连接(四次握手)

8. 浏览器解析index.html文件,将web页显示出来

 

404 200 304 500

状态码:404  可连接服务器,但是服务器无法取得所请求的网页,请求资源不存在

        200  表明该请求被成功的完成,所请求的资源发送到客户端。

        304 自从上次请求后,请求的网页未被修改过

        500  服务器错误  

32.Tcpudp的区别:

 

1. tcp面向连接(如打电话就需要先拨号建立连接);udp是无连接,即发送数据前不需要建立连接

2. tcp提供可靠的服务,也就是说,通过tcp连接传送的数据,无差错,不丢失,不重复,且按顺序到达;udp尽最大的努力交付,即不保证可靠交互

3. Tcp面向字节流,实际上是tcp把数据看成一连串无结构的字节流;udp是面向报文的,udp没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如ip电话,实时视屏会议等)

4. 每一条tcp连接只能是点到点的,udp支持一对一,一对多,多对一和多对多的交互通信

5. tcp首部开销20字节,udp的首部开销小,只有8个字节

6. Tcp的逻辑通信信道是全双工的可靠信道,udp则是不可靠信道

33. 处理错误的方法有几种?各有什么特点

  三种方法:

  方法一:是函数用返回值来告知调用者是否出错。这种方式最大的不便是,因为函数不能直接把计算结果通过返回值赋给其他变量,同时也不能把这个函数计算的结果直接作为参数传递给其他函数。

  方法二:当发生错误时设置一个全局变量,此时我们可以在返回值中传递计算结果了,使用方便,也有不好之处就是,调用者很容易就会忘记去检查全局变量,因此在调用出错的时候忘记做相应的错误处理,从而留下安全隐患。

  方法三:就是异常处理,当函数出错的时候,抛出一个异常。

 

34.问:函数重载和覆盖的区别?

答:①定义放面: 
函数重载:在相同作用域下(如果有关键字const,那么他必须用来修饰指针或者引用),函数名相同,参数列表不同,不能仅依靠返回值。 
覆盖:就是派生类和基类中函数名相同,并且参数列表页相同,返回值也相同。基类的同名函数必须是虚函数(被virtual修饰),派生类覆盖基类的同名函数。 
②使用方面:函数重载多用于运算符的重载,而覆盖多用于继承。 
③函数的确认:函数重载是在编译阶段确定需要调用那个函数,而覆盖是在运行的时候,才确定下来调用的是那个函数。 
④函数是否相同:函数重载是生成的符号不同,而覆盖相同的函数。 
相同方面:函数名都是必须相同的。

35.问:模板为什么不能分离编译?

答:该问题需要知道一点编译、链接的过程,这个我有一篇文章是关于编译、链接的。编译、链接小知识
我们都是c++中函数的模板很强大,一段代码,完成的类型很多的。 
C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来。如果模板函数和调用分别在不同.cpp文件里面,写函数的那个文件,在边界阶段由于没有接到什么指示说有人要用,所以就没有将它生成机器代码,可是那个调用的文件中,他要用呀!编译的时候,他寄希望于编译器,等到各个文件都被编译好,开始链接时,就出现了懒做现象,编译器只能报错了! 
在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

36.问:虚函数内存如何分配?

答:1.单继承无虚函数覆盖:基类和派生类生成一张虚函数表,父类的虚函数按照顺序,依次填入虚函数表中,接着就是子类的虚函数,依次按照声明顺序填入虚函数表中。
2.单继承有虚函数覆盖:基类和派生类还是会生成一张虚函数表,显示基类按照声明,将虚函数填入虚函数表中。派生类看自己的虚函数有和基类构成覆盖的,就可以直接将该函数地址填到基类和该函数同名的位置。
3.多继承无虚函数覆盖:按照基类的声明顺序,依次将基类的虚函数地址填入虚函数表里面,再接着是派生类的虚函数地址,依次填入虚函数表中。
4.多继承有虚函数覆盖:按照基类的声明顺序,依次将基类的虚函数地址填入虚函数表里面,再接着是派生类的虚函数地址,如果该派生类中的函数在多个基类中都有一个覆盖函数,将所有基类中此函数位置的地址都写成派生类该函数的地址。
如果文字表述感觉不是很直观,我这里还有一篇博客是专门画图说明的。[有图的直观说明](http://blog.csdn.net/qq_35256722/article/details/74594394)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

37.问:如何防止构造函数的隐士提升?

答:关键字explicit加在构造函数前面。

38.问:malloc底层是怎么实现

答:有个do_brk()是分配前128k。有一个内存库,free之后没有归还操作系统而是放在内存库里面。 
do_mmap()是分配后128k的内存,free直接交换内存的。 
malloc分配的物理内存不连续,虚拟地址是连续。 
注: 这个不详细,准备将详细的再写一篇文章。

39.问:malloc内存申请有内存碎片,如何防止内存碎片?

答:内存碎片式由于要申请的连续空间比内存池中的空间大,就形成了内存碎片。
  • 1
  • 2

40.问:const修饰的成员函数内,是否能修改成员变量的值?

答:可以,const 修饰的是这个this指针,指针指向不能改,但是指针的内容可以改。结果和 int *const p 相同。

41.问:如果new[1] 那么可不可以直接delete,只有一个元素么?

答:new在申请1个字节大小的空间时,内存为了好管理,给他带了一个4个字节头和4个字节的尾巴。那么delete只是释放一个元素,那个头和尾巴没有被释放,造成了内存泄漏了。
  • 1
  • 2

42.问:空的vector求sizeof是多少?

答:32位下是12,64位下是24;说一下32位的吧! 
这个得先说一下,vector发底层有3个指针:begin vector 、end vector 、vector capactiy。所以sizeof就是这三个指针的大小。



 

 

 

 





 
 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值