1.某文件中定义的静态全局变量(或称静态外部变量)其作用域是本文件。

解释:静态全局变量限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此避免在其他源文件中引起错误。

2.如何判断一段程序是由C编译器编译还是C++编译器编译?

答案:利用内置宏

1
2
3
4
5
#ifdef __cplusplus
cout << "C++"
else
cout << "C"
#endif

3.C++函数中值的传递方式(传参)有哪几种?

答案:C++函数的三种传递方式为:值传递、指针传递、引用传递。
拓展:
引用传递:①是一种更安全和简便的指针,可以在函数内部直接更改外部的值;
②节省传递空间的消耗

4.c++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。

5.C和C++有什么不同?

①从机制上:C是面向过程的(但C也可以编写面向对象的程序);C++ 是面向对象的,提供了类。但是,C++ 编写面向对象的程序比C容易。
②使用场景:C适合要求效率高的场合,如嵌入式;C++ 适合更上层的,复杂的;linux核心大部分是C语言写的,因为它是系统软件,效率要求极高;
③C++ 是C的超集,C++ 比C扩充了很多的东西;
④C语言是结构化编程语言,C++ 是面向对象编程语言,C++ 侧重于对象而不是过程,侧重于类的设计而不是逻辑的设计。

6.C和C++中struct有什么区别?

①C中struct没有保护机制,可以封装数据但不能隐藏数据,C++ 中的struct有保护机制,默认为public;
②C中struct中不能定义函数,但可以有函数指针,C++ 中struct可以定义函数,可以包括类的所有东西,例如构造函数、析构函数、友元函数等;
③C中struct是用户自定义数据类型,C++ 中struct是抽象数据类型。

7.C++中struct和class的区别?

①默认继承权限。如果不明确制定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理;
②成员的默认访问权限。class的成员默认为private权限,struct默认是public权限。
除了以上两点,class和struct基本是一个东西,语法上没有任何其他区别。

8.int id[sizeof(unsigned long)];这个对吗?为什么?

正确,这个sizeof是编译时运算符,编译时就确定了,可以看出是和机器有关的常量。

9.C的malloc和C++的new?

①malloc是库函数,不在编译器控制范围之内;new是运算符,在编译器的控制范围之内。
②调用malloc时,从堆中申请内存;调用new时,从堆中申请内存并为内存调用构造函数。

10.多态的作用?

①不必编写每一子类的功能调用,可以直接把不同子类当父类看,屏蔽子类之间的差异,提高代码的复用率;
②父类引用可以调用不同子类的功能,提高了代码的扩充性和可维护性。

11.多态类中的虚函数表是在编译时期,还是在运行时期建立的?

虚函数表是在编译期就建立的,各个虚函数这时被组织成了一个虚函数的入口地址的数组。而对象的隐藏成员–指向虚函数表的指针是在运行期–也就是构造函数被调用时进行初始化的,这是实现多态的关键。

12.面向对象的三个基本特征,并简单叙述之?

①封装:将客观事物抽象成类,每个类对自身的数据和方法进行保护(private,protected,public);

②继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外的编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,具体的实现留给子类来做)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

③多态:接口的多种不同的实现方式即为多态。父对象可以根据当前赋值给他的子对象的特性以不同的方式运作。简单的说,就是:允许将子类类型的指针赋值给父类类型的指针。多态主要是为了抽象。

构成多态的条件:

  • 调用函数的对象必须是指针或者引用;
  • 被调用的函数必须是虚函数,且完成虚函数的重写;
  • 父类指针指向子类对象

13.main函数执行之前,还会执行什么代码?

全局对象的构造函数会在main函数执行之前执行。

14.内联函数在编译时是否做参数类型检查?

只要是函数都会做参数类型检查,所以内联函数要做参数类型检查,这内联函数跟宏比的优势。

15.内存的分配方式有几种?

①静态存储区域分配。内存在程序编译的时候就已经分配好了,这快内存在程序的整个运行期间都存在。例如全局变量和静态局部变量;
②在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限;
③从堆上分配,亦称为动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

16.对于一个频繁使用的短小函数,在C语言中应用什么实现,在C++中应用什么实现?

C语言用带参数的宏定义;C++用inline(内联函数),内联函数一般有两种,一种是在class内定义的成员函数全部默认为内联函数,另一种是在类内声明,类外定义(加上inline关键字)的成员函数。同时,当编译器觉得该函数可以被优化成内联函数时也会将其设为内联函数。

17.全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?

①作用域不同:全局变量具有全局作用域。静态全局变量只作用于本源文件,而非静态全局变量只需在一个源文件中定义,就可以作用于所有的源文件,当然,其他不包含该全局变量定义的源文件需要用extern关键字再次声明这个全局变量;局部变量只有局部作用域,即只限定义的函数内使用。
②生命周期不同:全局变量随主程序创建而创建,随主程序销毁而销毁,局部变量它只在函数执行期间存在,函数一次调用执行结束后,变量被撤销,所占内存也被收回。
③内存分配空间不同:全局变量、静态全局变量\局部变量都在静态存储区分配空间,而局部变量在栈里分配空间。
操作系统和编译器通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载,局部变量则分配在栈里面。

18.有了malloc/free为什么还有new/delete?

malloc与free是C/C++ 语言的标准库函数,new/delete是C++ 的运算符。它们都可以用于申请动态内存和释放内存。对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在灭亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++ 语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理成员与释放内存工作的运算符delete。
malloc只分配内存,new建立的是对象,即分配空间与执行构造函数;
free只释放内存,delete负责调用析构函数清除对象的成员与释放内存。

