【C++笔记】《C++编程思想-卷二》笔记

《C++编程思想》笔记

 

Volume 2

 

第一章 异常处理

增强错误恢复能力是提高代码健壮性的最有力的途径之一。

将try块嵌套在for、while、do或者 if快中,并且触发异常来试图解决问题,然后重新测试try块中的代码。

1.5 清理

(1).资源管理

异常处理的魅力之一在于程序能够从正常的处理流程中跳转到恰当的异常处理器中。如果异常抛出时,程序不能做恰当的清理工作,那么异常本身并没有什么用处。编写代码终于到异常时,就应该要注意一旦异常发生,那么资源是否被正确清理了。大多数情况不用担心,但是,如果在一个对象的构造函数中抛出异常时就要特别注意,因为对象的构造函数中一旦抛出异常,那么这个对象的析构函数就不会被调用,从而,在构造函数中分配的资源就不会被释放,这样就发生了内从泄露,同样,经常会伴随着“悬空指针”(naked pointer)。为了防止资源泄露,可以使用以下两种方式来资源分配:

(a).在构造函数中来捕获异常,用于释放资源。

(b).在对象的构造函数中分配资源,并且在对象的析构函数中释放资源。

(2)使所有事物都成为对象

资源获得初始化技术:使资源分配成为局部对象生命周期的一部分(如通过模版(在模板中包含了对象的动态创建,并在模板中设置成员指针指向创建的对象),使得对象资源的分配成为局部生命周期),那么如果某次资源分配失败了,那么在栈反解析的时候、其他已经获得所需资源的对象能够被恰当的清理。

(3) auto_ptr

在一个典型的C++程序中动态分配内从是频繁使用的资源,所以,C++标准中提供了一个RAII封装类,用于指向分配的堆内存的指针,这就使得程序能够自动释放这些内存。Auto_ptr类模板定义于头文件<memory>中。使用auto_ptr不需要显示地删除原始指针,因为在栈反解的时候auto_ptr的对象会自动的删除原始指针。由于,通过值引用的类对象总会被析构,所以当对象被析构时,这个对象的auto_ptr成员总是能释放它所封转的原始指针,所以,auto_ptr类模版可以很容易用于指针数据成员。

(4)标准异常

       所有的标准异常类都是从exception类派生而来,它主要派生出类:logic_error和runtime_error类,logic_error用于描述程序中出现的逻辑错误,如:传递无效的参数。运行时错误是指那些无法预料的事件所造成的错误,如:硬件故障、内存耗尽。Logic_error和runtime_erroe提供了一个std::string的构造函数,这样就可以将消息保存在这两种类型的异常对象中,通过exception::what()函数,读者可以从对象中得到它所保存的消息。而std::exception却没有提供这样的构造函数,所以,用户最好从runtime_error和logic_error来派生自己的异常,而不要直接从std::exception类派生。

       输入、输出流iOS::failure也是从exception派生而来,但是,它没有子类。我们一般使用下面所列的异常类,或者把他们作为基类来派生自己的更加具体的异常类:

      

从logic_error派生而来的异常类

Domain_error :报告违反了前置条件

Invalid_argument: 表明抛出这个异常的函数接收到了一个无效的参数

Out_of_range :报告一个参数越界错误

Bad_cast: 抛出这个异常的原因是运行时类型识别(runtime type identification)中发现程序执行了一个无效的动态类型转化(dynamic_cast)表达式.

Bad_typeid: 当表达式typeid(*p)中的参数p是一个空指针是抛出这个异常(这个也是运行时类型识别的特性)

  

从runtime_error派生的异常类

Range_error:报违反了后置条件

Overflow_error:算数溢出错误

Bad_alloc:报告一个失败的存储分配

(5)、异常规格说明.

异常规格说明,它是函数声明的修饰符,写在参数列表的后面。它再次使用关键字throw,函数可能抛出的所有异常的类型应该被写在throw之后的括号里面。如下所示:

Void f() throw(toobig,toosmall,divzero);这个函数可能会抛出的三种异常。当函数抛出异常时最好使用异常规格说明。如果,函数所抛出的异常没有列在异常规格说明的异常集中,那么默认的函数unexpected()会被调用,默认的unexpected()会调用。会调用terminate()函数。

Set_unexpected()函数:同terminate()一样可以使用这个set_unexpected()函数来设置用户自己的意外的异常。编程人员必须包含<expection>头文件。

(6) 异常规格说明和继承

       由于异常规格说明在逻辑上也是函数声明的一部分,所以在继承层次结构上也必须保持一致(函数的返回值可以发生协变,但参数不可以,参数必须一致)。

(7)什么时候不用异常规格说明

       当无法知道会发生什么异常时,不要用异常规格说明,例如:模版函数或者模版类。

1.9在编程中的异常使用指南

(1)什么时候避免使用异常

       只有当函数不符合它的规格说明的时候才抛出异常。

1、  不要再异步事件中使用异常

由于异常和它的处理器必须处于相同的函数调用栈上,而异步事件必须由完全独立的代

码来处理,这些代码不是正常流程中的一部分(典型的例子是中断服务程序和事件循环),所以,无法使用C++中的异常来处理异步事件,所以,不要在中断处理程序中抛出异常。所以,处理这种异常的典型方法是,中断处理程序设置一个标记,程序的主干代码同步的检查这个标记。

2、  不要在处理简单错误是使用异常

如果能够得到足够的信息来处理错误,那么就不要使用异常。程序员应该在当前语境中处理这个错误,而不是将一个异常抛出到更大的上一层的语境中。

此外,C++在遇到机器层时间如除零错误时,不会抛出异常。读者可以认为其他一些机制,如操作系统或者硬件会处理这种事件。这样异常处理可以专注的处理程序级的异常。

3、  不要讲异常用于程序的流程控制

4、  不要强迫自己使用异常

某些程序是相当简单的,显示一个消息然后退出程序就可以了,最好把清理工作交给操作系统,而不必费尽地捕获所有异常并释放所有资源。简单地说如果读者不需要异常,就不要强迫自己使用它们。

