大家好,我是小康。
今天咱们聊聊 C++ 类里面那些不太起眼,但其实非常重要的“隐形英雄”。你可能会想,C++ 是不是很复杂,怎么还有那么多自动生成的函数?这些函数究竟是做什么的呢?想象一下,如果你的类没有它们,代码会变得多么麻烦。是不是有点好奇,它们是怎么默默帮助你提升效率的呢?
来,先抛个问题给大家:C++ 的空类里,默认提供了哪些函数呢?你能猜到几个吗?想一想,然后咱们一起来揭开这些“幕后功臣”的神秘面纱。
微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。
1. 默认构造函数:
“喂,创建一个空的对象吧!”
你可能会问:“类的对象要怎么创建呢?”答案就是使用构造函数。如果你没有给类写构造函数,C++ 会自动为你创建一个“默认无参构造函数”。这个构造函数干嘛用的呢?它就是用来创建对象的时候,给对象的成员变量赋一个初始值。要是你没有做显示初始化,那成员的值可能是不确定的,比如数字会是随机值,指针可能会是 nullptr
。
来看代码:
class MyClass {
public:
int x;
MyClass() { // 默认无参构造函数
}
};
int main() {
MyClass obj; // 创建对象时会调用默认构造函数
std::cout << obj.x << std::endl; // 输出随机值
}
2. 默认析构函数:
“嘿,别担心,我会帮你收拾残局!”
当你创建一个对象时,C++ 会自动为你生成一个析构函数。它的任务就是在对象生命周期结束时自动清理资源。简单来说,就是当对象被销毁时,析构函数会自动被调用,释放对象占用的资源。不过,如果类里没有特别的资源需要清理,默认析构函数就足够了,它什么都不做。
class MyClass {
public:
int value;
MyClass() { value = 10; }
~MyClass() { } // 默认析构函数,啥也不做
};
但如果你的类里有动态分配的内存,比如使用 new
分配了内存,那么这个默认的析构函数就不够用了。因为它只是简单地销毁对象本身,却不会释放你用 new
分配的内存。结果呢?你会得到 内存泄漏。
需要手动写析构函数的情况:
当类内部有像 new
这样的动态内存分配时,你需要手动写一个析构函数,确保这些内存能被正确释放掉,不然每次创建对象时,都会“留下垃圾”,久而久之就会造成内存泄漏。
class MyClass {
public:
int* ptr;
MyClass() { ptr = new int(10); } // 动态分配内存
~MyClass() { delete ptr; } // 释放动态分配的内存,防止内存泄漏
};
小结一下:
- 默认析构函数什么都不做,它会帮你销毁对象,但不会释放
new
分配的内存。 - 有动态资源时(比如
new
分配的内存),一定要自己写析构函数,用delete
手动释放内存,防止内存泄漏。 - 没有动态资源时,默认的析构函数就够用了。
3. 默认拷贝构造函数:
“我要给你一模一样的副本!”
有时候,我们希望“复制”一个对象。比如你有一个对象 obj1,要把它赋值给另一个对象 obj2。这时候就需要用到拷贝构造函数。如果你没有自己写,C++ 会自动给你生成一个默认的拷贝构造函数。它会把原对象的成员变量逐个复制过来。
class MyClass {
public:
int value;
MyClass(int val) : value(val) {}
MyClass(const MyClass& other) : x(other.x) { // 默认拷贝构造函数
}
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 或者这样写 MyClass obj2(obj1); 默认拷贝构造函数被调用
std::cout << obj1.x << " " << obj2.x << std::endl; // 输出 10 10
}
运行结果会显示: 10 10
这段代码中,obj2
会有和 obj1
一样的值,因为默认拷贝构造函数进行了“浅拷贝”,逐个成员地复制了数据。这对于像整数这类简单类型完全没问题,两个对象会独立存在。
指针的“浅拷贝”
但是,如果类中有指针成员,默认拷贝构造函数就会有问题。因为它只是简单地复制指针的值(即内存地址),导致两个对象指向同一块内存。这样,当一个对象销毁时,另一对象仍然会持有这个内存地址,从而可能引发“双重释放”的问题。
假设你的类有一个指针成员,像这样:
class MyClass {
public:
int* ptr;
MyClass(int val) {
ptr = new int(val); // 动态分配内存
}
MyClass(const MyClass& other) : ptr(other.ptr) { // 默认拷贝构造函数
}
~MyClass() { // 析构函数
delete ptr; // 释放内存
}
};
int main(){
MyClass obj1(10);
MyClass obj2(obj1);
}
说明:
在这个例子中,我们有一个类 MyClass
,里面有一个指针 ptr
,指向动态分配的内存。在 main
函数里,我们创建了 obj1
对象,并通过拷贝构造函数创建了 obj2
对象。
默认的拷贝构造函数 MyClass(const MyClass& other)
是做“浅拷贝”的,它只是简单地将 other.ptr
的值(即内存地址)赋给 ptr
。这就意味着,obj1
和 obj2
都指向同一块内存。
问题来了:
- 现在,
obj1
和obj2
都有指向同一块内存的指针。 - 当
obj1
和obj2
被销毁时,它们的析构函数会分别调用delete ptr;
来释放ptr
指向的内存。 - 结果就会出现“双重释放”的问题:
obj1
销毁时会释放ptr
指向的内存,然后obj2
销毁时再试图释放同一块内存。这会导致程序崩溃,因为你不能两次释放同一块内存。
解决办法:
为了避免这个问题,通常我们会实现一个“深拷贝”的拷贝构造函数,确保每个对象有自己独立的内存,而不是共享同一个内存区域。
MyClass(const MyClass& other) {
ptr = new int(*(other.ptr)); // 为每个对象分配不同的内存
}
这样,obj1
和 obj2
就会有各自独立的 ptr
,指向不同的内存空间,避免了“双重释放”问题。
4. 默认重载赋值运算符:
“我来帮你复制数据吧!”
赋值运算符 =
用来在两个对象之间传递数据。当你把一个对象赋值给另一个对象时,C++ 会自动调用赋值运算符函数。如果你没有自己写,C++ 会给你提供一个默认的赋值运算符重载函数。这个默认函数会“逐个成员”进行赋值,也就是它会对每个成员变量进行拷贝,就像拷贝构造函数一样。
来看看代码怎么写?
class MyClass {
public:
int value;
MyClass(int val) : value(val) {}
// 默认赋值运算符做浅拷贝:逐个成员赋值
MyClass& operator=(const MyClass& other) : value(other.value) { // 默认重载赋值运算符函数
return *this;
}
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 默认重载赋值运算符函数被调用
std::cout << obj1.x << " " << obj2.x << std::endl; // 输出 10 10
}
问题:浅赋值的陷阱
不过,这个默认的赋值运算符只是做“浅赋值”。它只会复制对象的成员值,而不会考虑成员是不是指向动态分配的内存。和拷贝构造函数类似,如果类里有指针成员,两个对象可能会指向同一块内存,导致双重释放的问题。
class MyClass {
public:
int* ptr;
MyClass() { ptr = new int(10); }
MyClass& operator=(const MyClass& other) {
ptr = other.ptr;
return *this; // 返回当前对象的引用
}
~MyClass() { delete ptr; }
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = obj1; // 默认重载赋值运算符函数被调用
} // obj1和 obj2 在 main 函数结束,会分别调用析构函数,导致同一段内存释放两次,造成程序错误。
深赋值解决问题
如果你的类里有动态分配的资源(比如 new
创建的内存),你就需要手动重载赋值运算符,做一个深赋值。也就是说,要为每个指针成员重新分配内存,并复制内容。这样,两个对象就不会共享同一块内存了,互不干扰。
class MyClass {
public:
int* ptr;
MyClass() { ptr = new int(10); }
// 自定义赋值运算符,进行深拷贝
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 避免自赋值
delete ptr; // 先释放原来的内存
ptr = new int(*other.ptr); // 重新分配内存并复制内容
}
return *this; // 返回当前对象的引用,支持链式赋值
}
~MyClass() { delete ptr; }
};
小结一下:
- 默认赋值运算符只是做浅拷贝,它会简单地复制成员变量的值。
- 如果有指针成员,默认的浅拷贝可能导致多个对象指向同一块内存,容易出问题。
- 自定义赋值运算符来做深拷贝,确保每个对象都有独立的内存空间,避免出现资源共享问题。
5. 默认重载取址运算符:
“我要返回对象的地址!”
在 C++ 中,每个对象都占有一块内存空间,也就是它有一个内存地址。有时候,我们需要知道一个对象在内存中的具体位置,这时候就需要用到 取址运算符&
。你可以通过它来获取对象的地址。
如果你没有重载取址运算符,C++ 会为你提供一个默认的版本,直接返回对象的内存地址。简单来说,默认的取址运算符就像是说:“嘿,给你对象的地址,随便用!”
class MyClass {
public:
int value;
MyClass(int val) : value(val) {}
// 默认重载取址运算符函数
MyClass* operator&() {
return this; // 这里只是返回对象本身的地址
}
};
int main() {
MyClass obj; // 创建一个对象
MyClass* ptr = &obj; // 使用默认的取址运算符,获取对象的地址
std::cout << ptr << std::endl; // 输出对象的地址
}
在这个例子中,我们创建了一个 MyClass
类型的对象 obj
,然后通过 &obj
获取了它的地址,最终将地址输出到控制台。C++ 自动调用了默认的取址运算符,直接给我们返回了 obj
的内存地址。
微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。
6. 默认重载取址运算符(const):
“我要返回常量地址!”
我们知道,常量对象的值不能被修改,那如果你需要获取常量对象的地址怎么办?这时,C++ 为你准备了一个const 版本的取址运算符,专门处理这种情况。
如果你有一个常量对象或者常量引用,C++ 会自动调用这个特殊的版本,返回一个const 类型的指针。这意味着你不能通过这个指针修改对象的值,它会保证你只能读取对象的内容,而不能修改它。
class MyClass {
public:
int x;
MyClass(int val) : x(val) {}
// 重载取址运算符的 const 版本函数
const MyClass* operator&() const {
return this; // 返回对象本身的地址,且指针类型为 const
}
};
int main() {
const MyClass obj(10); // 创建一个 const 对象
const MyClass* ptr = &obj; // 使用 const 版本的取址运算符
std::cout << ptr << std::endl; // 输出对象的地址
}
在这个例子中,我们创建了一个常量对象 obj
,然后通过 &obj
获取了它的地址。由于是常量对象,C++ 自动调用了const 版本的取址运算符函数,返回了 const MyClass*
类型的指针。这就确保了我们不能通过 ptr
来修改 obj
的值,保证了对象的“只读”安全。
7. 默认移动构造函数(C++11):
“搬家速度快,效率高!”
C++11 引入了“移动语义”,让我们可以更高效地处理资源。简单来说,移动构造函数就是帮我们“搬家”的,它会将资源从一个对象转移到另一个对象,而不需要复制数据。这样做,能大大提高程序的效率,尤其是在处理大数据时。
怎么做的?
当你有一个临时对象 (比如一个将要被销毁的对象)时,移动构造函数就会被调用。它直接把临时对象的资源(比如指针)“搬”到新对象中,原对象变成了“空壳”,没有资源了。这比直接复制要快得多。
代码示例:
class MyClass {
public:
int* ptr;
MyClass(int val) { ptr = new int(val); }
// 默认的移动构造函数
MyClass(MyClass&& other) {
ptr = other.ptr; // 资源转移到新对象
other.ptr = nullptr; // 原对象清空资源,避免二次释放
}
~MyClass() {
delete ptr; // 析构时释放内存
}
};
int main() {
MyClass obj1(10); // 创建一个对象 obj1,临时对象
MyClass obj2 = std::move(obj1); // 使用 std::move 调用移动构造函数
// MyClass obj2 = MyClass(20); // 创建一个临时对象并转移资源到 obj2,调用移动构造函数
// obj2 拥有资源,obj1 的 ptr 变成 nullptr
std::cout << obj2.ptr << std::endl; // 输出 obj2 的指针地址
std::cout << obj1.ptr << std::endl; // 输出 nullptr
}
说明:
MyClass(MyClass&& other)
:这是移动构造函数,接收一个右值引用对象(临时对象、或者std::move()返回的)。ptr = other.ptr
:把临时对象的资源(指针)转移给新对象。other.ptr = nullptr
:清空原对象的资源,避免二次释放内存。
为什么要用它?
- 效率提升:移动比复制快,特别是当对象很大时。
- 避免浪费资源:移动语义避免了不必要的内存分配和释放。
8. 默认重载移动赋值运算符(C++11):
“ 我来接管你的资源,别浪费了! ”
移动赋值运算符和移动构造函数有点像,它也是在你将一个临时对象赋值给另一个对象时,帮助你高效地转移资源,避免浪费时间和内存进行复制操作。这样做不仅能提高性能,特别是在处理临时对象时,还能减少不必要的内存分配和释放。
怎么做的?
当你用一个临时对象给另一个对象赋值时,移动赋值运算符就会被触发。它会把临时对象的资源(比如指针)快速转移到目标对象中,而不会再去做重复的内存分配操作。原本的临时对象就像是被“收拾”了一样,它的资源被清空了。
代码示例:
class MyClass {
public:
int* ptr;
MyClass(int val) { ptr = new int(val); }
// 默认的移动赋值运算符
MyClass& operator=(MyClass&& other) {
if (this != &other) {
delete ptr; // 先清理掉当前对象的资源
ptr = other.ptr; // 将临时对象的资源转移过来
other.ptr = nullptr; // 清空临时对象的资源,防止二次释放
}
return *this;
}
~MyClass() {
delete ptr; // 析构时释放内存
}
};
怎么使用?
在实际使用时,移动赋值运算符通常会在你用一个临时对象赋值给另一个对象时自动调用。比如下面这种情况:
MyClass obj1(10); // 创建对象 obj1
MyClass obj2(20); // 创建对象 obj2
obj2 = MyClass(30); // 创建一个临时对象并转移资源到 obj2,调用移动赋值函数
// obj2 = std::move(obj1); // 调用移动赋值运算符函数,obj1 资源转移到 obj2
解释:
MyClass& operator=(MyClass&& other)
:这是移动赋值运算符,它接收一个右值引用(也就是临时对象)。delete ptr
:先清理当前对象的资源,确保不发生内存泄漏。ptr = other.ptr
:将临时对象的资源(比如指针)转移到目标对象。other.ptr = nullptr
:清空临时对象的资源,避免它在销毁时释放资源两次。
为什么要用它?
- 性能提升:移动比复制要快,尤其是处理大数据或复杂对象时,避免了不必要的内存复制和分配。
- 避免浪费资源:利用移动语义,避免了不必要的资源浪费,让程序更加高效。
总结:
看完这些,你是不是对 C++ 类中默认函数有了更清楚的了解?C++ 自动帮你生成了这些默认的函数,省去了很多麻烦。默认构造函数、析构函数、拷贝构造函数等,都是 C++ 提供的“幕后英雄”,让你在写代码时少了一些负担。
不过,记住,有时候这些默认函数可能并不完全“聪明”。比如,默认的拷贝构造函数和赋值运算符可能会出现浅拷贝或浅赋值的情况,导致一些意外的麻烦。所以在特殊情况下,你可能需要自己写这些函数,确保它们能按你想要的方式工作。
希望今天的内容能帮你更好地理解这些默认函数,它们虽然看似简单,但却在 C++ 类的实现中发挥着不可忽视的作用。下次创建类时,记得,它们就在那里,默默地为你工作哦!
最后:
觉得有收获的话,记得点赞、收藏、关注!,顺便分享给你的小伙伴们,一起学编程!😉
如果你还想继续挖掘更多编程技巧,快来关注我的公众号「跟着小康学编程」,这里有一堆干货等着你!有问题或者想聊的,评论区见!技术的路上,我们一起走,大家一起进步,绝对不孤单!💪
怎么关注我的公众号?
扫下方公众号二维码即可关注。
另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。