数据结构,第二张图中画波浪线的地方,把q->next赋值给q意味着什么呢?

Linux最常用的基础命令个人总结

32bit和64bit系统的区别、系统运行机制

32bit=内存的最大寻址空间是2**32,也就是说最大只能使用4GB的内存
64bit=内存的最大寻址空间是2**64,差不多支持16TB的内存

苹果系统绑定了硬件,微软系统只卖软件不绑定硬件

计算机;主要目的是计算:
人们最早使用的计算工具可能是手指,英文单词“digit”既有“数字”的意思,又有“手指“的意思。28个手指关节,10根手指
计算机的电源。类似于人类的心脏:电源给计算机供电,心脏给人体供血

打开软件是操作硬盘的,打开程序的快慢速度取决于硬盘的读写速度

电脑上应用程序的运行过程:打开一个软件,cpu先从磁盘中加载文件,也就是计算机将把硬盘里的数据读取到内存,程序就开始运行,在程序运行的过程中,产生的临时数据也会写入内存,程序的打开速度取决于硬盘的质量和硬盘的读写速度
软件打开后,的使用过程中所有的数据都是CPU直接从内存中读写和存放的所以软件使用过程中的流畅度,取决于cpu的性能和内存的大小(CPU值越大性能越强)

操作系统(Operating System, OS):是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境的程序集合。
硬盘:存储资料和数据的设备,有容量大,断电数据不丢失的特点。
内存:负责硬盘等硬件上的数据与CPU之间数据交换处理; 缓存系统中的临时数据。断电后数据丢失。内存中的数据是通过电流来表示的,所以一断电,内存中的数据就没有了

ASSIC  每一个字符统一都需要8个bit来存储

计算机的CPU是由集成电路这种电子部件构成的, 电路只可以表示两种状态——通电、断电,因为这个特性,计算机只能识别二进制数(0或1)

计算机的CPU只能认识 0 和 1 所以电脑只懂 0和 1组成的机器语言。

编码:将计算机能理解的语言转换成人类能懂的语言

将编程语言翻译成机器语言,即0和1, ——这个过程叫编译
编译就是:通过编译器执行一个编译的过程,把程序编译成机器语言。
解释就是:在程序运行的时候,通过解释器逐行解释代码,然后运行。

计算机中如果没有特殊的要求数字是从0开始算起的
平常生活中数字是从1开始算起的

二进制:逢二进一   ,二进制的表示数字: 0, 1

八进制: 逢八进一,八进制的表示数字:

十六进制: 逢十六进一,十六进制的表示数字: ABCDEF


平常我们上网所说的下载速度和宽带的速度的计算方式:

4M的网的话理论下载速度应该是

运营商的单位是bit,而我们下载的单位是K 即字节byte, 宽带运营商按照二进制来计算,所以100M的宽带要除8才是我们平常说的下载的速度。

定义好一套与计算机交互的语法规则,这套规则 就可称为一门编程语言
我们听不懂日语是因为不懂日语的语法规则
学编程 == 学语法规则
一堆指令的组合 == 》 软件

有很多套与计算机交互的语法规则,

C语言是编译型的语言,不太支持跨平台
C = 各个操作系统的开发语言 1973
C++ = C++是C语言的加强版 ,1983年,贝尔实验室的Bjarne Stroustrup在C语言基础上推出了C++[1] 。 C++进一步扩充和完善了C语言,是一种面向对象的程序设计语言。

C# = c sharpe =C#是微软公司发布的一种面向对象的、运行于.NET Framework之上的高级程序设计语言。并定于在微软职业开发者论坛(PDC)上登台亮相。C#是微软公司研究员Anders Hejlsberg的最新成果。C#看起来与Java有着惊人的相似;它包括了诸如单一继承、接口、与Java几乎同样的语法和编译成中间代码再运行的过程。但是C#与Java有着明显的不同,它借鉴了Delphi的一个特点,与COM(组件对象模型)是直接集成的,而且它是微软公司 .NET windows网络框架的主角。

Ruby 语言替代品。Ruby的作者于1993年2月24日开始编写Ruby,直至1995年12月才正式公开发布于fj(新闻组)。因为Perl发音与6月诞生石pearl(珍珠)相同,因此Ruby以7月诞生石ruby(红宝石)命名。

