2020秋招_C++基础、数据结构基础面经记录

文章索引

语法

malloc/free和new/delete的区别

深入理解C++ new/delete, new []/delete[]动态内存管理

  • malloc/free是C/C++标准库的函数;new/delete是C++操作符(编译器可以优化操作符,但无法优化库函数),可以重载。
  • malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配/释放内存空间还会调用构造函数和析构函数进行初始化与清理(清理成员),即可以实现动态的内存管理。
  • malloc/free管理内存失败会返回NULL(0),new/delete等的方式管理内存失败会抛出异常。
  • malloc/free需要手动计算类型大小且返回值为void*,需要强制类型转换成对应类型的指针;new/delete可自动计算类型的大小,返回对应类型的指针。
  • new/delete分配内存区域是自由存储区,不完全等于堆。布局new就可以自己指定内存分配的区域。
int* p = (int *)malloc(sizeof(int)*length);
free(p);
int* p = new int[length];
delete[] p;

PS:malloc、calloc、realloc的区别?

为什么C++声明保存在.h文件、定义保存在.cpp文件?

在编译执行之前的预处理阶段#inlcude指令会把.h文件的内容替换到当前位置。由于C++允许多次声明,但只允许一次定义。如果变量或者函数定义在.h文件中,在链接阶段就会有多个目标文件中存在同个变量或者函数的定义,这时候会引发错误fatal error LNK1169:找到一个或者多个重定义的符号

static、extern、const、volatile关键字

static:修饰局部变量改变存储方式、修饰全局变量改变作用域;static修饰的类成员,表示类的共享数据(静态区,被所有线程所共享)。

extern:可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

const修饰的变量必须初始化!
[转] C++中 const, volatile, mutable用法
const修饰数据成员:const数据成员的初始化只能在类的构造函数的初始化列表中进行。(不能在类声明中初始化const数据成员,因为类的对象未被创建时,编译器不知道const 数据成员的值是什么C++11之后可以在类声明中初始化const成员;构造函数体内不能初始化const成员,因为构造函数体内执行的只是赋值操作,初始化在初始化列表中进行)
初始化:当对象在创建时获得了一个特定值。

const修饰类对象/对象指针/对象引用:const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。

const修饰类的成员函数:用const修饰的成员函数不能改变对象的成员变量。

const常量与define宏定义的区别? 编译器处理方式、存储方式、类型和安全检查不同。
const和#define区别
volatile:遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问


static的作用

C/C++_static函数,static成员函数,static变量,static成员变量 再来理一理

  • static修饰全局变量和函数,改变其作用域为文件作用域,对其它文件不可见;static修饰的局部变量,保存在全局数据区(静态区),因此其生存周期不再是函数运行结束,而是直到程序运行结束。
  • static修饰类成员变量和成员函数,表明成员属于类而不是对象,为类的所有对象共享。
  • static成员变量必须在类内声明,在类外定义,声明时须加static,定义时候不加static。
  • static成员函数不存在隐含的this指针,因此不能访问非静态成员变量,也不能调用非静态成员函数。
  • 用const修饰类的成员函数(写在函数的最后,不是前面,前面是返回值为常量),表示该函数不能修改该类的状态,如不能在改函数里修改成员变量(除去mutable修饰的外),因为该函数存在一个隐式的this*,const修饰后为const this*,但是当static修饰成员函数的时候是没有this指针的,所以不能同时用static和const修饰同一个成员函数,不过可以修饰同一个成员变量
  • 静态成员函数不能为virtual修饰(静态成员函数属于类,无法实现多态)。

结构体分别有char int short类型,长度是多少?

字节对齐,对于32位系统,默认4字节对齐,占12个字节的大小。


strlen和sizeof

strlen 与 sizeof 的区别:

  • sizeof是运算符,strlen是库函数。
  • sizeof可以用类型、变量做参数,而strlen只能用 char* 变量做参数,且必须以\0结尾。
  • sizeof是在编译的时候计算类型或变量所占内存的大小,而strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度。
  • 数组做sizeof的参数不退化,传递给strlen就退化为指针了。

STL

