阿秀C++

文章目录


内存分区

参考1
参考2

在main执行之前和之后执行的代码可能是什么?

在 main 执行之前
设置栈指针和系统初始化:在执行 main 函数之前,系统会初始化栈、堆、CPU 寄存器等必要的运行时环境。
初始化全局和静态变量:所有全局变量和静态变量会在 main 函数之前被初始化。已初始化的变量会被加载到 .data 段,未初始化的变量则会在 .bss 段被清零(数值型short,int,long等为0,bool为FALSE,指针为NULL等等)。
全局对象和静态对象的构造:全局对象和静态对象的构造函数在 main 函数之前自动调用。这一过程由编译器生成的代码负责,通常在编译器生成的 _init 函数中进行。
调用特定属性函数:使用 attribute((constructor)) 修饰的函数会在 main 函数执行之前被自动调用。这种机制常用于库或框架初始化。(这是干什么?)
C++ 静态构造顺序:C++ 标准库提供的初始化代码,如标准输入输出流 cin、cout 等对象的构造,也会在 main 函数之前执行。
在 main 执行之后
全局对象和静态对象的析构:在 main 函数执行完毕后,所有全局对象和静态对象的析构函数会被自动调用,以释放资源。
调用使用 atexit 注册的函数:atexit 函数可以注册在程序结束时要执行的函数。这些函数会在 main 函数之后、全局对象析构之前执行。

void cleanup() {
    // 这段代码会在 main 函数之后执行
}

int main() {
    atexit(cleanup);
    return 0;
}

**调用特定属性函数:**使用 attribute((destructor)) 修饰的函数会在 main 函数和所有全局对象析构后被自动调用。
程序退出流程:最后,程序会调用 exit(),该函数会处理所有注册的 atexit 函数和清理工作,最终调用系统的 _exit() 进行最终退出。

attribute((constructor))

是 GCC(GNU Compiler Collection)提供的一个函数属性,用于标记一个函数,使得它在 main 函数之前自动执行。这个功能在编写需要进行初始化操作的库或程序时非常有用。
作用
**自动初始化:**当你使用 attribute((constructor)) 修饰一个函数时,这个函数会在 main 函数执行之前自动被调用。它通常用于在程序开始之前进行某些初始化工作,比如设置全局状态、初始化资源或进行必要的配置。
**库的初始化:**当你编写动态链接库(shared library)时,你可能需要在加载库时进行一些初始化操作,比如为该库分配必要的资源,或者注册某些功能。通过 attribute((constructor)),你可以确保这些初始化操作在库加载时自动执行,而不需要显式调用。

attribute((destructor))

这个属性修饰的函数会在 main 函数执行完毕和所有全局对象的析构函数执行之后自动调用。它常用于清理资源或进行收尾工作。
作用:
动态库的初始化与清理:当一个动态库被加载时,通常需要执行一些初始化操作(如初始化全局变量、创建必要的资源)。使用 attribute((constructor)) 可以确保这些操作在库加载时自动完成。同理,在库卸载时,可以使用 attribute((destructor)) 进行清理工作。
插件系统:在一些复杂的系统中,插件或模块可能需要在系统启动时自动注册或初始化。使用 constructor 可以实现这种自动化。

结构体内存对齐问题

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
  • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)
  • 内存对齐是为了提高处理器访问内存的效率。
  1. 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
  2. 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
  3. 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型
  • c++11以后引入两个关键字 alignas与 alignof。其中alignof可以计算出类型的对齐方式(指的是输出该结构体是几字节对齐的),alignas可以指定结构体的对齐方式(若alignas小于自然对齐的最小单位,则被忽略。)。

指针和引用的区别

指针本身就是一个变量,存放的是某个对象的地址,指针本身就有地址,所以可以有指向指针的指针;指针所指向的地址是可变的,指针所指向地址中的值也是可变的;
引用就是变量的别名,它是在已存在的变量上创建的,引用必须初始化,初始化后不可变;
指针可以为空,表示不指向任何有效的地址。引用必须在声明时初始化,并且不能在后续改变引用的绑定对象,不存在空值的引用。
指针通过&和*来实现对指针变量进行地址和取值的操作,引用仅通过引用初始化时的引用名即可访问。
指针通常用于动态内存分配、数组操作以及函数参数传递,引用通常用于函数参数传递、操作符重载以及创建别名。
提示:两者区别主要从:定义和声明、使用和操作、空值和空引用、可变性、实际用途来区别

- 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
- 指针可以有多级,引用只有一级
- 指针可以为空,引用不能为NULL且在定义时必须初始化
- 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
- sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
- 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
- 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间,(具体情况还要具体分析)。
- 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
- 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
- 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

在传递函数参数时,什么时候用指针,什么时候用引用?

  • 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
  • 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式

堆和栈的区别

堆和栈都是用于存储程序数据的内存区域。

栈是一种有限的内存区域,用于存储局部变量、函数调用信息等。堆是一种动态分配的内存区域,用于存储程序运行时动态分配的数据;
栈上的变量生命周期与其所在函数的执行周期相同,而堆上的变量生命周期由程序员显示的控制,可以使用new/malloc申请和delete/free释放;
栈上的内存分配和释放时自动的,速度比较快,而堆上的内存分配和释放需要手动操作,速度相对较慢

堆快一点还是栈快一点?

毫无疑问是栈快一点。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢

区别以下指针类型

int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
  • int* p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
  • int* p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

new /delete和malloc/free的异同

new和delete是如何实现的?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存

malloc和new的区别

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,不重载。
  • malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
  • malloc和free返回的是void类型指针(void*)(必须进行类型转换),new和delete返回的是具体类型指针。

既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?

  • malloc/free和new/delete都是用来申请内存和回收内存的。
  • 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free,所以new/delete是必不可少的。

被free回收的内存是立即返还给操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

宏定义和函数有何区别?

  • 宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
  • 宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
  • 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  • 宏定义不要在最后加分号。
  • #define MAX(a, b) ((a) > (b) ? (a) : (b))
  • // 使用宏为 int 类型定义别名 #define INTEGER int

宏定义和typedef区别?

