C++ - 类和对象 #类的默认成员函数 #构造函数 #析构函数 #拷贝构造函数 #运算符重载函数 #赋值运算符重载函数

c++初阶——类和对象(中) 大家看我们在调用fund2时是采用值拷贝,将实参的值拷贝给形参,_add指针存放的是一个指向动态开辟空间的地址,在值拷贝时,将值拷贝,当fund2函数结束时要销毁栈帧,就会将_add指针指向的空间一起销毁,然后当我们的程序走完后又会自动调用析构函数,又会将这块空间再次free一次,所以这里程序崩溃了。(这里编译器是强制报错的)我们知道类在调用成员函数时有一个this指针,它是指向调用函数的对象的,它的类型是date*的类型而我们调用函数的对象是从const date的类型,所以无法调用,那怎么才能调用呢? 阅读详情

目录

前言

一、类的默认成员函数

二、构造函数

三、析构函数

四、拷贝构造函数

五、赋值运算符重载函数

1)、运算符重载

2)、赋值运算符重载

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、类的默认成员函数

默认成员函数就是用户没有显式实现、编译器会自动生成的成员函数称为默认成员函数;一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这个6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解以下即可;其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个会在后面的博文中进行讲解。默认成员函数非常重要,也比较复杂,本篇博文会从以下两个方面来讲解:

  • 1、我们不写(显式实现)的时候,编译器默认生成的行为是什么,是否满足我们的要求?
  • 2、当编译器默认生成的函数不满足我们的要求,我们需要自己实现,那么如何实现?

简单来说,当我们不显式实现这个函数的时候编译器会默认帮我们生成,这个函数就是默认成员函数,默认成员函数有6个,在C++11 中还会增加2个;但是编译器帮我们实现的默认成员函数可能并不会符合我们的预期,所以此时就需要我们自己来实现这个默认成员函数;在这个过程中,我们需要理解编译器的默认生成的行为、以及我们如何实现;

可以将构造函数类比以前学习的Init (初始化函数):对该类中的成员变量进行初始化;还可以将析构函数类比成Destroy;

在C语言中可以直接对结构体进行初始化,但是对于C++来说就不行了,在C++中必须专门用一个函数去进行初始化;

  • 因为C++中的成员变量一般情况下都是私有的,无法在外部直接进行访问,只有通过函数内部访问;

二、构造函数

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

构造函数的特点:

  • 1、函数名与类名相同
  • 2、无返回值。(C++规定,构造函数不用写返回值)
  • 3、对象实例化时系统会自动调用其对应的构造函数
  • 4、构造函数可以重载
  • 5、如果在这个类中没有显示定义构造函数时,则C++编译器会自动生成以一个无参的默认构造函数,一旦用户显式定义编译器就不再自动生成;
  • 6、无参构造函数、全缺省构造函数、我们没有显式实现编译器默认生成构造函数,都叫做默认构造函数;但是这三个函数有且只有一个存在,不能同时存在(核心就是函数重载的函数参数类型、个数要不同,因为都是无参的,就只能存在一个,不然就重定义了);无参构造函数和全缺省构造函数虽然构成函数重载,但是调用的时候会产生歧义(当都没传参的时候,无参构造函数和全缺省构造函数就是一样的,编译器无法识别你究竟想要调用哪一个函数,就产生了歧义);而当我们没有显式实现构造的时候,编译器就会自动生成一个,所以说这三个函数只能有且只有一个存在,不能同时存在;需要注意的时,默认构造函数并不只是我们没有显式实现构造函数编译器默认生成的默认构造函数,实际上无参构造函数、全缺省构造函数也是默认构造函数;本质来说,不用传实参的构造函数就可以称为默认构造函数;
  •  7、我们没有显式实现,编译器默认生成的构造函数,对内置类型成员变量的初始化没有要求,也就是说是否初始化我们是不确定的,这种行为却决于编译器;而对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决(下篇博文讲解初始化列表)

注:C++将类型划分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,eg. int/char/double/指针等,自定义类型就是我们用class/struct/union等关键字自己定义的类型;

 构造函数与类名相同,如下:

我们可以写多个构造函数,让其构成重载:

运行结果如下:

需要注意的时,无参的构造函数在实例化的时候直接写做:Date d1; 就可以了,不能在其后加上() ,写做:Date d1(); --> 这是错误的写法;

Q1:为什么不能这么写?

  • 因为会产生歧义,与函数声明相冲突;

即便是带参的构造函数,在实例化对象时写的应该是数据而非类型,如下:

函数声明的时候,是一定要写参数类型的,参数变量名可写可不写;实例化对象的时候传参传数据就可,并不会与函数带参的声明相冲突;

在实例化对象的时候,会根据对象传参的个数、类型,系统自动调用对应的构造函数;构造函数只能放在公有,不能放在私有,因为我们要去外部调用构造函数;也可以写很多个构造函数让其构成重载;

接下来,我们进一步改造-->缺省参数的使用,如下:

有了全缺省构造,就不用写无参构造;这两个函数构成重载,语法上可以同时存在,但是在不传参调用的时候会出现歧义;这是因为当都没传参的时候,无参构造函数和全缺省构造函数就是一样的,编译器无法识别你究竟想要调用哪一个函数,就产生了歧义;

在VS中如果未给成员变量,初始化为随机值,不同编译器的行为不同,在有的编译器中会初始化为0;

在此处看,当我们未显式实现默认构造的时候,编译器默认生成的默认构造有什么意义?

  • 1、编译器对内置类型的数据的初始化是未定义的,具体情况取决于编译器;
  • 2、对于自定义类型成员变量,要求调用这个成员变量的默认构造来进行初始化;

注:自定义类型:class、struct、union 定义的类型

当编译器的行为不符合我们的需求的时候,此时需要我们自己实现:

同理,对于栈Stack 的实现,也需要我们自己来实现构造函数,因为栈的三个成员函数也是内置类型的数据,在VS中对于内置类型数据的初始化未定义,不符合我们的预期,所以需要我们自己实现;而当成员变量是自定义类型的数据的时候,若我们未显式实现其构造,编译器会自动调用该自定义类型的数据的默认构造函数,而如果这个自定义类型的数据没有自己的默认构造函数,就会报错;倘若在这个自定义类型数据没有默认构造函数的情况下而我们又想要初始化这个自定义类型成员变量,还可以用初始化列表进行初始化;

需要注意的是,默认构造函数不仅仅是我们未显式实现该类的构造函数而编译器默认生成的构造函数,还包括无参构造函数、全缺省构造函数;

这三个构造函数的特点:不用传参便可以调用;

这三个函数不可以同时存在,当我们要自己显式实现默认构造函数的时候,就只能写成无参构造或者全缺省构造,无参构造或者全缺省构造不可以同时存在否则就会产生歧义(此处不再赘述原因),而当我们没有显式实现构造函数的时候,编译器才会默认生成构造函数;所以这三个默认构造函数一定不会同时存在在一个类中;

