《读书笔记》系列3:C++编程思想

第1章 对象导言

极限编程

先写测试

XP革命性地改变了测试的概念,将它置于与 编码相等的优先地位。事实上,我们需要赢在编写被测试代码之前写测试额,而且这些测试时与代码永远在一起。这些测试必须在每次项目集成是都能成功执行。
先写测试有两个极其重要的作用:
(1) 它强制好类的接口有清楚的定义;
(2) 写测试的第二个重要作用,是在每次编译软件时运行这些测试。

结对编程

第3章 C++中的C

Make的行为:当输入make时,make程序在当前目录中寻找名为makefile的文件,该文件作为工程文件已经被建立。这个文件列出了源代码文件间的依赖关系。Make程序观察文件的日期。如果一个依赖文件的日期比它所依赖的文件旧,make程序执行依赖关系之后列出的规则。

后缀规则告诉make可以根据文件的扩展名去考虑怎样构建程序而不需用显示规则去构建一切。

Volatile

限定词const告诉编译器“这是不会改变的”;而限定时volatile则告诉编译器“不知道何时改变,不要对变量进行优化”。一个例子就是在多线程程序中,如果正在观察被另一个线程或进程修改的特殊标识符,这个标识符应该是volatile的,所以编译器不会认为它能够对标识符的多次读入进行优化。

C++的显示转换

应该小心使用转换,因为转换实际上要做的就是对编译器说“忘记类型检查,把它看错成其他类型了。”

第4章 数据抽象

大幅度提高生产效率的唯一办法就是使用其他人的代码,即是去使用库。在跨越多种体系结构的平台上,通常提供库的最明智的方法是使用源代码,这样它就能在新的目标机器上被重新配置和编译。

第8章 常量

Const的最初动机是取代预处理器#defines来进行值替代。从这以后它曾被用于指针、函数变量、返回类型、类对象以及成员函数。因为预处理器只做文本替代,没有类型检查。我们应该完全用const取代#define的值替代。

C++中的const默认为内部链接,也就是说,const仅在const被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。在C中,const默认时外部连接,所以声明 “const int num;”是正确的,但是在C++中不行。

在C++中,一个const不必创建内存空间,而在C中,一个const总是需要创建一块内存空间。在C++中,是否为const常量创建内存空间依赖于对它如何使用。如果一个const仅仅用来把一个名字用一个值代替,则不必分配内存空间。

(1) 指向const的指针:const int*u;“u是一个指针,它指向一个const int”;
(2) Const指针:int*const w=&a;“w是一个指针,这个指针是指向int的const指针”;因为指针本身

现在是const指针,编译器要求给它一个初始值(地址),这个值在指针生命期间内不变。然而可以改变他所指向的值(*w=2)。

不能把一个const对象的地址赋给一个非const指针,因为这样做可能通过被赋值的指针改变这个对象的值。
C++中,当传递一个参数时,首先选择按引用传递,而且是const引用。对于函数的创建者来说,传递地址总比传递整个类对象有效,如果按const引用来传递,意味着函数将不改变该地址所指的内容。从客户程序员的观点来看,效果就想按值传递一样。
编译器总是将临时类对象作为常量。

为了保证一个类对象为常量,引进了const成员函数:const成员函数只能对于const对象调用。

Const对象和成员函数:如果声明一个成员函数为const,则等于告诉编译器该成员函数可以为一个const对象所调用。一个没有被明确声明为const的成员函数被看成是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。不修改数据成员的任何函数都应该把它们声明为const,这样它可以和const对象一起调用。构造函数和析构函数都不是const成员函数,因为它们在初始化和清除时,总是对对象作些修改。

第9章 内联函数

为了既保持预处理器宏的效率又增加安全性,而且还能像一般成员函数一样可以在类里访问自如,C++引入了内联函数。

一般应该把内联定义放到头文件里。任何在类内部定义的函数自动地成为内联函数。

第10章 名字控制

Static的两种基本含义:(1)在固定的地址上进行存储分配,也就是说对象时一个特殊的静态数据区上创建的。(2)static控制名字的可见性,所以这个名字在这个单元或类之外是不可见的。

