C/C++最新2022年面试题总结(持续更新)

初级

1. C和C++的区别

  • C++增加new和delete进行内存分配,取代了C中的malloc和free。
  • C++添加了字符串类,取代C中的字符数组处理函数。
  • C++中使用iostream类库来做控制态的输入输出,取代了C中的stdio函数库。
  • C++中用try/catch/throw进行异常处理,取代了C中的setjmp()和longjmp()函数。
  • C++允许函数重载,参数类型不完全相同即可,而C不允许。
  • C++允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C必须要在函数开头部分定义。
  • C++中新增了引用。

2. C++指针和引用的区别

  1. 指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;
    引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作。
  2. 引用只有一级,而指针可以有多级。
  3. 指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作;
    引用传参的时候,传进来的就是变量本身,因此变量可以被修改。
  4. 引用必须在声明时初始化,且不能更换绑定的变量;
    指针可以在任何时候初始化,且非const的指针可重新赋值。
  5. 指针传参时,有可能传入空指针,因而需要进行空指针判断;
    引用则不存在空引用一说,因而能提高一些效率。
值传递、指针传递、引用传递的区别和效率
  • 值传递:有一个形参向函数所属栈区拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将耗费时间和空间(传值)
  • 指针传递:同样有一个形参向函数所属栈区拷贝数据的过程,但拷贝的数据是4个字节的地址(传值,但是传地址的值
  • 引用传递:同样有上述过程,但是针对地址的,相当于为该数据所在地址起了一个别名(传地址)
    效率:指针>引用>值传递,但推荐引用传递,代码逻辑上更加紧凑。

3. 结构体struct和共同体union的区别

  1. 结构体将不同类型的数据组合成一个整体,是自定义类型;
    共同体是不同类型的几个变量共同占用一段内存。
  2. 结构体中的每个成员都有自己独立的地址,它们是同时存在的;
    共同体中的所有成员占用同一段内存,它们不能同时存在(会覆盖)。
  3. sizeof(struct)是内存对齐后所有成员长度的总和;
    sizeof(union)是内存对齐后最长数据成员的长度。

4. #define、inline、const的区别

(1) #define

宏定义的常量没有类型,只是简单的替换;在预处理时进行替换;不可以用指针去指向;可以定义简单的函数。

(2) const

const定义的常量有类型名字,存放在静态区域;所定义的变量在编译时确定其值;可以使用指针指向;不可以定义函数。

(3) inline

inline 用于修饰函数,表示在调用该函数的时候直接在调用的地方进行展开,但是这只是向编译器提出这种需求,具体会不会展开取决于编译器本身。内联函数一般为简单函数,这将会减少函数调用时的压栈出栈函数等的开销。

对于普通的常量而言,宏定义的方式缺少类型检查,因而将会导致很多的隐患,而 const 机制可以很好的继承了他的优点,同时也克服了他的缺点。而对于 inline 而言,他是一种很好的替代宏函数的方式,对宏函数进行参数传递的时候并不会产生向宏函数这样的多次求值的错误,而且内联函数同时支持调试。因而我们在使用的过程中应该尽量的使用 const 常量和 inline 函数来替代宏函数的使用。

5. 重载overload、覆盖(重写)override、隐藏(重定义)overwrite的区别

(1) 重载

函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不相同。发生在一个类内部,不能跨作用域。

class Animal
{
public:
    void func1(int tmp)
    {
        cout << "I'm an animal -" << tmp << endl;
    } 
    void func1(const char *s)//函数的重载
    {
        cout << "I'm an animal func1 -" << s << endl;
    }
}
(2) 重定义

也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,指子类的函数屏蔽了与其同名的父类函数。除非将子类强制转换为父类,才可以重新调用父类的函数。

class Animal
{
public:
    void func1(int tmp)
    {
        cout << "I'm an animal -" << tmp << endl;
    }
    virtual void func2(int tmp)
    {
        cout << "I'm virtual animal func2 -" << tmp << endl;
    }
};
 
class Fish :public Animal
{
public:
    void func1()//函数的重定义 会隐藏父类同名方法
    {
        cout << "I'm a fish func1" << endl;
    } 
    void func2(int tmp) //函数的重写, 覆盖父类的方法 override
    {
        cout << "I'm a fish func2 -" << tmp << endl;
    }
};
(3)重写

也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数,必须要有virtual关键字。这样可以使用父类的对象或指针调用子类的函数。

当子类进行重写时,尽量加上override关键字,这样编译器可以检测是否重写,就不会产生不小心打错的情况。
当子类进行函数重写时,如果加上final关键字,则该函数就不能被它的子类继承。

6. malloc和new的区别

new/delete,malloc/free都是动态分配内存的方式

  1. malloc/free是库函数,new/delete是C++运算符
  2. malloc对开辟的空间大小严格指定,而new只需要对象名
  3. new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

7. delete和delete[]的区别

假如使用new[]创建一个对象数组,则delete只会调用第一个对象的析构函数(造成内存泄漏),而delete[]会调用每个成员的析构函数。
用new分配的内存用delete释放,用new[]分配的内存用delete[]释放。

8. 堆heap和栈stack的区别

  1. 大小不同:一般来讲,在32为系统下面,堆内存可达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的,例如,在vc6下面,默认的栈大小好像是1M。
  2. 速度不同:栈比较快,堆相对来说慢
  3. 分配方式不同:栈有静态和动态分配,存放函数的参数值、局部变量,由编译器自动分配释放;
    堆只有动态分配,是由new或malloc分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收。
  4. 因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片,使程序效率降低;
    栈则不会存在这个问题。
  5. 堆的生长方向是向上的,也就是向着内存地址增加的方向;
    栈的生长方式是向下的,是向着内存地址减小的方向增长。
C++内存管理

在C++中,内存被分成五个区:栈、堆、全局区/静态存储区、常量存储区、代码区
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区:存放全局变量和静态变量,程序运行结束操作系统自动释放。
常量存储区:存放的是常量,不允许修改,程序运行结束自动释放。
代码区:存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

int g_var = 0; // g_var 在全局区
char *gp_var;  // gp_var 在全局区

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

9. 虚函数和纯虚函数的区别

虚函数和纯虚函数都可以在子类中被重写

  1. 纯虚函数只有定义,没有实现,要用 =0 来结尾,故其在子函数中必须实现,它更像是对子类的约束(接口);
    虚函数既有定义,又有实现的代码,子函数可以重写父类的虚函数来实现子类的特殊化。
  2. 包含纯虚函数的类,被称为“抽象类”,抽象类不能直接new出对象,只有实现了纯虚函数的子类才能new出对象;
    包含虚函数的类可以直接new出对象。

10.大端存储和小端存储的区别

大端模式: 数据的高字节保存在内存的低地址中。
小端模式:数据的高字节保存在内存的高地址中。

// 在小端模式中
char a[4] = 0x11223344; // 则a[0] = 0x44, a[3] = 0x11

为什么要区分大小端:因为大小端有着各自的优点,比如小端存储当进行强制类型转换时不需要进行字节内容的调整,直接按照数据的大小尽心截断即可。而大端储存方式中符号位永远位于第一个字节,很方便判断正负。

11. 指针函数和函数指针的区别

指针函数:

与普通函数一样,只是它返回的是一个对象的指针。

注意:当函数返回指针时,一定要是一个指向全局变量、静态局部变量(两者均分配到全局存储区)或者分配到堆上的变量(使用malloc或者new分配的对象)的指针,因为局部变量的栈空间会在函数调用结束时被销毁,导致指针将访问一个未知的地址。

函数指针:

指向函数入口地址的指针,其声明需要明确的包含函数的返回类型和参数类型。
假如有函数:

void fun(int a, int b);

如果我们要定义一个指向函数fun的函数指针,则:

void (*p)(int, int);
p = fun; // 指向函数
p(4, 5); // 等同于fun(4, 5)

12. 指针数组和数组指针的区别

指针数组

是一个数组,是一个存放元素是指针的数组,其定义如下:

int* a[10]; //存放十个int*的指针数组;
数组指针

是一个指针,指针的类型是数组类型,即指针指向一个数组的首地址

int (*p)[10];
int a[5][10];
p = a; //p指向int类型 大小为n的数组指针,即该指针指向a的第一行数据
p++; //此时指针将指向a的第二行数组

13. vector和list的区别

vector

和数组类似,是一段连续的内存空间,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符,但是在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

list

由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list不支持随机访问,不支持“+”、“+=”、“<”等,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。

14. extern关键字

extern修饰变量和函数

extern可以置于变量或函数之前,表示变量或函数的定义在别的文件中,提示编译器在其它模块中寻找其定义。
注意:extern只能将其它文件中的非静态全局变量引入,而且在引入的同时不能进行赋值。相比较通过包含头文件的方式来引入其它文件中的变量,通过extern引入更为方便安全(不会扩大数据的作用域,不会破坏封装性)。

extern “C”

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

15. static关键字

限制变量的作用域(隐藏)

没有加static关键字的变量和函数具有全局可见性,加了static后,就会对其他源文件隐藏。

保持变量内容的持久性

被static修饰的变量在程序刚开始运行时就会初始化,并存储在静态存储区(还有全局变量),生存期为整个源程序。

默认初始化为0

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

类成员声明static

在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:

  1. 类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致它仅能访问类的静态数据和静态成员函数。
  2. 不能将静态成员函数定义为虚函数。
  3. 静态数据成员是静态存储的,所以必须对它进行初始化(在类外进行),<数据类型><类名>::<静态数据成员名>=<值>。
  4. 静态成员函数可以通过类名::静态成员函数的方式进行访问,不需要实例化。

注意:和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

16. const关键字

修饰类的成员变量,表示常量不可能被修改
修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数
const函数只能调用const函数,非const函数可以调用const函数

const和指针

int const p1中,const修饰p1的值,故p1的值不可改变,p1只能指向固定的一个变量地址,但可以通过p1来读写这个变量的值。
int const p2或者const int p2中,const修饰p1,故p1的值不可改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p1赋值不同的地址来改变指针指向。

17. mutable关键字

mutalbe的意思是“可变的,易变的”。
在C++中,mutable也是为了突破const的限制而设置的。
被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。

18. volatile关键字

volatile的作用: 确保本条指令不会因编译器的优化而省略,且要求每次直接读值。简单地说就是防止编译器对代码进行优化。当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,而不是使用保存在寄存器中的备份。还可以保证volatile变量间的顺序性,编译器不会进行乱序优化。

一个参数可以即是const又是volatile吗?

可以,一个例子是只读状态寄存器。

19. explicit关键字

只对一个实参的构造函数有效,只能在类内定义,它表示禁止对构造函数进行隐式转换。
当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用,而不能使用赋值初始化。

explicit A(const string &s):a(s) {};
A a("abc");//直接初始化。
a.fun(static_cast<A>(cin));//强制执行显式转换。

20. C++文件编译与执行的四个阶段

C/C++最新2022年面试题总结(持续更新)_第1张图片

  1. 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
  2. 编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。
  3. 汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符,如果我们在文本文件中打开hello.o文件,看到的将是一堆乱码。
  4. 链接阶段:链接器(ld)负责处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。

21、三种访问权限private、protected和public

public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。

三种关键字在继承中的表现形式

当子类是父类的public继承时,父类中的所有权限与原有的保持一致。
当子类是父类的protected继承时,父类中的public会降级为protected,其它不变。
当子类是父类的private继承时,父类中的所有权限都会降级为private类型。

22、面向对象的三大特性

(1)封装

把客观事物封装成抽象的类

(2)继承

实现继承、接口继承、可视继承

(3)多态

同一事物表现出不同事物的能力
重载是编译时多态,虚函数是运行时多态

23、定义和声明的区别

  • 变量的声明:仅仅告诉编译器,有某个类型的变量会被使用,但是编译器不会为它们分配内存
  • 变量的定义:编译器为变量分配内存
  • 函数的声明:仅仅告诉编译器有这个函数的存在,一般在头文件
  • 函数的定义:写明函数体,一般在源文件

中级

1、浅拷贝和深拷贝

浅拷贝:对于基本类型的数据以及简单的对象,它们之间的拷贝非常简单,就是按位复制内存,如下所示,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。

class Base{
public:
    Base(): m_a(0), m_b(0){ }
    Base(int a, int b): m_a(a), m_b(b){ }
private:
    int m_a;
    int m_b;
};
int main(){
    int a = 10;
    int b = a;  //浅拷贝
    Base obj1(10, 20);
    Base obj2 = obj1;  //浅拷贝
    return 0;
}

对于简单的类,默认的拷贝构造函数一般就够用了,我们也没有必要再显式地定义一个功能类似的拷贝构造函数。但是当类持有其它资源时,例如动态分配的内存、指向其他数据的指针等,默认的拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。
深拷贝:将对象和其所持其它资源一并拷贝,深拷贝必须显示地定义拷贝构造函数。
如果一个类拥有指针类型的成员变量,那么一般使用深拷贝,才能将指针指向的内容再复制出一份来,让原有对象和新对象相互独立不受影响。如果类的成员变量没有指针,一般使用浅拷贝。

2、隐式类型转换和显式类型转换

隐式类型转换

隐式类型转换:指不需要用户干预,编译器私下进行的类型转换行为。
转换原则:基本数据类型的转换以低精度到高精度,即保证精度不丢失;自定义对象的转换以子类对象到父类对象。
隐式转换风险: 隐式转换的风险一般存在于自定义的类构造函数中。 按照默认规定,只有一个参数的构造函数也定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象。
explicit关键字:c++ 中的 explicit 关键字只能用于修饰只有一个参数的类构造函数,即作用为:禁止隐式调用类内的单参数构造函数。

显式类型转换

显示类型转换就是指强制类型转换,主要有两种方式:函数型和类C型。

double x = 10.3;
int y;
y = int (x);    // 函数型
y = (int) x;    // 类C型

这些类型转换的通用形式的功能足以满足大多数基本数据类型的需求。但是,这些操作符可以不加区别地应用于类和指向类的指针上,这可能导致代码在语法正确的情况下导致运行时错误。编译器检查不出错误,可能导致运行时出错。对于指针来说,不受限制的显式类型转换允许将任何指针转换为任何其他指针类型,而不依赖于指针所指向的类型。
所以C++引入的四种类型转换,不同场景下不同需求使用不同的类型转换方式,同时有利于代码审查。
static_cast
const_cast
dynamic_cast
reinterpret_cast

3、auto关键字

C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型。
注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。因为C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
auto 类型推导的简单例子:

auto n = 10;	//10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
auto f = 12.8;	//12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
auto p = &n;	//&n 的结果是一个 int* 类型的指针,所以推导出变量 p 的类型是 int*。
auto url = "http://c.biancheng.net/cplus/";	//由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。
// auto与其它具体类型混合使用
int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int
auto的限制
  1. 使用 auto 的时候必须对变量进行初始化
  2. auto 不能在函数的参数中使用
  3. auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中
  4. auto 关键字不能定义数组
  5. auto 不能作用于模板参数
auto的应用
  1. 使用 auto 定义迭代器,迭代器的类型有时候比较复杂,书写起来很麻烦
int main(){
    vector< vector<int> > v;
    //vector< vector >::iterator i = v.begin();
    auto i = v.begin();  //使用 auto 代替具体的类型
    return 0;
}
  1. auto 用于泛型编程,当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛型编程中。

4、decltype关键字

decltype 是“declare type”的缩写,译为“声明类型”, 它与auto作用一样,都用来在编译时期进行自动类型推导。
它与auto的用法区别:

auto varname = value;	//auto根据=右边value来推导变量类型
decltype(exp) varname = value;	//decltype根据exp表达式来推导变量类型,与value无关,故可以不用初始化
decltype(exp) varname;
decltype的exp的推导规则
  1. 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  2. 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  3. 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
//普通表达式
int a = 0;
decltype(a) b = 1;  //b 被推导成了 int
decltype(10.8) x = 5.5;  //x 被推导成了 double
decltype(x + 100) y;  //y 被推导成了 double
//函数声明
int& func_int_r(int, char);  //返回值为 int&
int&& func_int_rr(void);  //返回值为 int&&
int func_int(double);  //返回值为 int
//decltype类型推导
decltype(func_int_r(100, 'A')) e = 1000;  //a 的类型为 int&
decltype(func_int_rr()) f = 0;  //b 的类型为 int&&
decltype(func_int(10.5)) g = 0;   //c 的类型为 int
//带有括号的表达式
decltype(a) h = 0;  //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
decltype((a)) b = 100;  //obj.x 带有括号,符合推导规则三,b 的类型为 int&。
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0;  //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
decltype(n = n + m) d = c;  //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&

5、lambda匿名函数

匿名函数即没有名字的函数。使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。

定义方式

C++中的匿名函数通常定义为

[capture](parameters)->return-type{body}

当parameters为空的时候,()可以被省去,当body只有一个return或者返回为void,那么”->return-type“可以被省去,下面对其中的参数一一解释
capture:

  • [] //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
  • [x, &y] //x 按值捕获, y 按引用捕获.
  • [&] //用到的任何外部变量都隐式按引用捕获
  • [=] //用到的任何外部变量都隐式按值捕获
  • [&, x] //x显式地按值捕获. 其它变量按引用捕获
  • [=, &z] //z按引用捕获. 其它变量按值捕获

parameters:存储函数的参数
return-type:函数的返回值
body:函数体

使用例子

直接写在main函数中,与inline函数相似。

int main(){
    int x=1,y=2,z=0;
    auto add = [&z](auto x,auto y){z=x+y;return z;};
    auto res = add(x,y);
    cout<<res<<z<<endl;
}

与sort()函数等参数中可传入函数的方法等搭配

int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素按照从小到大进行排序
    sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
    return 0;
}

