C++ virtual关键字说明

vim使用:

vi ~/.vimrc

set nu -> 设置行号
set tablestop=4 -> 设置tab键为4空格


shift+v 向下箭头全选 之后按= 重新编排结构

引用:
按值传递
按引用传递,避免复制大量数据的开销,可以提高性能
引用必须初始化
引用和变量指向同一块内存

引用和指针的差别:
指针是一个变量,可以把它再赋值成指向别处的地址;
建立引用时必须进行初始化并且决不会再关联其他不同的变量

没用引用的指针和引用的应用
有空指针无孔引用

***** (Object Oriented Programing)
类 对象 封装 ***
面向对象编程开发范式的特性
万物皆对象
程序是一组对象彼此之间在发送消息
每个对象都有自己的内存占用,可以组装成更大对象
每个对象都有类型,特定类型的所有对象可以接受相同消息
类:类是创建对象的模板和蓝图(抽象)
对象:是类的实例化结果(实实在在的存在的)
||
行为:对象能干什么
状态:对象的属性,行为的结果
标识:对象的唯一标识
定义一个类的步骤:定义类名;编写;类的数据成员代表属性;编写类的方法代表行为
protected -> 继承的时候会使用

在类中定义的成员函数一般为内联函数,即使没有明确用inline标识
this指针当前对象所占内存对象的地址

封装:
将数据成员和成员函数包装进类中,加上具体实现的隐藏,共同被称为封装,其结果是一个同时带有特性和行为的数据类型。
定义类,定义其数据成员、成员函数的过程称为封装类。


构造函数和析构函数 ****
声明变量赋初值
构造函数用于隐式类型转换
析构函数只有一个,不能重载


标准库类型string (***)
操作方法

static类成员
静态数据成员在类外分配空间和初始化
在static成员函数中不能使用this指针
即使没有实例化类的对象,static数据成员和成员函数仍然可以使用

动态内存分配 *****
new/delete
malloc/free需要配对使用
new[]/delete[]生成或释放对象数组
new/delete是运算符,可以重载,malloc/free是函数调用

BSS段
data段
静态变量 -> 全局/静态数据区中
rodata存放在常量数据
栈中存储自动变量或者局部变量,以及传递的参数等;
堆是用户程序控制的存储区,存储动态产生的数据。

拷贝构造函数 ****
是一种特殊的构造函数,具有单个形参,此形参是对该类型的引用。
拷贝构造函数:Student(const Student&); //保证传入的对象是不变的
用一个对象构造另一个对象
拷贝临时对象

浅拷贝:地址
深拷贝:申请内存区域
如果一个类需要析构函数来释放资源,则它也需要一个拷贝构造函数。
如果想禁止一个类的拷贝构造,需要将拷贝构造函数声明为private。 (=delete , C++11)


const关键字 ***
const出现在星号左边,表示被指物是常量;
const出现在星号右边,表示指针自身是常量;
const数据成员必须使用成员初始化列表进行初始化; ***


友元函数和友元类 ***
在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问。
友元(friend)机制允许一个类将对其非公有成员的访问权授予指定的函数或类。

函数重载 ***
两个以上函数,取相同的函数名,但是形参个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,
自动确定调用哪一个函数,这就是函数的重载。

#ifdef __cplusplus
extern "C" {
#endif

void func();

#ifdef __cplusplus
}
#endif

查看符号表:
objdump -t main.o -> objdump输出符号表


函数模板 -> 类模板 ****
泛型编程:独立于任何特定类型的方式编写代码
模板是泛型编程的基础
STL/BOOST

使用typename以及class区分

