C++ 笔面试知识点大全 附超详细解析 【持续更新中】 (校招/实习/大厂/笔试/面试)

目录

      • 关键字
        • auto
        • decltype
        • const
        • static
        • extern
        • explicit
        • volatile
        • inline
      • Lambda表达式
      • 顶层const和底层const
      • 类型转换
      • 多态,虚函数,隐藏和重写
          • 虚函数的实现机制:
          • 构造函数和析构函数能否为虚函数
          • override
      • 钻石(菱形)继承
      • 内存管理
          • 堆和栈的区别
          • 内存对齐
          • new和malloc的区别
      • 智能指针
      • 左值与右值
      • 指针和引用的区别
      • STL 容器
      • 协程
      • size_t
      • Union

关键字

auto

自动类型推导,编译器在编译期间通过初始值推导出变量的类型

使用auto定义的变量必须要有初始值

int a=2;
double b=1.3;
auto c = a+b;  //c为double类型,值为3.3
decltype

声明类型推导,和auto一样是在编译时期进行的自动类型推导,通过表达式自动推导出要定义的变量的类型,但不使用该表达式的值来初始化变量,这是与auto的区别

decltype(a+b) d = 0;
const

修饰变量时 表示定义常量,不可修改, 因此const对象声明时必须初始化

const修饰的成员函数不能修改类中的数据成员,也不能调用非const函数(还是为了防止其修改数据)

const修饰的函数可以重载,也可以重载为非const函数,重载的非const函数可以修改数据成员。调用时会优先调用其非const的重载。

关于const修饰指针和引用的问题参见下文的底层const与顶层const

例如:

class P{
	int x;
	public:
    void func(int a) const
    {
      //x=a;  会产生编译错误,const成员函数不能修改数据成员
      cout<<"const func"<<endl;
      return;
    }
    void func(int a,int b)
    {
      x=a+b;
      cout<<"non-const override"<<endl;
      return;
    }
    void print()
    {
      cout<<x<<endl;
    }

};
int main()
{
  P p;
  p.func(1,2);
  p.print();
  return 0;
}
static

1.作用于文件作用域:(在文件中直接修饰变量或函数) ,表示这些变量和函数只在本文件中可见,其他文件不可见,可以避免重定义问题

2.作用于函数作用域: 修饰局部静态变量,使得该变量只会进行一次初始化(将其生命周期延长到程序结束运行),不会在每次调用该函数时重新初始化,但只在该函数中可见。如下:

int count()
{
    static int c = 0;
    return ++c;
}
int main()
{
    std::cout << count();
    std::cout << count();
    std::cout << count();
    return 0;
}
//输出:123

3.用于类成员的声明:静态数据成员和静态成员函数,static表示这些数据和函数是所有类对象共享的一种属性,而非每个类对象独有,在一个类对象中修改静态数据会影响所有类对象,因为使用的是同一份拷贝。

需要注意的是静态数据成员必须在类外进行初始化,并且可以不实例化直接对static静态成员进行访问,如下所示:

class A
{
public:
    static int value;
    static void print()
    {
        std::cout << value;
    }
};
int A::value = 0;  //在类外初始话静态数据成员,不可缺少

int main()
{
    int a = A::value;
    A::print();
    return 0;
}
extern

修饰全局变量,函数时:显式说明该变量/函数定义在其他文件中

extern int a; 
extern void fun();

与“C”连用,表示以下内容按C语言规则编译, 从而实现C和C++的混合编译

extern "C"
{
	extern int i;
	extern void func();
}
explicit

只用于修饰单参数的类构造函数,声明该构造函数必须显式调用,不能隐式调用;

什么是类构造函数的隐式调用: 当一个类具有单参构造函数时,可以直接使用赋值运算符创建类的对象,编译器会隐式调用单参构造函数进行初始化,如下所示:

class A
{
private:
    int value;
public:
    A(int _value):value(_value){}
    void print()
    {
    	cout<<value<<endl;
    }
};
int main()
{
    A a=7;  //构造函数的隐式调用
    a.print();  
    return 0;
}