perl = Unix平台上开发出来的语言,做文字处理非常强大, 可以写出没人能看懂的代码

shell = 脚本语言, 简单易学,基于unix,linux, 做一些简单的系统管理任务, 运维人员必学

scalar = Scala是一门多范式的编程语言,一种类似java的编程语言[1] ,大数据开发

erlang = 是一种通用的面向并发的编程语言,它由瑞典电信设备制造商爱立信,函数式编程

go ===Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。

javascript = 是当下使用最为广泛的语言,主要写前端的语言,
vb = 微软的脚本语言,bat脚本

虚拟机中的三种网络模式:

Bridged(桥接模式) : 桥接模式就是将我们虚拟机中的网卡的网络地址 放在我们真实的物理机的网卡上。 这样的话,我们的虚拟机就好像跟我们的宿主机所在的局域网中的一台机器一样。
NAT(网络地址转换模式) : 在宿主机上制作一个虚拟网卡,通过这个网卡,给虚拟机分配IP。虚拟机通过宿主机去上网。
Host-Only(模式): 该模式下虚拟机只能访问到物理主机。无法访问外网。

林尼克斯:linux对硬件的基础要求:

软件配置:64位操作系统,win7旗舰版或者Win10专业版 :不要用企业版:很耗资源
硬件配置:16G内存,i7,固态硬盘200G
类型:游戏本(性能最强的)
处理器:GHz 值越大越好,越大运算速度越快

 Linux的创始人:林纳斯,脱袜子
向百度发送4个测试网络是否可用的的报文信息
netstat 查看当前的网络状态
nslookup 某个IP对应的网站的域名查询工具

DNS:Domain Name System 完成IP地址与主机域名之间的相互转换,这个转换过程称为域名解析
根据域名查到对应的IP叫正向域名解析
由IP地址查到对应的域名叫反向域名解析
dig 查询域名对应的IP地址
host 域名查询工具
host 查询域名对应的IP地址
arping 发送arp请求给邻居主机,功能与ping命令类似,但是arping是基于ARP广播机制的所以arping只能测试同一网段的主机的网络的连通行,ping命令是基于ICMP协议的,ICMP协议是可以路由的,所以ping命令可以测试任意网段的主机的连通行

学习中想到的一些乱七八糟:

懒有两种:思想上的懒,身体上的懒
有脚踏实地的敢干,有仰望星空的敢想,不断的给自己更多的可能性

不给别人添麻烦:如果可以的话,不要在公共场合吃有味道的东西,因为有味道你会影响到你旁边的人,你觉得可以的味道,别人不想闻到的呀
人的素质和气质的培养是需要很长期的坚持的培养的

“以史为镜,可以知兴衰;以人为镜,可以知得失”出自于《旧唐书·魏徵传》。
一个人用铜当镜子,可以照见衣帽是不是穿戴得端正;用历史当镜子,可以知道国家兴亡的原因;用人当镜子,可以发现自己的对错
生活中脸皮子一定要厚,不要动不动就生气了
跟老板抬杠,这样的话,你会没有前途的

表达能力不好的话,不能把一件事情描述清楚

多做一些尝试,更多的准备,更充分的准备

1. 除了静态 static 数据成员外,数据成员不能在类体中被显式地初始化。

类的数据成员通过类的构造函数进行初始化。 

2. 我们可以声明一个类但是并不定义它.

这个声明向程序引入了一个名字 Screen,指示 Screen为一个类类型。

       但是我们只能以有限的方式使用已经被声明但还没有被定义的类类型,如果没有定义类那么我们就不能定义这类类型的对象。因为类类型的大小不知道,编译器不知道为这种类类型的对象预留多少存储空间。

       但是,我们可以声明指向该类类型的指针或引用,允许指针和引用是因为它们都有固定的大小, 这与它们指向的对象的大小无关,但是,因为该类的大小和类成员都是未知的,所以要等到完全定义了该类,我们才能将解引用操作符( * )应用在这样的指针上,或者使用指针或引用来指向某一个类成员。

       只有已经看到了一个类的定义,我们才能把一个数据成员声明成该类的对象。在程序文本中还没有看到该类定义的地方,数据成员只能是该类类型的指针或引用。例如下面是类StackScreen的定义,它有一个数据成员是指向 Screen类的指针,这里 Screen只有声明没有定义:  

       因为只有当一个类的类体已经完整时,它才被视为已经被定义,所以一个类不能有自身类型的数据成员,但是,当一个类的类头被看到时,它就被视为已经被声明了,所以一个类可以用指向自身类型的指针或引用作为数据成员。例如 :

