面向对象编程

  C++使用struct、class来定义一个类:struct的默认成员权限是public,class的默认成员权限是private;除此之外,二者基本无差别。

1
2
3
4
5
6
7
8
9
class Student{
private:
string name;
double score;
public:
double GetSorce(){
return score;
}
};

上面学生的类并不是真实世界中的学生,只是一个抽象的概念,并不包含真实世界中学生的所有属性,只是把一些属性抽象出来。

  

  面向对象的误区:对象是对现实世界中具体物体的反映,继承是对物体分类的反映?这个观念是错误的。举个例子,现实生活中我们往往把正方形看作是长和宽都相等的特殊的长方形,如果把这个思想引入到C++中,可能会这么设计继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class rectangle  //长方形
{
virtual void SetLength(double a)
{
……
}
};

class square : public rectangle //正方形,继承长方形类
{
virtual void SetLength(double a)
{
……
}
}

  上面设计了两个类,一个长方形类,一个正方形类,其中正方形类继承了长方形类以及内部的方法SetLength。当我们调用长方形类SetLength的方法时,我们只是修改了长方形的长,长方形的宽不受影响;但是如果我们调用了正方形对象的SetLength方法,不仅长会受到影响,宽也会受到影响。这个从面向对象的继承体系来说就有很大的问题了。所以我们不要把现实世界中的关系代入到面向对象编程中。

  

抽象法则

具体类型的抽象

  • 让自定义的类像内置类型一样

  一个普通的int型变量,可以完成加、减、乘、除、比较、输出、自增等等一系列操作;如果现在有一个自定义的复数类型,我们自然也希望可以像使用int型变量一样使用它,同时它对我们是一个黑盒,一种抽象,我们不需要关心内部是如何实现的。

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
//complex.h
#pragma once

#include <iostream>
using namespace std;

class Complex
{
public:
Complex(); // 默认构造函数
Complex(double r, double i); // 构造函数
virtual ~Complex(); // 析构函数
Complex(const Complex& x); // 拷贝构造
Complex& operator=(const Complex &c); // =号运算符


double GetReal( ) const { return _real; } //const修饰函数,函数体内成员变量不允许改变
void SetReal(double d) { _real = d; }
double GetImage() const { return _image; }//const修饰函数,函数体内成员变量不允许改变
void SetImage(double i) { _image = i; }

// 运算符重载
//这里函数参数传递的是引用,因为可能从外面传递另外一个complex,但我们不想生成一个副本
//重载等号操作符时返回的是引用,可以让返回值在其他地方使用
Complex operator+(const Complex &c) const;
Complex& operator+=(const Complex &c);
Complex operator-(const Complex &c) const;
Complex& operator-=(const Complex &c);
Complex operator*(const Complex &c) const;
Complex& operator*=(const Complex &c);
Complex operator/(const Complex &c) const;
Complex& operator/=(const Complex &c);
Complex& operator=(const Complex &c);

bool operator==(const Complex &c) const;
bool operator!=(const Complex &c) const;
bool operator>(const Complex &c) const;
bool operator>=(const Complex &c) const;
bool operator<(const Complex &c) const;
bool operator<=(const Complex &c) const;


// 前置和后置++
Complex& operator++(); //前置++
Complex operator++(int); //后置++
Complex& operator--(); //前置--
Complex operator--(int); //后置--


//标准输入输出IO重载
//输入输出需要把当前的Complex对象传递进来,也要把外部的ostream或istream传递进来
//但是传递进来的ostream本身不是当前类内的成员变量,不能访问当前类的属性
//所以定义为friend
friend ostream& operator<<(ostream& os, const Complex &x);
friend istream& operator>>(istream& is, Complex &x);

private:
double _real; // 复数的实部
double _image; // 复数的虚部
};
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//complex.cpp
#include "stdafx.h"
#include "complex.h"

Complex::Complex()
{
_real = 0.0;
_image = 0.0;
//cout << "Complex::Complex()" << endl;
}

