1 多态的实现
存在虚函数的类至少有一个(多继承会有多个)一维的虚函数表叫做虚表(virtual table),属于类成员,虚表的元素值是虚函数的入口地址,在编译时就已经为其在数据端分配了空间。编译器另外还为每个类的对象提供一个虚表指针(vptr),指向虚表入口地址,属于对象成员。在实例化派生类对象时,先实例化基类,将基类的虚表入口地址赋值给基类的虚表指针,当基类构造函数执行完时,再将派生类的虚表入口地址赋值给基类的虚表指针(派生类和基类此时共享一个虚表指针,并没有各自都生成一个),在执行父类的构造函数。
以上是C++多态的实现过程,可以得出结论:
- 1 有虚函数的类必存在一个虚表。
- 2 虚表的构建:基类的虚表构建,先填上虚析构函数的入口地址,之后所有虚函数的入口地址按在类中声明顺序填入虚表;派生类的虚表构建,先将基类的虚表内容复制到派生类虚表中,如果派生类覆盖了基类的虚函数,则虚表中对应的虚函数入口地址也会被覆盖,为了后面寻址的一致性。
链接:https://www.jianshu.com/p/189956c94cef
struct和class的区别
1、struct的默认成员权限是public
2、class的默认成员权限是private
C++中可以使用struct、class来定义一个类
C++编程规范
1、全局变量:g_
2、成员变量:m_
3、静态变量:s_
4、常量:c_
extern “C”,C++在调用C语言API时,需要使用extern "C"修饰C语言的函数声明
背景: C++编译器默认会对符号名(变量名、函数名等)进行改编、修饰, 重载时会生成多个不同的函数名,不同编译器(MSVC、g++)有不同的生成规则
链接:https://www.jianshu.com/p/746e291f5190
1.什么是虚函数?什么是纯虚函数?
虚函数是允许被其子类重新定义的成员函数。
虚函数的声明:virtual returntype func(parameter);引入虚函数的目的是为了动态绑定;
纯虚函数声明:virtual returntype func(parameter)=0;引入纯虚函数是为了派生接口。(使派生类仅仅只是继承函数的接口)
2.基类为什么需要虚析构函数?
防止内存泄漏。想去借助父类指针去销毁子类对象的时候,不能去销毁子类对象。假如没有虚析构函数,释放一个由基类指针指向的派生类对象时,不会触发动态绑定,则只会调用基类的析构函数,不会调用派生类的。派生类中申请的空间则得不到释放导致内存泄漏。
3.当i是一个整数的时候i++和++i那个更快?它们的区别是什么?
几乎一样。i++返回的是i的值,++i返回的是i+1的值,即++i是一个确定的值,是一个可以修改的左值。
4.vector的reserve和capacity的区别?
reserve()用于让容器预留空间,避免再次分配内存;capacity()返回在重新进行分配以前所能容纳的元素数量。
5.如何初始化const和static数据成员?
通常在类外申明static成员,但是static const的整型(bool,char,int,long)可以在类中声明且初始化,static const的其他类型必须在类外初始化(包括整型数组)。
6.static 和const分别怎么用,类里面static和const可以同时修饰成员函数吗?
static的作用:
对变量:
1.局部变量:
在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。
1)内存中的位置:静态存储区
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。
注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对他进行访问),但未改变其作用域。
2.全局变量
在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。
1)内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
2)初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非他被显示初始化)
3)作用域:全局静态变量在声明他的文件之外是不可见的。准确地讲从定义之处开始到文件结尾。
注:static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:(1)不会被其他文件所访问,修改(2)其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。
对类中的:
1.成员变量
用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化 。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。
特点:
1.不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。
2.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
3.静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的指针或引用。
2.成员函数
- 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
- 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(5,3);当static成员函数在类外定义时不需要加static修饰符。
- 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。
不可以同时用const和static修饰成员函数。
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
const的作用:
1.限定变量为不可修改。
2.限定成员函数不可以修改任何数据成员。
3.const与指针:
const char *p 表示 指向的内容不能改变。
char * const p,就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。
7.指针和引用的区别
本质上的区别是,指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。
而引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。
注:
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2)可以有const指针,但是没有const引用(const引用可读不可改,与绑定对象是否为const无关)
注:引用可以指向常量,也可以指向变量。例如int &a=b,使引用a指向变量b。而为了让引用指向常量,必须使用常量引用,如const int &a=1; 它代表的是引用a指向一个const int型,这个int型的值不能被改变,而不是引用a的指向不能被改变,因为引用的指向本来就是不可变的,无需加const声明。即指针存在常量指针int const *p和指针常量int *const p,而引用只存在常量引用int const &a,不存在引用常量int& const a。
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;
(8)指针使用时需要解引用(*),引用则不需要;
8.什么是多态?多态有什么用途?
C++ 多态有两种:静态多态(早绑定)、动态多态(晚绑定)。静态多态是通过函数重载实现的;动态多态是通过虚函数实现的。
1.定义:“一个接口,多种方法”,程序在运行时才决定要调用的函数。
2.实现:C++多态性主要是通过虚函数实现的,虚函数允许子类重写override(注意和overload的区别,overload是重载,是允许同名函数的表现,这些函数参数列表/类型不同)。
注:多态与非多态的实质区别就是函数地址是静态绑定还是动态绑定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并产生代码,说明地址是静态绑定的;如果函数调用的地址是 需要在运行期间才确定,属于动态绑定。
3.目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。
4.用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。
用一句话概括:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
关于重载、重写、隐藏的区别
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
Override(覆盖或重写):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
虚函数表:
详细解释可以参考博客:https://www.cnblogs.com/jin521/p/5602190.html
多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
注:对象不包含虚函数表,只有虚指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
- 原始基类的虚函数表
下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)
- 单继承时的虚函数(无重写基类虚函数)
假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表。
Derive Class继承了Base Class中的3个虚函数,准确说是该函数的实体地址被拷贝到Derive Class的虚函数列表中,派生新增的虚函数置于虚函数列表后面,并按声明顺序摆放。
- 单继承时的虚函数(重写基类虚函数)
现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base::x()这一项,指向了自己的虚函数。
- 多重继承时的虚函数(class Derived :public Base1,public Base2)
这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。
它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。
注:有关以上虚函数表等详见c++对象模型。链接地址:https://www.cnblogs.com/inception6-lxc/p/9273918.html
纯虚函数:
定义: 在很多情况下,基类本身生成对象是不合情理的。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)纯虚函数不能再在基类中实现,编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。称带有纯虚函数的类为抽象类。
特点:
1,当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;(避免类被实例化且在编译时候被发现,可以采用此方法)
2,这个方法必须在派生类(derived class)中被实现;
目的:使派生类仅仅只是继承函数的接口。
9.vector中size()和capacity()的区别。
size()指容器当前拥有的元素个数(对应的resize(size_type)会在容器尾添加或删除一些元素,来调整容器中实际的内容,使容器达到指定的大小。);capacity()指容器在必须分配存储空间之前可以存储的元素总数。
size表示的这个vector里容纳了多少个元素,capacity表示vector能够容纳多少元素,它们的不同是在于vector的size是2倍增长的。如果vector的大小不够了,比如现在的capacity是4,插入到第五个元素的时候,发现不够了,此时会给他重新分配8个空间,把原来的数据及新的数据复制到这个新分配的空间里。(会有迭代器失效的问题)
10.new和malloc的区别。
详细参考:链接
- new是运算符,malloc()是一个库函数;
- new会调用构造函数,malloc不会;
- new返回指定类型指针,malloc返回void*指针,需要强制类型转换;
- new会自动计算需分配的空间,malloc不行;
- new可以被重载,malloc不能。
11.C++的内存分区
- 栈区(stack):主要存放函数参数以及局部变量,由系统自动分配释放。
- 堆区(heap):由用户通过 malloc/new 手动申请,手动释放。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局/静态区:存放全局变量、静态变量;程序结束后由系统释放。
- 字符串常量区:字符串常量就放在这里,程序结束后由系统释放。
- 代码区:存放程序的二进制代码。
12.vector、map、multimap、unordered_map、unordered_multimap的底层数据结构,以及几种map容器如何选择?
底层数据结构:
- vector基于数组,map、multimap基于红黑树,unordered_map、unordered_multimap基于哈希表。
根据应用场景进行选择:
- map/unordered_map 不允许重复元素
- multimap/unordered_multimap 允许重复元素
- map/multimap 底层基于红黑树,元素自动有序,且插入、删除效率高
- unordered_map/unordered_multimap 底层基于哈希表,故元素无序,查找效率高。
13.内存泄漏怎么产生的?如何避免?
- 内存泄漏一般是指堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。
- 更广义的内存泄漏还包括未对系统资源的及时释放,比如句柄、socket等没有使用相应的函数释放掉,导致系统资源的浪费。
VS下检测内存泄漏方法:
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
//在入口函数中包含 _CrtDumpMemoryLeaks();
//即可检测到内存泄露
//以如下测试函数为例:
int main()
{
char* pChars = new char[10];
//delete[]pChars;
_CrtDumpMemoryLeaks();
system("pause");
return 0;
}
解决方法:
- 养成良好的编码习惯和规范,记得及时释放掉内存或系统资源。
- 重载new和delete,以链表的形式自动管理分配的内存。
- 使用智能指针,share_ptr、auto_ptr、weak_ptr。
14.说几个C++11的新特性
- auto类型推导
- 范围for循环
- lambda函数
- override 和 final 关键字
-
/*如果不使用override,当你手一抖,将foo()写成了f00()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。 所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:*/ class A { virtual void foo(); } class B :public A { void foo(); //OK virtual foo(); // OK void foo() override; //OK } class A { virtual void foo(); }; class B :A { virtual void f00(); //OK virtual void f0o()override; //Error };
/*当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:*/ class Base { virtual void foo(); }; class A : Base { void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写 void bar() final; // Error: 父类中没有 bar虚函数可以被重写或final }; class B final : A // 指明B是不可以被继承的 { void foo() override; // Error: 在A中已经被final了 }; class C : B // Error: B is final { };
- 空指针常量nullptr
- 线程支持、智能指针等
15.C和C++区别?
C++在C的基础上增添类,C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
16.const与#define的区别
1.编译器处理方式
define – 在预处理阶段进行替换
const – 在编译时确定其值
2.类型检查
define – 无类型,不进行类型安全检查,可能会产生意想不到的错误
const – 有数据类型,编译时会进行类型检查
3.内存空间
define – 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大
const – 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝
4.其他
在编译时, 编译器通常不为const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
宏替换只作替换,不做计算,不做表达式求解。
17.悬空指针与野指针区别
- 悬空指针:当所指向的对象被释放或者收回,但是没有让指针指向NULL;
{
char *dp = NULL;
{
char c;
dp = &c;
}
//变量c释放,dp变成空悬指针
}
void func()
{
char *dp = (char *)malloc(A_CONST);
free(dp); //dp变成一个空悬指针
dp = NULL; //dp不再是空悬指针
/* ... */
}
- 野指针:那些未初始化的指针;
int func()
{
char *dp;//野指针,没有初始化
static char *sdp;//非野指针,因为静态变量会默认初始化为0
}
18.struct与class的区别?
本质区别是访问的默认控制:默认的继承访问权限,class是private,struct是public;
19.sizeof和strlen的区别?
功能不同:
sizeof是操作符,参数为任意类型,主要计算类型占用内存大小。
strlen()是函数,其函数原型为:extern unsigned int strlen(char *s);其参数为char*,strlen只能计算以" "结尾字符串的长度,计算结果不包括" "。
char* ss="0123456789";
//s1=4,ss为字符指针在内存中占用4个字节
int s1=sizeof(ss);
//s2=10,计算字符串ss的长度
int s2=strlen(ss);
参数不同:
当将字符数组作为sizeof()的参数时,计算字符数组占用内存大小;当将字符数组作为strlen()函数,字符数组转化为char*。因为sizeof的参数为任意类型,而strlen()函数参数只能为char*,当参数不是char*必须转换为char*。
char str[]="abced";
//a为6(1*6),字符数组str包含6个元素(a,b,c,d,e, ),每个元素占用1个字节
int a= sizeof(str);
//len为5,不包含" ",
int len=strlen(str);
//str[0]是字符元素a,所以b=1
int b= sizeof(str[0]);
20.32位,64位系统中,各种常用内置数据类型占用的字节数?
char :1个字节(固定)
*(即指针变量): 4个字节(32位机的寻址空间是4个字节。同理64位编译器)(变化*)
short int : 2个字节(固定)
int: 4个字节(固定)
unsigned int : 4个字节(固定)
float: 4个字节(固定)
double: 8个字节(固定)
long: 4个字节
unsigned long: 4个字节(变化*,其实就是寻址控件的地址长度数值)
long long: 8个字节(固定)
64位操作系统
char :1个字节(固定)
*(即指针变量): 8个字节
short int : 2个字节(固定)
int: 4个字节(固定)
unsigned int : 4个字节(固定)
float: 4个字节(固定)
double: 8个字节(固定)
long: 8个字节
unsigned long: 8个字节(变化*其实就是寻址控件的地址长度数值)
long long: 8个字节(固定)
除*与long 不同其余均相同。
21.virtual, inline, decltype,volatile,static, const关键字的作用?使用场景?
inline:在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
#include <stdio.h>
//函数定义为inline即:内联函数
inline char* dbtest(int a) {
return (i % 2 > 0) ? "奇" : "偶";
}
int main()
{
int i = 0;
for (i=1; i < 100; i++) {
printf("i:%d 奇偶性:%s /n", i, dbtest(i));
}
}//在for循环的每个dbtest(i)的地方替换成了 (i % 2 > 0) ? "奇" : "偶",避免了频繁调用函数,对栈内存的消耗
decltype:从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。
volatile:volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
static:
- 隐藏
在变量和函数名前面如果未加static,则它们是全局可见的。加了static,就会对其它源文件隐藏,利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲 突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏 。
2.static变量中的记忆功能和全局生存期
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
#include <stdio.h>
int fun(){
static int count = 10; //在第一次进入这个函数的时候,变量a被初始化为10!并接着自减1,以后每次进入该函数,a
return count--; //就不会被再次初始化了,仅进行自减1的操作;在static发明前,要达到同样的功能,则只能使用全局变量:
}
int count = 1;
int main(void)
{
printf("global local static
");
for(; count <= 10; ++count)
printf("%d %d
", count, fun());
return 0;
}
---基于以上两点可以得出一个结论:把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的。
3.static的第三个作用是默认初始化为0(static变量)
最后对static的三条作用做一句话总结。首先static的最主要功能是隐藏,其次因为static变量存放在静态存储区,所以它具备持久性和默认值0。
4.static的第四个作用:C++中的类成员声明static(有些地方与以上作用重叠)
在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:
(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致 了它仅能访问类的静态数据和静态成员函数。
(2)不能将静态成员函数定义为虚函数。
(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊 ,变量地址是指向其数据类型的指针 ,函数地址类型是一个“nonmember函数指针”。
(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就 产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X W indow系统结合,同时也成功的应用于线程函数身上。 (这条没遇见过)
(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问 时间,节省了子类的内存空间。
(6)静态数据成员在<定义或说明>时前面加关键字static。
(7)静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误)
(8)静态成员初始化与一般数据成员初始化不同:
初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
初始化时不加该成员的访问权限控制符private,public等;
初始化时使用作用域运算符来标明它所属类;
所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>
(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志。
22.深拷贝与浅拷贝的区别?
1.什么时候用到拷贝函数?
b.一个对象以值传递的方式从函数返回;
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝;
2.是否应该自定义拷贝函数?
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
3.什么叫深拷贝?什么是浅拷贝?两者异同?
4.深拷贝好还是浅拷贝好?
如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
参考博客:https://blog.csdn.net/caoshangpa/article/details/79226270
http://www.cnblogs.com/BlueTzar/articles/1223313.html
23.派生类中构造函数,析构函数调用顺序?
构造函数:“先基后派”;析构函数:“先派后基”。
24.C++类中数据成员初始化顺序?
1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
3.类中const成员常量必须在构造函数初始化列表中初始化。
4.类中static成员变量,只能在类内外初始化(同一类的所有实例共享静态成员变量)。
初始化顺序:
- 1) 基类的静态变量或全局变量
- 2) 派生类的静态变量或全局变量
- 3) 基类的成员变量
- 4) 派生类的成员变量
25.结构体内存对齐问题?结构体/类大小的计算?
注:内存对齐是看类型,而不是看总的字节数。比如:
#include<iostream>
using namespace std;
struct AlignData1
{
int a;
char b[7];//a后面并不会补上3个字节,而是由于char的类型所以不用补。
short c;
char d;
}Node;
struct AlignData2
{
bool a;
int b[2];//a后面并不会补上7个字节,而是根据int的类型补3个字节。
int c;
int d;
}Node2;
int main(){
cout << sizeof(Node) << endl;//16
cout << sizeof(Node2) << endl;//20
system("pause");
return 0;
}
补充:
- 每个成员相对于这个结构体变量地址的偏移量正好是该成员类型所占字节的整数倍。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节。
- 最终占用字节数为成员类型中最大占用字节数的整数倍。
- 一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。
struct AlignData1
{
char c;
short b;
int i;
char d;
}Node;
这个结构体在编译以后,为了字节对齐,会被整理成这个样子:
struct AlignData1
{
char c;
char padding[1];
short b;
int i;
char d;
char padding[3];
}Node;
含有虚函数的类的大小:链接
补充:联合体的大小计算:
联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。
union U1
{
int n;
char s[11];
double d;
}; //16,char s[11]按照char=1可以整除
union U2
{
int n;
char s[5];
double d;
}; //8
26.static_cast, dynamic_cast, const_cast, reinpreter_cast的区别?
补充:static_cast与dynamic_cast
- cast发生的时间不同,一个是static编译时,一个是runtime运行时;
- static_cast是相当于C的强制类型转换,用起来可能有一点危险,不提供运行时的检查来确保转换的安全性。
- dynamic_cast用于转换指针和和引用,不能用来转换对象 ——主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息。
#include <iostream>
using namespace std;
class CBasic
{
public:
virtual int test(){return 0;}
};
class CDerived : public CBasic
{
public:
virtual int test(){ return 1;}
};
int main()
{
CBasic cBasic;
CDerived cDerived;
CBasic * pB1 = new CBasic;
CBasic * pB2 = new CDerived;
CBasic * pB3 = new CBasic;
CBasic * pB4 = new CDerived;
//dynamic cast failed, so pD1 is null.
CDerived * pD1 = dynamic_cast<CDerived * > (pB1);
//dynamic cast succeeded, so pD2 points to CDerived object
CDerived * pD2 = dynamic_cast<CDerived * > (pB2);
//pD3将是一个指向该CBasic类型对象的指针,对它进行CDerive类型的操作将是不安全的
CDerived * pD3 = static_cast<CDerived * > (pB3);
//static_cast成功
CDerived * pD4 = static_cast<CDerived * > (pB4);
//dynamci cast failed, so throw an exception.
// CDerived & rD1 = dynamic_cast<CDerived &> (*pB1);
//dynamic cast succeeded, so rD2 references to CDerived object.
CDerived & rD2 = dynamic_cast<CDerived &> (*pB2);
return 0;
}
注:CBasic要有虚函数,否则会编译出错;static_cast则没有这个限制。
27.智能指针
- 智能指针是在 <memory> 头文件中的std命名空间中定义的,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它们对RAII“获取资源即初始化”编程至关重要,RAII的主要原则是为将任何堆分配资源(如动态分配内存或系统对象句柄)的所有权提供给其析构函数包含用于删除或释放资源的代码以及任何相关清理代码的堆栈分配对象。大多数情况下,当初始化原始指针或资源句柄以指向实际资源时,会立即将指针传递给智能指针。
- 智能指针的设计思想:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
- unique_ptr只允许基础指针的一个所有者。unique_ptr小巧高效;大小等同于一个指针且支持右值引用,从而可实现快速插入和对STL集合的检索。
- shared_ptr采用引用计数的智能指针,主要用于要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时)的情况。当所有的shared_ptr所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。最安全的分配和使用动态内存的方法是调用make_shared标准库函数,此函数在动态分配内存中分配一个对象并初始化它,返回对象的shared_ptr。
28.计算类大小例子
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;
类中用static声明的成员变量不计算入类的大小中,因为static data不是实例的一部分。static的属于全局的,他不会占用类的存储,他有专门的地方存储 (全局变量区)
29.大端与小端的概念?各自的优势是什么?
- 大端与小端是用来描述多字节数据在内存中的存放顺序,即字节序。大端(Big Endian)指低地址端存放高位字节,小端(Little Endian)是指低地址端存放低位字节。
- 需要记住计算机是以字节为存储单位。
- 为了方便记忆可把大端和小端称作高尾端和低尾端,eg:如果是高尾端模式一个字符串“11223344”把尾部“44”放在地址的高位,如果是地尾端模式,把“44”放在地址的低位。
各自优势:
- Big Endian:符号位的判定固定为第一个字节,容易判断正负。
- Little Endian:长度为1,2,4字节的数,排列方式都是一样的,数据类型转换非常方便。
举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
- 1)大端模式:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
- 2)小端模式:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
30.C++中*和&同时使用是什么意思?
template <class T>
void InsertFront(Node<T>* & head, T item)
上面一个函数的声明,其中第一个参数*和&分别是什么意思?
head是个指针,前面为什么加个&
本来“* head”代表的是传指针的,但是只能改变head指向的内容,而“* &head”意思是说head是传进来的指针的同名指针,就能既改变*head指向的内容,又能改变head这个指针。比如:main()有个Node<int>* p,int t;当调用insertFront(p,t)是,如果template <class T> void InsertFront(Node<T>* & head, T item)中有对head进行赋值改变时,main()中的p也会跟着改变,如果没有&这个别名标识时,p则不会随着head的改变而改变。
31.C++vector与list区别
https://www.cnblogs.com/shijingjing07/p/5587719.html
32.C语言中static关键字作用
在C语言中static的作用如下
第一、在修饰变量的时候,static修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
第二、static修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是extern外部声明也不可以。
第三、static修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。Static修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为0;
(1)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用static修饰
(2)考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用static)
在C++中static关键字除了具有C中的作用还有在类中的使用
在类中,static可以用来修饰静态数据成员和静态成员方法
静态数据成员
(1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
(2)静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。
(3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若为对静态数据成员赋初值,则编译器会自动为其初始化为0
(4)静态数据成员既可以通过对象名引用,也可以通过类名引用。
静态成员函数
(1)静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
(2)非静态成员函数有this指针,而静态成员函数没有this指针。
(3)静态成员函数主要用来访问静态数据成员而不能访问非静态成员。
33.C/C++中堆和栈的区别
讲解全面的一篇博客:https://blog.csdn.net/Fiorna0314/article/details/49757195
34.定义一个空类编译器做了哪些操作?
如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建。所有这些函数都是inline和public的。
定义一个空类例如:
class Empty
{
}
一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,一般编译过就相当于:
class Empty
{
public:
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
};
需要注意的是,只有当你需要用到这些函数的时候,编译器才会去定义它们。
35.友元函数和友元类
36.什么情况下,类的析构函数应该声明为虚函数?为什么?
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
37.哪些函数不能成为虚函数?
不能被继承的函数和不能被重写的函数。
1)普通函数
普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义。因为编译器会在编译时绑定函数。
而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。
2)友元函数
友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
3)构造函数
首先说下什么是构造函数,构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员,显然是不符合语义的。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,因此无法使用多态特性,这是矛盾的。因此构造函数不允许继承。
4)内联成员函数
我们需要知道内联函数就是为了在代码中直接展开,减少函数调用花费的代价。也就是说内联函数是在编译时展开的。而虚函数是为了实现多态,是在运行时绑定的。因此显然内联函数和多态的特性相违背。
5)静态成员函数
首先静态成员函数理论是可继承的。但是静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。、
38.编写一个有构造函数,析构函数,赋值函数,和拷贝构造函数的String类
//.h
class String{
public:
String(const char* str);
String(const String &other);
~String();
String & operate=(const String &other);
private:
char* m_data;
};
//.cpp
String::String(const char*str){
if(str==NULL){
m_data=new char[1];
*m_data='