C++(在Mystring类中碰到的构造函数和析构函数以及深拷贝和浅拷贝的问题)

以实例开始:从我的视角看我对这个代码中涉及知识点的疑惑吧!

首先上代码,是不是觉得很长,没关系慢慢看,会有收获的!想知道是不是比我聪明,就看看你问的问题是不是比我少吧!

cpp 复制代码
class Mystring {
private:
	char* str;
public:
	//构造函数
	Mystring(const char* ps = nullptr):str(nullptr)
	{
		if (nullptr != ps && *ps != '\0') {
			int len = strlen(ps);
			str = (char*)calloc(len+1, sizeof(char));
			assert(str != NULL);
			strcpy(str, ps);
		}
	}
	//析构函数
	~Mystring(){
		free(str);
		str = NULL;
	}
	//深拷贝
	Mystring(const Mystring& it):str (nullptr) {
		if (it.str != nullptr && *(it.str) != '\0') {
			str=(char*)calloc(strlen(it.str)+1,sizeof(char));
		    strcpy(str, it.str);
		}
	}
	//运算符重载
	Mystring &operator=(const Mystring& it) {
		if (this != &it) {
			free(str);
			str = nullptr;
			if (it.str != nullptr && *it.str != '\0') {
				str = (char*)calloc(strlen(it.str) + 1, sizeof(char));
				assert(str != nullptr);
				strcpy(str, it.str);
			}
		}
		return *this;
	}
    //打印输出
	void PrintInfo() const{
		if (nullptr != str) {
        cout << "str:"<<str << endl;
		}
		else {
        cout << "Empty" << endl;
		}
		
		return;
	}
};

struct Node {
	int value;
};

int main() {
	int* p = nullptr;
	int Node::* s = nullptr;
	s = &Node::value;

	Mystring s1;
	Mystring s4;
	Mystring s2("Happy");

	s1.PrintInfo();
	s2.PrintInfo();
	Mystring s3(s2);
	s3.PrintInfo();
	s4 = s2;
	s4.PrintInfo();
	return 0;
}

(1)Mystring类和其他类有什么区别吗?

普通类成员变量是 值类型(比如存Int类型的类),数据直接存在自己的对象里

Mystring类成员变量是 指针类型 (比如存char*类型的类**)**,真实字符串存在堆内存里,对象只存地址。

(2)为什么要区分 Private 和 Public?

class 类的默认权限是 Private ; struct 默认权限是 Public

Private是封装的核心目的是为保证数据内容的合法性,不让外部乱改类内部的重要数据,

通过 public 函数间接修改 private 变量,就可以做校验

  • private:把变量锁起来,不让外面乱碰
  • public 函数:给你一个合法的入口
  • 在函数里加判断:保证数据永远是合法的

这就是封装 的意义:保护数据,防止被乱改,让类更安全、更健壮。

(3)构造函数?

构造函数是做什么的?

对象一创建,他就自动跑,用来初始化对象

构造函数的特点是什么?
  • 函数名和类名一模一样
  • 没有返回值,连 void 都不用写
  • 创建对象时,构造函数自动调用
  • 可以有参数(重载)
结合本代码例子讲一讲构造函数:
cpp 复制代码
//构造函数
	Mystring(const char* ps = nullptr):str(nullptr)
	{
		if (nullptr != ps && *ps != '\0') {//指针不为空和不为空串
			int len = strlen(ps);
			str = (char*)calloc(len+1, sizeof(char));//calloc(个数,类型字节)
          //char* str = (char*)calloc(len+1, sizeof(char));//错误的
			assert(str != NULL);
			strcpy(str, ps);
		}
	}

作用:创建对象时,根据传入的字符串,在堆上开辟空间并拷贝内容

  • 为什么函数的参数是const char* ps = nullptr?

(1)为什么是 const char* 类型------传入的是字符串常量

const 是为了:

  • 防止在构造函数里不小心修改字符串内容

  • 能接收 const 和非 const 的字符串

  • 符合 C++ 常量正确性规范

  • 不写 const 有些编译器会报警 / 报错

(2)为什么要有 = nullptr ------ 默认参数

