【C++修仙录02】筑基篇:类和对象(中)

嗨~大家好,这里是春栀怡铃声的博客~

"做你害怕的事,然后发现,不过如此~"

哈喽呀,今天我们继续与C++智斗~

目录

类的默认成员函数

const写在函数末尾的情况

构造函数

构造函数的特点:

析构函数

析构函数的特点:

拷贝构造函数

拷贝构造函数的特点:

野引用

赋值运算符重载

运算符重载

赋值运算符重载的特点


类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。

⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可

这4个:构造函数、析构函数、拷贝构造函数、赋值运算符重载

我们学习默认成员函数时,第一我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。

第二要研究如果默认成员函数不符合我们的要求,我们怎么写成员函数去实现

const写在函数末尾的情况

加了 const 编译器就能拦住你修改变量?秘密在于 C++ 类的隐藏参数:this 指针。

当你调用一个成员函数时,编译器会偷偷把当前对象的地址作为参数传进去。

对于普通成员函数 void func():

隐藏的 this 指针类型是 Data* const this(指针本身不能变,但指针指向的对象可以被修改)。

对于常成员函数 void func() const:

隐藏的 this 指针类型变成了 const Data* const this(不仅指针本身不能变,指针指向的对象也被视为了常量)。

因为 this 指向的对象变成了 const,所以通过 this 访问到的所有成员变量也都被当成了常量,自然就无法赋值了。

在写 C++ 的类时,请养成一个极其良好的习惯:

只要一个成员函数不需要修改对象内部的数据(比如各种 get() 函数、打印函数),就立刻、马上、毫不犹豫地在它末尾加上 const!

构造函数

完成初始化工作

构造函数的特点:

  1. 函数名与类名相同。

  2. 无返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

  3. 对象实例化时系统会自动调用 对应的构造函数。

  4. 构造函数可以重载。

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。

请看这段代码,类名为Date ,构造函数的函数名也是Date ,并且没有返回值,

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)//构造函数  全缺省形式
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	private:
	int _year;
	int _month;
	int _day;
	
};
int main()
{
	Date d1(2024, 7, 5);
    d1.Print();

	Date d2(2024, 7, 6);
    d2.Print();
	return 0;
}

这里构造函数是以全缺省形式写的,还有无参和带参函数:

// 1.无参构造函数

Date()

{

_year = 1;

_month = 1;

_day = 1;

}
// 2.带参构造函数

Date(int year, int month, int day)

{ _year = year;

_month = month;

_day = day;

}

相比于无参和带参形式,全缺省更具有优势,

全缺省在没有传参的时候 有自己初始值,如果此时执意打印也不会出错,传参后就保持传参的数值。

这个构造函数需要我们自己动手写才能达到这个效果,接下来我们看一个不需要自己动手写的构造函数

cpp 复制代码
#include<iostream>
using namespace std;
class MyQueue
{
public:
 //编译器默认⽣成 MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化 
private:
   Stack pushst;
   Stack popst;
};
int main()
{
    MyQueue mq;
    return 0;
}

析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

1.析构函数名是在类名前加上字符 ~

  1. 无参数无返回值 。(这里跟构造类似,也不需要加void)

  2. ⼀个类只能有**⼀个析构函数** 。若未显式定义,系统会自动生成默认的析构函数

  3. 对象生命周期结束时 ,系统会自动调用析构函数

  4. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员
    调用他的析构函数。

  5. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。

7.对于我们在构造函数那节讲的Date 类型,由于只是有相关变量(_year 、_month 、_day)的处理,没有特别的,所以无需自己动手写析构函数

而对于Stack 类**,有资源申请时** ,⼀定要自己写析构, 否则会造成资源泄漏。有指针的释放,置为空指针,所以不能直接套用编译器默认那一套(换句话说,编译器那一套已经过时,满足不来我们新的需求,需要自己实现)

cpp 复制代码
include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n =4)  //构造函数
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()  //自动调用析构函数,后定义的先析构
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
int main()
{
	Stack st1;
    Stack st2;
	return 0;
}

注意!这里调用析构函数后,先析构st2 后析构st1

拷贝构造函数

如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。

