【面经】C++篇

1、C/C++内存模型

1. :栈⽤于存储函数的局部变量、函数参数和函数调⽤信息的区域。函数的调⽤和返回通过栈来管理。向下增长。
2. :堆⽤于存储动态分配的内存的区域,由程序员⼿动分配和释放。使⽤ new delete malloc 和 free 来进⾏堆内存的分配和释放。向上增长。
3. 全局/静态区: 全局区存储全局变量和静态变量。⽣命周期是整个程序运⾏期间。在程序启动时分配,程序结束时释放。
4. 常量区: 常量区也被称为只读区。存储常量数据,如字符串常量。
5. 代码区:存储程序的代码。
2、编译的四个过程

1、预处理:引入头文件,宏替换,删除注释…生成以.i为结尾的预编译文件

2、编译:检查语义语法规错误,无错误则生成以.s为结尾的汇编文件

3、汇编:将汇编代码解释为二进制的cpu指令,生成.o为结尾的可重定向目标文件

4、链接: 将多个目标文件及所需要的库连接成最终的可执行目标文件

什么是extern"C"?
可以 C++中使用C 的已编译好的函数模块,这时候就需要用到 extern”C”
也就是 extern“C” 都是在c++ 文件里添加的。
extern 链接阶段起作用(四大阶段:预处理 -- 编译 -- 汇编 -- 链接)。
3、c++11的新特性

C++11新特性(全详解) - 知乎

找出了比较简单的几条:

  • 1、用auto关键字来支持自动类型推导
  • 2、引入了范围for循环,提供了一种简洁而直观的方式来遍历容器、数组、字符串和其他可迭代对象。比如for(char c : str){..}。
  • 3、引入了新的智能指针类(独占式、共享式、弱引用),用于更安全和方便地管理动态分配的资源,避免内存泄漏和悬空指针等问题。
  • 4、统一初始化:直接初始化、拷贝初始化、列表初始化
  • 5、nullptr是c++11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针
  • 6、lambda匿名函数
4、malloc/free和new/delete的区别
  1. malloc和free是函数(C/C++),new和delete是操作符(C++)。new 和 delete 在 C++ 中提供了更高级、更类型安全的内存管理功能。
  2. 申请空间:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
  3. 类型强转与否:malloc的返回值为void*, 在使用时必须强转;而new后跟的是空间的类型,就不需要强转。说明:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  4. 申请空间失败时:malloc返回的是NULL;而new会抛异常。
  5. 申请自定义类型对象时:malloc/free只会开辟空间,不会调用构造函数与析构函数,而new会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
1G内存的计算机中能否malloc(1.2G)?为什么?
是有可能申请 1.2G 的内存的。
应用程序通过 malloc 函数可以向程序的虚拟空间
申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
什么是虚拟内存?

虚拟内存是一种计算机系统内存管理技术,它为应用程序提供了一个连续可用的内存空间,但实际上,这部分内存可能分布在物理内存的不同碎片中,甚至部分暂时存储在外部磁盘存储器上。 

5、什么是内存泄漏,怎么避免?

内存泄漏:程序未能释放掉不再使用的内存

  1. 养成良好的编码规范,申请的内存空间记着匹配的去释放内存
  2. 使用内存泄漏工具检测(在linux环境下可以使用内存泄漏检测工具Valgrind,我们写代码是可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致。)
  3. 采用RAII思想或者智能指针来管理资源
6、野指针是什么?悬空指针是什么?
  1. 野指针:野指针指访问一个已删除或访问受限的内存区域的指针,野指针不能判断是否为NULL来避免。指针没有初始化,释放后没有置空,越界。指向内存被释放的内存或者没有访问权限的内存的指针。
  2. 悬空指针:一个指针的指向对象已被删除,那么就成了悬空指针
“野指针”的成因主要有3种

①、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

char *p = NULL;

char *str = new char(100);

②、指针p被free或者delete之后,没有置为NULL

③、指针操作超越了变量的作用范围

如何避免野指针

(一)指针初始化

将指针初始化为NULL

char *   p  = NULL;

malloc分配内存

char * p = (char * )malloc(sizeof(char));

