面试知识点(C++)

仅作个人记录
参考了网上的很多资料

文章目录

构造、拷贝构造/赋值、析构

构造函数和析构函数是否可以抛出异常

构造函数可以抛出异常
动态创建对象要进行两个操作:分配内存调用构造函数。若在分配内存时出错,会抛出bad_alloc异常;若在调用构造函数初始化时出错,不会存在内存泄漏。

析构函数不推荐抛出异常
如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如内存泄漏的问题。

什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

1、当没有定义任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有定义拷贝构造函数时,会自动生成默认拷贝构造函数。
2、深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存(用于存放复制的对象),使这个增加的指针指向这个新的内存。
3、默认拷贝构造函数属于浅拷贝
4、当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现内存多次被释放的问题。

当我们想要类的行为像一个值的时候,意味着它也应该有自己的状态,拷贝的时候副本和原对象是完全独立的,这时候就应该使用深拷贝。

类的行为像一个指针的时候,则会共享状态,副本和原对象共享相同的底层数据,这时候就是浅拷贝。

构造函数怎么减少开销

使用初始化列表进行初始化可以减少开销。

当类的成员不是内置成员的时候,比如是另一个自定义类型的时候,使用列表初始化可以对成员进行直接初始化(调用拷贝构造函数),而如果是在构造函数的函数体里面进行初始化的话,就需要隐式调用构造函数+拷贝构造函数(赋值)两个操作,多了一次构造函数的调用。

构造函数和析构函数的执行顺序?

构造函数
1.首先调用父类的构造函数;
2.调用成员变量的构造函数;
3.调用类自身的构造函数。

析构函数
4.派生类自己的析构函数
5.对象成员析构函数
6.基类析构函数(与构造顺序正好相反)

特例
局部对象,在退出程序块时析构
静态对象,在定义所在文件结束时析构
全局对象,在程序结束时析构
继承对象,先析构派生类,再析构父类
对象成员,先析构类对象,再析构对象成员(由外而内)

对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。

C++为什么需要成员初始化列表

参考:C++必须使用【初始化列表】初始化数据成员的三种情况_小凡的专栏-CSDN博客

C++必须使用【初始化列表】初始化数据成员的三种情况:

  • 需要初始化的数据成员是对象的情况,并且这个对象只有含参数的构造函数,没有无参构造函数。这时必须使用初始化列表,因为使用初始化列表可以不必调用默认构造函数进行初始化,而是直接调用拷贝构造函数初始化
  • 需要初始化const修饰的类成员或初始化引用成员数据(常量成员和引用成员);
  • 子类初始化父类的私有成员,需要在(并且也只能在)参数初始化列表中显式调用父类的构造函数。

关于第三点可能不好理解:为什么调用基类的构造函数只能写在初始化列表中?_JunJie的个人博客-CSDN博客

为了确保调用派生类的构造函数的时候,已经调用过基类的某个构造函数,完成了派生类的继承自基类部分的数据成员的初始化。即确保调用顺序是:先调用基类的构造函数,再调用派生类的。

因为,调用派生类的构造函数时,可能会调用继承自基类的函数。因此,调用派生类的构造函数时,必须确保继承自基类的部分已构造完毕,而将基类构造函数的调用写在初始化列表中,能更好地做到这一点。

另外,如果基类不存在默认构造函数,则派生类在进行构造函数的时候,必须将基类的构造函数写在初始化列表中的,否则编译不会通过。也就是说,在派生类进入构造函数函数体以后,基类的构造函数默认已经是完成了的

还有就是效率原因: 类对象的构造顺序显示,进入构造函数体后,是对成员变量的赋值操作,显然,赋值和初始化是不同的。这样就体现出了效率差异,如果不用成员初始化列表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用(拷贝构造函数),如果是类对象,这样做效率就得不到保障。

对于类类型来说,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。

构造函数初始化的顺序和其在类中声明时的顺序是一致的,与列表的先后顺序无关

class foo{
 private:
   int a, b;
};

构造函数列表的初始化方式不是按照列表的的顺序,而是按照变量声明的顺序。比如foo里面,a在b之前,那么会先构造a再构造b。所以无论foo():a(b + 1), b(2){}还是foo():b(2),a(b+1){}都不会让a得到期望的值。

什么时候会调用拷贝构造函数

什么时候会调用“拷贝构造函数”_lhs7758523的专栏-CSDN博客

  1. 一个对象作为函数参数,以 值传递的方式传入函数体;void print( string str)
  2. 一个对象作为函数返回值,以 值传递的方式从函数返回; string print(string str){ return str;}
  3. 一个对象用于给另外一个对象进行初始化(常称为 复制初始化);string a(“123”); string b(a);
1.
string  str1("123")//执行1次普通构造函数
2.
string str1;//执行1次普通构造函数
str1 = "123";//执行1次普通构造函数和执行1次赋值函数
3.
string str1("123");//执行1次普通构造函数
string str2 = str1;//执行1次拷贝构造函数
string str3(str1);//执行1次拷贝构造函数
4.
Tstring getTstring(){
 Tstring a;//普通
 a = a;//赋值
 return a;//拷贝
}
5.
Tstring getTstring(){
 Tstring a;//普通
 a = "123";//普通+赋值
 return a;//拷贝
}

explicit关键字

声明为explicit的构造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象。

有一个类 A,构造函数是A(int x),同时又有一个拷贝构造函数,那么 A a = 1, A b(2),两个语句分别调用了哪个构造函数?

都是调用带参构造函数,但是有隐式的类型转换发生。

拷贝构造函数需要参数是同类对象

如果这两个都是用了普通构造函数,那在什么情境下会产生区别?什么时候不可以用?

如果有explicit关键字修饰,就不能 a = 1(隐式转换),以及移动构造函数会使得 a = 1 调用的是移动构造函数。

explicit关键字作用于单个参数的构造函数,如果构造函数有多个参数,但是从第二个参数开始,如果各参数均有默认赋值,也可以应用explicit关键字。

智能指针

shared_ptr里面引用计数是什么类型?

1、引用计数不可以是一个普通的int型变量,因为无法实现一个资源块下的多个对象的引用计数相同,例如:

shared_ptr<A> p = new A;
...
auto q =  p;

这样的话,q和p的计数器应该是相同的,但是很明显这儿是无法同步p和q的引用计数的,假设p里面的计数+1之后,q里面的计数并不会变化。

2、引用计数也不可以是一个static变量,因为同时存在的智能指针可以指向不同的资源,也就拥有不同的引用计数。这很好解释,因为static对象是类的所有对象共享的。

因此将计数器counter设计成一个指向int的指针,指向相同资源的智能指针的counter也指向相同的int值,这样对counter做修改时,就会影响到所有拥有这个counter的智能指针。

参考:
智能指针之计数器 - 知乎
引用计数的智能指针的实现 - cyendra - 博客园

函数内定义了一个share_ptr对象,但是函数内抛出异常了没有回来继续执行,会造成内存泄漏吗?

不会,因为退出函数时,销毁shared_ptr的时候会检查其引用计数,如果该指针是指向该内存的唯一指针,那么内存会被释放掉

share_ptr引用计数有没有什么问题?怎么解决这个问题?

利用weak_ptr解决 “循环引用”_qq_43684922的博客-CSDN博客
会出现循环引用的问题(比如一个双向链表或者出现相互引用的情况的时候):

#include <iostream>
#include <memory>
using namespace std;

class B;
class A{
public:
    shared_ptr<B> bptr;
    ~A(){cout << "A destroy" << endl;}
};
class B{
public:
    shared_ptr<A> aptr;
    ~B(){cout << "B destroy" << endl;}
};
int main(){
    {
        shared_ptr<A> pa(new A);
        shared_ptr<B> pb(new B);
        pa->bptr = pb;
        pb->aptr = pa;
    }
    return 0;
}

上面的循环引用会导致两个资源的计数器最后均为1,都没有正确释放,造成内存泄露。

解决方法:用weak_ptr解决,将某一个里面的智能指针成员改为weak_ptr即可,或者改为普通指针也可以

#include <iostream>
#include <memory>
using namespace std;

class B;
class A{
public:
    shared_ptr<B> bptr;
    ~A(){cout << "A destroy" << endl;}
};
class B{
public:
    weak_ptr<A> aptr;
    ~B(){cout << "B destroy" << endl;}
};
int main(){
    shared_ptr<A> pa(new A);
    shared_ptr<B> pb(new B);
    pa->bptr = pb;
    pb->aptr = pa;
    return 0;
}

weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。

unique_ptr的独占式怎么实现?

unique_ptr的拷贝构造和拷贝赋值均被声明为delete,因此无法实施拷贝和赋值操作。

shared_ptr与多线程

多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要进行加锁处理。

c++内存空间

在C++中,程序在内存中的存储主要被分为五个区:

1、栈区(stack):由编译器自动分配释放 ,用于存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈。它的内存分配是连续分配的,当声明变量时,那么编译器会自动接着当前栈区的结尾来分配内存(调整栈顶指针),生长方向是高地址向低地址方向延伸

2、堆区(heap):一般由程序员手动分配释放,若程序员不释放,程序结束时可能由OS回收。它与数据结构中的堆是两回事,分配方式倒是类似于链表,在内存中的分布是不连续的,它们是不同区域的内存块通过指针链接起来的,生长方向是低地址向高地址方向延伸

3、全局/静态区(static):全局变量和静态变量的存储是放在一块的,在程序编译时进行分配。

4、常量区:用于存放常量字符串。

5、程序代码区:存放函数体(类的成员函数、全局函数) 的二进制代码。
在这里插入图片描述

