c++面试题

1.static的使用

1)修饰局部变量:在函数内部使用static修饰局部变量,会使它成为静态局部变量。静态局部变量只会被初始化一次,且只有在第一次调用该函数时才会被初始化,之后每次调用该函数时都会保留上一次的值.从原来的栈区存放改为全局区。

2)修饰全局变量:在函数外部使用static修饰全局变量,会使它成为静态全局变量。静态全局变量的作用域仅限于当前文件,即只能在当前文件内被访问,不能被其他文件访问。

3)修饰成员变量:在类中使用static修饰成员变量,会使它成为静态成员变量。静态成员变量的生命周期与程序的生命周期相同,即它只会被初始化一次,且存在于所有类对象之外。静态成员变量在类内部声明,在类外部需要进行定义和初始化。

4)修饰成员函数:在类中使用static修饰成员函数,会使它成为静态成员函数。静态成员函数不依赖于任何类对象,只能访问类的静态成员变量和静态成员函数,不能访问类的非静态成员变量和非静态成员函数。静态成员函数通过类名来访问。

2.const的使用

1)修饰变量:变量的值不能改变

2)修饰指针:左定值(常量指针)右定向(指针常量)

3)修饰成员变量:const修饰成员变量,只在某个对象的生命周期中是常量,因为类可以创建多个对象,不同的对象其const数据成员值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象还没有创建时,编译器还不知道const数据成员值是多少。所以只能在类的构造函数的初始化列表中初始化。

4)修饰成员函数:可以使用类中的所有成员变量,但是不能改变他们的值。

3.指针常量,常量指针,常指针常量

1)指针常量:可以改变指针指向的值,但是不能改变指针的指向

2)常量指针:可以改变指针的指向,但是不能改变指针指向的值

3)常指针常量:指针的指向和指向的值都不能改变

4.指针和引用的异同

相同点:都可以对变量进行改变

不同点:

1)指针本质是一个变量,需要分配内存,引用是给一个对象起别名,不需要分配内存

2)引用在定义时必须初始化,而指针可以不用初始化

3)指针初始化可以为空,但是引用必须是一个已有对象的引用

4)指针和引用的自增运算结果不同。指针是指向下一个地址,而引用是引用的变量值+1

5)Sizeof不同,指针得到的是指针的大小,引用得到的是引用变量的大小。

5.如何理解多态

定义:同一操作作用于不同的对象,产生不同的执行结果。C++多态意味着当调用虚成员函数时,会根据调用类型对象的实际类型执行不同的操作。

在日常生活中的多态例子:买票时,成人买票全价,如果是学生那么半价,如果是军人,就可以优先买票。不同的人买票会有不同的实现方法,这就是多态。

c++的多态分为两种:

1. 编译时多态:重载

2. 运行时多态:重写即虚函数。虚函数本身其实就是回调函数

C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写

实现:通过虚函数实现,用virtual声明的成员函数就是虚函数,允许子类重写。声明基类的指针或者引用指向不同的子类对象,调用相应的虚函数,可以根据指针或引用指向的子类的不同从而执行不同的操作。

        Overload(重载):函数名相同,参数类型或顺序不同的函数构成重载。

        Override(重写):派生类覆盖基类用virtual声明的成员函数。

        Overwrite(隐藏):派生类的函数屏蔽了与其同名的基类函数。派生类的函数与基类函数同名,但是参数不同,隐藏基类函数。如果参数相同,但是基类没有virtual关键字,基类函数将被隐藏。

6.虚函数表

       1)带有虚函数的类,编译器会为其分配一个虚函数表(用数组实现),里面记录了虚函数的地址,当此类被继承时,若子类重写了虚函数,则在子类的虚函数表中覆盖父类的虚函数地址,否则继承父类的虚函数地址。

        2)实例化后,对象有一个虚函数指针,虚函数指针指向虚函数表,在程序运行时,通过虚函数指针找到虚函数表中对应的函数地址,调用虚函数

7.常用数据结构

vector:向量,连续存储,可随机访问。
deque:双向队列,连续存储,随机访问。
list:链表,内存不连续,不支持随机访问。
stack:栈,不可随机访问,只允许再开头增加/删除元素。
queue:单向队列,尾部增加,开头删除。不支持随机访问
set:集合,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
map:图,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
hash_set:哈希表,随机访问。查找、插入、删除时间复杂读为O(1)。

8.TCP和UDP通信方式

TCP

服务器端(被动连接,需创建自己的地址信息)

1.创建一个套接字 --socket()

2.绑定IP地址、端口等信息到socket()上 --bind

3.设置套接字监听--listen()

4.等待客户端的连接请求 --accept()(当没有连接到来时会阻塞)

5.发送、接收数据 --send()和recv(),或者read()、和write()

6.关机网络连接 --close()

客户端

1.创建一个套接字 --socket()

2.连接服务器 --connect()

3.接收、发送数据--send()和recv(),或者read()和write()

4.关闭网络连接 --close()

当接收/发送缓冲区满了之后,send()和recv(),或者read()和write()也会阻塞

TCP多线程通信:父线程监听(accept),子线程通讯,实现一对多

多线程还要考虑是否要进行线程同步,及多个线程是否访问共享资源(在多线程中,全局区和堆区是共享的,栈区不共享)

服务器并发 | 爱编程的大丙 (subingwen.cn)

UDP

1.创建套接字

2.如果准备接收数据,需要绑定端口

3.发送数据sendto(),接收数据recvfrom()

4.关闭套接字

9.C和C++的区别

C语言是面向过程的,C++是C的超集,继承并扩展了C语言,是面向对象的

10. struct 和class 有什么区别?

1)struct的成员默认是public属性的,class的成员默认是private属性的

2)struct继承默认是public属性的,class继承默认是private属性的

3)“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数

11.extern "C"的作用

extern "C" 的主要作用就是为了能够正确实现C++代码调用其它C语言代码。加上extern "C"后,会提示编译器这部分代码按C语言(而不是C++)的方式进行编译。

12.C++重载、重写、多态

重载:位于同一个类中,函数名字相同,形参列表不同

重写:被重写的函数必须是virtual,一般用于子类在继承父类是,重写父类中的方法,函数名称和参数列表都相同

多态:要有继承,要有虚函数重写,父类指针或引用指向子类对象。

13.多态的理解

多态:就是多种形态,C++的多态分为静态多态和动态多态。静态多态就是重载,因为在编译器决议确定,所以成为静态多态。动态多态即运行时多态是通过继承重写基类的虚函数实现的多态,因为在运行时决议确定,所以称为动态多态,也叫运行时多态。

14.对虚函数机制的理解,单继承、多继承、虚继承条件下虚函数表的结构

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。将基类虚表中的内容拷贝一份到子类虚表中,如果派生类重写了基类某个虚函数,就用派生类自己的虚函数替换掉原先基类虚函数的入口地址。

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。多重继承会有多个虚函数表,几重继承就会有几个虚函数表。这些表按照派生的顺序一次排列,如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖虚函数表相应的位置,如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。

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

15.如果虚函数是有效的,那为什么不把所有函数设为虚函数?

虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候会产生一定的系统开销,这是没有必要的。另外,虚函数的调用相对于普通函数要更慢一些,因为每次都要查找虚函数表,有一定的时间开销。

16.为什么要虚继承?

非虚继承时,显然D会继承两次A ,内部就会存储两份A的数据,浪费空间,而且还会有二义性,D调用A的方法时,由于有两个A,究竟调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚继承下,只有一个共享的基类对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类,在虚继承下,基类对象的复制以及由此引起的二义性被消除了。

17.构造函数为什么一般不定义为虚函数、析构函数一般定义为虚函数

1.构造函数不可以定义为虚函数:虚函数的调用需要通过虚函数指针指向虚函数表,虽然虚函数表在编译时就有,但虚函数指针在创建对象之后才有,因此无法调用虚构造函数

2.析构函数一般定义为虚函数:在动态多态时,子类重写父类的虚析构函数,父类指针或引用指向子类,在析构指向子类的父类指针时,根据虚函数表找到子类的析构函数,从而正确释放子类对象的资源。

