当前位置:首页 > 公众号精选 > C语言与CPP编程
[导读]多态什么是多态,有什么用C多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。实现:C多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和o...

多态

什么是多态,有什么用

C 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。

  • 定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。
  • 实现:C 多态性主要是通过虚函数实现的,虚函数允许子类重写 override(注意和 overload 的区别,overload 是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
注:多态与非多态的实质区别就是函数地址是静态绑定还是动态绑定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并产生代码,说明地址是静态绑定的;如果函数调用的地址是需要在运行期间才确定,属于动态绑定。

  • 目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
  • 用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
用一句话概括:在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

重写、重载与隐藏的区别

Overload 重载

在 C 程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。

  • 相同的范围(在同一个类中);
  • 函数名字相同;
  • 参数不同;
  • virtual 关键字可有可无;

Override(覆盖或重写)

是指派生类函数覆盖基类函数,特征是:

  • 不同的范围(分别位于派生类与基类);
  • 函数名字相同;参数相同;
  • 基类函数必须有 virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为 virtual 函数,不管有没有加 virtual,因此重写的时候不加 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 用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。

全局变量

在全局变量之前加上关键字 static,全局变量就被定义成为一个全局静态变量。

  • 内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
  • 初始化:未经初始化的全局静态变量会被程序自动初始化为 0(自动对象的值是任意的,除非他被显示初始化)
  • 作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
注:static 修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:

  • 不会被其他文件所访问,修改
  • 其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的 .c 文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。
注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。

对类

成员变量

用 static 修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static 成员必须在类外进行初始化(初始化格式:int base::var=10;),而不能在构造函数内进行初始化,不过也可以用 const 修饰 static 数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

特点:

  • 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
  • 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
  • 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。

成员函数

  • 用 static 修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含 this 指针。
  • 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当 static 成员函数在类外定义时不需要加 static 修饰符。
  • 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
不可以同时用 const 和 static 修饰成员函数。

C 编译器在实现 const 的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数 const this*。但当一个成员为 static 的时候,该函数是没有 this 指针的。也就是说此时 const 的用法和 static 是冲突的。

我们也可以这样理解:两者的语意是矛盾的。static 的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而 const 的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

const的作用:

  • 限定变量为不可修改。
  • 限定成员函数不可以修改任何数据成员。
  • const 与指针:
const char *p 常量指针,可以换方向,不可以改内容

char * const p,指针常量,不可以换方向,可以改内容

构造函数

构造函数调用顺序

  • 虚基类构造函数(被继承的顺序)
  • 非虚基类构造函数(被继承的顺序)
  • 成员对象构造函数(声明顺序)
  • 自己的构造函数

自身构造函数顺序

  • 虚表指针(防止初始化列表里面调用虚函数,否则调用的是父类的虚函数)
  • 初始化列表(const、引用、没有定义默认构造函数的类型)
  • 花括号里的 (初始化列表直接初始化,这个先初始化后赋值)

this 指针

创建时间:成员函数调用前生成,调用后清除

如何传递给成员函数:通过函数参数的首参数来传递

extern 关键字

  • 置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
  • extern “C” void fun(); 告诉编译器按C的规则去翻译

以下关键字的作用?使用场景?

  • inline:在 c/c 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
  • decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。
  • volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

浅拷贝与深拷贝

什么时候用到拷贝函数?

  • 一个对象以值传递的方式传入函数体(参数);
  • 一个对象以值传递的方式从函数返回(返回值);
  • 一个对象需要通过另外一个对象进行初始化(初始化)。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝

默认拷贝构造函数是浅拷贝。如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如 A=B。这时,如果 B 中有一个成员变量指针已经申请了内存,那 A 中的那个成员变量也指向同一块内存。这就出现了问题:当 B 把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

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

用法:dynamic_cast < type-id > ( expression )

dynamic_cast 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(同一基类的两个同级派生类)。

在类层次间进行上行转换时,dynamic_caststatic_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
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