更详细的分区:C中存储分区详解 - 唯一诺 - 博客园

heap怎么分配(malloc底层原理)

malloc 底层实现及原理_zj-CSDN博客

1)当开辟的空间小于 128K 时,调用brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata来实现分配。
2)当开辟的空间大于 128K 时,会使用mmap()系统调用函数在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

为什么栈比堆快

  • 栈由系统自动分配,读取速度较快。是连续的地址空间, 是LIFO原则的存储机制,结构简单,对栈数据的定位相对比较快速,cpu命中率高。有寄存器直接对栈进行访问(esp,ebp),可以直接从地址取数据放至目标地址。

  • 堆是由程序员手动分配的动态内存(随机),一般速度比较慢,而且容易产生内存碎片。地址空间不连续,管理方式类似链表。对堆访问,只能是间接寻址,第一步将分配的地址放到寄存器,然后取出这个地址的值,然后放到目标地址。

而且栈使用的是一级缓存, 被调用时通常处于存储空间中,调用完毕立即释放(可以循环使用),堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定。

操作系统怎么释放栈上对象?

操作系统中栈与堆的理解_xiaokugua_250的专栏-CSDN博客

栈是向下增长的。

在i386机器上,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。
在这里插入图片描述
此处栈底的地址是0xbfffff,而esp寄存器标明了栈顶,地址为0xbffffff4。在栈上压入数据会导致esp减小,弹出数据使得esp增大。相反,直接减小esp的值也等效于在栈上开辟空间,直接增大esp的值等下鱼在栈上回收空间

在程序的运行中,栈保存了一个函数调用所需要的维护信息,通常称为堆栈帧(Stack Frame)或活动记录(Activate Record)。堆栈帧一般包括如下几方面内容

  • 函数的返回值和参数
  • 临时变量–包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 保存的上下文–包括函数调用前后需要保持不变的寄存器

在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对的,ebp寄存器执行了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。

EBP 和 ESP 详解_测试开发小白变怪兽-CSDN博客

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在这里插入图片描述

所以操作系统是通过调整esp寄存器(栈指针寄存器)来达到释放对象的效果的

类的成员函数(普通函数)占空间吗?

一个Class对象需要占用多大的内存空间。最权威的结论是:

  • 非静态成员变量总合。
  • 加上编译器为了CPU计算,作出的数据对齐处理。
  • 加上为了支持虚函数,产生的额外负担

普通函数不占用空间(不算在sizeof里面),函数是放在代码段(代码段是多个类的对象所共享的,不计算在对象的大小之内)的,和静态成员(存储在静态区/全局区)一样是所有对象共享的。

类的成员函数(普通函数)不占空间,那么程序是怎么找到函数的地址的?

成员函数编译阶段生成了对应的符号表,运行的时候去符号表里面进行查找。

右值引用

从4行代码看右值引用(转)_zj-CSDN博客

std::move()

将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义(延长临时变量的生命周期)。
从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

左值和右值

不是很严谨的来说,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式)右值指的则是只能出现在等号右边的变量(或表达式)。或者左值就是在程序中能够寻址的东西,右值就是一个具体的真实的值或者对象(匿名对象),没法取到它的地址的东西(不完全准确),因此没法对右值进行赋值,但是右值并非是不可修改的,比如自己定义的class, 可以通过它的成员函数来修改右值。

如何扩展右值生命周期

右值引用可以延长临时变量的生命周期。通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

右值引用有什么用

自从引入了这个右值引用之后,我们把之前通常叫的引用称为左值引用。