5、  新异常,老代码

在老代码中,添加了出抛出新的异常的代码。那么可以在使用新代码的覆盖范围最大的代码放入try块中(可能是整个main函数)。然后追加一个catch(…)。或者更好的办法是将发生异常的代码隔离在try块中。

(2) 什么时候使用异常

在下列情况下使用异常:

修正错误并重新调试产生异常的函数

在重新调试中的函数外面补偿一些行为以便使程序得以继续执行。

在当前语境中做尽可能多的事,并把同样类型的异常重新抛出到更高的层次的语境中。

在当前语境中做尽可能多的事,并把一个不同类型的异常抛出到更高层次的语境中去。

终止程序

将使用不同错误处理模式的函数(尤其是C库函数)封装起来,以便使用异常来代替原有的错误处理模式。

简化。如果建立的错误处理模式使事情变得复杂并且难以使用,那么异常可以使错误处理更加简单有效得多。

使建立的库和程序更加安全。使用异常既是一种短期投资(方便调试)优势一种长期投资(系统健壮)。

1、  什么时候使用异常规格

异常规格说明就想函数原型:具有提醒使用者的作用。因为总会出现无法预料的异常,

所以,如果要使用异常规格说明的话,最好编写自己的unexpected()函数,在这个函数中将异常消息记录到日志中,然后抛出异常或者终止。

2、  从标准异常开始

如果,系统的异常类能满足程序的要求,那么就是用系统的异常类。如果系统的异常类

不能满足程序的要求尽量从现有的标准异常类中继承出一个。尽量让用户可以使用what()函数接口。

3、  嵌套用户自己的异常

如果为用户自己的特定类创建异常,最好在这个特定的类中或者包含这个特定类的命名

空间中嵌套异常类,这就为读者提供了一个明确的信息——这个异常类只在用户自己定义的特定类中使用,也防止污染全局名字空间。即使这个异常类是从标准异常类中继承而来。

4、  使用异常层次结构

异常层次结构为用户的类或库可能遇到的不同类型的重要错误提供一个有价值的分类

方法。

5、多重继承(MI)

       唯一必须要用到多重继承的情况是:当需要将一个对象的指针向上类型转换成两个不同的基类类型时——也就是说,读者同时需要这两个基类的多态行为。多态继承的异常类的任何一个基类的异常处理都能处理这个异常。

5、  通过引用而不是通过值来捕获异常

好处:a)当异常对象被传递到异常处理器中的时候,避免进行不必要的对象拷贝。

a)        当派生类对象被当做基类对象捕获时,避免对象切割。

b)       如果,采用捕获指针类型的异常,则会在代码中引进紧耦合——抛出异常的代码和捕获异常的代码必须协商好,内从由谁释放、有谁开辟,同时由于在堆内存耗尽的时候也可能会发生异常,所以,这也造成一个问题。如果程序抛出异常对象,一场系统负责处理所有与存储有关的问题。

6、  在构造函数中抛出异常

有两种方法来报告在构造对象期间发生的异常:

a)        设置非局部的标记,并且希望用户检查它

b)       返回一个未完成的创建对象,并且希望用户检查它。但是用户必须注意对象内部的指针和它的清理方式。

7、  不要再析构函数内部触发异常

析构函数会在抛出异常的其他过程中被调用,所以,绝不要再析构函数中抛出异常或者

在析构函数中执行其他可能触发抛出异常的操作。如果析构函数中抛出异常,这个新的异常可能会在现存的异常到达catch子句之前被抛出,这会导致程序程序调用terminate()函数。如果,在析构函数中调用的函数可能会抛出异常,应该在这个析构函数中编写一个try块,并把这些函数调用放到try块中,析构函数必须自己处理所有这些异常。绝对不能有任何一个异常从析构函数中抛出。

8、  避免悬挂指针

如果需要给指针分配资源,那么悬挂指针通常意味着构造函数的弱点。如果在构造函数

中抛出异常,因为指针没有析构函数,那么这些资源将无法释放。请使用auto_ptr或者其他智能指针来处理指向堆的内存。

1.10 使用异常造成的开销

一个throw表达式就像是一个特殊的系统函数调用,它接收异常对象作为参数并且沿着执行调用链向上回溯。为了完成这项工作,需要运行栈的支持。

 

 

 

 

 

 

 

 

 

第二章 对象的创建与使用

语言的翻译过程-解释器-编译器

 

头文件包含

C++中新定义的方法都是有名字空间的 比如cout就属于std名字空间 如果include头文件的时候加上.h,默认会usingnamespace 否则需要自己加上 using namespace XXX 对于C中已经定义的方法如printf,没有影响的
iostream.h是包含输入/输出流处理的头文件,iostream就什么都不是了 
但用iostream要加名词空间namespace
#include<iostream.h> 或者是 #include<iostream>  using namespace std; 二者都行
#include<iostream.h>是C语言中比较通用的 
#include<iostream>  using namespace std;  是C++中比较通用的

#include<cstdio>和#include<stdio.h>与上文同理

 

关于输入输出流:cout<<hex (dec &oct)<< 15 ;可以按照预定的进制改变输入和输出流的状态 。

 

文件的读写:

#include<fstream>

#include<string>

usingnamespacestd;

intmain()

{

    ifstream in("Scopy1.txt"); //如果文件不存在不会自己创建

    ofstreamout("Scopy2.txt");   //文件不存在会自己创建

    strings;

    while (getline(in, s)) //一次获得一行的字符从in->s

    {

        out<<s<<"\n";

    }

   return 0;

}

 

第三章 C++中的C

Switch case中可以用break,不能用continue

Break和continue只能用于循环语句中,用于加速中循环。

 

goto谨慎使用,建议只在跳出多层循环的地方使用。

Loop:**************

goto Loop;

 

 

C++中的引用

以引用传递允许一个函数去修改外部对象,就像传递一个指针所做的那样。

 

#include<iostream>

usingnamespacestd;

 

voidfunction(int &r)

