【Accelerated C++】重点回顾(续)

看了刘未鹏推荐过的C++入门经典《Accelerated C++》,读此书的过程中一再感叹“大师就是大师,他可以很轻松的把东西的本质呈现在你面前”,这本书采用了现实中与我们很接近的学生成绩信息管理这一例子,层层推进,一步一步引出C++中的知识点,根据任务提出解决方案,让你感受到C++中一些设计的比要性。

一下是我读此书时做的笔记,谨用于记忆。


ps:这篇是第二篇,之前的笔记请看这个链接【Accelerated C++】重点回顾


第八章、编写泛型函数


    C++语言的一个重要特性就是可以使用并创建泛型函数。

2、而且仅有一个类型以特定的方式支持特定的操作集时,这个类型才是迭代器。

3、实现泛型函数的语言特性叫做‘模板函数’(template function)
      --模板是标准库的基石。
    template <class T>
    T median( vector<T> v ){
        ...
    }
    T为类型参数。

4、模板实例化
    当我们一个vector<int>对象调用median函数时,系统会高效的创建并编译这个函数的一个实例,而且在这个函数中所有T都会用int来取代。

    C++标准并没有说明系统改如何管理模板实例化,所以每个系统都按照它自己的特殊方式来解决实例化。

    模板实例化有两点需要明确的:
        1)C++系统遵循的是传统的编辑-编译-链接模板,实例化往往不是发生在编译时期,就是在链接时期。直到模板实例化,系统才会检验模板的代码是否可用用于指定的类型。
        2)当前的大多数系统都要求,为了可用实例化一个模板,模板的定义,而不仅仅是声明,都必须是系统可以访问的。这意味着:定义模板的源文件和头文件都必须是可访问的。很多系统都希望模板的头文件可以包含源文件 --->函数声明与定义都在.h文件中。

5、标准库定义了5个迭代器种类:
    1)顺序只读访问     -->输入迭代器                    只能输入
    2)顺序只写访问     -->输出迭代器                    只能输出
    3)顺序读写访问     -->前向迭代器(所有标准库都满足),既能输入,也能输出
    4)可逆访问         -->双向迭代器(容器类都支持) ,   既能输入,也能输出
    5)随机访问         --高效的随机访问任意元素,       既能输入,也能输出



6、为什么下界常常是指向最后一个元素的下一个值?     (更简单,可靠)
    1)如果这个区间没有任何元素,那么就没有能标记末尾的最后一个元素。--如果在开始元素之前的位置标记为空区间,会降低程序的可靠性。
    2)可以让我们只需要比较迭代器的相等性和不等性。
        while( begin != end )
            ++begin;
    3)可以为我们提供一种罪自然的方式来表示'区间之外'。

7、如果没有标准库需要区分输入输出迭代器和前向迭代器的话,为什么还分出这两个种类呢?
    其中的一个原因是,并不是所有迭代器都与容器相关联。比如:
        标准库提供了可以和输入输出流绑定的迭代器。用于istream的迭代器满足输入迭代器的要求,用于ostream的迭代器满足输出迭代器的要求。
        使用适当的流迭代器,我们可以通过普通的迭代器操作来控制一个istream或者一个ostream。 流迭代器就是一个模板。

8、C++标准库的一个重要贡献就是,通过把迭代器用作算法和容器之间的粘合剂,算法可以获得数据结果的独立性。此外,算法使用的迭代器都要求支持某些操作,这样我们就可以根据这些操作来分解算法,这就意味着,我们可以很容易的把一个容器和能够在这个容器上使用的算法匹配起来。

第九章、定义新类型


1、C++有种类型:
    内置类型和类。


2、许多C++设计都基于这样的一种思想:
    允许程序员创建于内置类型一样易用的类型。


3、即便一个程序从来没有直接的创建人和const对象,它也可能会通过调用创建大量的const对象的引用。
    如:当我面把一个非const对象传递给带有一个const引用的函数时,这个函数就会把这个对象当做是const对象来处理,编译器也将只允许它调用这种对象的const成员。


4、何时为成员函数,何时为非成员函数?
    有一个通用的规则:如果这个函数会改变对象的状态,那么这个函数就应该成为这个对象的成员。


5、struct与class的唯一区别:
    默认保护的方式不同。默认保护会应用于第一个保护标签之前的所有成员。


    struct:第一个保护标签之前的成员都是public
    class: 第一个保护标签之前的成员都是private