区别:
左值引用:我们之前所说的别名
右值引用:大部分时间和左指引用一样,但是有一点区别,他能够绑定到临时变量(右值)
作用:
(1)避免拷贝,提高性能,实现move()(移动语义
(2)避免重载参数的复杂性,实现forward()
(3)完美转发std::forward<T>(u)

C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题

  • 通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去;
  • 通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。

std::forward与std::move

std::forward<T>(u)有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题

std::move(val)无条件的转为右值引用,而std::forward是有条件的转为右值引用,更准确的说叫做Perfect forwarding(完美转发),而std::forward里面蕴含着的条件则是Reference Collapsing(引用折叠)。

std::move不move任何东西,std::forward也不转发任何东西。在运行时,他们什么都不做。不产生可执行代码,一个比特的代码也不产生。

std::move和std::forward只是执行转换的函数(确切的说应该是函数模板)。std::move无条件的将它的参数转换成一个右值,而std::forward当特定的条件满足时,才会执行它的转换。

const与static

const 有什么用途

1.定义只读变量,或者常量(只读变量和常量的区别参考下面一条);
2.修饰函数的参数和函数的返回值;
3.修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不能修改成员变量的值,因此const对象只能调用const成员函数;
4.只读对象。只读对象只能调用const成员函数。

static 有什么用途

1.静态(局部/全局)变量
2.静态函数
3.类的静态数据成员
4.类的静态成员函数

函数形参用 const & 来修饰,有什么好处?

const禁止修改,引用不进行值复制,减少复制,提高效率。

const, static能够修饰同一个类成员函数?

不可以同时用const和static修饰成员函数

C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。

理解:static不属于任何类的实例,是所有实例共享的,不存在this指针,而const是存在隐式的参数const this*的。

用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化

在C中用const 能定义真正意义上的常量吗?C++中的const呢?

不能。

c中的const仅仅是从编译层来限定,不允许对const 变量进行赋值操作,在运行期是无效的,所以并非是真正的常量(比如通过指针对const变量是可以修改值的)

但是c++中是有区别的,c++在编译时会把const常量加入符号表,以后(仍然在编译期)遇到这个变量会从符号表中查找,所以在C++中是不可能修改const变量的

补充:

  1. c中的局部const常量存储在栈空间,全局const常量存在只读存储区,所以全局const常量也是无法修改的,它是一个只读变量。
  2. 这里需要说明的是,常量并非仅仅是不可修改,而是相对于变量,它的值在编译期已经决定,而不是在运行时决定。
  3. c++中的const 和宏定义是有区别的,宏是在预编译期直接进行文本替换,而const发生在编译期,是可以进行类型检查和作用域检查的
  4. c语言中只有enum可以实现真正的常量。
  5. c++中只有用字面量初始化的const常量会被加入符号表,而变量初始化的const常量依然只是只读变量。
  6. c++const成员变量只能在初始化列表中进行初始化。

全局变量、静态全局变量、静态局部变量和普通局部变量的区别

按存储区域分:

1、全局变量、静态全局变量和静态局部变量都存放在内存的全局数据区

2、局部变量存放在内存的栈区

按作用域分:

1、全局变量在整个工程文件内都有效;

2、静态全局变量只在定义它的文件内有效;

3、静态局部变量只在定义它的函数内有效,且程序仅分配一次内存,函数返回后,该变量不会消失(会等到程序结束);局部变量在定义它的函数内有效,但是函数返回后失效(释放)。

4、全局变量和静态变量如果没有手动初始化,则由编译器初始化为0。局部变量的值不可知。

5、静态局部变量与全局变量共享全局数据区,但静态局部变量只在定义它的函数中可见。静态局部变量与局部变量在存储位置上不同,使得其存在的时限也不同,导致对这两者操作的运行结果也不同。

C++全局变量和静态变量初始化的阶段

关于C++全局变量和静态变量初始化的一些总结 - 拂石 - 博客园

1、全局变量
全局变量无疑要在main函数开始前执行完成,但可细分为在编译时和在运行时初始化,即static initialization和dynamic initialization。

static initialization:
静态初始化是针对那些较为简单的,c++内部定义的数据结构,如int,double,bool及其数组结构的初始化。又可分为zero和const两种方式。

  • 对于zero初始化,编译时编译器将其分配在bss段,不占用rom空间;
  • const初始化,也就是我们指定了全局变量的初始值,编译器会将其分配在data段,占用rom空间。

dynamic initialization:
动态初始化,这种初始化针对的是需要调用构造函数才能完成的初始化。这种初始化会在main函数执行前由运行时库调用对应的代码进行初始化。

静态初始化先于动态初始化,这一点很好理解。静态初始化在编译时初始化,直接写进.bss或.data段,程序执行时直接加载,而动态初始化只能在运行时由运行时库调用相应的构造函数进行初始化

2、类的静态成员

C++规定,const的静态成员(const static int a = 0;)可以直接在类内初始化,而非const的静态成员(ststic int a;)需要在类外声明以初始化

对于后一种情况,我们一般选择在类的实现文件中初始化。

至此,具体的初始化方式和上面所说的又是一致的,可在编译期间初始化,也可以在运行时初始化。

c++ 全局变量初始化的一点总结 - twoon - 博客园

全局变量的作用域一定比局部变量的作用域范围大吗?

不一定。

若在函数中定义与全局变量名字相同的局部变量,则全局变量在该函数中将不起作用,因此全局变量的作用域并不一定比局部变量的作用域大

C++程序编译过程

主要分四个步骤:预处理编译汇编链接

  • 预处理:引入头文件,宏定义的替换,去除注释、添加行号,处理条件编译指令以及特殊符号,会保留所有的#pragma编译器指令。生成*.i文件。

  • 编译过程:对预处理后的文件进行语法分析,词法分析,语义分析,符号汇总,然后生成汇编代码,同时可能还会做一些编译器的优化。生成*.s的汇编文件。

  • 汇编过程:把汇编语言代码翻译成目标机器指令的过程。生成*.o文件。

  • 链接程序:由汇编程序生成的目标文件并不能立即执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库函数等等。所有这些问题,都需要经链接程序的处理方能得以解决。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体(可执行文件)。

链接方式分为静态链接和动态链接

静态链接和动态链接

参考:深入浅出静态链接和动态链接_kang___xi的博客-CSDN博客

静态链接

后缀是.a。

由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。

链接器在链接静态链接库的时候是以目标文件为单位的。比如引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了最后的可执行文件中。

缺点
1、浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,那么多个程序中都会含有printf.o,所以同一个目标文件都在内存存在多个副本;
2、更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
优点
1、在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快

动态链接

后缀是.so,基本思想:把链接过程推迟到运行时再进行。

把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

下面简单介绍动态链接的过程:

假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。

缺点
1、把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失(执行相对较慢)。
优点
1、节省空间:即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分副本,而是这多个程序在执行时共享同一份副本。
2、更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

动态链接库和静态链接库的区别,他们是怎么调用函数的?

GCC编译过程与动态链接库和静态链接库 - 三年一梦 - 博客园
1、库的作用
实现代码的解耦重用对外提供服务(export)

典型应用:exe调用dll。较小的软件可能会倾向于把功能全部写入到exe文件里面,但是大的软件一般会有很多的dll文件,因为很多代码可能需要重用,使用dll可以节省空间。
2、库的分类

  • 静态链接库(静态库):打包在exe文件里面,在编译的时候会与他们的调用者exe打包成为一个exe文件(可执行文件)。windows下是.lib文件,Linux下是.a文件。
  • 动态链接库(共享库):与exe是独立的两个文件,windows下是dll,Linux下是.so文件。可执行文件在运行时才会动态调用共享库,exe调用dll不会影响它的重用,其他人也可以调用它(是共享的),不像静态库那样被打包进去。

模板与分离编译

C++ – 模板与分离编译_我的博客-CSDN博客

普通函数支持分离编译

模板不支持分离编译(以模板函数为例):对于模板函数来说,若在Template.h里面声明,在Template.cpp里面定义,在Test.cpp里面调用。当编译完成后就会生成一个Template.o和Test.o的目标文件,当调用这个模板函数时,发现当前只有它的声明,则它会去Template.o的符号表里面去找,但是模板只有在实例化之后才能生成相应的代码,就会发现符号表里面没有该模板函数的地址,就会在链接时出错。

如何解决?可以在声明后,显示实例化,或者不要采用分离编译(将其声明和实现放在一起)。

编译原理,怎么链接的,相互依赖问题是在链接的时候解决吗?如果一个程序用了另一个程序的代码,是分开编译的吗?

1、看上面的静态链接和动态链接

2、是在链接的时候解决的

3、是分开编译的

C+ +语言支持“分别编译”(separate compilation):

一个程序所有的内容,可以分成不同的部分分别放在不同的.cpp文件里。

.cpp文件里的东西都是相对独立的,在编译(compile)时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接(link)就行了

比如,在文件a.cpp中定义 了一个全局函数“void a() {}” ,而在文件b.cpp中需要调用这个函数。
即使这样,文件a.cpp和文件b.cpp并不需要相互知道对方的存在,而是可以分别地对它们进行编译, 编译成目标文件之后再链接,整个程序就可以运行了。

include <> 与 " "的区别

#include<>直接从编译器自带的函数库中寻找文件
#include""是先从自定义的文件中找 ,如果找不到在从函数库中寻找文件

头文件中的 ifndef/define/endif 是干什么用的?

条件编译指令,作用是防止头文件被重复包含或者定义。

内存泄漏

什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。

动态分配的内存未释放或者无法释放,但是原本指向这块内存的指针却失去了对这块内存的控制,导致内存的浪费。

  1. 使用的时候要记得指针的长度
  2. malloc的时候得确定在那里free
  3. 对指针赋值的时候应该注意被赋值指针需要不需要释放
  4. 动态分配内存的指针最好不要再次赋值
  5. 在C++中应该优先考虑使用智能指针

c++内存泄露在程序退出之后还存在吗?

所谓内存泄露往往发生在程序运行的时候,操作系统会在程序退出之后回收内存。

工程代码出现了堆栈泄漏,如何快速定位?

1、vs下调用EnableMemLeakCheck();
2、检查代码的动态内存分配部分的代码有没有对应的释放操作;
3、valgrind内存泄露工具使用:内存泄漏检测工具valgrind神器 - 知乎

内存泄漏类型

参考:C++中内存泄漏的几种情况_Coding everyday…-CSDN博客

1、在类的构造函数和析构函数中没有匹配的调用new和delete函数

2、没有正确地清除嵌套的对象指针

3、在释放对象数组时在delete中没有使用方括号

4、指向对象的指针数组不等同于对象数组
对象数组是指:数组中存放的是对象,只需要delete []p即可;

指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。

5.缺少拷贝构造函数
浅拷贝可能会导致两次释放相同的内存,是一种错误的做法,同时可能会造成堆的崩溃。

C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。

所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符(深拷贝),要么禁用拷贝构造函数和重载赋值运算符

6、缺少重载赋值运算符
与上一个情况类似

7、没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

8、智能指针的循环引用也可能导致内存泄露

野指针以及产生的情况

野指针:指向被释放的或者访问受限内存的指针。

造成野指针的原因:

  • 指针变量没有被初始化(如果值不定,可以初始化为NULL)
  • 指针被free或者delete后,没有置为NULL,free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL
  • 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。

宏和内联

宏和内联(inline)函数的比较?

  1. 首先宏是C中引入的一种预处理功能,宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。

  2. 内联(inline)函数是C++中引用的一个新的关键字,C++中推荐使用内联函数来替代宏代码片段。内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch(如果程序中有N次调用了内联函数则会有N次展开函数代码,则函数内部的开销会远高于调用开销,内联无意义。),并且内联函数本身不能直接调用自身如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数

  3. 内联函数将函数体直接展开到调用内联函数的地方,这样减少了参数压栈,跳转,返回等过程

  4. 由于内联发生在编译阶段,所以内联相较宏,是有参数检查和返回值检查的,因此使用起来更为安全;而宏则是在预处理阶段进行文本替换的。

  5. 需要注意的是, inline会向编译期提出内联请求,但是是否内联由编译期决定(当然可以通过设置编译器,强制使用内联)。

  6. 由于内联是一种优化方式,在某些情况下,即使没有显示的声明内联,比如定义在class内部的方法,编译器也可能将其作为内联函数。

  7. 内联函数不能过于复杂,最初C++限定不能有任何形式的循环,不能有过多的条件判断,不能对函数进行取地址操作等,但是现在的编译器几乎没有什么限制,基本都可以实现内联。

内联inline什么时候会失效?

1、inline只在release版本生效,它在debug版本是不生效的(仍然会开辟空间)
2、inline只是给编译器的一个建议,在递归,循环中,将不会处理
3、inline基于实现的,而不是基于声明,需要有函数体,inline加在函数声明上是没有意义的,inline是基于实现的,必须写在函数实现上
4、inline的实现要写在头文件中,写在源文件中没有调用点,代码无法展开

为什么不能用宏代替大型函数

参考22

宏定义是在预处理阶段进行宏名替换的(这个过程叫宏展开,是以代码膨胀为代价的)。宏定义不具备参数检查和类型检查,而且宏代替大型函数的话调试阶段也无法进行错误检查(无法调试),宏就相当于打补丁,而且宏不支持递归,运算优先级容易出问题。

定义一个宏来返回两个参数中最大的值

#define max(a,b) (((a)>(b))?(a):(b))

动态内存分配

C++中有了malloc / free , 为什么还需要 new / delete?

  1. malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  2. 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。

由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

也即是说:
new进行三个工作:动态内存分配,构造函数的执行,返回新分配并指向构造的对象的指针
delete也会执行两个操作:变量的析构,动态内存的释放

new是带有类型检查的,返回的是指向所分配的对象的指针
而malloc返回的则是void* 的指针,需要进行类型转换之后才是需要的类型的指针

malloc和new的区别,两者申请内存失败时会返回什么?怎么让new不抛出异常而返回null?

new内存分配失败时,会抛出bac_alloc异常

malloc分配内存失败时返回NULL(空指针)

new操作符可以抑制异常的抛出,就可以返回NULL:

int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针
        if ( p == NULL ) // 如此这般,这个判断就有意义了
            return -1;

malloc/free不允许重载,而new/delete运算符是支持重载的。

malloc分配的时候需要指定内存大小,为什么free的时候不需要指定?

当调用malloc(size)时,实际分配的内存大小大于size字节,这是因为在分配的内存区域头部有类似于

struct control_block {    
	unsigned size;    
	int used;
};

这样的一个结构,如果malloc函数内部得到的内存区域的首地址为void *p,那么它返回给你的就是p + sizeof(control_block),而调用free(p)的时候,该函数把p减去sizeof(control_block),然后就可以根据((control_blcok*)p)->size得到要释放的内存区域的大小。这也就是为什么free只能用来释放malloc分配的内存,如果用于释放其他的内存,会发生未知的错误。

C++ delete[] 是如何知道数组大小的?

现在的编译器大多使用两种方法,

  • 一种是cookie, 一个记录分配空间大小的内存小块绑定在分配内存的地址头部
  • 二是使用表来对分配了的指针进行管理,每一个分配了空间的指针都在表中对应着分配空间的大小。

new有没有系统调用?

因为new的底层实际上也是malloc实现动态内存分配的,而malloc会使用系统调用brk或mmap去获取内存。

malloc底层实现;malloc申请8KB空间,是否连续?

malloc 底层实现及原理_zj-CSDN博客

使用malloc分配的内存空间在虚拟地址空间上是连续的,但是转换到物理内存空间上有可能是不连续的,因为有可能相邻的两个字节是在不同的物理分页上

delete 指针后为什么需要置为NULL?

delete指针只是编译器释放该指针所指向的内存空间(该空间可以给其他变量使用),而不会删除这个指针本身。这可能会导致后续申请指针时,系统新建的指针指向的地址可能会跟delete掉的指针相同,此时如果修改delete掉的指针的内容就会导致对新建的指针内容的修改

为了防止这种情况的发生,需要delete掉后立即置为NULL(避免变成野指针),同时在新建指针的时候需要判断新建的指针是否为NULL,为NULL才是申请成功。

对null的delete可以无数次,因为delete会直接跳过NULL。

面向对象

什么是面向对象(OOP)?面向对象的意义?

面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象(封装)继承动态绑定(多态)

  • 数据抽象:我们可以将类的接口和实现进行分离,在设计逻辑的稳定和变化之间寻找到一个分割点。

  • 继承:可以定义相似的类型并对相似关系进行建模,提高代码的复用率

  • 动态绑定,可以一定程度上忽视相似类型的区别,而以同统一的方式(接口)使用他们的对象

虚函数、纯虚函数

  • 虚函数:在类的成员函数定义前加 virtual 关键字,该函数将被作为虚函数。虚函数被继承后仍为虚函数。虚函数的在子类中可以被override(覆盖)、overload (重载 )。

  • 纯虚函数:纯虚函数除了有virtual 关键字外,还令它等于0,以表为纯虚函数。拥有纯虚函数的类称为抽象类 。抽象类不能被实例化, 只有实现了这个纯虚函数的子类才能new出对象(实例化)。类的继承往往越往后越具体,相反地,越往祖先越抽象,以至于没法实例代。之所以定义抽象类,是为了给以后的子类留下公共接口

构造函数可以是虚函数么,析构函数是虚函数么?

  • 构造函数不能为虚函数虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中。若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数–>构造函数了。

  • 析构函数可以为虚函数:而且当要使用基类指针或引用调用子类对象时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题

假设子类B继承自基类A:

A *p = new B; 
delete p;

1、此时,如果类A的析构函数不是虚函数,那么delete p将会仅仅调用A的析构函数,只释放了B对象中的A(父类)部分,而派生类的部分未被释放,造成内存泄露。
2、如果类A的析构函数是虚函数,delete p将会先调用B的析构函数,再调用A的析构函数,释放B对象的所有空间(析构的时候先调用子类的析构函数,然后才是父类的,与构造的时候相反)。

补充:
B *p = new B; delete p;
时也是先调用B的析构函数,再调用A的析构函数。

虚函数表是怎么实现的?

如果一个类包含了虚函数,那么在创建该类的对象时就会为该对象创建一个虚函数表指针,该指针指向类的虚函数表,也就是说虚函数表和类对象本身并不是存储在一起的,只是有一个指针指向的关系。

参考:获取C++虚表地址和虚函数地址_qhh0205-CSDN博客

大概是这样的布局(虚函数表指针会存放在对象的首地址处,不过多重继承的时候可能会不同,可以看下面多重继承的内存分布):

在这里插入图片描述
而类的成员(函数,变量)布局是这样的(参考:C++面试必备之虚函数 - 知乎):
在这里插入图片描述
也即是说:

普通函数、虚函数、虚函数表都是同一个类的所有对象公有的,只有成员变量和虚函数表指针(vptr)是每个对象私有的(每个对象都有一个),sizeof的值也只包括vptr和var所占内存的大小,并且vptr通常会在对象内存的最起始位置。(不用每个对象都有一个虚函数表,虚函数表大家共享,用指针指向即可,节省空间)

虚函数表(包含*func_b()的指针数组)一般是存储在全局数据区的(方便所有对象共享),表里面存放的也是函数的地址(指针,指向函数的实现位置)

所以虚函数调用过程是:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

虚函数表的继承问题

参考:C++虚函数表的继承问题总结_Jacksqh的博客-CSDN博客

每个含有虚函数的类都会有自己的虚函数表,所以如果子类继承父类,而父类有自己的虚函数的话,虚函数也会被子类继承,所以子类也会有自己的虚函数表(将父类的虚函数表复制而来,表里的指针会和父类里的一样指向相同的函数地址)

补充:
第一点:严格说是同一的类的不同对象都有自己的虚函数表指针,只是指向相同的虚函数表(共用)
第二点:子类和父类中的虚函数表中没重写的虚函数也是共用的(重写了就会进行替换)

单继承

当父类定义了虚函数时,在子类进行继承的时候会将父类的虚函数表也给继承下来。所以那一些虚函数在子类中也是virtual类型的,如果要对父类中的虚函数进行重写时或添加虚函数,顺序是:(图表说明为图一)
1、先将父类的虚函数列表复制过来
2、重写虚函数时是把从父类继承过来的虚函数表中对应的虚函数进行相应的替换
3、如果子类自己要添加自己的虚函数,则是把添加的虚函数加到从父类继承过来虚函数表的尾部

在这里插入图片描述

多继承

例如Son类继承自Father和Mother类:

那么son的对象会有两个虚函数指针分别指向Father类中的虚函数表,以及Mother类中的虚函数表(可以理解为有继承自几个不同对象就有多少个虚函数指针)。也就是说sizeof的话,至少会有两个指针的大小。
在这里插入图片描述

多重继承的优缺点

允许一个类指向多个基类,这样继承的结构叫多重继承。

优点:对象可以调用多个基类中的接口

缺点:如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性(菱形继承)。

菱形继承

c++关于菱形继承&虚继承的问题总结_Suhw的博客-CSDN博客

class A
{
public:
    int _a;
};

class B : public A
{
public:
    int _b;
};

class C : public A
{
public:
    int _c;
};

class D : public B, public C
{
public:
    int _d;
};

在这里插入图片描述

解决方法:虚继承

C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。

class 派生类: virtual 基类1,virtual 基类2,…,virtual 基类n
{
…//派生类成员声明
};
class A
{
public:
    int _a;
};

class B : virtual public A
{
public:
    int _b;
};

class C : virtual public A
{
public:
    int _c;
};

class D : public B, public C
{
public:
    int _d;
};

当我们求没有使用虚继承之前的class D的大小,结果是20,但是在使用了虚继承后大小变为24。
在这里插入图片描述

public、prtected、private继承

继承访问说明符(派生类访问说明符)对于派生类成员(以及友元)能否访问其直接基类的成员没什么影响,派生访问说明符的目的是控制派生类用户对基类成员的访问权限,以及继承自派生类的新类的访问权限。

public继承相当于将父类的成员展开在子类的public部分,其他继承方式也是类似的。

注意区分类的成员可调用与类的对象可调用两种区别:对象只能调用public方法,而成员函数可以调用本类的私有的方法。

更详细的看这儿:
C++:继承访问属性(public/protected/private) - Tom文星 - 博客园

C++如何实现多态?运行时多态还是编译时多态?编译时多态是什么?

运行期多态(动态多态)

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。

编译时多态(静态多态)

编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。主要有两种实现方式:

  • 函数重载:包括普通函数的重载和成员函数的重载
  • 函数模板的使用:对模板参数而言,多态是通过模板实例化和函数重载解析实现的。以不同的模板参数实例化导致调用不同的函数

虚表和虚指针

虚表:虚表存储了一系列的函数指针,每个类都有一个虚表,这是类级别的。
虚指针:指向虚表的指针,每个对象都有一个虚指针,这是对象级别的。

编译器在下面两个地方添加代码来维护和使用虚表:

  • 构造函数。对象在构造时,会创建一个虚指针,指向类的虚表
  • 多态函数调用。当多态调用发生的时候,程序会先根据虚指针去查看虚表,在虚表中会找到匹配的函数,之后进行函数调用,这是发生在运行期的,会损失一定的性能。

虚函数不要在构造函数中调用,因为构造函数中构造虚指针,此时调用虚函数,那么虚函数就会像非虚函数那样执行,不会动态绑定

C++ 关于子类重写父类方法,并在构造函数中调用时是调用子类中的方法还是父类中的方法?

C++ 关于子类重写父类方法,并在构造函数中调用的问题? - 知乎
会调用父类中的方法。

#include <string>
#include <iostream>

class Base {
public:
    Base() : name_("base"){
        echo();
    }
    void slap(){
        echo();
    }
    virtual void echo(){
        std::cout << name_ << std::endl;
    }
private:
    std::string name_;
};

class Derived : public Base {
public:
    Derived() : name_("derived"){}
    virtual void echo(){
        std::cout << name_ << std::endl;
    }
private:
    std::string name_;
};

int main(){
    Derived object;
    auto p = new Derived();
    //object.slap();
    //object.echo();
}

子类对象在创建时,会首先调用父类构造函数(然后才是自身成员变量的构造函数,最后才是子类的构造函数)。那么子类调用父类的构造函数时,此时的object 此时还是一个(正在初始化的)Base 对象,子类对象实际上尚未生成,所以会调用父类中的方法。

关于虚函数的内联

虚函数一般不要声明成内联inline的形式。在父类引用和父类指针调用的虚函数,不能内联;但是子类对象的虚函数可以内联。但是建议不要内联。

inline是在编译时将函数类容展开到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以也不知道应该把哪一个函数的代码进行展开

所以当虚函数表现多态性的时候不能内联。

静态函数可以是虚函数吗?

静态函数不能是虚函数!!!(静态函数只有一个,所有的对象都是共享的,与虚函数的性质相违背)。static静态函数没有this指针,而对于virtual虚函数,它的调用恰恰使用this指针。

在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr->vtable ->virtual虚函数。

多态的分类

面向对象的多态分为四类:
重载多态:函数重载,运算符重载
强制多态:不同类型数据运算,强制类型转换
包含多态:动态绑定(虚函数)
参数多态:模板多态

虚函数可以私有吗?

可以,但是需要注意:类里面得有友元的定义,如果没有友元的话,会编译错误,无法访问私有成员。

添加了友元之后,虚函数私有并不影响动态绑定。

class Base {
friend int main();//注意这儿
private:
	virtual void fun() { std::cout << "Base Fun"; }
};

class Derived : public Base {
public:
	void fun() { std::cout << "Derived Fun"; }
};

int main()
{
	Base *ptr = new Derived;
	ptr->fun();
	return 0;
}

面向对象六大基本原则(设计模式)

面向对象六大基本原则的理解 - laden666666 - 博客园

1.开闭原则
六大原则中最基本的原则。开闭原则指的是,一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

2.里氏替换原则
使用接口的时候,我们必须确保子类能够替换父类所出现的任何地方,也即所有引用基类的地方必须能透明地使用其子类的对象。

3.依赖倒置原则
这个原则讲究解耦,他指的是让高层模块不要依赖低层模块

高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;要针对接口编程,不要针对实现编程。

4.职责单一原则
职责单一原则指的是一个模块(接口)的功能尽量是单一的,这样不同功能的接口就不会耦合在一起了,同时维护起来也方便。

不要存在多于一个导致类变更的原因。通俗来说,即一个类只负责一项职责。

5.接口隔离原则
接口隔离原则强调每个类继承的接口一定要尽量最少,不能继承无用的接口,保证接口隔离原则的前提是先要保证职责单一原则。

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

6.最小知道原则(迪米特原则)
最小知道原则指的是模块是所有的依赖都要保存最少,这一点和接口隔离原则有点重复,或者可以说接最小知道原则包含接口隔离原则,同时最小知道原则还有对外界影响最小的意味。

一个对象应该对其他对象保持最少的了解。只与你直接的朋友通信,而避免和陌生人通信。

怎么样算一个好的接口?

C++:如何正确的定义一个接口类_netyeaxi的专栏-CSDN博客

首先给接口类下了定义:接口类应该是只提供方法声明,而自身不提供方法定义的抽象类。接口类自身不能实例化,接口类的方法定义/实现只能由接口类的子类来完成

对于C++,其接口类一般具有以下特征:

1.最好不要有成员变量,但可以有静态常量(static const或enum,静态常量可以共享)
2.要有纯虚接口方法
3.要有虚析构函数,并提供默认实现
4.不要声明构造函数(一般抽象类不需要实例化)

如下就是一个最简单的例子:

class Testable{
public:
    static const int START = 1;  // #1
    static const int STOP = 2;
 
    virtual void test() = 0;  // #2: 接口方法,纯虚函数
 
    virtual ~Testable() {};   // #3: 从C++11开始可以: virtual ~Testable() = default;
};

1、如果成员变量,尤其是可变的成员变量,定义在接口中,等于是把实现细节暴露出来了,不符合接口定义的要求,所以一般不在接口中定义可变的成员变量。
而常量可以定义在接口中,因为有时接口需要返回状态,而这些状态可以定义成常量放在接口中。
2、由于不能让接口类自身能够实例化,并且需要子类必须实现接口暴露的方法,所以接口方法都要声明成纯虚函数。
声明成纯虚函数意味着接口类自身不需要提供方法的定义,方法的定义需要由接口类的子类提供,并且接口类自身也因此变成了抽象类而不能被实例化。
3、

  • 在使用接口类的指针访问接口类的子类的实例时,当对接口类的指针做delete时,如果接口类的析构函数不是虚析构函数的话,将只会调用接口类的析构函数,接口类的子类的析构函数将不会被调用,内存泄露将会产生,所以接口类的析构函数必须定义成虚析构函数。
  • 如果接口类的析构函数不提供默认实现,即如果接口类的析构函数是纯虚析构函数的话,接口类的子类将被迫必须提供析构函数的实现,这样对接口类的子类不友好。
  • 在C++11中也可以用: virtual ~Testable() = default; 替代 virtual ~Testable() {};

4、不要显式定义任何的构造函数,但也不要在接口中加入如下代码来禁止生成构造函数:

	Testable() = delete;
	Testable(const Testable&) = delete;

因为C++的调用机制要求子类的构造函数调用时一定会先调用父类的构造函数,如果禁止生成构造函数,代码编译时会报错。如果程序员不显式的提供构造函数,编译器也会隐式的加上构造函数的,虽然这些构造函数对于接口类来说实际没有什么意义。

最后再结合上面的设计模式的几个原则,一个好的接口还应该:尽量保证职责单一,不要依赖于下层的接口实现,并且继承的接口应该尽可能的少。总之应该往几个原则上面去思考。

类/结构体

c++空类

  • 占用空间一个字节

空类型实例中不包含任何信息,但是当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例(你想想,这个实例的指针至少要指向一块内存空间吧)。至于占多少空间,由编译器决定。Visual Studio中每个空类型的实例占用1字节的空间。

每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址。

  • 那如果在该类中添加一个构造函数和析构函数,再求sizeof(),得到的结果又是多少?

还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关(成员函数是属于类的,实例只是拥有调用权,而且成员函数的静态绑定,也是在编译阶段完成的),编译器也不会因为这两个函数而在实例中添加任何额外的信息。函数是放在代码区的,是所有类对象共享的,sizeof不会进行计算

  • 如果析构函数标记为虚函数呢?

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

怎么让类只能在堆上生成实例,而不能在栈上生成?

限制一个类的对象实例,只能在堆上分配,或者只能在栈上分配_身在边城 心在编程-CSDN博客

在 C++ 中创建一个类对象分为静态和动态,一种是静态建立,如 A a, 一种是动态建立 A * a = new A;

静态建立类对象:是由编译器在栈空间为对象分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象,使用这种方法,直接调用类的构造函数。

动态建立类对象:是使用 new 运算符将对象建立在堆空间中。这个过程分为两步,第一步先是执行 operator new() 函数,在堆空间中搜索合适的内存并进行分配;第二步才调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

只能在栈内存上实例化的类

将操作符operator new和operator delete定义为private,这样使用new操作符创建对象时候,无法调用operator new,delete销毁对象也无法调用operator delete。

只能在堆内存上实例化的类

将析构函数定义为private,在栈上不能自动调用析构函数,只能手动调用。

把析构函数设置为私有的,则编译器在栈中建立对象的时候会报错,因为对象无法访问析构函数,导致不可以在栈中创建,虽然可以解决问题,但是有缺陷:

如果把该类作为基类,需要继承的的时候,析构函数要设置为 virtual ,然后在子类重写,如果把析构函数为私有的,在子类不可见,无法实现多态。(后续测试发现,如果把析构函数设置为私有的,会导致子类无法继承这个类,因为析构函数不能访问。)

#include <iostream>
#include <vector>
using namespace std;

class A  {  
public:  
    void destory(){
    	delete this;
    }  
private:  
    ~A(){}  
};  

class B : public  A{
public:
    B() : b(0){} //报错,因为此处的初始化列表会隐式调用基类的默认构造函数,但是对应析构函数为私有的,调用失败
private:
    int b;
};

int main() {
	B* b = new B;
	return 0;
}

C++ 类中一共有三种权限,可以把析构函数设置为 protected ,该权限的成员函数在类外依然不可以访问,但是可以在子类访问到,这样就解决了继承的问题

class A  {  
protected:  
    A(){}  
    ~A(){}  
public:
	// 这样写是为了代码的规范
	// 如果使用 new 来进行申请空间,却使用 destroy 函数释放感觉怪怪的
	// 所以使用函数进行申请空间,使用函数释放空间  
    static A* create()  {  
        return new A();  
    }  
    void destory()  {  
        delete this;  
    }  
};  
#include <iostream>
#include <vector>
using namespace std;

class A  {  
public:  
    void destory(){
    	delete this;
    }  
protected:  
    ~A(){}  
};  

class B : public  A{
public:
    B() : b(0){}
private:
    int b;
};

int main() {
	B* x = new B();
	return 0;
}

如何防止一个类被拷贝?

C++11之前,当我们希望一个类不能被拷贝,就会把构造函数定义为private,但是在C++11里就不需要这样做了,只需要在构造函数后面加上=delete来修饰就可以了。

定义为private只是外部不能访问,但是内部函数比如类的某个成员函数依旧可以调用,而delete就是大家都不能用。

如何防止一个类被继承

方法1:将构造函数设置为私有,因为继承肯定会调用基类的构造函数,所以把它设置为私有就无法访问,也就无法继承了

方法2:利用关键字 final ,在类名的后面加上这个关键字,可以防止该类被继承
C++11之final关键字_《致青春》——云梦泽-CSDN博客

如何让一个类只创建一个对象

C++单例模式_zj-CSDN博客

饿汉模式:在类还没有实例化对象前,在类中就有一个对象,而且不能创建其他的对象。执行效率高,获取对象快,但是在类加载的时候就初始化对象,会浪费内存空间,用空间换取时间的方式

class Singleton
{
private:
	static Singleton instance;//事先存在这一个对象
	Singleton();//私有
	~Singleton();
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
public:
	static Singleton& getInstance() {
		return instance;
	}
}
// initialize defaultly
Singleton Singleton::instance;//事先初始化好,需要的时候返回

懒汉模式:用时间换取空间,需要的时候再创建对象,如果已经创建好了就不会再分配内存空间。它在类加载的时候不会被初始化

#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak

class Singleton{
public:
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    static Singleton* get_instance(){
        if(m_instance_ptr == NULL){//直到调用时才进行创建
              m_instance_ptr = new Singleton;
        }
        return m_instance_ptr;
    }
    void use() const { std::cout << "in use" << std::endl; }
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
    Singleton(const Singleton&) = delete;//禁止拷贝
    Singleton& operator=(const Singleton&) = delete;//禁止赋值
    static Singleton* m_instance_ptr;
};

Singleton* Singleton::m_instance_ptr = nullptr;//静态成员变量必须在外部初始化

int main(){
    Singleton* instance = Singleton::get_instance();
    Singleton* instance_2 = Singleton::get_instance();
    return 0;
}

但是这段代码还存在很大的问题?
线程是否安全?锁竞争?对象会不会被加载到寄存器?

volatile 保持内存的可见性:所有线程都能看到共享内存的最新状态,如果一个线程创建了 data 对象,但是被优化到寄存器,别的线程不知道,可能会继续创建对象。每次读取前必须先从主内存刷新最新的值,每次写入后必须立即同步回主内存当中

竞态条件:从多进程间通信的角度来讲,是指两个或多个线程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

最推荐的局部静态变量式(线程安全):如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这是C++11标准中的Magic Static特性。

#include <iostream>

class Singleton
{
public:
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(const Singleton&) = delete;//禁止拷贝
    Singleton& operator=(const Singleton&) = delete;//禁止赋值
    static Singleton& get_instance(){//静态函数
        //静态局部变量生存期:第一次被执行初始化时分配内存,直到程序运行结束才销毁。
        //而且只有该函数作用域内才能访问,并且不会在每次调用时重置或者重新初始化
        //静态局部变量保存在全局数据区,作用域为局部作用域
        static Singleton instance;
        return instance;
    }
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
};

int main(int argc, char *argv[])
{
    Singleton& instance_1 = Singleton::get_instance();//注意这儿需要声明单例的引用才可以获取对象
    Singleton& instance_2 = Singleton::get_instance();
    return 0;
}

struct和union

  • struct
    各成员各自拥有自己的内存,各自使用互不干涉,同时存在的,遵循内存对齐原则。一个struct变量的总长度等于所有成员的长度之和。
  • union
    各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权(对该内存的读写),各变量共用一个内存首地址。因而,联合体比结构体更节约内存。一个union变量的总长度至少能容纳最大的成员变量,而且要满足是所有成员变量类型大小的整数倍。不允许对联合体变量名直接赋值或其他操作。

变长结构体怎么实现

C++变长结构体_light7866的博客-CSDN博客

变长结构体,其实真正意义上并不是结构体的大小可变,而是使用结构体中的变量代表一个地址,从而访问超出结构体大小范围的数据。

可变大小结构体如何定义详解 - Perfect_Code - 博客园

方法1:使用指针

1 typedef struct _S_HB_TIME_REPORT_INFO
2 {
3     uint16_t msg_id;
4     uint16_t msg_buf_len;
5     char *p_msg_buf;
6 }__attribute__((packed)) S_HB_TIME_REPORT_INFO;

如上定义的结构体,成员变量p_msg_buf为一个指针,指向一个不确定长度的字符串,长度由msg_buf_len决定。在使用时可以动态给p_msg_buf分配msg_buf_len的内存,也可以p_msg_buf指向一个已知地址的字符串。

方法2:使用变长数组:int a[0],相当于占位符。

C++定义变长数组方法(两种方法)_code进击者-CSDN博客

A是一个类,new A和new A()的区别

也就是带不带括号的区别。
C++创建类对象时(无参)后不加括号与加括号的区别_随意的风的专栏-CSDN博客

#include <iostream>  
class MyClass
{
public:
	MyClass()
	{
		std::cout << "Hello MyClass!" << std::endl;
	}
	MyClass(int i) :num(i)
	{
		std::cout << "Hello MyClass!------int" << std::endl;
	}

	void MyMethod()
	{
		std::cout << "输出成员num: " << num << std::endl;
	}

private:
	int num;
};
int main()
{
	//---------------对于调用构造函数  
	MyClass c1;//表示使用不带参数的构造函数,或者有默认参数值的构造函数。  
	MyClass c2();//不会调用无参构造函数,各种情况下该处是声明一个返回值为MyClass类型的函数而已  
	MyClass c3(1);//调用参数为int的构造函数  
	/*---------------对于new关键字加括号和不加括号的区别---
	1.对于自定义类型来说没有区别,都是使用默认构造函数
	2.对于内置类型来说加括号会初始化
	*/
	std::cout << std::endl;
	MyClass *c4 = new MyClass();
	c4->MyMethod();
	MyClass *c5 = new MyClass(1);
	c5->MyMethod();
	MyClass *c6 = new MyClass;
	c6->MyMethod();

	//内置类型  
	std::cout << std::endl;
	int *pint1 = new int(1);
	int *pint2 = new int();
	int *pint3 = new int;

	std::cout << *pint1 << " " << *pint2 << " " << *pint3 << std::endl;
	return 0;
}

C++在new时的初始化的规律可能为:

  • 对于有构造函数的类(自定义类型),不论有没有括号,都用默认构造函数进行初始化
  • 如果没有构造函数(内置类型),则不加括号的new只分配内存空间,不进行内存的初始化,而加了括号的new会在分配内存的同时初始化为0

函数

函数调用时如何查找的?

名字查找先于类型检查

作用域屏蔽名字内层作用域中声明的名字将隐藏外层作用域中声明的同名实体(内层声明一个变量read会隐藏外层定义的read()函数)

c++编译器怎么支持的重载

在C++中,为了支持重载机制,在编译时,要对函数的名字进行一些处理,比如加入函数的返回类型等来加以区别;在C中,只是简单的函数名字而已。如函数void func(int i),C++会把它编译成类似_fun_int或_xxx_funIxxx这样的增加了参数类型的符号,这也是C++可以实现重载的原因;而C则把该函数编译成类似_fun的符号,C链接器只要找到该函数符号就可以链接成功,它假设参数类型信息是正确的。故而,C和C++在编译时生成函数名字的方式是不同的

缺省传参

缺省参数是声明或定义函数时为函数的参数指定一个默认值 。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

缺省参数的分类:

(1)全缺省参数:函数的每个参数都有缺省值,传参时,可传任意多个参数,且参数的传参是从左依次往右进行

(2)半缺省参数:函数的部分参数有缺省值,且缺省值要从右往左依次给。比如:

void Test(int a, int b = 0, int c = 0){}

注意:
· 带缺省值的参数必须放在参数列表的最后面
· 缺省参数不能同时在函数声明和定义中出现,只能在其中一个出现;
· 缺省值必须是常量或者全局变量;

所以缺省传参的顺序就是压栈顺序从右往左给出。函数参数入栈的时候是实参进入函数的栈帧,从左到右依次赋值,所以带默认值的参数需要放在右边防止参数匹配出错。

函数调用进行了哪些步骤?

函数调用过程理解_萧萧落木的独白-CSDN博客

参数入栈:将函数的参数从右向左依次压入系统栈中
返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行(保存现场
代码区跳转:处理器从当前代码区跳转到被调用函数的入口处
栈帧调整:
a、保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)
b、将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)
c、给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶)

