面经-C语言基础(一)

目录

虚函数和纯虚函数

堆变量和栈变量

C/C++堆和栈的区别

逻辑地址和物理地址和虚拟内存

动态链接库和静态链接库的区别

sizeof和strlen的区别:

如何判断new和malloc是否成功:

内存申请失败该如何处理?

C++是不是类型安全的?

C和C++的区别,C++11的新特性

区别:

新特性:

 

 

c++中内存的五大区

内存的分配方式有几种?

智能指针

野指针

static关键字

C++11中的四种类型转换

深入理解数据库索引采用B树和B+树的原因

顶层指针和底层指针

const 与 #define  的比较  ,const 有什么优点?

 有了 malloc/free  为什么还要 new/delete ?

你要的虚类都在这

如果一个类有两个父类,都有虚函数,则此类中有几个虚函数表指针

虚函数

虚继承和多继承

抽象类

public继承 private继承 protected继承

C++源文件从文本到可执行文件的过程

智能指针的实现

volatile关键字和extern关键字分别在哪个阶段起作用?

对反码,补码的理解--减法的实现

c++之类内定义引用成员

右值引用

为什么栈内数据的处理速度比堆中的快

array和vector,数组三者区别和联系

volatile的作用


虚函数和纯虚函数

  • 定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现

  • 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

virtual void funtion1()=0;

有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。

 

堆变量和栈变量

 

new/malloc产生的变量都在堆中,动态分配的变量在堆中分配,局部变量/临时变量/函数参数值在栈里面分配。

 

程序为栈变量分配动态内存,在程序结束为栈变量清除内存,但是堆变量不会被清除。

C/C++堆和栈的区别

堆和栈的区别

1.       管理方式不同

栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。

2.       空间大小不同

栈的空间有限;堆内存可以达到4G,。

3.       能否产生碎片不同

栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete

会造成内存的不连续,从而造成大量的碎片。

4.       生长方向不同

堆的生长方式是向上的,栈是向下的。

5.       分配方式不同

堆是动态分配的。栈可以是静态分配和动态分配两种,但是栈的动态分配由编译器释放。

6.       分配效率不同

栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

l  堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家  尽量用栈,而不是用堆。

l  栈和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

l  无论是堆还是栈,都要防止越界现象的发生。

 

 

逻辑地址和物理地址和虚拟内存

 

        逻辑地址(baiLogical Address) 是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。

        物理地址(Physical Address) 是指用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

        虚拟内存:指的是以较小的页粒度为单位,在有限内存装入更多更大的程序。即,程序不需要全部装入即可运行,运行时根据需要动态调入内存,若内存不够还需要置换一部分空间。

 

动态链接库和静态链接库的区别

详细解释

        静态连接就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;

        动态链接就是把用的函数所在文件模块(DLL)和调用函数所在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。 

        静态链接库:由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件

       

静态链接的优缺点

        静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快

动态链接的优缺点

        动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失

 

sizeof和strlen的区别:

 

sizeof是运算符,其值在编译时即计算好了,获得保证能容纳实现所建立的最大对象的字节大小。具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:数组——编译时分配的数组空间大小;指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4)

strlen是函数,返回字符串的长度,到/0为止。(strlen不包含0/sizeof包含)

 

如何判断new和malloc是否成功:

 

new失败会抛出异常,当然,标准的C++也提供了一个方法来抑制new抛出异常,而返回空指针:

 int* p = new (std::nothrow) int;

malloc失败则返回空指针。成功就返回分配成功的指针

内存申请失败该如何处理?

(1 )判断指针是否为 NULL(C++11以上建议用nullptr) ,如果是则马上用return  语句终止本函数。

(2 )判断指针是否为 NULL,如果 ,如果是则马上用 exit(1) 终止整个程序的运行

(3 )为 new  和malloc  设置异常处理函数。例如 Visual C++可以用 可以用_set_new_hander  函数为 new 设置用户自己定义的异常处 设置用户自己定义的异常处理函数,也可以让 malloc  享用与 new 相同的异常处理函数。