6、C++11新版for循环

C++ 11 标准中,为 for 循环添加了一种全新的语法格式,(与旧版限定遍历范围不同,新版的只会逐个遍历指定序列的每个元素),如下所示:

// declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。可以用auto
// expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。
for (declaration : expression){
    //循环体
}

下面程序演示了如何用 C++ 11 标准中的 for 循环遍历实例一定义的 arc 数组和 myvector 容器:

int main() {
    char arc[] = "http://c.biancheng.net/cplus/11/";
    //for循环遍历普通数组
    for (char ch : arc) {
        cout << ch;	// 输出的字符串和 "!" 之间还输出有一个空格,这是因为新格式的 for 循环在遍历字符串序列时,
        			// 不只是遍历到最后一个字符,还会遍历位于该字符串末尾的 '\0'(字符串的结束标志)。
    }
    cout << '!' << endl;	//http://c.biancheng.net/cplus/11/ !
    vector<char>myvector(arc, arc + 23);
    //for循环遍历 vector 容器
    for (auto ch : myvector) {	//这里的 ch 不是迭代器类型,而表示的是 myvector 容器中存储的每个元素。
        cout << ch;
    }
    cout << '!';	//http://c.biancheng.net/!
     for (int num : {1, 2, 3, 4, 5}) {
        cout << num << " ";	//1 2 3 4 5
    }
    return 0;
}