指针和引用作为形参,哪一个效率更高一点

一样,引用的极大部分底层实现都是指针,所以两种编译后的结果都是一样的

main()函数执行之前之后执行了那些操作?

  • main函数执行之前:
    具体来说:
    1、初始化栈的指针
    栈用于存储一些需要的局部变量或者其他数据
    2、静态变量或者全局变量的初始化(data段
    3、如果全局变量未初始化,如果是内置类型会执行默认初始化,数值型short,int,long等为0,bool为FALSE,指针为NULL,等等(bss段
    4、一些非内置类型,会调用默认构造函数进行初始化
    5、main()函数的参数压栈(int argc, char **argv)
    6、可能还会有标准输入输出或者错误流的初始化
  • main函数执行之后:
    那么相应的,main()函数执行之后,也需要进行相应资源的释放,比如要销毁堆内存,以及流的关闭。

怎么在main函数之前和之后执行代码?

参考: 如何在main函数之前和之后执行代码_凡辰道-CSDN博客

根据上一个问题知道,全局对象的构造函数会在main 函数之前执行,全局对象的析构函数会在main函数之后执行。那么就可以在构造函数/析构函数里去定义一些在main函数执行之前/之后的操作了:

#include <iostream>
using namespace std;

class A{
public:
    A(){cout << "before main()" << endl;}
    ~A(){cout << "after main()" << endl;}
};

A a; //全局变量
int main(){
    cout << "this is in main()" << endl;
    return 0;
}

当然除了全局对象之外,全局静态对象也是可以的。

除此之外,C\C++还提供了另一个方法:onexit( func )(对于C)或atexit( func )(对于C++),他们注册的函数会在mian函数之后执行

#include <iostream>
using namespace std;

class A{
public:
    A(){cout << "before main()" << endl;}
    ~A(){cout << "after main()" << endl;}
};

A a; //全局变量
void func1(){
    cout << "function 1" << endl;
}
int main(){
    cout << "this is in main()" << endl;
    atexit(func1);
    return 0;
}

输出:
在这里插入图片描述
onexit和atexit可以混用,但是注册函数时,是一个入栈的过程,执行是出栈,所以后注册的会先执行

析构函数在onexit和atexit注册的函数执行之后执行。

函数指针和指针函数

函数指针:
是指向函数的指针变量,即重点是一个指针。 格式:类型说明符 (*函数名)(参数)

int (*f) (int x); 

指向函数的指针包含了函数的地址,可以通过它来调用函数,其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数实现地址。

指针函数:

就是指针的函数,表示是一个函数,函数返回类型是某一类型的指针。

int *f(x, y);

char *c = NULL,怎么设计函数 void fun( ),把c传入, 给c分配内存

指针的引用或者指针的指针

char* p = NULL; int ret = m_alloc(&ptr,size); 实现这个函数 int m_alloc(char ** ptr, int size);参数传NULL是否会有问题?

void m_alloc(char **p, int size){
    *p = (char *)malloc(sizeof(char) * size);
}

参数传入NULL会有问题,因为对NULL解引用是未定义的。

char a[] = {‘a’, ‘b’, ‘c’}; sizeof(a)是多少为什么;

3
当sizeof的参数是数组名时,计算的是整个数组的存储大小;当sizeof的参数是指针时,计算的是指针的大小(8字节,64位系统)。
sizeof和strlen的区别_zj-CSDN博客

函数指针怎么做形参?

int add(int x, int y){
    return x+y;
}

int f(int x, int y, int(*fun)(int, int)){
    return fun(x, y);
}

如何减少递归函数的栈开销?

可以使用尾递归优化或者修改为循环

尾递归,递归优化_petershuang的博客-CSDN博客

程序调用自身的编程技巧称为递归( recursion)尾递归是一种特殊的递归,递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的

尾递归的优势
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得高很多。

函数一直递归会有什么影响?如果不传参,栈还会溢出吗?

对最近的面试遇到问题的查漏补缺_weixin_42067304的博客-CSDN博客

一直递归的话,可能会造成递归栈溢出

即使不传参,也可能会栈溢出。因为函数递归时,栈里保存了上一次递归的函数的状态,不仅仅是参数或者局部变量,哪怕没有参数或局部变量也会溢出。因为栈里至少保存了上一次递归函数的返回地址
在这里插入图片描述
优化方法:写成循环,或者写成尾递归(其实就是循环),让编译器进行尾递归的优化。

函数中死循环会怎么样?如果是多线程环境下某一个线程死循环了呢?

1、死循环会造成cpu资源的浪费
JAVA多线程之当一个线程在执行死循环时会影响另外一个线程吗? - hapjin - 博客园
2、如果是多线程,那么如果多个线程使用的是同一把锁,那么会导致其他的线程也无线循环。如果使用的不是同一把锁,那么不会影响其他线程

sizeof和strlen的区别

sizeof和strlen的区别_zj-CSDN博客

如果仅仅对于字符串来说,sizeof会考虑末尾的 \0,strlen则不会。

而更深层次的区别是:

  • sizeof是一个操作符,而strlen是库函数
  • sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为’\0’的字符串作参数
  • sizeof是在编译的时候就将结果计算出来了,是类型所占空间的字节数,所以以数组名做参数时计算的是整个数组的大小。而strlen是在运行的时候才开始计算结果,这是计算的结果不再是类型所占内存的大小,数组名就退化为指针了。
  • sizeof计算数据类型占内存的大小(所以必然会考虑\0),strlen计算字符串实际长度。
  • sizeof不能计算动态分配空间的大小(动态分配实际会得到一个指针,sizeof(指针) = 4)

STL

C++ map用什么实现?为什么不用哈希表?

map的底层是红黑树实现的。

因为map是有序的,红黑树方便排序。
unordered_map,是由哈希表实现的(不需要顺序)。

红黑树存储结构的存取是O(logn),而哈希表是O(1),当然这是在哈希表没有冲突的情况下的。而红黑树的内存占用要比哈希表高。

map容器怎么查找元素?如果map的key是string类型查找匹配的速度很慢怎么办?处理哈希冲突的方法

map.find(key), map.count(key),map.equal_range(key)

hashmap查找时候要算hash,这个最坏时间复杂度是O(M)(M是key字符串的长度),如果你的key非常非常非常非常非常非常……长,基于比较的map(红黑树map)通常只使用头几个字符进行比较,而hashmap(unordered_map)要O(M)地算出hash

处理哈希冲突:开放寻址法(线性探测、二次探测),链表法(红黑树、跳表代替链表)。

std::sort()底层原理

快速排序分段递归,当数据量小的时候是插入排序,当数据量大的时候,是堆排序;堆排序能改善递归层数过深的问题。

数据量大时采用快速排序 Quick Sort,分段递归排序。一旦分段后的数据量小于某个阈值,为避免Quick Sort的递归调用带来过大的额外开销,就改用插入排序 Insertion Sort。如果递归层次过深,还会改用堆排序 Heap Sort。

所以按数据量小到大分别是:插入排序,快速排序,堆排序。

堆排序和快速排序对比

一般来说快速排序要比堆排序性能好:

  • 堆排序数据访问的方式没有快速排序友好
    对于快速排序来说,数据是(局部)顺序访问的。而对于堆排序来说,数据是跳着访问(堆化的过程)的,对 CPU 缓存是不友好的。

  • 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序。
    快速排序数据交换的次数不会比逆序度多;但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。

最后上面一个问题也说了,当数据量特别大的时候,快速排序的递归调用可能额外的开销很大,这时候堆排序会更好一点

堆排序的应用

top-k问题,用于实现优先级队列,数据流的中位数问题,多路归并问题

哈希表长度为什么是质数

哈希表的大小取决于一组质数,原因是在hash函数中,要用这些质数来做模运算(%)。

分析发现,如果不是用质数来做模运算的话,很多生活中的数据分布,会集中在某些点上,哈希冲突的概率就比较高。所以采用了质数进行取模运算。

质数取模可以更好地避免哈希冲突。

vector a; sizeof(a) 是多少?

sizeof(vec)只取决于vector里面存放的数据类型,与元素个数无关。该值应该是与编译器相关的

我的笔记本上面是16(首尾迭代器,记录空间大小的变量,可能还需要记录cap?)。

map和unordered_map的区别

map:map内部实现是红黑树,具有自动排序的功能,因此map内部元素都是有序的,红黑树的每一个节点都代表着map的一个元素。map的查询、插入、删除操作的时间复杂度都是O(logn)。

unordered_map:unordered_map内部实现是哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1))。因此,其元素的排列顺序是无序的。

