侯捷 C++学习笔记之面向对象高级编程(一)
侯捷 C++学习笔记之面向对象高级编程(一)
头文件与类的声明
class template 类(模板)简介
- 例如我们定义下面的一个类,我们可以不把一个类里面定义的变量类型(如:
double
)直接就写死,而是可以定义一个模板template
,具体如下面的例子:
1 |
|
- 当我们具体需要使用上面的的类并定义确定的变量类型的时候可以进行如下操作:
1 |
|
inline (内联)函数
- 若函数在
class
本体内部定义(并非声明)完成,就会进入内联函数的候选,但是该函数是否真正变成内联函数由编译器所决定。 - 内联函数具有执行效率比较高的特性,但并非所有定义在
class
本体内的函数都能由编译器编译为内联函数。
1 |
|
- 要么在外部定义函数的时候加上
inline
前缀,如:
1 |
|
构造函数
- 构造函数是用来创建对象的函数,其有如下特点:
- 不需要返回类型
- 具有初始列(
initialization list
),别的函数没有
1 |
|
- 使用构造函数创建对象,在创建对象的过程中自然而然地使用构造函数,例:
1 |
|
函数重载(overloading)
- 同名的函数可以有多个,其传入的参数不一样,称为函数重载(在编译器看来是不同名的)。
- 例如:
- 从上图可以看出,当同名的函数传入的参数类型,数量等不一致时,编译器可以区分出这两个函数,称为函数重载。
- 函数的重载也一般发生在构造函数中。
- 但是例如上面方框中的两个构造函数,由于第一个函数存在默认值,当使用该构造函数创建对象,并且采用默认值的时候,编译器将无法确认使用哪个函数,因此上面的写法时错误的。
参数传递与返回值
常量成员函数(const member functions)
const
定义这个参数的内容是不会改变的,在函数里面表示这个函数将不会改变任何参数。- 例如:
1 |
|
- 对于上述类的创建和函数的调用,可能有以下两种情况:
- 没加
const
,运行没问题:
1 |
|
- 加了
const
,但是如果在类的函数中没加 const,将会编译失败。
1 |
|
参数传递
- 参数传递尽量都传引用,当所传的参数不希望被函数所修改可以在引用前面加上
const
,当所传参数可能被函数所修改时就不在前面加上const
。 - 例如:
1 |
|
- 引用传递的调用例子:
1 |
|
返回值传递
- 与参数传递相似,返回值传递也尽量传引用,但返回值的传递有时候将无法传引用。
- 例如:
1 |
|
无法使用引用传递返回值的情况
- 要返回的参数是在函数内部创建的将不能采用引用传递。
- 例如:
1 |
|
- 例如上面的例子,
temp
是在函数内部创建的,因此不能够使用引用传递来返回值!- 因为当这个函数运行结束后,
temp
变量将会被释放。
- 因为当这个函数运行结束后,
可以使用引用传递返回值的情况
- 只要返回的参数不是在函数内部创建的,是在外部就有该参数空间的都可以采用引用传递来返回值。
- 例如:
1 |
|
- 从上面的例子可以看到,
ths
是从函数外部传入的参数,其空间本来在函数外部就存在,因此可以使用引用传递来返回值。
友元(friend)
- 友元可以令表示的函数能够访问该类的私有成员,包括私有变量和私有函数。
- 例如:
1 |
|
- 友元函数调用的例子:
1 |
|
- 能够自由取得
friend
的private
成员。
相同 class 的各个 objects 互为 friends(友元)
- 如题,在创建了相同
class
的各个objects
是互为友元的。
1 |
|
- 调用情况如下:
1 |
|
- 解释如下:在创建了相同
class
的两个objects
(是c1
和c2
)是互为友元的。
操作符重载与临时对象
操作符重载之一:成员函数(this)
- 在
class
中实现操作符的重载:
1 |
|
- 需要注意,
this
是隐藏参数,不需要写在函数参数传递的过程中:
return by reference 语法分析
- 传递者无需知道接收者是以引用的形式接收
操作符重载之二:非成员函数(无 this)
- 为了应付用户的三种用法,对应开发三种函数;
- 与成员函数方式的区别在于非成员函数采用的是全局函数(
global
):
- 可以看出,上面的函数绝对不可以采用返回引用的形式,因为它们返回的必定是个本地对象(
local object
)。 - 同时,
typename()
创建的是临时对象,小括号中没有参数就是默认值。例如int(5)
等,表明其是临时的,生命周期很短,在下一行便结束。在上述的例子中也有体现:
“+”
表示正,“-”
表示负的情况:
- 判断两个复数是否相等(两种情况
==
和!=
):
- 输出运算符
“<<”
也称为流插入运算符,是一个二目运算符,该操作符的重载具有特殊用法: - 该操作符重载的过程是将重载函数右边的参数作用到左边的参数中:
带指针的class用法
- 前面的是不带指针的
class
的用法介绍,后续介绍带指针的class
的用法。
以 string class 为例
三个特殊函数(Big Three)
1 |
|
构造函数和析构函数
- 带有指针的构造函数
1 |
|
- 析构函数(释放掉不用的动态内存)
1 |
|
- 创建对象和使用
- 大括号
“{}”
内表示其作用域,离开作用域后析构函数自然被调用
1 |
|
- 总结
class 内带有指针成员
class
内带有指针成员必须使用拷贝构造函数和拷贝赋值函数,不然会出现以下问题:
拷贝构造函数
- 拷贝构造函数代码
1 |
|
- 拷贝构造函数的调用
1 |
|
- 特别之处:
拷贝赋值函数
- 拷贝赋值函数代码
1 |
|
- 一定要在拷贝赋值函数中加入检测自我赋值,检测自我赋值不止为了效率还有安全性:
- 拷贝赋值函数的调用
1 |
|
output 函数
output
函数,其中操作符<<
的重载是将其重载函数的右边参数作用到左边参数,必须写成非成员函数(即全局函数):
1 |
|
output
函数的调用:
1 |
|
栈(stack)和堆(heap)
- 栈(
stack
)是存在于是某作用域(scope
)的一块內存空间(memory space
)。例如在调用函数时,函数本身即会形成一个stack
用来存放它所接收的参数,以及返回地址。在函数本体 (function body
) 内声明的任何变量,其所使用的內存块都取自上述stack
。 - 堆(
heap
),或system heap
,是指由操作系统提供的一块global
內存空间,程序可动态分配 (dynamic allocated
) 从某内存中获得若干区块 (blocks
)。
c1
便是所谓的stack object
,其生命在作用域 (scope
) 结束之际结束。这种作用域內的object
,又称为auto object
,因为它会被析构函数自动清理。
c2
便是所谓的static object
,其生命在作用域 (scope
) 结束之后仍然存在,直到整个程序结束。
c3
便是所谓的global object
,其生命在整个程序结束之后才结束。也可以把它视为一种static object
,其作用域是整个程序。
heap objects 的生命周期
P
所指的便是heap object
,其生命在它被delete
之际结束。
- 以上出现内存泄漏 (
memory leak
),因为当作用域结束,p
所指的heap object
仍然存在,但指针p
的生命却结束了,作用域之外再也看不到p
(也就没机会delete p
)。
new 和 delete 的具体机制
new
:先分配memory
, 再调用构造函数
delete
:先调用析构函数,再释放memory
array new 一定要搭配 array delete
细节扩展补充
静态(static)
- 非静态的成员函数有
this
指针,能够通过this
指针来区分不同的对象。 - 而静态的成员函数没有
this
指针,只能处理静态数据。 - 类中的成员函数,若未使用类中的成员变量,则可声明为静态成员函数,函数声明前加上
static
关键字。 - 静态成员函数不属于对象,属于类。静态成员函数不包含编译器提供的
this
指针。 在类没有实例化对象前就存在,由于没有this
指针,所以也就没有特定的成员变量可以使用。 - 静态成员函数与成员函数用法上的主要不同为:
- 静态成员函数可以独立访问,调用静态成员函数时,不需要实例化任何对象。只需要使用类名 + 命名空间标识符 (
::
) 加函数名即可调用。
- 静态成员函数可以独立访问,调用静态成员函数时,不需要实例化任何对象。只需要使用类名 + 命名空间标识符 (
- 静态(
static
)函数的调用方法:- 通过
class
名调用; - 通过对象调用。
- 通过
1 |
|
把构造函数放在 private 区
- 只有调用
A::getInstance();
这个函数对象a
才会被创建出来,并通过类似A::getInstance().setup();
函数的对外接口在外部被调用。 - 这种方式也成为单例模式:单例模式是一种设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。
cout 函数
- cout 是 ostream 类的一个对象:
类模板(class template)
- 类模板的作用:建立一个通用类,类中的成员数据类型可以不具体指定,用一个虚拟的类型来代表。
函数模板(function template)
- 函数模板的作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,不用为每一个类型定义新的函数。
- 下面例子中的
<
需要在stone
类中进行操作符函数的重载,这样编译器才能知道如何对stone
类进行比大小。
命名空间(namespace)
- 命名空间(
namespace
)是一种将标识符组织在一起的方式,用于防止名字冲突。
组合、委托与继承
组合(composition)
- 组合(
composition
)指的是一个对象拥有并可以控制另一个对象的生命周期。在class
中的例子如下:
- 利用设计模式中的适配器模式书写,表示
queue
类拥有deque
类,且deque
类的功能比queue
类更加齐全,只需要把deque
类稍加改造便可实现queue
类的功能,例子如下:
组合的内存占用计算
- 组合的内存占用需要把其拥有和部分的内存全部加上来进行计算。
组合关系下的构造函数和析构函数
- 组合关系下构造函数为"由内而外",析构函数为"由外而内"。
委托(delegation)
- 两个类之间用指针相连称为委托(
delegation
),因此两个类之间的生命并不一致,而上述组合关系下两个类的生命是一致的。 - 下面例子采用
Pimpl
(Pointer to Implementation
,指向实现的指针)的设计模式,也是一种惯用法,用于实现封装和隐藏类的实现细节。 Pimpl
模式通过将类的具体实现细节放在一个单独的类中,并在主类中使用指向该实现类的指针,从而实现类的接口与实现的分离。
继承(inheritance)
- 继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。
- 这个已有的类称为基类,新建的类称为派生类。继承代表了
is a
关系。 - 派生类可以得到父类的成员变量和成员函数。
从内存的角度看继承
- 在继承关系下,派生类的构造函数为"由内而外",析构函数为"由外而内"。
侯捷 C++学习笔记之面向对象高级编程(一)
http://example.com/2024/10/27/houjie_cpp/