{

    cout<<"r="<<r<<endl;

    cout<<"&r="<< &r<<endl;

    r = 5;

    cout<<"r="<<r<<endl;

    cout<<"&r="<< &r<<endl;

}

intmain()

{

    intx = 1;

    cout<<"x="<<x<<endl;

    cout<<"&x="<< &x<<endl;

    function(x);

    cout<<"x="<<x<<endl;

    cout<<"&x="<< &x<<endl;

    cin>>x;

    return 0;

}

 

 

void*类型的指针:(不建议常用void*类型的指针,void类型的引用更是禁止使用)

它意味着任何类型都可以间接引用那个指针,一旦间接引用了void*类型的指针,就意味丢失了原来的类型信息,所以使用时一定要(强制)类型转换为使用类型。

volatile变量:

告诉compiler不能做任何优化;

表示用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能把他放在cache或寄存器中重复使用。

用法和const一样。

 

sizeof()提供对有关数据分配的内存大小;

 

asm 嵌入汇编指令
  _asm是一个语句的分隔符。不能单独出现,必须接汇编指令。一组被大括号包含的指令或一对空括号。
例:
    _asm

    {

        movdx, 0xD007

        OUTal, dx

    }
也可以在每个汇编指令前加_asm
    _asm  mov al, 2

    _asm  mov dx, 0xD007

    _asm  outal, dx

简单的说就是在C++/C中使用ASM汇编语言。

 

字符串向数值的转换:

#include<cstdlib>

#include<iostream>

usingnamespacestd;

intmain( )

{

    inti;

    charbuffer[256];

    printf("Enter a number: ");

    fgets(buffer, 256, stdin);

    i = atoi(buffer);

    printf("The value entered is %d.", i);

    system("pause");

    return 0;

}

 

#include<cstdlib>

#include<iostream>

usingnamespacestd;

intmain()

{

    chara[] = "-100";

    charb[] = "123";

    intc;

    c = atoi(a) + atoi(b);

    printf("c=%d\n", c);

    system("pause");

    return 0;

}

输出:  c=23

 

变量和表达式转换成字符串方法:

#defineP(A) cout << #A<< ": "<< (A)<< endl;

在变量前面使用#可以吧后面的表示服转变成字符串输出;

 

C语言assert()宏:

预处理器产生测试断言的代码,如果断言为假,程序终止给出错误信息。

#include<cassert>

usingnamespacestd;

intmain()

{

    intaaa = 1000;

    assert(aaa == 100);

    return 0;

}

给出错误信息:Assertion failed: aaa == 100,file e:\vs2015\test\0\0\0.cpp, line 20

 

管理分段编译:make和makefile  请查阅相关资料

我们现在使用的C++编译器大都使用分段编译(separate compilation)的方法进行编译的。这种方法一般情况是一个可执行文件有很多的源代码文件编译而成。我们在编译他的时候先把这些文件编译成一个一个的obj文件,然后由link程序把他们连接成我们想要的那个唯一的可执行文件。但是我们为什么要把他编译成一个一个的obj文件然后在连接成一个可执行文件那,为什么不把所有的源代码放到一个文件里,然后在把它直接编译成可执行文件不是更简单吗?是的,那样更简单,但是当一个相当大的文件由很多的源代码组成,有很多的程序员对他进行修改,如果我们每个程序员都想对自己修改的文件进行执行查看,那么就是说我们在程序员查看即编译成可执行文件的时候,无论程序中的其他部分是否进行了修改,我们都要没有选择的将所有的源代码都重新编译一边。这样是不是工作效率太低了。而如果我们使用分段编译的话,我们只需要将改动的那部分代码所在的obj重新编译,其他的不用在编译,然后将他们连接在一起组成可执行文件,现在看来就高效了吧。有人会迷惑,编译器是怎么知道那个文件,那个模块被改动,需要重新编译的呢。问到这里就是主角makefile了,她就是分段编译的主角和灵魂。

 

第四章 数据抽象

库的使用是改进生产效率最重要的方式之一!

 

防止头文件重复包含的两种方法:

1.   windows平台下的宏 #pragma once

2.   条件编译语句

#ifndefsample_H_H

#definesample_H_H

typedefstructsample {

    inttrueClass;

    doublefeature[13];

}SAMPLE;

#endifsample_H_H

 

不要再头文件中放置“使用(usingnamespace ***)”指令。

 

 

第五章 隐藏实现

友元函数:

我们已经知道类具有封装特性,只有类的成员函数才可以访问私有变量。

友元函数是一种定义在类外部的普通函数,但是需要先在类体内声明,为了区别成员函数在前面加friend。友元不是成员函数,但是可以访问成员私有变量。提高了效率但是破坏了封装。

 

友元函数的特点是能够访问私有成员的非成员函数,语法上看和其他普通的函数没有区别,也就是说在定义和使用上和普通函数一样。例如:

#include<iostream>

usingnamespacestd;

 

classPoint

{

public:

    Point(doublexx, doubleyy) { x = xx; y = yy; }

    voidGetxy();

    frienddoubleDistance(Point &a, Point &b);

private:

    doublex, y;

};

 

voidPoint::Getxy()

{

    cout<<"("<<x<<","<<y<<")"<<endl;

}

 

doubleDistance(Point &a, Point &b) //非成员函数(友元函数)

{

    doubledx = a.x - b.x;

    doubledy = a.y - b.y;

    returnsqrt(dx*dx + dy*dy);

}

 

intmain()

{

    Pointp1(3.0, 4.0), p2(6.0, 8.0);

    p1.Getxy();

    p2.Getxy();

    doubled = Distance(p1, p2);

    cout<<"Distance is"<<d<<endl;

    cin>>d;

    return 0;

}

 