6、访问器函数
    因为我们隐藏了数据成员,因此我们需要定义一个访问器函数来进行访问。
    std::string name() const{ return str; }
    那么函数是一个const成员函数,它不带有任何参数,并且返回一个string值,也就是str的一个副本。通过复制str,而不是返回它的一个引用。


    这样的函数经常被允许简单地访问隐藏的数据,这样就破坏了我们想要得到的封装性。
    
    只有当访问器是类的抽象接口的一部分时,才应该提供访问器。
    这里,我们抽象出了一个学生和一个对应的最终成绩,所以提供一个name函数是合适的。换句话说,我们并没有提供其他的成绩--midterm、final、或者homework的访问器。因为这些成绩都是时下的基本组成部分,而不是接口的组成部分。


7、当创建标准库中某个类的一个对象时,标准库可以保证这个对象开始时会有一个适当的值。


8、构造函数 
    是特殊的成员函数,它定义了对象如何初始化。我们没有办法显示调用一个构造函数。但是创建类的一个对象时就会自动的调用适当的构造函数。
    如果没有定义任何够做函数,编译器就会为我们合成一个。


9、默认合成的构造函数的规则:
    1)如果一个对象的类型是定义了一个或多个构造函数的类的话,那么适当的构造函数会完全控制这个类的对象的初始化。
    2)如果一个对象的类型是内置类型的话,那么值初始化会把它设置为0,而默认初始化会提供一个不明确的值。
    3)否则,这个对象的类型只能是没有定义任何构造函数的类。
        如果是这样的话,对这个对象进行值初始化或者默认初始化就会对它的每个数据成员进行适当的值初始化或者默认初始化。如果这个对象的任何一个数据成员的类型也是一个类,而且这个类带有自己的构造函数的话,初始化过程就会递归的进行。


10、我们应该确保每个数据成员任何时候都有一个有意义的值。


11、默认构造函数:
    不带有任何参数的构造函数被认为是默认构造函数。
    它的工作是确保对象的数据成员被适当地初始化。


12、系统在创建一个新的类对象时,会发生下面几个连续的步骤:
    1)系统会分配内存空间,以保存这个对象。
    2)根据构造函数的初始化列表,初始化这个对象。
    3)执行构造函数的函数体。


13、系统会初始化每个对象的每个数据成员。不管够做函数初始化列表中是否有这些成员。
    虽然够做函数的函数体随后可能会改变这些初始值,但是初始化列表会在构造函数的函数体执行前执行。


    通常来说,显示的为一个成员提供一个初始值,要比在构造函数的函数体中对这个成员进行赋值要好的多。
        通过这样的初始化,而不是进行赋值操作,我们可以避免做两次相同的工作。


14、对于内置类型的成员来说,在构造函数中为它们提供一个值显得尤为重要。
    因为,如果没有初始化,那么在局部生存空间中声明的对象就会使用垃圾信息来初始化。


15、成员初始化的顺序取决于它们在类中声明的顺序。
    因此,应该要避免顺序的依赖性--->在构造函数的函数体中对这些成员赋值,而不是在构造函数初始化列表中初始化它们。


第十章、管理内存和底层数据结构


1、理解标准库的关键是:
    使用核心语言的编程工具和技巧,这些方法在其他地方也派的上用场。

2、数组和指针:
    数据也是一种容器。
    指针是一种随机访问的迭代器。

3、指针是表示对象的地址的一个值。每个不同的对象都有一个唯一的地址,这个地址表示计算机中保存这个对象的内存空间。
    如果x是一个对象,那么&x就是它的地址,
    如果p是一个对象的地址,那么*p就是对象本身。

    &x中的&是一个取地址操作符。
    *是一个解引用操作符,作用类似于迭代器上的*的作用。

4、程序员们经常使用0值来初始化指针,因为
    可以把0转换为指针时可以生成一个值,它可以确保与指向任何对象的指针的值不同。
    而且,常数0是唯一一个可以转换成指针类型的整数值。
    从0转换生成的值,常常称为空指针。

5、指向函数的指针
    没有程序可以创建或修改一个函数,只有编译器可以做这个工作。
    对于函数来说,程序所能做的,仅仅是调用它或者取得它的地址。

6、int (*fp)(int);
    fp是一个函数指针,它所指的函数带有一个int类型的参数,并且返回int类型的结果。
    int next( int n )
    {
        return n+1;  
    }
    可以使用下列任一条,使fp指向next函数:
        fp = &next;
        fp = next; 
    可以完全这样的操作:
        i = (*fp)(i); 
        i = fp(i); 
7、
8、数组:
    不能动态的增长或收缩,编译时期,必须知道数组汇总的元素个数。
    数组名:就是一个指向数组中首元素的指针。

9、字符串直接量
    一个字符串直接量实际上是一个const char数组,它包含的元素个数比字面上的字符数多1.--‘\0’
    字符串直接量就是一个指针,指向空字符终止的数组的首字符。

