C++11并发编程-条件变量(condition_variable)详解
C++:继承访问属性(public/protected/private)
C++之Lambda表达式
C++中 =defaule 和 =delete 使用
解析:回顾进程的空间模型,如图2-1所示,与1.1.5节的图相比,多了一个program break指针,Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。
图2-2堆内部机制
获取了break地址,也就是内存申请的初始地址,下面是malloc的整体实现方案:
malloc函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。 调用malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。 调用free函数时,它将用户释放的内存块连接到空闲链表上。 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。
答:是有可能申请1.2G的内存的。
解析:回答这个问题前需要知道malloc的作用和原理,应用程序通过malloc函数可以向程序的虚拟空间申请一块虚拟地址空间,与物理内存没有直接关系,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。
我们要申请空间的大小为1.2G=230 × 1.2 Byte ,转换为十六进制约为 4CCC CCCC ,这个数值已经超过了int类型的表示范围,但还在unsigned的表示范围。幸运的是malloc函数要求的参数为unsigned 。在当前正在使用的Windows环境中,可申请的最大空间超过1.9G。实际上,具体的数值会受到操作系统版本、程序本身的大小、用到的动态/共享库数量、大小、程序栈数量、大小等的影响,甚至每次运行的结果都可能存在差异,因为有些操作系统使用了一种叫做随机地址分布的技术,使得进程的堆空间变小。感兴趣的读者可以去研究操作系统中的相关内容。
综上,是有可能通过malloc( size_t ) 函数调用申请超过该机器物理内存大小的内存块的。
相同:
都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
区别:
指针和引用之间怎么转换:
指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。
引用转指针:把引用类型的对象用&取地址就获得指针了。
int a = 0;
int *pA = &a;
void fun(int &va){}
此时调用: fun(pA);
pA是指针,加个号后可以转换成该指针指向的对象,此时fun的形参是一个引用值,pA指针指向的对象会转换成引用va。
解析:检索内存:顾名思义,对某段内存进行遍历搜索。
内存分配:
1、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
extern”C”的作用在于C++代码中调用的C函数的声明,或C++中编译的函数要在C中调用。
解析:我们可以在C++中使用C的已编译好的函数模块,这时候就需要用到extern”C”。这是为了避免C++ name mangling,主要用于动态链接库,使得在C++里导出函数名称与C语言规则一致(不改变),方便不同的编译器甚至是不同的开发语言调用。
那么我们来看看C++语音规则和C语音规则有何不同。如果我们定义一个函数:
int fun(int a);
如果是C++编译器,则可能将此函数改名为int_fun_int,(C++重载机制就这么来的)。如果有加上extern”C修饰,则c++编译器会按照C语音编译器一样编译为_fun。
注意:
1)C++调用一个C语言编写的.so库时,包含描述.so库中函数的头文件时,应该将对应的头文件放置在extern “C”{}格式的{}中,。
2)C中引用C++中的全局函数时,C++的头文件需要加extern “C”,而C文件中不能用extern “C”,只能使用extern关键字。
3)也就是extern“C” 都是在c++文件里添加的!
1)头文件声明时加extern,定义时不要加,因为extern可以多次声明,但只有一个定义。
2)extern在链接阶段起作用(四大阶段:预处理–编译–汇编–链接)。
__stdcall和__cdecl都是函数调用约定关键字,我们先来看看__stdcall和__cdecl调用方式的概念:
总结:
因为以上2点,_cdecl这种调用约定的特点是支持可变数量的参数,比如printf方法,__stdcall不支持可变数量的参数。
假设函数fun()作为调用者调用printf打印东西时,可以输入不同数量的参数,printf作为被调用者,并不知道调用者fun()到底将多少参数压入堆栈,因此printf就没有办法自己清理堆栈,所以只有函数退出之后,由fun清理堆栈,因为fun总是知道自己传入了多少参数。
自己动手实现memcpy()时就需要考虑地址重叠的情况。我们来看个简单的例子。有一个5个元素的数组,不妨设为int arr = {1,2,3,4,5};考虑2种情况:
看一下代码:
void *Memcpy(void *dst, const void *src, size_t1, size)
{
char *psrc; //源地址
char *pdst; //目标地址
if(NULL == dst || NULL == src)
{
return NULL;
}
if((src < dst) && (char *)src + size > (char *)dst) //源地址在前,对应上述情况2,需要自后向前拷贝
{
psrc = (char *)src + size - 1;
pdst = (char *)dst + size - 1;
while(size--)
{
*pdst-- = *psrc--;
}
}
else //源地址在后,对应上述第一种情况,直接逐个拷贝*pdst++ = *psrc++即可
{
psrc = (char *)src;
pdst = (char *)dst;
while(size--)
{
*pdst++ = *psrc++;
}
}
return dst;
}void *Memcpy(void *dst, const void *src, size_t1, size)
{
char *psrc; //源地址
char *pdst; //目标地址
if(NULL == dst || NULL == src)
{
return NULL;
}
if((src < dst) && (char *)src + size > (char *)dst) //源地址在前,对应上述情况2,需要自后向前拷贝
{
psrc = (char *)src + size - 1;
pdst = (char *)dst + size - 1;
while(size--)
{
*pdst-- = *psrc--;
}
}
else //源地址在后,对应上述第一种情况,直接逐个拷贝*pdst++ = *psrc++即可
{
psrc = (char *)src;
pdst = (char *)dst;
while(size--)
{
*pdst++ = *psrc++;
}
}
return dst;
}
首先要知道C++是完全兼容C语言的,因此大家可能会随着学习的深入觉得C++中的struct并没有必要保存,因为struct可以完成的事情,class都可以完成。甚至在C++中struct也可以有构造函数,析构函数,结构体之间也可以继承等等。也就是C++中的struct和class其实意义一样。
总结:C++中存在struct的唯一意义就是为了让C语言程序员有归属感,是为了让C++编译器兼容以前用C语言开发的项目。
答:两者最大区别是struct里面默认的访问控制是public,而class中的默认访问控制是private。
在C语言中,static作用:“改变生命周期” 或者 “改变作用域”。有以下特性:
const就是常量修饰符,const变量应该在声明的时候就进行初始化,如果在声明常量的适合没有提供值,则该常量的值是不确定的,且无法修改。
const修饰主要用来修饰变量、函数形参和类成员函数:
解析:主要有以下区别
解析:.volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,编译器对访问该变量的代码就不再进行优化,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
答:volatile关键词的作用是影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。
案例:我们来看看以下几个使用volatile的案例:
int i=0;
int main(void)
{
...
while (1){
if (i) dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中断产生时,在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该是volatile int i;
int *output = (unsigned int *)0xff800000;//定义一个IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++){
*output = i;
}
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为9,所以编译器最后给你编译编译的代码结果相当于:
int init(void)
{
*output = 9;
}
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
例如:
volatile int *output=(volatile unsigned int *)0xff800000;//定义一个I/O端口
解析:这几个概念面试官偶尔会调皮的问一下,所以还是要区分好几个概念,不要混淆,以上4个概念都可以从最后两个字开始记起,后面两个字代表这是个什么东东,前面两个字代表这个是什么类型:
(1)常量指针:也叫常指针,最后两个字是“指针”,代表这是一个指针,但指向的是一个常量,如下:
int a = 0;
const int *p = &a; //不可以通过p改变a
(2)指针常量:后面两个字是“常量”,代表这是个常量,不过是指针类型的常量,
int a = 0;
int *const p = &a; //从后往前看,这是个指针常量,指向的a的值可以改变,但p本身不可改变
注意:如果从代码来区分常量指针指针常量,那么可以从后往前看const的位置,
const int p = &a //从后往前看,const修饰的是p,所以指针p指向的数值不可变
int *const p = &a; //从后往前看,const修饰的是p,所以指针p本身不可变
(3)常量引用:后两个字是“引用”,那么这个是引用,并且是常量的引用,那么就有两个性质,如下:
double a;
const int &r = a; //正确 性质1:不可通过常量引用r来改变a
const int &r = 10;//正确 性质2:常量引用可以直接引用具体数值
(4)没有引用常量:后面两个字代表这个是常量,前面代表这个是引用类型的常量,然而常量就是常量了,并没有引用类型的常量。
解析:如何理解这句话呢,首先,没有指向引用的指针,因为指针是本质上是指向某一块内存空间的,而引用只是一个变量的别名,本身是没有地址的,如果要创建一个指针指向某个引用,那么其实指向的是这个引用所引用的对象,看下面代码:
int v = 1;
int &ri = v; //整型变量v的引用
int *p = &ri; //指针p其实指向的是变量v
其次,有指针的引用,我们直接看代码:
int v = 1;
int *p = &v;
int *&rp = p;
第一个是要理解int *&rp = p; 这是定义了一个变量rp,还是从后往前看,距离rp左边最近的修饰符决定rp是个什么东东,剩下的就是rp的具体值。因此我们发现距离rp左边最近的是&,代表rp是个引用,所以int *&rp = p; 可以看作int *(&rp )= p; 如果我们把(&rp)当作一个整体,又可以看作int * RP = p;到此为止,我们就可以很明显的知道这句话其实就是定义了一个引用rp指向指针p。
解析:作用域规则告诉我们一个变量的有效范围,它在哪儿创建,在哪儿销毁。变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的开括号配对的第一个闭括号。也就是说,作用域由变量所在的最近一对括号确定。
答:
解析:C++中,四个与类型转换相关的关键字:static_cast、const_cast、reinterpret_cast dynamic_cast。
int main()
{
const int constant = 26;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 3;
cout<< "constant: "<<constant<<endl; //26
cout<<"*modifier: "<<*modifier<<endl; //3
return 0;
}
加上const属性:const int* k = const_case
const_case只能转换指针或引用 不能转换变量
const int i = 3;
int j = const_cast(i);是不行的
class CBasic{
public:
CBasic(){};
~CBasic(){};
virtual void speak() { //要有virtual才能实现多态,才能使用dynamic cast,如果父类没有虚函数,是编译不过的
printf("dsdfsd");
}
private:
};
//哺乳动物类
class cDerived:public CBasic{
public:
cDerived(){};
~cDerived(){};
private:
};
int main()
{
CBasic cBasic;
CDerived cDerived;
CBasic * pB1 = new CBasic;
CBasic * pB2 = new CDerived;
//dynamic cast failed, so pD1 is null. pB1指向对象和括号里的Derived *不一样,转换失败
CDerived * pD1 = dynamic_cast<CDerived * > (pB1);
//dynamic cast succeeded, so pD2 points to CDerived object
//dynamic cast 用于将指向子类的父类指针或引用,转换为子类指针或引用 ,pB2指向对象和括号里的Derived *一样,转换成功
CDerived * pD2 = dynamic_cast<CDerived * > (pB2);
//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;
}
#include
int main()
{
printf("int = %d\n",sizeof(int));
printf("short = %d\n",sizeof(short));
printf("long int = %d\n",sizeof(long int));
printf("long long int = %d\n",sizeof(long long int));
printf("char = %d\n",sizeof(char));
printf("_Bool = %d\n",sizeof(_Bool));
printf("float = %d\n",sizeof(float));
printf("double = %d\n",sizeof(double));
printf("long double = %d\n",sizeof(long double));
}
输出
int = 4
short = 2
long int = 4
long long int = 8
char = 1
_Bool = 1
float = 4
double = 8
long double = 12
32位编译器: | 64位编译器: |
---|---|
char :1个字节 | char :1个字节 |
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节) | char*(即指针变量): 8个字节 |
short int : 2个字节 | short int : 2个字节 |
int: 4个字节 | int: 4个字节 |
unsigned int : 4个字节 | unsigned int : 4个字节 |
float: 4个字节 | float: 4个字节 |
double: 8个字节 | double: 8个字节 |
long: 4个字节 | long: 8个字节 |
long long: 8个字节 | long long: 8个字节 |
unsigned long: 4个字节 | unsigned long: 8个字节 |
if (flag) // 表示flag为真
if (!flag) // 表示flag为假
其他为不良写法。
if (value == 0)
if(value != 0)
应当将 if (x == 0.0) // 隐含错误的比较
转化为 if ((x>=-EPSINON) &&(x<=EPSINON)) 或者 if(abs(x) <= EPSINON)
其中EPSINON是允许的误差(即精度)。
const float EPSINON = 0.000001,至于为什么取0.000001,可以自己按实际情况定义。
if (p ==NULL) // p与NULL显式比较,强调p是指针变量
if (p != NULL)
i++ :先引用后增加,先在i所在的表达式中使用i的当前值,后让i加1
++i :先增加后引用,让i先加1,然后在i所在的表达式中使用i的新值
A operator ++() //前++
{
*this=this+1;
return *this;
}
A operator ++(int) //后++
{
A t=*this; //先保存一份变量
++(*this); //调用前++
return t;
}
数组名代表整个数组的时候只有两种情况,
1.sizeof(数组名),这里的数组名表示整个数组。
2.&数组名,这里的数组名表示整个数组。
#include
int main()
{
int a[]={1,2,3,4};
printf("%d\n",sizeof(a)); //16 a表示数组的首元素,首地址因此用sizeof计算它们的值,就是整个二维数组所占用的内存空间
printf(“%p\n”,a++);
printf(“%p\n”,a);
printf("%d\n",sizeof(a+0)); //4 a+0为a[0]的首地址
printf("%d\n",sizeof(*a)); //4 首元素大小 就相当于 sizeof(*a)=4 =sizeof(a[0]);
printf("%d\n",sizeof(a+1)); //4 从首元素向后偏移一个整型的地址,即第二个元素的地址
printf("%d\n",sizeof(a[1])); //4 a[1]的字节数为4
printf("%d\n",sizeof(&a)); //4 &a为数组地址和a即a[0]的地址一样
printf("%d\n",sizeof(&a+1)); //4 跳过整个数组 指向数组后面的一个地址
printf("%d\n",sizeof(&a[0])); //4 首元素地址
printf("%d\n",sizeof(&a[0]+1));
return 0;
}
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数 ( An array of ten pointers to functions that take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试 的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答 案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?
strncat:将一个字符数组的前n个拷到目标串,并在后面加上’\0’
strcpy:目标串要有足够的空间放置src,否则出现缓冲区溢出
strncpy:如果n大于src,将src拷贝完后,会一直追加’\0’,效率低
如果n小于src,并不会在dest后追加‘\0’
snprintf:会在拷贝结束后自动添加‘\0’,更加安全。
memset:内存初始化
memcpy:内存拷贝
char* strcat(char* s1,const char* s2)
{
char* s;
for(s=s1;*s!='\0';s++);
for(;(*s=*s2)!='\0';s++,s2++);
return s1;
}
int strcmp(const char* s1,const char* s2)
{
for(;*s1==*s2;s1++,s2++)
if(*s1=='\0')
return 0;
return *(unsigned char*)s1<*(unsigned char*)s2?-1:1;
}
char* strcpy(char* s1,const char* s2)
{
char* s=s1;
for(s=s1;(*s++=*s2++)!='\0';);
return s1;
}
char* strncpy(char* s1,const char* s2,size_t n)
{
char* s=s1;
for(;n>0&&*s2!='\0';n--)
*s++=*s2++;
for(;n>0;n--)
*s++='\0';
return s1;
}
void* memset(void *s,int c,size_t n)
{
const unsigned char uc=c;
unsigned char* su;
for(su=s;0<n;++su,--n)
*su=uc;
return s;
}
void* memcpy(void* s1,const void* s2,size_t n)
{
char* su1;
const char* su2;
for(su1=s1,su2=s2;n>0;su1++,su2++)
*su1=*su2;
return s1;
}
C语言是面向过程的一种编程语言,而C++则是面向对象的一种编程语言。
面向过程就是分析并解决问题,并将解决问题的步骤一步一步的实现,使用时依次调用就行。
面向对象编程就是把问题分解成各个对象,建立对象的目的不是为了完成某一个步骤,而是为了描述某个事物在整个问题的步骤中的行为。
面向过程的性能比面向对象高,因为类的调用需要实例化,开销比较大,比较耗资源。但是面向过程却没有面向对象那样易于维护,以及易复用,易扩展。由于面向对象有,封装,继承,多态等性质,可以设计出低耦合的系统。
解析:因为C++支持多重继承,那么在这种情况下会出现重复的基类这种情况,也就是说可能出现将一个类两次作为基类的可能性。比如像下面的情况
为了节省内存空间,可以将DeriverdA、DeriverdB对Base的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如 下:
class Base
class DeriverdA:public virtual Base; //虚继承
class DeriverdB:public virtual Base; //虚继承
class D:public DeriverdA,DeriverdB; //普通继承
虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
注意:不要全部都使用虚继承,因为虚继承会破坏继承体系,不能按照平常的继承体系来进行类型转换(如C++提供的强制转换函数static_cast对继承体系中的类对象转换一般可行的,这里就不行了)。所以不要轻易使用虚继承,更不要在虚继承的基础上进行类型转换,切记切记!
sizeof计算对象所占内存的大小详解
解析:关于类的内存布局主要是考某个类所占用的内存大小,以下通过几个案例加以分析。
(1)虚继承:如果是虚继承,那么就会为这个类创建一个虚表指针,占用4个字节
#include
class A {
public:
int a;
}; //sizeof(A)=4,因为a是整形,占用4字节
class B : virtual public A {
public:
int b;
};//sizeof(B)=4(A副本)+4(虚表指针占用4字节)+4(变量b占用4字节)=12
class C : virtual public B {
};//sizeof(c)= 12(B副本)+4(虚表指针) = 16,如果这里改为直接继承,那么sizeof(c)=12,因为此时就没有虚表指针了
(2)多重继承:如果是以虚继承实现多重继承,记得减掉基类的副本
#include
class A {
public:
int a;
};//sizeof(A) = 4
class B : virtual public A {
};// sizeof(B) =4+4=8
class C : virtual public A {
};//sizeof(C) =4+4=8
class D : public B, public C{
};
//sizeof(D)=8+8-4=12这里需要注意要减去4,因为B和C同时继承A,只需要保存一个A的副本就好了,sizeof(D)=4(A的副本)+4(B的虚表)+4(C的虚表)=12,也可以是8(B的副本)+8(c的副本)-4(A的副本)=
(3)普通继承(含有:空类、虚函数)
class A //result=1 空类所占空间的大小为1
{
};
class B //result=8 1+4 字节对齐后为 8
{
char ch;
virtual void func0() { }
};
class C //result=8 1+1+4 字节对齐后为 8,没有继承的,此时类里即使出现多个虚函数,也只有一个虚指针
{
char ch1;
char ch2;
virtual void func() { } //也只有一个虚指针
virtual void func1() { } //也只有一个虚指针
};
class D: public A, public C //result=12 8(C的副本)+4(整形变量d占用4字节)=12
{
int d;
virtual void func() { } //继承了C,C里已经有一个虚指针,此时D自己有虚函数,
virtual void func1() { } //也不会创建另一个虚指针,所以D本身就变量d需要4字节
};
class E: public B, public C //result=20 8( B的副本)+8(C的副本)+4(E本身)=20
{
int e;
virtual void func0() { } //同理,E不会创建另一个虚指针,所以E本身就变量e需
virtual void func1() { } //要4字节
};
(4)虚继承(多重继承和虚函数)
class CommonBase
{
int co;
};// size = 4
class Base1: virtual public CommonBase
{
public:
virtual void print1() { }
virtual void print2() { }
private:
int b1;
};//4(父类副本)+4(自己有虚函数,加1个虚指针空间)+4(自身变量b1)+4(虚继承再加1个虚指针空间)=16
class Base2: virtual public CommonBase
{
public:
virtual void dump1() { }
virtual void dump2() { }
private:
int b2;
};//同理16
class Derived: public Base1, public Base2
{
public:
void print2() { }
void dump2() { }
private:
int d;
};//16+16-4+4=32
前辈总结说:如果不是虚继承的类,即便有虚函数也不会因此增加存储空间,如果是虚继承的类,没有虚函数就添加一个虚指针空间,有虚函数不论多少个,就添加两个虚指针空间。
本人将前辈总结归纳为:如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间,如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指针空间。
(5)虚继承与虚函数
class A
{
public:
virtual void aa() { }
virtual void aa2() { } //如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间
private:
char ch[3];
}; // 1+4 =补齐= 8
class B: virtual public A //如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指//针空间。
{
public:
virtual void bb() { }
virtual void bb2() { }
}; // 8(副本)+4(虚继承)+4(虚指针)= 16
【小结】重要的事情讲三遍!!!
如果此时类里有一个或多个虚函数,那么需要加1个虚指针空间,如果还是虚继承,那么需要再加1个虚指针空间,最多就2个虚指针空间。
解析:父类的同名函数和父类成员变量被隐藏不代表其不存在,只是藏起来而已,C++有两种方法可以调用被隐藏的函数:
1)用using关键字:使用using后,父类的同名函数就不再隐藏,可以直接调用,如下:
class Child:public Parent
{
public:
Child(){};
using Parent::add;
int add(void){};
};
2)用域操作符,可以调用基类中被隐藏的所有成员函数和变量。
如子类child和父类father都有add()函数,可以通过下面代码实现子类对象调用父类的add()函数:
Child c;
c.Parent::add(10);
解析:只要是有涉及到c++的面试,面试官百分百会问到多态相关的问题,尤其是让你解释下多态实现的原理,此时首先要知道多态实现的三个条件:
1)要有继承
2)要有虚函数重写
3)要有父类指针(父类引用)指向子类对象
答:编译器发现一个类中有虚函数,便会立即为此类生成虚函数表vtable。虚函数表的各表项为指向类里面的虚函数的指针。编译器还会在此类中隐含插入一个指针vptr(对 vc 编译器来说,它插在类的内存地址的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr 与 vtable 的关联代码,即将vptr 指向对应的 vtable,将类与此类的vtable 联系了起来。
另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this 指针,这样依靠此 this 指针即可得到正确的 vtable,如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
上面这段话可能有点难以理解,我本人当初也是理解好一会哈,我们直接看个例子:
#include "stdafx.h"
#include
#include
using namespace std;
class Father
{
public:
void Face()
{
cout << "Father's face" << endl;
}
virtual void Say()
{
cout << "Father say hello" << endl;
}
};
class Son:public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
};
void main()
{
Son son;
Father *pFather=&son; //隐式类型转换
pFather->Say();
}
程序输出:Son say hello
我们重点来看这行代码Father *pFather=&son;
此时指向基类的指针pFather已经变成指向具体的类son的this指针,那么我们调用这个pFather父类指针,就相当于调用了等号右边的类即子类son的this指针,这个this所能调用的函数,自然就是子类son本身的函数。即pFather->Say();这行代码调用的是子类的Say()函数。因此我们就成功的实现了用父类指针pFather调用子类函数,也就是实现了多态。
解析:
简单的来说,浅拷贝是增加了一个指针,指向原来已经存在的内存。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。
而深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。深拷贝和浅拷贝的不同之处,仅仅在于修改了下拷贝构造函数,以及赋值运算符的重载。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
提问:什么时候需要自定义拷贝构造函数?
答:默认拷贝构造函数执行的是浅拷贝,对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。
参考:https://www.jianshu.com/p/77f6a0074dc3
解析:
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常(析构函数里delete this指针也会造成程序崩溃,因为delete this指针就是要调用析构函数,这样就变成无限循环了),会造成程序崩溃的问题。
答:析构函数不能抛出异常,除了资源泄露还可能造成程序崩溃。
解析:类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
//全局函数,传入的是对象
void g_Fun(CExample C)
{
cout<<"test"<<endl;
}
int main()
{
CExample test(1);
//传入对象
g_Fun(test);
}
调用g_Fun()时,会产生以下几个重要步骤:
(1)test对象传入形参时,会先会产生一个临时变量,就叫C吧。
(2)然后调用拷贝构造函数把test的值给C。整个这两个步骤有点像:CExample C(test);
(3)等g_Fun()执行完后,析构掉C对象。
//全局函数
CExample g_Fun()
{
CExample temp(0);
return temp;
}
int main()
{
g_Fun();
}
当g_Fun()函数执行到return时,会产生以下几个重要步骤:
(1)先会产生一个临时变量,就叫XXXX吧。
(2)然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp);
(3)在函数执行到最后先析构temp局部变量。
(4)等g_Fun()执行完后再析构掉XXXX对象.
CExample A(100);
CExample B = A; //这句和下句都会调用拷贝构造函数。
CExample B(A);
在实现多态时(基类指针可以指向子类的对象),如果析构函数是虚函数,那么当用基类操作子类的时候(基类指针可以指向子类的对象),如果删除该基类指针时,就会调用该基类指针指向的子类析构函数,而子类的析构函数又自动调用基类的析构函数,这样整个子类的对象完全被释放。这是最理想的结果。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不会像上一段那样调用子类析构函数,这样就会造成子类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
参考:https://www.cnblogs.com/mengfanrong/p/4011342.html
注意:面试时起码回答出第一和第二点,最好还有第三点,第四第五是加分项。
答:
解析:纯虚函数声明:virtual函数类型 函数名(参数表列)= 0;
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。如果在一个类中声明了纯虚函数,在其派生类中没有对其函数进行定义,则该虚函数在派生类中仍然为纯虚函数。
注意:
(1)纯虚函数没有函数体;
(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
(3)这是一个声明语句,最后有分号。
Ps:抽象类:不用定义对象而只作为一种基本类型用作继承的类叫做抽象类(也叫接口类),凡是包含纯虚函数的类都是抽象类,抽象类的作用是作为一个类族的共同基类,为一个类族提供公共接口,抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
解析:
静态类型:对象在声明时采用的类型,在编译期既已确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。
在面向对象编程中,创建对象时系统会自动调用构造函数来初始化对象,构造函数是一种特殊的类成员函数,它有如下特点:
1.构造函数的名字必须和类名相同,不能任意命名;
2.构造函数没有返回值;
3.构造函数可以被重载,但是每次对象创建时只会调用其中的一个;
C++中的构造函数可以分为4类:
以Student类为例,默认构造函数的原型为
Student();//没有参数
Student(int num=10,int age=10);
Student(int num,int age);//有参数
Student(const Student&);//形参是本类对象的引用
Student(int r);//形参是其他类型变量,且只有一个形参
1)默认构造函数和初始化构造函数在定义类的对象的时候,完成对象的初始化工作。
Student s2(1002,1008);
2)复制构造函数用于复制本类的对象。
默认的复制构造函数可能会发生【浅拷贝】的问题
Student(Student &b)
{
this.x=b.x;
this.y=b.y;}
Student (s2);//将对象s2复制给s3。注意复制和赋值的概念不同。
Student s4;
s4=s2;//这种情况叫做赋值,自己体会吧
3)转换构造函数的作用是将一个其他类型的数据转换为一个类的对象。转换构造函数也是一种构造函数,它遵循构造函数的一般原则,我们通常把仅有一个参数的构造函数用作类型转换,所把它称为转换构造函数。
转换构造函数中的类型数据可以是普通类型,也可以是类类型。
下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004
Student(int r)
{
int num=1004;
int age= r;
}
解析:至于什么是重写、重载(overload)、覆盖,读者可以自行了解,这是必须掌握的重点概念哦。
(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual的区别:重写的基类中被重写的函数必须要有virtual 修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。
当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
从概念上讲,调用构造函数时,对象在程序进入构造函数函数体之前被创建。也就是说,调用构造函数的时候,先创建对象,再进入函数体。所以如果类成员里面有引用数据成员与const数据成员,因为他们在创建时初始化,若是在构造函数中初始化则会报错。
只有构造函数可以使用初始化列表语法。
class MyClass
{
private:
int a;
int b;
const int max;
};
MyClass(int c)
{
a = 0;
b = 0;
mac = c;//这里会出错 const数据成员若是在构造函数中初始化则会报错。
}
正确的是:
MyClass(int x):a(0),b(0),max(x)
{
}
MyClass(int x):max(x)
{
a = 0;
b = 0;
}
对于普通数据类型,复合类型(指针,引用)等,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的。对于用户定义类型(类类型),结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体时已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这时调用一个构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)。
#include
Using namespace std;
Class A
{
Public:
A()
{
cout<<"A()"<<endl;
}
A(int a)
{
value = a;
cout<<"A(int"<<value<<")"<<endl;
}
A(const A& a)
{
value = a.value;
cout<<"A(const A& a):"<<value<<endl;
}
int value;
};
class B
{
public:
B():a(1)
{
b = A(2);
}
A a;
A b;
};
int main()
{
B b;
system("pause");
}
以上代码对于变量a使用初始化列表初始化,对于变量b使用构造函数初始化;
对于用户定义类型,使用列表初始化可以减少一次默认构造函数调用过程.简单的来说,对于用户定义类型:
1)如果使用类初始化列表,直接调用对应的构造函数即完成初始化
2)如果在构造函数中初始化,那么首先调用默认的构造函数,然后调用指定的构造函数,要调用2次,所以不推荐在构造函数内初始化
问题:引用是否能实现动态绑定,为什么引用可以实现
答:只有指定为虚函数的成员函数才能进行动态绑定,且必须通过基类类型的引用或指针进行函数调用,因为每个派生类对象中都拥有基类部分,所以可以使用基类类型的指针或引用来引用派生类对象。而指针或引用是在运行期根据他们绑定的具体对象确定。
explicit关键字的作用就是防止类构造函数的隐式自动转换. explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数