为了防止被隐式调用,可以加上explicit关键字:

class A
{
private:
    int value;
public:
    explicit A(int _value):value(_value) {}
    void print()
    {
    	cout<<value<<endl;
    }
};

此时再进行之前的隐式调用构造函数的初始化操作A a=7就会报错。

volatile

告知编译器不要优化该变量,阻止编译器将该变量读取到寄存器而不写回, CPU读写被volatile修饰的变量时将会直接使用其内存地址,而非只是修改寄存器中的值,从而保证缓存一致性。

使用场景:当一个变量可能同时被多个线程访问且该访问不可检测和控制时,如果该变量被优化读写(缓存到寄存器),那么内存中的值被修改时寄存器中的缓存可能不会同步修改从而导致缓存不一致。 此时应使用volatile修饰该变量。

inline
  1. 用于定义内联函数,内联函数会在调用时直接在调用点展开,而不是进行地址跳转,这样可以减少函数调用的开销。提高运行效率。编译器在编译时期就会将内联函数的函数体嵌入每个调用该函数的语句中,将所有内联函数的调用表达式用其函数体替换。

  2. 与#define定义的区别:#define只会单纯的替换而不会对函数本身进行参数检验等,容易出错。

inline定义的函数和普通函数一样会被编译器检查

3. 内联函数可以定义在头文件中,并被多个cpp文件include时也不会产生重定义错误。

类内定义的成员函数默认是内联函数, 不需要加inline声明

class A
{
    void print()  //默认是内联函数
    {
        std::cout<<"A";
    }
}

类外定义的


Lambda表达式

C++ 11引入的特性,用于实现闭包(匿名函数对象)

基本写法:

[capture](params)->type{body}

type为返回值的类型,当无返回值或者返回值类型明确时可以省略

在capture中定义参数使用什么方式捕获:

  1. [] :capture为空, 表示将不使用外部变量

  2. [x,&y] : 使用值传递的方式捕获外部变量x,使用引用传递的方式捕获外部变量y,并可以在lambda表达式中使用外部变量x和y

  3. [&] 所有外部变量按引用传递捕获

  4. [x,&] x按值传递捕获,其他外部变量按引用传递捕获

  5. [=] 所有外部变量按值传递捕获

  6. [&x,=] x按引用传递捕获,其他外部变量按值传递捕获

    使用引用传递方式捕获的外部变量可以在Lambda表达式内被修改,而值传递方式捕获的外部变量不会受lambda的任何影响(因为只是创建了一个副本)

    常用场景

STL vector类的sort函数,使用lambda表达式定义排序规则

