C++ 基础

1. 引用和指针有什么区别?

一般指的是某块内存的地址,通过这个地址,我们可以寻址到这块内存;而引用是一个变量的别名。指针可以为空,引用不能为空。

2. #define, extern, static和const有什么区别?

#define主要是用于定义宏,编译器编译时做相关的字符替换工作,主要用来增加代码可读性。

const定义的数据在程序开始前就在全局变量区分配了空间,生命周期内其值不可修改。

static修饰局部变量时,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束,static修饰全局变量,全局变量在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问。

extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

3. 静态链接和动态链接有什么区别?

静态链接,无论缺失的地址位于其它目标文件还是链接库,链接库都会逐个找到各目标文件中缺失的地址。采用此链接方式生成的可执行文件,可以独立载入内存运行;
动态链接,链接器先从所有目标文件中找到部分缺失的地址,然后将所有目标文件组织成一个可执行文件。如此生成的可执行文件,仍缺失部分函数和变量的地址,待文件执行时,需连同所有的链接库文件一起载入内存,再由链接器完成剩余的地址修复工作,才能正常执行。

4. 变量的声明和定义有什么区别

变量的定义:用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。
变量的声明:用于向程序表明变量的类型和名字。程序中变量可以声明多次,但只能定义一次。

5. volatile 和 mutable 有什么作用

在C++中,mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。
象const一样,volatile是一个类型修饰符。volatile修饰的数据,编译器不可对其进行执行期寄存于寄存器的优化。这种特性,是为了满足多线程同步、中断、硬件编程等特殊需要。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的直接访问。

6. 全局变量和局部变量有什么区别?操作系统和编译器是怎么知道的?

全局变量是整个程序都可访问的变量,生存期从程序开始到程序结束;局部变量存在于模块中(比如某个函数),只有在模块中才可以访问,生存期从模块开始到模块结束。
全局变量分配在全局数据段,在程序开始运行的时候被加载。局部变量则分配在程序的堆栈中。因此,操作系统和编译器可以通过内存分配的位置来知道来区分全局变量和局部变量。

7. shared_ptr, weak_ptr, unique_ptr 分别是什么?

unique_ptr 实现独占式拥有或严格拥有的智能指针,通过禁用拷贝构造和赋值的方式保证同一时间内只有一个智能指针可以指向该对象;shared_ptr增加了引用计数,每次有新的shared_ptr指向同一个资源时计数会增加,当计数为0时自动释放资源;构造新的weak_ptr指针不会增加shared_ptr的引用计数,是用来解决shared_ptr循环引用的问题。

8. RAII是什么?

RAII技术的核心是获取完资源就马上交给资源管理。标准库中的智能指针和锁便是比较常用的RAII工具。RAII类需要慎重考虑资源拷贝的合理性。

9. 右值引用有什么作用?

普通引用为左值引用,无法指向右值,但是 const 左值引用可以指向右值;右值引用指向的是右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过 std::move 指向该左值。右值引用和 std::move 被广泛用于在 STL 和自定义类中实现移动语义,避免拷贝,从而提升程序性能。

10. 函数重载和函数重写

重写(覆盖)的规则: 1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载。
2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public > protected > default > private)。
3、重写的方法的返回值必须和被重写的方法的返回一致。
4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类。
5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。
6、静态方法不能被重写为非静态的方法(会编译出错)。 重载的规则: 1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样)。
2、不能通过访问权限、返回类型、抛出的异常进行重载。
3、方法的异常类型和数目不会对重载造成影响。

11. C++的顶层const和底层const?

顶层 const 表示指针本身是个常量; 底层 const 表示指针所指的对象是一个常量a。

12. 拷贝初始化、直接初始化、列表初始化?

直接初始化实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。
拷贝初始化实际上是要求编译器将右侧运算对象拷贝到正在创建的对象中,通常用拷贝构造函数来完成。
C++11标准中{}的初始化方式是对聚合类型的初始化,是以拷贝的形式来赋值的。

C++面向对象

1. 纯虚函数和虚函数表

如果类中存在虚函数,那么该类的大小就会多4个字节,然而这4个字节就是一个指针的大小,这个指针指向虚函数表,这个指针将被放置与类所有成员之前。对于多重继承的派生类来说,它含有与父类数量相对应的虚函数指针。

2. 为什么基类的构造函数不能定义为虚函数?