拷贝构造函数的特点:

  1. 拷贝构造函数是构造函数的⼀个重载。

  2. 拷贝构造函数的第⼀个参数必须是 类型对象的引用 ,使用传值方式编译器直接报错 ,因为语法逻辑上会引发无穷调用递归。拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值。

  3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

  4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型

员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构

造。

  1. 这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。

  2. 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以少

拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。

关于Date 类的拷贝构造如下

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
 public:
	  Date(int year,int month,int day)  //构造函数 ,带参构造函数
	 {
		  _year = year;
		  _month = month;
		  _day = day;
	 }
	  Date(const Date &d) //拷贝构造  不加 & 会报错  必须是引用
		                  //加const 保护d不被改变
	                      //d是d1的别名 d2拥有_year等,接下来的步骤是把d1中的值拷贝到d2中
	 {
		  _year = d._year;
		  _month = d._month;
		  _day = d._day;
	 }
	  void Print()
	  {
		  cout << _year << "/" << _month << "/" << _day << endl;
	  }

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2026, 4, 1);
	d1.Print();


	Date d2(d1); //拷贝d1的值给d2 ---拷贝构造
    //Date d2=d1; 也是拷贝构造,这是另一种写法
	d2.Print();
    return 0;
}

关于Stack 类的拷贝构造

cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n =4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()  //自动调用析构函数,后定义的先析构
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	Stack st3 =st1; //也是拷贝构造 ,规定
	return 0;
}

1.如果我们不自己写拷贝构造函数,而是直接调用默认拷贝构造,程序会崩溃

Why?

我们想一想,在调用默认拷贝构造过程中,并没有申请新空间,所以我们st2 和st1 指向同一个空间,如下图:

指向同一个空间,意味着析构函数起作用时,这个空间被析构了2次!所以程序会崩溃!

那我们怎么解决呢?自己写一个拷贝构造函数,需要申请空间~

cpp 复制代码
Stack(const Stack &st) //正确的拷贝构造函数  深拷贝
{
	_a = (STDataType*)malloc(sizeof(STDataType)*st._capacity);
	if (_a == nullptr)
	{
		perror("malloc fail");
		return;
	}
	memcpy(_a, st._a, sizeof(STDataType) * st._top);  
	_capacity = st._capacity;
	_top = st._top;
}

野引用

如果说 C++ 中的"引用是给一个变量起了个贴心的"小名",那么野引用就像是你试图拨打前任已经注销的电话号码。

想象一下:刚谈恋爱时,你存了对方的号码并备注为"亲爱的"(这叫建立引用)。后来你们分手了,对方把号码注销了(这叫变量被销毁、内存被释放)。几个月后,这个号码被运营商重新分配给了一个卖保险的大哥(内存被分配给了其他程序)。

这时,如果你习惯性地再拨打"亲爱的"找人借钱:

情况 A: 提示空号,直接挂断(程序直接 Crash 崩溃)。

情况 B: 电话接通了,但对面传来一句"大哥买保险吗?"(程序没崩,但读写了完全错乱的垃圾数据)。

在 C++ 中,这种情况被称为未定义行为(Undefined Behavior),俗称"薛定谔的代码"------它有时好用,有时崩溃,全看运气。

赋值运算符重载

运算符重载

当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编 译报错。

换一种说法:现在有2个钱包,一个钱包里面有200元,另一个则有50元,现在我想让编译器算算这2个钱包一共有多少元,可惜编译器只能对它认识的类型做加法!我们的钱包相加是自定义类型,这时候,运算符重载就排上用场啦

请看下面代码示意:

cpp 复制代码
#include <iostream>
using namespace std;

class Wallet {
public:
    int money;
    Wallet(int m) 
   {
        money==m; 
   }

    // 重载 '+' 运算符
    // 函数名就是 operator+
    Wallet operator+(const Wallet& other) {
        // 创建一个新钱包,里面的钱等于两个钱包的钱之和
        return Wallet(this->money + other.money); 
    }
};

int main() {
    Wallet a(200); // 钱包A有200块
    Wallet b(50);  // 钱包B有50块

    // 现在编译器认识这个加号了!
    // 实际上它在底层偷偷调用了:a.operator+(b)
    Wallet c = a + b; 

    cout << "新钱包里有: " << c.money << " 块钱" << endl; 
    // 输出: 新钱包里有: 250 块钱
    return 0;
}

运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。

重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元 运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数⽐运算对象少⼀个。

运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。(就是说原来的运算符优先级不改变)

不能通过连接语法中没有的符号来创建新的操作符:比如operator@
.* :: sizeof ?: . 注意以上5个运算符不能重载。

重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int

operator+(int x, int y)

⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义

重载++运算符时,有前置++和后置++ ,运算符重载函数名都是operator++, 无法很好的区分。

C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。

请看代码演示:

cpp 复制代码
class Date
{
public:
	Date(int year=2026, int month=4, int day=18)
  {
	_year = year;
	_month = month;
	_day = day;
  }
    
    void Date::Print() const  
  {
	cout << _year << "/" << _month << "/" << _day << "/"<<endl;
  }

//d1++
Date  operator++(int) 
{
	Date tmp = *this;
	tmp += 1;
	return tmp;
}

//++d1
Date& operator++()
{
	*this += 1;
	return *this;
}

private:
	int _year;
	int _month;
	int _day;
};

注意看前置++我们实现的运算符重载返回的是引用,而后置++并没有返回引用

这是为什么呢?

前置++:先完成++后使用,*this本身会发生改变,传结果时可以直接传*this

后置++:先完成使用后++,*this 本身不会改变,传结果时必须要有一个新值在原来*this基础上+1,最后返回新值,新值的生命周期从调用完这个函数后就结束了,所以如果这里依旧选择传引用返回的话,会造成野引用的出现(野引用)

重载<<和>>时,需要重载为全局函数 ,因为重载为成员函数,this指针默认抢占了第⼀个形参位
置,第⼀个形参位置是左侧运算对象,调用时就变成了对象<<cout,不符合使用习惯和可读性。

重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对

象。

cpp 复制代码
class Date
{
 friend ostream& operator<<(ostream& out, const Date& d);
 friend istream& operator>>(istream& in, Date& d); //为什么都有引用  
//in 和out前面加 & 是规定 
//为什么const 不全部都加?
//因为输入的返回值 需要改变,不能加const
public:
	Date(int year=2026, int month=4, int day=18)
  {
	_year = year;
	_month = month;
	_day = day;
  }
    
    void Date::Print() const  
  {
	cout << _year << "/" << _month << "/" << _day << "/"<<endl;
  }


private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream &out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl; 
	return out;
}
istream& operator>>(istream &in, Date &d)
{
	while (1)
	{
		cout << "请输入年月日>";
		in >> d._year >> d._month >> d._day;
		if (!d.Check()) //加d.
		{
			cout << "输入错误";
			d.Print();  
			cout << "重新输入";
		}
		else
		{
			break;
		}
	}

	return in;
}

以日期类为例,介绍<< >> 如何重载

重点:

1.设计成全局函数

2.使用友元函数调用类的私有

3.为什么const 不加到输入重载函数中?因为输入的返回值 需要改变,不能加const

4.in 和out前面加 & 是规定

赋值运算符重载的特点

赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分,拷贝构造用于**⼀个对象拷贝初始化给另⼀个要创建的对象。**

赋值运算符重载的特点:

  1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成

const 当前类类型引用,否则会传值传参会有拷贝

  1. 有返回值,且建议写成当前类类型引用 ,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。

  2. 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。

相关推荐
楼田莉子2 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式
故事和你912 小时前
洛谷-数据结构-1-3-集合3
数据结构·c++·算法·leetcode·贪心算法·动态规划·图论
春栀怡铃声2 小时前
【C++修仙录02】筑基篇:类和对象(上)
开发语言·c++·算法
ulias2123 小时前
leetcode热题 - 3
c++·算法·leetcode·职场和发展
大彼方..3 小时前
深入学习cpp初阶模板
开发语言·c++·学习
老四啊laosi3 小时前
[C++进阶] 25. C++11新特性(一)
c++·c++11·右值
零号全栈寒江独钓3 小时前
基于c/c++实现linux/windows跨平台ntp时间戳服务器
linux·c语言·c++·windows
ulias2123 小时前
进程初识(1)
linux·运维·服务器·网络·c++
t***5443 小时前
Orwell Dev-C++ 和 Embarcadero Dev-C++ 哪个更好
开发语言·c++