C++是不是类型安全的?

不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。

 

C和C++的区别,C++11的新特性

区别:

1、C是面向过程的语言,而C++是面向对象的语言。

那么什么是面向对象?
        面向对象:面向对象是一种对现实世界的理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。

2、C和C++动态管理内存的方法不一样,C是使用malloc、free函数,而C++不仅有malloc/free,还有new/delete关键字。那malloc/free和new/delete差别?

3、C++的类是C中没有的,C中的struct可以在C++中等同类来使用,struct和类的差别是,struct的成员默认访问修饰符是public,而类默认是private。

        所以,C++具有封装继承多态三大特性

4、C++支持重载,而C不支持重载。C++支持重载在于C++名字的修饰符与C不同,例如在C++中函数 int f(int) 经过名字修饰之后变为_f_int,而C是_f,所以C++才会支持不同的参数调用不同的函数。

5、C++中有引用,而C没有。那指针和引用有什么差别?

6、C++全部变量的默认连接属性是外连接,而C是内连接。

7、C中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以。

8、C++有很多特有的输入输出流。

 

新特性:

 

auto关键字

nullptr关键字

智能指针:新增了share_ptr和weak_ptr用于解决内存管理的问题。

初始化列表:使用初始化列表来对类进行初始化

右值引用:

lambda表达式

引入了线程库,使得在C++在并行编程时可以不需要依赖第三方库,而且在原子操作中引入了原子类的概念。atomic原子操作用于用于多线程资源互斥操作。

新增STL容器array和tuple

 

C++的几大特性

https://blog.csdn.net/weixin_42678507/article/details/97111466

 

 

 

 

  1. 内存泄漏:由于疏忽或错误,造成了程序未释放掉不再使用的内存,失去了对该段内存的控制,造成了内存的浪费。

1).堆内存的泄漏,也就是程序在运行过程中动态申请的内存空间不再使用后没有及时释放,导致那块内存不能被再次使用。(malloc, new, realloc)

2).系统资源浪费, 指程序使用系统分配的资源比如Bitmap, handle, socket 等没有使用相应的函数释放掉。

  1. 内存溢出:(Out Of Memory,简称OOM)是指程序在申请内存时,没有足够的内存空间供其使用,就是说,你要的内存空间超过了系统实际分配的空间。出现out of memory;比如申请了一个 int,但给它存了 long 才能存下的数,那就是内存溢出。

内存泄露最终导致内存溢出。

造成内存溢出的原因通常有两种:

第一种是由于长期保持某些资源的引用,垃圾回收器无法回收它,从而使该资源不能够及时释放,也称为内存泄露;

另外一种是当需要保存多个耗用内存过大或当加载单个超大的对象时,该对象的大小超过了当前剩余的可用内存空间。a[10]初始化时给了11个元素。

 

 

c++中内存的五大区

 

在C++中,内存分成5个区,他们分别是堆、栈、(自由存储区)代码区、全局/静态存储区和常量存储区。

栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除变量的存储区。里面的变量通常是局部变量、函数参数等。

堆,就是那些由malloc/new等分配的内存块,用free/delete来结束自己的生命的。

自由存储区,就是那些由 new 分配的内存块,一般一个 new 就要对应一个 delete 。如果程序员没有释放掉,那么在程序结束后,操作系统 可能 会自动回收。

代码区  存放函数体的二进制代码。

存放程序执行的CPU指令,我们的代码最后在编译的时候都会转化为相应的CPU指令,告诉计算机如何执行程序

全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改 。

堆区和自由存储区的区别与联系
(1)malloc申请的内存在堆上,使用free释放。new申请的内存在自由存储区,用delete释放
(2)堆(heap)是c语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,
当程序运行时调用malloc()时就会从中分配,调用free可把内存交换。而自由存储区是C++中通过new和delete
动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器
默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现,
这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。
记住:
(1)堆是c语言和操作系统的术语,是操作系统维护的一块内存。自由存储是C++中通过new和delete动态
分配和释放对象的抽象概念。
(2)new所申请的内存区域在C++中称为自由存储区,编译器用malloc和free实现new和delete操作符时,
new申请的内存可以说是在堆上。
(3)堆和自由内存区有相同之处,但并不等价。