Complex::Complex(double r, double i)
{
_real = r;
_image = i;
//cout << "Complex::Complex(double r, double i)" << endl;
}

Complex::Complex(const Complex& c)
{
_real = c._real;
_image = c._image;
//cout << "Complex::Complex(const Complex& c)" << endl;
}

Complex& Complex::operator= (const Complex& c)
{
if (this != &c) //如果二者相等,就不需要做操作,这里比较的是地址
{
_real = c._real;
_image = c._image;
}
return *this;
}

Complex::~Complex()
{
_real = _image = 0.0;
//cout << "Complex::~Complex()" << endl;
}

Complex Complex::operator+ (const Complex& c) const
{
//Complex tmp;
//tmp._real = _real + c._real;
//tmp._image = _image + c._image;
//return tmp;

//上面的写法,在return的时候会触发拷贝构造
//调试打断点可以发现,return会进入“=”操作符的重载,重载函数入参的地址和tmp这个临时对象的地址不一样
//这个tmp被传递到拷贝构造里了

return Complex(_real + c._real, _image + c._image);
//临时对象优化,上面的写法就不会触发拷贝构造,直接走了自定义的构造函数
}

Complex& Complex::operator+= (const Complex& c)
{
_real += c._real;
_image += c._image;

return *this;
}

Complex Complex::operator-(const Complex &c) const
{
return Complex(_real - c._real, _image - c._image);
}

Complex& Complex::operator-=(const Complex &c)
{
_real -= c._real;
_image -= c._image;

return *this;
}

Complex Complex::operator*(const Complex &c) const
{
return Complex(_real*c._real - _image*c._image, _real*c._image + _image*c._real);
}

Complex& Complex::operator*=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
_real = tmp._real*c._real - _image*c._image;
_image = tmp._real*c._image + tmp._image*c._real;
return *this;
}

Complex Complex::operator/(const Complex &c) const
{
double t = c._real*c._real + c._image*c._image;
return Complex((_real*c._real - _image*(-c._image)) / t, (_real*(-c._image) + _image*c._real) / t);
}

Complex& Complex::operator/=(const Complex &c)
{
Complex tmp(*this); //拷贝构造函数
double t = c._real*c._real + c._image*c._image;
_real = (tmp._real*c._real - tmp._image*(-c._image)) / t;
_image = (tmp._real*(-c._image) + tmp._image*c._real) / t;
return *this;
}

bool Complex::operator==(const Complex& c) const
{
return (_real == c._real) && (_image == c._image);
}

bool Complex::operator!=(const Complex& c) const
{
return !( (_real == c._real) && (_image == c._image) );
}

bool Complex::operator>(const Complex &c) const
{
return (_real > c._real) && (_image > c._image);
}

bool Complex::operator>=(const Complex &c) const
{
return (_real >= c._real) && (_image >= c._image);
}

bool Complex::operator<(const Complex &c) const
{
return (_real < c._real) && (_image < c._image);
}

bool Complex::operator<=(const Complex &c) const
{
return (_real <= c._real) && (_image <= c._image);
}

Complex& Complex::operator++ () // 前置++
{
_real++;
_image++;
return *this;
}

Complex Complex::operator++ (int) // 后置++
{
//先用中间变量,然后执行计算,之后把原始值传递出去
//Complex tmp(*this);
//_real++;
//_image++;
//return tmp;

//上面的方法会调用拷贝构造,使用下面方法优化
return Complex(_real++, _image++);
}

Complex& Complex::operator--() //前置--
{
_real--;
_image--;
return *this;
}

Complex Complex::operator--(int) //后置--
{
return Complex(_real--, _image--);
}


//注意输入输出的写法,虽然在类内声明了,但本身并不是类的成员
//所以定义为全局函数,不要加“Complex::”
ostream& operator<<(ostream& os, const Complex &x)
{
os << "real value is " << x._real << " image value is " << x._image;
return os;
}

