C++面试知识点总结

知识点总结

  1. <<符号表示该语句将把这个字符串发送给cout;该符号指出了信息流动的路径;cout的对象属性包括一个插入运算符(<<),它可以将其右侧的信息插入到流中
  2. endl:重起一行。在输出流中插入endl将导致光标移动到下一行开头,它确保程序继续运行前刷新输出(将其立即显示在屏幕上)
  3. 空格、制表符、回车统称为空白,
  4. volatile 可以保证对特殊地址的稳定访问
  5. 抽象和类。类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法合成一个整洁的包。一般类规范由两个部分组成(类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述共有接口;类定义:描述如何实现类成员函数)
  • 公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口
  • 数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的
  • 实现类成员函数(有两个特殊的特征):1.定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类。2.类方法可以访问类的private组件
  • 定义位于类声明中的函数都会自动成为内联函数;也可以在类声明之外使用inline限定符定义成员函数,使其成为内联函数
  • 调用成员函数时,它将使用被用来调用它的对象的数据成员
  • OOP程序员常依照客户/服务器模型来讨论程序设计(客户只能通过以公有方式定义的接口使用服务器,即客户唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行,服务器设计人员只能修改类设计的实现细节,不能修改接口)
  • 指定类设计的第一步是提供类声明,第二步是实现类成员函数

典型的类声明格式

class className

