C++31~50

^ _ ^摘自阿秀

31、形参和实参的区别?

  1. 形参只有在函数内部有效,在被调用时分配内存,在调用结束后,释放分配的内存单元。
  2. 实参在传参时必须要有确定的值。
  3. 实参和形参应在数量上、类型上、顺序上应严格一致,否则会发生类型不匹配。
  4. 函数调用发生的数据传送时单向的,只能把实参的值传送给形参。
  5. 当形参和实参不是指针的时候,他们是不同的变量,在内存中位于不同的位置。

32、值传递、指针传递、引用传递的区别和效率

大佬博客讲得好

值传递就是最普通的传递方式,比如函数定义为fun(int a),在调用的地方有int x=6, 使用fun(x)就可以了。这种方式在fun(int a)函数内部的对a的修改 不能导致外部x的变化。

指针传递其实也就是地址传递,函数定义为fun(int *a),形参为指针,这就要求调用的时候传递进去一个参数的地址,例如int x=6; fun(&x)。 这种方式在fun(int a)函数内部的对a的修改导致外部x的变化。

引用传递只有C++支持,相比前两种方式用的比较少,但也非常有用。引用传递函数定义为fun(int &a),这里&符号是引用而不是取地址的意思,调用方式和值传递类似,例如int x=6; fun(x)。 但是这种方式在fun(int a)函数内部的对a的修改导致外部x的变化。

效率方面:指针传递和引用传递效率比值传递效率高,一般主张用引用传递,代码逻辑上更加紧凑。(Tip:要去看C++1~30中的指针和引用的区别

33、静态变量什么时候初始化?

静态变量的初始化时间
静态变量存储在虚拟地址空间的数据段和bss段。

  • C语言中其在代码执行之前初始化,属于编译期初始化。
  • C++中,由于对象的引入,对象的生成必须要调用构造函数,因此,C++规定全局或者局部静态对象当且仅当对象首次用到时进行构造。

34、类的继承是什么?

类和类之间的关系

has-A包含关系,用以描述一个类由多个部件类,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类

use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现。

is-A继承关系,关系具有传递性。

继承的相关概念

所谓继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类。

继承的特点

子类拥有父类的所有属性和方法,子类可以拥有父类没有属性和方法,子类对象可以当作父类对象使用。

继承中访问控制

35、面向对象的特性

①封装

  • 封装:隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互,将数据和操作数据的方法进行有机结合。
  • 访问限定符
    (1)public(共有)
    (2)protected(保护)
    (3)private(私有)

②继承

继承机制是面向对象程序设计使代码可以复用的最重要的手段
继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用

③多态

静态多态

静态多态,也叫早绑定(编译期多态)

一个类有多个重名函数,但是参数和返回值不同,这是函数的重载。实例化这个类的对象时,通过对象调用这两个函数,计算机在编译时,就会自动调用对应的函数。
即程序运行之前,在编译阶段就已经确定下来到底要使用哪个函数,
可见:很早就已经将函数编译进去了,称这种情况为早绑定或静态

动态多态

动态多态,也叫晚绑定(运行期多态)

动态多态是有前提的,它必须以封装和继承为基础。在封装中,将所有数据封装到类中,在继承中,又将封装着的各个类使其形成继承关系
如果想要实现动态多态,就必须使用 虚函数

运行期多态发生的三个条件:继承关系、虚函数覆盖、父类指针或引用指向子类对象

在父类中,把想要实现多态的成员函数前加上virtual 关键字,使其成为虚函数
使用父类指针指向子类对象,调用函数时,调用的就是对应子类的函数

36、虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”。

  1. 通过父类型的指针访问子类自己的虚函数
  2. 当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定
  3. 虚函数的实现过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。
  4. 虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数亦或是纯虚函数,亦或是派生类中隐式声明的这些虚函数都会生成这张虚函数表。
  5. 虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。

37、构造函数为什么不能是虚函数

当我们通过new来创建一个对象的时候,第一步是申请需要的内存,第二步就是调用构造函数。试想,如果构造函数是虚函数,那必然需要通过vtbl来找到虚构造函数的入口地址,显然,我们申请的内存还没有做任何初始化,不可能有vtbl的。因此,构造函数不能是虚函数。

38、析构函数为为什么要定义为虚函数

在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生。

解释:
基本上只有涉及到 virtual 函数时,才会发生动态绑定,如果基类的析构函数不是虚函数,那么当基类的指针指向派生类时,delete的时候,只会调用基类的析构函数,而不会调用派生类的析构函数。

39、虚函数和纯虚函数的区别

C++ 中虚函数和纯虚函数都可以在子类中被重写

区别

1、纯虚函数只有定义,没有实现;而虚函数既有定义,也有实现的代码。

纯虚函数一般没有代码实现部分,如 virtual void print() = 0; 2)而一般虚函数必须要有代码的实现部分,否则会出现函数未定义的错误。