// vector> arr(n,vector(2));
sort(arr.begin(),arr.end(),
        [](auto &a,auto &b)->bool{return a[0]>b[0]||(a[0]==b[0] && a[1]

顶层const和底层const

顶层const(top-level const):指针本身是const,不可修改指针的值(指针不能再指向别的地址),即该指针为指向int类型的const指针, 请参考下面的例子:

int a=1;
int b=2;
int * const p=&a;
p=&b; //错误,指针是const,不能修改
*p=b; //正确,指针指向的值不是const可以修改

底层const(low-level const):指针指向的对象是const,不可修改指针指向的值,即该指针为指向const int类型的指针, 需要注意的是const int指针也可以指向非const的int对象,但在指针看来是const int,不能修改。

参考下面的例子

const int *p=&a;
p=&b; //正确
*p=b; //错误
a=b;  //正确

一个指针可以既具有顶层const又具有底层const:

const int * const p=&a;

类型转换

const_cast

const_cast关键字用于指针或引用, 只能去除底层const, 即把一个指向const int类型的指针改成指向int类型的指针,不能将const指针的const属性去除, 也不能将指向的对象的const属性去除

int a=1;
const int b=2;
int c=3;

const int *p1=&a;
const int *p2=&b;

int *pa=const_cast<int*>(p1);
int *pb=const_cast<int*>(p2);

*pa=c; //正确,现在pa为指向int类型的指针且a不是const
*pb=c; //错误,const_cast不能去除b的const属性,属于未定义行为

static_cast

可以用于任何具有明确定义类型的类型转换,前提是不能包含底层const

例如:

  • 原有的自动类型转换,例如 short 转 int、int 转 double、const 转非 const、向上转型等;
  • void 指针和具体类型指针之间的转换,例如void *int *char *void *等;
  • 有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如 double 转 Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。

下面的例子中为了得到整数除法的精确结果,使用static_cast将int类型强制转换为double类型

int a=100;
int b=3;
double res = static_cast<double>(a)/b;

reinterpret_cast

改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。

dynamic_cast

将一个基类对象指针(或引用)转换成继承类指针或引用,转换失败返回NULL, 引用返回失败则抛出bad_cast异常。

其他三种转换是在编译时期完成的,而动态类型转换正如其名是在运行时才进行的,并且运行时会进行类型检查。

dynamic_cast也可以将继承类对象指针或引用转成基类


多态,虚函数,隐藏和重写

为不同的数据类型的实体提供统一的接口

编译时多态(静态多态): 通过模板和函数重载实现

模板:以不同的模板参数具现化导致调用不同的函数,或者因函数重载而根据参数列表来确定调用的函数版本。

实现静态多态的类之间不需要有继承关系,但需要有相同的隐式接口

例如:

class A
{
	public:
		void Print()
		{
			std::cout<<"A\n";
		}
}
class B
{
	public:
		void Print()
		{
			std::cout<<"B\n";
		}
}
template <typename T>
void TemplatePrint(T & t)
{
	t.Print();
}
int main()
{
	A a;
	B b;
	TemplatePrint(a);
	TemplatePrint(b);
	return 0;
}

函数重载:通过编译时的函数重载解析来实现多态,名字相同的函数必须有不同数量或类型的参数

运行时多态(动态多态)

通过继承和虚函数实现

当我们使用基类的引用或指针来调用基类中定义的虚函数时,直到运行时才会决定执行哪个版本。判断的依据是引用或指针绑定的对象的真实类型。

当期仅当通过指针或引用调用虚函数时才会在运行时解析。且只在这种情况下对象的动态类型才可能和静态类型不同。

虚函数必须是基类的

例如现在有两个类A和B,A是B的基类。A* p=new B; 类指针对象p的静态类型为A,即它的声明类型,动态类型为B,这是在运行时动态绑定的

class Animal
{
public :
    virtual void shout() = 0;
};
class Dog :public Animal
{
public:
    virtual void shout(){ cout << "汪汪"<<endl; }
};
class Cat :public Animal
{
public:
    virtual void shout(){ cout << "喵喵"<<endl; }
};


int main()
{
    Animal * anim1 = new Dog;  //动态绑定指针类型
    Animal * anim2 = new Cat;
     
   //藉由指针(或引用)调用的接口,在运行期确定指针(或引用)所指对象的真正类型,调用该类型对应的接口
    anim1->shout();
    anim2->shout();
 
   return 0;
}
虚函数的实现机制:

虚函数是通过虚函数表来实现的,虚函数表包含了一个类(所有)的虚函数的地址,在有虚函数的类对象中,它内存空间的头部会有一个虚函数表指针(虚表指针),用来管理虚函数表。当子类对象对父类虚函数进行重写的时候,虚函数表的相应虚函数地址会发生改变,改写成这个虚函数的地址,当我们用一个父类的指针来操作子类对象的时候,它可以指明实际所调用的函数(即动态类型绑定为子类)。

使用基类的引用或指针调用虚函数时会在运行时动态绑定,通过虚函数表指针与虚函数表确定调用哪个函数。

虚函数表存在于类中而非类对象中,同一个类的不同对象共享一张虚函数表(为了节省内存空间),实例对象的空间头部有指向该虚函数表的虚表指针。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

(《C++ Primer》 p536)

构造函数和析构函数能否为虚函数

​ 1.不能,虚函数需要一个指向虚表的虚表指针才能调用,而该指针存在于类对象的内存空间中,类对象还未构造时该指针不存在,也就无法调用,因此构造函数不能是虚函数。从使用逻辑上来讲,构造函数是在创建对象时该对象自身调用的,而不是通过父类的指针或引用来调用的,设为虚函数毫无意义。

​ 2.析构函数可以是虚函数。当使用基类的指针或引用来操作派生类对象时,为了防止析构时仅仅析构基类而不析构派生类,需要将基类的析构函数设为虚函数

override

需要注意的是,在子类中定义与基类虚函数同名但是形参列表不同的函数时,视作对该虚函数的重载而非是重写(覆盖),即该函数为独立的函数,也不会被写入虚表。

为了避免人为错误导致重写变成重载,可以在想要重写时加上override关键字, 这样如果与基类中的虚函数形参列表不一致,将会无法通过编译。

class A
{
    virtual void f1();
    virtual void f2();
    virtual void f3();
};
class B:A
{
    void f1() override; //正确,是对基类中虚函数f1的覆盖
    void f2(int a) override; //错误,加了override不允许重载
    void f3(int a);  //正确,是对基类中虚函数f3的重载
};

隐藏

子类中有和父类的同名同参数列表的非虚函数时,调用该函数将默认调用子类的,父类中的该函数被隐藏


钻石(菱形)继承

指一个子类的两个父类继承自同一个基类,形成菱形的继承关系。

这样会存在二义性的问题,因为两个父类会对公共基类的数据和方法产生一份拷贝,因此对于子类来说读写一个公共基类的数据或调用一个方法时,不知道是哪一个父类的数据和方法,会导致编译错误。

可以采用虚继承的方法解决这个问题(父类继承公共基类时用virtual修饰),这样就只会创造一份公共基类的实例,不会造成二义性,代码如下。

class Base
{
  protected:
  	int a;
  public:
   void print()
  {
    cout << "base" << endl;
  }
};
class FaA : virtual public Base  //使用虚继承,不会影响自身,只会影响该类的子类
{
};
class FaB : virtual public Base
{ 
};
class Son : public FaA, public FaB
{
};
int main()
{
  Son s;
  s.print();
  return 0;
}

或者使用双冒号运算符显式标明该变量具体来自哪个类:

class Base
{
protected:
	int a;
public:
	void print()
	{
		cout << "base" << endl;
	}
};
class FaA : public Base  
{
	
};
class FaB : public Base
{
};
class Son : public FaA, public FaB
{
	void seta(int _a)
	{
		FaA::a = _a;
	}
};
int main()
{
	Son s;
	s.FaA::print();
	
	return 0;
}


内存管理

内存区域:

(操作系统的概念)

C语言中使用malloc, free动态分配和释放空间,能分配较大的内存

C++中使用new和delete申请和释放,用来存储程序动态分配的对象,称为自由空间(自由存储区)。可能造成内存泄漏和非法指针

为函数的局部变量分配内存,能分配较小的内存

全局/静态存储区

用于存储全局变量和静态变量

常量存储区

专门用来存放常量

代码区

存放CPU执行的机器指令,函数体的二进制代码

堆和栈的区别

(1)堆中的内存需要手动申请和手动释放,栈中内存是由操作系统自动申请和自动释放;

(2)堆能分配的内存较大(4G(32位机器)),栈能分配的内存较小(1M);

(3)在堆中分配和释放内存会产生内存碎片,栈不会产生内存碎片;

内存碎片:所有的内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。假设当某个客户请求一个 43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此产生的多余空间就叫内部碎片。

(4)堆的分配效率低,栈的分配效率高;

(5)堆地址从低向上,栈由高向下。

如何限制类的对象在堆上创建和在栈上创建?

C++中类的对象的建立分为静态建立和动态建立。

静态建立: 在栈上分配内存。直接调用类的构造函数创建对象是静态建立,如:A a();

动态建立: 在堆上分配内存。使用new运算符在堆上创建对象,程序会在堆上寻找合适的内存并分配,然后调用构造函数创建对象并返回指向该对象的指针,如:A * p=new A();

内存对齐

(1)内存对齐的原因:关键在于CPU存取数据的效率问题。为了提高效率,计算机从内存中取数据是按照一个固定长度的。比如在32位机上,CPU每次都是取32bit数据的,也就是4字节;若不进行对齐,要取出两块地址中的数据,进行掩码和移位等操作,写入目标寄存器内存,效率很低。内存对齐一方面可以节省内存,一方面可以提升数据读取的速度;

(2)内容:内存对齐指的是C++结构体中的数据成员,其内存地址是否为其对齐字节大小的倍数。

(3)对齐原则:1)结构体变量的首地址能够被其最宽基本类型成员的对齐值所整除;2)结构体内每一个成员的相对于起始地址的偏移量能够被该变量的大小整除;3)结构体总体大小能够被最宽成员大小整除;如果不满足这些条件,编译器就会进行一个填充(padding)。

