C++知识概要

  1. static的用法和作用
  • 在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量。存储在静态存储区,在整个程序运行期间一直存在。同时全局静态变量在声明他的文件之外是不可见的
  • 在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。存储在静态存储区,作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且该过程中值保持不变。
  • 在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
  • 在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用
  • 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名
    static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual 没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 是通过 this 指针调用的,所以不能为 virtual;虚函数的调用关系,this->vptr->ctable->virtual function。
  1. 静态变量初始化

静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存。在 C++ 中,初始化是在执行相关代码时才会进行初始化。

  1. 虚函数可以声明为 inline 吗

不可以
虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。
虚函数要求在运行时进行类型确定,而内联函数要求在编译期完成相关的函数替换

  1. static 修饰符

static 修饰成员变量,在数据段分配内存。
static 修饰成员函数,在代码区分配内存。

  1. 一个派生类构造函数的执行顺序如下
  1. 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)
  2. 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)
  3. 类类型的成员对象的构造函数(按照初始化顺序)
  4. 派生类自己的构造函数
  1. 必须使用成员列表初始化的四种情况
  • 当初始化一个引用成员时
  • 当初始化一个常量成员时
  • 当调用一个基类的构造函数,而它拥有一组参数时
  • 当调用一个成员类的构造函数,而它拥有一组参数时
  1. 构造函数为什么不能为虚函数
  • 虚函数对应一个指向虚函数表的指针,但是这个指向vtable 的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable 来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找 vtable 呢?所以构造函数不能是虚函数。
  • 因为构造函数本来就是为了明确初始化对象成员才产生的,然而 virtual function 主要是为了在不完全了解细节的情况下也能正确处理对象。另外,virtual 函数是在不同类型的对象产生不同的动作,现在对象还没有产生,也就不能使用 virtual 函数来完成你想完成的动作
  1. 析构函数为什么要虚函数

C++中基类采用 virtual 虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

  1. 构造函数析构函数可以调用虚函数吗

在构造函数和析构函数中最好不要调用虚函数
构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别
即使构造函数或者析构函数如果能成功调用虚函数, 程序的运行结果也是不可控的

  1. 空类的大小是多少?为什么
  • C++空类的大小不为 0,不同编译器设置不一样,vs 设置为 1
  • C++标准指出,不允许一个对象(当然包括类对象)的大小为 0,不同的对象不能具有相同的地址
  • 带有虚函数的 C++类大小不为 1,因为每一个对象会有一个 vptr 指向虚函数表,具体大小根据指针大小确定
  • C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址
  1. 移动构造函数
A(A&& b){
  ***
}
// a = std::move(b)
  1. 移动赋值
A& operator=(A&& b){
  ***
  return *this;
}
  1. 类如何实现只能静态分配和只能动态分配

前者是把 new、delete 运算符重载为 private 属性。后者是把构造、析构函数设为 protected 属性,再用子类来动态创建
建立类的对象有两种方式:

  1. 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;
  2. 动态建立,就是使用 new 运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象
  1. 什么情况会自动生成默认构造函数

带有默认构造函数的类成员对象
带有默认构造函数的基类
带有一个虚函数的类
带有一个虚基类的类
合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化

  1. 如何消除隐式转换

C++中提供了 explicit 关键字,在构造函数声明的时候加上 explicit 关键字,能够禁止隐式转换
如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为 explicit 加以制止隐式类型转换,关键字 explicit 只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit。

  1. 派生类指针转换为基类指针,指针值会不会变

将一个派生类的指针转换成某一个基类指针,编译器会将指针的值偏移到该基类在对象内存中的起始位置

  1. C 语言的编译链接过程

源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件

  • 预处理
    读取 c 源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号
  • 编译
    编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码
  • 汇编
    汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程
  • 链接阶段
    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
  1. 容器内部删除一个元素
  1. 顺序容器
    erase 迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list 除外),所以不能使用 erase(it++)的方式,但是erase 的返回值是下一个有效迭代器;
    it = c.erase(it);
  2. 关联容器
    erase 迭代器只是被删除元素的迭代器失效,但是返回值是 void,所以要采用 erase(it++)的方式删除迭代器;
    c.erase(it++)
  1. vector 越界访问下标,map 越界访问下标?vector 删除元素时会不会释放空间