宏 (#define) 和 typedef 都是在 C 和 C++ 中定义别名的方式,

  • 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  • 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
  • 宏不检查类型;typedef会检查数据类型。
  • 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
  • 宏不受作用域限制,只要在定义之后可以全局使用。
    typedef 受作用域限制,它只能在定义的作用域内使用。
    宏是文本替换:
    宏是通过预处理器在编译前进行简单的文本替换。例如,#define INTEGER int 会将所有出现 INTEGER 的地方替换为 int。由于是文本替换,宏在处理复杂类型时容易出错。
    typedef 是类型定义:
    typedef 直接定义一个类型别名,并且在编译时生效。它不会产生文本替换,因此更加安全和可靠,尤其在处理指针、函数指针或复杂类型时。
  • 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。
  • // 使用 typedef 为 int 类型定义别名 typedef int INTEGER;
#include <iostream>

// 定义一个函数指针类型,指向返回 int 类型、接收两个 int 参数的函数
typedef int (*FuncPtr)(int, int);

int add(int a, int b) {
    return a + b;
}

int main() {
    FuncPtr p = add;  // 使用 typedef 定义的函数指针类型
    std::cout << "Sum = " << p(3, 4) << std::endl;
    return 0;
}

变量声明和定义区别?

  • 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
  • 相同变量可以在多处声明(外部变量extern),但只能在一处定义。

strlen和sizeof区别?

  • sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。

  • sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串

  • 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。

  • ` int main(int argc, char const *argv[]){

    const char* str = "name";
    
    sizeof(str); // 取的是指针str的长度,是8
    strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
    return 0;
    

    }`

常量指针和指针常量区别?

我写的

a和&a有什么区别?

假设数组
int a[10];
int (*p)[10] = &a;
  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

C++和Python的区别

包括但不限于:

  • Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。python可以很方便的跨平台,但是效率没有C++高。
  • Python使用缩进来区分不同的代码块,C++使用花括号来区分
  • C++中需要事先定义变量的类型,而Python不需要,Python的基本数据类型只有数字,布尔值,字符串,列表,元组等等
  • Python的库函数比C++的多,调用起来很方便

C++和C语言的区别

  • C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
  • 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
  • C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
  • C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
  • 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。
  • C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的
  • 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
  • C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等

C++与Java的区别

语言特性

  • Java语言给开发人员提供了更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性强
  • Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在C++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题
  • C++也可以在其他系统运行,但是需要不同的编码(这一点不如Java,只编写一次代码,到处运行),例如对一个数字,在windows下是大端存储,在unix中则为小端存储。Java程序一般都是生成字节码,在JVM里面运行得到结果
  • Java用接口(Interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省却了在实现和维护上的复杂性
    垃圾回收
  • C++用析构函数回收垃圾,写C和C++程序时一定要注意内存的申请和释放
  • Java语言不使用指针,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题
    应用场景
  • Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)
  • Java在Web 应用上具有C++ 无可比拟的优势,具有丰富多样的框架
  • 对于底层程序的编程以及控制方面的编程,C++很灵活,因为有句柄的存在

C++中struct和class的区别

相同点

  • 两者都拥有成员函数、公有和私有部分
  • 任何可以使用class完成的工作,同样可以使用struct完成
    不同点
  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
  • class默认是private继承,而struct模式是public继承
    引申:C++和C的struct区别
  • C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)
  • C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
  • struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

define宏定义和const的区别

编译阶段

  • define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用
    安全性
  • define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
  • const常量有数据类型,编译器可以对其进行类型安全检查
    内存占用
  • define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
  • 宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
  • 宏不检查类型;const会检查数据类型。
  • 宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

C++中const和static的作用

