面向对象高级编程
- 一、面向对象高级编程上
- (1)C++代码基本形式
- (2)Header中的防卫式声明
- (3)不带指针类的实现过程
- 1. 防卫式声明
- 2. 访问级别
- 3.构造函数
- 4.重载
- 4.1 成员函数(有this)
- 4.2 非成员函数(无this)
- 4.3 重载输入输出流
- 5.常量成员函数
- 6.参数传递和返回值传递
- 6.1 参数传递
- 6.2 返回值
- 7.友元
- 8. inline函数
- (4) 带指针类的实现过程
- 1. 防卫式声明
- 2. Big Three
- 2.1 构造函数
- 2.2 拷贝构造
- 2.2 拷贝赋值
- 2.3 析构函数
- 3.输出函数
- (5) 堆、栈与内存管理
- 1. 栈的生命期
- 2. 堆的生命期
- (6) new和delete
- 1.new
- 2.delete
- (7) static
- 单例
- (8) 模板
- 1.类模板
- 2.函数模板
- (9) namespace
- (10) 类与类之间的关系(面向对象的思想)
- 1.继承
- 从内存角度看
- 2.复合
- 从内存的角度看
- composition关系下的构造和析构
- 3.委托
- (11) 虚函数与多态
- Template Method
- 继承+复合关系
- 继承+委托关系
- observer
一、面向对象高级编程上
(1)C++代码基本形式
(2)Header中的防卫式声明
C++头文件中的防卫式声明(也称为包含卫士或头文件保护)是为了防止头文件内容在同一编译单元中被多次包含。这种做法通常使用预处理器指令实现,如下所示:
防卫式声明的主要目的和优点包括:
避免重复包含:在一个项目中,多个源文件可能都会包含相同的头文件。如果头文件被多次包含,它的内容(如变量定义、函数声明等)将在同一编译单元中出现多次,从而导致编译错误。防卫式声明确保了即使头文件被多次包含,其内容也只被处理一次。
防止编译错误:没有防卫式声明的头文件如果包含多次,可能会引起重复定义的错误。例如,重复的函数定义、类定义或全局变量定义都会导致编译错误。
提高编译效率:通过防止头文件内容的重复包含,可以减少编译器的工作量,提高编译效率。
模块化设计:防卫式声明支持模块化设计,允许开发者在多个文件中重用相同的头文件,而不必担心重复包含的问题。
(3)不带指针类的实现过程
1. 防卫式声明
2. 访问级别
思考哪些类型的数据,放在private下,哪些数据放在public下。
思考需要哪些函数,哪些放在public下,哪些放在private下。
3.构造函数
- 不需要有返回值,是否需要默认值,参数如何传递(加不加引用),初始列(只有构造函数有),是否需要在函数体内做其他一些事情。
- 构造函数的访问级别是否设为私有
静态局部变量 a。由于其是静态的,它只会被初始化一次,即在第一次调用 getInstance() 方法时。
静态局部变量 a 的生命周期是从它被创建开始直到程序结束。
每次调用 getInstance() 方法时,都会返回对同一个静态局部变量 a 的引用。任何后续对 getInstance() 的调用都返回对同一个实例的引用,而不会创建新的对象。
4.重载
- 注意是否有默认值
4.1 成员函数(有this)
在class中的每一个成员函数都默认有一个不可见的参数this
,this指向了当前成员函数的调用者。
4.2 非成员函数(无this)
我们想实现复数的三种加法运算符重载。这三种函数绝不可以return by reference,因为他们返回的必然是local对象,在函数体执行完后释放。
语法:typename();
4.3 重载输入输出流
两种选择,一种是成员函数,一种是非成员函数。
当你重载一个操作符作为成员函数时,第一个参数总是调用它的对象实例本身,这对于大多数操作符是适用的,因为它们通常是以对象为中心的操作,比如 obj + something 或 obj == another_obj。
但是,对于流操作符 << 而言,通常的用法是将对象写入输出流,如 std::cout << obj。在这种情况下,输出流对象(如 std::cout)应当是操作符的第一个参数,而要输出的对象实例应当是第二个参数。因为 std::cout 是 std::ostream 类的实例,而不是你自定义类型的实例,所以不能将 << 作为你自定义类型的成员函数来重载,因为这会要求 std::ostream 对象作为成员函数的调用者。
写成 成员函数,用法不符合逻辑,因此只能写为非成员函数。
5.常量成员函数
class里面的函数分为 会改变数据 和 不会改变数据 两种。数据就是this指针所指向的成员变量。
不会改变数据内容的,加上const,用于声明某个成员函数不会修改对象的状态,即这是个只读函数。
6.参数传递和返回值传递
三种:值传递、引用传递、const修饰的引用传递。
6.1 参数传递
6.2 返回值
那么什么时候return by value?
如果在函数内部创建了一个变量,并且需要返回该变量,那么考虑值传递,因为当该变量在栈区,函数执行结束后就释放了。
7.友元
8. inline函数
函数若在class body内定义完成,便成为inline候选。
是否可以inline由编译器决定,无论加不加inline关键字。
(4) 带指针类的实现过程
1. 防卫式声明
2. Big Three
字符串里的字符有大有小,所以不能用字符数组来存储,因此在需要的时候开辟内存空间,将指针指向字符串即可(动态分配方式)。
如果想要实现这样的功能,那么构造函数怎么设计?
2.1 构造函数
2.2 拷贝构造
如果不重写拷贝构造或者拷贝赋值,那么编译器默认的拷贝构造和拷贝赋值函数,是简单的将一个对象的值赋值给另一个对象,也就是说,在有指针成员的情况下,两个对象最后指针指向同一块内存空间,这就是浅拷贝问题。我们想要的是,指向两个不同的独立的内存空间,虽然内存空间里存放的值一样。
浅拷贝会带来堆区的内存重复释放,多次调用析构函数会造成程序崩溃。也会造成内存泄漏,b所指向的空间丢失了。
避免这种情况,也就是深拷贝,如下:
这两种都是调用了拷贝构造函数。
2.2 拷贝赋值
**注意检测自我赋值。**为了正确性与效率。
2.3 析构函数
3.输出函数
在重载<<之前,因为我们实现的是全局函数,又不想声明成MyString类的友元,因此我们需要写一个取得字符指针的函数。
因为ostream可以直接接收字符指针,因此我们取得字符指针传递给ostream即可。
(5) 堆、栈与内存管理
何谓堆、栈?
1. 栈的生命期
栈是存在某作用域的一块内存空间中。例如当调用函数,函数本身会形成一个stack来存放它接收的参数,以及返回地址。在函数体内声明的任何变量,其所使用的内存块都取自于stack。
c1就是所谓的stack object,其生命在作用域结束后结束,又称为auto object,会自动被清理(调用析构函数)。
c2是静态对象,作用域结束后生命也不会结束,直到程序结束后,结束生命。
c3是全局对象,其生命在整个程序结束后结束。
2. 堆的生命期
new:先分配memory,再调用构造函数。
delete:先调用析构函数,再释放内存。
array new一定要搭配array delete。
(6) new和delete
1.new
2.delete
析构函数先将字符串指针m_data指向的动态分配的空间销毁,之后再释放指向MyString对象的指针。
注意:array new 一定要搭配 array delete。
(7) static
在没有使用static声明对象的时候,我们创建了三个复数对象c1、c2、c3,这三个对象调用了同一个函数real,但是编译器怎么知道是哪个对象调用的函数real,这个时候凭借的就是this指针。所有成员函数都隐含一个this指针。谁调用real,谁就是this,所以要传入地址。
比如c1调用real,那么传入c1的地址,real里的this指针就是c1。因此不同的对象调用real,传入的this是各自的对象,不同的。
在调用real函数时候,对象的this指针自动传递进去,因此编译器知道是哪个对象调用该函数。
然而,加了static后的数据,与对象脱离,不属于对象,单独在内存中开辟空间。
多个对象拥有同一个静态数据,不属于任何一个对象所独有。比如说:银行系统中的利率,银行的账户对象有多个,但是每个账户共享同一个利率。
静态函数没有this指针,因此不能够像一般函数一样去访问对象中的普通数据,所以只能访问静态数据。
如何调用静态函数?
静态数据成员的定义必须在类外部进行,而且需要在定义时加上数据类型。
单例
诉求:只产生一个对象。
外界无法创建A的对象,因此要提供一个接口可以使外界能取到唯一的A的对象。
但是如果外界一直不使用a,那么这样有点浪费。
这样写的好处是,如果没有人用这个单例,那么该对象就不存在。如果有人使用这个单例,那么该对象有且仅有一份。
(8) 模板
1.类模板
语法:
在类声明中,欲采用通用数据类型的数据成员、成员函数的参数或返回类型前面需要加上类型参数。
2.函数模板
语法:
(9) namespace
东西包装在命名空间中。可以保证同名的东西不会交叉,在各自的命名空间中封装。
(10) 类与类之间的关系(面向对象的思想)
1.继承
inheritance,表示is-a
继承的特性:
- 父类的数据是可以完整继承下来的。
从内存角度看
子类的对象中含有父类的成分。
2.复合
Compositon,表现为has-a
比如,queue这个class中有个变量c,c是一个deque类型。那么queue和deque表现出来的关系就是复合。
Adapter:已有完善、功能很强大的类,根据客户需求修改接口来实现具有一小部分功能的类(改造一下),所使用的接口都是已有的类里的。
从内存的角度看
composition关系下的构造和析构
构造就像搭积木,一层一层由内像外。因此在调用container的构造函数时候,第一步先调用component的构造函数。
析构就像剥洋葱,由外向内一层一层剥开。因此最后才调用component的析构函数。
3.委托
两个类之间的关系是用指针相连。
如图所示:String类只是一个对外的接口,具体的实现都在StringRep类中。
有三个不同的String对象同时指向"Hello",那么是共享同一份数据。
特点就是:读时共享,写时拷贝。copy on write。如果a想要改变内容,那么单独提供一份副本给a,b、c继续指向同一个"Hello"。
(11) 虚函数与多态
函数继承的是调用权,子类可以调用父类的函数。
重点在于:子类需不需要重新定义父类的函数。
- 非虚函数:子类不可以重写。
- 虚函数:子类可以重写,不重写的话也有默认的定义。
- 纯虚函数:子类一定要重写,否则编译不通过。
- 空函数:子类不需要重写。
Template Method
父类设计好某些功能的通用、一般性的实现,只留无法决定的函数交给子类去重写。
如图,创建一个子类的对象myDoc,调用父类的函数OnFileOpen(),在OnFileOpen函数的执行过程中,发现Serialize函数被子类重写了,因此转过去执行子类重写的该函数,最后再回到父类的OnFileOpen中执行。
继承+复合关系
构造和析构
继承+委托关系
observer
什么情况需要用到这种设计模式?
假设我们有一份数据,但是需要用多种方式对这份数据进行查看。
如图所示,我们只有一份数据,但是需要用多种不同的方式对数据进行查看(直观、柱状图、折线图等等)。拥有同一份数据的副本我们就可以想到委托的特点读时共享。