二、类与对象(二)

类与对象(二) 类与对象的定义与访问、面向对象的方法支持继承、多态机制、定义类和对象 阅读详情

8 this指针

8.1 this指针的引入

我们先来定义一个日期的类Date:

	#include <iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;     // 年
	int _month;    // 月
	int _day;      // 日
};
int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

对于上面一个类,有这样一个问题:

Date类中有Init与Print两个成员函数,函数体中并没有关于不同对象的区分,那么当d1调用Init函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象的呢?

C++通过引入this指针来解决这个问题。实际上,C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

8.2 this指针的特性

  1. this指针的类型:类的类型 const*,所以成员函数中,不能给this指针赋值。
  2. this指针只能在成员函数的内部使用。
  3. this指针本质上是成员函数的形参,所以this指针是存储在栈中的。当对象调用成员函数时,函数将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. this指针

例1:下面程序编译运行的结果是什么?

#include <iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
void test1()
{
	A* p = nullptr;//空指针
	p->Print();
}
void test2()
{
	A* p = nullptr;//空指针
	(*p).Print();
}
int main()
{
	test1();
	test2();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,程序正常运行了,这是为什么呢?

这是因为成员函数Print实际上在公共的代码段而并不在对象里面,所以虽然p是一个空指针,但p->Print()在这里并不代表解引用,而是直接去公共区域调用了函数Print,(*p).Print()也同理。

如果是这样的话,那能不能不用对象直接调用Print函数呢?

#include <iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	Print();
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,编译器报错了。这是因为Print会受到类域的限制,如果不用对象直接调用Print函数那么编译器将无法找到Print函数。

例2:下面程序编译运行的结果是什么?

#include <iostream>
using namespace std;
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,程序崩溃了。这是因为PrintA函数体内部的cout << _a << endl语句等价于cout << this->_a << endl而此时PrintA函数的参数为空指针,那么对空指针进行解引用自然就会发生崩溃了。

9 C语言和C++实现Stack的对比

9.1 C语言实现

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int DataType;
typedef struct Stack
{
	DataType* array;
	int capacity;
	int size;
}Stack;

void StackInit(Stack* ps)
{
	assert(ps);
	ps->array = (DataType*)malloc(sizeof(DataType) * 3);
	if (NULL == ps->array)
	{
		assert(0);
		return;
	}
	ps->capacity = 3;
	ps->size = 0;
}

void StackDestroy(Stack* ps)
{
	assert(ps);
	if (ps->array)
	{
		free(ps->array);
		ps->array = NULL;
		ps->capacity = 0;
		ps->size = 0;
	}
}

void CheckCapacity(Stack* ps)
{
	if (ps->size == ps->capacity)
	{
		int newcapacity = ps->capacity * 2;
		DataType* temp = (DataType*)realloc(ps->array,
			newcapacity * sizeof(DataType));
		if (temp == NULL)
		{
			perror("realloc申请空间失败!!!");
			return;
		}
		ps->array = temp;
		ps->capacity = newcapacity;
	}
}

void StackPush(Stack* ps, DataType data)
{
	assert(ps);
	CheckCapacity(ps);
	ps->array[ps->size] = data;
	ps->size++;
}

int StackEmpty(Stack* ps)
{
	assert(ps);
	return 0 == ps->size;
}

void StackPop(Stack* ps)
{
	if (StackEmpty(ps))
		return;
	ps->size--;
}

DataType StackTop(Stack* ps)
{
	assert(!StackEmpty(ps));
	return ps->array[ps->size - 1];
}

int StackSize(Stack* ps)
{
	assert(ps);
	return ps->size;
}

int main()
{
	Stack s;
	StackInit(&s);
	StackPush(&s, 1);
	StackPush(&s, 2);
	StackPush(&s, 3);
	StackPush(&s, 4);
	printf("%d\n", StackTop(&s));
	printf("%d\n", StackSize(&s));
	StackPop(&s);
	StackPop(&s);
	printf("%d\n", StackTop(&s));
	printf("%d\n", StackSize(&s));
	StackDestroy(&s);
	return 0;
}

可以看到,在用C语言实现Stack时,Stack相关操作函数有以下共性:

  1. 每个函数的第一个参数都是Stack*。
  2. 函数中必须要对第一个参数检测,因为该参数可能会为NULL。
  3. 函数中都是通过Stack*参数操作栈的.
  4. 调用时必须传递Stack结构体变量的地址。

结论:C语言中结构体只能定义存放数据的结构,而操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相对复杂,涉及到大量指针操作,稍不注意可能就会出错。

9.2 C++实现

#include <iostream>
#include <stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
public:
	void Init()
	{
		_array = (DataType*)malloc(sizeof(DataType) * 3);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = 3;
		_size = 0;
	}
	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}
	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }
	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity *
				sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);

	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Pop();
	s.Pop();
	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Destroy();
	return 0;
}