{

private:

data member declarations

public:

member function prototypes

};

    公有部分的内容构成了设计的抽象部分——共有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。C++通过类使得实现抽象、数据隐藏和封装等OOP特性很容易

  1. 类的构造函数和析构函数
  • C++的目标之一是让使用类对象就像使用标准类型一样,但是由于数据部分的访问状态是私有的,这意味着程序不能直接数据成员,只能通过成员函数来访问数据成员,因此需要设计合适的成员函数。C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员;构造函数的名称与类名相同,没有返回值,没有返回类型,也没被声明为void类型。程序声明对象时,将自动调用构造函数;(注意)***构造函数的参数表示的不是类成员,而是赋给类成员的值***
  • 通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(参数列表)都不同。通常构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配
  • 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值
  • 使用构造函数。第一种方法是显式地调用构造函数,第二种是隐式地。构造函数被用来创建对象,而不能通过对象来调用
  • 默认构造函数。是在未提供显式初始值时,用来创建对象的构造函数,(也就是说,如果没有提供任何构造函数,则C++将自动提供默认构造函数)
  • 析构函数。用构造函数创建对象后,程序负责跟踪该对象,,直到其过期为止,对象过期时,程序将自动调用一个特殊的成员函数——析构函数,完成清理工作。析构函数的名字:在类名前加上~;析构函数也可以没有返回值和声明类型,并且析构函数没有参数。如果构造函数使用了new,则必须提供使用delete的析构函数
  • 列表初始化。
  • const成员函数。保证函数不会修改调用对象,将const关键字放在函数的括号后面。只要类方法不修改调用对象,应尽可能将其声明为const。
  1. this指针。this指针指向用来调用成员函数的对象(this被用作为隐藏参数换递给方法),简单来说就是谁(哪个类对象)调用成员函数,this指针就指向谁。每个成员函数(包括构造函数和析构函数)都有一个this指针,(this是对象的地址,而不是对象本身);
  2. 类是用户定义的类型,对象是类的实例,每个对象都存储自己的数据,而共享类方法。
  3. 运算符重载。运算符重载是一种形式的C++多态(前面介绍了C++是如何使用户能够定义多个名称相同但特征标不同的函数——称之为函数重载或函数多态)。重载运算符格式:operatorop(argument-list) ,op必须是有效的C++运算符(例如operator+(),重载+运算符)
  4. 不要返回指向局部变量或临时对象的引用,在函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据
  • 重载的运算符不必是成员函数,但必须至少有一个操作数是用户定义的类型,防止用户为标准类型重载运算符
  • 使用运算符时不能违反运算符原来的句法规则,不能修改运算符的优先级
  • 大多数运算符都可以通过成员或非成员函数进行重载,但是=:赋值运算符,():函数调用运算符,[ ]:下标运算符,->:通过指针访问类成员的运算符这四种只能通过成员函数进行重载。
  1. 友元。C++控制对类对象私有部分的访问,通常共有类方法提供唯一的访问途径(过于严格),因此C++提供了另外一种形式的访问权限:友元(共有3种:友元函数;友元类;友元成员函数
  • 创建友元的第一步是将其原型放在类声明中,并在其原型声明中加上关键字friend这意味着(1.虽然它是在类声明中声名的,但它不是成员函数,因此不能使用成员运算符来调用2.虽然它不是成员函数,但是它与成员函数的访问权限相同)
  • 第二步是编写函数定义。因为它不是成员函数,所以不要使用类作用域限定符,不要在定义中使用关键字friend。只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据
  • 友元是否有悖于OOP?不对,应该将友元函数看作类的扩展接口的组成部分,类方法和友元只是表达类接口的两种不同机制。
  1. 只接受一个参数的构造函数定义了从参数类型到类类型的转换,如果使用explixit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换,;转换函数(从类类型转换到某种类型)转换函数形式:operator typeName(); typeName指出了要转换成的类型{1.转换函数必须是类方法,2.转换函数不能指定返回类型,3.转换函数不能有参数}
  2. 类继承。从已有的类派生出新的类,并且派生类继承了原有类(基类)的特征,包括方法;
  • 公有派生:派生类对象包含基类对象,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的共有和保护方法访问;(派生类对象存储了基类的数据成员(派生类继承了基类的实现);派生类对象可以使用基类的方法(派生类继承了基类的接口))
  • 派生类需要有自己的构造函数(构造函数必须给新成员和继承的成员提供数据);根据需要添加额外的数据成员和成员函数;
  • 派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问(具体地说,派生类构造函数必须使用基类构造函数)
  • 创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数,可以使用初始化列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数;派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数
  • 派生类对象可以使用基类的方法,条件是方法不是私有的;另外,基类指针可以在不进行显式类型转换的情况下指向派生类对象,基类引用可以在不进行显示类型转换的情况下引用派生类对象。但是基类指针或引用只能用于调用基类方法。不能用来调用派生类方法,
  1. C++有3种继承方式:公有继承、保护继承和私有继承(公有继承是一种is-a关系,即派生类对象也是一个基类对象,对基类对象执行的任何操作也可以对派生类对象执行)
  2. 希望同一个方法在派生类和基类中的行为是不同的(换句话说,方法的行为应取决于调用该对象的方法),这种行为叫多态——具有多种形态,即同一个方法的行为随上下文而异,有两种方法可以实现(1.在派生类中重新定义基类的方法;2.使用虚方法
  • 使用virtual来定义虚函数,(只在声明中出现,实现中不会出现)如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
  • 方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法;
  • 为什么需要使用虚析构函数?基类声明一个虚析构函数,是为了确保释放派生对象时,按正确的顺序调用析构函数
  1. 静态联编和动态联编。将源代码中的函数调用解释为特定执行的代码块被称为函数名联编,C/C++编译器可以在编译过程中完成这种联编,称之为静态联编。虚函数使得这项工作变得困难,使用哪一个函数不能在编译时确定,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编。(编译器对虚方法使用动态联编
  • 为什么有两种类型的联编以及为什么默认为静态联编?静态联编的效率更高,因此被设置为默认(如果类不用做基类,则不需要动态联编,如果派生类不重新定义基类的任何方法,也不需要使用动态联编);指出不需要重定义该函数(仅将那些预期将被重定义的方法声明为虚的)
  • 虚函数的工作原理?C++规定了虚函数的行为,但将实现方法留给了编译器作者。编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针,——这种数组被称为虚函数表,虚函数表中存储了为类对象进行声明的虚函数的地址。
  • 构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数,因此派生类不继承基类的构造函数,所以将类构造函数声明为虚的无意义
  • 析构函数应当是虚函数除非类不用做基类
  • 友元不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数
  • Protected,对于外部世界而言,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
  1. 抽象基类(abstract base class, ABC),C++通过纯虚函数提供未实现的函数,纯虚函数声名的结尾处为=0 。当声明中包含纯虚函数时,则不能创建该类的对象(包含纯虚函数的类只用作基类),要成为真正的ABC,必须至少包含一个纯虚函数。。原型中的=0使虚函数成为纯虚函数。
  2. 类设计回顾
  • 默认构造函数。默认构造函数要么没有参数,要么所有的参数都有默认值,如果没有定义任何构造函数,编译器将定义默认构造函数;如果派生类构造函数的成员初始化列表中没有显式调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。提供构造函数的动机之一是确保对象总能被正确地初始化。
  • 复制构造函数。复制构造函数接受其所属类的对象作为参数(使用复制构造函数的情形:1.将新对象初始化为一个同类对象。2.按值将对象传递给函数。3.函数按值返回对象4.编译器生成临时对象),如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义
  • 赋值运算符。默认的赋值运算符用于处理同类对象之间的赋值,(不要把赋值和初始化混淆),如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值
  • 构造函数。构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用,这是构造函数不被继承的原因之一。继承意味着派生类对象可以使用基类的方法,但是构造函数在完成其工作之前对象并不存在
  • 析构函数。一定要定义显式析构函数来释放类构造函数使用new分配的所有内存。对于基类,即使它不需要析构函数,也应提供一个虚析构函数,这样,当通过指向对象的基类指针或引用来删除派生对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。
  • 转换。使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换;将类对象转化为其他类型,应定义转换函数,explicit用于转换函数,允许使用强制类型转换进行转换,但不允许隐式转换
  • 按值传递对象与传递引用。通常编写使用对象作为参数的函数,应按引用而不是按值来传递对象。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数;
  • 返回对象与返回引用。两者在外型的唯一区别是函数原型和函数头。返回引用可节省时间和内存,函数不能返回在函数中创建的临时对象的引用,当函数结束时,临时对象将消失。
  • 虚方法。设计虚类时,必须明确是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用动态联编;反之就不必将其声明为虚的。
  • 友元函数。友元函数并非类成员,因此不能继承。
  1. 面向对象的三大特性:封装、继承、多态
  • 封装:就是把客观事物封装成抽象的类,可以使某个属性只能被当前类使用,从而避免被其他类或对象进行操作——保证了安全性。也可以让使用者不必了解具体类的内部实现细节,而只需要通过提供给外部的访问接口来访问类中的属性和方法——简化编程。
  • 封装的意义
  • 保护或防止代码(数据)被无意破坏
  • 保护成员属性,不让类以外的程序直接访问和修改
  • 隐藏方法细节,简化编程
  • 继承:指的是可以让某个类型的对象获得另一个类型的对象的属性的方法,继承可以使得子类沿用父类的成员(属性和方法),而无需重新编写原来的类并且可以对父类的成员(属性和方法)进行扩展。通过继承创建的新类称为“子类“或”派生类“,被继承的类称为”基类“、”父类“,继承提高了代码的复用性和维护性。
  • 子类可以继承父类非私有成员
  • 子类可以有自己特有的成员,并不会把父类的成员赋值给子类,而去引用
  • 子类可以重写父类的方法,而重新定义了父类中的方法,叫做重写
  • 子类可以在父类提供方法的基础上,额外新增一些功能
  • 子类无法继承父类的构造方法
  • 子类不能继承父类中不符合访问权限的成员
  • 多态:接口的多种不同的实现方式即为多态——接口重用!多态是以继承和封装为基础的,一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。多态分为静态多态和动态多态,静态多态是通过重载和模板技术实现的,在编译期间确定;动态多态是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。
  1. 类的访问权限:private、protected、public
  • 类的一个特征就是封装,public和private作用就是实现这一目的;用户代码(类外)可以访问public成员而不能访问private成员,private成员只能由类成员(类内)和友元访问
  • 类的另一个特征就是继承,protected的作用就是实现这一目的;protected成员可以被派生类对象访问,不能被用户代码(类外)访问
  • 不管是否继承,前面两个规则都适用!有public、private、protected三种继承方式,它们相应地改变了基类成员的访问属性。
  • Public继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:public、protected、private
  • Private继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:private、private、private
  • Protected继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:protected、protected、private
  1. 内存分区:全局区、堆区、栈区、代码区
  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等
  • 堆区(heap):一般由程序员分配释放(动态内存申请与释放),若程序员不释放,程序结束时可能由操作系统回收
  • 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,该区域在程序结束后由操作系统释放
  • 常量区:字符串常量和其他常量的存储位置,程序结束后由操作系统释放
  • 程序代码区:存放函数体的二进制代码
  1. C++和C的区别
  • C++是面向对象的语言,而C是面向过程的语言,C++语言中有类和对象以及继承多态这样的OOP语言必备的内容,此外C++支持模板,运算符重载,异常处理机制以及非常强大的C++标准模板库
  • 两者的一个典型区别还有动态内存分配,C++引入new/delete运算符,取代了C中的malloc/free库函数
  • C++引入类、引用、函数重载的概念,C没有
  1. 堆和栈的区别:
  • 分配和管理方式不同:堆是动态分配的,其空间的分配和释放都由程序员控制;栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成(如局部变量的分配);动态分配由alloca()函数进行分配,但由编译器释放
  • 产生碎片不同:对堆来说,频繁使用new/delete或者malloc/free会造成内存空间的不连续,使程序效率低;对栈来说,不存在碎片问题,因为栈具有先进后出的特性
  • 生长方向不同:堆是从内存的低地址向高地址方向增长;栈与之相反
  • 申请大小限制不同:栈顶和栈底是预设好的,大小固定;堆是不连续的内存区域,大小可以调整
  1. malloc/free和new/delete的区别
  • malloc和new都是在堆上开辟内存的;malloc只负责开辟内存,没有初始化功能,new不但开辟内存,还可以进行初始化
  • malloc是函数,开辟内存需要传入字节数【如malloc(100)表示在堆上开辟了100个字节的内存,返回void*,表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址】,new是运算符,开辟内存需要指定类型,因此不需要进行强转
  • malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败,new底层调用也是malloc来开辟内存的,new比malloc多的就是初始化功能
  • malloc开辟的内存永远通过free来释放,而new单个元素内存,用的是delete,如果new[ ]数组,用的是delete[ ]来释放内存。
  • memcpy,realloc函数绝对不能在C++中使用,因为这些函数进行的都是内存值拷贝(也就是对象的浅拷贝),会发生浅拷贝这个问题!
  • 前面也讲过new的底层也是通过malloc来开辟内存的,new比malloc多一项功能,就是开辟完内存还可以进行初始化操作
  • delete比free多一项功能就是在释放内存前,还可以析构指针指向的对象,new和delete配对使用,new[ ]和delete[ ]配对使用
  • new和delete不仅仅是运算符,它们实际上是运算符重载函数的调用,对应的函数名是operator new和operator delete
  • 当使用自定义类类型,而且提供了析构函数的时候,那么new和delete千万不能混用,会导致对象的析构和内存释放有问题;除此之外,new和delete从逻辑上来说,是可以混用的,但是最好不要这样做
  1. 指针和引用的区别
  • 相同点就是,两者都是地址的概念;指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名。
  • 指针是一个变量,存储的是一个地址,指向内存的一个存储单元;引用是原变量的一个别名,跟原来的变量实质上是同一个东西。
  • 指针可以有多级,引用只能有一级
  • 指针可以在定义的时候不初始化,引用必须在定义的时候初始化
  • 指针可以指向NULL,引用不可以为NULL
  • 指针初始化之后可以再改变,引用不可以
  • sizeof的运算结果不同,引用得到的是所指向的变量(对象)的大小,指针得到的是指针本身的大小,sizeof其作用就是返回一个对象或者类型所占的内存字节数
  • 自增运算意义不同
  • 可以有const指针,但是没有const引用
  1. 浅拷贝和深拷贝有什么区别?
  • 浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间;深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经过深拷贝后的指针是指向两个不同地址的指针
  1. 静态内存分配和动态内存分配有什么区别?
  • 内存的静态分配和动态分配的区别主要是两个:一是时间不同。静态分配发生在程序编译和连接的时候,动态分配则发生在程序调用和执行的时候;二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由malloc进行分配。不过栈的动态分配和堆不同,它的动态分配是由编译器释放,无需程序员手工实现。
  • 对于一个进程的内存空间而言,可以在逻辑上分成3部分:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈“,”栈“和”堆“是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构,进程的每个线程都有私有的”栈“,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰
  • 静态内存分配是在编译时完成的,不需要占用CPU资源;动态内存分配是在运行时完成的,动态内存的分配与释放需要占用CPU资源
  • 静态内存分配是在栈上分配的,动态内存是在堆上分配的
  • 动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要
  • 静态分配内存需要在编译前确定内存块的大小,而动态分配内存不需要编译前确定内存大小,根据运行时环境确定需要的内存块大小,按照需要分配内存即可(静态内存分配是按计划分配,而动态内存分配是按需分配)
  1. C++函数中值的传递方式有哪几种?

值传递、指针传递和引用传递

  • 传值调用:向函数传递参数的值,即把参数的值复制给函数的形式参数,此时修改函数内的形式参数,并不会影响到函数外的实际参数
  • 向函数传递参数的指针,即把参数的地址复制给形式参数,在函数内,该地址用于访问要用到的实际参数,这意味着修改形式参数会影响实际参数
  • 引用调用:向函数传递参数的引用,即把引用的地址复制给形式参数,该引用用于访问要用到的实际参数,修改形式参数会影响实际参数。传递引用就是传递实参本身(引用传递适用于变量中含有的数据很大)
  1. list和vector区别:
  • list是由双向链表实现的,内存空间是不连续的。优点:插入和删除效率较高,只需要在插入的地方更改指针的指向即可,不用移动数据;缺点:查询效率较低,时间复杂度为O(n)
  • vector拥有一段连续的内存空间,并且起始地址不变,与数组类似。优点:便于随机访问,时间复杂度为O(1),缺点:因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)
  1. 覆盖和重载有什么区别
  • 覆盖是指派生类中重新定义的函数,其函数名、参数列表、返回类型与父类完全相同,只是函数体存在区别;覆盖只发生在类的成员函数中;
  • 重载是指两个函数具有相同的函数名,不同的参数列表,不关心返回值;当调用函数时,根据传递的参数列表来判断调用哪个函数;重载可以是类的成员函数,也可以是普通函数。
  1. delete和delete[ ]区别
  • delete只会调用一次析构函数,而delete[ ]会调用每一个成员的析构函数
  • 需要考虑分两种情况:基本数据类型和自定义数据类型。基本的数据类型对象没有析构函数,因此使用delete和delete[ ]两种都可以,对于内建简单数据类型,两者的功能是相同的;自定义数据类型,假设通过new申请一个对象数组,返回一个指针,对于此对象数组的内存释放需要做两件事:一是释放最初申请的那部分空间,二是调用析构函数完成清理工作(对于内存空间的清理,由于申请时记录了其大小,因此无论使用delete还是delete[ ]都能将这片空间完整释放,而问题就出在析构函数的调用上,当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete[ ]的话,将会逐个调用析构函数)这里需要注意的是,既然不加方括号也能完整释放内存,只是没有多调用几个析构函数,好像没有多大问题?但是加入析构函数需要释放系统资源(如文件、线程、端口),这些东西使用了而不释放将造成严重的后果,会造成潜在的危险。所以new的时候用了[ ],delete的时候也需要使用[ ] !!!
  1. 子类析构要调用父类的析构函数吗?
  • 析构函数调用的次序是先派生类的析构函数后基类的析构函数,即基类的析构函数调用的时候,派生类的信息已经全部销毁了;
  • 定义一个对象时先调用基类的构造函数,然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数,然后调用基类的析构函数
  1. 多态、虚函数、纯虚函数
  • 多态:是对于不同对象接受相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;在程序编译时多态性体现在函数和运算符的重载上;
  • 虚函数:在基类中冠以关键字virtual的成员函数,它允许在派生类中对基类的虚函数重新定义
  • 纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在,纯虚函数不具备函数的功能。丛基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类,抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例
  1. 什么是“引用“?声明和使用”引用“要注意哪些问题?
  • 引用就是某个目标变量的别名,对应用的操作与对变量直接操作效果完全相同
  • 声明一个引用的时候,一定要对其进行初始化。引用声明完毕之后,相当于目标变量有两个名称(目标原名称和引用名),不能再把该引用名作为其他变量名的别名。
  • 声明一个引用,不是新定义了一个变量,它只是表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
  1. 将“引用“作为函数参数有哪些特点?
  • 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作
  • 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本
  • 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元
  1. 在什么时候需要使用“常引用”?

既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用

  1. 结构体与联合体有何区别?两者最大的区别在于内存利用
  • 结构体struct,各成员各自拥有自己的内存,各自使用互不干涉,遵循内存对齐原则,一个struct变量的总长度等于所有成员的长度之和;联合体union各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权,各变量共用一个内存首地址,一个union变量的总长度至少能容纳最大的成员变量,而且要满足是所有成员变量类型大小的整数倍
  • 结构体和联合体都是由不同的数据类型成员组成,但在任何同一时刻,联合中只存放了一个被选中的成员(所有成员共用一块地址空间),而结构的所有成员都存在(不同成员的存放地址不同)
  • 对于联合体的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于结构的不同成员赋值是互不影响的
  1. 重载(overload)和重写(overwrited,覆盖)的区别?
  • 重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)
  • 重写:是指子类重新定义父类虚函数的方法
  • 从实现原理上来说,重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的);重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的
  1. main函数执行以前,还会执行什么代码?
    • 静态对象、全局对象的构造函数
    • 一些全局变量、对象和静态变量、对象的空间分配和赋初值
    • 运行时库的初始化
  2. 数组和指针的区别?
  • 数组是用于存储多个相同类型数据的集合;指针相当于一个变量,但是它存放的是其他变量在内存中的地址
  • 赋值:同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝
  • 数组在内存中是连续存放的(多维数组在内存中是按照一维数组存储的,只是在逻辑上是多维的),指针的存储空间不能确定
  • 求sizeof: sizeof(数组名),数组名表示整个数组,计算的是整个数组的大小,单位是字节;在32位平台下,无论指针是什么类型,sizeof(指针名)都是4,在64位平台下,无论指针是什么类型,sizeof(指针名)都是8
  • 用运算符sizeof可以计算出数组的容量(字节数),sizeof(p),p为指针得到的是一个指针变量的字节数,而不是p所指的内存容量;C/C++语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
  • 指针数组和数组指针:
  • 指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素(比如int* arr[8],arr是一个含有8个int*的数组)
  • 数组指针:它实际上是一个指针,该指针指向一个数组(比如int(arr*)[8],指针arr指向一个大小为8个整型的数组)
  1. const与#define相比,有何优点?
  • 就起作用的阶段而言:#define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用;就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。const有对应的数据类型,是要进行判断的;就存储方式而言:#define只是进行展开,有多少地方使用就替换多少次,它定义的宏常量在内存中有若干个备份,const定义的只读变量在程序运行中只有一份备份;从代码调试的方便程度而言:const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了
  • const作用:定义常量、修饰函数参数、修饰函数返回值,被const修饰的东西都受到强制保护;
  • const常量有数据类型,编译器可对其进行安全检查
  • const可节省空间,避免不必要的内存分配
  1. 引用和指针有什么区别?
  • 引用必须被初始化,指针不必
  • 引用初始化后不能被改变,指针可以改变所指的对象
  • 不存在指向空值的引用,但是存在指向空值的指针
  1. 基类的析构函数不是虚函数,会带来什么问题?
  • 基类的构造函数不是虚函数的话,删除指针时,只有基类的内存被释放,派生类的没有,这样就内存泄漏了
  • 当基类指针指向派生类的时候,如果析构函数不声明为虚函数,在析构的时候,就不会调用派生类的析构函数,从而导致内存泄漏
  1. 全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
  • 生命周期不同:全局变量随主程序创建而创建,随主程序销毁而销毁;全局变量的作用域是这个程序块,而局部变量作用于当前函数;局部变量在局部函数内部,甚至局部循环体内部存在,退出就不存在
  • 使用方式不同:通过声明后全局变量程序的各个部分都可以用到,局部变量只能在局部使用
  • 操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段,并且在程序开始运行时被加载,局部变量则被分配在栈区;如果是全局变量的话,编译器在将源代码翻译成二进制代码时就为全局变量分配好一个虚拟地址,所以程序在对全局变量的操作时是对一个硬编码的地址操作;局部变量在编译时不分配空间,在局部变量所在的函数被调用时才真正分配。所以操作系统通过变量的分配地址就可以判断出是局部变量和全局变量