std::unorder_map的key部分是const类型的,在哈希表中std::pair的类型不是std::pair<std::string, int>,而是std::pair<const std::string, int>。
但是这不是循环体外变量p的声明类型。后果就是,编译器竭尽全力去找到一种方式,把std::pair<const std::string, int>对象转化为std::pair <string, int>对象(p的声明类型)。
这个过程将通过复制m的一个元素到一个临时对象,然后将这个临时对象和P绑定完成。在每个循环结束的时候这个临时对象将被销毁。
std::unorder_map <std::string, int> m;
for(const std::pair <std::string, int>& p:m)
{
//do something with p
}
||
for(cont auto& p:m)
{
//as before
}

decltype一般只是复述一遍你所给他的变量名或者表达式的类型。
decltype和引用
decltype返回表达式结果对应的类型,结果有可能是引用类型
解释:
decltype(r)的结果是应用类型,如果r作为表达式的一部分,此时r代表的是所指的类型,表达式的结果表示的是一个值而非引用。
如果表达式是解引用操作,则decltype将得到引用类型,解引用指针得到的是指针所指的对象,可以赋值
赋值是产生引用的一种典型表达式

decltype((variable))的结果是引用
如果变量加上了一层或多层括号,编译器认为变量是一种可以作为赋值语句左值的表达式。

decltype声明返回数据指针的函数
声明一个返回数组指针的函数,三种方法:
1、Type(*function(parameter_list))[demension],比如;int(*f(int))[10]
2、C++11有有一种简化声明的方法,尾置返回类型:auto f(int i)-> int(*)[10];
3、使用decltype:int a[10]; decltype(a) *arrPtr(int i);

记住:
1、decltype几乎总是得到一个变量或表达式的类型而不需要任何修改
2、对弈非变量的类型为T的左值表达式,decltype总是返回T&
3、C++14支持decltype(auto),它的行为就像auto,从初始化操作类推到类型,但是它推到类型时使用decltype规则。

数组指针和指针数据
函数指针---函数的调用可哟通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为参数传递给其他函数,也就是回调函数。

函数指针数据&指向函数指针数组的指针
函数指针数组,其意义就是定义一个数组,数据的内容均是指向函数的指针。 int (*arr1[10])();
指向函数指针数据的指针,其表达式为:void (*(*p)[5]))(void) 表示一个指向有5个元素,每个元素为指向一个返回值为空的函数的数组的指针。


拷贝构造函数
C++有三种情况下会调用拷贝构造函数(可能有纰漏),
第一种情况是函数形实结合时,
第二种情况是函数返回时,函数栈区的对象会复制一份到函数的返回去,
第三种情况是用一个对象初始化另一个对象时也会调用拷贝构造函数。
除了这三种情况下会调用拷贝构造函数,另外如果将一个对象赋值给另一个对象,这个时候会调用重载的赋值运算符函数。
强调:一定要注意指针的浅层赋值问题。(浅拷贝、深拷贝)
||
移动构造函数
有时候我们会遇到这样一种情况,我们用对象a初始化对象b,然后对象a我们就不再使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容
赋值一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
||
拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。
但是,上面提到,指针的浅层复制是非常危险的呀,没错,确实很危险,而且通过上面的例子,我们业务可以看出,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,
若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,
由于 有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间)
||
但要注意,我们这样使用有一个前提是:用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构。如果说,我们用a初始化了b后,仍要对a进行操作,用这种浅层复制的方法就不合适了
||
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。
(关于右值引用大家可以查找资料->)
这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用一个右值,或者将亡值初始化两一个对象的时候,才会调用移动构造函数。
而那个move语句,就是将一个左值变成一个将亡值。
||
reference:https://www.cnblogs.com/qingergege/p/7607089.html

