面向对象的基本概念
面向对象的意义
- 将日常生活中习惯的思维方式引入程序设计中
- 将需求中的概念直观的映射到解决方案中
- 以模块为中心构建可复用的软件系统
- 提高软件产品的可维护性和可扩展性
类和对象是面向对象中的两个基本概念
- 类:指的是一类事物,是一个抽象的概念
- 对象:指的是属于某个类的具体实体
- 类是一种模型,这种模型可以创建出不同的对象实体
- 对象实体是类模型的一个具体实例
一个类可以有多个对象,而一个对象必然属于某个类
类和对象的意义
- 类用于抽象的描述一类事务所特有的属性和行为
如:电脑拥有CPU,内存和硬盘,并且可以开机和运行程序
- 对象是具体的事物,拥有所属类中描述的一切属性和行为
如:每一只老虎都有不同体重,不同食量等
一些有趣问题
类一定存在实际的对象吗?
不一定,比如:恐龙,理论上来看,考古学家通过化石构建出一个恐龙,但是现在并不存在一个真真切切的恐龙
类的对象数目是确定的吗?
不一定确定,比如有多少只老虎?
类一定来源于现实生活?
不一定,虽然将现实中类的思想移到编程思想中,但一些辅助类需要我们自己抽象构建
类都是独立的吗?类之间存在关系吗?
类不是独立的,类之间显然存在关系
对象实例一定只属于一个类?
不一定。
对象实例可能完全相同吗?
现实中没有两片相同的叶子,程序设计中有吗?
类之间的基本关系
继承
- 从已存在类细分出来的类和原类之间具有继承关系(is-a)
- 继承的类(子类)拥有原类(父类)的所有属性和行为
比如华南虎和老虎是继承关系(单向)。华南虎 is a 老虎而不能反过来说老虎 is a 华南虎

组合
- 一些类的存在必须依赖于其它的类,这种关系叫组合
- 组合的类在某一局部上有其他类组成
电脑由CPU,主板,内存,硬盘等,而电脑这个类依赖于CPU类,主板类,内存类,硬盘类等组成

组合关系的特点
- - 挎其它类的对象作为当前类的成员使用
- - 当前类的对象与成员对象的生命期相同
- - 成员对象在用法上与普通对象完全一致
类的表示法

但是编译器懂这个图吗?我们可以简化成编译器比较能理解的表示法

上面图属性重复定义,我们继续改进

继续改进,使用了大括号

继续改进,我们用struct来代替中文的类。用:来代替中文的继承。

我们继续改进,用变量描述属性,函数描述行为

经过多次改进,这样表示不遗漏信息且成功让C++编译器能够读懂。
类与封装
类通常分为以下两个部分

