黑马程序员——自学c++2

来源:互联网 发布:淘宝二手电脑店铺推荐 编辑:程序博客网 时间:2024/05/16 15:23

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

1.赋值运算符的重载
步骤:
1)判断是不是自赋值,如果是的话,则什么操作都不做,直接跳到第四步。
2)如果不是的话,释放掉原来的空间,在堆上开辟一片新的空间。
3)实现参数内容到新分配空间的拷贝。
4)返回*this,实现链式调用。
2.格式(以Account类为例):const Accoutn&  operator=(const Account& account)
其中const是为了防止(c1=c2)=c3,因为这样赋值是没有意义的。
为引用是因为:对于简单类型来说int  a;int b ;a = b以后,返回值也是a,是它本身
3.三大件:
拷贝构造函数
赋值运算符的重载
析构函数

当类的数据成员包含指针时,并且指针指向堆上的空间。需要写这三大件
4.重载++,—
分为前置和后置,为了区分这两种版本,需要再后置的情况下带一个参数,这个参数没有什么实质的意义,只是为了区分
前置++(以Fraction为例)
Fraction & operator++();
分析:因为前置++允许这种操作:++++c,所以返回值不能是const,又因为++操作改变的是c本身,所以返回值要是引用。
后置++
const Fraction operator++(int)
分析:因为后置++不允许这个操作:c++++,所以返回值要是const,因为后置++返回值是一个局部变量,所以返回值不能为引用。


1.拷贝构造函数
1)作用:利用已存在的对象来创建一个新的对象。
2)调用拷贝构造函数的情况
(1)一个对象需要通过另一个对象来初始化。
        a)CExample B=A;
        b)CExample B(A);
        c)CExample A;
               CExample *p = new CExample(A);
(2)对象以值传递的方式传入函数的参数
(3)对象以值返回的方式从函数返回
3)构造函数的形式
Vector(const Vector& vector);
其中参数必须是一个类对象的引用:如果参数以值传递的方式传入拷贝构造函数中,则会再次调用拷贝构造函数,这样就会形成一个死循环。
2.析构函数
1)如果一个类种没有包含一个析构函数,编译器将提供一个默认的析构函数。如果类中的数据成员有指针类型,则编译器将不会释放掉指针指向的堆空间,从而造成内存泄露。这种情况下,我们必须要定义一个析构函数来释放指针指向的堆空间。
2)当我们想深拷贝的时候,赋值运算符重载,拷贝构造函数,析构函数都是必须要实现的。


1.类型转化与构造函数
1)当我们需要一个A类对象作为参数的时候,可是给提供的却是一个T类型的变量,这是编译器会在A类中寻找有没有一个参数类型是T类型的,且参数个数是一个,或者是有多个参数但是参数是带有默认值的构造函数,如果有的话则调用这个构造函数将T类型的变量转换成为A类的一个对象。如果在这个构造函数的前面加一个关键字explicit,则不会实现这个过程。
2)一个例子:
char* a=“first string”;
print(a);print()函数要接受一个String类型的对象,在String类中有一个构造函数的参数是一个char*类型的。
编译器发现a不是String类的对象,会调用这个类型转化构造函数,创建一个temp临时对象,然后调用print函数,最后再将这个temp临时变量释放掉。
3)又是一个例子
String b =“bad initialization”;
编译器会将上面的一句话转换成:String b(”bad initialization”);
2.对象数组
String ss[5];调用默认构造函数
String* sss= new String();调用默认构造函数
String*ssss = new String[10];调用默认构造函数在堆上分配十个对象大小的空间
String ss[5] = {String(),String(),String(),String(),String()};显示的进行初始化
对象数组没有初始化会调用默认的构造函数惊醒初始化。
3.继承
派生类继承了基类的所有的成员函数,除了构造函数,析构函数,赋值运算符重载的函数

父类的私有成员,在派生类中也是私有成员,但是在派生类中是不可以访问的。

派生类可以定义和基类一模一样的成员,这样派生类中定义的成员函数将覆盖从基类中继承来的成员函数(override)

如果不写继承方式的话,默认的是private的继承方式

private:只能在本类中访问
protected:在本类中和子类中都可以访问
public:在本类中,子类中,类的外面中都可以访问

1.继承的使用
1)通过继承可以构造一个类层次结构反映出这些类的关系,类所代表的身份在自然界中存在着天然的继承关系。如学生,大学生,研究生
2)实现代码的重用。
2.将派生类对象作为基类对象使用
当继承方式为公有继承的时候,派生类对象可以被当成基类对象使用。
1)可以将派生类对象赋值给基类的对象,但是不能将基类的对象赋值给派生类的对象。
2)基类的引用可以指向派生类对象。
3)基类的指针可以指向派生类的对象。
在以上2)3)两种情况下,当基类和派生类中定义了相同的成员函数,使用指向派生类对象的基类引用或者指针调用该成员函数时,表现的是基类中得版本。