”三值“:左值,纯右值,将亡值
1、尽可能地将这些概念介绍清楚。
2、为后续介绍完美转发和移动语义做好铺垫。
||
表达式:由运算符(operator)和运算对象(operand)构成的计算式(类似于数学上的计算表达式)。
举例:字面值(literal)和变量(variable)是最简单的表达式,函数的返回值也被认为是表达式。
||
值类别:表达式是可求值的,对表达式求值将得到一个结果,这个结果有两个属性:类型和值类别。
在C++11以后,表达式按值类别分,必须属于以下三者之一:左值,将亡值,纯右值
其中,左值和将亡值合称泛左值(generalized lvalue, glvalue),纯右值和将亡值合成右值(right value,rvalue)。
||
实际上,无论是左值、将亡值还是纯右值,目前都没有一个精准的定义。它们实际上表征了表达式的属性,而这种属性的区别主要体现在使用上。
首先做到:给定一个表达式,能够正确地判断出它的值类型。
||
左值:能够用&取地址的表达式是左值表达式。
举例:函数名和变量名(实际上是函数指针和具名变量,具名变量如std::cin、std::endl等)、返回左值引用的函数调用、前置自增/自减运算符连接的表达式++i/--i、
由赋值运算符或复合赋值运算符连接的表达式(a==b,a%=b)、解引用表达式*p、字符串字面值"abc"等。
纯右值:
1、本身就是赤裸裸的、纯粹的字面值,如3、false;
2、求值结果相当于字面值或是一个不具名的临时对象;
举例:除了字符串字面值以外的字面值、返回非引用类型的函数调用、后置自增/自减运算符连接的表达式i++/i--、算术表达式(a+b,a&b,a<<b)、
逻辑表达式(a&&b、a||b、~a)、比较表达式(a==b、a>=b、a<b)、取地址表达式(&a)等。
1、++i是左值,i++是右值。
前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;
而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果对i加1前i的一份拷贝,所以它时不具名的。
假设自增前i的值是6,那么,++i得到的结果是7,这个7有个名字,就是i;而i++得到的结果是6,这个6是i加1前的一个副本,它没有名字,i不是它的名字,i的值此时也是7.
2、解引用表达式*p是左值,取地址表达式&a是纯右值。
&(*p)一定是正确的,因为*p得到的是p指向的实体,&(*p)得到的就是这一实体的地址,正是p的值。由于&(*p)的正确,所以*p是左值。
而对&a而言,得到的是a的地址,相当于unsigned int型的字面值,所以是纯右值。
3、a+b、a&&b、a==b都是纯右值
a+b得到的是不具名的临时对象,而a&&b和a==b的结果非true即false,相当于字面值。
||
将亡值:将亡值是由右值引用的产生而引起的,将亡值与右值引用息息相关。(不具名的右值引用是右值)
1、返回右值引用的函数的调用表达式
2、转换为右值引用的转换函数的调用表达式
在C++11中,我们用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源(所谓资源,就是指new出来的东西),而当我们用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符⑤来移动资源,从而避免拷贝,提高效率(关于这些知识,在后续文章讲移动语义时,会详细介绍)。当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(析构)。也就是说,当一个右值准备完成初始化或赋值任务时,它已经“将亡”了。而上面1)和2)两种表达式的结果都是不具名的右值引用,它们属于右值(关于“不具名的右值引用是右值”这一点,后面还会详细解释)。又因为
1)这种右值是与C++11新生事物——“右值引用”相关的“新右值”
2)这种右值常用来完成移动构造或移动赋值的特殊任务,扮演着“将亡”的角色
所以C++11给这类右值起了一个新的名字——将亡值。


*****
右值引用就是让返回的右值(临时对象)重获新生,延长生命周期。临时对象析构了,但是右值引用存活。
这里有一个函数就是move函数,它能够将左值强制转换成右值引用。

总结了左值引用和右值引用的绑定规则(函数类型对象会有所例外):

(1)非const左值引用只能绑定到非const左值;
(2)const左值引用可绑定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能绑定到非const右值;
(4)const右值引用可绑定到const右值和非const右值。

注意:
字符串字面值是左值。-> 字符串字面值首字符是其地址。
具名的右值引用是左值,不具名的右值引用是右值。
移动构造函数, 注意避免悬垂指针。