c++使用的最多的数据结构(vector,vector内存管理和底层实现?)

STL vector的内部实现原理及基本用法
C++ vector(STL vector)底层实现机制(通俗易懂)
内存管理 -> 动态扩容

  • 内存分配机制:size表示vector中已有元素的个数,capacity表示vector最多可存储的元素的个数。为了降低二次分配时的成本,vector实际配置的大小可能比需求的更大一些,以备将来扩充,这就是capacity的概念。capacity是大于等于 size的,当 size和capacity相等时,如果继续使用 push_back() 添加元素,vector 就会扩容(在VS 下,扩容都是以 1.5 倍扩大,但是,在 gcc 编译环境下,是以 2 倍的方式扩容)。
  • 内存释放:调用析构函数释放内存,先销毁所有已存在的元素,然后释放所有内存。

底层实现 -> 三个迭代器(指针)
vector底层实现

//_Alloc 表示内存分配器,此参数几乎不需要我们关心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
    
    
    ...
protected:
	pointer _M_start;
	pointer _M_finish;
	pointer _M_end_of_storage;
};

_M_start 指向的是 vector 容器对象的起始字节位置;_M_finish 指向当前最后一个元素的末尾字节;_M_end_of_storage 指向整个 vector 容器所占用内存空间的末尾字节。
大小:size=_M_finish - _M_start;
容量:capacity=_M_end_of_storage - _M_start;
分别对应于resize()、reserve()两个函数。

vector动态扩容的过程:并不是在原有内存地址的末尾继续扩大两倍,因为堆上的连续内存不一定够用。通常会重新申请一块扩大后的堆内存,并把原来内存中的元素通过移动语义一个个移动过去。


vector与list区别

vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。 list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。


unordered_map和map的区别

c++中map与unordered_map的区别
内部实现机理:unordered_map,哈希表,顺序存储,元素是无序的,查找效率O(1);map,红黑树,链式存储,元素是有序的,查找效率基本维持在O(logn)。
优缺点及适用处:map以空间换时间,适用于有顺序要求的问题,如查找某个范围内的key时效率更高;unordered_map查找速度非常快,但哈希表建立比较消耗时间。
PS:unordered_set和set同理。


红黑树

红黑树(一)之 原理和算法详细介绍


迭代器

在设计模式中有一种模式叫迭代器模式,简单来说就是提供一种方法,在不需要暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素,这种设计思维在STL中得到了广泛的应用,是STL的关键所在。通过迭代器容器算法可以有机的粘合在一起,只要对算法给予不同的迭代器,就可以对不同容器进行相同的操作。

C ++迭代器用于对数据结构中的元素进行顺序访问或随机访问。因此,对于根据定义不允许顺序或随机访问的数据结构,迭代器没有任何意义。这就是顺序容器适配器stack、queue、priority_queue没有迭代器的原因。
PS:STL顺序容器vector、list(双向链表)、forward_list(单链表)、deque(双端队列)、array、string,关联容器set、map、multiset(元素可以重复)、multimap(key可以重复)、unordered_set、unordered_map,容器适配器queue、stack、priority_queue(优先队列)。

STL底层数据结构实现


迭代器与指针的区别

迭代器与指针的差别
一个是类模板,一个是存放一个对象地址的指针变量。

  • 迭代器是类模板,类似于智能指针,内部封装了指针,通过重载了指针的一些操作符(++、–、*、->等),模拟指针的一些功能。迭代器提供了比指针更高级的行为,可以根据不同类型的数据结构来实现不同的++、–等操作。
  • 指针能指向函数而迭代器不行,迭代器只能指向容器;指针是迭代器的一种。指针只能用于某些特定的容器;迭代器是指针的抽象和泛化。所以,指针满足迭代器的一切要求。
  • 迭代器在使用后就释放了,不能再继续使用,但是指针可以!!
ite = find(vec.begin(),vec.end(),88);
vec.insert(ite,2,77); //迭代器标记的位置前,插入数据;
cout << *ite << endl; //会崩溃,因为迭代器在使用后就释放了,*ite的时候就找不到它的地址了;