在类体中定义的成员函数默认是inline函数,可以显示的加上inline;

通常,在类体外定义的成员函数不是 inline 的,但是,这样的函数也可以被声明为 inline函数。可以通过显式地在类体中出现的函数声明上使用关键字 inline 或者通过在类体外出现的函数定义上显式使用关键字 inline,或者两者都用。

但是,程序通常不直接修改类对象,又是在必须修改类的对象时,才调用公有成员函数集来完成,为尊重类对象的常量性,编译器必须区分不安全与安全的成员函(即区分试图修改类对象与不试图修改类对象的函数 ),例如: 

类的设计者通过把成员函数声明为 const 以表明它们不修改类对象,例如 :

只有被声明为 const 的成员函数才能被一个const 类对象调用,关键字 const 被放在成员函数的参数表和函数体之间,对于在类体之外定义的const 成员函数,我们必须在它的定义和声明中同时指定关键字 const。

把一个修改类数据成员的函数声明为 const 是非法的,例如,在如下简化的 Screen定义中:

但是,把一个成员函数声明为const 并不能阻止程序员可能做到的所有修改动作,把一个成员函数声明为 const 可以保证这个成员函数不修改类的数据成员 。但是,如果该类含有指针,那么在 const 成员函数中就能修改指针所指的对象,编译器不会把这种修改检测为错误,例如 :

尽管_text 不能被修改,但是_text 的类型是 char* 在类 Text 的 const 成员函数中可以修改_text 指向的字符,成员函数 bad()反映了一种不良的程序设计风格。

在这种情况下 类对象的常量性决定了调用哪个函数 :

构造函数和析构函数是两个例外,即使构造函数和析构函数不是const 成员函数,const 类对象也可以调用它们。当构造函数执行结束,类对象已经被初始化时,类对象的常量性就被建立起来了,析构函数一被调用,常量性就消失。所以一个 const 类对象从构造完成时刻到析构开始时刻,这段时间内被认为是const。


     可以将成员函数声明为volatile, 如果一个类对象的值可能被修改的方式是编译器无法控制或检测的,例如:如果它是表示 I/O端口的数据结构,则把它声明为 volatile 与 const 类对象类似,对于一个 volatile类对象,只有 volatile成员函数、构造函数和析构函数可以被调用:

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:


1) 并行设备的硬件寄存器(如:状态寄存器)
3) 多线程应用中被几个任务共享的变量


回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。


1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:


1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:


由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

volatile 相当于告诉编译器, 由它声明的东西的易变的, 不确定的, 可能由外部程序 (如中断程序) 改变的, 禁止编译器对其读写操作进行优化, 如果定义: 
则编译器可能会将其优化, 而放到 CPU 寄存器中, 这在多数情况下是好的, 然而在有些情况下, 我们会要求一些变量必须在内存中 (如驱动程序, 中断处理程序等等), 这时编译器这个优化就是引起问题, 为了避免这种情况, 应该这样定义: 

PS: volatile 通常也用来阻止编译器具优化操作, 如你有一个非精确延时函数:

有些编译会足够聪明地注意到这个函数本质上是什么也没干, 会将针对这个函数的调用优化掉, 但这样是不对的, 所以你应该这么声明:

Screen位置,即使 Screen对象是 const 也应该允许修改_cursor,因为这么做对于监视 Screen对象内容是必需的,而且又不会修改 Screen本身的内容 。

   为了允许修改一个类的数据成员,即使它是一个const 对象的数据成员,我们也可以把该数据成员声明为mutable(易变的), mutable 数据成员永远不会是 const 成员,即使它是 一个const 对象的数据成员,mutable 成员总可以被更新,即使是在一个 const 成员函数中 。为把一个成员声明为mutable 数据成员,我们必须把关键字 mutable 放在类成员表中的数据成员声明之前:


先要理解class的意思。class应该理解为一种类型,象int,char一样,是用户自定义的类型。(虽然比int char这样build-in类型复杂的多,但首先要理解它们一样是类型)。用这个类型可以来声明一个变量,比如int x, myclass my等等。这样就像变量x具有int类型一样,变量my具有myclass类型。
理解了这个,就好解释this了,my里的this 就是指向my的指针。如果还有一个变量myclass mz,mz的this就是指向mz的指针。 这样就很容易理解this 的类型应该是myclass *,而对其的解引用*this就应该是一个myclass类型的变量


通常在class定义时要用到类型变量自身时,因为这时候还不知道变量名(为了通用也不可能固定实际的变量名),就用this这样的指针来使用变量自身。 
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行
第一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;

第二种情况是,当参数与成员变量名相同时,如this->n = n (不能写成n = n);

第三种情况是, this指针含有被调用的类对象的地址。例如:

如果 sobj 指向的对象的地址与 this指针值相等 ,则 sobj 和 this指向同一对象,拷贝动作是不必要的。


this指针是存在与类的成员函数中,指向被调用函数所在的类实例的地址。
根据以下程序来说明this指针

4. 关于this指针的一个精典回答:
你可以看见桌子、椅子、地板等,
但是房子你是看不到全貌了。
对于一个类的实例来说,
你可以看到它的成员函数、成员变量,
this是一个指针,它时时刻刻指向你这个实例本身。

静态类成员包括静态数据成员和静态函数成员两部分。

类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时,静态数据成员还具有以下特点:


1.静态数据成员的定义。


静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中。 
其定义方式与全局变量相同。举例如下:


注:不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif或者#pragma once也不行。


2.静态数据成员被 类 的所有对象所共享,包括该类派生类的对象。

即派生类对象与基类对象共享基类的静态数据成员。举例如下:

// 结果为1,2;可见派生类与基类共用一个静态数据成员。


3.静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。


4.★静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。


5.★静态数据成员的值在const成员函数中可以被合法的改变

这个特性,我不知道是属于标准c++中的特性,还是vc6自己的特性。 举例如下: 

1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用 类成员函数指针来储存。


2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。


最后要说的一点是,静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。


   在一个类的内部定义另一个类,我们称之为嵌套类(nested class),或者嵌套类型。之所以引入这样一个嵌套类,往往是因为外围类需要使用嵌套类对象作为底层实现,并且该嵌套类只用于外围类的实现,且同时可以对用户隐藏该底层实现。
   虽然嵌套类在外围类内部定义,但它是一个独立的类,基本上与外围类不相关。它的成员不属于外围类,同样,外围类的成员也不属于该嵌套类。嵌套类的出现只是告诉外围类有一个这样的类型成员供外围类使用。并且,外围类对嵌套类成员的访问没有任何特权,嵌套类对外围类成员的访问也同样如此,它们都遵循普通类所具有的标号访问控制。
   若不在嵌套类内部定义其成员,则其定义只能写到与外围类相同的作用域中,且要用外围类进行限定,不能把定义写在外围类中。例如,嵌套类的静态成员就是这样的一个例子。
   前面说过,之所以使用嵌套类的另一个原因是达到底层实现隐藏的目的。为了实现这种目的,我们需要在另一个头文件中定义该嵌套类,而只在外围类中前向声明这个嵌套类即可。当然,在外围类外面定义这个嵌套类时,应该使用外围类进行限定。使用时,只需要在外围类的实现文件中包含这个头文件即可。
   另外,嵌套类可以直接引用外围类的静态成员、类型名和枚举成员(假定这些成员是公有的)。类型名是一个typedef名字、枚举类型名、或是一个类名。

     类也可以定义在函数体内 这样的类被称为局部类(local class), 局部类只在定义它的局部域内可见,与嵌套类不同的是,在定义该类的局部域外没有语法能够引用局部类的成员, 因此,局部类的成员函数必须被定义在类定义中,在实际中,这就把局部类的成员函数的复杂性限制在几行代码中,否则,对读者来说,代码将变得很难理解。 

    因为没有语法能够在名字空间域内定义局部类的成员 ,所以也不允许局部类声明静态数据成员。

    在局部类中嵌套的类可以在其类定义之外被定义,但是,该定义必须出现在包含外围局部类定义的局部域内。在局部域定义中的嵌套类的名字必须由其外围类名限定修饰,在外围类中,该嵌套类的声明不能被省略。例如:

    外围函数没有特权访问局部类的私有成员,当然,这可以通过使外围函数成为局部类的友元来实现。

    同嵌套类一样,局部类可以访问的外围域中的名字也是有限的,局部类只能访问在外围局部域中定义的类型名、静态变量以及枚举值,例如:

     在局部类体内,不包括成员函数定义中的 的名字解析过程是,在外围域中查找出现在局部类定义之前的声明,在局部类的成员函数体内的名字的解析过程是:在查找外围域之前 ,首先直找该类的完整域 。

     还是一样,如果先找到的声明使该名字的用法无效,则不考虑其他声明,即使在fooBar() 中使用 val 是错的,编译器也不会找到全局变量val ,除非用全局域解析操作符限定修饰 val,如 ::val 。