创建线程
int pthread_create(pthread_t *thread, pthread_att_t *attr, void *(*start_route)(void *), void *arg)
thread是一个pthread_t类型的指针,可以简单理解为线程ID,
attr表示该线程的属性,具体没有看,一般的程序中都设置为NULL,表示默认属性。
start_route是线程函数体的函数之指针,
arg是线程函数的参数。
线程函数的类型是void *fun(void *)也就是可以带一个指针参数,也可以返回一个指针。


父线程回收子线程资源
当子线程运行结束后,还有以下资源要回收。
int pthread_join(pthread_t th, void **thread_return)
th是pthread_t类型的变量,可能理解为线程ID,
thread_return是子线程函数的返回值。


#include <semaphore.h>
semaphore常见函数介绍:
初始化信号量:
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem表示待初始化的信号量
pshared表示共享属性,Linux中貌似只能设置为0
value表示信号量的初始值

申请资源
int sem_wait(sem_t *sem);

释放资源
int sem_post(sem_t *sem);

销毁信号量
int sem_destroy(sem_t *sem);


#icnlude <unistd.h>
初始化一个互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
mutex表示待初始化的互斥锁;
mutex表示待初始化的互斥锁,mutexattr表示互斥锁的属性,没仔细研究,一般程序中都是使用的NULL。表示默认属性。

互斥锁加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。
命名冲突问题:(菱形继承)
类A派生出类B和类C,类D继承自类B和类C,这个时候类A中成员变量和成员函数继承到类D中变成了两份,
一份来自A->B->D,另一份来自A->C->D这条路径。

在一个派生类中保留着基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,
但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。
假如类A有一个成员变量a,那么在类D中直接访问a就会产生歧义,编译器不知道它究竟来自A->B->D,还是来自A->C->D这条路径。

虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。


C++标准库中的iostream类就是一个虚继承的实际应用案例。iostream从istream和ostream直接继承而来,而istream和ostream又都继承自一个公共的名为base_ios的类,
是典型的菱形继承。此时istream和ostream必须采用虚继承,否则将导致iostream类中保留两份base_ios类的成员。

https://blog.csdn.net/csdn1126274345/article/details/79606609


预计三学时
virtual虚继承及虚基类

1.语法
struct CSubClass:public virtual CBase {};其中CBase称之为CSubClass的虚基类,而不是说CBase就是个虚基类,因为CBase还可以不是虚继承体系中的基类。

2.语义
在C++语言中,仅仅有两个地方可以使用virtual这个关键字,一个就是类成员虚函数和虚继承。
virtual定义:
1. 实质上的,实际上的;瑞然没有实际的事实、形式或名义,但是在实际上或效果上存在或产生的;*
2. 虚的,内心的:在头脑中存在的,尤指臆想的产物。用于文学批评中。

采用第一个定义,也就是说被virtual所修饰的事物或现象在本质上是存在的,但是没有直观的形式表现,无法直接描述或定义,需要通过其他的间接方式或手段才能够体现出其实际上的效果。
存在,但间接。
其中关键就在于存在、间接和共享这三种特征。

对虚继承而言,理解这三个特性:
存在:表示虚继承体系和虚基类确实存在,间接性表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成,
间接性:表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成,
共享性:表象在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。

一旦出现了虚基类,就必须在每一个继承类中都必须包含虚基类的初始化语句。
虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象(这里和多继承是完全不同的)。
这样一来,既然是共享的那么每一个子类都不会独占,但是总还是必须要有一个类来完成基类的初始化过程(因为所有的对象都必须被初始化,哪怕是默认的)。,
同时还不能够重复进行初始化,那到底谁应该初始化呢?
C++标准中(也是很自然的)选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),而在最下层继承子类中实际执行初始化过程。
所以上面在每一个继承类中都要书写初始化语句,但是在创建对象时,而仅仅会在创建对象用的类构造函数中实际的执行初始化语句,其他的初始化语句都会被压制不调用。
<作者的意思是:一个继承体系中,A->B->C->D,构造一个D对象的话,A在内存中只有一份真实的存在,而不是4份,就存在着谁来初始化的问题。当然是D,因为生成的对象是D的对象!>


