[C++] Классы и объекты (часть 2)

Оглавление

1. 6 функций-членов класса по умолчанию.

2. Конструктор

2.1 Концепция

2.2 Особенности

3. Деструктор

3.1 Концепция

3.2 Особенности

4. Копировать конструктор

4.1 Концепция

4.2 Характеристики

5. Перегрузка оператора присваивания

5.1 Перегрузка оператора

5.2 Перегрузка оператора присваивания

5.3 Перегрузка Pre++ и post++

6. константные члены

7. Получение адреса и перегрузка оператора получения константного адреса


1. 6 функций-членов класса по умолчанию.

Если класс не имеет членов, он называется пустым классом.

Неужели в пустом классе ничего нет? Нет, если в каком-либо классе ничего не записано, компилятор автоматически сгенерирует следующие 6 функций-членов по умолчанию.

Функция-член по умолчанию: если пользователь не реализует ее явно, функция-член, созданная компилятором, называется функцией-членом по умолчанию.

class Date {};

2. Конструктор

2.1 Концепция

Для следующего класса Date:

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. Однако было бы немного затруднительно вызывать этот метод для установки информации каждый раз при создании объекта. Можно ли установить информацию, когда объект создано?

Конструктор — этоспециальная функция-член, Имя совпадает с именем класса, а автоматически вызывается компилятором при создании объекта типа класса, чтобы гарантировать что каждый элемент данных All имеет подходящее начальное значение ивызывается только один раз на протяжении всего жизненного цикла объекта.

2.2 Особенности

Конструктор — это специальная функция-член. Следует отметить, что хотя имя конструктора и называется конструктором, основная задача конструктора нет. Вместо открытия места для создания объекта объект инициализируется.

Особое завоевание:

        ​ ​ 1. Имя функции совпадает с именем класса.

        ​ ​ 2. Нет возвращаемого значения.

        ​ 3. Компилятор автоматически вызывает соответствующий конструктор при создании экземпляра объекта.

        ​ ​ 4. Конструкторы могут быть перегружены.

class Date
{
public:
	// 1.无参构造函数
	Date()
	{}

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

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

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

        5. Если в классе нет явно определенного конструктора, компилятор C++ автоматически сгенерирует конструктор по умолчанию без параметров. Как только пользователь явно определит его, компилятор больше не будет его генерировать.

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

        6. Что касается функции-члена по умолчанию, сгенерированной компилятором, у многих детей возникнут сомнения: если конструктор не реализован, компилятор сгенерирует конструктор по умолчанию. Но кажется, что конструктор по умолчанию бесполезен? Объект d вызывает конструктор по умолчанию, созданный компилятором, но объект d _year/_month/_day по-прежнему является случайным значением. Другими словами, конструктор по умолчанию, сгенерированный компилятором, здесь бесполезен? ?

        Ответ: В C++ типы делятся на встроенные (базовые) и пользовательские. Встроенные типы — это типы данных, предоставляемые языком, например: int/char..., пользовательские типы — это типы, которые мы определяем сами с помощью class/struct/union и т. д. Если вы посмотрите на следующую программу, вы обнаружите, что компилятор генерирует структуру по умолчанию. Функция вызовет свою функцию-член по умолчанию для члена пользовательского типа _t.

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	// 自定义类型
	Time _t;
};

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

Примечание. В C++11 исправлена ​​ошибка, связанная с тем, что члены встроенного типа не инициализируются, а именно:Переменные-члены встроенного типа могут быть заданные значения по умолчанию при объявлении в классе .

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
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;
}

        7. Конструктор без параметров и конструктор по умолчанию называются конструкторами по умолчанию, и конструктор по умолчанию может быть только один.
Примечание. Конструкторы без аргументов, конструкторы, использующие все значения по умолчанию, а также конструкторы, создание которых по умолчанию не предусмотрено компилятором, могут считаться конструкторами по умолчанию.

3. Деструктор

3.1 Концепция

Благодаря предыдущему изучению конструкторов мы знаем, как возникает объект и как он исчезает?

Деструктор: В отличие от функции конструктора, деструктор не завершает уничтожение самого объекта.Уничтожение локального объекта завершается компилятором. Когда объект уничтожается, он автоматически вызывает деструктор для завершения очистки ресурсов в объекте.

3.2 Особенности