在该程式中的Point类中说明了一个友元函数Distance(),他在说明时前边加friend关键字,标识他不是成员函数,而是友元函数。 他的定义方法和普通函数定义相同,而不同于成员函数的定义,因为他无需指出所属的类。但是,他能够引用类中的私有成员,函数体中 a.x,b.x,a.y,b.y都是类的私有成员,他们是通过对象引用的。在调用友元函数时,也是同普通函数的调用相同,不要像成员函数那样调用。p1.Getxy()和p2.Getxy()这是成员函数的调用,要用对象来表示。而Distance(p1, p2)是友元函数的调用,他直接调用,无需对象表示,他的参数是对象。

友元类:

友元以是一个类,当一个类是另外一个类的友元时,这就意味着这个类的任何成员都是另一个类的友元函数。友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。定义友元类的语句格式如下:
      friend class 类名;
      其中:friend和class是关键字,类名必须是程序中的一个已定义过的类。
      例如,以下语句说明类B是类A的友元类:
      class A
      {
             …
      public:
             friendclass B;
             …
      };
      经过以上说明后,类B的所有成员函数都是类A的友元函数,能存取类A的私有成员和保护成员。

      使用友元类时注意:
            (1) 友元关系不能被继承。 
            (2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
            (3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

注意事项:

1.友元可以访问类的私有成员。

2.只能出现在类定义内部,友元声明可以在类中的任何地方,一般放在类定义的开始或结尾。

3.友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。

4.类必须将重载函数集中每一个希望设为友元的函数都声明为友元。

5.友元关系不能继承,基类的友元对派生类的成员没有特殊的访问权限。如果基类被授予友元关系,则只有基类具有特殊的访问权限。该基类的派生类不能访问授予友元关系的类。

 

 

第六章 初始化与清除

 

聚合初始化:

C++中可以在集合(数组、类、结构体)定义的时候,在{ }中指定初始值的方式,来初始化集合。是结构体以及类中,构造函数是同过的正式接口来强制初始化的,所以,构造函数必须通过被调用来进行初始化(类中也是一样的)。

 

聚合类是一种没有用户定义的构造函数,没有私有(private)和保护(protected)非静态数据成员,没有基类,没有虚函数。这样的类可以由封闭的大括号用逗号分隔开初始化列表。下列的代码在 C 和 C++ 具有相同的语法:
    structC

    {

        inta;

        doubleb;

    };

 

    structD

    {

        inta;

        doubleb;

        Cc;

    };

    // initialize an object of type C with aninitializer-list

    Cc = { 1, 2 };

    // D has a sub-aggregate of type C. In such casesinitializer-clauses can be nested

    Dd = { 10, 20,{ 1, 2 }};

如果一个类里面包含了用户自定义的构造函数,而又用{ xx, xx, ...}来初始化它的对象,编译器就会报错

聚合定义为:

·        数组

·        没有以下内容的类、结构和联合:

o    构造函数

o    私有或受保护的成员

o    基类

o    虚函数

编译器不允许在包含构造函数的聚合中使用数据类型。

 

第七章 函数重载与默认参数

 

Struct和Class唯一不同的地方在于,Struct默认为public,而Class默认为private。

 

使用默认参数必须遵守两条规则:只有参数列表的后部参数才可以是默认的,也就是说,不可以在一个默认参数后面又跟上一个非默认参数;一旦在一个函数调用中开始使用默认参数,那么这个参数后面所有参数都必须是默认的。

 

参数占位符: void function(int n, int)   第二个参数就是占位符,这样方便修改函数定义而满足增加的需求而不需要修改声明导致编译器报错,相当于是预留参数。

 

 

 

第八章 常量

因为预处理器会引入错误,所以我们应该完全用const取代#define的值替代。

       关键字const能将对象、函数参数、返回值和成员函数定义为常量,并能消除预处理器的值替代而不使预处理器的影响。

 

 

 

第九章 内联函数

 

宏名全部大写。

 

内联函数仅建议函数比较小的时候用。

第十章 名字控制

 

1、  静态元素

静态变量:静态变量存储于固定地址,而非栈式分配,在调用时,只有第一次调用才进

行初始化。

静态对象:同静态变量一致同样需要初始化操作。但是,编译器自动赋值(也称零赋值

只对内部类型有效,而对于用户自定义类型必须用构造函数来初始化。静态对象的构造函数在进入main函数体前执行。静态对象的析构函数会在main函数退出后执行。所以,利用这一点在main函数之前和之后执行一定的代码。

2、  名字空间

创建一个名字空间与创建一个类类似,但是,可以像重定义一个类一样,在多个文件中

用同一个标识符来定义一个同一个命名空间。

       同样有时为了方便可以用namespace关键来给一个名字起一个别名。

3、  koenig也称ADL,即参数相关查找

如果一个函数使用了一个命名空间的成员。那么在调用函数时,对该函数的查找范围会将这个成员所在的命名空间也包括进去。

4、  未命名的名字空间

每个翻译单元都可包含一个未命名的名字空间,但每个翻译单元只可以有一个未命名的

名字空间。在这个空间中的名字自动地在本翻译单元无限制地有效。

       可以通过三种方式来使用命名空间中的成员:1)、用作用域的方式 2)、用using namespace一次性的将一个命名空间中的所有成员都引入到当前翻译单元。3)、用using声明引用名字某一个指定的成员。

      Using 引入的成员的作用域的范围为使用using指令时位置的作用域的范围与命名空间的名字以及在哪里定义没有关系。

5、  C++中的静态成员

类的静态成员:为类的所有对象共同拥有的一个单一的固定存储空间。所以,存储空间

的定义必须是在一个单独的地方,而不是在类的内部,也只能是在类的外部的实现的地方。

类的静态数组:静态数组有常量和非常量两种。除整型静态常量成员可以在类中初始化外浮点型静态常量成员以及所有的静态数组以及静态常量数组都必须在类的外部进行初始化。

静态常量对象:可以在类中创建静态常量对象以及这样的对象的数组。不过不能在本类中用内联的形式初始化这些静态常量对象以及这样的对象的数组。而必须要在类的外部对这样的对象以及对象数组进行初始化。

6、  嵌套类以及局部类中的静态成员

可以在一个嵌套类(类中类)中定义一个静态成员变量。但是,却不能将一个静态成员