通过下标访问 vector 中的元素时不会做边界检查,即便下标越界。也就是说,下标与 first 迭代器相加的结果超过了 finish 迭代器的位置,程序也不会报错,而是返回这个地址中存储的值。如果想在访问 vector 中的元素时首先进行边界检查,可以使用 vector 中的 at 函数。通过使用 at 函数不但可以通过下标访问 vector 中的元素,而且在 at 函数内部会对下标进行边界检查
map 的下标运算符[]的作用是:将 key 作为下标去执行查找,并返回相应的值;如果不存在这个 key,就将一个具有该 key 和 value 的默认值插入这个 map
erase()函数,只能删除内容,不能改变容量大小; erase 成员函数,它删除了 itVect 迭代器指向的元素,并且返回要被删除的 itVect 之后的迭代器,迭代器相当于一个智能指针,之后迭代器将失效。;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择 deque 容器

int main(){
  vector<int> vec(10, 0);
  int arr[10] = {0,0,0,0,0,0,0,0,0,0};
  cout << vec[11] << endl; // 输出值
  cout << *(vec.begin()+11) << endl; // 输出值
  cout << vec.at(11); // 报错,越界
  cout << arr[11]; // 输出值
}
  1. vector 的增加删除都是怎么做的?为什么是 1.5 倍

vector 通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素
初始时刻 vector 的 capacity 为 0,塞入第一个元素后 capacity 增加为 1
不同的编译器实现的扩容方式不一样,VS2015 中以 1.5 倍扩容,GCC 以 2 倍扩容
对比可以发现采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到 O(n)的时间复杂度,因此,使用成倍的方式扩容
以 2 倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间
向量容器 vector 的成员函数 pop_back()可以删除最后一个元素
而函数 erase()可以删除由一个 iterator 指出的元素,也可以删除一个指定范围的元素
还可以采用通用算法 remove()来删除 vector 容器中的元素
采用 remove 一般情况下不会改变容器的大小,而 pop_back()与 erase()等成员函数会改变容器的大小,使得之后所有迭代器、引用和指针都失效

  1. 函数指针

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分
函数指针声明

int (*pf)(const int&, const int&);

上面的 pf 就是一个函数指针,指向所有返回类型为 int,并带有两个 const int & 参数的函数。应该注意的是 *pf 两边的括号是必须的否则就是声明了一个返回int *类型的函数
函数指针赋值

指针名 = 函数名;
指针名 = &函数名;
  1. c/c++的内存分配,详细说一下栈、堆、静态存储区

代码段
只读,可共享; 代码段(code segment/text segment )通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等
数据段
储存已被初始化了的静态数据。数据段(data segment )通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
BSS 段
未初始化的数据段。BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文 Block Started by Symbol 的简称。BSS 段属于静态内存分配(BSS 段 和 data 段的区别是 ,如果一个全局变量没有被初始化(或被初始化为 0),那么他就存放在 bss 段;如果一个全局变量被初始化为非 0,那么他就被存放在 data 段)
堆(heap )
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack)
栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{} ”中定义的变量(但不包括 static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/ 恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
共享内存映射区域
栈和堆之间,有一个共享内存的映射的区域。这个就是共享内存存放的地方。一般共享内存的默认大小是 32M

综上:
栈区(stack) — 由编译器自动分配释放,存放函数的参数值,局部变量的值等其操作方式类似于数据结构中的栈
堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
全局区(静态区)(static) — 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放
文字常量区 — 常量字符串就是放在这里的。程序结束后由系统释放
程序代码区 — 存放函数体的二进制代码

  1. 堆与栈的区别

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生 memory leak
空间大小:一般来讲在 32 位系统下,堆内存可以达到 4G 的空间,但是对于栈来讲,一般都是有一定的空间大小的
碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高,堆则是 C/C++函数库提供的

  1. 野指针是什么?

野指针:指向内存被释放的内存或者没有访问权限的内存的指针。它的成因有三个:1. 指针变量没有被初始化。2. 指针 p 被 free 或者 delete 之后,没有置为 NULL。3.指针操作超越了变量的作用范围 (觉得存在错误)

  1. 悬空指针和野指针有什么区别

野指针:野指针指,访问一个已删除或访问受限的内存区域的指针,野指针不能判断是否为 NULL 来避免。指针没有初始化,释放后没有置空,越界
悬空指针:一个指针的指向对象已被删除,那么就成了悬空指针。野指针是那些未初始化的指针

  1. 内存泄漏

内存泄漏 是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制 (内存泄露的排查诊断与解决)

  1. new 和 delete 的实现原理, delete 是如何知道释放内存的大小的
  1. new 表达式调用一个名为 operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间
  2. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值
  3. 对象被分配了空间并构造完成,返回一个指向该对象的指针

new 简单类型直接调用 operator new 分配内存;而对于复杂结构,先调用 operator new 分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用 operator new;对于复杂数据结构,new[] 先调用 operator new[]分配内存,然后在 p 的前四个字节写入数组大小 n,然后调用 n 次构造函数,针对复杂类型,new[]会额外存储数组大小
delete 简单数据类型默认只是调用 free 函数;复杂数据类型先调用析构函数再调用 operator delete;针对简单类型,delete 和 delete[]等同。假设指针 p 指向 new[]分配的内存。因为要 4 字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是 p-4 指向的内存。而 delete 会直接释放 p 指向的内存,这个内存根本没有被系统记录,所以会崩溃
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了

  1. 使用智能指针管理内存资源,RAII

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

  1. 内存对齐
  1. 分配内存的顺序是按照声明的顺序。
  2. 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
  3. 最后整个结构体的大小必须是里面变量类型最大值的整数倍
class A{
   int a;
   double b;
};

class B{
   int a, b;
   double c;
};

class C{
   int a;
   double b;
   int c;
};
class D{
   int a;
   double b;
   int c,d;
};

int main(){
   cout << sizeof(int) << " " << sizeof(double) << endl;
   cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C) << " " << sizeof(D) << endl;
}
// out
/*
4 8
16 16 24 24
*/
  1. 为什么内存对齐