在C++中,通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装。在使用时就像使用自己的成员一样,更符合人对一件事物的认知。 而且和C语言相比,每个方法不需要传递Stack*的参数,编译器在编译之后会将该参数自动还原,即C++中Stack*参数是编译器维护的,C语言中需要用户自己维护。

10 类的默认成员函数

之前我们说过,如果一个类中什么成员都没有,简称为空类。任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数指的就是用户没有显式实现,但是编译器会生成的成员函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11 构造函数

11.1 构造函数的概念

我们以下面一个描述日期的类Date为例:

#include <iostream>
using namespace std;
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Init(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

对于Date类,可以通过公有方法Init给对象设置日期,但如果每次创建对象时都调用该方法设置信息,还是有点麻烦。那能否在对象创建时,就将信息设置进去呢? C++中,引入了构造函数来解决这个问题。

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开辟空间创建对象,而是初始化对象。

11.2 构造函数的特性

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载,也就是说构造函数允许对象有多种初始化的方式。

例:

#include <iostream>
using namespace std;
class Date
{
public:
    // 1.无参构造函数
    Date()
    {}

    // 2.带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

int main()
{
    Date d1; // 调用无参构造函数
    Date d2(2015, 1, 1); // 调用带参的构造函数
    Date d3();

    d1.Print();
    d2.Print();
    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    //d3.Print(); // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

例:

#include <iostream>
using namespace std;
class Date
{
public:
    
    // 如果用户显式定义了构造函数,编译器将不再生成
   /* Date(int year, int month, int day)
    {
    _year = year;
    _month = month;
    _day = day;
    }*/
    

    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

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

int main()
{
    // 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
    // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
    // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
    Date d1;
    d1.Print();
    return 0;
}

放开前运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

放开后运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 由于C++把类型分成内置类型(如:int/char等)和自定义类型(如使用class/struct/union等自己定义的类型),而C++的语法又规定编译器生成的默认构造函数不会对内置类型进行处理,也就是说对于内置类型的成员,虽然调用了默认构造函数但是依旧是随机值,而对于自定义类型的成员则会去调用它的默认构造函数。

注意:不传参数就可以调用的构造函数就叫默认构造函数,一般建议每个类都提供一个默认构造函数。

例:

#include <iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
	void Print()
	{
		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日";
		this->_t.Print();
	}
	/*Date()
	{
		cout << "Date()" << endl;
	}*/
private:
	//基本类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

注意:C++11中针对内置类型成员不初始化的缺陷打了补丁,打了补丁后内置类型成员变量在类中声明时可以给默认值。

例:

#define _CRT_SECURE_NO_WARNINGS	1
//构造函数缺陷
#include <iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
	void Print()
	{
		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日";
		this->_t.Print();
	}
	/*Date()
	{
		cout << "Date()" << endl;
	}*/
private:
	//基本类型型(内置类型)
	int _year = 2023;
	int _month = 10;
	int _day = 3;
	//自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 也就是说,如果既写了无参构造函数又写了全缺省的构造函数,那么编译的时候编译器会报错。

例:

#include <iostream>
using namespace std;
class Date
{
public:
    //无参的构造函数
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
    //全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date d1;
}
int main()
{
	Test();
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

12 析构函数

12.1 析构函数的概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

12.2 析构函数的特性

  1. 析构函数名是在类名前加上字符~。
  2. 无参数无返回值。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,也就是说,析构函数不能重载。
  4. 对象生命周期结束时,C++编译系统会自动调用析构函数。

例:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}
int main()
{
	TestStack();
	return 0;
}
  1. 由于内置类型成员的销毁不需要资源清理,是最后由系统直接将其内存回收,所以不需要调用析构函数;而对于自定义类型的成员则需要调用它的析构函数,不过这个自定义类型成员的析构函数不能被直接调用,而是由包含这个自定义类型成员的类的析构函数调用。换言之,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数即可;而有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

例:

#include <iostream>
using namespace std;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	//基本类型型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,在main函数中根本没有直接创建Time类的对象,但是最后还是调用了Time类的析构函数,这就是因为main函数中创建了Date类对象d,而d中包含了4个成员变量,其中_year、_month, _day三个是内置类型成员,销毁时不需要资源清理,而_t是Time类对象,所以在销毁d时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,所以编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,也就是说当Date的对象销毁时,要保证其内部每个自定义对象都能被正确销毁。
总结:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数。

13 拷贝构造函数

13.1 拷贝构造函数的概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。

而在C++中,拷贝构造函数就可以实现创建一个与已存在对象一模一样的新对象。

13.2 拷贝构造函数的特性

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数只有单个形参,该形参只能是对本类类型对象的引用(一般常用const修饰),而且在用已存在的类类型对象创建新对象时由编译器自动调用,如果使用传值方式进行传参那么编译器会直接报错,因为C++规定自定义类型的传值需要去调用拷贝构造函数,也就是在使用传值方式进行传参的过程中会调用拷贝构造函数,而由于这个拷贝构造函数是以传值方式实现的受C++语法的限制会又调用拷贝构造函数,层层调用最终导致无穷递归调用。

例:

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date(const Date d)       //错误写法:编译报错,会引发无穷递归
    //{
    //    _year = d._year;
    //    _month = d._month;
    //    _day = d._day;
    //   cout << "Date(const Date d)" << endl;
    //}
    Date(const Date& d)      // 正确写法
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
        cout << "Date(const Date& d)" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    Date d2(d1);
    return 0;
}

错误写法运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原因图解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正确写法运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果没有显式定义拷贝构造函数,那么编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。其中内置类型按照字节方式直接拷贝,而自定义类型则调用其拷贝构造函数完成拷贝。

例:

#include <iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
	void Print()
	{
		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日";
		this->_t.Print();
	}
private:
	//基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Time _t;
};

int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	d2.Print();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝,那么对于所有的类是不是都不需要自己来显式实现呢?我们可以通过下面的类来感受一下:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们以同样的方式对Stack类的对象s2进行拷贝构造时,程序崩溃了,这是什么原因呢?

![![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=C%3A%5CUsers%5CHackerKevin%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-](https://img-blog.csdnimg.cn/64088cfcaf1a48e4bbae1b869341bd97.png)
20231003203650522.png&pos_id=img-LibCRJDs-1700659738351)

我们可以通过上图帮助我们理解崩溃的原因。在main函数中,s1对象通过调用构造函数创建,而在构造函数中,默认申请了10个元素的空间,然后将1、2、3、4存了进去。

在后续构造s2对象的过程中,由于s2对象使用s1拷贝构造,而Stack类没有显式定义拷贝构造函数,所以编译器会给Stack类生成一份默认的拷贝构造函数,而又因为默认拷贝构造函数是按照值进行拷贝的,也就是说默认拷贝构造函数会将s1中的内容原封不动地拷贝到s2中,所以s1和s2指向了同一块内存空间。

当程序退出时,s2和s1都要销毁。而根据析构“后进先出”(即后创建的先销毁)的原则,s2将先被销毁,此时s2销毁时调用析构函数已经将0x11223344的空间释放了,但是s1中仍然指向0x11223344这块空间,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,必然会造成程序崩溃。

结论:类中一旦涉及到资源申请时,一定要写拷贝构造函数,否则就是浅拷贝;而类中没有涉及资源申请时,写还是不写拷贝构造函数都可以。

  1. 拷贝构造函数典型调用场景:
    1. 使用已存在对象创建新对象
    2. 函数参数类型为类类型对象
    3. 函数返回值类型为类类型对象

例:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year, int minute, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

程序解读:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结:为了提高程序效率,一般对象传参时,尽量使用引用类型;返回时根据实际场景,能用引用尽量使用引用。

14 赋值运算符重载

14.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,让自定义类型对象也可以使用运算符。

运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名为:operator + 需要重载的运算符符号

函数原型:返回值类型 + operator +(参数列表)

注意:

  1. 不能通过连接其他符号来创建新的操作符,比如operator@。
  2. 重载操作符必须有一个类类型参数。
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
  4. 作为类成员函数重载时,其形参看起来比实际操作数数目少1,但是成员函数里还隐藏了一个this参数。
  5. 特别注意:.* 、::、sizeof、 ?:、.这5个运算符不能重载,这个经常在笔试选择题中出现。

例:用全局的operator==实现判断Date类相等:

//全局的operator==
#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
//private:
    int _year;
    int _month;
    int _day;
};
bool operator==(const Date& d1, const Date& d2)//第一个参数为左操作符,第二个参数为右操作符
{
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;
}
void Test()
{
    Date d1(2023, 9, 27);
    Date d2(2023, 9, 27);
    Date d3(2023, 9, 27);
    Date d4(2023, 9, 26);
    cout << (d1 == d2) << endl;//d1 == d2会被转换成operator==(d1,d2)
    cout << (d3 == d4) << endl;
}

int main()
{
    Test();
	return 0;
}

成员变量为私有时运行结果:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

成员变量为公有时运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里会发现运算符重载成全局的就需要成员变量是公有的,但如果这样的话封装性就无法得到保证了。这里其实可以用我们后面学习的友元解决,或者干脆重载为成员函数。

例:

//重载为成员函数
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,成员函数都有一个默认的隐藏参数,即左操作数是this,指向调用函数的对象
    bool operator==(const Date & d2)
    {
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2023, 9, 27);
    Date d2(2023, 9, 27);
    Date d3(2023, 9, 27);
    Date d4(2023, 9, 26);
    cout << (d1 == d2) << endl;
    cout << (d3 == d4) << endl;
}
int main()
{
    Test();
    return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

14.1.1 运算符重载的复用

刚才我们实现了判断Date类相等的函数operator==,那当我们还想实现诸如operator>、operator<、operator>=这样逻辑相似的函数时,如果每一个函数都要单独写一段代码进行实现,那未免也太麻烦了,有没有什么简化的方法呢?

这里我们就可以通过对运算符重载的复用来实现,还是以Date类为例,要实现所有的比较关系的话,我们实际上只需在实现operator==的基础上,再实现一个operator<或者operator>即可:

	bool operator==(const Date & d2)
	{
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }
    bool operator<(const Date& d)
    {
        return _year < d._year
            || (_year == d._year && _month < d._month)
            || (_year == d._year && _month == d._month && _day < d._day);  
    }
    bool operator<=(const Date& d)
    {
        return *this < d || *this == d;
    }
    bool operator>(const Date& d)
    {
        return !(*this <= d);
    }
    bool operator>=(const Date& d)
    {
        return !(*this < d);
    }
    bool operator!=(const Date& d)
    {
        return !(*this == d);
    }

可以看到,上面的代码只具体实现了operator==和operator<,其他的关系直接通过这两个函数的复用就实现了,以operator>为例,operator>就是通过复用operator<=,然后对它的判断结果进行取反来进行实现的。

实际上,上面这一套判断逻辑,对所有的类均适用。

14.2 赋值运算符重载

以往赋值运算符=只能在内置类型之间使用,而如果要让自定义类型也能通过=进行赋值,就需要对赋值运算符进行重载。

有了刚才实现运算符重载的经验,那我们实现赋值运算符的重载实际上也没有什么难度。

例:

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void operator=(const Date& d)//赋值运算符重载
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2023, 9, 27);
    Date d2;
    d1.Print();
    d2.Print();
    d2 = d1;
    d1.Print();
    d2.Print();
}
int main()
{
    Test();
    return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,我们设计的赋值运算符重载实现了它的功能,但实际上当前设计的还是存在缺陷的,比较突出的一点就是它不支持连续赋值,因为它的返回类型是void。

要实现连续赋值,那么它应该返回当前被赋值的对象,也就是返回左操作数的值。除此之外,我们还应该考虑到自己给自己赋值的情况,尤其在需要深拷贝时,会降低程序运行的效率,所以遇到这种情况时,我们直接返回即可。那么对于刚才的operator=函数我们可以进行如下改造:

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
   /* void operator=(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }*/
    bool operator==(const Date& d2)
    {
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }
    bool operator!=(const Date& d)
    {
        return !(*this == d);
    }
    Date& operator=(const Date& d)//支持连续赋值的重载赋值运算符
    {
        if (this != &d)//地址不一样时才赋值
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
            return *this;
        } 
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2023, 9, 27);
    Date d2;
    Date d3;
    d1.Print();
    d2.Print(); 
    d3.Print();
    d3 = d2 = d1;
    d1.Print();
    d2.Print();
    d3.Print();
}
int main()
{
    Test();
    return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,改造后的operator=函数就支持连续赋值了。

需要注意的是,赋值运算符只能重载成类的成员函数而不能重载成全局函数,原因在于赋值运算符如果不显式实现,那么编译器就会生成一个默认的赋值运算符重载,此时如果用户再在类外自己实现一个全局的赋值运算符重载,那么就和编译器在类中生成的默认赋值运算符重载冲突了,所以赋值运算符重载只能是类的成员函数。

例:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}
void Test()
{
	Date d1(2023, 9, 27);
	Date d2;
	Date d3;
	d3 = d2 = d1;
}
int main()
{
	Test();
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里还需要注意的是,由编译器生成的默认赋值运算符重载,是以值的方式逐字节拷贝,也就是说,对于内置类型成员变量是直接赋值的,但是对于自定义类型成员变量则需要调用对应类的赋值运算符重载才能完成赋值。

例:

#include <iostream>
using namespace std; 
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	//Time& operator=(const Time& t)  //
	//{
	//	if (this != &t)
	//	{
	//		_hour = t._hour;
	//		_minute = t._minute;
	//		_second = t._second;
	//	}
	//	return *this;
	//}
	void Print()
	{
		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日";
		this->_t.Print();
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2(2023, 10, 4);
	d1.Print();
	d2.Print();
	d1 = d2;
	d1.Print();
	d2.Print();
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以,虽然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,但是对于一些涉及到资源管理的类,则必须要自己实现赋值运算符的重载,否则会出现无法预料的结果。

例:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们以同样的方式对Stack类的对象s2进行拷贝构造时,程序崩溃了,原因就在于Stack类中涉及到了资源管理,而Stack的赋值运算符重载又是依靠编译器实现的。

图解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论:

  1. 赋值运算符重载格式:
    1. 参数类型:const T&,传递引用可以提高传参效率。
    2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
    3. 检测是否自己给自己赋值。
    4. 返回*this:要复合连续赋值的含义。
  2. 赋值运算符只能重载成类的成员函数而不能重载成全局函数。
  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
  4. 如果类中未涉及到资源管理,那么赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自行实现。
14.2.1 赋值运算符重载和拷贝构造之间的辨析

我们通过下面这段代码来感受一下赋值运算符重载和拷贝构造的区别:

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator==(const Date& d2)
    {
        return _year == d2._year
            && _month == d2._month
            && _day == d2._day;
    }
    bool operator!=(const Date& d)
    {
        return !(*this == d);
    }
    Date& operator=(const Date& d)//支持连续赋值的重载赋值运算符
    {
        if (this != &d)//地址不一样时才赋值
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
            return *this;
        } 
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test()
{
    Date d1(2023, 9, 27);
    Date d2 = d1;//拷贝构造
    d1.Print();
    d2.Print();
    Date d3;
    d3 = d1;//赋值重载
    cout << "--------------------------------" << endl;
    d1.Print();
    d2.Print();
    d3.Print();
}
int main()
{
    Test();
    return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于赋值重载是在两个已经定义好的对象之间进行的,虽然Date d2 = d1;这条语句中用了赋值重载运算符=,但是这条语句的意思是用d1来初始化d2,也就是用一个已经定义好的对象来初始化一个正在定义的对象,所以Date d2 = d1;这条语句实际上是拷贝构造,而d3 = d1;这条语句才是赋值重载。

14.3 前置++和后置++重载

前置++和后置++的重载之所以要单独拎出来讲,是因为它们和运算符+、-相比,有需要注意的地方。

由于前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。

例:

#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
    //后置++:返回+1之前的结果
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++;
	d.Print();
	d1.Print();
	cout << "-------------------------" << endl;
	d = ++d1;
	d.Print();
	d1.Print();
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

15 const成员函数

在引出const函数之前,我们先来看下面这种情况:

#include <iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a = 10;
};
int main()
{
	A aa;
    //const A aa;
	aa.Print();
	return 0;
}

const修饰aa前的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

const修饰aa后的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当对象aa没有被const修饰时,它能够顺利运行,但是当aa被const修饰后再运行编译器就报错了。

之所以会报错,是因为这里涉及到一个权限被放大的问题。

在这个例子中,当我们把aa传进Print函数时,本质上传的是aa的地址,aa在没有被const修饰前,&aa的类型为A*,而Print的隐藏参数this的类型为A* const,也就是说当把aa传进Print函数后this的指向是不能被改变的,这是个权限缩小的过程,所以编译器允许;而当aa被const修饰后,&aa的类型就成了const A*,也就是说这个时候aa是不能被修改的,但传进Print函数后却反而可以被修改了,这个过程就把this的权限放大了,而这是不被编译器所允许的。

又由于this是隐藏的参数,我们没有办法进行修改,所以我们只能对函数用const进行修饰,那么我们就将const修饰的成员函数称为const成员函数,这个const实际修饰的是成员函数隐藏的this指针,修饰后this的类型就变成了const A*。

虽然我们实际情况下很少在定义的时候用const修饰变量,但是像下面的情况却并不少见:

#include <iostream>
using namespace std;
class A
{
public:
	void Print() const
	{
		cout << _a << endl;
	}
private:
	int _a = 10;
};
void Func(const A& x)
{
	x.Print();
}
int main()
{
	A aa;
	Func(aa);
	return 0;
}

const修饰Print函数前的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

const修饰Print函数后的运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,当我们把对象传给某个参数被const修饰的函数,而这个函数的内部所调用的函数却没有被const修饰时,就容易出错。

因此,只要函数内部不对成员变量进行改变一般都建议用const修饰一下,加上之后const对象和普通对象都可以调用。

16 取地址及const取地址操作符重载

对于这两个操作符一般不需要重载,使用编译器生成的默认取地址的重载即可。

例:

#include <iostream>
using namespace std;
class A
{
public:
	/*A* operator&()
	{
		cout << "My &:";
		return this;
	}
	const A* operator&() const
	{
		cout << "My const&:";
		return this;
	}*/
private:
	int _a = 10;
};
int main()
{
	A aa;
	const A bb;

	cout << &aa << endl;
	cout << &bb << endl;
	return 0;
}

使用编译器默认生成的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用自己写的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

只有特殊情况,才需要重载,比如想让别人获取到指定的内容:

#include <iostream>
using namespace std;
class A
{
public:
	A* operator&()
	{
		return nullptr;//拒绝取地址
	}
	const A* operator&() const
	{
		return nullptr;//拒绝取地址
	}
private:
	int _a = 10;
};
int main()
{
	A aa;
	const A bb;

	cout << &aa << endl;
	cout << &bb << endl;
	return 0;
}

运行结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2021C++实验二 类与对象 A 计算圆面积 题目描述 描述 编写一个圆类Circle,实现半径的输入、面积的计算和输出。要求在类中分别写3个函数实现输入半径、计算面积、输出面积。 输入 输入一行,输入圆的半径(double类型)。 输出 输出一行,输出圆的面积(保留小数点后两位)。 输入样例 1 3 输出样例 1 28.27 提示 1、在输出面积前使用如下语句:cout< 2、使用acos(-1.0)来得到圆周率的值,可将其设置为常量,因此:const ... 阅读详情

相关推荐

葵花 Himawari 8/9号数据学习记录-01-数据初识

葵花卫星学习记录,第一章:卫星的来源与卫星数据的波段介绍

weixin_44111909的博客 532

实验二 类与对象

4.定义一个Ladder类,声明实例变量UpperBottom,height,声明类变量LowerBottom,定义方法setUpperBottom,getUpperBottom,setLowerBottom,getLowerBottom,setHeight,getHeight,getArea,3.定义一个zuoye类,创建对象circle,参数传值,设置半径Radius,创建对象circular,调用setHeight,setBottom方法,计算圆锥体积,实现对象的组合,

m0_63421253的博客 1441

innovus 命令每日精要 | 掌控Innovus布线的利器——setNanoRouteMode详解

在数字IC后端设计的复杂世界里,布线优化是至关重要的一步。今天,我来给大家深入讲解Innovus工具中setNanoRouteMode命令的精妙之处,让你轻松驾驭布线策略,提升设计质量。setNanoRouteMode是Innovus中用于配置NanoRoute绕线行为的关键命令。通过其丰富的参数,你可以精细控制布局后阶段的绕线策略、信号完整性优化、工艺适配及设计规则修复等流程。下面,我将按功能分类总结其核心选项,并针对不同场景给出建议。

weixin_50820814的博客 3524

c++类与对象二

类的实例化、大小计算、this指针详解

sdmdnf的博客 1083

生物信息学名词解释 | K-mer (长度为k的短序列)

K-mer 何谓K-mer 所谓Kmer,即为一段长度为k的DNA片段,是由测序reads剪切一部分得到的。k为一个奇数,k=几,就为几mer。 比如:我的测序reads长度为100bp,我将这100bp打断成17bp的短片段,打断后的17bp段片段就叫17mer,可以获得(100-17+1)条k-mer序列。 Table1 k-mers for GTAGAGCTGT. k k-mers 1 G,T,A,G,A,G,C,T,G,T 3 GTA,TAG,AGA,GAG,AGC,GCT,CT

qq_50637636的博客 2万+

知识图谱本体建模之RDF、RDFS、OWL详解

(一)知识图谱本体建模之RDF、RDFS、OWL详解 1.语义网体系 知识图谱于2012年由Google提出,并不是新概念,而是由语义网络(Semantic Network)衍生而来。语义网络由相互连接的节点和边组成,节点表示概念或者对象,边表示他们之间的关系。RDF,RDFS与OWL都是基于语义网的知识表示框架,在节点和边的取值上做了约束,制定了统一标准,为多源数据的融合提供了便利。RDF,RDFS/OWL 属于语义网技术栈,它们的提出,使得语义网克服了语义网络的缺点。其语义网技术栈如下所示。 2.RD

周周的专栏 1万+

类与对象——实验二

【实验目的】 1、掌握类的构造函数和析构函数的概念和使用方法; 2、掌握对象数组、对象指针的定义和使用方法; 3、掌握new和delete的使用方法; 【实验内容】 1、 设计描述平面坐标上的点CPoint类,该类满足下述要求: •具有x,y坐标信息; •具有带默认形参值的构造函数,参数分别用于初始化x和y坐标信息; •具有获取x、y信息的GetX和GetY函数,具有设置x、y信息的SetX和SetY函数; 2、 设计一个矩形类CRectangle,该类满足下述要求: •具有矩形的左下角和右上角两个点的坐标

alcohol_sun的博客 1625

YOLOv8车辆碰撞识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置+目标检测)

随着我国汽车保有量的持续攀升,道路交通事故数量居高不下,车辆碰撞损伤的快速、精准评估已成为交通安全管理、保险理赔定损以及汽车后市场服务中的关键环节。传统的人工目视查验方法高度依赖评估人员的专业经验,不仅主观性强、效率低下,而且在复杂光照、多角度遮挡等真实场景下难以保证评估的一致性和可复现性。计算机视觉与深度学习技术的迅猛发展,为目标检测任务提供了强有力的技术支撑,使得基于视觉的车辆损伤自动化识别成为可能。

斌擎科技 128

类与对象(二)

面向程序的三大特性:封装、继承、多态封装:通过将数据(属性)和操作数据的方法(行为)绑定在一起,隐藏内部实现细节,仅对外提供可控的访问接口。

2401_88051411的博客 1万+

一、C++反作弊对抗实战 (基础篇 —— 2.注册表注入DLL)

在Windows NT/XP操作系统中,当需要加载user32.dll的程序启动时,user32.dll会加载注册表键HLM\Software\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_dlls下面列出的所有模块。

Koma的主页 1096

USB驱动(URB)

本文介绍了Linux内核中USB驱动开发的核心概念URB(USB Request Block)。URB是主机与USB设备间传输数据的抽象结构体,包含目标设备、端点信息、数据缓冲区等关键字段。文章详细解析了URB的生命周期(分配、填充、提交、回调、释放)、四种传输类型(控制、批量、中断、等时)及其应用场景,并提供了鼠标设备的中断传输示例代码。最后强调了URB使用注意事项,包括DMA缓冲区分配、错误处理和资源释放等。URB作为USB驱动与内核通信的桥梁,是构建稳定USB驱动程序的基础。

m0_49476241的博客 915

【C++】类与对象②

而_t是Time类对象,所以在d销毁时,

DerrictC的博客 2129

近五年体内微/纳米机器人赋能肿瘤精准治疗综述:以 GBM 为重点

摘要 微/纳米机器人在实体瘤治疗领域展现出突破性潜力,通过主动运动能力解决传统纳米药物递送效率低、渗透不足等核心问题。2021-2025年间形成三条技术路线:1) 磁驱、声驱等运动增强递送实现深层渗透;2) 与免疫治疗融合实现原位免疫唤醒;3) 针对胶质母细胞瘤开发跨屏障递送系统。研究重点从单一运动性能转向"运动-分布-疗效"的系统评价,需建立标准化指标评估运动学参数、肿瘤分布均匀性和安全性。未来挑战在于临床转化的可制造性、闭环操控及生物相容性优化,特别是对血脑屏障穿透等复杂场景的系统集