放到一个局部类中(定义在函数中的类),因为,没办法对类中的静态成员进行外部的初始化,外部没办法找到函数中的类中的静态成员。

7、  静态成员函数

类的静态成员函数是与类关联而不是与类中的对相关联。所以,调用静态成员函数时,

并不传递this指针。没有this指针,静态成员函数当然就不能访问类的其他的非静态的成员以及非静态的成员函数,而只能访问类中的静态类型的成员以及静态的成员函数。

8、  静态初始化的依赖性

在指定的翻译单元中静态对象严格按照定义的顺序执行初始化。但是,如果一个静态

对象的作用域为多个文件则问题出现了。本对象的初始化究竟按照那一个文件的顺序来,或者这个静态变量的初始化与另外的一个变量的值有关系,或者有两个静态变量的初始化相互关联问题就更微妙了,这样这些静态变量的初始值就可能直接与这些相互关联的静态变量的初始化话的顺序相关,初始化的顺序不同很可能引起初始值不同。

       所以,最好不要用让静态变量发生依赖,或者将所有的静态变量的定义放在同一个文件中。或者用下面的两种技术:

技术一:

       在库文件中加上一个额外的类。这个类负责库中的静态对象的动态初始化。这样当本库的头文件在包含它的第一个编译单元内被初始化,其余的单元都会被通过#ifndefine的预处理操作所忽略。

技术二:

       它基于这样的一个思想:函数在被调用的时候,函数内的成员对象将被初始化,而在另外的任何时刻是不会进行初始化操作的。这样就我们可以将所要定义的静态对象的定义放到一个会返回该类对象的引用的函数中。这样我们可以通过控制什么时候调用什么函数来决定什么时间定义去那一个类的对象同时进行该对象的初始化。

9、  替代连接说明

当在写程序需要调用一个C库中的函数。但是,由于C++编译器为实现函数的重载对于

函数在内部的表示进行了与C不一样的表示(多加了一些后缀)。这样经过编译器的翻译后。连接器将无法解释C库的函数。就没办法使用C库中的函数。为了能够使用C库中的函数。C++编译器对extern关键字进行了重载。用extern后跟一个表示C或则C++库的字符串来指定所要连接的函数的连接类型为C还是C++;

 

第十一章 引用和拷贝构造函数

 

位拷贝就是传递的值是参数的地址,值拷贝就是传递的值是参数本身的值。

 

第十二章 运算符重载

                               

1、  运算符重载的实质

a) 只是函数的另外一种调用形式,只是为了语法上的方便而已。

b) 基于运算符的实质只是为了语法上的方便,所以,运算符重载不能滥用,而且只能在类中使用,只有在能使类的更加易写或者更加易读的情况下才使用运算符重载否则适得其反。

2、  运算符重载的语法

a)        参数的个数:运算符是一员的还是二元的(即1个还是2个参数)。运算符被定义为全局的(对于一元为一个参数,二元为两个参数)还是成员函数(对于一元没有参数,对于二元为一个参数)。

3、不常用的运算符

a) 下标运算符operator[]。运算符new 、delete以及逗号运算符。

b) 指针间接运算符’->’用于实现灵巧指针。

c) 运算符”=”重载时,只能作为成员函数而不能为友元函数重载,因为友元函数的重载为全局式的这样就相当重定义了内置的“=”,这个是编译器所不允许的。

4、不能重载的运算符

a)Operator.:类中的成员访问运算符

b)Operator.*:成员指针间接引用

c)不存在用户定义的运算符,将引起编译器无法决定运算符的优先级。

5、在嵌入类与它的载体类

当在嵌入类中定义它的一个载体类的对象作为嵌入类的成员变量时,该嵌入类的成员变

量只能为载体类的引用的形式的成员变量。

6、  非成员运算符

运算符的左侧不是当前类的对象,而是别的类的对象时,即为非成员运算符的重载。如:输入、输出流:<< 和 >>。

7、重载赋值符

a) 内置类型:重载的对象的成员都为内置的数据类型或者不包含指针时。可以以复制的方式直接复制各个成员的值。

b) 类中指针:当类中有指针时(除用引用计数外),重载赋值符一定要复制所有所有,包指针所指的内容。

c) 引用计数:当对象比较大需要很大的内存或者初始化过高,则可以采用引用技术的方式。即每当有一个对象指向它时就将该存储空间的引用数+1。删除对象时只有引用的计数为0时才将这个对象销毁。但为了避免一个对象真在写而别的对象在读或者写时引起数据的不一致性,采用写拷贝计数。即,写时先查看引用计数是否>1,若是,则在写之前拷贝这块存储单元。

d)如果程序员未在类中对operator=进行重载编译器会自动创建一个,这个运算符模仿拷贝运算符的行为(用位拷贝),如果类中还包含对象(成员对象)或者是从别的类继承的,对于这些对象,operator=将被递归地调用。所以,如果,自己真想使用operator=重载,一定要在类中定义自己的赋值运算符重载。

8、自动类型转换

a)构造函数的类型转换:

类中如果有一个只带一个参数(另一个类的对象或者引用)的类的构造函数,那么这个构造函数允许编译器自动地调用本构造函数,从而将参数自动地转化为一个这个类的对象。

有时这样的转化会出问题,所以,如果不想让编译器自动调用这个构造函数实现类型转化,可以在这个构造函数前面加上关键字explicit,表示本构造函数不能被编译器自动调用,而只能由程序员显示的调用,如:f(Two(one)),其中的Two(one)为以one对象为参数显式的调用Two的构造函数以产生一个Two的对象。

为目的类型进行转换

b)运算符的类型转换

可以在本类(源类)中创建一个运算符重载的成员函数,函数的命名规则为:operator+“将要转化为的类的类名,即返回的成员类型”+(),在这个函数中实现了转化的方法,返回的转换后的类的一个对象。

全局函数的运算符可以转换左右任一操作数,而成员函数的重载必须保证左侧的操作数已经处于正确的形式。将如果全局重载不会带来问题,则全局重载会带来较大的便利。

