Skip to content

C++

基础

C和C++的区别以及C++的特点

  1. C++ 在设计上兼容大量 C 语言语法,但 C 并不是严格意义上的 C++ 子集。C++11 之后又引入了 nullptrauto、Lambda、右值引用、智能指针等新特性。
  2. C++ 支持面向对象编程,引入了类、封装、继承、多态等机制;C 语言主要是面向过程的编程语言。
  3. C 语言中指针、强制类型转换、手动内存管理等特性容易带来安全问题。C++ 通过 const、引用、四种类型转换、异常、智能指针等机制改善了安全性和可维护性。
  4. C++ 的可复用性更强。模板和标准模板库 STL(Standard Template Library)体现了泛型编程思想,容器和算法分离,使用上比 C 语言函数库更灵活。
  5. C++ 兼顾抽象能力和运行效率,适合对性能、工程组织和可维护性都有要求的场景。

include头文件双引号""和尖括号<>的区别?

区别

尖括号 < > 通常用于包含系统头文件,双引号 " " 通常用于包含项目内的自定义头文件。两者的主要区别是预处理阶段的查找路径不同。

查找路径

< > 的头文件:优先从编译器配置的系统头文件路径中查找。

" " 的头文件:通常先从当前源文件所在目录或项目目录查找,再查找系统头文件路径。

动态链接与静态链接区别?

静态链接:链接阶段会把需要的库代码合并到最终可执行文件中。程序运行时不再依赖对应的静态库文件。Windows 下静态库常见后缀为 .lib,Linux 下常见后缀为 .a

动态链接:链接阶段不把动态库代码直接合并进可执行文件,而是在程序加载或运行时解析动态库符号。如果缺少对应动态库,程序可能无法启动或运行失败。Windows 下动态库常见后缀为 .dll,Linux 下常见后缀为 .so

区别

  1. 静态链接会把目标文件和静态库链接成一个完整的可执行程序;动态链接是在加载或运行时解析动态库中的函数符号。
  2. 静态链接生成的可执行文件体积较大,但部署时依赖较少;动态链接生成的可执行文件体积较小,但运行时依赖动态库。
  3. 静态链接中同一份库代码可能被多个程序各自包含,空间占用较大;动态链接可以让多个程序共享同一份动态库代码。

静态类型/动态类型和静态绑定/动态绑定

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

代码到可执行文件过程?

gcc 是 GNU 编译器集合,支持 C、C++、Objective-C、Fortran 等多种语言。g++ 通常用于编译 C++ 程序,并会自动链接 C++ 标准库;用 gcc 编译 C++ 时则需要手动指定 C++ 标准库。

Compile Process

g++ test.cpp -o testg++ test.cpp 会自动执行上述流程。

g++ -std=c++11 a.cpp 表示按 C++11 标准编译;-I 参数用于指定头文件搜索目录。

选项阶段作用示例输出
-E预处理宏替换、头文件展开、去掉注释g++ -E test.cpp -o test.i.i
-S编译生成汇编文件g++ -S test.i -o test.s.s
-c汇编生成目标文件,不能直接执行g++ -c test.s -o test.o.o
-o链接指定最终输出文件名g++ test.o -o test可执行文件

原码、反码、补码?

整型数值在计算机的存储里,最左边的一位代表符号位,0代表正数,1代表负数。

  1. 原码:为二进制的数,如:10 原码为0000 1010

  2. 反码:正数的反码与原码相同:如:10 原码为0000 1010,反码为0000 1010

    负数为原码0变1,1变0,(符号位不变):如:-10 原码为1000 1010,反码为1111 0101

  3. 补码:正数的补码与原码相同:如:10 原码为0000 1010,补码为0000 1010

    负数的补码为反码加1:如:-10 反码为1111 0101,补码为1111 0110

正数:原码、反码、补码相同。

负数:反码是在原码基础上除符号位外按位取反,补码是在反码基础上加 1。

大端 小端?

大端存储:字数据的高字节存储在低地址中 (网络字节序)

小端存储:字数据的低字节存储在低地址中 (主机字节序)

在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输

c++
#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;
}

结构体和共用体的区别?

  1. struct和union都是由多个不同的数据类型成员组成。 struct的所有成员都存在;但在任何同一时刻, union中只存放了一个被选中的成员,共用体是共用内存空间,所以每个成员都是读写同一个内存空间,那么内存空间里面的内容不停的被覆盖,而同一时刻,都只能操作一个成员变量。否则会出现读错误。
  2. 在不考虑字节对齐的情况下,struct变量的总长度等于所有成员长度之和。Union变量的长度等于最长的成员的长度。
  3. struct的不同成员赋值是互不影响的;而对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了。

枚举

enum是一个派生数据类型,可以声明、定义一个整型常数集合。所不同的是,集合里面的整型常数是用其他名字代替的,但只是代替,其本质还是一个整型常数。(默认从0开始顺序定义,常用于定义状态码)

C++中 struct 和 class 的区别?

struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;

struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的,例如:

c++
struct A{
    int iNum;    // 默认访问控制权限是 public
}
class B{
    int iNum;    // 默认访问控制权限是 private
}

在继承关系中,struct 默认是公有继承,而 class 是私有继承;