当基类的指针或者是引用指向派生类的对象时,可以将基类的指针或者是引用强制转换成派生类指针或者是引用。

可以将派生类的对象强制转换成基类的对象,但是不能将基类对象强制转换成派生类的对象。

3.派生类的构造函数,析构函数,赋值运算符重载函数。
派生类通过继承是不能继承基类的构造函数,析构函数,赋值运算符重载函数的。所以在派生类中要重写这三种函数。

1)创建一个派生类的对象时,要先调用基类的构造函数,然后再调用派生类的构造函数,所以在派生类的构造函数中要指明要调用的基类的构造函数,如果没有指明的话,则要调用基类的默认的构造函数。
2)在释放派生类的对象的时候,要先调用派生类的析构函数,然后再调用基类的构造函数。

虚函数

1.当基类指针或引用指向派生类的对象,系统会认为它指向的是基类的对象(但实际上指向的是派生类的对象。)在基类中有func()函数,派生类中又覆盖了这个函数。当通过这个指针或引用来访问func()函数时,调用的基类中的func()函数,而不是调用派生类中的func()函数。
2.我们想要的是:基类指针或引用指向一个派生类对象时,通过基类指针或引用调用这个函数时,能通过实际类型而不是声明类型来判断调用哪个函数。
3.为了实现这个目的,我们在基类中的func()函数前面加virtual关键字。这样当通过基类的指针或引用调用这个函数时,就能通过指针所指向对象的类型来判断

4.在一个类中通过指针来删除指针指向的对象(这个对象是分配在堆上的,需要用delete来释放内存)的时候,基类的析构函数会被调用,但是这个指针有可能指向一个派生类的对象,删除时j,将只调用基类的析构函数,而不会调用派生类的析构函数,这样就有可能总成内存的泄露。
(不调用派生类的析构函数的原因是:编译器将这个指针指向的对象看成是基类的对象来处理。但是这个对象实际上可能是一个派生类对象。)

5.为了解决这个问题,我们需要将基类的析构函数声明为virtual。这样delete p时会判断p所指向的对象的类型,然后在释放空间。这样就能调用到派生类的析构函数了。

6.当通过基类对象的引用或指针来调用虚函数时,哪个类中(基类还是派生类)函数被调用取决于程序运行时所指向对象的真正类型。这被称为动态绑定。

7.多态需要的条件
1)有继承
2)函数的重写(函数的覆盖)
3)要有虚函数
4)基类的指针或引用指向派生类的对象。

8.抽象类与纯虚函数
1)纯虚函数:只有函数声明,没有函数体的虚函数,并且在函数定义的最后加上=0。如:virtual void func() = 0;
2)抽象类:至少有一个纯虚函数的类。
3)不能用抽象类实例化一个对象,但是可以声明一个抽象类的指针或者引用指向派生类的对象。
4)如果在派生类中没有实现基类中全部的纯虚函数,则没有实现的纯虚函数会被继承到派生类中,还是纯虚函数,派生类是抽象类。只有实现了基类中所有的纯虚函数以后,派生类才不是抽象类,才能创建一个对象。                                                                                                                                                                                                                                                                                                      
异常处理
将可能产生异常的代码放在try块中
异常可能在try块中的语句中产生,或者在try块中调用的函数中产生,或者是在try块中调用的函数中调用的其他函数中产生异常。
想一下,异常可能会在代码中产生。在异常发生之前,通过用测试(经常是条件表达式)来判断异常是否会发生。如果符合异常发生的条件,则用throw语句将异常抛出。例如,下面的例子中,有一个条件判断除数是否为0,如果为0,则抛出一个异常。
在异常抛出后,程序流返回给调用者。如果异常在try块中产生了,则在try块中产生异常代码之后的代码将不会被执行。通常,在try块后面经常跟着catch块。catch块是一个异常处理者。
参数的名字可以被省略。因此,catch需要根据给定的类型来捕获异常。但是它不能使用从throw语句中抛出的信息(有问题)。如果在catch块中的类型匹配抛出的表达式,则catch块开始执行。在一个try块后可能存在许多个catch块。程序寻找到匹配的一个catch块,执行匹配的catch块里面的内容。catch块执行完之后,如果程序还没有被终止的话,则继续执行后面的代码。如果没有找到匹配的catch块,程序跳到上层的try-catch块中(如果有的话)。如果没有匹配的的,那么程序将被终止。
注意,catch块不能访问定义在try块中的局部变量,因为执行catch块的时候,try块已经执行完了。
异常处理机制将程序处理代码从源代码的执行序列中分离出来。异常处理代码,也就是catch块,通常放在一个函数中。这个函数不同于异常产生的函数。如果一个异常能直接在其产生的作用域中直接进行处理,这种机制是不推荐的,因为它不被设计来优化性能。

