类和对象(中)


🎁个人主页: 工藤新一¹

🔍系列专栏: C++面向对象(类和对象篇)

🌟心中的天空之城,终会照亮我前方的路

🎉欢迎大家点赞👍评论📝收藏⭐文章


文章目录

类和对象(中)

一、类中的默认成员函数

​默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我 们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是 C++11 以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯ 去学习:


二、构造函数

构造函数 是特殊的成员函数,需要注意的是,构造函数 虽然名称叫构造,但是 构造函数 的主要任务并 不是开空间创建对象(我们常使⽤的 局部对象是栈帧创建时,空间就开好了 ),⽽是对象实例化时初始化 对象。构造函数 的本质是要替代我们以前 StackDate 类中写的 Init函数 的功能,构造函数 ⾃动调⽤的 特点就完美的替代的了 Init

构造函数特点:

​ 1、对象实例化时系统会自动调用对应的构造函数

​ 2、构造函数可以发生重载,默认构造不仅仅局限于默认构造函数,默认构造的特点:不传参即可调用

​ 3、如果类中没有显式定义构造函数,则 C++ 编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成

cpp 复制代码
C++
    //调用默认构造函数时
    注意区分:函数声明
    Date func();//--->函数声明
	Date d();//--->编译器会无法区分(认为这也是函数声明)

	Date d1;//--->默认构造的正确调用方式
	/*
		与 Java 中的调用默认构造不同
		Date d = new Date();
	*/

​ 4、无参构造函数全缺省构造函数 、我们不写构造时编译器默认⽣成的 默认构造函数都叫做默认构造函数但是这三个函数有且只有⼀个存 在,不能同时存在⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造


cpp 复制代码
C++
	//1、无参构造
	Date()
	{
		year = 1;
		month = 1;
		day = 1;
	}

	//2、全缺省构造
	Date(int year = 1, int month = 1, int day = 1)
	{
		this->year = year;
		this->month = month;
		this->day = day;
	}
//在语法上 无参构造与全缺省构造可以构成函数重载

相较于无参构造,全缺省构造的优点:

  • 实例对象可无参调用
  • 实例对象可有参调用

​ 5、我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器 。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	
	//全缺省构造(作用:初始化)
	Stack(int n = 4)
	{
		arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (arr = nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}

		capacity = n;
		top = 0;
	}

private:

	STDataType* arr;//维护堆区数据
	int capacity;
	int top;
};

//两个栈实现队列
class MyQueue
{
public:
	//编译器默认生成 MyQueue 的构造函数调用了 Stack 的构造,完成了对两个成员的初始化

private:
	//类对象
	Stack pushst;
	Stack popst;
};
int main()
{
	//实例化的同时调用了 MyQueue 的构造函数
	MyQueue mq;

	return 0;
}

构造函数的调用顺序:


三、析构函数

  • 析构函数并非是释放对象 ,具有 "先构造后析构" 的特性(有构造一定有析构)
  • 对于自定义数据类型不显示析构,会导致内存泄漏

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


析构函数的特点:

  • 析构函数名是在类名前加上字符 ~(按位取反,:逻辑取反)

  • ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数,且对象⽣命周期结束时,系统会⾃动调⽤析构函数

  • 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数

  • 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类 型成员⽆论什么情况都会⾃动调⽤析构函数

  • 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如 Date;如 果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如 MyQueue;但是有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏,如 Stack

cpp 复制代码
C++
class Stack
{
public:
    
    Stack(int n = 4)
    {
        //动态申请资源 - 需要我们手动释放数据
		arr = (STDataType*)malloc(sizeof(STDataType) * n);
        top = 0;
        capacity = n;
    }
    
    ~Stack() 
    {
		free(arr);
        arr = nullptr;
        top = capacity = 0;
    }
};

  • ⼀个局部域的多个对象,C++规定后定义的先析构

3.1默认构造初始内置数据类型的问题



四、拷贝构造函数

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

​ 同时,存在一个小技巧:如果代码中需要 显示实现析构函数并释放资源 ,那么往往会伴随着 显示拷贝构造的实现 (写 深拷贝 ),因此 析构与拷贝构造往往会同时出现