Q2:什么情况下没有默认构造?

  • 如果显式地实现了带参的构造函数,此时就没有无参、全缺省的构造函数,当然编译器也不会自动生成默认构造;

也就是说如果在类Stack 之中中显式实现带参的构造函数。那么在写MyQueue时(我们没有实现MyQueue的构造函数),MyQueue 中的成员函数有Stack,那么编译器初始化Stack类型的数据的时候回去找这个自定义类型数据的默认构造函数,但是Stack 只有我们实现的带参的构造函数,而没有默认构造函数,此时编译器会报错,代码如下:

typedef int STDataType;
class Stack
{
public:
	//显式实现的带参的构造函数
	Stack(int n)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}

		_capacity = n;
		_top = 0;
	}

private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

运行结果如下:

要是Stack 没有默认构造,那么编译器无法生成类MyQueue的默认构造函数,而就是想初始化MyQueue,此时该怎么办?

需要显式写,借助初始化列表来实现;如下:

此时便可以回答我们写在开头的问题:

Q:我们不写(显式实现)的时候,编译器默认生成的行为是什么,是否满足我们的要求?

  • 1、对于内置类型数据,编译器不作处理(不同编译器的行为不同,没有定义)
  • 2、对于自定义类型数据,编译器会自动调这个类的默认构造函数;

Q:什么情况下默认构造,我们不用自己显式实现,编译器自己默认生成的就够用?

  • 该类中的成员变量为自定义类型数据的时候(注:就现学阶段而言,往后学还有其他的情况,在学习了初始化列表和缺省值的概念之后)

Q:编译器默认生成的函数不满足我们的要求,我们需要自己实现,那么如何自己实现;

  • 掌握构造函数的特点;大部分的构造函数均是需要我们自己实现的;

三、析构函数

析构函数与构造函数的功能相反,析构函数并不是完成对对象本身的销毁,比如在局部创建对象时,该对象是存储在栈帧中的,函数栈帧结束栈帧销毁,这个对象所占用的空间自己就释放了,完全不用我们去管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。可以将析构函数的功能类比为Stack 中实现的Destroy的功能,而Date 没有Destory ,其实就是没有资源需要释放,所以严格来说Date 是不需要析构函数的;

析构函数的特点:

  • 1、析构函数名是在类名前加上字符 ~
  • 2、无参数无返回值(此处跟构造函数的要求一样,这是C++的规定)
  • 3、一个类只能有一个析构函数。倘若没有显式定义析构函数,系统会自动生成默认的析构函数
  • 4、对象生命周期结束的时候,系统会自动调用析构函数
  • 5、跟构造函数类似,我们显式实现析构函数,编译器就会自动生成的析构函数,对于内置类型成员变量不作处理,而对于自定义类型的成员变量会去调用这个自定义类的析构函数;
  • 6、我们显式实现了析构函数,对于自定义类型的成员变量也会去调用他自己的析构函数,也就是说自定义类型成员变量无论在什么情况下均会自动调用析构函数
  • 7、如果类中没有申请使用额外的资源的时候,可以不用写析构函数,直接使用编译器默认生成的析构函数就行了,如:Date(Date 中的成员变量均为内置类型的数据,并不涉及资源);如果默认生成的析构函数就可以用,我们可以不用显式实现析构函数,编译器自动默认生成的析构函数就足以完成我们所设想的工作,如:MyQueue(因为MyQueue 中的成员变量为自定义类型的数据,编译器会去自动调用自定义类型成员的析构函数);但是如果涉及资源,就一定要自己显式实现析构函数,否则就会造成资源泄露,例如:Stack (Stack 中存在在堆上开辟的空间);
  • 8、一个局部域的多个对象,C++ 规定后定义的先析构(函数栈帧);

栈帧中的变量又称为自动变量,栈帧中的变量开空间、释放空间均是编译器或指令的角度,系统直接就处理了,无需我们管;

C++ 规定了,对象在销毁的时候会自动调用析构函数;

Q1:日期类的析构函数该如何写?

  • 日期类对象存储在栈帧之中,其所在的函数结束时,该函数栈帧便销毁,此日期类对象也跟着销毁,所以日期类对象无需析构函数,因为它没有资源可以释放;

Q2:哪些类是需要析构函数的?

  • 当该类中的成员变量涉及资源的时候,例如:用malloc 在堆上开辟空间;栈、队列、链表等;即有资源不在栈中的成员变量的对象,我们均需要显式实现析构函数

Stack 的析构函数如下,需要释放在堆上开辟的空间:

构造函数和析构函数的其中一大优势:可以更好地替代之前我们写的Init、Destroy 函数,编译器会自动调用;自动调用的优势很大,可以避免忘记调用Init 、Destroy 而带来的错误;

日期类我们没有实现析构函数,但编译器会自动生成析构函数,也就意味着,对于日期类是有析构函数的,不过由于日期类中的成员变量全为内置类型,编译器自动生成的析构函数不对内置类型的数据做处理;而对于自定义类型的成员变量,当其对象的生命周期结束的时,会调用该自定义成员自己的析构函数,如下MyQueue:

调试:

其中需要注意的是,对于有自定义成员的类无论我们有没有显式实现其析构函数,编译器均会去自动调用该自定义成员的析构函数;从上例就可以显然地看出来,我们并未给给MyQueue 显式实现析构函数,但是它会去调用Stack 的析构函数来释放资源;

小结:日期类,我们要写构造函数,不用写析构函数;栈Stack 我们要写构造函数,也要写析构函数;队列MyQueue 不需要写构造函数,也不需要写析构函数;

Q3:比对C语言中没有类的情况下来解决括号匹配的问题:

C语言:

 

C++:

通过对比,有构造与析构函数与没有构造与析构函数的区别还是很大的;

四、拷贝构造函数

如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值(一个参数未自身类型的引用,其他参数均是缺省参数),则此构造函数也叫做拷贝构造函数,也就是说拷贝构造函数是一个特殊的构造函数;

拷贝构造函数的特点:

  • 1、拷贝构造函数是构造函数的一个重载
  • 2、拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器就会直接报错,因为语法逻辑上会引发无穷递归调用;拷贝构造函数可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值;
  • 3、C++规定自定义类型对象进行拷行为必须调用拷贝构造函数,所以自定义类型传值传参和传值返回均会调用拷贝构造函数完成;
  • 4、若未显式定义拷贝构造函数,编译器会自动生成拷贝构造函数。自动生成的拷贝构造函数对于内置类型成员变量会完成值拷贝(也叫做浅拷贝,一个字节一个字节地拷贝),对自定义类型成员变量会去调用他的拷贝构造函数;