从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有必要成为虚函数。

当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。

3. 什么时候需要定义虚析构函数?

一般基类的虚成员函数,子类重载的时候要求是完全一致,也就是除了函数体,都要一毛一样。而析构函数同样也是成员函数,虚析构函数也会进入虚表,唯一不同的是,函数名并不要求一致,而且,你如果不写,编译器也会帮你生成,而且如果基类有virtual,编译器也会默认给子类添加。但是不论如何它依旧遵守多态的规则,也就是说,如果你的析构函数是虚函数,调用虚函数的规则也遵守多态原则,也就是会调用子类的析构函数,这和其他虚函数的机制完全一致,并没有什么不同。而子类析构函数具有析构掉基类的职责,所以不会造成内存泄漏。而基类并不知道自己的子类。

4. 构造函数和析构函数能抛出异常吗?

不能。

5. 多继承存在什么问题?如何消除多继承中的二义性?

在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性。解决二义性的方案:利用作用域运算符::,用于限定派生类使用的是哪个基类的成员;在派生类中定义同名成员,覆盖基类中的相关成员。

6. 如果类A是一个空类,那么sizeof(A)的值为多少?如果不为空大小是多少?

A为空,大小是1;不为空,A的大小是所有非静态成员大小之和。

7. 类型转换分为哪几种?各自有什么样的特点?

自动类型转换

特点: 数据范围从小到大转换,不需要进行代码的特殊处理,编译器自动完成。

强制类型转换

特点: 数据范围从大到小转换,需要进行特殊的格式处理,会损失精度。

类型转换函数

1) static_cast(静态类型转换)
静态类型转换,编译的时c++编译器会做类型检查,基本类型能转换但是不能转换指针类型
2) reinterpreter_cast(重新解释类型转换,interpreter,v.诠释,说明)
若不同类型之间,进行强制类型转换,用reinterpret_cast进行重新解释
3) dynamic_cast(动态类型转换)
C++中重要的,安全的基类和子类之间转换,运行时类型检查
4) const_cast(常量类型准换)
去除变量的只读属性

8. RTTI是什么?其原理是什么?

RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。简单的讲,RTTI是在一个类的虚函数表里面添加了一个新的类型条目。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。

C++通过以下的两个操作提供RTTI:
1) typeid运算符,该运算符返回其表达式或类型名的实际类型。
2) type_info类里面的比较运算符 3) dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。

9. C++的空类有哪些成员函数

默认构造函数、 默认拷贝构造函数、 默认析构函数、 默认赋值运算符,以及取址运算符和 const 取址运算符。

10. 虚函数表属于类还是对象?虚函数表什么内存空间?

虚函数表是class specific的,也就是针对一个类来说的,这里有点像一个类里面的staic成员变量,即它是属于一个类所有对象的,不是属于某一个对象特有的,是一个类所有对象共有的。

gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

C++ STL

1. vector, array, deque 的区别

vector是动态数组,array被封装成容器的C++数组,deque是双向数组,首尾都支持增删。

2. Vector如何释放空间?

想要彻底释放内存,C11引入了shrink_to_fit();,在执行完clear()后执行,可完全释放内存

3. 如何在共享内存上使用STL标准库?

1) 想像一下把STL容器,例如map, vector, list等等,放入共享内存中,IPC一旦有了这些强大的通用数据结构做辅助,无疑进程间通信的能力一下子强大了很多。

我们没必要再为共享内存设计其他额外的数据结构,另外,STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装,默认情况下有它们自己的内存管理方案。

当一个元素被插入到一个STL列表(list)中时,列表容器自动为其分配内存,保存数据。考虑到要将STL容器放到共享内存中,而容器却自己在堆上分配内存。

一个最笨拙的办法是在堆上构造STL容器,然后把容器复制到共享内存,并且确保所有容器的内部分配的内存指向共享内存中的相应区域,这基本是个不可能完成的任务。

2) 假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?

一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。

进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

4. map 、set、multiset、multimap 底层原理及其相关面试题

底层数据结构都是红黑树。

5. vector迭代器失效的情况

当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。

当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。

6. vector 的 reserve() 和 resize() 方法之间有什么区别?

reserve 是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve 只是保证 vector 中的空间大小(capacity)最少达到参数所指定的大小 n 。reserve()只有一个参数。

resize()可以改变有效空间的大小,也有改变默认值的功能。capacity 的大小也会随着改变。resize() 可以有多个参数。