(4)如何对齐**:**声明数据结构时,字节对齐的数据依次声明,然后小成员组合在一起,能省去一些浪费的空间,不要把小成员参杂声明在字节对齐的数据之间。

注意:

  1. 32位系统的指针占4字节,64位系统的占8字节,

  2. 该内存对齐原则适用于结构体对象和类对象

new和malloc的区别

new返回指定类型的指针,自动计算需要的内存大小

malloc必须用户指定要分配的内存大小,且返回void* 指针,必须强制转换为实际类型指针


智能指针

用于解决内存泄漏问题。

智能指针是模板类,行为类似常规指针,但是负责自动释放所指向的对象(占用的内存)

shared_ptr

(C++ Primer P400)

允许多个指针指向同一个对象,

可以使用new运算符返回的指针来初始化,例如:

struct A
{
	int val;
	A(int _val) :val(_val) {}
};
int main(){
shared_ptr<A> p(new A(1));
}

最安全的分配和使用动态内存的方法是调用make_shared标准库函数

//p1指向一个值为"aaaaa"的string
shared_ptr<string> p1 = make_shared<string>(5,'a');  
//使用auto来简化,p2指向一个空的vector
auto p2=make_shared<vector<int>>();

我们可以调用shared_ptr类的拷贝构造函数来创建一个新的智能指针指向同一个对象

