小古银的官方网站(完整教程):http://www.xiaoguyin.com/
C++入门教程视频:https://www.bilibili.com/video/av20868986/
目录
面向对象编程中级知识——前言
这部分教程将讲解类的其他更多的用法,当学完这部分内容之后,就可以设计出更好用的类。
详述构造函数
构造函数有很多种,而这篇教程是把所有的构造函数都说清楚。
复制构造函数和默认复制构造函数
基础示例 1
#include <iostream> // std::cout std::endl
class point_t
{
public:
point_t(int a, int b);
public:
int x;
int y;
};
int main(void)
{
point_t point1(555, 666);
auto point2 = point1;
std::cout << "point1横坐标的值是:" << point1.x << std::endl;
std::cout << "point1纵坐标的值是:" << point1.y << std::endl;
std::cout << "point2横坐标的值是:" << point2.x << std::endl;
std::cout << "point2纵坐标的值是:" << point2.y << std::endl;
return 0;
}
point_t::point_t(int a, int b)
: x(a)
, y(b)
{
}
输出结果:
point1横坐标的值是:555
point1纵坐标的值是:666
point2横坐标的值是:555
point2纵坐标的值是:666
基础讲解 1
我们知道,当一个对象被声明创建的时候,会调用构造函数。而对象声明的时候也可以被初始化,而不同的初始值又会调用不同的构造函数。当我们为对象初始化为同类型的左值时,调用的就是复制构造函数。最重要的是,所有的类如果不显式地声明和定义复制构造函数,那么就会有一个默认的复制构造函数。而默认复制构造函数的行为是:将对象的成员变量直接赋值给新对象的成员变量。
上面代码中,编译器会为point_t
添加一个默认复制构造函数,那么当auto point2 = point1;
复制创建时,默认复制构造函数的行为如下:
point2.x = point1.x;
point2.y = point1.y;
基础示例 2
#include <iostream>
class test
{
public:
test(void); // 无参数构造函数
test(const test &x); // 复制构造函数
~test(void);
private:
int *m_pointer;
};
int main(void)
{
test obj1; // obj1 调用无参数构造函数
test obj2 = obj1; // obj2 调用复制构造函数
return 0;
}
test::test(void)
: m_pointer(new int(1024))
{
}
test::test(const test &x)
: m_pointer(new int(*(x.m_pointer)))
{
}
test::~test(void)
{
delete m_pointer;
m_pointer = nullptr;
}
基础讲解 2
先看看复制构造函数的声明:
test(const test &x);
复制构造函数的参数必须只有一个(参数多了就是普通的构造函数,还是会有默认复制构造函数),而且它的类型也必须是自身类型的引用。只要显式写明了复制构造函数,那么就没有默认的复制构造函数,这时候就要在构造函数里手动写复制操作的代码,如果不写的话,就等于没有操作,什么都不会发生。
以下代码中,在obj2
初始化时赋值obj1
,那么obj2
调用的构造函数就是复制构造函数,并且将obj1
作为复制构造函数的参数进行处理:
test obj2 = obj1;
接下来就是定义复制构造函数:
test::test(const test &x)
: m_pointer(new int(*(x.m_pointer)))
{
}
由于它是构造函数,所以它也是有初始化列表。m_pointer
初始化为一个新内存空间的首地址,并且把x
的成员变量m_pointer
保存的内存里的值作为初始值赋值给新内存空间。
注意:虽然x
直接调用了私有成员m_pointer
,但仍然是在test
类内使用的,所以是正确使用,不会报错。
由于已经显式地写明复制构造函数,那么就不会再有默认复制构造函数,这时候要手动实现复制了。
基础拓展 2
在看接下来内容时,不妨思考一下,假设上面代码中的test
不写复制构造函数,会造成怎样的结果?
由于默认的复制构造函数只会直接复制成员变量,也就是obj1.m_pointer
赋值给obj2.m_pointer
,也就是说它们保存着同一个内存地址。
当obj1
和obj2
离开作用域时,各自调用析构函数。在其中一个离开作用域时,先调用了析构函数,释放了内存地址所代表的内存空间;接着另外一个调用析构函数,由于地址此时已经不代表任何内存空间了,继续释放就会使程序崩溃。
移动构造函数和默认移动构造函数
上面是当初始值是左值时,调用复制构造函数;那么当初始值是右值时,就会调用移动构造函数。
- 在没有写出复制构造函数和移动构造函数的情况下,就会存在默认复制构造函数和默认移动构造函数,对应的就是所有成员变量逐个复制和逐个移动。
- 在写出复制构造函数但没有写出移动构造函数的情况下,当初始值是左值时,将调用复制构造函数;当初始值是右值时,也是调用复制构造函数。
- 在写出移动构造函数但没有写出复制构造函数的情况下,当初始值是左值时,编译报错;当初始值是右值时,将调用移动构造函数。
- 在写出复制构造函数和移动构造函数的情况下,当初始值是左值时,将调用复制构造函数;当初始值是右值时,将调用移动构造函数。
基础示例
#include <iostream> // std::cout std::endl
class point_t
{
public:
point_t(int a, int b);
point_t(point_t &&x);
public:
int x;
int y;
};
int main(void)
{
point_t point1(555, 666);
auto point2 = std::move(point1);
std::cout << "point1横坐标的值是:" << point1.x << std::endl;
std::cout << "point1纵坐标的值是:" << point1.y << std::endl;
std::cout << "point2横坐标的值是:" << point2.x << std::endl;
std::cout << "point2纵坐标的值是:" << point2.y << std::endl;
return 0;
}
point_t::point_t(int a, int b)
: x(a)
, y(b)
{
}
point_t::point_t(point_t &&obj)
: x(obj.x)
, y(obj.y)
{
obj.x = 0;
obj.y = 0;
}
输出结果:
point1横坐标的值是:0
point1纵坐标的值是:0
point2横坐标的值是:555
point2纵坐标的值是:666
基础讲解
类point_t
的移动构造函数的声明如下:
point_t(point_t &&x);
类point_t
的移动构造函数的定义如下:
point_t::point_t(point_t &&obj)
: x(obj.x)
, y(obj.y)
{
obj.x = 0;
obj.y = 0;
}
初始值是右值时调用移动构造函数,而上面代码定义的移动构造函数实现了移动的语义。将右值的所有成员变量全部直接赋值给新对象的成员变量,然后使右值的成员变量全部清空。
那么这时候就可以这样使用了:
point_t point1(555, 666);
auto point2 = std::move(point1);
默认构造函数
既然有默认复制构造函数和默认移动构造函数,同样也会有普通的构造函数。当类中什么构造函数都没有的时候,就会存在默认构造函数。默认构造函数就是没有参数的构造函数,而且它什么都不做。在类中只要明确写出一个构造函数,无论是有参数的构造函数还是复制构造函数还是移动构造函数,编译器在编译时就不会给类加一个默认的构造函数。
explicit
基础示例 1
实现一个integer_t
类用来保存int
类型的值,成员函数get()
用于返回保存的整数值。
#include <iostream> // std::cout std::endl
class integer_t
{
public:
integer_t(int n);
int get(void) const noexcept;
private:
int m_value;
};
int main(void)
{
integer_t value1(666);
integer_t value2 = 2333;
std::cout << "value1: " << value1.get() << std::endl;
std::cout << "value2: " << value2.get() << std::endl;
return 0;
}
integer_t::integer_t(int n)
: m_value(n)
{
}
int integer_t::get(void) const noexcept
{
return m_value;
}
输出结果:
value1: 666
value2: 2333
基础讲解 1
当构造函数只需要传入一个参数时,对象的初始化可以不使用()
而使用=
。所以复制构造和移动构造的初始化,既可以用()
也可以用=
,它们是一样的。如下:
std::string text1;
auto text2 = text1; // 调用复制构造函数
auto text3(text1); // 跟上面一样
基础示例 2
#include <iostream> // std::cout std::endl
class integer_t
{
public:
explicit integer_t(int n);
int get(void) const noexcept;
private:
int m_value;
};
int main(void)
{
integer_t value(666);
std::cout << "value: " << value.get() << std::endl;
// integer_t value2 = 2333; // 去掉开头注释将会报错
return 0;
}
integer_t::integer_t(int n)
: m_value(n)
{
}
int integer_t::get(void) const noexcept
{
return m_value;
}
基础讲解 2
explicit
的作用:禁止通过=
对对象进行初始化。
在构造函数的声明前面加上关键字explicit
,函数定义不需要修改:
explicit integer_t(int n);
那么现在如果再用=
赋值2333
的话就会报编译错误。当然,关键字explicit
同样适用于复制构造函数和移动构造函数。
实际上,explicit
应该用在不符合理解的初始化上,可以用也可以不用。上面代码中的integer_t value2 = 2333;
,将整数赋值给整数对象是符合理解的,不应该加上explicit
。而std::vector<int> x(6);
,初始化时创建6个元素,这个时候用=
的话,在读代码时就不容易理解了(容器x
赋值为整数数据6是不符合正常的理解的),所以std::vector
的这个构造函数使用了explicit
。
委托构造函数和目标构造函数
基础示例
#include <iostream> // std::cout std::endl
class point_t
{
public:
point_t(void);
point_t(int a, int b);
public:
int x;
int y;
};
int main(void)
{
point_t point;
std::cout << "point横坐标的值是:" << point.x << std::endl;
std::cout << "point纵坐标的值是:" << point.y << std::endl;
return 0;
}
point_t::point_t(void)
// : x(0)
// , y(0)
// 上面代码可以简化成下面代码
: point_t(0, 0)
{
}
point_t::point_t(int a, int b)
: x(a)
, y(b)
{
}
输出结果:
point横坐标的值是:0
point纵坐标的值是:0
基础讲解
从代码中可以看到,无参数的构造函数在初始化列表里调用了有参数的构造函数,让有参数的构造函数帮忙初始化成员变量。当一个类的成员变量比较多的时候,这样写就可以大幅度减少代码量了。
这种情况,为了区分这两个构造函数,无参数的构造函数又叫做委托构造函数;而无参数构造函数里面的初始化列表中的构造函数又叫做目标构造函数。
注意:委托构造函数的初始化列表只能有目标构造函数,不能再有其他的成员变量,否则编译报错。
默认析构函数
既然说了那么多的构造函数和那么多的默认构造函数,那么现在顺便再提醒一下析构函数。放心,我会很简短地说明的。
当没有明确写出析构函数时,编译器会给类加上一个默认的析构函数,它也是什么都不做的。
巩固练习
还记得之前设计的simple_vector
类吗?它并不完美,现在需要继续完善simple_vector
:
simple_vector
有一个构造函数需要添加关键字explicit
,找出来并添加上去。- 为
simple_vector
添加复制构造函数和移动构造函数。