C 八股文(一)
时间:2021-09-18 17:45:37
手机看文章
扫描二维码
随时随地手机看文章
[导读]多态什么是多态,有什么用C多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。实现:C多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和o...
多态
什么是多态,有什么用
C 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。- 定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。
- 实现:C 多态性主要是通过虚函数实现的,虚函数允许子类重写 override(注意和 overload 的区别,overload 是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
- 目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
- 用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
重写、重载与隐藏的区别
Overload 重载
在 C 程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无;
Override(覆盖或重写)
是指派生类函数覆盖基类函数,特征是:- 不同的范围(分别位于派生类与基类);
- 函数名字相同;参数相同;
- 基类函数必须有 virtual 关键字。
Overwrite(重写)隐藏,
是指派生类的函数屏蔽了与其同名的基类函数,规则如下:- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
虚函数和纯虚函数
- 虚函数:为了实现动态绑定。使用基类的引用或指针调用虚函数的时候会发生动态绑定。
- 纯虚函数:抽象类
- 构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。
基类为什么需要虚析构函数?
防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。构造/析构函数调用虚函数
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果。虚函数表
- 产生时间:编译期
- 存储位置:只读数据段 .rodata
- 虚指针:类的每一个对象都包含一个虚指针(指向虚表),存在对象实例的最前面四个字节
- 虚指针创建时间:构造函数
const 相关
如何初始化 const 和 static 数据成员?
通常在类外申明 static 成员,但是 static const 的整型( bool,char,int,long )可以在类中声明且初始化,static const 的其他类型必须在类外初始化(包括整型数组)。static 和 const 分别怎么用,类里面 static 和 const 可以同时修饰成员函数吗?
static 的作用:对 static 的三条作用做一句话总结。首先 static 的最主要功能是隐藏,其次因为 static 变量存放在静态存储区,所以它具备持久性和默认值 0。对变量
局部变量
在局部变量之前加上关键字 static,局部变量就被定义成为一个局部静态变量。- 内存中的位置:静态存储区
- 初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
- 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。
全局变量
在全局变量之前加上关键字 static,全局变量就被定义成为一个全局静态变量。- 内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
- 初始化:未经初始化的全局静态变量会被程序自动初始化为 0(自动对象的值是任意的,除非他被显示初始化)
- 作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
- 不会被其他文件所访问,修改
- 其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的 .c 文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。
对类
成员变量
用 static 修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static 成员必须在类外进行初始化(初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用 const 修饰 static 数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。特点:- 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上
#ifndef
#define
#endif
或者#pragma once
也不行。 - 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
- 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。
成员函数
- 用 static 修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含 this 指针。
- 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
base::func(5,3)
;当 static 成员函数在类外定义时不需要加 static 修饰符。 - 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
- 限定变量为不可修改。
- 限定成员函数不可以修改任何数据成员。
- const 与指针:
const char *p
常量指针,可以换方向,不可以改内容char * const p
,指针常量,不可以换方向,可以改内容构造函数
构造函数调用顺序
- 虚基类构造函数(被继承的顺序)
- 非虚基类构造函数(被继承的顺序)
- 成员对象构造函数(声明顺序)
- 自己的构造函数
自身构造函数顺序
- 虚表指针(防止初始化列表里面调用虚函数,否则调用的是父类的虚函数)
- 初始化列表(const、引用、没有定义默认构造函数的类型)
- 花括号里的 (初始化列表直接初始化,这个先初始化后赋值)
this 指针
创建时间:成员函数调用前生成,调用后清除如何传递给成员函数:通过函数参数的首参数来传递extern 关键字
- 置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
- extern “C” void fun(); 告诉编译器按C的规则去翻译
以下关键字的作用?使用场景?
- inline:在 c/c 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
- decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。
- volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
浅拷贝与深拷贝
什么时候用到拷贝函数?- 一个对象以值传递的方式传入函数体(参数);
- 一个对象以值传递的方式从函数返回(返回值);
- 一个对象需要通过另外一个对象进行初始化(初始化)。
C 类中成员初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。类中 const 成员常量必须在构造函数初始化列表中初始化。类中 static 成员变量,只能在类外初始化(同一类的所有实例共享静态成员变量)。构造过程
- 分配内存
- 进行父类的构造,按照父类的声明顺序(递归过程)
- 构造虚表指针,对虚表指针赋值
- 根据初始化列表中的值初始化变量
- 执行构造函数{}内的
构造函数初始化列表
const 或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。与对数据成员赋值的区别:- 内置数据类型,复合类型(指针,引用):结果和性能上相同。
- 用户定义类型(类类型):结果上相同,但是性能上存在很大的差别。
vector 中 size() 和 capacity() 的区别
size() 指容器当前拥有的元素个数(对应的resize(size_type)
会在容器尾添加或删除一些元素,来调整容器中实际的内容,使容器达到指定的大小。);capacity()
指容器在必须分配存储空间之前可以存储的元素总数。size 表示的这个 vector 里容纳了多少个元素,capacity 表示 vector 能够容纳多少元素,它们的不同是在于 vector 的 size 是 2 倍增长的。如果 vector 的大小不够了,比如现在的 capacity 是 4,插入到第五个元素的时候,发现不够了,此时会给他重新分配 8 个空间,把原来的数据及新的数据复制到这个新分配的空间里。(会有迭代器失效的问题)定义一个空类编译器做了哪些操作
如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是 inline 和 public 的。强制类型转换
static_cast用法:static_cast < type-id > ( expression )
q1. 为什么需要 static_cast 强制转换?- void指针->其他类型指针 (不安全)
- 改变通常的标准转换
- 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。
dynamic_cast < type-id > ( expression )
dynamic_cast 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(同一基类的两个同级派生类)。在类层次间进行上行转换时,dynamic_cast
和static_cast
的效果是一样的;在进行下行转换时,dynamic_cast
具有类型检查的功能,比static_cast
更安全。reinpreter_cast它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。const_cast该运算符用来修改类型的 const 或 volatile 属性。除了 const 或 volatile 修饰之外, type_id 和 expression 的类型是一样的。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。volatile 关键字- 使用方法:
int volatile x
; - 作用:编译器不再优化。让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值 。
内存管理
C 内存分配
- malloc:在内存的动态分配区域中分配一个长度为 size 的连续空间,如果分配成功,则返回所分配内存空间的首地址,否则返回 NULL,申请的内存不会初始化。
- calloc:分配一个 num * size 连续的空间,会自动初始化为0。
- realloc:动态分配一个长度为 size 的内存空间,并把内存空间的首地址赋值给 ptr,把 ptr 内存空间调整为 size。
C 内存分配:
-栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。- 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。- - 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
- 代码区:存放程序的二进制代码。
结构体字节对齐问题?结构体/类大小的计算?
默认字节对齐
各成员变量存放的起始地址相对于结构的起始地址的偏移量必须是该变量的类型所占用的字节数的倍数,结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数 n 字节对齐。pragma pack(n)
- 如果 n 大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式;
- 如果 n 小于该变量的类型所占用的字节数,那么偏移量为 n 的倍数,不用满足默认的对齐方式;
- 如果 n 大于所有成员变量类型所占用的字节数,那么结构体的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数(两者相比,取小);
虚函数的大小计算
假设经过成员对齐后的类的大小为 size 个字节。那么类的 sizeof 大小可以这么计算:size 4*(虚函数指针的个数 n)。联合体的大小计算
联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:- 大小足够容纳最宽的成员;
- 大小能被其包含的所有基本数据类型的大小所整除。
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
指针和引用
区别
- 定义:指针是一个对象,引用本身不是对象,只是另一个对象的别名;
- 指针是“指向”另外一种类型的复合类型;
- 引用本身不是一个对象,所以不能定义引用的引用;
- 引用只能绑定到对象上,它只是一个对象的别名,因此引用必须初始化,且不能更换引用对象。
指针
可以有 const 指针,但是没有 const 引用(const 引用可读不可改,与绑定对象是否为 const 无关)注:引用可以指向常量,也可以指向变量。例如int