内存的分配方式有几种?

  • 从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。
  • 在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 从堆上分配:亦称动态内存分配。程序在运行的时候用malloc或 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete释放 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

 

智能指针

 

C++中有四个智能指针:auto_ptr,shared_ptr,unique_ptr,weak_ptr。其中后三个是C++11支持,并且第一个已经被11弃用了。

为什么使用智能指针:

智能指针的作用就是管理一个指针,因为存在以下情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上避免这个问题,因为智能指针是一个类,当超出了类的作用域时,会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

1. auto_ptr(C++98方案,C++11被抛弃)

采用所有权模式。

如:auto_ptr< string > p1(new string ("Hello"));
auto_ptr< string> p2;
p2=p1;

此时不会报错,p2剥夺了P1的所有权,并且当访问p1的时候会报错。所以auto_ptr的缺点:存在潜在的内存崩溃问题。

2、 unique_ptr (替代auto_ptr)

也是采用所有权模式(单一所有权,所以无法被拷贝)。但是它实现独站式拥有或严格拥有的概念,保证同一时间内只有一个智能指针可以指向该对象。

如:unique_ptr < string > p1(new string ("Hello"));
unique_ptr < string> p2;
p2=p1;//此时会报错

编译器认为p2=p1非法,也就避免了p1指向无效数据的问题。比auto_ptr更安全。

另外,当unique_ptr是临时右值时,编译器允许赋值。比如:

#1 unique_ptr < string > p1(new string ("Hello"));
unique_ptr < string> p2;
p2=p1;//此时会报错
#2 unique_ptr < string> p3;
p3=unique_ptr <string> (new string ("Hello"));//此时是允许的

在#2 中创建的临时对象在所有权让给p3后就会被销毁。

如果非要执行类似#1 的操作那么需要借助库函数std::move。

unique ptr< string > p1,p2;
p1=demo("hello");
p2=move(p1);
p1=demo("you");//此时所有权转让给p2后可以继续赋值使用。

3、 shared_ptr

shared_ptr实现共享是拥有的概念。多个指针可以指向相同的对象,该对象及其相关资源会在“最后一个引用被销毁”时候释放。它采用计数机制来表明资源被几个指针共享。 可以通过use_count()来查看资源的所有者的个数。

除了使用new来构造shared_ptr,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

4、 weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr,weak_ptr 只是提供了对管理对象的一个 访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题, 如果说两个 shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0,资源永远不 会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调用 lock 函数来获得 shared_ptr。

考官问道,大多是考察shared_ptr的,可以这样答:

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11 中最常用的 智能指针类型为 shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。 该引用计数的内存在堆上分配。当新增一个时引用计数加 1,当过期时引用计数减一。只有引用 计数为 0 时,智能指针才会自动释放引用的内存资源。对 shared_ptr 进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过 make_shared 函数或者通 过构造函数传入普通指针。并可以通过 get 函数获得普通指针。

野指针

 

简单回答可以是:"野指针指未初始化或未清零的指针,它指向的内存地址不是程序员想要的

 

野指针的定义:野指针指向一个已删除的对象或未申请访问受限内存区域的指针。

与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。需对指针进行初始化。

为什么会出现野指针:

1.指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存(int *p = &a, 这就是指向合法的内存了。)。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。

2.指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

看到2我明白了,上面说的:对C++来说,内存泄漏就是new出来的对象没有delete,俗称野指针。我觉得不太准确,内存泄漏说的是为对象申请的内存未被释放,野指针说的是指向已被删除的或者未申请受限访问内存的指针。怎么能说内存泄漏俗称野指针呢?这就像说保险柜没清理(内存泄漏)和指向特殊保险柜的指示牌(野指针)是一回事一样。