红黑树与平衡二叉树

红黑树是一种是一种不严格的平衡二叉查找树,是“近似平衡”的。

1、红黑树放弃了追求完全平衡,追求近似平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入删除最多只需要三次旋转就能达到平衡,实现起来也更为简单。红黑树的高度近似 log2n,插入、删除、查找操作的时间复杂度都是 O(logn)。更具体的:插入最多旋转2次,删除最多旋转3次。

2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知

哈希原理

散列函数(哈希函数):
散列函数计算得到的散列值是一个非负整数;
如果 key1 = key2,那 hash(key1) == hash(key2);
如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

散列冲突解决方法:

  1. 开放寻址法:线性探测,二次探测,双重散列
    线性探测:往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
    我们可以将删除的元素,特殊标记为 deleted。当线性探测查找的时候,遇到标记为 deleted 的空间,并不是停下来,而是继续往下探测。

  2. 链表法(链表可以使用红黑树或者跳表进行优化)

哈希开链过长如何解决?

使用红黑树或者跳表这样的数据结构(同上)

有了哈希表,为什么还需要红黑树?

第一,散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于红黑树来说,只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。

第二,散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树(红黑树)的性能非常稳定,时间复杂度稳定在 O(logn)

第三,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。

第四,散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

map线程安全吗?

