C++面向对象程序设计复习笔记(上)
侯捷 C++面向对象程序设计的上半部分笔记
Class Without Pointer Members
一、头文件与类的声明
1. “.h” 和 “.cpp”
- “.h”和”.cpp”并不是绝对的,也有”.hpp”等后缀命名方式
- 两类 class,class with pointer members ,class with no pointer members
- C++的一个编译单元由一个.cpp 和它包含的.h 文件组成,编译单元在编译后,和其他编译单元进行链接生成可执行文件
2. 防卫式声明
1 | //头文件布局 |
- 防止重复包含,先写 1,2 然后可能需要写 0
3. inline(内联函数)
1 | class complex |
inline 写法:
1 | inline double |
- inline 函数的执行速度会变快,但 inline 只是对编译器的一种建议,能否变成 inline 函数要看编译器如何决定,一般足够简单的函数才可以变成 inline 函数
4. access level(访问级别)
1 | class complex |
1 | { |
- 所有数据和不准备被外界调用的函数都应该放在 private 中,外界不可以来拿(访问)private 里的数据
二、构造函数
1. constructor(ctor,构造函数)
- 使用初始化参数列表而不是赋值,初始化列表的执行在
{}
之前,Cpp 属于 Free Format 编程语言,可以随意换行,加任意个空格 - 构造函数无法被直接调用,在对象初始化时被调用
- 构造函数可以有很多个——overloading 重载(同名,但返回值和参数不同)
Q:为什么同一个名字的函数可以存在多个呢?编译器是如何知道要调用哪一个的呢?
A:在编译的时候,编译器会将函数的返回值、参数等编码,和函数名组成一个新的名称,所以 overload 的 function 在编译器中的名字是不同的,不存在两个名称相同的函数;但图中黄色部分的重载是不行的,因为不写对象初始化参数时,两个函数都可以被调用,编译器不知道调用哪个,就会报错
- 将构造函数写在 private 里,如单例模式
1 | class A { //Singleton单例模式 |
三、参数传递与返回值
1. const member function(常量成员函数)
- 不改变数据的函数一定要加 const,比如只是拿数据出来打印的函数
Q:why?
A:如果变量或对象前加 const,则对象或变量的内容是不可改的。如上图所示,如果对象前加了 const 是无法调用对象非 const 成员函数的,因为不加 const 的成员函数在编译器看来就意味着可能改变对象的值,所以就会报错。
2. pass by value vs. pass by reference(to const)(参数传递)
- pass by value,整包传过去。如上图 double,把值传过去,double 是四个字节,传递的参数也是四个字节,如果要传的参数是一百个字节那就传一百个字节。传参会占用堆栈空间,所以 C 里面就可以使用指针的形式来传递参数,一百个字节的参数只需要传递四个字节的地址就可以了。
- pass by reference,通过引用传递。如上图 ostream&,引用的底层是指针,但是外在表现得更漂亮,引用传递和指针一样快,就是传递指针的速度,尽量所有的参数传递都传引用,四个字节以上的参数都应该传引用,double 也可以传引用。
- pass by reference to const,通过不可修改的引用传递。因为引用实际上还是指针,所以如果函数把传入的参数改了,那么原来的数据也会受到影响,所以为了防止函数更改数据,要给引用加上 const,意味引用指向的数据不可以更改。
3. return by value vs. return by reference(to const)(返回值传递)
- 如果可以的情况下,返回值也应该用引用传递
- 当静态创建的空间随着函数结束而释放时,是无法返回引用的,只能返回值
4. friend(友元)
- 友元函数不属于类的成员函数,但友元函数可以来拿(访问)类中的 private 数据
- 不要用太多的 friend,因为这样会打破 C++封装的特性,可以通过类中的其他成员函数来拿类的 private 数据,虽然这样会慢一点
5. 相同 class 的各个 objects 互为 friends(友元)
四、操作符重载与临时对象
1. operator overloading -1(双目操作符重载,by 成员函数)
- 类中每个成员函数都有一个隐藏的参数 this,一般在函数参数列表的首位(位置与编译器有关)
- C++使用操作符时,相当于调用左操作数的重载函数,即 c2.operator+=(c1);
- assignment plus,+=
2. return by reference 语法分析
Q:传引用和传指针都是四个字节,为什么参数传递时使用传引用而不是传指针呢?
A:因为传引用的话,传递者不需要知道接受者是以 reference 的形式接收,这样的话就可以直接 return 对象,而传指针的话,就必须 return 对象的指针,这样就无法实现连续的+=了。如果不是为了实现连续的+=,重载函数其实可以不要返回值,直接用 void 就行了
3. operator overloading -2(双目操作符重载,by 非成员函数)
1 | //complex c1,c2,c3 |
- 对于如果操作符没有对应的成员函数重载,则查看对应的非成员函数重载
- 使用非成员函数进行 overload 则无法使用默认隐藏的 this,故参数有两个
- 如果非成员函数 overload 想直接访问类的 private 数据则应该将其添加为 friend
Q:为什么这里的 return 不 return 引用呢?
A:+不同于+=,+=会改变左操作数自己,而+不会,所以这里不能使用引用而因以 const complex&来传递进来参数,返回值自然也就只能使用值传递。+所得的值,必定是一个 local object,是之前不存在的,只能在函数里初始化,只能进行值传递。如果是=的话,则应该改变左
操作数,并 return 一个引用
4. temp object(临时对象,”typename()”)
- 上一个代码块中可以看到 return 后跟的是一个类似于创建 complex 对象的写法,这就是临时对象
- 临时对象的写法是”typename()”,创建一个临时的、无名的、下一行结束生命周期的临时变量。
5. operator overloading -3(单目运算符重载,非成员函数)
1 | inline complex |
- 负号是产生了一个新的数,是 local object,所以 return by value
- 单目运算符如+、-,左边没有操作数,”+c1”在 C++中相当于调用全局函数 operator+(c1)
- 任何一个操作符重载都可以写成成员函数或非成员函数的形式,不一定就哪种实现一定好,除了”<<”
6. operator overloading -4 (<<双目运算符重载,非成员函数)
- 双目运算符进行运算时,编译器调用左操作数的 overloading function,故我们要对 cout 进行重载
- cout 的类 ostream 封装在标准库文件中,不应该被改动,故使用非成员函数对<<进行重载
1 | ostream& operator<<(ostream& os,const complex& x) |
总结
1. 所有的数据放在 private 里
2. 使用初始化列表来初始化对象
3. 所有大于四字节的参数传递都用引用,除了 local object
4. 所有不想被修改的地方都要加 const
Class With Pointer Members
五、三大函数:拷贝构造,拷贝复制,析构
1. Implement A Class With Pointer Members(string.h)
- 构造函数,拷贝构造函数,拷贝复制函数,析构函数,编译器会默认给一套
Q:默认的一套有什么坏处?
A:默认的一套只是简单的将值复制了一份,如果 Class 中含有指针,那么得到的指针所指向的位置也是相同的,当其中一个对象释放掉之后,另一个指针就会报错。所以对于 Class With Pointer Members,我们需要自己写重写拷贝构造函数
- string 类的数据成员应该是一个指向字符串的指针,这样方便动态的创建不同大小的 string,如果是固定的数组,则会造成空间的浪费
- 在 32 位的环境下,一个指针是 4 个 bytes
2. Big Three(三个特殊函数)
1 | class string{ |
- const T _不能赋值给 T_,因为这样 const 就失去了意义
- 字符串自 C 语言以来的概念:即一个指向一个字符数组的指针,这个数组最末尾是’\0’
- 另外有得到两种字符串长度的设计方式,一个是在最末尾加’\0’,一个是在最开头加字符串长度
- 动态分配的内存要用 delete 手动释放,如果一个指针离开作用域后不 delete 掉就会发生内存泄漏,泄露的内存没有指针指向它
- 深拷贝和浅拷贝,浅拷贝拷贝地址,深拷贝拷贝地址里的内容
- strcpy(a,b),将指针 b 的内容拷贝到指针 a 指向的空间,需要 a 指向的空间足够容纳 b 指向的内容
- T&,&表示 reference;&object,&表示取地址
六、堆,栈与内存管理
0. Size of Variables
In 32bit,char:1bytes; short:2bytes; int、float、long、adress:4bytes;double:8bytes
In 64bit,char:1bytes; short:2bytes;int、float:4bytes;double、long、adress:8bytes
1. Stack(栈)
- 存在于作用域(scope)的一块内存空间(memory space)
- 当你调用函数,函数本身即会形成一个 stack 用来存放它所接收的参数,以及返回地址,在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述 stack,离开作用域自动释放。
2. Heap(堆)
- 又名 system heap,是指由操作系统提供的一块 global 内存空间,程序可动态分配(dynamic allocated)从中获得若干区块,需要手动释放。
3. Stack Object,Static local objects,Global objects,Heap objects
- Stack Object 在 scope 内定义,scope 结束自动释放
- Static local objects 在 scope 内定义,其生命在 scope 结束之后仍然存在,直到整个程序结束
- Global objects 在 scope 外定义,其生命在直到整个程序结束,其作用域是整个程序
- Heap objects 程序动态分配,需要手动释放
4. new: 先分配内存,再调用 ctor
5. delete:先调用 dtor,再释放内存
6. 内存分配
- 红色部分是 cookie,灰色部分是 debug header,绿色部分是数据占用空间,蓝色部分是填充空间
- 内存的分配需要是 16 的倍数,单位 bytes
- cookie 是用于告诉系统需要回收的空间的大小,如第一列,共 64 个 byte,64 在十六进制下是 40H,因为内存分配的大小为 16 的倍数,所以后四位一定是 0000,这样就可以借用最后一个 bit 来表示这块空间是给出去还是收回来,1 表示把这块空间给程序了,程序获得了这块空间
7. Array new 一定要搭配 Array delete
Q:为什么是(83)+(32 + 4)+ (42)+4?
A:这里的最后一个 4 是用来表示数组的长度(VC 下)
- delete 先调用析构,再删除空间,使用 delete 释放 new [ ],发生内存泄漏的不是 delete 的指针指向的空间,而是 new 的类数组里,后面的 class 的指针成员指向的空间
- so,如果 new [ ]创建的是 class without pointer member,则使用 delete 释放并不会造成内存泄漏,虽然不会,但也不要这样写,要养成好习惯。
七、扩展补充:类模板,函数模板,及其他
1. Static
- 上图黄色部分可写可不写,不写编译器会自动帮你加上去
- 关于 this point,所有的成员函数都有一个缺省的参数 T* this;所以在类调用自己成员函数时,也可以这样理解:
1 |
|
Q:为什么成员函数只有一份,而且成员函数没有参数也可以处理不同的数据呢?
A: 因为缺省的 this pointer 的存在,object 在调用 member function 时,会将自己的指针传入 member function
- Static data members 和 Static function members 只有一份;
Static data members 应用:银行 1000w 客户,存款是 data member,利率是 static data member
Static function members 应用:处理静态数据
Q:” Static function members 应用:处理静态数据 “ Why?
A:静态成员函数与普通成员函数的区别就是它没有 this pointer 的参数,所以它不能针对性地访问不同 object 的不同数据,它的任务就是处理 Static data members
1 | Account.h |
- 让变量获得内存的这一行叫定义
2. 把 ctors 放在 private 里
1 | class Singleton{ |
这种写法是一种设计模式,单例模式(singleton mode),上面已经提到过了,这里要注意的是 static 的用法
Q:为什么 static function members 里直接声明了一个局部变量还返回了它的引用?如果函数结束后,static 局部变量没有释放的话,那下一次调用该函数时不会重复定义吗?
A:” static singleton a;”中的 static 继承的是 C 语言的特性,在 C 语言中,static 有两个作用,对其他文件隐藏该文件的 static 全局变量 和 延长局部变量的生存周期。在第二种用法中,static 声明的局部变量只在语句第一次运行时被初始化,之后的”static singleton a;”都不会执行,此外 static 变量的生存周期与文件相同,随程序结束而被释放。
static 在 cpp 中还有第三种用法,那就是在类中使用,保证静态数据成员和函数成员的唯一性
3. Namespace
- namespace std{…} ,包装的一种策略,可以用于防止重名
4. Template
函数模板和类模板的一个区别,类模板在创建对象时需要声明对象的类型,而函数模板不需要,编译器会根据函数的参数自动推导使用的类型
5. More details
八、组合、委托与继承
1. Composition(组合),表示 has-a
1 | //adapter,将deque改装成queue |
,queue has a deque
2. Composition (组合)关系中的构造和析构
3. Delegation (委托),Composition by reference
1 | // file String.hpp |
Q:Why called “by reference”?not “by pointer”?
A:学术上没有 by pointer 的说法,所以叫 by reference
- Delegation 关系下,class with pointer 要注意重载拷贝构造、拷贝赋值和析构函数
- 以上的例子是一种非常经典的实现,calledpimpl,pointer to implement,又称 handle/body(左边是手柄,右边是主体),一个指针指向为我实现所有功能的 class,body 可以随意变动,而不影响 handle 方,handle 方不用再编译,只需要 body 方编译,相当于编译防火墙。
- “reference counting ”and “copy on write ”
4. Inheritance(继承),表示 is - a
1 | struct _List_node_base |
- 子类 is a 父类,子类继承父类的数据成员(表现在内存上)和方法(表现在调用权上)
5. Inheritance(继承)关系下的构造和析构
父类的 dtor 一定要是虚函数,这是为了防止内存泄漏,看下面的这种情况
1 | class Base |
以上代码会产生内存泄露,因为 new 出来的是 Derived 类资源,采用一个基类的指针来接收,析构的时候,编译器因为只是知道这个指针是基类的,所以只将基类部分的内存析构了,而不会析构子类的,就造成了内存泄露,如果将基类的析构函数改成虚函数,就可以避免这种情况,因为虚函数是后绑定,其实就是在虚函数列表中,析构函数将基类的析构函数用实际对象的一组析构函数替换掉了,也就是先执行子类的虚函数再执行父类的虚函数,这样子类的内存析构了,父类的内存也释放了,就不会产生内存泄露。
九、虚函数与多态
1. 三种虚函数
- non - virtual function:不希望 derived class override,同普通函数,若子类定义了父类的同名函数,那么父类函数在子类中会隐藏,父类指针指向的子类对象会调用父类函数,子类指针指向的子类对象会调用子类函数
- virtual function:希望 derived class override,且 base class 已经有默认定义,虚函数通常的两种用法,模版方法和多态
- pure virtual function:一定要 derived class override,base class 没有默认定义,后面要写”= 0”
2. derived class 调用父类函数
- CMyDoc 调用的父类的函数,通过 CDocument::OnFileOpen(&myDoc);谁调用的传谁的 this pointer,函数则是 base class 的函数,其参数列表缺省的 this 指针的类型是 base 型,传入的 this 指针是指向子类的指针,所以函数内的 this->Serialize()就是父类指针指向子类(向上转型)调用虚函数,符合动态绑定要求,运行时调用子类的函数。如果父类不加 virtual,就是静态绑定,编译时父类 OnFileOpen()中的 Serialize()就会变成”Call 父类 Serialize()的地址”,就变成了调用父类的 Serialize()。
- template method,设计模式之一,模板方法,延后实现,MFC就是使用的这种写法
3. Inheritance + Composition 关系下的 ctor and dtor
十、委托相关设计
1. Inhertance + Delegation,Composite(混合模式)
- vector 容器里放的东西大小需要一致,所以放指针
2. Inhertance + Delegation,Prototype(原型模式)
- 原型模式,提前做出一个子类对象
- 格式 name:type
- 这类图的下划线表示 static 量
- “-“表示 private,”#”表示 protected,”+”表示 public(也可以不写)
- 函数的继承表现在调用权上
1 | //Image.h |
如果想要动态创建一个对象,需要用到 new typename() 的方式,而父类是不知道 typename 的,这个 typename 只有未来诞生的子类知道,所以只能由子类来 new 一个自己,那父类如何获得这个对象呢?
答案是子类 new 自己时,在构造函数中将自己的联系方式交给父类,所以父类里需要一个数组来存储子类对象的指针 prototype ,父类通过这个指针就可以访问到子类。
但是类的数据成员是没有多态的,子类继承的 prototype 数组是具有独立内存的,如果子类创建和父类同名的数据成员也会覆盖掉继承的父类的数据成员(父类的数据成员仍然存在,但是需要用命名空间来 call),所以子类如果想向父类的 prototype 添加内容,需要将父类的 protoytype 数组设为 static(当然了),static 数据成员在继承体系内独一份,子类父类共享。
static 数据成员不被子类继承,父类子类共享,private 下的 static 成员除外
这样一来就可以将子类添加到父类里,父类中 prototype 是 private,所以需要提供给子类访问的接口,这个函数该怎么写呢?
首先它不需要是一个虚函数,因为所有子类进行的操作都一样,不需要多态;其次它仅仅只处理静态数据成员,所以最好是以个静态函数,这样的话,不需要创建对象也可以调用。那么这个函数就设计好了:
static voide addPrototype(Image * image){prototype[_nextSlot++] = image;}
static 数据成员最好交给 static 成员函数来处理
目前,我们实现了子类 new 自己时,会将自己交给父类,可是这与我们的目标父类创建未来的子类并不一致,这是未来的子类创建自己时,将自己交给父类。创建权在子类手里,父类并没有权力创建,但是既然父类拥有了子类的地址,那么这也不是件难事,只要子类给父类提供一个创建自己的接口就好了,接下来我们来设计这个函数。
首先每个子类的这个函数都不一样,因为每个子类的 new typename() 都不一样,所以需要多态,父类需要将这个函数设计成虚函数,交给子类实现多态,那么是不是纯虚函数呢?显然是纯虚函数,因为父类自己用不到这个函数,都是子类用来创建自己的,而且子类必须给父类提供一个创建自己的功能,每个子类都要实现,那么这个父类中这个函数就设计好了:
virtual clone() const = 0; //仅仅创建而不改变数据成员,所以是 const
利用虚函数来实现多态
子类只需要实现多态就行了,那么子类的 clone 中可以直接调用自己的构造函数来 clone 吗?这是不可以的,因为第一个被调用的 ctor 会将子类加入父类 prototype 数组,如果每个 clone 都调用这个 ctor,那父类 prototype 数组会存在大量子类地址,而我们实际上只要有一个地址就能满足需求了,所以需要重写一个不加入 prototype 的 ctor 供 clone 调用。
LandSatImage(int dummy){ _id =_count;} //dummy 是假参数,只是为了实现重载
_LandSatImage* clone(){return new LandSatImage(1);}
这样一来,我们的目标基本达到了,子类创建一个自己,通过调用父类的静态函数将自己的地址交给父类的静态数据成员,然后父类通过调用子类实现的虚函数来创建子类。但是还剩下两个问题:
- 目前只有子类对象创建过后,父类才能创建子类对象,有没有办法将这个过程提前呢?
- 父类又不知道子类的名称,所有子类在它眼里都是指针,如果来创建指定的子类呢?
我们继续改造,先看第一个问题,声明子类后,在程序里手动创建子类,这是一般情况,想要提前也是可以的,声明一个静态子类对象,在程序开始时就会初始化在静态变量区,这样就可以提前创建子类,父类就可以直接用了。
另外,因为子类的构造函数会将自己加入父类,而父类的空间有限,所以要将子类的构造函数设为 private,防止外界随意创建子类,子类的静态对象也应是 private,防止除父类外的人随意访问这个静态对象,通过 clone 函数来创建子类对象(clone 函数一定是 public 的,因为父类需要调用这个函数)。
下面看第二个问题,对于父类来说,每个 prototype 里的数据都是一个未知类型的指针,那如何选择想要的指针,创建想要的对象呢?可以通过枚举 enum 来实现。
父类先实现好定义几种枚举,子类则实现一个一个返回这些枚举类型的函数,这样就通过事先约定好的枚举名称解决了。
C++面向对象程序设计复习笔记(上)
https://fly.meow-2.com/post/note/houjie/object-oriented-programming.html