18.为什么虚函数表中有两个析构函数

这是因为对象有两种构造方式,栈构造和堆构造,所以在对应的实现上,对象也有两种析构方法,其中堆上的对象和栈上的对象的析构不同之处在于,栈内存的析构不需要执行delete函数,会自动被回收。

19.为什么基类的析构函数要是虚函数

在析构指向子类的父类指针时,根据虚函数表找到子类的析构函数,从而正确释放子类对象的资源

20. volatile 关键字的作用?什么时候需要使用volatile 关键字

volatile关键字告诉编译器其修饰的变量是易变的,它会确保修饰的变量每次读操作都从内存里读取,每次写操作都将值写到内存里。volatile关键字就是给编译器做个提示,告诉编译器不要对修饰的变量做过度的优化,提示编译器该变量的值可能会以其它形式被改变。

volatile用于读写操作不可以被优化掉的内存,用于特种内存中。

(volatile 意思是易变的,是一种类型修饰符,在C/C++中用来阻止编译器因误认某段代码无法被代码本身所改变,而造成的过度优化。编译器每次读取 volatile 定义的变量时,都从内存地址处重新取值。

这里就有点疑问了,难道编译器取变量的值不是从内存处取吗?

并不全是,编译器有时候会从寄存器处取变量的值,而不是每次都从内存中取。因为编译器认为变量并没有变化,所以认为寄存器里的值是最新的,另外,通常来说,访问寄存器比访问内存要快很多,编译器通常为了效率,可能会读取寄存器中的变量。但是,变量在内存中的值可能会被其它元素修改,比如:硬件或其它线程等。)

21.各种强制类型转换的原理及使用

C++中有四个强制类型转换的关键字

1.static_cast:用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。

用法:static_cast <类型说明符> (变量或表达式)

int a = 10;
int b = 3;
double result = static_cast<double>(a) / static_cast<double>(b);

它主要有如下几种用法:
    (1)用于类层次结构中基类和派生类之间指针或引用的转换
      进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
      进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的
    (2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证
    (3)把空指针转换成目标类型的空指针
    (4)把任何类型的表达式转换为void类型
    注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。

static_cast:可以实现C++中内置基本数据类型之间的相互转换。

2.const_cast:用于去除指向常量对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用

用法:const_cast<type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
    常量指针被转化成非常量指针,并且仍然指向原来的对象;
    常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

    const int a = 10;
     
    const int * p = &a;
 
    int *q;
 
    q = const_cast<int *>(p);
 
    *q = 20;    //fine

3.reinterpret_cast:reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型

用法:reinterpret_cast<type_id> (expression)

 type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
    它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。

4.dynamic_cast:

用法:dynamic_cast<type_id> (expression)

(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。

(2)不能用于内置的基本数据类型的强制转换。

(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。

dynamic_cast用于类继承层次间的指针或引用转换。主要还是用于执行“安全的向下转型(safe downcasting)”,

也即是基类对象的指针或引用转换为同一继承层次的其他指针或引用。

至于“向上转型”(即派生类指针或引用类型转换为其基类类型),本身就是安全的,尽管可以使用dynamic_cast进行转换,但这是没必要的, 普通的转换已经可以达到目的,毕竟使用dynamic_cast是需要开销的

22.什么时候用指针,什么时候用引用

对于那些函数,它们只使用传递过来的值,而不对值进行修改

1)如果数据对象很小,如内置数据类型或小型结构,使用按值传递

2)如果数据对象是数组,则使用指向const的指针

3)如果数据对象是较大的结构,则使用const指针或者const引用,以提高程序的效率

4)如果数据对象是类对象,则使用const引用

对于那些函数,它们需要修改传递过来的值

1)如果数据对象很小,如内置数据类型或小型结构,则使用指针

2)如果数据对象是数组,则只能使用指针

3)如果数据对象是较大的结构,则使用指针或者引用,以提高程序的效率

4)如果数据对象是类对象,则使用引用

23.一般什么情况下会出现内存泄漏?怎么用C++在编码层面尽量避免内存泄漏。

内存泄漏是指程序向系统申请分配内存使用(new),用完却没有归还(delete),结果申请的那块内存程序不能使用,而系统也无法再将它分配给需要的程序。

如何避免:

1.将基类的析构函数设为虚函数

2.new和delete配对使用

3.使用智能指针

24.对面向对象的理解

C++面向对象的三大特性是封装、继承、多态。

C++面向对象编程就是把一切事物都变成一个个对象,用属性和方法来描述对象的信息,比如定义一个猫对象,猫的眼睛、毛发、嘴巴就可以定义为猫对象的属性,猫的叫声和走路就可以定义为猫对象的方法。

25.内存分区

代码区:存放函数体的二进制代码,由操作系统进行管理

全局区(静态区):存放全局变量,静态变量和常量

栈区:存放函数参数、局部变量等

堆区:由程序员进行手动分配和释放,若程序员不释放,在程序结束时有操作系统进行回收。

26.纯虚函数

纯虚函数是只有声明没有实现的虚函数,包含纯虚函数的类是抽象类,不能被实例化,只有实现了这个纯虚函数的子类才能生成对象。

27.深拷贝和浅拷贝

浅拷贝是对对象指针的复制,原对象和副本指向的是相同资源

深拷贝是开辟一片新的内存空间,将原对象的资源复制到新的空间,返回新空间的地址

深拷贝可以避免重复释放和写冲突。

28.悬挂指针和野指针、空指针、void指针

悬挂指针:当指针所指向的对象被释放后,指针的指向没有改变,还是指向被回收的内存地址

野指针:未初始化的指针

空指针:使用null或者nullptr初始化,不指向任何对象

Void指针:一种特殊类型的指针,可以存放任意对象的地址,任意类型的指针可以指向void指针,反之不行,void指针需要强制类型转换才能赋值给其他指针

29.new和malloc有什么区别

1.new和delete是c++的关键字/运算符,malloc和free是c++/c语言的标准函数

2.malloc需要显示地指定分配的内存大小,new不需要

3.new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上分配内存。注:凡是通过new操作符进行内存申请,该内存即为自由存储区

4.new操作符内存分配成功时,返回对象类型,无需进行类型转换,故new是符合类型安全的操作符,malloc返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型

5.new操作符内存分配失败时,抛出bad_alloc异常,malloc内存分配失败时返回NULL

6.new操作符有构造和析构函数,在开辟空间的同时,会调用自定义对象的构造函数来完成初始化,malloc只会开辟空间

7.new相对malloc效率要低,因为new的底层封装了malloc

30.malloc的内存可以用delete释放吗

可以,但是一般不这么用。malloc和free是C语言中的函数,C++为了兼容C语言保留下来这一对函数。简单来说,new可以理解为,先执行malloc来申请内存,后调用构造函数来初始化对象,delete是先执行析构函数,后使用free来释放内存。若先new再使用free来释放空间的话,可能会出现一些错误。而先使用malloc,再使用delete的话没有问题。

31.malloc出来20字节内存,为什么free不需要传入20呢,不会产生内存泄漏吗?

因为不能保证程序员使用free时传入的参数是和malloc一致的,从而导致内存泄漏等问题。现在free的解决方式是让free函数自己确定要释放多少内存,可以使用的方式是在申请内存时多申请一些空间来存储内存大小,在free时再获取这个大小进行释放。

32.new[]和delete[]一定要配对使用吗?new[]和delete[]为何要配对使用?

1.不一定,当类型为int、float等内置类型时,可以不配对使用,但是建议还是配对使用。

2.new[]为一个数组申请内存时,编译器还会悄悄地在内存中保存一个整数,用来表示数组中元素的个数。因为在delete一块内存时,我们不仅要知道指针指向多大的内存,更重要的是要知道指针指向的数组中对象的个数。因为只有知道了对象数量才能一一调用它们的析构函数,完成对数组中所有对象的清理。如果使用的是delete,则编译器只会将指针所指的对象当作单个对象来处理。所以对于数组,需要使用delete[]来处理,符号[]会告诉编译器在delete这块内存时,先去获取保存的那个元素数量值,然后再进行一一清理。

