【C++入门】类的6个默认成员函数、运算符重载、初始化列表、const成员、static成员

目录

引言

构造函数

引入构造函数

构造函数的特征

一些细节 

析构函数

析构函数的特性

注意事项 

拷贝构造函数

书写格式

使用细节

拷贝构造的典型应用场景 

运算符重载

意义与格式

注意事项 

赋值运算符重载

const成员

两个经典问题

再谈构造函数—初始化列表

注意事项 

static成员

概念

特性 

两个问题 


引言

一个类中什么都没有,简称为空类

空类中真的什么都没有吗?并不是的,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数

默认成员函数:用户没有显示实现,编译器会自动生成的函数叫默认成员函数。

构造函数

引入构造函数

下面通过一段代码引入:

class Date
{
public:
	void Init(int year = 1, int month = 1, int day = 1)
	{
		_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(2024, 4, 6);
	d1.Print();

	Date d2;
	d2.Init(2024, 4, 5);
	d2.Print();

	return 0;
}

对于Date类,我们可以通过公有函数Init来给对象设置日期,但如果每次创建对象时都要调用该函数来设置日期,未免有点麻烦。有没有一种方法,在对象创建时,就将信息设置进去呢?构造函数就是为解决此问题而生的。

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

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

构造函数的特征

> 函数名与类名相同

> 无返回值

> 对象实例化时编译器自动调用对应的构造函数

> 构造函数可以重载

示例:

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

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

一些细节 

如果我们没有显示的写构造函数,那么编译器会自动生成一个默认构造函数,一旦我们显示的定义了,编译器将不会在自动生成。

不写默认构造:

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

运行结果是:

输出的均为随机值。

 显式写了构造函数,但不是不用传参就能调用的,也会报错,请看下面的代码:

class Date
{
public:

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

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

int main()
{
	Date d1;

	return 0;
}

 

内置类型成员在类中声明时可以给默认值(缺省值)。

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 = 1; //在类中声明时可以给缺省值
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d1(2024, 4, 18);
	d1.Print();