设计在循环中STL迭代器删除指定位置的元素? -> 主要区分顺序容器还是关联容器。顺序容器删除和插入一个元素,由于后面所有元素的位置都要移动,后面的迭代器会失效,需要用erase()、insert()返回有效的迭代器;关联容器插入和删除元素仅仅只会使当前的迭代器失效,迭代器递增后,仍能访问到下一个元素。
PS:在C++11中,顺序容器erase(iter)或者insert(iter)后,迭代器iter仍有效,并且指向删除或者插入位置的下一个元素。

#include <iostream>
#include <map>
#include <vector>
using namespace std;
int main() {
    
    
    map<int,string> mp;
    mp[0] = "zero"; mp[1] = "one";mp[2] = "two";mp[3] = "three";
    for(map<int,string>::iterator iter = mp.begin(); iter!=mp.end(); ) {
    
    
        if(iter->second == "one"){
    
    
            mp.erase(iter); // 删除后迭代器指向删除的元素
            // cout << iter->second << endl; //返回"one"
            iter++;  // vector与map的区别在于此,map此处迭代器需要手动指向下一个元素
        }else {
    
    
            iter++;
        }
    }
    for(map<int,string>::iterator iter = mp.begin(); iter!=mp.end(); iter++) {
    
    
        cout << iter->second << endl;
    }

    vector<int> vec{
    
    1,2,3,4};
    for(vector<int>::iterator iter = vec.begin(); iter!=vec.end();) {
    
    
        if(*iter == 2){
    
    
            vec.erase(iter); // 删除后迭代器自动指向下个有效的元素
            // cout << *iter << endl; // 返回3
        }else {
    
    
            iter++;
        }
    }
    for(vector<int>::iterator iter = vec.begin(); iter!=vec.end(); iter++) {
    
    
        cout << *iter << endl;
    }
    return 0;
}

面向对象

类的成员变量有unique_ptr无法进行拷贝构造和赋值

#include <memory>
class Impl{
    
    };

class Factory{
    
    
private:
    std::unique_ptr<Impl> impl;
};

int main(){
    
    
    Factory f;
    Factory f1(f);
}

由于unique_ptr类型的成员变量impl的拷贝构造函数是被删除的,所以类Factory的拷贝构造函数也会被隐式地删除。
在这里插入图片描述
C++智能指针作为成员变量


构造函数尽量不要抛出异常

C++ 构造函数抛出异常注意事项

  1. 构造函数抛出异常导致内存泄漏
    在 C++ 构造函数中,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生。因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员对象,包括其基类的成员,即执行直接基类和成员对象的析构函数。
  2. 使用智能指针管理内存资源

条款08:析构函数不要抛出异常

Effective C++ 8:析构函数不要抛出异常
由于析构函数常常被自动调用,在析构函数中抛出的异常往往会难以捕获,引发程序非正常退出或未定义行为。例如,对象数组被析构时,会抛出多于一个的异常,然而同时存在的异常在C++标准中是禁止的, 因此程序会非正常退出。

Effective C++读书笔记(8): 析构函数不能抛出异常
栈展开的前提是已经有一个未处理的异常,并且栈展开会自动调用函数本地对象的析构函数,如果这时对象的析构函数时又抛出一个异常,现在就同时有两个异常出现,但C++最多只能同时处理一个异常,因此程序这时会自动调用std::terminate()函数,导致我们所谓的闪退或者崩溃。


条款07:为多态基类声明virtual析构函数

多态就是通过基类的指针,可以调用不同的派生类的函数。基类的析构函数声明为virtual,由基类指针调用派生类的析构函数,从而析构派生类类对象。

PS:只有当类中至少有一个virtual函数,才声明virtual析构函数。(声明了虚函数的类生成的对象会有一个虚表指针vptr,对象的体积会增加)


对多态的理解

C++ 多态
多态性(polymorphism)可以简单地概括为一个接口,多种方法,它是面向对象编程领域的核心概念。
C++支持两种多态性:编译时多态性,运行时多态性。

  • 编译时多态性(静态多态):通过重载函数实现
  • 运行时多态性(动态多态):通过虚函数实现