若构造是初始化,那么拷贝构造就是拷贝初始化;

Date 中的拷贝构造如下:

#include<iostream>

using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	Date(const Date& d)//const 不是必须的,但是使用引用是必须的
	{
		//this 指针指向d2 对象,d 是对象d1 的引用
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2025, 5, 1);
	Date d2(d1);//用d1 初始化d2
	return 0;
}

拷贝构造函数与构造函数一样,编译器会自动调用,如下调试:

Q1:为什么形参为引用?

传参有三种方式:传值传参、传址传参、传引用传参(传引用传参本质上是传址传参,只不过在使用层面上,引用比地址更为方便使用)

C++规定传值传参会调用拷贝构造函数,如下例:

调用Func1 这个函数前,首先是要进行传值传参,因为Date 是自定义类型,所以会调用拷贝构造函数来实现传参,而调用拷贝构造的过程相当于将实参d1 中的值拷贝放入形参d 之中;

测试一下,我们在拷贝构造函数中写一条打印的语句,如下:

Date 类的实现如下:

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	Date(const Date& d)//const 不是必须的,但是使用引用是必须的
	{
		cout << "调用了拷贝构造函数" << endl;
		//this 指针指向d2 对象,d 是对象d1 的引用
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

运行结果如下:

图解如下:

调用Func1(d1) ,由于实参d1 为自定义类型对象,所以在调用Func1(d1) 前首先会调用拷贝构造函数将实参d1拷贝初始化给实参d .传值传参完成之后就会继续调用Func1,函数Func1 执行完成之后便会回到调用Func1 的语句上;

传值传参会调用拷贝构造,传值返回也会调用拷贝构造,如下:

Q2:为什么传值返回也会调用拷贝构造函数?

  • 因为C++规定传值返回并不是直接返回此处的变量 d ;如果要返回的对象为局部对象,那么出了作用域之后该对象便会销毁,如果直接返回这个变量本身,作为该函数调用的返回值是有问题的;故而在返回的时候,首先会将要返回的值放在临时对象之中,然后再返回临时对象,即用临时对象作为该函数的返回值;同理,也不能使用引用传参返回局部对象,因为出了该局部对象的作用域,该局部对象的空间就被释放了;

自定义类型的传值传参、传值返回均比内置类型要付出更大的代价(拷贝带来的消耗),因为内置类型均是一些系统自己定义的类型,它再指令级就可以完成拷贝;其对象均比较小,一般都是 1~8 byte;而自定义类型可能是一些很大的对象,其拷贝的问题牵涉很复杂(深、浅拷贝)。

Q3:如果拷贝构造采用传值传参而不使用引用传参会发生什么?

  • 如果编译器不进行强制性的检查,那么便会导致无穷递归;

当d1作为实参传值传参传递给形参d的时候,C++规定自定义类型对象传值传参、传值返回会调用拷贝构造函数,那么在实现拷贝构造的时候又调用拷贝构造去完成传值传参,显然一定是会导致无穷递归;当然,有的编译器会进行强制的检查来避免无穷递归的这个问题;

无穷递归示意图:

每次要调用拷贝构造函数之前要传值传参,传值传参是一种拷贝又形成了一个新得拷贝,就形成了无穷递归;

在清楚了这个问题的基础上我们自然也就理解了为什么拷贝构造函数必须要使用传引用传参,而非传值传参;

  • 这是因为传值传参(用一个对象去初始化另外一个同类型的对象)本身就是一种拷贝的行为,而由于自定义类型对象的拷贝是需要调用拷贝构造去完成的,所以自定义类型对象传值传参就会导致调用的拷贝构造又需要去调用拷贝构造……于是就形成了无穷递归;而引用传参并不需要才调用拷贝构造来完成,因为引用的特点在于语法层上:引用不开辟空间,引用是取别名;

注:

1、引用的底层是指针,上层是取别名,不要把底层和上层搞混了;

2、即使是在底层上进行分析也完全没有问题的,因为指针属于内置类型,传址传参无需调用拷贝构造来实现;

3、拷贝构造函数的第一个参数也可以用指针来实现,但是若用指针实现,那么这个函数就不叫拷贝构造函数了,而是构造函数,如下:

若写成这样,那么这个函数就不叫拷贝构造函数,而叫做构造函数;

这样写的缺点:

  • 1、利用指针,传参时需要取地址,麻烦,显然没有传引用传参使用起来更加方便;
  • 2、传值传参与传值返回并不会调用此版本的函数,因为编译器只会制动调用拷贝构造函数,而这个函数只是构造函数;

Q4:为什么拷贝构造函数的第一个参数要加上const ?

  • 加上const ,可以增强程序的健壮性;

因为拷贝构造函数仅仅只是为了完成拷贝,并没有其他的目的,为了避免修改原数据、使用错误(例如,将this 指向的对象与赋值对象写反了)就可以加上const 来修饰引用;

还有就是,如果想写一些比较逻辑,但是将判断相等的 == 写作了赋值 = ,如下图:

Date 类的实现代码如下:

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& d)
	{
		cout << "调用了拷贝构造函数" << endl;
		//此处想判断一下,但是将判断相等的 == 写作了赋值 = 
		if (d._year = _year)
		{
			//代码
		}
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}

	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

运行结果如下:

在上例中实现的Date 类构造函数的实现,将比较逻辑是否相等 的== 写作了赋值= , 就导致了错误;

d._year = _year ,那么此时就会改变形参d ,而形参又是实参d1 的别名,改变形参也会影响实参;初衷本来是为了让对象d1 初始化d2 ,结果还将d1 中的数据给修改了,显然这是非常离谱的;所以,为了增强程序的健壮性(为避免出错),一般情况下在不改变实参的函数中使用传引用传参,形参会用const 来修饰;

并且形参用const 修饰还有一个好处:实参为const , 非const 均可进行传参(权限只能平移、缩小,而不能放大); 

健壮性强的拷贝构造的实现如下:

当然,上面这个版本显式地用了this 指针,也可以省略,如下:

Q5:拷贝构造函数可以有多少个参数?除了引用是否还可以有其他参数?

  • 在拷贝构造函数中是可以有除了引用地其他的参数,但是规定:必须是缺省参数;

拷贝构造函数除了第一个参数是对应类的引用之外,其他参数必须是缺省参数,如果存在其他参数并且不是缺省参数,那么这个函数就不再是拷贝构造函数,而是构造函数,如下图:

Q6:当我们没有显式实现构造函数,编译器自动生成的构造函数的行为是什么?

  • 编译器自动生成的拷贝函数只进行浅拷贝;

对于不涉及资源的类:

编译器自动生成拷贝构造函数完成了其所要完成的工作;可见,编译器自动生成拷贝构造函数与其自动生成的构造函数是不一样的;可将编译器自动生成的构造、析构的特点看为一组;构造函数与析构函数统一不对内置类型做处理(或者说没有规定构造、析构函数要对内置类型数据进行处理,具体行为取决于编译器的实现,不同的编译器的行为可能不同),而在有自定义类型成员变量的类中的构造、析构会去调用该自定义类型的构造、析构;

编译器自动生成的拷贝构造函数的特点:对内置类型的成员完成值拷贝(浅拷贝),对自定义类型的成员变量而言会去调用它自己的拷贝构造函数;

Q:什么是值拷贝(浅拷贝)?

  • 类似于memcpy 的拷贝,一个字节一个字节地拷贝;

而正因为日期类的成员变量均为内置类型,所以自动生成的拷贝构造函数可以完成对应的值拷贝(浅拷贝);这样看来,对于内置类型、自定义类型,编译器自动生成的拷贝构造函数均可以处理,是不是就意味着所有类的拷贝构造函数我们均不用实现?

  • 只能说大部分的拷贝构造函数不用我们自己实现,依然有小部分的情况需要我们来实现;其核心问题是深浅拷贝的问题;

eg. C++实现栈,其构造、析构均需要我们自己显式实现,拷贝构造也是如此;

未显式实现拷贝构造函数:

类Stack 的(部分)实现代码如下:

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

	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity =  2 * _capacity;
			STDataType* tmp = (STDataType*)realloc(_a, sizeof(STDataType) * newcapacity);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				return;
			}

			_a = tmp;
			_capacity = newcapacity;
		}
		//插入数据
		_a[_top++] = x;
	}
	//析构
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	int _capacity;
	int _top;
};

测试代码:

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

	Stack st2(st1);

	return 0;
}

调试结果如下:

运行结果如下:运行异常

我们未实现Stack 的拷贝构造函数,Stack st2(st1); 则调用编译器自动生成的拷贝构造,st2 一个字节一个字节地拷贝st1 中的内容,从调试结果可看出,st1 与 st2 中_a 指向的空间相同;

当main 函数结束后,main函数的函数栈帧销毁了,即对象st1 st2 的生命周期结束了,此时编译器会自动调用st2 st1 的析构函数。然而此处st2 拷贝st1 是浅拷贝,那么st2 与 st1 指向了同一块空间;后实例化的先析构,所以当栈帧销毁的时候会先调用st2 的析构函数,st2 指向的空间释放了归还给了操作系统,而接下来还会调用st1 的析构函数 ,显然就会再次释放那块已经归还给操作系统的空间 --> 空间重复释放;

存在两个问题:

  • 1、此块内存会被析构两次(free 了两次)
  • 2、一个对象修改数据会影响另外一个

一个在堆上动态开辟的空间不可以free 两次;因为若将堆上动态开辟的空间释放,该空间就会归还给操作系统,而此时这块空间也有可能会被别人申请走,倘若此时又将其释放,就会导致别人那里出现了野指针;而即使未被别人申请走,也无法释放一块不属于当前程序的空间;

修改:凡是有一个成员变量指向“资源”类,要么不让拷贝(以后会学习的防拷贝),要么拷贝就做深拷贝(不仅仅是拷贝成员变量中的数据,如果该成员变量指向了资源,还会对指向的资源进行拷贝),如下图:

Stack 拷贝构造显式实现的代码:

	//拷贝构造 st2(st1)
	Stack(const Stack& st)//st 是st1 的别名,this 指向st2
	{
		//首先是开辟和st1 一样大的空间
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		//将st1._a 中的数据拷贝放入st2._a 中
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		//处理剩余两个成员变量
		_top = st._top;
		_capacity = st._capacity;
	}

带上显式实现的拷贝构造,在构造、拷贝构造、析构中添加打印语句,然后再次运行上例中的代码:

而对于MyQueue 来说,因为他的两个成员变量均为Stack类型,Stack 中实现了构造、拷贝构造、析构,所以MyQueue 什么都不用实现,编译器会自动调用Stack 中的,运行结果如下:

调试结果:

注:成员变量全为自定义类型的成员,我们无需实现拷贝构造,编译器自动生成的拷贝构造会自动去调用该自定义类中的拷贝构造函数;

在C++ 中设计拷贝构造其中最重要的一点是因为为了解决存在资源类的拷贝问题;

C语言中的结构体变量可以在函数中传参(传值传参),也可作为函数的返回,均是进行值拷贝(语言机制完成),C语言中并没有拷贝构造的概念;值拷贝也是C语言中的坑,如果该结构体所占用的内存很大,那么此处拷贝的消耗便会很大,倘若该结构还涉及资源(例如栈),坑就更大了,如下:

C语言中的结构体传参实际上就是值拷贝,同样也存在多次释放一块空间的可能性危险;

解决:专门写一个函数去完成深拷贝,以及各种传参处理……处理起来比较麻烦……

Q7:传引用返回与传值返回

  • 传值返回也会调用拷贝构造;

注:拷贝构造函数有两种写法:

Q:为什么会有这两种写法?

若被拷贝的数据来自一个函数的返回值,倘若使用方法一便会特别别扭,反而使用方法二会好一些,如下图:

函数Func 采用的是传值返回,变量st1 的类型为自定义类型,并且此类还涉及了资源,故而此处调用拷贝构造函数为自己实现深拷贝;

需要注意的是,不能使用传引用返回,因为对象st1 是一个局部对象,出了作用域是会被销毁的;如下例,使用传引用返回,得到的就是随机值:

Func的返回值为st1 的别名,而Func的返回值会用来初始化st2(深拷贝) ,而当Func 返回数据的时候就意味着Func函数栈帧销毁了,故而其中的数据为随机值;实则在深拷贝的时候就已经越界了,但是由于编译器是对越界进行抽查,此处未抽查到,故而没有报错;

五、赋值运算符重载函数

1)、运算符重载