19.如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。你是如何处理内存耗尽的?

判断指针是否为NULL,如果是则马上用return语句终止本函数,或者马上用exit(1)终止整个程序的运行,或者为new和malloc设置异常处理函数。

20.用C++写个程序,如何判断一个操作系统是16位还是32位还是64位?

定义一个指针p,打印出sizeof(p),结果打印结果是2,则表示该操作系统是16位,如果是4,则表示是32位,如果是8,则表示是64位。

21.为什么需要使用堆,使用堆空间的原因?

因为直到运行时才知道一个对象需要多大内存空间,这时可以需要多少就申请多少;不知道对象的生存期到底有多长,需要释放时就释放。

22.为什么数组名作为参数,会改变数组的内容,而其它类型如int却不会改变变量的值?

当数组名作为参数时,传递的实际上是地址。而其他类型如int作为参数时,由于函数参数值实质上是实参的一份拷贝,被调函数内部对形参的改变并不影响实参的值。

23.请讲一讲析构函数和虚函数的用法和作用?

析构函数是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用,有释放内存空间的作用。虚函数是C++多态的一种表现,声明该函数可能会被子类重写,使用虚函数,我们可以灵活的进行动态绑定,当然是以一定的开销为代价的。

24.引用和指针有什么区别?

①引用必须被初始化,指针不必;
②引用初始化以后它所指的对象不能改变,即不能指向另外的对象,指针可以改变所指的对象;
③不存在指向空值的引用,但是存在指向空值的指针。
④指针是变量,这个变量存放的是所指内容的地址,“sizeof指针”得到的是指针本身的大小;引用是别名(实质是一个指针常量),与所引用变量占用同一内存空间,“sizeof引用”得到的是所指向的变量的大小。

25.文字常量和常变量?

常量指值不能更改的量,C/C++中常量分为两种:文字常量和常变量
①文字常量包括数值常量、字符常量和符号常量。其特点是编译后写在代码区,不可寻址,不可更改,属于指令的一部分。
②常变量指定义时必须初始化且值不可修改的变量,与其他变量一样被分配空间,可以寻址,注意,字符串常量属于常变量。
常变量的存储方式与普通变量差不多,全局常变量存储在静态存储区的常量区;局部常变量存储在栈区。

26.你觉得如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?

①程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知他们从何处来、表示什么;
②在程序的很多地方输入同样的数字或字符串,难保不发生书写错误;
③如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。

27.在C++中有没有虚构造函数?

构造函数不能是虚的,要构造一个对象,必须清楚知道要构造什么,否则无法构造一个对象。析构函数可以是虚的。

28.重复多次fclose一个打开过一次的FILE *fp指针会有什么结果,并请解释。

导致文件描述符结构中指针指向的内存被重复释放,如果此时指针指向了别的对象,就会导致一些不可预期的异常。

29.重载(overload)、重写(override,也叫覆盖)、重定义(redefinition,也叫隐藏)的区别?

重载:在同一个类中,指允许存在多个同名函数,而这些函数的参数表不同;
重写:用于继承,子类重新定义父类虚函数的方法;
重定义:不同类中,用于继承,派生类与基类的函数同名,屏蔽基类的函数。

30.C++是不是类型安全的?

不是,两个不同类型的指针之间可以强制转换,int型也可以当字符型数据操作。

31.C++里面是不是所有的动作都是main()引起的?如果不是,请举例。

不是,比如全局对象的初始化,就不是由main函数引起的。举例:
class A{};
A a; //先执行构造函数
int main() {}

32.C++中virtual和inline的含义分别是什么?

在基类成员函数的声明前加上virtual关键字,意味着将该成员函数声明为虚函数。inline与函数的定义体放在一起,使该函数称为内联。inline是一种用于实现的关键字,而不是用于声明的关键字。
①虚函数的特点:如果希望派生类能够重新定义基类的方法,则在基类中将该方法定义为虚方法,这样就可以启用动态编联。
②内联函数的特点:使用内联函数的目的是为了提高函数的运行效率。内联函数体的代码不能过长,因为内联函数省去调用函数的时间是以代码膨胀为代价的。内联函数不能包含循环体,因为执行循环语句要比调用函数的开销大。

33.const关键字?有哪些作用。

至少包含下列n个作用:
①欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
②对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或两者同时指定为const;
③在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
④对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
⑤对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不能被修改。

34.VC中,编译工具条内的Debug和Release选项是什么含义?

Debug通常称为调试版本,它包含调试信息,会增加一些额外的信息,并且不做任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。Debug带有大量的调试代码,运行时需要相应的运行库,发布模式程序紧凑不含有调试代码和信息,直接可以运行(如果不需要运行库)。Debug和Release模式都会生成可执行文件。

35.C++中public、protected、private的区别?

  1. private、public、protected的访问范围:
  • private:只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;
  • protected:可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;
  • public:可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问。
    住:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数。
  1. 类的继承后方法的属性变化:
  • 使用private继承,父类的所有方法在子类中变为private;
  • 使用protected继承,父类的protected和public方法在子类中变为protected,而private方法不变;
  • 使用public继承,父类中的方法属性不发生改变。

36.是不是一个父类写了一个virtual函数,如果子类覆盖它的函数不加virtual,也能实现多态?

virtual修饰符会被隐形继承的,所以可加可不加,也可以实现多态。

