博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++基础知识校招面试题,附答案
阅读量:541 次
发布时间:2019-03-05

本文共 16490 字,大约阅读时间需要 54 分钟。

目录

       

       

       

1、C++为什么要提出引用?(引用和指针的区别?)

       引用变量在功能上等于一个常量指针(到底是常量指针还是指针常量?有的书上写的常量指针,有的博客里写的指针常量,那就不纠结这个问题吧。总之这里指的是顶层const),即一旦指向某一个单元就不能在指向别处。

(1)我们在用指针的使用经常犯的错误是什么?
       操作野指针;
       不知不觉改变了指针的值,而后还以为该指针正常。
       如果我们要正确的使用指针,我们不得不人为地保证这两个条件。而引用的提出就是解决这个问题。
       引用区别于指针的特性是:
        必须初始化(保证不是野指针);
        一个引用永远指向他初始化的那个对象(保证指针值不变)。
所以引用的提出就是:让人为地保证这两个条件变成让编译器保证。这样可以减少错误的产生。

2程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。

3用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。

       

       

       

2、使用const而不是#define来定义常量?

  • const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而后者仅仅进行字符替换,没有类型安全检查,并且在字符替换过程中会产生一些意料不到的错误。
  • const方法可以很方便地用于复合类型。
    如const int a[3]={1,2,3};
  • const标识符遵循变量的作用域原则,可以创建作用域为全局、名称空间、数据块的常量。

       

       

       

3、C++左值和右值的区别?

  • 左值:lvalue(locator value)代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
  • 右值:rvalue通过排他性来定义,每个表达式不是lvalue就是rvalue。因此从上面的lvalue的定义,rvalue是在不在内存中占有确定位置的表达式。

       

       

       

4、全局变量?

特点:

  • ① 作用域:全局可见。
           全局变量(外部变量)是在函数外部定义的,它的作用域为从变量的定义处开始,到本程序文件的末尾。
           注:通常把超出一个函数的作用域称为全局作用域,其他几种(如块作用域)不超出一个函数的作用域称为局部作用域。
  • ② 存储空间:静态存储区
           系统会在执行时将全局变量分配在静态存储区,在程序执行期间,对应的存储空间不会释放,一直到程序结束才会释放。
           注:一个程序在内存中占用的存储空间可以分为3个部分:程序区(存放可执行程序的代码)、静态存储区(存放静态变量)、动态存储区(存放动态变量)。
  • ③ 优先度:全局变量优先度低于局部变量
    当全局变量和局部变量重名时,会屏蔽全局变量,局部优先。
           

优点使用全局变量程序运行时速度会快一点,因为内存不需要再分配。

缺点使用全局变量会占用更多的内存,因为其生命期长。

       

全局变量作用域的扩展和限制

  • ① 扩展:使用extern关键字可以对全局变量的作用域进行扩展。

            前面提到,全局变量的作用域为从变量的定义处开始,到本程序文件的末尾。若想在本文件全局变量定义之前引用该全局变量,可以在引用之前用extern关键字对该变量进行说明,有了此说明,就可以从说明之处起,合法地引用该变量。
            若想在一个文件(设为a.cpp)中引用另一个文件(设为b.cpp)中已定义的全局变量,可以在a.cpp中extern关键字对该全局变量进行说明,在编译和连接时,系统就会知道该全局变量已经在其他文件(b.cpp)中定义过了。
           注:在编译时遇到extern,系统会现在本文件中查找全局变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时在其他文件中查找全局变量的定义,如果找到,就将作用域扩展到本文件;如果还找不到,按出错处理。

  • ② 限制:使用static关键字可以限制全局变量的作用域。(又称之为隐藏)

           全局变量默认是有外部链接性的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern对全局变量进行声明,就可以使用全局变量。
           如果希望全局变量仅限本文件引用,而不能被其他文件引用,可以在定义全局变量时在前面加一个static关键字。
           

全局变量和全局静态变量的区别:

       首先,并不是说全局变量在定义时加了static关键字才是静态存储,不加static就是动态存储,不是的。不管加不加static,全局变量都是存储在静态存储区的,都是在编译时分配存储空间的,两者只是作用域不同,全局变量默认具有外部链接性,作用域是整个工程,全局静态变量的作用域仅限本文件,不能在其他文件中引用。

       

       

       

5、static关键字?

