Skip to main content

类-构造函数与析构函数

·994 words·5 mins· loading
Raven005
Author
Raven005
A little bit about you
Table of Contents

#

C 语言的结构体

struct TEST
{ // NOTE: C语言的结构体不能有函数
  // why: 因为一个结构体对象是要声明在栈或者堆中,而任何一个函数都要声明在代码区中
  int i;
  int t;
};

C++类的本质就是 C 语言的结构体外加几个类外的函数,C++最后都要转化为 C 语言来实现,类外的函数就是通过 this 来指向这个类的

简单介绍
#

面试的时候经常会听到一个问题,谈一下对面向对象和面向过程的理解

1. 面向对象和面向过程是一个相对的概念
2. 面向过程是按照计算机的工作逻辑来编码的方式,最典型的面向过程的语言就是 C 语言了,C 语言直接对应汇编,汇编又对应电路
3. 面向对象是按照人类的思维来编码的一种方式,C++完全支持面向对象功能,可以按照人类的思维来处理问题
4. 举个例子,要把大象装冰箱,按照人类的思路自然是分三步,打开冰箱,将大象装进去,关上冰箱。

要实现这三步,我们就要首先有人,冰箱这两个对象。人有给冰箱发指令的能 力,冰箱有能够接受指令并打开或关闭门的能力。但是从计算机的角度讲,计算机只能定义一个叫做人和冰箱的结构体。人有手这个部位,冰箱有门这个部位。然后从天而降一个函数,是这个函数让手打开了冰箱,又是另一个函数让大象进去,再是另一个函数让冰箱门关上。那么,如何用面向过程的C语言模拟出面向对象的能力呢?类就诞生了,在类中可以定义专属于类的函数,让类有了自己的动作。回到那个例子,人的类有了让冰箱开门的能力,冰箱有了让人打 开的能力,不再需要天降神秘力量了。从开发者的角度讲,面向对象显然更利于程序设计。用面向过程的开发方式,程序一旦大了,各种从天而降的函数会非常繁琐,一些用纯 c 写的大型程序,实际上也是模拟了面向对象的方式。

#include <iostream>

// NOTE: 面向对象一大的特点就是能在类中定义函数

class IceChest {

public:
  // NOTE: 冰箱开门或关门
  void openDoor() {}
  void closeDoor() {}
};

class Person {
public:
  // NOTE: 人能打开或关闭冰箱
  void openIceChest(const IceChest &iceChest) {}
};

int main() { return 0; }

构造函数
#

类相当于定义了一个新类型,该类型生成在堆或栈上的对象时内存排布和 C 语言相同。但是 C++规定,C++有在类对象创建时就在对应内存将数据初始化的能力,这就是构造函数

#include <iostream>

// 仿C写法
struct CTest { // NOTE: C语言是无法在定义对象是将里面的数据初始化的
  int i;
  int i2;
};

struct CPPTest {
public:
  CPPTest(int i_, int i2_) : i(i_), i2(i2_) {}

  //复制构造函数
  CPPTest(const CPPTest& t2) :i(t2.i) , i2(t2.i2) {}
  // NOTE: 加const是为了只读取成员变量的值,不能修改值

  int i;
  int i2;
};

int main() {
  CTest t1;
  CPPTest t2( 1,2 ); // NOTE: C++中如果未初始化对象,会调用构造函数,但是默认的构造函数是什么都不做的
  std::cout << t1.i << std::endl << t1.i2 << std::endl;
  std::cout << t2.i << std::endl << t2.i2 << std::endl;
  return 0;
}

构造函数的类型

  1. 普通构造函数自定义参数,一般是初始化成员变量

  2. 复制构造函数用另一个对象来初始化对象对应内存

  3. 移动构造函数也是用一个对象来初始化对象

  4. 默认构造函数当类中没有构造函数是,编译器会为该类生成一个默认的构造函数,在最普通的类中,默认构造函数什么都没做,对象对应的内存没有初始化

总结: 构造函数就是C++提供的 必须有的 在创建对象初始化对象的方法

默认什么都不做也是一种初始化方式

移动构造函数
#

对象移动的概念
#

  1. 对一个体积比较大的类进行大量的拷贝操作是非常消耗性能的,因此 C++11 中加入了 对象移动 的操作
  2. 所谓的对象移动,其实就是把该对象所占据的内存空间的访问权限转移给另一个对象,比如一块内存原本属于 A ,在进行“移动语义”后,这块内存就属于 B

移动语义为什么可以提高效率
#

因为我们的各种操作会经常进行大量的“复制构造”,“赋值运算”操作,这两个操作非常耗时间。移动构造是直接转移权限,效率就提高了

注意事项:

  • 在进行转移操作后,被转移的对象就不能继续使用了,所以对象移动一般都是对临时对象进行操作,因为临时对象就要被销毁了
#include <iostream>
#include <string.h>
#include <string>

class Test {
public:
  //NOTE: 不需要重新new一遍来消耗时间
  Test() = default;