7. unordered_map , unordered_set 底层原理及其相关面试题

底层数据结构都是哈希表,都是通过开链法解决冲突。

8. STL内存优化?

1) 严格遵守”commit or rollback”原则。该原则规定,在批量初始化过程中。要么产生全部的必要元素。要么不产生一个元素,即要么不做,做了就做好做全。

2) 在初始化过程中,会先推断待初始化的元素类型是否为内置类型,若为内置类型POD(Plain Old Data),则直接调用更加底层的函数,上面三个函数相应的底层函数分别为:memmove(b1,b,e-b)、fill(b,e,t)和fill(b,n,x)。若数据类型为其它类型,则循环调用construct(iter,t)函数,这样做的目的是为了提高效率。

9. emplace和push的区别?

push则是先构造元素,再将其插入容器;emplace可以直接传入构造对象需要的元素,然后自己调用其构造函数。

C++内存管理

1. 变量的存储位置?程序的内存分配?

在C++中,内存区分为5个:堆、栈、自由存储区、全局/静态存储区、常量存储区。new是在自由存储区开辟内存。

在C中,内存区分为堆、栈、全局/静态存储区、常量存储区。malloc是在堆上开辟内存。

2. 内存的分配方式有几种?

1) 从全局存储区域分配:这时内存在程序编译阶段就已经分配好,该内存在程序运行的整个周期都有效,如:全局变量、static静态变量。

2) 从栈区分配:在执行函数的时候,函数中的局部变量的存储单元都可以从栈中分配,函数执行结束后这些存储单元都会被自动释放,实现从栈中分配存储单元运算操作内置于处理器的指令集中,效率很高 但是分配的内存容量有限。

3) 从堆中分配:也称为动态内存分配,在程序运行期间,可以使用malloc和new申请任意数量的内存单元,由程序员决定在什么时候使用free和delete释放内存。

4. 堆和栈有什么区别?
  1. 栈由系统自动分配/回收,而堆是人为手动分配/回收;
  2. 栈获得的空间较小,而堆获得的空间较大;
  3. 栈由系统自动分配,速度较快,而堆一般速度比较慢;
  4. 栈是连续的空间,而堆是不连续的空间。
5. 静态内存分配和动态内存分配有什么区别?
  1. 时间不同:

静态分配发生在程序的编译和链接的时候。

动态分配发生在程序调入和执行的时候。

  1. 空间不同:

静态分配只能是有栈来分配(有编译器来完成,比如定义一个局部变量 int b = 1)

动态分配可以是堆分配(malloc分配,需要手动回收内存)或者栈分配(编译器来完成,自动回收内存)

  1. 灵活度不同:

静态分配需要提前指定空间大小,不能再动态改变大小。

动态分配不需要提前分配存储空间,可以动态的调整大小。

  1. 生命周期不同:

静态分配的内存在程序一开始运行就会分配内存,直到程序结束了,内存才会被释放。

动态分配的内存是在程序调用函数时才被分配,函数结束了,动态内存就应该被释放掉(别忘了手动释放)。

6. 如何构造一个类,使得只能在堆上或只能在栈上分配内存?

容易想到将构造函数设为私有。在构造函数私有之后,无法在类外部调用构造函数来构造类对象,只能使用new运算符来建立对象。然而,前面已经说过,new 运算符的执行过程分为两步,C++提供new运算符的重载,其实是只允许重载operator new()函数,而operator new()函数只用于分配内存,无法提供构造功能。因此,这种方法不可以。

当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。因此,将析构函数设为私有,类对象就无法建立在栈上了。

7. 浅拷贝和深拷贝有什么区别?

深拷贝拷贝指针所指地址的数据;浅拷贝拷贝指针本身。

8. 字节对齐的原则是什么?

字节对齐与具体编译器相关,但一般都遵循以下三条规则: 1) 结构、联合或类的数据成员,第一个相对于首地址放在偏移为0的地方; 2) 结构、联合或类的各成员相对于首地址的偏移量,都是#pragma pack指定的数值和该成员大小中较小那个的整数倍。如有需要编译器会在成员之间加上填充字节; 3) 结构、联合或类的总大小为最宽基本类型成员大小与#pragma pack指定的数值中较小那个的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

更多技术分享浏览我的博客:

https://thierryzhou.github.io

标签:

更新时间: