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);

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

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

相关推荐
地平线开发者6 小时前
profiler debug 工具用法与高一致性策略
算法·自动驾驶
编程大师哥6 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog6 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008116 小时前
FastAPI APIRouter
开发语言·python
Benszen6 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木6 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌6 小时前
算法解题思路指南
算法
MC皮蛋侠客6 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者6 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