c++ Flashcards
C和C++的区别
从设计思想上来说,C是面向过程的语言而C++是面向对象语言
具体来说就是C++具有封装继承和多态三种特性,并且C++还加入了许多更安全的功能,比如四种强制类型转换,C++还支持范式编程,比如类模板和函数模板等
还有就是具体语法和操作上也有许多区别,比如动态内存管理,C是malloc/free,C++是new/delete,还有C++支持函数重载,C++支持引用等等
数组和指针的区别
数组和指针是完全不同的两个概念,数组用来保存物理存储上连续的一组数据,而指针可以保存一个地址,通过这个地址可以找到任何类型的变量或者函数本身,然后对它进行操作。数组和指针之间其实并没有关系,但是在C语言中,对数组名进行操作时,通常也就是把数组名作为右值使用时,会隐式转换成指向数组中第一个元素的指针,这在使用上可能会对初学者造成一些困惑。并且从汇编的层次看,给数组变量赋值就和给普通变量赋值一样,并没有通过指针进行操作,而通过指针给变量赋值还需要分配地址给指针,指针解引用找到变量地址等操作,所以说用指针访问内存的效率还不如用数组访问内存的效率
指针和引用的区别
我们通常可以用一种说法来形容引用,就是引用其实就是给变量起了一个别名,还有一种说法是引用是一种更安全的指针
首先,从底层实现上来说,定义一个引用变量和定义一个指针变量在汇编指令上是没有任何区别的,它们在汇编中都是通过指针实现的,通过引用变量修改所引用的内存的值,和通过指针解引用修改指针指向的内存的值,这两种操作的底层指令也是一模一样的,其实都会先从一个指针内存中把它指向的元素的地址取出来,再把新的值保存到那个地址中
但是在使用上指针和引用还是有很大的区别的,比如说,引用在创建的时候必须初始化,而指针可以不初始化;引用只有一级引用,没有多级引用,而指针可以有多级指针;对所指的对象进行操作时,指针必须先解引用,而对引用直接进行操作就可以修改引用所指向的对象;指针在使用过程中可以指向其它对象,但是引用不能改变指向的对象;可以有const指针,但是没有const引用;使用sizeof对指针求值结果是4,而对引用求值则是被引用对象的大小
static关键字的作用
- 用于修饰全局变量,当同时编译多个文件的时候,所有没有加static的全局变量和函数都是全局可见的,其它的源文件也可以访问,并且未加static的全局变量在符号表里是global符号,对其它目标文件可见,这样的符号是要参与符号解析的,加了static之后就变成了local符号,对其它目标文件就不可见了,只在当前文件可见,不参与符号解析过程。所以多个源文件可以定义同名的static全局变量,不会产生重定义错误。简单来说,static修饰全局变量就是改变变量的作用域,让它只能在本文件中使用
- 用于修饰局部变量,就是将变量放在.data段或者.bss段,初始化且初始化不为0就是.data段,没有初始化或者初始化为0放在.bss段。这样的话当程序一运行起来就会给它分配内存,并且进行初始化,同时也是唯一一次初始化,它的生命周期是整个源程序,程序结束内存才释放,但是它的作用域还是和普通局部变量相同,就是只能在该函数里使用该变量。简单来说static修饰局部变量就是改变它的生存期,变得和整个程序的生命周期一样
- 用于修饰普通函数,和修饰全局变量一样,函数经过编译会产生一个函数符号,被static修饰后就变成local符号,不参与符号解析,只在本文件中可见
- 用于修饰类的成员变量,修饰之后类的成员变量就变成静态成员变量,不属于对象而属于类,它不能在类的内部初始化,类中只能声明,定义需要在类外,并且类外定义的时候不用加static关键字了,只需要表明类的作用域
- 还有就是修饰类的成员函数,会将其变成静态成员函数,也不属于对象,属于类,形参不会生成this指针,仅能访问类的静态数据和静态成员函数,调用不依赖对象,所以不能修饰虚函数,用类的作用域调用
谈谈对模板的理解
模板分为函数模板和类模板
一个函数模板代表了一整个函数家族,它会根据实参类型产生函数的特定类型的版本,在程序生成的编译阶段,编译器会根据传入的实参类型来推演生成对应类型的函数,比如说当你设置的typename T传入的实参类型是int,那么编译器就会在编译的时候通过推演将T确定为int类型,然后生成一份专门处理int类型的代码
还有就是类模板,类模板和函数模板基本一样,但是它不是一个具体的类,类模板实例化的结果才是真正的类,类模板实例化就是在类模板名字后加一个间括号,然后把类型放进间括号里,这种实例化的方式叫显示实例化,函数模板也支持,但是函数模板还支持传实参自动实例化的方式,叫隐式实例化,类模板就不支持了
const的作用
const首先在C语言和C++中是不太一样的,在C语言中主要就是用来修饰一个变量使它变成一个称之为“常变量”的东西。而C++中const的用法大概分成下面几种:
- const用于定义一个常量,在C++源代码编译的时候,所有出现const常量名字的地方都会被常量的初始值替换,所以C++的const常量定义的时候必须初始化否则会报错
- 修饰函数的形参,一般当我们函数传参时传的是不想它发生改变的参数的时候,一般我们就用“类型 + const + 引用 + 变量“的形式设置一个const引用的形参,这样一来既节省了实参复制成形参过程的消耗,还能保证在函数内部不会对实参造成更改,非常方便
- const修饰函数的返回值,这样一来函数的返回值就不能随便被修改了,并且函数的返回值就只能赋给同样类型的const常量
- 用const修饰类的成员函数,就是我们一般把不会修改成员属性的成员函数都用const修饰,这样的话当不小心在函数里修改了数据成员或者调用了非const成员函数的时候编译器会报错
C和C++中const的区别
首先C和C++const修饰的变量的原理就不一样,C语言里const的变量就是当作一个变量来编译生成指令的,所以我们一般称为常变量,它其实只是语法上限制了修饰后的变量做左值,其实它的内存还是可以访问并且被修改的。而C++在编译的时候把所有出现const常量名字的地方都用常量的初始值给替换了
因此C中的const和C++的const在使用上就有了区别比如C的const常变量不用初始化,C++的const常量必须初始化,再比如C中创建一个const修饰的变量a,a不能在初始化数组的时候放进中括号里,就是int array[a]会出错,而C++里的const常量就没问题
还有一点就是C语言里const变量都是全局的,C++中视位置而定
const和#define的区别
在C++中const和#define都可以用来定义常量,但是区别还是很大的
首先在原理上,#define是在预处理阶段进行的单纯的替换的操作,不会做任何的检查和计算,但是const是在编译期作为一个符号表中的常量进行操作,所以const可能是会分配内存的而#define没有内存
并且#define定义的常量没有类型,编译器不会对它进行安全检查,可能会出错,而const定义的常量是有类型的,比较安全。还有就是#define不重视作用域,所以#define不能用来定义类的专属常量也不能提供任何封装性,而const可以。所以Effective C++里面也说了尽量用const、enum、inline替换#define
函数重载原理以及C语言为什么没有函数重载
C++代码编译成汇编代码的时候生成函数符号的命名规则是和函数名以及参数列表都有关的,所以说同名不同参数列表的函数生成汇编代码之后是不同的函数符号,调用的时候就相当于调用两个不同的函数。同时这也是C语言没有函数重载的原因,C语言生成函数符号就只是在函数名前面加了个下划线,只取决于函数名,和参数列表无关,所以在编译器看来即使参数列表不同也是同一个函数,就会报函数重定义的
讲一下C++中的面向对象三大特性?
面向对象三大特性就是封装、继承和多态
封装
封装就是把客观事物抽象成数据和操作集合,也就是类,这样就可以对外部隐藏一些属性和实现的细节,只开放特定的公共访问的部分也就是public,这样就提高了安全性,同时因为一个类可以实例化很多对象,它们都能调用同样的成员函数,提高了代码的复用性。其中类的成员权限有三种,分别是public、protected和private,public是对外部可见的,protected是外部可以访问但不能继承的,private是外部不可见的
继承
继承就是使一个类获得另一个类的所有内容,包括成员属性和成员函数,继承也能提高代码的复用性,同时它让类和类之间产生了关系,正因为有了这种关系才有了面向对象里的多态,继承的方式有三种,也是public、protected和private,派生类中继承来的成员是什么权限,取决于继承方式和基类成员权限这二者取权限较小的,当然基类的private成员在派生类中不可见,也就不会继承
多态
C++中的多态分为静态多态和动态多态,静态多态有重载、模板、隐藏,动态多态就是覆盖,其中和面向对象有关系的就是隐藏和覆盖。
隐藏就是指有一个子类继承了父类,但是子类中定义了一个和父类中的一个成员函数函数名相同的函数,在这种情况下,当子类对象调用那个函数的时候,调用的是子类的函数,因为父类的函数被隐藏了。但是,如果把这个子类对象的指针强转成父类的指针类型,此时再调用该函数,调用的就是父类的成员函数。
覆盖只在继承虚函数的时候出现,父类中一个的函数加上virtual关键字就成了虚函数,通过虚函数表和虚函数表指针实现了一个子类继承了父类同名虚函数,把一个子类对象的指针强转成父类指针,再调用那个函数,调用的还是子类的成员函数,因为父类的虚函数的地址在子类对象的虚函数表里已经被替换掉了
什么是菱形继承、虚继承
菱形继承简单来说就是某个类的两个父类拥有一个相同的父类,比如说A类继承自B类和C类,B类和C类又都继承自D类,这就是菱形继承
菱形继承会产生两个问题,一个是冗余,一个是二义,冗余就是比如刚刚的D类中会包含两份一模一样的A类的代码,二义就是因为有了两份代码之后不知道具体该调用哪份代码
虚继承就是用于解决菱形继承这种问题的,通过虚继承继承出的子类实例化出的对象会带有虚基类指针,就是vbptr,指向自己的基类,作用是可以描述自己的基类,当发现被继承的另一个基类中也有一个相同的虚基类时,两个基类就只保留一个,说的通俗点就是通过虚继承,每个子类就知道自己的父类是谁了,当发现父类有重复的时候,就只留下一个,这样就解决了菱形继承中冗余和二义的问题,但是因为每个对象都多了一个或几个虚基类指针,这就增加了开销
C++中的四种类型转换
由于C语言的类型转换很简单,分成两种,一种隐式转换,在有些情况下会出问题,一种强制类型转换又把所有的情况都混合在一起导致代码不清晰,也比较容易出错,所以C++设计了4种类型转换
- const_cast
用于删除变量的const属性,也就是将const变量转换为非const
- static_cast
用于隐式转换,就是编译器本来会隐式执行的转换,这里显式的表示出来,但是不可以用于两个不相关类型的转换
- reinterpret_cast
用于无关类型之间的转换,例如指针到整数类型,函数指针到另一个函数的指针,其实它可以把任何类型的指针相互转换,但是这样可能就会出现各种问题,所以要少用这个
- dynamic_cast
是一种动态转换,只能用于含有虚函数的类,将父类对象的指针转换为子类对象的指针或引用,会先检查能否转换成功,能则转换,不能则返回NULL
对智能指针的理解
如果在一个函数里使用了动态内存分配,这个函数又有多种跳出的方式,比如多处return或者异常处理,那么在每一处跳出前都要free或者delete,这样就非常的麻烦,并且很容易忘,一旦忘了就会出现内存泄漏,智能指针就是用来解决这个问题的
所谓智能指针,其实是一个类模板,它的主要功能就是模拟一个指针,因为我们知道当超出类的作用域时,类会自动调用析构函数,析构函数自动释放资源,所以智能指针的原理就是函数结束时自动调用析构释放内存空间,就不需要手动释放了
C++中有四种智能指针,其中前三种是差不多的,但是在进行拷贝的时候有区别,拷贝就是比如说一个p1指针,指向一个new出来的对象,这个时候我再创建一个p2指针,把p1赋给p2,此时p1,p2就都指向new开辟的同一块内存,那可能就会出现释放同一块内存两次的严重问题,三种不同的智能指针其实就是三种解决这个问题的不同方法
- auto_ptrC++98的设计,现在基本上已经弃用了,它解决拷贝的问题的方法是p1赋给p2后,p1就失效了,程序再访问p1就会出错,也就是说每次拷贝都留了一个p1那样的坑,一访问就崩溃,这是很危险的,所以就不用了
- unique_ptr这个就能简单粗暴的解决拷贝的问题,既然拷贝会出问题那它就直接禁止拷贝
- shared_ptr如果需要用到拷贝的话,就可以用更靠谱一点的支持拷贝的shared_ptr,shared_ptr有一个引用计数机制,在其内部会给每个资源维护一个计数,记录着该资源同时在被几个对象所共享,每销毁一个对象计数就减一,只有减到零的时候才释放掉该资源,这就实现了共享
- weak_ptrweak_ptr是配合shared_ptr来使用的,因为shared_ptr在使用的过程中可能会出现循环引用的问题,比如两个shared_ptr相互引用,那么计数就永远不会下降为0,资源永远不会释放,weak_ptr就用来解决这种问题,协助shared_ptr工作,它是一种对对象的弱引用,不会增加引用计数,它可以和shared_ptr相互转化
final和override
这两个是C++11的新关键字,final修饰的类不能被继承,它修饰的虚函数就不能再被重写了
override则是声明子类的某个函数必须重写父类的某个虚函数,如果父类的虚函数你没有重写编译器就会报错
模板类中可以用虚函数吗?
可以,在模板类里使用虚函数和普通类中使用虚函数一样,但是有一点就是当模板类传入不同的类型时,就是两个完全不同的类,例如假如A 是B的父类,但是A却不是B的父类
但是模板函数不能是虚函数,因为实例化带有虚函数的类的时候需要创建虚函数表,但是模板函数在实例化完成之前编译器并不能确定会实例化出几种,也就无法创建出具体的虚函数表,所以不支持虚函数模板