class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,例如:

c++
template<typename T, typename Y>    // 可以把typename 换成 class 
int Func(const T& t, const Y& y) { 
    // ...
}

C++结构体和C结构体的区别?

(1)C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。

(2)C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。

(3)C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。

(4)C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名,而 C++ 中可以省略 struct 关键字直接使用。

C++有几种传值方式,区别是什么?

  1. 值传递:形参即使在函数体内值发生变化,也不会影响实参的值;在函数传参的过程中,函数会为形参申请新的内存空间,并将实参的值复制给形参。形参的改变当然不会影响实参的值。
  2. 引用传递:形参在函数体内值发生变化,会影响实参的值;
  3. 指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;

全局变量和局部变量的区别?

  1. 作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
  2. 内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
  3. 生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了
  4. 使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量。

当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值

数据类型

初始化默认值

int

0

char

'\0'

float

0

double

0

pointer

NULL

C++内存分布模型

Cpp Memory Layout

如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆栈段组成。

  1. 代码段:存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)。

  2. 数据段data:存放程序中已初始化非零全局变量静态变量的一块内存区域。

  3. BSS 段:存放程序中未初始化全局变量静态变量的一块内存区域。

  4. 可执行程序在运行时又会多出两个区域:堆区栈区。

    **堆区:**动态申请内存用。堆从低地址向高地址增长。

    栈区:存储局部变量函数参数值。栈从高地址向低地址增长。是一块连续的空间。

  5. 最后还有一个文件映射区(共享区),位于堆和栈之间。

初始化为 0 的全局变量在 BSS 还是 data?

BSS 段通常用来存放未初始化的全局变量、静态变量,以及初始化为 0 的全局变量、静态变量。该区域可读写,程序执行前会被系统自动清零。

堆和栈的区别?

  1. 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值局部变量的值等,栈有着很高的效率;堆一般由程序员分配释放,堆的效率比栈要低的多
  2. 堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
  3. 空间大小: 栈的空间大小并不大,一般最多为2M,超过之后会报Overflow错误。堆的空间非常大,理论上可以接近3G。(针对32位程序来说,可以看到内存分布,1G用于内核空间,用户空间中栈、BSS、data又要占一部分,所以堆理论上可以接近3G,实际上在2G-3G之间)。
  4. 能否产生碎片: 栈的操作与数据结构中的栈用法是类似的。‘后进先出’的原则,以至于不可能有一个空的内存块从栈被弹出。因为在它弹出之前,在它上面的后进栈的数据已经被弹出。它是严格按照栈的规则来执行。但是堆是通过new/malloc随机申请的空间,频繁的调用它们,则会产生大量的内存碎片。这是不可避免地。

什么是野指针/悬空指针,如何避免?

野指针:没有初始化的指针,指向的位置不确定。

悬空指针:指针曾经指向有效内存,但该内存已经被释放或生命周期已经结束。

如何避免

野指针:定义指针变量时及时初始化,暂时不用时置空。

悬空指针freedelete 后立即置空,避免继续访问已经释放的内存。

使用指针时不要超出所指对象的生命周期。

优先使用智能指针管理资源。

数组和指针的区别?

数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址

指针:指针相当于一个变量,但是它和普通变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。

区别:

  1. 赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝

  2. 存储方式: 数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。

    指针:指针本身就是一个变量,作为局部变量时存储在栈上。

  3. sizeof:数组元素个数可以用 sizeof(数组名) / sizeof(元素类型) 计算。

    32 位平台下,sizeof(指针) 通常为 4;64 位平台下,sizeof(指针) 通常为 8。

引用和指针的区别?

  1. 指针是实体,占用内存空间;引用是别名,与变量共享内存空间。
  2. 指针不用初始化或初始化为NULL;引用定义时必须初始化。
  3. 指针中途可以修改指向;引用不可以。
  4. 指针可以为NULL;引用不能为空。
  5. sizeof(指针)计算的是指针本身的大小;而sizeof(引用)计算的是它引用的对象的大小。
  6. 如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
  7. 指针使用时需要解引用;引用使用时不需要解引用‘*’。
  8. 有二级指针;没有二级引用。

数组指针与指针数组的区别?

数组指针是一个指针变量,指向了一个一维数组, 如int (*p)[4](*p)[4]就成了一个二维数组,p也称行指针;指针数组是一个数组,只不过数组的元素存储的是指针变量, 如int *p[4]

指针函数与函数指针的区别?

指针函数本质是一个函数,其返回值为指针。 函数指针本质是一个指针,其指向一个函数。

c++
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);

指针函数返回一个指针。 函数指针使用过程中指向一个函数。通常用于回调函数的应用场景。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。

const的作用?

