读书笔记(鹏)

C++编程思想重点笔记(下)

2014-12-02  本文已影响109人  小敏纸

上篇请看:C++编程思想重点笔记(上)

  1. 宏的好处与坏处

三个有用的特征:字符串定义、字符串串联和标志粘贴。

 **字符串定义**的完成是用#指示,它容许设一个标识符并把它转化为字符串,然而**字符串串联**发生在当两个相邻的字符串没有分隔符时,在这种情况下字符串组合在一起。在写调试代码时,这两个特征是非常有效的。 
```cpp
#define DEBUG(X) cout<<#X " = " << X << endl
```
 上面的这个定义可以打印任何变量的值。
 我们也可以得到一个跟踪信息,在此信息里打印出它们执行的语句。
 ```cpp
 #define TRACE(S) cout << #S << endl; S
 ```

#S定义了要输出的语句。第2个S重申了语句,所以这个语句被执行。当然,这可能会产生问题,尤其是在一行for循环中。
cpp for (int i = 0 ; i < 100 ; i++ ) TRACE(f(i)) ;
因为在TRACE( )宏里实际上有两个语句,所以一行for循环只执行第一个。
cpp for (int i = 0 ; i < 100 ; i++ ) cout << "f(i)" << endl; f(i); // 第二条语句脱离了for循环,因此执行不到
解决方法是在宏中用逗号代替分号

 **标志粘贴**在写代码时是非常有用的,用##表示。它让我们设两个标识符并把它们粘贴在一起自动产生一个新的标识符。例如:
 ```cpp
 #define FIELD(A) char *A##_string;int A##_size  
 ```
 此时下面的代码:
 ```cpp
 class record{  
       FIELD(one);  
       FIELD(two);  
       FIELD(three);  
       //...  
 };
 ```
 就相当于下面的代码:
 ```cpp
 class record{  
       char *one_string,int one_size;  
       char *two_string,int two_size;  
       char *three_string,int three_size;    
       //...  
 };
 ```

输出:

a = 4 band(++a)0 a = 5
a = 5 band(++a)8 a = 8
a = 6 band(++a)9 a = 9
a = 7 band(++a)10 a = 10
a = 8 band(++a)0 a = 10
a = 9 band(++a)0 a = 11
a = 10 band(++a)0 a = 12

  1. 存储类型指定符
    常用的有staticextern
    不常用的有两个:一是auto,人们几乎不用它,因为它告诉编译器这是一个局部变量,实际上编译器总是可以从 变量定义时的上下文中判断出这是一个局部变量。所以auto是多余的。还有一个是register,它也是局部变量,但它告诉编译器这个特殊的变量要经常用到,所以编译器应该尽可能地让它保存在寄存器中。它用于优化代码。各种编译器对这种类型的变量处理方式也不尽相同,它们有时会忽略这种存储类型的指定。一般,如果要用到这个变量的地址, register指定符通常都会被忽略。应该避免用register类型,因为编译器在优化代码方面通常比我们做得更好。

  2. 位拷贝(bitcopy)与值拷贝的区别(很重要)
    由1个例子来说明:一个类在任何时候知道它存在多少个对象,可以通过包含一个static成员来做到,如下代码所示:

    #include <iostream>
    using namespace std;
    class test {
        static int object_count;
    public:
        test() {
            object_count++;
            print("test()");
        }
        static void print(const char *msg = 0) {
            if(msg) cout << msg << ": ";
            cout << "object_count = " << object_count << endl;
        }
        ~test() {
            object_count--;
            print("~test()");
        }
    };
    int test::object_count = 0;
    // pass and return by value.
    test f(test x) {
        x.print("x argument inside f()");
        return x;
    }
    int main() {
        test h;
        test::print("after construction of h");
        test h2 = f(h);
        test::print("after call to f()");
        return 0;
    }
    

然而输出并不是我们期望的那样:

test(): object_count = 1
after construction of h: object_count = 1
x argument inside f(): object_count = 1
~test(): object_count = 0
after call to f(): object_count = 0
~test(): object_count = -1
~test(): object_count = -2

在h生成以后,对象数是1,这是对的。我们希望在f()调用后对象数是2,因为h2也在范围内。然而,对象数是0,这意味着发生了严重的错误。这从结尾两个析构函数执行后使得对象数变为负数的事实得到确认,有些事根本就不应该发生。

