面试题之C++理论

目录

三类总结

一、基础知识类

1、static与const的区别

2、指针常量与常量指针的区别

3、联合体、结构体、类

4、C++的对象特性

5、智能指针

6、C++垃圾回收算法

7、内存泄漏

8、内存溢出

9、指针和引用的区别

10、什么是野指针,怎么产生

11、右值引用

MOVE语义

12、Lambda表达式

13、const与#define

14、内存的分配方式有几种?

 15、i++和++i的区别

二、具体开发类、对象知识类: 多态、虚函数、虚表、虚指针、模板类、模板函数、new、delete、malloc、free、类实例化调用步骤

1、多态,虚函数,纯虚函数

2、模板函数

3、类模板、模板类

4、虚表,虚指针,虚继承,虚类

5、类实例化调用步骤

6、new、delete、malloc、free关系

7、构造函数和析构函数能不能定义为虚函数

三、高级开发类:设计模式

1、设计模式的种类


三类总结

一、基础知识类:static、const、#define、指针、左值、右值、野指针、内存泄漏、内存溢出、i++、++i、内存分配方式、内敛函数等

二、具体开发类、对象类: 多态、虚函数、虚表、虚指针、模板类、模板函数、new、delete、malloc、free、类实例化调用步骤、

三、高级开发类:设计模式

一、基础知识类

1、static与const的区别

static

  1. static局部变量 将一个变量声明为函数的局部变量,那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中
  2. static 全局变量 表示一个变量在当前文件的全局内可访问,在其他.c文件内就会被隐藏
  3. static 函数 表示一个函数只能在当前文件中被访问,在其他.c文件内就会被隐藏,
  4. static 类成员变量 表示这个成员为全类所共有
  5. static 类成员函数 表示这个函数为全类所共有,而且只能访问静态成员变量

const

  1. const 常量:定义时就初始化,以后不能更改。
  2. const 形参:func(const int a){};该形参在函数里不能改变
  3. const修饰类成员函数:该函数对成员变量只能进行只读操作
  4. const修饰函数的参数,这个参数在这个函数不能被修改,只能读

原理:

static关键字的作用:

(1)函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值; 
(2)在模块内的static全局变量和函数可以被模块内的函数访问,但不能被模块外其它函数访问; 
(3)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝; 
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

const关键字的作用:

(1)阻止一个变量被改变 
(2)声明常量指针和指针常量 
(3)const修饰形参,表明它是一个输入参数,在函数内部不能改变其值; 
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量; 
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。

2、指针常量与常量指针的区别

1、指针常量 指针是一个常量,指针的内存地址是一个常量不能改变,但是内存地址对应的内容是可以通过指针改变的

2、常量指针 指向常量内容的一个指针,指向的内容不能改变,但是指针的内存空间可以改变,改变指针内存地址从而改变指针指向其他常量

3、联合体、结构体、类

区别:类成员默认是protect的,结构体和联合体的成员是公开的public

相同点:结构体和类的每个成员都有属于自己的空间

联合体只会分配一次内存

4、C++的对象特性

封装,继承和多态。

5、智能指针

参考自:智能指针学习

智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放

智能指针

一、为什么要使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

另一句话:智能指针就是偏向于类的设计,弱化指针的功能,自动管理内存,避免内存泄漏

1、auto_ptr c++ 98,在C++ 11不被建议使用

auto_ptr< string > str(new string(“我要成为大牛~ 变得很牛逼!”)); auto_ptr<vector< int >> av(new vector< int >()); auto_ptr< int > array(new int[10]);

智能指针的三个常用函数:

get() 获取智能指针托管的指针地址

release() 取消智能指针对动态内存的托管

reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

2、unique_ptr

特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源

  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。

  4. 在容器中保存指针是安全的

#include<bits/stdc++.h>
using namespace std;
class Test {
public:
    Test() { cout << "Test的构造函数..." << endl; }
    ~Test() { cout << "Test的析构函数..." << endl; }
​
    int getDebug() { return this->debug; }
​
private:
    int debug = 20;
};
​
int main()
{
​
    unique_ptr<Test> p1(new Test());
​
}

auto_ptr 与 unique_ptr智能指针的内存管理陷阱

auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str);  // p1托管str指针
{
    auto_ptr<string> p2;
    p2.reset(str);  // p2接管str指针时,会先取消p1的托管,然后再对str的托管
}
​
// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;
​

为了解决这样的问题,我们可以使用shared_ptr指针指针!

3、shared_ptr

shared_ptr可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!

调用use_count函数可以获得当前托管指针的引用计数。

