C++

基础

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

  1. C语言是C++的子集,C++可以很好兼容C语言。但是C++11又有很多新特性引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。
  2. C++是面向对象的编程语言,C++引入了新的数据类型 ,由此引申出了三大特性:封装、继承、多态。而C语言则是面向过程的编程语言。
  3. C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;
  4. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。STL的一个重要特点是数据结构和算法的分离,其体现了泛型化程序设计的思想。C++的STL库相对于C语言的函数库更灵活、更通用
  5. C++语言编写出的程序结构清晰、易于扩充,程序可读性好
  6. C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;

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

区别

尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件,编译器预处理阶段查找头文件的路径不一样。

查找路径

<>的头文件:编译器设置的头文件路径-->系统变量。

""的头文件:默认从项目当前目录查找头文件。

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

静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

区别

  1. 静态链接是将各个模块的obj和库链接成一个完整的可执行程序;而动态链接是程序在运行的时候寻找动态库的函数符号(重定位)
  2. 静态链接运行快、可独立运行;动态链接运行较慢(事实上,动态库被广泛使用,这个缺点可以忽略)、不可独立运行。
  3. 静态链接浪费空间,存在多个副本,同一个函数的多次调用会被多次链接进可执行程序,当库和模块修改时,main也需要重编译;动态链接节省空间,相同的函数只有一份,当库和模块修改时,main不需要重编译。

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

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

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

代码到可执行文件过程?

gcc是GNU编译器合集,能编译C, C++, Objective-C, Objective-C++, Fortran, Ada, D, Go, and BRIG (HSAIL)多种语言,g++能编译 c & c++,g++会自动链接标准库STL,而gcc不会自动链接STL

image-20210919202217064

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

g++ -std=c++11 a.cpp 支持c++11编译 -I参数是用来指定头文件所在目录

option 功能 举例 输出格式
-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 *.out(默认)

原码、反码、补码?

整型数值在计算机的存储里,最左边的一位代表符号位,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地址转换为大端存储,这样才能进行网络传输

#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 的,例如:

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

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

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

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

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++内存分布模型

img

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

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

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

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

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

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

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

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

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

BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

堆和栈的区别?

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

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

野指针就是没有被初始化的指针

悬空指针是指指针指向的内存空间已被释放或不再有效。

如何避免

野指针:指针变量未及时初始化 => 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 => 释放操作后立即置空。

使用时不要超出指针作用域

使用智能指针

数组和指针的区别?

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

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

区别:

  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]

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

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

指针函数: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、? : (条件运算符)

四种强制类型转换

xxx_cast<type-id> (expression);

static_cast

最常见的类型转换。它可以用于基本数据类型之间的转换,也可以用于指向父类和子类之间的指针或引用的转换

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

dynamic_cast

主要用于处理基类和派生类之间的转换。如果类型转换不安全,它会返回空指针NULL。这是唯一一种在运行时执行类型检查的转换。要求转换的类具有多态性质(虚函数)。type-id:类的指针、引用、void*

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

const_cast

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

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

允许进行任何指针或整型的转换。它可以将任何类型的指针转换为任何其他类型的指针,将指针或引用转换为一个整型,将一个整型转换为指针或引用类型要转换的类型必须是指针、引用或算术类型。

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++里面有四个指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr,auto_ptr被C++11弃用(存在潜在的内存崩溃问题)。

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

实现原理:只需要将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符

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

成员函数

//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和右值引用)
  • 委托构造函数
  • 转换构造函数
#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

成员初始化列表

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

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&);
}

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

赋值运算符

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

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

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

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

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

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

注:类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符。

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

如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。否则无法完成拷贝,而且栈也会满。

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

类的对象需要拷⻉时,拷⻉构造函数将会被调用,以下的情况都会调用拷⻉构造函数:

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

左值和右值?

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

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

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

右值引用的作用?

C++11引入右值引用&&主要是为了实现移动语义完美转发

移动语义为了避免临时对象的拷贝,为类增加移动构造函数。

完美转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

移动语义的原理?

img

移动语义为了避免临时对象的拷贝,为类增加移动构造函数。移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr

类的访问权限有几种?

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

继承类型和访问属性

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

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

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

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

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

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

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

    消除二义性的方法:

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

    消除路径二义性的方法:

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

虚基类与虚继承是什么?

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

image-20210909102027074

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

//间接基类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;
}

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

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

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

//间接基类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 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

image-20210909102539106

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

image-20210909102606304

多态的实现?

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

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

虚函数的实现原理

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

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

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

虚函数表

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

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

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

虚函数表指针是虚函数表所在位置的地址。虚函数表指针属于对象实例。因而通过new 出来的对象的虚函数表指针位于堆声名对象的虚函数表指针位于栈

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

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

纯虚函数?

virtual void fun() = 0;

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

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

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