7、智能指针

传统的指针,没有及时释放不再使用的内存资源,容易造成堆内存泄漏(忘记释放),二次释放(会导致程序运行崩溃),资源已被释放但是指针没有改变执行(成为了野指针)。
智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存
C++11 标准提出了三个新的智能指针:unique_ptr、shared_ptr和 weak_ptr。它们是 C++11 标准提供的模板类,用于管理 new 申请的堆内存空间。需要包含头文件memory
这些模板类定义了一个以堆内存空间(new 申请的)指针为参数的构造函数,在创建智能指针对象时,将 new 返回的指针作为参数。同样,这些模板类也定义了析构函数,在析构函数中调用 delete 释放 new 申请的内存。当智能指针对象生命周期结束时,系统调用析构函数释放 new 申请的内存空间。

unique_ptr

创建 unique_ptr 智能指针对象的语法格式如下所示:

unique_ptr<T>  智能指针对象名称(指针);	//指针指用new运算符申请堆空间返回的指针

unique_ptr 智能指针的用法示例代码如下所示:

unique_ptr<int> pi(new int(10));	//智能指针对象 pi,用于管理一个 int 类型堆内存空间指针;
class A {};
unique_ptr<A> pA(new A);	//智能指针对象 pA,用于管理一个 A 类型的堆内存空间指针。