Деструктор — это специальная функция-член, еехарактеристики следующие:

        1. Имени деструктора предшествует имя класса с символами ~ .

        2. Нет параметров и нет типа возвращаемого значения.

        3. Класс может иметь только один деструктор. Если это не указано явно, система автоматически сгенерирует деструктор по умолчанию. Примечание. Деструкторы не могут быть перегружены.

        ​ ​ 4. Когда жизненный цикл объекта заканчивается, система компиляции C++ автоматически вызывает деструктор.

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

        5. Что касается деструктора, автоматически сгенерированного компилятором, сделает ли он что-нибудь? В следующей программе мы увидим, что деструктор по умолчанию, созданный компилятором, вызывает свой деструктор для члена пользовательского типа.

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

/*
  程序运行结束后输出:~Time()
  在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对象销毁时,要保证其内部每个自定义对象都可以正确
  销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
  注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
*/

        6. Если в классе нет приложения ресурса, деструктор не нужно писать, и можно напрямую использовать деструктор по умолчанию, сгенерированный компилятором, например класс Date; при наличии приложения ресурса он должен быть написано, иначе это приведет к утечке ресурсов, например класса Stack.

4. Копировать конструктор

4.1 Концепция

В реальной жизни может существовать человек, идентичный вам, которого мы называем близнецом.

Можете ли вы при создании объекта создать новый объект, аналогичный существующему?

Конструктор копирования:имеет только один формальный параметр, который является конструктором копирования a>. Автоматический вызов, которая определяется компилятором при создании нового объекта с использованием существующего объекта типа классаСсылка на объект типа класса (обычно используется с модификацией const)

4.2 Характеристики

Конструктор копирования также является специальной функцией-членом со следующими характеристиками:

        ​ ​ 1. Конструктор копирования — это перегруженная форма конструктора.

        2. Параметр конструктора копирования имеет только один и должен быть ссылкой на объект типа класса, если вы используете метод передачи по значению , компилятор напрямую сообщит об ошибке , потому что это вызовет бесконечные рекурсивные вызовы.

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// Date(const Date& d) // 正确写法
	Date(const Date d)     // 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

        3. Если он не определен явно, компилятор сгенерирует конструктор копирования по умолчанию. Объект конструктора копирования по умолчанию копирует объект в порядке байтов в соответствии с объемом памяти.Этот тип копирования называется поверхностным копированием или копированием значения.

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;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d1;

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

Примечание. В конструкторе копирования по умолчанию, созданном компилятором, встроенные типы копируются непосредственно в байтовой форме, тогда как пользовательские типы вызывают свои конструкторы копирования для завершения копирования.

        4. Конструктор копирования по умолчанию, созданный компилятором, уже может завершить копирование значения с порядком байтов, но вам все равно необходимо реализовать его явно самостоятельно. ? Конечно, такие классы, как классы дат, не нужны. А как насчет следующих классов? Попробуйте проверить?

// 这里会发现下面的程序会崩溃掉?这里就需要以后讲的深拷贝去解决。
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;
}

Примечание. Если в классе нет приложения ресурсов, конструктор копирования может быть написан или нет; как только приложение ресурсов задействовано, конструктор копирования должен быть написан, в противном случае это будет неполная копия.

        ​ ​ 5. Типичные сценарии вызова конструктора копирования:

                • Используйте существующие объекты для создания новых объектов.

                •                          

                • Функция возвращает тип значения объекту типа класса.

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

Чтобы повысить эффективность программы, обычно старайтесь использовать ссылочные типы при передаче параметров объекта.При возврате используйте ссылки, насколько это возможно, в соответствии с реальным сценарием.

5. Перегрузка оператора присваивания

5.1 Перегрузка оператора

В C++ введена перегрузка операторов для улучшения читаемости кода. Перегрузка операторов — это функция со специальным именем, которая также имеет тип возвращаемого значения, имя функции и список ее параметров, тип возвращаемого значения и список параметров аналогичны спискам обычных функций.

Имя функции следующее: за ключевым словом оператор следует символ оператора, который необходимо перегрузить.

Прототип функции:Оператор типа возвращаемого значения (список параметров)

