C++学习笔记(四)——类和对象(中)

目录

1. 类的6个默认成员函数

2. 构造函数

2.1 概念

2.2 特性

3. 析构函数

3.1 概念

3.2 特性

4. 拷贝构造函数

4.1 概念

4.2 特性

5. 赋值运算符重载

5.1 运算符重载

5.1 赋值运算符重载

6. const成员

6.1 const修饰类的成员函数

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

 7. 日期类的实现(对以上知识的运用,可以练练手)


1. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类,空类中什么都没有?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

class Date{};

注意:这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了,则编译器就不生成了。

2. 构造函数

2.1 概念

概念:构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调

,保证每个数据成员都有 一个合适的初始值并且在对象的生命周期内只调用一次

2.2 特性

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

其特征如下:

  • 函数名与类名相同
  • 无返回值
  • 对象实例化编译器自动调用对应的构造函数
  • 构造函数可以重载
class Date
{
public:
	// 构造函数
	// 1.无参构造函数
	Date()
	{

	}
	// 2. 有参构造
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 3.全缺省
	//Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //
private:
	int _year;
	int _month;
	int _day;
};
void test(){
  Date d1;//调用无参构造函数
  Date d2(2022,1,18);//调用带参的构造函数
}

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

默认构造函数有三种:

  • 编译器默认生成的构造函数(对内置类型不会处理,还是随机值;对于自定义类型会去调用他们自己的构造函数初始化)
  • 全缺省构造函数
  • 无参构造函数

在我们不实现构造函数时,编译器就会调用编译器默认生成的构造函数,看下面一串代码,编译器的默认构造函数能否实现成员变量的初始化呢? 

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


int main()
{
	Date  d1;
	d1.Print();

	return 0;
}

 
 

三个值都是随机值,编译器的默认构造函数并没有进行对成员变量进行初始化,是不是说这个函数就没有什么用呢?

解答: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;
}

总结:1.默认构造函数内置类型(int/char/double)不会处理,还是随机值;对于自定义类型(struct/class)会去调用他们自己的构造函数初始化。

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

3.一般情况建议都是写一个全缺省的构造函数,这种方式能够适应大多数场景。

3. 析构函数

3.1 概念

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

3.2 特性

析构函数是特殊的成员函数

其特征如下:

  • 析构函数名是在类名前加上字符~
  • 无参数无返回值
  • 一个类有且仅有一个析构函数.若没有显示定义,则系统自动生成析构函数。
  • 对象声明周期结束时,C++编译系统会自动调用析构函数。
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()//析构函数
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
//同样对于默认析构函数:对于内置类型成员不做处理,对于自定义类型会去调用他的析构函数

如果我们没有实现析构函数,编译器会自动生成一个析构函数,且这个析构函数对内置类型不处理,对自定义类型会调用它的析构函数,这一点和编译器生成的默认构造函数有点相似。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

class B
{
public:
	B()
	{
		cout << "B()" << endl;
	}

	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b;
};

int main()
{
	A a;
	B b;

	return 0;
}
// 因为对象是定义在函数中,函数调用会建立栈帧,
	// 栈帧中的对象构造和析构也要很符合后进先出。
	// 所以这里的顺序是: A()->B()->~B()->~A()

4. 拷贝构造函数

4.1 概念

在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

4.2 特性

  •  拷贝构造函数是构造函数的一个重载形式;

  • 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用;

class Date {
public:
	Date(int year = 2020, int month = 2, int day = 20)
	{
		_year = year;
		_month = month;
		_day = day;
	}
  // Date(Date d)
//若使用传值拷贝,则在传参时会先调用拷贝构造函数(因为传值拷贝时需先将原数据拷贝到临时空间再使用临时空间中的数据进行赋值),这样又会出现拷贝,这样就会陷入循环往复过程。   
   {
      _year=d._year;
      _month=d._month;
      _day=d._day;
     }//
	Date(const Date& d)//这里传参时也推荐加 const 因为被拷贝的对象是不应该被修改的
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
// 总结:
// 1.Date这样的类,需要的就是浅拷贝,那么默认生成的拷贝构造就够用了,不需要自己写
// 2.但是像Stack这样的类,需要的是深拷贝,浅拷贝会导致析构两次,程序就会崩溃等问题

传值引发无穷递归的原因: 