https://blog.csdn.net/weixin_45419660/article/details/140331278
static

  • 不考虑类的情况
    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
  • 考虑类的情况
    • static成员变量:只与类关联,不与类的对象关联定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    • static成员函数:**不具有this指针,无法访问类对象的非static成员变量和非static成员函数;**不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
      const
  • 不考虑类的情况
    • const常量在定义时必须初始化,之后无法更改
    • const形参可以接收const和非const类型的实参,例如
    • `// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}
  • 考虑类的情况
    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
  • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值
#include <iostream>

class MyClass {
private:
    const int value;

public:
    // 构造函数初始化列表,用于初始化 const 成员变量
    MyClass(int v) : value(v) {}

    int getValue() const {
        return value;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2(20);

    std::cout << "obj1 value: " << obj1.getValue() << std::endl;
    std::cout << "obj2 value: " << obj2.getValue() << std::endl;

    // obj1.value = 15; // 错误:const 成员变量不能被修改

    return 0;
}

#include <iostream>

class MyClass {
private:
    int data;
    mutable int mutableData; // 可以在 const 成员函数中修改

public:
    MyClass(int d, int m) : data(d), mutableData(m) {}

    // const 成员函数,承诺不修改非 mutable 成员变量
    int getData() const {
        return data;
    }

    // 可以修改 mutable 成员变量
    void modifyMutableData() const {
        mutableData++;
    }

    int getMutableData() const {
        return mutableData;
    }
};

int main() {
    const MyClass constObj(100, 200);
    MyClass nonConstObj(300, 400);

    // const 对象只能调用 const 成员函数
    std::cout << "constObj data: " << constObj.getData() << std::endl;
    constObj.modifyMutableData();
    std::cout << "constObj mutableData after modification: " << constObj.getMutableData() << std::endl;

    // 非 const 对象可以调用所有成员函数
    std::cout << "nonConstObj data: " << nonConstObj.getData() << std::endl;
    nonConstObj.modifyMutableData();
    std::cout << "nonConstObj mutableData after modification: " << nonConstObj.getMutableData() << std::endl;

    return 0;
}

C++的顶层const和底层const

概念区分

  • 顶层const:指的是const修饰的本身是一个常量,无法修改,指的是指针,就是 * 号的右边
  • 底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
int a = 10;
int* const b1 = &a;        
//顶层const,b1本身是一个常量
const int* b2 = &a;        
//底层const,b2本身可变,所指的对象是常量
const int b3 = 20;                    
//顶层const,b3是常量不可变
const int* const b4 = &a;  
//前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a;                   
//用于声明引用变量,都是底层const

区分作用

  • 执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const
  • 使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const
const int a;
int const a;
const int *a;
int *const a;
  • int const a和const int a均表示定义常量类型a。
  • const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)
  • int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

数组名和指针(这里为指向数组首元素的指针)区别?

  • 二者均可通过增减偏移量来访问数组中的元素。
  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

final和override关键字

override
当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

class A{    virtual void foo();}
class B : public A{
    void foo(); //OK
    virtual void foo(); // OK    
    void foo() override; //OK}

如果不使用override,当你手一抖,将**foo()写成了f00()**会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

class A{    virtual void foo();};
class B : public A{    
virtual void f00(); //OK,这个函数是B新增的,不是继承的    
virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错};

final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

class Base{    virtual void foo();};
 class A : public Base{    
 void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写};
 class B final : A // 指明B是不可以被继承的{
     void foo() override; // Error: 在A中已经被final了}; 
 class C : B // Error: B is final{};

拷贝初始化和直接初始化

  • 当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:**直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。**拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下

string str1(“I am a string”);//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用构造函数对str2进行初始化
string str3 = “I am a string”;//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造 str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数

  • 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了
    • (语句1和语句3等价),但是需要辨别两种情况。
    • 当拷贝构造函数为private时:语句3和语句4在编译时会报错
    • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
      我的理解:
      语句2:当使用一个构造函数直接初始化对象时,称之为直接初始化。对于语句 string str2(str1);,它直接调用了 string 类的拷贝构造函数,将 str1 的内容拷贝到 str2 中,因此这是直接初始化。
      语句3:拷贝初始化发生在使用等号 = 时。在这个例子中,字面量 “I am a string” 是一个 const char* 类型的字符串,C++ 会先将它转换为 std::string,然后使用这个临时的 std::string 对象来初始化 str3。
      语句4;str4 是通过等号 = 来初始化的,这种形式的初始化在 C++ 中被称为拷贝初始化。这里 str1 是一个已经存在的 std::string 对象,编译器会使用 std::string 的拷贝构造函数来创建 str4,从而实现对 str1 的拷贝。
      string str2(str1); 是直接初始化,尽管它调用的是拷贝构造函数。直接初始化和拷贝构造函数并不矛盾,前者是初始化形式,后者是实现方式。

初始化和赋值的区别:

  • 对于简单类型来说,初始化和赋值没什么区别
  • 对于类和复杂数据类型来说,这两者的区别就大了,举例如下:
class A{
public:    
	int num1;    
	int num2;
public:    
	A(int a=0, int b=0):num1(a),num2(b){};     // 构造函数  
	A(const A& a){};    // 拷贝构造函数
	//重载 = 号操作符函数    
	A& operator=(const A& a){        num1 = a.num1 + 1;        num2 = a.num2 + 1;        return *this;    };
	};
int main(){    
	A a(1,1);    //构造函数 A(int a, int b),将 num1 初始化为 1,num2 也初始化为 1。
	A a1 = a; //拷贝初始化操作,因为它使用了 =,所以会调用类 A 的拷贝构造函数 A(const A& a)。然而,由于该拷贝构造函数没有任何操作(空函数体),a1 的成员变量 num1 和 num2 未被初始化或拷贝。   
	A b;    //构造函数 A(int a = 0, int b = 0),将 b 的 num1 和 num2 初始化为 0。
	b = a;//这是一个赋值操作(而不是初始化)。它调用了重载的赋值操作符 A& operator=(const A& a)。该操作符函数将 a 的 num1 和 num2 的值加 1 后,分别赋值给 b 的 num1 和 num2
	return 0;
	}

对于类来说,主要是涉及到重载的赋值操作符(这里的赋值操作不仅仅是简单的拷贝,而是经过了特定的处理,体现了赋值操作符的灵活性。)、拷贝构造函数和构造函数;

extern"C"的用法

为了能够正确的在C++代码中调用C语言的代码:在程序中加上extern "C"后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++;
哪些情况下使用extern “C”:
(1)C++ 代码中调用 C 语言代码:当 C++ 代码需要调用用 C 语言编写的库或函数时,需要使用 extern “C” 来确保函数名不被改变。
(2) C++ 头文件中使用:如果头文件既需要在 C 代码中包含,也需要在 C++ 代码中包含,则可以使用 extern “C” 来确保该头文件中的声明可以被 C++ 正确处理。
(3)跨语言协同开发:在一个项目中,如果有开发者使用 C 语言编写代码,而另一些开发者使用 C++,那么 extern “C” 可以帮助他们无缝地集成各自的代码。
举个例子,C++中调用C代码:
C语言代码:

// add.c
int add(int a, int b) {
    return a + b;
}

C语言头文件:

// add.h
#ifndef ADD_H
#define ADD_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);

#ifdef __cplusplus
}
#endif

#endif // ADD_H

C++代码C++调用C

#include <iostream>
#include "add.h"

int main() {
    int result = add(3, 4); // 调用C语言中的add函数
    std::cout << "The result is: " << result << std::endl;
    return 0;
}

编译和运行:

gcc -c add.c -o add.o   # 编译C文件
g++ main.cpp add.o -o main   # 链接C++文件和C对象文件
./main

解释:宏 __cplusplus,它是由 C++ 编译器自动定义的,用来区分当前的编译环境是 C 还是 C++。
如果当前编译器是 C++ 编译器,__cplusplus 宏就会被定义。具体值为一个数字,表示 C++ 标准的版本。例如,在 C++98 中,该值为 199711L,在 C++11 中为 201103L,在 C++17 中为 201703L 等。
如果当前编译器是 C 编译器,__cplusplus 宏不会被定义。
如果 __cplusplus 被定义(即当前使用的是 C++ 编译器),则 #ifdef __cplusplus 之后的代码块会被编译。也就是:

extern "C" {
#endif

int add(int a, int b);

#ifdef __cplusplus
}

会被执行;表示其中的内容函数声明是按 C 语言的规则进行名字修饰的。
如果 __cplusplus 未定义(即当前使用的是 C 编译器),则 extern “C” { 不会被执行,编译器会直接跳过这个部分。此时,函数声明将以普通的 C 语言方式进行处理。
对于 C++ 编译器,在 extern “C” 块中的函数声明,如 int add(int a, int b);,将被编译器按 C 语言的名字修饰规则处理,即不进行 C++ 名字修饰。
对于 C 编译器,函数声明如 int add(int a, int b); 正常编译,因为 extern “C” 块根本不会存在于最终的编译结果中。
当代码在 C++ 编译器下编译时,__cplusplus 宏存在,extern “C” 块会启用,告诉编译器按照 C 的方式处理函数名,防止 C++ 编译器进行名字修饰,使得 C++ 代码能够调用 C 代码中的函数。
当代码在 C 编译器下编译时,__cplusplus 宏不存在,extern “C” 块不会启用,函数声明会按照 C 的常规方式处理。
C调用C++函数
C++ 代码 (cpp_functions.cpp)

#include <iostream>

// 使用 extern "C" 告诉编译器这个函数要按照 C 的规则导出
extern "C" {
    void cpp_function(int x) {
        std::cout << "Called C++ function with value: " << x << std::endl;
    }
}

C++ 头文件 (cpp_functions.h)

#ifndef CPP_FUNCTIONS_H
#define CPP_FUNCTIONS_H

// 使用 extern "C" 声明函数,以便 C 代码可以调用
#ifdef __cplusplus
extern "C" {
#endif

void cpp_function(int x);

#ifdef __cplusplus
}
#endif

#endif // CPP_FUNCTIONS_H

C 代码 (main.c)

#include <stdio.h>
#include "cpp_functions.h"

int main() {
    int value = 42;
    // 调用 C++ 函数
    cpp_function(value);
    return 0;
}

为了让这个函数可以被 C 代码调用,我们在函数定义前使用了 extern “C”。
extern “C” 确保 cpp_function 在生成的目标文件中不会使用 C++ 的名字修饰规则,这样 C 语言可以直接调用。
编译和运行
编译 C++ 文件:

g++ -c cpp_functions.cpp -o cpp_functions.o	//编译 C++ 文件:
gcc -c main.c -o main.o	//编译 C 文件:
g++ main.o cpp_functions.o -o main//链接生成可执行文件:由于 C++ 函数的实现依赖于 C++ 标准库和其他 C++ 特性,最终链接时仍然需要使用 C++ 编译器(如 g++),而不是 gcc。
./main	//./main





方式 1: 在 C 语言头文件中使用 extern “C”

C 语言头文件 (add.h):

#ifndef ADD_H
#define ADD_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);

#ifdef __cplusplus
}
#endif

#endif // ADD_H

C++ 文件 (main.cpp):

#include <iostream>
#include "add.h"

int main() {
    int result = add(3, 4);
    std::cout << "The result is: " << result << std::endl;
    return 0;
}

优点
清晰和标准化:在头文件中使用 extern “C” 确保 C++ 编译器按照 C 的链接规则处理函数声明。
多用途:该头文件可以被 C 和 C++ 代码使用,避免名字修饰问题。
维护性:只需在头文件中声明一次,适用于项目中多个 C++ 文件。
缺点
头文件设计:需要在头文件中添加 extern “C” 声明,对现有头文件的修改可能需要注意兼容性。

在 C++ 文件中包含 C 语言头文件并使用 extern “C”

C 语言头文件 (add.h):

#ifndef ADD_H
#define ADD_H

int add(int a, int b);

#endif // ADD_H

C++ 文件 (main.cpp):

#include <iostream>

extern "C" {
#include "add.h"
}

int main() {
    int result = add(3, 4);
    std::cout << "The result is: " << result << std::endl;
    return 0;
}

优点
直接:简单地在 C++ 文件中使用 extern “C” 来处理 C 语言的头文件,无需修改头文件本身。
缺点
局部性:每次包含 C 语言头文件时都需要 extern “C”,可能导致维护困难。
不适合大规模使用:如果多个 C++ 文件需要调用相同的 C 函数,重复添加 extern “C” 声明会显得繁琐。

在 C++ 头文件中使用 extern “C”

C++ 头文件 (add.h):

#ifndef ADD_H
#define ADD_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);

#ifdef __cplusplus
}
#endif

#endif // ADD_H

C++ 实现文件 (add.cpp):

在这里插入代码片
#include "add.h"

int add(int a, int b) {
    return a + b;
}

C 文件 (main.c):

#include <stdio.h>
#include "add.h"

int main() {
    int result = add(3, 4); // 调用 C++ 函数
    printf("The result is: %d\n", result);
    return 0;
}

gcc -c main.c -o main.o          # 编译 C 文件
g++ -c add.cpp -o add.o          # 编译 C++ 文件
g++ main.o add.o -o main         # 链接生成可执行文件

方式 2: 在 C 文件中直接调用 C++ 函数

C++ 实现文件 (add.cpp):

extern "C" int add(int a, int b) {
    return a + b;
}

C 文件 (main.c):

#include <stdio.h>

extern int add(int a, int b); // C++ 函数的声明

int main() {
    int result = add(3, 4); // 调用 C++ 函数
    printf("The result is: %d\n", result);
    return 0;
}

gcc -c main.c -o main.o          # 编译 C 文件
g++ -c add.cpp -o add.o          # 编译 C++ 文件
g++ main.o add.o -o main         # 链接生成可执行文件

第二种方式,C++环境仍然能够调用函数,但是确实C++调用C;而非C++调用C++;因为函数文件已经改变了连接方式,改为了C方式。
extern “C” 只是改变了函数的链接方式,使其遵循 C 语言的链接规范,但并不改变函数的实际功能或可调用性。
extern “C” 的作用是告诉编译器在编译 add 函数时,使用 C 语言的链接规则。这意味着:
函数名不会经过 C++ 的名称修饰(name mangling)。
编译器生成的符号表中,add 函数的符号名将与 C 语言的风格一致。

野指针和悬空指针

都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。

  • 野指针
    野指针,指的是没有被初始化过的指针
int main(void) 
{         
	int* p;     // 未初始化    
	std::cout<< *p << std::endl; // 未初始化就被使用       
	 return 0;
}
  • 因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。
  • 悬空指针
    悬空指针,指针最初指向的内存已经被释放了的一种指针。
int main(void) {   
int * p = nullptr;  
int* p2 = new int;   
 p = p2;  delete p2;
}

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。 避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
产生原因及解决办法:
野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

C和C++的类型安全

什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。
类型安全的编程语言与类型安全的程序之间,没有必然联系。好的程序员可以使用类型不那么安全的语言写出类型相当安全的程序,相反的,差一点儿的程序员可能使用类型相当安全的语言写出类型不太安全的程序。绝对类型安全的编程语言暂时还没有。
没复制完,不想复制

C++中的重载、重写(覆盖)和隐藏的区别

(1)重载(overload)
重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。举个例子:

class A{    ...   
 virtual int fun();    
 void fun(int);    
 void fun(double, double);    
 static int fun(char);   
...}

2)重写(覆盖)(override)
重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:

  • 与基类的虚函数有相同的参数个数
  • 与基类的虚函数有相同的参数类型
  • 与基类的虚函数有相同的返回值类型
    举个例子:
//父类
class A{
public:    
	virtual int fun(int a){}
}
//子类
class B : public A{
public:    
//重写,一般加override可以确保是重写父类的函数    
virtual int fun(int a) override{}
}

重载与重写的区别:

  • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
  • 重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体
    (3)隐藏(hide)
    隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:
  • 两个函数参数相同,但是基类函数不是虚函数。**和重写的区别在于基类函数是否是虚函数。**举个例子:
//父类
class A{
public:    
	void fun(int a){                cout << "A中的fun函数" << endl;        }
};
//子类
class B : public A{
public:    
	//隐藏父类的fun函数    
	void fun(int a){                cout << "B中的fun函数" << endl;        }
};
int main(){    
	B b;    
	b.fun(2); //调用的是B中的fun函数    
	b.A::fun(2); //调用A中fun函数    
	return 0;
}
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。举个例子:
//父类
class A{
public:    
	virtual void fun(int a){                cout << "A中的fun函数" << endl;        }
};
//子类
class B : public A{
public:    
//隐藏父类的fun函数   
virtual void fun(char* a){           cout << "A中的fun函数" << endl;   }
};
int main(){    
B b;    
b.fun(2); //报错,调用的是B中的fun函数,参数类型不对    
b.A::fun(2); //调用A中fun函数    
return 0;}

C++有哪几种的构造函数

C++中的构造函数可以分为4类:

  • 默认构造函数
  • 初始化构造函数(有参数)
  • 拷贝构造函数
  • 移动构造函数(move和右值引用)
  • 委托构造函数
  • 转换构造函数
#include <iostream>
using namespace std;

class Student {
public:
    // 默认构造函数,没有参数
    Student() {
        this->age = 20;
        this->num = 1000;
    }

    // 初始化构造函数,有参数和参数列表
    Student(int a, int n) : age(a), num(n) {}

    // 拷贝构造函数,这里与编译器生成的一致
    Student(const Student& s) {
        this->age = s.age;
        this->num = s.num;
    }

    // 转换构造函数,形参是其他类型变量,且只有一个形参
    Student(int r) {
        this->age = r;
        this->num = 1002;
    }

    ~Student() {}

public:
    int age;
    int num;
};

int main() {
    Student s1;            // 调用默认构造函数
    Student s2(18, 1001);  // 调用带参数的构造函数
    int a = 10;
    Student s3(a);         // 调用转换构造函数
    Student s4(s3);        // 调用拷贝构造函数

    printf("s1 age: %d, num: %d\n", s1.age, s1.num);
    printf("s2 age: %d, num: %d\n", s2.age, s2.num);
    printf("s3 age: %d, num: %d\n", s3.age, s3.num);
    printf("s4 age: %d, num: %d\n", s4.age, s4.num);

    return 0;
}

  • 默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作
  • 复制构造函数用于复制本类的对象
  • 转换构造函数用于将其他类型的变量,隐式转换为本类对象

浅拷贝和深拷贝的区别

浅拷贝
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
深拷贝
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

#include <iostream>
#include <cstring>  // 头文件包含了 memcpy 和 strlen
using namespace std;

class Student {
private:
    int num;
    char *name;

public:
    Student() {
        name = new char[20];  // 为 name 分配内存
        cout << "Student" << endl;
    }

    ~Student() {
        cout << "~Student " << &name << endl;
        delete[] name;  // 释放内存
        name = NULL;
    }

    // 拷贝构造函数
    Student(const Student &s) {
        // 浅拷贝:当对象的 name 和传入对象的 name 指向相同的地址
        name = s.name;

        // 深拷贝
        // name = new char[20];
        // memcpy(name, s.name, strlen(s.name) + 1);  // 复制字符串

        cout << "copy Student" << endl;
    }
};

int main() {
    {
        // 花括号让 s1 和 s2 变成局部对象,方便测试
        Student s1;
        Student s2(s1);  // 复制对象
    }

    system("pause");
    return 0;
}

浅拷贝:
在浅拷贝中,拷贝构造函数将 name 指针直接复制给新对象。因此,s1 和 s2 的 name 指针指向同一块内存区域。
当 s1 和 s2 离开作用域时,它们的析构函数会被调用,两次 delete 同一块内存,导致 “double free or corruption” 错误。
深拷贝:
在深拷贝中,拷贝构造函数为新对象分配一块新的内存,并复制原对象内存中的数据到新对象的内存中。
这样,每个对象拥有自己独立的内存,不会导致重复释放内存的问题。程序能够正常执行并正确释放内存。
从执行结果可以看出,浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。

内联函数和宏定义的区别

  • 在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
  • 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
  • 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义
  • 内联函数有类型检测、语法判断等功能,而宏没有
    内联函数适用场景:
  • 使用宏定义的地方都可以使用 inline 函数。
  • 作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。

内联函数

内联函数是 C++ 提供的一种优化手段,用于减少函数调用的开销。当函数被定义为内联函数时,编译器会尝试在每个调用点直接插入函数的代码,而不是进行常规的函数调用。这可以提高性能
内联函数通过在函数定义前加上 inline 关键字来声明。

inline int square(int x) {
    return x * x;
}

在调用 square(5) 时,编译器会将代码直接替换为 5 * 5,而不是进行一次函数调用。

public,protected和private访问和继承权限/public/protected/private的区别?

  • public的变量和函数在类的内部外部都可以访问。
  • protected的变量和函数只能在类的内部和其派生类中访问。
  • private修饰的元素只能在类内访问。
    (一)访问权限
    派生类可以继承基类中除了构造/析构、赋值运算符重载函数之外的成员,但是这些成员的访问属性在派生过程中也是可以调整的,三种派生方式的访问权限如下表所示:注意外部访问并不是真正的外部访问,而是在通过派生类的对象对基类成员的访问。
    在这里插入图片描述

派生类对基类成员的访问形象有如下两种:

  • 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
  • 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
    (二)继承权限
    public继承
    公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,都保持原有的状态,而基类的私有成员任然是私有的,不能被这个派生类的子类所访问
    protected继承
    保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元函数访问,基类的私有成员仍然是私有的,访问规则如下表
    在这里插入图片描述

private继承
私有继承的特点是基类的所有公有成员和保护成员都成为派生类的私有成员,并不被它的派生类的子类所访问,基类的成员只能由自己派生类访问,无法再往下继承,访问规则如下表
在这里插入图片描述

如何用代码判断大小端存储

大端存储:字数据的高字节存储在低地址中
小端存储:字数据的低字节存储在低地址中
例如:32bit的数字0x12345678
所以在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输
小端模式中的存储方式为:
在这里插入图片描述

大端模式中的存储方式为:
在这里插入图片描述

了解了大小端存储的方式,如何在代码中进行判断呢?下面介绍两种判断方式:
方式一:使用强制类型转换-这种法子不错

#include <iostream>
using namespace std;

int main() {
    int a = 0x1234;
    // 由于 int 和 char 的长度不同,借助 int 型转换成 char 型,只会留下低地址的部分
    char c = (char)(a);

    if (c == 0x12)
        cout << "big endian" << endl;
    else if (c == 0x34)
        cout << "little endian" << endl;

    return 0;
}

这段代码用于检查系统的字节序(endianness)。
a 赋值为 0x1234,这是一个 16 位的十六进制数。
c 通过类型转换从 int 转换为 char,只保留低地址部分(低字节)的值。
如果 c 等于 0x12,表示系统是大端(big-endian)字节序;如果 c 等于 0x34,则表示系统是小端(little-endian)字节序。
方式二:巧用union联合体

#include <iostream>
using namespace std;

// union 联合体的重叠式存储,endian 联合体占用内存的空间为每个成员字节长度的最大值
union endian {
    int a;
    char ch;
};

int main() {
    endian value;
    value.a = 0x1234;
    // a 和 ch 共用 4 字节的内存空间
    if (value.ch == 0x12)
        cout << "big endian" << endl;
    else if (value.ch == 0x34)
        cout << "little endian" << endl;

    return 0;
}

联合体 (union): 联合体中的所有成员共享同一块内存,其大小由最大成员的字节长度决定。在这个例子中,int 和 char 成员共享 4 字节的内存。

内存管理

C++11新标准

C++ 11有哪些新特性?

  • nullptr替代 NULL
  • 引入了 auto 和 decltype 这两个关键字实现了类型推导
  • 基于范围的 for 循环for(auto& i : res){}
  • 类和结构体的中初始化列表
  • Lambda 表达式(匿名函数)
  • std::forward_list(单向链表)
  • 右值引用和move语义

auto、decltype和decltype(auto)的用法

(1)auto
C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同,
**auto 让编译器通过初始值来进行类型推演。从而获得定义变量的类型,所以说 auto 定义的变量必须有初始值。**举个例子:

//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型

//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt

//引用和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*

(2)decltype
有的时候我们还会遇到这种情况,**我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。**还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

int func() {
    return 0;
} // 普通类型

decltype(func()) sum = 5; 
// sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func()

int a = 0;
decltype(a) b = 4; 
// a的类型是int, 所以b的类型也是int

// 不论是顶层const还是底层const, decltype都会保留
const int c = 3;
decltype(c) d = c; 
// d的类型和c是一样的, 都是顶层const

int e = 4;
const int* f = &e; 
// f是底层const

decltype(f) g = f; 
// g也是底层const

// 引用与指针类型
// 1. 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i;
decltype(j) k = 5; 
// k的类型是 const int&

// 2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; 
// 此时是int类型

// 3. 对指针的解引用操作返回的是引用类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; 
// c是int&类型, c和j绑定在一起

// 4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
int i = 3;
decltype((i)) j = i; 
// 此时j的类型是int&类型, j和i绑定在一起

(3)decltype(auto)
decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

C++中NULL和nullptr区别

算是为了与C语言进行兼容而定义的一个问题吧
NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。编译器一般对其实际定义如下:

#ifdef __cplusplus
    #define NULL 0
#else
    #define NULL ((void *)0)
#endif

在C++中指针必须有明确的类型定义。但是将NULL定义为0带来的另一个问题是无法与整数的0区分。因为C++中允许有函数重载,所以可以试想如下函数定义情况:

#include <iostream>

using namespace std;

void fun(char* p) {
    cout << "char*" << endl;
}

void fun(int p) {
    cout << "int" << endl;
}

int main() {
    fun(NULL);
    return 0;
}

// 输出结果:int

那么在传入NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
nullptr的一种实现方式如下:

const class nullptr_t {
public:
    template<class T>
    inline operator T*() const {
        return 0;
    }

    template<class C, class T>
    inline operator T C::*() const {
        return 0;
    }

private:
    void operator&() const {}

} nullptr = {};

以上通过模板类和运算符重载的方式来对不同类型的指针进行实例化从而解决了(void*)指针带来参数类型不明的问题,**另外由于nullptr是明确的指针类型,所以不会与整形变量相混淆。**但nullptr仍然存在一定问题,例如:

#include <iostream>

using namespace std;

void fun(char* p) {
    cout << "char* p" << endl;
}

void fun(int* p) {
    cout << "int* p" << endl;
}

void fun(int p) {
    cout << "int p" << endl;
}

int main() {
    fun((char*)nullptr); // 语句1
    fun(nullptr);        // 语句2
    fun(NULL);           // 语句3
    return 0;
}

// 运行结果:
// 语句1:char* p
// 语句2: 报错,有多个匹配
// 语句3:int p

智能指针的原理、常用的智能指针及实现

原理
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源
常用的智能指针
(1) shared_ptr
实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。

  • 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
  • 每次创建类的新对象时,初始化指针并将引用计数置为1
  • 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
  • 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
  • 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
    (2) unique_ptr
    unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。
    (3) weak_ptr
    weak_ptr:弱引用。 引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
    (4) auto_ptr
    主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。
    auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。
    auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。
    智能指针shared_ptr代码实现:
template<typename T>
class SharedPtr{
public:        
    SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))        {}                           SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount) {                                 (*_pcount)++;        
    }        
    SharedPtr<T>& operator=(const SharedPtr& s){                
        if (this != &s)                
        {                        
            if (--(*(this->_pcount)) == 0)                        
            {                                
                delete this->_ptr;                                
                delete this->_pcount;                        
            }                        
            _ptr = s._ptr;                        
            _pcount = s._pcount;
            *(_pcount)++;                
        }                
        return *this;        
   }        
   T& operator*() { return *(this->_ptr); }        
   T* operator->() { return this->_ptr; }        
   ~SharedPtr() {                
       --(*(this->_pcount));                
       if (*(this->_pcount) == 0)                
       {                        
           delete _ptr;                        
           _ptr = NULL;                        
           delete _pcount;                        
           _pcount = NULL;                
       }        
    }
private:
    T* _ptr;
    int* _pcount;//指向引用计数的指针
};

说一说你了解的关于lambda函数的全部知识

  1. 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;
  2. 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。
  3. lambda表达式的语法定义如下:
    [capture] (parameters) mutable ->return-type {statement};
  4. lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

智能指针的作用;

  1. C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
  2. 智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
  3. 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr p4 = new int(1);的写法是错误的
    拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象
  4. unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
  5. 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
  6. weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.

说说你了解的auto_ptr作用

  1. auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
  2. auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
  3. auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
  4. 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;
  5. Auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;
  6. auto_ptr支持所拥有的指针类型之间的隐式类型转换。
  7. 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
  8. T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。

智能指针的循环引用

循环引用是指使用多个智能指针share_ptr时,出现了指针之间相互指向,从而形成环的情况,有点类似于死锁的情况,这种情况下,智能指针往往不能正常调用对象的析构函数,从而造成内存泄漏。举个例子:

#include <iostream>
#include <memory>

using namespace std;

template <typename T>
class Node {
public:
    Node(const T& value)
        : _pPre(nullptr)
        , _pNext(nullptr)
        , _value(value)
    {
        cout << "Node()" << endl;
    }

    ~Node() {
        cout << "~Node()" << endl;
        cout << "this:" << this << endl;
    }

    shared_ptr<Node<T>> _pPre;
    shared_ptr<Node<T>> _pNext;
    T _value;
};

void Funtest() {
    shared_ptr<Node<int>> sp1(new Node<int>(1));
    shared_ptr<Node<int>> sp2(new Node<int>(2));

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

    sp1->_pNext = sp2; // sp1的引用+1
    sp2->_pPre = sp1;  // sp2的引用+1

    cout << "sp1.use_count:" << sp1.use_count() << endl;
    cout << "sp2.use_count:" << sp2.use_count() << endl;
}

int main() {
    Funtest();
    system("pause");
    return 0;
}

// 输出结果:
// Node()
// Node()
// sp1.use_count:1
// sp2.use_count:1
// sp1.use_count:2
// sp2.use_count:2

从上面shared_ptr的实现中我们知道了只有当引用计数减减之后等于0,析构时才会释放对象,而上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。
在实际编程过程中,应该尽量避免出现智能指针之前相互指向的情况,如果不可避免,可以使用使用弱指针——weak_ptr,它不增加引用计数,只要出了作用域就会自动析构。

手写实现智能指针类需要实现哪些函数?

  1. 智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象。
    除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。
    通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1
  2. 一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数;

智能指针出现循环引用怎么解决?

弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

STL模板库

什么是STL?

C++ STL从广义来讲包括了三类:算法,容器和迭代器。

  • 算法包括排序,复制等常用算法,以及不同容器特定的算法。
  • 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
  • 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

解释一下什么是trivial destructor

“trivial destructor”一般是指用户没有自定义析构函数,而由系统生成的,这种析构函数在《STL源码解析》中成为“无关痛痒”的析构函数。
反之,用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露
对于trivial destructor,如果每次都进行调用,显然对效率是一种伤害,如何进行判断呢?
《STL源码解析》中给出的说明是:
首先利用value_type()获取所指对象的型别,再利用__type_traits判断该型别的析构函数是否trivial,若是(__true_type),则什么也不做,若为(__false_type),则去调用destory()函数
也就是说,在实际的应用当中,STL库提供了相关的判断方法**__type_traits**,感兴趣的读者可以自行查阅使用方式。除了trivial destructor,还有trivial construct、trivial copy construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。

cahtGPT:
trivial destructor(平凡析构函数) 是指一个默认的、编译器生成的析构函数,具有以下特性:
用户未自定义析构函数:如果用户没有为类自定义析构函数,编译器会自动生成一个默认的析构函数。
没有特殊操作:trivial destructor 不执行任何用户定义的操作,只是简单地“存在”,不会释放动态分配的内存,也不会调用其他成员的析构函数。
行为简单:trivial destructor 不依赖任何类的其他成员,不包含虚函数表指针、基类或虚基类的析构函数调用。
相反,如果用户自定义了析构函数,即使只是一个空的析构函数,那么这个析构函数就是non-trivial destructor。这种析构函数可能需要处理复杂的资源管理,比如释放动态分配的内存、关闭文件句柄等。
判断trivial destructor的意义
对性能敏感的应用程序中,频繁调用 trivial destructor 可能会降低效率,因为这些调用并不做任何实际的工作。STL库在实现过程中,通过使用 __type_traits 等机制判断一个类型是否具有 trivial destructor,从而避免不必要的析构函数调用,提高执行效率。
__type_traits 的作用
在 STL 实现中,__type_traits 是一个用于判断类型特性(如是否具有 trivial destructor)的机制。通过它,STL可以在编译期确定一个类型是否有 trivial destructor,并根据判断结果采取不同的处理策略:
trivial destructor (__true_type):如果类型的析构函数是平凡的,STL可以选择不调用析构函数,避免不必要的性能开销。
non-trivial destructor (__false_type):如果类型的析构函数是非平凡的,STL 会调用 destroy() 函数来正确释放资源。
Trivial 特性的其他方面
除了 trivial destructor,C++ 还有其他类似的特性:
Trivial Constructor:一个没有自定义构造函数的类型,由编译器生成的默认构造函数称为 trivial constructor。
Trivial Copy Constructor:如果类型没有自定义的拷贝构造函数,由编译器生成的拷贝构造函数称为 trivial copy constructor。
这些 trivial 特性让编译器能够在适当情况下使用高效的低级内存操作,如 memcpy、malloc 等,避免不必要的构造、析构操作,从而提升性能。
Trivial Destructor:因为没有复杂的逻辑操作和资源管理,trivial destructor 的对象在内存上是简单和稳定的,所以可以安全地使用 memset 和 memcpy 进行快速的内存操作。
Non-Trivial Destructor:这些对象可能包含资源管理、复杂的对象状态和内存结构,因此直接使用 memset 和 memcpy 会跳过这些重要的析构逻辑,可能导致资源泄漏、状态损坏或未定义行为。

使用智能指针管理内存资源,RAII是怎么回事?

  1. RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。
    因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
  2. 智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。
    毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。

chatGPT
RAII的核心理念是将资源的获取和释放与对象的生命周期绑定在一起,从而确保资源在不再需要时自动释放,避免资源泄漏和其他相关问题。
在RAII中,资源的获取通常在对象的构造函数中进行,而资源的释放则在对象的析构函数中完成。由于C++保证了对象的析构函数会在对象超出作用域时自动调用,这使得RAII可以确保资源在任何情况下(包括异常情况)都能得到正确的释放。
> RAII 的优点
异常安全性:RAII确保资源在异常情况下也会被正确释放,因为在异常发生时,栈上的对象会自动销毁,其析构函数会被调用,从而释放资源。
自动化资源管理:将资源的管理封装在对象的生命周期内,减少了显式管理资源的需要,从而降低了代码复杂度和错误的可能性。
简化代码:RAII简化了资源管理的代码逻辑,使代码更简洁、更易读,并减少了手动释放资源的操作。
常见的RAII应用
智能指针:如 std::unique_ptr 和 std::shared_ptr,它们用于自动管理动态内存。
锁类:如 std::lock_guard 和 std::unique_lock,它们用于自动管理锁的获取和释放,确保多线程环境下的资源安全。
文件流:如 std::ifstream 和 std::ofstream,它们用于自动管理文件的打开和关闭。

迭代器:++it、it++哪个好,为什么

1for(iterator it = V.begin(); it != V.end(); ++it)    
2for(iterator it = V.begin(); it != V.end(); it++) 

由于it是用户自定义类型的,编译器无法对其进行优化,即无法直接不生成临时对象,进而等价于++it,所以每进行一次循环,编译器就会创建且销毁一个无用的临时对象。
对于int i,i++ 和 ++i 在release下,二者效率是等价的,因为编译器对其进行优化了。

  1. 前置返回一个自加之后的引用,后置返回一个对象(自身的值副本,所以不能为左值);不需要创建临时对象,直接对迭代器进行增量操作,然后返回引用。
// ++i实现代码为:
int& operator++(){
  *this += 1;  
  return *this;
  } 
  1. 前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低;必须创建一个临时对象来保存当前迭代器的状态,然后再进行增量操作。这个临时对象的创建和销毁会带来额外的开销,
//i++实现代码为:                 
int operator++(int)                 
{
    int temp = *this;                      
    ++*this;                         
    return temp;                  
} 

说一下C++左值引用和右值引用

C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。

  1. 在C++11中所有的值必属于左值、右值两者之一,**右值又可以细分为纯右值、将亡值。**在C++11中可以取地址的、有名字的就是左值,反之,**不能取地址的、没有名字的就是右值(将亡值或纯右值)。**举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
  2. C++11对C++98中的右值进行了扩充。**在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。**将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
  3. 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
  4. 右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

左值(Lvalue)和右值(Rvalue):

左值(Lvalue):指的是有持久存储位置的对象,可以取地址,有名字。例如,变量 a 是一个左值,可以通过 &a 获取它的地址。
右值(Rvalue):指的是没有持久存储位置的值,通常是临时对象或字面量。例如,表达式 3 + 4 或函数返回值 func() 是右值,无法通过取地址操作访问。

左值引用和右值引用:

左值引用(Lvalue Reference):绑定到左值的引用类型,用于对具有持久存储的对象进行引用。例如,int &ref = a;。
右值引用(Rvalue Reference):绑定到右值的引用类型,用于捕获临时对象,以便通过移动语义优化性能。例如,int &&rref = 10;。
右值引用使用两个与号来声明,例如:

int&& rref = 10; // 10是一个int类型的右值

移动语义:移动语义允许我们将资源从一个对象转移到另一个对象,而不是拷贝。这通常通过移动构造函数和移动赋值操作符来实现,这两者都接受一个右值引用作为参数。

class MyClass {
public:
    MyClass(MyClass&& other) { // 移动构造函数
        // 从other中窃取资源
    }

    MyClass& operator=(MyClass&& other) { // 移动赋值操作符
        if (this != &other) {
            // 释放当前资源
            // 从other中窃取资源
        }
        return *this;
    }
};

使用移动语义可以减少创建和销毁临时对象的开销,特别是对于包含动态分配内存或其他资源的类来说,这种优化是非常有价值的。

右值引用的应用:

移动语义:右值引用通过转移资源(如内存)来避免拷贝,提高性能。这通常使用 std::move 来将左值强制转换为右值引用,使其可以绑定到右值引用类型。
完美转发:利用右值引用和模板,使得函数能够根据参数的实际类型完美转发,从而保留右值或左值属性。
参考文献:https://blog.csdn.net/kelvin_yin/article/details/138551173

注意事项:
右值引用本身是一个左值,因为它有名字,所以如果你想将一个右值引用再次以右值的形式传递,你需要使用std::move来转换它。
不要返回局部变量的右值引用,因为局部变量在函数返回后会被销毁,这会导致悬垂引用。
使用右值引用时必须谨慎,确保不会误用导致资源泄漏或者其他未定义行为。

map和unordered_map相关面试题

StL中红黑树实现

在这里插入图片描述
红黑树是一种多路平衡搜索树(采用中序遍历是一种有序的结构);
有序是通过比较key的大小保持有序;
通过key来区分不同的节点
O(1)拿到最小和最大的节点:红黑树迭代器(begin和rbegin);
header节点维护了左右指针;

散列表

在这里插入图片描述
在这里插入图片描述

散列表是一个指针数组,还有链表;散列表也是通过key区分不同的节点;
通过key进行hash,然后生成一个数,对数组长度进行取余,得到索引值;决定key所存储的位置;
负载因子:也就是hash冲突;用负载因子来评判实际存储元素的个数/数组的长度,来评判要不要扩缩容;如果扩容了,size变大,所有节点位置都需要改变,改变的过程叫做rehash;
当有hash冲突的时候,通过链表的方式;缺点:O(1)复杂度变为O(n);

底层采用红黑树的容器

map
set
multimap
mutiset

底层采用散列表的容器

unorder_map
unorder_set
unorder_multimap
unorder_multiset

红黑树好AVL树的区别

红黑树就是平衡各条链路黑节点的个数相等;通过维持黑节点个数一致实现相对平衡;
红黑树是通过引入红黑节点在引入新节点的时候避免树的调整频率,提高效率;
AVL树:左右子树相差高度最大为1;平衡要求高;查询效率高,添加和删除的操作频繁;
红黑树具有较小的平衡代价,插入和删除的效率更高,AVL树提供更严格的平衡,查找性能更有;

unordered_map是否有缩容操作?

在这里插入图片描述
在这里插入图片描述

unorder_map的性能优化

在这里插入图片描述
如果一开始设定初始大小很小,在不断添加的时候,会不断扩容,rehash,再扩容rehash,这样效率很低;

map和unorder_map的区别

在这里插入图片描述
map:红黑树,
unorer_map:散列表;
所以查找性能散列表最快是O(1)
红黑树是log 2 n;
红黑树有序,散列表无序;
红黑树没有内存浪费;散列表存在空间浪费;

key为字符串,且不区分大小写,map和unorder_map分别怎么处理?

map:通过比较运算符所以要实现小于符号;因为这是有序的嘛
unorder_map::要实现等于符号;还要实现hash;

key为结构体或类对象,map和unorder_map需要怎么处理?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值