C++ 面向对象-浅拷贝与深拷贝、初始化列表

本文最后更新于:2022年4月22日 上午

C++ 面向对象-浅拷贝与深拷贝、初始化列表

深拷贝与浅拷贝

上一篇我们已经了解到,如果一个 class我们没有提供拷贝构造函数的话,编译器会默认帮我们提供,编译器提供的拷贝构造函数内部其实就是浅拷贝

如下所示,我们给类新增一个成员,这个成员是一个指针变量

因为它是一个指针变量,所以我们在赋值的时候需要赋一个引用地址,所以可以通过 height = new double(pHeight);来得到一个地址,之前我们说过通过 new 关键字分配的内存在 堆内存空间中,且需要开发者手动进行释放,所以我们在 析构函数 中对该变量进行释放

class Person {

public:
  string name;
  double* height;

  // 默认构造函数
  Person() {
    cout << "Person 默认构造函数被调用" << endl;
  }

  // 有参构造函数
  Person(string pName) {
    cout << "Person 有参构造函数被调用" << endl;
    name = pName;
    height = NULL;
  }

  Person(string pName, double pHeight) {
    cout << "Person 有参构造函数被调用" << endl;
    name = pName;
    height = new double(pHeight);
  }


  ~Person() {
    cout << name <<": Person 析构函数被调用" << endl;

    if (height != NULL) {
      delete height;
      height = NULL;
    }
  }

};


// ...

Person p1("小明",1.88);
Person p2(p1);

cout << "p2 姓名:" << p2.name << endl;
cout << "p2 height:" << *p2.height << endl;

运行上述代码后会发现报错

C++核心编程.exe 已触发了一个断点。

其实原因就是我们在析构函数中对内存进行释放时出现了问题

我们知道栈是一个先进后出的数据结构,在上述代码中,我们创建了 p1p2两个对象,所以我们在出栈的时候,会先将p2 弹出栈,p2在销毁的时候会执行其对应的析构函数,将其 height 的指针进行释放。紧接着p1开始出栈,执行对应的析构函数,这时候因为 p2 已经将其 所指向的内存数据进行释放了,p1再通过这个内存地址去操作这个内存数据就相当于在操作野指针,所以编译运行的时候会报错。

理解这个报错原因需要了解

  • 栈空间先进后出的特性

  • C++ 中指针保存的是一个内存地址,如果对这个内存地址进行过释放后继续去操作会发生野指针错误(没有权限)

那么我们理解了错误的原因后就很好解决了,我们可以手动编写 拷贝构造函数,然后在进行拷贝的时候通过深拷贝的方式进行赋值,这样就可以避免上述的问题了

class Person {

public:
  string name;
  double* height;

  // 默认构造函数
  Person() {
    cout << "Person 默认构造函数被调用" << endl;
  }

  // 有参构造函数
  Person(string pName) {
    cout << "Person 有参构造函数被调用" << endl;
    name = pName;
    height = NULL;
  }

  Person(string pName, double pHeight) {
    cout << "Person 有参构造函数被调用" << endl;
    name = pName;
    height = new double(pHeight);
  }

  Person(const Person& p) {
    cout << "Person 拷贝构造函数被调用" << endl;
    name = p.name;

    // 默认的拷贝构造函数如下所示,是一个浅拷贝,赋值的内存地址,所以在析构函数中删除时会出现问题
    // height = p.height;

    // 我们需要重新成该方式
    height = new double(*p.height);
  }

  ~Person() {
    cout << name <<": Person 析构函数被调用" << endl;

    if (height != NULL) {
      delete height;
      height = NULL;
    }
  }

};

// ...

Person p1("小明",1.88);
Person p2(p1);

cout << "p2 姓名:" << p2.name << endl;
cout << "p2 height:" << *p2.height << endl;

这时候重新运行,可以看到正常输出

Person 有参构造函数被调用
Person 拷贝构造函数被调用
p2 姓名:小明
p2 height:1.88
小明: Person 析构函数被调用
小明: Person 析构函数被调用

初始化列表

初始化列表可以简化我们通过构造函数来给属性进行赋值的过程,使用方式也比较简单,在构造函数后面使用 : xxx(yyy)就可以进行使用,如果有多个的话可以使用逗号进行分割,dart中也吸取了该特性,可以在之前的 dart语法的文章中进行查看

#include <iostream>;
using namespace std;