- 当使用类的时候,不需要关心其内部实现细节
- 当创建类时,才需要考虑其内部实现细节
例如
普通用户使用手机,只需要学习如何发短信,打电话,拍照,等等等
手机开发工程师,需要考虑手机内部的实现细节
封装的基本概念
根据经验:并不是类的每个属性都是对外公开的
比如:女孩子不希望外人知道自己的体重和年龄
比如:男孩子不希望别人知道自己的身高和收入
而一些类的属性时对外公开的
比如:人的姓名,学历,国籍等
必须在类的表示法中定义属性和行为的公开级别
类似文件系统中文件的权限
C++中类的封装
- 成员变量:C++中用于表示类属性的变量
- 成员函数:C++中用于表示类行为的函数
- C++中可以给成员变量和成员函数定义访问级别
public 成员变量和成员函数可以在类的内部和外界访问和调用
private 成员变量和成员函数只能在类的内部被访问和调用
类成员的作用域
- 类成员的作用域都只在类的内部,外部无法直接访问
- 成员函数可以直接访问成员变量和调用成员函数
- 类的外部可以通过类变量访问public成员
- 类成员的作用域与访问级别没有关系
C++中用struct定义的类中所有成员默认为public
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <stdio.h>
int i = 1;
struct Test { private: int i;
public: int j; int getI() { i = 3; return i; } };
int main() { int i = 2; Test test; test.j = 4; printf("i = %d\n", i); printf("::i = %d\n", ::i); printf("test.j = %d\n", test.j); printf("test.getI() = %d\n", test.getI()); return 0; }
|
类的真正形态
一个问题:
经过不停的改进,结构体struct变得越来越不像它原来在C语言的样子了
类的关键字
- struct在C语言中已经有了自己的含义,必须继续兼容
- 在C++中提供了新的关键字class用于类定义
- class和struct的用法是完全相同的
class和struct区别
在用struct定义类是,所有的成员的默认访问级别为public
在用class定义类时,所有成员的默认访问级别为private
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <stdio.h>
struct A { int i; int getI() { return i; } };
class B { int i; int getI() { return i; } };
int main() { A a; B b; a.i = 4; printf("a.getI() = %d\n", a.getI()); b.i = 4; printf("b.getI() = %d\n", b.getI()); return 0; }
|
- C++中的类支持声明和实现的分离
- 将类的实现和定义分开
.h头文件中只有类的声明(成员变量和成员函数的声明)
.cpp源文件中完成类的其他实现(成员函数的具体实现)
对象的构造
一个问题:对象中成员变量的初始值是多少?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <stdio.h>
class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } };
Test gt;
int main() { printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0; }
|
1 2 3 4 5 6 7 8
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test gt.i = 0 gt.j = 0 t1.i = 1139214448 t1.j = 32765 pt->i = 0 pt->j = 0
|
类其实是一种数据类型,这种数据类型定义的变量即可在静态变量区,也可以在栈空间,也可以在堆空间。
因此gt.i,gt.j在静态变量区,初始值为0。t1.i,t1.j在栈上初始化为随机值,而pt->i,pt->j在堆上随机初始化,这里结果是0只是一个巧合。
对象的初始化
生活中的对象都是在初始化后上市的
初始状态是对象普遍存在的一个状态
解决方案:
- 在类中提供一个public的initialize函数
- 对象创建后立即调用initialize函数进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include <stdio.h>
class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } void initialize() { i = 1; j = 2; } };
Test gt;
int main() { gt.initialize(); printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); t1.initialize(); Test* pt = new Test; pt->initialize(); printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0; }
|
观察我们的代码,initialize()函数是一个普通函数,要求我们自己手工调用,而如果忘记调用或者调用顺序不对,运行结果可能是不对的
因此我有一个大胆的想法:能不能让initialize在对象定义时自动被调用?
构造函数
C++可以定义与类名相同的特殊成员函数,这种特殊的成员函数叫做构造函数
- 构造没有任何返回类型的声明
- 构造函数在对象定义时自动被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } Test() { printf("Test() Begin\n"); i = 1; j = 2; printf("Test() End\n"); } };
|
带有参数的构造函数
- 构造函数可以根据需要定义参数
- 一个类中可以存在多个重载的构造函数
- 构造函数的重载遵循C++重载的规则
注意: 对象的定义和对象声明不同
- 对象定义 : 申请对象的空间并调用构造函数
- 对象声明 : 告诉编译器存在这样一个对象
构造函数的自动调用
与重载规则联系起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <stdio.h>
class Test { public: Test() { printf("Test()\n"); } Test(int v) { printf("Test(int v), v = %d\n", v); } };
int main() { Test t; Test t1(1); Test t2 = 2; int i(100); printf("i = %d\n", i); return 0; }
|
注意初始化的写法,回想C语言int i = 100;这是初始化,而int i(100);这也是初始化。
同样地,Test t2 = 2;也是初始化,Test t2(2);也是初始化。
但是注意
1 2 3 4
| Test t; Test t2 = 2;
t = t2;
|
这里t = t2是赋值而不是初始化,初始化是初始化,赋值是赋值,二者不可混为一谈!
初始化与赋值不同
初始化:对正在创建的对象进行初值设置
赋值:对已经存在的对象进行值设置
构造函数的手动调用【特殊情况】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <stdio.h>
class Test { private: int m_value; public: Test() { printf("Test()\n"); m_value = 0; } Test(int v) { printf("Test(int v), v = %d\n", v); m_value = v; } int getValue(){ return m_value; } };
int main() { Test ta[3] = {Test(), Test(1), Test(2)};
for(int i = 0;i < 3;i++){ printf("ta[%d].getValue() = %d\n",i,ta[i].getValue()); }
Test t = Test(100);
printf("t.getValue() = %d\n", t.getValue());
return 0; }
|
1 2 3 4 5 6 7 8 9 10
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test Test() Test(int v), v = 1 Test(int v), v = 2 ta[0].getValue() = 0 ta[1].getValue() = 1 ta[2].getValue() = 2 Test(int v), v = 100 t.getValue() = 100
|
特殊的构造函数
- 无参构造函数 :没有参数的构造函数
当类中没有定义任意的构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
反过来说,假设我们的类中已经定义了构造函数,编译器将不再提供无参构造函数
- 拷贝构造函数 :参数为
const class_name&的构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制。
一道经典面试题:以下这个类有什么东西?
C++空类的大小不为0,存在一个无参构造函数等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <stdio.h>
class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; }
};
int main() { Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0; }
|
运行上述程序,观察结果如下,可以知道调用了默认的构造函数Test t1和默认的拷贝构造函数Test t2 = t1。
1 2 3 4
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test t1.i = -1661124032, t1.j = 21985 t2.i = -1661124032, t2.j = 21985
|
注意,如果我给上述代码仅仅只提供一个拷贝构造函数,

