一、C++与C语言的联系
c语言是面向过程的结构化语言,c++是面向对象的的程序设计语言,在c语言的基础上进行了扩充和完善,并且c++兼容了c语言的面向过程的特点。在C++中可以使用继承、多态进行面向对象的编程。
面向对象与面向过程的区别
面向过程
面向过程编程是就分析出解决问题题的不走,然后把这些步骤一步一步的实现,使用的时候一个一个的一次调用就可以了。
面向对象
面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个市委在整个解决问题的步骤中的行为。
举个例子(玩五子棋)
使用面向过程的思想来考虑就是:开始游戏,白棋先走、绘制画面、轮到黑子、绘制画面、判断输赢、重复之前的过程,输出最终结果。
使用面向对象的思想来考虑就是:玩家系统、棋盘系统、判定系统、输出系统。
面向对象就是高度的将实物抽象化,也就是功能的划分,面向过程就是自顶向下编程,也就是步骤的划分
具体语言的区别
1、关键字不同
C99有32个关键字
C++98有63个关键字
一些关键字细微的区别
1、struct:在C语言猴子那个struct定义的变量中不能由函数,在C++中可以有函数
2、malloc:malloc的返回值是void*,在C语言中可以赋值给任意类型的指针,在C++中必须要进行强制类型转换,否则会报错。
3、class和struct:class是对struct的扩展,struct的默认访问权限是public,而class的默认访问全显示private
2、后缀名不同
C源文件的后缀是.c,C++源文件的后缀是.cpp,在VS中,如果在创建源文件的时候什么都不给,默认的就是.cpp
3、返回值不同
在C语言中,如果一个函数没有指定返回值得类型,默认的返回值为int类型,并且会返回一个随机数,一般为0xCCCCCCCC,C++中如果一个函数没有返回值,则必须要指定为void,否则编译不会通过。
4、参数列表不同
在C语言中,函数没有指定参数列表的时候,默认可以接受多个参数,但是不支持无名参数,在C++中,因为严格的参数类型检测,没有参数列表的函数,默认为void,不接受任何参数,但是他支持无名参数。
5、缺省参数
缺省参数的声明或定制函数时的参数指定一个默认值。在调用该函数时,如果没有指定实参则可以采用该默认值,则使用指定的参数。但是这在C语言中是不支持的。
6、函数重载
函数重载是函数的一种特殊情况,指的是在同一作用域中,声明几个功能类似的同名函数,这些同名函数的形参列表必须不同,或者是在类中使用const修饰的函数和没有使用const修饰的函数,常用来处理实现功能类似但是数据类型不同的问题。在C语言中没有函数重载,是因为C语言对函数名的修饰只是在函数名前添加一个下划线,但是C++对函数名的修饰会添加上该函数的返回值和参数列表。
7、标准输入输出
在C语言中使用的是scanf()和printf()来实现的,但是C++中是使用类来实现的。cin、cout对象,他们本身并不是C++语言的组成部分,在C++中不提供内在的输入输出运算符,这时与其他语言不相同的地方,他的输入和输出是通过C++中的类来实现的,cin和cout都是这些类的实例,是在C++语言的外部实现的。
8、动态内存管理
C语言使用的是malloc/free函数,C++在此基础上还添加了new/delete两个关键字。
9、const修饰
C语言中const修饰的变量不可以用在定义数组时的大小,并且在定义的时候可以不设定初始值,但是在C++中修饰的变量在定义的时候必须要设定初始值,并且可以用在定义数组的大小,,如果不进行取地址或解引用的话,是存放在符号表中的,不开辟内存。
二、C++面向对象
面向对象的特点
维护性、复用性、扩展性。
封装体现了维护性,按照信息屏蔽的原则,把对象的属性和操作结合在一起,构成一个独立的对象。通过限制对属性和操作的访问权限,可以将属性隐藏在对象内部,对外提供一定的接口,在对象之外只能通过接口对对象进行操作,这样增加了对象的独立性,外部的对象不能直接操作对象的属性,只能使用对象提供的服务,从而保证了数据的可靠性。
继承体现了复用性,当定义了一个类后,又需要定义一个新类但是这个类与原来的类相比只是增加或修改了部分属性和操作,这时可以引用原来的类派生出新的类,新类中只需要描述自己特有的属性和操作,这样就大大的简化了对问题的描述,提高了程序的复用性。
多态体现了扩展性,多态就是一个接口多种实现,当需要添加新的模块功能的时候,不需要改变原来的功能,只需要添加新的即可,这样就实现了扩展性。
面向对象的优点
易于维护:可读性比较高,如果有改变的需求,由于继承的存在,维护也只是局部模块,所以说维护起来是非常方便和较低成本的。
质量高:可重用现有的,在以前的项目的领域中一杯测试过的类使系统满足业务需求并具有较高的质量。
效率高:在软件开发时,根据设计的需要对现实事件的事务进行抽象,产生类。这样结局问题的方法接近于日常生活和自然的思考方式,必定会提高软件开发的效率和质量。
1、c语言是面向过程的结构化 语言,易于调试和维护。
2、表现能力和处理能力极强,可以直接访问内存的物理地址。
3、C语言实现了对硬件的编程操作,也适合于引用软件的开发。
概述
封装可以使得代码模块化,继承可以扩展已经存在的代码,他们的目的是为了代码重用。而多态的目的是为了接口重用。
封装
封装是设计类的一个基本原理,是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据与对数据进行的操作进行有机的结合,从而形成类,其中的数据和函数都是类的成员。
继承
如果B是继承了A,那么就把这个B称为是A的子类,把A称为B的父类。继承可以使子类具有父类的各种属性和方法和方法,就不用再次编写相同的代码。子类继承父类的同时,可以重新定义某些属性,并重定义其中的一些方法,也就是隐藏父类中原有的属性和方法,使其获得于父类不同的功能。
单继承
单继承就是一个派生类继承一个基类。单继承的继承规则为:所有继承下来的基类成员变量存放在派生类添加的成员变量之前,也就是基类的成员变量的内存地址低于派生类的内存地址,可以看做是将基类的内存空间进行了一次拷贝,并且在拷贝的内存空间后面加上派生类自己的成员。
多继承
菱形继承
菱形继承存在的问题就是数据二义性,相应的解决方案就是虚拟继承。多继承的继承规则是:以单继承的方式按照父类声明的顺序继承每个父类,可以看做是按照声明的顺序将每个父类的内存空间拷贝到一起,并且在后面添加上派生类自己的成员。
虚拟继承
虚拟继承是解决C++中多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这样会存在两个问题,一个是对于存储空间的浪费,还有就是数据的二义性。虚拟继承就是针对这两个问题出现的,虚拟继承的底层实现原理与编译器相关,一般通过虚基类和虚基表实现,每个虚继承的子类都有一个虚基类指针和虚基表,当虚拟继承的子类被当做父类继承时,虚基类指针也会被继承。实际上vbptr指的是虚基类表指针,这个指针指向虚基类表,在虚基类表中记录了虚基类与本类的偏移地址,通过偏移地址,可以找到虚基类的成员,虚拟继承和虚函数有着相同之处,都是利用了虚指针和虚表,虚基类表中存储的是虚基类相对于直接继承类的便宜,而虚函数表中存储的时候虚函数的地址。
继承中的访问权限
父类的私有成员在子类中无论以什么方式继承都是不可见的,这个的不可见指的是private成员仍然被继承到了子类中,但是在语法上限制子类对象不管实在类内还是在类外都是无法访问它的。
子类以公有方式继承父类时,父类的成员在子类中保持原有的属性。
子类以保护方式继承父类时,父类中的公有成员在子类中成了保护成员。
子类以私有方式继承父类时,父类中所有成员在子类中都是私有的。
使用class时默认的继承方式时私有的,使用struct时则默认的继承方式是共有的。
还有一点就是友元是类级别的,不存在继承的问题,也就是子类不能继承父类的友元关系。
多态
多态可以简单的概括为“一个接口,多种方法”,字面意思是多种形态。多态分为静态多态和动态多态。
静态多态
静态多态也称作静态绑定或者是早绑定。地址的绑定是编译器在编译的时候完成的,编译器根据函数实参的类型,可以推断出要调用那个函数,这里可能会进行隐式的类型转换,如果有对应的函数就调用了,否则编译报错。静态多态又分为函数重载和泛型编程。
函数重载
函数重载是在相同的作用域中,只有函数的名称相同,参数个数或参数类型不同。编译器根据函数不同的参数表,对同名函数的名称修饰,然后这些同名函数就成了不同的函数。这个在C语言中是不支持的,因为c语言中对函数的名称修饰较为简单,在VS2013编译器中,c语言对函数名称修饰的处理只关注到了函数名,对函数名的修饰只是简单的在函数名前添加_,而c++语言除了函数名,还关注了函数的参数,对函数名的修饰时候要加上参数,通过对函数名称的修饰不同,编译器调用函数时所找的符号就不同。
泛型编程
泛型编程指的是编写独立于特定类型的代码,泛型编程在C++中的主要是实现为函数模板和类模板。泛型编程的特性有如下几点:
1、函数模板并不是真正的函数,他只是C++编译器生成具体的函数的一个模子。
2、函数模板本身并不生成函数,实际生成的函数是替换函数模板的那个函数,这种替换在编译期就绑定了。
3、函数模板不是只编译一份满足多重需要,而是为每一种替换他的函数编译一份。
4、函数模板不允许自动类型转换。
5、函数模板不可以设置默认模板参数。
动态多态
C++中的动态多态是基于虚函数的。对于相关的对象类型,确定他们之间的一个共同的功能集,然后在父类中把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,完成具体的功能。操作函数通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。
虚函数
虚函数之所以叫做虚函数,是因为他的推迟联编和动态联编,一个类的虚函数的调用并不是在编译的时候确定的,而是在运行的时候确定的,虚函数通过基类的指针或者引用指向派生类对象实现多态。
纯虚函数
纯虚函数指的是在基类中声明的虚函数,但是没有在基类中定义,要求在任何派生类中都要定义自己实现方法。如果一个类中有纯虚函数,则这个类被称为抽象类,由于这个类的构建并不完成,所以不能生成一个对象。继承了抽象类的派生类必须要将纯虚函数实现,否则同样是抽象类,不能生成对象。
虚函数表
在C++中虚函数通话四通过虚函数表来实现的,这个表中存放的是虚函数的地址,他是属于类的,不属于某个具体的对象,在一个类中只有一个虚表,所有对象共享同一份虚表。为了指定对象的虚表,在对象构造的时候就在对象的内部包含了虚表指针_vfptr,一般是放在头部。
关于虚函数表有两种情况是要分清楚的,多继承和多重继承中的虚表是不一样的。
多继承指的是有一个子类继承了两个基类,比如说有A,B,C三个类,在A和B类中都有虚函数,C类依次继承了A类和B类,这时候C类中的虚表就有了A类和B类两个虚表,并且C类中的虚表指针是以A类虚表地址为基础的,如果想要获取到B类虚表的地址可以让指针向后偏移A类的大小或者给出一个B类的指针指向C类对象发生一个天然的转换,需要注意的是在C类中的重写的虚函数会覆盖A类和B类中的同名虚函数,如果C类中的虚函数在A类和B类中没有,就添加到A类的虚函数表中,但是A类指针不可以调用,如果是只在A类或者B类中有的虚函数,在C类中没有,那么只能是拥有虚函数的父类和C类可以调用。
多重继承就是B类继承了A类,C类继承了B类,在B类中的重写的虚函数会在虚函数表中覆盖A类的同名虚函数,并将B类新添加的虚函数放在B类虚函数表的末尾,C类也是如此,C类的虚表是从B类中继承的,在C类中的重写的虚函数会在虚函数表中覆盖B类的同名虚函数,并将C类新添加的虚函数放在C类虚函数表的末尾。
静态多态多态的比较
静态多态
优点
1、静态多态通过模板编程为C++带来了泛型设计的概念,比如STL。
2、静态多态是在编译期完成的,所以效率很高,编译器可以对其进行优化。
缺点
由于模板是实现静态多态,所以模板的不足也是静态多态的劣势,比如调试困难、编译耗时、代码膨胀。
动态多态
优点
1、实现与接口分离,可复用。
缺点
1、运行时绑定,导致一定程度上的运行时开销。
2、编译器无法对虚函数进行优化。
3、笨重的类继承体系,对接口的修改影响整个类层次。
不同点
本质不同:
早晚绑定,静态多态是在编译期决定的,由模板实现完成,而动态多态是在运行期间决定的,由继承、虚函数实现。
接口方式不同:
动态多态的接口是显式的,以函数名为中心,通过虚函数在运行期间实现,静态多态的接口是隐式的,以有效表达为中心,通过模板在编译期间完成。
应用形式上:
静多态是发散式的,让相同的实现代码应用于不同的场合。
动多态是收敛式的,让不同的实现代码应用于相同的场合。
思维方式上:
静多态是泛型式编程风格,它看重的是算法的普适性。
动多态是对象式编程风格,它看重的是接口和实现的分离度。
相同点
够可以实现多态性,静态多态/编译期多态,动态多态/运行期多态。
都可以是使接口和实现分离,一个是模板定义接口,类型参数定义实现,一个是基类定义接口,继承类负责实现。
虚函数面试题
- inliine函数可以实虚函数码?
不可以,因为inline函数没有地址,无法将他存放到虚函数表中。
- 静态成员可以是虚函数吗?
不能,因为静态成员函数中没有this指针,使用::成员函数的嗲用用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?
不可以,因为对象中的虚函数指针是在对象构造的时候初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,最好将析构函数设置为虚函数,因为这样可以避免内存泄漏的问题,如果一个父类的指针指向了子类的的对象,如果子类对象中的虚函数没有写成多态的,他只会调用父类的析构函数,不会调用自己的析构函数,但是他创建对象的时候调用了构造函数,所以说就用子类的构造函数就应该该取调用他的析构函数,这样才能保证所有的必须释放的资源都是放了,才可以保证不会有内存泄漏。如果是多态的,就会先去调用子类的析构函数,然后再取调用父类的析构函数,这样子类和父类的资源就都可以释放。
- 对象访问普通函数快还是虚函数快?
如果是普通对象,是一样快的,如果是指针对象或者是引用对象,调用普通函数更快一些,因为构成了多态,运行时调用虚函数要先到虚函数表中去查找。这样然后才拿到韩式的地址,这样就不如直接可以拿到函数地址的普通函数快。
- 虚函数表时再什么阶段生成的?他存放在哪里?
虚函数时再编译阶段生成的,他一般存放再代码段,也就是常量区。
- 是否可以将类中的所有成员函数都声明称为虚函数,为什么?
虚函数是在程序运行的时候通过寻址操作才能确定真正要调用的的函数,而普通的成员函数在编译的时候就已经确定了要调用的函数。这个两者的区别,从效率上来说,虚函数的效率要低于普通成员函数,因为虚函数要先通过对象中的虚标指针拿到虚函数表的地址,然后再从虚函数表中找到对应的函数地址,最后根据函数地址去调用,而普通成员函数直接就可以拿到地址进行调用,所以没必要将所有的成员函数声明成虚函数。
- 虚函数表指针被编译器初始化的过程怎么理解的?
当类中声明了虚函数是,编译器会在类中生成一个虚函数表VS中存放在代码段,虚函数表实际上就是一个存放虚函数指针的指针数组,是由编译器自动生成并维护的。虚表是属于类的,不属于某个具体的对象,一个类中只需要有一个虚表即可。同一个类中的所有对象使用同一个虚表,为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在每个对象的头添加了一个指针,用来指向虚表,并且这个指针的值会自动被设置成指向类的虚表,每一个virtaul函数的函数指针存放在虚表中,如果是单继承,先将父类的虚表添加到子类的虚表中,然后子类再添加自己新增的虚函数指针,但是在VS编译器中我们通常看不到新添加的虚函数指针,是编译器故意将他们隐藏起来,如果是多继承,在子类中新添加的虚函数指针会存放在第一个继承父类的虚函数表中。
- 多态的分类?
静态绑定的多态的是通过函数的重载来实现的。动态绑定的多态是通过虚函数实现的。
- 为什么要引入抽象类和纯虚函数?
为了方便使用多态特性,在很多情况下由基类生成对象是很不合理的,纯虚函数在基类中是没有定义的,要求在子类必须加以实现,这种包含了纯虚函数的基类被称为抽象类,不能被实例化,如果子类没有实现纯虚函数,那么它他也是一个抽象类。
- 虚函数和纯虚函数有什么区别?
从基类的角度出发,如果一个类中声明了虚函数,这个函数是要在类中实现的,它的作用是为了能让这个函数在他的子类中能被重写,实现动态多态。纯虚函数,只是一个接口,一个函数声明,并没有在声明他的类中实现。对于子类来说它可以不重写基类中的虚函数,但是他必须要将基类中的纯虚函数实现。虚函数既继承接口的同时也继承了基类的实现,纯虚函数关注的是接口的统一性,实现完全由子类来完成。
- 什么是多态?他有什么作用?
多态就是一个接口多种实现,多态是面向对象的三大特性之一。多态分为静态多态和动态多态。静态多态包含函数重载和泛型编程,进程多态是程序调用函数,编译器决定使用哪个可执行的代码块。静态多态是由继承机制以及虚函实现的,通过指向派生类的基类指针或者引用,访问派生类中同名重写成员函数。堕胎的作用就是把不同子类对象都当作父类来看,可以屏蔽不同子类之间的差异,从而写出通用的代码,做出通用的编程,以适应需求的不断变化。
三、常见关键字的作用
static
static有静态局部变量、静态全局变量和静态方法三种使用方法,他们的共同点就是在本文件中声明的静态变量和静态方法是不能被其他文件所使用的,和对应的extern关键字,extern关键字声明的全局变量和函数在整个工程中都是可以被使用的。
全局变量
有static声明的全局变量,只能在函数体外部被定义,并且只能在本文件中有效,这点就是区别于普通的全局变量,普通的全局变量在其他的文件中也是可见的。在函数体重可以定义同名的局部变量,这时会隐藏这个静态的,如果要使用静态的全局变量,需要在变量名前添加::作用域运算符。
局部变量
static局部变量同样只能在本文件中使用,静态局部变量的生命周期不随着函数的结束而结束,只能在第一调用函数的时候回他进行初始化,之后调用就会跳过初始化,他会在函数结束之后在内存中保存当前的结果,而不会像普通的局部变量在清栈的时候销毁,在内存中他区别与局部变量的是局部变量每次调用函数时分配的内存空间可能是不一样的,但是静态局部变量具有全局唯一性的特点,每次调用使用的时候用的都是同一块内存空间,但是这也造成了一个不可重入的问题。(现在有两个进程A、B都要去调用这个函数fun(),如果是A先调用函数fun,在运行函数的时候突然失去了运行权,但是已经将局部变量修改成了自己要试用的值,由于使用的是同一块内存空间,进程B调用函数的时候也将局部变量修改成了自己要使用的值,当进程A需要继续执行的时候,由于这块内存空间中的值已经被修改了,所有进程A就得不到自己想要的结果)。
方法
static数据成员和成员函数
在C++中继承了C语言中的static这个关键字,并且在类中给了第三种定义方法,表示只属于一类而不是属于类的某个对象的变量和函数。这个和普通的成员最大的区别就是在类中是唯一的,并且在内存中只有一份,普通成员函数调用的时候需要传入this指针,但是静态成员函数调用的时候是没有this指针的,只能在调用的时候使用类名加作用域来调用。在设计多线程操作的时候,有POSIX库下的线程函数要求是全局的,所以普通的成员函数是无法直接作为线程函数的,但是静态的成员函数是可以做线程函数的。
static函数和普通函数
普通函数的定义和声明默认是extern的,在同一个工程中的其他文件中是可见的,如果在另一个文件中定义了相同的函数就会穿线重定义错误,当然这个重定义和继承中的 重定义是不一样的,这里的重定义指定的命名冲突。静态函数在内存中只有一份,但是普通的函数在每个被调用中都会维护一份拷贝。
extern
extern置于变量或函数前,用于标示变量或函数的定制在别的文件中,提示编译器遇到这个变量或函数要在其他的模块中查找。
extern “C”
如果是extern“C” void fun(int a, int b);这样是高数编译器在编译fun这个函数的时候要按照C的规则去编译,而不是按照C++的,这一点主要是与C++支持重载,C语言不支持重载和函数被C++编译器编译后在苦衷的名字与C语言的不同有关。
当extern不与“C”在一起修饰变量或者函数时,比如extern int a;他的作用就是声明函数或者全局变量的作用范围和关键字,其生命的函数和变量可以在本工程中的所有文件中使用。需要注意的是他只是一个声明,并不是定义。
const
使用const修饰类的成员变量的时候,必须要在初始化列表进行初始化,并且引用类型的成员变量和没有默认默认构造函数的对象成员也必须要在初始化列表进行初始化,如果有继承的关系,如果父类没有默认的构造函数,也必须要在初始化列表进行初始化,初始化列表对数据成员的初始化顺序时按照数据成员的声明顺序严格执行的。
const修饰成员函数的时候,一般是放在成员函数的最后面,修饰的类的成员函数中隐藏的this指针,代表不可以通过this指针修改类的数据成员,这个使用方法也可以与普通的相同的成员函数构成重载。
关于const还有一个问题就是传参和赋值的问题,一般来说使用const修饰的变量是安全的,没有使用const修饰的变量是不安全的,在传参的时候可以让非const修饰的变量传给const修饰的,但是const修饰的变量不可以传给非const修饰的形参,这就相当于将安全的变量交给了不安全的变量。
volatile
volatile一般用来修饰变量,他的存在是因为我们的程序在进行编译的时候编译器会进行一系列的优化,比如说某个变量被修饰为const,编译器就会认为这个值是只读的,就会在寄存器中保存这个变量的值,每次需要的时候直接从寄存器中读取,但是有的时候会在不经意间修改了这个变量的值,那么编译器是并不知道的,还是从寄存器中进行读取,这样就会造成结果不匹配。但是如果使用volatile声明后,就是相当与告诉编译器这个变量随时会给变,需要每次都要从内存中读取,不需要优化,从而避免了这个问题,volatile的应用场景最多的是多线程对
define、const、inline区别
define作用域程序的预处理节点,而预处理主要的工作是宏替换、去注释以及条件编译,而define起作用的地方就在宏替换阶段,只是单纯的将宏替换为代码。但是define只是单纯的代码替换,不会进行类型的检查,很容易出错。在C++中建议使用const、枚举定义常量,这样就会有类型检查。于是C++中有提供了一个inline关键字,可以实现和define相同的功能,并且支持类型检查和调试,一般生命在函数的定义前面,但是inline只是对编译器的一种建议,一般建议代码为3-5航左右,并且没有复杂的逻辑结构,例如循环、递归之类的。
四、malloc/free和new/delete的区别
1、malloc是从堆上开辟空间,而new是从自由存储区开辟空间。自由存储区是C++抽象出来的概念,不仅可以是堆,还可以是静态存储区。
2、malloc是函数,而new是关键字。
3、malloc对开辟的空间大小需要严格的指定,而new只需要对象名。
4、malloc开辟的空间既可以给单个对象使用也可以给数组使用,释放的方式都是free();而new开辟对象数组需要使用new[size],释放是使用delete[]。
5、malloc成功的返回值是void*,需要用户进行强转,申请空间失败会返回NULL,所以在使用的时候需要进行判空处理,new成功返回的是对象指针,不需要强转,失败抛出异常,但是为了最大程度的兼容C,C++的new也支持失败返回NULL,但是一般不使用。
6、new不仅负责开辟空间,还会去调用对象的构造函数和析构函数。
7、new申请空间的效率要低于malloc,因为new的底层是通过malloc实现的。
五、引用和指针的区别
指针
指针是一个变量,只不过这个变量中存储的是一个地址,指向内存的一个存储单元。比如说一个类型T,T*就是一个指向T的指针类型。
引用
引用其实就是一个对象的一个别名,主要用于函数传参和返回值类型,类型+&+变量名 = 被引用对象。
区别
1、引用是某块内存的别名,而空指针指向的是一块内存,他的内容是所指内存的地址。
2、引用在创建的时候必须要别初始化,但是指针可以为空,所以在使用使用指针的时候就要进行判空处理,引用就可以不用。
3、引用一旦进行了初始化就不能改变指向,指针可以改变自己的指向。
4、对弈引用来说,使用const修饰时,不同的const位置的意义是一样的,都表示指向的对象是常量。对于指针来说,const int *p 说明p是指向常量的指针, int * const p 说明p本身就是一个常量。
5、引用的大小是指向对象的大小,而指针在32位机上是四字节,在64位机上是8字节,是指针本身的大小。
6、对引用++操作就是对引用指向的对象进行++操作,但是对指针++操作,表示的是地址的变化,向后移动一个指针的大小。
7、指针传递和引用传递
指针传递传递的是地址,在函数中定义了一个局部的指针变量,存放的是实参的地址,消耗了内存,可以对地址进行加减操作,指向另一个变量,由于传递的是地址,所以不需要返回值,因为实际修改的就是实参的值。
引用传递同样是地址,但是不会在函数中消耗内存,直接对地址进行使用,对函数中的引用变量的加减操作直接影响外部的实参,并且不能指向另一个变量。在实际使用中传递引用的时候如果不希望实参被改变,通常要用const将其修饰。
8、引用不可以有多级只能是一级,但是指针可以是多级的。
六、深拷贝和浅拷贝
浅拷贝指的是将原始对象中的数据型字段拷贝到新对象中,将引用型对象的引用赋值到新对象中去,不把引用的对象复制进去,所以原始对象和新对象引用同一对象,新对象中的引用型字段发生变化会导致原始对象中对应的字段发生变化。
深拷贝是在引用方面不同,深拷贝就是重新创建一个新的和原始字段内容相同的字段,所以两者的引用是不同的。其中一个对象发生变化并不会影响另一个对象。
编译系统会在我们自己没有定义拷贝构造的时候调用默认的拷贝构造函数,进行浅拷贝,也就是两个对象使用同一份资源,当第一个对象进行了析构后,第二对象就悬空了,如果再对他进行析构,由于编译器找不到对应的资源,就会崩溃。所以对含有指针成员的对象或类中存在资源的对象进行拷贝的时候,必须要自己定义拷贝构造函数,实现深拷贝。
七、类中的成员函数占空间吗?怎么调用?
类中的普通成员函数和静态成员函数是不占用类的内存的,只有在类中函数有虚函数的时候才会在类中添加一个虚函数指针,增加一个指针的大小。类中的成员函数实际上与普通的全局函数一眼,只不过是在编译的时候在成员函数中添加了一个指向当前对象的this指针,成员函数的地址是全局已知的,所以对象的内存空间中是没有必要去保存成员函数的地址的。在编译的时候就已经绑定了,类的属性值得是类中的数据成员,他们是实例化一个对象的时候就为数据成员分配了内存,但是成员函数是所有对象多公有的。
但是空类的大小是1个字节,这是为了保证两个不同对象的地址不同。类的实例化是在内存中分配一块地址,每个势力在内存中欧拥有独一无二的地址。同样的,空类也会实例化,所以编译器会给类隐含的添加一个字节,这样空类实例化后就有了独一无二的地址了。在一个空类中,在第一次实例化对象的时候就创建了默认的成员函数,并且这个成员函数是public和inline的。
八、NULL和nullptr的区别
传统意义上来说,c++把NULL、0视为同一种东西,有些编译器将NULL定义为 ((void*)0),有些将其定义为0
c++不允许直接将void隐式的转化为其他类型,但是如果NULL被定义为 ((void*)0),当编译char *p = NULL,NULL只好被定义为0。
还有:void func(int);void func(char*);
如果NULL被定义为0,func(NULL)会去调用void func(int),这是不合理的
所以引入nullptr,专门用来区分0、NULL。nullptr的类型为nullptr_t,能够隐式的转换为任何指针。所以用空指针就尽可能的使用nullptr。