同一个行为(基类中的纯虚函数)有多个不同表现形式或形态(派生类中的虚函数)的能力。如基类中求面积的纯虚函数,在不同的派生类中(三角形类,正方形类…)实现它的方式不一样。

多态与非多态的实质区别就是函数地址是在编译阶段确定还是运行阶段确定。


虚函数、纯虚函数

virtual关键字修饰,定义纯虚函数、虚函数的目的是为了实现多态。其核心理念就是通过基类访问派生类定义的函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。

base_class* ptr = new derived_class();,编译阶段确定ptr的类型为base_class*,运行时候ptr->func()调用的是derived_classd(实际实例化的对象类型)的虚函数。(假设基类中定义了纯虚函数func,且在派生类中实现)

纯虚函数定义在基类中,没有函数体,virtual void func() = 0;,在派生类中必须对该函数进行实现(在派生类中实现纯虚函数的函数即虚函数),该实现过程叫override(重写/覆盖)。
PS: overload(重载)是同一类中同名函数不同参数列表,调用哪个函数编译阶段可以确定,是静态的多态。

多态基类的析构函数为什么是虚函数? -> 通过基类的指针来销毁派生类对象,基类的析构函数需要设置成虚函数。
总结:基类析构函数无法访问派生类的析构函数,便无法析构派生类;基类的构函数设置成虚函数后,便可以通过虚函数表找到到派生类析构函数的地址,从而完成派生类的析构。


虚函数的实现

虚函数表指针、虚函数表
对象分配内存后,内存布局的最前面存在虚函数表指针;虚函数表指针指向全局静态区域的虚函数表;虚函数表记录了虚函数在代码区域的地址,从而实现基类的指针访问派生类定义的虚函数。
PS:由于对象分配内存后才存在虚函数表指针,因此对象的构造函数不能为虚函数。