编译报错,这是因为编译器觉得我们已经给类提供了一个构造函数(拷贝构造函数也是构造函数),编译器就不给我们提供无参构造函数了,因此在代码32行的Test t;这行编译器报错error: no matching function for call to ‘Test::Test()’
拷贝构造函数
拷贝构造函数的意义
- 浅拷贝 :拷贝后的对象的物理状态相同
- 深拷贝 :拷贝后的对象的逻辑状态相同
编译器提供的默认拷贝函数仅仅是浅拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <stdio.h>
class Test { private: int i; int j; int* p; public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } void free() { delete p; }
Test(int v) { i = 1; j = 2; p = new int; *p = v; } };
int main() { Test t1(3); Test t2 = t1; printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP()); printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP()); return 0; }
|
我们继续修改代码,给类对象增加一个int型指针p并且删除拷贝构造函数,创建一个我们写的构造函数
然后打印,可以看到两个对象t1和t2的p指针都指向堆的同一块内存
1 2 3 4
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test t1.i = 1, t1.j = 2, t1.p = 0x5641c9546eb0 t2.i = 1, t2.j = 2, t2.p = 0x5641c9546eb0
|
但是如果我们在main函数分别用t1,t2两个对象调用成员函数free()释放对应的内存则会造成重复释放同一块内存的严重错误!


既然t1初始化的时候指向了堆空间的一块内存,我们t2拷贝构造的时候应该指向堆空间的一块新的内存。
1 2 3 4 5 6 7
| Test(const Test& t) { i = t.i; j = t.j; p = new int; *p = *t.p; }
|
现在虽然p指针变量存储的地址变量不一样的,但是指向的内存空间存储的值是一样的。
这也就是深拷贝。
什么时候需要进行深拷贝?
对象中有成员指代了系统中的资源
- 成员指向了动态内存空间
- 成员打开来外存中的文件
- 成员使用了系统中的网络端口
一般性原则 :自定义拷贝构造函数,必然要实现深拷贝!!!
初始化列表的使用
类中是否可以定义const成员?
答案:可以,是一个只读变量。
下面的类定义是否合法?如果合法,ci的值是什么,存储在哪里?
1 2 3 4 5 6 7 8 9
| class Test { private: const int ci; public: int getCI(){ return ci; } }
|
注意,类定义合法,但是未对const int ci进行初始化,我们要定义一个构造函数来初始化ci变量,但是ci是一个只读变量不能作为左值,因此我们引出了初始化列表,用初始化列表对ci进行初始化。
C++中提供了初始化列表对成员变量进行初始化
语法规则
1 2 3 4
| ClassName::ClassName():m1(v1), m2(v2), m3(v3) { }
|
成员初始化顺序
- 成员的初始化顺序与成员的声明顺序相同
- 成员的初始化顺序与初始化列表中的位置无关
- 初始化列表先于构造函数的函数体执行
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <stdio.h>
class Value { private: int mi; public: Value(int i) { printf("i = %d\n", i); mi = i; } int getI() { return mi; } };
class Test { private: Value m2; Value m3; Value m1; public: Test() : m1(1), m2(2), m3(3) { printf("Test::Test()\n"); } };
int main() { Test t; return 0; }
|
Test类的三个成员Value类m1,m2,m3应该如何初始化呢?
1 2 3 4
| private: Value m2(2); Value m3(3); Value m1(1);
|
首先这种方式是不允许的,语法错误。要初始化成员变量只能使用初始化列表。(c++11新特性允许为数据成员提供类内初始值)
改为Test() : m1(1), m2(2), m3(3)接着运行,成功运行。
1 2 3 4 5 6
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test i = 2 i = 3 i = 1 Test::Test()
|
如图所示,可以看到一个特别奇怪的现象:
- 成员的初始化顺序与初始化列表中的位置无关
- 成员的初始化顺序与成员的声明顺序相同
- 初始化列表先于构造函数的函数体执行
这个十分的重要!!!
类中const成员
- 类中的const成员会被分配空间的
- 类中的const成员的本质是只读变量
- 类中的const成员只能在初始化列表中指定初始值
对象的构造顺序
局部对象的构造顺序
C++的类可以定义多个对象,那么对象的构造顺序怎样的
对于局部对象
当程序执行流到达对象的定义语句时进行构造
1 2 3 4 5 6 7 8 9 10
| int i = 0; Test a1 = i;
while(i < 3) Test a2 = ++i;
if(i < 4) Test a = a1; else Test a(100);
|