运算符重载的特点:

  • 1、当运算符被用于类型的对象的时候,C++语言允许我们通过运算符重载的形式指定该运算符新的含义;C++规定自定义类型对象使用运算符的时候,必须转换成调用对应运算符重载函数,若没有对应运算符重载函数,则会编译报错;
  • 2、运算符重载是具有特殊名字的函数,它的名字是由operator 和后面要定义的运算符共同构成,和其他函数一样,运算符重载函数也具有其返回类型和参数列表以及函数体;
  • 3、运算符重载函数的参数个数与该运算符的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数;二元运算符的左侧运算对象传给运算符重载函数的第一个参数,右侧运算对象传给运算符重载函数的第二个参数;
  • 4、如果一个运算符重载函数是成员函数,则它的第一个运算对象默认传给隐式的this 指针,因此运算符重载函数作为成员函数,实际显式的参数比运算符对象少一个;
  • 5、运算符重载以后,其优先级和结合性对应的内置类型运算符保持一致;
  • 6、不能通过链接语法中没有的符号来创建性的操作符,例如:operator@
  • 7、.*    ::    sizeof    ?:     .  注意这五个运算符不可以重载。运算符重载函数至少会有一个参数(this 指针传递),即运算符至少要有一个运算对象,并且不能通过运算符重载改变内置类型对象的含义,例如: int operator+(int x , int y)
  • 8、一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date 类重载operator- 就有意义,但是重载operator+ 就没有意义;因为日期相减可以算出两个日期之间相差的天数,但是两个日期相加就没有什么意义了;
  • 9、重载 ++ 运算符的时候,分为前置 ++ 、后置++ ,运算符重载函数名都是operator++ ,就不太好区分这两个函数,于是C++规定,增加一个int 形参,让后置++ 与 前置++ 在参数个数上区分开来,与前置++ 的运算符重载函数构成函数重载;
  • 10、 重载 << 和 >> 时,需要重载为全局函数。因为当运算符重载函数作为成员函数的时候,this指针默认抢占第一个形参的位置,第一个形参的位置就是左侧运算对象(左操作数),即 原本想要的效果是cout<< 对象,结果变为:对象 << cout ,不符合使用习惯和可读性;重载为全局函数把ostream/istream 放到第一个形参位置就可以了,第二个形参位置为自定义类型的对象;

编译器在程序员和CPU之间本质是充当了翻译的功能;内置类型的对象可以直接使用各种操作符这是因为内置类型是语言本身自己定义的,是一些简单的类型,其计算均会直接转换成对应的指令,如下图:

代码以及运行结果:

转汇编:

但是自定义类型的数据的计算,对于CPU来说是非常复杂的,并不是转换成两三句指令便就可以搞定的,它需要很多条指令;如果想要实现+,编译器是不知道如何进行+,并且也不知道这样做究竟是否有意义,故而只有我们自己来实现逻辑, 然后编译器再编译,eg.日期相加没有意义;

相当于,我们要用一系列的逻辑(最好写成一个函数)来实现自定义类型数据的运算符逻辑,这个函数就是运算符重载函数;

Q1:运算符重载函数是如何规定的?

  • operator + 运算符;如果运算符重载函数为成员函数,其this 指针指向第一个参数,如果该运算符为二元运算符,第一个参数为左操作数(this 指向的),第二个参数为右操作数;

注:有些操作符(运算符)是不用区分左、右操作数的,但是有些操作数是需要区分的;

我们先在全局实现:

注:C++中引用能做的事,指针也能做,但是有些地方用引用比用指针方便,并且用引用的代码可读性更高;

修改:

当然,我们也可以显式调用运算符重载函数。如下:

更推荐让编译器自己去调用,代码的可读性更高;无论是编译器自己调用还是我们显式调用,本质上都是调用的同一个函数;

在全局实现的 == 重载函数:

报错的原因:因为这些变量均是Date 中私有的成员变量,在类外部无法访问;

核心:受访问限定符的限制;

注:私有的private ,在类外不可使用该类的对象直接访问私有中的成员变量、成员函数,但是在类中对象(并非专指调用的对象(this 指向的)),只要任何该类的对象在类中均可访问其成员变量;

解决:

  • 方法一:利用友元函数(下一篇博文中会讲解)
  • 方法二:间接访问(java 中喜欢用的方式)
  • 方法三:将该函数重载为类Date 的成员函数(公有)

至于方法二,就是在public 中实现能获取的私有成员变量的成员函数,调用该成员函数相当于就间接拿到了私有的成员变量;即换种形式将数据弄出来,如下:

方法三:

其中需要注意的是,在运算符重载的时候,有多少个操作数就有多少个参数,其中this 指针默认为第一个参数,如果有两个操作数的情况,this 指针指向左操作数;

Q2:关于不可进行运算符重载的五个运算符

  • .*    ::    sizeof    ?:     . 

.*  点星操作符,C++新增的运算符;

Q: 如何获取一个成员函数的指针?

注:函数指针:存放函数地址的指针;函数名为函数的地址,函数指针需要说清楚这个函数的参数类型、参数名可以省略以及该函数的返回类型;

函数指针与数组指针的特点:定义的变量名以及typedef 的类型名称是嵌套在其中的

当Func 为一个全局函数的时候:

函数Func 的函数指针pf:void(*pf) ()   -->  *与 pf 结合代表了pf 为指针,void 为该函数返回类型,() 之中放这个函数的参数,此时为无参;

那个函数Func 的函数指针的类型为: void (*) () 

typedef  为PF一下: typedef void(*PF)() ;

如若Func为一个成员函数:

由于成员函数有一个隐藏的this 指针,语法规定其函数前面要增加一个域作用限定符;

成员函数Func 的函数指针pf:void (A :: *pf) ()

成员函数Func 的函数指针类型 : void (A :: *)()

typedef  为PF一下: typedef void(A :: * PF)();

Q2.1:如何获得成员函数的函数指针?

  • 若直接将成员函数名Func作为其函数指针,编译器在编译的时候首先就会在全局进行查找,并不会到类域中进行查找;如果想要让编译器到类域中去找,此时需要利用域作用限定符指定一下;C++规定,普通的函数名就代表函数指针、成员函数不能直接用函数名代表成员函数指针,要在成员函数前面加 &;

Q2.2:利用函数指针调用函数,成员函数和普通函数一样吗?

为什么呢?

  • 成员函数跟全局的函数最大的不同:成员函数的参数列表中有一个隐藏的this 指针;而this 指针不可以显式传参,此时便会借助于操作符:点星操作符 .* 

正确使用成员函数指针如下:

   Q3:函数重载与运算符重载没有关联,不要搞混了

  • 函数重载:函数名相同但是函数参数个数、类型不同的函数构成函数重载;
  • 运算符重载:重载该运算符以后可以让自定义类型的数据也使用运算符;相当于写了一个函数去定义一个自定义类型用该运算符的动作行为;

两个函数名相同的运算符重载函数参数个数、类型不同,又可以构成函数重载;

注意区分这两个概念;eg.重载减法的时候,在日期类中,可以是日期-日期,也可以是日期-天数,如下:

注:运算符重载重新定义了此运算符的行为,但这两个函数又构成函数重载;

自定义类型的数据通过编译器暗中调用运算符重载达到使用运算符的好处:可读性强

运算符重载的优势:它是一种标准的、统一的,任何人均认识并且提高代码可读性的一种方式;

2)、赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象的直接的拷贝赋值,这里需要注意与拷贝构造进行区分,拷贝构造函数时用用于一个对象拷贝初始化构造另外一个对象;

