这一章主要讲数据的封装发展。
delete对应new,数组new对应数组delete。
当编译时,如果在栈上创建一个变量,那么这个变量的存储单元由编译器自动开辟和释放。编译器准确的知道需要多少存储容量,根据这个变量的活动范围知道这个变量的生命周期。而对动态内存分配,编译器不知道需要多少存储单元,不知道他的生命周期,不能自动清除。
在C语言中,可以使用没有声明的函数。因为,C语言会配置默认的搜索路径,*.c,全局查找。
每个独立的C文件是一个翻译单元。
在C中,可以将任意类型的指针赋值给void*,也可以将void*赋值给任意类型的指针;在C++中,只允许将任意类型的指针赋值给void*,不允许将void*赋值给任意类型。
对象就是变量,他的最纯正的定义是“一块存储区”,在这里面能存放数据,而且还隐含着对这些数据进行的操作。
一个结构体的大小存在内存对齐的情况,目的是提高执行效率。
对于编写头文件的一点原则:只限于声明,即只限于对编译器的信息,不涉及通过生成代码或创建变量而分配存储的任何信息。
头文件多次被include,从而出现多次声明,解决方法是防卫式声明,即#ifndef #define #endif
编译器默认选择的名字(选择最接近的)可能不是我们想要的名字,作用域解析运算法::可以避免这种情况,例如要查找全局的a变量,使用::a。
这一章主要讲类的访问控制。
访问控制中的protected值得说一下:子类可访问,但是对象不能访问。
友元:不管是函数还是类,在一个类中声明为友元,那么这个友元函数或者类就能访问这个类的私有成员,并且友元类或者函数不属于这个类。
在一个特定的“访问块”内,变量在内存中肯定是连续存放的,但是这些访问块本身可以不按声明的顺序在对象中出现。
访问控制信息通常在编译期间消失。
在编程环境中,当一个文件被修改,或它所以来的头文件被修改时,项目管理员需要重新编译该文件。(易碎的基类问题)解决这个问题的技术有时称为句柄类,具体做法是:在一个类的头文件中声明一个类,但是不给完整的类实现,具体的实现放在对应的cpp文件中。这样,对于这个不给完整实现的类就是一个句柄类,当修改这个句柄类时,只涉及到相应的cpp文件,其他包含这个头文件的一些文件,不需要做重新编译。
这一章主要讲构造函数与析构函数,内容并没有多少,多深。
封装和访问控制在改进库的易用性方面取得了重大进展。
传递到构造函数的第一个参数是this指针,也就是调用这一函数的对象的地址,这个地址是有operator new()创建返回的。不过,对于构造函数来说,this指针指向一个没有初始化的内存块,构造函数的作用就是正确的初始化该内存块。
聚合就是多个事物聚集在一起。例如,数组,类,结构体都是聚合类型,其中数组是单一类型的聚合。当产生一个聚合对象时,要做的只是指定初始值就行,然后初始化工作由编译器去承担。
如果给出的初始值个数多余数组元素的个数,编译器会给出一条出错信息,但如果个数少于元素个数且不为0,对于没有给出的部分初始化为0。当没有指定初始化值的时候,不会进行初始化。数组还有一种叫自动计数的快速初始化的方法,就是让编译器按初始值的个数自己去决定数组的大小。
在C++中,可以对不同的函数使用相同的名字,只要求函数的参数不同。编译期会修饰这些名字、范围和参数来产生内部名以供它和连接器使用。总的来说,函数的重载,是根据函数名、参数类型及数量进行重载。
可以看到1中并没说函数重载会根据返回值来重载,因为,有可能会存在函数调用忽略返回值的情况,这种情况下会让编译期摸不清头脑,不知调用哪一个函数。因此,一个程序中出现返回值不同,函数签名相同的两个函数的时候,会出报错信息。
struct和class的唯一区别在于,struct默认为public的,而类默认为private。union也可以带有构造函数、析构函数、成员函数甚至访问控制。
默认参数是函数声明时,就已给定一个值,如果在函数调用时没有给定这一参数的值,编译期就会自动地插上这个值。使用默认参数有两条规则:
占位符参数:void f(int x, int, float flt);这种语法允许吧一个参数用作占位符而不去使用它。其目的是在于以后可以修改函数定义而不需要修改所有的函数调用。但是,不能把默认参数当做一个标志去决定执行函数的那一块,这是基本规则。在这种情况下,只要能够,应该把函数分解成两个或者多个重载的函数。
const最初动机是取代预处理器#define来进行值替换。因为预处理器只做一些文本替代,它既没有类型检测概念,也没有类型检测的功能,所以预处理器的只替代会产生一些微小的问题,这些问题在C++中可以通过使用const来避免。
cosnt默认为内部连接,也就是说仅在const被定义过的文件里才是课间的,而其它单元是不可见的。当定义一个const时,必须赋值给他,除非用extern作了清楚地说明。通常C++编译期并不为const创建存储空间,相反把它这个定义保存在他的符号表中。当然这仅仅是通常,因为存在特殊情况(由于编译器不能完全避免const分配内存,索引const定义必须默认内部连接):
需要辨别的一个点是cosnt int *p;和int *const p;前者说明p是一个指针,他指向一个const int,也就是说,指针指向的内容是不能发生变化的,但是指针本身是可以改变指向的;后者说明p是一个指针,这个指针是指向int的const指针,在这种情况下,指针的指向是不可以发生变化的,但是指针指向的内容本身是可以改变的。
int *p, a;这个语法说明,声明了一个int类型的指针和int类型的变量。因此,*是和变量结合的,而不是与int结合的。
如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个右值。因为const修饰的常量,不能被赋值,也不能被修改。
编译器生成的临时对象全都默认为const,因为编译器不会对这些临时变量进行修改。
带const引用参数的函数比布带const的更具一般性。因为带const引用参数的函数,能够接受左值和右值作为对应的参数传递,就像第6条所说的,如果使用临时对象作为参数进行传递的时候,编译器默认他为const的,如果函数的一个引用参数没有const修饰,那么这个临时对象是会报错的。即,const和非const都能传递进const修饰的参数,而非const修饰的参数只能接受非const值。这就像,const对象只能调用const成员函数,而不能调用非成员函数。
const成员变量不能给初值,必须在构造函数初始化列表进行初始化(const static除外)。
const修饰成员函数,与同名的非const成员函数构成重载。const修饰的是this指针,即,const修饰的成员函数不能对类中的成员变量进行修改。
对于一个const常量,我们可以在类声明里使用关键字mutable,以指定一个特定的数据成员可以在一个const对象中发生改变。这和volatile 有点相似,但是volatile的意思是“在编译器认识的范围外,这个数据可以被改变”,也就是告诉编译器不要擅自做出有关该数据的任何假定,优化期间尤其如此。
宏可以不要普通的函数调用代价就可以是指看起来像函数调用。预处理器直接用宏代码代替宏调用,所以没有了参数压栈、生成汇编语言的call,返回参数,执行汇编语言的return等开销。但是使用宏,会出现一些问题:
为了即保持预处理器宏地效率又增加安全性,而且还能像一般成员函数一样可以在类里自由访问,C++引入了内联函数。在C++中,宏地概念是作为内联函数来实现的,而内联函数不论从哪一方面上说,都是真正的函数。唯一不同之处是内联函数在适当的地方想宏一样展开,所以不需要函数调用的开销。因此,应该永远不适用宏,而只使用内联函数。
针对inline声明的函数,当编译器看到这个定义,他把函数类型(函数名+返回值)和函数体放在符号表中(不仅仅是内联函数,所有的函数都会放到这里面)。当使用函数时,编译器检查以确保调用是正确的且返回值被正确使用,然后将函数调用替换成函数体,因而消除了开销。内敛代码的确占用空间,但假如函数较小,这实际上比为了一个普通函数调用产生的代码(参数压栈、执行CALL)占用的空间还少。
内联仅是对编译器的一个建议,编译器会不会被强迫内联任何代码。
只有在类声明结束后,其中的内联函数才会被计算。
对于宏定义:#后面跟参数,就是将该参数变成一个字符串;参数##其他字符(a## _string),就是生成一个新的标识符。
能用一个函数指针指向内联函数嘛?
关于static的所有使用最基本的概念是指“位置不变的某个东西”,不管这里指的是在内存中的物理位置还是指在文件中的可见性。这句话精炼的概述了static的含义。
在固定的地址上进行存储分配,也就是说对象是在一个特殊的静态数据区上创建的,而不是每次函数调用时在堆栈上产生的;对一个特定的编译单位来说是局部的,static控制名字的可见性,所以这个名字在这个编译单元或者类或者函数之外是不可见的。
对于文件,或者说是编译单元:static和extern一对概念,static修饰变量只在当前文件可见,对应的是内部连接,extern修饰变量可以用于其他文件(或者说是,该变量来自其他文件),对应的是外部连接。
对于函数,static声明该变量的作用域是当前函数(函数外是不能使用这个static变量的),第一次调用这个函数会对static变量做一个初始化的工作,但是后面>1次的调用,直接使用该变量。这个也叫做局部静态变量。extern在函数中,表示该变量来自其他编译单元,和第二点中的extern相同。==值得强烈注意的一点是:==书中说局部静态变量是线程不安全的,但这只是针对C++11之前的版本,C++11之后,局部静态变量是线程安全的,也就是说,当一个线程正在做static变量初始化工作的时候,其他线程将会被阻塞。因此,局部静态变量可以很方便的做单例。
对于类,类的静态成员变量拥有一块单独的存储区,不管该类创建多少个对象,这些对象的静态数据成员都共享这一块静态存储空间。类中的静态数据成员的语法很有讲究:要在类中声明,类外定义。(对这种语法的理解:在外部定义才能让编译器看到,才能为其分配存储空间。如果在类中定义,就有点像函数的局部静态变量,没有运行到这一行,不会分配存储空间)。但是对于const static,需要直接在类中定义初始化。
嵌套类中可以使用静态成员变量,但是在局部类中不能使用静态成员变量。如果说,要在局部类中使用静态成员变量,那么我们怎么做初始化工作呢?
静态成员函数只能操作静态成员变量,调用静态函数。原因是,静态成员函数没有this指针。静态成员函数的调用可以直接使用类名进行调用。
静态对象的构造总是在main之前,静态对象的析构总是在main退出时的exit或者atexit中进行。静态对象的销毁和静态对象的创建顺序正好相反。
命名空间的创建:
using我觉得最重要的需要理解的点是静态初始化的相依性这一小节。在一个指定的编译单元中,静态对象的初始化顺序严格依照对象在该单元中定义的出现顺序,清除顺序正好相反;但是对于作用域为多个翻译单元的静态对象来说,不能保证严格的初始化顺序,也没有办法来指定这种顺序。因为有三种方案来处理这个问题:
C++中提供一个替代连接说明,他是通过重载extern关键字来实现的。extern后跟一个字符串来指定想要声明的函数的连接类型,后面是函数声明。例如:extern “C” float(int a, int b);这样编译器就会使用C语言的编译方法来编译这个函数。
当创建一个C++对象时,会发生两件事情:1)为对象分配内存;2)调用构造函数来初始化这个内存。对象在哪里和如何被创建无关紧要,但是构造函数总是需要被调用。对这句话的解读:创建C++堆对象,使用operator new来进行,为对象分配内存是operator new()干的工作,这个分配内存的工作是可以被程序员操作,或者说是重载的。重载,可以使用自己的内存分配策略,也可以使用new-handler来处理分配不到内存时的情况,主要是抛异常,用catch来处理异常;在调用构造函数来初始化内存这一块,是编译器做的工作,程序员无法介入。
对象可以在静态内存、堆、栈上进行分配内存。
在堆里创建对象包含额外的时间空间开销,主要包括要去操作系统维护的空闲内存链表(姑且认为使用链表维护空闲内存的)查找合适的内存用于返回。
将一个指针指向NULL,那么不会调用这个指针指向的对象的析构函数。因为NULL就没有虚构函数一说。 所以在移动构造中,要讲原来的指针置NULL,这才会是的传入的参数出作用域后发生析构,导致新建的对象内存也跟着被析构的原因。
delete void*是可能会出错的,因为当你用一个void去接受一个堆上创建的新对象,去delete这个对象,本身就是void的,没有析构函数,所以只会释放掉这个void*类型的内存空间,但是不会调用这个对象的析构函数。比如说这段代码void* b = new Object(40); delete b;,那么,只做了释放内存的工作,没有调用Object类的析构函数,有可能会产生内存泄漏。
重载operator new(),可以重载全局的,也可以只针对一个类进行重载。重载时,默认有一个size_t sz参数,这个是编译器给出的,含义是需要分配的内存空间大小。也可以加入一些自己的参数,比如我想要实现在指定的内存空间调用构造函数生成对象,那么就可以传一个内存空间的地址过去,并作为operator new()的返回,这样构造函数就是初始化的这个指定的内存空间(有点疑惑的是,这种实现方式是不是叫placement new)。
如果要自己提供内存OOM时对应的策略,可以引入new头文件,使用set_new_handler(slef_func)来定义行为。
数组new得对应数组delete,要不然会出现内存泄漏的问题。即new Type[size] ----> delete []
operator new过程:operator new()分配内存,调用构造函数初始化内存;operator delete过程:调用析构函数,operator delete()释放内存。
组合:我们简单地在一个新类中创建已存在类的对象;继承:不修改已存在的类,而是采取这个已存在类的心事,并将代码加入其中。
为什么建议在初始化列表中进行父类的、成员变量(不管是内建类型还是自定义类型)的初始化?在进入新类的构造函数体之前调用所有其他的构造函数,这样,对子对象的成员函数所做的任何调用总是转到了这个被初始化的对象中。一旦遇到了左括号,就认为所有的子对象已经被初始化了,我们的精力就可以集中在想要完成的任务上面。
构造函数和析构函数的调用顺序:构造,从基类开始及构造;析构,从子类开始析构。对于自定义成员变量的构造,初始化列表是不能控制它们的构造顺序的,而是由成员对象在类中声明的次序所决定的,因为,如果初始化列表能够决定他们的构造顺序,那么,不同的构造顺序对应不同的构造函数,析构时就不知道析构的顺序,会产生相关性问题。
基类中有定义的函数,在派生类的定义中明确定义操作和返回类型,这是普通成员函数的重定义(终于知道怎么描述这个行为了)。任何时候重新定义了一个基类中的一个重载函数,在新类之中所有的其他版本都会自动地隐藏。
不是所有的函数都能自动地从基类继承到派生类中的:构造函数、析构函数、operator=。为什么operator=也例如其中?因为,他完成类似于构造函数的活动,赋值构造。
组合和继承的选择方式:选择组合时,希望新类内部具有已存在类的功能,而不希望已存在的类作为他的接口;相反,继承就是希望在新类中使用一个已存在的类作为他的接口。还有一种选择方式是:询问是否需要从新类向上类型转换。
private继承能解决什么问题?隐藏基类的部分功能。在private继承中,对于要使用的基类的功能,可以用:using base::func();
protected能解决什么问题?就这个类的用户而言,他是private的,但他可以被从这个类继承来的任何类使用。protected继承,不常用,他的存在只是为了语言的完备性。
继承,默认是private继承。
不管我们如何认为我们必须用多继承,我们总能通过单继承完成。
继承最重要的方面不是他为新类提供了成员函数,而是他是基类与新类之间的关系,这种关系可被描述为:“新类属于原有类的类型”
编译器允许向上类型转化,而不需要显示地说明或做其他标记。
如果没有声明定义,编译器会自动给出默认构造函数,拷贝构造函数,operator=和析构函数。
如果允许编译器为派生类生成拷贝构造函数,他将首先自动地调用基类的拷贝构造函数,然后再是各成员对象的拷贝构造函数。