shared_ptr使用陷阱:shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!

4、weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。

6、C++垃圾回收算法

基本概念

有向可达图与根集

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

è¿éåå¾çæè¿°

当存在一个根节点可到达某个堆节点时,我们称该堆节点是可达的,反之称为不可达。不可达堆节点为垃圾。可见垃圾收集的目标即是从从根集出发,寻找未被引用的堆节点,并将其释放。

1、引用计数算法

其基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。

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

2、Mark & Sweep 算法  标记清除算法

由标记阶段和回收阶段组成,标记阶段标记出根节点所有可达的对节点,清除阶段释放每个未被标记的已分配块。

因此在收集垃圾时需要中断正常程序,在程序涉及内存大、对象多的时候中断过程可能有点长

è¿éåå¾çæè¿°

3、节点复制算法

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

è¿éåå¾çæè¿°

4、分代回收

以上三种基本算法各有各的优缺点,也各自有许多改进的方案。通过对这三种方式的融合,出现了一些更加高级的方式。而高级GC技术中最重要的一种为分代回收。它的基本思路是这样的:程序中存在大量的这样的对象,它们被分配出来之后很快就会被释放,但如果一个对象分配后相当长的一段时间内都没有被回收,那么极有可能它的生命周期很长,尝试收集它是无用功。为了让GC变得更高效,我们应该对刚诞生不久的对象进行重点扫描,这样就可以回收大部分的垃圾。为了达到这个目的,我们需要依据对象的”年龄“进行分代,刚刚生成不久的对象划分为新生代,而存在时间长的对象划分为老生代,根据实现方式的不同,可以划分为多个代。

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

而其他的改进算法数量非常庞大,但大都基于上述的三种基本算法。

C++垃圾回收机制

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

这里写图片描述

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

上图中的垃圾收集器为一个保守的垃圾收集器。保守的定义是:每个可达的块都能够正确地被标记为可达,而一些不可达块却可能被错误地标记为可达。其根本原因在于C/C++语言不会用任何类型信息来标记存储器的位置,即对于一个整数类型来说,语言本身没有一种显式的方法来判断它是一个整数还是一个指针。因此,如果某个整数值所代表的地址恰好的某个不可达块中某个字的地址,那么这个不可达块就会被标记为可达。所以,C/C++所实现的垃圾收集器都不是精确的,存在着回收不干净的现象。而像JAVA的垃圾收集器则是精确回收。

7、内存泄漏

在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。

常见的内存泄露造成的原因

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

非静态内部类创建静态实例造成的内存泄漏

资源未关闭造成的内存泄漏

8、内存溢出

应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出

内存越界、

9、指针和引用的区别

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

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

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

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

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

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

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

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

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

10、什么是野指针,怎么产生

野指针产生原因:
1.指针变量未初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
2.指针释放后之后未置空
有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。
3.指针操作超越变量作用域
不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

11、右值引用

你可以对某个值进行取地址运算,如果不能得到地址,那么可以认为这是个右值

什么是左值引用呢?
左值引用,就是绑定到左值的引用,通过&来获得左值引用
那么,什么是左值呢?
左值,就是在内存有确定存储地址、有变量名,表达式结束依然存在的值。

顾名思义,什么是右值引用呢?
右值引用,就是绑定到右值的引用,通过&&来获得右值引用
那么,什么又是右值呢?
右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。

引入右值引用的原因

(1)替代需要销毁对象的拷贝,提高效率:某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象,这样就减少了内存和运算资源的使用,从而提高了运行效率;

(2)移动含有不能共享资源的类对象:像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。

MOVE语义

可以将左值转化为右值引用,move语义就是把旧指针的值复制到新指针,并把旧指针的值赋为NULL解决产生多余临时变量的问题

用法:下列代码的str我用int代替了下,str没有销毁,说明右值只对对象有用,string底层也是一个封装的对象

#include<bits/stdc++.h>
using namespace std;
int main()
{
    string str = "Hello";
    vector<string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    cout << "After copy, str is \"" << str << "\"\n";
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(move(str));
    cout << "After move, str is \"" << str << "\"\n";
    cout << "The contents of the vector are \"" << v[0]<< "\", \"" << v[1] << "\"\n";
}

12、Lambda表达式

创建一个匿名函数

13、const与#define

请说出const与#define 相比,有何优点?

答案:

const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。

2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

14、内存的分配方式有几种?

【参考答案】

一、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。举例:全局变量,static定义的变量。

二、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。举例:函数内的局部变量

三、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

一般问完内存的分配方式,还会问内存分配的顺序,如下