istream& operator >> (istream& is, Complex &x)
{
is >> x._real >> x._image;
return is;
}
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
int main()
{

Complex a(1.0, 2.0);
cout << a.GetReal() << endl; // 1.0
cout << a.GetImage() << endl; // 2.0
a.SetImage(2.0);
a.SetReal(3.0);
cout << a.GetReal() << endl; // 3.0
cout << a.GetImage() << endl; // 2.0



Complex a(3.0, 2.0);

//系统也会默认有等号运算符的重载,我们自己不实现也可以用
Complex c;
c = a + b;

//注意上面的写法和Complex c = a + b;的含义是不一样的
//上面的行为是先声明后赋值
//Complex c = a + b的行为是定义
//调试时需要区别,如果要调试赋值操作,不要写成定义流程了

//实际工程中尽量写Complex c = a + b;这个,减少一个默认构造函数提高效率


Complex d;
d = c++;
cout << d << endl;
cin >> d;
cout << d << endl;

return 0;
}

  类中有个this指针,指向当前对象本身。

  类创建后会系统默认创建一个构造函数,我们可以自己实现构造函数。但我们如果重写了构造函数,那么原始的默认构造函数就不存在了,如果想使用需要重新声明实现。

  等号运算符也一样,系统会默认帮我们重载。不过我们最好不要过于相信系统默认的重载,在复杂情况下运算的结果可能不是我们想要的。

  程序中的临时对象一定要注意优化,避免产生临时对象,否则会触发拷贝构造。

  

抽象类型的抽象

  数学中有不同的图形,比如长方形、原型、三角形;多种图形计算周长、面积的方法不同,但都需要一个计算方法。我们可以抽象出一个图形类Shape,用Shape类进行公共层面的抽象操作。

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
#include "stdafx.h"
#include <iostream>
using namespace std;

// 抽象类
// 抽象类的特征是虚函数后面都是"=0",意味着不可以有实际的对象
class Shape
{
public:
virtual double Area() const = 0;// 子类方法实现不一致时要加上virtual
virtual void Show() = 0;
void SetColor(int color) { _color = color; }
void Display() //一个共性的方法
{
cout << Area() << endl;
}
private:
int _color;
};

class Square: public Shape
{
public:
Square(double len) :_len(len) { } //用参数列表初始化
void Show() { cout << "Square" << endl; }
double Area() const
{
return _len*_len;
}
private:
double _len; //边长
};


class Circle : public Shape
{
public:
Circle(double radius) :_radius(radius) {} //用参数列表初始化
void Show() { cout << "Circle" << endl; }
double Area() const
{
return 3.1415926*_radius*_radius;
}

private:
double _radius; //半径
};

class Triangle : public Shape
{
public:
Triangle(double len, double height) :_len(len), _height(height){}
void Show() { cout << "Triangle" << endl; }
double Area() const
{
return 0.5*_len*_height;
}

private:
double _len; //底
double _height; //高
};

int main()
{
// 多态的实现,可以让我们面对变化,尽可能少修改原有的逻辑,直接扩充逻辑
const int shapeNum = 3;

Square s1(2.0);
s1.SetColor(1);
Circle c1(2.0);
Triangle t1(2.0, 3.0);

Shape* shapes[shapeNum];
shapes[0] = &s1;
shapes[1] = &c1;
shapes[2] = &t1;

for (unsigned int index = 0; index < shapeNum; index++)
{
shapes[index]->Display();
}

cout << endl;
cout << sizeof(s1) << endl;

return 0;

}

对象模型和虚函数

  C++的对象模型中,子类对象中包含了父类。父类中有一个虚函数列表,是个类似数组的结构。对象模型中只保留成员变量信息和虚函数列表,其他的共有函数是通过this指针来访问的。