让我们来看一下函数f()通过传值方式传入参数那一处。原来的对象h存在于函数框架之外,同时在函数体内又增加了一个对象,这个对象是传值方式传入的对象的拷贝,这属于位拷贝,调用的是默认拷贝构造函数,而不是调用构造函数。然而,参数的传递是使用C的原始的位拷贝的概念,但test类需要真正的初始化来维护它的完整性。所以,缺省的位拷贝不能达到预期的效果。

当局部对象出了调用的函数f()范围时,析构函数就被调用,析构函数使object_count减小。 所以,在函数外面, object_count等于0。h2对象的创建也是用位拷贝产生的(也是调用默认拷贝构造函数),所以,构造函数在这里也没有调用。当对象h和h2出了它们的作用范围时,它们的析构函数又使object_count值变为负值。

总结:

为了达到我们期望的效果,我们必须自己定义拷贝构造函数:

test(const test& t) {
 object_count++;
 print("test(const test&)");
}

这样输出才正确:

test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0

引申

test(): object_count = 1
after construction of h: object_count = 1
test(const test&): object_count = 2
x argument inside f(): object_count = 2
test(const test&): object_count = 3
~test(): object_count = 2
after call to f(): object_count = 2
test(const test&): object_count = 3
x argument inside f(): object_count = 3
test(const test&): object_count = 4
~test(): object_count = 3
~test(): object_count = 2
~test(): object_count = 1
~test(): object_count = 0

  1. 非自动继承的函数
    构造函数、析构函数和赋值函数(operator=)不能被继承。

  2. 私有继承的目的
    private继承的目的是什么,因为在类中选择创建一个private对象似乎更合适。将private继承包含在该语言中只是为了语言的完整性。但是,如果没有其他理由,则应当减少混淆,所以通常建议用private成员而不是private继承
    然而,private继承也不是一无用处。
    这里可能偶然有这种情况,即可能想产生像基类接口一样的接口,而不允许处理该对象像处理基类对象一样。private继承提供了这个功能

引申

能对私有继承成员公有化吗?
当私有继承时,基类的所有public成员都变成了private。如果希望它们中的任何一个是可视的,可以办到吗?答案是可以的,只要用派生类的public选项声明它们的名字即可(新的标准中使用using关键字)。
cpp #include <iostream> class base { public: char f() const { return 'a'; } int g() const { return 2; } float h() const { return 3.0; } }; class derived : base { public: using base::f; // Name publicizes member using base::h; }; int main() { derived d; d.f(); d.h(); // d.g(); // error -- private function return 0; }
这样,如果想要隐藏这个类的基类部分的功能,则private继承是有用的

  1. 多重继承注意向上映射的二义性。比如base(有个f()方法)有两个子对象d1和d2,且都重写了base的f()方法,此时子类dd如果也有f()方法则不能同时继承自d1和d2,因为f()方法存在二义性,不知道该继承哪个f()方法。
    解决方法是对dd类中的f()方法重新定义以消除二义性,比如明确指定使用d1的f()方法。
    当然也不能将dd类向上映射为base类,这可以通过使用虚继承解决,关键字virtual,base中的f()方法改成虚函数且d1和d2的继承都改为虚继承,当然dd继承d1和d2用public继承即可。

  2. C语言中如何关闭assert断言功能?
    头文件:<assert.h>或<cassert>
    在开发过程中,使用它们,完成后用#define NDEBUG使之失效,以便推出产品,注意必须在头文件之前关闭才有效。

    #define NDEBUG
    #include <cassert>
    
  3. C++如何实现动态捆绑?—即多态的实现(很重要)
    C++中为了实现多态,编译器对每个包含虚函数的类创建一个表(称为VTABLE,虚表)。在 VTABLE中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。
    为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。

在vtable表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址。
下面举个例子说明:
```cpp
#include <iostream>
enum note { middleC, Csharp, Cflat };

class instrument {
public:
    virtual void play(note) const {
        cout << "instrument::play" << endl;
    }
    virtual char* what() const {
        return "instrument";
    }
    // assume this will modify the object:
    virtual void adjust(int) {}
};

class wind : public instrument {
public:
    void play(note) const {
        cout << "wind::play" << endl;
    }
    char* what() const {
        return "wind";
    }
    void adjust(int) {}
};

class percussion : public instrument {
public:
    void play(note) const {
        cout << "percussion::play" << endl;
    }
    char* what() const {
        return "percussion";
    }
    void adjust(int) {}
};

class string : public instrument {
public:
    void play(note) const {
        cout << "string::play" << endl;
    }
    char* what() const {
        return "string";
    }
    void adjust(int) {}
};

class brass : public wind {
public:
    void play(note) const {
        cout << "brass::play" << endl;
    }
    char* what() const {
        return "brass";
    }
};

class woodwind : public wind {
public:
    void play(note) const {
        cout << "woodwind::play" << endl;
    }
    char* what() const {
        return "woodwind";
    }
};

instrument *A[] = {
    new wind,
    new percussion,
    new string,
    new brass
};
```