1. 我们可以给构造函数的参数提供一个缺省的实参。 


    C++语言新手常犯的错误是:按如下方式声明一个用缺省构造函数初始化的对象 


     被编译器解释为定义了一个没有参数,返回一个 Account 类型对象的函数——完全不是我们的意图,用缺省构造函数初始化类对象的正确声明是去掉尾部的小括号。 


只有当没有构造函数或声明了缺省构造函数时,我们才能不指定实参集来定义类对象。  如果定义了带参数的构造函数,最好也定义一个默认的构造函数:

     成员初始化表只能在构造函数定义中被指定,而不是在其声明中 。该初始化表被放在参数表和构造函数体之间,由冒号开始,下面是双参数的构造函数,部分利用了成员初始化表:

4. 构造函数用作转换操作符


     缺省情况下,单参数构造函数或者构造函数有多个参数(除了第一个参数外, 其他都有缺省实参 )被用作转换操作符,在上面的程序段中的print()的调用里 Account 构造函数被编译器隐式地应用,以便把一个文字字符串转换成一个 Account 对象,尽管这种转换在这种情况下并不合适。 

     无意的隐式类转换,如把 oops 转换成一个 Account 对象,已经被证明是很难跟踪的错误源,关键字 explicit 被引入到标准 C++中 以帮助我们抑制这种不受欢迎的编译器辅助行为 ,explicit 修饰符通知编译器不要提供隐式转换: 

     用一个类对象初始化该类的另一个对象被称为缺省按成员初始化(default memberwise initialization ),在概念上 ,一个类对象向该类的另一个对象作拷贝是通过依次拷贝每个非静态数据成员来实现的。

     缺省按成员的初始化对于类的正确行为常常是不合适的, 通过定义拷贝构造函数的显式实例,我们可以改变缺省的行为,我们的Account 类要求我们这样做, 否则两个Account 对象会有相同的帐号,这在该类的规范中显然是不允许的。  
    拷贝构造函数有一个指向类对象的引用作为形式参数,传统上被声明为 const ,下面是它的实现:


     编译器判断是否为 Account 类声明了一个显式的拷贝构造函数,如果声明了拷贝构造函数,并且是可以访问的 ,则调用它;如果声明了拷贝构造函数但是不可访问,则acct2的定义就是一个编译时刻错误; 如果没有声明拷贝构造函数的实例,则执行缺省的按成员初始化。如果我们后来引入或去掉了一个拷贝构造函数的声明,则用户程序无需改变,但是,需要重新编译它们 。


      析构函数是一个特殊的由用户定义的成员函数,当该类的对象离开了它的域或者delete表达式应用到一个该类的对象的指针上时,析构函数会自动被调用。 析构函数的名字是在类名前加上波浪线 ~ ,它不返回任何值也没有任何参数,因为它不能指定任何参数, 所以它也不能被重载。尽管我们可以为一个类定义多个构造函数,但是我们只能提供一个析构函数,它将被应用在类的所有对象上。下面是Account 类的析构函数:

      一般地, 如果一个类的数据成员是按值存储的,比如 Point3d 的三个坐标成员,则无需析构函数。并不是每一个类都要求有析构函数,即使我们为该类定义了一个或多个构造函数 ,析构函数主要被用来放弃在类对象的构造函数或生命期中获得的资源,如释放互斥锁或删除由操作符new分配的内存。

    C++语言在内部保证,不会用 delete操作符删除不指向任何对象的指针, 所以我们无需再编写代码来保证这一点 :


      无论何时,当在一个函数内删除一个独立的堆对象时,最好是用 auto_ptr 类对象而不是一个实际的指针 。对于堆上的类对象尤其应该这样做,否则的话,如果应用 delete表达式失败(比如一个异常被抛出的情况下 ),不仅会导致内存泄漏 而且析构函数也不会被调用 。例如, 下面是我们用auto_ptr 重写之后的程序示例,它被稍做修改因为 auto_ptr 对象不支持被显式地重置以指向第二个对象,除非赋值第二个 auto_ptr 。

      在某些程序情况下,有必要显示地对一个特殊类对象调用析构函数。这常常发生在和定位 new操作符结合的时候。让我们看一个例子,


      时,实际上我们已经分配了一个大小等于 Image 型对象的新的堆存储区。相关联的内存区没有被初始化,里面是上次使用之后的一段随机位序列。当我们写:


      时,没有新的内存被分配。相反,ptr 被赋值为与 arena 相关联的地址,通过 ptr,内存被解释为一个 Image 类对象。然而,虽然没有分配内存,但是构造函数被应用在现有的存储区上。实际上,定位 new 操作符允许我们在一个特定的、预分配的内存地址上构造一个类对象。 

      则除了调用析构函数,我们还删除了底层的堆存储区,这不是我们希望的。我们可以显式地调用 Image 的析构函数:


  不会导致调用 Image 的析构函数,因为 arena 的类型是 char*。记住,只有当 delete 表达式中的指针指向一个带有析构函数的类类型时,编译器才会调用析构函数。 