auto p1 = make_shared<vector<int>>();
auto p2(p1);  //p1和p2指向同一个对象

每个shared_ptr都有一个关联的计数器,称为引用计数,当我们拷贝一个shared_ptr时计数器就会递增。

当一个shared_ptr被赋予一个新的值或者被销毁(比如离开作用域),计数器就会递减。

当一个shared_ptr的引用计数变为0,就会自动释放自己所管理的对象。(调用对象的析构函数)

循环引用问题:

如果两个shared_ptr互相指向彼此所在的对象,就会出现循环引用问题,导致双方的资源都无法释放。参见下面的例子:

struct A
{
	shared_ptr<A> next;
	int val;
	A(int _val) :val(_val) {}
	~A()
	{
		std::cout << "~A()\n";
	}
};

int main()
{
	auto p1 = make_shared<A>(1);
	auto p2 = make_shared<A>(2);
	p1->next = p2;
	p2->next = p1;
	return 0;
}

运行后可以发现p1和p2指向的结构体对象在程序结束时都没有调用析构函数,没有释放资源。

这是因为shared_ptr是一种强引用,当引用自身存在时,引用的对象的资源也不能被销毁。如此当引用的对象本身包含有强引用时将会导致引用自身既是引用又是引用的对象所包含的资源。

循环引用的问题可以引入weak_ptr来解决,参见下文weak_ptr

unique_ptr

独占资源所有权的智能指针,一个对象在同一时间只能被一个指针指向。该指针不能拷贝构造和赋值,但可以进行移动构造和移动赋值构造(使用move函数),示例如下:

std::unique_ptr<A> p1(new A(1));
std::unique_ptr<A> p2=std::move(p1);

关于标准库函数move可以参见下文 右值引用

weak_ptr