  •  若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

对自定义类型(Stack等),编译器还是会对他进行浅拷贝,这样就会将原来对象开辟空间的地址给拷贝了,这样的后果是:
1.两个对象共用一块空间,在调用析构函数时该空间会被释放两次
2.其中一个对象插入删除数据会导致另一个对象也插入删除了数据
所以像Stack这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足要求,需要实现深拷贝,这里不做过多介绍后面章节会分析.

class String
{
public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
	}
private:
	char* _str;
};
int main()
{
	String s1("hello");
	String s2(s1);
}

 代码直接崩了,这就是浅拷贝带来的后果:相同内存被析构两次 

5. 赋值运算符重载

5.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
    操作符有一个默认的形参this,限定为第一个形参
  • * 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
     

代码示例:‘>’

class Date
{
public:
    Date(int year = 2022, int month = 2, int day = 20)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    
    // bool operator>(Date* this, const Date& d2)
 
    bool operator>(const Date& d2)
    {
        
    if (_year > d2._year)
	{
		return true;
	}
	else  if(_year == d2._year && _month > d2._month)
	{
		return true;
	}
	else if (_year == d2._year && _month == d2._month && _day > d2._day)
	{
		return true;
	}
	else {
		return false;
	}
    }
private:
    int _year;
    int _month;
    int _day;
};
void Test ()
{
    Date d1(2021, 1, 1);
    Date d2(2021, 1, 2);
    cout<<(d1 >d2)<<endl;// <<优先级高于==,这里需加括号
    //等于d1.operator>(d2)
}

 

5.1 赋值运算符重载

赋值运算符主要有以下五大点:

  • 参数类型
  • 返回值
  •  检测是否自己给自己赋值
  •  返回*this
  • 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
class Date
{
public:
	Date(int year = 2000, int month = 9, int day = 18)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 赋值运算符重载也是一个默认成员函数,也就是说我们不写编译器会自动生成
	// 编译器默认生成的赋值运算符重载跟拷贝构造的特性是一样的
	// 内置类型,会完成浅拷贝,自定义类型,会调用他的赋值运算符重载完成拷贝
	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;
};
 
 
int main()
{
	Date d1;
    Date d2(2022,2,20);
    d1=d2;//这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的
 
	Date d3;
	d3 = d2 = d1;//连续赋值
	d2.Print();
	d3.Print();
 
	Date d4 = d1; // 拷贝构造
	return 0;     // 拷贝构造:拿一个已经存在的对象去拷贝初始化另一个要创建的对象
}                 // 赋值重载:两个已经存在的对象拷贝
            

赋值运算符重载和拷贝构造函数的区别

相同:没有显示定义时,编译器都会默认生成一个,对于内置类型进行字节序的浅拷贝,对             自定义类型会调用它自身的拷贝构造函数或operator=。
不同:拷贝构造:用一个已经存在的对象初始化一个马上要创建的对象
           赋值重载:两个已经存在的对象拷贝

6. const成员

6.1 const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

class Date
{
public :
void Display () const
{
cout<<"Display () 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 ;
d1.Display ();//非const可以调用const;const不能调用非const(权限不能放大)
const Date d2;//建议成员函数中,不需要改变成员变量,都加上const。
d2.Display ();
}

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

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

这两个默认成员函数一般不用重新定义 ,编译器默认会生成

class Date {
public:
	Date(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d) {
		_year = d._year;
	}
	Date* operator&() {
		cout << "Date* operator&()" << endl;
		return this;
	}
 
	const Date* operator&() const {
		cout << "const Date* operator&() const" << endl;
		return this;
	}
 
private:
	int _year;
	int _month;
	int _day;
};
 
int main() {
	Date d1(2019, 4, 1);
	const Date d2(2019, 3, 31);
 
	Date* pa1 = &d1;
	const Date* pd2 = &d2;
	system("pause");
	return 0;
}