一个异常可以在一个catch块中被捕获,然后可以再次抛给顶层的catch块。或者在catch块中可能产生一个新的异常对象被顶层的try块捕获…
An exception can be caught in a catch block, and throws again to an upper level catch block.  Or, the catch block may throw a new exception object to the next nesting try level. 

这里有一种catch块可以捕获所有类型的异常

        catch( ... )                // Use three dots in the parameter list使用三个点
        {
                // ...
        }
因为此种catch块没有参数,捕获的异常对象将不能被使用。
如果这里还有比这个更“常用”的catch块,它们必须放在这个catch块之前才能产生作用
抛出的异常可能是一个类的对象。如果这个类派生自一个基类,那么就可以把其基类对象作为catch块的参数就可以捕获这个异常。
如果这里有一个以派生类对象作为参数的catch块,则这个catch块必须放置在以基类对象作为参数的catch块之前才会产生作用。


想一下,在一个函数正调用另外一个函数的时候,在那个函数中抛出一个异常。调用者终止,局部变量被销毁。尤其是,如果这里有一个类的对象,将会调用此类的析构函数。如果局部对象是一个指针变量,则会产生内存泄露。
为了避免这种情况的内存泄露,这个指针需要抛给调用者,调用者负责对内存进行释放。这可以通过在类中封装指向错误信息的指针来实现,最后抛出这个类的对象即可。为了避免发生拷贝,catch块中需要是这个对象的引用类型。

这也可以通过在调用者中通过捕获异常来做到,并且把异常抛给上一级。

如果异常在整个程序中都没有找到任何能与之匹配的catch块,程序将会调用terminate()函数,然后terminate()函数将会调用abort()函数来终止程序。这里有两个函数被调用的原因是,在调用abort()函数之前,你可能会让terminate()函数调用用户自定义的一些函数。这通过调用定义在<eh.h>头文件中的set_terminate()函数来实现。函数set_terminate()接收
void(*)(),也就是指向返回值类型为void,没有参数的函数的函数指针作为参数。实际参数是用户自定义函数的名字,此函数指定了,如发生未处理异常程序该如何做。
在这个try块中,函数divide()抛出一个异常。这个异常没有被捕获。随后terminate()函数被调用。terminate()函数首先调用my_terminate(),然后调用abort()函数结束程序

一个函数可以通过异常表达式指定函数抛出异常的特定类型。
这个函数可能抛出一个int类型或者一个Error_message对象类型的异常。如果一个函数期望不抛出任何类型异常,可以用throw()来指定。
有指定抛出的特定类型,则函数可能会抛出任何类型的异常。

指定特定类型的异常通常被用来添加一层错误类型检查。如果一个被抛出的异常不是指定特定类型的异常,则unexpected()函数会被自动调用。这个函数默认调用terminate()函数。或者调用用户自定义的set_unexpected()函数。

模板
1.函数模板
函数模板是一样通用的类型,函数的参数可以是任意的类型,判断是什么样的类型是根据传进来的参数类型判断的。
#include <iostream>
using namespace std;

template<class T>
void printArray(T *p, int size);

int main(int argc, const char * argv[])
{
    const int max = 5;
    int iArray[max] = {1, 2, 3, 4, 5};
    float fArray[max] = {1.1f, 2.2f, 3.3f, 4.4f, 5.5f};
    char* cArray[max] = {"one", "two",  "three", "four", "five"};
    
    printArray(iArray, max);
        printArray(fArray, max);
    printArray(cArray, max);
        
    return 0;
}

template<class T>
void printArray(T *p, int size)
{
    for(int i = 0; i < size; i++)
    {
        cout << p[i] << ", ";
    }
    cout << endl;
}

2.类模板是一种通用类,这种通用的类中只是成员变量的类型是不同的。
1)在类的声明时:需要在类声明前添加template<class T>;这里T代表是一种类型,可以是普通的变量也可以是自己定义的类。
2)在实现类中的函数的时候,在每个函数的前面都要写上template<class T>;
3)在使用类的时候应该这样使用:(如Stack类)Stack<int> stack;这里stack是对象的名称。<>中的int用来指定T的类型。
4)模板的参数可以带默认值如:template<class T = int>;说明如果没有指定T的类型,则默认为int,如果指定类型了,则为指定的类型。需要注意的是:在实现成员函数时,模板中是不能带默认值的。
使用时如下:Stack<> stack;这是默认的T的类型为int类型。
5)模板还可以拥有指定类型的参数被作为常来使用,这个指定类型的参数的值可以在对象定义的时候指定,也可以带有默认值。当创建对象时没有指定值时,则使用默认的值。如:template<class T, int max_size = 10>; 使用时:Stack<int> stackl这样则使用默认值10;Stack<int ,20> stack;这样则使用指定的值20

0 0
原创粉丝点击