用已有合法的可访问的内存地址对指针初始化

char num[ 30] = {0};

char *p = num;

(二)指针用完后释放内存,将指针赋NULL。

delete(p);

p = NULL;

7、传参:值传递、引用传递、指针传递

值传递:

1、当函数不需要修改原始值,只是使用该值时,值传递也是一个不错的选择:函数内部对参数的修改不会影响到原始值,确保了数据的安全性。

2、对于简单的数据类型,如整数、字符等,值传递是最合适的选择

3、需要额外的内存来存储参数的副本,对于大型对象的传递,值传递会导致性能下降,可能会导致内存消耗过大。

引用传递:

  1. 当函数需要修改原始值时,引用传递是最常用的方式。
  2. 对于大型对象的传递,引用传递可以提高性能
  3. 无法传递空值,需要保证引用的有效性
  4. 不需要手动管理内存

指针传递:

1、当函数需要修改原始值时,指针传递是一个不错的选择:指针传递是通过将参数的内存地址传递给函数来实现的。函数可以通过指针来直接访问和修改原始值。

2、对于需要传递大型对象的情况,指针传递可以提高性能。

3、指针可能为空或者指向无效的内存地址,需要进行有效性检查

4、需要手动管理内存

 指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调用函数的形式参数被作为被调用函数的局部变量处理,会在栈中开辟内存空间以存放有主调函数传递进来的实参值,从而形成了实参的一个副本。值传递的特点是:被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

       引用参数传递过程中,被调函数的形式参数作为局部变量在栈中开辟了内存空间,但这时存放的是由主调函数放进的实参变量的地址。被调用函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数的实参变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针或指针引用。

指针和引用的区别?

指针:变量,独立,可变,可空,替身,无类型检查;

引用:别名,依赖,不变,非空,本体,有类型检查;

在C++中,指针和引用是两种用于间接访问内存地址的机制,但它们之间存在一些重要的区别。

  1. 定义与初始化指针:指针是一个变量,其值为另一个变量的地址。指针在使用前必须被初始化,否则它可能指向一个随机的内存地址,这可能导致未定义的行为。引用:引用是已存在变量的别名。引用在定义时必须被初始化,且一旦初始化后,就不能再指向其他变量。

  2. 可空性指针:指针可以为空(nullptr),表示它不指向任何对象。引用:引用在定义时必须指向一个有效的对象,不能为空。

  3. 可重赋值性指针:指针的值(即它所指向的地址)可以在任何时候被改变,使其指向另一个对象。引用:引用的值(即它所引用的对象)在初始化后不能被改变。一旦一个引用被初始化为引用一个对象,它就不能再引用另一个对象。

  4. 内存占用指针:指针本身是一个变量,会占用一定的内存空间(通常与机器的字大小相同)。引用:引用不占用额外的内存空间,它只是一个别名,与所引用的对象共享同一块内存。

  5. 运算指针:可以对指针进行各种运算,如加法、减法、比较等,以改变它所指向的地址。引用:引用不支持这样的运算,它只是一个对象的别名,可以像操作对象本身一样操作引用。

  6. 用途指针:常用于动态内存分配、函数参数传递(尤其是当需要修改原始数据时)、数据结构(如链表、树等)的实现等。引用:常用于函数参数传递(当不需要修改原始数据,但希望避免数据拷贝时)、类成员变量的别名等。

8、static的用法
1 )用 static 修饰局部变量:使其变为静态存储方式 (静态数据区 ),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
2 )用 static 修饰全局变量:使其只在本文件内部有效,而其他文件不可连接或引用该变量。
3 )用static修饰函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的(这一点在大工程中很重要很重要,避免很多麻烦,很常见)。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。
9、const的用法——只读!
const 主要用来修饰变量、函数形参和类成员函数:
1 )用 const修饰常量:定义时就初始化,以后不能更改,只读
2 )用 const 修饰形参 func(const int a){}; 该形参在函数里不能改变,只读
3 )用 const 修饰类成员函数:该函数对成员变量只能进行只读操作,就是 const类成员函数是不能修改成员变量的数值的。
const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

