C++小结
一、构造函数和析构函数
对象的构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响, 只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
二、虚继承
虚继承是多重继承中特有的概念。虚基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚继承,而A就成了虚基类。
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
三、多态
1、存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针(指针在64位机器上占8个字节)。虚表是和类对应的,虚表指针是和对象对应的。
2、在构造函数中进行虚表的创建和虚表指针的初始化。
3、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么 虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
4、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
5、虚表的一个不好的地方是,既然知道的虚指针,就可以根据地址调用相关的虚函数,即使该虚函数是私有的(linux平台是这样的)。
四、static
静态数据成员:
1) 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
2) static 成员变量和普通 static 变量一样,编译时在静态数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
3) 静态成员变量必须初始化,而且只能在类体外进行,不用加访问权限控制符private,public等。例如:
int Student::num = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化,一般是 0。静态数据区的变量都有默认的初始值,而动态数据区(堆区、栈区)的变量默认是垃圾值。
4) 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
静态成员函数与非静态成员函数的根本区别是:非静态成员函数有 this 指针,而静态成员函数没有 this 指针。由此决定了静态成员函数不能访问本类中的非静态成员,只能访问静态数据成员。
五、const
const限定符的作用就是限定一个变量的值不能改变(因为有的时候我们希望一个变量的值不被改变)。为了这个蛋疼的功能,给C++添加了很多复杂性。
1、初始化:由于const变量的值定了以后不能再改变,所以规定const必须(显式)初始化。const变量的初始化和变量的初始化一样,因为从语义上来说,只是变量赋初值而已。
由于编译期间,编译器可能会把程序中const变量替换成其值,所以每个文件的const变量都应该初始化即定义。为了支持这一用过,const变量是默认只在本文件内有效。可以通过extern实现一处实现,多处声明,这样编译器就不会进行值替换。
const数据成员的初始化只能在类的构造函数的初始化列表中进行。但static const类型的初始化还是在类外初始化。
2、const引用:表示该引用不会改变其绑定的变量的值。const引用和引用的初始化略有不同:引用初始化时要求引用类型和变量类型严格一致,
但const引用绑定的对象可以是非const变量(从语义上来说所绑定的对象是不是const对const引用无关紧要)或变量的类型可以转换成引用的类型。
因为在引用初始化的类型转换中,会先产生一个临时量,然后引用绑定的是这个临时量。这个临时量是const的,所以非const引用不能绑定该临时量,但是const引用可以。编译器为了增加引用的使用范围做了一些工作,使const引用看起来使用范围更广。
3、const和指针:常量指针表示指针本身是const,指向常量的指针表示指向的变量是个常量。指针赋值,包括const指针初始化,还是主要看所指向的变量类型是否一致(可以进行类型转化)。
最后,非const引用和非指向const类型的指针不能绑定和指向const变量,const引用和指向const类型的指针可以绑定和指向非const变量。(原因很好理解)
六、引用和指针
1、引用即使别名,所以引用定义时必须有一个显式的对象。引用的类型要与所绑定的类型严格匹配,一些const引用的情况除外。
引用的底层实现还是指针,通过编译器编译后的汇编代码可以看出。即编译过程中,遇到引用都是变成对引用所绑定对象的地址的操作,所以没有引用这个变量即引用不是变量。
指针是变量,存放的地址。通过->(成员选择运算符)可以操作指定地址的内存(或 解引用和.运算符)。
2、指针和引用的区别:1、指针可以重新赋值或者进行别的运算,而引用不可以(有点像常量指针* const p)。2、引用必须初始化,而指针可以默认初始化(这也有可能出现未定义的情况)。3、引用的作用就是更加符合人们的使用习惯,而且一定程度上确保操作的安全(不会出现指针操作失误引起的野指针等情况,与指针相比)。4、“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小。
七、智能指针
1、 智能指针是利用RAII(在对象的构造函数中执行资源的获取(指针的初始化),在析构函数中释放(delete 指针):这种技法把它称之为RAII(Resource Acquisition Is Initialization:资源获取即初始化))来管理资源。利用的是栈对象的生存周期。
2、解决的问题是new了一个对象,但是忘记delete或者在未delete之前,程序出现异常,导致对象没有释放。
(1)auto_ptr,头文件<memory>C++11提供了更多的智能指针,但是在此之前只有auto_ptr。但是auto_ptr存在的最大的问题是不能共享所有权,可以转移所有权,即:
auto_ptr<string> p1(new string("test"));
auto_ptr<string> p2;
p2 = p1;
p1的指针为NULL,p2的指针指向new string。如果再使用p1,就会造成程序崩溃。在容器中经常有赋值操作,所以,auto_ptr不能用于容器。但是进行简单的释放对象操作还是可以的,但是要小心使用。
不能使用C++11,可以使用boost库来解决这些问题。
(2)scoped_ptr,头文件<boost::scoped_ptr.hpp>,与auto_ptr类似,但是不能进行所有权的转移。上述代码中,如果使用的是boost::scoped_ptr,则p2 = p1;这条语句,编译器不会通过。因为boost::scoped_ptr的赋值运算符是私有的。
如果可以的话尽量用scoped_ptr来代替auto_ptr。
(3)shared_ptr,头文件<boost::shared_ptr.hpp>,它维护了一个引用计数器,能够共享所有权,当引用计数器为0时,才释放对象。这样shared_ptr可以用于STL容器。
所以,要用于容器,或者多个智能指针要指向同一个对象,选择shared_ptr。
但维护引用计数器,肯定要有一些消耗,简单的替代delete作用,可用scoped_ptr或者auto_ptr。
九、extern “C”
在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
答案与分析:
C++语言在编译的时候为了解决函数的重载问题,在生成汇编代码时会将函数名与参数(或者返回值)联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况。此时对引用的C函数需要用extern “C”修饰,这告诉编译器,请保持我的名称,用C的方式处理函数名。