AllenLV的博客 5094

实验二 Java简单类与对象

Java实验报告 班级 2020级软件工程3班 学号 20209155 姓名 毛路桥 完成时间 2022年3月27日 实验二 Java简单类与对象 一、 实验目的 (1) 掌握类的定义,熟悉属性、构造函数、方法的作用,掌握用类作为类型声明变量和方法返回值; (2) 理解类和对象的区别,掌握构造函数的使用,熟悉通过对象名引用实例的方法和属性; (3) 理解static修饰符对类、类成员变量及类方法的影响。 二、 实验内容 写一个名为Recta.

m0_55673081的博客 4342

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

C++——类与对象(二)

C++类与对象的第二篇 感谢各位大佬的阅读!!! 求点赞收藏评论关注!!!

2301_80401288的博客 1832

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

面向对象<二>类与对象

目录前言一、Java类1.1、属性1.2、方法方法重载值传递机制1.3、构造器二、类的对象2.1、对象创建和使用2.2、对象的内存解析To be continued 前言 这是面向对象的第二篇啦!!本博客主要分享下面向对象编程中第一个重要的知识点——类与对象。众所周知,类和对象几乎可以说是面向对象编程的核心部分,没有类和对象就无法写一个程序,因此其重要性无需多言,接下来,let‘s go! 在这里先把其他几篇的链接放在这边,大家有需要的可以自行查看哦。 1.面向对象<一>思想篇 2.面向对象&l

