C++初阶---类和对象(类的默认成员函数和其他)

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶---类和对象(类的默认成员函数和其他)相关的知识,希望对你有一定的参考价值。

概览

任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数(包括但不限于这6个)


类的6个默认成员函数

  1. 构造函数完成初始化工作
  2. 析构函数完成清理工作
  3. 拷贝构造函数使用同类对象初始化创建对象
  4. 赋值操作符重载把一个对象赋给另一个对象
  5. 取地址操作符重载:返回地址
  6. const取地址操作符重载

①构造函数

1.概览

如下代码所示

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1,d2;
	d1.SetDate(2018,5,1);
	Date d2;
	d2.SetDate(2018,7,1);
	return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,过于麻烦
为此引出构造函数:


定义
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器 自动调用保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次
注意
构造函数是特殊的成员函数,其主要任务并不是开空间创建对象,而是初始化对象


2.特性

注意默认构造函数(不传参就可以调用的那个函数)


特性

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载(见下方分析)
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成(见下方分析)
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数(见下方分析)
  7. 默认构造函数对于内置类型调用编译器自己生成的默认构造函数貌似没有用但是对于自定义类型就有用了
  8. 成员变量的命名风格(见下方分析)

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 ;
};
int main(){
	Date d1; // 调用无参构造函数
	Date d2 (2015, 1, 1); // 调用带参的构造函数
}

此处注意
只有有参的构造函数时不能无参调用,编译器会报错


特性5

类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(不传参就可以调用的那个函数),一旦用户显式定义编译器将不再生成

class A
{
public:
	A()
	{
	_a1=0;
	_a2=1;
	}
private:
	int _a1;
	int _a2;
};
class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	*/
private:
	int _year;
	int _month;
	int _day;
	A _a;
};
int main()
{
	Date d;// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
}

但是我们可以看到,创建的对象只要没有定义了的构造函数都是随机值

解释
编译器默认生成构造函数的时候

  1. 内置类型不会初始化(内置类型是指 int,char…)
  2. 自定义类型才会调用他的无参构造函数进行初始化
  3. 如果类A中也没有自定义构造函数,同样也会是随机值
  4. 注意 默认生产的构造函数发现类中没有无参构造函数时也会报错(解释的不清)

在c++11中,语法委员会打了一个补丁,如下可以用缺省值进行初始化

class Date
{
pubic:
	Date (int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	
private:
	int _year=1;
	int _month=1;
	int _day=1;
	A _a=10;//下面会说到隐式类型转换
	int* _p=(int*)malloc(sizeof(int))*10;//可以调用函数做缺省值
	static int _n;//静态成员在这里不能给缺省值,不能在构造函数初始化,参见下面的static成员部分
};

注意此处仍是成员变量定义,像函数给缺省值那样,不是初始化


特性6

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个


下面代码会出错

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

解释
编译器会出现歧义,不能明确知道调谁
注意
无参构造函数全缺省构造函数我们没写编译器默认生成的构造函数,都可以认为是默认成员函数


特性8

如下代码

class Date
{
public:
	Date(int year)
	{
	// 这里的year到底是成员变量,还是函数形参?
	year = year;
	}
private:
	int year;
};

解释
创建出来的对象中的year是随机值,局部性原则,因为year = year;这条语句以最近的year为主
如果想要正确初始化应该是this->year=year;


所以我们给类成员变量命名时建议在前面加一个标识符‘_’,如_year

4.构造函数体赋值

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

上述构造函数调用之后,对象中有了一个初始值,但是不能将其称作为类对象成员的初始化
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

5.初始化列表

在没有接触列表初始化之前,我们想要传参初始化一个自定义类可以这样

class A
{
public:
	A(int a=0)
	{
		_a=a;
	}
private:
	int _a;
};
class B
{
public:
	B(int a,int b)
	{
		// A aa(a);
		// _aa=aa;
		_aa=A(a);//创建一个_a=a的匿名对象来赋值给_aa
		_b=b;
	};
private:
	int _b=1;
	A _aa;
}
int main()
{
	B b(10,20);
	return 0;
}
  1. 创建一个_a=a的匿名对象来赋值给_aa,会调用两次A的构造函数 (因为有自定义类型_aa,调用B的构造函数会先去调用一次A的默认构造函数,第二次 是匿名对象创建调用一次)
  2. 同时还调用了一次默认的拷贝构造函数,用来给_aa传值拷贝(内置类型_a)

    可以看到初始化_aa的代价较大

所以就引出了初始化列表
格式
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式,如下

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

我们将上面的讲到的_aa改为初始化列表初始化会这样:

只调用一次构造函数,效率提高
解释(只调用一次默认构造)
即使没显式的写初始化列表,也会在初始化列表处调用A的构造函数(因为有自定义类型_aa)

注意

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化 (见下面分析)
  3. 类中包含以下成员(引用成员变量,const成员变量自定义类型成员(该类没有默认构造函数)),必须放在初始化列表位置进行初始化(见下面分析)
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

注意2

对以下代码反汇编

public:
	B(int a, int b)
	{
		// A aa(a);
		// _aa=aa;
		_aa = A(a);//创建一个_a=a的匿名对象来赋值给_aa
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};


可以看到在初始化列表处调用A的构造函数


所以
尽管我们没有显式的写初始化列表,这里也是认为有初始化列表的_b和_aa会使用默认初始化列表进行初始化

我们也可以认为初始化列表是对象成员变量定义的地方

注意3