调用这个构造函数时可以不传参

总之:定义了一个构造函数,可以接收一个字符串指针,有参数传入参数,没有参数默认nullptr 作为参数

  • 为什么后面要写 str(nullptr)

把成员变量 str 初始化为空指针 nullptr

":" 后面这一堆,是在构造函数执行前,给成员变量赋初值的------成员初始化列表

为什么必须显示初始化------指针如果不初始化里面是垃圾随机值

  • 为什么char* str = (char*)calloc(len+1, sizeof(char));是错误的?

char* str 定义了一个局部变量和类成员 str 重名,在{}里是str是局部变量,不是类成员

  • 给char* str扩容是给局部变量扩容
  • 局部变量出了 {} 就销毁了,类成员str还是nullptr

正确写法: str = (char*)calloc(len+1, sizeof(char));

这样才是给类成员扩容

**更加标准的写法:**this->str = (char*)calloc(len+1, sizeof(char));

更加明确是给此类成员扩容

  • 几种扩容函数的格式?

malloc:类型* 指针=(类型*)malloc(元素个数*sizeof(元素类型));

eg.char* p=(char*)malloc(10*sizeof(char));

calloc:类型* 指针=(类型*)calloc(元素个数,sizeof(元素类型));

eg.char* p=(char*)calloc(10,sizeof(char));

realloc:类型* 指针=(类型*)realloc(旧指针,元素个数*sizeof(元素类型));

eg.char* p=(char*)realloc(s,10*sizeof(char));
new:指针=new 类型 [元素个数*sizeof(元素类型)];

eg.p=new(char)[10*sizeof(char)];

(4)析构函数?

析构函数是做什么的?

对象要死的时候(离开作用域、程序结束)自动跑,用来清理内存。

析构函数的特点是什么?
  • 析构函数与类名相同,但在前面加上字符'~'
  • 析构函数无函数返回类型,不带有参数
  • 一个类只有一个析构函数
为什么要析构?
  • 对象销毁时,释放堆上的字符串内存
  • 避免内存泄漏
  • 如果不释放,程序一直占着内存不用

(5)为什么在Mystring类中区分深拷贝和浅拷贝?

浅拷贝:
浅拷贝做了什么?

C++默认生成的拷贝构造函数和运算符重载函数,只会做一件事情:

逐字节拷贝成员变量**------就是把指针地址复制过去,不会复制堆里字符串的内容**

最后浅拷贝的后果是什么?
cpp 复制代码
Mystring s2("Happy");
Mystring s3(s2);

s2.str 和 s3.str 指向同一块堆内存:

  • 一个改全改,
  • 一个析构释放空间另一个变成野指针,
  • 最后两个对象析构,同一个内存释放两次,程序崩溃
深拷贝:
深拷贝做了什么?

自己写拷贝构造函数和赋值重载函数:

开辟新的内存空间、将堆空间中的内容复制过去、让新对象拥有独立的内存、两个对象互不干扰

深拷贝的效果?

s3 和 s2 有各自的的堆空间:

  • 给 s3.str 新开辟一块内存
  • 把 s2 的字符串内容复制过去
  • 两个指针指向不同内存,完全独立,析构时各自释放自己的内存,不会造成内存崩溃
结合本代码例子讲一讲深拷贝:
cpp 复制代码
Mystring(const Mystring& it):str(nullptr) 
{
	if (it.str != nullptr && *(it.str) != '\0') 
	{
		// 1. 开新空间
		str = (char*)calloc(strlen(it.str)+1, sizeof(char));
		
		// 2. 把内容复制过去
		strcpy(str, it.str);
	}
}
  • 为什么是 Mystring?

因为这是 Mystring 类的拷贝构造函数,函数名必须和类名一样。

  • 为什么参数 const Mystring& it 要加引用?

& 引用

  • 不能用值传递 Mystring it
  • 因为值传递会触发拷贝构造
  • 那就会无限递归 → 直接崩溃
  • 所以必须用引用 &

it是被拷贝的对象

  • 这句it.str != nullptr && *(it.str) != '\0'的作用是什么?

用来判断被拷贝的对象 it(也就是 s2)有有效字符串吗