3.模型
为了实现上面所说的三种语句含义,在考虑对象的实现模型(也就是内存模型)时就很自然了。在C++中对象实际上就是一个连续的地址空间的语义代表,我们来分析虚继承下的内存模型。
1.存在
也就是说在对象内存中必须要包含虚基类的完整子对象,以便能够完成通过地址完成对象的标识。那么至于虚基类的子对象会存放对象的那个位置(调皮、中间、尾部)则由各个编译器选择,没有差别。
2.间接
间接性表明了在直接虚继承子类中一定包含了某种指针(偏移或表格)来完成通过子类方位虚基类子对象(或成员)的间接手段(因为虚基类子对象是共享的,没有确定关系),
至于采用何种手段由编译器选择。
(eg:在VC8中在子类中放置了一个虚基类指针vbc,该指针指向虚函数表中的一个slot,该slot中存放着虚基类子对象的偏移量的负值,实际上就是以补码表示的int类型的值,在计算虚基类子对象首地址时,需要将该偏移量取绝对值相加,这个主要是为了和虚表中只能存放虚函数地址这一要求相区别,因为地址是原码表示的无符号int类型的值)
3.共享
表明了在对象的内存空间中仅仅能够包含一份虚基类的子对象,并且通过某种间接的机制来完成共享的引用关系。

4.性能
由于有了间接性和共享性两个特征,所以决定了虚函数体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。
时间:
在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),
其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。
空间:
由于共享所以不存在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。

5.应用
一般情况下,如果你确定出现多继承没有必要,必须要共享基类子对象的时候可以考虑采用虚继承关系(C++标准ios体系就是这样的)。
由于每一个继承类都必须包含初始化语句而又仅仅只在最底层子类中调用,这样可能就会使得某些上层子类得到的虚基类子对象的状态不是自己所期望的(因为自己的初始化语句被压制了),
所以一般建议不要在虚基类中博啊汗任何数据成员(不要有状态),只可以作为接口类来提供。


分析:
1. 由于虚函数引入的间接性指针所以导致了虚继承类的尺寸会增加4个字节;
2. 由于layout输出可以看出,虚基类子对象被放在了对象的尾部(偏移为16),并且vbc指针必须紧紧的接在虚基类子对象的前面,所以vbc指针所指向的内容为“偏移-4”;
3. 由于V8将偏移放在了虚函数表中,所以为了区分函数地址和偏移,所以偏移是为了补码int表示的负值
4. 间接性可以通过性能来看出,在虚继承体系同通过指针访问成员时的时间一般是一般类访问情况下的4倍左右,符合汇编语言输出文件中汇编语句的安排。

为什么要引入虚拟继承
虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要(因为这样只会降低效率和占用更多的空间)。
白杨《RTTI、虚函数和虚基类的开销分析及使用指导》

“在正确的场合使用恰当的特性” 对称职的C++程序员来说是一个基本标准。想要做到这点,首先要了解语言中每个特性的实现方式及其开销。
C++引入的额外开销体现在以下两个方面:
1.编译时开销
模板、类层次结构、强类型检查等新特性,以及大量使用了这些新特性的C++模板、算法库都增加了C++编译器的负担。
但是应该看到,这些新机能在不降低,甚至(由于模板的内联能力)提升了程序执行效率的前提下,明显减轻了广大C++程序员的编码量。

2.运行时开销
运行时开销恐怕是程序员最关心的问题之一了。相对与传统C程序而言,C++中有可能引入额外运行时开销的新特性包括:
1.虚基类
2.虚函数
3.RTTI(dynamic_cast和typeid)
4.异常
5.对象的构造和析构