有时想在两次函数调用之间保留一个变量的值,可以通过定义一个全局变量来实现,但这样一来,这个变量就不仅仅只受这个函数的控制。C和C++都允许在函数内部定义一个static对象,这个对象将存储在程序的静态数据区中,而不是在堆栈中。

无论什么时候设计一个包含静态变量的函数时,都应该记住多线程问题。

在文件作用域内,一个被明确声明为static的对象或函数的名字对翻译单元来说是局部于该单元的。
用可见性术语来说,static的反义词是extern,它明确地声明了这个名字对所有的翻译单元都是可见的。
可以在一个名字空间的类定义之内插入一个友元(friend)函数。这样这个函数就成了名字空间的一个成员函数。别的类也可以调用此函数。

C++中的静态成员:所有的这些对象的静态数据成员都共享这一块静态存储空间,这就为这些对象提供了一种互相通信的方法。但静态数据属于类,它的名字只在类的范围内有效,并且可以是public、private或者protect。如果一个静态数据成员被声明但没有定义是,链接器会报告一个错误。
定义必须出现在类的外部(不允许内联)而且只能定义一次,因此它通常放在一个类的实现文件中。

第11章 引用和拷贝构造函数

编译器使用拷贝构造函数通过按值传递的方式在函数中传递和返回对象。它通常用于函数的参数表中和函数的返回值,但也可以独立使用。

常量引用:const int& num。在函数参数中使用常量引用特别重要。这是因为我们的函数也许会接收临时对象,这个临时对象是由另一个函数的返回值创立或由函数使用者显示地创立的。临时对象总是不变的,因此如果不使用常量引用,参数将不会被编译器接受。

拷贝构造函数是通过传值方式传递和返回用户定义类型的根本所在。编译器在没有提供拷贝构造函数时将会自动地创建。有一个简单的技术防止通过按值传递方式传递:声明一个私有拷贝构造函数。

指向成员的指针:指针是指向一些内存地址的变量,既可以是数据的地址,也可以是函数的地址。

第12章 运算符重载

定义重载的运算符就像定义函数,只是该函数的名字是operator@。

第13章 动态对象创建

Malloc()和free()是库函数,因此不再编译器控制范围之内。New和delete能完成动态内存分配及初始化组合动作,完成清理及释放内存组合动作,保证所有对象的构造函数和析构函数调用。

当创建一个C++对象时,会发生两件事:(1)为对象分配内存。(2)调用构造函数来初始化那个内存。

Malloc()只是分配了一块内存而不是生成一个对象,所以它返回了一个void*类型指针。而C++不允许将一个void*类型指针赋予任何其他指针,所以必须做类型转换(强制转换)。

C++中,当用new创建一个对象时,它就在堆里为对象分配内存并为这块内存调用构造函数。默认的new还进行检查以确信在传递地址给构造函数之前内存分配是成功的,所以不必显示地确定调用是否成功。通过new运算符,在堆里创建对象的过程变得简单了—只是一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。

Delete表达式首先调用西沟函数,然后释放内存(经常是free())。Delete只用于删除由new创建的对象。如果正在删除的对象的指针是0,将不发生任何事情。为此,人们经常建议在删除指针后立即把指针赋值为0以免对它删除两次。对一个对象删除两次可能会产生某些问题。

用于数组的new和delete。当使用new在堆上创建对象数组时,还必须多做一些操作。需要使用delete []ptr;才能把数组中的所有对象释放。空的方括号告诉编译器产生代码,该代码的任务是将从数组创建时存放在某处的对象数量取回,并为数组的所有对象调用析构函数。

第14章 继承和组合

在C++中,我们通过创建新类来重用代码,关键技巧是使用别人已经创建好的类类,但不修改已存在的代码。第一种方法是:简单地在新类中创建已存在类的对象,因为新类是有已存在类的对象组合而成,所以这种方法称为组合。第二种方法是继承。

对于多重继承,要给出多个基类名,它们之间用逗号分开。

