概述:
设计良好的类类型可以向内置类型一样容易使用 0.0
这个部分会比较枯燥,都是些基本的概念理解 T.T
类对其成员的访问形式主要有以下两种:
1)内部访问:在类中的创元函数对类的成员的访问。
2)对象访问:在类外部,通过类的对象对类的成员的访问。
类的成员可以有public、protected、private三种访问属性,类的成员函数( 内部访问 )以及友元函数可以访问类中所有成员,但是在类外通过类的对象( 对象访问 ),就只能访问该类的共有成员。
8.2.1 成员函数
调用成员函数实际上是使用对象来调用的。每个成员函数(除了static成员函数外)都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址。
8.2.2 构造函数
构造函数是特殊的成员函数,和类同名,没有返回类型,可以有多个构造函数,每个构造函数必须由于其他构造函数不同数目或类型的形参。
若使用编译器自动生成的默认构造函数则:
1)类成员:运行该类型的默认构造函数来初始化。
2)内置或复合类型的成员的初始化值依赖于对象的作用域:在局部作用域中这些成员不能被初始化,而在全局作用于中它们被初始化为0。
在C++中, 成员变量的初始化顺序与变量在类型中的声明顺序相同,而与它们在构造函数的初始化列表中的顺序无关 :
class A { private: int i; int j; public: A() : j(0), i(j + 2) {} void print() { cout<<"i : "<<i<<" , j : "<<j<<endl; } }; int main(void) { A a; a.print(); return 0; }
因此上面这个例子,会首先初始化i,而初始化i的参数j还没有初始化,是一个垃圾值,因此i就是一个垃圾值。初始化j时,根据参数0对其初始化,故j = 0。
有些数据成员 必须在构造函数初始化列表中进行初始化 :没有默认构造函数的类类型的成员,以及const类型的成员变量和引用类型的成员变量。
复制构造函数(拷贝构造函数)
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
定义:只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰)。
复制构造函数的作用:
1)根据另一个同类型的对象初始化一个对象。
2)复制一个对象,将它作为实参传给一个函数或从函数返回时复制一个对象:
class Myclass { public: Myclass(int n) { number = n ; } Myclass(const Myclass &others) { number = other.number; count<<"a"; } private: int number; }; void fun(Myclass p) { Myclass temp(p); } int main(void) { Myclass obj1(10), obj2(0); Myclass obj3(obj1); fun(obj3); return 0; }
上述代码中,调用了三次复制构造函数,第一次是main中的Myclass obj3(obj1),第二次是实参obj3到fun形参p,第三次是函数fun中的"Myclass temp(p);"语句。
3)初始化顺序容器中的元素。
4)根据元素初始化列表初始化数组元素。
8.2.3 析构函数
析构函数可用于释放对象构造时或在对象的生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
看一个例子,以下代码输出的结果是什么:
class A { public : A(){cout << "A";} ~A(){cout<<"~A";} }; class B{ public: B(A &a) : _a(a) { // _a(a)调用了复制构造函数 cout<<"B"; } ~B(){cout<<"~B";} private: A _a; }; int main(){ A a; // 在定义对象的时候,自动调用构造函数 B b(a); // 输出B之前调用A的复制构造函数 return 0; }
输出:AB~B~A~A。由内向外构造,按对象的构造的逆序析构。对于复合对象,先析构主对象,再析构其包含的对象。构造过程是:A A B,那么析构必然是对称的 B A A。而复制构造函数采用的是系统自动生成的版本,没有输出。
8.2.4 构造函数与析构函数的调用顺序
派生时,构造函数和析构函数是不能继承的,为了对基类成员进行初始化,必须对派生类重新定义构造函数和析构函数,并在构造函数的初始化列表中调用基类的构造函数。
派生类构造函数的一般格式为:
派生类名 (总参数表):基类构造函数(参数表) { // 函数体 };
必须将基类的构造函数放在派生类的初始化列表中,以调用基类构造函数完成基类数据成员的初始化(若无,则调用基类默认构造函数),派生类构造函数的调用顺序为:
1)完成对象所占整块内存的开辟,由系统在调用构造函数时自动生成。
2)调用基类的构造函数完成基类成员的初始化。
3)若派生类中含对象成员、const成员或引用成员,则必须在初始化表中完成其初始化。
4)派生类构造函数体执行。
当对象被删除时,派生类的析构函数被执行。析构函数不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数,这和执行构造函数时的顺序正好相反。
看下面的题,注意和上面题的区别,这里有继承:
class A { public : A(){cout << "A";} ~A(){cout<<"~A";} }; class B : public A{ // 单继承 public: B(A &a) :_a(a) { cout<<"B"; } ~B(){cout<<"~B";} private: A _a; }; int main(){ A a; B b(a); return 0; }
输出为:AAB~B~A~A~A。
多继承时,派生类的构造函数初始化列表需要调用各个基类的构造函数。
注意:此时构造函数初始化列表只能控制用于初始化基类的值,不能控制基类的构造次序。基类的构造函数按照基类构造函数在派生列表中的出现次序调用。
首先调用虚基类的构造函数,虚基类如果有多个,则虚基类构造函数的调动顺序是此虚基类在当前派生类中出现的顺序而不是他们在成员初始化表中的顺序。
8.2.5 操作符重载
操作符重载的函数的名字为operator后跟着所定义的操作符的符号。
注:由四个不能重载的操作符:"::"、".*"、"."、"?:",简单记忆,带"点"的都不能 ^_^
赋值操作符重载
需要注意的地方:
1)把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身(即*this)的引用。
2)传入的参数的类型声明为常量引用。
3)记得释放实例自身已有的内存。
4)判断传入的参数是不是和当前的实例(*this)是否为同一个实例。
下面是一个例子:
// 声明 class CMyString{ public: // 省略构造函数和析构... CMyString& CMyString::operator =(const CMyString &str); private: char* m_pData; } //补充后的内容: CMyString& CMyString::operator =(const CMyString &str) { if(this == &str) return *this; delete []m_pData; m_pData = NULL; m_pData = new char[strlen(str.m_pData)+1]; strcpy(m_pData, str.m_pData); return *this; }
输出操作符<<重载
输出操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回对ostream形参的引用,简单的定义如下:
ostream& operator <<(ostream& os, const classtype &object) { os<<//..... return os; }
operator new 与 operator delete的重载
不能重定义new和delete表达式的行为,能够被重载的是全局函数operator new和operator delete。
new的执行过程:首先,调用名为operator new的标准库函数,分配足够大的原始未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。
delete的执行过程:首先,对其所指向的对象运行适当的析构函数;然后,通过调用名为operator delete的标准库函数释放该对象的内存。
如下面的例子:
class X { public: X() {cout<<"constructor "}; static void* operator new(size_t size) { cout<<"new "; return ::operator new(size); } static void operator delete(void* pointee) { cout<<"delete "; ::operator delete(pointee); } ~X() {cout<<"destructor "}; }; int main(void) { x* px = new X(); delete px; return 0; }
输出为"new constructor destructor delete"。
8.3.1 重载
1)相同的范围(在同一个类中);
2)相同的函数名字;
3)不同的参数列表;
4)virtual关键字可有可无。
8.3.2 覆盖
派生类中覆盖基类中同名函数,要求基类必须是虚函数,且:
1)与基类的虚函数有相同的参数个数;
2)与基类的虚函数有相同的参数类型;
3)与基类的虚函数有相同的返回类型:或者与基类虚函数的相同,或者都返回指针(或引用),并且派生类虚函数所返回的指针(或引用)类型是基类中被替换的虚函数所返回的指针(或引用)类型的子类型(派生类型)。
如下,B中的fun1覆盖了A中的fun1:
class A { public: virtual void fun1(int, int) {} }; class B : public A { public: void fun1(int, int)() };
注意:覆盖方法中,调用方法体是根据对象的类型来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的。
8.3.3 隐藏
隐藏是指在某写特殊情况下,派生类中的函数屏蔽了基类中的同名函数,包括:
1)两个函数参数相同,但基类不是虚函数。
2)两个函数参数不同,无论基类函数是否为虚函数,基类函数都会被屏蔽。