1.访问符号
访问符号public、private、protected可以多次出现在类的定义中,给定的访问符号应用到下一个访问符号出现为止。如果类是用struct关键字定义的,则在第一个访问符号之前的成员是共有的,如果类是用class关键字定义的,则这些是私有的。
类对其成员的访问主要有两种形式:
1)内部访问:由类的成员函数对类的成员进行访问。
2)对象访问:在类外部,通过类的对象对类进行访问。
类的成员函数(内部访问)以及友元可以访问类中所有成员,但是在类外通过类的对象(对象访问)只能访问该类的共有成员。
上述权限并没有考虑有继承的情况。
2.成员函数
在类内部,声明成员函数是必需的,而定义成员函数是可选的。在类内部定义的成员函数默认为inline(内联函数)。
成员函数实际是使用对象来调用的。除static成员函数外,每个成员函数都有一个额外的,隐含的形参this,在调用成员函数时,形参this初始化为调用函数的对象的地址(然后由这个对象调用该成员函数)。
在成员函数中处理非静态数据成员时,隐式的类对象就会发生.如:
void Point3d::translate(const Point3d &pt)
{
x += pt.x;
y += pt.y;
z += pt.z;
}
实际上xyz的存取是由this指针完成的,其函数参数可以理解为:
void Point3d::translate(Point3d * const this, const Point3d &pt)
{
this -> x += pt.x;
this -> y += pt.y;
this -> z += pt.z;
}
3.构造函数
构造函数:
构造函数为特殊成员函数,和类同名,且没有返回值,构造函数可以重载。
如果没有为一个类显式定义任何构造函数,则编译器将自动为这个类生成默认构造函数,则此时类中的每个成员,使用与初始化变量相同的规则进行初始化。
1)类成员(如string):运行该类的默认构造函数进行初始化。
2)内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为0。
成员初始化列表:
在冒号和花括号之间的代码成为构造函数的初始化列表,它为类的一个或多个数据成员指定初始值。
在C++中,成员变量的初始化顺序与变量在类型中的声明顺序相同,而与他们在构造函数的初始化列表中的顺序无关。
从概念上讲,可以认为构造函数分为两个阶段:初始化阶段和普通计算阶段。
类类型的数据成员总是在初始化阶段进行初始化,如果不这样做而在函数体里初始化,则相当于先调用默认构造函数进行初始化,再在函数体里赋值,相比于直接利用初始化列表,效率较低。
必须使用成员初始化列表的几种情况如下:
没有默认构造函数的类型成员,const类型的成员变量和引用类型的成员变量,都必须在构造函数初始化列表中进行初始化。
复制构造函数:
赋值构造函数、赋值操作符和析构函数总称为复制函数,编译器自动实现这些操作,但类也可以定制自己的版本。如果类需要析构函数,则需要所有这三个复制控制成员。
实现复制控制版本最困难的部分,在于识别何时需要覆盖默认版本。当类具有指针成员时,类需要定义自己的复制控制成员。
只有单个形参,而且该形参是对本类类型对象的引用,(常用const修饰)这样的构造函数称为复制构造函数(或称拷贝构造函数),复制构造函数可由编译器隐式调用。
复制构造函数用于:
1)根据另一个同类型的对象初始化一个对象。
C++支持两种初始化形式:
直接初始化(如 string dots(10, ‘.’);),将初始化值放在圆括号中。
复制初始化,使用=号,复制初始化的形式有两种
a) string null_book1("9-999-99999-9"); //直接初始化
string null_book2 = null_book1; //复制初始化
或
string null_book2(null_book1); //复制初始化
b)string null_book3 = "9-999-99999-9"
2)复制一个对象,将它作为实参传给一个对象或者从函数返回时复制一个对象。
当参数或返回值为类类型时,将由复制构造函数进行复制
3)初始化顺序容器中的元素
容器的这种构造方式使用默认构造函数和复制构造函数,如:
Vector<string> svec(5);
编译器首先使用strin默认构造函数创建一个临时值来初始化svec,然后使用复制构造函数将临时值复制到 svec的每个元素。
4)根据元素初始化列表初始化数组元素
需要注意的是,如拷贝构造函数中的参数不是一个引用,即形如:
CClass(const CClass c_class)
就相当于采用了传值方式,而传值方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数,因此拷贝构造函数的参数必须是一个引用。
深复制与浅复制:
浅复制就是被复制对象的所有对象都含有与原来对象相等的值,而所有其他对象的引用仍然指向原来的对象,换言之,浅复制仅仅复制所考虑的对象,被复制的对象的所有对象都含有与原来对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的哪些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
4.析构函数
析构函数可用于释放对象构造时或在对象生命周期中所获得的资源,不管类是否定义了自己的构造函数,编译器都自动执行非static数据成员的析构函数。
当一个类中包含着另外一个类时,被包含的类在包含他们的类对象(宿主对象)被构造之前构造。即:由内向外构造。在调用析构函数时,按对象构造的逆序析构。
5.构造函数与析构函数调用顺序
单继承:
派生时,构造函数和析构函数是不能继承的,为了对基类成员进行初始化,必须对派生类重新定义构造函数和析构函数,并且在构造函数初始化列表中调用基类构造函数。
派生类构造函数的一般格式为:
派生类名(总参数表):基类构造函数(参数表)
{
//函数体
};
必须将基类的构造函数放在派生类的初始化列表中,已调用构造函数完成基类数据成员初始化。派生类构造函数的实现顺序为:
1)完成对象所占整块内存开辟,由系统调用构造函数时自动完成。
2)调用基类构造函数完成基类成员初始化。
3)若派生类中含有对象成员、const成员或引用成员,则必须在初始化列表中进行初始化。
4)派生类构造函数体执行。
当对象被删除时,派生类析构函数被执行,析构函数不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序是先执行派生类析构函数,再执行基类析构函数,这和执行构造函数时的顺序正好相反。
多继承:
多继承时,派生类构造函数的初始化列表需要调用各个基类的构造函数。
此时构造函数初始化列表只能控制用于初始化基类的值,不能控制基类的构造次序,基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
析构函数的顺序依然是按照构造函数运行的逆序执行。
虚继承:
首先调用虚基类的构造函数,虚基类如果有多个,则虚基类构造函数的调用顺序是此虚基类在当前类派生表中出现的顺序而不是他们在成员初始化表中出现的顺序。
所有虚基类构造函数调用完毕后,然后按照多继承的规则调用其他的构造函数。
5.操作符重载
操作符重载的名字为 operator后跟着所定义的操作符的符号。想其他任何函数一样,操作符重载函数有一个返回值和一个形参表,形参表必须具有与该操作符(操作数)数目相同的形参(如果操作符是一个类成员,则包含隐式this指针)。比如复制是二元运算,所以该操作符函数有两个形参:第一个形参对应左操作数,第二个形参对应右操作数。
大多数操作符可以定义为成员函数和非成员函数。当操作符为成员函数时,他的第一个操作数隐式绑定到this指针,有些操作符(包括赋值操作符)必须是类的成员函数。比如赋值操作符必须是类的成员函数,所以this指针绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。
不能重载的操作符有: . .* ?: ::
赋值操作符重载:
与复制构造函数一样,如果类没有定义自己的赋值运算符,则编译器会合成一个。
复制操作符重载需要关注一下几点:
1)需要把返回值类型声明为该类型的引用,并在函数结束前返回实例自身(即*this)的引用,只有这样,才可以允许连续赋值。
2)需要把传入从参数的类型声明为常量引用。
3)需要释放实例自身的内存。
4)需要判断传入的参数是不是和当前实例是同一个实例,如果是同一个,则不进行赋值操作,直接返回。
输出操作符 << 的重载:
输出操作符应接受ostream&的作为第一个形参,对类类型const对象的引用作为第二个形参,并返回对ostream的引用。
重载输出操作符的简单定义如下:
Ostream & operator << (ostream os, const ,const ClassType &object)
{
Os << // ...
Return os;
}
第一个形参是对os对象的引用,在该对象上产生输出。
第二个形参一般是对要输出的类类型的引用,返回类型使对一个ostream引用,他的值通常是输出操作符所操作的ostream对象。
Operator new 与operator delete的重载
New的指向过程为:
1)调用名为operator new 的标准库函数,分配足够大的原始的未初始化的内存,以保存指定类型的一个对象。
2)运行该类型的一个构造函数,用指定初始化式构造对象。
3)返回指向新分配并构造的对象的指针。
Delete的执行过为:
1)对sp指定的对象运行适当的析构函数。
2)调用名为operator delete 的标准库函数释放该对象所用内存。
New(即new operator)和delete 我们不能定义其表达式的行为(重载), 但是它们的内部实现operator new和operator delet 是可以被重载的,
Operator new的特点是:
1)只分配所要求的空间,不调用相关对象的构造函数。
2)可以被重载;
3)重载时,返回类型必须为 void*;
4)重载时,第一个参数必须为表达要分配空间的大小(字节),类型为size_t;
5)重载时,可以带其他参数。
将构造函数或析构函数设为私有,可以限制栈对象生成。
将operator new设为私有,可限制堆对象生成。
6.成员函数的承载、覆盖与隐藏
成员函数重载的特征:
1)相同的范围(在同一个类中);
2)相同的函数名字;
3)不同的参数列表;
4)Virtual 关键字可有可无;
另外,成员函数重载不能出现函数函数参数的个数和类型均相同,仅仅依靠返回值不同来区分的函数,因为这和普通函数的重载是完全一致的。
成员函数的覆盖特征:
1)不同的范围(分别位于派生类与基类);
2)相同的函数名字;
3)相同的参数;
4)基类函数必须有virtual关键字。
重载与覆盖的区别如下:
1)覆盖是子类和父类之间的关系,是垂直关系;重载是一个类中不同方法之间的关系,是水平;
2)覆盖要求参数列表相同,重载要求参数列表不同;覆盖要求返回类型相同,重载则不要求;
3)覆盖关系中,调用方法体是根据对象的类型来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的。
成员函数的隐藏:
隐藏是指在某些情况下,派生类中的函数屏蔽了基类的同名函数,这些情况包括:
1)两个函数参数相2同,但基类函数不是虚函数,和覆盖的区别在于基类函数是否是虚函数。
2)两个函数参数不同,无论基类函数是否是虚函数,基类函数都会被覆盖,和重载的区别在于两个函数不在同一类中。