深拷贝、浅拷贝、写时复制

  • 浅拷贝:只拷贝指针地址,C++默认拷贝构造函数与赋值运算符重载都是浅拷贝;节省空间,但容易引发多次释放;
  • 深拷贝:重新分配堆内存,拷贝指针指向内容;浪费空间,但是不会导致多次释放;

  深拷贝的思想比较常见,比如C++的一个优化策略叫写时复制。有个信息存放在内存空间中,如果大家都去读取,那么内存中保留一份即可,但如果有地方需要写数据,那么会复制出一个新的地址空间存放相同数据,写操作作用在新地址空间上。

  深拷贝和浅拷贝各有优劣,如果想兼有二者的优点,有两种可用方案:第一是使用引用计数,用shared_ptr的思路,每有一个指针指向对象,引用计数+1,直到引用计数清零时再清理内存;第二种是C++11的新标准移动语义move,把资源让渡,既可以避免重新创建空间,也防止空间释放导致新问题。

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//自定义字符串类String操作

class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数(深拷贝)
String(String&& other); // 移动构造函数,两个&&代表右值引用
~String(void); // 析构函数
String& operator= (const String& other); // 赋值函数
String& operator=(String&& rhs)noexcept; // 移动赋值运算符

friend ostream& operator<<(ostream& os, const String &c); // cout输出

private:
char *m_data; // 用于保存字符串
};


// String 的普通构造函数
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
if (m_data != NULL)
{
*m_data = '\0';
}
else
{
exit(-1);
}
}
else
{
int len = strlen(str);
m_data = new char[len + 1]; //把'\0'的空间留出来
if (m_data != NULL)
{
strcpy(m_data, str);
}
else
{
exit(-1);
}
}
}

// 拷贝构造函数
// 开辟新的内存空间了,属于深拷贝
String::String(const String &other)
{
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}
}

// 移动构造函数
String::String(String&& other)
{
if (other.m_data != NULL)
{
// 资源让渡
m_data = other.m_data;
// 之前的联接断开
other.m_data = NULL;
}
}

// 赋值函数
String& String::operator= (const String &other)
{
if (this == &other)
{
return *this;
}
// 释放原有的内容
delete[] m_data;
// 重新分配资源并赋值
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}

return *this;
}

// 移动赋值运算符
String& String::operator=(String&& rhs)noexcept
{
if(this != &rhs)
{
// 清空原有的
delete[] m_data;

//赋值新的
m_data = rhs.m_data;

// 我们接管了内存,原有内存不需要释放,把链接断开即可
rhs.m_data = NULL;
}
return *this;
}

// String 的析构函数
String::~String(void)
{
if (m_data != NULL)
{
delete[] m_data;
}
}

ostream& operator<<(ostream& os, const String &c)
{
os << c.m_data;
return os;
}

int main()
{
String s1("Hello"); // 构造函数
cout << s1 << endl;
//String s2 = s1; // 调用拷贝构造函数
String s2(s1); // 调用拷贝构造函数
cout << s2 << endl;
String s2A(std::move(s1)); // 移动构造函数
cout << s2A << endl;
String s3; // 无参构造函数
cout << s3 << endl;
s3 = s2; // 调用赋值函数
cout << s3 << endl;
String s3A; // 无参构造函数
s3A = std::move(s2A); // 移动赋值运算符
cout << s3A << endl;

return 0;
}

面向对象三大特性

  • 封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问;封装可以使得代码模块化;
  • 继承性:让某种类型对象获得另一个类型对象的属性和方法,继承可以扩展已经存在的代码;
  • 多态性:同一事物在保有原来特点的情况下表现出不同事物的能力,即不同对象会产生不同的行为;多态的目的是为了接口重用;

  面向对象是软件工程发展到一定阶段为了管理代码和数据提出的一种方法,它没有解决以前解决不了的问题,不是万能的,只是为我们便捷的开发出能适应快速变化的软件提供了可能。面向对象不是对现实世界的映射,但它的封装性可以把问题简化;它的继承性可以减少代码重复,避免重新发明轮子;它的多态可以实现灵活的功能扩充,提升开发效率;