C++之所以比C“低效”,其根本原因在于:由于对某些特性的实现方式及其 导致的开销不够了解,致使程序员在错误的场合使用了错误的特性。而这些错误基本都集中在:
把异常当作另一种流控机制,而不是仅将其用于错误处理中
一个类和/或其基类的构造、析构函数过于臃肿,包含了很多非初始化/销毁范畴的代码
滥用或不正确地使用RTTI、虚函数和虚基类机制


RTTI:
时间开销:几次整形比较和一次取址操作(可能还会有1、2次整形加法)
空间开销:每类型一个type_info对象(包括类型ID和类名称),典型情况下小于32字节

虚函数:
时间开销:一次整形加法和一次指针间接引用
空间开销:每类型一个虚表,典型情况下小于128字节,每对象若干个(大部分情况下是一个)虚表指针,典型情况下小于8字节。

虚基类:
时间开销:从直接继承的子类中访问虚基类的数据成员或其虚函数时,将增加两次指针间接引用和一次整形加法(大部分情况下可以优化为一次指针间接引用)。
空间开销:每类型一个虚基类表,典型情况下小于32字节,每对象若干虚基类表指针,典型情况下小于8字节,在同时使用了虚函数的时候 ,虚基类表可以合并到虚表(virtual table)中,每对象的虚基类表指针(vptr)也可以省略(只需vptr即可)。

其中,“每类型”或“每对象”是指用到该特性的类型\对象。对于为用到这些功能的类型以及对象,则不会增加上述开销。

"dynamic_cast" 用于在类层次结构中漫游,对指针或引用进行自由的向上、向下或交叉强制。"typeid" 则用于获取一个对象或引用的确切类型,与 "dynamic_cast" 不同,将 "typeid" 作用于指针通常是一个错误, 要得到一个指针指向之对象的type_info,应当先将其解引用(例如:"typeid(*p);")。

一般地讲,能用虚函数解决的问题就不要用 "dynamic_cast",能够用 "dynamic_cast" 解决的就不要用 "typeid"

C++虚函数原理
类中的成员函数分为静态成员函数和非静态成员函数,而非静态成员函数又分为普通函数和虚函数。

Q:为什么使用虚函数。
A: 使用虚函数,可以获得良好的可扩展性。
在一个设计比较好的面向对象程序中,大多数函数都是与基类的接口进行通信。
因为使用基类接口时,调用基类接口的程序不需要改变就可以适应新类。
如果用户想添加新能够,他就可以从基类继承并添加相关的新功能。

Q:简述C++虚函数作用及底层实现原理
A: 要点是要答出虚函数和虚函数表指针的作用。
虚函数是用来实现动态绑定的。
C++中虚函数使用虚函数表和虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引本身以及父类的虚函数的地址,
假如子类重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的函数的地址(子类中可以不是虚函数,但是必须同名);
虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处),它指向对象所在类的虚函数表的地址;
在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。

虚函数表是每个(有虚函数的)类对应一个。虚函数表指针是每个对象一个。
虚函数表里只能存放虚函数,不能存放普通函数。
如果一个函数不是虚函数,那么对它的调用(即该函数的地址)在编译阶段就会确定。调用虚函数的话(它的地址)要运行时才能确定。
虚函数的函数入口是动态绑定的。在运行时,程序根据基类指针指向的实际对象, 来调用该对象对应版本的函数。
(用该对象的虚函数表指针找到其虚函数表,进而调用不同的函数。)(只有是虚函数的情况下才会这样做(用虚函数表指针取查虚函数表)。非虚函数只直接就调用自己的。)

1.为什么使用虚基类函数?(什么情况下要用虚析构函数?)
在存在类继承并且析构函数汇中需要析构某些资源时,析构函数需要是虚函数。否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄漏。

2.一个对象访问普通成员函数和虚函数那个更快?
访问普通成员函数更快,因为普通成员函数的地址在编译阶段就已确定,因此在访问时直接调用对应地址的函数;
而虚函数在调用时,需要首先在虚函数表中寻找虚函数所在地址,因此相比普通成员函数速度更慢一些。