33.基类的析构函数一般写成虚函数的原因

在析构指向子类的父类指针时,根据虚函数表找到子类的析构函数,从而正确释放子类对象的资源

34.什么情况下会调用拷贝构造函数

1)一个对象以值传递的方式传入函数体

2)一个对象以值传递的方式从函数返回

3)一个对象需要另一个对象进行初始化

35.为什么拷贝构造函数必须是引用传递而不是值传递

当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个复本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参; 而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导致又一次调用类A的拷贝构造函数,这就是一个无限递归。

36.指针传递和引用传递的区别

指针传递的是变量“地址”,引用传的是变量“别名”。区别就是指针传递的时候是新声明了一个指针(指向相同地址)然后传进来,而引用则是将变量本身传进来(只不过换了个名字)。都可以对原值进行访问和修改,无需创建副本。

37.成员初始化列表的概念,为什么使用成员初始化列表会快一些

成员初始化列表就是在类或者构造函数中,在参数列表后以冒号为开头,逗号为分隔的一系列初始化字段

快:因为使用成员初始化列表进行初始化的话,会直接调用传入参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程。

38.静态链接和动态链接

静态链接:本质是将多个文件打包成一个文件;过程就是将库中被调用的代码复制到调用模块中。

优点:

1.链接静态库生成的可执行程序,执行速度更快

2.可执行程序的执行不依赖库的存在

缺点:

1.链接静态库生成的可执行程序,文件体积相对较大

2.更新困难,维护成本高

动态链接:把调用的函数所在的文件模块和调用函数所在文件的位置等信息链接进目标程序,程序在运行时再从文件模块中寻找相应的代码。

优点:

1.链接动态库生成的可执行程序体积更小,节省空间

2.易于链接,便于更新维护

缺点:

1.链接动态库生成的可执行程序执行速度相对较慢

2.可执行程序的执行依赖库文件的存在

39.虚函数的实现原理

1)带有虚函数的类,编译器会为其分配一个虚函数表(用数组实现),里面记录了虚函数的地址,当此类被继承时,若子类重写了虚函数,则在子类的虚函数表中覆盖父类的虚函数地址,否则继承父类的虚函数地址。

2)实例化后,对象有一个虚函数指针,虚函数指针指向虚函数表,在程序运行时,通过虚函数指针找到虚函数表中对应的函数地址,调用虚函数

40.指针函数和函数指针

 指针函数:就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。

int *fun(int x,int y);

 函数指针:其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向函数的指针。

int (*fun)(int x,int y);

41.内存对齐

为什么要进行内存对齐?比如结构体的成员可以有不同的数据类型,所占的大小也不一样。同时,由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来

对齐规则:第一个数据成员放在偏移量为0的地方,以后每个数据成员的起始位置都要从自身大小的整数倍开始存储

内存对齐的好处:1.加快内存读取效率2.便于在不同平台之间移植

内存对齐的缺点:空间浪费

42.TCP和UDP的区别

1.TCP是面向连接的,UDP是无连接的

2.TCP是可靠的,UDP是不可靠的

3.TCP是面向字节流的,UDP是面向数据报文的

4.TCP只支持点对点通信,UDP支持一对一,一对多,多对多

5.TCP有拥塞控制机制,UDP没有

6.TCP协议下双方发送接受缓冲区都有,UDP并无实际意义上的发送缓冲区,但存在接受缓冲区

43.arr、&arr[0]和&arr的不同

首先如果打印的话,三个打印的完全一样,都是数组首元素的地址。

arr是首元素的地址

&arr[0]是首元素的地址

&arr是整个数组元素的地址

arr+1和&arr[0]+1都只移动四个字节,但是&arr+1是整个数组的后一位

44. char a,char a[],char *a,char *a[],char * *a 之间的区别

1. char a

定义了一个存储空间,存储的是char类型的变量

2. char a[]

是一个字符数组,数组中的每一个元素是一个char类型的数据

3. char *a

字符串的本质(在计算机眼中)是其第一个字符的地址,c和c++中操作字符串是通过内存中其存储的首地址来完成的
对于char a[]来说a代表的是数组的首地址,那么对char *a来说a代表的也是字符串的首地址
因此char a[]和char *a可以放到一块看,这两个没有本质区别。
但是要注意对于char s[]和char* a我们可以有`a=s`,但不能有`s=a`,因为创建数组的时候s的地址不为空已经确定,但是a是一个空指针,不能将非空的地址指向空指针

4. char *a[]

`*`的优先级是低于`[]`的,因此要先看`a[]`再看 `*`
因此这是一个char数组,数组中的每一个元素都是指针,这些指针指向char类型

`char *a[ ] = {"China","French","America","German"}`

5. char **a

两个**代表相同的优先级,因此从右往左看,即`char*(*a)`
char *a不就是一个字符串数组,a代表首地址。那么char * (*a)就是和char *a[]一样的数据结构

45.一维数组名和二维数组名的区别

不管是一维还是多维数组,都是内存中一块线性连续空间,因此在内存级别上,其实都只是一维。所以一维数组名是指向该数组的指针,二维数组名也是指向该数组的指针,但是+1之后,跳过的是一行。

46.二维数组名为什么不能直接赋值给二级指针?

一句话来说就是二维数组名是行指针,也就是指向数组的指针。而二级指针是指向指针的指针,它们不是同一类型。

47.用const和#define定义常量哪个更好?define宏常量和const常量的区别?是否可以做函数参数

1.类型和安全检查不同

宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;

const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查

2.编译器处理不同

宏定义是一个"编译时"概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束于编译时期;

const常量是一个"运行时"概念,在程序运行使用,放在内存中的数据段中。

3.存储方式不同

宏定义是直接替换,不会分配内存,存储于程序的代码段中;

const常量需要进行内存分配,存储于程序的数据段中

是否可以做函数参数:宏定义和const常量可以在函数的参数列表中出现

48.typedef与#define的区别

typedef故名思意就是类型定义的意思,但是它**并不是定义一个新的类型而是给已有的类型起一个别名**。主要有两个作用,第一个是给一些复杂的变量类型起别名,起到简化的作用。第二个是定义与平台无关的类型,屏蔽不同平台之间的差异。在跨平台或操作系统的时候,只需要改typedef本身就可以
#define为一宏定义语句,本质就是文本替换

区别

1. 关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。\#define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。

2. #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

3. 对指针操作不同。

49.#include<>和include""的区别

include<>一般用来查找标准库文件所在目录,在编译器设置的include路径内搜索;

include "" 的查找位置是当前源文件所在目录

50.c++中四种变量存储类型总结

在C++语言中,变量的存储类共有如下四种: 

(1)auto存储类(自动存储类)

(2)static存储类 (静态存储类)

(3)extern存储类  (外部存储类)

(4)register存储类(寄存器存储类)
auto存储类,即自动存储类。在函数内部定义的变量,如果不指定其存储类,那么它就是auto类变量。这个是最常见的,所以我们不加关键字auto------auto int b

extern存储类,如果在一个文件中要引用另一个文件中定义的外部变量,则在此文件中应用extern关键字把此变量说明为外部的。

register存储类,为了提高某些自动类变量或函数参数的处理速度,可以在定义这些变量的类型说明符的前面加上register关键字,以通知编译系统为这些变量分配寄存器来存放其值。若使用register(而非auto)存储类标识代码块内的变量,编译器就会将变量缓存于处理器内的寄存器中,此种情况下不能对该变量或其成员变量使用引用操作符&以获取其地址,因为&只能获取内存空间中的地址

51.C++中this指针相关问题

1.什么是this指针,作用是什么?

类成员函数是怎么区分d1和d2对象的?怎么样就能做到把d1,d2分别赋值

类的成员函数本身就有一个隐含的参数,该参数就是所谓的this指针。在不同的对象调用的时候,编译器会自动将该对象的地址赋予"this"