下图画的是指针数组A[]。

指针数组A
下面看到的是通过instrument指针对于brass调用adjust()。instrument引用产生如下结果:
动态绑定
编译器从这个instrument指针开始,这个指针指向这个对象的起始地址。所有的instrument对象或由instrument派生的对象都有它们的VPTR,它在对象的相同的位置(常常在对象的开头),所以编译器能够取出这个对象的VPTR。VPTR指向VTABLE的开始地址。所有的VTABLE有相同的顺序,不管何种类型的对象。 play()是第一个,what()是第二个,adjust()是第三个。所以编译器知道adjust()函数必在VPTR + 2处。这样,不是“以instrument :: adjust地址调用这个函数”(这是早捆绑,是错误活动),而是产生代码,“在VPTR + 2处调用这个函数”。因为VPTR的效果和实际函数地址的确定发生在运行时,所以这样就得到了所希望的晚捆绑。向这个对象发送消息,这个对象能断定它应当做什么。

引申 — 对象切片###

当多态地处理对象时,传地址与传值有明显的不同。所有在这里已经看到的例子和将会看到的例子都是传地址的,而不是传值的。这是因为地址都有相同的长度,传派生类型(它通常稍大一些)对象的地址和传基类(它通常小一点)对象的地址是相同的。如前面解释的,使用多态的目的是让对基类对象操作的代码也能操作派生类对象。
如果使用对象而不是使用地址或引用进行向上映射,发生的事情会使我们吃惊:这个对象 被“切片”,直到所剩下来的是适合于目的的子对象。在下面例子中可以看到通过检查这个对象的长度切片剩下来的部分。
cpp #include <iostream> using namespace std; class base { int i; public: base(int I = 0) : i(I) {} virtual int sum() const { return i; } }; class derived : public base { int j; public: derived(int I = 0, int J = 0) : base(I), j(J) {} virtual int sum() const { return base::sum() + j; } }; void call(base b) { cout << "sum = " << b.sum() << endl; } main() { base b(10); derived d(10, 47); call(b); call(d); }
函数call( )通过传值传递一个类型为base的对象。然后对于这base对象调用虚函数sum( )。 我们可能希望第一次调用产生10,第二次调用产生57。实际上,两次都产生10。 在这个程序中,有两件事情发生了

  1. RTTI—运行时类型识别(很重要)

运行时类型识别(Run-time type identification, RTTI)是在我们只有一个指向基类的指针或引用时确定一个对象的准确类型

也可以用before(typeinfo&)查询一个typeinfo对象是否在另一个typeinfo对象的前面(以定义实现的排列顺序),它将返回true或false。如果写:
cpp if(typeid(me).before(typeid(you))) //...
那么表示我们正在查询me在排列顺序中是否在you之前。
- RTTI的第二个用法叫“安全类型向下映射”。使用dynamic_cast<>模板。

 **两种方法的使用举例如下:**
```cpp
#include <iostream>
#include <typeinfo>
using namespace std;
class base {
    int i;
public:
    base(int I = 0) : i(I) {}
    virtual int sum() const { return i; }
};
class derived : public base {
    int j;
public:
    derived(int I = 0, int J = 0) : base(I), j(J) {}
    virtual int sum() const { return base::sum() + j; }
};
main() {
    base *b = new derived(10, 47);
    // rtti method1
    cout << typeid(b).name() << endl; // P4base
    cout << typeid(*b).name() << endl; // 7derived
    if(typeid(b).before(typeid(*b)))
        cout << "b is before *b" << endl;
    else
        cout << "*b is before b" << endl;
    // rtti method2
    derived *d = dynamic_cast<derived*>(d);
    if(d) cout << "cast successful" << endl;
}
```
 **注意1:**这里如果没有多态机制,则RTTI可能运行的结果不是我们想要的,比如如果没有虚函数,则这里两个都显示base,一般希望RTTI用于多态类。

注意2:运行时类型的识别对一个void型指针不起作用。void *确实意味着“根本没有类型信息”。
cpp void *v = new stimpy; stimpy* s = dynamic_cast<stimpy*>(v); // error cout << typeid(*v).name() << endl; // error

上一篇 下一篇

猜你喜欢

热点阅读