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 已触发了一个断点。
其实原因就是我们在析构函数中对内存进行释放时出现了问题
我们知道栈是一个先进后出的数据结构,在上述代码中,我们创建了 p1和p2两个对象,所以我们在出栈的时候,会先将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
一个内存大小:4class 中的this 指针
class函数中可以使用 this,这个this为谁调用它谁就是this(和 js 中类似,但是没有js中的this复杂)
this 的好处
解决形参和成员属性重名
实现链式调用 (注意返回的
Person和Person&的区别)
需要注意成员函数中如果使用了
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:60const 修饰成员函数
通过给 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 协议,转载请注明出处。