C++面向对象编程中对象的赋值操作详解
对象初始化:构造函数与复制构造函数详解
在C++面向对象编程中,构造函数是类设计的核心环节。常规对象初始化依赖于构造函数,即使未显式定义,编译器也会生成默认版本。然而,还存在一种特殊的初始化方式——通过已有对象创建新对象,这便涉及复制构造函数。本文将以栈(Stack)类为例,系统解析对象初始化、复制构造以及赋值操作的核心机制与常见陷阱。
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

首先查看栈类的声明。在Stack.hpp头文件中,我们定义了栈的基本结构:
//Stack.hpp
#pragma once
class Stack {
public:
Stack(int max_size);
~Stack();
//复制构造函数
Stack(const Stack& s);
bool IsEmpty() const;
bool IsFull() const;
void Push(int data);
void Pop();
int Top() const;
private:
int* buffer_;
int* top_;
int capacity_;
};
接下来在Stack.cpp中实现成员函数。注意此处内存分配使用std::malloc而非new,释放则对应使用std::free。复制构造函数的实现尤为关键,它需要为新对象分配独立内存空间,并完整拷贝原对象数据。
//Stack.cpp #include "Stack.hpp" #includeStack::Stack(int max_size) : capacity_(max_size), //buffer_(new int[max_size]), buffer_(static_cast (std::malloc(sizeof(int) * max_size))), top_(buffer_) {} Stack::~Stack() { std::free(buffer_); } Stack::Stack(const Stack& s) : capacity_(s.capacity_), buffer_(static_cast (std::malloc(sizeof(int) * s.capacity_))), top_(buffer_ - s.buffer_ + s.top_) { std::memcpy(buffer_, s.buffer_, sizeof(int) * s.capacity_); } bool Stack::IsEmpty() const { return top_ <= buffer_; } bool Stack::IsFull() const { return top_ >= buffer_ + capacity_; } void Stack::Push(int data) { if (this->IsFull()) throw "Stack is full!"; *top_++ = data; } void Stack::Pop() { if (this->IsEmpty()) throw "Stack is Empty!"; top_--; } int Stack::Top() const { if (this->IsEmpty()) throw "Stack is Empty!"; return *(top_ - 1); }
完成类定义后,我们编写测试代码验证功能。首先创建s1对象并压入两个数值,随后通过复制构造创建s2。此时s1与s2栈顶元素应相同。接着向s2压入新值,两个栈的栈顶将发生变化——这正是深拷贝(deep copy)的预期效果。
然而问题常隐藏于看似普通的操作中。我们再创建s3对象,并使用默认赋值运算符将s1赋值给它。此时隐患出现:默认赋值操作仅执行浅拷贝(shallow copy),直接将s1的buffer_指针地址复制给s3。当s3压入新值时,虽然top_指针位置改变,但s1与s3的buffer_指向同一内存区域。程序结束时,两个对象的析构函数将先后释放同一内存块,导致典型的“重复释放”(double free)错误。
//OverloadAssignment #include#include "Stack.hpp" int main() { Stack s1(16); s1.Push(1); s1.Push(2); Stack s2 = s1; //调用复制构造函数 std::cout << "s1 top: " << s1.Top() << std::endl; std::cout << "s2 top: " << s2.Top() << std::endl; s2.Push(3); std::cout << "s2 pushed 3" << std::endl; std::cout << "s1 top: " << s1.Top() << std::endl; std::cout << "s2 top: " << s2.Top() << std::endl; Stack s3(16); s3 = s1; //调用赋值运算符重载 std::cout << "s3 top: " << s3.Top() << std::endl; //观察报错信息,s3的析构函数被调用了两次,说明s3和s1指向了同一块内存区域,导致内存被重复释放了。 }
运行程序,控制台输出看似正常:
s1 top: 2 s2 top: 2 s2 pushed 3 s1 top: 2 s2 top: 3 s1 top: 2 s3 top: 2 s3 pushed 3 s1 top: 2 s3 top: 3
但程序结束时发生崩溃。根源很明确:我们仅定义了复制构造函数,未自定义拷贝赋值运算符。编译器生成的默认operator=仅执行浅拷贝,导致两个对象共享动态内存所有权。析构时同一内存区域被释放两次,引发堆损坏与程序崩溃。
重载赋值运算符实现深拷贝
因此,当类管理动态资源时,仅实现复制构造函数并不足够,必须显式重载赋值运算符。这是C++资源管理类设计的重要准则。
在Stack.hpp中声明赋值运算符重载函数,并在Stack.cpp中实现:
//重载赋值运算符
//函数返回当前对象的引用,支持链式赋值(如s3 = s2 = s1)。s2 = s1返回s2的引用,s3 = s2实际执行s3 = (s2 = s1),最终s3获得s1的值。
Stack& Stack::operator =(const Stack& s) {
//检查自赋值
if (this == &s)
return *this;
//释放原有资源
std::free(buffer_);
capacity_ = s.capacity_;
buffer_ = static_cast(std::malloc(sizeof(int) * s.capacity_));
top_ = buffer_ - s.buffer_ + s.top_;
std::memcpy(buffer_, s.buffer_, sizeof(int) * s.capacity_);
return *this;
}
实现需注意三个要点:第一,返回Stack&类型以支持链式赋值;第二,起始处检查自赋值情况,避免不必要的资源释放;第三,必须优先释放当前对象原有资源,再分配新空间并执行深拷贝。实现后重新运行测试代码,内存重复释放错误即会消失。
这种参数为自身const引用的赋值函数,常被称为拷贝赋值函数。它与拷贝构造函数共同构成类对象“值语义”的基础。
赋值运算符不仅限于拷贝赋值。以复数类为例,我们还可定义从double到Complex类型的赋值:
class Complex {
public:
Complex(double r, double i) : real_(r), imag_(i) {};
Complex& operator =(double r) {
real_ = r;
imag_ = 0.0;
return *this;
}
private:
double real_, imag_;
};
隐式构造函数与explicit关键字
谈及赋值,另一个易混淆概念是隐式构造。若类未声明特定赋值函数但存在单参数构造函数,编译器可能隐式调用该构造函数完成“赋值”。
class Complex {
public:
Complex(double r) : real_(r), imag_(0.0) {};
Complex(double r, double i) : real_(r), imag_(i) {};
public:
double real_, imag_;
};
int main() {
Complex c1(1, 2);
//使用隐式构造函数
Complex c2 = 45.0;
//使用隐式构造函数
c1 = 4;
std::cout << c1.real_ << " + " << c1.imag_ << "i" << std::endl;
std::cout << c2.real_ << " + " << c2.imag_ << "i" << std::endl;
return 0;
}
隐式转换虽带来便利,也可能导致意外歧义与错误。若不希望构造函数被隐式调用,可在其前添加explicit关键字。
以Stack类为例,构造函数接受int参数表示最大容量。若允许隐式构造,st = 20;语法虽合法但语义模糊——是修改栈容量还是压入数值20?为避免歧义,可为构造函数添加explicit:
class Stack {
public:
explicit Stack(int max_size);
//……
int main (){
Stack st(16);
st = 20; //编译报错
}
回到复数类示例。若存在多个构造函数,隐式构造可能引发重载决议歧义。此时为含默认参数的构造函数添加explicit,可强制调用者明确意图。
class Complex {
public:
Complex(double r) : real_(r), imag_(0.0) {};
explicit Complex(double r = 0, double i = 0) : real_(r), imag_(i) {};
public:
double real_, imag_;
};
int main() {
Complex c1(1, 2);
//使用了隐式构造函数
Complex c2 = 45.0;
//使用了隐式构造函数
c1 = 4;
std::cout << c1.real_ << " + " << c1.imag_ << "i" << std::endl;
std::cout << c2.real_ << " + " << c2.imag_ << "i" << std::endl;
return 0;
}
实际工程中是否使用explicit需权衡便利性与安全性。普遍建议是:对于单参数构造函数,除非有充分理由允许隐式转换,否则应声明为explicit。这能避免许多隐蔽错误,使代码意图更清晰。
补充说明:C++11标准后,隐式构造功能更丰富。若构造函数所有参数均有默认值,甚至支持多参数隐式构造(通过初始化列表语法):
class Complex {
public:
Complex(double r = 0, double i = 0) : real_(r), imag_(i) {};
public:
double real_, imag_;
};
int main() {
Complex c1(1, 2);
//使用了隐式构造函数
Complex c2 = 45.0;
//使用了隐式构造函数
c1 = 4;
std::cout << c1.real_ << " + " << c1.imag_ << "i" << std::endl;
std::cout << c2.real_ << " + " << c2.imag_ << "i" << std::endl;
//使用多参数隐式构造
c1 = { 45, 56 };
std::cout << c1.real_ << " + " << c1.imag_ << "i" << std::endl;
return 0;
}
这再次提醒我们,深入理解构造、赋值与隐式转换的底层机制,对编写健壮、清晰的C++代码至关重要。
热门专题
热门推荐
迅捷路由器双频开启后网速变慢?三步系统调优,释放千兆真实性能 很多朋友发现,家里的迅捷路由器明明开启了2 4G和5G双频,可用起来网速反而时快时慢,追剧卡顿、游戏高延迟成了家常便饭。这背后,问题往往出在几个容易被忽视的细节上:默认开启的“双频合一”功能、信道自动选择的“偷懒”逻辑,以及频段配置与使用
选择虚拟币交易所需综合考量安全性、交易对、费用及用户体验。头部平台各具特色:币安适合多元交易者,Coinbase便于新手入门,OKX在衍生品领域领先,Kraken以安全合规著称。新兴平台如Bybit、KuCoin则在特定市场或功能上表现突出。投资者应根据自身需求,优先考虑资产安全与合规性,再结合交易习惯选择合适平台。
荣耀100 Pro不支持红外遥控功能,硬件层面未配备红外发射模块,因此无法直接通过手机发射红外信号控制传统空调。根据荣耀官方技术规格及多轮实测验证,该机型未集成红外硬件,系统设置中亦无“智能遥控”入口,桌面实用工具文件夹内亦未预置相关应用;用户若需实现空调控制,须借助荣耀智慧空间APP接入兼容的智能
华硕主板重启后U盘启动失效?系统性排查与精准解决 遇到华硕主板重启后U盘启动失效这事儿,确实挺让人头疼。但你不用焦虑,这通常不是什么玄学问题,根源往往出在引导设置、启动介质或固件兼容性这几个有章可循的技术环节上。咱们一步步来,把问题拆解清楚。 一、确认BIOS启动顺序与设备识别状态 第一步,得先让主
U盘数据恢复:从逻辑故障到物理损坏的全攻略 遇到U盘数据丢失或彻底“罢工”时,别慌,路通常有两条:要么借助靠谱的软件工具自行尝试,要么交给有资质的专业机构处理。如何选?其实关键看故障类型。对于分区丢失、误删除、中毒这类逻辑性故障,市面上的专业恢复工具是主力军,像数据蛙恢复专家、DiskGenius、