拷贝构造的特点:

  • 指针 指向 动态开辟的数据时,需要显示实现拷贝构造函数 ,因此 指针 并非一定需要 深拷贝 ,如 迭代器文件指针 (指向打开的文件),这些只需 浅拷贝 即可

4.1无穷递归(链式)调用

  • 拷⻉构造函数是构造函数的⼀个重载
  • 拷⻉构造函数的第⼀个参数必须是类的类型对象的引⽤,使⽤ 传值⽅式 编译器直接 报错,因为语法逻辑上会引发 无穷递归调用(链式调用)。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。

值传递带来的链式调用问题:


  • C++ 规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。

举例描述:

函数调用特点:要想调用当前有参函数要先传参,需要优先调用拷贝构造函数进行参数的功能运算

**

**



4.2返回值、返回引用

传值返回(返回值) 会产⽣⼀个 临时对象调用拷贝构造 ,而 传值引用返回(返回引用) ,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但如果返回对象是⼀个当前函数局部作用域下的局部对象,函数结束就销毁了,那么使⽤ 值返回 是有问题的,这时的引⽤相当于⼀个 " 野引用(悬垂引用) " ,类似 野指针**返回引用** 可以减少拷⻉,但是⼀定要确保返回对象,在当前函数执行结束后依然存在


引发的问题(内置类型):


更严重的问题(自定义类型):




对比 Stack 的值返回








4.3默认拷贝构造

  • 对于内置数据类型,进行浅拷贝(值拷贝)

编译器生成默认拷贝构造函数,也可完成拷贝工作,这个⾃动⽣成的拷⻉构造 对内置类型成员变量(与析构、构造的区别) 会进行 值拷贝/浅拷贝 (⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造


4.4浅拷贝 - 堆区数据重复释放

cpp 复制代码
typedef int STDataType;
class Stack
{
public:

	Stack(int n = 4)
	{
		arr = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == arr)
		{
			perror("malloc fail!");
				exit(1);
		}

		_capacity = n;
		_top = 0;
	}

	void StackPush(STDataType x)
	{
		if (_top == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
			STDataType* temp = (STDataType*)realloc(arr, newCapacity * sizeof(STDataType));
			if (temp == nullptr)
			{
				perror("realloc fail!");
				exit(1);
			}

			arr = temp;
			_capacity = newCapacity;
		}
		arr[_top++] = x;
	}

	~Stack()
	{
		cout << "调用 Stack 析构函数" << endl;
		if (arr != nullptr)
		{
			//delete arr;--->写法错误,malloc 动态开辟的空间需要 free 释放
            free(arr);
			arr = nullptr;
		}
	}

private:

	STDataType* arr;
	size_t _capacity;
	size_t _top;
};


class MyQueue
{
public:

private:
	Stack pushst;
	Stack popst;
};

int main()
{
	Stack st1;
	st1.StackPush(1);
	st1.StackPush(2);
	st1.StackPush(3);

	Stack st2(st1);
	return 0;
} 

于是,我们再一次见到了我们的老朋友:

浅拷贝所带来的问题:堆区数据重复释放


浅拷贝带来的问题是堆区内存重复释放,为了解决这个问题,引入了深拷贝在堆区开辟一块新空间,从而使拷贝构造出的指针指向这块新的堆区数据,再通过析构函数分别释放这两块堆区数据

cpp 复制代码
C++

    Stack(const Stack& s)
	{
		_top = s._top;
		_capacity = s._capacity;
		
		//分配足够的空间(与数组容量相同的空间)
		arr = (STDataType*)malloc(sizeof(STDataType) * _capacity);
    
    	memcpy(arr, s.arr, sizeof(STDataType) * s.top);
	}



五、运算符重载

  • 当运算符被⽤于自定义类型的对象时,C++ 语⾔允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载

另外,函数重载与运算符重载,都描述了 "重载" 这个词汇,但他们的意义是不同的:

  • 函数重载:返回值类型、函数名相同,参数不同
  • 运算符重载:重新定义(自定义数据类型)运算符的行为
  • 两个基于相同运算符的重载,又可以形成函数重载
cpp 复制代码
//形成函数重载
d1 - 100 ---> d1.operator(100);
d1 - d2 ---> d1.operator(d2);

5.1浅识汇编层

cpp 复制代码
C++
class Date
{
public:

	Date(int year, int month, int day)
		: year(year), month(month), day(day) {
	}

private:

	int year;
	int month;
	int day;
};

int main()
{
	int a = 10, b = 20;
	bool ret = a < b;
	return 0;
} 

转入 汇编码:


​ 由于内置类型是一个比较简单的数据类型,因此系统库中在底层内化了一些指令(比较指令:cmp),进行一些功能(如:解引用、比较、随机访问 [] 等运算操作符)

​ 但系统并不会给自定义数据类型提供指令,因为自定义数据类型可以很复杂,可以使用各种各样的符号操作,系统无法判断


5.2重载运算符

5.2.1重载 ==

  • 运算符重载 也具有返回值、参数列表、函数体
  • 操作符也包括一元运算符(*p、&p...),二元运算符(x < y),那么重载运算符函数时,运算符函数的参数个数与运算对象数相同,如上述比较两个自定义数据类型大小时:
cpp 复制代码
class Date
{
public:
    //声明友元函数
	friend bool operator== (const Date& d1, const Date& d2);

	Date(int year, int month, int day)
		: year(year), month(month), day(day) { }

private:

	int year;
	int month;
	int day;
};

//加入引用避免拷贝,const的引用使函数体中d1、d2数据不会被修改
bool operator== (const Date& d1, const Date& d2)
{
	return d1.year == d2.year 
        && d1.month == d2.month 
        && d1.day == d2.day;
}

/*方法二:
	或者类内提供 Get 方法(Java 常用)
	d1.GetYear() == d2.GetYear();
*/
/*方法三:
	直接放到类中,重载为成员函数 operator==();
	运算符重载为成员函数
*/

int main()
{
    /*
    	如果const Date d1;
    	形参接收时也需要加入 const 修饰,避免权限放大(const (转为)---> 非const)
    */
	Date d1(2025, 5, 6);
	Date d2(2025, 5, 6);

    //两种写法
    d1 == d2;
	operator==(d1, d2);
	return 0;
} 


将运算符重载为成员函数:


原因:如果⼀个 重载运算符函数是成员函数 ,则它的第⼀个运算对象默认传给隐式的 this指针,因此运算符重载作为成员函数时,其函数参数⽐运算对象少⼀个(因为被隐藏的 this指针 本身就算一个参数)

因此编译器会将代码隐式转换为:



cpp 复制代码
class Date
{
public:

	Date(int year, int month, int day)
		: year(year), month(month), day(day) { }

	bool operator== (const Date& d) const
	{
		return year == d.year
			&& month == d.month
			&& day == d.day;
		
	}

private:

	int year;
	int month;
	int day;
};

int main()
{
	Date d1(2025, 5, 6);
	Date d2(2025, 5, 6);

	cout << (d1 == d2) << endl;
	cout << d1.operator== (d2);

	return 0;
} 

这就是二元运算符,通常左对象传入 this指针,右对象作为形参


5.2.2重载 << 流插入运算符
  • 在C++中,<<流插入运算符 (Stream Insertion Operator),它是标准库中重载的一个运算符,用于向输出流(如 std::cout、文件流等)插入数据。
cpp 复制代码
C++
	cout << d1 == d2 << endl;






另外,对于内置类型的直接比较也同样需要注意优先级的问题:


5.2.3运算符重载注意事项
  • 运算符重载无法根据 C / C++ 语法中不存在的符号创建新的操作符,比如:operator@ ();
  • 重载操作符至少有一个内置类型(即至少存在一个隐式的 this指针),无法通过运算符重载改变内置类型对象的含义,如:int operator+ (int x, int y);

有五个运算符不能发生重载(选择题常考):

  • " : : " , 作用域限定符
  • " sizeof "
  • " ? : " , 三目运算符
  • " . " , 调用操作符
  • " .* " , 成员指针运算符

5.2.4成员指针运算符
5.2.4.1函数指针(回调函数)与成员函数指针
  • C++ 几乎抛弃了 函数指针 这个触发 回调函数 的方式,由更简洁的 lambda 表达式进行了高效替代,因此对于 函数指针,我们目前只需浅尝辄止即可

cpp 复制代码
C++
class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};

void f()
{
	cout << "f()" << endl;
}

int main()
{
	//函数指针 - 作为回调函数
	void(*func1)() = f;
	(*func1)();//函数指针调用

	return 0;
} 