C++默认析构函数不是虚函数,因为申明虚函数会创建虚函数表,占用一定内存,当不存在继承的关系时,析构函数不需要申明为虚函数。

若存在继承关系时,析构函数必须申明为虚函数,这样父类指针指向子类对象,释放基类指针时才会调用子类的析构函数释放资源,否则内存泄漏

构造函数不能为虚函数,当申明一个函数为虚函数时,会创建虚函数表,那么这个函数的调用方式是通过虚函数表来调用。若构造函数为虚函数,说明调用方式是通过虚函数表调用,需要借助虚表指针,但是没构造对象,哪里来的虚表指针?但是没有虚表指针,怎么访问虚函数表从而调用构造函数呢?这就成了一个先有鸡还是先有蛋的问题。

构造与析构的顺序?

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

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

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

深拷贝与浅拷贝的区别?

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

什么是this指针?

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

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

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

重载、重写、隐藏?

重载overload

函数名相同,参数列表不同(参数类型、参数顺序),不能用返回值区分。

特点:作用域相同;函数名相同;参数列表必须不同,但返回值无要求;

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

作用效果:编译器根据函数不同的参数列表,将函数与函数调用进行早绑定,重载与多态无关,与面向对象无关,它只是一种语言特性。

重写override

派生类重定义基类的虚函数,既会覆盖基类的虚函数(多态)。

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

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

作用效果:父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数,这就是C++的多态。

隐藏hide

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

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

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

空类大小?

sizeof(A) 的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有一个独一无二的地址,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。

空类有哪些成员函数?

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

友元类和友元函数?

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

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

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

模板

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

#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的变量是说这变量可能会被意想不到地改变,系统总是重新从它所在的内存读取数据。每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为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共享指针成环,造成循环引用计数,资源得不到释放。

如何避免内存泄漏?

良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。

将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

使用智能指针。

一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。

new/delete和malloc/free的异同?

int *p = new int[2];
int *q = (int *)malloc(2*sizeof(int));
  1. 都可用于内存的动态申请和释放
  2. new是操作符,而malloc是函数。
  3. new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
  4. malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
  5. new可以被重载;malloc不行
  6. new分配内存更直接和安全。
  7. new发生错误抛出异常,malloc返回null

new/delete实现原理?

  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
//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 deque(双端数组) list(双向链表) queue stack set/unordered_set map/unordered_map
是否支持迭代器 × ×
尾部添加 push_back push_back push_back push
尾部删除 pop_back pop_back pop_back
首部添加 push_front push_front push
首部删除 pop_front pop_front pop pop
插入(迭代器) insert insert insert insert insert
删除(迭代器) erase erase erase erase(key) erase(key)
改变大小 resize resize resize
交换内容 swap swap swap swap swap
清除 clear clear clear clear clear
是否为空 empty empty empty empty empty empty empty
大小 size size size size size size size
第一个元素 front front front front
最后一个元素 back back back back
访问元素 at() at() at() top(栈顶)
指定键的个数 count count
寻找指定键 存在返回迭代器,反之返回s.end() find find

vector> dp(n,vector(m,0)) n行m列二维数组

vector访问元素的速度要比deque快

set不允许出现重复 所有的元素都会被自动排序(默认从小到大) 不能直接修改它的元素值

multiset 允许出现键值重复,unordered_set unordered_multiset元素不会自动排序

map中是pair p(key,value) map.first是key 只出现一次 map.second是value map[key]=value

map[x]++ 把key放在map中计数

迭代器

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

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 引出符后面,就可以像普通函数那样,用圆括号声明入口参数,用花括号定义函数体,方便写程序,使程序代码更加简洁。

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 表达式调用完后也就不存在了(也有被拷贝保存的可能),这就最小化了它的影响范围,让代码更加安全。

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++ 在这里给予了非常大的灵活性。
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 表达式应慎用“[&]”捕获引用,而且,最好是在“[]”里显式写出变量列表,避免捕获不必要的变量。

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

泛型的 lambda

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

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的形式,明确地告诉编译器:“应该实现这个函数,但我不想自己写。”这样编译器就得到了明确的指示,可以做更好的优化。

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

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

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() 函数

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

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

move() 函数

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

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

GDB

g++ -g 添加gdb调试选项

linux系统支持gdb,mac支持lldb。https://lldb.llvm.org/use/map.html为gdb和lldb命令对照表。

先编译生成.out文件g++ -g -std=c++11 a.cppgdb a.out进入调试页面,最后rrun运行。

b打断点,info breeak查看断点信息,del 1删除断点。

nnext继续执行

pprint打印变量或变量地址

sstep进入函数调试

shell ls可以使用终端命令等,set logging on打开日志模式

watchpoint查看变量是否变化,info查看watchpoint信息。

ulimit -a 调试core文件

Copyright © YZJ 2022 all right reserved,powered by Gitbook更新时间: 2023-10-04 09:18:45

results matching ""

    No results matching ""