37.如何打印出当前源文件的文件名以及源文件的当前行号?

cout << __FILE__;
cout << __LINE__;
FILELINE是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。(C中也有)

38.当一个类A中没有声明任何成员变量与成员函数,这时sizeof(A)的值是多少,请解释一下编译器为什么没有让它为零。

为1;
如果是0的话,声明一个class A[10]对象数组,而每一个对象占用的空间是0,这是就没办法区分A[0],A[1]…了。

39.static修饰类的成员函数时,变成静态成员函数,不属于对象,而属于类,形参不会生成this指针,仅能访问类的静态数据和静态成员函数,调用不依赖对象,所以不能作为虚函数,用类的作用域调用。如果你希望在一个函数中对一个变量只执行一次初始化,以后不再初始化,使用上一次结果,就应该使用静态局部变量。static修饰成员变量时,static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为它分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了该静态变量,也会影响到其他对象。static 成员变量必须在类声明的外部初始化。

40.函数模版与类模版有什么区别?

函数模版的实例化是由编译程序在处理函数调用时自动完成的,即调用的时候不需要指定参数类型,由编译器自己判断;而类模版的实例化必须由程序员在程序中显式地指定,即使用时必须指定参数的类型。

41.函数重载,我们靠什么来区分调用的是哪个函数?靠返回值判断行不行?

重载主要靠函数签名来区分不同的函数,即函数名,函数参数类型和参数个数,而不关心返回值类型。
如果同名函数的参数不同(包括类型、顺序不同),那么容易区分出它们是不同的。如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。在C/C++程序中,我们可以忽略函数的返回值。在这种情况下,编译器和程序员都不知道哪个函数被调用,所以只能靠参数而不是靠返回值类型的不同来区分。

42.所有的运算符都能重载吗?

在C++ 运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。
①不能改变C++ 内部数据类型(如int,float等)的运算符;
②不能重载’.’,因为’.’在类中对任何成员都有意义,已经成为标准用法;
③不能重载目前C++运算符集合中没有的符号,如#,@,$等。原因有两点,一是难以理解,二是难以确定优先级。
④对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。

43.基类的析构函数不是虚函数,会带来什么问题?

1
2
3
class Bclass A{...}
A &a = new B
delete a

如果析构函数不是虚函数,此时就会调用A中的析构,这时B中的析构函数就用不上,成员就不能被清除,内存得不到释放,从而造成内存泄漏。

44.介绍一下模版和容器。如何实现?

模版是实现泛型编程的一种机制,它体现了一种通用和泛化的思想。
容器是一种特定用途的类。STL主要容器:

  1. 序列式容器:

    • 向量(vector) 连续存储的元素
    • 列表(list) 由节点组成的双向链表,每个节点包含着一个元素
    • 双端队列(deque) 一种双向开口的连续线性空间,可在头部和尾部插入和删除元素
  2. 适配器容器

    • 栈(stack) 元素值后进先出(LIFO)
    • 队列(queue) 元素值先进先出(FIFO)
    • 优先队列(priority_queue) 元素的次序是由作用于所存储的值对上的某种谓词决定(即优先级,排序准则)的一种队列
  3. 关联式容器

    • 集合(set) 由红黑树实现,内部元素根据其值自动排序,内部元素不允许重复
    • 多重集合(multiset) 和set相同,只不过multiset允许有重复的元素
    • 映射(map) 由{键,值}对组成的集合,由红黑树实现,内部按照键进行排序,不允许有重复的键,可以有重复的值
    • 多重映射(multimap) 与map相同,只不过multimap允许有重复的键
    • 对(pair) 只有一对键值对

45.拷贝构造函数相关的问题,深拷贝,浅拷贝,临时对象等。

深拷贝意味着拷贝了资源和指针,会重新开辟一份内存空间;而浅拷贝只是拷贝了指针,没有拷贝资源(默认是浅拷贝),这样就使得两个指针指向同一份资源,造成对同一份内存析构两次,程序崩溃。所以拷贝构造函数需要使用深拷贝。
临时对象的开销比局部对象小些。

46.请你谈谈你在类中是如何使用const的?

①const修饰成员表示该成员为只读;
②const修饰形式参数,表示不能修改该参数的值;
③const修饰函数,表示该函数不能修改对象的数据成员;
④const修饰函数返回值,表示函数返回值不能作为“左值”,也就不能修改。

47.请你谈谈是如何使用return语句的?

①return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁;
②要搞清楚返回的究竟是“值”、“指针”还是“引用”;
③如果函数返回值是一个对象,要考虑return语句的效率。

48.①return String(s1+s2);和②String temp(s1+s2);return temp;一样吗?

对于①,这是临时对象的语法,表示“创建一个临时对象并返回”。
对于②,将发生三件事。首先,temp对象被创建,同时完成初始化;然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中;最后。temp在函数结束时被销毁(调用析构函数)。
然而,“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了构造和析构的花费,提高了效率。

49.字符指针、浮点数指针、以及函数指针这三种类型的变量哪个占用的内存最大?为什么?

指针变量也占用内存单元,而且所有指针变量占用内存单元的数量都是相同的,即一个机器字长。

50.C++的空类,默认产生哪些成员函数?

1
2
3
4
5
6
7
8
9
class Empty{
public:
Empty(); //缺省构造函数
Empty(const Empty&); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty&); //赋值运算符
Empty& operator&(); //取址运算符
const Empty* operator&() const; //取值运算符const
}

51.静态方法与非静态方法的区别?