可以看到执行顺序非常正常
但是如果增加goto语句可能忽略某些类的构造而未进行初始化,引起灾难性的错误。

幸运的是g++编译器发现了这个错误并且提醒了我们。
但是并不是所有编译器都能发现这个错误,比如vs2010。

堆对象的构造顺序
- 当程序执行流到达new语句时创建对象
- 使用new创建对象将自动触发构造函数的调用
1 2 3 4 5 6 7 8 9 10 11
| int i = 0; Test* a1 = new Test(i);
while( ++i < 10) if(i % 2) new Test(i);
if( i < 4) new Test(*a1); else new Test(100);
|

全局对象的构造顺序
- 全局对象的构造顺序是不确定的!!!
- 不同编译器使用不同的规则确定构造顺序
在进入main函数之前不存在程序执行流的概念。
这里我创建了四个全局对象分别位于t1.cpp,t2.cpp,t3.cpp,test.cpp内然后运行观察到构造函数执行顺序是t4->t1->t2->t3。

再到windows下的vs2010下执行相同的代码。发现顺序又不一样。

小结
- 局部对象的构造顺序依赖于程序的执行流
- 堆对象的构造顺序依赖于new的使用顺序
- 全局对象的构造顺序是不确定的
如果构造函数抛出异常?
构造函数中拋出异常
- 构造过程立即停止
- 当前对象无法生成
- 析构函数不会被调用
- 对象所占用的空间立即收回(不会内存泄漏)
工程项目中的建议
- 不要在构造函数中拋出异常
- 当构造函数可能产生异常时 , 使用二阶构造模式
避免在析构函数中拋出异常 ! !
析构函数的异常将导致 :对象所使用的资源无法完全释放。
对象的销毁
一般而言,需要的销毁的对象都应该做清理
解决方案:
- 为每一个类都提供一个public的free函数
- 对象不再需要时立即调用free函数进行清理
回想如果我们的电脑手机等软件出现问题
为什么重启可以解决90%的问题?
这是因为软件不稳定造成的,长时间运行出现的不稳定多半是内存泄漏引起的。
存在的问题:
- free只是一个普通的函数,必须显示的调用
- 对象销毁前没有做清理,很可能造成资源泄漏
那么我们的C++编译器是否能够自动调用某个特殊的函数进行对象的清理?
答案是可以的,析构函数。
析构函数

