【面试题汇总】C/C++语法基础

134 阅读23分钟

C/C++语法

简述 C++ 从代码到可执行二进制文件的过程

源文件.c预处理成.i文件,编译成汇编语言.s文件,汇编成机器指令打包成.o文件,链接成可执行文件。

  1. 预处理器(cpp):过滤所有注释,对宏定义进行替换,添加头文件文本,处理条件预编译指令,添加行号和文件名标识,得到另一个已.i作为文件扩展名的C程序。
  2. 编译器(ccl):将.i文本文件翻译成.s文本文件,包含一个汇编语言程序,包含函数main的定义。
  3. 汇编(as):将.s汇编代码翻译成机器指令,并打包成二进制的.o可重定位目标程序
  4. 链接器(ld):将所有不同的.o目标文件链接并合并,得到可执行目标文件,分静态链接和动态链接。

静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

说说 include 头文件的顺序以及双引号""和尖括号<>的区别
  1. 区别:

    1. 尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件
    2. 编译器预处理阶段查找头文件的路径不一样
  2. 查找路径

    1. <>的查找路径:编译器设置的头文件路径 --> 系统变量
    2. ""的查找路径:当前头文件目录 --> 编译器设置的头文件路径 --> 系统变量
说说C语言和C++的区别
  1. C++是面向对象的编程语言;C语言是面向过程的编程语言。
  2. C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
  3. C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。C++的STL库相对于C语言的函数库更灵活、更通用

[参考答案]从机制上: c是面向过程的(但c也可以编写面向对象的程序) ; C++是面向对象的,提供了类。但是C++编写面向对象的程序比c容易。 从适用的方向: c适合要求代码体积小的,效率高的场合,如嵌入式; c++ 适合更上层的,复杂的; linux核心大部分是c写的,因为它是系统软件,效率要求极高。 从名称上也可以看出,C++比c多了+,说明c++是c的超集;那为什么不叫c+而叫c++呢,是因为c++比c来说扩充的东西太多了,所以就在c后面放.上两个+;于是就成了c++。 C语言是结构化编程语言,C++是面向对象编程语言。 C++侧重于对象而不是过程,侧重于类的设计而不是逻辑的设计。

简述一下 C++ 中的多态

在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为,与之相对应的编译时绑定函数称为静态绑定。多态分为静态多态和动态多态。

  1. 静态多态:静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
  2. 动态多态:动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向子类对象时,就调用子类中定义的虚函数。 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。 实现动态多态的条件: 有继承关系,有虚函数重写(被 virtual 声明的函数叫虚函数),有父类指针(父类引用)指向子类对象。 动态多态的实现原理:当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构,虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。
C++多态的作用
  1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用。
  2. 接口重用,为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。
简述一下什么是面向对象

面向对象思想是基于面向过程思想。 面向对象的思想是尽可能模拟人类的思维方式,使得软件开发尽可能接近人类认识世界、解决现实问题的方法和过程,把客观世界中实体抽象为问题域中的对象。面向对象以对象为核心,该思想认为程序由一系列对象组成。

面向对象思想的特点: 更符合人类思维习惯,将复杂的问题简单化,将我们从执行者变成了指挥者

面向对象的三大特征:封装、继承、多态

  1. 封装:将事物属性和行为封装到一起,也就是 C++ 中的类,便于管理,提高代码的复用性。事物的属性和行为分别对应类中的成员变量和成员方法。
  2. 继承:继承使类与类之间产生关系,能够提高代码的复用性以及可维护性。
  3. 多态:多态意味着调用成员函数时,会根据调用方法的对象的类型来执行不同的函数。
简述一下面向对象的三大特征

面向对象的三大特征是:封装、继承、多态。

封装——隐藏内部实现:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限。封装的好处是隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。

继承——复用现有代码:C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过来的表现其共性,而新增的成员体现了其个性。继承可以提高代码的复用性;提高代码的拓展性;是多态的前提。

多态——改写对象行为:在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。

C++对象继承中,子类不同继承权限,对子类及外部代码访问权限的影响
继承方式基类public成员基类protected成员基类private成员
public继承publicprotected派生类中不能使用
protected继承protectedprotected派生类中不能使用
private继承privateprivate派生类中不能使用
C++中,class和struct有什么区别?
  1. class中的成员默认是private,而struct的成员默认为public。
  2. class默认的继承方式是private,而struct的默认继承方式是public。
  3. class还可以用于表示模板类型,struct则不行。
C++ 结构体和 C 结构体的区别(C++ 和 C 中 struct 的区别)
  1. c++中struct结构体可以有成员函数和静态成员,C语言中没有。
在有继承关系的父子类中,构造和析构一个子类对象时,父子构造和析构的执行顺序分别是怎样的?

父类构造——》子类构造——》子类析构——》父类析构

父类的构造函数和析构函数一定要声明为 virtual 吗?

父类析构函数必须声明为virtual,目的是删除父类指针时可以调用到子类的析构函数。如果基类的析构函数没有声明为virtual,当父类的指针删除一个派生类对象时,将不会执行到子类的析构函数。