当程序运行结束时,即使没有 delete,编译器也会调用 unique_ptr 模板类的析构函数释放 new 申请的堆内存空间。
unique_ptr 智能指针对象之间不可以赋值,如果需要实现 unique_ptr 智能指针对象之间的赋值,可以调用 C++ 标准库提供的 move() 函数,示例代码如下所示:

unique_ptr<string> ps(new string("C++"));
unique_ptr<string> pt;
pt = move(ps);   //正确,可以通过编译
pt = ps;   //错误,不能对unique_ptr智能指针赋值
shared_ptr

shared_ptr 是一种智能级别更高的指针,它在实现时采用了引用计数的方式,多个 shared_ptr 智能指针对象可以同时管理一个 new 对象指针。

每增加一个 shared_ptr 智能指针对象,new 对象指针的引用计数就加 1;当 shared_ptr 智能指针对象失效时,new 对象指针的引用计数就减 1,而其他 shared_ptr 智能指针对象的使用并不会受到影响。只有在引用计数归为 0 时,shared_ptr 才会真正释放所管理的堆内存空间。
shared_ptr 提供了一些成员函数以更方便地管理堆内存空间,下面介绍几个常用的成员函数。

  • get()函数用于获取对象指针
  • use_count()函数用于获取new对象的引用次数
  • reset()函数用于取消shared_ptr智能指针对象对new对象的引用