注意上述代码中如果忘记释放堆上的内存delete pt;那么将不会执行析构函数
析构函数的定义准则:
当类中自定义了构造函数,并且构造函数中使用了系统资源(如:内存申请,文件打开等),需要自定义析构函数。
析构函数的顺序
单个对象
单个对象创建时构造函数的调用顺序
- 调用父类的构造过程
- 调用成员变量的构造函数(如果成员也是类)(成员变量的调用顺序与声明顺序相同)
- 调用类自身的构造函数
析构函数与对应构造函数的调用顺序相反
- 调用类自身的析构函数
- 调用成员变量的析构函数
- 调用父类的析构过程
多个对象
多个对象析构顺序与构造顺序相反
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include <stdio.h>
class Member { const char* ms; public: Member(const char* s) { printf("Member(const char* s): %s\n", s); ms = s; } ~Member() { printf("~Member(): %s\n", ms); } };
class Test { Member mA; Member mB; public: Test() : mB("mB"), mA("mA") { printf("Test()\n"); } ~Test() { printf("~Test()\n"); } };
Member gA("gA");
int main() { Test t; return 0; }
|
1 2 3 4 5 6 7 8 9 10
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test Member(const char* s): gA Member(const char* s): mA Member(const char* s): mB Test() ~Test() ~Member(): mB ~Member(): mA ~Member(): gA
|
关于栈对象和全局对象,类似于入栈和出栈的顺序,最后构造的对象最先析构!!
堆对象的析构发生在使用delete的时候,与delete的使用顺序相关!!
神秘的临时对象

观察这一段程序,作者的程序意图是在main函数中执行Test t;希望t中mi的值被设置为0。
- 在Test()中以0作为参数调用Test(int i)
- 将成员变量mi的初始值设置为0

但是执行观察运行结果:成员变量mi的值为随机值。
构造函数是一个特殊的函数
- 是否可以直接调用?
- 是否可以在构造函数中调用构造函数?
- 直接调用构造函数的行为是什么?
答案:
- 直接调用构造函数将产生一个临时对象
- 临时对象的生命周期只有一条语句的时间
- 临时对象的作用域只在一条语句中
- 临时对象是C++中值的警惕的灰色地带
因此上面的程序问题在于第10行创建了一个临时对象,因此上述程序的构造函数等价于一个空的构造函数。
那我们如果对代码进行复用操作?
我们可以自己定义一个init函数。在不同的构造函数中调用init函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <stdio.h>
class Test { int mi; void init(int i) { mi = i; } public: Test(int i) { init(i); } Test() { init(0); } void print() { printf("mi = %d\n", mi); } };
int main() { Test t; t.print();
return 0; }
|
1 2 3
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test mi = 0
|
我们应该在编程里尽力地减少代码中临时对象的产生。
C++编译器在不影响最终执行结果地前提下,也会尽力减少临时对象的产生。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <stdio.h>
class Test { int mi; public: Test(int i) { printf("Test(int i) : %d\n", i); mi = i; } Test(const Test& t) { printf("Test(const Test& t) : %d\n", t.mi); mi = t.mi; } Test() { printf("Test()\n"); mi = 0; } int print() { printf("mi = %d\n", mi); } ~Test() { printf("~Test()\n"); } };
Test func() { return Test(20); }
int main() { Test t = Test(10); Test tt = func(); t.print(); tt.print(); return 0; }
|
39行Test t = Test(10);理论分析:1.生成临时对象。(调用构造函数)2.用临时对象初始化t对象(调用拷贝构造函数)
事实上上述代码等价于直接调用一次构造函数Test t = 10,编译器会进行优化仅调用一次构造函数使得程序的性能得到提升
40行Test tt = func()理论分析:1.生成临时对象并且返回 2.函数结束后销毁临时对象 3.用函数返回的临时对象初始化tt对象(调用拷贝构造函数)。
事实上,C++编译器会进行优化,尽量减少临时对象的产生。Test tt = func(); ==> Test tt = Test(20); ==> Test tt = 20;
1 2 3 4 5 6 7 8
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test Test(int i) : 10 Test(int i) : 20 mi = 10 mi = 20 ~Test() ~Test()
|
关于类成员的疑问
问题:
1 2 3 4
| Test::Test(const Test& t) { mi = t.mi; }
|
为什么可以访问另一个对象的private成员。
成员函数和成员变量都是隶属于具体对象的吗?
- 从面向对象的角度,对象由属性(成员变量)和方法(成员函数)构成
- 从程序运行的角度,对象由数据和函数构成
数据可以位于栈,堆,和全局数据区
函数只能位于代码段(所有对象共享一套成员函数)
测试程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| #include <stdio.h>
class Test { int mi; public: int mj; Test(int i); Test(const Test& t); int getMi(); void print(); };
Test::Test(int i) { mi = i; }
Test::Test(const Test& t) { mi = t.mi; } int Test::getMi() { return mi; }
void Test::print() { printf("this = %p\n", this); }
int main() { Test t1(1); Test t2(2); Test t3(3); printf("t1.getMi() = %d\n", t1.getMi()); printf("&t1 = %p\n", &t1); t1.print(); printf("t2.getMi() = %d\n", t2.getMi()); printf("&t2 = %p\n", &t2); t2.print(); printf("t3.getMi() = %d\n", t3.getMi()); printf("&t3 = %p\n", &t3); t3.print(); return 0; }
|
1 2 3 4 5 6 7 8 9 10 11
| fengyun@ubuntu:~/share$ g++ test.cpp -o test fengyun@ubuntu:~/share$ ./test t1.getMi() = 1 &t1 = 0x7ffc483ea880 this = 0x7ffc483ea880 t2.getMi() = 2 &t2 = 0x7ffc483ea888 this = 0x7ffc483ea888 t3.getMi() = 3 &t3 = 0x7ffc483ea890 this = 0x7ffc483ea890
|
结论
- 每一个对象拥有自己独立的属性(成员变量)
- 所有的对象共享类的方法(成员函数)
- 方法(成员函数)能够直接访问任何所属类对象的属性(成员变量)
- 方法中隐藏参数this用于指代当前对象
成员函数只有一套,可以访问任何所属类对象的属性。
类的静态成员变量
回顾类的成员变量属性
- 通过对象名能够访问public 成员变量
- 每个对象的成员变量都是专属的
- 成员变量不能在对象之间共享
那么如果有以下的需求:
- 统计在程序运行期间某个类的对象数目
- 保证程序的安全性( 不能使用全局变量 )
- 随时可以获取当前对象的数目
在 C+ + 中可以定义静态成员变量
- 静态成员变量属于整个类所有
- 静态成员变量的生命期不依赖于任何对象
- 可以通过类名直接访问公有静态成员变量
- 所有对象共享类的静态成员变量
- 可以通过对象名访问公有静态成员变量
静态成员变量的特性
- 在定义时直接通过 static 关键字修饰
- 静态成员变量需要在类外单独分配空间
- 静态成员变量在程序内部位于全局数据区
语 法 规 则 :
Type ClassName::VarName = value;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Test { private: static int cCount; public: Test() { cCount++; } ~Test() { --cCount; } int getCount() { return cCount; } };
|
类的静态成员函数
继续回顾之前的需求
- 统计在程序运行期间某个类的对象数目【满足】
- 保证程序的安全性( 不能使用全局变量 )【满足】
- 随时可以获取当前对象的数目( Failure ) 【未满足】
我们可以将static int cCount;设为public,但是这样就不安全了
我们需要什么?
- 不依赖对象就可以访问静态成员变量
- 必须保证静态成员变量的安全性
- 方便快捷的获取静态成员变量的值
在 C++中可以定义静态成员函数
- 静态成员函数是类中特殊的成员函数
- 静态成员函数属于整个类所有
- 可以通过类名直接访问公有静态成员函数
- 可以通过对象名访问公有静态成员函数
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

