C++11 @4

2022-02-01  本文已影响0人  Drew_MyINTYRE

Class 介绍

程序总还是有顺序,有流程的。但是在这个流程里,开发者更多关注的是对象以及对象之间的交互,而不是孤零零的函数。

Class 还支持抽象,继承和多态。这些概念完全就是围绕面向对象来设计和考虑的,它关注的是类和类之间的关系。

C++ 类有和 Java 一样的访问权限控制,关键词也是 publicprivateprotected 三种。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.ageA.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,整个过程经历了两次拷贝,实在有点浪费
//如果定义了移动构造函数,省了两次拷贝,运行效率会有明显提升

直接初始化拷贝初始化 的细微区别:

拷贝赋值函数

我们先来思考下赋值函数解决什么问题?

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;
}

记住两个原则:

如果没有定义移动函数怎么办?

如果类没有定义移动构造或移动赋值函数,编译器会调用对应的拷贝构造或拷贝赋值函数。所以,使用 std::move 不会带来什么副作用,它只是表达了要使用移动之法的愿望。

析构函数

当类的实例达到生命终点时,析构函数将被调用,其主要目的是为了清理该实例占据的资源。

// .h
~Base(); // 析构函数

// .cpp
Base::~Base() {
    if(age != null) {
        delete[] age;
        age = nullptr;
    }
    cout << "destructor invoked" << endl;
}

Java 中与析构函数类似的是 finalize 函数。但绝大多数情况下,Java 程序员不用关心它。而 C++ 中,我们需要知道析构函数什么时候会被调用:

总结:

// 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
上一篇 下一篇

猜你喜欢

热点阅读