3.内联函数、构造函数、静态成员函数都不可以是虚函数。

4.构造函数中可以调用虚函数吗?
可以,但是没有意义,起不到动态绑定的效果。父类构造函数中调用的仍然是父类版本的构造函数,子类中调用的仍然是子类版本的构造函数。

5. 简述C++中虚继承的作用及底层实现原理?
虚继承用于解决多继承条件下的菱形继承问题,底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避免菱形继承中的二义性问题。

C++虚函数实现多态原理
1. 前言
C++中虚函数的作用主要是实现了多态的机制。
关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

2.虚函数表
对C++了解的人都应该知道虚函数(Virtual Funcition)是通过一张虚函数表(Virtual Table)来实现的。简称V-Table。
在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其能真实反应实际的函数。
这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,
这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

***编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。
***这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

3. 一般继承(无虚函数覆盖)
4. 一般继承(有虚函数覆盖)
5. 多重继承(无虚函数覆盖)
注意:子类并没有覆盖父类的函数
对于子类实例中的虚函数表,每个父类都有自己的虚表;子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明的顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
6. 多重继承(有虚函数覆盖)
对于子类实例中的虚函数表,父类虚函数表中重写函数位置被替换成了子类的函数指针。这样,我们就可以任一静态类型来指向子类,并调用了子类的相应函数。
其他子类成员放在父类虚表后面。
7. 安全性
1).通过父类型的指针访问子类自己的虚函数。
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。
虽然可以看到在子类的父类虚表中有继承的虚函数,但是我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1();//编译出错
任何妄图使用父类指针想调用子类的未覆盖父类的成员函数的行为都会被编译视为非法,所以,这样的程序根本无法通过编译。
但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。
2).访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来
访问这些non-public的虚函数,这是很容易做到的。

**********
为什么构造函数不能是虚函数?
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。
虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。

网络上还有一个很普遍的解释是这样的:
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。
假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

本人对这个观点并不认同,这主要是因为用什么方式实现虚函数是编译器的事情,使用Vtable只是大多数编译器采用的一种手段,
表编译器实现不了虚构造函数,编译器之所以不支持虚构造函数主要原因就是没有必要,所以正好这种实现方式也不支持,巧合而已。

为什么析构函数不能重载?
虚析构函数没哥哥对象只有一个,且没有参数,无法重载。

拷贝构造函数和拷贝赋值运算符可以是虚函数吗?
**构造函数(包括拷贝构造)没有虚函数一说。
子类构造函数可显示调用父类某一构造函数否则就默认会先调用父类无参构造函数,默认父类没有无参构造函数,则子类构造函数必须显示调用父类有参构造函数。
拷贝构造函数也需要显示调用父类拷贝构造函数,否则默认先调用父类无参构造函数。
析构函数可以是虚函数,以使得delete指向子类的父类指针时能够析够子类。
拷贝赋值函数可以为虚。
*************
构造函数,赋值操作符不应该是虚函数
在有继承层次类的时候,我们已经知道析构函数应该是虚函数,但很多人对构造函数和赋值操作符应不应该是虚函数并不清楚。

首先,构造函数不能是虚函数,因为构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。

对于赋值操作符,虽然可以在基类中将成员函数operator=定义成虚函数,但这样做并不会影响派生类中赋值操作符的使用。因为每个类有自己的赋值操作符。
每个类的赋值操作符都有一个和类本身类型相同的形参,该类型必须不同于继承层次中任意其他类的赋值操作符的形参类型。因此子类的赋值操作符和基类的赋值操作符并不是同一个。
但是,在这个子类中仍然有基类的那个操作符,但不是赋值操作符。

将赋值操作符设为虚函数容易让人混淆,因为虚函数必须在基类和派生类中具有相同的形参,基类赋值操作符有一个形参是自身类类型的引用,
如果该操作符为虚函数,则每个类都将得到一个虚函数成员,该成员定义了参数为一个基类对象的operator=。但是,对于派生类而言,这个操作符与赋值操作符是不同的。