基于shared_ptr实现,用于解决shared_ptr循环引用的问题

以上文说明循环引用的例子:

struct A
{
	weak_ptr<A> next;
	int val;
	A(int _val) :val(_val) {}
	~A()
	{
		std::cout << "~A()\n";
	}
};

int main()
{
	auto p1 = make_shared<A>(1);
	auto p2 = make_shared<A>(2);
	p1->next = p2;
	p2->next = p1;
	return 0;
}

我们把对象内部的指针换成weak_ptr后就能正确地释放资源了。这是因为weak_ptr是一种弱引用,自身的存在和所指向资源的存在相互独立,指向资源可以在其析构之前被销毁

weak_ptr可以通过expired函数获知资源是否有效 (通过获取引用计数查看其是否为0),这一点shared_ptr无法做到,因为共享指针不允许自身析构前(引用计数大于0时)指向资源被销毁

weak_ptr还可以通过lock函数,使用自身指向的资源和引用计数构造一个shared_ptr,来将资源锁住防止其销毁。如果计数器中引用计数为0, 那么返回的shared_ptr将会是一个无参构造的shared_ptr。

shared_ptr<_Ty> lock() const _NOEXCEPT
		{	// convert to shared_ptr
		return (shared_ptr<_Ty>(*this, false));
    	//用当前weak_ptr构造shared_ptr,计数器不变,若引用计数为0,返回无参构造的shared_ptr
		}

参考:从源码理解智能指针(二)—— shared_ptr、weak_ptr_HerofH_的博客-CSDN博客


左值与右值

《C++ Primer》 P121

当一个对象被用作 右值(rvalue) 的时候,用的是对象的值(内容)

当一个对象被用作 左值(lvalue) 的时候,用的是对象的身份(在内存中的位置)

  • 赋值运算符 = 需要一个(非const)左值作为左侧运算对象,得到的结果也是左值

  • 取地址符& 作用于一个左值,返回一个指向该左值的指针,这个指针是右值

  • 内置解引用运算符*,下标运算符[],迭代器解引用运算符,string和vector的下标运算符 结果都是左值

右值引用

《C++ Primer》 P471, 从4行代码看右值引用 - qicosmos(江南) - 博客园 (cnblogs.com)

右值引用是必须绑定到右值的引用,使用&&来获得

int i=42;
int &r=i;  //左值引用
int &&rr=i*42; //右值引用,乘法运算的临时结果是右值
int &&rrr=i; //错误,i是左值

​ 右值要么是字面常量,要么是表达式求值过程中创建的临时对象(例如:int && a=getVar();),因此右值引用具有如下特性:

​ 1.所引用的对象将要被销毁(临时对象)

​ 2.该对象没有其他用户(因为是临时创建的资源,不可能被占用)

​ 因此使用右值引用可以自由接管所引用对象的资源。并且使用右值引用绑定右值可以延长该右值的生命周期。

​ 需要注意的是,右值引用本身是一个变量,即本身是一个左值,因此不能像下面这样创建右值引用的右值引用

int &&rr1=42;  //正确
int &&rr2=rr1;  //错误,右值引用rr1本身是左值

标准库move函数可以从左值创建一个右值引用:

int &&rr3=std::move(rr1); //正确,move函数为左值rr1创建了一个右值引用并返回

注意使用move函数将该左值转为右值之后就不能再使用该变量了(已经销毁),否则会引发异常

此外,除了右值引用,常量左值引用也可以绑定右值,但普通的左值引用不行:

const int & a =42; //正确
int &a=42;	//错误

右值引用的应用

移动构造函数(移动语义)

​ 我们知道当类中出现堆内存的指针或引用时,必须实现其深拷贝构造函数,否则拷贝时会出现悬空指针的问题. 但是当引用的堆内存很大时,深拷贝构造函数的开销会很大,为了优化,需要实现移动构造函数,只是将引用的所有权移交而不需要进行深拷贝。

​ 移动构造函数的第一个参数是该类类型的一个右值引用,以此保证移动后源对象可以安全地销毁.

