目录
- 默认构造函数
- 默认析构函数
- RAII技术
- 默认拷贝构造
- 深拷贝和浅拷贝
- 默认拷贝赋值运算符
- 移动构造函数(C++11起)
- 默认移动赋值运算符(C++11起)
- 取地址及const取地址操作符重载
- 取地址操作符重载
- 常量取地址操作符重载
- 输出结果
- 扩展:前置++和后置++重载
- 重载规则
C++中的六大默认成员函数是编译器在特定条件下自动生成的成员函数,用于管理对象的生命周期和资源操作。它们分别是:
默认构造函数
作用:初始化对象,当类没有显式定义任何构造函数时生成。
生成条件:用户未定义任何构造函数。
注意:若类有其他构造函数(如带参数的构造函数),需显式使用 = default 声明默认构造函数。
clhttp://www.devze.comass Person { public: //Person() //{ //} 不写的话默认自动生成 void GetAge() { std::cout << _age << std::endl; } private: int _age; }; int main() { Person p; p.GetAge(); }
其特征如下:
函数名与类名相同。
无返回值。
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
编译器生成默认的构造函数会对自定类型成员调用的它的默认成员
函数。C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
默认析构函数
作用:释放对象资源,默认析构函数调用成员变量的析构函数。
生成条件:用户未定义析构函数。
注意:若类管理动态资源(如堆内存),需自定义析构函数以避免内存泄漏
class Person { public: //Person() //{ //} 不写的话默认自动生成 void GetAge() { std::cout << _age << std::endl; } ~Person() { } private: int _age; }; int main() { Person p; p.GetAge(); }
RAII技术
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种管理资源的编程技术。它通过将资源的生命周期与对象的生命周期绑定在一起,利用C++的构造函数和析构函数来自动管理资源,从而避免了手动分配和释放资源可能带来的问题,如内存泄漏、资源未正确释放等。
RAII的核心思想
- 资源在对象构造时获取:当一个对象被创建时,它的构造函数负责获取所需的资源(例如,动态内存分配、文件打开、网络连接等)。
- 资源在对象销毁时释放:当对象离开作用域或被显式删除时,其析构函数会自动释放之前获取的资源。
优点
- 异常安全性:由于资源管理由构造和析构函数自动处理,即使程序中抛出了异常,也能确保资源得到正确释放。
- 简化代码:开发者不需要手动跟踪每个资源的状态,并且可以在不使用显式的
try-finally
块的情况下保证资源的释放。 - 防止资源泄露:只要对象被正确地创建并最终销毁,资源就会被正确释放。
示例
以下是一个简单的例子,展示了如何使用RAII来管理动态分配的内存:
#include <IOStream> class ResourceHandler { private: int* data; public: // 构造函数:资源获取 ResourceHandler() { data = new int(10); // 分配资源 std::cout << "Resource acquired." << std::endl; } // 析构函数:资源释放 ~ResourceHandler() { delete data; // 释放资源 std::cout << "Resourpythonce released." << std::endl; } void showData() const { std::cout << "Data: " << *data << std::endl; } }; void useResource() { ResourceHandler handler; handler.showData(); // 不需要手动释放资源,handler离开作用域时会自动调用析构函数 } int main() { useResource(); return 0; }
在这个例子中,ResourceHandler
类负责管理一个整数类型的动态分配内存。构造函数在对象创建时分配资源,而析构函数在对象销毁时释放这些资源。这样就确保了无论函数useResource
如何退出(正常结束或因异常退出),资源都会被正确释放。
应用场景
RAII不仅限于内存管理,还可以应用于其他资源类型,如文件句柄、网络套接字、数据库连接等。标准库中的智能指针(如std::unique_ptr
、std::shared_ptr
)、锁机制(如std::lock_guard
、std::unique_lock
)都是RAII原则的实际应用案例。通过使用这些工具,可以有效地减少资源管理错误,提高代码的安全性和可靠性。
默认拷贝构造
声明形式:ClassName(const ClassName&)
作用:通过已有对象初始化新对象,默认执行浅拷贝。
生成条件:用户未定义拷贝构造函数。
注意:若类包含指针或动态资源,需自定义深拷贝防止重复释放。
class Person { public: Person() { } Person(const Pepythonrson& person) { this->_age = person._age; } ~Person() { } void GetAge() { std::cout << _age << std::endl; } pythonprivate: int _age; }; int main() { Person p; p.GetAge(); }
深拷贝和浅拷贝
class Stack { public: //初始化 Stack() { _array = new int[20]; } //默认生成拷贝构造 //析构 ~Stack() { delete[] _array; } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; Stack s2(s1); }
这里我没有写实际的拷贝构造函数,这里s2调用的默认的拷贝构造,所以s2_array的地址就是s1中_array的地址,这就叫浅拷贝:
这样代码就会有问题,因为一个地址会被析构两次:
正确的方法应该是给s2的array开辟一块新的空间:
class Stack { public: //初始化 Stack() { _array = new int[20]; } Stack(const Stack& st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } //析构 ~Stack() { delete[] _array; } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; Stack s2(s1); }
这样的拷贝我们称为深拷贝,再次运行程序:
默认拷贝赋值运算符
声明形式:ClassName& operator=(const ClassName&)
作用:将已有对象的值赋给另一个对象,默认浅拷贝。
生成条件:用户未定义拷贝赋值运算符。
注意:需处理自赋值问题,并在资源管理时实现深拷贝。
class Stack { public: //初始化 Stack() { _array = new int[20]; } Stack(const Stack& st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } Stack& operator=(const Stack& st) { if (this != &st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } return *this; } //析构 ~Stack() { delete[] _array; } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; Stack s2; s2 = s1; }
移动构造函数(C++11起)
声明形式:ClassName(ClassName&&)
作用:通过右值引用“窃取”资源,避免深拷贝开销。
生成条件:用户未定义拷贝操作、移动操作或析构函数。
注意:移动后源对象应处于有效但未定义状态(如空指针)。
class Stack { public: //初始化 Stack() { _array = new int[20]; } Stack(const Stack& st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } Stack& operator=(const Stack& st) { if (this != &st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } return *this; } void swap(Stack& st) { std::swap(_array, st._array); std::swap(_size, st._size); std::swap(_capacity, st._capacity); } //移动构造函数 Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0) { swap(st); } //析构 ~Stack() { delete[] _array; } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; Stack s2(std::move(s1)); }
默认移动赋值运算符(C++11起)
声明形式:ClassName& operator=(ClassName&&)
作用:通过右值引用转移资源所有权。
生成条件:同移动构造函数。
注意:需正确处理自移动赋值。
class Stack { public: //初始化 Stack() { _array = new int[20]; } Stack(const Stack& st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } Stack& operator=(const Stack& st) { if (this != &st) { _array = new int[10]; _size = st._size; _capacity = st._capacity; } return *this; } void swap(Stack& st) { std::swap(_array, st._array); std::swap(_size, st._size); std::swap(_capacity, st._capacity); } //移动构造函数 Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0) { swap(st); } //移动复制构造 Stack& operator=(Stack&& st) { swap(st); st._array = nullptr; st._size = 0; st._capacity = 0; return *this; } //析构 ~Stack() { delete[] _array; } private: int* _array; size_t _size; size_t _capacity; }; int main() { Stack s1; Stack s2; s2 = std::move(s1); }
在C++中,前置++
和后置++
运算符可以通过成员函数或非成员函数的形式进行重载。两者的主要区别在于参数列表和返回值:
- 前置
++
:增加对象的值,并返回增加后的对象引用。 - 后置
++
:首先保存当前对象的状态,然后增加对象的值,最后返回之前保存的对象的副本。
取地址及const取地址操作符重载
在C++中,取地址操作符(&
)和常量取地址操作符(const &
)通常不需要显式地重载,因为编译器提供了默认的实现,它们分别返回对象或常量对象的内存地址。然而,在某些特定情况下,你可能想要自定义这些操作符的行为。
取地址操作符重载
当你重载取地址操作符时,你通常是为了改变其默认行为,例如返回一个代理对象的地址而不是原始对象的地址。不过这种情况非常少见,大多数时候并不需要这样做。
常量取地址操作符重载
类似地,重载常量版本的取地址操作符也是为了提供特定的行为,但同样,这并不是常见的需求。
示例代码
尽管不常见,这里还是给出如何重载这两种操作符的基本示例:
#include <iostream> class MyClass { private: int value; public: jJfyfXD MyClass(int val) : value(val) {} // 重载取地址操作符 int* operator&() { std::cout << "非const取地址操作符被调用" << std::endl; return &value; } // 重载const取地址操作符 const int* operator&() const { std::cout << "const取地址操作符被调用" << std::endl; return &value; } }; int main() { MyClass obj(10); const MyClass constObj(20); int* addr = &obj; // 调用非const版本 const int* constAddr = &constObj; // 调用const版本 std::cout << "*addr: " << *addr << std::endl; std::cout << "*constAddr: " << *constAddr << std::endl; return 0; }
输出结果
非const取地址操作符被调用
const取地址操作符被调用*addr: 10*constAddr: 20
在这个例子中,我们为MyClass
类重载了取地址操作符和常量取地址操作符。当通过非常量对象调用operator&()
时,会调用非常量版本的操作符,并打印一条消息。而当通过常量对象调用operator&()
时,则调用常量版本的操作符,并打印另一条不同的消息。
注意事项
- 谨慎使用:一般情况下,不需要也不建议重载这两个操作符,除非有特别的需求。这是因为它们改变了标准语义,可能会导致混淆或者不可预期的行为。
- 保持一致性:如果你决定重载这些操作符,请确保它们的行为符合逻辑且一致,避免引入错误。
- 理解限制:需要注意的是,即使你重载了取地址操作符,也无法阻止使用内置的取地址操作来获取对象的实际地址。例如,
&obj
总是可以获得obj
的实际地址,除非你完全隐藏了对象的访问方式。
总的来说,重载取地址操作符和常量取地址操作符是一种高级技巧,适用于特定场景下的特殊需求。在大多数日常编程任务中,这种重载是不必要的。
扩展:前置++和后置++重载
重载规则
- 前置
++
只需要一个参数(即调用该运算符的对象本身),并且通常返回一个指向修改后的对象的引用。 - 后置
++
需要两个参数:第一个是调用该运算符的对象本身,第二个是一个int类型的占位参数,用于区分前置和后置形式。后置++
返回的是操作前对象的一个副本(通常是通过值返回)。
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Count { public: //重载后置++ Count operator++() { ++_count; return *this; } //后置++ Count operator++(int) { Count temp = *this; ++_count; return temp; } int getCount() const { return _count; } private: int _count = 0; }; int main() { Count myCounter; std::cout << "Initial count: " << myCounter.getCount() << std::endl; ++myCounter; // 调用前置++ std::cout << "After prefix increment: " << myCounter.getCount() << std::endl; Count d; d = myCounter++; // 调用后置++ std::cout << "After postfix increment: " << d.getCount() << std::endl; return 0; }
到此这篇关于C++六大默认成员函数的实现的文章就介绍到这了,更多相关C++ 默认成员函数内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论