(6)运算符重载函数?

运算符重载函数是做什么的?

就是给 +、=、==、[]、<< 这些符号,重新定义它在你的类里怎么干活

在Mystring中其他几种常用的几种运算符重载格式?
加号 + 字符串拼接
cpp 复制代码
//字符串拼接 +
    Mystring operator+(const Mystring& it) const {
        Mystring temp;
        int len1 = (str != nullptr) ? strlen(str) : 0;
        int len2 = (it.str != nullptr) ? strlen(it.str) : 0;

        temp.str = (char*)calloc(len1 + len2 + 1, sizeof(char));
        if (str != nullptr) strcat(temp.str, str);
        if (it.str != nullptr) strcat(temp.str, it.str);

        return temp;
    }
字符串比较
cpp 复制代码
// 字符串比较 ==
    bool operator==(const Mystring& it) const {
        if (str == nullptr && it.str == nullptr) return true;
        if (str == nullptr || it.str == nullptr) return false;
        return strcmp(str, it.str) == 0;
    }
下标返回
cpp 复制代码
   // 下标访问 []
    char& operator[](int index) {
        assert(str != nullptr);
        assert(index >= 0 && index < (int)strlen(str));
        return str[index];
    }
结合本代码的例子讲讲运算符重载函数:
cpp 复制代码
Mystring &operator=(const Mystring& it) {
	//1.检查是否是自己给自己赋值
	if (this != &it) {
    //2.将当前空间free掉
		/*delete[]str;*/
		free(this->str);
    //3.str置空
		this->str = nullptr;
    //4.it.str分配空间
		if (it.str != nullptr && *it.str != '\0') {
			/*char* str = new(it.str)char[sizeof(strlen(it.str)+1)];*/
			this->str = (char*)calloc(strlen(it.str) + 1, sizeof(char));
			//assert(str != nullptr);
		    //5.拷贝
			strcpy(this->str, it.str);
		}
	}
	return *this;
}
  • operator=的作用?

赋值运算符重载,在主函数中可以写s3=s2

  • const Mystring& it的作用?

作用:让 it 等于 = 右边的对象

& 用引用避免拷贝浪费

const 只读取不修改原变量

  • Mystring & 返回值的作用?

作用:返回当前对象的引用 *this

目的:支持链式赋值 eg.s3=s2=s1

  • 先释放旧内存,那里面的东西放在哪里?(以s3=s2为例子)

首先:free(this->str);

释放的是指针指向的旧字符串内存,不是释放指针本身!指针还在只是不指向,变成了野指针

然后:this->str=nullptr;

将指针指向空,安全

其次:this->str = (char*)calloc(strlen(it.str) + 1, sizeof(char));

申请新空间

最后:strcpy(this->str, it.str);

改变指针指向,让指针指向新空间

你真棒!看到这里啦,给自己送个花花吧!

相关推荐
hz_zhangrl2 小时前
CCF-GESP 等级考试 2026年3月认证C++四级真题解析
c++·程序设计·gesp·c++四级·gesp2026年3月·gesp c++四级
Chasing Aurora2 小时前
Python后端开发之旅(五)——DL
开发语言·pytorch·python·深度学习
Fang fan2 小时前
Java集合
java·开发语言·算法
AI成长日志2 小时前
【笔面试算法学习专栏】链表操作专题:反转、环形检测与合并
学习·算法·面试
liulilittle2 小时前
TC Hairpin NAT 驱动使用手册(个人版)
服务器·开发语言·网络·c++·网络协议·tcp/ip·tc
njidf2 小时前
C++与量子计算模拟
开发语言·c++·算法
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #70:爬楼梯(朴素递归、记忆化递归、动态规划等六种实现方案详解)
算法·leetcode·动态规划·递归·斐波那契·矩阵快速幂·爬楼梯
爱学习的程序媛2 小时前
【Web前端】深入解析JavaScript异步编程
开发语言·前端·javascript·ecmascript·web
IAUTOMOBILE2 小时前
两大王者-Laravel vs ThinkPHP:PHP 框架终极对决,谁更适合团队或者个人!
开发语言·php·laravel