​ 看下面这个例子

class A
{
private:
	int* ptr;
public:
	A(int val) :ptr(new int(val)) {}
	A(const A& a) :ptr(new int(*a.ptr)) {
		std::cout << "Copy Constructor of A\n";
	}  //深拷贝构造函数
	A(A&& a) noexcept :ptr(a.ptr)     //移动构造函数
	{
		a.ptr = nullptr;   //将右值引用的指针置空,否则会引发异常
		std::cout << "Move Constructor of A\n";
	}
	~A() { delete ptr;  }
};

A GetA(int val)
{
	A a(val);
	return a;
}
int main()
{
    A a(1);
	A x = a;  //a是左值,使用拷贝构造函数
	A y = GetA(2);  //函数返回值是右值,使用移动构造函数
    A z = std::move(a);
    
}

​ 输出为:

Copy Constructor of A
Move Constructor of A
Move Constructor of A

指针和引用的区别

  1. 指针的值可以改变,即指针可以在运行时重新指向另一个地址,但引用所绑定的对象一旦绑定就不能改变。

  2. 指针占用内存空间,实质是值为地址的变量,而引用是否占内存,取决于编译器的实现。
    如果编译器用指针实现引用,那么它占内存;如果编译器直接将引用替换为其所指的对象,则其不占内存。

  3. 可以使用sizeof得到指针的大小,sizeof作用于引用时得到的是引用绑定的对象的大小

  4. 指针可以为空,但引用必须有绑定的对象。


STL 容器

序列式容器(Sequence containers)

每个元素均有固定的位置,取决于插入的时机和地点,且与元素值无关。

  • vector

    相当于可拓展的数组(动态数组),它的随机访问快,在中间插入和删除慢,但在末端插入和删除快。

    • 优点:支持随机访问,即 [] 操作和 .at(),所以查询效率高。

    • 缺点:当向其头部或中部插入或删除元素时,为了保持原本的相对次序,插入或删除点之后的所有元素都必须移动,所以插入的效率比较低。

    • 适用场景:适用于对象简单,变化较小,并且频繁随机访问的场景。

  • deque

    由一段一段的定量连续空间构成。一旦要在 deque 的前端和尾端增加新空间,便配置一段定量连续空间,串在整个 deque 的头端或尾端

    • 按页或块来分配存储器的,每页包含固定数目的元素。
    • deque 是 list 和 vector 的折中方案。兼有 list 的优点,也有vector 随机线性访问效率高的优点。
    • 适用场景:适用于既要频繁随机存取,又要关心两端数据的插入与删除的场景。

关联式容器(Associative containers),元素位置取决于特定的排序准则以及元素值,与插入次序无关。

  • map

    底部实现:红黑树

为什么不用AVL树

  1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。

  2. 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。

  3. map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。

  • unordered_map

    底部实现:哈希表

  • set

  • unordered_set

参考:[C++ STL] 各容器简单介绍 - 知乎 (zhihu.com)


协程

协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程

操作系统并不知道协程的存在,它只知道线程,因此在协程调用阻塞IO操作的时候,操作系统会让线程进入阻塞状态,当前的协程和其它绑定在该线程之上的协程都会陷入阻塞而得不到调度,这往往是不能接受的。

对系统IO进行封装,改为异步调用的形式来避免阻塞操作。


size_t

size_t类型是一个类型定义,通常将一些无符号的整形定义为size_t,比如说unsigned int或者unsigned long,甚至unsigned long long。size_t的每一个标准C实现会选择足够大的无符号整形来代表该平台上最大可能出现的对象大小。在诸如memcpy之类的需要获取内存中对象大小的时候,使用size_t可以获得不同平台之间更好的兼容性。

参考:为什么size_t重要?(Why size_t matters) - Jeremy’s blog (jeremybai.github.io)


Union

结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。共用体占用的内存等于最长的成员占用的内存

参考: C语言共用体(C语言union用法)详解 (biancheng.net)

你可能感兴趣的:(C/C++,c++,面试)