2、包含纯虚函数的类不能定义其对象,而包含虚函数的则可以。包含纯虚函数的类是抽象类

40、为什么要有虚函数,还要有纯虚函数?

  • 1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  • 2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

41、new和malloc的区别

  1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  2. new申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显示地指出所需内存的大小
  3. new分配内存成功会返回与对象类型严格匹配的指针,故new时符合类型安全性的操作符。而malloc内存分配成功则返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型。
  4. new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL
  5. new会先调用operator new函数,申请足够内存(底层用malloc实现)。然后调用类型构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(底层用free实现)。
  6. malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

42、类成员初始化方式?

  1. 赋值初始化,通过在函数体内进行赋值初始化;
  2. 列表初始化,在冒号后使用初始化列表进行初始化。

区别
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

初始化列表是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式,那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体未执行。

43、构造函数、析构函数的执行顺序:

继承关系的构造函数和析构函数的执行顺序为:

1、父类构造函数执行。

2、子类构造函数执行。

3、子类析构函数执行。

4、父类析构函数执行。

组合关系的构造函数和析构函数执行顺序为:

1、执行类成员对象的构造函数。

2、执行类自己的构造函数。

3、执行类自己的析构函数。

4、执行类成员的析构函数。

44、C++的四种强制转化

①reinterpret_cast

const_cast<type_id>(expression)
type_id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型间强制转换。

②const_cast

const_cast<type_id>(expression)
该运算符用来修饰类型的const或volatile属性。除了const或volatile修饰之外,type_id和expression的类型是一样的。用法如下:

  • 常量指针被转换成非常量的指针,并且仍然指向原来的对象。
  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象。
  • const_cast一般用于修改底指针。如const char *p形式

③static_cast(编译时转换)

static_cast<type_id>(expression)
把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。主要用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用 引用的转换
    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的。
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,没有动态类型检查,所以是不安全的。
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum
  • 空指针转换成目标类型的空指针
  • 任何类型的表达式转换成void类型
    int val = 110119;
    static_cast<void>(val);

static_cats不能转换掉expression中的const、volatile、或者_unaligned属性

④dynamic_cast

必须有继承关系的类之间才能转换,并且在基类中有虚函数才可以,可以进行向上(派生类向基类)、向下(基类向派生类),或者横向的转换。
有一种特殊的情况就是可以把类指针转换成void*类型。

45、结构体大小计算

bb写的,bb写的

46、静态类型和动态类型,静态绑定和动态绑定的介绍

  • 静态类型:对象在声明时采用的类型,在编译器已确定
  • 动态类型:通常时指一个指针或引用目前所指对象的类型,是在运行期决定的
  • 静态绑定:在编译期绑定的是静态类型,默认参数是静态绑定。
  • 动态绑定:在运行期绑定动态类型,virtual是动态绑定。
class A{
	void func(){cout << "A:func()\n"; }
}
class B : public A{
	void func(){cout << "B:func()\n"; }
}
class C : public A{
	void func(){cocut << "C:func()\n"; }
}
int main(){
	C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;  
	B* pb = new B(); //pb的静态类型和动态类型也都是B*;  
	A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;  
	pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;  
	C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL; 
	pa->func(); //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();  
	pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func();  
	pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;  
	return 0;
}

若是虚函数的话:

pa->func(); //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用; 
pc->func(); //C::func() pc的动、静态类型都是C*,因此也是先在C中查找; 
pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;

47、左值&右值

48、引用是否能实现动态绑定,为什么可以实现?

可以。
引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数,注意只能调用虚函数。只有虚函数具有动态绑定。

49、全局变量和局部变量的有什么区别?

  1. 生命周期不同
    全局变量随主程序创建而创建,随主程序销毁而销毁;
    局部变量在局部函数内部,甚至局部循环体内部存在,退出就不存在。
  2. 使用方式不同
    全局变量分配在全局/静态存储区,通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

50、类如何实现只能静态分配和只能动态分配

  1. 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建。
  2. 建立类的对象有两种方式:
    • 静态建立,静态建立一个类对象,就是由编译器为对象在空间中分配内存。
    • 动态建立,A* p = new A();动态建立一个类对象,就是使用new运算符为对象在空间中分配内存。这个过程分为2步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象。
  3. 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值