class Date
{ 
public :
 void SetDate(int year , int month , int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
int main()
{
 Date d1, d2;
 d1.SetDate(2018,5,1);
 d2.SetDate(2018,7,1);
 return 0;
 }

--------------------------------------------------------------
原函数名:void SetDate(int year,int month,int day)
实际上:void SetDate(int year,int month,int day,Date *this)
--------------------------------------------------------------
原代码:
_year = year;
_month = month;
_day = day;
实际代码:
_year = this->year;
_month = this->month;
_day = this->day;

什么是this指针,作用是什么?:this指针是C++类成员函数的一个隐含参数,当对象调用该成员函数的时候,编译器会将该对象的地址作为参数传入,通过this指针来对不同类对象进行操作,进而实现各个功能。

2.this指针存放在哪里?

1.this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传给this形参。所以对象中不存储this指针。

2.this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,也就是成员函数的其他参数正常都是存放在栈中,而this指针参数则是存放在寄存器中

3.this指针可以为空吗?

this指针是类成员函数的一个隐含参数,不需要人为操作,因此它与其它成员函数不同,不存于栈中,而是在寄存器里。this指针可以为空,但有条件,当我们要进行的操作不需要this指针去指向某个对象(例如仅仅是打印一个东西)的时候就可以令它为空

4.this指针什么时候创建

this在成员函数的开始执行前构造,在成员函数的执行结束后清除

5.在成员函数中调用delete this会出现什么问题?

在类对象的内存空间中,只有数据成员和虚函数表指针,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。

在调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

6.如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

52.inline内联函数与宏定义

1.程序先执行函数调用之前的语句
2.流程的控制转移到被调用函数的入口处,同时进行参数传递
3.执行被调用函数中函数体的语句
4流程返回到函数调用的下一条指令处,将函数返回值带回
5接着执行主函数中未执行的语句。

这样的调用过程,就要求在转去被调用函数之前,要记下当时执行的指令的地址,还要保护现场(记下当时相关的信息),以便在函数调用之后,流程返回到先前记下的地址处,并且根据记下的信息恢复现场,然后继续执行。这些都要花费时间。
c++提供一种提高效率的方法,在编译时将所调用函数的代码直接嵌入到主调函数中,而不是将流程转出去,这种嵌入到主函数的代码称为内嵌函数,或者叫内置函数,或者叫内联函数。

区别:

1.参数类型安全性: 内联函数比宏定义更加类型安全。内联函数会对参数进行类型检查,而宏定义不会。这意味着,使用内联函数可以避免一些潜在的类型错误。
2.编译器优化: 内联函数是在编译期间展开的,因此它可以进行更多的编译器优化。而宏定义则是在预处理器展开,不能进行编译器优化。因此,使用内联函数通常可以获得更好的性能。
3.调试: 内联函数比宏定义更容易进行调试。因为内联函数是实际函数的一份副本,可以通过调试器跟踪到内联函数的执行过程。而宏定义则无法通过调试器进行调试。
4.名称空间: 内联函数位于名称空间中,而宏定义不属于任何名称空间。这意味着,内联函数可以避免名称冲突问题,而宏定义可能会导致名称冲突。
5.大小和可读性: 内联函数比宏定义更易于阅读和维护。宏定义的代码通常比较冗长,而内联函数则可以使用常规的C++语法编写,更加简洁易懂。另外,内联函数可以利用C++的函数重载和模板等特性,提高代码的可读性和可维护性。

53.explicit关键字

在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。

  • explicit 关键字只能用于类内部的构造函数声明上
  • explicit 关键字作用于单个参数的构造函数。
  • 在C++中,explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换
  • 禁止类对象之间的隐式转换

54.friend友元类和友元函数

**友元函数**能够使得普通函数直接访问类的保护数据和私有数据成员,避免了类成员函数的频繁调用,可以节约处理器开销,提高程序的效率,但所矛盾的是,即使是最大限度大保护,同样也破坏了类的封装特性,这即是友元的缺点,在现在cpu速度越来越快的今天我们并不推荐使用它。

**友元类**友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。    

55.c++运行机制

预处理、编译、汇编和链接

1.预处理

C/C++语言最常见的预处理就是将所有的“#define”删除,并且展开所有的宏定义。而预处理其实还包括:处理所有的条件编译指令,比如“#if”、处理“#include”预编译指令、删除所有的注释、添加行号和文件名标识等。

2. 编译

编译会将源代码由文本形式转换成机器语言,编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。\

56.创建派生类对象,构造函数的执行顺序是什么?析构函数的执行顺序?

构造函数执行顺序

1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是该基类在派生类中出现的顺序,而不是他们在成员初始化列表中的顺序。
2. 成员类对象构造函数。如果有多个成员类构造函数调用顺序是对象在类中被声明的顺序,而不是他们在成员初始化列表中的顺序
3. 派生类构造函数

析构函数顺序

1. 派生类析构函数
2. 成员类对象的析构函数
3. 调用基类的析构函数

57.final关键字

在 C++11 中引入了 final 关键字,它用于修饰类、虚函数和成员函数,表示它们不能被继承或重写。final 关键字的作用是为了确保某些代码不被修改或扩展,从而提高代码的可维护性和可读性。

58.有了malloc/free为什么还要new/delete?

对于一些非内部数据类型(eg:类对象)来说,光用maloc/free无法满足要求。对象在创建的同时要自动执行构造函数,对象在消亡的时候要自动执行析构函数,而由于malloc/free是库函数而不是运算符,不在编译器的控制权限内(库函数是编译好的代码由链接器链接到为我们的代码中),也就不能自动执行构造函数和析构函数。所以,在c++中需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。

59.free释放内存的理解,如何知道要释放的空间大小?

对于glibc的malloc算法,**空间的大小记录在参数指针指向地址的前面,free的时候通过这个记录即可知道要释放的内存有多大。

60.虚函数和虚函数指针的位置

虚函数表存储在可执行目标文件的只读数据段(全局区).rodata)、虚函数存储在代码区(.text)。虚表指针存储位置与对象所在位置相同,如果不是静态或全局对象,要等到可执行目标文件加载到内存在运行时创建,可能在栈、也可能在堆或全局区(数据段)等。。

61.lambda表达式

[capture list]     (parameters)     mutable throw()     -> return-type     { statement }
  [捕获列表]   [参数列表(可省略)]  [函数选项(可省略)] [返回类型(可省略)]     [函数体]
捕获列表:lambda表达式的捕获列表精细的控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
1.[]                不捕获任何变量
2.[=]               以传值的方式捕获所有外部变量
3.[&]               以传引用的方式捕获所有外部变量
4.[x,&y]            x以传值的方式捕获,y以传引用的方式捕获
5.[=,&x]            x以传引用的方式捕获,其余变量以传值的方式捕获
6.[&,x]             x以传值的方式捕获,其余变量以传引用的方式捕获

62.c++智能指针

1.为什么要用智能指针

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

2.auto_ptr、unique_ptr、shared_ptr、weak_ptr

1)auto_ptr

用 法: auto_ptr<类型> 变量名(new 类型)

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

被c++11抛弃的原因:

1.复制或者赋值都会改变资源的所有权,比如STL容器内的元素必须支持可复制和可赋值。

// auto_ptr 被C++11抛弃的主要原因
auto_ptr<string> p1(new string("I'm Li Ming!"));
auto_ptr<string> p2(new string("I'm age 22."));

cout << "p1:" << p1.get() << endl;        //p1:012A8750
cout << "p2:" << p2.get() << endl;        //p2:012A8510

// p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
// 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
p1 = p2;	
cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;        //p1:012A8510
cout << "p2:" << p2.get() << endl;        //p2:00000000    //p2变成了NULL

---------------------------------------------------------------------------------------
vector<auto_ptr<string>> vec;
auto_ptr<string> p3(new string("I'm P3"));
auto_ptr<string> p4(new string("I'm P4"));

// 必须使用std::move修饰成右值,才可以进行插入容器中
vec.push_back(std::move(p3));
vec.push_back(std::move(p4));

cout << "vec.at(0):" <<  *vec.at(0) << endl;
cout << "vec[1]:" <<  *vec[1] << endl;