  1. const成员变量:const类型的成员必须在定义的时候初始化
  2. 引用成员变量: 也必须在定义的时候初始化
  3. 自定义类型前面已经说到,当没有默认构造函数的时候只能在定义的时候初始化
class A
{
public:
	A(int a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a,int c, int& ref)
		:_c(c)
		,_ref(ref)
		,_aa(a)
	{
		//_c=c;//const变量定义了就不能被改变
		//_ref=ref;//不行必须在定义的时候初始化
		_ref = 100;//对ref进行修改
	}
private:
	const int _c;
	int& _ref;
	A _aa;
};
int main()
{
	int ref = 10;
	B b(0, 1, ref);
	return 0;
}

注意4

看下面这个例子:

class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}
void Print() {
	cout<<_a1<<" "<<_a2<<endl;
}
private:
	int _a2;
	int _a1;
}
int main() {
	A aa(1);
	aa.Print();
}

结果输出:1,随机值


解释
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中出现的先后次序无关
声明的时候是先声明的_a2,所以会先执行_a2(a1),在执行_a1(a),打印的时候自然是1,随机值
建议
建议声明的顺序尽量和初始化列表保持一致,避免出现上面的问题

总结

  1. 尽量使用初始化列表初始化,因为就算你不显式的用初始化列表,成员也会先用初始化列表初始一遍
  2. 有些成员是必须在初始化列表初始的,(引用,const,没有默认成员函数)的成员
  3. 初始化列表和函数体内初始化,可以混用,互相配合,例如下面代码:
List()
	:_head(BuyNode(0))
{
	//有些用初始化列表不能完成初始化,还是要用函数体内初始化
	_head->next=_head;
	_head->prev=_head;
};

6.关键字explicit

我们先看下面的代码

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout<<"A(int a)"<<endl;
	}
	A(const A& aa)
	{
		cout<<"A(const A& aa)"<<endl;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	//下面的本质是一个隐式类型转换
	A aa2=2;
	return 0;
}

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用类似于double类型强转为int类型,中间会产生临时变量,这里相当于把int类型的2强转为A类,同理会产生临时对象
关于隐式类型转换参考:C++初阶—C++基础入门概览的常引用部分

  1. 对于 A aa1(1); 是直接调用构造函数
  2. 对于 A aa2=2; 相当于是用一个整形变量给A类型对象aa2赋值,实际编译器背后会用2构造一个无名对象(临时),最后用无名对象(临时)给aa2对象进行赋值(去拷贝构造aa2),但编译器对其进行了优化,结果和 A aa1(1);一样了
    参考(编译器优化):题目–拷贝构造,析构,静态成员变量…的题目1

explicit关键字 用explicit修饰构造函数,将会禁止单参构造函数的隐式转换
如图报错,验证了上面是编译器优化的结果,本质上其实是类型转换


上面探讨的是单参数构造函数(C++98不支持多参数隐式类型转换),
所以当我们使用的是多参数构造函数时(C++11支持)这样:

A aa2={12};

同理 explicit关键字可以禁止多参构造函数的隐式转换


②析构函数

1.概览

我们知道一个对象是由构造函数进行初始化的,那么析构函数就是清理空间
定义
与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数是特殊的成员函数


2.特性

特性

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

3.特性分析:

特性5

如下代码

class Stack
{
public:
	Stack (int capacity = 10)
	{
		_a = (int*)malloc(capacity * sizeof(int));
		_capacity = capacity;
	}
	~Stack()
	{
	free(_a);
	_a=nullptr;
	_top=_capacity=0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class SeqList
{
public :
	SeqList (int capacity = 10)
	{
		_pData = (int*)malloc(capacity * sizeof(int));
		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;
	Stack st;
};



可以看到编译器自己生成的默认析构函数同构造函数一样,不会处理内置类型成员,自定义类型成员会去调用他的析构函数

析构函数的调用

类的析构函数调用完全按照构造函数调用的相反顺序进行调用

  1. 全局对象先于局部对象进行构造
  2. 静态对象先于普通对象进行构造

③拷贝函数构造

1.概览

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

2.特性

特性

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
  4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们仍需要自己实现,像Stack这种类,不能使用字节序拷贝

3.特性分析

特性2

拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
(编译器会强制检查终止程序)


假设我们的拷贝构造函数是这样的:
(Date d=d2也是拷贝构造语法)

Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

生成默认拷贝构造函数无穷递归


所以我们使用引用传参
同时为了防止出现d._day = _day;的情况,不是输出型函数用const进行保护

Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	//d._day = _day;
}
int main()
{
	Date d2(2020,1,1);
	Date d3(d2);
	//也可以Date d3=d2;
}

特性3 和 特性4

在编译器生成默认拷贝构造函数时和之前的构造函数·析构函数不同
拷贝构造函数不会区分内置类型和自定义类型成员

  1. 对于内置类型成员拷贝构造函数对象按内存存储按字节序完成拷贝,为浅拷贝或值拷贝
  2. 对于自定义类型,会去调用他的拷贝构造函数进行构造

如下代码:
对于Stack这种类必须自己写拷贝构造函数

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{// 像Stack这样的类,对象中的资源需要清理工作,就用析构函数
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main(以上是关于C++初阶---类和对象(类的默认成员函数和其他)的主要内容,如果未能解决你的问题,请参考以下文章

C++初阶---类和对象

C++ 初阶类和对象

C++ 初阶类和对象

C++初阶:类和对象(中篇)构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

C++初阶第五篇——类和对象(中)(构造函数+析构函数+拷贝构造函数+赋值操作符重载)

C++进阶:继承C++为什么要引入继承 | 继承概念及定义 | 基类和派生类对象赋值转换 | 继承中的作用域 | 派生类的默认成员函数 | 继承与友元/静态成员 | 复杂的菱形继承及菱形虚拟继承