下面通过案例演示 shared_ptr 智能指针的使用,C++ 代码如下:

int main()
{
    //创建shared_ptr智能指针对象language1、language2、language3
    shared_ptr<string> language1(new string("C++"));
    shared_ptr<string> language2 = language1;
    shared_ptr<string> language3 = language1;
    //通过智能指针对象language1、language2、language3调用get()函数
    cout << "language1: " << language1.get() << endl;	//language1: 0x1410e70
    cout << "language2: " << language2.get() << endl;	//language2: 0x1410e70
    cout << "language3: " << language3.get() << endl;	//language3: 0x1410e70
    cout << "引用计数:";
    cout << language1.use_count() <<" ";	// 3
    cout << language2.use_count() <<" ";	// 3
    cout << language3.use_count() <<endl;	// 3
    language1.reset();
    cout << "引用计数:";
    cout << language1.use_count()<<" ";		// 0
    cout << language2.use_count()<<" ";		// 2
    cout << language3.use_count() << endl;	// 2
    cout << "language1: " << language1.get() << endl;	//language1: 0
    cout << "language2: " << language2.get() << endl;	//language2: 0x1410e70
    cout << "language3: " << language3.get() << endl;	//language3: 0x1410e70
    return 0;
}
weak_ptr

相比于 unique_ptr 与 shared_ptr,weak_ptr 智能指针的使用更复杂一些,它可以指向 shared_ptr 管理的 new 对象,却没有该对象的所有权,即无法通过 weak_ptr 对象管理 new 对象。
weak_ptr 最常见的用法是验证 shared_ptr 对象的有效性。