三、可能出现的程序代码膨胀

     毫无疑问,内联析构函数可能时程序代码膨胀的一个源泉,因为它被插入到函数中的每个退出点,以析构每一个活动的局部类对象。例如,在如下代码段中:

     在每个 return语句之前,析构函数都必须被内联地展开,在 Account 类的析构函数的情况下,由于它的长度较小,所以多次展开的相关开销也较小。但是 如果已经发现它确实是一个问题,则解决方案是:或者把析构函数声明为非内联的,或者重新改写程序代码。一种可能的重写方案是在每个case 标签中用

设想在一个军事应用程序里,有一个表示敌人目标的类:

//类的静态成员要在类外定义;

敌人的坦克是一种特殊的敌人目标,所以会很自然地想到将它抽象为一个以公有继承方式从enemytarget派生出来的类。因为不但要关心敌人目标的总数,也要关心敌人坦克的总数,所以和基类一样,在派生类里也采用了上面提到的同样的技巧:

最后,假设程序的其他某处用new动态创建了一个enemytank对象,然后用delete删除掉:

这样会发生严重问题,因为c++语言标准关于这个问题的阐述非常清楚:当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。实际运行时经常发生的是,派生类的析构函数永远不会被调用。在本例中,这意味着当targetptr 删除时,enemytank的数量值不会改变,那么,敌人坦克的数量就是错的。

如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。

所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。

抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。

这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:

这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。

注意:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。

以下列出几种初始化的方法:

要记得将此数组所指的所有对象删除。如果忘了会产生资源泄露。还有就是该方法与对象数组相比需要额外内存用于存放指针。(过度使用内存 这一问题可以避免,见第三种方法)

注意:该方法维护比较困难。在数组内对象结束生命时,要以手动方式调用destructors,最后还得调用operator delete释放raw memory。

C++为类中提供类成员的初始化列表

类对象的构造顺序是这样的: 1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员
2.进入构造函数后在构造函数中执行一般计算

使用初始化列表有两个原因:


如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。

因为ABC有了显示的带参数的构造函数,那么他是无法依靠编译器生成无参构造函数的,所以没有三个int型数据,就无法创建ABC的对象。 ABC类对象是MyClass的成员,想要初始化这个对象abc,那就只能用成员初始化列表,没有其他办法将参数传递给ABC类构造函数。

另一种情况是这样的:当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的


类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对他们的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次复制操作符的调用,如果是类对象,这样做效率就得不到保障。

注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在声明时的顺序是一致的,与列表的先后顺序无关所以要特别注意,保证两者顺序一致才能真正保证其效率。