const和define的区别

  •  #define max 100 ; 定义的常量是没有类型的(不进行类型安全检查,可能会产生意想不到的错误), 所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义 的宏变量在预处理阶段的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换。
  • const int max = 255 ; 定义的常量有类型(编译时会进行类型检查)名字,存放在内存的静态区域 中,在编译时确定其值。在程序运行过程中const变量只有一个拷贝,而#define所定义的宏变量却有多 个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多
10、volatile的用法

与const对立,一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量在内存中的值,而不是使用保存在寄存器里的备份(虽然读写寄存器比读写内存快)。
  1. 提醒编译器不要对该变量相关的代码进行优化,避免出现意外的负面作用;
  2. 对类似的表达式不进行编译层面的指令重排。编译指令重排也是一种编译器优化手段,这条严格来说也是第一条的变种。
以下几种情况都会用到 volatile
1、并行设备的硬件寄存器(如:状态寄存器)
2、一个中断服务子程序中会访问到的非自动变量
3、多线程应用中被几个任务共享的变量

多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程可见。

读取变量在内存里的值,而不要读它的备份。

11、逻辑运算符

if (A && B) 如果 A 为 false ,整个表达式就为 false,不再计算 B 的值了。
if (A & B) 如果 A 为 false ,整个表达式就为 false,但还要计算 B 的值。
if (A && B++) 如果A 为 false,&&不会再计算后面的值
if (A & B++) 如果A 为 false,&则会计算后面的值

||也具有“短路效果”

12、sizeof、strlen

sizeof(变量),是一个操作符,返回一个对象或者类型所占的内存字节数,而不是元素个数!因为还和元素的类型有关。如果对象是指针类型,指针是占4个字节的空间(对32位机)。

strlen(str),是一个函数,返回字符串的长度。直到'\0'为止了,计数结果不包括\0。

13、sizeof(struct)sizeof(union)内存对齐

内存对齐作用:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存, 处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体的内存大小
  1. 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类 型的整数倍
  2. 整个结构体占用内 存大小是结构体内最大数据成员的最小整数倍
  3. 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再 考虑当前类型以及最大结构体内类型

联合体union的大小

  1. 找到占用字节最多的成员;
  2. union的字节数必须是占用字节最多的成员的字节的倍数,而且需要能够容纳其他的成员

13、inline内联函数
C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量 消耗。为了解决这个问题,特别的引入了 inline 修饰符,表示为内联函数。
大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程 序还必须转向一个新位置执行 C++ 中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义 ( 注意是定义而非声明 )的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中 的每个调用点上 内联地 展开。

15、指针

16、面对对象
三特性:封装、继承、多态
public、 protectedprivate三者在继承情况下的访问权限

public

  • 当一个成员(无论是数据成员还是成员函数)被声明为 public 时,它可以在任何地方被访问:在类的内部、从类的派生类以及通过类的对象
  • 在继承中,如果基类中的成员是 public 的,那么在派生类中,这个成员也是 public 的。

protected

  • protected 成员可以在类的内部和从类的派生类中被访问,但不能通过类的对象来访问。
  • 在继承中,如果基类中的成员是 protected 的,那么在派生类中,这个成员也是 protected 的。这意味着派生类的成员函数和友元可以访问它,但派生类的对象或派生类之外的代码不能访问它。

private

  • private 成员只能在类的内部被访问,无论是从派生类还是通过类的对象都不能访问。
  • 在继承中,如果基类中的成员是 private 的,那么在派生类中,这个成员是不可见的。这意味着派生类不能访问这个成员,无论它是通过继承获得的,还是通过其他方式。

继承中的访问权限遵循以下规则:

  • 公有继承(public inheritance):基类的 public 和 protected 成员在派生类中保持其原有的访问权限。基类的 private 成员在派生类中是不可见的。
  • 保护继承(protected inheritance):基类的 public 和 protected 成员在派生类中变为 protected。基类的 private 成员在派生类中是不可见的。
  • 私有继承(private inheritance):基类的所有成员(publicprotected 和 private)在派生类中都变为 private。这意味着即使基类的成员在基类中是 public 或 protected 的,派生类也不能直接访问它们。
  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值