8、内联函数

为消除函数调用的时空开销,C++提供一种提高效率的方法,即在编译时将函数调用处用函数体来替换,类似于宏定义,这种函数称为内联函数(Inline Function)。
用法:在函数定义前添加关键字inline,一般只将短小的、频繁调用的函数声明为内联函数。
注意:内联函数里面不能使用循环语句

内联函数和宏定义的区别
  • 宏定义只是简单的字符串替换,内联函数可以进行参数类型的检查,且具有返回值
  • 宏定义没有括号容易引起歧义,而内联函数没有

9、友元函数

借助友元(friend),可以使其它类中的函数或全局函数访问当前类的private成员,(即、将B类中的函数或者全局函数,在A类中声明为友元函数,那么该函数便可以访问A类中的一切成员)

class Student{
public:
	Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){}
	friend void show(Student *pstu); // 将全局函数show声明为友元函数
private:
	char *m_name;
	int m_age;
	float m_score;
};

void show(Student *pstu){
	cout << pstu->m_name << pstu->m_age << pstu->m_score << endl;		// 访问Student的私有成员
	// cout << m_name << m_age << m_score << endl; 	// 不能直接使用Student的成员,要借助对象,因为show不是成员函数,没有this指针
}

上述可将其他类的成员函数声明为友元函数;还可以将整个类声明为另一个类的“朋友”,即友元类,友元类中的所有成员都是另一个类的友元函数。

10、构造函数

  • 函数名和类名相同,没有返回值,void都不行
  • 不需要用户显式调用,在创建对象时自动执行
  • 必须是public的
  • 可以有多个重载的构造函数,根据创建函数的实参来确定使用哪个
  • 构造函数不能被继承,只能调用父类的构造函数,使用super来调用父类的带参构造函数
构造函数的调用顺序

父类构造函数 -> 子类成员函数的构造函数 -> 子类构造函数

构造函数的类型

默认构造函数
初始化构造函数(带参数)
拷贝构造函数:当使用实例化对象初始另一个对象时调用
移动构造函数:(move和右值引用)
委托构造函数
转换构造函数