Ft_Blackben的博客 229

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

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

【C++】类与对象 二

C++类和对象

zjk147258的博客 198

类与对象二

在前面类与对象一的章节中,我们了解了类与对象的一些基础概念,比如类的定义,对象的初始化等,在类与对象二,我们来共同学习更加进阶的概念——包。

2401_87650593的博客 305

【C++】—— 类与对象(二)

构造和析构函数来咯

yusjushd的博客 1830

java实验二:类与对象

类与对象编程练习题目一:学生信息处理题目二:用类描述计算机中CPU的速度和硬盘的容量题目三:共饮同井水 题目一:学生信息处理 根据下面要求,编写一个Java应用程序: ①定义类StuCard,用到类成员变量(人数)和实例成员变量:学号、姓名、性别、专业、年级、籍贯等; ②初始化的时候用到构造方法,提供两个以上构造方法; ③多个类(业务类StuCard和测试主类),用到包的概念,且分别放到不同的包中; ④用到类成员访问权限,要求类成员变量为Public,实例成员变量为Private; ⑤编写所有数据成员的访问

jynidl的博客 5028

二、类与对象(一)

面向过程和面向对象初步认识//类的引入//类的定义//类的定义方式//类的成员变量命名规则的建议//类的访问限定符及封装//public、protected、private//封装//类的作用域//类的实例化//类的大小

HackerKevon的博客 511

实验二:类与对象

实验三:类与对象 一、实验目的 练习类的定义和对象的声明; 观察构造函数和析构函数的执行过程; 学会声明公有、私有成员数据和成员函数,并调用函数解决实际问题 二、实验环境 PC计算机一台; Dev-C++开发环境。 三、实验内容 1、编写一个程序,设计一个点类Point,求两个点之间的距离。 2、设计一个正方体类Cube,它能计算并输出正方体的体积和表面积。 实验要求:Cube类包含三个私有数据成员a(正方体边长)、volume(体积)和area(表面积),另设计两个构造函数以及seta()(设置

weixin_45786421的博客 1461

【Java】类与对象(实验二)

源代码:

想做职场小白吗? 我来教您....... 2027
上一篇: 二、类与对象(一)
下一篇: 【上海大学数字逻辑实验报告】一、基本门电路
Kutbas
博客等级 码龄7年 737粉丝 55原创
评论 42
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kutbas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值