cpp面向对象程序设计

cpp面向对象程序设计——侯捷

一、简介

  1. 目标: 正规的、大气的编程习惯
  2. 学习语言要学习,cpp语言和标准库两个部分
  3. c和cpp的区别
    • c语言 全局数据和函数
    • cpp 数据成员和函数成员
  4. 类可以分为带有指针和不带有指针
  5. 头文件需要包含防止重复声明的语句
  6. inline可以让函数很快,所以可以尽量写,但是编译器不一定实现的时候让函数称为内联函数,如果函数很复杂就会不是inline
  7. 建议所有的数据都应该放在private中
  8. 当然可以在构造函数中使用赋值的方法来,将构造函数中收到的函数值赋给变量,但是不够大气,应该使用构造函数的初始化列表的方法。一般赋值的方法都有两个阶段,先初始化再赋值,如果放在{}内实现,就相当于放弃了初始化的过程
  9. 编译器会把函数名和函数参数变成只有编译器才能看懂的形式,这种机制保障了函数可以重载。编译器内部绝对不允许两个同名的函数存在。
  10. 把构造函数放在private里面?为了实现单例模型,外界只能使用一次。使用的时候也不是普通的调用构造的方式
  11. 类中函数可以分为会改变成员数据和不会改变成员数据两种,对于不会改变成员数据的成员函数,要在{}实现之前加上const,这种函数叫常量成员函数。如果不加const,在使用的时候,如果加了const,那么就会编译器报错,当然,如果使用者不加const,也不会报错。
  12. 函数的参数尽量不要传值,在c中使用的是传指针,在cpp中有一种更好的方式,是传引用。当然,引用在内部的实现也是指针。传引用的时候,如果不希望你改,那么可以使用const complex &x。
  13. 函数的返回值也尽量传引用。
  14. 使用友元的方式,可以取到类中的数据,比使用类中的函数要快一些,但是这种方法会破坏封装性
  15. 相同的class中的各个成员互为友元friend,也就是类中的成员可以直接访问类的private数据
  16. 函数返回引用的时候,那个引用最好是已经存在了,也就是返回的那个引用必须要有位置存放,这样才可以。如果是在这个函数里面定义的引用,函数结束,引用直接消失了,就没有办法返回了
  17. 在cpp中操作符就可以看做是一种函数,这就涉及到操作符的重载了。
  18. 谁调用这个函数,this指针就指向谁,有的编译器this是第一个参数,有的是最后一个参数。
  19. 传递者无需知道接受者是以reference形式进行接受。
  20. 使用成员函数的运算符重载是有this指针的,使用非成员函数的运算符重载没有this指针。而且例子中使用成员函数的运算符重载可以返回引用,因为这个时候是知道返回的位置在哪的,但是使用非成员函数的运算符重载不能返回引用,只能返回值,因为这个时候不知道返回的结果在哪。
  21. 没有名称的complex()叫做临时变量,生命周期在下一行就没了,可以用来做函数的返回值,这样可以使用返回值优化
  22. << 的运算符重载返回的时候,不能写const,因为每一次的重载都在改变cout的状态。
  23. 要特别注意,this指针是一个保留的关键字,只能在类的内部调用,如果是在类的外面进行调用的时候,可以省略写成ths,不然就会一直报错!!!友元函数的实现可以看做是类中的一个函数。
  24. 带着指针的这种类,如果只是使用编译器给的这种,不能完成我们所需要的。因为会浅拷贝。类中带有指针,多半是因为要动态分配内存,所以需要释放,也就是delete。
  25. 拷贝构造,拷贝赋值,析构函数也叫做big three
  26. 拷贝构造:浅拷贝,两个指针指向同一个内容,很危险。深拷贝,以一个指针指向的内容为蓝本,创造另外一个同样的内容。
  27. 拷贝赋值 s2 = s1
  • s2先delete掉自己
  • 创建一块和s1一样大的地方
  • 将s1复制过来
  1. 拷贝构造如果是自我赋值需要自我检测,如果不检测,按照正常的拷贝赋值的方式就会出错,如果是自我赋值,直接返回指针就好了。如何看是不是自我赋值,不能只看内容,需要看的是指针是不是所指的位置。
  2. 为什么<< 的重载不能写成成员函数,因为如果写成成员函数,输出的内容s1和cout应该写成 s1 << cout, 这是不被接受的,所以需要写成全局函数。
  3. 栈:是存在于某一个作用域内的一块内存空间,在{}内,只要离开作用域生命周期就停止了。
    堆:是操作系统提供的一块全局内存空间,如果自己动态分配new,就需要手动释放delete。
  4. Complex * pc = new Complex(1, 2);的过程如下
    1
    2
    3
    void * mem = operator new( sizeof(Complex) );//分配内存
    pc = static_cast<Complex*>(mem);//转型
    pc->Complex::Complex(1, 2);//构造函数
    上面的operator new内部实际上调用的malloc函数
    delete pc的过程如下
    1
    2
    String::~String(ps); // 析构函数 
    operator delete(ps); // 释放内存
    上面的delete内部调用的是free(ps);
  5. 使用new分配内存的时候,在分配的位置上下会各有一个cookie,用来记录分配的地址由多大,因为你在delete的时候,只是给了一个指针,那么释放多大的内存呢,使用上下的cookie来记录。比如0x00000041,的最后一位表示的是分配了出去,如果是0表示没有分配出去,前面的4代表16进制,表示64,也就是分配了64个字节出去。上下两个cookie的值一般是一样的。
  6. 如果是在调试模式下的new,会在cookie下面多有一些调试信息,一般是32字节。然后分配的空间会凑到16的倍数。
  7. new String[3] 的时候,一定要使用delete[] p, 如果使用delete p就只删除了一次。
  8. 不管什么样的函数都可以写成inline,这种写法就是建议编译器将函数编译成内联函数,但是具体的能不能成功,实际上看编译器自己的优化方式。
  9. static静态的,静态的变量是属于一个类的,静态的函数不依赖于类的实例。
  10. cout为什么可以接受那么多的输出,因为在cout的实现过程由很多的重载。
  11. 复合,我的里面有你,我和你的关系就是复合。我的总大小就是我的大小加上你的大小。构造应该由内到外,析构的时候应该由外到内。
  • 构造的时候,先构造你,在构造我 Container::Container(…):Component(){…}; 当然构造的时候调用的是你的默认构造函数,因为你的构造函数可能由很多,编译器只能调用默认的构造函数,如果你想调用特定的构造函数,需要自己写
  • 析构的时候,先析构我,再析构你 Container::~Container(…){… ~Component() };
  1. 委托,我里面有的是你的指针,delegation(composition by reference),有点类似于陈伟课上说的依赖,但是不完全相同。我在编译的时候和你就没有关系,这种也叫做编译防火墙。我也叫Handle,你叫Body。这种方法可以做共享
  2. 继承,你是一种我,比如我是动物,你是狗,那么你是一种我,也就是狗是一种动物。
  3. 继承下面的构造与析构,可以看作是子类里面有父类的成分,也就是说父类大于子类,与上面的类似
  • 构造子类的时候,先创建父类再子类
  • 析构子类的时候,先析构子类再父类,注意,父类的析构函数必须是virtual,否则就会出现undefined behavior
  1. 函数的调用实际上调用的是父类函数的继承权
  2. 虚函数
  • 非虚函数,不希望派生类重新定义(override)它
  • virtual函数,希望派生类重新定义(override)它,且你对它已经有了默认的定义
  • pure virtual函数,希望派生类一定要重新定义(override)它,且你对它没有默认的定义
  1. 父类中的一些函数的具体实现,留在子类中去完成,子类使用的时候,由于子类是继承自父类,子类可以直接调用父类的函数。这样父类就可以由别人提前写好,这种方法叫做设计模式中的template method,也就是行为设计模式。当然,父类中的那个函数,需要是虚函数。
  2. 上面44所产生的最好的产品,就是mfc,是微软开发的cpp类库
  3. 子类A继承自父类B,同时子类A和类C是复合关系,那么构造子类A的时候,父类B先构造,然后是类C,最后是子类A。析构的时候,先是子类A,然后是类C,然后是父类B。
  4. 委托+继承是最强大的