class Person {

private:
  string name;
  int age;

public:
  // 初始化列表, dart 中语法也和此类似,可以简化 name = pName,age = pAge 这种写法
  PersonInit(string pName, int pAge) :name(pName), age(pAge) {
    cout << "构造函数被调用" << endl;
  }

  string getName() {
    return name;
  }

  int getAge() {
    return age;
  }
};

int main() {

  Person p("小明", 18);

  cout << "名字:" << p.getName() << endl;
  cout << "年龄:" << p.getAge() << endl;

  system("pause");
  return 0;
}

输出

构造函数被调用
名字:小明
年龄:18

静态函数与静态变量

静态函数和静态变量都可以 通过 类和实例化出来的对象进行访问

静态函数只能访问静态变量

class Person {


public:
  
  static string name;

  static void sayHello() {
    cout << "hello world" << endl;
  }


  static void sayName() {
    cout << "name:" << name << endl;
  }

private:
  static void sayHello2() {
    cout << "hello world" << endl;
  }

};

// 给静态成员赋初始值需要加上类型
string Person::name = "小明";

// ...

Person p;
p.sayHello();
p.sayName();
//p.sayHello2();  不允许访问 private 修饰的静态方法

Person::sayHello();
Person::sayName();
//Person::sayHello2();

输出

hello world
name:小明
hello world
name:小明

成员变量和成员函数 的内存分开存储

C++ 中 如果一个 class 中什么都没有写,占用空间为 1 字节,原因可以理解成 每次 new 一个对象时都需要是一个独一无二的对象,所以需要1个字节

class Test2 {

};

class Test22 {
  int age;
  
  void sayHello() {}
};

void test2() {
  Test2 t2;

  cout << "空对象大小:" << sizeof(t2) << endl;    // 输出 1
  
  Test22 t22;

  // 输出 4 因为一个 int 占用的是 4 字节,但是如果还有 bool 或者 char 也会认为是4个字节,暂时不清楚什么原因
  // 且函数不会计算到对象的内存身上,因为 函数是不作为某一个对象的函数,函数可以通过 this 指针来指向当前的对象
  cout << "一个内存大小:" << sizeof(t22) << endl;    
  
}

输出

空对象大小:1
一个内存大小:4

class 中的this 指针

class函数中可以使用 this,这个this为谁调用它谁就是this(和 js 中类似,但是没有js中的this复杂)

this 的好处

  • 解决形参和成员属性重名

  • 实现链式调用 (注意返回的 PersonPerson& 的区别)

需要注意成员函数中如果使用了 this,则不能通过空指针去调用这个函数

class Test3 {
public:
  string name;
  int age;

  Test3(string name, int age) {
    // 如果不使用 this
    // name = name  不会修改对象的 name,需要修改 形参或者 成员name 的名称

    // 使用 this 可以访问到当前的对象,且可以避免形参与属性重名的问题
    this->name = name;
    this->age = age;
  }


  // 返回 this 实现链式调用 
  // 这里需要注意的时 该函数的返回值类型需要是一个 引用,不能是一个值,
  // 例如: `Test3 addTest3Age(Test3& t)` 就会每次都调用 拷贝构造函数返回一个新的对象,导致年龄计算失误 
  Test3& addTest3Age(Test3& t) {
    this->age += t.age;

    return *this;
  }
};

void test3() {

  // this 解决形参重名
  Test3 t3("小明", 10);
  
  Test3 t31("小红", 20);
  Test3 t32("小刚", 30);

  // 实现链式调用, jquery 的链式调用也和这个类似
  t3.addTest3Age(t31).addTest3Age(t32);

  cout << "t3 的age:" << t3.age << endl;  // 60;

}

输出

t3 的age:60

const 修饰成员函数

通过给 class 中的成员函数加上 const 关键字,可以限制通过 this去访问成员成员变量。如果希望该函数能够继续访问,需要给对应的成员变量加上 mutable关键字进行修饰

class Test4 {
public:
   mutable string name;
  int age;

  Test4(string name,int age ) {
    this->name = name;
    this->age = age;
  }

  // 修改名称
  // 如果通过 const 修饰的话将不能通过 this 去修改成员变量,因为  const 相当于是修饰 this
  // 需要使用 mutable 修饰成员变量才可以访问
  void changeName(string name,int age) const {
    this->name = name;
    // this->age = age; 不能访问普通成员变量
  }

};
void test4() {
  Test4 t4("小明",18);
  t4.changeName("大明",19);

  cout << t4.name << endl;
}

输出

大明

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。