// 风险来了:
vec[0] = vec[1];	// 如果进行赋值,问题又回到了上面一个问题中。
cout << "vec.at(0):" << *vec.at(0) << endl;       
cout << "vec[1]:" << *vec[1] << endl;

2.不支持对象数组的内存管理

auto_ptr<int[]> array(new int[5]);	// 不能这样定义,会报错

2)unique_ptr 

C++11用更严谨的unique_ptr 取代了auto_ptr

unique_ptr特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr赋值构造,也无法进行左值赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的

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

unique_ptr<string> p1(new string("I'm Li Ming!"));
unique_ptr<string> p2(new string("I'm age 22."));
	
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

p1 = p2;					// 禁止左值赋值
unique_ptr<string> p3(p2);	// 禁止左值赋值构造

unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2);	// 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

B. 在 STL 容器中使用unique_ptr,不允许直接赋值 

vector<unique_ptr<string>> vec;
unique_ptr<string> p3(new string("I'm P3"));
unique_ptr<string> p4(new string("I'm P4"));

vec.push_back(std::move(p3));
vec.push_back(std::move(p4));

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

vec[0] = vec[1];	/* 不允许直接赋值 */
vec[0] = std::move(vec[1]);		// 需要使用move修饰,使得程序员知道后果

cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;

C. 支持对象数组的内存管理 

除了上面ABC三项外,unique_ptr的其余用法都与auto_ptr用法一致。

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;

这是由于auto_ptr 与 unique_ptr的排他性所导致的!
为了解决这样的问题,我们可以使用shared_ptr指针指针! 

3)shared_ptr

unique_ptr 这种排他型的内存管理并不能适应所有情况,如果需要多个指针变量共享怎么办?

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

1.引用计数的使用

class Person {
public:
	Person(int v) {
		this->no = v;
		cout << "构造函数 \t no = " << this->no << endl;
	}

	~Person() {
		cout << "析构函数 \t no = " << this->no << endl;
	}

private:
	int no;
};

// 仿函数,内存删除
class DestructPerson {
public:
	void operator() (Person *pt) {
		cout << "DestructPerson..." << endl;
		delete pt;
	}
};


shared_ptr<Person> sp1;

shared_ptr<Person> sp2(new Person(2));

// 获取智能指针管控的共享指针的数量	use_count():引用计数
cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;

// 共享
sp1 = sp2;

cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl << endl;

shared_ptr<Person> sp3(sp1);
cout << "sp1	use_count() = " << sp1.use_count() << endl;
cout << "sp2	use_count() = " << sp2.use_count() << endl;
cout << "sp2	use_count() = " << sp3.use_count() << endl << endl;

2.构造

3.初始化 

方式一:构造函数

shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
shared_ptr<int> up2(up1);  // 使用智能指针up1构造up2, 此时int(10) 引用计数为2

方式二:使用make_shared 初始化对象,分配内存效率更高(推荐使用)
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr; 用法:
make_shared<类型>(构造类型对象需要的参数列表);

shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);

4.赋值

shared_ptrr<int> up1(new int(10));  // int(10) 的引用计数为1
shared_ptr<int> up2(new int(11));   // int(11) 的引用计数为1
up1 = up2;	// int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2

5.主动释放对象

shared_ptrr<int> up1(new int(10));
up1 = nullptr ;	// int(10) 的引用计数减1,计数归零内存释放 
// 或
up1 = NULL; // 作用同上 

6.重置 

p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
p1是一个指针!

7.交换

p1 和 p2 是智能指针

std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2);    // 交换p1 和p2 管理的对象,原对象的引用计数不变

shared_ptr使用陷阱

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

如下代码:
Boy类中有Girl的智能指针;
Girl类中有Boy的智能指针;
当他们交叉互相持有对方的管理对象时…

#include <iostream>
#include <string>
#include <memory>

using namespace std;

class Girl;

class Boy {
public:
	Boy() {
		cout << "Boy 构造函数" << endl;
	}

	~Boy() {
		cout << "~Boy 析构函数" << endl;
	}

	void setGirlFriend(shared_ptr<Girl> _girlFriend) {
		this->girlFriend = _girlFriend;
	}

private:
	shared_ptr<Girl> girlFriend;
};

class Girl {
public:
	Girl() {
		cout << "Girl 构造函数" << endl;
	}

	~Girl() {
		cout << "~Girl 析构函数" << endl;
	}

	void setBoyFriend(shared_ptr<Boy> _boyFriend) {
		this->boyFriend = _boyFriend;
	}

private:
	shared_ptr<Boy> boyFriend;
};


void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());

	// 陷阱用法
	spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);
	// 此时boy和girl的引用计数都是2
}


int main(void) {
	useTrap();

	system("pause");
	return 0;
}

运行截图: 

如下图:
当我们执行useTrap函数时,注意,是没有结束此函数,boy和girl指针其实是被两个智能指针托管的,所以他们的引用计数是2

useTrap函数结束后,函数中定义的智能指针被清掉,boy和girl指针的引用计数减1,还剩下1,对象中的智能指针还是托管他们的,所以函数结束后没有将boy和gilr指针释放的原因就是于此。

所以在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!

4)weak_ptr

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

1.弱指针的使用;
weak_ptr wpGirl_1; // 定义空的弱指针
weak_ptr wpGirl_2(spGirl); // 使用共享指针构造
wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针

2.弱指针也可以获得引用计数;
wpGirl_1.use_count()

3.弱指针不支持 * 和 -> 对指针的访问;

4.在必要的使用可以转换成共享指针 lock();

使用代码:

shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());

// 弱指针的使用
weak_ptr<Girl> wpGirl_1;			// 定义空的弱指针
weak_ptr<Girl> wpGirl_2(spGirl);	// 使用共享指针构造
wpGirl_1 = spGirl;					// 允许共享指针赋值给弱指针

cout << "spGirl \t use_count = " << spGirl.use_count() << endl;
cout << "wpGirl_1 \t use_count = " << wpGirl_1.use_count() << endl;

	
// 弱指针不支持 * 和 -> 对指针的访问
/*wpGirl_1->setBoyFriend(spBoy);
(*wpGirl_1).setBoyFriend(spBoy);*/

// 在必要的使用可以转换成共享指针
shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();

cout << sp_girl.use_count() << endl;
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;

请看Boy类 

#include <iostream>
#include <string>
#include <memory>

using namespace std;

class Girl;

class Boy {
public:
	Boy() {
		cout << "Boy 构造函数" << endl;
	}

	~Boy() {
		cout << "~Boy 析构函数" << endl;
	}

	void setGirlFriend(shared_ptr<Girl> _girlFriend) {
		this->girlFriend = _girlFriend;


		// 在必要的使用可以转换成共享指针
		shared_ptr<Girl> sp_girl;
		sp_girl = this->girlFriend.lock();

		cout << sp_girl.use_count() << endl;
		// 使用完之后,再将共享指针置NULL即可
		sp_girl = NULL;
	}

private:
	weak_ptr<Girl> girlFriend;
};

class Girl {
public:
	Girl() {
		cout << "Girl 构造函数" << endl;
	}

	~Girl() {
		cout << "~Girl 析构函数" << endl;
	}

	void setBoyFriend(shared_ptr<Boy> _boyFriend) {
		this->boyFriend = _boyFriend;
	}

private:
	shared_ptr<Boy> boyFriend;
};


void useTrap() {
	shared_ptr<Boy> spBoy(new Boy());
	shared_ptr<Girl> spGirl(new Girl());

	spBoy->setGirlFriend(spGirl);
	spGirl->setBoyFriend(spBoy);
}


int main(void) {
	useTrap();

	system("pause");
	return 0;
}

在类中使用弱指针接管共享指针,在需要使用时就转换成共享指针去使用即可!

使用场景:

C++ 智能指针最佳实践&源码分析 - 知乎 (zhihu.com)

1.unique_ptr:一般在不需要多个指向同一个对象的指针时使用,即这个对象在另一个对象内部或方法内部使用时优先使用unique_ptr

2.shared_ptr:shared_ptr一般在需要多个执行同一个对象的指针使用。在我看来可以简单的理解:这个对象需要被多个 Class 同时使用的时候。

