C++类总结

最近重新整理了一下C++的面向对象的一些知识,记录一下。

什么是面向对象?#

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。面向对象的三大特性如下所示:

  1. 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
  2. 继承:子类继承父类的特征和行为,子类有父类的非private方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
  3. 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式,有关多态,我在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
class Complex {
public:
Complex() {
_real = 0;
_image = 0;
cout << "Complex()" << endl;
}
Complex(double r, double i)
{
_real = r;
_image = i;
cout << "Complex(double r, double i)" << endl;
}// constructor

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

virtual ~Complex() {
cout << "des" << endl;
} // destructor

double real() const { return _real; }
void real(double d) { _real = d; }
void image(double i) { _image = i; }
double image() const { return _image; }

Complex operator+ (const Complex& x)
{
Complex tmp;
tmp._real = x._real;
tmp._image = x._image;
return tmp;
}

//返回对其的引用,优化
Complex& operator= (const Complex& x)
{
// 只有不相等才赋值
if (this != &x)
{
cout << "Complex& operator= (const Complex& x)" << endl;
_real = x._real;
_image = x._image;
}
// return itself
return *this;
}

private:
double _real; // "_" represents member
double _image;
};

运算符重载#

可以对复数类进行运算符重载,在这里使用&引用可以节省空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Complex operator+ (const Complex& x)
{
Complex tmp;
return tmp(_real + x._real, _image + x._image);
}

// 返回对其的引用,优化
Complex& operator= (const Complex& x)
{
// 只有不相等才赋值
if (this != &x)
{
_real = x._real;
_image = x._image;
}
// return itself
return *this;
}

// 前置和后置操作符重载

重载前置后置操作符#

++,—操作符也可以进行重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    // 前置操作符
Complex& operator++ ()
{
_real++;
_image++;
return *this;
}
// 后置操作符:参数有个int
Complex& operator++ (int)
{
Complex temp;
temp = *this;
_real++;
_image++;
return temp;
}
// 优化后置操作符实现:不让临时对象产生
Complex& operator++ (int)
{
// 避免了拷贝构造
return Complex(_real++, _image++);
}

相对来说,前置操作符的效率更高更好,因为没有拷贝构造和析构的过程。

重载标准IO#

要注意的点在于,重载标准IO不能将其作为成员函数,但是由于需要访问到Complex类成员内的私有变量,所以应该将其设为友元(friend)。

1
2
3
4
5
class Complex {
public:
friend ostream& operator<< (ostream& os, const Complex& x);
friend istream& operator>> (istream& in, Complex& x);
};

在实现时,要注意输入操作的第二个形参Complex& x不能是const的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 输出
ostream& operator<< (ostream& os, const Complex& x)
{
os << x._real << x._image;
return os;
}

// 输入
istream&
operator>> (istream& in, Complex& x)
{
in >> x._real >> x._image;
return in;
}

拷贝构造#

拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。作用是用一个已存在的对象去初始化同类型的新对象。

如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个隐含的复制构造函数。

这个构造函数执行的功能是:用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员。

如果不希望编译器的构造隐式拷贝构造函数,可以使用=delete指示编译器不生成默认复制构造函数。

在对象的编写中,要避免临时对象的产生,否则容易出现不必要的拷贝构造。

1
2
3
4
5
6
7
8
9
10
Complex c = a; // 调用拷贝构造函数
Complex c(a); // 调用拷贝构造函数
Complex c = a + b; // 对于下面的实现,此时不一定会调用拷贝构造函数,编译器会进行一定的优化,直接将Complex c指向tmp的地址
Complex operator+ (const Complex& x)
{
Complex tmp;
tmp._real = x._real;
tmp._image = x._image;
return tmp;
}

可以将其优化一下,避免产生临时对象:

1
2
3
4
5
// 优化版本
Complex operator+ (const Complex& x)
{
return Complex(x._real, x._image);
}