为了说明清楚,假设有这样一个类:


1、foo(){}和foo(int i = 0){}都被认为是默认构造函数,因为后者是默认参数。两者不能同时出现。


2、构造函数列表的初始化方式不是按照列表的的顺序,而是按照变量声明的顺序。比如foo里面,a在b之前,那么会先构造a再构造b。所以无论 foo():a(b + 1), b(2){}还是foo():b(2),a(b+1){}都不会让a得到期望的值。如果先声明b再声明a则会更好。


3、构造函数列表能够对const成员初始化。比如foo里面有一个int const c;则foo(int x) : c(x){}可以让c值赋成x。不过需要注意的是,c必须在每个构造函数(如果有多个)都有值。

另一篇关于初始化列表的文章:

C++初始化类的成员,不但可以用构造函数(constructor)完成,而且可以用初始化类成员列表来完成。MFC大量用到此方法。例如有些初学者可能不大理解如下代码:

他们觉得这个构造函数的定义应该只能这样写:

其实两种方法都可。但是有些情况下,只能用第一种,而且通常情况下用第一种也会效率高些。

其实,第一种方法是真正的初始化(initialization),而在构造函数内实现的“=”操作其实是赋值(assign)。这两种方法的一切区别从这儿开始。区别大概如下:

  1. 我们知道普通变量编译器都会默认的替你初始化。他们既能初始化,也能被赋值的,而常量(const)按照其意思只能被初始化,不能赋值。否则与变量就无区别了。所以常量成员(const member)只能用成员初始化列表来完成他们的“初始化”,而不能在构造函数内为他们“赋值”
  2. 我们知道类的对象的初始化其实就是调用他的构造函数完成,如果没有写构造函数,编译器会为你默认生成一个。如果你自定义了带参数的构造函数,那么编译器将不生成默认构造函数。这样这个类的对象的初始化必须有参数。如果这样的类的对象来做另外某个类的成员,那么为了初始化这个成员,你必须为这个类的对象的构造函数传递一个参数。同样,如果你在包含它的这个类的构造函数里用“=”,其实是为这个对象“赋值”而非“初始化”它。所以一个类里的所有构造函数都是有参数的,那么这样的类如果做为别的类的成员变量,你必须显式的初始化它,你也是只能通过成员初始化列表来完成初始化。例如:

A::A():B(...) //你必须显式初始化它,因为他的所有构造函数
//都是有参数的,之后才能被赋值。
B=...; //因为如上所写,已经初始化了,才能被赋值,否则错误。

——————————————————————————————————————

黄色的既是初始化列表,他们会在构造函数正式调用前被调用,且他们的初始化顺序并不是根据 初始化列表中出现的顺序,而是他们声明的顺序来初始化。如上:

一般用于初始化 常量类型,静态类型的数据,或者不能独立存在的数据


一、按成员初始化(与构造函数拷贝构造函数有关) 


     被称为缺省的按成员初始化(default memberwise initialization),缺省是因为它自动发生,无论我们是否提供显式构造函数,按成员是因为初始化的单元是单个非静态数据成员,而不是对整个类对象的按位拷贝。 


     对于大多数实际的类定义, 由于考虑到类的安全性以及用法正确性,所以说缺省的按成员初始化是不够的,最经常出现的情况是 一个类的数据成员是一个指向堆内存的指针,并且这块内存将由该类的析构函数删除,就如Account 类中的_name 成员一样 。

oldAcct也会受到影响,这种指向错误很难跟踪 。

    指针”别名 (aliasing) 问题”的一种解决方案是,分配该字符串的第二个拷贝 ,并初始化 newAcct._name 以指向这份新的拷贝,为实现这一点,我们必须改变 Account 类的缺省按成员初始化,我们通过提供一个显式的拷贝构造函数来做到这一点。   

    类的内部语义也可能使缺省的按成员初始化无效,比如前面所解释的,不能有两个Account 类的对象持有同一个帐号,为了保证这一点,我们必须改变 Account 类的缺省按成员初始化,下面是解决这两个问题的拷贝构造函数:

    除了提供拷贝构造函数,另一种替代的方案是完全不允许按成员初始化,这可以通过下列两个步骤实现:  
    1 把拷贝构造函数声明为私有的,这可以防止按成员初始化发生在程序的任何一个地方(除了类的成员函数和友元之外)。  
    2 通过有意不提供一个定义,但是,我们仍然需要第 1 步中的声明,可以防止在类的成员函数和友元中出现按成员初始化。C++语言不会允许我们阻止类的成员函数和友元访问任何私有类成员,但是通过不提供定义,任何试图调用拷贝构造函数的动作虽然在编译系统中是合法的,但是会产生链接错误, 因为无法为它找到可解析的定义。  

