整理的C/C++相关的面经知识点

自己在找工作的过程中,参考了一些博客并自己整理的有关C/C++面经知识点。

C语言中 new和malloc的区别

  • **申请内存所在位置:**new/delete是操作符,malloc/free是函数;new操作符从自由存储区(free store)上位对象动态分配内存空间,而malloc函数从从堆上动态分配内存。而且new在申请对象时会调用对象的构造函数和析构函数
    **自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请该内存
  • **返回类型安全性:*new操作符内存分配成功时,返回的是对象类型的指针。类型严格与对象匹配,无须进行类型转换,而malloc内存分配成功则是返回void,需要通过强制类型转换转换成我们需要的类型。
  • **内存分配失败时的返回值:**new内存分配失败时,会抛出bad_alloc异常,不会返回NULL;malloc内存分配失败时返回NULL。
  • 使用new操作符申请内存时无需指定内存大小,编译器会根据类型信息自动进行计算;而malloc需要显式的指定所需内存的大小
C++中有了maoolc/free, 为什么还需要new/delete?

malloc与free是c++/C语言的标准库函数,new/delete是C++的运算符,他们都可以用于申请动态内存和释放内存
对于非内部数据类型的对象而言,只用malloc/delete无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限范围内,不能够把执行构造函数和析构函数的任务强加于malloc/delete。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。

malloc内存分配机制是,在哪里分配内存,最大可以申请多大的内存

  • 它是将可用的内存块连接为一个长长的列表,即所谓的空闲链表,调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,将该内存块一分为二(一块大小与用户请求的大小相等,另一块大小就是剩下的字节)
    调用free函数时,它将用户释放的内存块连接到空闲链上,到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了,于是,malloc函数请求延时,并开始在空闲链上检查各内存片段,并进行整理,将相邻的小空闲块合并成较大的内存块。
  • malloc函数是在堆上面分配内存
  • Linux下虚拟地址空间分配给进程的是3GB,Windows下默认的是2GB,真正能够使用多少内存跟你的机器和编译器决定了,
    在Linux系统能申请到的内存地址空间在2.8GB左右,而在windows下面(32位)得到的地址空间在1.2GB左右

malloc(0)的返回值问题

  1. malloc(0)在我的系统里是可以正常返回一个非NULL值的。返回了一个正常的地址。

  2. malloc(0)申请的空间到底有多大不是用strlen或者sizeof来看的,而是通过malloc_usable_size这个函数来看的。—当然这个函数并不能完全正确的反映出申请内存的范围。strlen和sizeof都是计算为0

  3. malloc(0)申请的空间长度不是0,在我的系统里它是12,也就是你使用malloc申请内存空间的话,正常情况下系统会返回给你一个至少12B的空间。这个可以从malloc(0)和malloc(5)的返回值都是12,而malloc(20)的返回值是20得到。—其实,如果你真的调用了这个程序的话,会发现,这个12确实是”至少12“的。

  4. malloc(0)申请的空间是可以被使用的。

C语言程序中为什么要从main函数开始

main只是开发工具所规定的一个特殊函数名称而已,它既不是程序的入口,也不是必须要有的函数
如:

  • 一.在Linux C中,使用attribute关键字,声明constructor和destructor,可以自定义程序入口点,不一定是在main函数开始执行
  • 二.在标准C中,编译器在编译的时候把你的程序开始执行的地址设为main函数的地址,汇编中可以自由通过end伪指令制定
    在VS中给你可以通过这么设置:
    项目->属性->配置属性->链接器->高级->入口点,改为你想入口点的函数名
  • 三.在单片机C中,在C语言运行前都有汇编程序编写的启动程序,里面对单片机进行的初始化,同时设置了C程序的入口main函数

new运算符的原理(底层使用了operator new(),最终调用了malloc),new运算符重载,怎么写重载函数,重载的定义

//++++++++++++++++++++++++++++++++++++++++++++//
首先区分new operator和operator new这两个概念,前者new operator就是new操作符,一般,在new一个对象的时候,底层做了如下两步:

  1. 为对象分配内存

  2. 调用构造函数初始化这块内存
    在第一步中分配内存时就使用了operator new,其实现机制

    void *operator new(std::size_t size) throw(std::bac_alloc)
    {
    if(size==0)
    size=1;
    void *p;
    while((p=::malloc(size))==0)
    {
    std::new_handler nh=std::get_new_handler();
    if(nh)
    nh();
    else
    throw std::bad_alloc();
    }
    return p;
    }
    void operator delete(void *ptr)
    {
    if(ptr)
    ::free(ptr);
    }
    在第二步中,调用构造函数初始化内存,如果不涉及继承,这一步非常简单,就是给内存赋值,重点是operator new分配内存上,其处理流程如下:

  3. 分配内存如果成功则直接返回,否则;

  4. 查看new_handler是否可用,new handler的作用用于再释放一部分内存

  5. 如果可用则调用之并跳转到1,否则;

  6. 抛出bad_alloc异常
    //++++++++++++++++++++++++++++++++++++++//