  // Test(const Test &test) {
  //
  //   if (test.str) {
  //     str = new char[strlen(str) + 1](); // strlen在统计的时候,不会算结束符
  //     // strcpy()(str , strlen(test.str) + 1 , test.str); // WARNING: it doesn't
  //     // work
  //     strcpy(str, test.str);
  //   } else {
  //     str = nullptr;
  //   }
  // }

  Test(Test &&test) {
    if (test.str) {
      str = test.str;
      test.str = nullptr;
    } else {
      str = nullptr;
    }
  }

  // Test &operator=(const Test &test) {
  //   if (this == &test)
  //     return *this;
  //   if (str) {
  //     delete[] str;
  //     str = nullptr;
  //   }
  //   if (test.str) {
  //     str = new char[strlen(str) + 1]();
  //     strcpy(str, test.str);
  //   } else {
  //     str = nullptr;
  //   }
  //   return *this;
  // }
  Test &operator=(Test &&test) { // NOTE:不要加const,后面要修改
  //因为用右值引用函数参数就是为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,
  //但是一旦加了const就无法改变
    if (this == &test)
      return *this;

    if (str) {
      delete[] str;
      str = nullptr;
    }
    if (test.str) {
      str = test.str;
      test.str = nullptr;
    } else {
      str = nullptr;
    }
    return *this;
  }

private:
  char *str = nullptr;
};

Test makeTest() {

  Test t;
  return t;
}

int main() {

  Test t = makeTest();

  return 0;
}