如果不申明为 virtual 会怎样?能否将父类构造函数也声明成virtual?

不能,虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而父类构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。

什么是虚函数?什么是纯虚函数?什么是抽象类?

虚函数:某基类中声明为virtual并在一个或多个派生类中重新定义的成员函数叫做虚函数。

纯虚函数:纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上 " = 0 ",表明此函数为纯虚函数。

抽象类:含有纯虚函数的类。抽象类无法实例化,即无法创建对象。因为纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。抽象类通常作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

虚函数表指针(vptr)在哪?有什么作用?什么是虚函数表(vftable)?表中存储了什么信息?

对象首地址开始4字节或8字节的位置,存储虚函数表指针,该指针指向该对象的虚函数表。

虚函数表是一个存储多个函数指针的数组。表中每个元素指向一个函数。

  • 若父类对象存在虚函数,则虚函数表存放对象中各个虚函数地址。
  • 若子类对象未重写父类虚函数,则虚函数指针指向的是父类的虚函数。
  • 若子类对象重写了父类虚函数,那么虚函数指针保存的是子类重写父类虚函数的地址。
什么是重载?两个函数只有返回值不一样,是否构成重载?

在C++中,重载声明是指:用相同名称重新声明一个在该作用域内声明过的函数或方法,但是二者参数列表和定义(实现)不相同。重载分为函数重载和运算符重载。通过函数重载,实现了静态联编,体现了静态多态性。 不能,函数重载只依赖于函数名及其参数类型,与返回值类型无关。C++函数重载底层原理是基于编译器的name mangling机制,该机制在重命名函数时,生成唯一的,只与函数名、函数参数类型有关的标识符。

什么是name mangling机制?

使用一种编码方法,在编译阶段将函数名进行转换,加入域和参数信息,对于同名不同参的函数,编译器在进行name mangling操作时,会通过函数名和其参数类型生成唯一标识符,来支持函数重载。Name mangling的目的就是避免重复。

C++中函数的overload(重载)、override(覆盖)、overwrite(隐藏)三者区别?
  1. overload(重载):C++ 允许多个函数拥有相同的名字,但参数列表必须不同。借助重载,一个函数可以实现多种相似功能。
  2. override(覆盖):派生类在虚函数声明时使用override,表示此虚函数是对基类中一个虚函数的重载,且该函数必须重载其基类中的同名函数,否则代码将无法通过编译。
  3. overwrite(隐藏):是指派生类的函数屏蔽了与其同名的基类函数。
extern关键字的作用是什么?当使用extern "C"修饰函数时有什么作用?
  1. 在一个文件中要使用其他文件中的全局变量,用extern关键字声明。
  2. 作用是以C编译的方式去编译该函数。关闭name mangling机制,使得函数重写时,名字不变。
static关键字的作用?
  1. 对函数声明static,使该函数被定义为静态函数,存储到静态存储区,从全局函数转换为内部函数,当同时编译多个文件时,内部函数仅在当前文件可见
  2. 对变量声明static,存储到静态存储区,且默认初始化为0。 对于全局变量,static使其仅在当前文件可见。对于局部变量,static使其生存周期变成整个源函数。
  3. 对类成员声明static,使静态数据成员是类的成员,而不是对象的成员,从而使静态成员函数无法定义为虚函数,且属于整个类而非类的某个对象。
explicit关键字的作用?

explicit关键字在C++中用于类构造函数或转换函数,其作用是防止隐式类型转换和复制初始化。当一个类的构造函数可以接收一个参数,则该构造函数也可以作为一个隐式转换操作符,使用explicit声明构造函数,编译器将不自动调用该构造函数进行类型转换,必须显示地创建一个对象。

C++程序在内存中是如何划分的?

从高地址到低地址依次划分为:栈区->堆区->全局区->代码区

1. 栈区(stack):由编译器自动分配和释放,存放程序运行时,函数分配的局部变量、函数参数、返回数据、返回地址等参数。

2. 堆区(heap):保存new分配的对象,通过delete释放,一般由程序员自行分配和释放,其分配类似于链表。

3. 全局区(静态区static) :分data区、bss区和常量区,程序结束后由系统释放。

data区(gvar):已初始化数据段,全局变量、静态变量和常量 ​ bss区:存放未初始化的全局变量、静态变量。 ​ 常量区存放不可修改的常量,如const修饰的全局变量、字符串常量等。

4. 代码区(.text):存放程序执行代码

内存的分配方式有几种?
  1. 静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。
  2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  3. 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
C++中的局部变量、全局变量、局部静态变量、全局静态变量的区别?
定义位置作用域生命周期存储位置
全局变量在所有函数外部定义所有源文件整个程序运行期间静态存储区,固定分配
局部变量在函数体内部中定义定义该变量的函数体内函数调用至返回前栈区
全局静态变量在所有函数外部定义只在定义的文件中被访问整个程序运行期间静态存储区,固定分配
局部静态变量在函数体内部中定义,只初始化一次定义该变量的函数体内整个程序运行期间静态存储区,固定分配
对一个空类型计算sizeof值应该是多少?为什么?若空类中增加一个构造和析构函数,则sizeof为多少?