指针常量(顶层const)和常量指针(底层const)

  1. const修饰普通类型的变量,告诉编译器某值是保持不变的。

  2. const 修饰指针变量,根据const出现的位置和出现的次数分为三种

    1. 常量指针:指针指向一个常量对象,目的是防止使用该指针来修改指向的值。const int* ptr;
    2. 指针常量:将指针本身声明为常量,这样可以防止改变指针指向的位置。int* const ptr;
    3. 指向常量的常指针:一个常量指针指向一个常量对象
  3. const修饰参数传递,可以分为三种情况。

    1. 值传递的 const 修饰传递,一般这种情况不需要 const 修饰
    2. 当 const 参数为指针时,可以防止指针被意外篡改。
    3. 自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。
  4. const修饰函数返回值,分三种情况。

    1. const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
    2. const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
    3. const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
  5. const修饰类成员函数

    const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。

    常量对象可以调用类中的 const 成员函数,但不能调用非 const 成员函数; (原因:对象调用成员函数时,在形参列表的最前面加一个形参 this,但这是隐式的。this 指针是默认指向调用函数的当前对象的,所以,很自然, this 是一个常量指针 test const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即小括号) 后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test const,即不可以通过 this 指针来改变指向对象的值。

static的作用?

控制变量的存储方式和可⻅性

1.静态局部变量

用于函数体内部修饰变量

(1)该变量在全局数据区分配内存(局部变量在栈区分配内存); (2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化(局部变量每次函数调用都会被初始化); (3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化); (4)它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体外面使用它(局部变量在栈区,在函数结束后立即释放内存);

2.静态全局变量和静态函数

定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。

(1)静态全局变量不能被其它文件所用(全局变量可以); (2)其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);

3.静态成员变量

所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;

不需要实例化对象即可访问

注意不能再类内部初始化!要在类外部初始化,初始化时不加static!(如果类外定义函数时在函数名前加了static,因为作用域的限制,就只能在当前cpp里用,歧义)

4.静态成员函数

静态成员函数没有 this 指针,因此只能直接访问类的静态成员。

这个函数不需要实例化对象即可访问

为什么静态成员变量通常要在类外初始化?

静态成员变量属于类本身,不属于某个具体对象。类内声明只是告诉编译器存在这个成员,通常还需要在类外提供一次定义和初始化,否则可能没有实际存储空间。

为什么静态成员函数不能访问非静态成员?

静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。既然它没有指向某一对象,也就无法对一个对象中的非静态成员进行访问。

为什么要少使用#define?

  1. 由程序编译的四个过程,知道宏是在预编译阶段被展开的。在预编译阶段是不会进行语法检查、语义分析的,宏被暴力替换,正是因为如此,如果不注意细节,宏的使用很容易出现问题。比如在表达式中忘记加括号等问题。

  2. 正因为如此,在C++中为了安全性,我们就要少用宏。

    不带参数的宏命令我们可以用常量const来替代,比如const int PI = 3.1415,可以起到同样的效果,而且还比宏安全,因为这条语句会在编译阶段进行语法检查。

    而带参数的宏命令有点类似函数的功能,在C++中可以使用内联函数或模板来替代,内联函数与宏命令功能相似,是在调用函数的地方,用函数体直接替换。但是内联函数比宏命令安全,因为内联函数的替换发生在编译阶段,同样会进行语法检查、语义分析等,而宏命令发生在预编译阶段,属于暴力替换,并不安全。

什么是内联函数?

内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。

内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。

为什么使用内联函数?

函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。

如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数

