C++11 @4
Class 介绍
程序总还是有顺序,有流程的。但是在这个流程里,开发者更多关注的是对象以及对象之间的交互,而不是孤零零的函数。
Class 还支持抽象,继承和多态。这些概念完全就是围绕面向对象来设计和考虑的,它关注的是类和类之间的关系。
C++ 类有和 Java 一样的访问权限控制,关键词也是 public、private 和 protected 三种。Java 中,每个成员(包含函数和变量)都需要单独声明访问权限,而 C++ 则是分组控制的。
如果没有指明访问权限,则默认使用
private访问权限。
构造,赋值和析构函数
构造函数主要的功能是完成类实例的初始化,也就是对象的成员变量的初始化。C++ 默认构造函数(没有参数或者所有参数都有默认值),构造函数中请使用 初值列表 的方式来完成变量初始化。注意,const 是 C++ 中的常量修饰符,与 Java 的 final 类似。
// int* age;
// static const int size = 512;
// Base 是一个类
// 默认的构造函数
// 这就是 初值列表的方式来完成变量初始化
Base::Base() : intField(100), boolField(true), age(new int[size]) {
// do sth
}
// 携带参数的构造函数
Base::Base(int a) : intField{a}, boolField{true}, age{new int[size]} {
// 成员初始化用的是 {}
// do sth
}
// 拷贝构造函数
Base::Base(const Base& other) : intField{other.intField}, boolField{other.boolField}, age{nullptr} {
if(other.age != nullptr) {
age = new int[Base::size];
memcpy(age, other.age, size);
}
}
拿上面的 int* age 来举🌰,假设新创建的对象名为 B,它用已有的对象 A 进行拷贝构造:
B.age 和 A.age 将指向同一块内存。如果 A 对这块内存进行了操作,B 知道吗?更有甚者,如果 A 删除了这块内存,而 B 还继续操作它的话,岂不是会崩溃?(浅拷贝,两个对象操作同一块内存,就会出这些问题,引以为戒!!!)
所以,对于这种情况,拷贝构造函数中使用了所谓的深拷贝(deepcopy),也就是将
A.age的内容拷贝到 B 对象中(B 先创建一个大小相同的数组,然后通过memcpy进行内存的内容拷贝),而不是简单的进行赋值(浅拷贝,shallow copy)。
Tips: 深拷贝,先创建同大小内存,然后将内存内容拷贝过来。
浅拷贝 对应于 值拷贝,而 深拷贝 对应于 内容拷贝。对于非指针变量类型而言,值拷贝和内容拷贝没有区别,但对于 指针型变量 而言,值拷贝和内容拷贝差别就很大了。
对比一下 Java 的 clone???
Base A; // 构造A对象
Base B(A); // 直接用A对象来构造B对象,这种情况是“直接初始化”
Base C = A; // 定义C的时候即赋值,这是真正意义上的拷贝构造。
怎样会触发拷贝构造函数的调用呢?
-
当函数的
参数为非引用的类型时,调用这个函数并传递实参时,实参的拷贝构造函数将被调用; -
函数的
返回类型为一个非引用的对象时,该对象的拷贝构造函数将被调用。
举个例子(例100):
Base getTemporyBase() {
Base temp; // 默认构造函数将被调用
return temp; // 对象的拷贝构造函数将被调用
}
void test() {
// 1,temp 对象析构
// 2,拷贝构造对象 boss
Base boss = getTemporyBase();
}
//最后临时对象析构,boss 对象析构,从 temp 到 boss,整个过程经历了两次拷贝,实在有点浪费
//如果定义了移动构造函数,省了两次拷贝,运行效率会有明显提升
直接初始化 和 拷贝初始化 的细微区别:
-
针对 Base B(A),Base 确实定义了一个形参为
constB&的构造函数。而B(A)的语法恰好满足这个函数,所以这个构造函数被调用是理所当然的。这样的构造是很直接的,没有任何疑义的,所以叫直接初始化。 -
而对于
Base C = A的理解却是将 A 的内容拷贝到正在创建的 C 对象中,这里包含了拷贝和构造两个概念,即拷贝 A 的内容来构造C。所以叫拷贝构造。
拷贝赋值函数
我们先来思考下赋值函数解决什么问题?
int a = 0;
int b = a;
对于基本内置数据类型而言,赋值操作似乎是天经地义的合理,但对于类类型呢?
Base A; // 构造一个对象A
Base B; // 构造一个对象B
B = A; // A可以赋值给B吗?
从面向对象角度来看,把一个对象赋值给另外一个对象会得到什么?
赋值函数本身没有什么难度,无非就是在准备接受另外一个对象的内容前,先把自己清理干净。另外,赋值函数的关键知识点是利用了C++中的
操作符重载(Java 不支持操作符重载)。
// 在 .h 文件中声明
Base& operator=(const Base &other); // 拷贝赋值函数
// 在 cpp 文件中实现
// 拷贝赋值函数
Base& Base::operator=(const Base &other) {
// C++ 中,this 是指针
this->data = other.data;
// * 解引用符号
(*this).much = other.much;
// 先将自身洗剥干净
if(age != nullptr) {
delete[] age;
age = nullptr;
}
// 深拷贝 other 对象的 age
if(other.age != nullptr) {
age = new int[Base::size];
memcpy(age, other.age, size)
}
return *this; // 返回的是 Base& 类型
}
移动构造和移动赋值函数
下图可以很好的诠释
移动的含义,A.C 不再指向自己创建的内存,而 B.C 占领了那块内村。如果使用拷贝之法,A 和 B 对象将各自有一块内存。如果使用移动之法,A 对象将不再拥有这块内存,反而是 B 对象拥有 A 对象之前拥有的那块内存。
移动之后,A、B对象的命运会发生怎样的改变?
很简单,B 自然是得到A的全部内容,A 则掏空自己,成为无用之物。注意,A对象还存在,但是你最好不要碰它,因为它的内容早已经移交给了B。
举个例子:
// 在 .h 中声明 移动构造函数
Base(Base &&other);
// 在 .cpp 中实现移动构造函数
// 移动构造函数,注意是两个 &&
Base::Base(Base &&other)
: data(other.data), much(other.much), age(other.age) {
other.age = nullptr;
}
// 在 .h 中声明 移动赋值函数
Base &operator = (Base &&other);
// 在 .cpp 文件中实现 移动赋值函数
Base& Base::operator = (Base &&other) {
data = other.data;
much = other.much;
if(age != nullptr) {
delete[] age;
age = nullptr;
}
age = other.age;
other.age = nullptr;
return *this;
}
记住两个原则:
-
如果确定被转移的对象(比如例100中的 temp 对象)不再使用,就可以使用移动构造/赋值函数来提升运行效率。
-
我们要保证
移动构造/赋值函数被调用,而不是拷贝构造/赋值函数被调用。例如,上述代码中Base y = x这段代码实际上触发了拷贝构造函数,这不是我们想要的。为此,我们需要 强制 使用移动构造函数,方法为Base y = std::move(x)。move是std标准库提供的函数,用于将参数类型强制转换为对应的右值类型。通过move函数,我们表达了 强制使用移动函数 的想法。
如果没有定义移动函数怎么办?
如果类没有定义移动构造或移动赋值函数,编译器会调用对应的拷贝构造或拷贝赋值函数。所以,使用 std::move 不会带来什么副作用,它只是表达了要使用移动之法的愿望。
析构函数
当类的实例达到生命终点时,析构函数将被调用,其主要目的是为了清理该实例占据的资源。
// .h
~Base(); // 析构函数
// .cpp
Base::~Base() {
if(age != null) {
delete[] age;
age = nullptr;
}
cout << "destructor invoked" << endl;
}
Java 中与析构函数类似的是 finalize 函数。但绝大多数情况下,Java 程序员不用关心它。而 C++ 中,我们需要知道析构函数什么时候会被调用:
-
栈上创建的类实例,在退出作用域(比如函数返回,或者离开花括号包围起来的某个作用域)之前,该实例会被析构。
-
动态创建的实例(通过
new操作符),当delete该对象时,其析构函数会被调用。
总结:
-
构造函数,分为
默认构造,普通构造,拷贝构造和移动构造; -
赋值函数,分为
拷贝赋值和移动赋值。请读者先从原理上理解 拷贝 和 移动 的区别和它们的目的; -
析构函数。
// TypeClass.h
#ifndef NDK_SAMPLE_TYPECLASS_H
#define NDK_SAMPLE_TYPECLASS_H
namespace type_class {
void test();
// Base 类位于 type_class 命名空间里。
class Base {
public:
Base(); //默认构造函数
Base(int a); //普通构造函数
Base(const Base& other); // 拷贝构造函数
Base &operator=(const Base& other); // 拷贝赋值函数
Base(Base&& other); // 移动构造函数
Base &operator=(Base&& other); // 移动赋值函数
~Base(); // 析构函数
protected:
// 成员函数在头文件里实现
int getData() {
return data;
}
// / 成员函数在头文件里声明,源文件里实现
int deleteC(int a, int b = 100, bool test = true);
private:
int data;
double much;
static const int size = 512; //静态成员变量
int *age;
};
};
#endif //NDK_SAMPLE_TYPECLASS_H