	return 0;
}

总结:

默认生成的构造函数,对内置类型成员不做处理。内置类型包括int、double及任意类型的指针等。

默认生成的构造函数,对自定义的类型成员,会调用默认构造函数(不用传参就能调用的那个函数)。 

内置类型成员在类中声明时可以给缺省值。

析构函数

与构造函数相反,析构函数是完成对象中资源的清理工作。对象在销毁时会自动调用析构函数

析构函数的特性

> 析构函数函数名在类型前面加上~

> 无参数无返回值

> 一个类只能有一个析构函数,若未显式定义,编译器会自动生成默认构造函数

> 析构函数不能重载

> 对象生命周期结束时,自动调用

> 对于内置类型,自动调用析构函数,对于内置类型不会,因为内置类型中没有资源要清理

注意事项 

如果类中没有申请资源,析构函数可以不写,直接使用编译器自动生成的析构函数,比如Date类,

如果涉及到资源的申请,就必须要写析构函数,清理相应资源,否则会导致内存泄漏。

拷贝构造函数

书写格式

只有单个形参,形参类型为类类型对象的引用(一般加const修饰),在用已存在的类类型对象创建新的对象时,编译器自动调用。

//错误写法
    Date(const Date d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

//正确写法
    Date(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

第一种写法,编译时编译器报错。

因为采用传值方式,会导致无穷递归。 正确的做法应该是要传引用

还需注意,不要将拷贝构造函数和构造函数混淆,通过它们的参数列表可以很好区分。

使用细节

对于内置类型,可以直接使用编译器自动生成的拷贝构造,编译器生成的拷贝构造函数是按字节序完成拷贝的,这种拷贝方式叫做浅拷贝,或者值拷贝

对于自定义类型,需要调用自己写的拷贝构造,原因如下。

请看下面的例子: 

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

总结:

如果没有涉及资源的申请,拷贝构造函数是否写都可以,但是,一旦涉及到资源的申请,就必须写拷贝构造函数,否则就是浅拷贝。 

拷贝构造的典型应用场景 

> 使用已存在对像创建新对象

> 函数参数类型为类类型对象

> 函数返回值类型为类类型对象

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(2024, 4, 19);
	Test(d1);
	return 0;
}

自己实现了析构函数释放空间,就需要实现拷贝构造函数。 

运算符重载

意义与格式

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

关键字:operator

格式:返回值类型 operator运算符(参数列表)

注意事项 

> 不能通过连接其他符号来创建新的操作符:比如 operator@
> 重载操作符必须有一个类类型参数
> 用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不能改变其含义
> 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的this
> .*  ::  sizeof  ?:  .   注意以上 5 个运算符不能重载。

赋值运算符重载

> 参数类型:const T&  , 传引用可以提高传参效率。

> 返回值类型: T& , 返回引用可以提高效率,有返回值的目的是为了支持连续赋值。

> 检查是否自己给自己赋值,应避免这种情况发生,因为会降低程序的效率。

> 返回的是 *this,这一点在实现的时候自然就明白了。

class Date
{
public:
	Date(int year = 1, 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;
};

int main()
{
	Date d1(2024, 4, 19);
	Date d2;

	d1 = d2;

	return 0;
}

 能不能把赋值重载函数定义在全局呢?

请看代码:

class Date
{
public:
	Date(int year = 1, 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;
};

//全局
Date& operator=(Date& d1, Date& d2)
{
	if (&d1 != &d2)
	{
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	}
	return d1;
}

int main()
{
	Date d1(2024, 4, 19);
	Date d2;

	d1 = d2;

	return 0;
}

运行结果:

结论:

赋值运算符重载函数必须是类的成员函数。

原因:赋值重载函数是一个默认成员函数,如果在类中未显示定义,那么编译器会自动生成一个(逐字节拷贝),这就会与我们定义在全局的赋值重载函数冲突,所以赋值运算符重载函数必须为类的成员函数。

 和拷贝构造函数一样,如果没有涉及资源管理,那么赋值重载函数是否实现都可以,一旦涉及到资源管理,必须实现赋值重载函数。

const成员

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

两个经典问题

1. const 对象可以调用非 const 成员函数吗?
2. const 对象可以调用 const 成员函数吗?
下面依次解答。
1. const 对象可以调用非 const 成员函数吗?
不可以,const对象中的成员是不可以修改的,而非const对象中的成员是可以修改的,这是典型的 权限放大 ,是不允许的。

 2. const对象可以调用const成员函数吗?

可以, 非const对象调用const成员函数是一种权限缩小的行为,是允许的。

再谈构造函数—初始化列表

通过一段简单代码引入:

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

在对象实例化时通过上述的构造函数后,类的成员变量有了一个初值,但这并不是初始化,严格来说,应该是赋值,因为在函数体内,可以给同一变量多次赋值,但每一个变量的初始化只有一次,所以,在函数体内给值根本不叫初始化。类的成员变量真正初始化的地方在初始化列表。

初始化列表是构造函数的一部分,由冒号开始,逗号分割,每个成员变量后跟着的括号放初始值或表达式。

    Date(int year = 1970, int month = 1, int day = 1)
        :_year(year)      //初始化列表
        ,_month(month)
        ,_day(day)
    {}

注意事项 

1)每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2)尽量使用初始化列表初始化。初始化列表是所有成员变量(static成员变量除外)定义和初始化的位置,不管是否显式在初始化列表写,都会走初始化列         表进行定义和初始化。如果在初始化列表中给初始值,那么就会用初始值来初始化,如果没给,那么将会用缺省值来初始化。

3)成员变量在初始化列表中的初始化顺序与声明的顺序一致,与初始化列表中的顺序无关。

 

static成员

概念

声明为static的类成员称为类的静态成员。类的静态成员包含两类:静态成员变量和静态成员函数。

用static修饰的成员变量叫做静态成员变量。

用static修饰的成员函数叫做静态成员函数。

静态成员变量必须在类外进行初始化。上面我们说过,初始化列表是用来给成员变量初始化的,具体的说,是用来给实例化出的对象中的变量初始化的,而静态成员为所有类对象共享,不属于某个具体对象,所以它不是在初始化列表初始化的。

class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1) 
		:_year(year)
		, _month(month)
		, _day(day) 
		{}
private:
	int _year;
	int _month;
	int _day;
	static int _m;//静态成员变量的声明
};

int Date::_m = 0;//静态成员变量的初始化

特性 

1)静态成员为所有类对象共享,不属于某个具体的对象,存放在静态区。

2)静态成员变必须在类外定义,在类外定义式不加关键字static,类中只是声明。

3)类的静态成员可以用类名::静态成员或对象.静态成员这两种方式来访问。

4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员。

5)静态成员也是类的成员,受访问限定符的限制。

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

	static int Get_m()
	{
		return _m;
	}

private:
	int _year;
	int _month;
	int _day;
	static int _m;//静态成员变量的声明
};

int Date::_m = 0;//静态成员变量的初始化

int main()
{
	cout <<Date::Get_m() << endl;

	return 0;
}

 可以通过静态成员函数去访问静态成员变量。

两个问题 

1)静态成员函数可以调用非静态成员函数吗?

不可以直接调用。请看下面例子:

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

	int Add(int x, int y)
	{
		return x + y;
	}

	static int Get_m()
	{
		int ret = Add(1, 2);
		return _m;
	}

private:
	int _year;
	int _month;
	int _day;
	static int _m;//静态成员变量的声明
};

int Date::_m = 0;//静态成员变量的初始化

int main()
{
	cout <<Date::Get_m() << endl;

	return 0;
}

运行结果:

 

2)非静态成员函数可以调用静态成员函数吗?

非静态成员函数可以直接调用静态成员函数。

 


  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值