特点:用来控制存储方式和可见性

  • ① 存储空间:静态存储区(控制变量的存储方式)
           静态变量存储在静态存储区(存储在静态存储区的变量,如果不显式地对其进行初始化,系统会将其初始化为0),在程序执行期间,对应的存储空间不会释放,一直到程序结束才会释放。
           static控制变量的存储方式体现在局部变量上。局部变量存储在动态存储区,在局部变量定义前加上static,该局部变量就变成了局部静态变量,局部静态变量存储在静态存储区,即使函数调用结束,它占用的存储空间也不会释放,但其他函数不能引用该局部静态变量。当下一次调用该函数时,不会再次对该局部静态变量进行初始化,它的值是上一次函数调用结束时的值。
           对全局变量而言,存储方式没有什么改变,因为全局变量和全局静态变量都存储在静态存储区。
  • ② 作用域:(控制变量、函数的可见性)
           static控制变量的可见性体现在全局静态变量和静态函数上。
           全局变量默认具有外部链接性,作用域是整个工程。使用static关键字可以限制全局变量的作用域,全局静态变量的作用域仅限本文件,它对在其他文件不可见,也就是说不能在其他文件中引用该全局静态变量,但其他文件中可以定义和它名字相同的变量,不会发生冲突。
           在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数的不同在于,它只能在声明它的文件当中可见,不能被其它文件使用,其它文件中可以定义相同名字的函数,不会发生冲突。
           局部静态变量的作用域与局部变量的作用域相同,其作用域都是从定义开始到函数或程序块结束为止。

       

类中的static关键字:

       在类中声明static变量或者函数时,初始化时使用作用域运算符(::)来标明它所属类,静态成员是类的成员(所有对象中共享的成员),而不是某一个对象的成员。

  • ① 静态数据成员

           在类内数据成员的声明前加上关键字static,该数据成员就是类内的静态数据成员。
           静态数据成员和普通数据成员一样遵从public,protected,private访问规则。
    对于非静态数据成员,每个对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只会被分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
           因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操作它。
           同全局变量相比,使用静态数据成员有两个优势:
           静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
           可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;

  • ② 静态成员函数

           与静态数据成员一样,我们也可以创建一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务。静态成员函数与静态数据成员一样,都是类的内部实现,属于类定义的一部分。普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的,如函数fun()实际上是this->fun()。但是与普通函数相比,静态成员函数由于不与任何的对象相联系,因此它不具有this指针。
           非静态成员函数可以任意地访问静态成员函数和静态数据成员;
           静态成员函数不能访问非静态成员函数和非静态数据成员;静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
           

什么时候用static?

       需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

为什么要引入static?

       函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题:如果想将函数中此变量的值保存至下一次调用时,如何实现?最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

       

       

       

6、C++和C的区别?

设计思想上:

C++是面向对象的语言,而C是面向过程的结构化编程语言,C++在C的基础上增加了类。

语法上:

C++具有封装、继承和多态三种特性。
C++相比C,增加了许多类型安全的功能,比如强制类型转换。
C++支持范式编程,比如模板类、函数模板等。
在C++中,引用是一个经常使用的概念。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
在C++语言中,仍然支持malloc()和free()来分配和释放内存,同时增加了new和delete来管理内存。
C++支持函数重载,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。
       

       

       

7、智能指针(smart pointer)?

       智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

       C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr,其中后三个是c++11支持,并且第一个已经被11弃用。
       
为什么要使用智能指针?
       动态内存的使用中很容易出现问题。比如,申请的空间忘记释放,造成内存泄漏;或者在后面还会使用到该指针的情况下释放了内存,在这种情况下就引用了非法内存的指针。
       用智能指针可以很大程度上的避免这些问题,因为智能指针就是一个类(而且还是像vector这样的模板类,当我们创建一个智能指针时,还必须提供额外的信息——指针可以指向的类型),当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
       
shared_ptr:
       shared_ptr允许多个指针指向同一个对象。当指向某一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过析构函数来完成销毁工作的。
       
weak_ptr:
       weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator* 和 operator-> ,因此取名为 weak,表明其是功能较弱的智能指针。它的最大作用在于协助 shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。

环形引用的例子:

class B ;class A{
public: shared_ptr pb; A() {
}; ~A() {
cout << "delete A" << endl; };};class B{
public: shared_ptr pa; B() {
}; ~B() {
cout << "delete B" << endl; };};void fun() {
shared_ptr
p1(new A()); shared_ptr p2(new B()); cout << p1.use_count() << endl; // 1 cout << p2.use_count() << endl; // 1 p1->pb=p2; p2->pa=p1; cout << p1.use_count() << endl; // 2 cout << p2.use_count() << endl; // 2}

       以上代码存在内存泄漏,将class B中的shared_ptr改为weak_ptr就可以避免内存泄漏了。

       
unique_ptr:
       unique_ptr“独占”所指对象。两个unique_ptr不能指向一个对象,即unique_ptr不共享它所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库(STL)算法。只能移动unique_ptr,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个unique_ptr,并且原始unique_ptr不再拥有此资源。
       

摒弃auto_ptr的原因?

       一句话总结就是:避免因潜在的内存问题导致程序崩溃。unique_ptr比auto_ptr更加安全,因为auto_ptr有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr则禁止了拷贝语义,但提供了移动语义,即可以使用 std::move()进行控制权限的转移。

void fun() {
auto_ptr
p(new string("abc")); auto_ptr
p1(p); // 拷贝语义 cout << *p << endl; // 非法访问 unique_ptr
p2(new string("defg")); unique_ptr
p3=move(p2); // 禁止拷贝语义,只能通过move转移权限 cout << *p3 << endl;}

       

       

       

8、堆和栈?

数据结构中的堆和栈?

(1)栈就像装数据的桶或箱子
我们先从大家比较熟悉的栈说起吧,它是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。
(2)堆像一棵倒过来的树(好像大根堆小根堆就是这种)
而堆就不同了,堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
       

内存分配中的栈和堆?

       函数的调用过程由栈来实现,函数返回地址、实参和局部变量都采用栈的方式存放。堆区用于分配程序员申请的内存空间。
(1) 栈
       栈由操作系统自动分配释放,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
       其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到低(向上生长),所以后定义的变量地址低于先定义的变量。栈中存储的数据的生命周期随着函数的执行完成而结束。
(2) 堆
       堆由程序员分配释放,若程序员不释放,程序结束时由OS回收,分配方式倒是类似于链表。
       堆的内存地址生长方向与栈相反,由低到高(向下生长),但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。
       

堆(heap)和栈(stack)的区别?

(1) 申请方式和回收方式不同
栈是系统自动分配空间的,例如我们定义一个 char a;系统会自动在栈上为其开辟空间。而堆则是程序员根据需要自己申请的空间,例如malloc(10);开辟十个字节的空间。由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
(2) 申请后系统的响应
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:堆上申请空间的效率比栈要低得多。
首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

       

       

       

9、程序的内存模型分为那几个区域?

(1)代码区(.text段):存放代码。

(2)静态存储区(.data段+.bss段):存放全局变量、静态变量、常量。其中初始化过的放在.data段,未初始化过的放在.bss段。
(3)堆区:由程序员根据需要自己申请空间(使用new或malloc),需要自己回收这些空间(使用delete或free),如果没有回收,程序结束时操作系统会回收这些空间。
(4)共享库的内存映射区域:动态链接库是在程序运行时才被链接的,这一块空间是为动态链接库准备的。
(5)栈区:存放函数的参数、返回值、局部变量等。
(6)内核虚拟内存空间:这段地址空间只能在内核态时才能访问,一般情况下进程运行在用户态,当发生中断时(比如线程切换、程序异常),就会进入内核态,处理完中断后,再回到用户态。
在这里插入图片描述

       

       

       

10、malloc/free和new/delete的区别?

在这里插入图片描述

       
       

11、那你知道如果用free去清理new出来的内存会产生什么问题吗?

       new实际过程中做了两步操作,第一步是分配内存空间,第二步是调用类的构造函数;delete也同样是两步,第一步是调用类的析构函数,第二步才是释放内存;而malloc()和free()仅仅是分配内存与释放内存操作。

       如果通过new分配的内存,再用free去释放,就会少一步调用析构函数的过程。简单数据类型(例如int [ ]),使用new分配内存后,可以使用free来释放上述释放的内存,效果与delete相同。而复杂数据类型(例如一个类指针),则不能用free来进行内存的释放,因为free与delete不同,不会执行类的析构函数。

参考:

       

       

       

12、vector实现?

       vector内部使用动态数组的方式实现的。

       初始化时,vector会申请比当前要容纳元素所需的内存更大一块的内存,预留出空闲空间,用于之后存放新添加的元素,这样就不需要每添加一个新元素就重新分配整个容器的内存空间。当不断向数组内添加元素时,如果数组的内存不够了,就要重新申请一块内存,一般是当前大小的两倍,然后把原数组的内容拷贝过去。
       vector和普通数组一样都支持随机访问,但当在数组中头部或者中间添加或删除元素时,都要移动数组元素,如果频繁地进行这样的操作,会导致vector性能降低。
       vector的析构函数会先销毁所有已存在的元素,然后释放所有内存。
       vector的一个特点:内存空间只会增长,不会减小。

参考:

       

       

       

13、迭代器失效了解吗?

       向容器中添加或删除元素都有可能会使容器的迭代器失效。

       

  • 向容器添加元素后:
    (1) vector和string:若存储空间重新分配,则迭代器全部失效。若存储空间没有重新分配,则指向插入位置之前的元素的迭代器仍有效,之后的元素的迭代器将会失效。
    (2) deque:插入元素到除了首尾位置之外的任何位置,都会使迭代器、指针、引用失效。插入元素到首尾,迭代器会失效,指向原来元素的指针和引用仍有效。
    (3) list和forward_list:迭代器、指针、引用仍然有效。
           
  • 在容器中删除一个元素后:
    (1) vector和string:指向被删除元素之前的迭代器、引用、指针仍有效。当我们删除元素时,尾后迭代器总是会失效。
    (2) deque:在首尾位置之外的任何位置删除元素,迭代器、指针、引用都会失效。如果删除的是尾元素,那么尾后迭代器失效,其他迭代器、引用、指针仍有效。如果删除的是首元素,迭代器、指针、引用仍有效。
    (3) list和forward_list:指向除删除元素外的其他元素的迭代器、指针、引用仍然有效。

       

       

       

14、std::sort原理?

对要排序的元素数目有一个阈值,如果大于该阈值则是用快速排序,如果小于阈值则用插入排序。

       

       

       

15、深拷贝和浅拷贝?

(1) 浅拷贝

       浅拷贝只是增加了一个指针指向已存在的内存地址,实际内存中并没有重新开一块内存复制原来地址内的内容。比如变量的引用就是浅拷贝,这个变量只是多了一个别名,内存中并没有额外用一块内存重新保存这个变量,我改变引用的值,原来变量的值也会跟着改变。
(2)深拷贝
       深拷贝是申请了一个新的内存保存复制的内容,而且增加了一个指针并且使这个指针指向这块新的内存,相当于有两块内存保存了一样的内容。我改变这块新内存中的内容,原来那块内存中的内容不会改变。

       

       

       

16、为什么list不能使标准库算法sort()?

       list的内存空间不是连续的,标准库算法中的操作需要随机访问迭代器。因此forward_list也不可以。

       

       

       

17、解决哈希冲突的方法有哪些?

(1)开放地址法

       当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
(2)再哈希法
       当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。
(3)建立一个公共溢出区
除了原本的存储空间外,另外开辟一个存储空间用以存储发生冲突的记录。

       

       

       

18、容器适配器?

       C++提供了三种容器适配器(container adapter):栈stack、队列queue、优先队列priority_queue。stack和queue基于deque实现,priority_queue基于vector实现。

       假设我需要一个栈结构,可以用deque来模拟,只在一端进行元素插入和弹出,另一端不动,但我不能防止别人在deque的另一端进行操作,因此deque并不能严格地满足我的要求。我对它进行封装,作一些限制,使它留出的接口只能在一端进行插入和删除,这样就实现了stack。实际上stack也是使用的deque,只是对deque进行了封装改变了对外的接口而已。因此,stack、queue、priority_queue这样的类一般称为容器适配器,它们只是基本容器类型(vector,dequeue,list)的适配。

       

       

       

19、STL(标准模板库或泛型库,内容太多,这里只是简述)?

       STL包含有大量的模板类和模板函数,是一个基础模板的集合。STL的六大组成部分:容器(Containers)、算法(Algorithms)、迭代器(Iterators)、适配器(container adaptors)、仿函数(functor)、空间配置器(allocators)。

(1) 容器:
① 顺序容器:vector、deque、list、forward_list、array、string。
② 关联容器:set、map、multiset、multimap、unordered_set、unordered_map、unordered_multimap、unordered_multiset。
(2) 算法: 各种常用的算法如:sort、search、copy、erase……
(3) 迭代器: 是所谓的“泛型指针”。迭代器是一种“smart pointer”,是行为类似指针的对象,迭代器最重要的工作就是对*(解引用操作符)、->(成员指针运算符?)进行重载。大多数的时候,把它理解为指针是没有问题的(指针是迭代器的一个特例,它也属于迭代器),其实迭代器底层实现比指针复杂得多,内容过多,可以去看STL源码。
(4) 仿函数: 行为类似函数,可作为算法的某种策略。
(5) 适配器: 一种用来修饰容器或仿函数或迭代器接口的东西,修饰容器的叫容器适配器,修饰仿函数的叫仿函数适配器,修饰迭代器的叫迭代器适配器。
(6) 配置器: 负责空间配置与管理。

       

       

       

20、C++哪些容器不支持迭代器?

在这里插入图片描述

       

       

       

21、函数名后加const?

class MyClass {
public: int GetData(int Id,int Type,char* pData) const;}

       首先要注意只有类的成员函数才能在函数名后面加上const,这时成员函数叫做常量成员函数。常量成员函数在执行期间不能修改成员变量的值(静态成员变量除外),也不能调用同类的非常量成员函数(同样的静态成员函数除外)。

       该函数不能修改对象内的任何成员,只能发生读操作,不能发生写操作。任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误。

       

       

       

22、静态成员函数为什么不能访问本类中的非静态成员?

       静态成员函数是属于整个类的,而不是属于某一个对象的。非静态成员函数有this指针,this指针指向该对象本身,非静态成员函数是属于某一个具体的对象的。而静态成员函数并不属于某一对象,没有this指针。假如真的用静态成员函数去访问非静态成员函数,由于非静态函数是属于某一个对象的,没有this指针的静态函数根本不知道应该调用哪个对象的非静态成员函数。因此静态成员函数不能访问本类中的非静态成员。

       

       

       

23、为什么静态成员函数不能声明为const?

       将成员函数声明为const就是表示该函数不能修改该对象内的任何成员,只能发生读操作,不能发生写操作。但是static成员不属于对象,所以不能将static函数声明为const。

       

       

       

24、内存对齐?

在这里插入图片描述

一个类占多少字节?
(1) 空类:1字节。(为什么?)
(2) 只含有函数(也含构造和虚构函数):1字节,因为函数不占用类空间。
(3) 有虚函数:无论有多少个虚函数,都占4个字节(一个指向虚函数表的指针,64位系统指针是8个字节)。
(4) 只有数据成员:
例如:class A{
int a; char b; int c;
}
占12个字节,因为要考虑到内存对齐问题,这里以4字节对齐。
(5) 类中的静态成员变量不占类的内存,静态成员变量存储在静态存储区,并且静态成员变量的初始化必须在类外初始化,且只能初始化一次。
(6) 类与类之间也要内存对齐。比如一个类占18个字节,那个编译器会为它补齐2个字节,占0~19的内存,下一个类从偏移量为20的地址开始存放。

为什么要内存对齐?

       比如以4字节对齐为例,一个int类型4字节的数据如果放在内存地址2开始的位置,那么这个数据占用的内存地址为2、3、4、5,那么这个数据就被分为了2个部分,一个部分在地址0~3中,另外一部分在地址4~7中,由于32位CPU一次读取4个字节,所以,CPU会分2次进行读取,先读取地址0~3中内容,再读取地址4~7中数据,最后CPU提取并拼接出正确的int类型数据。那么如果我们把这个int类型4字节的数据放在从地址0开始的位置,CPU只要进行一次读取就可以得到这个int类型数据了。
       因此内存对齐可以让内存存取更有效率。
       

       

       

25、大端存储、小端存储?

大端存储:

数据的尾端字节存储在内存的高地址。即头端字节存储在内存的低地址,数据依序增加存储。
小端存储:
数据的尾端字节存储在内存的低地址。即头端字节存储在内存的高地址,数据依序递减存储。
比如,一个int型数据用十六进制表示0x 00 01 02 03,一个16进制数字=2^4,占4位,2个十六进制数字占8位即1个字节,内存中是一个字节一个字节存放的。
在这里插入图片描述
扩展:如何判断自己的计算机是大端还是小端?
       

       

       

26、虚析构函数?

例:class A{}; // 为了简略,构造函数和析构函数都没写,自己脑补吧

class B:public A{};
class C:public B{};
A* p= new C();
delete p;

(1) 如果析构函数~A()没有加virtual:

A()—B()—C()–~A();
在通过基类指针p删除派生类对象时,只会调用基类的虚构函数,不会调用派生类的析构函数,造成内存泄漏。
(2) 如果析构函数~A()加了virtual:virtual ~A(){};
A()—B()—C()–C()–B()–~A();
析构函数前添加virtual,必须要在最最根上的基类的析构函数前添加,只要根类A的析构函数设置为虚函数,A的所有子类的析构函数都为虚函数,他们前面加不加virtual关键字都可以。

       

       

       

27、构造函数可以是虚函数吗?不可以

       构造函数是特殊的,是没有虚函数的概念的。构造函数是不继承的,创建子类对象时,将调用子类的构造函数,子类的构造函数将自动调用父类的构造函数。

       

       

       

       

       

       

以下几个问题问法不一样,其实问的都是多态的东西

请参考,看完肯定能懂:

       
       
       

28、静态联编和动态联编?(重载overloaded和重写、覆盖的区别?)

  • 对象的静态类型:对象在声明时采用的类型(编译器确定)。

  • 对象的动态类型:当前所指的对象类型,动态类型是动态绑定的,可以改变。

  • 联编(binding)

    就是明确调用关系,指明调用函数的语句调用的究竟是哪一个函数。

  • 静态联编(静态绑定、早绑定)

    静态联编是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定。绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。(为了保证程序运行时的效率,凡是能在编译期做的事情就不会在运行期干)。

  • 动态联编(动态绑定、晚绑定)

    动态联编是指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定。绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

       注意:绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的bug。

       

       

29、C++静态多态和动态多态?

       多态就是多种形态,C++的多态分为静态多态与动态多态。

       静态多态就是编译器根据函数实参的类型判断出要调用哪个函数。比如函数重载和函数模板。
       动态多态依靠的是虚函数表和动态绑定机制,因为是在运行时根据对象的类型在虚函数表中寻找调用函数的地址来调用相应的函数,所以称为动态多态。

多态具体解答见后。

       

       

30、C++多态,关于虚函数、虚函数表、虚指针、重写与覆盖?

       在一个类的某个函数前加上virtual关键字,这个函数就变成了虚函数,当这个类中存在虚函数时,编译器会给这个类创建一个虚函数表,虚函数表里存放了这个类中所有虚函数的地址。

       一个类只会有一个虚函数表,类的所有对象是共享这个虚函数表的,每个对象的内存中都会有一个指向这个虚函数表的指针。这也是为什么不管类中有多少个虚函数,对象的内存中都只会多4个字节的原因,因为对象中只是多了一个指向这个类的虚函数表的指针。
       如果基类中有虚函数,基类就会有一个虚函数表,派生类继承于这个基类,不管派生类中有没有虚函数,派生类中都会自动创建一个虚函数表,虚函数表中存放基类所有虚函数的地址,也就是派生类会继承基类的虚函数。如果我们在派生类中重写和基类的虚函数,那么在派生类的这个虚函数表里,重写的派生类虚函数就会覆盖基类的同名虚函数。
       举个例子,比如基类是A,派生类有B、C、D,这些类中都有虚函数f的实现,如果我们想调用某个派生类的虚函数f,可以用一个基类指针指向这个派生类对象,程序在运行时,会根据基类指针指向的对象类型,去查这个类的虚函数表,找到虚函数的地址。
       如果基类指针指向的是类B,就会根据类B的虚指针找到类B的虚函数表,在虚函数表中查找虚函数f,因为在类B中重写了虚函数f,因此在类B的虚函数表中基类A的虚函数f的地址已经被类B的虚函数f的地址覆盖掉了,因此找到的这个虚函数f地址是派生类B的虚函数地址,然后就可以去调用它了。如果基类指针指向的是类C,就会去查类C的虚函数表,指向的类D,就会去查类D的虚函数表。
       通过基类指针指向的对象类型去调用不同类的同名虚函数,就实现了多态。

       请参考,看完肯定能懂:

在这里插入图片描述

       

       

       

31、C++多继承?

       派生类只有一个基类,称为单继承。除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类。

       多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的Java、C#等干脆取消了多继承。
       多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

class D: public A, private B, protected C{
//类D新增加的成员}

       D是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

       多重继承的优点很明显,就是对象可以调用多个基类中的接口,多重继承的缺点是什么呢?如果派生类所继承的多个基类有相同的基类(菱形继承),而派生类对象需要调用这个最原始的祖先类的接口方法,就会容易出现二义性(编译不通过)。
       

       

       

32、菱形继承与虚继承?

菱形继承

(1)概念:A作为基类,B和C都继承与A。最后一个类D又继承于B和C,这样形式的继承称为菱形继承。
(2)菱形继承的缺点
数据冗余:在D中会保存两份A的内容
访问不明确(二义性):因为D不知道是以B为中介去访问A还是以C为中介去访问A,因此在访问某些成员的时候会发生二义性
(3)缺点的解决:
数据冗余:通过下面“虚继承”技术来解决(见下)
访问不明确(二义性):通过作用域访问符::来明确调用。虚继承也可以解决这个问题。
在这里插入图片描述

参考:

       

       

       

33、让C++程序变得更高效更安全的一些技巧?

(1)用const代替#define来定义常量,更安全

(2)用inline代替#define来定义短小的函数,更安全

(3)为类的数据成员赋初值,用成员初始化列表,而不是在构造函数内赋值。更高效。例:

用成员初始化列表:

MyClass(int _a,int _b):a(_a),b(_b){
} // 初始化数据成员a、b

在构造函数内赋值:

MyClass(int _a,int _b){
a=_a;b=_b;}

以上两种方法实现功能一样,但前者更高效,是在进入构造函数本体之前进行a、b初始化,后者是已经初始化后,再在构造函数里立刻对a、b赋新值。前者更高效。

       

       

       

34、一个程序从源代码到可执行程序的过程?

预处理、编译、汇编、链接(指静态链接)。

  • 一、 预处理
           C语言的宏替换和文件包含的工作,不归入编译器的范围,而是交给独立的预处理器。主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:
    1、删除所有的#define,展开所有的宏定义。
    2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”。
    3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
    4、删除所有的注释,“//”和“/**/”。
    5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
    6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
           
  • 二、编译
    把预处理之后生成的xxx.i(C)或xxx.ii(C++)文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
           
  • 三、 汇编
    将汇编代码转变成机器可以执行的指令(机器码文件,二进制)。汇编过程是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程由汇编器as完成。
    经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。但是,经过预编译、编译、汇编之后,生成机器可以执行的目标文件之后,还有一个问题——变量a和数组arr的地址还没有确定。这就需要链接器来搞定啦。
           
  • 四、链接(这里讲的链接,严格说应该叫静态链接。)
    链接操作最重要的步骤就是将函数库中相应的代码组合到目标文件中。链接就是将不同部分的代码和数据收集和组合成一个单一文件的过程,也就是把不同目标文件合并成最终可执行文件的过程。当然,务必知道:这个过程不涉及内存。

参考:

       

       

35、链接?

链接可以分为三种情形:

1,编译时(compile time)链接,也就是我们常说的静态链接,是在源代码被翻译成机器代码时完成的;
2,装入时(load time,加载时)链接,也就是程序被加载器加载到内存时;
3,运行时(run time)链接,应用程序执行时。

       

       

36、类中的 public, protect, private?

  • (1)public 修饰的变量和函数在类的内部和外部都可以访问;

  • (2)private 修饰的变量和函数只有在类的内部可以访问;

  • (3)protected 修饰的变量和函数在类的内部可以访问,还可以在派生类中访问。如果类没有派生出其他类,那么 protected 和 private 是完全相同的,protected 和 private 一样只能在类的内部访问,不能在类的外部访问。

  • public protect private继承

           继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单的说,继承是指一个对象直接使用另一对象的属性和方法。C++中的继承关系就好比现实生活中的父子关系,继承一笔财产比白手起家要容易得多。继承的方式有三种分别为公有继承(public),保护继承(protect),私有继承(private)。

在这里插入图片描述

       

       

37、struct和class区别?

C++中保留了C语言的 struct 关键字,并且加以扩充。在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。

C++中的struct和class基本是通用的,唯有几个细节不同:
(1)使用class时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
(2)class 继承默认是private继承,而struct继承默认是public继承。
(3)class 可以使用模板,而struct不能。

       

       

38、用类的空指针调用成员函数?

解释起来篇幅很长,参考:

转载地址:http://szvg.baihongyu.com/

你可能感兴趣的文章