10、3中内存管理
    1)自动内存管理
        系统在运行时为这个局部变量分配所需内存,并子退出时释放。
        int* invalid_pointer()
        {
            int x; 
            return &x;  
        }
        这个函数会返回局部变量x的引用,但是,返回时,x所占的内存已经被释放。&x创建的指针现在是无效的。

    2)静态内存管理
        
        int* pointer_to_static()
        {
            static int x; 
            return &x; 
        }
        
        程序对x分配一次内存,而且只分配一次,并且只要程序在运行,我们就不释放这个变量的内存。
        
        warnning:每次返回一个相同的对象的指针
        
    3)动态分配内存
        删除一个零指针没有什么影响。

        int *p = new int(42); 
        ++*p;  //*p is now 43
        delete p; 
        int* pointer_to_dynamic()
        {
            return new int(0); 
        }
        调用这个函数的程序应该在适当的时候负责释放这个对象。


第十一章、定义抽象数据类型


1、当设计一个类时,一般都首先指定需要提供的接口。

2、内存分配
    使用new T[n] 不仅会分配好内存空间,还会调用T的默认构造函数来初始化每个元素。
    如果我们使用了new T[n],那么我们就对T强加了一个要求:只有当T有一个默认构造函数时,用户才能创建一个Vec<T>对象。

3、explicit:
    意思是:只有在用户明确地调用这个构造函数的地方,编译器才能使用这个构造函数,否则无法使用。
    这个关键字只在带有一个参数的构造函数的定义中有意义。

4、通常的,迭代器本身也是类。

5、一般来说,操作符函数可以是成员函数,也可以是非成员函数。
    然而,索引操作符是必须为成员函数的操作之一。

6、索引操作符可以找到底层数组的对应位置,然后返回这个位置的元素的引用。
    返回引用的原因:
        如果保持在容器中的对象很大的话,应该避免对这些对象进行复制,这样效率才更高。

7、类的作者可以控制对戏那个创建,复制,赋值以及销毁过程中发生的一切。如果没有定义这些操作,编译器会合成这些定义。--可能会导致莫名其妙的行为。

8、复制构造函数
    无聊是显示复制,还是隐式复制,都是有一个叫做复制构造函数的特殊构造函数来控制的。
    复制构造函数是一个成员函数。

    Vec(const Vec& v); 
    复制不应该改变被复制的对象,所以使用const。

7、如果我们复制指针的值,那么复制的指针和原先的指针就都指向相同的底层数据。
    warnning:其中一个数值的改变会引起另一个值的改变,因为它们指向的是同一个数据。
    解决方案:当复制一个Vec对象时,需要分配新的空间。

8、赋值操作符不同于复制构造函数的地方是:
    赋值操作符往往需要删掉左操作符已有的值,然后用新的值,也就是右操作符的值来替换。
    
    需要考虑自我赋值情况的处理。

9、Vec<T>::operator=( const Vec& rhs )
    {
        ...
        return *this; 
    }

    使用*this:
        this关键字只在成员函数内部有效。
    须确保返回时,引用的对象仍存在,因此不能返回局部对象的引用。

10、赋值不是初始化
    理解赋值和初始化之间的区别是学好C++的关键之一。
    初始化: 复制构造函数
    赋值:   operator=

    区别:
        赋值总会删除先前的值,而初始化不会这样。而且,初始化会创建一个新的对象,同时为这个对象提供一个值
    
    两者区别重要的原因:
        1)构造函数总是用来控制初始化
        2)operator=成员函数总是用来控制赋值操作

    初始化会发生在:
        1)变量声明中
        2)在函数如果中,传递函数参数的时候
        3)在函数返回语句中,返回一个值的时候
        4)在构造函数初始化列表中

    赋值发生在:
        在表达式中使用=操作符的时候
    
    如:
        string url_ch = "zerocool";         //initialization
        string spaces(url_ch.size(), '');   //initialization
        string y;                           //initialization
        y = url_ch;                         //assignment

11、析构函数:无人和参数与返回值,控制类的对象被销毁时发生的操作。

12、良好的习惯:为每个类提供一个 默认构造函数(可能是显式或隐式的提供)

13、如果类的作者没有定义赋值构造函数、赋值操作符、析构函数---编译器会生成默认版。
    默认版的函数会定义为递归操作————根据元素类型的适当规则、赋值、复制或者销毁数据元素。
    warnning:默认析构函数来销毁一个指针时,并不会释放指针指向的内存空间。