3.指针操作超越变量作用域

就是在你超过作用域后,内存已经释放,这时指向那个被释放的内存的指针就是野指针,你却还调用,这样不行。

static关键字

 

1、全局静态变量

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

存放在静态存储区,在整个程序运行期间一直存在。

初始化:未经初始化的全局静态变量会自动初数化为0。

作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义开始,到文件结尾。(只需在一个源文件中定义,就可以作用于所有的源文件。)

2、局部静态变量

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

内存中的位置:静态存储区。

初始化:未经初始化的局部静态变量会自动初数化为0。

作用域:作用域是局部作用域。当定义它的函数或者语句块结束的时候,作用域结束。但当局部变量离开作用域后,并没有被销毁,而是驻留在内存中,直到该函数再次被调用,并且值不变。

3、静态函数

在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都

是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。并且该函数只能在本cpp内使用,不会同其他cpp中的同名函数冲突。

 warning: 不要再头文件中声明 static 的全局函数(头文件声明的可以被其他cpp复用,如果是static就没必要声明在头文件,在该cpp文件声明就行)。

不要在 cpp 内声明非 static 的全局函数(在头文件声明),如果你要在多个 cpp 中复用该函数,就把它的声明提到头文件里去,否则 cpp 内部声明需加 上static 修饰。

4、类的静态成员

在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态成员还不会破坏隐藏的原则,保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。

5、类的静态函数

静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。

在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);  (需要加上类名)

C++11中的四种类型转换

static_cast

  1. 基础数据类型转换(基本类型)
  2. 同一继承体系中类型的转换(父子类型)
  3. 任意类型与空指针(void *)之间的转换(指针类型)

dynamic_cast

执行派生类指针或引用与基类指针或引用之间的转换。

  1. 其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行运行时类型检查;
  2. 基类中要有虚函数,因为运行时类型检查的类型信息在虚函数表中,有虚函数才会有虚函数表;
  3. 可以实现向上转型和向下转型,前提是必须使用public或protected继承;

const_cast

  1. 只能对指针或者引用去除或者添加const属性
  2. 对于变量直接类型不能使用const_cast
  3. 不能用于不同类型之间的转换,只能改变同种类型的const属性

reinterpret_cast

从字面意思理解是一个“重新解释的类型转换”。也就是说对任意两个类型之间的变量我们都可以个使用reinterpret_cast在他们之间相互转换,无视类型信息。不推荐使用。

深入理解数据库索引采用B树和B+树的原因

 

0

顶层指针和底层指针

(1)const char *p

(2)char const *p

(3)char * const p

说明上面三种描述的区别;

(1)p 是一个指向const char 的指针,p是可以改变指向的,但是p 指向的值是不能改变的;

(2)p指向的恰好 是一个指向const 的char 的普通指针;

(3)p是一个指针,这个指针是指向char 的const 指针。 (1) 和(2)的定义是一样的。

 

const 与 #define  的比较  ,const 有什么优点?

const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。 

有些集成化的调试工具可以对  const 常量进行调试,但是不能对宏常量进行调试。

 有了 malloc/free  为什么还要 new/delete ?

malloc与 free 是 C++/C 语言的标准库函数,new/delete 是 C++的运算符。

它们都可用于申请动态内存和释放内存。 对于非内部数据类型的对象而言,只用 malloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 数的任务强加于 malloc/free 。 因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new ,以及一个能完成清理与释放内存工作的运算符delete 。注意 new/delete 不是库函数。

你要的虚类都在这

如果一个类有两个父类,都有虚函数,则此类中有几个虚函数表指针

(当基类有虚函数时):

1. 每个类都有虚指针和虚表;

2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

来自 <https://www.cnblogs.com/jerry19880126/p/3616999.html>

虚函数

  • 虚函数的定义:    

  虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public(可以定义为privateor proteceted, 但是对于多态来说,没有意义),在基类的类定义中定义虚函数的一般形式:

virtual 函数返回值类型虚函数名(形参表)

{ 函数体 }

  • 定义虚函数的限制

(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。

  实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。

(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

(3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名同返回值同参数个数同参数类型函数。

  • 为什么虚函数必须是类的成员函数

  虚函数诞生的目的就是为了实现多态,在类外定义虚函数毫无实际用处。

  • 为什么类的静态成员函数不能为虚函数

   如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(在内存中只有一份拷贝;通过类名或对象引用访问静态成员)本身就是相矛盾的。

  • 为什么构造函数不能为虚函数

  因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保V-Table的构建成功呢?

注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。

当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。

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

 

虚继承和多继承

在继承时,使用逗号分隔,即可实现继承多个对象。继承的本质是将基类对象作为成员创建。如下图展示了多继承时基类的创建关系

class cat
{
public:
        cat(){cout<<"cat对象被创建"<<endl;}
};

class people
{
public:
        people(){cout<<"people对象被创建"<<endl;}
};

class CatPeople : public cat, people
{
public:
        CatPeople(){cout<<"CatPeople对象被创建"<<endl;}
};

int main()
{
        CatPeople cp;

return 0;
}

可是如果有如下的继承结构,即多继承的基类AB继承自同一基类BASE

 

那么BASE会被创建两次。详情请:C++虚继承简单讲解。如下的代码演示了这一特性:

(其中BASE=animal,A=cat,B=people,C=CatPeople)

class animal

{

public:

int age;

animal(){cout<<"animal对象被创建"<<endl;}

};

 

class cat : public animal

{

public:

cat(){cout<<"cat对象被创建"<<endl;}

};

 

class people : public animal

{

public:

people(){cout<<"people对象被创建"<<endl;}

};

 

class CatPeople : public cat, people

{

public:

CatPeople(){cout<<"CatPeople对象被创建"<<endl;}

};

 

int main()

{

CatPeople cp;

 

return 0;

}

  1. 这造成了额外的开销,基类在底层子类有多份数据。
  2. 继承类和基类同名函数产生了二义性。也就是 在某些情况下,不知道这个函数是基类的还是继承类的函数。

class CatPeople : public cat, people

{

public:

CatPeople(){cout<<"CatPeople对象被创建"<<endl;}

age = 123;  //这里有二义性

};

 

如果用virtual来修饰继承,即虚继承,那么将会只创建一次基类变量,赋值也不存在歧义问题:

class animal

{

public:

int age;

animal(){cout<<"animal对象被创建"<<endl;}

};

 

class cat : virtual public animal

{

public:

cat(){cout<<"cat对象被创建"<<endl;}

};

 

class people : virtual public animal

{

public:

people(){cout<<"people对象被创建"<<endl;}

};

 

class CatPeople : public cat, people

{

public:

CatPeople(){cout<<"CatPeople对象被创建"<<endl; age=123;}

};

 

int main()

{

CatPeople cp;

 

return 0;

}

 

抽象类

只包含虚函数。虚函数未被实现,需要用 xxx = 0 来修饰

virtual void speak() = 0;        

 

继承一个虚基类需要实现其所有的虚函数,这起到了规范化的作用,因为有些对象必须实现特定的函数,否则无法正确运行。

如果未实现虚基类所有虚函数,那么会报错。

 

下面的代码演示了继承一个抽象类,并且实现其抽象方法的过程

class animal

 

{

public:

        virtual void speak() = 0;

};

class cat : public animal
{
public:
        void speak(){cout<<"cat speak!"<<endl;}                
};

int main()
{
        animal* a = new cat();
        a->speak();

return 0;
}

 

public继承 private继承 protected继承

 

(1) 公有继承(public)

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

(2) 私有继承(private)  ------------------默认的继承方式(如果缺省,默认为private继承)

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

子类也不能转换成相应的基类,如果转换,会报错:“不允许对不可访问的基类进行转换”。

(3) 保护继承(protected)

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。

1.1 对于公有继承方式

 

(1) 基类成员对其对象的可见性:

公有成员可见,其他不可见。这里保护成员同于私有成员。

 

(2) 基类成员对派生类的可见性:

公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。

 

(3) 基类成员对派生类对象的可见性:

公有成员可见,其他成员不可见。

所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。

 

1.2 对于私有继承方式

 

(1) 基类成员对其对象的可见性:

公有成员可见,其他成员不可见。

(2) 基类成员对派生类的可见性:

公有成员和保护成员是可见的,而私有成员是不可见的。

(3) 基类成员对派生类对象的可见性:

所有成员都是不可见的。

所以,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。

 

1.3 对于保护继承方式

这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,对基类成员有不同的可见性。

上述所说的可见性也就是可访问性。关于可访问性还有另的一种说法。这种规则中,称派生类的对象对基类访问为水平访问,称派生类的派生类对基类的访问为垂直访问。

一般规则

公有继承时,水平访问和垂直访问对基类中的公有成员不受限制;

私有继承时,水平访问和垂直访问对基类中的公有成员也不能访问;

保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承。

对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其他的函数访问。

基类与派生类的关系:任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。

 

 

C++源文件从文本到可执行文件的过程

  1. 预处理阶段:对源文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。生成 .i 或 .ii 文件
  2. 编译阶段:将预编译文件进行词法分析、语法分析、语义分析及优化之后,生成汇编代码文件。
  3. 汇编阶段:将汇编文件转化成机器码(二进制文件),生成可重定位目标代码,生成.o文件
  4. 链接阶段:将多个目标文件及所需的库连接成最终的可执行目标文件。.exe

   动态链接和静态链接

          

静态链接:后缀是.a,主要在编译的时候将库文件里面代码搬迁到可执行的文件中;

动态链接:后缀是.so,主要在执行的时候需要转换到库文件代码执行;

两种链接的优缺点:

(1)静态的链接产生的可执行的文件体积比较的大;而动态链接的可执行文件的体积比较小;

(2)动态的链接的编译的效率比较的高;

(3)静态链接的可执行的文件执行的效率高

(4)静态链接的可执行的文件的“布局”比较好一点;

 

c++中赋值运算符重载为什么要用引用做返回值?

答案1:

class string{

public:

string(const char *str=NULL);

string(const string& str);     //copy构造函数的参数为什么是引用呢?  我相信大家都懂的!

string& operator=(const string & str); //赋值函数为什么返回值是引用呢?

~string();

};

    如果 返回值, return *this后马上就调用拷贝构造函数。

但是万一由于没有定义拷贝构造函数  ,就会调用默认的拷贝构造函数。

我们知道调用默认的拷贝构造函数时当在类中有指针时就会出错(浅拷贝,指向同一块内存空间)。

所以如果你不用引用做返回时      就必须定义自定义的拷贝构造函数。

当然咯  有指针成员时 必须要注意    自定义  拷贝构造了

 

答案2:

  • 允许进行连续赋值
  •       防止返回对象(返回对象也可以进行连续赋值(常规的情况,如a = b = c,而不是(a = b) = c))的时候调用拷贝构造函数和析构函数导致不必要的开销,降低赋值运算符的效率

 

 

而拷贝构造函数不能进行值传递因为从参数的传递本身就是复制。如果不使用引用类型,复制构造函数传递参数时又会调用拷贝构造函数,造成递归调用了。故用引用类型来传递。

智能指针的实现

template <typename T>

class shared_ptr{

preivate:

T *ptr;

int *use_count;

 

public:

 

shared_ptr(T *P){//smart_ptr<int> p(new int(2))//构造函数

ptr = p;

try{

use_count = new int(1);

}

catch(…){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

shared_ptr(const shared_ptr<T> &ori){

//shared_ptr <int> p(q);,q是一个智能指针 //拷贝构造函数, 传引用不能传值,因为从实参到形参又会调用拷贝构造函数。。。循环

ptr = ori.ptr;

use_count = ori.use_count;

++(*use_count);//关键 要自加1

}

 

smart_ptr<T> &operator = (const smart_ptr<T> &rhs){//q = p //重载 =

++(*rhs.use_count);赋值给了p,那么rhs就多了一个引用。

--(*use_count); // 摆脱原有的指向

if((*use_count) == 0 ){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

ptr = rhs;

*use_count = *rhs.use_count;

return *this;

}

 

~shared_ptr(){//析构

if(ptr){

--(*use_count);

if((*use_count) == 0 ){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

}

}

 

//重载运算符

T operator *(){//把智能指针当做普通指针使用的解引用

return *ptr;

}

T *operator ->(){//取成员操作,前面的*表明这是指针 指针才能->取

return ptr;

}

T *operator+(int i){//定义指针加一个常数

T* temp = ptr + i;

return temp;

}

int operator - (smart_ptr<T> & t1, smart_ptr<T> &t2){//定义两个指针相减

return t1.ptr - t2.ptr;

}

 

int getcount(){

return *use_count;

}

};

//重载 -

template <typename T>

int shared_ptr<T>::opreator - (smart_ptr<T> &t1, smart_ptr<T> &t2){

return t1.ptr - t2.ptr;

}

//构造函数

template <typename T>

shared_ptr<T>::shared_ptr(T *P){

ptr = p;

try{

use_count = new int(1);

}

catch(...){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

}

//拷贝构造

template <typename T>

shared_ptr<T>::shared_ptr(const shared_ptr<T> &ori){

use_count = ori.use_count;

this->ptr = ori.ptr;

++(*use_count);//++是因为多了自己。

}

//重载 =

template <typename T>

shared_ptr<T>::shared_ptr<T> &operator = (const shared_ptr<T>& rhs){

++(*rhs.use_count);//拷贝构造就是双方都有++;

--(*use_count);

if(*use_count == 0){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

this->ptr = rhs.ptr;

*use_count = *(rhs.use_count);

return *this;

}

//析构

template <typename T>

shared_ptr<T>::~shared_ptr(){

if(ptr && --*use_count == 0){

delete ptr;

ptr = nullptr;

delete use_count;

use_count = nullptr;

}

}

//重载 + * ->

template <typename T>

T* shared_ptr<T>::operator +(int i){

T* temp = ptr + i;

return temp;

}

 

template <typename T>

T* shared_ptr<T>::operator*(){

return *ptr;

}

 

template <typename T>

T* shared_ptr<T>::operator->(){

return ptr;

}

volatile关键字和extern关键字分别在哪个阶段起作用?

volatile应该是在编译阶段,extern在链接阶段。

volatile关键字的作用是防止变量被编译器优化,而优化是处于编译阶段,所以volatile关键字是在编译阶段起作用。

 

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读

 

在析构函数中调用delete this,会不会有问题

在类的成员函数2113中能不能调用delete this?答案是肯定的,能调用,而5261且很多老一点的库4102都有这种代码。1653假设这个成员函数名字叫release,而delete this就在这个release方法中被调用,那么这个对象在调用release方法后,还能进行其他操作,如调用该对象的其他方法么?答案仍然是肯定 的,调用release之后还能调用其他的方法,但是有个前提:被调用的方法不涉及这个对象的数据成员和虚函数。说到这里,相信大家都能明白为什么会这样 了。

根本原因在于delete操作符的功能和类对象的内存模型。当一个类对象声明时,系统会为其分配内存空间。在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当 调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

为什么是不可预期的问题?delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题才对啊?这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

 

大致明白在成员函数中调用delete this会发生什么之后,再来看看另一个问题,如果在类的析构函数中调用delete this,会发生什么?实验告诉我们,会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存” (来自effective c++)。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

来自 <https://zhidao.baidu.com/question/1240307065148180379.html?qbl=relate_question_0&word=%BF%C9%D2%D4%C9%BE%B3%FDthis%C2%F0>

对反码,补码的理解--减法的实现

 

https://blog.csdn.net/lc_miao/article/details/84929497

c++之类内定义引用成员

c++类内可以定义引用成员变量,但要遵循以下三个规则:

1、不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量。否则会造成引用未初始化错误。

2、构造函数的形参也必须是引用类型

3、不能在构造函数里初始化,必须在初始化列表中进行初始化。

右值引用

 

左值和右值的概念:

左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。

右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

比如 int a = 5; int b = a;

a,b都是用户定义的变量,可以随便赋值。5是字面值常量。 你可以对a,b求地址 比如 int* p = &a;

但是你不能对5求地址。 比如int *p = &5;

另外 C++ primer里面提到 前缀++返回左值,后缀返回右值。也可以通过左值与右值的概念来理解++操作法。比如 ++a = 10; 可以编译通过 (a++) = 10; 编译错误。同样 你也不能对 (a++)的值取地址。

其他的右值概念比如 函数的返回值等 也不能取地址。

右值引用是C++11中的概念,目的就是解决上面所说的函数返回值等问题。比如你返回了一个vector,可能包含很多元素,一次return就会产生一个临时变量,需要构造与析构。而实际上你可能只是这样写 vector ret = getVector(); 本身的ret还需要从临时的返回值里面再构造一次。产生浪费。C++ 11里面配合MOVE语义可以解决这种情况。

 

右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。它的主要目的有两个方面:

1. 转移语义,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

2. 能够更简洁明确地定义泛型函数。

为什么栈内数据的处理速度比堆中的快

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。 

array和vector,数组三者区别和联系

 

共同点

(1.)都和数组相似,都可以使用标准数组的表示方法来访问每个元素(array和vector都对下标运算符[ ]进行了重载)

(2.)三者的存储都是连续的,可以进行随机访问

 

不同点

(0.)数组是不安全的,array和vector是比较安全的(有效的避免越界等问题)

(1.)array对象和数组存储在相同的内存区域(栈)中,vector对象存储在自由存储区(堆)

(2.)array可以将一个对象赋值给另一个array对象,但是数组不行

(3.)vector属于变长的容器,即可以根据数据的插入和删除重新构造容器容量;但是array和数组属于定长容器

(4.)vector和array提供了更好的数据访问机制,即可以使用front()和back()以及at()(at()可以避免a[-1]访问越界的问题)访问方式,使得访问更加安全。而数组只能通过下标访问,在写程序中很容易出现越界的错误

(5.)vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器

(6.)vector和array提供了size()和Empty(),而数组只能通过sizeof()/strlen()以及遍历计数来获取大小和是否为空

(7.)vector和array提供了两个容器对象的内容交换,即swap()的机制,而数组对于交换只能通过遍历的方式逐个交换元素

(8.)array提供了初始化所有成员的方法fill()

(9.)由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否有效问题

(10.)vector和array在声明变量后,在声明周期完成后,会自动地释放其所占用的内存。对于数组如果用new[ ]/malloc申请的空间,必须用对应的delete[ ]和free来释放内存

volatile的作用

    定义为volatile的变量是说这变量可能会被意想不到地改变,即在你程序运行过程中一直会变,你希望这个值被正确的处理,每次从内存中去读这个值,而不是因编译器优化从缓存的地方读取,比如读取缓存在寄存器中的数值,从而保证volatile变量被正确的读取。

 

在单任务的环境中,一个函数体内部,如果在两次读取变量的值之间的语句没有对变量的值进行修改,那么编译器就会设法对可执行代码进行优化。由于访问寄存器的速度要快过RAM(从RAM中读取变量的值到寄存器),以后只要变量的值没有改变,就一直从寄存器中读取变量的值,而不对RAM进行访问。

而在多任务环境中,虽然在一个函数体内部,在两次读取变量之间没有对变量的值进行修改,但是该变量仍然有可能被其他的程序(如中断程序、另外的线程等)所修改。如果这时还是从寄存器而不是从RAM中读取,就会出现被修改了的变量值不能得到及时反应的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值