平台原因(移植原因)

  • 不是所有的硬件平台都能访问任意地址上的任意数据的;
  • 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

性能原因:

  • 数据结构(尤其是栈)应该尽可能地在自然边界上对齐
  • 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
  1. 宏定义一个取两个数中较大值的功能
#define MAX(x,y) (x > y ? x:y)
  1. define 与 inline 的区别

define 是关键字,inline 是函数

宏定义在预处理阶段进行文本替换,inline 函数在编译阶段进行替换
inline 函数有类型检查,相比宏定义比较安全

  1. printf 实现原理

在 C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf 的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了。

  1. hello world 程序开始到打印到屏幕上的全过程
  • 用户告诉操作系统执行 HelloWorld 程序(通过键盘输入等)
  • 操作系统:找到 helloworld 程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
  • 操作系统:创建一个新进程,将 HelloWorld 可执行文件映射到该进程结构,表示由该进程执行 helloworld 程序。
  • 操作系统:为 helloworld 程序设置 cpu 上下文环境,并跳到程序开始处。
  • 执行 helloworld 程序的第一条指令,发生缺页异常
  • 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行 helloworld 程序
  • helloword 程序执行 puts 函数(系统调用),在显示器上写一字符串
  • 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
  • 操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
  • 视频硬件将像素转换成显示器可接收和一组控制数据信号
  • 显示器解释信号,激发液晶屏
  • OK,我们在屏幕上看到了 HelloWorld
  1. 模板类和模板函数的区别是什么

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加<T>,而函数模板不必

  1. C++四种类型转换
  • static_cast 能进行基础类型之间的转换,也是最常看到的类型转换。它主要有如下几种用法:1. 用于类层次结构中父类和子类之间指针或引用的转换,2. 进行下行转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的,3. 用于基本数据类型之间的转换,如把 int 转换成 char,把 int 转换成 enum,4. 把 void 指针转换成目标类型的指针(不安全!!) 5. 把任何类型的表达式转换成 void 类型
  • const_cast 运算符用来修改类型的 const 或 volatile 属性。将一个 const 的指针或引用转换为非 const。除了去掉 const 或 volatile 修饰之外,type_id 和 expression 得到的类型是一样的。但需要特别注意的是 const_cast 不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
  • reinterpret_cast 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针
  • dynamic_cast 主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。转型失败会返回 null(转型对象为指针时)或抛出异常 bad_cast(转型对象为引用时)。 dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。当使用 dynamic_cast 时,该类型必须含有虚函数,这是因为 dynamic_cast 使用了存储在 VTABLE 中的信息来判断实际的类型,RTTI 运行时类型识别用于判断类型。typeid 表达式的形式是 typeid(e),typeid 操作的结果是一个常量对象的引用,该对象的类型是 type_info 或 type_info 的派生。C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
  1. 全局变量和 static 变量的区别