赋值运算符重载函数的特点:

  • 1、赋值运算符重载函数是一个运算符重载函数,规定必须重载为成员函数。赋值运算符重载的参数建议写成const 当前类类型的引用,否则直接使用传值传参会有拷贝而产生消耗;
  • 2、有返回值,建议写成当前类类型引用,引用返回可以提高效率,赋值运算符重载函数有返回值的目的是为了支持连续赋值的场景;
  • 3、当我们没有显式实现赋值运算符重载函数的时候,编译器会自动生成一个默认赋值运算符重载函数,默认的赋值运算符重载函数的行为跟默认拷贝构造函数类似,对内置类型的成员变量完成值拷贝(浅拷贝,一个字节一个字节地拷贝),对于自定义类型的成员变量会去调用他的赋值重载函数;
  • 4、像Date 这样的类成员变量全是内置类型的成员变量且没有指向什么资源,编译器自动生成的赋值运算符重载函数就可以完成需要的拷贝工作,所以不需要我们显式实现赋值运算符重载函数;而像Stack 这样的类,虽然也都是内置类型,但是_a 指向了资源,编译器自动生成的赋值运算符重载函数完成的值拷贝(浅拷贝,一个字节一个字节的拷贝)不符合我们的预期,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue 这样的类,其成员变量均是自定义类型Stack , 编译器自动生成的赋值运算符重载函数会去调用Stack中的赋值运算符重载函数,也不需要我们显式实现MyQueue 的赋值运算符重载。这里还有一个小技巧,如果一个类的显式实现了析构并且释放了资源,那么也就意味着这个类需要显式实现赋值运算符重载函数,否则就不需要;

        注:注意区分拷贝构造与赋值运算符重载

Q1:如何实现赋值运算符重载函数?

  • 首先是传参问题;其参数可以使用传值传参,但是更推荐使用传引用传参(不想改变实参加上const 修饰形参就可以了),减少了拷贝次数提高效率;但是拷贝构造的参数必须使用传引用传参,因为倘若使用传值传参会陷入无穷递归;

实现出来:

测试:

实际上,我们还有连续赋值,再测试一下:

这里报错了,为什么呢?

首先,我们先来回忆一下连续赋值,连续赋值的结合性是从右往左,例如:

在这个例子中连续赋值体现为,10赋值给k ,而赋值表达式是有返回值的,赋值表达式 k=10 的返回值为该赋值运算符的左操作数,即返回k , 而k又会被赋值给 j……

所有的重载运算符函数均要给返回值才会支持该表达式具有赋值的属性;

同理,若想要赋值运算符重载函数支持连续赋值,那么赋值运算符重载函数就需要有返回值,实现如下:

没有使用传值返回,而是采用传引用返回的原因:

  • 传值返回会调用拷贝构造,即返回的不是*this本身,而是返回 *this 的拷贝(临时对象);如果该自定义类型的对象很大,那么传值返回拷贝的消耗就比较大,而此处 this 指针指向左操作数,即出了此赋值运算符重载函数,*this 也不会销毁,完全可以使用传引用返回;

还需要注意的是,虽然说引用的底层就是指针,但是此处不可以使用传址返回,如下:

d2 = d3 这个表达式的返回值要为Date 对象才能支持连续赋值,而现在返回值为地址,所以报错;可见,引用与指针的功能虽然相同,但是引用有着自己的不可替代性;

实际上,有时候也会出现自己给自己赋值的情况,我们可以再优化一下代码,当自己给自己赋值的时候,直接返回便可,参考代码如:

	//返回左操作数 , *this = d 
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}

赋值运算符重载函数和拷贝构造函数一样,绝大多数的情况下(内置类型并且不涉及资源、自定义类型的成员变量)是不需要我们显式实现赋值运算符重载的,编译器自动生成的就够用; 

当我们不显式实现赋值运算符重载,测试一下:

像日期类这种其成员变的类型均为内置类型并且不涉及资源的,编译器自动生成的赋值运算符重载函数就已经够用了,同样的,拷贝构造、析构也不用我们显式实现;

  • 小技巧:只要不需要我们显式实现析构函数,那么拷贝构造函数、赋值运算符重载函数也无需显式实现;
  • 核心:没有资源的释放那么也没有深拷贝的必要;

而像Stack 这种涉及了资源的类,就需要我们自己写构造函数(基本上均需要写)、拷贝构造函数、析构函数、赋值运算符重载函数;

我们未显式实现Stack 中的赋值运算符重载函数:

程序崩溃了,这是因为浅拷贝的原因;在上例中,st1 = st2 ; 进行的是浅拷贝,将st2 中给的数据一个字节一个字节地拷贝到st1 中,那么此时st1 与 st2 就指向同一块空间,就会导致同一块空间多次释放的问题;

调试:

需要我们显式实现赋值运算符重载函数;

如何实现?

  • 直接进行值拷贝(将st2 中的数据拷贝放入st1 之中),然后其他内置类型的数据进行赋值处理;这是不行的,能这样操作的前提是:st1 与 st2 的空间一样大;不然倘若st2 的空间大于st1 中的空间,那么st2 中的数据放入st1 之中就有可能放不下;而若st2 的空间比st1 的空间小,那么将st2 中的数据放入st1 之中有可能会浪费空间;

我们可以灵活处理,比较这两个空间是否一样大,能否直接拷贝、空间浪费地多不多……此处采用更加出粗暴的方式:先将st1 中的空间释放,然后根据st2 空间的大小给st1 开辟空间,再将st2 中的数据拷贝放入st1新开辟的空间之中,过程如下图:

代码如下:

调试一下:

注:对于需要深拷贝的类,其拷贝构造函数和赋值运算符重载函数的代价都很大,不到万不得已的场景,深拷贝的类不去拷贝;

当然,同样还存在自己给自己赋值的情况;

在深拷贝下,自己给自己赋值是存在bug 的,因为我们此处实现的逻辑是先释放空间、开辟新空间、拷贝数据;此时如果自己给自己赋值,就会给自己的空间释放掉并将其中的内容设置为随机值(free 会将该空间归还给操作系统,并将该空间的数据设置为随机值),执行拷贝的逻辑的时候会出现越界访问;

特殊处理一下自己给自己赋值的情况,Stack 的赋值赋值运算符重载函数的参考代码如下:

	//赋值运算符重载
	// st1 = st2 --> *this = st
	Stack& operator=(const Stack& st)
	{
		if (this != &st)
		{
			//释放旧空间
			free(_a);
			//开辟新空间
			_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
			if (_a == nullptr)
			{
				perror("malloc fail");
				return *this;
			}
			//拷贝数据
			memcpy(_a, st._a, sizeof(STDataType) * st._top);
			_capacity = st._capacity;
			_top = st._top;
		}

		return *this;
	}