14、三者缺一不可的规则:
    由于复制构造函数、析构函数以及赋值操作符如此紧密的联系在一起,所以它们之间的关系就形成了一个三者缺一不可的规则:如果一个类需要一个析构函数,那么它也需要其他两个。

15、使用new:不仅会分配内存空间,还会把这些内存初始化(即使我们不适用这些元素)


第十二章、使类的对象像数值一样工作


本章重点讨论如何为类设计良好的接口。

1、友元
    用来使非成员函数读写成员数据。
    可以定义写成类定义中的任何地方):不管它是跟在private标签,还是public标签后,都没有什么区别。
    由于友元函数有特殊的访问权限,所以它是类的接口的一部分。因此(最好在类定义的开头位置,靠近类的公有接口的地方)

2、定义二元操作符
    非成员函数-->对称性
    把二元操作符定义为非成员函数就是一个很好的习惯。这样做,我们可以保持操作数之间的对称性。

    如果一个操作符是一个类的成员,那么这个操作符的左操作数就不能是自动类型转换的结果。

3、explicit
    一般来说,如果一个构造函数是用来定义对象的结构的构造方式,而不是定义对象内容的构造方式的话,这个构造函数就会被定义为explicit,、。如果够做函数的参数是对象的一部分的话,这种构造函数就不应该定义为explicit。

4、void*类型
    指向void的指针常常被叫做通用指针,这是因为这种指针可以指向任何类型的对象。
    当然,我们不能对这种指针解引用,原因是它指向的对象的类型还是未知的。
    但是我们可以把void*转换为bool类型。

5、类型转换和内存管理
    很多C++程序对系统的接口都是用C语言或者汇编语言来完成的,这些语言都是使用以空字符终止的字符数组来保存字符串数据的。

6、类型转化是通过非explicit的够做函数定义的,并且这种构造函数只带有一个单独的参数:类型转换也可以通过类型转换操作符来定义,形式为operator type-name()。



第十三、使用继承和动态绑定


1、继承是oop的基石。

2、系统是如何创建派生类的对象,
    系统首先会为这个对象分配空间。接下来,它会执行适当类型的构造函数来初始化这个对象。如果这个对象是派生类的对象,就需要给构造过程再加一步,那就是构造对象的基类部分。派生类的对象是按照下面的方式来构造的:
    1、为整个对象分配空间(包括基类成员和派生类成员)
    2、调用基类的构造函数来初始化对象中的基类部分
    3、使用构造函数初始化列表直接初始化派生类的成员
    4、如果派生类的构造函数的函数体中有语句,就执行这些语句

3、多态和虚函数
    关键字virtual只能用在类定义中。
        如果这个函数是在声明之外单独定义的话,我们就不需要在定义中重复关键字virtual了。

4、动态绑定
    虚函数的运行时选取只与什么时候通过引用或者指针来调用这个函数有关。
    如果通过一个对象(而不是通过对象的引用或者指针)来调用一个虚函数,当然可以在编译时知道这个对象的准确类型。一个对象的类型是确定的:它就是它定义时的类型,不会再运行时改变。
    相反,基类的引用或者指针可以引用或指向基类的对象,也可以引用或指向这个基类的派生类的对象,也就是说,引用和指针的类型以及引用和指针绑定的对象的类型,在运行时可能会改变。

5、静态绑定:在编译时绑定

6、动态绑定和静态绑定的区别是理解C++如何支持OOP的关键。
    如果我们用一个对象来调用一个虚函数,这个调用就是静态绑定,因为除了编译时期,对象的类型不可能在执行的过程中发生变化。
    相反,如果我们通过一个指针或者引用来调用一个虚函数,这个函数就是动态绑定,也就是说,是在运行时绑定的。在运行时,使用哪个版本的虚函数,取决于这个引用和指针绑定的对象的类型。

7、我们可以在需要指向基类的指针或引用的地方使用派生类类型,这个例子就是OOP的核心概念————多态。
    它的意思是,一个类型可以代表多种类型。
    C++是通过虚函数的动态绑定特性来支持多态的。

8、虚析构函数
    如果一个指向基类的指针被用来删除派生类的对象时,基类就需要一个虚析构函数。如果这个类没有别的理由需要一个析构函数的话,这个虚析构函数就必须被定义,而且函数体可以为空。
    虚析构函数也可以继承。

9、派生类没有必要中定义虚函数。
    如果一个类没有重定义虚函数,它就会继承这个函数在继承层次中离当前最近的定义。
    然而,首先出现在这个类中的虚函数必须被定义。

10、覆盖
    如果派生类的函数与基类一样,就可以覆盖基类的函数。

11、友元不能被继承,也不能传递。


from: http://blog.csdn.net/cyh_24/article/details/8276138

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值