 7. 日期类的实现(对以上知识的运用,可以练练手)

Date.h

Date.h
#pragma once
#include<iostream>
using namespace std;
class Date {
public:
	Date(int year = 1, int month = 1, int day = 1);
	void print();
	int GetMonthDay(int year,int  month);//获取每个月的天数
	bool operator>(const Date& d);//比较第一个数是否比第二个数大
	bool operator<(const Date& d);//以下同理
	bool operator>=(const Date& d);
	bool operator<=(const Date& d);
	bool operator==(const Date& d);
	bool operator!=(const Date& d);
	//d1+=100
	Date& operator+=(int day);//在一个数本身加上天数,本身改变
	//d1+100
	Date operator+(int day);//一个数加上给定的天数是哪天?本身不改变
	//d1-=100
	Date& operator-=(int day);
    //d1-100
	Date operator-(int day);
	Date operator--(int);
	Date& operator--();
	Date operator++(int);//后置++,为了区分前置++与其构成函数重载,需要加int(规定,不允许是其他类型)
	Date& operator++();//前置++
	int operator-(const Date& d);//两个日期相减
	void PrintWeekDay() const;//打印今天是星期几

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

Date.c 

#include"Date.h"
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
	if(!(_year >= 0 && (_month > 0 && _month < 13) && (_day>0 && _day <= GetMonthDay(year, month))))
	{
		cout << "非法日期" << endl;
	}
}
void Date::print()
{
	cout << _year << "-" << _month << "-" << _day << endl; 
}
int Date::GetMonthDay(int year, int month)
{
	int MonthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = MonthDayArray[month];
	if (month == 2 && (year % 4 == 0 && year % 400 != 0) || year % 100 == 0)
	{
		day += 1;
	}
	return day;
}
bool Date::operator>(const Date& d)
{
	if (_year > d._year)
	{
		return true;
	}
	else  if(_year == d._year && _month > d._month)
	{
		return true;
	}
	else if (_year == d._year && _month == d._month && _day > d._day)
	{
		return true;
	}
	else {
		return false;
	}
}
bool Date::operator==(const Date& d)
{
	if (_year == d._year && _month == d._month && _day == d._day)
	{
		return true;
	}
	else {
		return false;
	}
}
bool Date::operator>=(const Date& d)
{
	return (*this > d || *this == d);
}
bool Date::operator<(const Date& d)
{
	return !(*this >= d);
}
bool Date::operator<=(const Date& d)
{
	return !(*this > d);
}
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}
Date& Date::operator+=(int day)
{
	//2021-12-28 +=300
	_day += day;
	while ( _day> GetMonthDay(_year, _month))
	{
		_month += 1;
		_day -= GetMonthDay(_year, _month);
		while (_month == 13)
		{
			_year += 1;
			_month = 1;
		}
	}
	return *this;
}
Date Date::operator+(int day)
{
	Date ret(*this);
	ret += day;
	return ret;
}
Date& Date::operator-=(int day)
{
	//2021-1-20 -45
	_day -= day;
   while(_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month);
	}
   return *this;
}
Date Date::operator - (int day)
{
	Date ret(*this);
	ret -= day;
	return ret;
}
//前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
Date Date::operator++(int)
{
	Date ret(*this);
	*this += 1;
	return ret;
}
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
Date Date::operator--(int)
{
	Date ret(*this);
	*this -= 1;
	return ret;
}
int Date::operator-(const Date& d)
{
	Date max = *this;
     Date min = d;
	 int flag = 1;
	 if (*this < d)
	 {
		 max = d;
		 min = *this;
		 flag = -1;
	 }
	 int count = 0;
	 while (min != max)
	 {
		 min++;
		 count++;
	 }
	 return flag * count;
}
void Date::PrintWeekDay() const
{
	const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
	Date start(1900, 1, 1);
	int count = *this - start;
	cout << arr[count % 7] << endl;
}

以上是类和对象的部分内容,有不足的地方或者对代码有更好的见解,欢迎评论区留言共同商讨,共同进步!! 

 

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
在面向对象的编程,C语言并不直接支持类和抽象的概念。引用提到,final关键字用来修饰方法,表示该方法不能在子类中被覆盖。而abstract关键字用来修饰抽象方法,表示该方法必须在子类中被实现。然而,在C语言,没有对应的关键字来实现类和抽象的概念。 相反,C语言通过结构体来模拟类的概念。结构体是一种用户自定义的数据类型,可以包含多个不同类型的数据成员。通过结构体,我们可以将相关的数据和功能组合在一起。然而,C语言的结构体不支持继承和多态等面向对象的特性。 在C语言,我们可以使用函数指针来模拟抽象类和接口的概念。函数指针可以指向不同的函数,通过使用函数指针,我们可以实现多态性,即在运行时根据函数指针指向的具体函数来执行不同的操作。 综上所述,C语言并不直接支持面向对象的类和抽象的概念,但可以使用结构体和函数指针来实现类似的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [面向对象——类和对象](https://blog.csdn.net/shouyeren_st/article/details/126210622)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [面向对象编程原则(06)——依赖倒转原则](https://blog.csdn.net/lfdfhl/article/details/126673771)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

接受平凡 努力出众

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

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

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

打赏作者

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

抵扣说明:

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

余额充值