1.静态方法属于类所有,类实例化前即可使用;
2.非静态方法可以访问类中的任何成员,静态方法只能访问类中的静态成员;
3.因为静态方法在类实例化前就可以使用,而类中的非静态变量必须在实例化之后才能分配内存;
4.static内部只能出现static变量和其他static方法,而且static方法中还不能使用this等关键字,因为它是属于整个类;
5.静态方法效率上要比实例化高,静态方法的缺点是不自动进行销毁,而实例化的则可以做销毁;
6.静态方法和静态变量创建后始终使用同一块内存,而使用实例的方式会创建多个内存;
主要区别:静态方法在创建对象前就可以使用了,非静态方法必须通过new出来的对象调用。

52.hashmap扩容机制?

当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16 * 0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍(resize方法),然后重新计算每个元素在数组中的位置(即再hash),而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。

53.高内聚低耦合?

内聚:模块内部的代码, 相互之间的联系越强,内聚就越高, 模块的独立性就越好。 一个模块应该尽量的独立,去完成独立的功能;
耦合主要是讲模块与模块之间的联系。
高内聚低耦合一般同时出现,一个模块只实现一个功能,那它的内聚就很高,那么外部模块调用它的机会就很少,即低耦合。

54、C++ vector扩容原理?

新增元素:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;
对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;
初始时刻vector的capacity为0,塞入第一个元素后capacity增加为1;
size是指vector中的元素个数,capacity是指vector的容量
不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。

55.vector和list的区别?

①vector底层实现就是数组,表示它能快速随机访问存储的元素,通过下标 index 访问,数组支持随机访问, 查询速度快, 增删元素慢;
list使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入数度较快!另外,他还可以用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。;
②如果查找一个指定位置的数据,vector使用的时间是0(1),而如果移动一个指定位置的数据花费的时间为0(n-i),n为总长度,这个时候就应该考虑到使用list,因为它移动一个指定位置的数据所花费的时间为0(1),而查询一个指定位置的数据时花费的时间为0(i)。
③vector和list都不是线程安全,它们都是线程异步的,需要自己去实现同步操作。

56.内联函数和宏

1、宏容易出错;
2、宏不可调试;
3、宏无法操作类的私有对象;
4、内联函数可以更加深入的优化;
使用宏代码最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生意向不到的边际效应。例如:
#define MAX(a,b) (a)>(b)?(a):(b)
语句:
result = MAX(i,j)+2
将被预处理器扩展为:
result=(i)>(j)?(i):(j)+2;
由于运算符”+”比运算符”?”的优先级高,所以上述语句并不等价于期望的。

57.子类构造、析构时调用父类的构造、析构函数顺序

定义一个对象先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数,也就是说在基类的析构调用的时候,派生类的信息已经全部销毁了。

58.c++中重载、重写(覆盖)和隐藏(重定义)

  1. 重载
    重载从overload翻译过来,是指同一可访问区内被声明的几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

    • 相同的范围(在同一个作用域中)
    • 函数名字相同
    • 不同参数列表
    • virtual 关键字可有可无
    • 返回类型可以不同
  2. 重写(覆盖)
      重写翻译自override,是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰

    • 不在同一个作用域(分别位于派生类与基类)
    • 函数名字相同
    • 相同参数列表(参数个数,两个参数列表对应的类型)
    • 基类函数必须有 virtual 关键字,不能有 static,大概是多态的原因吧…
    • 返回值类型相同,否则报错
    • 重写函数的访问权限修饰符可以不同。尽管 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的
  3. 隐藏(重定义)
      隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

    • 不在同一个作用域(分别位于派生类与基类)
    • 函数名字相同
    • 返回类型可以不同
    • 参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)而不是被重写
    • 参数相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

59.静态库和动态库?

静态库:

静态库可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。比如在我们日常编程中,如果需要使用printf函数,就需要包stdio.h的库文件,使用strlen时,又需要包string.h的库文件,可是如果直接把对应函数源码编译后形成的.o文件直接提供给我们,将会对我们的管理和使用上造成极大不便,于是可以使用“ar”压缩程序将这些目标文件压缩在一起,形成libx.a静态库文件。
注:静态库命名格式:lib + “库名称”+ .a(后缀) 例:libadd.a就是一个叫add的静态库

  • 静态链接:
    对于静态库,程序在编译链接时,将库的代码链接到可执行文件中,程序运行时不再需要静态库。在使用过程中只需要将库和我们的程序编译后的文件链接在一起就可形成一个可执行文件。
    • 优点:
      1. 发布程序的时候,不需要提供对应的库,因为已经打包到了可执行程序中
      2. 加载库的速度快
    • 缺点:
    1. 内存和磁盘空间浪费:
      静态链接方式对于计算机内存和磁盘的空间浪费十分严重。假如一个c语言的静态库大小为1MB,系统中有100个需要使用到该库文件,采用静态链接的话,就要浪费近100M的内存,若数量再大,那浪费的也就更多。例如:程序1和程序2都需要用到Lib.o,采用静态链接的话,那么物理内存中就会存放两份对应此文件的拷贝。
    2. 更新麻烦:
      一个程序编好后,有时需要做一些修改和优化,如果我们要修改的刚好是库函数的话,在接口不变的前提下,使用静态库的程序则需要将静态库重新编译好后,将程序再重新编译一遍。
动态库:

程序在运行时才去链接动态库的代码,多个程序共享库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。

  • 动态链接:
    由于静态链接具有浪费内存和模块更新困难等问题,提出了动态链接。基本实现思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完整的程序,而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。所以动态链接是将链接过程推迟到了运行时才进行。
    同样,假如有程序1,程序2,和Lib.o三个文件,程序1和程序2在执行时都需要用到Lib.o文件,当运行程序1时,系统首先加载程序1,当发现需要Lib.o文件时,也同样加载到内存,再去加载程序2当发现也同样需要用到Lib.o文件时,则不需要重新加载Lib.o,只需要将程序2和Lib.o文件链接起来即可,内存中始终只存在一份Lib.o文件。
  • 优点:
    1. 毋庸置疑的就是节省内存;
    2. 减少物理页面的换入换出;
    3. 在升级某个模块时,理论上只需要将对应旧的目标文件覆盖掉即可。新版本的目标文件会被自动装载到内存中并且链接起来,即使用动态库的程序只需要将动态库重新编译就可以了
    4. 程序在运行时可以动态的选择加载各种程序模块,实现程序的扩展。
  • 缺点:
    1. 发布程序的时候,需要将动态库提供给用户
    2. 动态库没有被打包到应用程序中,加载速度相对较慢 (其实速度还是挺快的)

60. 智能指针

普通指针存在的问题:

  1. 忘记delete内存
  2. 使用已经释放掉的资源
  3. 同一块内存释放两次
  4. 发生异常时的内存泄漏

智能指针本质

本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。

智能指针的原理:智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类是栈上的对象,智能指针指向堆上开辟的空间,函数结束时,栈上的函数会自动被释放,智能指针指向的内存也会随之消失,防止内存泄漏。

三种智能指针

  1. shared_ptr允许多个指针指向同一个对象,每有一个指针指向该对象引用计数就+1,撤销一个指针引用计数就-1

  2. unique_ptr则“独占”所指向的对象,故不支持拷贝和赋值

  3. weak_ptr指向一个由shared_ptr管理的对象,但不会改变shared_ptr的引用计数。weak_ptr不控制所指向对象的生存期,所以,即使有weak_ptr指向对象,对象也还是会被释放。由于weak_ptr所指对象可能不存在,所以我们不能用weak_ptr直接访问对象,而必须调用lock(),若不存在,则返回一个空shared_ptr,若存在,则返回weak_ptr所指对象的shared_ptr

注意事项

  1. shared_ptr的循环引用问题,解决方法:其中一方使用普通指针或者使用weak_ptr
  2. 切记,让所有的智能指针都有名字,以防止内存泄漏
  3. 优先使用make_unique(shared)而非直接使用new

61. malloc和new的区别

  1. malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化。
  2. malloc是函数,开辟内存需要指定大小,默认返回类型为void,因此malloc的返回值需要强转成指定类型的地址;*new是运算符,** 开辟内存需要指定类型,返回指定类型的地址,因此不需要进行强转,new根据数据类型自动计算出所需要的大小。
  3. malloc开辟内存失败返回NULL,new开辟内存失败抛出bad_alloc类型的异常,需要捕获异常才能判断内存开辟成功或失败。

62. strcpy和memcpy的区别

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
  2. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,如果空间不够,就会引起内存溢出。memcpy则是根据其第3个参数决定复制的长度。
  3. 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy,由于字符串是以“\0”结尾的,所以对于在数据中包含“\0”的数据只能用memcpy。

63. strcpy需要注意的地方

strcpy只是复制字符串,但不限制复制的数量。很容易造成缓冲溢出, 也就是说,不过dest有没有足够的空间来容纳src的字符串,它都会把src指向的字符串全部复制到从dest开始的内存

64. typedef与define声明有何不同?

  • 使用define 宏定义,只是名字的替换
  • 使用typedef 类型定义,出来之后,是一个真真切切的 类型。
    1
    2
    3
    4
    5
    6
    7
    #define BYTE unsigned char      // typedef 与 #define 功能重合

    typedef char * STRING; // #define 没有的功能
    STRING name, sign; //char *name, *sign;

    #define STRING char * //定义多个变量时,导致只有第一个有效。
    STRING name, sign; //char *name, sign;
  1. typedef 这种方式,编译器会把 STRING 解释为一个 类型的标识符 !此过程是编译器处理的!
  2. define 这种方式,只是把名称替换掉而已,此过程是在 预处理器中处理的!

65. 函数参数相关内容

  1. 函数的参数都是原数据的“副本”,因此在函数内无法改变原数据
  2. 函数中参数都是传值,传指针本质上也是传值,只不过它的值是指针类型罢了。
  3. 如果想要改变入参内容,则需要传该入参的地址(指针和引用都是类似的作用),通过解引用修改其指向的内容。

66. gcc和g++

相同:

  • 可以通过执行 gcc 或者 g++ 指令来调用 GCC 编译器。
  • 一般使用 gcc 指令编译 C 语言程序,用 g++ 指令编译 C++ 代码。但gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。

不同:

    • 只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别
    • 但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。
  1. 使用g++ 编译需要按照C++ 标准,而C++ 标准对代码书写规范的要求更加严格;
  2. 很多 C++ 程序都会调用某些标准库中现有的函数或者类对象,而单纯的 gcc 命令是无法自动链接这些标准库文件的。
    • 如果想使用 gcc 指令来编译执行 C++ 程序,需要在使用 gcc 指令时,手动为其添加 -lstdc++ -shared-libgcc 选项,表示 gcc 在编译 C++ 程序时可以链接必要的 C++ 标准库