赋值运算符重载函数的默认特性可以结合拷贝构造来学习;最后,注意区分拷贝构造函数与赋值运算符重载函数!

  • 拷贝构造函数:用于一个对象拷贝初始化构造另一个同类型的对象;
  • 赋值运算符重载函数:用于完成两个已经存在的对象进行直接的赋值;

总结

1、当我们不显式实现这个函数的时候编译器会默认帮我们生成,这个函数就是默认成员函数,默认成员函数有6个,在C++11 中还会增加2个;但是编译器帮我们实现的默认成员函数可能并不会符合我们的预期,所以此时就需要我们自己来实现这个默认成员函数;在这个过程中,我们需要理解编译器的默认生成的行为、以及我们如何实现;

2、构造函数是特殊的成员函数,在对象实例化时去初始化对象;

3、析构函数并不是完成对对象本身的销毁,对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。

4、如果一个构造函数的第一个参数是自身类型的引用,且任何额外的参数都有默认值(一个参数未自身类型的引用,其他参数均是缺省参数),则此构造函数也叫做拷贝构造函数,也就是说拷贝构造函数是一个特殊的构造函数;

5、自定义类型对象使用运算符的时候,必须转换成调用对应运算符重载函数,若没有对应运算符重载函数,则会编译报错;

6、赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象的直接的拷贝赋值,这里需要注意与拷贝构造进行区分,拷贝构造函数时用用于一个对象拷贝初始化构造另外一个对象;

免费替代Cursor!TRAE中文版这些隐藏功能90%的人不知道 本文深度评测了免费AI编程工具TRAE中文版,揭示其超越代码补全的隐藏功能。重点介绍了其SOLO模式如何重塑从需求到部署的全流程开发,以及对微信小程序、Taro、Ant Design Pro等国产技术栈的深度适配能力,为中文开发者提供了高度本地化的高效解决方案。 阅读详情

相关推荐

基于stm32的数控恒压恒流电源设计方案

基于stm32的数控恒压恒流电源设计方案 基于stm32的数控恒压恒流电源设计方案 不包实物,可自己动手制作,可代做 本设计采用220V市电输入工频变压器,将220V交流电压降为24V交流电压,经过全桥整流加电容滤波,输出约32V直流电压.可调输出电压方案采用线性稳压调整器方案,即运放处于比较调整状态,运放输出驱动P型MOS管,电压输出端由电阻分压反馈至运放同相输入端,运放反相输入端由STM32单片机控制TLC5615数模转换器输入模拟电压,根据运放构成比较器原理,运放的同相端和反相端始终趋向于电压相等的特

m0_72265581的博客 7222

【c++】类的6个默认成员函数之赋值运算符重载

本文将带你详细理解赋值运算符重载,帮助小白快速学习理解

2502_90570010的博客 1009

从d、q轴电流到电机性能:FOC控制中角度偏移的实战影响与调试策略

本文深入探讨了永磁同步电机FOC控制中d、q轴电流与角度偏移对电机性能的影响及调试策略。通过分析角度偏移导致的效率下降、电流异常等问题,提供了实用的四步调试法和参数优化技巧,帮助工程师有效提升电机控制精度和效率。

weixin_29264201的博客 294

类和对象(中)【类的6个默认成员函数】 【零散知识点】 (万字)

有关【类的6个默认成员函数】的学习: 【构造函数】【析构函数】【拷贝构造函数】【赋值运算符重载】【取地址以及const取地址操作符重载】以及【运算符重载】 【const成员】【完整日期类的实现】

aaa114514aaaa的博客 2315

Boost电路双闭环控制MATLAB仿真

通过电流内环控制,我们可以改变目标平均电流基准值的大小来闭环改变占空比,再改变输出电压与电路电流,直到电路电流达到基准电流才达到稳态。但是我们的基准电流依旧是外部提供的,下边的电压外环控制就是电路自己计算基准电流。

Fourglsl的博客 5847

PD虚拟机网络初始化失败的权限修复指南

本文针对Parallels Desktop虚拟机常见的“网络初始化失败”问题,提供了详细的权限修复指南。通过分析问题根源,指导用户安全备份并修改关键的`network.desktop.xml`和`dispatcher.desktop.xml`配置文件,以恢复PD虚拟机的网络连接。文章还包含了修复后的验证步骤及预防措施,帮助用户彻底解决并避免此类权限问题。

qqq44的博客 851

C++学习专栏专栏收录该内容5 篇文章订阅专栏#新星杯·14天创作挑战营·第11期# 前引:在C++的面向对象编程中,对象模型是理解语言行为的核心。无论是类的成员函数如何访问

c++类与对象上篇 这是阿鲤对类与对象理解的部分总结,若有请慷慨指出 一:类的6个默认成员 二:构造函数 三:析构函数 四:拷贝构造函数 五:赋值运算符重载 六:const成员函数 七:取地址及const取地址及操作符重载 一:类的6个默认成员 如果一个类中什么成员都没有,简称为空类。并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。本期我们将继续类和对象的学习,重点讨论C++类中的成员函数,例如构造函数,析构函数,拷贝构造函数,运算符重载函数,const成员函数等等。

2501_92129429的博客 1706

ffmpeg中av_read_frame阻塞的原因与解决方案

最近用到ffmpeg,想要av_read_frame立即返回,但是看网上的博客,都没有详细的解说,那就自己看源码。 两种方法: 设置回调打断函数和设置超时 超时,但是如果只设置该参数,依然会在av_read_frame中阻塞, av_dict_set(&inputDic, "stimeout", "2*1000*1000", 0); // milliseconds //打断回调 m_avformatCtx->interrupt_callback.callback = interru

weixin_43360707的博客 1万+

C++中类,this指针,构造函数,析构函数。拷贝构造函数,初步理解运算符重载,初步理解赋值运算符重载

本文介绍了C++中类和对象的核心概念,主要内容包括: 类的定义格式及访问修饰限定符(public/private/protected)的作用; this指针的原理及其在成员函数调用中的作用; 构造函数的特性(函数名与类名相同、无返回值、自动调用等)及其重载规则; 析构函数的功能(资源清理)和调用时机; 拷贝构造函数的定义方式、参数要求(必须为引用)和自动生成的规则。 文章通过代码示例展示了这些概念的实际应用,并解释了相关语法规则和底层原理,帮助理解C++面向对象编程的基本机制。

m0_63112274的博客 1199

FPGA图像处理之直方图均衡实现

直方图均衡的基本原理,就是对在图像中像素个数多的灰度值(即对画面起主要作用的灰度值)进行拉伸,而对像素个数较少的灰度值(即对画面不起主要作用的灰度值)进行合并,从而提高对比度,使图像清晰,达到增强的目的。综合以上两个条件,累积分布函数是个好的选择,因为累积分布函数是单调增函数(控制大小关系),并且值域是0到1(控制越界问题),所以直方图均衡化中使用的是累积分布函数。

FZWzsl的博客 1428