调用拷贝构造函数的情况总结:

  1. 定义一个对象时,以本类另一个对象作为初始值,发生拷贝构造:

    1
    2
    Complex c = a; // 调用拷贝构造函数
    Complex c(a); // 调用拷贝构造函数
  2. 如果函数的形参为对某对象的引用,调用函数时,将使用实参对象初始化形参对象,发生拷贝构造;

    1
    2
    3
    4
    5
    6
    7
    8
    //形参为Complex类对象的函数
    void fun1(Complex p) {
    cout << p.real() << endl;
    }
    // 优化:传入const reference,避免不必要的拷贝构造
    void fun1(const Complex &p) {
    cout << p.real() << endl;
    }
  3. 如果函数的返回值是对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生拷贝构造。(类似于Complex c = a + b;的语句,编译器会进行一定的优化)

    1
    Complex d = fun2();

常量成员函数#

常量成员函数不能改变调用它的对象的内容,而常量对象,以及其引用或者制作都只能调用常量成员函数。

构造函数#

合成的默认构造函数#

深拷贝(deep copy)与浅拷贝(shallow copy)问题#

深浅拷贝问题一直是个比较重要的问题,简单来说,如果变量在堆中被动态分配了内存,那么一般需要深拷贝。

浅拷贝#

在C++中,默认都是浅拷贝,只拷贝指针地址,拷贝构造函数与赋值运算符重载都是浅拷贝。浅拷贝只会发生在没有进行动态内存分配的过程中,如果有变量在堆中被分配了空间,那么就会拷贝相同的指针地址。如下图:

浅拷贝说明

浅拷贝说明:

优点是节省空间。

问题是:如果某一个指向breath内存空间指针的指针释放了breath,容易引发多次释放问题。

深拷贝#

深拷贝就是重新分配堆内存,拷贝指针指向的内容,复制一份到新的堆内存。

深拷贝说明

深拷贝相对比较浪费空间,但是不容易引发多次释放。

例子:string类的实现:

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
class myString
{
public:
friend std::ostream& operator<< (std::ostream& o, const myString& s);
myString(const char* str = nullptr);
myString(const myString& other);
myString(myString&& other);
~myString();

myString& operator=(const myString& other);
myString& operator=(myString&& rhs) noexcept;
private:
char* m_data;
};

myString::myString(const char *str) {
if (str == nullptr)
{
m_data = new char[1];
if (m_data != nullptr)
{
*m_data = '\0';
}
else
{ exit(-1);}
}
else
{
int len = strlen(str);
m_data = new char[len+1];
if (m_data != nullptr)
{
strcpy(m_data, str);
} else { exit(-1); }
}
}

myString::myString(const myString &other) {
//m_data = other.m_data;
int len = strlen(other.m_data);
m_data = new char[len+1];
if (m_data != nullptr)
{
strcpy(m_data, other.m_data);
} else { exit(-1); }
}

myString::~myString() {
if (m_data != nullptr)
{
delete [] m_data;
}
}

myString &myString::operator=(const myString &other) {

if (this == &other)
return *this;

// 此处为深拷贝
// 释放原有的资源
delete [] m_data;
// 重新分配资源并赋值
int len = strlen(other.m_data);
m_data = new char[len+1];
if (m_data != nullptr)
{
strcpy(m_data, other.m_data);
} else { exit(-1); }
return *this;
}

如何兼有两个的优点?#

方法一:使用引用计数,具体见C++ primer 461页

方法二:std::move()语句

C++提供了移动语义去避免不必要的拷贝,在重新分配内存的过程中,从旧内存拷贝到新内存是不必要的,只需要移动元素即可。

为了支持移动操作,C++11标准引入了一个新的引用类型,右值引用(rvalue reference),通过&&来获得右值引用,其重要性质就是只能绑定到一个将要销毁的对象身上。

标准库std::move函数就可以将一个左值转换到一个对应的右值引用类型:

1
int &&rr3 = std::move(rr1);

对于上面的string类,也可以定义相应的移动构造函数和移动赋值运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
myString::myString(myString &&other) {
if (other.m_data != nullptr)
{
// 资源让度
m_data = other.m_data;
other.m_data = nullptr;
}
}

myString &myString::operator=(myString &&rhs) noexcept {
if (this != &rhs)
{
delete [] this->m_data;
m_data = rhs.m_data;
rhs.m_data = nullptr;
}
return *this;
}