(计算机基础 核心知识)C++

1.简述智能指针

智能指针其作用是管理一个指针,避免程序员申请的空间在函数结束时忘记释放,造成内存泄漏这种情况滴发生。

然后使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

2.动态链接和静态链接区别

静态连接库就是把 (lib) 文件中用到的函数代码直接链接进目标程序程序运行的时候不再需要其它的库文件动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从 DLL 中寻找相应函数代码,因此需要相应 DLL 文件的支持。

静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。

静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

动态库就是在需要调用其中的函数时,根据函数映射表找到该函数然后调入堆栈执行。如果在当前工程中有多处对dll文件中同一个函数的调用,那么执行时,这个函数只会留下一份拷贝。但如果有多处对 lib 文件中同一个函数的调用,那么执行时该函数将在当前程序的执行空间里留下多份拷贝,而且是一处调用就产生一份拷贝。

3.说一下 define、const、typedef、inline 使用方法?

1、const 与 #define 的区别

const 定义的常量是变量带类型,而 #define 定义的只是个常数不带类型

define 只在预处理阶段起作用,简单的文本替换,而 const 在编译、链接过程中起作用;

define 只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;

define 预处理后,占用代码段空间,const 占用数据段空间;

const 不能重定义,而 define 可以通过 #undef 取消某个符号的定义,进行重定义;

define 独特功能,比如可以用来防止文件重复引用。

对于 define 来说, 宏定义实际上是在预编译阶段进行处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进行字符串的展开,遇到多少次就展开多少次,而且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运行时系统并不为宏定义分配内存,但是从汇编 的角度来讲,define 却以立即数的方式保留了多份数据的拷贝。

对于 const 来说, const 是在编译期间进行处理的,const 有类型,也有类型检查,程序运行时系统会为 const 常量分配内存,而且从汇编的角度讲,const 常量在出现的地方保留的是真正数据的内存地址,只保留了一份数据的拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const 常量分配内存,而是直接将 const 常量添加到符号表中,省去了读取和写入内存的操作,效率更高。

2、#define 和别名 typedef 的区别

执行时间不同,typedef 在编译阶段有效,typedef 有类型检查的功能#define 是宏定义,发生在预处理阶段,不进行类型检查

功能差异,typedef 用来定义类型的别名,定义与平台无关的数据类型,与 struct 的结合使用等。

在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数

栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。

在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

inline 的使用是有所限制的,inline 只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如 while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。

其次,因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。

因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。

声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。

关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

如下风格的函数 Foo 不能成为内联函数:

inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}

而如下风格的函数 Foo 则成为内联函数:

void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起

所以说,inline 是一种"用于实现的关键字",而不是一种"用于声明的关键字"。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

内联是以**代码膨胀(复制)**为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

宏定义在预处理阶段进行文本替换,inline 函数在编译阶段进行替换;

inline 函数有类型检查,相比宏定义比较安全;

4.析构函数一般写成虚函数的原因 & 构造函数为什么一般不定义为虚函数

直观的讲:是为了降低内存泄漏的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那 么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。

如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。

构造函数为什么一般不定义为虚函数

  • 虚函数调用只需要知道“部分的”信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的话,是需要知道对象的完整信息的。特别是,需要知道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
  • 而且从目前编译器实现虚函数进行多态的方式来看,虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则。

5.new / delete ,malloc / free 区别

都可以用来在堆上分配和回收空间new /delete 是操作符,malloc/free 是库函数。

执行 new 实际上执行两个过程:1.分配未初始化的内存空间(malloc);2.使用对象的构造函数对空间进行初始化;返回空间的首地址。如果在第一步分配空间中出现问题,则抛出 std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第二步构造对象时出现异常,则自动调用 delete 释放内存。

执行 delete 实际上也有两个过程:1. 使用析构函数对对象进行析构;2.回收内存空间(free)。

以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,而 malloc 得到的是未初始化的空间。所以 new 是 new 一个类型,而 malloc 则是malloc字节长度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete 一个类型,free 一个字节长度的空间。

为什么有了 malloc/free 还需要 new/delete? 因为对于非内部数据类型而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数,对象在消亡以前要自动执行析构函数。由于 mallo/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行的构造函数和析构函数的任务强加于 malloc/free,所以有了 new/delete 操作符。

6.简单说一下函数指针

从定义和用途两方面来说一下自己的理解:

首先是定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。

在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。

其次是用途:调用函数和做函数的参数,比如回调函数。

示例:

char * fun(char * p)  {…}  // 函数fun
char * (*pf)(char * p);    // 函数指针pf
pf = fun;                 // 函数指针pf指向函数fun
pf(p);                    // 通过函数指针pf调用函数fun

7.C++ 和 Java 区别 & C 和 C++ 区别

**指针:**Java 语言让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的自动管理功能,从而有效的防止了 C++ 语言中的指针操作失误的影响。但并非 Java 中没有指针,Java 虚拟机内部中还是用了指针,保证了 Java 程序的安全。

