(本博客是笔者根据侯捷老师的课程进行的总结,便于自己复习,也分享于网上,便于和更多人学习交流,若有侵权,请告删,若有误,请不吝赐教)(图来自于pdf文档,需要的朋友可以在评论区留言)
主要分为两章:
Object Based (基于对象) vs. Object Oriented (面向对象)
Object Based : 面对的是单一class的设计。
Object Oriented : 面对的是多重classes的设计,classes和classes之间的关系
目录
第一章 Object Based (基于对象)
1、C++编程简介
2、头文件与类的声明
3、构造函数
4、参数传递与返回值
5、操作符重载与临时对象
6、中途小小总结 - 正规、大气的编码习惯
7、三大函数:拷贝构造、拷贝赋值、析构(以带指针成员的String类为例)
8、堆、栈与内存管理
9、扩展补充:静态成员
第二章 Object Oriented (面向对象)
1、组合与继承
2、虚函数与多态
3、委托相关设
· 以如下两个类为案例进行学习与分析
c++ class 最经典的一种分类就带指针和不带指针的。那么这里给出两个标准库中的例子,一个不带指针的类-complex,一个带指针的类-string。
· C++的历史
· C++的演化
(下面2 - 6小节以complex类为例展开阐述了一些类的特性和良好的编码习惯)
· C++ programs代码基本形式
区别就是标准库用尖括号<>,头文件用双引号"" 。
· Header(头文件)中的防卫式的声明(别人一看你这么写,就知道是正规的ITer)
例如complex.h中
# ifndef _COMPLEX_
# define _COMPLEX_
…
…
# ednif
(第二次进入进来,就不会重复声明)
那么总结起来主要四点:
a、防卫式声明:防止头文件被重复包含;
b、前置声明:声明头文件中用到的类和函数;
c、类声明: 声明类的函数和变量,部分简单的函数可以在这一部分加以实现;
d、类定义:实现前面声明的函数。
· inline(内联)函数
在c/c++中,为了解决频繁调用小的函数导致大量消耗栈空间的问题,引入内联函数,内联函数会直接在调用点展开(类似#define一个函数)。在类声明内定义的函数,自动成为inline函数;在类声明外定义的函数,需要加上inline关键字才能成为inline函数。但是是否能够真正的内联,还要看编译器的做法,inline只是编程者给编译器的一个建议,如果函数足够简单,我们就把它声明为inline就好了。
· access level(访问级别)
c ++类型中的数据和函数有三种访问级别:
private:只能被本类的函数访问、protected:能被本类的函数和子类的函数访问、public:可以被所有函数访问。数据最好被封装起来,不要被外界看到,即放在priate下。
· constructor(ctor,构造函数)
初值列,是一种大气的写法,只有构造函数才有这种写法。
· ctor(构造函数)可以有很多个 - overloading(重载)
注意3点:
(1)、c++中同名函数,即函数重载,但实际编译器很根据实际情况重新取名;
(2)、overloading常常出现在构造函数中;
(3)、图中构造函数2和构造函数1冲突;
· constructor(ctor,构造函数)被放到private区域
(这样就不允许外界创建对象)(那有什么用,见下面)
· 单例模式
· const member functions (常量成员函数)
成员函数可按照是否改变成员变量来划分,如果类的成员函数后面加了const关键字,说明这个函数体内是不能对成员变量进行更改。不会改变成员变量的函数名后面应该一律加上const。如果函数后面没有加const,而使用者创建对象时在对象前面加const,就会出错。所以,大气且正规的写法是,类的创建者应该在不改变成员函数的函数名后面一律加上const关键字。
· 参数传递:pass by value vs. pass by reference (to const)
by value : 整包传过去,不管数据多大,压到栈里面去。(如果数据很大,就很浪费空间,所以尽量不要by value)那么当数据很大时,把这个数据的地址传过去,就很省事。即指针或者引用。若不希望在函数体内对输入参数进行修改,应使用const修饰输入参数,且函数的参数应尽量使用引用传递。
· 返回值传递:return by value vs. return by reference (to const)
为提高效率,若函数的返回值是原本就存在的对象,则应以引用形式返回。若函数的返回值是临时变量,则只能通过值传递返回。
· friend (友元)
友元函数不受访问级别的控制,可以自由访问对象的所有成员。
· 相同 class 的各个 objects 互为 friends (友元)
同一类的各个对象互为友元,因此在类定义内可以访问其他对象的私有变量。
在C++中的操作符重载有两种形式,一种是在类中声明public函数实现操作函重载(这种情况下,操作符作用在左操作数上);另一种是在类外声明全局函数实现操作符重载。
1、在类中声明public函数complex& operator += (const complex&);
在内外实现定义:
complex::operator +=函数的参数和返回均使用引用,输入参数在函数体内不期望被更改,所以加上const修饰。函数体内调用友元函数__doapl,其第一个参数接受成员函数内隐式的this指针,其在函数体内会被更改,所以不应该加const,第二个参数接受重载函数的参数,不期望被更改,加上const修饰。重载函数的返回类型为complex&,是为了支持连加等的操作,如果为void,就不支持,如c3+=c2+=c1。
同时也说明使用引用传递参数和返回值的好处在于传送者无需知道接收者是否以引用形式接收,只需要和值传递一样写代码就行,不需要改动。再入下面的例子:
int& f(int* tmp) {
return *tmp;
}
int& f(int& tmp) {
return tmp;
}
2、在类外声明的全局函数complex operator + (const complex& x, const complex& y);
inline complex operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));
}
为什么这里不把+这个操作设为成员函数,因为+,不仅有复数+复数/实数,还有实数+复数/实数,如果某个对象是复数,就会受限于实数+复数/实数,如1 + (2, 3i)。
此处return complex(),返回的对象没有名称,是临时才要的,函数结束,就会被销毁。如右边加黄的临时变量,下一条语句时,临时变量就被销毁了。
注意:与重载+的考虑方法类似,<<操作符通常的使用方式是cout<
大气、正规的编程习惯:(编写类时,应该复合如下的要求)
1、数据一定要放在private下面;
2、参数尽可能的是以reference来传递,要不要加const看实际情况;
3、返回值如果可以的话,尽可能也以reference来传递;
4、在class的body里面的函数,该加const就加,否则使用者使用时报错,就是设计类没有设计好(例如,不希望且不会改变类的成员变量时一律加上const关键字);
5、构造函数,尽可能使用初值列的形式给成员变量赋值;
注意:const的两个个地方。
1、加在参数列表里,例如
const int& tmp tmp变量在函数体内不能被更改
const class_name& tmp tmp对象的成员在函数体内不能被更改
2、加在类中某个函数名后面
return_type fun() const {} 类的成员变量在函数体内不能被更改
· string.h中,其结构与complex.h类似。
· Big Three, 三个特殊函数
使用指针成员变量m_data管理String类中的字符串数据。
String(const String& str); // 函数接受的是自己类型的东西,称之为拷贝构造函数。
String& operator=(const String& str); // 拷贝赋值函数。
~String(); // 析构函数
拷贝构造与赋值构造的区别:
1、拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值构造函数时对于一个已经被初始化的对象来进行赋值操作。
2、拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个新对象。赋值构造函数是把一个新的对象赋值给一个原有的对象。
对于不带有指针的类,这3个函数可以使用编译器默认为我们生成的版本;但是编写带有指针的类时就有必要定义这3个特殊函数。
· ctor 和 dtor (构造函数 和 析构函数)
构造函数中执行数据的深拷贝和析构函数执行的释放。
· class with pointer members 必须有 copy ctor(拷贝构造) 和 copy op=(拷贝复制)
第二种属于浅拷贝,如果利用a指针改变了串Hello,那么b指向的也改变了,很危险。下面介绍深拷贝。
· 分析 copy ctor (拷贝构造函数)
先创建足够空间,然后把被拷贝的值复制过来。
· 分析 copy assignment operator (拷贝赋值函数)
1、先清空原来的数据,delete掉自己;
2、然后重新分配空间
3、拷贝过来
· 一定要在 operator= 中检查是否 self assignment
需要注意,这里检测自我赋值很重要,不可忽略。否则会把自己给删除了。
· 何谓 stack (栈), 何谓 heap (堆)
Stack,是存在于某作用域 (scope) 的一块內存空间(memory space)。例如当你调用函数,函数本身即会形成一个 stack 用来放置它所接收的参数,以及返回地址。在函数本体 (function body) 內声明的任何变量,其所使用的內存块都取自上述 stack。
Heap,或谓 system heap,是指由操作系统提供的一块 global 內存空间,程序可动态分配 (dynamicallocated) 从其中获得若干区块 (blocks)。
· stack objects 的生命期
c1 便是所谓 stack object,其生命在作用域 (scope) 结束之际结束。这种作用域内的 object,又称为 auto object,因为它会被“自动”清理。
· static local objects 的生命期
c2 便是所谓 static object,其生命在作用域 (scope)结束之后仍然存在,直到整个程序结束。
· global objects 的生命期
c3 便是所谓 global object,其生命在整个程序结束之后才结束。你也可以把它视为一种 static object,其作用域是“整个程序”。
· heap objects 的生命期
左侧,P 所指的便是 heap object,其生命在它被 deleted 之际结束。
右侧,出现内存泄漏 (memory leak),因为当作用域结束,p 所指的 heapobject 仍然存在,但指针 p 的生命却结束了,作用域之外再也看不到 p(也就没机会 delete p)
· new:先分配 memory, 再调用 ctor
· 动态分配所得的内存块 (memory block), in VC
第一个表,是在调试模式下获得的内存大小,52个字节,但是分配时会按照16的倍数分配,多加几个pad,所以会分到64个。其中,00000041是十六进制,刚好表示65,那么大分配小为64,最后一位为1,表示被分配出去了。后面的表格同样。第二个表,是在Release模式下,就没有灰色的。
· 动态分配所得的 array
· array new 一定要搭配 array delete
· static
非静态成员,每一个对象有一份,静态成员只有一份,非静态函数和静态函数都只有一份,不同的是,非静态函数访问成员时,会有一个默认的this point指针,而静态函数没有,那么它就不能像非静态函数一样去处理非静态成员,他只能处理静态成员。
静态成员需要在类外面加 double Account::m_rate = 8.0这句(定义)
类之间的关系有复合(composition)、委托(aggregation)和继承(extension)3种,下面一一介绍。
· Composition (复合), 表示 has-a
deque是已经有的东西,客户想要一个新的东西,但是功能和deque差不多,于是把deque拿来稍微改造,适配一下,即这种模式 - 适配模式。(queue中的每个功能都用C来实现)
那么queue中有deque,则queue复合了deque,has-a的关系。
· 从内存角度分析Composition (复合)
构造由内向外,析构由外向内。
· Delegation (委託). Composition by reference.
用指针has就叫委托。
· Inheritance (继承), 表示 is-a
· Inheritance (继承) 关係下的构造和析构
· Inheritance (继承) with virtual functions (虚函数)
· Inheritance (继承) with virtual
前面几个步骤不是固定的,只有open file是根据选择的文件而不同,因此前面几个步骤写成父类。
· Inheritance+Composition 关係下的构造和析构
这种情况的构造函数和析构函数可以做一个实验验证一下。
· Delegation (委託) + Inheritance (继承)
图中四幅图像,以及右边的三幅图像,都是同一份数据,但是展示的不同形状,只要数据一变化,图像也会跟着变化,这就是观察者模式,需要用一个指针指向数据,即委托。
紫色框中的Composite类,继承了Componet类,同时还可以加Primitive,因为Primitive也继承了Componet类。而Composite可以add Componet指针。