c++ stl 没有支持多线程,所以多线程操作map的话很可能会出现程序执行异常的情况发生,通常会是段错误。

如果多个线程只读map,是安全的,但是涉及到写操作的话,是线程不安全的,可以使用互斥锁解决。

关键字

Volatile关键字的作用

防止在共享的空间发生读取的错误只保证其可见性,不保证原子性使用volatile指每次从内存中读取数据,而不是从编译器优化后的缓存中读取数据,简单来讲就是防止编译器优化

volatile 只作用在编译器上,但我们的代码最终是要运行在 CPU 上的。尽管编译器不会优化导致指令乱序,但 CPU 的乱序执行(out-of-order execution)已是几十年的老技术了。所以Volatile关键字不能保证原子性

谈谈 C/C++ 中的 volatile - 知乎

四种强制类型转换

c++类型转换_zj-CSDN博客
1.static_cast

类似于C风格的强制转换。无条件转换(只要不包含底层const,都可以进行强制类型转换),静态类型转换。

2.const_cast

去掉类型的const或volatile属性,把const类型的指针变为非const类型的指针

3.dynamic_cast

该操作符用于运行时检查该转换是否类型安全

4.reinterpret_cast

reinterpret即为重新解释,为运算对象的位模式提供较低层次上的重新解释,但是不改变其值

