游乐游手机版
首页/编程语言/文章详情

C++面向对象编程中对象的赋值操作详解

时间:2026-05-08 16:54
对象初始化:构造函数与复制构造函数详解 在C++面向对象编程中,构造函数是类设计的核心环节。常规对象初始化依赖于构造函数,即使未显式定义,编译器也会生成默认版本。然而,还存在一种特殊的初始化方式——通过已有对象创建新对象,这便涉及复制构造函数。本文将以栈(Stack)类为例,系统解析对象初始化、复制

对象初始化:构造函数与复制构造函数详解

在C++面向对象编程中,构造函数是类设计的核心环节。常规对象初始化依赖于构造函数,即使未显式定义,编译器也会生成默认版本。然而,还存在一种特殊的初始化方式——通过已有对象创建新对象,这便涉及复制构造函数。本文将以栈(Stack)类为例,系统解析对象初始化、复制构造以及赋值操作的核心机制与常见陷阱。

c++面向对象之对象的赋值详解

首先查看栈类的声明。在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"
#include 
Stack::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。此时s1s2栈顶元素应相同。接着向s2压入新值,两个栈的栈顶将发生变化——这正是深拷贝(deep copy)的预期效果。

然而问题常隐藏于看似普通的操作中。我们再创建s3对象,并使用默认赋值运算符将s1赋值给它。此时隐患出现:默认赋值操作仅执行浅拷贝(shallow copy),直接将s1buffer_指针地址复制给s3。当s3压入新值时,虽然top_指针位置改变,但s1s3buffer_指向同一内存区域。程序结束时,两个对象的析构函数将先后释放同一内存块,导致典型的“重复释放”(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引用的赋值函数,常被称为拷贝赋值函数。它与拷贝构造函数共同构成类对象“值语义”的基础。

赋值运算符不仅限于拷贝赋值。以复数类为例,我们还可定义从doubleComplex类型的赋值:

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++代码至关重要。

来源:https://www.jb51.net/program/363483muf.htm
上一篇Spring Boot中ConfigurationProperties配置绑定详解与使用教程 下一篇JavaScript内存泄漏的预防与排查方法详解
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在ThinkPHP中实现定时任务与命令行调度方法
编程语言 · 2026-07-04

如何在ThinkPHP中实现定时任务与命令行调度方法

用ThinkPHP实现定时任务时,很多开发者第一步就卡在命令行报错上,直接输入php think your:command却无法识别——这种情况绝大多数是因为命令类的注册方式存在问题。下面先梳理几个核心要点。 ThinkPHP 6 中 think 命令如何正确触发自定义指令 直接运行 php thi

ThinkPHP API接口防重放攻击实现方法
编程语言 · 2026-07-04

ThinkPHP API接口防重放攻击实现方法

先说几个核心判断:API防重放攻击这件事,做对了是道防火墙,做错了就是个心理安慰。很多开发者到踩坑了才明白——验签这东西,放错位置、漏掉字段、存错nonce,每一环都能让整个安全体系直接归零。 验签必须放在中间件里,不能在控制器里写 ThinkPHP 的请求生命周期中,中间件是唯一能在路由匹配、参数

ThinkPHP文件上传必须验证扩展名安全必要性分析
编程语言 · 2026-07-04

ThinkPHP文件上传必须验证扩展名安全必要性分析

在使用ThinkPHP进行文件上传时,ext扩展名验证通常是开发者首先接触的关键环节。但你真的了解它的实际工作原理吗?它仅比对文件名后缀,而不读取文件内容,甚至对空格和大小写都极其敏感。更为重要的是——它是TP文件上传验证五层防线中不可忽视的第一道关卡,一旦配置遗漏,整个validate验证链将直接

ThinkPHP关联模型自动写入与更新使用教程
编程语言 · 2026-07-04

ThinkPHP关联模型自动写入与更新使用教程

需要明确的是,ThinkPHP关联模型并没有提供所谓的“自动写入 更新”魔法开关。所谓的“自动”功能,实际上都需要开发者手动编写配置逻辑才能生效。核心原则在于:主模型和从模型必须分开独立处理,时间戳字段和业务字段需依靠修改器或钩子接管;批量操作则要规规矩矩地绕过模型逻辑来执行——只有理解透彻这些要点

BoxLayout中仅居中一个组件其他默认左对齐
编程语言 · 2026-07-04

BoxLayout中仅居中一个组件其他默认左对齐

在 Java Swing 中使用 BoxLayout 的 Y_AXIS 方向布局时,很多初学者容易掉进一个常见陷阱:希望将某个组件单独设置为中心对齐,但当调用 `setAlignmentX(CENTER_ALIGNMENT)` 后,却发现其他组件也跟着发生了偏移,完全达不到预期效果。实际上,关键之处