new,delete在C++中被归为运算符,所以可以重载他们
new的行为:

  • 先开辟内存空间

  • 再调用类的构造函数
    在开辟内存空间的部分,可以被重载
    delete的行为:

  • 先调用类的析构函数

  • 再释放内存空间
    释放内存空间的部分,可以被重载
    为什么要重载?
    有时候需要实现内存池的时候需要重载它们,频繁的new和delete对象,会造成内存碎片,内存不足等问题,调用构造函数placement new。

memset函数的作用,有哪些参数

函数原型:

extern void *memset(void *buffer, int c, int count)

说明:把buffer所指内存区域的前count个字节设置成字符c,返回指向buffer的指针

void *memset(void *dest, int val, size_t count)
{
	if(dest == NULL || count < 0)
		return NULL;
	char *pdest = (char *)dest;
	while(count-- > 0)
	{
		*pdest++ = val;
	}
	return dest;
}

linux系统应用程序的内存空间分配(内核空间和进程空间),一般进程空间多大,内核空间多大,进程空间分布是什么样的,堆区最大空间是多少

  • 在Linux系统中内核空间为1GB(0xC0000000到0xFFFFFFFF),用户进程空间为3GB(0x00000000到0xBFFFFFFF)
  • 在Windows系统中默认的内存分配是内核空间2GB,用户进程空间为2GB
    进程空间分布:
  • stack(栈):存储局部,临时变量,在函数调用时,存储函数的返回指针,用于控制函数的调用和返回,在程序开始时自动分配内存,结束时自动释放内存,(Linux系统通常在8MB左右)
  • Heap(堆):存储动态内存分配,需要程序员手工分配,手工释放
  • uninitialized data(BSS):在程序运行初未对变量进行初始化的数据
  • initialized data(Data):在程序运行初已经对变量进行初始化的数据
  • Text(程序段):程序代码在内存中的映射,存放函数体的二进制代码
    堆区的大小受限于计算机系统中有效的虚拟内存

C++中的模板机制了,原理,类模板和函数模板在定义时的区别

函数模板机制结论:

  • 编译器并不是把函数模板处理成能够处理任意类的函数
  • 编译器从函数模板通过具体类型产生不同的函数
  • 编译器会对函数模板进行两次编译
  • 在声明的地方对模板代码本身进行编译;在调用的地方的地方对参数替换后的代码进行编译

模板就是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码,可以实现算法与数据结构的分离。
函数重载不是一种面向对象的编程,而只是一种语法规则,重载跟多态没有什么直接关系

二叉树的原理,平衡二叉树的原理,二叉树的遍历方式,二叉树的最大节点数,二叉树插入删除的时间复杂度,二叉树插入的时间复杂度与树的节点数和树深度的关系,二叉树的优点

数组和链表的区别,它们的应用场景

  • 链表是链式的存储结构,数组是顺序的存储结构,在内存中,数组是一块连续的区域
  • 链表的插入与删除元素相对数组较为简单,不需要移动元素,但查找某个元素较为复杂
  • 链表查找某个元素较为简单,但插入与删除元素较为复杂,且最大长度在编程一开始时就指定,扩充长度不如链表方便
  • 数组在内存中连续,在栈区,链表在内存中不连续,一般在堆区

**应用场景:**需要快速访问数据,很少或不插入或者删除元素,用数组
需要经常插入或者删除元素用链表数组结构

C++继承机制,虚继承原理

继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单地说,继承是指一个对象直接使用另一个对象的属性和方法。继承呈现了面向对象程序设计的层次结构,原始类称为基类,继承类称为派生类,也分别叫做父类和子类,子类也可以为父类,被另外的类继承。继承的方式有三种分别为公有继承(public)、保护继承(protect)、私有继承(private)

  • **公有继承:**基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员对派生类是不可见的,也不能被这个派生类的子类所访问
  • **私有继承:**基类的公有成员和保护成员作为派生类的私有成员,并且不能被这个派生类的子类所访问
  • **保护继承:**基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然私有,且对派生类不可见
    public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象

private/protected继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则
一个类有多个基类,这样的继承关系称为多继承

=====class 派生名:访问控制符 基类名1,访问控制符 基类名2

虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)

虚继承:

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承(菱形继承)而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:

class A

class B1:public virtual A;

class B2:public virtual A;

class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

虚函数机制,一个类有虚函数,有成员变量,求所占的内存大小