运算符重载为源类执行转换。

重载时要注意VC6.0未完全实现C++标准不能将.h文件中的成员函数重载为友元函数。

                       

 

第十三章 动态对象的创建

 

1、  对象的创建

a) 创建一个对象时会发生两件事:为对象分配内存、调用构造函数来初始化那块内存。

b)分配内存:可以在程序运行前在静态存储空间分配;无论什么时候在一个特殊的执行点,即在左大括号时可以在存储单元上创建一定的存储;同时也可以从一块称为为堆的地方分配,在运行时分配这些内存。

c)C语言利用mallocfree等函数来动态的申请内存。但是,由于malloc申请的只是一块内从而不是一个对象,所以,在使用之前必须对malloc返回的void*进行强制转化,然后对这块内存进行初始化,同时,释放时也必须有free来释放。

d)Operator new:由于,对象的内存的申请和对象的初始化是分开的。所以,很容易被遗忘,导致程序出错,而在C++中把创建一个对象的动作都集成在了一个new的运算符里。

e)  Operator delete:new的反面是delete表达式,delete先调用析构函数,然后释放内存。Delete释放时需要对象的地址。用new申请的内存不用free释放以免对象还未调用析构函数,就释放了内存。

f) 使用delete释放内存时,如果指针指的为一个对象,则使用:delete ptr,但如果指向的是一个数组,则为了释放数组的所有内存而不是数组中的第一个元素,必须告诉编译器这是一个数组,使用如下方式:delete []ptr;

g)当内存耗尽时,继续使用new进行存储空间的分配会引起异常。引起异常后默认会调用new.h文件中new-hander函数,若想在发生内存耗尽时使用自己的异常处理函数,则可以使用重载new-hander的方法或者可以写一个异常处理函数,然后将此函数的入口地址设置为发生内存耗尽异常时的默认处理函数。

h)如果程序对内存分配和释放的效率以及内存的有效使用要求比较严格。则可以以重载运算符new和delete的方式重载全局内存分配函数以及释放函数(new 和delete)或者是针对特定类的分配函数以及分配函数(new 和delete)。

2、  构造函数的调用

使用new来创建一个新的对象时我们知道分为两步:1、申请一块内存,并返回这块内

存的指针;2、在该内存空间上调用构造函数初始化这块内存。所以,如果在第一步中存在内存不足,则第二步的构造函数并不会被调用。

3、  定位new和delete

在一些嵌入式系统中可能要求一个对象和一个特定的硬件是同义的。所以,就需要在特

定的地址存放特定的对象。而这个可以在重载运算符new和delete时,添加新的参数(如:地址或者对象的引用来)达到这一目的。

 

 

 

 

第十四章 继承和组合

 

1、组合:新类中创建已存在类的对象,由此类中会包含已存在类对象,此种方式为组合。

在构造函数的调用的过程中要注意的是首先会调用基类的构造函数,然后调用子对象的构造函数,子对象构造函数的调用顺序是根据其定义的顺序而定的。为什么在构造函数中初始化子对象以及基类对象的对象你哦?这是C++的一个强化机制,确保在构造当前对象的时候就已经调用了基类的构造函数。

2、名字隐藏:任何时候重新定义了基类中的一个重载函数,在新类之中所有其他的版本则被自动地隐藏。这种方式与普通的重载不一样,只要返回类型的不同也会隐藏基类的函数。

3、非自动继承的函数:operator=类似于构造函数的功能,因此是不能够被继承的。但是在继承过程中,若没有显示的创建,编译器会创建默认的构造函数和拷贝构造函数。

小结:1、若没有给派生类显示的定义拷贝构造函数调用的将会是构造函数。若构造函数没有显示的定义则会调用默认的构造函数。因为这两个函数都不会自动的继承。

4、如何选择组合与继承

   利用组合的时候,新类不希望已存在的类作为其接口。新类应该作为嵌入类的私有对象。仅提供一些函数。若希望已存在的类中的每一个变量与成员都囊括,则应该使用的是继承。

5、向上类型转换

由继承类向基类进行类型转换。但是这个类接口可能会失去一些成员函数。

派生类中拷贝构造函数的调用顺序:基类的拷贝构造函数、各对象的拷贝构造函数。派生类是由基类派生的,在调用构造函数的时候会首先调用的是基类的构造函数,所以当使用向上类型转换的时候,派生类的引用就相当于基类的引用。但是随之的是会丢失一部分的函数,因为在基类中并不存在。

 

第十五章 多态性和虚函数

 

1.虚函数(impure virtual)

C++的虚函数主要作用是运行时多态,父类中提供虚函数的实现,为子类提供默认的函数实现

子类可以重写父类的虚函数实现子类的特殊化。

  如下就是一个父类中的虚函数:

class A

{

public:

   virtual void out2(string s)

    {

       cout<<"A(out2):"<<s<<endl;

    }

};

 

2.纯虚函数(pure virtual)

C++中包含纯虚函数的类,被称为是抽象类抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。

C++中的纯虚函数更像是只提供申明,没有实现,是对子类的约束,是接口继承

C++中的纯虚函数也是一种运行时多态

  如下面的类包含纯虚函数,就是抽象类

class A

{

public:

   virtual void out1(string s)=0;

   virtual void out2(string s)

    {

       cout<<"A(out2):"<<s<<endl;

    }

};

 

3.普通函数(no-virtual)

普通函数是静态编译的,没有运行时多态,只会根据指针或引用的字面值类对象,调用自己的普通函数

  普通函数是父类为子类提供的强制实现

  因此,在继承关系中,子类不应该重写父类的普通函数,因为函数的调用至于类对象的字面值有关。

4.程序综合实例

#include <iostream>

using namespace std;

 

class A

{

public:

    virtual void out1()=0;  ///由子类实现

   virtual ~A(){};

   virtual void out2() ///默认实现

    {

       cout<<"A(out2)"<<endl;

    }

   void out3() ///强制实现

    {

       cout<<"A(out3)"<<endl;

    }

};

 

class B:public A

{

public:

   virtual ~B(){};

   void out1()

    {

       cout<<"B(out1)"<<endl;

    }

   void out2()

    {

       cout<<"B(out2)"<<endl;

    }

   void out3()

    {

       cout<<"B(out3)"<<endl;

    }

};

 

int main()

{

    A*ab=new B;

   ab->out1();

   ab->out2();

   ab->out3();

   cout<<"************************"<<endl;

    B*bb=new B;

   bb->out1();

   bb->out2();

   bb->out3();

 

   delete ab;

   delete bb;

   return 0;

}

 

 

1 虚函数与向上类型转换

使用向上类型转换时,将派生类的对象传递给一个基类的对象,用转换后基类的对象调用相应的成员函数时,调用的是基类的成员函数,而并非派生类对象的成员函数。但是,程序在运行时,我们经常希望无论什么时候一个类的对象都是调用的本类的成员函数而不是基类的成员函数。所以,为了达到这一目的,C++中采用“晚捆绑”技术,即在程序运行时才将函数体与函数调用想联系起来,而不是过程型语言中的“早捆绑”:捆绑在程序运行之前由编译器进行捆绑。

       而,要使用晚捆绑在c++中使用关键字vitual,这个技术只在使用含有vitual函数的基类的地址时才起作用。虽然,虚函数很重要,但是,由于使用虚函数会有额外的开销(速度问题),所以,C++设计为可选(通过virtual进行选择)。但是,程序设计时,作为一个好的程序员应该考虑的问题是程序的瓶颈方面,而不是在猜测方面下功夫。所以,如果为了程序的效率考虑的话,只需要不适用虚函数的函数即可。

2 C++中晚捆绑的实现

为了实现晚捆绑,编译器需要进行一定的动作。多数的编译器通过如下实现:编译器为每一个包含虚函数的类新建了一个VTABLE,这个表中的每一项为指向这个类的一个虚成员函数的函数入口地址。并在每个包含虚函数的类的对象中存放了一个指向本类所对应的VTABLE表的地址的指针。接下来,在编译器编译时,如果遇到一个用基类指针做虚函数调用时,编译器自动将能够找到该对象的类所对应的VPTR,并在VTABLE中查找函数地址的代码。

       在C++中,成员函数的调用时除了将函数参数表中所定义的参数的同时,也将本对象的this指针也默认传到了函数体。所以,成员函数知道它工作在那个特殊的对象上。

3 虚函数的遭捆绑

如果,编译器清楚的知道它所使用对象的确切类型,则可以使用遭捆绑技术。如,基类对象调用基类的虚函数时。

4 抽象基类和纯虚函数

如果,仅仅希望基类给派生类提供一个公共的接口,而不希望实际的创建一个基类的对

象时,我们可以在基类中定义一个纯虚函数来实现这一功能。若某个类中含有一个纯虚函数,则本类为抽象类,抽象类不能实例化式的创建它的一个对象。

       使用virtual关键字,并在函数末尾加=0来定义一个纯函数,也即定义了一个抽象类。

Note:

       纯虚函数禁止对象使用值传递方式调用,这样即防止了对象切片,也保证了向上类型转换期间总是使用指针或者引用。

5 纯虚函数——基类代码的重用

       虽然,编译器不允许生成抽象基类的对象,但是,在基类中对纯虚函数提供定义是可能的。首先,在类的声明的外部,以全局变量范围的形式,定义这个虚函数。然后,在抽象基类的派生类中可以通过:“类名::抽象基类的虚函数名()”的方法来调用抽象基类的虚函数的定义的函数体,这样就实现了,对于基类的一段公共代码的重用。

6 继承和虚函数的VTABLE

针对指向基类对象的指针的处理时编译器只能把它作为基类对象处理,既是它指的是一个派生类的对象,因为编译器这时是无法确定该指针具体是指向那个派生类的对象的。在VTABLE中可能有,也可能没有一些其他函数的地址,但是,无论怎样,对这个VTABLE地址做虚函数调用时就可能调用了该类所没有的函数,这不是我们想要的,所以,在向上转换的函数中,编译器不允许我们对只存在派生类中的函数做虚函数调用。

       构造函数不能为虚函数,而析构函数必须为虚函数,这样可以保证对象的正确的生成,同时保证向上类型转化后的对象能够被正确的析构,而不至于发生内存泄露。

7 对象切片

在使用虚函数时,如果,用按值传递的方式来向上转换,会发生对象切片。即,基类的拷贝构造函数被调用,拷贝构造函数初始化VPTR(虚函数入口地址表指针),并将只属于基类对象的部分拷贝,因此派生类的对象在此过程中真的变成了一个基类的对象。但是,如果将基类的虚函数变为纯虚函数时,按值传递方式的向上类型转换将被编译器禁止,因为,需要调用拷贝构造函数来生成一个基类的对象,而抽象类是不允许生成对象的。

8 重载和重新定义

C++中允许虚函数的重载,但是在派生类中如果重写了基类的虚函数,则不允许改变重定义过的函数的返回值(如果,不是虚函数则允许),从而保证了向上类型转变后进行虚函数调用出现返回值与派生类不同的问题。

       另外,如果这个返回值被改变为原返回类型的派生类型也是允许的,这样也同样遵循了合约,同时,返回了对象所属类的确切类型,这常用的,但返回基类类型通常会解决我们的问题,所以,也是一个特殊的功能。

9 内联的构造函数

内联的构造函数会降低函数调用的代价,但是,虚函数的构造函数设计为内联时可能会出现以下情况:

编译器会将一些隐藏代码插入到构造函数中,包括:初始化VPTR,检查this指针,调用基类的构造函数。所以,我们在设计抽象类时要考虑是否要把构造函数去掉内联性。

10 虚函数在构造函数中的行为

由于构造函数中可能只是部分形成对象——我们只知道基类被初始化但哪个类将会从