C++与C混用需要注意什么

Extern “C”是由C++ 提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extren “c”后,C++就能直接调用C函数了。

C和C++混合编译问题 - 784692237 - 博客园

extern关键字

修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。

也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:

1.使用头文件,然后声明它们,然后其他文件去包含头文件

2.在其他文件中直接extern

例子:C/C++中 extern 关键字详解_不很正派的专栏-CSDN博客

头文件:state.h 源文件:state.cpp
其它源文件:t1.cpp,t2.cpp,t3.cpp, 这些源文件都包含头文件state.h。
需要定义一个全局变量供这些源文件中使用:方法如下
1、在 state.h声明全局变量: extern int a;
2、在state.cpp中定义该全局变量:int a = 10;
这样其它源文件就可以使用该变量。

一个变量声明必须同时满足两个条件,否则就是定义:
(1)声明必须使用extern关键字;(2)不能给变量赋初值

extern int a; //声明
int a; //定义
int a = 0; //定义
extern int a =0; //定义

头文件中应使用extern 关键字声明全局变量(不定义),如果这个变量有多个文件用到,可以新建一个cpp,在其中定义,把这个cpp加入工程即可。头文件一般不要定义任何变量。

一般在头文件中申明,用extern, 在cpp中定义。 如果在头文件中定义,如果这个头文件被多个cpp引用,会造成重复定义的链接错误