class B
{
private:
    std::shared_ptr<A> a_;

public:
    B(std::shared_ptr<A>& a): a_(a) {}
};

class C
{
private:
    std::shared_ptr<A> a_;

public:
    C(std::shared_ptr<A>& a): a_(a) {}
};

std::shared_ptr<B> b_;
std::shared_ptr<C> c_;

void test_A_B_C()
{
    std::shared_ptr<A> a = std::make_shared<A>();
    b_ = std::make_shared<B>(a);
    c_ = std::make_shared<C>(a);
}

3.weak_ptr:如果A想要调用BC的方法怎么办呢?可否在A中定义BCshared_ptr呢?答案是不可以,这样会产生循环引用,导致内存泄露.此时就需要weak_ptr出场了,即两个类需要交叉使用智能指针的时候需要用到weak_ptr。

63.多线程

多线程的优点和使用场景

多线程程序在提高计算机系统的并发性和响应性方面有着极其重要的作用。它可以更好地利用计算机的多核和多处理器资源,在提高系统吞吐量的同时缩短响应时间。常见的使用场景包括:

  • 程序需要用户交互并保持响应性
  • 后台任务需要异步完成
  • 大量计算密集型任务需要加速

进程和线程的区别:

进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。

线程同步的方式:

互斥锁、读写锁、条件等待、信号量

同步和异步

完全理解同步/异步与阻塞/非阻塞 - 知乎 (zhihu.com)

同步:同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,并定时的去访问,直到收到返回信息才继续执行下去;

异步:异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

是否是同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。

阻塞和非阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。

同步阻塞、同步非阻塞、异步阻塞、异步非阻塞

假设小明需要在网上下载一个软件:

  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,这是同步阻塞;
  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,但是软件下载完成其实是会「叮」的一声通知的(但小明依然那样干等着),这是异步阻塞;(不常见)
  • 如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了,这是同步非阻塞;
  • 如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件,这是异步非阻塞

64.如何定义一个只能在堆上(栈上)生成对象的类?

综述:在c++中,建立对象有两种方式,分为静态建立和动态建立,如代码所示:

//静态建立
A a;
//动态建立
A *a = new A; 

静态建立对象时,由编译器自动为对象在栈中分配内存,是直接移动栈顶指针,腾出适当的空间,然后再这片空间上调用构造函数生成对象,这种静态建立是直接调用类的构造函数

动态建立对象时,程序员使用new 运算符在堆中建立。这个过程分为两步,第一部是调用operator new()函数在堆空间中搜索出来适当的内存并进行分配,第二步是调用构造函数生成对象,初始化这片内存空间,间接调用构造函数。

1.只能在堆上生成对象的类。

只能在堆上也就意味着不能再栈上,在栈上是编译器分配内存空间,构造函数来构造栈对象。在栈上当对象周期完成,编译器会调用析构函数来释放栈对象所占的空间,也就是说编译器管理了对象的整个生命周期。**编译器在调用构造函数为类的对象分配空间时,会先检查析构函数的访问性**,不光是析构函数,编译器会检查所有非静态函数的访问性。因此,如果类的析构函数是私有的,编译器不会为对象在栈上分配内存空间。

(扩展一下,如果把析构函数写在private中的话,不能用A a这种静态方式建立对象。但也会有其他问题,如果这个类是父类的话,通常要将析构函数加上virtual关键字,然后再子类重写,实现多态性。但是子类不能访问private成员,可以使用protected,子类可以访问父类的protected成员,但不能访问private。

另一个问题,当用new建立后,无法delete。因为delete会调用对象的析构函数,但是析构函数在类外无法访问。)

2.只能在栈上生成对象的类。

只有使用new运算符才会在堆上创建对象。设为私有即可。

动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间(这种方法,间接调用类的构造函数),但是operator new()函数用于分配内存,无法提供构造功能,**所以不能将构造函数设为私有**;

 class  A  
  {  
  private :  
      void * operator  new ( size_t  t){}      // 注意函数的第一个参数和返回值都是固定的   
      void  operator  delete ( void * ptr){}  // 重载了new就需要重载delete   
  public :  
      A(){}  
      ~A(){}  
  }; 

65.C++如何阻止类被实例化?

一个类不想被实例化通常有两种情况:一种是抽象类,一种是工具类。
工具类:

我们需要一个类来封装加、减、乘、除。这个类就是一个典型的工具类,用它创建对象没有意义,可以直接通过类名调用静态成员函数。

class Calculate
{
public:
    static int add(int x, int y);
    static int sub(int x, int y);
    static int mul(int x, int y);
    static int div(int x, int y);
};

如何阻止:

方法一:类中包含纯虚函数。

方法二:构造函数私有

66.C++所有构造函数

类对象被创建时,编译器为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。因此构造函数的的作用是初始化对象的成员函数。

1.默认构造函数:**如果没有人为构造函数,则编译器会自动默认生成一个无参构造函数。

2.一般构造函数:**包含各种参数,一个类可以有多个一般构造函数,前提是参数的个数和类型和传入参数的顺序都不相同,根据传入参数调用对应的构造函数。

3.拷贝构造函数:**拷⻉构造函数的函数参数为对象本身的引用,用于根据⼀个已存在的对象复制出⼀个新的该类的对象,⼀般在函数中会将已存在的对象的数据成员的值⼀⼀复制到新创建的对象中。如果没有显示的写拷⻉构造函数,则系统会默认创建⼀个拷⻉构造函数,但当类中有指针成员时,最好不要使⽤编译器提供的默认的拷⻉构造函 数,最好⾃⼰定义并且在函数中执⾏深拷⻉。

4.移动构造函数:**有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。但是指针的浅层复制是非常危险的。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了(pointer dangling)。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间)

5.赋值构造函数:**=运算符的重载,类似拷贝构造函数,将=右边的类对象赋值给类对象左边的对象,不属于构造函数,=两边的对象必须都要被创建。

6.类型转换构造函数:**有时候不想要隐式转换,用explict关键字修饰。一般来说带一个参数的构造函数,或者其他参数是默认的构造函数      

67.构造函数、析构函数是否能抛出异常

构造函数可以抛出异常

对象只有在构造函数执行完成之后才算构造妥当,c++只会析构已经完成的对象。因此如果构造函数中发生异常,控制权就需要转移出构造函数,执行异常处理函数。在这个过程中系统会认为对象没有构造成功,导致不会调用析构函数。在构造函数中抛出异常会导致当前函数执行流程终止,在构造函数流程前构造的成员对象会被释放,但是如果在构造函数中申请了内存操作,则会造成内存泄漏。另外,如果有继承关系,派生类中的构造函数抛出异常,那么基类的构造函数和析构函数可以照常执行的。

解决办法:用智能指针来管理内存就可以

C++标准指明析构函数不能、也不应该抛出异常

析构函数不能抛出异常原因有两个:

1. 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2. 异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding)。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。

解决办法:把异常完全封装在析构函数内部,决不让异常抛出函数之外

68.C++成员变量的初始化顺序问题

1. 初始化基类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
2. 初始化派生类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
3. 初始化基类的普通成员变量和代码块,再执行父类的构造方法;
4. 初始化派生的普通成员变量和代码块,在执行子类的构造方法;

69.怎么在栈上栈上分配内存?

alloca函数分配的内存不需要手动释放,和普通的栈上对象的处理一样:超出作用域自动回收内存。

70.带返回值的函数如果不return会怎么样?

首先不是BUG,这种情况属于**未定义的行为**,而未定义的行为不会导致编程失败。

当main函数没有return结尾的时候会在生成的目标文件中自动加入return 0

71.raii基本理解与使用

C++: RAII原理、应用与实践——应该使用对象来管理资源 - 知乎 (zhihu.com)

RAII是Resource Acquisition Is Initialization的缩写,即“资源获取即初始化”。它是C++语言的一种管理资源、避免资源泄漏的惯用法,利用栈的特点来实现,这一概念最早由Bjarne Stroustrup提出。在函数中由栈管理的临时对象,在函数结束时会自动析构,从而自动释放资源,因此,我们可以通过构造函数获取资源,通过析构函数释放资源