C++11新特性

  1. 原始字符串字面量的定义为:R “xxx(raw string)xxx”,原始字符串必须使用()括起来,括号前后可以加其他字符串,所加的字符串会被忽略,并且要求加的字符串必须在括号两边同时出现
  2. Long long 类型。C++标准要求long long 整型可以在不同平台上有不同的长度,但至少有64位。它占的字节数越多,对应能够存储的数值也就越大
  3. 扩展的整型。C++11只定义了以下5种标准的有符号整型:signed char、short int、int、long int、long long int;但是每一种有符号整型都有一种对应的无符号整数版本,且有符号整型与其对应的无符号整型具有相同的存储空间大小
  4. 自动类型推导。使用auto关键字可以自动推导出变量的实际类型,auto并不代表一种实际的数据类型,只是一个类型声明的“占位符”,(使用auto声名的变量必须进行初始化,以让编译器推导出它的实际类型);当变量是指针或引用类型时,推导的结果中会保留const、volatile关键字;

1.auto关键字不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须给修饰的变量赋值,因此二者矛盾

2.不能用于类的非静态成员变量的初始化

3.不能使用auto关键字定义数组

4.无法使用auto推导出模板参数

可以用于STL遍历容器比较方便

  1. 某些情况下,不需要或不能定义变量,但是希望得到某种类型,就可以使用C++11提供的decltype(declare type)关键字,它的作用是在编译器编译的时候推导出一个表达式的类型,语法: decltype(表达式)。它只是用于表达式类型的推导,并不会计算表达式的值