Уведомление:

  • Новые операторы не могут быть созданы путем объединения других символов: например,operator@.
  • Перегруженные операторы должны иметь параметр типа класса.
  • Операторы, используемые для встроенных типов, не могут изменить свое значение.Например: встроенный целочисленный тип + не может изменить свое значение.
  • При перегрузке в качестве функции-члена класса ее формальные параметры кажутся на 1 меньше количества операндов, поскольку первый параметр функции-члена при этом скрыт.
  •  .*   ::   sizeof   ?:   . < /span>  Обратите внимание, что указанные выше пять операторов нельзя перегрузить. Это часто встречается в вопросах с несколькими вариантами ответов на письменных экзаменах.
// 全局的operator==
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(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout << (d1 == d2) << endl;
}
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;
};

5.2 Перегрузка оператора присваивания

1. Формат перегрузки оператора присваивания

  • Тип параметра: const T&, передача ссылки может повысить эффективность передачи параметров
  • Тип возвращаемого значения: T& возврат ссылки может повысить эффективность возврата, а целью возврата значения является поддержка непрерывного присваивания
  • Проверьте, присваиваете ли вы себе значение
  • Вернуть *this: для соблюдения смысла непрерывного присваивания
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;
	}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

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

2. Оператор присваивания можно перегрузить только как функцию-член класса и нельзя перегрузить как глобальную функцию.

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

// 编译失败:
// error C2801: “operator =”必须是非静态成员

Причина: Если оператор присваивания не реализован явно, компилятор сгенерирует оператор по умолчанию. В настоящее время, если пользователь реализует глобальную перегрузку оператора присваивания вне класса, это будет конфликтовать с перегрузкой оператора присваивания по умолчанию, созданной компилятором в классе. Следовательно, перегрузка оператора присваивания может быть только функцией-членом класса.

3. Когда пользователь не реализует это явно, компилятор сгенерирует перегрузку оператора присваивания по умолчанию, копируя побайтно в форме значения . Примечание. Переменные-члены встроенного типа назначаются напрямую, тогда как переменные-члены пользовательского типа должны вызывать перегрузку оператора присваивания соответствующего класса для завершения назначения.

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

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

Теперь, когда перегруженная функция оператора присваивания по умолчанию, сгенерированная компилятором, может завершить копирование значения порядка байтов, вам все еще нужно реализовать ее самостоятельно ? ? Конечно, такие классы, как классы дат, не нужны. А как насчет следующих классов? Попробуйте проверить?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后学的深拷贝去解决。
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;
}

Примечание. Если в классе не задействовано управление ресурсами, оператор присваивания может быть реализован или нет; как только управление ресурсами задействовано, его необходимо реализовать.

5.3 Перегрузка Pre++ и post++

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

    // 后置++:
    // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
    // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
    // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
    // 而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}

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

int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++;    // d: 2022,1,13    d1:2022,1,14
	d = ++d1;    // d: 2022,1,15    d1:2022,1,15
	return 0;
}

6. константные члены

"Функция-член", измененная с помощью const, называется константной функцией-членом, const изменяет функцию-член класса и фактически изменяет функцию-членНеявно Включенный указатель this указывает, что ни один член класса не может быть изменен в этой функции-члене.

Давайте посмотрим на следующий код

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}

	void Print() const
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}

private:
	int _year;  // 年
	int _month; // 月
	int _day;   // 日
};

void Test()
{
	Date d1(2022, 1, 13);
	d1.Print();

	const Date d2(2022, 1, 13);
	d2.Print();
}

Пожалуйста, рассмотрите следующие вопросы:

  1. Может ли константный объект вызывать неконстантные функции-члены?
  2. Могут ли неконстантные объекты вызывать константные функции-члены?
  3. Могут ли другие неконстантные функции-члены вызываться внутри константной функции-члена?
  4. Могут ли другие константные функции-члены вызываться внутри неконстантной функции-члена?

7. Получение адреса и перегрузка оператора получения константного адреса

Эти две функции-члена по умолчанию обычно не нужно переопределять, и компилятор сгенерирует их по умолчанию.

class Date
{
public:
	Date* operator&()
	{
		return this;
	}

	const Date* operator&()const
	{
		return this;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

Эти два оператора обычно не нужно перегружать. Просто используйте перегрузку с получением адреса по умолчанию, созданную компилятором. Перегрузка требуется только в особых случаях, например Let другие получают указанный контент!


Конец статьи

Guess you like

Origin blog.csdn.net/m0_73156359/article/details/134632834