这个基类继承而来是不知道的。而虚函数在继承层次上是“向前”和“向外“进行调用的,他可以调用派生类中的函数,但是,如果我们在构造函数中如果也这样做就会出现问题,因为在构造函数中调用的函数中操作的成员可能还没有生成呢。

       所以,C++规定构造函数中的虚函数的调用,调用的只是虚函数的本地版本。另外,这也实现了,在类层次的构造函数中对于VPTR的从基类到派生类依次处理。

12 纯虚析构函数

基类中必须为纯虚析构函数提供一个函数体,用来析构是能够正确的从派生类到基类的

析构,但是,当从某个含有析构函数的类中继承出一个类时,并不要求派生类提供纯虚函数的定义,因为,编译器会自动的生成析构函数,从而实现对析构函数的定义。这样虚析构函数和纯虚析构函数的唯一区别就是阻止基类的实例化。

      所以,通常如果我们类中要有一个虚函数,那么我们立即增加一个虚析构函数,从而保证程序的鲁棒性。

13 析构函数中的虚机制

如果在不同函数中调用虚函数,则编译器会使用“晚绑定”的方式调用该函数。但是,

如果是在析构函数中调用虚函数,则,虚机制被忽略,只调用此虚函数的本地版本。因为,所调用的这个虚函数可能是本析构函数的派生类的成员函数。而,由析构的由外向内(基类)性,此时,它的派生类的对象已经被析构了,所以,会产生错误。

14 单根继承(基于对象的继承)

当我们需要使多个不同类的对象具有同样的操作时,我们可以先创建一个非常简单的只

包括纯虚析构函数的基类,然后,这些不同的类都从这个基类继承而产生,最后,在基类上添加该操作,即实现了该功能,但是,由于未必所有的类都是新生成的,有一些类时编译器已经定义好的,这样为了使这样的类的对象也能有这样的操作,就需要多重继承,但是,多重继承会相当复杂,解决这一问题模版类提供了很好的技术路线。

15、运算符重载

由于Virtual的基类提供接口,执行时调用具体的相应的派生类的操作的性质,我门可

以将virtual用于运算符重载的实现上,主要为处理数学部分,如:矩阵、向量和标量的运算上。但是由于,但一个虚函数只能进行单一指派——即,只能判断一个未知对象的类型。所以,如果重载的运算符有两个或两个以上的向上类型转换,这样具体调用虚函数时需要两个或两个以上的对象指派。这时就需要使用多重指派技术,即在一个虚函数中调用虚函数,引起第二个虚函数的调用。当最后一个虚函数被调用时,已经得到了每一个对象的类型。

16、向下类型转换

在揭开typeid神秘面纱之前,我们先来了解一下RTTI(Run-TimeType Identification,

运行时类型识别),它使程序能够获取由基指针或引用所指向的对象的实际派生类型,即允许“用指向基类的指针或引用来操作对象”的程序能够获取到“这些指针或引用所指对象”的实际派生类型。在C++中,为了支持RTTI提供了两个操作符:dynamic_cast和typeid。
    dynamic_cast允许运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转化类型,与之相对应的还有一个非安全的转换操作符static_cast。

Dynamic_cast用于显示类型转换,当使用它时仅当类型转换是正确的并是成功的时,

返回值是一个指向所需类型的指针,否则它将返回0来表示这并不是正确的类型。Static_cast,dynamic_cast都不允许类型转换到该类层次的外面。但,static_cast静态的浏览类层次总是有风险的。所以,一般使用dynamic_cast。

 

 

第十六章 模板介绍

 

1、  模版的作用

继承和组合提供了重用对象代码的方法,而C++的模版提供了重用源代码的方法。当引入模版时,就不再使用基于对象的类的层次结构来实现容器了。模版提供了更好的机制。

2、模版在头文件中的布局

       由于可能存在多重定义的问题,一般不讲类的实现放在头文件中,但是,模版的定义很特殊,template<….>之后的任何东西都编译器当时都不为它分配存储空间,而是一直等到被一个模版实例告知。在编译器中有机制能去掉同一模版的多重定义。所以,在C++中一般讲模版的声明和定义都放入一个头文件中。

1、模版中的常量

模版的参数并不局限于类定义的类型,可以使用编译器内置的类型。这些参数值在编译期间变成模版的特定示例的常量,同时也可以对这些参数使用默认值。同时,在C++中,当我们遇到需要创建大量的对象,但不访问每一个变量时,我们可以在类中设置一个指向所需的类的对象的指针,但是在构造函数中并不初始化这指针,而是,在使用时才初始化,这样就不用给所有的对象都分配存储空间,所以,节约了内存。即所谓的“懒惰初始化”的方法来。

2、模版的创建

由于,普通类到模板类之间需要修改的代码很少,即是适度透明的,所以,一般情况下先创建和调试一个普通类,然后再将其改造成模板类,一般要比直接创建一个模板类快的多。

3、容器对于其中存放的对象的所有权

容器一般不拥有所存放对象的所有权,这样,对于对象的销毁工作也由客户程序员来做。

但我们也可以实现能够让用户选择容器对于所存放对象的所有权的容器。这平常是在容器中设置一个变量来管理容器的所有权,在容器的构造函数中给与容器的所有权设置默认值,同时也可以,增加一定的函数来读取和设置容器的所有权。

7、迭代器

       迭代器也是一个对象,它以遍历的方式访问容器中的其他对象。它提供了一种与具体的容器是如何对容器中的对象进行存取的实现无关的访问,使得对于容器中元素的访问有一个统一的标准。使得客户程序员使用迭代器访问容器中的对象时可以不考虑是哪一个容器,什么容器。所以,可以写出一个通用的对象访问程序,当容器改变了,用户程序仍可以不用改变。

       技术路线:

A、 将容器类作为迭代器类的成员对象,然后,用迭代器的成员函数对容器的访问进行封,从而提供统一接口。

B、   将容器类作为迭代器类的潜入友元类,从而,实现一个容器类必然有一个对应的迭代器来统一的访问容器。

为了安全性,可以引入一个“结束哨兵”来防止迭代器的越界。但是,在C++标准库中

为了效率,并未加入这样的代码,所以,使用时要留意。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值