因为新类的构造函数没有权利访问这个子对象的私有数据成员,所以不能直接对它们进行初始化。解决的方法是使用构造函数的初始化列表。

Is-a关系用继承表达,而has-a关系用组合表达。

第15章 多态性和虚函数

多态性改善了代码的组织性和可读性,同时也使创建的程序具有可扩展性。访问控制通过使细节数据设为private,将接口从具体实现中分离开来。虚函数则根据类型来解耦。

向上类型转换。取一个对象的地址,并将其作为基类的地址来处理,这称为向上类型转换。如下代码:

Class A{
Public:
Void play() const{。。。};
};

Class B:public A{
Public:
Void play() const{…};
};

Class C:public A{
Public:
Void play() const{…};
};

Void func(A & a)
{
a.play();
};
int main()
{
C c;
Func(c)
}

Func()接受一个A,但也不拒绝任何A派生的类,无需类型转换就能将对象传给func。

把函数体与函数调用相联系称为捆绑。当捆绑在程序运行之前完成时,这称为早捆绑。使用virtual可以实现晚捆绑。在派生类中virtual函数的重定义通常称为重写。注意,仅需要在基类中声明一个函数为virtual,调用所有匹配基类声明行为的派生类函数都将使用虚机制。

抽象基类和纯虚函数:只是想对基类进行向上类型转换,使用它的接口,不希望用户实际地创建一个基类的对象。要做到这一点,可以在基类中加入至少一个纯虚函数,使其基类称为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0。编译器会保证不能生成抽象类的对象。当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出来的类也将是抽象。

纯虚函数禁止对抽象类的函数以传值方式调用。通过抽象类,保证在向上类型转换期间总是使用指针或引
用。

编译器对新类创建一个新VTABLE表,并且插入新函数的地址,对于没有重新定义的虚函数使用基类函数的地址。

虚函数和构造函数:当创建一个包含有虚函数的对象时,必须初始化它的VPTR以指向相应的VTABLE。因为生成一个对象是构造函数的工作,所以设置VPTR也是构造函数的工作。编译器在构造函数的开头部分秘密地插入能初始化VPTR的代码。

构造函数有一个专门的工作:确保对象被正确地建立。在构造函数内,必须想办法保证所有成员都已经建立,保证它的唯一方法是让基类构造函数首先被调用。这样,当在派生类构造函数中,在基类中能访问的所有成员都已经被初始化。

对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。虚机制在构造函数中不工作。

当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过他自己的VTABLE的调用,而不是最后派生的VTABLE(所有构造函数被调用后才会有最后派生的VTABLE)。

析构函数和虚拟析构函数:构造函数是不能为虚函数的,但析构函数能够且常常必须是虚的。析构函数的析构顺序是由最晚派生的类开始,并向上到基类。每个析构函数知道它所在类从哪一个派生而来,但不知道它派生出哪些类。

#include <iostream>
using namespace std;
class Base1{
public:
    ~Base1(){cout << "~Base1()\n";}
};

class Derived1:public Base1{
public:
    ~Derived1(){cout << "~Derived1()\n";}
};

class Base2{
public:
    virtual ~Base2(){cout << "~Base2()\n";}
};

class Derived2:public Base2{
public:
    ~Derived2(){cout << "~Derived2()\n";}
};

int main(int argc,char *argv[])
{
    Base1* bp= new Derived1;
    delete bp;
    Base2* b2p= new Derived2;
    delete b2p;
    system("pause");
}

不把析构函数设置为虚函数是一个隐匿的错误,因为它常常不会对程序有直接的影响,但某些情况下会存在内存泄露。

作为一个准则,任何时候我们的类中都要有一个虚函数,我们应当立即增加一个虚析构函数,即使它什么也不做。

析构函数中的虚机制:在析构函数中,只有成员函数的“本地”版本被调用;虚机制被忽略。(就是说,在析构函数中,不会调用派生类里的虚函数。因为在析构到本类时,派生类的析构函数已经被调用,派生类的成员变量已被析构,不能再调用派生类中的函数)。

第16章 模板介绍

猜你喜欢

转载自blog.csdn.net/qguanri/article/details/50054007
今日推荐