struct Raii{
    A* p;
    Raii(A* _p) : p{ _p } {};
    ~Raii() {
        delete p;
    }
};

void foo() {
    Raii ptr(new A());
}

为什么要用RAII

  1. 忘记delete释放内存。比如释放原指针指向的内存前就改变了指针的指向。
  2. 程序抛出异常后导致无法delete。比如上面的①处,如果f函数抛出异常,没有机会运行delete,从而导致内存泄漏。
  3. 需求变更后,修改了函数,新增了分支,提前返回,却没有delete;现实情况代码复杂的话可能没有这么显而易见。
RAII<A> p1(new A{ 114,514,1919810 });
RAII<A> p2 = p1;

但是以上代码会存在一个问题,p1,p2同时有了A的所有权,当我们析构函数释放时,显然这个内存会被释放两次,所以我们需要使用move来移交所有权。

RAII<A> p1(new A{ 114,514,1919810 });
RAII<A> p2 = move(p1);

72.C++程序优化的方法 

- 空间足够时,可以将经常需要读取的资源,缓存在内存中。
- 尽量减少大内存对象的构造与析构,考虑缓存暂时不用的对象,等待后续继续使用。
- 尽量使用C++11的右值语义,减少临时对象的构造。
- 简单的功能函数可以使用内联。少用继承,多用组合,尽量减少继承层级。
- 在循环遍历时,优化判断条件,减少循环次数。
- 优化线程或进程的同步方式,能用原子操作的就不用锁。能应用层同步的就不用内核对象同步。
- 优化堆内存的使用,如果有内存频繁的申请与释放,可以考虑内存池。
- 优化线程的使用,节省系统资源与切换造成的性能损耗,线程使用频繁的可以考虑线程池。
- 尽量使用事件通知,谨慎使用轮循或者sleep函数。
- 界面开发中,耗时的业务代码不要放在UI线程中执行,使用单独的线程去异步处理耗时业务,提高界面响应速度。
- 经常重构、优化代码结构。优化算法或者架构,从设计层面进行性能的优化。

73.C++11 RVO/NRVO机制

RVO (return value optimization) 和NRVO (named return value optimization) 是C++在处理 “返回一个class object的函数” 时常用的优化技术,主要作用就是消除临时对象的构造和析构成本。

int get_max(){    //RVO
    int a;
    return a;
}
//改成
int get_max(int& a){    //NRVO
    int a = max;
}

74.空指针到底是什么

由系统保证空指针不指向任何实际的对象或函数,也就是说,任何对象或者函数的地址都不可能是空指针,空指针与任何对象或函数的指针值都不相等。空指针表示“未分配”或者“尚未指向任何地方”。它与未初始化的指针有所不同,空指针可以确保不指向任何对象或函数,而未初始化指针可能指向任何地方。

75.c/c++参数入栈顺序和参数计算顺序

c/c++中规定了函数参数的压栈顺序是从右至左(以栈方式存储)

76.mutable关键字

mutable:可变的

首先想到的是在lamba表达式中有这个东西,表示如果是值传递的,可以修改,不加这个mutable属性不能修改,表示传递过来的是常量。虽然在匿名函数内部改变了变量的值,但是在外部还是原来的值

除了lamba表达式中的,就只有类中的了

mutable 在类中只能够修饰非静态数据成员,用来修饰一个 const 示例的部分可变的数据成员的。如下代码:

struct Test
{
    int a;
    mutable int b;
};

const struct Test test = {1,2};
test.a = 10;  # 编译错误
test.b = 20;  # 允许访问

77.c++出现的内存问题

c++中内存问题大致有这几个方面:

1. 缓冲区溢出。一般来说vector或者string都很智能帮我们管理缓冲区,所以很多时候不会出现
2. 指针悬挂/野指针,用shared_ptr和weak_ptr就可以解决
3. 重复释放,用boost的scoped_ptr或者仔细检查,只在对象析构的时候释放一次
4. 内存泄漏,也可以用scoped_ptr,这个比其他的来说还算正常,至少用一段时间内感觉不出来
5. new[]和delete不配对,moduo书中给的方案是把new[]统统换成vector就行
6. 内存碎片,这个就非常深邃了

78.i++和++i的区别

1.

a=i++ , a 返回原来的值a=i,i=i+1;
a=++i , a 返回加1后的值,a=i+1,i=i+1。
也就是i++是先赋值,然后再自增;++i是先自增,后赋值。

2.第二个区别就是: i++ 不能作为左值,而++i可以。

79.变量声明和定义的区别?

- **声明**仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间

- **定义**要在定义的地方为其分配存储空间。

相同变量可以在多处声明(外部变量extern),但只能在一处定义**

80.什么是线程安全的

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

81.什么是socket

Socket 的中文翻译过来就是“套接字”。套接字是什么,我们先来看看它的英文含义:插座。

Socket 就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个 Socket 实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP 地址和端口”,我就接通谁。

实际上,Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口,供应用层调用,实现进程在网络中的通信。Socket 起源于 UNIX,在 UNIX 一切皆文件的思想下,进程间通信就被冠名为文件描述符(file descriptor),Socket 是一种“打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

82.什么是虚函数?

虚函数(Virtual Function)是在面向对象编程中用于实现动态多态性的一种机制。通过将基类中的成员函数声明为虚函数,可以在派生类中重写(Override)这些函数,从而根据对象的实际类型确定调用的函数版本。 

c++11

1.了解auto 和decltype 吗?

auto:可以让编译器在编译时就推导出变量的类型

decltype:用于推导表达式类型

int func() { return 0; };
decltype(func()) i; // i是int
 
int x = 0;
decltype(x) y; // y是int
decltype(x + y) z; // z是int

2.谈一谈你对左值和右值的了解,了解左值引用和右值引用吗?

左值:在内存中有确定存储地址、有变量名、表达式结束依然存在的值。

左值引用:绑定到左值的引用,通过&来获得左值引用。

右值:在内存中没有确定存储位置、没有变量名,表达式结束就会销毁的值。

右值引用:绑定到右值的引用,通过&&来获得右值引用。

int a1 = 10; // 非常量左值
const int a2 = 10; // 常量左值
 
int& b1 = a1; // 非常量左值引用
const int& b2 = a2; // 常量左值引用
 
int&& c1 = 10; // 非常量右值引用
const int&& c2 = 10; // 常量右值引用,10是非常量右值  

3.了解移动语义和完美转发吗?

为什么需要右值引用,通过右值引用可以充分使用临时变量,减少不必要的拷贝,提高效率。         

移动语义:可以理解为转移所有权,拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,如何利用移动语义,主要通过移动构造函数。C++移动语义_c++ 可移动-优快云博客

        完美转发:指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数也是右值。聊聊C++中的完美转发 - 知乎 (zhihu.com)

4.平时会用到function、bind、lambda 吗,都什么场景下会用到?

std::function是C++11标准库中提供的一种可调用对象的通用类型,它可以存储任意可调用对象,如函数指针,函数对象,成员函数指针和lambda表达式。std::function类模板是一个类似于函数指针的类型,但它是可以处理任意可调用对象的,并且可以检查调用对象是否为空。

 基本语法:

std::function<return_type(parameter_types)> var_name;

int func(int x, int y) { return x + y; }
std::function<int(int, int)> f = func;
 
class A {
public:
    int mem_func(int x) { return x * x; }
};
std::function<int(A*, int)> f2 = &A::mem_func;

 std::function对象可以像普通函数一样调用,并且可以使用bool类型的运算符来检查调用对象是否为空。

std::function<int(int, int)> f;
if (f)
    std::cout << f(1, 2) << std::endl;
else
    std::cout << "f is empty" << std::endl;

std::bind的头文件是 <functional>,它是一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。

std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个防函数;
  • 只绑定部分参数,减少可调用对象传入的参数。

1.std::bind绑定普通函数

double callableFunc (double x, double y) {return x/y;}
auto NewCallable = std::bind (callableFunc, std::placeholders::_1,2);  
std::cout << NewCallable (10) << '\n';                       