二、成员类对象的初始化

把 C风格字符串的_name 声明,替换成 string 类类型的_name 声明,会发生什么变化?

     缺省的按成员初始化依次检查每个成员,如果成员是内置或复合数据类型,则直接执行从成员到成员的初始化。例如,在我们原来的Account 类定义中,因为_name 是一个指针,所以它直接被初始化: 


     这两个对象就被识别为 Account 类对象,如果 Account 类提供了一个显式的拷贝构造函数则调用它以完成初始化,否则应用缺省的按成员初始化;类似地,当一个成员类对象被识别出来时,则递归应用相同的过程。

    在我们的例子中, string 类提供了显式拷贝构造函数,通过调用该拷贝构造函数,_name被初始化。 现在我们可以认为 缺省Account 拷贝构造函数被定义如下:

    Account 类的缺省按成员初始化过程现在可以正确地处理_name 的分配和释放,但是 拷贝帐号仍然不正确 ;因此,我们仍然必须提供一个显式的拷贝构造函数,下面的代码不是十分正确。你能看出为什么吗?  

     该实现不完全正确是因为我们没有区分开初始化和赋值,结果,调用的不是string 拷贝构造函数,而是在隐式初始化阶段调用了缺省的 string 构造函数,并且在构造函数体内调用了string 拷贝赋值操作符。修正很简单:


     再次强调 ,真正的工作是在一开始就意识到我们需要提供一个修正 两个实现的结果都是_name 持有 rhs._name 的值, 只不过 第一个实现要求做两次重复工作,一个一般性的规则是:在成员初始化表中初始化所有的成员类对象 。

三、按成员赋值(与拷贝赋值操作符有关)

    缺省的按成员赋值( default memberwise assignment ),所处理的是 用一个类对象向该类的另一个对象的赋值操作,其机制基本上与缺省的按成员初始化相同;但是它利用了一个隐式的拷贝赋值操作符来取代拷贝构造函数,例如:


     在缺省情况下,用 oldAcct 的相应成员的值依次向 newAcct 的每个非静态成员赋值,在概念上就好像编译器已经生成下列拷贝赋值操作符:


     通过提供一个显式的拷贝赋值操作符的实例,可以改变缺省的按成员赋值,我们在这操作符实例中实现了正确的类拷贝语义,拷贝赋值操作符的一般形式如下:


     应该防止一个类对象向自己赋值, 因为对于(先释放与该对象当前相关的资源 ,以便分配与被拷贝对象相关的资源)这样的拷贝赋值操作符 拷贝自身尤其不合适。例如 ,考虑Account拷贝赋值操作符:

    3 如果它不能被调用,则会产生一个编译时刻错误,否则,调用它执行赋值操作;  
    5 在缺省按成员赋值下,每个内置类型或复合类型的数据成员被赋值给相应的成员;  
    6 对于每个类成员对象,递归执行1到 6 步,直到所有内置或复合类型的数据成员都被赋值。


     会调用缺省的按成员赋值,就好像编译器为我们生成了下面的拷贝赋值操作符:


     但是 Account 类对象的缺省按成员赋值仍然不合适,同为_acct_nmbr 成员也被按成员拷贝了,我们仍然必须提供一个显式的拷贝赋值操作符, 但是它以成员类 string 对象的方式来处理 name :


     如果希望完全禁止按成员拷贝的行为,那么就需要像禁止按成员初始化一样,将操作符声明为 private,并且不提供实际的定义。

     一般来说,应该将拷贝构造函数和拷贝赋值操作符视为一个个体单元,因为在我们需要其中一个的时候,往往也需要另外一个;而试图禁止一个的时候,也很可能需要禁止另一个。 


我要回帖

更多关于 一张图看懂波浪 的文章

 

随机推荐