67. C++四种类型转换及其作用

  1. static_cast(静态转换)
    • static_cast<目标类型>(原始数据)
    • 可以进行基础数据类型转换
    • 父类与子类之间指针或引用的类型转换(向上或向下转换都可以)
    • 没有父子关系的自定义类型之间不可以转换
    • 把空指针转换成目标类型的空指针
    • 把任何类型的表达式转换为void类型
  2. dynamic_cast(动态装换)
    • dynamic_cast是运行时处理的,运行时要进行类型检查。
    • 主要用于类层次结构中父类和子类的上行转换和下行转换
    • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的
    • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全,此时基类中一定要有虚函数,否则编译不通过
    • dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
    • 基础类型不可以转换
    • dynamic_cast非常严格,失去精度或者不安全都不可以转换
  3. const_cast(常量转换)
    • 用来修改类型的const属性
    • 常量指针被转化成非常量指针(或反过来),并且仍然指向原来的对象;
    • 常量引用被转换成非常量引用(或反过来),并且仍然指向原来的对象;
    • 注意: 不能直接对非指针和非引用的变量使用const_cast操作符去直接移除它的const
  4. reinterpret_cast(重新解释转换) – 最不安全,不推荐
    • 用法:reinterpret_cast (原始数据)
    • type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
    • 用于进行没有任何关联之间的转换,比如一个字符指针转换为一个整形数
    • 改变指针或引用的类型
    • 将整型转换为指针或引用类型。
    • 可以将指针或引用转换为一个足够长度的整型,此中的足够长度具体长度需要多少则取决于操作系统,如果是32位的操作系统,就需要4个字节及以上的整型,如果是64位的操作系统则需要8个字节及以上的整型。

68. C++对C的增强

1. 全局变量检测增强

C语言会忽略对全局变量重定义的检测,但不会忽略对局部变量的检测,C++中都会报错

1
2
3
//C语言编译通过,C++编译失败
int a;
int a = 10;

2. 函数检测增强

包括函数形参类型检测,形参数目检测,函数返回值检测,C都会忽略,C++不会

1
2
3
4
5
6
7
8
//C中函数形参没有参数类型,没有返回值,调用参数过多都会忽略
int test(m, n){

}

void test01(){
test(1,2,3);
}

3. 类型转换检测增强

C语言中malloc开辟内存空间时默认生成void*指针,可以转换成任意指针,C++ 中则不行,必须显式的进行强制转换。在C++中,不同类型的变量一般是不能直接赋值的,需要相应的强转

1
2
//C语言能编译通过,C++编译失败
char *p = malloc(64);

4. struct增强

  • C中strcut中不能有函数,C++中可以有,并且与class的区别在于默认继承权限和成员的默认访问权限不同。
  • 通过如下方式声明struct时,C语言定义使用结构体时必须使用struct,C++可以不用。
    1
    2
    3
    4
    5
    6
    struct Person{
    int a;
    };

    struct Person myperson; //C
    Person myperson; //C++

    5. bool类型增强

    C语言中没有bool类型,C++中有bool类型,其中sizeof(bool)=1

6. 三目运算符增强

a > b?a : b;

C语言中返回的是值,C++ 中返回的是变量本身,所以C++ 中三目运算符表达式返回值可以做左值

7. const增强

(1)是否可以修改

C语言中的全局const不可修改,是真常量,如果对其修改会出现访问冲突,另外不可以声明数组的大小(这是C的缺陷,也是为什么替代不了define);局部const为伪常量,可以进行修改,同时不可以用于声明数组的大小(真假都不可以声明数组的大小)。

1
2
3
4
5
6
7
8
9
const int m = 0; //全局静态变量受到保护,不可修改
void test01(){
const int n = 1; //伪常量,可以通过地址进行修改
int *p = (int *)&n; //可以不加强制转换
*p = 100;
printf("%d\n", n);

int am[n]; //n不可用于声明大小,不是常量值
}

C++不管全局还是局部都是真常量,不可修改,同时可以初始化数组,原因如下(取地址时会分配临时内存):

(2)链接属性

C语言的const默认是外部链接,C++默认是内部链接

1
2
3
4
5
//1.cpp
const int a = 10;

//2.cpp
extern const int a;

C语言中进行访问时可以的,但C++中需要在1.cpp的声明前加extern,则2.cpp才可以访问

(3)const分配内存

是否分配内存,我们可以根据const修饰的变量是否能够修改来确定。

编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高,但是在下列几种情况下编译器会为const定义的常量分配内存的。

  1. 取地址时,const会分配临时内存,不可以进行间接修改
    1
    2
    3
    4
    5
    6
    void test003(){
    const int m = 10; //用常量给m赋值,此处不分配内存
    int *p = (int *)&m;//分配临时内存,不可以进行间接修改
    *p = 100;
    cout << m << endl;
    }
  2. 加上extern关键字,编译器也会为变量分配内存
    • 因为使用了extern,我们将可能在外部文件使用该变量,而const默认的是内部链接,所以我们必须要为之分配内存的。
  3. 用普通变量初始化const变量,会分配内存
    1
    2
    3
    4
    5
    6
    7
    void test003(){
    int m = 10;
    const int b = m; //会分配内存,所以可以通过指针修改b
    int *p = (int *)&b;
    *p = 100;
    cout << b << endl; //100
    }
  4. 自定义数据,加const也会分配内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct Person
    {
    int m_age;
    };
    void test03()
    {
    const Person p1; //会分配内存,通过指针可以修改
    Person *p = (Person*) &p1;
    p->m_age = 10;
    }