没有任何成员变量和成员函数的空类,求 sizeof 的结果是1。原因是,虽然空类型的实例中不包含任何信息,但是声明该类型的实例时,它必须在内存中占有一定的空间,否则无法使用这个实例。其占用内存大小由编译器决定。 其sizeof仍为1,因为调用构造和析构函数,只需知道地址既可。 若在空类型中把析构函数标记为虚函数,编译器一旦发现有虚函数,就会为该类型生成虚函数表,并在该类型的实例中添加一个指向虚函数的指针,此时sizeof的结果是4或8。

程序运行时,类实例函数如何与实例对象进行绑定的?

使用函数调用约定_this_call。C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数*this,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动将对象本身的地址作为类成员函数的第一个隐含参数传递给函数。

C++函数在传参过程中参数传递的方式有哪些?

值传递:把参数的实际值赋值给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。

指针传递(*):把参数的地址赋值给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。

引用传递(&):把参数引用的地址复制给形式参数。。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。

C++函数默认使用传值调用方式来传递参数。

指针和引用的区别
  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(,具体情况还要具体分析)。
  • 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
  • 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
  • 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
解释浅拷贝和深拷贝及其区别

浅拷贝:在浅拷贝过程中,原始对象和拷贝对象共享相同的资源。如果资源是动态分配的内存,那么两个对象将指向同一块内存。如果字段是基本数据类型,那么对该字段执行逐位拷贝。如果字段是引用类型 (例如指针或引用),则拷贝引用但不拷贝引用的对象。因此,原始对象及其副本引用同一个对象。

深拷贝:深拷贝创建了原始对象的一个完全独立的副本,拷贝对象拥有自己的资源副本,不与原始对象共享。深拷贝不仅复制对象的成员变量,还复制指针指向的内存块的内容。如果对象包含动态分配的内存,深拷贝会分配新的内存并复制内容。

区别:

  • 资源管理:浅拷贝不会复制指针指向的资源,而深拷贝会。
  • 内存分配:浅拷贝不分配新的内存给拷贝对象,深拷贝会为拷贝对象分配新的内存。
  • 独立性:浅拷贝的对象依赖于原始对象的资源,深拷贝的对象是独立的。
  • 风险:浅拷贝可能引起资源管理错误,如多次释放同一内存,深拷贝则避免了这个问题。
C++左值是什么?

左值:具有确定内存地址和名称的表达式,通常出现在赋值操作符左侧,也可在右侧。左值可作为函数调用的参数,即可“存储”或“绑定”到内存位置的表达式,它有一个相对稳定的内存地址,且有一段较长的生命周期。

C++右值是什么?

右值:不具有确定内存地址的表达式,通常出现在赋值操作的右侧。右值通常表示临时值或字面量,不能被赋值,不能出现在赋值表达式左侧,其生命周期很短,一般是暂时性的,表达式的结果、函数返回值也是右值。

左值引用、右值引用是什么?左值引用和右值引用的区别?

左值引用是绑定到左值的引用。左值引用必须被初始化为左值,并且可以用于修改它所引用的左值的值。左值引用的声明使用单个 & 符号。

右值引用是绑定到右值的引用。右值引用的声明使用双写的 && 符号。右值引用的主要用例是移动语义和完美转发,它们允许程序避免不必要的拷贝,特别是在资源转移或函数参数传递时。

什么是移动语义?

移动语义旨在优化资源的转移,特别是在对象的复制操作中。移动语义的核心思想是允许一个对象的资源(如动态分配的内存、文件句柄等)被"移动"到另一个对象中,而不是进行复制。这可以减少不必要的资源复制,提高程序的效率。

什么是完美转发?

完美转发允许模板函数在转发参数时保留参数的原始值类别(左值或右值)。这意味着模板可以接受任意类型的参数,并将它们以原始的值类别传递给其他函数。完美转发通常与右值引用和 std::forward 一起使用。在C++中,模板函数经常用于编写通用的代码,这些代码可以处理不同类型的参数。然而,在C++11之前,模板函数在转发参数时无法保留参数的值类别。这可能导致一些问题,特别是在涉及到移动语义时。完美转发解决了这个问题,允许模板函数无损地转发参数。

什么是智能指针?

C++11 中引入了智能指针(Smart Pointer),它利用了一种叫做 RAII(资源获取即初始化)的技术将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。这使得智能指针实质是一个对象,行为表现的却像一个指针。

智能指针主要解决以下问题: (1)内存泄漏:内存手动释放,使用智能指针可以自动释放。 (2)共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题。

C++11的三个智能指针类型:share_ptr、unique_ptr、weak_ptr。

它们的特点: (1)unique_ptr独占对象的所有权,由于没有引用计数,性能较好。 (2)share_ptr共享对象的所有权,但性能略差。 (3)weak_ptr配合share_ptr,解决循环引用问题。

常见的函数调用约定有哪些?分别有什么区别?
现代C++特性演变