多重继承:C++ 支持多重继承但 Java 不支持,但支持一个类继承多个接口,实现 C++ 中多重继承的功能,又避免了 C++ 的多重继承带来的不便。

数据类型和类:Java 是完全面向对象的语言,所有的函数和变量必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可以实现自己的特点和行为。Java 中取消了 C++ 中的 struct 和 union

**自动内存管理:**Java 程序中所有对象都是用 new 操作符建立在内存堆栈上,Java 自动进行无用内存回收操作,不需要程序员进行手动删除。而 C++ 中必须由程序员释放内存资源,增加了程序设计者的负担。Java 中当一个对象不再被用到时, 无用内存回收器将给他们加上标签。Java 里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作来删除。

Java 不支持操作符重载。操作符重载被认为是 C++ 的突出特性。

Java 不支持预处理功能。C++ 在编译过程中都有一个预编译阶段,Java 没有预处理器,但它提供了 import 与 C++ 预处理器具有类似功能。

Java 中不提供 goto 语句,虽然指定 goto 作为关键字,但不支持它的使用,使程序简洁易读。

Java 的异常机制用于捕获例外事件,增强系统容错能力。

首先,C 和 C++ 在基本语句上没有过大的区别。

C++ 有新增的语法和关键字,语法的区别有头文件的不同和命名空间的不同,C++ 允许我们自己定义自己的空间,C 中不可以。关键字方面比如 C++ 与 C 动态管理内存的方式不同,C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,而且 C++ 中在指针的基础上增加了引用的概念,关键字例如 C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要求,还有 dynamic_cast 增加类型安全方面的内容。

函数方面 C++ 中有重载和虚函数的概念C++ 支持函数重载而 C 不支持,是因为 C++ 函数的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后面,例如,int func(int,double)经过名字修饰之后会变成funcint_double,而 C 中则会变成 _func,所以 C++ 中会支持不同参数调用不同函数。

C++ 还有虚函数概念,用以实现多态。

类方面,C 的 struct 和 C++ 的类也有很大不同C++ 中的 struct 不仅可以有成员变量还可以成员函数,而且对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继承权限都是 public,C++ 中除了 struct 还有 class 表示类,struct 和 class 还有一点不同在于 class 的默认成员访问权限和默认继承权限都是 private。

C++ 中增加了模板还重用代码,提供了更加强大的 STL 标准库。

最后补充一点就是 C 是一种结构化的语言,重点在于算法和数据结构。C 程序的设计首先考虑的是如何通过一个代码,一个过程对输入进行运算处理输出。而 C++ 首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信息得到输出。

8.构造函数析构函数可否抛出异常 & 作用

C++ 只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在对象 b 的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。

用 auto_ptr 对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;

如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++ 会调用 terminate 函数让程序结束;

如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,就是你不写,编译器也自动调用一次。

析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。

析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。 当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。

9.内存泄漏的定义,如何检测与避免?

定义:内存泄漏简单的说申请了一块内存空间,使用完毕后没有释放掉。 它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄漏了。

如何检测内存泄漏

  • 首先可以通过观察猜测是否可能发生内存泄漏,Linux 中使用 swap 命令观察还有多少可用的交换空间,在一两分钟内键入该命令三到四次,看看可用的交换区是否在减少。
  • 还可以使用 其他一些 /usr/bin/stat 工具如 netstat、vmstat 等。如发现波段有内存被分配且从不释放,一个可能的解释就是有个进程出现了内存泄漏。
  • 当然也有用于内存调试,内存泄漏检测以及性能分析的软件开发工具 valgrind 这样的工具来进行内存泄漏的检测。

10.什么情况下会调用拷贝构造函数(三种情况)

类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数:

  • 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。
  • 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。
  • 一个对象需要通过另外一个对象进行初始化。

11.深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)& 为什么拷贝构造函数必需时引用传递,不能是值传递?

当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下, 系统会调用默认的拷贝函数-即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。

但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针指向同一个地址,当对象快要结束时,会调用两次析构函数,而导致指野指针的问题。

所以,这时必需采用深拷贝。深拷贝与浅拷贝之间的区别就在于深拷贝会在堆内存中另外申请空间来存储数据,从而也就解决了野指针的问题。简而言之,当数据成员中有指针时,必需要用深拷贝更加安全

为什么拷贝构造函数必需时引用传递,不能是值传递?

为了防止递归调用。当一个对象需要以值方式进行传递时,编译器会生成代码调用它的拷贝构造函数生成一个副本,如果类 A 的拷贝构造函数的参数不是引用传递,而是采用值传递,那么就又需要为了创建传递给拷贝构造函数的参数的临时对象,而又一次调用类 A 的拷贝构造函数,这就是一个无限递归。

12.构造函数的执行顺序?析构函数的执行顺序?

构造函数顺序

  • 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
  • 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  • 派生类构造函数

析构函数顺序

  • 调用派生类的析构函数
  • 调用成员类对象的析构函数
  • 调用基类的析构函数

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容

全部评论

相关推荐

评论
点赞
3
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务