内联函数使用的条件

  • 以下情况不宜使用内联:

    (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

    (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

  • 内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。

不能被重载的运算符

1、 . (成员访问运算符)

2、* (成员指针访问运算符)

3、:: (域运算符)

4、sizeof (长度运算法)

5、? : (条件运算符)

四种强制类型转换

c++
xxx_cast<type-id> (expression);

static_cast

最常见的类型转换。它可以用于基本数据类型之间的转换,也可以用于有继承关系的指针或引用之间的转换,但不会进行运行时类型检查。

  1. 基本数据类型之间的转换。
  2. 将任何类型转换为void类型。
  3. 将空指针转换成目标类型的指针。
  4. 用于类层次结构中基类和派生类之间指针或引用的转换。向上转换(派生类转换为基类)是安全的;向下转换(基类转换为派生类)没有动态类型检查,是不安全的。
c++
int i = 10;
double d = static_cast<double>(i);  // 整型转为浮点型

dynamic_cast

主要用于基类和派生类之间的安全转换,会进行运行时类型检查。转换失败时,指针转换返回 nullptr,引用转换会抛出异常。使用 dynamic_cast 的类通常需要包含虚函数。

c++
Base *b = new Derived();
Derived *d = dynamic_cast<Derived*>(b);  // 基类指针转为派生类指针
if (d != nullptr) {
  // 转换成功
} else {
  // 转换失败
}

const_cast

用于修改常量对象的常量属性。需要注意的是,使用 const_cast 去掉常量性质并修改数据可能导致未定义的行为。**只能用于转换指针或引用,type_id和expression的类型是一样的。

c++
int num = 100;
const int* p1 = &num;
//将常量指针转换为普通类型指针,去除const属性
int* p2 = const_cast<int*>(p1);
*p2 = 200;
int a = 100;
const int& ra = a;
//将常量引用转换为普通类型引用,去除const属性
int& ra1 = const_cast<int&>(ra);
ra1 = 200;

reinterpret_cast

用于低层次的重新解释,常见于不同指针类型之间、指针和整数之间的转换。它不保证转换后的语义安全,应该谨慎使用。

c++
char c = 'a';
int d = reinterpret_cast<int&>(c);
int* p=NULL;
float* q = NULL;
p = reinterpret_cast<int*>(q);
q = reinterpret_cast<float*>(q);

智能指针

智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,智能指针实质是一个对象,行为表现的却像一个指针。智能指针就是一个,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。这样程序员就不用再担心内存泄露的问题了。

C++ 常见智能指针包括 unique_ptrshared_ptrweak_ptr。早期的 auto_ptr 已被 C++11 弃用,并在 C++17 中移除。

shared_ptr

shared_ptr 实现共享式拥有概念,智能指针可以指向相同对象。该对象和其相关资源会在最后一个引用被销毁时候释放。

实现原理:

  • 构造函数中计数值初始化为1
  • 拷贝构造函数中计数值加1
  • 赋值运算中,左边的对象引用计数减去1,右边的对象引用计数加上1
  • 析构函数中引用计数要减去1
  • 在赋值和析构函数中,如果计数值减去1后为0,则调用delete释放对象

weak_ptr

weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数

解决shared_ptr内存泄露,共享指针的循环引用计数问题:当两个类中相互定义shared_ptr成员变量,同时对象相互赋值时,就会产生循环引用计数问题,最后引用计数无法清零,资源得不到释放。

可以使用weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。

unique_ptr

unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。 保证同一时间内只有一个智能指针可以指向该对象

实现原理:禁止拷贝构造和拷贝赋值,只允许资源所有权转移。

c++
unique_ptr<string> p3(new string("auto"));
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错

成员函数

c++
//unique_ptr
release();    // 返回一个指向被管理对象的指针,并释放所有权
reset();    //替换被管理对象
swap();    //交换被管理对象
get();    //返回指向被管理对象的指针
//shared_ptr
reset();    //替换被管理对象
swap();    //交换被管理对象
get();    //返回指向被管理对象的指针
use_count();    //返回 shared_ptr 所指对象的引用计数
//weak_ptr
reset();    //替换被管理对象
swap();    //交换被管理对象
use_count();    //返回 shared_ptr 所指对象的引用计数
lock();    //创建管理被引用的对象的shared_ptr

类 对象

面向对象和面向过程的区别?

面向过程(Procedure Oriented 简称 PO):把事情拆分成几个步骤(相当于拆分成一个个的方法和数据),然后按照一定的顺序执行。

面向对象(Object Oriented 简称 OO):面向对象会把事物抽象成对象的概念,先抽象出对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法。

C++有几种构造函数?

  • 默认构造函数
  • 初始化构造函数(有参数)
  • 拷贝构造函数
  • 移动构造函数(move和右值引用)
  • 委托构造函数
  • 转换构造函数
c++
#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("s2 age:%d, num:%d\n", s4.age, s4.num);
    return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s2 age:10, num:1002

成员初始化列表

用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。

c++
class complex
{
public:
    complex(double r = 0, double i = 0)
        : re(r), im(i)
        {}
    complex& operator += (const complex&)
    double real () const { return re; }
    double imag () const { return im; }
private:
    double re, im;
    friend complex& __doapl (complex* , const complex&);
}

拷贝构造函数和赋值运算符重载的区别?

赋值运算符

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的。

  • 拷贝构造函数是函数,赋值运算符是运算符重载。

  • 拷贝构造函数会生成新的类对象,赋值运算符不能。

  • 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。

  • 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:

    cpp
    Student s;
    Student s1 = s;    // 调用拷贝构造函数
    Student s2;
    s2 = s;    // 赋值运算符操作

注意:类中如果直接管理堆资源,通常需要自定义析构函数、拷贝构造函数和拷贝赋值运算符,避免浅拷贝导致重复释放或内存泄漏。

拷贝构造函数的参数类型为什么必须是引用?

如果拷贝构造函数的参数不是引用,例如 CClass(const CClass c_class),就会采用值传递。值传递本身又需要调用拷贝构造函数,从而造成递归调用。因此拷贝构造函数的参数必须使用引用,通常写成 const CClass&

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

类对象需要拷贝时,拷贝构造函数会被调用。常见场景包括:

  1. 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。
  2. 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。
  3. 一个对象需要通过另外一个对象进行初始化。

左值和右值?

C++ 中有两种类型的表达式:

  • **左值(lvalue):**指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
  • **右值(rvalue):**术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。

变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。

右值引用的作用?

C++11 引入右值引用 &&,主要用于实现移动语义和完美转发。

移动语义用于减少临时对象或可转移资源对象的拷贝开销。

完美转发是指把参数继续转交给另一个函数时,尽量保持参数原本的左值、右值属性。

移动语义的原理?

Move Semantics

移动语义通过移动构造函数或移动赋值运算符转移资源所有权。它不像拷贝构造那样重新分配空间并复制数据,而是接管原对象持有的资源,再把原对象的资源指针置为 nullptr,避免重复释放。

类的访问权限有几种?

  1. 公有成员(变量和函数)允许类成员和类外的任何访问,由public限定;
  2. 私有成员(变量和函数)只限于类成员访问,由private限定;
  3. 受保护成员(变量和函数)允许类成员和派生类成员访问,不允许类外的任何访问。所以protected对外封闭,对派生类开放。

继承类型和访问属性

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

总结: 不管是哪种继承方式,派生类中新增成员可以访问基类的公有成员和保护成员,无法访问私有成员。但是只有公有继承中,派生类的对象能访问基类的公有成员。使用友元(friend)可以访问保护成员和私有成员。

多继承存在什么问题?如何消除多继承的二义性?

  1. 多继承会增加程序的复杂度,使得程序的编写和维护比较困难,容易出错

  2. 在继承时,基类之间或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性,即同名二义性;

    消除二义性的方法:

    • 利用作用域运算符 ::,用于限定派生类使用的是哪个基类的成员
    • 在派生类中定义同名成员,覆盖基类的相关成员
  3. 当派生类从多个基类派生,而这些基类又来自同一个间接基类时,访问共同基类成员可能产生另一种不确定性,即路径二义性

    消除路径二义性的方法:

    • 消除同名二义性的两种方法同样有用
    • 还可以使用虚继承,使得不同路径继承来的同名成员在内存中只有一份拷贝。

虚基类与虚继承是什么?

多继承很容易产生错误,典型的就是菱形继承,是一种路径二义性的情况:

Diamond Inheritance

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:

cpp
//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: public A{
protected:
    int m_b;
};

//直接基类C
class C: public A{
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    // 因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

利用作用域运算符 ::消除二义性:

cpp
void seta(int a){ B::m_a = a; } // 表示使用 B 类的 m_a,反之使用C的同理

利用虚继承解决:在继承方式前面加上 virtual 关键字就是虚继承

cpp
//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

Virtual Inheritance

C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

Iostream Diamond Inheritance

多态的实现?

利用虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

多态其实一般就是指继承加虚函数实现的多态,多态可以分为静态多态和动态多态。静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态,一般情况下我们不区分这两个时所说的多态就是指动态多态。

虚函数的实现原理

C++实现虚函数的原理是虚函数表+虚表指针

当一个类里存在虚函数时,编译器会为类创建一个虚函数表,虚函数表是一个数组,数组的元素存放的是类中虚函数的地址

同时为每个类的对象添加一个隐藏成员,该隐藏成员保存了指向该虚函数表的指针。该隐藏成员占据该对象的内存布局的最前端。

Virtual Function Table

虚函数表在什么时候创建?每个对象都有一份虚函数表吗?

当一个类里存在虚函数时,编译器会为类创建一个虚函数表,发生在编译期。

虚函数表只有一份,而有多少个对象,就对应多少个虚函数表指针

虚函数表指针保存虚函数表的地址,属于对象实例的一部分。因此,通过 new 创建的对象,其虚函数表指针随对象位于堆上;在栈上声明的对象,其虚函数表指针随对象位于栈上。

虚函数表位于只读数据段(.rodata),即:C++内存模型中的常量区(用于维护只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等);

虚函数代码则位于代码段(.text),也就是C++内存模型中的代码区

纯虚函数?

c++
virtual void fun() = 0;

纯虚函数的类称为抽象类**(Abstract Class)。之所以说它抽象,是因为它无法实例化**,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

析构函数必须为虚函数吗?构造函数可以为虚函数吗?

C++ 默认析构函数不是虚函数,因为声明虚函数会引入虚函数表和虚表指针,带来额外开销。当类不存在继承使用场景时,析构函数通常不需要声明为虚函数。

如果类作为基类使用,析构函数通常应声明为虚函数。这样通过基类指针删除派生类对象时,才能正确调用派生类析构函数释放资源,否则可能造成资源泄漏。

构造函数不能为虚函数。虚函数调用依赖对象中的虚表指针,而构造函数执行时对象还没有完整构造完成,虚表指针也不能用于完成真正的动态绑定。

构造与析构的顺序?

构造顺序:基类构造函数>对象成员构造函数>子类构造函数

析构顺序:子类析构函数>对象成员析构函数>基类析构函数

从里向外构造,从外向里析构。

深拷贝与浅拷贝的区别?

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

什么是this指针?

在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this指针。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址

this是一个指针,它时时刻刻指向你这个实例本身

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

重载、重写、隐藏?

重载(overload)

函数名相同,参数列表不同,包括参数个数、参数类型或参数顺序不同。函数重载不能只靠返回值区分。

特点:作用域相同;函数名相同;参数列表必须不同;返回值类型不作为重载依据。

特殊情况:若某一重载版本的函数前面有virtual关键字修饰,则表示它是虚函数,但它也是重载的一个版本

作用效果:编译器根据参数列表在编译期决定调用哪个函数,属于静态绑定。

重写(override)

派生类重新定义基类中的虚函数,用于实现运行时多态。

特点:作用域不同;函数名、参数列表、返回值类型兼容;基类函数是 virtual

特殊情况:若派生类重写函数是一个重载版本,那么基类的其他同名重载函数将在子类中隐藏。

作用效果:基类指针或引用指向派生类对象时,可以调用派生类重写后的函数。

隐藏(hide)

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数。

两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。

两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。

空类大小?

sizeof(A) 的值通常为 1。即使类没有数据成员,编译器也需要保证不同对象拥有不同地址,因此会为空类对象分配 1 个字节。

空类有哪些成员函数?

c++
class Empty
{
public:
    Empty(); // 缺省构造函数
    Empty( const Empty& ); // 拷贝构造函数
    ~Empty(); // 析构函数
    Empty& operator=( const Empty& ); // 赋值运算符
    Empty* operator&(); // 取址运算符
    const Empty* operator&() const; // 取址运算符 const
};

友元类和友元函数?

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。但是另一个类里面也要相应的进行声明。

友元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员和保护成员。但是需要在类的定义中声明所有可以访问它的友元函数。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

模板

程序所实现的功能基本相同,不同的仅是数据类型不同。而模板正是一种专门处理不同数据类型的机制。

c++
#include<iostream> 
using namespace std; 
template<typename type1,typename type2>//函数模板 
type1 Max(type1 a,type2 b) 
{ 
   return a > b ? a : b; 
} 
void main() 
 { 
  cout<<"Max = "<<Max(5.5,'a')<<endl; 
}

该模板有个比较隐晦的bug,那就是a、b只有在能进行转型的时候才能进行比较,否则 a > b 这一步是会报错的,这个时候往往需要对于 > 号进行重载。

模版特例化

定义:对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上

模板函数特例化

必须为原函数模板的每个模板参数都提供实参,且使用关键字template后跟一个空尖括号对<>,表明将原模板的所有模板参数提供实参

注意:模板及其特例化版本应该声明在同一个头文件中,且所有同名模板的声明应该放在前面,后面放特例化版本。

类模板特例化

原理类似函数模板,**不过在类中,我们可以对模板进行特例化,也可以对类进行部分特例化。**对类进行特例化时,仍然用template<>表示是一个特例化版本。

类模板的部分特例化

不必为所有模板参数提供实参,可以指定一部分而非所有模板参数,一个类模板的部分特例化本身仍是一个模板,使用它时还必须为其特例化版本中未指定的模板参数提供实参(特例化时类名一定要和原来的模板相同,只是参数类型不同,按最佳匹配原则,哪个最匹配,就用相应的模板)

explicit

  • explicit 关键字只能用于类内部的构造函数声明上
  • 被explicit修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换

final override

final来限制某个类不能被继承,或者某个虚函数不能被重写。如果修饰函数,final只能修饰虚函数,并且要放到类或者函数的后面

override确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,还可以防止因疏忽把本来想重写基类的虚函数声明成隐藏。

extern

关键字:在C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

声明外部变量/函数:当你在一个文件中声明变量/函数,但实际的定义在另一个文件中时,你可以使用extern关键字来告诉编译器该变量是在其他文件中定义的。

编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名

assert

assert(expr) 如果expr表达式为假,assert输出信息并终止程序执行,如果为真,assert什么也不做。

#ifdef、#else、#endif、#ifndef的作用?

作用一:条件编译

一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

#ifdef 标识符
程序段 1
#else 
程序段 2
#endif

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

作用二:避免文件重定义

在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define#ifndef#ifdef#endif避免头文件重定义。

volatile mutable

volatile 表示变量可能被编译器无法感知的外部因素修改,因此每次读取都应从内存中重新取值,而不是直接使用寄存器缓存。它不能替代锁或原子操作来解决多线程同步问题。

mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能够修饰非静态数据成员。

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

只能在堆上

方法:将析构函数设为私有

原因:C++ 是静态綁定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

只能在栈上

方法:将new 和delete 重载为私有

原因:在堆上生成对象,使用new 关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

内存管理

什么是内存泄露?

简单地说就是申请了一块内存空间,使用完毕后没有释放掉。

(1)new和malloc申请资源使用后,没有用delete和free释放;

(2)子类继承父类时,父类析构函数不是虚函数。

(3)比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。

(4)shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。

如何避免内存泄漏?

养成良好的资源管理习惯:申请内存后,使用完毕要用匹配的释放方式释放。

也可以自行记录已分配内存,释放后从记录中删除,程序结束时检查是否还有未释放资源。

优先使用智能指针。

常见工具也可以辅助检测内存泄漏,如 ccmallocDmallocLeakyValgrind 等。

new/delete和malloc/free的异同?

c++
int *p = new int[2];
int *q = (int *)malloc(2*sizeof(int));
  1. 都可用于动态申请内存。
  2. new 是操作符,malloc 是 C 标准库函数。
  3. new 会先分配内存,再调用构造函数;delete 会先调用析构函数,再释放内存。malloc/free 只负责申请和释放原始内存,不会调用构造函数和析构函数。
  4. malloc 需要显式指定申请大小,返回值通常需要强制类型转换;new 根据类型自动计算大小,返回对应类型指针。
  5. new 可以被重载,malloc 不可以。
  6. new 申请失败默认抛出异常,malloc 申请失败返回 NULL

new/delete实现原理?

  • new 的实现过程:先调用 operator new 分配足够大的原始内存;再调用构造函数初始化对象;最后返回指向该对象的指针。
  • delete 的实现过程:先调用对象的析构函数;再调用 operator delete 释放该对象占用的内存。
c++
//operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
//尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)

     if (_callnewh(size) == 0)
     {
         // report no memory
         // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
         static const std::bad_alloc nomem;
         _RAISE(nomem);
     }
return (p);
}

malloc/free实现原理?

malloc 在申请内存时,一般会通过brk 或者mmap系统调用进行申请。

  • 申请内存小于128K时,会使用系统函数**brk堆区**中分配;
  • 当申请内存大于128K时,会使用系统函数**mmap映射区**分配。

brk是将数据段(.data)的最高地址指针_edata往高地址推;

mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存

什么是字节对齐?

为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐

为什么要字节对齐?

(1)需要字节对齐的根本原因在于CPU访问数据的效率问题

(2)一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。

(3)各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始

结构体字节对齐三原则:

1.每个数据成员存储起始位置要从该成员大小的整数倍开始

2.结构体作为成员时应从内部最大元素的整数倍地址开始存储

3.结构体总大小为内部最大成员的整数倍

STL

常见容器对比

容器底层结构迭代器支持随机访问主要特点
vector动态数组支持,随机访问迭代器支持,[] / at()尾部插入删除快,内存连续,随机访问快
deque分段连续数组支持,随机访问迭代器支持,[] / at()首尾插入删除快,随机访问略慢于 vector
list双向链表支持,双向迭代器不支持任意位置插入删除快,不支持下标访问
queue容器适配器不直接支持迭代器不支持先进先出,只能访问队首和队尾
stack容器适配器不直接支持迭代器不支持后进先出,只能访问栈顶
set / unordered_set红黑树 / 哈希表支持,双向迭代器 / 前向迭代器不支持存储 key,set 自动有序,unordered_set 无序
map / unordered_map红黑树 / 哈希表支持,双向迭代器 / 前向迭代器不支持存储 key-value,map 按 key 有序,unordered_map 无序

常用操作对比

操作vectordequelistqueuestackset / unordered_setmap / unordered_map
尾部添加push_backpush_backpush_backpushpush--
尾部删除pop_backpop_backpop_back-pop--
头部添加-push_frontpush_front----
头部删除-pop_frontpop_frontpop---
插入insertinsertinsert--insertinsert
删除eraseeraseerase--erase(key)erase(key)
改变大小resizeresizeresize----
交换内容swapswapswapswapswapswapswap
清空clearclearclear--clearclear
是否为空emptyemptyemptyemptyemptyemptyempty
大小sizesizesizesizesizesizesize
访问首元素frontfrontfrontfront---
访问尾元素backbackbackback---
访问栈顶----top--
查找 key-----find / countfind / count

二维数组常用写法:

cpp
vector<vector<int>> dp(n, vector<int>(m, 0));

补充说明:

  • vector 内存连续,随机访问通常比 deque 更快。
  • set 不允许重复元素,元素会按规则自动排序,不能直接修改元素值。
  • multiset 允许 key 重复;unordered_setunordered_multiset 不会自动排序。
  • map 中元素类型是 pair<const Key, Value>first 是 key,second 是 value。
  • map[key] = value 可以插入或修改 key 对应的 value;map[x]++ 常用于计数。

迭代器

迭代器是类模板不是指针(表现得像指针)

c++
container::iterator iter
container::const_iterator citer
  1. *iter :返回迭代器iter所指元素引用
  2. iter->mem :等价于(*iter).mem,解引用iter并获取该元素的名为mem的成员
  3. ++iter :令iter指示容器中的下一个元素
  4. --iter :令iter指示容器中的上一个元素
  5. iter + n:迭代器加上一个整数仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。
  6. iter - n:迭代器减去一个整数仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。

C++11新特性

lambda 函数式编程

lambda 的形式

C++ 没有为 lambda 表达式引入新的关键字,并没有“lambda”这样的词汇,而是用了一个特殊的形式“[]”,术语叫“lambda 引出符”(lambda introducer)。在 lambda 引出符后面,就可以像普通函数那样,用圆括号声明入口参数,用花括号定义函数体,方便写程序,使程序代码更加简洁。

cpp
auto f1 = [](){};      // 相当于空函数,什么也不做
auto f2 = []()                 // 定义一个lambda表达式
{
    cout << "lambda f2" << endl;

    auto f3 = [](int x)         // 嵌套定义lambda表达式
    {
        return x*x;
    };// lambda f3              // 使用注释显式说明表达式结束

    cout << f3(10) << endl;
};  // lambda f2               // 使用注释显式说明表达式结束

C++ 里,每个 lambda 表达式都会有一个独特的类型,而这个类型只有编译器才知道,我们是无法直接写出来的,所以必须用 auto。

不过,因为 lambda 表达式毕竟不是普通的变量,所以 C++ 也鼓励程序员**尽量“匿名”使用 lambda 表达式。**也就是说,它不必显式赋值给一个有名字的变量,直接声明就能用。

由于“匿名”,lambda 表达式调用完后也就不存在了(也有被拷贝保存的可能),这就最小化了它的影响范围,让代码更加安全。

cpp
vector<int> v = {3, 1, 8, 5, 0};     // 标准容器
cout << *find_if(begin(v), end(v),   // 标准库里的查找算法
            [](int x)                // 匿名lambda表达式,不需要auto赋值
            {
                return x >= 5;        // 用做算法的谓词判断条件 
            }                        // lambda表达式结束
        )
     << endl;                        // 语句执行完,lambda表达式就不存在了
  • find_if()的第三个参数是一个 lambda 表达式的谓词。这个 lambda 表达式以值的方式捕获 value,并在 lambda 参数大于 value 时返回 true。

lambda的变量捕获

lambda 的“捕获”功能需要在“[]”里做文章,由于实际的规则太多太细,记忆、理解的成本高,所以记住几个要点:

  • “[=]”表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改;
  • “[&]”是按引用捕获所有外部变量,内部以引用的方式使用,可以修改;
  • 也可以在“[]”里明确写出外部变量名,指定按值或者按引用捕获,C++ 在这里给予了非常大的灵活性。
cpp
int x = 33;               // 一个外部变量

auto f1 = [=]()           // lambda表达式,用“=”按值捕获
{
    //x += 10;            // x只读,不允许修改
};

auto f2 = [&]()         // lambda表达式,用“&”按引用捕获
{
    x += 10;            // x是引用,可以修改
};

auto f3 = [=, &x]()       // lambda表达式,用“&”按引用捕获x,其他的按值捕获
{
    x += 20;              // x是引用,可以修改
};

在使用捕获功能的时候要小心,对于“就地”使用的小 lambda 表达式,可以用“[&]”来减少代码量,保持整洁;而对于非本地调用、生命周期较长的 lambda 表达式应慎用“[&]”捕获引用,而且,最好是在“[]”里显式写出变量列表,避免捕获不必要的变量。

cpp
class DemoLambda final
{
private:
    int x = 0;
public:
    auto print()              // 返回一个lambda表达式供外部使用
    {
        return [this]()      // 显式捕获this指针
        {
            cout << "member = " << x << endl;
        };
    }
};

泛型的 lambda

C++14 里,lambda 表达式可以实现“泛型化”,相当于简化了的模板函数,具体语法利用了 auto:

cpp
auto f = [](const auto& x)        // 参数使用auto声明,泛型化
{
    return x + x;
};

cout << f(3) << endl;             // 参数类型是int
cout << f(0.618) << endl;         // 参数类型是double

string str = "matrix";
cout << f(str) << endl;          // 参数类型是string

= default 和 = delete

= default=delete 是 C++11 新增的专门用于六大基本函数的用法,对于比较重要的构造函数和析构函数,应该用= default的形式,明确地告诉编译器:“应该实现这个函数,但我不想自己写。”这样编译器就得到了明确的指示,可以做更好的优化。

cpp
class DemoClass final 
{
public:
    DemoClass() = default;  // 明确告诉编译器,使用默认实现
   ~DemoClass() = default;  // 明确告诉编译器,使用默认实现
};

另一种 = delete 的形式。表示明确地禁用某个函数形式,且不限于构造 / 析构,可以用于任何函数(成员函数、自由函数)。比如说,如果想要禁止对象拷贝,就可以用这种语法显式地把拷贝构造和拷贝赋值delete掉,让外界无法调用。

cpp
class DemoClass final 
{
public:
    DemoClass(const DemoClass&) = delete;              // 禁止拷贝构造
    DemoClass& operator=(const DemoClass&) = delete;  // 禁止拷贝赋值
};

补充

sizeof和strlen有什么区别?

  • sizeof是一个操作符,strlen是库函数
  • sizeof计算的是数据类型占内存的大小,strlen计算的是字符串实际的长度
  • sizeof的参数可以是数据的类型,也可以是变量,但strlen的参数只能是以 \0 为结尾的字符串
  • 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。

bind() 函数

c++
#include <functional>
std::bind(待绑定的函数对象/函数指针/成员函数指针,参数绑定值1,参数绑定值2,...,参数绑定值n);

bind的第一个参数是待绑定的函数对象或者函数指针,之后跟随多个参数以设定待绑定函数的参数绑定方式。待绑定函数有多少个参数,则bind后便需要多少个参数以一一声明其参数的绑定方法.当参数绑定为某一固定值时,则其对应参数绑定值可以使一个变量或常量.当需要将参数与绑定所生成的函数对象的某个参数相关联时,则需要用到在标准中预定义的几个常量 _1、_2、_3等.这些常量声明在std::placeholders命名空间内为占位符来改变参数的顺序,并且可以设置函数中默认的几个参数来减少输入参数的数量。

move() 函数

c++
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type &&>(t);
}

它唯一的功能是将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义

GDB

g++ -g 用于在编译时添加调试信息。

Linux 系统常用 GDB,macOS 常用 LLDB。GDB 和 LLDB 的命令对照可以参考:https://lldb.llvm.org/use/map.html

先编译生成可调试文件:g++ -g -std=c++11 a.cpp,再执行 gdb a.out 进入调试界面,最后用 rrun 运行程序。

b 用于打断点,info breakpointsinfo b 用于查看断点信息,del 1 用于删除编号为 1 的断点。

nnext:单步执行,不进入函数。

pprint:打印变量或变量地址。

sstep:进入函数调试。

shell ls:在 GDB 中执行终端命令。set logging on:打开日志模式。

watch:观察变量是否变化。info watchpoints:查看观察点信息。

ulimit -a:查看 core 文件相关限制。

Powered by VitePress