Jialong's Blog
沉潜 自由 追寻幸福
八股文——Cpp相关

new/delete和malloc/free的区别

  • malloc/free是C/C++的库函数,需要stdlib.h;new/delete是C++的关键字;
  • 都可用于申请动态内存和释放内存,new/delete在对象创建的时候自动执行构造函数,对象消亡前自动执行析构函数,底层实现其实也是malloc/free
  • new无需指定内存块的大小,编译器会根据类型信息自行计算;malloc需要显式地支持所需内存的大小
  • new返回指定类型的指针,无需进行类型转换;malloc默认返回类型为void*,必须强行转换为实际类型的指针
  • new内存分配失败时会抛出bad_alloc异常;malloc失败时返回NULL

malloc的底层实现

Linux下:

  • 开辟空间小于128K时,通过brk()函数
    • 将数据段.data的最高地址指针**_edata向高地址移动,即增加堆**的有效区域来申请内存空间
    • brk分配的内存需要等到高地址内存释放以后才能释放,这也是内存碎片产生的原因
  • 开辟空间大于128K时,通过mmap()函数
    • 利用mmap系统调用,在堆和栈之间文件映射区域申请一块虚拟内存
    • 128K限制可由M_MMAP_THRESHOLD选项进行修改
    • mmap分配的内存可以单独释放
  • 以上只涉及虚拟内存的分配,直到进程第一次访问其地址时,才会通过缺页中断机制分配到物理页中

指针和引用的异同点;如何相互转换

  • 引用是别名,指针是地址
  • 指针变量在符号表上对应的地址值为指针变量的地址;而引用在符号边上的地址值为引用对象的地址;指针可以改变指向的对象,而引用不行;二者都可以改变指向对象的值。
  • 硬件通过地址访问内存位置,因此引用可以理解为一个常量指针,只能绑定到初始化它的对象上