CMake教程:C++ CMake自定义find_package并在其它项目使用

find_package 是 CMake 中一个非常重要的命令,用于在系统中找到外部库或包,并将其包含到项目中。: 要查找的包的名称。[version]: 可选的版本号。如果指定了版本号,CMake 会尝试找到该版本或更高版本的包。[REQUIRED]: 如果指定了这个关键字,找不到包时会产生一个错误并终止配置过程。: 指定要查找的包的组件。: 指定可选的组件,即使找不到这些组件,配置过程也会继续。: 指定查找包的模式,CONFIG表示查找配置文件,MODULE表示查找模块文件。

琅嬛福地 1563

椋鸟C++笔记#3:类的默认成员函数

萌新的学习笔记,写错了恳请斧正。

椋鸟的博客小窝 1005

【教程4>第8章>第44节】本章整体思维导图与学习总结

本教程系列提供FPGA/MATLAB/Simulink学习资源,包含《MATLAB入门100例》、《FPGA入门100例》、《Simulink入门60例》等基础教程,以及《FPGA/MATLAB/Simulink联合开发》进阶教程和《人工智能基础应用研究》专题,适合不同层次的学习者系统掌握相关技术。

FPGA/MATLAB学习教程/源码/项目合作开发 449

C++编程之旅-- -- --默认成员函数(全详解)

如果一个类是没什么也没有,就叫做空类。空类并非什么也没有,它占一个字节。编译器会自动生成6个默认成员函数。默认成员函数:指没有显式实现(自己写),编译器生成的成员函数。

2403_87827685的博客 1007

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

类和对象(中):对象生命周期与运算符重载

本文介绍了C++类中的默认成员函数,重点关注构造函数和析构函数。构造函数在对象创建时自动调用,用于初始化成员变量,具有无返回值、可重载等特点,推荐使用全缺省构造函数。析构函数在对象生命周期结束时自动调用,用于资源清理,类名加~作为标识。编译器默认生成的构造函数和析构函数对内置类型不做处理,但对自定义类型会调用其默认构造/析构函数。大多数情况下需要手动实现这些函数,特殊场景下编译器生成的默认函数可能够用。

Stellen的博客 527

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

【Cpp】类和对象#拷贝构造 赋值重载

Cpp,类和对象,拷贝构造,赋值重载,浅拷贝与深拷贝

石墨博客 930

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

C++类中的拷贝构造函数

文章目录前言二、拷贝构造函数1.拷贝构造函数定义2.默认拷贝构造函数存在的问题3.解决方式三 总结四 参考 前言 最近项目的技术栈是C++,涉及到了C++中类的相关知识,其中对于类中的拷贝构造函数、移动赋值运算符、移动构造函数和移动赋值运算符等函数有点混淆,在此记录下。 # 一、C++中的基本构造函数 C++是面向对象的语言,涉及到类,一涉及到类肯定就有成员变量和程序员函数等概念。成员变量就是这个类中相关的属性,比如以人为例,身高和体重就可以作为人这个类的成员变量。 我们在实际使用类的过程中,需要在对.

yuerenzirao的博客 1941

C++ -- 类和对象【中】

本文详细解读了类的6个默认成员函数,包括构造、析构、拷贝构造、赋值重载、取地址重载

2301_80059080的博客 630

详解C++类与对象(三)

1. 拷贝构造函数和赋值运算符重载。

2302_80836956的博客 797

【C++入门】运算符重载详解

(1)运算符:运算符分为算术运算符(+、-、*、/)、关系运算符(=、!=、==)、逻辑运算符(&&、!)等;(2)运算符重载:给运算符赋予新的含义,可以根据运算符操作的对象进行恰当的操作;(3)不是所有的运算符都支持运算符重载,有的运算符的含义在任何情况下都是确定的,所以不需要重载;

weixin_42031299的博客 3307

[C++]---类和对象(二){一.构造函数 二.析构函数  三.拷贝构造函数 四.赋值运算符重载 五.const成员  六.取地址及const取地址操作符重载}

目录 一.构造函数 作用: 特点: 注意: 二.析构函数 特点: 三.拷贝构造函数 特点: 四.赋值运算符重载 定义方法 特点: 以赋值运算符为例: 五.const成员 定义: 关于const几个问题 六.取地址及const取地址操作符重载 一般来说一个最简单的空类会有最基本的六大成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载...

夜微雨的博客 254

c++中class类中的四大常用默认函数

万字长文帮你搞懂类的四大常用默认函数,在c++中,有四个常用的默认函数以及两个不常用的。 本文主要介绍这四种函数:构造函数、析构函数、拷贝构造、赋值重载。 实现四种函数的注意事项、编译器默认生成与手动生成的差异,以及手动实现需要注意的坑。 介绍了浅拷贝与深拷贝的区别以及如何实现深拷贝。

none 2026

c++中delete对象后 调用成员函数_C++ 构造函数&运算符重载

C++ 构造函数&运算符重载构造函数:创建对象时调用的类成员函数 构造函数可以重载 默认构造函数:无参数构造函数 名称和类名相同 有参构造函数:名称和类名相同 参数列表的参数表示的不是类成员 而是要赋给类成员的值 拷贝(复制)构造函数:一种特殊的构造函数 函数名称和类名称相同 拷贝构造函数的参数列表里必须有一个参数是本类型的一个引用变量 析构函数:删除对象时调用的类成员函数 没有参数 在程...

weixin_39728909的博客 276

C++类和对象(四)---六个默认的成员函数

1.类中六个成员函数 内容 class Test { }; 如上代码所示,如果一个类中什么都没有,则称之为空类,空类中真的什么都没有吗,答案当然是否,那这个空类会生成哪些类内成员? 一般情况下,在类中会生成六个默认的成员函数,分别为构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,&运算符重载函数,const修饰的&运算符重载函数。 下面进行详细讲解???? 1.构造函数 构造函数...

董皓宇 219

C++----类和对象(二)

所以这里自定义类型。

woyaochengweiche的博客 664

C++学习笔记基础篇25——拷贝构造函数&析构函数冲突问题:重载函数&重载命令符

拷贝构造函数 依然使用Student作为例子展开,其中CStudent(CStudent& stud) 就是拷贝构造函数,可以实现在使用类去初始话一个对象时,用同一个类的另外一个对象初始化。 class CStudent { public: char *p_name; char sex; int num; int age; CStudent(){}; CStudent(char* pname, char t_sex, int t_num, int t_age) :sex(t_sex),

weixin_44567900的博客 658
上一篇: C++ - 类和对象 # 类的定义 #访问限定符 #类域 #实例化 #this 指针 #C++ 与 C语言的比较
下一篇: C++ - 类和对象 #日期类的实现
秦少游在淮海
博客等级 码龄3年 1071粉丝 88原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值