1.表达式为普通变量或者普通表达式或类表达式,在这种情况下,使用decltype推导出的类型和表达式的类型是一致的

2.表达式是函数调用,使用decltype推导出的类型和函数返回值一致(对于纯右值【在表达式结束后不再存在的数据,也就是临时性数据】而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略这两个限定符)

3.表达式是一个左值,或者被括号()包围,使用decltype推导出的是表达式类型的引用(如果有const、volatile限定符不能忽略)

  1. for循环新语法。在基于范围的for循环中,不需要再传递容器的两端,循环会自动以容器为范围展开,并且循环中也屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算for(declaration : expression) { }。declaration 表示遍历声明,expression是要遍历的对象,可以是表达式、容器、数组、初始化列表等;如果需要在遍历过程中修改元素的值,则需要使用引用。对应基于范围的for循环来说,冒号后边的表达式只会被执行一次。在得到遍历对象之后会先确定好迭代的范围,基于这个范围直接进行遍历。
  2. 指针空值类型——nullptr。一般会在定义指针的同时完成初始化操作,或在指针尚未明确时初始化为NULL,避免产生野指针(没有明确指向的指针,操作这种野指针极有可能导致程序发生异常),C++98/03中将一个指针初始化为空指针的方式有两种:0,NULL。但是在C++中NULL和0是等价的(这样导致函数重载的时候无法区分)。因此C++11引入一个新的关键字nullptr,专门用来初始化空类型指针,不同类型的指针变量都可以使用nullptr来初始化。nullptr无法隐式转换为整型,但是可以隐式匹配指针类型。
  3. lambda表达式。就地匿名定义目标函数或函数对象,不需要额外写一个命名函数或函数对象,避免了代码膨胀和功能分散;