全局变量(外部变量)的说明之前再冠以 static 就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个原文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。static 全局变量与普通的全局变量的区别是 static 全局变量只初始化一次,防止在其他文件单元被引用。
static 函数与普通的函数作用域不同。只在当前源文件中使用的函数应该声明为内部函数(static),内部函数应该在当前源文件
中说明和定义。对于可在当前源文件以外使用的函数应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。static 函数与普通函数最主要区别是 static 函数在内存中只有一份,普通静态函数在每个被调用中维持一份拷贝,程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)中

  1. 迭代器++it, it++ 哪个好

前置返回一个引用,后置返回一个对象
前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
++i实现

int& operator++()
{
  *this += 1;
  return *this; 
}

i++实现

int operator++(int) 
{
  int temp = *this; 
  ++*this; 
  return temp; 
}
  1. 模板和实现可不可以不写在一个文件里面?为什么?

因为在编译时模板并不能生成真正的二进制代码,而是在编译调用模板类或函数的 CPP 文件时才会去找对应的模板声明和实现,在这种情况下编译器是不知道实现模板类或函数的 CPP 文件的存在,所以它只能找到模板类或函数的声明而找不到实现,而只好创建一个符号寄希望于链接程序找地址。但模板类或函数的实现并不能被编译成二进制代码,结果链接程序找不到地址只好报错了。
模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

  1. 执行 int main(int argc, char *argv[])时的内存结构

参数的含义是程序在命令行下运行的时候,需要输入 argc 个参数,每个参数是以 char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。

  1. 大端小端,如何检测

大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址端。
小端模式,是指数据的高字节保存在内存的高地址中,低位字节保存在在内存的低地址端。
检测1直接读取存放在内存中的十六进制数值,取低位进行值判断

int a = 0x12345678;
int *c = &a;
c[0] == 0x12 大端模式
c[0] == 0x78 小端模式
  1. 有了 malloc/free,为什么还要 new/delete

对于类类型的对象而言,用 malloc/free 无法满足要求的。对象在创建的时候要自动执行构造函数,消亡之前要调用析构函数。由于 malloc/free 是库函数而不是运算符,不在编译器控制之内,不能把执行构造函数和析构函数的任务强加给它,因此,C++还需要 new/delete。

  1. 为什么拷贝构造函数必须传引用不能传值

拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象)。拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归。

  1. this 指针调用成员变量时,堆栈会发生什么变化

当在类的非静态成员函数访问类的非静态成员时,编译器会自动将对象的地址传给作为隐含参数传递给函数,这个隐含参数就是 this 指针。即使你并没有写 this 指针,编译器在链接时也会加上 this 的,对各成员的访问都是通过 this 的。例如你建立了类的多个对象时,在调用类的成员函数时,你并不知道具体是哪个对象在调用,此时你可以通过查看 this 指针来查看具体是哪个对象在调用。This 指针首先入栈,然后成员函数的参数从右向左进行入栈,最后函数返回地址入栈。

  1. 智能指针怎么用?智能指针出现循环引用怎么解决?
  1. shared_ptr
    调用一个名为 make_shared 的标准库函数,shared_ptr<int> p = make_shared<int>(42); 通常用 auto 更方便,
    auto p = …;shared_ptr<int> p2(new int(2));
    每个 shared_ptr 都有一个关联的计数器,通常称为引用计数,一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象; shared_ptr 的析构函数就会递减它所指的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。
  2. unique_ptr
    一个 unique_ptr 拥有它所指向的对象。某个时刻只能有一个 unique_ptr指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。
  3. weak_ptr
    weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象,将一个 weak_ptr 绑定到一个 shared_ptr 不会改变引用计数,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放,即使有 weak_ptr 指向对象,对象还是会被释放。
  4. 弱指针用于专门解决 shared_ptr 循环引用的问题,weak_ptr 不会修改引用计数,即其存在与否并不影响对象的引用计数器。循环引用就是:两个对象互相使用一个 shared_ptr 成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存
posted @ 2021-05-21 20:55  范中豪  阅读(596)  评论(0编辑  收藏  举报