11、析构函数

  • 函数名为类名前面加~,没有返回值,void都不行
  • 不需要用户显示调用,销毁对象时自动执行
  • 不能被重载,只能有一个析构函数
析构函数的调用顺序

子类析构函数 -> 子类成员函数的析构函数 -> 父类析构函数

12、虚函数

使用虚函数,可以使父类指针访问子类的成员(包括成员函数和成员变量),它的唯一用途就是构成多态,即,父类指针即可以按照父类的方式做事,又可以按照子类的方式做事,有多种形态。

  • 只需要在虚函数声明处加上virtual关键字,定义处可以不加
  • 只有子类的虚函数覆盖父类的虚函数(函数原型相同),才构成多态
  • 构造函数不能是虚函数,析构函数可以
  • 静态函数不能声明为虚函数
纯虚函数
  • 纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
  • 包含纯虚函数的类称为抽象类,它不能直接被实例化,通常作为父类,需要子类将纯虚函数实现之后才可以创建对象。
  • 只有类中的虚函数才能被声明为纯虚函数,其它函数不可以。
class{
public:
	virtual void area(int a, int b) = 0;	// 将area声明为纯虚函数,其子类将其实现,才可以创建对象
};

13、运算符重载

运算符重载规则:

  • 只能重载已有的且被允许重载的运算符。
  • 重载之后,运算符的优先级结合性不会改变,也不能够改变。
  • 重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型

15、内存泄漏以及如何避免

内存泄漏

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了

避免内存泄漏的方法

计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
一定要将基类的析构函数声明为虚函数
对象数组的释放一定要用delete []

16、C++11新特性

  • nullptr替代 NULL
  • 推导:auto和decltype
  • 基于范围的 for 循环
  • 类和结构体的统一初始化列表
  • Lambda 表达式(匿名函数)
  • 新增容器:array(栈内存的vector)、forward_list(单向链表的list)、tuple(元组)
  • 右值引用和move语义

17、异常处理机制

异常情况:
  • 语法错误(编译错误):比如变量未定义、括号不匹配、关键字拼写错误等,这类错误可以及时被编译器发现
  • 运行时错误:比如数组下标越界、系统内存不足等,这类错误能通过编译且能运行,但运行时会报错,导致程序崩溃,需要异常处理机制
异常处理机制:
  • throw:将异常抛出到上一级处理
  • catch:将异常捕获在本级进行处理

高级

1、new的三种类型

(1)plain new

普通不加任何参数的new,它在空间分配失败的情况下,抛出异常bad_alloc,而不是返回NULL

int main()
{
	try
	{
		char *p = new char[10e11];	// 内存分配失败
		delete p;
	}
	catch (const bad_alloc &ex)
	{
		cout << ex.what() << endl;
	}
	return 0;
}
//执行结果:bad allocation
(2)nothrow new

加参数nothrow,在空间分配失败时不抛出异常,而是返回NULL

int main()
{
	char *p = new(nothrow) char[10e11];	// 内存分配失败
	if (p == NULL) 
	{
		cout << "alloc failed" << endl;
	}
	delete p;
	return 0;
}
//运行结果:alloc failed
(3)placement new

这类new允许在一块已经分配成功的内存上重新构造对象,不用担心内存分配失败,因为它不分配内存,只是调用对象的构造函数。

2、零拷贝

零拷贝是一种避免CPU将数据从一块存储拷贝到另一块存储的技术,可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数**emplace_back()很好地体现了零拷贝技术,它跟push_back()**函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数向容器尾部添加元素时,首先会创建这个元素,然后将这个元素拷贝或移动到容器中;而使用emplace_back()是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。

3、strcpy和memcpy的区别

  • 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  • 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
  • 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

4、回调函数

**定义:**当发生某种事件时,系统或者其它函数自动调用的一段函数(相当于中断处理函数和QT中的槽函数)
**本质:**回调函数就是一个通过函数指针调用的函数,把函数的地址作为参数传递给另一个函数

5、内存池

内存池(memory pool)是一种内存分配方式,在真正使用内存之前,先申请分配一定数量的内存块作内存池,当有新内存需求时,从内存池中分出一部分内存块,避免了多次向内存申请,产生大量的内存碎片。

你可能感兴趣的:(c++,后端,面试,c语言,学习)