最终解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Test { private: static int cCount; public: Test() { cCount++; } ~Test() { --cCount; } static int GetCount() { return cCount; } };
int Test::cCount = 0;
|
小结:
- 静态成员函数是类中特殊的成员函数
- 静态成员函数没有隐藏的 this 参数
- 静态成员函数可以通过类名直接访问
- 静态成员函数只能直接访问静态成员变量( 函数 )
二阶构造模式
关于构造函数
- 类的构造函数用于对象的初始化
- 构造函数与类同名并且没有返回值
- 构造函数在对象定义时自动被调用
那么引出了如下问题:
- 如何判断构造函数的执行结果?
- 在构造函数中执行 return 语句会发生什么?
- 构造函数执行结束是否意味着对象构造成功 ?
答案:
- 无法判断构造函数执行结果
- return会直接返回
- 并不意味对象构造成功
构造函数能决定的只是对象的初始状态 , 而不是对象的诞生 ! !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <stdio.h>
class Test { int mi; int mj; public: Test(int i, int j) { mi = i; return; mj = j; } int getI() { return mi; } int getJ() { return mj; } };
int main() { Test t1(1, 2); printf("t1.mi = %d\n", t1.getI()); printf("t1.mj = %d\n", t1.getJ()); return 0; }
|
以上代码,构造函数中有return语句并且提前返回,我们运行结果如下:对象的初始化并不成功
1 2 3
| fengyun@ubuntu:~/share$ ./test t1.mi = 1 t1.mj = -1096823344
|
我们可以自己手动保存一个status变量,若构造函数执行成功,status置为true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <stdio.h>
class Test { int mi; int mj; bool mStatus; public: Test(int i, int j) : mStatus(false) { mi = i; return; mj = j; mStatus = true; } int getI() { return mi; } int getJ() { return mj; } int status() { return mStatus; } };
int main() { Test t1(1, 2); if( t1.status() ) { printf("t1.mi = %d\n", t1.getI()); printf("t1.mj = %d\n", t1.getJ()); } return 0; }
|
但是这种解决方法仍然不够优美。
半成品对象的概念
- 初始化操作不能按照预期完成而得到的对象
- 半成品对象是合法的 C+ + 对象 , 也是 Bug 的主要来源
二阶构造
工程开发中的构造过程可分为
- 资源无关的初始化操作
不可能出现异常情况的操作
- 需要使用系统资源的操作
可能出现异常情况 , 如 : 内存申请 , 访问文件

二阶构造实例一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class TwoPhaseCons { private: TwoPhaseCons(){ } bool construct() { return true; } public: static TwoPhaseCons* NewInstance(); }; TwoPhaseCons* TwoPhaseCons::NewInstance() { TwoPhaseCons* ret = new TwoPhaseCon();
if (!(ret && ret->construct())){ delete ret; ret = NULL; } return ret; }
|
注意13行TwoPhaseCons* ret = new TwoPhaseCon();,注意有个错误的说法:静态成员函数只可以访问静态成员变量/静态成员函数不能访问非静态成员。
静态函数没有默认的this对象指针。但是可以通过其他方式传入对象的地址,便可以在静态成员函数中访问到非静态成员函数。这种说法不够严密。仅仅是不能在静态成员函数中,使用this隐式或者显式调用非静态成员。因为静态函数不与对象绑定在一起,因此也不能声明成const的。
小结
- 构造函数只能决定对象的初始化状态
- 构造函数中初始化操作的失败不影响对象的诞生
- 初始化不完全的半成品对象是 Bug 的重要来源
- 二阶构造人为的将初始化过程分为两部分
- 二阶构造能够确保创建的对象都是完整初始化的
友元的尴尬能力
友元的概念
- 友元是 C++ 中的一种关系
- 友元关系发生在函数与类之间或者类与类之间
- 友元关系是单向的 ,不能传递

友元的用法
- 在类中以 friend 关键字声明友元
- 类的友元可以是其它类或者具体函数
- 友元不是类的一部分
- 友元不受类中访问级别的限制
- 友元可以直接访问具体类的所有成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Point { double x; double y; public: Point(double x, double y) { this->x = x; this->y = y; } double getX() { return x; } double getY() { return y; } };
double func(Point& p1, Point& p2) { double ret = 0; ret = (p2.y - p1.y) * (p2.y - p1.y) + (p2.x - p1.x) * (p2.x - p1.x); ret = sqrt(ret); return ret; }
|
注意29行ret = (p2.y - p1.y) * (p2.y - p1.y) + (p2.x - p1.x) * (p2.x - p1.x);我们C语言原先就是这样写的,
但是C++对对象的封装导致若要访问x,y必须调用getX(),getY()。
这么一个简单的语句要调用8次函数为人们所吐槽,因此C++设计者门为了兼容C语言而设计出了友元。
友元的尴尬
- 友元是为了兼顾 C 语言的高效而诞生的
- 友元直接破坏了面向对象的封装性(C++开发变为了C语言开发)
- 友元在实际产品中的高效是得不偿失的
- 友元在现代软件工程中已经逐渐被遗弃
Java,csharp诞生于C++,但是都没有友元这个能力,因此C++开发尽量减少友元的使用。
注意事项
- 友元关系不具备传递性
- 类的友元可以是其它类的成员函数
- 类的友元可以是某个完整的类 —所有的成员函数都是友元