cpp 复制代码
C++
class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};

void f()
{
	cout << "f()" << endl;
}

int main()
{
	//普通函数
	//函数指针 - 作为回调函数
	void(*func1)() = f;//函数名就是函数的地址
	(*func1)();//函数指针调用

	//成员函数的指针
	//指定成员函数指针的类域
	void(A::*func2)() = &A::func;//获取成员函数的地址,需要加入取地址符号
	
	//成员函数的调用需要通过实例对象进行调用
	A aa;
	(aa.*func2)();//调用成员函数指针

	return 0;
} 






5.2.5成员对象与类对象

5.3项目 - 时间计时器

Date.h 文件

cpp 复制代码
#include<cassert>

class Date
{
public:

	//可以将频繁调用的小函数变为内联函数
	int get_month_day(int year, int month);

	//内置数据类型拷贝构造时逐字节进行浅拷贝
	Date(int year = 1, int month = 1, int day = 1);


	//(日期加天数)返回值依然是日期
	Date operator+ (int day);

	Date operator- (int day);

private:

	int year;
	int month;
	int day;
};

Date.cpp 文件

cpp 复制代码
#include"Date.h"

Date::Date(int year, int month, int day)
{
	this->year = year;
	this->month = month;
	this->day = day;
}

int Date::get_month_day(int year, int month)
{
	
}

Date Date::operator+ (int day)
{
	
}

Date Date::operator- (int day)
{

}

  • static 所修饰的变量、函数,不存储在对象上(存储在静态区)

Test.cpp 文件

cpp 复制代码
#include"Date.h"

int main()
{
	Date d1;

	Date d2(d1 + 100);//拷贝构造,也可以写出
	Date d2 = d1 + 100;

	return 0;
} 


5.3.1重载 += 复合运算符(经典误区)

对于 " + " 运算符的重载,我们尤其需要注意 " + "(二元运算符) 与 " += "(复合赋值运算符) 的区别,前者不会改变 this 对象自身,而后者则回改变 this 对象自身的属性

经典错误案例:


举例阐述:




5.3.2重载 + 运算符
  • 返回运算结果(临时对象)



5.3.3重载 " + " 与重载 " += "

d1.operator(d2); 或 d1 + d2(d1 += d2)

  • " + ":不能改变 d1(*this)(前面内置数据类型的运算有详细说明,因此自定义数据类型也需遵循)
  • " += ":可以改变d1



5.4辨别深、浅拷贝


5.5赋值运算符重载

赋值运算符重载的特点:

  • 规定必须重载为成员函数(建议写成:const + &)

  • 赋值运算符支持 链式赋值(连续赋值)的方式

  • 没有显示实现时,编译器会生成默认赋值运算符重载,其默认函数与默认拷贝构造函数类似,对内置数据进行 浅拷贝(逐字节拷贝),对自定义变量会调用其赋值重载函数(因此,存在深、浅拷贝问题)

  • 遵循 **C++三大原则:**类中显示实现 析构函数 ,那么则需显示实现 拷贝构造、赋值运算符重载函数



this->day = day;:会导致赋值无法达到理想的效果,因为 this->day == day 所以,改错:this->day = d.day;






5.5.1C++三大原则


🌟 各位看官好我是工藤新一¹呀~

🌈 愿各位心中所想,终有所致!

相关推荐
天若有情6735 天前
从一个“诡异“的C++程序理解状态机、防抖与系统交互
开发语言·c++·交互·面向对象·状态机
麻辣长颈鹿Sir7 天前
【C++】使用箱线图算法剔除数据样本中的异常值
算法·信息可视化·数据分析·c/c++·数据处理
燃尽了,可无8 天前
C#面向对象三大特性的封装
开发语言·c#·面向对象
jjkkzzzz8 天前
sylar源码解析---RPC框架之模块化分发机制
rpc·c/c++·sylar
zaiyang遇见10 天前
牛客NC14661 简单的数据结构(deque双端队列)
数据结构·stl·双端队列·c/c++·信息学奥赛·程序设计竞赛
jjkkzzzz23 天前
Linux下的C/C++开发之操作Zookeeper
linux·zookeeper·c/c++
深度Linux1 个月前
Linux缓存调优指南:提升服务器性能的关键策略
c/c++·linux开发·性能调试