# 👧 面向对象
百度百科:面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)的主要思想是把构成问题的各个事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙一个事物在整个解决问题的步骤中的行为。面向对象程序设计中的概念主要包括:对象、类、数据抽象、继承、动态绑定、数据封装、多态性、消息传递。通过这些概念面向对象的思想得到了具体的体现。
在上面那段话中,你能看明白多少?
全都看不懂?没关系,我们一开始接触到这方面知识,并不需要一下子就完全掌握其中的全部概念。饭得一口一口吃,路得一步一步走,先来个初步理解:
“面 向 对 象 编 程”,我们可以拆成三个词语来分析:面向
、对象
、编程
- 面向: 我们大概听过类似的 "面向小学生开放 ..."、"面向未来我们要 ..."、"四个面向" 等等。这里
面向
的作用就是和这些话是同一个意思,或者换个同义词——面对。 - 对象: 指行动或思考时作为目标的事物。举例说明:
- 当你上网时,你控制的对象是 键盘、鼠标、电脑 ...
- 当你睡觉时,你依靠的对象是 枕头、床、被子 ...
- 当你上班时,你沟通的对象是 同事、客户、老板 ...
- 通过以上例子,其实可以发现,万物皆可作为对象,只是在不同的场景中,操作的对象不一样。
- 编程: 编写程序
所以,连起来可以这么解释:面向对象编程——面对某个任务,先分析出该任务中需要操作的对象,然后构建出对象,编写程序对这些对象进行操作,从而达到完成该任务的目的。
通过以上解释,我想你对面向对象编程的概念有了一个基础认识。
通过 对象
,我们可以引申出另外一个概念——类
。
# 1. 程序的内存模型
C++程序在执行时,将内存大方向划分为4个区域:
- 代码区:存放代码,由OS进行管理,不允许修改
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等。向下增——向着内存地址减小的方向增长
- 堆区:由程序员负责分配和释放,若没有释放,将在程序结束时由OS自动释放。向上增——向着内存地址增加的方向增长
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量也存放在此(全局变量放在全局区,局部常量不放在全局区)
- 该区域的数据在程序结束后由操作系统释放
📌 总结:
- C++中在程序运行前分为全局区和代码区代码区特点是共享和只读
- 全局区中存放全局变量、静态变量、常量
- 常量区中存放const修饰的全局常量和字符串常量
在程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值、局部变量等
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存(
new()
的对象可由delete
释放)int * arr = new int[10]; // 在堆区创建数组 delete [] arr; // 释放堆区数组
1
21
2
# 2. 引用
作用:给变量起别名
语法:数据类型 &别名 = 原名
本质:在C++内部实现是一个指针常量
int a = 10;
int &ref = a; // 自动转换为 int * const ref = &a;
ref = 20; // 内部发现ref是引用,自动帮我们转换为 *ref = 20;
2
3
2
3
引用必须初始化,初始化后不可改变
常量引用使用场景:用来修饰形参,防止误操作
// 一种用法 const int & ref = 10; // 等于:int temp = 10; const int &ref = temp; // 使用场景 void showValue(const int &val) { val = 100; // 报错:表达式必须是可修改的左值 cout << val << endl; }
1
2
3
4
5
6
7
8
91
2
3
4
5
6
7
8
9
# 3. 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同 或者个数不同 或者顺序不同
➰ 示例:
// 实现一个两数或三数相加的函数
int add (int a, int b) {
return a + b;
}
int add (int a, int b, int c) {
return a + b + c;
}
/*
Notes:
1.函数的返回值不可以作为函数重载的条件
2.引用作为重载的条件
3.函数重载碰到默认参数,出现二义性
*/
// 2.引用作为重载的条件
void func (int &a) {
cout << "func (int &a)调用" << endl;
}
void func (const int &a) {
cout << "func (const int &a)调用" << endl;
}
...
int a = 10;
func(a); // result >> func (int &a)调用
func(10); // result >> func (const int &a)调用
// func(10)结果解析:因为 int &a = 10 不合法,而 const int &a = 10 合法
// 3.函数重载碰到默认参数
void func2 (int a, int b = 10) {
cout << "func2 (int a, int b = 10)调用" << endl;
}
void func2 (int a) {
cout << "func2 (int a)调用" << endl;
}
func2(10); // error,出现二义性
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
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
# 4. 类和对象
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...
车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调...
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
# 封装
封装是C++面向对象三大特性之一
封装的意义:将属性和行为作为一个整体,表现生活中的事物,将属性和行为加以权限控制
语法:class 类名 { 访问权限: 属性/行为 };
➰ 示例:
/*
用户类
*/
class User {
// 公共权限
public:
// 属性
int age; // 年龄
int sex; // 性别
string id; // 唯一id
string username; // 用户名
// 行为/方法
string getUsername () { // 获取用户名
return username;
}
void setAge (int a) { // 重新设置年龄
age = a;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
访问权限 | 成员、类内 | 类外 |
---|---|---|
公共权限 public | 可以访问 | 可以访问 |
保护权限 protected | 可以访问 | 不可以访问 |
私有权限 private | 可以访问 | 不可以访问 |
➰ 示例:
/*
用户类
*/
class User {
// 公共权限
public:
int age; // 年龄
int sex; // 性别
string username; // 用户名
string getUsername () { // 获取用户名
return username;
}
protected:
string id; // 唯一id
void setAge (int a) { // 重新设置年龄
age = a;
}
private:
string password; // 密码
void setPw (String pw) { // 重新设置密码
password = pw;
}
};
...
User u1;
u1.sex; // 可以访问
u1.setAge(20); // 可以访问
u1.id; // error,不可以访问
u1.setPw("xxx"); // error,不可以访问
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
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
# struct和class区别
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
区别:
struct
默认权限为公共class
默认权限为私有
# 成员属性设置为私有
优点1: 将所有成员属性设置为私有,可以自己控制读写权限
优点2: 对于写权限,我们可以检测数据的有效性
# 对象的初始化和清理
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
- C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
# 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写
void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写
void
- 函数名称与类名相同,在名称前加上符号
~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
➰ 示例:
class User {
public:
int age;
int sex;
string username;
// 构造函数
User(int a, int s, string uname) { // 可以带参数
age = a;
sex = s;
username = uname;
}
// 析构函数
~User() {
cout << "Person的析构函数调用" << endl;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 构造函数的分类及调用
两种分类方式:
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
注意:
- 调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
- 不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
➰ 示例:
class User {
public:
int age;
int sex;
string username;
// 无参构造函数
User () {
}
// 有参构造函数
User (int a, int s, string uname) {
age = a;
sex = s;
username = uname;
}
//拷贝构造函数
User(const User& p) {
age = p.age;
}
}
...
// 括号法
User u1(15, 0, "张三");
// 显式法
User u2 = User(15, 0, "张三");
// 隐式法
User u3 = u2;
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构
//Person p5(p4); // Warning
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
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
# 拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象