C++面向对象高级编程(上)-基于对象&面向对象
文章目录
C++ 编程简介
单一class Object Based基于对象
多个class之间的交互 Object Oriented
C++ 98(1.0) C++ 03(TR1,Techincal Repoet1)
C++ 11(2.0) C++ 14
C++
C++语言 C++标准库
一、头文件与类的声明
例子
#include <> 标准库、 ” “ 自己的文件
头文件的防卫式声明
#ifndef __COMPLEX__
#define __COMPLEX__
...
#endif
头文件的布局
模板
inline(内联)函数
内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。
在类声明内定义的函数,自动成为inline
函数;在类声明外定义的函数,需要加上inline
关键字才能成为inline
函数.
inline
只是编程者给编译器的一个建议,在编译时未必会真正被编译为inline
函数.因此如果函数足够简单,我们就把它声明为inline
就好了.
access level(访问级别) public private
二、constructor(ctor,构造函数)
1、无返回值 2、构造函数的命名必须和类名完全相同 3、尽量使用构造函数初始化列表
class CExample {
public:
int a;
float b;
//构造函数初始化列表
CExample(): a(0),b(8.8)
{}
//构造函数内部赋值
CExample()
{
a=0;
b=8.8;
}
};
上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:
- 1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
- 2.const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
- 1.内置数据类型,复合类型(指针,引用)- 在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
- 2.用户定义类型(类类型)- 结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
构造函数的重载
1 和 2 是重复的, 默认值是相同的,无法判断
构造函数被放在private区(基本不会这么做)
设计模式 Singleton single :单一的; 单个的
常量成员函数
若成员函数中不改变成员变量,应加以const
修饰
若这类函数不加以const
修饰,则常量对象将不能调用这些函数
const complex c(2, 1); // 定义常量变量
c.real(); // 若 real() 函数不加以const修饰,则编译时会报错: error: passing 'const complex' as 'this' argument
三、参数的传递与返回值
pass by value VS. pass by refernence
-
参数值的传递
value 传递自己本身的大小 refernence 传递引用 传递的是地址(8个字节)
const complex& 保证传递的值是不会改变的
值本身大小 小于 地址大小(8字节) 时传值效率高
-
**返回值的传递 **
为提高效率,若函数的返回值是原本就存在的对象,则应以引用形式返回.
若函数的返回值是临时变量,则只能通过值传递返回.
在带有返回值的函数中,需要使用return语句返回一个表达式的值,如:return 表达式;一般函数返回值时都要建立临时变量,即用来拷贝副本。
引用返回值时,不产生值的副本,而是将其返回值直接传递给接收函数返回值的变量或对象。
*对于赋值函数,应当用“引用传递”的方式返回String对象。如果用“值传递”的方式,虽然功能仍然正确,但由于return语句要把 this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。
对于相加函数,应当用“值传递”的方式返回String对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象temp的“引用”。由于temp在函数结束时被自动销毁,将导致返回的“引用”无效。
-
友元函数
打破封装,直接拿类里面的数据
-
相同class的各种objects互为友元——调用 c2 的成员函数返回 c1 的成员变量
指针和引用的相同点和不同点:
从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值不会变)
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
★相同点:
- 都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
★不同点:
- 指针是一个实体,而引用仅是个别名;
- 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
- 引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有 的, 前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
- 引用不能为空,指针可以为空;
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
- 指针和引用的自增(++)运算意义不一样;
- 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
四、操作符重载与临时对象
在C++中的操作符重载有两种形式,一种是在类内声明public函数实现操作函重载(这种情况下,操作符是作用在左操作数上的);另一种是在类外声明全局函数实现操作符重载.
例如对于如下语句,有两种方式都可以实现操作符+的重载.
- complex c1;
- c1 + 2; // 需要重载操作符 +
在类内声明public函数complex::operator += (int)
在类外声明全局函数complex operator + (const complex&, double)这两种方式均可以实现操作符重载,为便于调用该类的用户使用,不同的操作符使用不同的方式进行重载
在类内声明public函数complex::operator += (int)
在类外声明全局函数complex operator + (const complex&, double) 所有人都可以用
操作符重载
任何成员函数函数都有一个隐藏的 this 指针,指向调用者
在类内声明public
函数重载+=
return by reference 的情况
从这个例子中也可以看出
- 使用引用传递参数和返回值的好处在于传送者无需知道接收者是否以引用形式接收,只需要和值传递一样写代码就行,不需要改动。
- c3 += c2 += c1;因为要支持连串使用complex&,否则就可以用void。
一定不能 return by reference 的情况
class之外,不带class名称为全局函数 全局函数(非成员函数)的操作符重载
临时对象(函数里临时创建的对象,即局部变量local object)随着函数的结束,被释放
在类外声明或函数重载+
虑到+
操作符有三种可能的用法如下:
complex c1(2,1);
complex c2;
c2 = c1 + c2; // 用法1: complex + complex
c2 = c1 + 5; // 用法2: complex + double
c2 = 7 + c1; // 用法3: double + complex
因为重载操作符的成员函数是作用在左操作数上的,若使用类内声明public
函数重载操作符的方法,就不能支持第3种用法了.因此使用在类外声明函数重载+
运算符.
在类外声明函数重载<<
ostream 只能用引用 标准输出流
与重载+
的考虑方法类似,<<
操作符通常的使用方式是cout<<c1
而非c1<<cout
,因此不能使用成员函数重载<<
运算符.
考虑到形如cout<<c1<<c2<<c3
的级联用法,重载函数的返回值为ostream&
而非void
.
总结 编写类的五件事情
在编写类的时候应该注意的5件事,通过这5件事可以看出你写的代码是否大气:
-
构造函数中使用列表初始化(initialization list)为成员变量赋值.
-
常量成员函数使用const修饰.
-
参数的传递尽量考虑使用引用传递,若函数体内不改变传入的参数,应加以const修饰.
-
返回值若非局部变量,其传递尽量考虑使用引用传递,
-
数据放入private中,大部分函数放入public中.
-
补充一点 若成员函数中不改变成员变量,应加以
const
修饰
五、拷贝构造函数、拷贝赋值函数和析构函数
带有指针成员变量的类——以字符串类String
为例
Big Three, 三个特殊函数 拷贝构造函数、拷贝赋值函数和析构函数
- 拷贝构造函数 String(const String& );
- 拷贝赋值函数 String& operator =(const String& );
- 析构函数 ~string();
构造函数和析构函数
这里有点问题 :一般在声明构造函数时指定默认参数
因为不改变 cstr 加个const
浅拷贝
改变的是指针指向的地址,造成内存泄漏
拷贝构造函数(深拷贝)
因为不改变 str 加个const 建议加个自我检测
拷贝赋值函数
拷贝赋值函数中要检测自我赋值,这不仅是为了效率考虑,也是为了防止出现bug
两个 &,一个 & 是引用的意思,另一个 & 是取地址的意思
检测自我赋值 销毁原空间 创建新空间 复制
六、堆、栈与内存管理
stack、heap、static、global的生命周期
栈(stack),是存在于某作用域(scope)的一块内存空间.例如当你调用函数,函数本身就会形成一个stack用来防治它所接收的参数以及返回地址.在函数本体内声明的任何变量,其所使用的内存块都取自上述stack.
堆(heap),是指由操作系统提供的一块global内存空间,程序可动态分配从其中获得若干区块.
-
stack object 的生命期
class Complex { ... }; // ... { Complex c1(1,2); }
程序中
c1
就是stack object,其生命周期在作用域(大括号)结束之际结束.这种作用域内的对象又称为auto object,因为它会被自动清理. -
static object的生命周期
class Complex { … }; // ... { static Complex c2(1,2); }
程序中
c2
就是static object,其生命周期在作用域(大括号)结束之后仍然存在,直到整个程序结束,
但定义它的函数或语句块结束时,其作用域随之结束 -
global object的生命周期
class Complex { … }; // ... Complex c3(1,2); int main() { ... }
程序中
c3
就是global object,其生命在在整个程序结束之后才结束,也可以将其视为一种static object,其作用域是整个程序. -
heap object的生命周期
class Complex { … }; // ... { Complex* p = new Complex; // ... delete p; }
程序中
p
指向的对象就是heap object,其生命周期在它被deleted之际结束.若推出作用域时忘记delete指针p
则会发生内存泄漏,即p
所指向的heap object 仍然存在,但指针p
的生命周期却结束了,作用域之外再也无法操作p
指向的heap object. -
内存泄漏
class Complex { … }; // ... { Complex* p = new Complex; }
以上出现内存泄漏(memory leak) ,因为当作用域结束,p所指的heap object仍然存在﹐但指针p的生命却结束了﹐作用域之外再也看不到p(也就没机会delete p)
new
和delete
过程中的内存分配
1、new : 先分配 memory,再调用 ctor
2、delete :先调用 dtor,再释放 memory
VC中对象在debug
模式和release
模式下的内存分布如下图所示,变量在内存中所占字节数必须被补齐为16的倍数,红色代表cookie
保存内存块的大小,其最低位的1
和0
分别表示内存是否被回收. 因为是16的倍数,所以为最后一位是不会用的,可以用来表示状态
数组中的元素是连续的,数组头部4个字节记录了数组长度:
3、array new一定要搭配array delete(养成好习惯)
根据数组在内存中的状态,自然可以理解为什么new[]
和delete[]
应该配对使用了: delete
操作符仅会调用一次析构函数,而delete[]
操作符依次对每个元素调用析构函数.对于String
这样带有指针的类,若将delete[]
误用为delete
会引起内存泄漏.
记得加const
七、拓展补充:类模板,函数模板及其他
static成员的意义
对于类来说,non-static
成员变量每个对象均存在一份,static
成员变量、non-static
和static
成员函数在内存中仅存在一份.其中non-static
成员函数通过指定this
指针获得函数的调用权,而non-static
函数不需要this
指针即可调用.
静态函数只能处理静态数据
static
成员函数可以通过对象调用,也可以通过类名调用.
class Account {
public:
static double m_rate;
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0;
int main() {
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}
static
成员变量需要在类声明体外进行初始化.
cout的补充
cout 是一种 ostream
class template ,类模板
function template ,函数模板
namespace 包装
用 std 空间 用 std 空间的 cout
八、组合与继承
类之间的关系有复合(composition)、委托(aggregation)和继承(extension)3种.
复合(composition)
复合表示一种has-a
(有一个)的关系,STL中queue
的实现就使用了复合关系.这种结构也被称为adapter模式
queue 和 deque 是同步的
复合关系下 构造由内而外,析构由外而内:
委托(aggregation;composition by reference)
String 和 StringRep 不是同步创建的 指针
委托将类的定义与类的实现分隔开来,也被称为编译防火墙.
继承(extension)
继承表示一种is-a
(是一个)的关系,STL中_List_node
的实现就使用了继承关系.
继承关系下 构造由内而外,析构由外而内:
九、虚函数与多态
成员函数有3种: 非虚函数、虚函数和纯虚函数
-
非虚函數(non-virtual function): 不希望子类重新定义(override)的函数.
-
虚函數(virtual function): 子类可以重新定义(override)的函数,且有默认定义.
-
纯虚函數(pure virtual function): 子类必须重新定义(override)的函数,没有默认定义.
使用虚函数实现框架: 框架的作者想要实现一般的文件处理类,由框架的使用者定义具体的文件处理过程,则可以用虚函数来实现.
将框架中父类CDocument
的Serialize()
函数设为虚函数,由框架使用者编写的子类CMyDoc
定义具体的文件处理过程,流程示意图和代码如下:
Base > Component > Derived
面向对象设计范例
使用委托+继承实现Observer模式
使用Observer模式实现多个窗口订阅同一份内容并保持实时更新
类结构图如下:
十、委托相关设计
使用委托+继承实现Composite模式
使用Composite模式实现多态,类结构图如下
使用委托+继承实现Prototype模式
Prototype模式示意图如下:
Prototype模式示意图如下: