类模板声明和定义都应放在.h文件。如果分别放在了h和cpp中,在使用的时候不能#include “CCircleQueue.h”,而是#include “CCircleQueue.cpp”。
变量的本质:是一块内存单元的别名,且内存单元的内容可变。
https://www.zhihu.com/zvideo/1418544488600469504?playTime=136.5
变量与内存的关系: CPU无法直接看到内存,CPU与内存在物理上是隔离的,只能通过寄存机获取变量。寄存器是只能存放一个数值的单元。
如果一个内存出现“屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯”
,或者打印出来是cdcdcdcdcdcdcd
,说明内存没有初始化为0.使用memset初始化为0
打调试日志fprintf(stderr, “%s \n”, FUNCTION);
C++ debug下运行正常,release下边直接闪退,最后排查发现竟然是两个类的成员变量没有初始化。各种编译器的默认操作是不同的,debug和release的默认操作也是不同的,所以尽量不要让编译器本身去做事情。
ping通的意思是:二台计算机的网络地址都为相同 且 本机地址不同,所以可以通。
<<优先级问题
char a = 0x07;
char b = 0x01;
char c = 0x07 << 4;
char d = c + b;
char e = (0x07 << 4) + b; //0x71
char f = 0x07 << 4 + b;// f = -32/224
以十六进制输出char数组
如果char数组是这样:char buf[5]={0xfd,0xfe,0xff,0xff,0xff};使用下边的函数
static printf_char(char* buff,int bytelen)
{
int i;
for(i=0; i<bytelen; i++)
{
printf("0x%02x ", (unsigned char)buff[i]);
if( 0 == (i+1)%8 )
printf("\n");
}
printf("\n");
}
结果为0xfd 0xfe 0xff 0xff 0xff
如果char数组是这样:char buf[5]={f,d,f,e,f,f,f,f,f,f};使用下边的函数
static void printf_char(char* buff,int bytelen)
{
int a = 2;
for (int i = 0;i<bytelen;i=i+2,a=a+2)
{
printf("0x%c%c",(unsigned char)buff[i], (unsigned char)buff[i+1]);
printf(" ");
if( a == 16)
{
printf("\n");
a = 0;
}
}
printf("\n");
}
结果为0xfd 0xfe 0xff 0xff 0xff
可重入函数和不可重入函数的概念
在函数中如果我们使用静态变量了,导致产生中断调用别的函数的 过程中可能还会调用这个函数,于是原来的 静态变量被在这里改变了,然后返回主体函数,用着的那个静态变量就被改变了,导致错误。这类函数我们称为不可重入函数。
如果是在函数体内 动态申请内存的话,即便 新的线程调用这个函数也没事,因为新的线程使用的是新的函数的 新申请的动态内存(静态变量只有一份,所以 多线程对于函数体内的静态变量改变 会有无法修复的结果),所以这类函数就是可重入函数。
进程与线程的区别
进程有独立的地址空间
线程是最小的调度单位。
进程切换时,耗费资源较大
父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信
没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作Student stu()或Student stu,在堆上创建对象可以写作Student *pstu = new Student()或Student *pstu = new Student,它们都会调用构造函数 Student()。
默认拷贝构造函数是直接匹配成员进行值传递
直接访问内存地址,void*地址转换
class ccccc
{
public:
ccccc();
int a=0;
int b=0;
};
//方法一: void*存放
c = new ccccc;
c->a=1;
c->b=2;
void **b= reinterpret_cast<void **>(&c);//void*
ccccc *d = reinterpret_cast<ccccc*>(*b);
printf("d->a = %d;d->b = %d;",d->a,d->b); //1 ,2
//方法二:void**存放
void * e = &c;
int* address = reinterpret_cast<int*>(d);
ccccc *f = (ccccc *)*address;//直接访问地址
printf("f->a = %d;f->b = %d;",f->a,f->b); //1 ,2
C++对字符串中所有指定的子串进行替换
/*
函数说明:对字符串中所有指定的子串进行替换
参数:
string resource_str //源字符串
string sub_str //被替换子串
string new_str //替换子串
返回值: string
*/
static std::string subreplace(std::string resource_str, std::string sub_str, std::string new_str)
{
std::string::size_type pos = 0;
while((pos = resource_str.find(sub_str)) != std::string::npos) //替换所有指定子串
{
resource_str.replace(pos, sub_str.length(), new_str);
}
return resource_str;
}
C查找指定字符位置
char *strchr(const char *str, int c),
即在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。strchr函数包含在C 标准库 <string.h>
无符号数和有符号数
C语言中无符号数和有符号数一起比较、运算时,有符号数会隐式转换到无符号数。
1.无符号数—>有符号数(最高位为1表示为负数)
首先判断无符号数的最高位是否为1,如果不为1,则有符号数就直接等于无符号数;如果为1,则将无符号数取补码,得到的数就是有符号数。
本质上就是无符号数在存储器中的二进制数直接按照有符号数来解析。
现象上也可理解为无符号数先看成有符号数然后取补码。因为取补码时符号位不变,正数的补码就是原码。
正数的补码为自身
负数的补码 = {原码符号bai位不变du} + {数值位按位取反后+1}
以unsigned char 和 signed char为例子:
定义
-128~+127
0~255
unsigned char ui;
signed char si;
1.1 将无符号数2转为有符号数
前提:
ui = 2;
si = ui;
结果:
si = 2;
2的原码是:0000 0010,最高位不为1,因此si = 0000 0010。
1.2 无符号数130转为有符号数
前提:
ui = 130;//无符号
si = ui;
结果:
si = -126;
130的原码是:1000 0010,最高位为1,对其取补码为1111 1110,所以si = 1111 1110 值得到的结果是-126。
2.有符号数—>无符号数
首先判断有符号数的最高位是否为1,如果不为1,则无符号数就直接等于有符号数;如果有符号数的最高位为1,则将有符号数取补码,得到的数就是
无符号数。
本质上是有符号数在存储器中的二进制数直接按照无符号数来解析,
2.1 将有符号数3转为无符号数
前提:
si = 2;
ui = si;
结果:
ui = 2;
2的原码是:0000 0010,可知最高位不为1,因此ui = 0000 0010。
2.2 将有符号数-2转为无符号数
-2的在存储器中按补码存放的,二进制表示为:1111 1110,此二进制数按照无符号数解析也就是首位不再表示符号则该值为254。
前提:
si = -2;
ui = si;
结果:
ui = 254;
另外,上述以char举例,如果换成short或者int等等,要注意只有首位才是符号位。
堆和栈的区别
-
管理方式不同
栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。
-
空间大小不同
栈的空间有限;堆内存可以达到4G,。
-
能否产生碎片不同
栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete
会造成内存的不连续,从而造成大量的碎片。
-
生长方向不同
堆的生长方式是向上的,栈是向下的。
-
分配方式不同
堆是动态分配的。栈可以是静态分配和动态分配两种,但是栈的动态分配由编译器释放。
32位编译器和64位编译器的字节问题
32位编译器:
char : 1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 4个字节
long long: 8个字节
unsigned long: 4个字节
64位编译器:
char : 1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int: 4个字节
unsigned int : 4个字节
float: 4个字节
double: 8个字节
long: 8个字节
long long: 8个字节
unsigned long: 8个字节
结构对齐问题
结构体所占的位数必然是结构体内占用位数最多的成员所占位数的倍数。
结构体作为成员,类型大小按其成员所含最大类型计算。
例子:
#pragma pack(n),n=(1,2,4,8,16)
VC编译器默认是8,linux为4。
目前考虑VC情况
typedef struct example1{
char a;//1字节
int b;//4字节
short c;//2字节
double d;//8字节
}example1;
typedef struct example2{
char a;//1字节
short b;//2字节
int c;//4字节
double d;//8字节
}example2;
sizeof(example1) = 24;
sizeof(example2) = 16;
原因:
example1:
按顺序观察abcda存储时,
a为char占用1个字节,直接存储
b为int占用4个字节,此时为了字节对齐,a也分配4个字节。所以此时ab共占用8个字节。
c为short占用2字节,此时为4字节对齐,所以c分配4个字节。所以此时abc共占用12个字节
d为double占用8个字节,abc此时共占用12个字节不是8的倍数,所以要补充4个字节,所以此时abc占用16个字节,abcd共占用24个字节
example2:
按顺序观察abcda存储时,
a为char占用1个字节,直接存储
b为short占用2字节,此时为2字节对齐,所以a分配2个字节。所以此时ab共占用4个字节
c为int占用4个字节,此时ab正好占用4个字节,所以不必再补充字节。所以此时abc共占用8个字节。
d为double占用8个字节,abc此时共占用8个字节且是8的倍数,所以要不需要补充字节,所以此时abc占用8个字节,abcd共占用16个字节
typedef struct example3{
char a;
example1 ex1;
}example3;
sizeof(example3)=32
原因:结构体作为成员,类型大小按其成员所含最大类型计算。
ex1为类型example1 ,example1 中最大类型为double,占用8个字节,所以example1 在example3中是8字节类型。char a占用一个字节,所以char a要补充7个字节。又因为ex1实际大小为sizeof(example1)=24,所以24+8=32。
class CTest
{
public:
CTest():m_chData(‘\0’),m_nData(0)
{
}
virtual void mem_fun(){}
private:
char m_chData;
int m_nData;
static char s_chData;
};
char CTest::s_chData=’\0’;
类大小计算
- 空类 大小为1
- 成员函数不占空间
- 静态成员不占空间
- 虚函数因为存在一个虚函数表,需要4个字节,数据成员对象如果为指针则为4字节
- 普通继承时,共享基类虚表vfptr(virtual function ptr)
- 虚继承时候,虚拟继承会给继承类添加一个虚基类指针(virtual base ptr 简称vbptr),其位于类虚函数指针后面,成员变量前面,若基类没有虚函数,则vbptr其位于继承类的最前端。
虚继承时候VC下继承类会有自己的虚函数指针,而在Gcc下是共用基类的虚函数指针
关于虚拟继承,首先我们看看为什么需要虚拟继承及虚极继承解决的问题。
虚继承作用
虚拟基类是为解决多重继承而出现的
假设类a是父类,b类和c类都继承了a类,而d类又继承了b和c,那么由于d类进行了两次多重继承a类,就会出现两份相同的a的数据成员或成员函数,就会出现代码冗余。同时,我们在d类的实例化对象中调用从a类继承来的数据成员或者成员函数的时候,无法分清是来自于b类或者是c类,会发生编译错误。这也就是我们常说的二义性。
故为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。那么解决这个问题的关键就是虚继承。
类的成员初始化问题
常量和引用,必须通过参数列表进行初始化。
有些成员变量的数据类型比较特别,它们的初始化方式也和普通数据类型的成员变量有所不同。这些特殊的类型的成员变量包括:
a.引用
b.常量
c.静态
d.静态常量(整型)
e.静态常量(非整型)
常量和引用,必须通过参数列表进行初始化。
静态成员变量的初始化也颇有点特别,是在类外初始化且不能再带有static关键字。
构造函数体里边的不是初始化,而是一种普通的运赋值运算,效率比初始列表低
类的静态成员
声明:
1、加static标识。
定义:
1、static数据成员必须在类定义体的外部定义;
静态成员属于类作用域,但不属于类对象,和普通的static变量一样,程序一运行就分配内存并初始化,生命周期和程序一致。
所以,在类的构造函数里初始化static变量显然是不合理的。
静态成员其实和全局变量地位是一样的,只不过编译器把它的使用限制在类作用域内(不是类对象,它不属于类对象成员),要在类的定义外(不是类作用域外)初始化。
虚函数的作用
首先:
虚函数主要是用来实现多态和多重继承的
其次:
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
虚函数的底层实现机制
实现原理:虚函数表+虚表指针
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:
如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
*p++、*(p++)、(p)++、++p、++*p的区别
后缀运算符++优先级高于前缀运算符++和*
后缀++结合律从左至右(先返回值后自增)
前缀++和*优先级相同结合律从右至左
*p++与*(p++)
相同,后缀++优先级更高,但后缀++先返回值(指针p),指针p与*结合之后,指针p再++,因此对应的结果是,输出指针p在自增前对应的值,指针p自增。(*p)++
括号优先级最高,因此先对指针p取值,然后后缀++先返回值p,再对p这个整体自增,因此对应结果是输出p的值,之后p的值自增1,指针p指向的位置不变。*++p 即*(++p)
,最左是*,但后面跟的是表达式 ++p 所以要先算++p++*p 即++(*p)
,最左是++ 但后面跟的是表达式p 所以要先算p
指针与引用的区别
指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已
C++什么时候用继承什么时候用组合
如果并不是需要一个类的所有东西(包括接口和熟悉),那么就不需要使用继承,使用组合更好
如果使用继承,那么必须所有的都继承,如果有的东西你不需要继承但是你继承了,那么这就是滥用继承
C++什么是友元,为什么要使用友元
什么是友元
友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制
为什么要使用友元
在实现类之间数据共享时,减少系统开销,提高效率。但它破坏了类的封装性和隐藏性
友元类 :
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
class Radius {
friend class Circle; //声明Circle为Radius的友元类
friend void Show_r(Radius &n); //声明Show_r为友元函数
}
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
使用友元函数的场景:
1)运算符重载的某些场合需要使用友元。
2)两个类要共享数据的时候
指针常量和常量指针
https://blog.csdn.net/weixin_41028621/article/details/89159896
指针和 const 谁在前先读谁 ;
*象征着地址,const象征着内容;
谁在前面谁就不允许改变。
例如:
int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量
const和int互换位置没有影响
const int *p1 =&b //const 在前,定义为常量指针
常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。
BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段、代码段、BSS段、堆栈段
BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)若程序员不释放,则会有内存泄漏,系统会不稳定,Windows系统在该进程退出时由OS释放,Linux则只在整个系统关闭时OS才去释放(参考Linux内存管理)。
栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
它是由操作系统分配的,内存的申请与回收都由OS管理。
//main.cpp
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //两个过程,1在常量区存入“abc”,在栈上开辟空间复制常量区的“abc”
char *p2; 栈
char *p3 = "123456"; //123456在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10);//堆
p2 = (char *)malloc(20);//堆
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
逗号表达式
逗号表达式的语法为:
表达式1,表达式2,表达式2...表达式n
C++顺序计算表达式1,表达式2,……,表达式n的值。例如:
int a,b,c;
a=l,b=a+2, c=b+3;
由于按顺序求值, 所以能够保证b一定在a赋值之后, c一定在b赋值之后。该逗号表 达式可以用下面3个有序的赋值语句来表示:
a=1;
b=a+2;
c=b+3;
逗号表达式是有值的,这一点是语句所不能代替的。***逗号表达式的值为第n个子表达 式的值,即表达式n的值
***。例如:
int a,b,c,d;
d=(a=1,b=a+2,c=b+3);
cout<<d<<endl;
输出结果为:
6
上例中输出的结果d即为c的值。
逗号表达式还可以用于函数调用中的参数。例如:
func(n,(j=1,j+4),k);
该函数调用有3个参数,中间的参数是一个逗号表达式。括号是必须的,否则,该函数有4个参数了。逗号表达式作为值的形式,可以用于几乎所有的地方。
C++中,如果逗号表达式的最后一个表达式为左值,则该逗号表达式为左值。例如:
(a=1,b,c+1,d)=5; //ok:即d=5
->在C中,逗号表达式是不能作左值的,所以 “(a=1,b,c+1,d)=5;”将通不过编译
只有一个普通成员函数的类的sizeof大小为1
空类也为1
多态的实现必要的三个条件:1、存在继承;2、虚方法重写;3、父类指针或引用指向子对象;
如果同时在类中,对于函数名相同的const函数和非const函数能够构成重载
它们被调用的时机为:如果定义的对象是常对象,则调用的是const成员函数;如果定义的对象是非常对象,则调用重载的非const成员函数。
C++运算符重载
对于一个运算符函数来说,重载运算符必须和用户定义的自定义类型的对象一起使用即它或者是类的成员,或者至少包含一个类类型的参数
运算符重载声明中的参数的顺序不能随意变更
MyClass operator *(double ,MyClass);
MyClass operator *(MyClass ,double);是重载
C++函数声明的时候后面加const
非静态成员函数后面加const(加到非成员函数或静态成员后面会产生编译错误),表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,任意修改它所在的类的成员的操作都是不允许的(因为隐含了对this指针的const引用);唯一的例外是对于mutable修饰的成员。加了const的成员函数可以被非const对象和const对象调用,但不加const的成员函数只能被非const对象调用。例如:
class A {
private: int m_a;
public:
A() : m_a(0) {}
int getA() const {
return m_a; //同return this->m_a;。
}
int GetA() {
return m_a;
}
int setA(int a) const {
m_a = a; //这里产生编译错误,如果把前面的成员定义int m_a;改为mutable int m_a;就可以编译通过。
}
int SetA(int a) {
m_a = a; //同this->m_a = a;
}
};
A a1;
const A a2;
int t;
t = a1.getA();
t = a1.GetA();
t = a2.getA();
t = a2.GetA(); //a2是const对象,
double和float
强制类型转换的优先级高于+ - * /
(short)10/10.2*2先进行强制转换再进行计算,又由于式子中存在浮点数,所以会对结果值进行一个自动类型的提升,浮点数默认为double,所以答案是double。
首先浮点型不能精确比较,错误例子while ( x != 1.0 )
智能while ( x < 0.0000001 )
内联函数
内联函数是使用inline关键字声明的函数,也成内嵌函数,它主要的作用是解决程序的运行效率。
使用内联函数的时候要注意:
1.递归函数不能定义为内联函数
2.内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。
3.内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。
4.对内联函数不能进行异常的接口声明。
数组的类型
待续
main 函数 执行前 和执行后会执行什么?
main函数之前主要进行初始化的工作,包括:
- List item
- 设置栈指针
- 初始化static 静态和global 全局变量,即data段的内容
- 将还没有初始化的全局变量进行赋值,eg:数值型 short int long 等为0,bool 为false 指针为null,等等,即 bss段
- 运行全局构造器,进行 C++ 中的函数构造
- 将main 函数的参数,argc,argv 等传递给main函数,才能 真正运行
之后再执行某个函数应该怎么做
如果你需要加入一段在main退出后执行的代码,可以使用atexit()函数,注册一个函数。
友元函数重载和成员函数重载
友元函数重载时,参数列表为1,说明是1元,为2说明是2元
成员函数重载时,参数列表为空,是一元,参数列表是1,为2元
逻辑运算符
-
^ 异或,相同为0,不同为1
- 取反
const成员函数,mutable修饰成员变量。
= default
在程序员重载了自己上面提到的C++编译器默认生成的函数之后,C++编译器将不在生成这些函数的默认形式。
静态成员需要在类内声明,类外初始化。只有整型静态常量才能在类中初始化。
类在编译的时候,先编译成员变量,再成员函数
不完全类型的定义(它是个啥)
定义:已经声明但是尚未定义的类型。不完全类型不能用于定义变量或者类的成员,但是用不完全类型定义指针或者引用是合法的。(来自C++ Primer P274)
对于类型来说:
声明(引用声明):告诉编译器这个类型已经存在,但此时编译器并不知道需要给该类型的对象分配多少字节的内存。
定义(定义声明):描述了该类型的细节,编译器由此可以知道需要给该类型的对象分配多大的内存。
换句话说,不完全类型的定义 就是 尚未定义完全的类型。(编译器尚不知道给该类型对象分配多大内存)
关于友元类的注意事项:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。
虚继承(Virtual Inheritance)
为了解决多继承(比如菱形继承)时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
这里总结几点吧(以下类都是针对有虚函数的类):
1,每个类都有虚函数表,这个虚函数表是在编译阶段构建,在代码段产生一个vtbl
2,每次实例化的时候,构造函数在前几个字节,产生一个指向虚函数表的指针,指向代码段的那个虚函数表
3,虚函数的实现与调整,是通过移动或变换虚函数表的指针来实现的。
4,纯虚函数是指只声明,但未被实现的虚函数,具有纯虚函数的类不能被实例化,为抽象类
C++的拷贝构造函数是C++默认的四个函数之一:构造函数、析构函数、赋值函数、拷贝构造函数
拷贝构造函数是一种特别的构造函数,在《深度探索C++对象模型》书中说,有三种情况,会导致拷贝构造函数被触发:
1,以一个object的内容作为另一个class object的初始值
class X {…}
X x;
X xx=x;
2,当object被当作参数传递给某个函数时
void foo(X x);
X xx;
foo(xx);
3,函数传回一个class object的时候
X foo_bar()
{
X xx;
// …
return xx;
}