程序在内存中的分配

  • 栈(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值(除static),其操作方式类似于数据结构中的栈。
  • 堆(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆(优先队列)是两回事,分配方式倒是类似于链表。
  • 全局区(静态区),data段:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(bss段),程序结束后由系统释放。
  • 常量区,rodata段:存放只读常量(const修饰的变量),程序结束后由系统释放。
  • 程序代码区,text段:存放函数体的二进制代码。

内存中堆和栈究竟有什么区别? -> C++内存分配方式详解(堆、栈、自由存储区、全局/静态存储区和常量存储区)

  1. 管理方式不同:栈由编译器管理,堆由人工管理。
  2. 空间大小不同:堆空间比栈空间大得多。
  3. 能否产生碎片不同:对于堆,频繁的 new/delete 势必会造成内存空间的不连续,产生大量的外碎片;栈先进后出,不产生碎片。
  4. 生长方向不同:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
  5. 分配方式不同:堆是动态内存分配。
  6. 分配效率不同:栈的分配效率高。

虚函数表

C++多态虚函数表详解

内存布局

在这里插入图片描述
在gcc编译器的实现中虚函数表vtable存放在可执行文件的只读数据段.rodata中。

普通继承下的虚函数表

在有继承情况下,只要基类有虚函数,子类不论实现或没实现,都有虚函数表。
子类中与基类虚函数同名的函数,也会自动加上virtual。
首先,子类会继承基类的虚函数表,如果重写了基类的虚函数会更新虚函数表;如果没有重写任何基类的虚函数,那么子类和基类的虚函数表是内容是一致的。
但是,基类的虚函数表和子类的虚函数表不是同一个表

多继承下的虚函数表(同时继承多个基类)

在这里插入图片描述
在多继承情况下,有多少个基类就有多少个虚函数表指针,前提是基类要有虚函数才算上这个基类。
注意:

  1. 子类虚函数会覆盖每一个父类的每一个同名虚函数。
  2. 父类中没有的虚函数而子类有,填入第一个虚函数表中,且用父类指针是不能调用。
  3. 父类中有的虚函数而子类没有,则不覆盖。仅子类和该父类指针能调用。

两个简单链表判断是否有重合

两个简单链表(无环)重合必定是前面部分不重合,后面部分重合(只可能两个节点汇聚到一个节点,不可能一个节点分叉出两个节点)。因此,找到两个链表最后一个节点,如果节点的地址相同,则存在重合。
判断第一个重合的节点:双指针从两个链表的头节点开始移动,移动到最后一个节点后从另一个链表的头节点开始移动,两个指针第一次相遇即为第一个重合的节点。


深拷贝和浅拷贝

拷贝的对象中的外部内存空间(例如对象中new了块新内存)没有被复制,拷贝的对象共用外部内存空间,即浅拷贝;拷贝的对象时,为对象中的外部内存空间也进行了复制,即深拷贝。
浅拷贝会出现野指针(悬挂指针),当对象调用析构的时候,共享的外部内存空间会被重复析构,造成堆内存的二次释放。
PS:野指针指向非法的内存地址,是很危险的,而且if不能判断一个指针是正常指针还是野指针。
野指针:没有被初始化的指针;指针被delete后没有被设置成NULL;指针操作超越了变量的作用域。


栈溢出(stack overflow)、堆内存泄露

  • 系统要在栈中不断保存函数调用时的现场和产生的变量,函数递归调用层次太深,会导致栈溢出;函数中局部数据结构(如数组)过大,也会导致栈溢出。
  • 程序未释放动态申请的且不再使用的内存,会造成内存泄露,长时间运行可能会引起系统“内存耗尽”。

快速排序最好和最快情况分析

实现原理:快速排序在一趟排序中将数字分割成为独立的两部分,左边一部分小于基准,右边一部分大于基准,基准的选择理论上可以选择数组中的任何一个数组,我们在这里考虑选择第一个数字。然后对两部分序列重复进行上述操作,快速排序可以用递归来完成。

时间复杂度:最好情况O(nlogn)——Partition函数每次恰好能均分序列,其递归树的深度就为 ( log ⁡ 2 n ) + 1 (\log{2n})+1 (log2n)+1,即仅需递归log2n次; 最坏情况O(n^2),每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序,如果用树画出来,得到的将会是一棵单斜树,也就是说所有所有的节点只有左(右)节点的树,即待排序的序列已经是升序或者降序序列;平均时间复杂度O(nlogn)。


快速排序优化实现

三种快速排序以及快速排序的优化

  1. 基准选择方法:三数取中法;-> 对升序或者降序数组效率提升较大(避免了形成单斜树的情况)。
  2. 当待排序序列的长度分割到一定大小后,使用插入排序; -> 对于很小和部分有序的数组,快排不如插排好。
  3. 在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割;-> 数组中有大量重复元素的情况有效率有显著的提升。
  4. 快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化;(这种优化编译器会自己优化,相比不使用优化的方法,时间几乎没有减少)
  5. 使用并行或多线程处理子序列。

快速排序采用多线程后更耗时?

  • 对于cpu密集型的程序,多线程反而更慢,因为线程上下文切换是要耗系统性能的;IO密集型程序,适合用多线程,充分利用cpu资源;
  • 快排是一个递归的过程, 在每一次都启动两个线程的方法来实现完全不可取, 会随着递归的层级产生2的n次方个线程,多次的创建,启动线程,明显会耗费太多的性能,线程太多之后,线程间上下文切换也是一个性能杀手。理论上对于cpu密集型程序,线程数=核数才是最快的。

尾递归为啥能优化?

  • 尾递归的情况下,我们保持这个函数的入口环境没意义,所以我们就可以把这个函数的调用栈给优化掉。

归并排序的过程,是否稳定?什么是稳定的排序算法?

先递归向下对半拆分直到子数组只有一个元素,然后再向上不断地合并相邻的数组,使合并后的数组有序。归并排序是稳定的排序算法。排序算法稳定是指排序过程中不会改变相同大小的元素的相对位置关系。
PS:不稳定的排序算法记忆口诀,“快希选堆”。


归并排序优化

递归到规模足够小(100~200)时用插入排序进行合并。对数组进行插入排序时数组的左右两部分已经有序,插入排序算法对局部有序的数组排序效率较高,由此提升插入排序的效率。


智能指针及其实现

c++11引入智能指针是为了避免堆内存非常频繁的new/delete操作,容易造成内存泄露、二次释放等问题。
智能指针采用RAII(资源获取就是初始化)思想,使用对象管理资源,在类的构造函数中获取资源,在类的析构函数中释放资源。

  • unique_ptr,独占对象的智能指针,即同一时刻只有一个unique_ptr指向给定对象。通过禁止拷贝和赋值构造函数,只允许移动拷贝和移动赋值构造函数实现。
  • shared_ptr,共享对象的智能指针,即同一时刻可以有多个shared_ptr指向给定对象。通过引用计数的方式实现。内部维护一个计数器,每拷贝或赋值一次计数器加1,每析构一次计数器减1,计数为0时自动释放指向的堆内存。
  • weak_ptr,专门用于解决shared_ptr循环引用的问题。

引用计数怎么做到线程安全的?

计数器设置为原子量。c++11 std::atomic<int>


指针和引用的区别

指针存储的是对象或者变量的地址,引用是对象或者变量的别名。
引用的好处 -> 引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

PS:引用一旦初始化,便不能改变绑定的对象。
C++引用必须初始化的原因
C++中引用不能再更改绑定的对象的理解

int main(){
    
    
    // 引用的赋值:是指引用初始化时,它的引用对象只能是变量,并且,一旦它指定为某一个对象的引用后,就不能更改了。
    // 但是,可以用这个引用来改变它的对象的值,从而达到引用的目的——作为变量对象的别名。 
    int a = 1;
    int& b = a; // 定义引用类型时必须有初始值对象(必须为左值)
    b = 2;
    cout << "a: " << a << endl;  // 2
    int c = 3;
    // 不能理解成:取消b是a的引用而将b作为c的引用。
    // 正确的理解应该是:利用引用b来改变它所指对象a的值,即相当于语句“b=3;”。
    b = c; 
    // &b = c;  // error: expression is not assignable, 绑定的对象不可改变
    cout << "a: " << a << endl; // 3
    b = 5;
    cout << "a: " << a << endl; // 5
    cout << "c: " << c << endl; // 3
    return 0;
}

函数指针和指针函数、数组指针和指针数组

  1. 指针函数:带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针。
    类型标识符 *函数名(参数表) int *f(x,y);
  2. 函数指针:指向函数(首地址)的指针变量,即本质是一个指针变量。
    函数指针说的就是一个指针,但这个指针指向的函数,不是普通的基本数据类型或者类对象。
    指向函数的指针包含了函数的地址,可以通过它来调用函数。
    声明格式:类型说明符 (*函数名)(参数) int (*f)(int a, int b);

例如:void (*fptr)();
把函数的地址赋值给函数指针,可以采用下面两种形式:
fptr=&Function;
fptr=Function;
取地址运算符&不是必需的,因为单单一个函数标识符就标号表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。
可以采用如下两种方式来通过指针调用函数:
x=(*fptr)();
x=fptr();
第二种格式看上去和函数调用无异。但是有些程序员倾向于使用第一种格式,因为它明确指出是通过指针而非函数名来调用函数的。

PS:指向一个数组的指针就是数组指针;如果一个数组的每一个元素都是指针,则这个数组是一个指针数组。


模板类和类模板、模板函数和函数模板

模板类与类模板、函数模板与模板函数等的区别
类模板是专门用于产生类的模板,由类模板产生的类叫做模板类。
vector<int>、vector<string>都是由vector类模板产生的模板类。

模板函数和函数模板也类似。
以上几个术语中,语言的重心在后面,前面的词是作为形容词使用的


绝不在构造和析构过程中调用虚函数

问题:现在假设有一个程序,编译好的,编译没有错误,但是运行的时候报错,报的错是你正在调用一个纯虚函数,请问这里面导致这个错误的原因可能是什么? -> 在基类的构造函数或者析构中调用了虚函数

构造的顺序:先基类,再派生类。当创建派生类实例的时候,先调用的是基类的构造函数,如果基类的构造函数中调用了虚函数,此时派生类还未被构造,即派生类的成员还未初始化,即基类中的纯虚函数还未被派生类实现。所以,运行的时候会出现“pure virtual method called”错误

析构的顺序:先派生类,再基类。当基类析构函数调用时,派生类已经析构结束,基类析构函数中调用虚函数,此时虚函数已经不存在。

如果父类有一个虚函数叫func_A,子类也实现这个函数,在子类的构造函数当中去调用这个func_A,运行的是谁的实现? -> 子类的实现(虚函数调用由指针指向的实际类型决定,子类的构造函数体调用func_A时,子类已经构造完成,所以能够调的是子类实现的虚函数func_A)

在普通的函数当中调用虚函数和在构造函数当中调用虚函数有什么区别? -> 构造函数中调用虚函数无法实现多态。(因为父类构造函数中调用虚函数时,子类还未被初始化,只能调用自身的虚成员函数,这样不是多态的表现)

#include <iostream>
using namespace std;

class A{
    
    
public:
    A(){
    
    
        // 在基类的构造函数中调用虚函数(基类中定义了纯虚函数)
        // test2(); // 异常终止:pure virtual method called
    }
    virtual void test2()=0;
    virtual void test(){
    
    
        cout << "parent" << endl;
    }
};

class B:public A{
    
    
public:
    B(){
    
    
        this->test();
    }

    virtual void test(){
    
    
        cout << "child" << endl;
    }

    virtual void test2(){
    
    
        cout << "child2" << endl;
    }
};

int main(){
    
    
    A* b;
    b = new B(); // 输出child
    return 0;
}

子类在调用构造函数时,父类的构造过程

描述一下,子类构造的时候,整个构造的过程,先怎么样,再怎么样。
构造顺序: 基类成员构造函数 -> 基类构造函数 -> 派生类成员构造函数 -> 派生类构造函数
析构顺序(与构造完全相反): 派生类析构函数 -> 派生类成员析构函数 -> 基类析构函数 -> 基类成员析构函数
是先构造父类的虚表指针还是先构造父类的成员? -> 先构造父类成员。因为构造顺序为:基类成员构造函数 -> 基类构造函数,虚表指针在基类对象构造后才存在,一般存在对象内存布局的最前面。

c++ 子类构造函数初始化及父类构造初始化
值得注意的是:父类只声明了带参数的构造函数时,编译器不会为其声明默认的构造函数,子类必须显示地调用父类的带参构造函数,否则会报错。
父类构造函数初始化


构造函数初始化列表和构造函数体

初始化列表用于对类本身的数据成员的进行初始化。
对象创建的过程:

  1. 数据成员初始化;(成员初始化顺序为类中定义成员的顺序,与初始化列表中的顺序无关)
  2. 执行被调用构造函数体内的动作。

初始化列表的写法和顺序有没有什么关系? -> 没有

在构造函数当中一部分是初始化列表一部分是在花括弧里面,你能说一下这些的顺序是什么么?差别是什么 ?和this指针的顺序?
顺序:构造函数先执行初始化列表,再执行函数体内的动作(赋值操作)。
差别:构造函数初始化列表是对类的成员做初始化(拷贝构造);而在构造函数体内会先对类成员调用默认构造函数进行初始化,再对类的数据成员进行了一次赋值操作(默认构造+赋值操作)。显然初始化列表的效率更高
初始化列表中不能使用this指针,在构造函数体内可以
this指针属于对象,只有对象完成构造、分配了相应的存储空间后才可以使用,而初始化列表发生在构造之前,所以不能使用。
那为什么在构造函数体内可以使用呢?
因为构造函数中的内容并不是初始化,而是赋值。(在执行构造函数体内的内容时,已经调用了默认构造函数,完成构造)

总结:初始化列表 -> 默认构造函数(完成对象构造) -> 构造函数体(由于对象已经分配内存,此时已经存在this指针和虚函数表指针)

虚函数表指针和构造函数体那个先被构造? -> 虚函数表指针

c++运行构造函数的时候虚函数表被构造出来了么? -> 被构造出来了。因为虚函数表在全局静态区,内存在编译的时候已经被分配好,被类的所有实例所共享。


内存管理

C/C++内存布局

C++内存泄漏

内存泄漏的场景分析和避免方法总结,C语言内存泄漏详解
智能指针、引用代替指针

对象在内存中的布局

对象在中分配内存。

  • 虚函数表指针属于对象,一般在对象内存布局的最前面。
  • 虚函数表属于类而不属于特定对象,是类的所有对象共有的。存放的是代码区(对象共有)中虚函数的地址,即函数指针。

成员变量和虚函数指针在堆中(对象私有),虚函数表在全局数据区(对象共有)。


如何判断类中是否存在特定的成员函数?

C++:如何判断类中是否存在特定的成员函数?
c++ 检查是否存在成员函数
调用HasFoo类的test方法去判断模板参数C是否含有foo方法。与test (SFINAE<C, &C::foo> *)的参数匹配,需要传入SFINAE类的指针。因此,调用test的方法的时候传入空指针即可。
void (C::*) () -> 指向C类中返回值为void,参数列表为()的成员函数的指针类型。

// 判断一个类中是否含有foo方法
class HasFoo
{
    
    
public :
    template <typename C, void (C::*) ()> 
    class SFINAE {
    
    };

    template <typename C> 
    static bool test (SFINAE<C, &C::foo> *) {
    
    
		return true;
    }

    template <typename C> 
    static bool test (...){
    
     // ...是指可变参数
		return false;
    }
};
 
class B
{
    
    
public :
    virtual void foo () {
    
    }
};

class D1 : public B
{
    
    
public :
    void foo () {
    
    }
 
};
 
class D3 : public B {
    
    };
 
#include <stdio.h>
int main ()
{
    
    
    bool ret=HasFoo::test<B>(nullptr);
	printf("%d\n",ret ); // 1
    ret=HasFoo::test<D1>(nullptr);
	printf("%d\n",ret ); // 1
    ret=HasFoo::test<D3>(nullptr);
	printf("%d\n",ret ); // 0
}

PS:C++11新特性–std::enable_if和SFINAE
SFINAE是英文Substitution failure is not an error的缩写,意思是匹配失败不是错误。这句话什么意思呢?当调用模板函数时编译器会根据传入参数推导最合适的模板函数,在这个推导过程中如果某一个或者某几个模板函数推导出来是编译无法通过的,只要有一个可以正确推导出来,那么那几个推导得到的可能产生编译错误的模板函数并不会引发编译错误。


指向数据成员的指针(pointer to data member)

#include <iostream>

class Foo{
    
    
public:
    void test1(){
    
    
        std::cout << "test1" << std::endl;
    }
    void test2(void(Foo::*)()){
    
    
        std::cout << "test2" << std::endl;
    }
    int a = 1;
    int b = 2;
};
// p、q是指向数据成员的指针
int main(){
    
    
    Foo foo;
    void (Foo::*p)() = &Foo::test1; 
    (foo.*p)();
    foo.test1();

    foo.test2(nullptr);
    foo.test2(p);

    int Foo::* q; // int A::*可以指向类A的任意一个类型为int的数据成员。
    q = &Foo::a;
    std::cout << foo.*q << std::endl;
    std::cout << foo.a<< std::endl;
    q = &Foo::b;
    std::cout << foo.*q << std::endl;
}

多线程

i++ 是线程安全的吗?

i++不是原子操作,也就是说,它不是单独一条指令,而是3条指令(3条汇编指令):

  1. 从内存中把i的值取出来放到CPU的寄存器中
  2. CPU寄存器的值+1
  3. 把CPU寄存器的值写回内存

由于线程不共享栈区,共享堆区和全局区,所以当且仅当 i 位于栈上是安全的,反之不安全(++i也同理)。 因为如果是全局变量的话,同一进程中的不同线程都有可能访问到。对于读值,+1,写值这三步操作,在这三步任何之间都可能会有CPU调度产生,造成 i 的值被修改,造成脏读脏写。

volatile不能解决这个线程安全问题。因为volatile只能保证可见性,不能保证原子性。 -> 可见性是指,当一个线程修改了某一个全局共享变量的数值,其他线程是否能够知道这个修改。
从volatile说到,i++原子操作,线程安全问题

猜你喜欢

转载自blog.csdn.net/XindaBlack/article/details/107120742
今日推荐