类与对象
C++面向对象的三大特性:封装、继承、多态
C++中,任何事物都为对象,对象有其属性和行为
具有相同性质的对象,我们可以抽象称为类
封装
封装的实现
意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法:class <类名>{ <访问权限>: <属性/行为>}
一些名词:
- 成员:类中的属性和行为的统称
- 成员属性(成员变量):类中的属性
- 成员函数(成员方法):类中的函数
e.g.1 设计一个圆类,求其周长
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
// 设计一个圆类,求其周长
const double PI = 3.14;
class circle{
// 访问权限: 公共权限
public:
// 属性:半径
int r;
// 行为:获取周长
double calC(){
return 2*PI*r;
}
};
int main(){
// 通过类创建一个对象——实例化
circle c1;
// 给对象的属性进行赋值,使用.
c1.r = 10;
// 调用行为
cout << "r="<< c1.calC()<<endl;
return 0;
}e.g.2 设计一个学生类,属性有姓名和学号,可以赋值并显示姓名和学号
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
#include <iostream>
#include <string>
using namespace std;
// 设计一个学生类,有姓名和学号,可以赋值并显示
class student{
public:
//属性
string name; // 姓名
int id; // 学号
// 行为
void display(){
cout << "name:" << name<<endl;
cout << "id:" << id <<endl;
}
void set_info(string s_name, int s_id){
name = s_name;
id = s_id;
}
};
int main(){
student stu1;// 实例化
student stu2;
// 一般赋值
stu1.name = "Jack";
stu1.id = 1;
stu1.display();
// 通过行为赋值
stu2.set_info("Mary", 2);
stu2.display();
return 0;
}权限
类可以将属性和方法放在不同权限下:
- public:公共权限,成员类内可以访问,类外也可以访问
- protected:保护权限,成员类内可以访问,类外不可以访问,子类可以访问父类的保护成员
- 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
#include <iostream>
#include <string>
using namespace std;
class person{
public: //公共权限
string name;
protected: // 保护权限
string car;
private: // 私有权限
int password;
public:
// 类内可以访问
void f(){
name = "Jack";
car = "xiaomisu7";
password = 123456;
}
};
int main(){
person p;
p.name = "Mary";
//p.car = "?" // protected 类外不可访问
//p.password = 123 // private 类外不可访问
return 0;
}struct与class的区别
C++中class与struct的唯一区别在于默认的访问权限不同
- class默认权限私有
- struct默认权限公共
成员属性私有化
优点:
- 可以自己控制读写权限
- 可以检测数据有效性
基本思路:用公有方法对私有属性进行操作
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
#include <iostream>
#include <string>
using namespace std;
class person{
// 属性私有
private:
string name; // 姓名:可读可写
int age = 18; // 年龄:只读
string hobby; // 爱好:只写
// 方法公有
public:
// 设置姓名
void setName(string s){
name = s;
}
// 获取姓名
void getName(){
cout <<"name:"<<name<<endl;
}
// 获取年龄
void getAge(){
cout <<"age:"<<age<<endl;
}
// 设置爱好
void setHobby(string s){
hobby = s;
}
};
int main(){
person p;
p.setName("Jack");
p.getAge();
p.setHobby("jerk");
return 0;
}对象的初始化和清理
对象的初始化和清理是很重要的安全问题:
- 一个对象或变量没有初始状态,对其使用后果未知
- 使用完一个对象或变量,没有及时清理,也会造成安全问题
构造函数与析构函数
C++提供了这两种函数,被编译器自动调用,完成对象初始化与清理工作
这两个工作是强制执行的,若我们不提供,编译器会提供自己的构造函数与析构函数,他们是空实现
构造函数:用于创建对象时为对象成员赋值
语法:
<类名>(){}无返回值,不用写void
函数名与类名相同
构造函数可以有参数,可以发生重载
调用对象时会自动调用构造,只会调用一次
析构函数:用于对象销毁前自动调用,执行清理操作
- 语法:
~<类名>(){} - 无返回值,不用写void
- 函数名与类名相同,名前面加~
- 析构函数不可以有参数,不可发生重载
- 程序在对象销毁前会自动调用析构。只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
class person{
public:
// 构造函数
person(){
cout << "构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
};
int main(){
person p; // 栈上的数据,执行完后会被释放,故执行析构函数
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
#include <iostream>
using namespace std;
class person{
public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};
int main(){
// 括号法
person p; // 默认构造函数被调用(不加括号)
person p1(10); // 有参构造函数被调用
person p2(p1); // 拷贝构造函数被调用
cout << "age of p1:" << p1.age << endl;
cout << "age of p2:" << p2.age << endl;
// 显式法
person p3 = person(20);
person p4 = person(p3);
// 隐式转换法
person p5 = 10; // 相当于 person p5 = person(10);
person p6 = p5; // 相当于 person p6 = person(p5);
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
48
49
#include <iostream>
using namespace std;
class person{
public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};
// 使用已经创建完毕的对象来初始化一个新对象
void test1(){
person p1(20);
person p2(p1);
}
// 值传递方式给函数参数传值
void test2(person p){
return ;
}
// 值传递返回局部对象
person test3(){
person p1;
return p1;
}
int main(){
person p1;
test1();
test2(p1);
person p2 = test3();
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
无参构造函数被调用
----------------
有参构造函数被调用
拷贝构造函数被调用
析构函数被调用
析构函数被调用
----------------
拷贝构造函数被调用
析构函数被调用
----------------
无参构造函数被调用
析构函数被调用
析构函数被调用构造函数调用规则
默认下,编译器至少给一个类添加三个函数:
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性值拷贝
调用规则:
- 若用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数 ,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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
using namespace std;
class person{
public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};
int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
return 0;
}
```
此种情况下,编译器使用了默认拷贝构造,进行了值传递
```c++
#include <iostream>
using namespace std;
class person{
public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};
int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
return 0;
}
```
此种情况下,由于有有参函数,编译器不会提供默认无参函数,同时会报错没有默认无参函数
```c++
#include <iostream>
using namespace std;
class person{
public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// // 有参构造
// person(int a){
// age = a;
// cout << "有参构造函数被调用" << endl;
// }
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};
int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
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
#include <iostream>
using namespace std;
class person{
public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a, int iheight){
age = a;
height = new int(iheight);
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person(){
// 在析构函数中进行堆区数据释放
if (height!=NULL){
delete height;
height = NULL;
}
cout << "析构函数被调用" << endl;
}
int age;
int* height; // 身高,使用指针将数据创建在堆区
};
int main(){
person p1(18,160);
cout << "p1的年龄是:" << p1.age << endl;
cout << "p1的身高是:" << *p1.height << endl;
person p2(p1);
cout << "p2的年龄是:" << p2.age << endl;
cout << "p2的身高是:" << *p2.height << endl;
return 0;
}该代码会卡住,原因是利用拷贝构造只进行了值传递,p1与p2执行完毕后,均执行析构函数,对height指向的空间进行释放,但是其中一个已经执行释放后,另一个再次释放会产生非法操作,故出错
这也就是浅拷贝的缺点:只进行了值传递,我们需要利用深拷贝来结局
1
2
3
4
5
6
7
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
// 深拷贝,在堆区开辟新的空间
height = new int(*p.height);
cout << "拷贝构造函数被调用" << endl;
}即在拷贝构造中,重新给拷贝的指针变量申请一块空间
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝出现问题
初始化列表
C++提供了初始化列表来初始化属性
语法:<构造函数>(): <属性1>(<值1>), <属性2>(<值2>) ... {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
class person{
public:
int a;
int b;
int c;
// 初始化列表赋值
person(int a, int b, int c):a(10), b(20), c(30){
}
};
int main(){
person p;
cout << "a:" << p.a << endl;
cout << "b:" << p.b << endl;
cout << "c:" << p.c << endl;
return 0;
}类对象作为类成员
C++中的成员可以是另一个类的对象,称为对象成员
如
1
2
3
4
class A{};
class B{
A a;
};该B类中有对象A作为成员,则A为对象成员
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 <iostream>
#include <string>
using namespace std;
class phone{
public:
string brand;
float price;
phone(string pname){
brand = pname;
cout << "phone构造函数被调用" << endl;
}
~phone(){
cout << "phone析构函数被调用" << endl;
}
};
class person{
public:
string name;
phone iphone;
person(string pname, string pbrand):name(pname),iphone(pbrand){
//相当于phone iphone = pname;的隐式转换
cout<< "person构造函数被调用" << endl;
}
~person(){
cout << "person析构函数被调用" << endl;
}
};
int main(){
person p("小明","苹果");
cout << "姓名:" << p.name << endl;
cout << "手机品牌:" << p.iphone.brand << endl;
return 0;
}1
2
3
4
5
6
phone构造函数被调用
person构造函数被调用
姓名:小明
手机品牌:苹果
person析构函数被调用
phone析构函数被调用注意构造函数的调用顺序:
- 当其他类对象作为本类成员,构造时先构造类对象,在构造自身
- 当其他类对象作为本类成员,析构时先析构自身,再析构类对象
静态成员
在成员变量与成员函数前加上关键字static,称为静态成员
分类:
- 静态成员变量
- 不属于某个对象,所有对象共享一份数据
- 编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有成员共享同一个函数
- 静态成员函数只能访问静态成员变量
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
#include <iostream>
using namespace std;
class person{
public:
static int a; // 需要类内声明,类外初始化
int c;
static void f(){
cout << "静态成员函数f调用"<<endl;
a = 100;
// c = 200; // 静态成员函数不可以访问非静态成员变量
}
private:
static int b; // 静态成员变量也有权限
};
int person::a = 100; // 在类外初始化,需要指定作用域
int person::b = 100; // 也需要在类外初始化
int main(){
person p; // 栈上的数据,执行完后会被释放,故执行析构函数
cout << p.a<<endl;
person p2;
p2.a = 200; // 数据共享,此处改变即一起改变
cout << p.a<<endl; // 通过对象进行访问
cout << person::a<<endl; // 通过类名进行访问
// cout << p.b<<endl; // 不可访问
// 同样两种访问模式
p.f();
person::f();
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
#include <iostream>
using namespace std;
class Singleton{
private:
static Singleton* m_insntance; // 存储该类的唯一实例
// 将构造与析构设为私有
Singleton(){}
~Singleton(){}
public:
// 删除拷贝构造与赋值运算符构造
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton&) = delete;
// 创建该唯一实例, 只能通过该方法创建获得对象
static Singleton* getInstance(){
if (!m_insntance){
m_insntance = new Singleton();
}
return m_insntance;
}
private:
string m_name;
public:
void setName(const string& name){
m_name = name;
}
const string& getName(){
return m_name;
}
};
Singleton* Singleton::m_insntance = NULL;
int main(){
// 第一次调用创建实例
Singleton* s1 = Singleton::getInstance();
cout << s1->getName()<<endl;
// 第二次调用使用已经创建的实例
Singleton* s2 = Singleton::getInstance();
cout << s1->getName()<<endl;
}C++对象模型与this指针
成员变量与成员函数分开存储
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
#include <iostream>
using namespace std;
class person{
};
class person2{
int a;
};
class person3{
int a;
static int b;
void f(){}
};
int main(){
person p;
person2 p2;
person3 p3;
// 占用内存为1
cout << "size of empty class: "<< sizeof(p)<<endl;
// 占用内存为4
cout << "size of class person2: "<< sizeof(p2)<<endl;
// 静态成员与非静态成员函数不属于类的对象上,故占用内存也为4
cout << "size of class person3: "<< sizeof(p3)<<endl;
return 0;
}this指针
C++通过提供this指针解决了区分多个对象调用同一块代码分不清的问题
this指针指向被调用的成员函数所属的对象,不需要定义,直接使用
用途:
- 形参和成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用
retrun *this
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 <iostream>
using namespace std;
class person{
public:
int age;
person(int age){
// 当形参和成员变量同名时,this 指针指向被调用成员函数所属对象
this->age = age; // 此时this->age和定义的成员变量age是同一个
}
// 成员函数返回对象本身
person& addage(person &p){
this->age+=p.age;
return *this;
}
};
int main(){
// 解决名称冲突
person p(18);
person p2(10);
cout <<"age:" <<p.age<<endl;
// 返回对象本身可以实现链式调用
p2.addage(p).addage(p).addage(p);
cout <<"age of p2:"<<p2.age<<endl;
return 0;
}空指针访问成员函数
C++允许空指针调用成员函数,但是要注意有没有用到this指针
如果用到this指针,要注意保证代码的鲁棒性
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
#include <iostream>
using namespace std;
class person{
public:
int age;
void show(){
cout <<"this is person class"<<endl;
}
void showage(){
// 防止指向空
if (this==NULL){
return ;
}
// 调用成员变量时,默认在前面加this->,但此时p是空指针,故指向的是空对象,this没有指向数据
cout << "age = "<<age <<endl;
}
};
int main(){
person *p = NULL;
p->show();
p->showage();
return 0;
}const修饰成员函数
常函数:
- 成员函数后加const称该函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数内依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象构造时需要对内部成员变量初始化(构造函数)
- 常对象只能调用常函数
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 <iostream>
using namespace std;
class person{
public:
int age;
mutable int b; // 加上mutable也可以在常函数中修改
// this指针本身是指针常量,指针指向是不可修改的,指针指向的值是可以修改的
// 而加上const修饰后,修饰的是this指向的值,让指向的值也不可修改
// 相当于const person * const this;
void show() const{
//this->age = 100; // 此时不可修改
this->b = 100;
}
void f(){}
person(){
}
};
int main(){
// 常对象
const person p;
// p.age = 100; // 常对象下不能修改值
p.b = 100; // 但有mutable的可以修改
// p.f() // 常对象只能调用常函数
p.show();
return 0;
}类的特殊成员函数
特殊成员函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,他们均可以用以下两个关键字修饰
=delete:可以在函数声明后添加
=delete来删除当前函数,如删除构造函数、删除不需要的传参类型=default:加在特殊函数后,可以用来声明一个默认特殊函数,还可以自动使用默认值初始化变量
可平凡复制类
C++中,各种数据类型及他们的数组、指针、枚举等在内存中是连续存储的,可以使用memcpy()函数进行拷贝,称为可平凡复制类型
一个类若满足以下条件,被称为可平凡复制类,也是一种可平凡复制类型
- 没有虚函数
- 没有用户自定义的特殊成员函数
- 有自动生成的析构函数
- 所有非静态成员变量也是可平凡复制类型
- 拷贝、移动构造函数,拷贝、移动赋值运算符中至少有一个是未被删除的
- 对于继承类,其父类要满足上述条件
可以使用<type_traits>库中的is_trivially_copyable<类>::value来判断是否可拷贝
标准布局类
C++中,所有标量类型都是标准布局类型
对一个非继承类,满足以下条件被称为标准布局类
- 没有虚函数
- 所有类成员都具有相同访问属性
- 所有类成员都是标准布局类型
- 若类是继承类,则满足该类和父类中只有一个类有非静态数据成员,且类型与访问控制满足上述条件
可以使用<type_traits>库中的is_standard_;leyout<类>::value来判断是否可拷贝
友元
友元可以让一个函数或者类访问另一个类中的私有成员
友元的关键字为friend
全局函数做友元
在class中声明对应全局函数,并在最前面加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
#include <iostream>
using namespace std;
class building{
friend void fir(building &building); // 声明该全局函数为building类的友元
public:
string sitting_room;
private:
string bedroom;
public:
building(){
sitting_room = "客厅";
bedroom = "卧室";
}
};
void fir(building &building){
cout << "访问公共属性:"<<building.sitting_room<<endl;
cout << "访问私有属性:"<<building.bedroom<<endl;
}
int main(){
building b;
fir(b);
}类做友元
在class中声明对应类,并在最前面加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
36
37
#include <iostream>
using namespace std;
class buildings;
class fri{
public:
buildings* building;
fri();
void visit();
};
class buildings{
friend class fri; // 声明fri类是building类的友元
public:
string sitting_room;
buildings();
private:
string bedroom;
};
// 在类外写成员函数
buildings::buildings(){
sitting_room = "客厅";
bedroom = "卧室";
}
fri::fri(){
building = new buildings;
}
void fri::visit(){
cout <<"访问公共属性:" <<building->sitting_room<<endl;
cout <<"访问私有属性:" <<building->bedroom<<endl; // 友元可以访问类中的私有属性
}
int main(){
fri f;
f.visit();
}成员函数做友元
在class中声明对应类中的成员函数,并在最前面加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#include
运算符重载
运算符重载可以对已有的运算符重新定义,赋予另一种功能以适用于不同的数据类型
如我们通过写成员函数来实现两个类相加属性后返回新的对象
可以通过编译器提供的通用名称operator来重载运算符以简化运算
重载的本质调用为(以person类为例):
- 成员函数:
person p3 = p1.operator<运算符>(p2) - 全局函数:
person p3 = p1.operator<运算符>(p1, p2)
注意:
- 运算符重载也可以实现函数重载
- 对于内置数据类型的表达式的运算符是不可以重载的
- 不要滥用运算符重载
加号运算符重载
成员函数:
1
2
3
4
5
6
person operator+(person &p){
person temp;
temp.a = this->a+p.a;
temp.b = this->b+p.b;
return temp;
}全局函数:
1
2
3
4
5
6
person operator+(person &p1, person &p2){
person temp;
temp.a = p1.a+p2.a;
temp.b = p1.b+p2.b;
return temp;
}左移运算符重载
1
2
3
4
5
6
7
8
// 只能全局重载左移运算符
// 若在类内重载会称为p.operator<<(cout) 即p<<cout (cout是ostream流中的对象)
// 这里先写cout即可实现cout在左边p在右边
// 返回值作为cout本身的引用,可以实现链式编程
ostream & operator<<(ostream &cout, person &p){
cout << "a=" << p.a << endl << "b=" << p.b;
return cout;
}递增运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 前置++
// 这里返回引用是为了确保只对同一个数据进行操作
myint& operator++(){
num++; // 先进行自增运算
return *this; // 再返回自身
}
// 后置++
// 这里为了防止重复,添加一个int占位符来实现函数重载以区分前后置
myint operator++(int){
myint temp = *this;
num++;
return temp;
}赋值运算符重载
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
#include<iostream>
using namespace std;
class person{
public:
int *age;
person(int age){
this->age = new int(age); // 年龄放在堆区
}
// 将age内存释放
~person(){
if (age!=NULL){
delete age;
age = NULL;
}
}
};
int main(){
person p1(1);
cout << "p1's age:" << *p1.age << endl;
person p2(2);
p2 = p1; // 赋值操作
cout <<"p2's age:"<<*p2.age<<endl;
}该代码出错,由于age开到了同一块堆区内存,故在p1赋值给p2时,两者的age指针都指向同一块区域
则当p2执行完毕,执行析构函数时,将该堆区内存释放;再等p1执行完毕,执行析构函数时,将已经释放的内存重复释放了,故报错
我们需要通过重载赋值运算符来实现深拷贝
1
2
3
4
5
6
7
8
9
10
// 重载赋值运算符
person& operator=(const person &p){
// 首先判断是否有属性在堆区,若有先释放,再深拷贝
if (age!=NULL){
delete age;
age = NULL;
}
age = new int(*p.age);
return *this;
}关系运算符重载
实现自定义数据类型的比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重载==运算符
bool operator==(const person &p){
if (this->name==p.name && this->age==p.age){
return true;
}
else return false;
}
// 重载!=运算符
bool operator!=(const person &p){
if (this->name==p.name && this->age==p.age){
return false;
}
else return true;
}大于小于类似
函数调用运算符重载
- 函数调用运算符即()
- 重载后的使用方式很像函数的调用,故也称为仿函数
- 仿函数没有固定写法
1
2
3
4
5
6
7
8
9
10
11
12
13
class print{
public:
//重载函数调用运算符
void operator()(string s){
cout << s << endl;
}
};
class add{
public:
int operator()(int n1, int n2){
return n1+n2;
}
};也可以用匿名函数对象进行调用
继承
有些类与其他类之间除了有共性,还有自己的特性,我们可以用继承的技术来减少重复代码优点:减少重复代码
语法:<子类>: <继承方式> <父类>
其中子类也称作派生类,父类也称基类
子类中的成员包含两部分:
一类是从基类继承过来的,一类是自己增加的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class page{
public:
void f1()
{
cout << "f1"<<endl;
}
void f2(){
cout << "f2" <<endl;
}
void f3(){
cout << "f3" << endl;
}
};
class page2:public page{
public:
void s1(){
cout << "s1"<<endl;
}
};继承方式
即:
- 私有成员子类均不可访问
- 公共继承下:各成员权限不变
- 保护继承下:public变为protected
- 私有继承下:public与protected变为private
继承中的对象模型
父类中所有非静态成员属性都会被子类继承
而私有属性被编译器隐藏,故访问不到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
class fa{
public:
int a;
protected:
int b;
private:
int c;
};
class son:public fa{
public:
int d;
};
int main(){
// 结果为16,即父类中所有属性都会被继承
cout << sizeof(son)<<endl;
}继承中的构造与析构顺序
子类继承父类后,创建子类对象时,也会调用父类的构造函数
顺序:先构造父类,再构造子类,析构相反
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;
class base{
public:
base(){
cout <<"base构造函数"<<endl;
}
~base(){
cout <<"base析构函数"<<endl;
}
};
class son:public base{
public:
son(){
cout <<"son构造函数"<<endl;
}
~son(){
cout <<"son析构函数"<<endl;
}
};
int main(){
son s;
}1
2
3
4
base构造函数
son构造函数
son析构函数
base析构函数同名成员处理方式
当子类和父类出现同名成员:子类的同名成员会隐藏掉所有父类中同名成员(函数重载也会被隐藏)
- 访问子类同名成员:直接访问
- 访问父类同名成员:加作用域
静态与非静态处理方式一样
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 <iostream>
using namespace std;
class fa{
public:
int a;
static int b;
fa(){
a = 100;
}
void f(){
cout <<"f in fa"<<endl;
}
static void ff(){
cout <<"ff in fa"<<endl;
}
};
class son:public fa{
public:
int a;
static int b;
son(){
a = 200;
}
void f(){
cout <<"f in son"<<endl;
}
static void ff(){
cout <<"ff in son"<<endl;
}
};
int fa::b = 10;
int son::b = 20;
int main(){
son s;
cout << "a in son:"<<s.a<<endl;
// 加作用域访问父类中的同名成员变量
cout << "a in fa:" <<s.fa::a<<endl;
s.f();
// 加作用域调用父类中的同名成员函数
s.fa::f();
// 非静态同名成员访问:通过对象
cout <<"static b in son:"<<s.b<<endl;
cout <<"static b in fa:"<<s.fa::b<<endl;
s.ff();
s.fa::ff();
// 非静态同名成员访问:通过类名
cout <<"static b in son:"<<son::b <<endl;
// 第一个::代表通过类名访问,第二个::代表访问父类作用域下
cout <<"static b in fa:"<<son::fa::b<<endl;
son::ff();
son::fa::ff();
}多继承
C++允许一个类继承多个类
语法:<子类>: <继承方式> <父类1>, <继承方式> <父类2>...
同名成员出现时需要加作用域区分
实际开发时不建议使用多继承
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
#include <iostream>
using namespace std;
class fa{
public:
int a;
fa(){
a = 100;
}
void f(){
cout <<"f in fa"<<endl;
}
};
class fa2{
public:
int a;
fa2(){
a= 200;
}
void f(){
cout <<"f in fa"<<endl;
}
};
class son:public fa, public fa2{
public:
int c;
int d;
son(){
c = 300;
d = 400;
}
};
int main(){
// 多继承子类空间
cout << "size of son:" <<sizeof(son)<<endl;
son s;
cout <<"a in fa:"<<s.fa::a<<endl;
cout <<"a in fa2:"<<s.fa2::a<<endl;
}菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类
菱形继承时,两个父类拥有相同数据,需要加作用域区分
但该数据对于最后的子类来说存在两份,会浪费空间,需要利用虚继承来解决问题
在需要虚继承的部分的访问权限前添加关键字vritual即可,此时对应父类称为虚基类
原理:此时的父类继承的是虚基类指针(vbptr),指向一个虚基类表,该表存储了指针位置以及对应偏移量,根据偏移量便可以找到继承的那唯一一份数据
缺点:虚继承占用内存很大
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
#include <iostream>
using namespace std;
class animal{
public:
int age;
};
// sheep 继承了 animal
class sheep: virtual public animal{};
// camel 继承了 animal
class camel: virtual public animal{};
// alpaca 继承了 sheep和camel
class alpaca: public sheep, public camel{};
int main(){
alpaca st;
// 菱形继承时,两个父类有相同数据,需要加作用域区分
st.sheep::age = 18;
st.camel::age = 20;
cout << "age of sheep: "<<st.sheep::age<<endl;
cout << "age of camel: "<<st.camel::age<<endl;
cout << "age of alpaca: "<<st.alpaca::age<<endl;
}多态
分类
- 静态多态:函数和运算符重载属于静态多态,复用函数名;函数地址在编译阶段确定
- 动态多态:派生类和虚函数实现运行时多态;函数地址在运行阶段确定
### 动态多态
满足条件:
存在继承关系
子类要重写父类的虚函数
(重写即函数返回值类型、函数名、参数列表完全相同)
使用:父类的指针或者引用指向子类对象
原理:父类记录的也是虚基类指针,指向对应虚基类表。当子类重写后,对应指向的虚基类表被现子类覆盖,得到对应参数
优点:
- 组织结构清晰
- 可读性强
- 前期后期拓展以及维护性高
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
#include<iostream>
using namespace std;
class animal{
public:
// 虚函数
virtual void speak(){
cout <<"speaking"<<endl;
}
};
class cat: public animal{
public:
void speak(){
cout <<"cat is speaking"<<endl;
}
};
// 地址在编译阶段确定函数地址,但为了让多个类输出对应,需要在运行时确定
// 当给animal中的speak变为虚函数后,便可以实现子类重写父类
void dospeak(animal &ani){ // 父类引用
ani.speak(); // 但调用的是animal中的speak函数
}
int main(){
cat cat;
dospeak(cat); // 父类引用指向了子类
}在开发时,提倡开闭原则:即对扩展进行开放,对修改进行关闭
纯虚函数与抽象类
多态中,通常父类中的虚函数实现是毫无意义的,都是调用子类中的重写内容
因此可以将虚函数改为纯虚函数
语法:virtual <返回值类型> <函数名>([参数列表]) = 0;
此时这个类称为抽象类
- 抽象类无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
class base{
public:
virtual void func() = 0; // 纯虚函数
};
class son :public base{
public:
// 子类必须重写父类中的纯虚函数
virtual void func(){
cout <<"func调用"<<endl;
}
};
int main(){
// base b; 不允许实例化抽象类
base *b = new son;
b->func();
}虚析构与纯虚析构
多态使用时,如果子类中有属性开辟到堆区,则父类指针在释放时就无法调用到子类的虚构代码,此时需要将父类中的析构函数改为虚析构或纯虚析构
- 可以解决父类指针释放子类对象
- 都需要有函数具体实现
其中,纯虚析构需要类外实现
有纯虚析构后该类便成为了抽象类
语法:virtual ~<类名>(){}或virtual ~<类名>()=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
#include <iostream>
using namespace std;
class animal{
public:
virtual void speak() = 0;
// 用虚析构解决父类指针释放子类对象时释放不完全的问题
virtual ~animal(){
cout <<"animal析构调用"<<endl;
}
};
class cat: public animal{
public:
cat(string name){
cname = new string(name);
}
virtual void speak(){
cout <<*cname<<"'s speaking"<<endl;
}
~cat(){
if (cname !=NULL){
cout <<"cat析构调用"<<endl;
delete cname;
cname = NULL;
}
}
string *cname;
};
int main(){
animal * ani = new cat("Tom");
ani->speak();
// 父类指针析构时不会调用子类析构函数,导致若子类有析构属性时不会被释放
delete ani;
}杂项
override与final限定符
在基类是虚函数的情况下,子类对该虚函数进行重写,在要重写的函数后面加上override可以防止因为子类的拼写错误,数据类型错误等问题,导致无法调用到子类而直接调用了基类
(若不相同会直接报错)
在虚函数声明的结尾加上final可以防止该虚函数被子类重写,在类声明的后面加上final可以防止该类被继承
注意这两个限定符不是保留关键字
const成员函数
在成员函数声明和定义的后面加上关键字const
const成员函数不能修改其成员变量,不能调用非const成员函数,不能通过const类型的对象调用非const成员函数
即常量对象只可以调用常量方法