默认移动构造函数和默认移动赋值运算符
#

  • 会默认生成移动构造函数和移动赋值运算符的条件:
    1. 只有一个类没有定义任何自己版本的拷贝操作(拷贝构造、拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才会合成
    2. 可以移动的意思是可以就行移动构造、移动赋值。所有的基础类型都是可以移动的。有移动语义的类也是可以移动的

析构函数
#

当类对象被销毁时,就会调用析构函数。栈上对象的销毁时机就是函数栈销毁时,堆上的对象销毁时机就是该堆内存被手动释放是,如果用 new 申请的这块堆内存,那调用 delete 销毁这块内存是就会调用析构函数

#include <iostream>

// 仿C写法
struct CTest { // NOTE: C语言是无法在定义对象是将里面的数据初始化的
  int i;
  int i2;
};

struct CPPTest {
public:
  CPPTest(int i_, int i2_ , int i3) : i(i_), i2(i2_) , pi(new int(i3)) {}

  //复制构造函数
  CPPTest(const CPPTest& t2) :i(t2.i) , i2(t2.i2) , pi(new int (*t2.pi)) {}
  // NOTE: 加const是为了只读取成员变量的值,不能修改值


  //模拟默认构造函数
  CPPTest(){}

  //析构函数
  ~CPPTest() {

    delete pi;
  } // NOTE:因为析构函数不带参数,所以无法重载

  int i;
  int i2;

  int* pi;
};

int main() {
  CTest t1;
  CPPTest t2( 1,2 , 3); // NOTE: C++中如果未初始化对象,会调用构造函数,但是默认的构造函数是什么都不做的
  std::cout << t1.i << std::endl << t1.i2 << std::endl;
  std::cout << t2.i << std::endl << t2.i2 << std::endl;


  CPPTest* pt2 = new CPPTest(1 , 2 , 3); // NOTE:调用在堆上

  delete pt2;
  return 0;
}

总结:当类对象销毁时有一些我们必须手动操作的步骤时,析构函数就排上了用场。所以,几乎所有的类我们都要写构造函数,析构函数未必需要

类的权限修饰
#

访问限定符
#

C++ 通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限(也成为可见性),分别表示:公有的、受保护的、私有的

class Base {
  public:
  protected:
  private:
};

访问权限
#

能不能使用该类中的成员

一般地,在类的内部,无论成员被声明为哪种,都是可以互相访问的;但在类的外部,如通过类的对象,则只能访问 public 属性的成员,不能访问 protected、private 属性的成员。

  • public: 可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问

  • protected: 可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问

  • private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象访问

private 关键字的作用在于更好地隐藏类的实现

注意事项
#

  1. 如果声明不写 public、protected、private ,则默认为 private
  2. 声明 public protected public 的顺序可以任意
  3. 在一个类中, public、protected、private 可以出现多次,每个限定符的有效范围到出现另一个限定符或类结束为止。但为了使程序清晰,应该每种限定符只出现一次

this 关键字
#

编译器将 this 解释为指向函数所作用的对象的指针

#include <iostream>

class Test {
private:
  std::string name;
  unsigned old;

public:
  Test(const std::string &name_, unsigned old_);
  ~Test();

  void outPut() const {
    std::cout << "name = " << name << " old = " << old << " ";
    std::cout << "name = " << this->name << " old = " << this->old;
  }
};

Test::Test(const std::string &name_, unsigned old_) : name(name_), old(old_) {}

Test::~Test() {}

int main() {
  Test test("cxy", 100);
  test.outPut();
  return 0;
}

当然,这么说并非完全正确,this 是一个关键字,只是我们将它当做指针理解罢了

常成员函数、常对象
#

在大型程序中,尽量加上 const 关键字可以减少很多不必要的错误

  1. 常成员函数就是无法修改成员变量的函数,可以理解为将 this 指针指向对象用 const 修饰的函数
  2. 常对象就是用 const 修饰的对象,定义好之后就再也不需要修改成员变量的值了

常成员函数注意事项:

  • 因为类成员函数已将 this 指针省略了,只能在函数后面加 const 关键字来实现无法修改类成员变量的功能了
  void outPut()const { std::cout << this->name << ' ' << this->old << std::endl; }
  1. 注意: 常函数无法调用普通函数,否则常函数的这个“常”字还有什么用?
  2. 成员函数能写作常成员就尽量写作常成员函数,可以减少出错几率
  3. 同名的常成员函数和普通成员函数是可以重载的,常量对象会优先调用常成员函数,普通对象会优先调用普通成员函数
#include <iostream>

class Test {
public:
  Test(const std::string &name_, unsigned old);
  ~Test();
  void outPut() const {
    std::cout << "const: " << this->name << ' ' << this->old << std::endl;
  }
  void outPut() { std::cout << this->name << ' ' << this->old << std::endl; }

private:
  std::string name;
  unsigned old;
};

Test::Test(const std::string &name_, unsigned old_) : name(name_), old(old_) {}

Test::~Test() {}

int main() {
  const Test test("cxy", 1010);
  test.outPut();
  Test test2("yxc" , 1000);
  test2.outPut();
  return 0;
}

常对象注意事项:

  1. 常对象不能调用普通函数
  2. 常函数在大型程序中真的很重要,很多时候我们都需要创建好就不再改变的对象,不要怕麻烦

总结: 常成员函数和常对象要多用

常用关键字
#

inline和mutable只要知道就好 default和delete需要掌握

inline
#

  1. 在函数声明或者定义中函数返回类型加上关键字 inline 就可以把函数指定为内联函数,关键字 inline 必须与函数定义放在一起才能使函数成为内联,仅仅 inline 放在函数声明前不起任何作用
  2. 内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是 C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率
#include <iostream>

class Test {
public:
  void outPut();
};

inline void Test::outPut() { std::cout << "hello world" << std::endl; }

int main() {

  Test test;
  test.outPut();
  return 0;
}

注意事项:

  • inline 关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联情况编译器都会采纳建议

总结:

  • 使用 inline 关键字就是一种提高效率,但加大编译后文件大的方式,现在很少用了

mutable
#

一般来说只有在统计函数调用次数才会用到

mutable 意为可变的,与 const 相对,被 mutable 修饰的成员变量,永远处于可变的状态,即使处于一个常函数中,该变量也可以被更改

#include <iostream>

class Test {

public:
  void outPut() const;
  mutable unsigned outPutCallcnt = 0;
};

void Test::outPut() const {
  ++outPutCallcnt;
  std::cout << "hello world" << std::endl;
}

int main() { // 统计调用次数
  Test test;
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();
  test.outPut();

  std::cout << test.outPutCallcnt << std::endl;

  return 0;
}

注意事项:

  • mutable 是一种万不得以的写法,但一个程序不得不使用 mutable 关键字时,可以认为这部分程序是一个糟糕的设计
  • mutable 不能修饰静态成员变量和常成员变量

总结:

  • mutalbe 关键字是一种没有办法的办法,设计时应该尽量避免

default
#

  1. 在编译时不会生成默认构造函数便于书写
  2. 也可以对默认复制构造函数,默认的复制运算符和默认的析构函数使用,表示使用的是系统默认提供的函数,可以使代码更加明显
  3. 现代 C++中,哪怕没有构造函数,也推荐将构造函数用`default`关键字来标记,可以让代码看起来更加直观,方便
#include <iostream>

class Test {
public:
  Test(unsigned old_) : old(old_) {}
  Test(const Test& test) = default;
  Test() = default;
  Test& operator=(const Test& test) = default;
  ~Test() = default;

  unsigned old;
};


int main() {


  return 0;
}

总结:

  • default 关键字还是推荐使用,在现代 C++代码中,如果需要使用一些默认的函数,推荐用 default 标记出来

delete
#

  • C++会为程序生成默认构造函数,默认复制构造函数,默认重载赋值运算符很多情况下,我们并不希望这些默认的函数被生成,在 C++11 以前,只能有将此函数声明为私有函数或是将函数只声明不定义两种方式
#include <iostream>

class Test {
public:
  Test(unsigned old_) : old(old_) {}
  Test(const Test& test) = delete;
  Test() = delete;
  Test& operator=(const Test& test) = delete;
  ~Test() = delete;

  unsigned old;
};


int main() {


  return 0;
}

总结:

  • delete 关键字还是推荐使用的,在现代 C++代码中,如果不希望一些函数默认生成,就用 delete 表示,这个功能还是很有用的,例如单例模式中