1.bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。因此std::bind(callableFunc,_1,2)等价于std::bind (&callableFunc,_1,2);
2._1表示占位符,位于<functional>中,std::placeholders::_1;
3.第一个参数被占位符占用,表示这个参数以调用时传入的参数为准,在这里调用NewCallable时,给它传入了10,其实就想到于调用callableFunc(10,2);

2.std::bind绑定一个成员函数

class Base
{
public:
    void display_sum(int a1, int a2)
    {
        std::cout << a1 + a2 << '\n';
    }
 
    int m_data = 30;
};
int main() 
{
    Base base;
    auto newiFunc = std::bind(&Base::display_sum, &base, 100, std::placeholders::_1);
    f(20); // should out put 120. 
}

1.bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。
2.必须显式地指定&Base::diplay_sum,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在Base::display_sum前添加&;
3.使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &base;
3.绑定一个引用参数

5.enum 和enum class 有什么区别?

枚举作用域是指枚举类型成员名字的作用域,起自其声明之处,终止枚举定义结束之处。enum与class enum区别在于是否限定其作用域。C语言规定,枚举类型(enum)的成员的可见范围被提升至该枚举类型所在的作用域内。这被认为有可能污染了外部的作用域,为此,C++11引入了枚举类(enum class)解决此问题。
 

#enum
enum Sex
{
	Girl,				
	Boy		
};
// 错误,编译器提示 Girl,Boy重定义
enum Student
{
	Girl,				
	Boy	
};

#enum class
enum class Sex
{
	Girl,				
	Boy		
};

enum class Student
{
	Girl,				
	Boy	
};

int main(int argc, char *argv[])
{
  Sex a = Sex::Gril; 
  Student b = Student::Gril;
  //两者处于不同作用域下,不会重定义
}

枚举定义将被限制在枚举作用域内,并且不能隐式转换为整数类型,但是可以显式转化为整数类型,

enum class Sex
{
	Girl,				
	Boy	
};
int main(int argc, char *argv[])
{
    Sex a=Sex::Gril;
    int d1 =a; // 错误,无法从“Girl”隐式转换为“int”。
	int d2 = int(a); // 正确,显示将enum class转换为整数
	return 0;
}

6.c++11关键字

1.noexcept:noexcept告诉编译器指定某个函数不抛异常(C++中的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。)

2.override:告诉编译器要重写父类的方法(函数参数、返回类型必须相同)

3.final关键字:该关键字用来修饰类,当用final修饰后,该类不允许被继承,在 C++ 11 中 final 关键字要写在类名的后面

4.=default:如果一个 C++ 类没有显式地给出构造函数、析构函数、拷贝构造函数、operator = 这几类函数的实现,在需要它们时,编译器会自动生成;或者,在给出这些函数的声明时,如果没有给出其实现,编译器在链接时就会报错。=default 如果标记这类函数,编译器会给出默认实现。

5.using:一般的using关键子我们都是用来声明当前文件的命名空间,比如标准库的命名空间std-> using namespace std;

STL

1.C++直接使用数组好还是使用std::array 好?std::array 是怎么实现的?

array是C++11中新提出来的容器类型,与内置数组相比,array是一种更容易使用,更加安全的数组类型,可以用来替代内置数组。作为数组的升级版,继承了数组最基本的特性,也融入了很多容器操作

数组初始化不能直接使用拷贝和赋值!!!数组的传递只能是遍历的形式来拷贝

和数组不同的是,array可以使用拷贝和赋值的形式进行初始化

array<int,10> ial1={0,1,2,3};

array<int,10> copy=ial1;//只要保证两者的类型一致就可以(包括元素类型和大小)

2.STL ⾥ resize 和 reserve 的区别?clear 是怎么实现的?

reserve()函数:会有可能将容量变大

(1)如果n大于容器现有的容量(capacity),比如你容器原来是100的容量,我现在指定n=200,那么就需要在自由内存区为整个容器重新分配一块新的更大的连续空间【因为vector是顺序容器,所以存储空间是连续的,如果之前的存储空间不够了,必须这样做】,然后将容器内所有的有效元素从旧位置全部复制到新位置,这个过程是调用拷贝构造函数,然后释放旧位置的所有存储空间,并调整容器的元素位置指示器。所以reserve的时候如果n比原来的大,结果只是让容器的冗余容量(即没有分配元素的存储区)变大,容器的实际大小,即元素个数并没有改变。

(2)如果n小于容器原来的容量,那么这个函数什么作用也没有了

resize()函数:会改变size的大小

(1)如果n大于容器当前的大小(即容器的size,这里和capacity无关),则在容器的末尾插入n-size()个初始值为c的元素;如果没有指定初始值,那就元素类型的默认构造函数来初始化。

(2)如果n小于容器当前的大小,则删除末尾的size()-n个元素,这样就导致容器的大小变了,size 变小了。但是这种类型的容器在删除一个元素的时候并不会释放元素本身的内存空间【这也是为了保留这块空间以避免将来要插入新元素的时候又要进行存储空间重分配】,所以容器的容量即capacity其实是没有改变的。

(3)n等于容器当前的大小,则什么也不做。

reserve和resize的共同点就是:都不缩减容器本身的容量。即对内存空间并没有影响

 clear实现:clear只是将vector的size置0,并不保证capacity为0,因此clear并不能释放vector已经申请的内存。

3.deque 的底层数据结构是什么?它的内部是怎么实现的?

4.map 和unordered_map 有什么区别?分别在什么场景下使用?

5.list 的使用场景?std::find 可以传入list 对应的迭代器吗?

6.string 的常用函数

=,assign() // 赋值
swap() // 交换
+=,append(),push_back() // 尾部添加字符
insert() // 插入字符
erase() // 删除字符
clear() // 清空字符
replace() // 替换字符
substr() // 截取子字符串

7.vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因

如果需要频繁插入,最好先指定vector的大小,因为vector在容器大小不够的时候会重新申请一块大小为原容器两倍的空间,并将原容器的元素拷贝到新容器中,并释放原空间,这个过程是十分耗时和耗内存的。频繁调用push_bac()会使得程序花费很多时间在vector扩容上,会变得很慢,这种情况可以考虑用list。

8.C++中vector和list的区别

vector和数组类似,拥有一段连续的内存空间。能很好的支持随机存取,它的vector::iterator支持“+”,“+=”,“<”等操作符

list是由双向链表实现的,因此内存空间是不连续的,只能通过指针访问数据,所以list的随机存取非常没有效率,但能高效的进行插入和删除,不支持随机访问,因此list::iterator不支持“+”,“+=”,“<”等操作符

9.vector会迭代器失效吗?什么情况下会迭代器失效?

会。

对于序列容器vector、deque来说,使用erase(iterator)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器

对于关联容器map、set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。

对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

10.c++STl中vector、list和map插入1000万个元素,消耗对比

毫无疑问vector最小

使用std::map和std::list存放数据,消耗内存比实际数据大得多

原因:std::list和std::map属于散列容器,容器的空间之间是通过指针来关联的,所以指针会占用一部分内存,当自身存放的数据较2*8(std::list,双向链表)差别不大时,会有很大的额外内存开销。为了避免此开销,可以使用线性容器,std::vector。

11.迭代器和指针的区别

迭代器实际上是对“遍历容器”这一操作进行了封装。迭代器不是指针,是类模板。重载了指针的一些操作符如:,->, * , ++, --等。

在编程中我们往往会用到各种各样的容器,但由于这些容器的底层实现各不相同,所以对他们进行遍历的方法也是不同的。例如,数组使用指针算数就可以遍历,但链表就要在不同节点直接进行跳转。c++我觉得是一门非常讲究方便的语言,显然这种情况是不能够出现的。因此就出现了迭代器,将遍历容器的操作封装起来,可以针对所有容器进行遍历。

12.STL6大部件

STL包含6大部件:容器、迭代器、算法、仿函数、适配器和空间配置器。

操作系统

C++ Qt常用面试题整理(不定时更新)_qt面试题-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值