一、在程序执行期间,变量存储空间有三种:
1、静态存储区。内存在程序编译的时候就已经分配好了,这块内存在程序执行期间都存在,
存储全局变量和静态变量。
2、栈存储区。内存是在程序执行期间才分配的,函数内局部变量及函数参数的存储单元,当
函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率
高但容量小。
3、堆存储区。在程序执行时由程序员用malloc或new申请的内存,程序员自己负责何时用
free或delete释放分配的内存。频繁的分配和释放不同大小的堆内存将会产生堆内碎片。
 

 15、i++和++i的区别

在程序开发中,++i 与 i++的区别在哪里? - 知乎

二、具体开发类、对象知识类: 多态、虚函数、虚表、虚指针、模板类、模板函数、new、delete、malloc、free、类实例化调用步骤

1、多态,虚函数,纯虚函数

多态

其实就是一种接口,多种实现方式,目的使得接口重用

多态分为静态多态和动态多态,静态多态包含函数重载、泛型编程。动态多态则是虚函数。

虚函数

1、virtual修饰的成员函数

2、允许基类指针访问调用派生类与基类相同的函数名

3、实现动态多态性

纯虚函数

  1. 没有函数体,virtual定义的时候需要在后面写上=0
  2. 包含纯虚函数的为抽象类,抽象类无法实例化对象
  3. 对于抽象类来说,它无法实例化对象,而对于抽象类的子类来说,只有把抽象类中的纯虚函数全部实现之后,那么这个子类才可以实例化对象

2、模板函数

用template<typename>T修饰的函数

例如T run(T x, T y){}

当函数被调用时会根据实参的类型来替换模板中的虚拟类型,进而实现不同函数的功能。

3、类模板、模板类

类模板

template<class T>修饰

模板类

模板类是类模板实例化后的一个产物

4、虚表,虚指针,虚继承,虚类

虚表

一个存放虚函数指针函数指针数组 ,虚函数表的指针存在于对象实例中最前面的位置

虚指针

虚指针又叫虚函数指针是一个虚函数的实现细节,带有虚函数的类中每一个对象都有一个虚指针指向该类的虚函数表 

虚指针是通过“类的对象”来体现:

(1)同一个类,创造的不同对象,其虚指针的值是一样的,全都是指向该类的虚函数表(函数指针数组的首地址);
(2)不同的类创建的对象,无论其基类的虚函数是否被重写,其虚指针的值(与其基类创建的对象相比)是不同的;

虚继承

解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两问题

  1. 浪费空间
  2. 存在二义性,通常可以将派生类的地址赋值给基类对象,但是多重继承可能存在一个基类多份拷贝
  3. 实现,虚继承一般通过虚基类指针和虚基表实现,每个虚继承的子类都有一个虚基类指针和虚基类表  (只选择第一个出现的成员就可以了,剔除掉以后所有的,解决二义性)

虚类

含有虚函数的类称为虚类

5、类实例化调用步骤

  • 父类静态代变量
  • 父类静态代码块
  • 子类静态变量
  • 子类静态代码块
  • 父类非静态变量(父类实例成员变量)
  • 父类构造函数
  • 子类非静态变量(子类实例成员变量)
  • 子类构造函数

虚类

含有虚函数的类称为虚类,

6、new、delete、malloc、free关系

New/delete是C++关键字,需要编译器的支持,malloc、free是库函数需要头文件的支持。

1、参数,new/delete无需指定内存块的大小,malloc需要显示的指定所需内存大小

2、返回类型,new返回的是对象类型的指针,而malloc返回的是是void*类型,需要强制转换成我们所需要的类型

3、分配失败New分配失败返回异常、malloc则返回NULL

4、自定义类型

(1 new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。

(2 然后调用类型的构造函数

(3 初始化成员变量,最后返回自定义类型指针。

delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

析构函数来完成一些清理工作,在析构函数中调用 delete 语句,就能确保对象运行中用 new 运算符分配的空间在对象消亡时被释放

5、内存区域

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

6.重载

New/delete允许重载,malloc、free不可以

7、构造函数和析构函数能不能定义为虚函数

构造函数不可以,析构函数可以

  1. 构造函数调用时需要明确对象的类型
  2. 虚函数执行依赖于虚函数表,而虚函数表是在构造函数里初始化的
  3. 析构函数可以定义为虚函数,因为在释放基类内存时,需要将子类的内存也释放掉,于是需要调用子类的析构函数,这里需要虚函数的定义

三、高级开发类:设计模式

1、设计模式的种类

待学

おすすめ

転載: blog.csdn.net/qq_41286356/article/details/118070541