69. 指针常量和常量指针

1. 概念

  • 指针常量就是指针本身是常量(本质是一个常量,只不过这个常量的类型是一个指针),换句话说,就是指针里面所存储的内容(内存地址)是常量,即指针的指向不能改变。但是,指针所指的内存地址所对应的内容是可以通过指针改变的。
  • 常量指针就是指向常量的指针(本质是一个指针,指向一个常量),换句话说,就是指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

    2. 声明

  • 指针常量的声明:数据类型 * const 指针变量。(char* const p),const在类型说明符后
  • 常量指针的声明:数据类型 const * 指针变量 或者 const 数据类型 指针变量。(`const char p或char const *p`),const在类型说明符前
  • 常指针常量的声明:数据类型 const * const 指针变量 或者 const 数据类型 * const 指针变量。(char const * const p或const char * const p

70. const和define

区别

  1. define在预编译阶段起作用,const在编译、运行的时候起作用;
  2. define只是简单的替换功能,没有类型检查功能,const有类型检查功能,可以避免一些错误
  3. define在预编译阶段就替换掉了,无法调试,const可以通过集成化工具调试

为什么鼓励用const代替define

  1. const有类型,可进行编译器类型安全检查,#define无类型,不可进行类型检查;
  2. const有作用域,而#define不重视作用域,默认定义处到文件结尾,如果定义在指定作用域下有效的常量,那么#define就不能用(当然可以用#undef解除作用域)
  3. 假如#define MAX 1024我们定义的宏MAX从来没被编译器看到过,因为在预处理阶段,所有的MAX已经被替换为了1024,于是MAX并没有将其加入到符号表中。但我们使用这个常量获得一个编译错误信息时,可能会带来一些困惑,因为这个信息可能会提到1024,但是并没有提到MAX,如果MAX被定义在一个你写的头文件中,你可能并不知道1024代表什么。

define是应用于预处理的,而const是在编译的时候处理的。对于单纯常量,使用const与enum代替宏,对于函数形式的宏,则使用inline与template替代。

71. const和volatile关键字

const

  1. const使得变量具有只读属性(但是不一定就是不能更改)
  2. const不能定义真正意义上的常量(因为有的用const定义的变量,仍然可以更改)
  3. const将具有全局生命期的变量存储于只读存储区(这个是对现代编译器是这样的,但是对ANSI编译器,仍然可以更改)
    • const修饰全局变量和静态局部变量–>只读,值不能改变,存储在只读存储区
    • const修饰局部变量–>只读,值可改变,存储在栈区

volatile

volatile的本意是“易变的”,因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错

用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能重复使用放在cache或寄存器中的备份。

volatile使用场景

volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:(多个变量之间或者某个变量的当前值和修改值之间没有约束)因此单独使用volatile还不足以实现计数器、互斥锁或者具有多个变量相关的不变式的类

1
2
3
4
对于简易性和可伸缩性的考虑,你可能更倾向于使用volatile而不是锁。
当使用volatile变量而不是锁的时候,某些习惯用法更易于编码和阅读。
因此,volatile变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。
在某些情况下,如果读的操作远远大于写的操作,volatile变量还可以提供优于锁的性能优势

只能在有限的一些情形下使用volatile变量代替锁。(也就是保证原子性)要使volatile变量提供理想的线程安全,必须同时满足下面两个条件

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明了,可以被写入volatile变量的这些有效值独立于任何程序的状态,包括变量的当前状态。(没有依赖)

为什么有了volatile还需要锁和为什么有了锁还需要volatile

  • volatile虽然能保证多线程的可见性,但不能保证原子性,所以还是会存在资源抢夺的问题
  • 加锁会有性能消耗
  • 加锁会产生阻塞
  • 在不需要保证原子性,只需要可见性的时候可以用volatile而不用加锁

72. 什么函数不能声明为虚函数?

常见的不能声明为虚函数的有:普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数

  1. 普通函数不能声明为虚函数。普通函数(非成员函数)只能被重载(overload),不能被重写(override),声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
  2. 构造函数不能声明为虚函数。构造函数一般用来初始化对象,只有在一个对象生成之后,才能发挥多态作用。如果将构造函数声明为虚函数,则表现为在对象还没有生成的时候来定义它的多态,这两点是不统一的。另外,构造函数不能被继承,因而不能声明为虚函数。
  3. 静态成员函数不能声明为虚函数。静态成员函数对于每个类来说只有一份代码,所有的对象都共享这份代码,它不归某个对象所有,所以也没有动态绑定的必要性。
  4. 内联(inline)成员函数不能声明为虚函数。内联函数就是为了在代码中直接展开,减少函数调用开销的代价。虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。另外,内联函数在编译时被展开,虚函数在运行时才能动态的绑定函数。
  5. 友元函数不能声明为虚函数。友元函数不属于类的成员函数,不能被继承。

73. 野指针

是什么?

野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

野指针不同于空指针,空指针是指一个指针的值为null,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针。

危害:

  • 野指针很可能触发运行时段错误( Sgmentation fault)
  • 因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。

产生原因:

  1. 指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会随机指向一个区域,因为任意指针变量(除了static修饰的指针)它的默认值都是随机的
  2. 指针所指向的地址空间已经被free或delete:在堆上malloc或者new出来的地址空间,如果已经free或delete,那么此时堆上的内存已经被释放,但是指向该内存的指针如果没有人为的修改过,那么指针还会继续指向这段堆上已经被释放的内存,这时还通过该指针去访问堆上的内存,就会造成不可预知的结果,给程序带来隐患,所以良好的编程习惯是:内存被free或delete后,指向该内存的指针马上置空。
  3. 指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。

74. 派生类的构造函数

  1. 如果基类拥有构造函数但没有默认构造函数,那么派生类必须显式地调用基类的某个构造函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class A{
    public
    A(int n){a=n};
    private
    int a;
    };

    class B:public A{
    public:
    B(int n):A(n){b=n}
    private:
    int b;
    };
  2. 如果基类有默认构造函数(未声明或显式声明),派生类也定义了一些构造函数,但是派生类的任何构造函数都没有显式地调用基类的某个构造函数,那么当创建一个派生类对象时,基类的默认构造函数会被自动调用。此时不用在派生类中显式调用基类的构造函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class A{
    public
    A();
    A(int n){a=n};
    private
    int a;
    };

    class B:public A{
    public:
    //调用A的默认构造函数
    B();//定义一个B的对象时,会先调用A(),再调用B();
    private:
    int b;
    };

总结: 派生类如果不想显式调用基类的构造函数,基类必须有有效的默认构造函数,否则派生类必须要显式调用基类的某一个构造函数。

75. 拷贝构造函数和赋值函数区别

  1. 拷贝构造函数是在对象初始化时,分配一块空间并初始化,而赋值函数是对已经分配空间的对象进行赋值操作;
  2. 实现上,拷贝构造函数是构造函数,通过参数的对象初始化产生一个对象。赋值函数则是把一个对象赋给另一个对象,需要先判断两个对象是否是同一个对象,若是则什么都不做,直接返回,若不是则需要先释放原对象内存,再赋值。

总结

  • 对象不存在,没有通过别的对象来初始化,就是构造函数;
  • 对象不存在,通过别的对象来初始化,就是拷贝构造函数;
  • 对象存在,通过别的对象来初始化,就是赋值函数

76. 有static、virtual之类的一个类的内存分布

  • static修饰成员变量
    • 静态成员变量在全局存储区分配内存,本类的所有对象共享,在还没生成类对象之前也可以使用
  • static修饰成员函数
    • 静态成员函数在代码区分配内存。静态成员函数和非静态成员函数的区别在于非静态成员函数存在this指针,而静态成员函数,所以静态成员函数没有类对象也可以调用
  • virtual
    • 虚函数表存储在常量区,也就是只读数据段
    • 虚函数指针存储在对象内,如果对象是局部变量,则存储在栈区内。

77. inline和define

  1. inline在编译时展开,define在预编译时展开
  2. inline可以进行类型安全检查,define只是简单的替换
  3. inline是函数,define不是函数
  4. define最好用括号括起来,不然会产生二义性,inline不会
  5. inline是一个建议,可以不展开,define一定会展开

78. inline函数要求

  1. 含有递归调用的函数不能设置为inline
  2. 循环语句和switch语句,无法设置为inline
  3. inline函数内的代码应很短小,最好不超过5行

79. static的作用

  1. 修饰全局变量时,用于限制该全局变量的使用范围。仅能在本文件属内使用该变量。
  2. 修饰局部变量时,用于修改该变量的存储控件类型。普通局部变量存储在栈区,当函数执行结束后,就会被清空。静态局部变量存储在静态区,当函数执行结束后,不会被清空。下次再次执行函数时,能保持上一回的值。
  3. 修饰类的成员变量和成员函数
    • 修饰类的成员变量时。就变成静态成员变量,不属于对象,而属于类。不能在类的内部初始化,类中只能声明,定义需要在类外。类外定义时,不用加static关键字,只需要表明类的作用域。
    • 修饰类的成员函数时。变成静态成员函数,也不属于对象,属于类。形参不会生成this指针,仅能访问类的静态数据和静态成员函数。调用不依赖对象,所以不能作为虚函数。用类的作用域调用。

80. 菱形继承

问题

B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。

这种继承方式在使用成员变量和成员函数的时候就会出现二义性,即使成员函数有不同的形参表也会出现二义性问题,同时还会浪费内存空间

虚继承

C++使用虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A
{
public:
int _a;
};

class B : virtual public A
{
public:
int _b;
};

class C : virtual public A
{
public:
int _c;
};

class D : public B, public C
{
public:
int _d;
};


将class B 和 class C设置为虚继承后,编译器将class A存放在了最下端,并在B和C类的前四个字节中存放了一个地址,当我们访问过去向下再多看四个字节时就会发现这其中存放了一个数字。而这个数字就类似于“偏移量”,记录了该类的首地址距父类首地址之间的字节差距。比如class B中,我们找到对应数字为14,但是这个数字是16进制,转为10进制为20,在class B的首地址加上20个字节就恰好是class A的首地址,同理class C。

81. 虚函数和纯虚函数

  • 类成员方法的生米前面加上virtual就变为虚函数
  • 在虚函数声明语句末尾加一个0就摇身变为纯虚函数
  • 含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类
  • 虚函数可以被直接使用,也可以被子类重载以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类只有声明而没有定义。
  • 如果一个类中含有纯虚函数,则该类不能实例化
    • 原因: 纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。

82.C++的五种编程范型

  1. 面向过程
  2. 基于对象(封装)
  3. 面向对象(封装、继承、多态)
  4. 泛型编程
  5. 函数式编程(lambda表达式)

—————————————-如有错误,欢迎指正!—————————————-

评论