**虚函数:**虚函数的调用是通过虚函数表指针和虚函数表来实现的

  • 虚函数表和类绑定,只此一份。虚函数表指针每个类对象都有,指针值都是同一个虚函数表的地址
  • 虚函数指针vfptr不属于数据成员,vfptr的值是在构造函数内容执行之前进行初始化的,构造函数是根据本类的类型进行初始化的。所以拷贝构造函数,或者赋值函数都不会影响到vfptr的值
  • 对虚函数的调用,都是通过实体的vfptr所指的虚函数表来进行调用的
    内存排布:子类继承父类的成员变量,内存上会先排布父类的成员变量,接着排布子类的成员变量,其中成员函数不占字节
  • 每个类都有虚指针和虚表
  • 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时),有多少个函数,虚表里面的项就会有多少,多重继承时,可能存在多个的基类虚表和虚指针
  • 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份

总结如下:

  • **无虚函数覆盖的继承:**虚函数按照其声明顺序放在虚函数表中,父类的虚函数在其子类的虚函数的前面
  • **有虚函数覆盖的继承:**派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧
  • **无虚函数覆盖的多重继承:**每个父类都有自己的虚函数表,派生类的虚函数被放在了第一个父类的虚函数表中(按照声明顺序排序)
  • **有虚函数覆盖的多重继承:**派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧
  • **无虚函数覆盖的虚继承:**派生类有单独的虚函数表,另外也单独保存一份父类的虚函数表
  • **有虚函数覆盖的虚继承:**派生类单独的虚函数表,另外也单独保存一份父类的虚函数表,派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧

C++中的多态

多态主要是有静态多态和动态多态

  • **静态多态:**主要是函数重载和泛型编程
  • **动态多态:**虚函数

静态多态:也称为静态绑定或早绑,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用的那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

动态多态:在程序执行期间判断所引用对象的实际类型,根据实际类型调用相应的方法,使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器实现动态绑定

总结:

  • 1.在编译过程中的联编的被称为静态联编(static binding)
  • 2.在运行时选择正确的虚方法,被称为动态联编
  • 3.编译器对非虚方法使用静态联编,编译器对虚方法使用动态联编

说说引用,什么时候用引用好,什么时候用指针好?

指针和引用的区别:

  • 非空区别:指针可以为空,而引用不能为空
  • 可修改区别:如果指针不是常指针,那么就可以修改指向,而引用不能
  • 初始化区别:指针在定义时可以不用初始化,而引用在定义的同时必须初始化
  • 从内存上来讲,系统为指针分配内存空间,而引用与绑定的对象共享内存空间,系统不为引用变量分配内存空间

使用引用参数的主要原因有两个:
•程序员能修改调用函数中的数据对象
•通过传递引用而不是整个数据–对象,可以提高程序的运行速度

一般的原则:
对于使用引用的值而不做修改的函数:
•如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
•如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
•如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
•如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)

对于修改函数中数据的函数:
•如果数据是内置数据类型,则使用指针
•如果数据对象是数组,则只能使用指针
•如果数据对象是结构,则使用引用或者指针
•如果数据是类对象,则使用引用

关键字const是什么含义

const是C++中常用的类型修饰符,常类型使指使用类型修饰符const说明的类型,意味着“只读”

const的作用:
  • **可以定义const常量:**如const int max = 100;

  • **便于进行类型检查:**const常量有数据类型,而#define宏常量没有数据类型,编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能产生意想不到的错误

  • **可以保护被修饰的东西:**防止意外的修改,增强程序的健壮性

  • 为函数重载提供了一个参考:

  • **可以节省空间,避免不必要的内存分配:**const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝

    #define PI 3.14159 //常量宏
    const double Pi = 3.14159 //此时并未将Pi放入ROM中
    double i=Pi; //此时为Pi分配内存,以后不在分配
    double I=PI; //编译期间进行宏替换,分配内存
    double j=Pi; //没有内存分配
    double J=PI; //再进行宏替换,有一次分配内存

**提高了效率:**编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率很高

const的使用
const int a;  
int const a;   //前两个作用一样,a是一个常整型数
const int *a;  //a是一个指向常整型数的指针(整型数是不可修改的,指针是可以修改的)---底层const
int *const a;  //a是一个指向整型数的常指针(指针指向的整型数是可以修改的,但指针是不可修改的)---顶层const
int const *a const;   //a是一个指向常整型的常指针(指针指向的整型数是不可以修改的,同时指针也是不可以修改的)

即指针使用const有:

  • 指针本身是常量不可变: char * const pContent;
  • **指针所指向的内容是常量不可变:**const char *pContent;
  • 两者都不可变: const char * const pContent;

**区别方法:**沿着*号划一条线:

  • **底层const:**如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;常量指针:指向的对象是个常量,不能修改其内容,只能更改指针指向的地址
  • **顶层const:**如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量,指针常量,不能修改指针指向的地址,只能修改其内容