语法形式:

[capture](params) opt->ret {body;};

capture是捕获列表[],捕获一定范围内的变量

params是参数列表(),和普通的参数列表一样,如果没有参数列表可以省略不写

opt是函数选项,不需要可以省略(mutable:可以修改按值传递进来的拷贝;exceprion:指定函数抛出的异常)

ret是返回值类型,lambda表达式的返回值是通过返回值后置语法来定义的

body是函数体,函数的实现,这部分不能省略,但函数体可以为空

  1. lambda表达式的捕获列表可以捕获一定范围内的变量,具体使用方式如下:

  • [] - 不捕捉任何变量
  • [&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
  • [=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
  • 拷贝的副本在匿名函数体内部是只读的
  • [=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
  • [bar] - 按值捕获 bar 变量, 同时不捕获其他变量
  • [&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
  • [this] - 捕获当前类中的this指针
  • 让lambda表达式拥有和当前类成员函数同样的访问权限
  • 如果已经使用了 & 或者 =, 默认添加此选项

在匿名函数内部,需要通过lambda表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下lambda表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。

一般情况下,不指定lambda表达式的返回值,编译器会根据return语句自动推导返回值的类型,但需要注意的是labmda表达式不能通过列表初始化自动推导出返回值类型。

使用lambda表达式捕获列表捕获外部变量,如果希望去修改按值捕获的外部变量,那么应该如何处理呢?这就需要使用mutable选项,被mutable修改时lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性。mutable选项的作用就在于取消operator()的const属性。

  1. 常量表达式修饰符——constexpr。C++11之前只有const关键字(变量只读,修饰常量【变量只读并不等价于常量】)。constexpr这个关键字是用来修饰常量表达式的,(常量表达式就是由多个≥1常量组成并且在编译过程中就得到计算结果的表达式)
  • 常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式的计算往往发生在程序的编译阶段,可以极大地提高执行效率,表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
  • 建议凡是表达“只读”语义的场景都使用const,表达“常量”语义的场景都使用constexpr
  • 对C++内置类型的数据,可以直接用constexpr修饰,但如果是自定义的数据类型(struct或class实现),直接使用constexpr修饰是不行的
  1. 常量表达式函数。constexpr并不能修改任意函数的返回值,使这些函数成为常量表达式函数,需要满足:1.函数必须要有返回值,并且return返回的表达式必须是常量表达式2.函数在使用之前,必须有对应的定义语句3.整个函数的函数体中,不能出现非常量表达式之外的语句(using指令、typedef语句以及static_assert断言、return语句除外)
  2. 右值引用。C++11新增了一个新的类型,标记为&&。{lvalue是loactor value,rvalue是 read value的缩写。左值是存储在内存中、有明确存储地址(可取地址)的数据,右值是可以提供数据值的数据(不可取地址),区分左值与右值的方法是:可以对表达式取地址就是左值,否则为右值}。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
  3. 转移和完美转发。C++11添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助std::move()函数,该函数可以将左值转换为右值。这个函数并不能移动任何东西,而是将对象的状态或所有权从一个对象转移到另一个对象,只是转移,并没有内存拷贝。一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了,如果需要按照参数原来的类型转发到另一个函数,可以使用std::forward()函数,该函数实现的功能称之为完美转发
  4. 列表初始化。
  • using的使用。可以通过typedef重定义一个类型【typedef 旧的类型名 新的类型名】,被重定义的类型并不是一个新的类型,仅是原有的类型取了一个新的名字。也可以使用using 【using 新的类型=旧的类型
1.为什么要使用智能指针?

智能指针其作用是管理一个指针,避免程序员申请的空间在函数结束时忘记释放而造成内存泄漏的情况发生。智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间

  • auto_ptr(C++98的方案,C++11已抛弃)采用所有权模式
    auto_ptrp1(new string("hello"));
    auto_ptrp2;
    p2=p1;

    此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

  • unique_ptr(替换auto_ptr)

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象,对于避免资源泄露特别有用

unique_ptr p3(new string("hello"));
unique_ptr p4;
p4=p3;

 此时会报错,编译器认为p4=p3非法,避免了p3不再指向有效数据的问题

因此,unique_ptr比auto_ptr更安全

  • shared_ptr(共享型,强引用)

 shared_ptr实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时释放。从名字share就可以看出来资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享

可以通过成员函数use_count()来查看资源的所有者个数,除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造,当调用release()时,当前指针会释放资源所有权,计数减1.当计数等于0时,资源会被释放

shared_ptr是为了解决auto_ptr在对象所有权的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

  • weak_ptr(弱引用)

 weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。

weak_ptr只是提供了对管理对象的一个访问手段,是用来解决shared_ptr相互引用时的死锁问题。如果两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它。

2.C++内存分配问题

栈:由编译器管理分配和回收,存放局部变量和函数参数

堆:由程序员管理,需要手动new malloc delete free进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况

全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量

常量存储区:存储常量,一般不允许修改

代码区:存放程序的二进制代码

3.C++中的指针参数传递和引用参数传递 

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

引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都会影响主调函数中的实参变量

引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量;而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或指针引用

4.const和static关键字

static作用:控制变量的存储方式和可见性。 

作用一:修饰局部变量。一般情况下,局部变量在程序中是存放在栈区,并且局部变量的生命周期在包含语句块执行结束时便结束了。但是如果使用static关键字会存放在静态数据区,其生命周期会一直延续到整个程序执行结束。(虽然static对局部变量进行修饰之后,其生命周期以及存储空间发生了变化,但其作用域并没有改变,还是限制在其语句块)

作用二:修饰全部变量。对一个全局变量,它既可以在本文件中被访问到,也可以在同一个工程中其他源文件被访问(添加extern进行声明即可)。用static对全局变量进行修饰改变了其作用域范围,由原来的整个工程可见变成了本文件可见

作用三:修饰函数。用static修饰函数,情况和修饰全局变量类似,也是改变了函数的作用域。

作用四:修饰类。对类中的某个函数用static修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,则表示该变量以及所有的对象所有,存储空间中只存在一个副本,可以通过类和对象去调用。

const关键字

5.C++是怎样定义常量的?常量存放在内存的哪个位置?

对于局部变量存放在栈区;对于全局常量,编译器一般不分配内存,放在符号表中以提高访问效率;字面值常量放在常量区;

6.C++中重载、重写、重定义的区别?

 重载是指同一可访问区内被声明的几个具有不同参数列表的同名函数,可以是参数类型,个数,顺序的不同,根据参数列表决定调用哪个函数;

重写,派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是static的,一定要是虚函数,且其他一定要完全相同;

重定义,派生类重新定义父类中相同名字的非virtual函数,参数列表和返回类型都可以不同,即父类中定义成virtual且完全相同的同名函数才不会被派生类中的同名函数所隐藏(重定义)。

你可能感兴趣的:(C++,c++,开发语言)