struct、union的异同

  • struct中每个变量一次存储;union中,每个变量都是从偏移地址零开始存储,同一时刻只有一个成员存储于该地址
  • struct内存大小遵循结构对齐原则(详见Cpp内存对齐
  • union内存大小为其最大成员的整数倍

extern C的作用

主要是为了能正确实现C++代码调用C语言代码。

C++支持函数重载,即不同名字空间namespace的两个函数原型声明可以完全相同,或者两个函数同名但参数列表不同;g++编译器会对此进行name mangling,生成全局唯一的符号名称,使链接器可以准确识别。

C语言不支持函数重载,即不允许同名符号,所以不需要这些工作,因此在C++代码中加入extern C,是为了链接规范

memcpy()函数注意的问题

函数原型声明void *memcpy(void *dest, void *src, unsigned int count);

memcpy函数用于把资源内存(src所指向的内存区域)中连续的count个字节数据拷贝到目标内存(dest所指向的内存区域)

  • 数据长度count的单位是字节,1byte = 8bit
  • 数据类型为char,则数据长度就等于元素的个数;其他数据类型则要注意数据长度的值
  • n * sizeof(type_name)的写法

memmove()函数与memcpy()函数的功能相同,区别是前者处理空间重叠问题,后者不处理。

memset()函数

函数原型声明void *memset(void *buffer,int c,int count);

初始化所指定的内存空间。memset把buffer指向的内存区域的前count个字节设置成某个字符的ASCII值,一般用于给数组、字符串等赋值。

strcat、strncat、strcmp、strcpy函数

char* strcpy(char *dest, const char *src);

strcpy拷贝函数,不会判断拷贝大小,也没有任何安全检查,不会检查目的地址内存是否够用;

char* strncpy(char *dest, const char *src);

strncpy拷贝函数,会计算复制字符串的大小,但没有检查目标的边界;

int strcmp(const char *str1, const char *str2);

比较两个字符串。str1指向的字符串大于str2指向的字符串时,返回正数,小于时返回负数,等于是返回0.

char* strcat(char *dest, const char *src);

连接两个字符串,把src连接到dest后面,返回dest地址。

机器大小端问题

详见测试机器大小端的方法

static的用法(定义和用途)

static修饰局部变量:使其变为静态存储方式(静态数据区),函数执行完成之后不会被释放,而是继续保存在内存中;

static修饰全局变量:使其只在本文件内部有效,其他文件不可链接或引用该变量;

static修饰函数:静态函数,即函数只在本文件内部有效,对其他文件不可见;避免同名干扰,同时保护。

const的用法(定义和用途)

const起到强制保护的修饰作用,可以预防意外改动,提高程序的健壮性

  • const修饰常量:定义时就初始化,以后不能更改;
  • const修饰形参:func(const int a); 该形参在函数里不能改变;
  • const修饰类成员函数:const类成员函数不能改变成员变量的数值

const常量和#define的区别

主要区别在于:编译阶段、安全性、内存占用等。

  • const定义的常量有类型名字,存放在内存的静态区,在编译时确定其值;
  • #define定义的常量是没有类型的一个立即数,编译器会在预处理阶段将程序中所有使用到该常量的地方进行拷贝替换
  • 由于#define的拷贝有很多分,故宏定义占用的内存要高得多

volatile的用法

编译器不会对volatile变量有关的运算进行编译优化:每次使用该变量必须从内存地址中读取,而不是保存在寄存器中的备份。

用到volatile的几种情况:

  • 并行设备的硬件寄存器(如状态寄存器)
  • 终端服务子程序会访问到的非自动变量
  • 多线程应用中被几个任务共享的变量

常量指针、指针常量、常量引用

  • 常量指针:指针本身是一个常量,指向的内存地址不能改变,但指向地址的内容可以改变
  • 指针常量:指针指向地址的内容是常量,指针本身可以改变。

详见后面对于char * constconst char*的说明。

变量的作用域(全局变量和局部变量)

  • 全局变量:在所有函数体外部定义,程序所在部分都可以使用,不受作用域的影响(生命周期一直到程序的结束)
  • 局部变量:局限于作用域内,默认为auto关键字修饰,即进入作用域时自动生成,离开作用域时自动消失
  • 局部变量可以和全局变量重名,在局部变量作用域范围内,全局变量失效,采用的是局部变量的值

sizeof和strlen

  • sizeof是一个操作符或关键字,不是一个函数,而strlen是一个函数
  • sizeof返回一个对象或类型占用的内存字节数,不会对其中的数据或指针作运算
  • strlen返回一个字符串的长度,不包括/0

sizeof(struct)和内存对齐

内存对齐的作用:

  • 移植原因:某些硬件平台只能在某些特定地址处取特定类型的数据;
  • 性能原因:数据结构应尽可能在自然边界上对齐,未对齐内存需要做两次内存访问,对齐仅需要一次。

详情对齐原则见Cpp内存对齐

char * const, const char *

const char *ptr是指向字符常量的指针,ptr是一个char*类型的常量,所指向的内容不能修改

char* const ptr是指向字符的指针常数,即const指针,不能修改ptr指针,但能修改该指针指向的内容。

inline函数

被频繁调用的函数会导致栈空间或栈内存的大量消耗,因此引入inline修饰函数,即内联函数;内联函数将在程序的每个调用点上内联式地展开。内联以代码膨胀为代价,省去了函数调用的开销,从而提高了函数的执行效率。

inline函数和#define的区别

  • 宏调用不执行类型检查,甚至连正常参数也不检查,但函数调用要检查
  • 宏使用的是文本替代,可能导致无法预料的后果。
  • 宏中的编译错误很难发现,其引用的是扩展的代码,而不是程序员键入的
  • 许多结构体使用宏或使用不同语法来表达很难理解,内联函数使用与普通函数相同的语言,可以随意内联或不内联。
  • 内联代码的调试信息通常比扩展的宏代码更加有用。

内存四区,变量存储区域(堆/栈)

  • 代码区:.text
  • 全局初始化区/静态数据区:.data,明确被初始化的全局变量、静态变量和常量数据,整个生命周期内都可能需要访问
  • 未初始化数据区:.bss,全局未初始化变量
  • 栈区stack:由编译器自动分配释放,存放函数的参数值、局部参数的值等。每当一个函数被调用,该函数的返回地址和调用信息,如某些寄存器内容,会被存储到栈区,这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,即C实现函数递归调用的方法
  • 堆区heap:用于动态内存分配

数组名和指针的区别

  • 数组名对应的是指向数据首元素地址的指针,但该指针所指的地址不能被改写
  • 指针是变量指针,所指向的地址可以更改
  • 对数组名取地址,得到的是数组首元素的地址;对指针取地址,得到的是指针变量所在地址
  • 对数组名使用sizeof,得到的是数组元素个数与数组元素类型字节数的成绩;而对指针使用sizeof得到的是指针类型的字节数

strcpy和memcpy的区别

  • 复制的内容不同:strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等
  • 复制的方法不同:strcpy不需要指定长度,遇到被复制字符的串结束符\0才结束,所以容易溢出。memcpy则根据其第三个参数决定复制的长度
  • 用途不同:通常子啊复制字符串时使用strcpy,而需要复制其他类型数据时则一般用memcpy

野指针

  • 野指针是指向内存未知区域或访问首先区域的指针,结果未知
  • 产生原因
    • 指针定义时未被初始化,默认值随机
    • 指向的内存区域被释放时,指针没有置空
    • 指针操作超越变量作用域,如函数返回指向栈内存的指针或引用

全局变量和静态变量的区别

  • 存储方式上没有区别,都是静态存储方式
  • 非静态全局变量作用域为震哥哥源程序;当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件都是有效的,而静态全局则限制了其作用域,只在该变量的源文件内有效。

左值和右值

左值可以用于赋值语句的左侧,右值不能。

左值与右值的根本区别在于是否允许取地址运算符&获得对应的内存地址。

explicit作用

用于修饰构造函数,使得其不能用于隐式转换和赋值初始化。被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎,因为它们禁止编译器执行非预期的类型转换,除非有一个好的理由允许构造函数被用于隐式类型转换,否则会将其声明为explicit。

++i和i++的区别

一般都用++i,因为其速度较快。

++i相当于:

i = i + 1;
return i;

i++相当于:

int j = i;
i = i + 1;
return j;

由于i++还必须拷贝值给j,所以速度较慢.

继承和多态

继承:类与类之间可以共用代码,实现代码重用。(基类和派生类)

多态:面向对象过程中,基类与派生类存在相同的方法,但有不同的方法体,调用是根据对象有选择地调用合适的方法。

多态的几个条件:

  • 必须是公有继承
  • 必须通过基类的指针或引用指向派生类对象来访问呢派生类方法
  • 基类的方法必须是虚函数

区别:

  • 多态的实现要求必须是共有继承
  • 继承关系中,父类更通用,子类更具体。父类具有一般的特征和行为,子类除了具有父类的特征和行为,还有自己特殊的行为和特征。
  • 继承关系中,并不要求基类方法一定是虚函数。多态要求基类方法必须是虚函数。
  • 多态:子类重写父类的方法,使得子类具有不同的实现。且运行时,根据实际创建的对象动态决定使用哪个方法。

参考

部分参考自八股文之C/C++ 高频知识点汇总


最后修改于 2022-02-18