指针和引用的区别?

primer书上有讲的很清楚

相同点:

  1. 都是地址的概念;
  2. 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
  3. 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

不同点:

  1. 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
  2. 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”
  3. 引用不能为空,指针可以为空;
  4. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
  5. 指针和引用的自增(++)运算意义不一样;
  6. 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
  7. 引用具有更好的可读性和实用性。

给c++添加个GC机制

GC基本算法及C++GC机制_melonstreet的专栏-CSDN博客

有向可达图与根集

垃圾收集器将存储器视为一张有向可达图。图中的节点可以分为两组:一组称为根节点,对应于不在堆中的位置,这些位置可以是寄存器、栈中的变量,或者是虚拟存储器中读写数据区域的全局变量;另外一组称为堆节点,对应于堆中一个分配块,如下图:

在这里插入图片描述
当存在一个根节点可到达某个堆节点时,我们称该堆节点是可达的,反之称为不可达。不可达堆节点为垃圾

可见垃圾收集的目标即是从从根集出发,寻找未被引用的堆节点,并将其释放

三种基本的垃圾收集算法及其改进算法

1、引用计数算法

引用技术算法是唯一一种不用用到根集概念的GC算法。其基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。这个思路可以参考C++ 引用计数技术及智能指针的简单实现 - melonstreet - 博客园

优点

  • 实现简单,在原生不支持GC的语言中也能容易实现出来
  • 这种垃圾收集机制是即时回收,也即是对象不再被引用的瞬间就立即被释放掉。

缺点

  • 若存在对象的循环引用,无法释放这些对象,例图:

在这里插入图片描述

  • 多个线程同时对引用计数进行增减时,引用计数的值可能会产生不一致的问题,必须使用并发控制机制解决这一问题,也是一个不小的开销。也即不是线程安全的
2、 Mark & Sweep 算法

这个算法也称为标记清除算法,为McCarthy独创。它也是目前公认的最有效的GC方案。

Mark&Sweep垃圾收集器由标记阶段回收阶段组成,标记阶段标记出根节点所有可达的对节点,清除阶段释放每个未被标记的已分配块。典型地,块头部中空闲的低位中的一位用来表示这个块是否已经被标记了

通过Mark&Sweep算法动态申请内存时,先按需分配内存,当内存不足以分配时,从寄存器或者程序栈上的引用出发,遍历上述的有向可达图并作标记(标记阶段),然后再遍历一次内存空间,把所有没有标记的对象释放(清除阶段)。因此在收集垃圾时需要中断正常程序,在程序涉及内存大、对象多的时候中断过程可能有点长。当然,收集器也可以作为一个独立线程不断地定时更新可达图和回收垃圾。该算法不像引用计数可对内存进行即时回收,但是它解决了引用计数的循环引用问题,因此有的语言把引用计数算法搭配Mark & Sweep 算法构成GC机制
在这里插入图片描述

3、 节点复制算法

Mark & Sweep算法的缺点是在分配大量对象时,且对象大都需要回收时,回收中断过程可能消耗很大。

而节点复制算法则刚好相反,当需要回收的对象越多时,它的开销很小,而当大部分对象都不需要回收时,其开销反而很大

算法的基本思路是这样的:从根节点开始,被引用的对象都会被复制到一个新的存储区域中,而剩下的对象则是不再被引用的,即为垃圾,留在原来的存储区域。释放内存时,直接把原来的存储区域释放掉,继续维护新的存储区域即可。过程如图:

在这里插入图片描述
可以看到,当被引用对象(非垃圾对象)很多时,需要复制很多的对象到新存储区域。

分代回收

以上三种基本算法各有各的优缺点,也各自有许多改进的方案。通过对这三种方式的融合,出现了一些更加高级的方式。

而高级GC技术中最重要的一种为分代回收

它的基本思路是这样的:程序中存在大量的这样的对象,它们被分配出来之后很快就会被释放,但如果一个对象分配后相当长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功

为了让GC变得更高效,我们应该对刚诞生不久的对象进行重点扫描,这样就可以回收大部分的垃圾。为了达到这个目的,我们需要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不同,可以划分为多个代。

一种回收的实现策略可以是:首先从根开始进行一次常规扫描,扫描过程中如果遇到老生代对象则不进行递归扫描,这样可大大减少扫描次数。这个过程可使用标记清除算法或者复制收集算法。然后,把扫描后残留下来的对象划分到老生代,若是采用标记清除算法,则应该在对象上设置某个标志位标志其年龄;若是采用复制收集,则只需要把新的存储区域内对象设置为老生代就可以了。而实际的实现上,分代回收算法的方案五花八门,常常会融合几种基本算法。

C++垃圾回收机制

C语言本身没有提供GC机制,而C++ 0x则提供了基于引用计数算法的智能指针进行内存管理。也有一些不作为C++标准的垃圾回收库,如著名的Boehm库。借助其他的算法也可以实现C/C++的GC机制,如前面所说的标记清除算法。

在这里插入图片描述
当应用程序使用malloc试图从堆上获得内存块时,通常都是以常规方式来调用malloc,而当malloc找不到合适空闲块的时候,它就会去调用垃圾收集器,以回收垃圾到空闲链表。此时,垃圾收集器将识别出垃圾块,并通过free函数将它们返回给堆。这样看来,垃圾收集器代替我们调用了free函数,从而让我们显式分配,而无须显式释放

上图中的垃圾收集器为一个保守的垃圾收集器。保守的定义是:每个可达的块都能够正确地被标记为可达,而一些不可达块却可能被错误地标记为可达。其根本原因在于C/C++语言不会用任何类型信息来标记存储器的位置,即对于一个整数类型来说,语言本身没有一种显式的方法来判断它是一个整数还是一个指针。因此,如果某个整数值所代表的地址恰好的某个不可达块中某个字的地址,那么这个不可达块就会被标记为可达。所以,C/C++所实现的垃圾收集器都不是精确的,存在着回收不干净的现象。而像JAVA的垃圾收集器则是精确回收。在《关于C++ 0x 里垃圾收集器的讲座》这篇文章里提到,C++标准提案中使用gc_strict、 gc_relax这样的关键字来描述一个内存区内有没有指针,但无法精确到每个数据上。实际上,早在07年,一份C++标准提案N2670就提出要将垃圾回收机制作为加入C++,最后提案是没有通过,其原因大概是因为实现复杂,由于语言本身原因存在这样那样的限制。所以在C++ 0x中除了shard_ptr、weak_ptr这些智能指针外,我们并没看看到GC机制的身影。而至于C++是如何解决引用计数的循环引用问题以及并发控制问题,我们将以另外一篇文章进行介绍。

如何给C++设计一个GC - lzprgmr - 博客园

如何给C++设计一个GC?

基本套路与Java应该是一致的,也就是Mark - Sweep - Compact:

C++中可分为两种类型:一是用户自定义类型;一种是内置的类型。

每次分配内存时候,都把内存地址保存到一个hashmap中,key为内存地址,value为false。这是准备工作,然后在某个时间点需要做垃圾回收的时候:

1、寻找以下三种用户自定义类型的对象:全局的静态的当前栈上的,把这些对象作为,然后递归寻找他们所引用的内存(成员), 并在hashmap中把这些内存地址为key的项的值设为true,表示不应回收。(Mark)

2、遍历hashmap,把value为false的项全部delete,并从hashmap中删除 (Sweep)

3、把hashmap中存在的内存对象,全部移动到一块集中的区域以减少内存碎片,同时注意修改这些对象被引用的地方,因为地址改变了。(Compact)
这种方法的话,也自动解决了循环引用导致的内存泄露问题。

【问题】

一、如何找到全局的;静态的;当前栈上的用户自定义对象?

二、如何拿到一个对象所引用的所以对象?

三、引用的对象地址改了,如何得到通知?

模板类中包含模板类友元和其他友元函数

模板类中包含模板类友元和其他友元函数_wy11933的博客-CSDN博客

将含友元的类称为主类,将要做友元类的模板类称为客类

1、如果想将客类的所有实例作为主类每个实例的友元,那么不需要再主类前声明客类模板,也不需要声明友元时加,并且在主类内部声明客类为主类友元之前要加上客类的模板参数列表,而且还不能和主类参数名一样,如:

template<typename T> class 主类
{
    template <typename X> //模板参数和主类不一样
    friend 客类; 
};

2、如果只想将拥有和主类相同实例的客类或模板函数作为主类的友元,那就先在主类前声明客类(带模板参数列表template), 然后在主类中声明客类friend(带模板参数,不带template)并且模板参数需要和主类的参数名相同。

template<typename > class 客类;
template<typename T> class 主类
{
    friend 客类<T>; //模板参数和主类一致
};

C++RAII机制

什么是RAII?

RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。

如何使用RAII?

当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了

文件描述符和文件指针的区别

文件描述符和文件指针_Payshent的博客-CSDN博客
文件描述符:在linux系统中打开文件就会获得文件描述符,它是一个很小的整数每个进程控制块(PCB)中保存着一份文件描述符表,文件描述符就是文件描述符表的索引,每个表项都有一个指向打开文件的文件指针,这个文件指针指向进程用户区中的一个被称为FILE的数据结构*。FILE结构包含一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某些意义上来说,文件指针就是文件描述符的句柄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值