函数中使用const

(1) const修饰函数参数

a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)

void function(const int Var);

b.参数指针所指向内容为常量不可变

void function(const char *Var);

c.参数指针本身为常量不可变(无意义,因为char *Var也是形参)

void function(char *const Var);

d.参数为引用,为了增加效率同时防止修改,修饰引用参数时:

void function(const Class& Var);  //引用参数在函数内不可以改变
void function(const TYPE& Var);   //引用参数在函数内为常量不可变

实现库函数strcpy的功能,不能使用任何库函数:

char *strcpy(char *strDest, const char *strSrc)
{
	if(strDest == NULL || strSrc == NULL)
		return NULL;
	if(strDest == strSrc)
		return strDest;
	char *temptr = strDest;
	while((*temptr++ = *strSrc++) != '\0');
	return temptr;
}

实现库函数strcmp函数的功能

int strcmp(const char *strDest, const char *strSrc)
{
	assert(strDest != NULL && strSrc != NULL);
	while(*strDest && *strSrc && *strDest == *strSrc)
	{
		strDest++;
		strSrc++;
	}
	return *strDest - *strSrc;
}

C语言中static有什么作用

  • 函数体内static变量的作用范围为该函数体,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值
  • 模块体内static全局变量可以被模块体内所有函数访问,但不能被模块外其他函数访问
  • 模块体内static函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内
  • 类中的static成员变量属于这个类所有,对类的所有对象只有一份拷贝
  • 类中的static成员函数属于整个类所有,这个函数不接收this指针,因而只能访问类的static成员变量。

C++中的struct和class的全部区别

  • C++中的struct具有了"类"的功能,与关键字class的区别在于struct中的成员变量和函数的默认访问权限为public,而class的为private
  • struct可以在定义的时候直接以{}对其成员变量赋初值,而class不能

C++内存管理

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

  • ,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • ,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

C编译的程序占用存储

  • **栈区(stack):**由编译器自动分配释放,存放函数的参数值,局部变量值等
  • **堆区(heap):**一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收
  • **全局/静态存储区(static):**全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放
  • **文字常量区:**常量字符串存放在这里
  • **程序代码区:**存放函数体的二进制代码

内存泄漏的情况

  • 缓冲区溢出
  • new/delete函数没有匹配
  • 浅拷贝
  • 野指针/空悬指针
  • 重复释放
  • 内存碎片

C++中成员变量的列表初始化

class A
{
private:
    int n1;
    int n2;

public:
    A():n2(0),n1(n2+2){}

    void Print(){
        cout << "n1:" << n1 << ", n2: " << n2 <<endl;  
    }
};

int main()
{

    A a;
    a.Print();

    return 0;
}

输出结果为n1:随机, n2:0

  • 成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。
  • 如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
  • 注意:类成员在定义时,是不能初始化的
  • 注意:类中const成员常量必须在构造函数初始化列表中初始化。
  • 注意:类中static成员变量,必须在类外初始化。

一个C++源文件从文本到可执行文件经历的过程

对于c/c++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

  • 预处理, 产生.ii文件 gcc -E hello.c -o hello.i
  • 编译, 产生汇编文件.S文件 gcc -S hello.i -o hello.S
  • 汇编,产生目标文件.o或.obj文件 gcc -C hello.S -o hello.o
  • 链接,产生可执行文件.out或.exe文件 gcc hello.o -o hello

gcc -Wall 允许发出GCC提供的所有有用的警告信息
gcc -w 关闭所有警告信息
使用GDB调试可执行文件之前,必须使用带-g或者-gdb编译选项的gcc命令来编译源程序
编译:gcc test.c -o test -g
GDB调试: gdb test
GDB调试当前正在运行的程序:
1.编译时带-g选项
2.运行程序
3.ps找到进程号
4.启动gdb, 使用attach选项,这时gdb会停止在程序的某处
5.按照GDB调式方法调式,当程序退出之后,依然可以使用run命令重启程序

C++的新特性

  • 关键字及新语法:auto, nullptr, for
  • STL容器:std::array, std::forward_list, std::unordered_map, std::unordered_set
  • 多线程:std::thread, std::atomic, std::condition_variable
  • 智能指针内存管理:std::shared_ptr, std::weak_ptr
  • 其他:std::function, std::bind和lamda表达式

C++中的虚类

C++中的虚类相当于模型类,定义虚类的方式是在:在类中创建纯虚函数,这就是多态的一种方式
纯虚类是不能创建对象的,但是它能创建指针对象,用他存储其派生类的对象,但是存储了派生类的对象也只能访问其有的成员

C++中的析构函数为什么是虚函数

如果基类的析构函数不是虚函数,在特定情况下会导致派生类无法被析构
情况一:用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
情况二:用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏

带参数的宏定义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值