因此,将赋值操作符设为虚函数很容易令人混淆,并且没有什么用处。
*********************

override(重写,覆盖)
(1)方法名、参数、返回值相同。
(2)子类方法不能缩小父类方法的访问权限。
(3)子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
(4)存在于父类和子类之间。
(5)方法被定义为final不能被重写。
(6)被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。

overload(重载,过载)
(1)参数类型、个数、顺序至少有一个不相同。
(2)不能重载只有返回值不同的方法名。
(3)针对于一个类而言。
(4)不能通过访问权限、返回类型、抛出的异常进行重载;
(5)方法的异常类型和数目不会对重载造成影响;

override应用中,最熟悉的覆盖就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。 除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。

override是在不同类之间的行为,overload是在同一个类中的行为。

https://www.cnblogs.com/yyxt/p/4243587.html


内存地址分配
1. 内存地址是从高地址到低地址进行分配的。
2. 函数参数列表的存放方式是,先对最右边的形参分配地址,后对最左边的形参分配地址。
3. Little-endian模型的CPU对操作数的存放方式是从低字节到高字节的。
0x4000 0x34
0x4001 0x12
4. Big-endian模型的CPU对操作数的存放方式是从高字节到低字节的。
0x4000 0x12
0x4001 0x34
5.联合体union的存放顺序是所有成员都从低地址开始存放。(默认的方式)
6.一个变量的地址是由它所占内存空间中最低位地址表示的。(默认的方式)
0x4000 0x34
0x4001 0x12
0x1234的地址位0x4000
7.堆栈的分配方式是从高内存地址向低内存地址分配的。(通变量分配方式类似)
int ivar = 0;
int iarray[2] = {11,22};
注意iarray[2]越界使用,比如对其赋值iarray[2]=0;那么则同时对ivar赋值为0,可能产生死循环,因为它们的地址相同,即&ivar等于&iarray[2]。

内存区划分
一.在C中划分这几个存储区
1.栈 由编译器自动分配释放;
2.堆 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收;
3.全局区(静态区),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束释放;
4.另外还有一个专门放常量的地方,程序结束释放。

二.在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
1.栈, 就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.堆, 就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3.自由存储区, 就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区, 全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用一块内存区。
5.常量存储区, 就是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)。
(程序代码区->存放函数体的二进制代码)

堆和栈有什么区别?
主要的区别有以下几点:
1. 管理方式不同
对于栈来讲,是由编译器自动管理,无需我们手工控制;
对于堆来说 ,释放工作由程序员控制,容易生成memory leak。
2. 空间大小不同
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。
但是对于栈来讲,一般都是有一定的空间大小的,栈开辟较大的值,可能增加内存的开销和启动时间。
3. 能够产生碎片不同
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
对于栈来讲,则不会存在这个问题,因为栈先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在它弹出之前,在它上面的后进的栈内容已经被弹出。
4. 生长方向不同
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增加。(定义函数参数)
5. 分配方式不同
堆都是动态分配的,没有静态分配的堆。
栈有两种方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,不需要我们手工实现。
6. 分配效率不同
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,
如果没有足够大小的空间(可能是由于内存碎片太多),就有肯呢个调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。
所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈区完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。


在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南:
(1) 如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;
两个对象包含都指向第三个对象的指针;
STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_Ptr,可使用Boost库提供的shared_ptr。

(2) 如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr,如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。
可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。


**********************************************
https://www.cnblogs.com/wxquare/p/4759020.html
1.智能指针的作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
  Animal a = new Animal();

  Animal b = a;

你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,

Animal a;

Animal b = a;

这里却是就是生成了两个对象。

    关于值语言参考这篇文章http://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

2.智能指针的使用
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr

2.1 shared_ptr的使用
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。
shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
get函数获取原始指针
注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。

猜你喜欢

转载自www.cnblogs.com/gzjgl/p/11114431.html