小结
- 友元是为了兼顾 c 语言的高效而诞生的
- 友元直接破坏了面向对象的封装性
- 友元关系不具备传递性
- 类的友元可以是其它类的成员函数
- 类的友元可以是某个完整的类
类中的函数重载
函数重载
- 函数重载的本质为相互独立的不同函数
- C+ + 中通过函数名和函数参数确定函数调用
- 无法直接通过函数名得到重载函数的入口地址
- 函数重载必然发生在同一作用域中
类中的成员函数可以进行重载
- 构造函数的重载
- 普通成员函数的重载
- 静态成员函数的重载
万变不离其宗
- 重载函数的本质为多个不同的函数
- 函数名和参数列表是唯一的标识(和有没有static关键字无关)
- 函数重载必须发生在同一作用域中
重载的意义
- 通过函数名对函数功能进行提示
- 通过参数列表对函数用法进行提示
- 扩展系统中已经存在的函数功能
扩展系统函数strcpy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <stdio.h> #include <string.h>
char* strcpy(char* buf, const char* str, unsigned int n) { return strncpy(buf, str, n); }
int main() { const char* s = "D.T.Software"; char buf[8] = {0}; strcpy(buf, s, sizeof(buf)-1); printf("%s\n", buf); return 0; }
|
重载能够扩展系统中已经存在的函数功能 !
那么重载是否也能够扩展其它更多的功能 ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include <stdio.h>
class Complex { int a; int b; public: Complex(int a = 0, int b = 0) { this->a = a; this->b = b; } int getA() { return a; } int getB() { return b; } friend Complex Add(const Complex& p1, const Complex& p2); };
Complex Add(const Complex& p1, const Complex& p2) { Complex ret; ret.a = p1.a + p2.a; ret.b = p1.b + p2.b; return ret; }
int main() {
Complex c1(1, 2); Complex c2(3, 4); Complex c3 = Add(c1, c2); printf("c3.a = %d, c3.b = %d\n", c3.getA(), c3.getB()); return 0; }
|
操作符重载
- C++ 中的重载能够扩展操作符的功能
- 操作符的重载以函数的方式进行
- 本 质 : 用特殊形式的函数扩展操作符的功能
- 通过 operator 关键字可以定义特殊的函数
- operator 的本质是通过函数重载操作符
1 2 3 4 5 6 7
| Type operator Sign(const Type pi, const Type p2) { Type ret;
return ret; }
|
因此只需简单的修改上述的代码,就可以实现重载操作符
1 2 3
| Complex Add(const Complex& p1, const Complex& p2)
Complex operator + (const Complex& p1, const Complex& p2)
|
1 2 3 4 5
| Complex c3 = Add(c1, c2);
Complex c3 = operator + (c1, c2);
Complex c3 = c1 + c2;
|
我们上述修改依然是基于友元的方法,那么如何不依赖友元重载操作符?
可以将操作符重载函数定义为类的成员函数,差别如下:
- 比全局操作符重载函数少一个参数(左操作数)
- 不需要依赖友元就可以完成操作符重载
- 编译器优先在成员函数中寻找操作符重载函数
1 2 3 4 5 6 7 8 9
| class Type { public: Type operator Sign(const Type& p) { Type ret; return ret; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include <stdio.h>
class Complex { int a; int b; public: Complex(int a = 0, int b = 0) { this->a = a; this->b = b; } int getA() { return a; } int getB() { return b; } Complex operator + (const Complex& p) { Complex ret; printf("Complex operator + (const Complex& p)\n"); ret.a = this->a + p.a; ret.b = this->b + p.b; return ret; } friend Complex operator + (const Complex& p1, const Complex& p2); };
Complex operator + (const Complex& p1, const Complex& p2) { Complex ret; printf("Complex operator + (const Complex& p1, const Complex& p2)\n"); ret.a = p1.a + p2.a; ret.b = p1.b + p2.b; return ret; }
int main() {
Complex c1(1, 2); Complex c2(3, 4); Complex c3 = c1 + c2; printf("c3.a = %d, c3.b = %d\n", c3.getA(), c3.getB()); return 0; }
|
小结
- 操作符重载是 C+ + 的强大特性之一
- 操作符重载的本质是通过函数扩展操作符的功能
- operator 关键字是实现操作符重载的关键
- 操作符重载遵循相同的函数重载规则
- 全局函数和成员函数都可以实现对操作符的重载
注意事项:
- C+ + 规定赋值操作符( = )只能重载为成员函数
- 操作符重载不能改变原操作符的优先级
- 操作符重载不能改变操作数的个数
- 操作符重载不应改变操作符的原有语义