【c++基础】经典问题总结1

1.面向对象基础知识

质料一链接

质料二链接

推荐质料链接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgeDuQaE-1619187545734)(C:\Users\Chen\AppData\Roaming\Typora\typora-user-images\1619146299805.png)]

免费书籍下载

1.1 static关键字

static 修饰的数据存放在全局数据区,限定了作用域,生命周期是直到程序结束。

C++中的用法(包含C中的用法):

  1. static修饰类成员变量 静态成员变量属于整个类,在类实例之间共享,也就是说无论创建多少个类的对象,该成员变量都只有一个副本。 同时由于静态成员变量属于整个类,所以只能在类内申明,在类外初始化。如果在类内就初始化,那么会导致每一个实例化的对象都拥有一个该成员变量,这是矛盾的。
  2. static修饰类成员函数 静态成员函数属于整个类,由于没有this指针,所以只能调用静态成员变量

1.2指针与引用的区别?

  1. 引用是变量的别名,本身不具有单独的内存空间,属于直接访问;指针是指向地址的变量,有单独的内存空间,属于间接访问。
  2. 引用在创建时就必须初始化,且不能更改绑定;指针可以不初始化,可以更改指向。

总的来说,引用既有指针的效率,同时也更加方便直观。

1.3简述extern关键字?

关键字 extern 对变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。

1.4堆和栈的区别?

  • 栈是由编译器自动分配释放的,存放在高地址处,往下进行存储,通常存放局部变量,形参,函数调用等,其操作方式类似于数据结构中的栈,不会产生外部碎片。
  • 堆是由程序员手动分配释放的,存放在低地址处,往上进行存储,通常为new malloc 等申请的内存块,其操作方式类似于数据结构中的链表,会产生外部碎片。
  • 堆属于动态分配,没有静态分配的堆;栈由静态分配与动态分配两种方式,但是栈的动态分配由编译器控制。

注: 静态内存分配在编译时期完成;动态内存分配在程序运行时期完成。

1.5深拷贝与浅拷贝

  • 浅拷贝 只是对指针的拷贝,拷贝后会有两个指针指向同一个内存空间;
  • 深拷贝 对指针指向的内容进行拷贝,拷贝后会有两个指针指向不同的内存空间;

浅拷贝可能会出现问题,因为两个指针指向同一块内存区域,一个指针的修改会造成另一个指针错误,如出现两个对象析构,两次delete内存的情况。

1.6内联函数(inline)是什么?

  • 我们知道如果频繁的调用一个函数,那么函数多次压栈会消耗过多的栈空间。 当函数被申明为内联函数之后,编译器编译时会把该函数的代码副本放置在每个调用该函数的地方 ,而不是按照普通的函数调用机制进行压栈调用;少了多次的压栈操作,栈空间的消耗就减小了。 避免了函数跳转和保护现场的开销

另外注意:

  1. 内联函数一般是不超过10行的小函数,内联函数中不允许使用循环和开关语句,因为如果内联函数过长或者过于复杂,那么内联展开之后同样会消耗许多栈空间,便得不偿失了。所以滥用内联函数反而可能会导致程序变慢。

  2. 类成员函数默认加上inline,但具体是否进行内联由编译器决定,类函数声明前加上inline是无效写法,只有在类函数定义前加上inline才有效。

1.7 struct 与 class 的区别?

struct和class的区别主要在于 默认访问级别 和 默认继承级别。

  1. 默认访问级别:struct中的成员默认是public,class中的成员默认是private。
  2. 默认继承级别:struct默认public继承,class默认private继承。

除了这两点外,struct 和 class 完全相同。

引申:C++和C的struct区别

C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成

员函数的定义,(C++中的struct能继承,能实现多态)

1.8 内存对齐是什么?(字节对齐)

现代计算机中的内存空间都是按照字节划分的,CPU实际读取内存时,是按照k字节进行读取而不是一个字节一个字节读取,这就是内存对齐;有了内存对齐之后,CPU可以一次性读取k字节的数据,变得更加高效。

注意

  1. k通常为最大成员数据类型的大小,结构体的大小也应该为k的整数倍。
  2. 在union,class,struct中均有内存对齐;但是也可以通过 #pragma push(k)#pragma pop() 来设置内存对齐的方式。

计算类在内存中的占用:参考资料1参考资料2

类所占空间 = 非静态成员变量 + 指向虚函数的指针(如果有虚函数) ;

  • 要考虑内存对齐问题,指针:32bit 目标平台寻址空间是4Byte(32bit),所以指针是 4Byte的;64bit 的指针是 8Byte

  • 不管类里面有多少个虚函数,类内部只要保存虚表的起始地址即可,虚函数地址都可以通过偏移等算法获得。(32位:4Byte、64位:8Byte)

  • 静态成员变量是在编译阶段就在静态区分配好内存的,所以静态成员变量的内存大小不计入类空间

  • C++编译系统中,数据和函数是分开存放的(函数放在代码区;数据主要放在栈区和堆区,静态/全局区以及文字常量区也有),实例化不同对象时,只给数据分配空间,各个对象调用函数时都都跳转到(内联函数例外)找到函数在代码区的入口执行,可以节省拷贝多份代码的空间

  • 类的静态成员变量编译时被分配到静态/全局区,因此静态成员变量是属于类的,所有对象共用一份,不计入类的内存空间

1.9 C++ 函数调用的压栈过程

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;
当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的
返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后
打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

1.9 陈述一下面向对象吧

面向对象:将需求或者业务进行抽象化

三大特性:继承、封装和多态

1.9.1 继承

继承就是子类继承父类,拥有父类的一些属性和方法。在无需重新编写原来的类的情况下对这些功能进行扩展 ,使得程序具有很灵活的扩展性。

1.9.2 封装

封装就是把客观事物封装成抽象的类,对数据和方法设置访问权限,避免外界干扰和不确定性访问。在日常使用中的链接库(.so/dll文件)、第三方api,都是封装起来的。

1.9.3 多态

多态是一个接口,多种方法。

C++中虚函数的唯一用处就是构成多态。

虚函数:虚函数是带有 virtual 关键字的 类成员函数。实现多态需要进行 虚函数重写,也就是派生类有一个和基类 函数名,参数,返回值完全相同 的成员函数,也就称为虚函数重写(覆盖)。这就是虚函数实现多态的方式。

虚函数表:有虚函数的类在编译时期都会生成一个 虚函数表,虚函数表实质上是一个 指针数组,存放 指向虚函数的指针,通过该指针我们可以调用虚函数;另外虚函数表是类对象之间共享的,在各个类对象之间不会存储整个虚函数表,只存放一个指向该虚函数表的指针,该指针通常是放在类内存的最前面。这就是虚函数表。该虚函数表存放在全局静态数据区DATA段

在生成派生类过程中,对虚函数表的操作有三个步骤

  1. 将基类中的虚函数表指针拷贝到派生类中;
  2. 派生类对基类虚函数进行覆盖(重写);
  3. 派生类将自己新增的虚函数依次添加在虚函数表后。

延伸1:虚函数表中的虚函数指针是如何实现偏移的?

通常编译器会将虚函数表指针放在对象内存的最前面,我们可以通过取该对象的地址得到该虚函数表指针,进而得到虚函数表,也就是一个指针数组(指针属于函数指针,指向虚函数),遍历这个数组,就可以得到我们想要的虚函数。

延伸2:每个实例化对象的虚函数表是否相同?

相同,因为虚函数表属于类对象之间共享的,只会有一个,每一个实例化对象都只会存放一个虚函数表指针,指向共享的虚函数表。

1.10 C++中哪些函数不能是虚函数?

  1. 构造函数不能是虚函数

如果将构造函数设置为虚函数,那么派生类将无法创建,因为无法调用基类的构造函数。

  1. inline内联函数不能是虚函数

因为内联函数会在编译时内联展开,而虚函数需要在运行时动态联编。

  1. 友元函数不能是虚函数

因为友元函数不属于类成员函数,虚函数必须是类成员函数。

  1. 静态成员函数不能是虚函数

因为静态成员函数是整个类共享的,虚函数无法进行覆盖。

1.12 重载,隐藏,覆盖的区别?

  • 重载同一个类中,函数名相同参数类型(或个数) 不同则为函数重载;如果只是 返回值不同 则不能称为重载。重载也称为编译时多态。
  • 隐藏 若派生类的函数名与基类的 函数名相同,派生类的函数则会吧基类的函数隐藏起来。
  • 覆盖 派生类中的函数与基类中的虚函数完全相同(函数名,参数,返回值均相同),那么称为覆盖。覆盖也称为运行时多态。

1.12.1 继承时的名字遮蔽

基类成员和派生类成员的名字一样时会造成遮蔽(包含参数不同),这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

1.13 构造函数与析构函数能否为虚函数?

  • 构造函数不能为虚函数

若构造函数为虚函数,那么派生类生成的过程中将会无法调用基类的构造函数。

  • 析构函数可以为虚函数

如果不将析构函数设置为虚函数,只能调用基类的析构函数,将无法调用派生类的析构函数从而造成内存泄漏。

1.14 析构函数可以抛出异常吗?

析构函数不能抛出异常,原因如下:

  1. 如果析构函数抛出异常,那么异常点之后的程序并不会执行,那么就会造成内存泄漏的问题。
  2. 严格来说,析构函数也是处理异常的一部分;如果之前发生异常,调用析构函数来释放内存,若是析构函数也抛出异常,将会让程序崩溃。

1.15 空类中自带哪些函数?

六个函数:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符
  • 取址运算符
  • 取址运算符const。

1.16 简述智能指针?

智能指针的本质也是指针,只是它可以帮助我们自动释放空间,避免了内存泄漏和野指针的情况。

目前常用的智能指针有三种auto_ptr已经淘汰):

  1. unique_ptr 一个对象只能由一个unique_ptr引用,当指针不再引用该对象时,该对象自动析构并释放内存。
  2. shared_ptr 一个对象可以由多个shared_ptr引用,对象的被引数量可以用引用计数(use_count)来表示,当对象的引用计数为0时,将该对象自动析构并释放内存。
  3. weak_ptr weak_ptr是一种弱引用,不会增加对象的引用计数,是用来打破shared_ptr相互引用时的死锁问题。 例如现在有两个类,一个类A,一个类B,类A里面有一个shared_ptr指向B,B里面有一个shared_ptr指向A,这样的话就算程序结束了这两个类也不会进行析构,因为他们的引用计数都还是1,这样会造成内存泄漏。
  • 延伸:智能指针是线程安全的吗?

首先智能指针的引用计数使用的是atomic原子操作,所以智能指针本身是线程安全的;但是对于智能指针托管的对象,在多线程环境下则需要加锁操作才行。

1.16 C++ 的emplace函数

性emplace向容器中添加新元素,在容器管理的内存空间中构造新元素,与insert相比,省去了构造临时对象,属于零拷贝,减少了内存开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值